dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.72k stars 3.17k forks source link

Passing in QueryModel from re-linq #4417

Closed biqas closed 8 years ago

biqas commented 8 years ago

Hi,

is it possible to to have an public method which is accepting QueryModel as input parameter instead of Expression parameter?

rowanmiller commented 8 years ago

@biqas EntityQueryModelVisitor is the type you are after. You can get a factory for this type from the IServiceProvider associated with a context.

var serviceProvider = context.GetInfrastructure<IServiceProvider>();
var factory = serviceProvider.GetService<IEntityQueryModelVisitorFactory>();
rowanmiller commented 8 years ago

Out of interest, what are you trying to do?

biqas commented 8 years ago

@rowanmiller Hi I'm trying to build out a library which is able to compose, configure and query services and for example for the querying topic I choose first raw expression trees as an input parameter contract but switched over to the QuerModel from re-linq. So in that case it would be great if EF had an public API to pass directly QueryModel's (from re-linq, or maybe EF-Team is abstracting them) this could open much more preprocessing of queries in a disconnected way.

Basically in one of the steps I'm analyzing amd striping away some partial linq statements and adding service specific linq statements and it is much easier to do so in re-linq.

Currently the process is: Expression is created, QueryModel is created based on the expression, modified QueryModel is converted back to expression, expression passed to EF, EF creates QueryModel based on the expression, QueryModel creates target-statements (SQL-statements).

Hope this makes it some how understandable? :)

rowanmiller commented 8 years ago

We discussed this more today, it's probably simpler to get the Database from the IServiceProvider and use the CompileQuery/CompileAsyncQuery methods.

biqas commented 8 years ago

Hi,

that sounds good, I will try it out in the next days.

rowanmiller commented 8 years ago

Feel free to reopen if you hit issues

biqas commented 8 years ago

Hi, I had finally time to test the proposed methods (CompileQuery), but ran into some issues.

using (var db = new BloggingContext())
{
    var serviceProvider = ((IInfrastructure<IServiceProvider>)db).Instance;
    var database = (IDatabase)serviceProvider.GetService(typeof(IDatabase));
    var queryContextFactory = (IQueryContextFactory)serviceProvider.GetService(typeof(IQueryContextFactory));

    var query = new EnumerableQuery<Blog>(new List<Blog>())
        .Where(x => x.Url != "");

    var queryModel = QueryParser.CreateDefault().GetParsedQuery(query.Expression);

    var fromExpression = Expression.Constant(db.Blogs);
    queryModel.MainFromClause.FromExpression = fromExpression;

    var compileQueryFn = database.CompileQuery<Blog>(@queryModel);
    var result = compileQueryFn(queryContextFactory.Create()).ToList();
}

I'm getting the following error:

"A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe."

BloggingContext is taken from the EF documentation.

Question is, is this the intended behavior if so, how can I get around it?

mikary commented 8 years ago

It looks like the MainFromClause is a constant expression with a value of InternalDbSet<Blog> (implementation type of DbSet<Blog>), but we are expecting an EntityQueryable<Blog>. The quick fix for this would be to replace:

var fromExpression = Expression.Constant(db.Blogs);

with:

var fromExpression = Expression.Constant(new EntityQueryable<Blog>(db.GetService<IAsyncQueryProvider>()));

The concurrency exception was basically a reentry test that was triggering because the query enumerated the inner query (db.Blogs) in the middle of enumerating the actual query.

I don't know if it will work for your scenario, but you might try replacing the IQueryCompiler service. This service is the one that is responsible for preprocessing the expression tree and compiling the QueryModel.

biqas commented 8 years ago

Hi, appreciate the quick investigation. It seems to work to an extend for my scenario.

The problem wich is left over I think is not directly related to this issue, which is:

public interface IBlog
{
    int BlogId { get; set; }

    string Url { get; set; }
}

public class Blog : IBlog
{
    public int BlogId { get; set; }

    public string Url { get; set; }

    public List<Post> Posts { get; set; }
}

Note the interface usage.

using (var db = new BloggingContext())
{
    var serviceProvider = ((IInfrastructure<IServiceProvider>)db).Instance;
    var database = (IDatabase)serviceProvider.GetService(typeof(IDatabase));
    var queryContextFactory = (IQueryContextFactory)serviceProvider.GetService(typeof(IQueryContextFactory));

    var query = new EnumerableQuery<IBlog>(new List<IBlog>())
        .Where(x => x.Url != "");

    var queryModel = QueryParser.CreateDefault().GetParsedQuery(query.Expression);

    var queryProvider = (IAsyncQueryProvider)serviceProvider.GetService(typeof(IAsyncQueryProvider));

    var fromExpression = Expression.Constant(new EntityQueryable<IBlog>(queryProvider));
    queryModel.MainFromClause.FromExpression = fromExpression;

    var compileQueryFn = database.CompileQuery<Blog>(@queryModel);
    var result = compileQueryFn(queryContextFactory.Create()).ToList();
}

So far I know EF is currently not supporting interface definitions, I think I have to rewrite the places where interfaces are used within the QueryModel.

Out of interest, why is EF not supporting or making use of structural matching for mappings and translating materialisation to types? (And of course then interfaces could be a good fit)

mikary commented 8 years ago

I don't think I have the context to answer this question properly, clearing the milestone so this can go back to triage.

rowanmiller commented 8 years ago

Interfaces are not supported yet, we'll probably handle this as part of https://github.com/aspnet/EntityFramework/issues/240 where you can tell us how to create instances of a type for a given interface.

biqas commented 8 years ago

Hi I faced one issue

using (var db = new BloggingContext())
{
    var serviceProvider = ((IInfrastructure<IServiceProvider>)db).Instance;
    var database = (IDatabase)serviceProvider.GetService(typeof(IDatabase));
    var queryContextFactory = (IQueryContextFactory)serviceProvider.GetService(typeof(IQueryContextFactory));

    var query = new EnumerableQuery<IBlog>(new List<IBlog>())
        .Where(x => x.Url != "")
        .Select(x => x,Posts);

    var queryModel = QueryParser.CreateDefault().GetParsedQuery(query.Expression);

    var queryProvider = (IAsyncQueryProvider)serviceProvider.GetService(typeof(IAsyncQueryProvider));

    var fromExpression = Expression.Constant(new EntityQueryable<IBlog>(queryProvider));
    queryModel.MainFromClause.FromExpression = fromExpression;

    var compileQueryFn = database.CompileQuery<Blog>(@queryModel);
    var result = compileQueryFn(queryContextFactory.Create()).ToList();
}

.Select(x => x,Posts);

if I change the projection to posts, then the call to

database.CompileQuery<Blog>(@queryModel);

throws an error

Additional information: Expression of type 'System.Collections.Generic.IEnumerable1[System.Collections.Generic.IList1[EFGetStarted.ConsoleApp.Post]]' cannot be used for return type 'System.Collections.Generic.IEnumerable`1[EFGetStarted.ConsoleApp.Post]'

Also tried to change the generic type parameter to:

database.CompileQuery<Post>(@queryModel); but no success.

Any recommendations?

smitpatel commented 8 years ago

Not sure exactly what you are trying to query but following code works (at least not throwing error. I don't have sample data to verify results)

using (var db = new BloggingContext())
{
    var serviceProvider = ((IInfrastructure<IServiceProvider>)db).Instance;
    var database = (IDatabase)serviceProvider.GetService(typeof(IDatabase));
    var queryContextFactory = (IQueryContextFactory)serviceProvider.GetService(typeof(IQueryContextFactory));

    var query = new EnumerableQuery<Blog>(new List<Blog>())
        .Where(x => x.Url != "")
        .SelectMany(x => x.Posts);

    var queryModel = QueryParser.CreateDefault().GetParsedQuery(query.Expression);

    var queryProvider = (IAsyncQueryProvider)serviceProvider.GetService(typeof(IAsyncQueryProvider));

    var fromExpression = Expression.Constant(new EntityQueryable<Blog>(queryProvider));
    queryModel.MainFromClause.FromExpression = fromExpression;

    var compileQueryFn = database.CompileQuery<Post>(@queryModel);
    var result = compileQueryFn(queryContextFactory.Create()).ToList();

}

Changes made:

    .Select(x => x,Posts);

This just throw compilation error on me. , is some invalid syntax. I converted it to x.Posts for which I had to change IBlog to Blog so that I can access the property inside Select. After above changes I hit the same exception as yours. The issue is Select(x => x.Posts) which returns List<Post> therefore return type of query becomes Enumerable<List<Post>> while the return type of CompileQuery<Post> is Enumerable<Post>. If you are looking to select posts in all blogs then you should use SelectMany

smitpatel commented 8 years ago

Feel free to re-open the issue if you encounter any further issues or have any questions.