EF, динамический выбор поля из базы данных для разных языков
Проблема какая:
В БД есть сущности, например города (Cities), у каждого города есть название на нескольких языках - ru_Name, en_Name и тд.
В коде, я хочу получать те города, которые в названии имеют какой-то текст:
Cities.Where(x => x.Name.Contains("something"))
Но, в зависимости от пришедшего в потоке языка (Thread.CurrentUICulture), мне, соответственно, необходимо .Name заменять на .ru_Name, en_Name и тд. чтобы поиск был по нужной колонке.
Вопрос: как такое сделать? Это должно быть универсально, так как таких сущностей и полей сотни в проекте.
Предполагаю, что надо использовать ExpressionTree, но с ними опыта никакого и не знаю как подойти к решению данной проблемы.
Ответы (2 шт):
var sql = $"select * dbo.Cities where dbo.Cities.{{Thread.CurrentUICulture}}_Name like '%{searchText}%'";
await _dbContext.Cities.FromSqlRaw(sql); // in dotnet <3.0 use 'FromSql'
Посидев какое-то время в интернете и познавая дзен ExpressionTrees, я всё таки пришёл к решению которое мне нужно - ExpressionVisitor
Небольшой пример:
class Program
{
static void Main()
{
var cities = new List<City>(Enumerable.Range(0, 30).Select(x => new City { ru_Name = $"ru{x}", en_Name = $"en{x * 2}" }))
.AsQueryable();
var exprVisitor = new ExprVisitor();
var filter = "1";
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("ru");
var query = cities
.Where(x => x.Name.Contains(filter))
.Visit(exprVisitor); // changing query here
foreach (var city in query)
Console.WriteLine(city.Name);
Console.ReadLine();
}
}
public class City
{
[ExpressionLocalizable]
public string Name
{
get;set; // здесь должны быть правильные доступы к полям на основе CurrentUICulture, но опустим это
}
public string ru_Name { get; set; }
public string en_Name { get; set; }
}
[AttributeUsage(AttributeTargets.Property)]
public class ExpressionLocalizableAttribute : Attribute { }
public class ExprVisitor : ExpressionVisitor
{
protected override Expression VisitMember(MemberExpression node)
{
if (node.Member.GetCustomAttribute<ExpressionLocalizableAttribute>() == null)
return base.VisitMember(node);
var nodeType = node.Expression.Type;
var localizedPropertyName = $"{Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName}_{node.Member.Name}";
var property = nodeType.GetProperty(localizedPropertyName);
if (property == null)
{
throw new NotSupportedException($"No such property '{localizedPropertyName}' in type '{nodeType.Name}'!");
}
return Expression.MakeMemberAccess(node.Expression, property);
}
}
public static class QueryExtensions
{
public static IQueryable<TResult> Visit<TResult>(this IQueryable<TResult> query, ExpressionVisitor visitor)
{
var expr = visitor.Visit(query.Expression);
return query.Provider.CreateQuery<TResult>(expr);
}
}
Что происходит:
Для модификации Query, используется ExpressionVisitor. Данный класс обходит Expression, и в случае когда посещает MemberExpression(что является node.NodeType == ExpressionType.MemberAccess), в данном случае это x.Name, проверяется у свойства Name атрибут ExpressionLocalizable, который является меткой того, что данное свойство надо будет заменить. А далее формирует имя свойства на которое необходимо заменить, получение данного свойства, и метод возвращает новый MemberExpression.
После обхода этим ExpressionVisitorом, я получаю новый Expression, из которого делаю новый IQueryable.
P.S.
1. Для определения что свойство надо заменять я использовал атрибут, соответственно этот атрибут надо расставлять во всех моделях. У меня их +-100, так что если вам лень столько менять - придётся искать другой способ для определения необходимости замены
2. Если у вас используется механизм кэширования скомпилированных Query/Expression, то после первой компиляции для первого условия (для первого языка) вы уже не получите метод для другого, поэтому необходимо решить проблему кеширования с ключом, основанным на этом условии. (наверно будут решать это через CuncurrencyCollection<string, Func<>>, где ключ это язык)
3. Также, дабы добавить гибкости, можно навесить параметры в атрибут, например чтобы была возможность добавить переопределение названия полей.
Кому интересно посмотреть, этот же пример: GitHub