在使用值对象时,如何映射和使用SQL Server用户定义函数与EF Core ?



我想调用一个用户定义的SQL Server函数,并使用一个值对象的属性作为参数。EF Core文档只展示了具有基本类型的示例。我无法创建一个工作映射。

业务领域的实体需要支持多语言文本属性。文本由用户提供。我们创建了一个内部使用字典的多语言值对象(ML)。在数据库中,多语言属性以JSON形式保存在nvarchar(max)列中。EF Core转换器处理与字符串数据类型之间的转换。

这是一个简化的国家实体,以便理解下面的测试代码:

public class Country : VersionedEntity
{
public string CountryId { get; set; }
public ML<CountryName> Name { get; set; }
}

数据库中的示例行如下所示:

德吴|[{"language"de-DE","value":"Deutschland"},{"language"en-US","value":"Germany"},{"language"it-IT","value":"Tedesco"}]

文本(即产品描述)可能不会被翻译成所有支持的语言。因此,用户可以定义一个语言偏好列表。将显示最佳匹配语言。

由于分页需要在数据库中排序,所以我在SQL Server中创建了一个用户定义函数GetLanguage,它为每行返回最佳匹配语言,并允许根据每个用户的语言偏好进行排序。

我声明的函数DbContext

public string? GetLanguage(string json, string preferredLanguages)
=> throw new NotSupportedException($"{nameof(GetLanguage)} cannot be called client side");

并映射到

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDbFunction(typeof(AccountsDbContext)
.GetMethod(nameof(GetLanguage),
new[] { typeof(string), typeof(string) })!);
}

我尝试在测试LINQ查询中调用该函数

string preferredLanguages = "de-DE,en-US,fr-FR";    
List<string?> list = _dbContext.Country.Select(c => _dbContext.GetLanguage(c.Name, preferredLanguages)).ToList();

不编译。c.Name创建一个错误,因为它的类型是ML而不是类型的字符串作为函数的第一个参数定义。

我期望EF Core不会转换属性,因为该函数在服务器上运行,只需要使用数据库的表列。它应该创建像

这样的SQL
Select dbo.GetLanguage(name, 'de-DE,en-US,fr-FR');

是否有办法告诉EF Core只使用表列?

为了测试,我尝试将函数声明更改为
public string? GetLanguage(ML<CountryName> json, string preferredLanguages)
=> throw new NotSupportedException($"{nameof(GetLanguage)} cannot be called client side");

和到

的映射
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDbFunction(typeof(AccountsDbContext)
.GetMethod(nameof(GetLanguage),
new[] { typeof(ML<CountryName>), typeof(string) })!);
}

代码编译了,但是我得到一个运行时错误。

系统。的参数'json'DbFunction"App.Accounts.Persistence.AccountsDbContext.GetLanguage (App.SharedKernel.Domain.ML祝辞,字符串)"有一个无效的类型'ML'。确保参数类型可以由当前提供程序映射。

类型是相同的,一个是完全限定的,另一个不是。我不明白为什么会出现这个错误,我该如何修复它。

当使用带有转换器的值对象时,这似乎是一个普遍问题。EF Core应该创建使用DB函数和表列的SQL,因为一切都应该在服务器上运行。我怎样才能做到这一点呢?

在https://github.com/dotnet/efcore/issues/28393上给出了答案。

然而,我们目前还没有很好的面向用户的api在复杂的情况下(此外,#25980可能会暂时阻止此操作)

但是,如果您只打算将列传递给函数,而不是例如,参数,那么应该可以让函数接受中显式指定存储类型函数映射

因此,一个解决方法是使用object作为DbFunction定义的类型,并声明数据库类型:

modelBuilder.HasDbFunction(typeof(AccountsDbContext)
.GetMethod(nameof(GetLanguage), new[] { typeof(object), typeof(string) })!,
builder =>
{
builder.HasParameter("json").HasStoreType("varchar(max)");
builder.HasParameter("preferredLanguages");
});

然后用对象类型转换调用函数:

string preferredLanguages = "de-DE,en-US,fr-FR";    
List<string?> list = _dbContext.Country.Select(c => _dbContext.GetLanguage((object)c.Name, preferredLanguages)).ToList();

创建清洁的SQL:

SELECT [dbo].[GetLanguage]([c],[CountryName], @__preferredLanguages_1

最新更新