简体   繁体   中英

How Can I Use a Method Returning IQueryable<T> in Compiled Linq to SQL Query Without Error

I have a method returning an IQueryable that I use in a compiled query. This method prevents me from having to incorporate a very large linq to sql query repeatedly while modifying just the WHERE clause. For example, internally we reference these items using a DB-generated identity column so I have one function that takes that the internal id value and returns the object searching on the internal id column. For our web services, we get a different id and I have another function that uses the same underlying query with only the different where clause (or singleordefault, etc.). All of this works great until I try to use this as a compiled query (which is essential in this case for performance). When I invoke the compiled query, I receive "The query contains references to items defined on a different data context." with a gigantic stack trace. Note that this only happens for a certain sequence of execution when this is running on our application server. If I set up a simple test case that just calls this method it works fine.

My question is, how can I get this working under NET 4.5? I realize that I was probably "lucky" in NET 4 that this worked since I was using the version of my function that references an instance variable for the datacontext in my dataaccess class. Here's the original version that worked under 4.0:

public IQueryable<Log> GetBaseLogQuery(EncDataContext context)
{
    var query =
        from pl in context.DbLog
        ... (lots of joins and lets)
        select new Log
        {
            LogID = pl.LogId,
            UniqueID = pl.UniqueID,
            ... many other memebers set
        }
    ;
    return query;
}

public IQueryable<Log> GetBaseLogQuery()
{
    return GetBaseQuery(Context); // Context is an instance variable
}

private static Func<EncDataContext, int, string, Log> _loadLogByUniqueId;
public Log LoadLog(int organizationId, string uniqueId)
{
    if (_loadLogByUniqueId == null)
    {
        var query = GetBaseLogQuery();
        _loadLogByUniqueId = CompiledQuery.Compile<EncDataContext, int, string, Log>(
            (context, orgId, uid) => query.SingleOrDefault(pl => pl.OrganizationId == orgId && pl.UniqueID == uid)
        );
    }
    var ret = _loadLogByUniqueId.Invoke(Context, organizationId, uniqueId);
    return ret;
}

If I try to use the method and pass it the datacontext directly in the compile like this:

_loadLogByUniqueId = CompiledQuery.Compile<EncDataContext, int, string, Log>(
    (context, orgId, uid) => GetBaseLogQuery(context).SingleOrDefault(pl => pl.OrganizationId == orgId && pl.UniqueID == uid)

I get a different error -- "Member access 'Int32 LogID' of 'BusinessEntities.Log' not legal on type 'System.Linq.IQueryable`1[BusinessEntities.Log]."

So after some research people mention needing to expand the method before compile time so that it can be translated into SQL by linq to sql. However, I'm not able to get this working. When I try to assign the output of GetBaseLogQuery to an expression so that I can call the LinqKit invoke on it, I get a type mismatch.

Is there any way for me to not have to duplicate the linq to sql query in GetBaseLogQuery in every compiled query I currently use it in?

Just a random try:

Transform your GetBaseLogQuery function to :

Func<EncDataContext,IQueryable<Log>> getBaseLogQuery;
public Expression<Func<EncDataContext,IQueryable<Log>>> GetBaseLogQuery(EncDataContext context)
{
    if(getBaseLogQuery==null)
        getBaseLogQuery=GetBaseLogQueryExpr().Compile();
    return getBaseLogQuery(context);
}

public Expression<Func<EncDataContext,IQueryable<Log>>> GetBaseLogQueryExpr()
{
    return context=>from pl in context.DbLog ..... etc ....; // contrainst: this MUST be a lambda.
}

And then, create this function:

static Expression<Func<EncDataContext, TArg1, TArg2, TReturn>> WrapQuery<T, TArg1, TArg2, TReturn>(Expression<Func<IQueryable<T>, TArg1, TArg2, TReturn>> todo)
{
    var baseQuery = GetBaseLogQueryExpr();
    var newQueryExpression = new ReplaceVisitor(todo.Parameters.First(), baseQuery.Body).Visit(todo.Body);
    List<ParameterExpression> @params = todo.Parameters.Skip(1).ToList();
    @params.Insert(0, baseQuery.Parameters.First());
    return Expression.Lambda<Func<EncDataContext, TArg1, TArg2, TReturn>>(newQueryExpression, @params.ToArray());
}

using class

    class ReplaceVisitor : ExpressionVisitor
    {
        readonly Expression to;
        readonly ParameterExpression @from;

        public ReplaceVisitor(ParameterExpression from, Expression to)
        {
            this.to = to;
            this.@from = @from;
        }

        public override Expression Visit(Expression node)
        {
            if (node == @from)
                return @to;
            return base.Visit(node);
        }
    }

You can create your query extension like this:

var resultExpr = WrapQuery((query,orgId,uid)=>query.SingleOrDefault(pl =>  pl.OrganizationId == organizationId && pl.UniqueID == uniqueId));
_loadLogByUniqueId = CompiledQuery.Compile(resultExpr);

Not tested.

Pros: Can be reused.

Cons: You'll have to rewrite a new WrapQuery function for each parameter count (that's quite easy, though... just a dump copy/paste/adapt)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM