Интересности      Книги      Утилиты    

30 января 2017 г.

Заменяем строковые константы с именами свойств в коде без рефлексии

Вполне себе практическая задача. В коде много строковых констант с именами свойств (методов, классов…). Хочется избежать таких магических строк, но без использования рефлексии, так как она достаточно медленно отрабатывает.
Самое простое решение – вынести все таки строки в отдельный класс и использовать их только туда.
Но есть еще способ. Это динамически парить такие константы из лямбда выражений. Из преимуществ это дает проверку времени компиляции и более простое переименование, но работает медленнее чем например подход с константами.
Например у нас есть выражение:
DocumentsRepository.Where(p => p.Name == "MyDoc.txt", "Tags", "SubDocuments", "Comments");

Суть – имеем какой-то репозиторий, у него есть реализованная операция Where. Ну пусть например так:
public IQueryable<T> Where(Expression<Func<T, bool>> expression, params string[] children)
{
    if (expression == null)
    {
        throw new ArgumentNullException("expression", "expression is null.");
    }
     if (children == null)
    {
        throw new ArgumentNullException("expression", "expression is null.");
    }
     IQueryable<T> query = this.ObjectSet;
    foreach (string child in children)
    {
        query = query.Include(child);
    }
    return query.Where(expression);
}

В качестве аргументов, передаются те подобъекты которые мы хотим подгрузить также из хранилища, потому что выключен lazy loading и подобъекты по умолчанию не вытягиваются. В идеале хотелось бы поменять выражение на такое:
DocumentsRepository.Where(p => p.Name == "MyDoc.txt")
    .With(p => p.Tags)
    .With(p => p.SubDocuments)
    .With(p => p.Comments);

Итак, реализуем. Первым шагом необходимо добавить метод расширения (extension method) для того чтобы создать такой плавающий синтаксис для With, вторым шагом – парсить лямбда выражение и подставлять строку для подгруздки дочерних объектов. With будет наш самописный метод расширения чтобы добавить вложенные сущности к загрузке вместе с обьектом из хранилища, по типу Include из EF.
public static IQueryable<Document> With<TPropOut>(this IQueryable<Document> query, 
Expression<Func<Document, TPropOut>> action)
{
    string name = Resolve(action);
    return query.Include(name);
}

Базовый вариант для парсинга строк:
private static string Resolve<TIn, TOut>(Expression<Func<TIn, TOut>> action)
{
    string path = new QueryPathVisitor().GetPathForProperty(action);
    return path;
}


И сам парсер:
private class QueryPathVisitor : ExpressionVisitor
{
    private Stack<string> stack;
    public string GetPathForProperty(Expression expression)
    {
        stack = new Stack<string>();
        Visit(expression);
        return stack.Aggregate(new StringBuilder(), 
                                  (sb, name) => (sb.Length > 0 ? sb.Append(".") : sb)
                                      .Append(name)).ToString();
    }
    protected override Expression VisitMember(MemberExpression expression)
    {
        if (stack != null)
            stack.Push(expression.Member.Name);
        return base.VisitMember(expression);
    }
    protected override Expression VisitMethodCall(MethodCallExpression expression)
    {
        if (IsLinq(expression.Method))
        {
            for (int i = 1; i < expression.Arguments.Count; i++)
            {
                Visit(expression.Arguments[i]);
            }
            Visit(expression.Arguments[0]);
            return expression;
        }
        return base.VisitMethodCall(expression);
    }
    private static bool IsLinq(MethodInfo method)
    {
        if (method.DeclaringType != typeof(Queryable) && method.DeclaringType != typeof(Enumerable))
            return false;
         return Attribute.GetCustomAttribute(method, typeof(ExtensionAttribute)) != null;
    }
    protected override Expression VisitMethodCall(MethodCallExpression expression)
    {
     if (IsLinq(expression.Method))
      {
         for (int i = 1; i < expression.Arguments.Count; i++)
          {
              Visit(expression.Arguments[i]);
         }
          Visit(expression.Arguments[0]);
         return expression;
    }
     return base.VisitMethodCall(expression);
    }

}

Теперь у нас есть возможность заменить выражение со строковыми константами вначале на то которое хотели:
DocumentsRepository.Where(p => p.Name == "MyDoc.txt")
    .With(p => p.Tags)
    .With(p => p.SubDocuments)
    .With(p => p.Comments);

Кстати, если говорить о конкретной реализации репозитория то метод из EntityFramework – Include содержит перегрузку которая принимает лямбда выражение. Но используя свой парсер мы можем применить такой же подход и для других подобных сценариев.

Кстати более сложные свойства можно распарсить так:
Document document = DocumentsRepository
    .Where(p => p.Name == "MyDoc.txt", "Comments", "Comments.Author")
    .SingleOrDefault();

Результат:
Document document = DocumentsRepository.Where(p => p.Name == "MyDoc.txt")
    .With(p => p.Comments)
    .With(p => p.Comments.Select(c => c.Author))
    .SingleOrDefault();

Комментариев нет:

Отправить комментарий