Как использовать выражения для динамического построения запроса LINQ при использовании интерфейса для получения имени столбца? - PullRequest
3 голосов
/ 07 ноября 2019

Я использую Entity Framework Core для хранения и получения некоторых данных. Я пытаюсь написать метод общего назначения, который будет работать на любом DbSet<T>, чтобы избежать дублирования кода. Этот метод выполняет запрос LINQ к набору, для которого ему нужно знать столбец «ключ» (т. Е. Первичный ключ таблицы).

Чтобы помочь с этим, я определил интерфейс, который возвращаетимя свойства, представляющего ключевой столбец. Затем сущности реализуют этот интерфейс. Следовательно, у меня есть что-то вроде этого:

interface IEntityWithKey
{
    string KeyPropertyName { get; }
}

class FooEntity : IEntityWithKey
{
    [Key] public string FooId { get; set; }
    [NotMapped] public string KeyPropertyName => nameof(FooId);
}

class BarEntity : IEntityWithKey
{
    [Key] public string BarId { get; set; }
    [NotMapped] public string KeyPropertyName => nameof(BarId);
}

Метод, который я пытаюсь написать, имеет такую ​​сигнатуру:

static List<TKey> GetMatchingKeys<TEntity, TKey>(DbSet<TEntity> dbSet, List<TKey> keysToFind)
    where TEntity : class, IEntityWithKey

В основном, с учетом DbSet, содержащего сущности типа TEntity и список ключей типа TKey , метод должен возвращать список ключей, которые в настоящее время существуют в связанной таблице в базе данных.

Запрос выглядит следующим образом:

dbSet.Where(BuildWhereExpression()).Select(BuildSelectExpression()).ToList()

В BuildWhereExpression я пытаюсь создать соответствующий Expression<Func<TEntity, bool>>, а в BuildSelectExpression Я пытаюсь создать соответствующий Expression<Func<TEntity, TKey>>. Тем не менее, я борюсь с созданием выражения Select (), что проще всего. Вот что у меня получилось:

Expression<Func<TEntity, TKey>> BuildSelectExpression()
{
    // for a FooEntity, would be:  x => x.FooId
    // for a BarEntity, would be:  x => x.BarId

    ParameterExpression parameter = Expression.Parameter(typeof(TEntity));
    MemberExpression property1 = Expression.Property(parameter, nameof(IEntityWithKey.KeyPropertyName));
    MemberExpression property2 = Expression.Property(parameter, property1.Member as PropertyInfo);
    UnaryExpression result = Expression.Convert(property2, typeof(TKey));
    return Expression.Lambda<Func<TEntity, TKey>>(result, parameter);
}

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

List<string> keys = GetMatchingKeys(context.Foos, new List<string> { "foo3", "foo2" });

Он генерирует этот запрос, который выглядит хорошо (примечание: реализации Where () пока нет):

SELECT "f"."FooId"
FROM "Foos" AS "f"

Но запрос просто возвращаетсписок, содержащий "FooId", а не фактические идентификаторы, хранящиеся в базе данных.

Мне кажется, что я близок к решению, но я просто немного разбираюсь в кругах с выражениями, не сделав многооб этом раньше. Если кто-то может помочь с выражением Select (), которое будет началом.

Вот полный код:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace StackOverflow
{
    interface IEntityWithKey
    {
        string KeyPropertyName { get; }
    }

    class FooEntity : IEntityWithKey
    {
        [Key] public string FooId { get; set; }
        [NotMapped] public string KeyPropertyName => nameof(FooId);
    }

    class BarEntity : IEntityWithKey
    {
        [Key] public string BarId { get; set; }
        [NotMapped] public string KeyPropertyName => nameof(BarId);
    }

    class TestContext : DbContext
    {
        public TestContext(DbContextOptions options) : base(options) { }
        public DbSet<FooEntity> Foos { get; set; }
        public DbSet<BarEntity> Bars { get; set; }
    }

    class Program
    {
        static async Task Main()
        {
            IServiceCollection services = new ServiceCollection();
            services.AddDbContext<TestContext>(
            options => options.UseSqlite("Data Source=./test.db"),
                contextLifetime: ServiceLifetime.Scoped,
                optionsLifetime: ServiceLifetime.Singleton);
            services.AddLogging(
                builder =>
                {
                    builder.AddConsole(c => c.IncludeScopes = true);
                    builder.AddFilter(DbLoggerCategory.Infrastructure.Name, LogLevel.Error);
                });
            IServiceProvider serviceProvider = services.BuildServiceProvider();

            var context = serviceProvider.GetService<TestContext>();
            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();

            context.Foos.AddRange(new FooEntity { FooId = "foo1" }, new FooEntity { FooId = "foo2" });
            context.Bars.Add(new BarEntity { BarId = "bar1" });
            await context.SaveChangesAsync();

            List<string> keys = GetMatchingKeys(context.Foos, new List<string> { "foo3", "foo2" });
            Console.WriteLine(string.Join(", ", keys));

            Console.WriteLine("DONE");
            Console.ReadKey(intercept: true);
        }

        static List<TKey> GetMatchingKeys<TEntity, TKey>(DbSet<TEntity> dbSet, List<TKey> keysToFind)
            where TEntity : class, IEntityWithKey
        {
            return dbSet
                //.Where(BuildWhereExpression())   // commented out because not working yet
                .Select(BuildSelectExpression()).ToList();

            Expression<Func<TEntity, bool>> BuildWhereExpression()
            {
                // for a FooEntity, would be:  x => keysToFind.Contains(x.FooId)
                // for a BarEntity, would be:  x => keysToFind.Contains(x.BarId)

                throw new NotImplementedException();
            }

            Expression<Func<TEntity, TKey>> BuildSelectExpression()
            {
                // for a FooEntity, would be:  x => x.FooId
                // for a BarEntity, would be:  x => x.BarId

                ParameterExpression parameter = Expression.Parameter(typeof(TEntity));
                MemberExpression property1 = Expression.Property(parameter, nameof(IEntityWithKey.KeyPropertyName));
                MemberExpression property2 = Expression.Property(parameter, property1.Member as PropertyInfo);
                UnaryExpression result = Expression.Convert(property2, typeof(TKey));
                return Expression.Lambda<Func<TEntity, TKey>>(result, parameter);
            }
        }
    }
}

При этом используются следующие пакеты NuGet:

  • Microsoft.EntityFrameworkCore, версия 3.0.0
  • Microsoft.EntityFrameworkCore.Sqlite, версия 3.0.0
  • Microsoft.Extensions.DependencyInjection, версия 3.0.0
  • Microsoft.Extensions.Logging.Console, Версия 3.0.0

Ответы [ 2 ]

3 голосов
/ 07 ноября 2019

В этом случае IEntityWithKey интерфейс является избыточным. Чтобы получить доступ к KeyPropertyName значению из метода BuildSelectExpression, вам понадобится экземпляр сущности, но у вас есть только Type объект.

Вы можете использовать отражение, чтобы найти имя ключевого свойства:

Expression<Func<TEntity, TKey>> BuildSelectExpression()
{
    // Find key property
    PropertyInfo keyProperty = typeof(TEntity).GetProperties()
        .Where(p => p.GetCustomAttribute<KeyAttribute>() != null)
        .Single();

    ParameterExpression parameter = Expression.Parameter(typeof(TEntity));
    MemberExpression result = Expression.Property(parameter, keyProperty);
    // UnaryExpression result = Expression.Convert(property1, typeof(TKey)); this is also redundant
    return Expression.Lambda<Func<TEntity, TKey>>(result, parameter);
}
0 голосов
/ 07 ноября 2019

Вот код, который я закончил для метода общего назначения:

static List<TKey> GetMatchingKeys<TEntity, TKey>(DbSet<TEntity> dbSet, List<TKey> keysToFind)
    where TEntity : class, IEntityWithKey
{
    PropertyInfo keyProperty = typeof(TEntity).GetProperties().Single(x => x.GetCustomAttribute<KeyAttribute>() != null);
    return dbSet.Where(BuildWhereExpression()).Select(BuildSelectExpression()).ToList();

    Expression<Func<TEntity, bool>> BuildWhereExpression()
    {
        ParameterExpression entity = Expression.Parameter(typeof(TEntity));
        MethodInfo containsMethod = typeof(List<TKey>).GetMethod("Contains");
        ConstantExpression keys = Expression.Constant(keysToFind);
        MemberExpression property = Expression.Property(entity, keyProperty);
        MethodCallExpression body = Expression.Call(keys, containsMethod, property);
        return Expression.Lambda<Func<TEntity, bool>>(body, entity);
    }

    Expression<Func<TEntity, TKey>> BuildSelectExpression()
    {
        ParameterExpression entity = Expression.Parameter(typeof(TEntity));
        MemberExpression body = Expression.Property(entity, keyProperty);
        return Expression.Lambda<Func<TEntity, TKey>>(body, entity);
    }
}

В конечном счете, в интерфейсе не было необходимости, так как код может использовать преимущества * 1004 в EF Core. * attribute.

Спасибо @Krzysztof за указание в правильном направлении.

...