It is possible to create a DbContext per query type (see this issue comment). Which would work for an anonymous type, but still requires you to know the result set at compile time.
EF Core does expose an API for executing raw sql with no results. With a little work, it is possible to expose and reuse their internal database query abstractions;
// Expose EF Core internal ExecuteReaderAsync, used for reading database results
// Roughly based on https://github.com/dotnet/efcore/blob/v5.0.3/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs#L377
public static Task<RelationalDataReader> ExecuteReaderRawAsync(this DatabaseFacade db, string sql, IEnumerable<object> parameters, CancellationToken cancellationToken = default)
{
var dependencies = ((IDatabaseFacadeDependenciesAccessor)db).Dependencies as IRelationalDatabaseFacadeDependencies;
var cmd = dependencies.RawSqlCommandBuilder
.Build(sql, parameters);
return cmd.RelationalCommand
.ExecuteReaderAsync(new RelationalCommandParameterObject(
dependencies.RelationalConnection,
cmd.ParameterValues,
null,
((IDatabaseFacadeDependenciesAccessor)db).Context,
dependencies.CommandLogger
), cancellationToken);
}
public static Task<RelationalDataReader> ExecuteReaderRawAsync(this DatabaseFacade db, string sql, CancellationToken cancellationToken = default)
=> ExecuteReaderRawAsync(db, sql, Enumerable.Empty<object>(), cancellationToken);
public static Task<RelationalDataReader> ExecuteReaderInterpolatedAsync(this DatabaseFacade db, FormattableString sql, CancellationToken cancellationToken = default)
=> ExecuteReaderRawAsync(db, sql.Format, sql.GetArguments(), cancellationToken);
That way EF Core will be responsible for managing the connection lifetime. But you'll still need to step through the results, or convert them to an IEnumerable yourself.
Since the whole point of EF Core is to provide an abstraction for saving and loading objects from the database. Implementing a dynamic query feels like an anti-feature that they will never implement.