Свойства перехвата первого запроса кода EF 4.2 и навигации - PullRequest
0 голосов
/ 13 января 2012

Примечание. Фактические сущности в моем проекте разные. Сценарий, который вы собираетесь прочитать, является упрощенным примером. Мой реальный пример затрагивает гораздо больше сущностей, чем перечислено здесь.

В моем проекте у меня есть следующие классы участников и групп:

public class Member
{
    public string Name { get; set; }
    public IList<Group> Groups { get; set; }
}

public class Group
{
    public string Name { get; set; }
    public IList<Member> Members { get; set; }
}

Моя реализация DbContext (код ModelBuilder опущен):

public class Db : DbContext
{
    public DbSet<Group> Groups { get; set; }
    public DbSet<Member> Members { get; set; }
}

Допустим, у меня есть требование, чтобы DbContext возвращал только те группы и участников, чье имя начинается с "X". Я могу реализовать это, изменив класс Db следующим образом:

public class Db : DbContext
{
    public IQueryable<Group> Groups
    {
        get
        {
        return from g in ((IObjectContextAdapter)this).CreateQuery<Group>("SELECT VALUE Groups FROM Groups")
                where g.Name.StartsWith("X")
                select g;
        }
    }

    public IQueryable<Member> Members
    {
        get
        {
        return from m in ((IObjectContextAdapter)this).CreateQuery<Member>("SELECT VALUE Members FROM Members")
                where m.Name.StartsWith("X")
                select m;
        }
    }
}

Теперь запросы, подобные следующим, возвращают только членов и группы с именами, начинающимися с «X»:

var members = db.Members.ToList();
var groups = db.Groups.ToList();

Проблема здесь связана с INCLUDES ...

var members = db.Members.Include(m => m.Groups).ToList();
var groups = db.Groups.Include(g => g.Members).ToList();

В то время как список «members» содержит только «members», имена которых начинаются с «X», свойство Groups содержит объекты Group с именами, которые не соответствуют. Тот же шаблон применяется к списку «групп» с несоответствующими объектами Member.

Есть ли в EF 4.2 функция, которую мне не хватает?

Как я могу повлиять на запросы, сгенерированные из свойств навигации?

Ответы [ 2 ]

1 голос
/ 16 января 2012

Примечание: я понимаю, что это может использовать некоторые уточнения. Это работает в моем сценарии. Код не гарантированно компилируется.

То, что я в итоге построил, это способ переопределить выполнение запроса и объединить метод, описанный в Совет 37 - Как сделать условное включение , чтобы заполнить кэш DbContext требуемыми связанными объектами.

Цели разработки:

  • Предоставить способ перехвата выполнения запроса
  • Необходимо предотвратить выполнение определенных операторов "Включить", зная, какие свойства навигации автор запроса имел в виду "Включить"
  • Необходимо заполнить кеш и перенаправить запрос в кеш, предотвращая ненужные вызовы в удаленное хранилище

Чтобы перехватить запрос:

  • Создать ReLinqContext<T>
  • Набор включает опции фильтрации (необязательно)
  • Определить функцию перехвата, которая принимает IQueryable<T>, возвращает IQueryable<T> и присваивает функцию ReLinqContext<T>
  • Определите базовый запрос и вызовите метод расширения AsReLinqQuery(), передав ReLinqContext<T> сверху

Имея в игре дополнительные классы, приведенные ниже, я могу изменить свои свойства DbContext следующим образом:


Пример тела Get для свойства DbContext.Groups

// Enables tracking of ReLinq queries
var ctx = new ReLinqContext<Group>();

// Configures ctx to log, but avoid execution of the Include(string) method
ctx.DisableIncludes();

ctx.Intercept = q =>
{
    // Extract the ObjectQuery<T> out of the DbSet<T>
    // This must be done for q.ChangeSource to work
    var groups = from a in Set<Group>.AsObjectQuery();

    // Rewrite the query to point to a new data source...
    var rewrittenQ = q.ChangeSource(groups);

    // load the results into the context...
    rewrittenQ.Load();

    // Get group ids from the cache
    var groupIds = (from g in Set<Group>().Local
                    select g.Id).ToList();

    // Load respective Member objects into the context...
    if (ctx.IncludePaths.Contains("Members"))
    {
        var members = from m in Set<Member>()
                      from g in m.Groups
                      where groupIds.Contains(g.Id) && m.Name.StartsWith("X")
                      select m;

        members.Load();
    }

    // Add additional if (ctx.IncludePaths.Contains("...")) checks here

    // Return a query that will execute against the DbContext cache
    return q.ChangeSource(Set<Group>().Local.AsQueryable());
};

// The call to ChangeSource during interception
// will allow actual data to be returned.
return new Group[0].AsReLinqQuery(ctx);

Дополнительный код для реализации ReLinqQueries

public static class ReLinqExtensions
{
    public static IQueryable<T> ChangeSource<T>(this IQueryable<T> oldSource, IQueryable<T> newSource)
    {
        return (IQueryable<T>) QueryableRebinder.Rebind(oldSource, newSource);
    }

    public static IReLinqQuery<T> AsReLinqQuery<T>(this IEnumerable<T> enumerable, IReLinqContext<T> context)
    {
        return AsReLinqQuery(enumerable.AsQueryable(), context);
    }

    public static IReLinqQuery<T> AsReLinqQuery<T>(this IQueryable<T> query, IReLinqContext<T> context)
    {
        return new ReLinqQuery<T>(query, (IReLinqContext)context);
    }

    public static IReLinqContext<T> DisableIncludes<T>(this IReLinqContext<T> context)
    {
        context.AllowIncludePath = path => false;
        return context;
    }
}

public static class DbSetExtensions
{
    public static ObjectQuery<T> AsObjectQuery<T>(this DbSet<T> source) where T : class
    {
        return (ObjectQuery<T>)DbSetUnwrapper.UnWrap(source);
    }
}

public interface IReLinqContext
{
    IList<string> IncludePaths { get; }
    Delegate Intercept { get; }
    Func<string, bool> AllowIncludePath { get; }
}

public interface IReLinqContext<T>
{
    IEnumerable<string> IncludePaths { get; }
    Func<IQueryable<T>, IQueryable<T>> Intercept { get; set; }
    Func<string, bool> AllowIncludePath { get; set; }
}

public class ReLinqContext<T> : IReLinqContext<T>, IReLinqContext
{
    private readonly IList<string> _includePaths; 

    public ReLinqContext()
    {
        _includePaths = new List<string>();
        IncludePaths = new ReadOnlyCollection<string>(_includePaths);
        Intercept = q => q;
        AllowIncludePath = path => true;
    }

    public IEnumerable<string> IncludePaths { get; private set; }
    public Func<IQueryable<T>, IQueryable<T>> Intercept { get; set; }
    public Func<string, bool> AllowIncludePath { get; set; }

    IList<string> IReLinqContext.IncludePaths { get { return _includePaths; }}

    Delegate IReLinqContext.Intercept
    {
        get
        {
            return Intercept;
        }
    }
}

public interface IReLinqQuery<T> : IOrderedQueryable<T>
{
    IReLinqContext<T> Context { get; }
    IReLinqQuery<T> Include(String path);
}

public class ReLinqQuery<T> : IReLinqQuery<T>
{
    public IReLinqContext<T> Context { get; private set; }
    private Expression expression = null;
    private ReLinqQueryProvider provider = null;

    public ReLinqQuery(IQueryable source, IReLinqContext context)
    {
        Context = (IReLinqContext<T>)context;
        expression = Expression.Constant(this);
        provider = new ReLinqQueryProvider(source, context);
    }

    public ReLinqQuery(IQueryable source, IReLinqContext context, Expression e)
    {
        if (e == null) throw new ArgumentNullException("e");
        expression = e;
        provider = new ReLinqQueryProvider(source, context);
    }

    public IEnumerator<T> GetEnumerator()
    {
        return ((IEnumerable<T>)provider.Execute(expression)).GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return ((IEnumerable)provider.Execute(expression)).GetEnumerator();
    }

    public IReLinqQuery<T> Include(String path)
    {
        ((IReLinqContext)Context).IncludePaths.Add(path);

        if (!Context.AllowIncludePath(path))
        {
            return this;
        }

        var possibleObjectQuery = provider.Source as DbQuery<T>;

        if (possibleObjectQuery != null)
        {
            return new ReLinqQuery<T>(possibleObjectQuery.Include(path), (IReLinqContext)Context);
        }

        return this;
    }

    public Type ElementType
    {
        get { return typeof(T); }
    }

    public Expression Expression
    {
        get { return expression; }
    }

    public IQueryProvider Provider
    {
        get { return provider; }
    }
}

public class ReLinqQueryProvider : IQueryProvider
{
    internal IQueryable Source { get; private set; }
    internal IReLinqContext Context { get; private set; }

    public ReLinqQueryProvider(IQueryable source, IReLinqContext context)
    {
        if (source == null) throw new ArgumentNullException("source");
        Source = source;
        Context = context;
    }

    public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
    {
        if (expression == null) throw new ArgumentNullException("expression");

        return new ReLinqQuery<TElement>(Source, Context, expression);
    }

    public IQueryable CreateQuery(Expression expression)
    {
        if (expression == null) throw new ArgumentNullException("expression");
        Type elementType = expression.Type.GetGenericArguments().Single();
        IQueryable result = (IQueryable)Activator.CreateInstance(typeof(ReLinqQuery<>).MakeGenericType(elementType),
                new object[] { Source, Context, expression });
        return result;
    }

    public TResult Execute<TResult>(Expression expression)
    {
        if (expression == null) throw new ArgumentNullException("expression");
        object result = (this as IQueryProvider).Execute(expression);
        return (TResult)result;
    }

    public object Execute(Expression expression)
    {
        if (expression == null) throw new ArgumentNullException("expression");

        var translated = ReLinqQueryUnwrapper.UnWrap(expression, Source);
        var translatedQuery = Source.Provider.CreateQuery(translated);
        //var query = CreateQuery(expression);

        var interceptedQuery = Context.Intercept.DynamicInvoke(translatedQuery);

        return interceptedQuery;
    }
} 

public class ReLinqQueryUnwrapper : ExpressionVisitor
{
    private readonly IQueryable _source;

    public static Expression UnWrap(Expression expression, IQueryable source)
    {
        var queryTranslator = new ReLinqQueryUnwrapper(source);

        return queryTranslator.Visit(expression);
    }

    public ReLinqQueryUnwrapper(IQueryable source)
    {
        _source = source;
    }

    #region Visitors
    protected override Expression VisitConstant(ConstantExpression c)
    {
        if (c.Type == typeof(ReLinqQuery<>).MakeGenericType(_source.ElementType))
        {
            return _source.Expression;
        }

        return base.VisitConstant(c);
    }
    #endregion
}

public class DbSetUnwrapper : ExpressionVisitor
{
    public static IQueryable UnWrap(IQueryable source)
    {
        var dbSetUnwrapper = new DbSetUnwrapper(source);
        dbSetUnwrapper.Visit(source.Expression);
        return dbSetUnwrapper._target;
    }

    private readonly IQueryable _source;
    private IQueryable _target;

    public DbSetUnwrapper(IQueryable source)
    {
        _source = source;
    }

    public override Expression Visit(Expression node)
    {
        if(node.NodeType == ExpressionType.Constant)
        {
            var c = (ConstantExpression) node;

            if (c.Type == typeof(ObjectQuery<>).MakeGenericType(_source.ElementType))
            {
                _target = (IQueryable)c.Value;
            }
        }

        return base.Visit(node);
    }
}

public class QueryableRebinder : ExpressionVisitor
{
    private IQueryable _oldSource;
    private IQueryable _newSource;

    public static IQueryable Rebind(IQueryable oldSource, IQueryable newSource)
    {
        var queryTranslator = new QueryableRebinder(oldSource, newSource);

        return newSource.Provider.CreateQuery(queryTranslator.Visit(oldSource.Expression));
    }

    public QueryableRebinder(IQueryable oldSource, IQueryable newSource)
    {
        _oldSource = oldSource;
        _newSource = newSource;
    }

    #region Visitors
    protected override Expression VisitConstant(ConstantExpression c)
    {
        if (typeof(IQueryable<>).MakeGenericType(_oldSource.ElementType).IsAssignableFrom(c.Type))
        {
            return Expression.Constant(_newSource);
        }

        return base.VisitConstant(c);
    }
    #endregion
}
0 голосов
/ 13 января 2012
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...