[英]How to use expressions to build a LINQ query dynamically when using an interface to get the column name?
我正在使用 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
基本上,給定一個包含TEntity類型實體的 DbSet 和一個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”的列表,而不是存儲在數據庫中的實際 ID。
我覺得我接近一個解決方案,但我只是在表達的東西上繞了一圈,以前沒有做過很多。 如果有人可以幫助使用 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 包:
在這種情況下IEntityWithKey
接口是多余的。 要從BuildSelectExpression
方法訪問KeyPropertyName
值,您需要有實體實例,但您只有 object 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);
}
這是我為通用方法最終得到的代碼:
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);
}
}
最終不需要接口,因為代碼可以利用 EF Core 對[Key]
屬性的使用。
感謝@Krzysztof 為我指明了正確的方向。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.