Самое простое решение – вынести все таки строки в отдельный класс и использовать их только туда.
Но есть еще способ. Это динамически парить такие константы из лямбда выражений. Из преимуществ это дает проверку времени компиляции и более простое переименование, но работает медленнее чем например подход с константами.
Например у нас есть выражение:
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();