EF, динамический выбор поля из базы данных для разных языков

Проблема какая:
В БД есть сущности, например города (Cities), у каждого города есть название на нескольких языках - ru_Name, en_Name и тд.
В коде, я хочу получать те города, которые в названии имеют какой-то текст:
Cities.Where(x => x.Name.Contains("something"))
Но, в зависимости от пришедшего в потоке языка (Thread.CurrentUICulture), мне, соответственно, необходимо .Name заменять на .ru_Name, en_Name и тд. чтобы поиск был по нужной колонке.

Вопрос: как такое сделать? Это должно быть универсально, так как таких сущностей и полей сотни в проекте.

Предполагаю, что надо использовать ExpressionTree, но с ними опыта никакого и не знаю как подойти к решению данной проблемы.


Ответы (2 шт):

Автор решения: Sergey
var sql = $"select * dbo.Cities where dbo.Cities.{{Thread.CurrentUICulture}}_Name like '%{searchText}%'";
await _dbContext.Cities.FromSqlRaw(sql); // in dotnet <3.0 use 'FromSql'
→ Ссылка
Автор решения: Alexandr Romanenko

Посидев какое-то время в интернете и познавая дзен 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

→ Ссылка