为 JObject 属性混合 EF Core Convensions 和 DBFunction

Posted

技术标签:

【中文标题】为 JObject 属性混合 EF Core Convensions 和 DBFunction【英文标题】:Mixing EF Core Convensions and DBFunction for a JObject property 【发布时间】:2019-10-09 04:53:44 【问题描述】:

我有一个具有 JObject 类型属性的实体,我需要能够针对这些属性使用 DbFunctions。

当我执行时,项目抛出一个异常,指出 DbFunction 不允许 JObject 类型的参数。

实体就像...

public class OrchestrationRun

   public long Id  get; set; 
    public JObject MetaData  get; set; 
    public JObject SystemMetaData  get; set; 

DbContext 看起来像...

public class MyDbContext : DbContext

    public MyDbContext(DbContextOptions options)
        : base(options)
    
    

    public virtual DbSet<OrchestrationRun> OrchestrationRun  get; set; 

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    
        modelBuilder.ApplyConfiguration(new OrchestrationRunConfiguration());

       // DbFunction mapping for JSON_VALUE
       modelBuilder.HasDbFunction( typeof(MyDbContext).GetMethod(nameof(JsonValue)))
                      .HasName("JSON_VALUE")
                      .HasSchema("");
    

    // DbFunction
    public static string JsonValue(JObject column, [NotParameterized] string path) => throw new NotSupportedException();

OrchestrationRunConfiguration 是...

public class OrchestrationRunConfiguration : IEntityTypeConfiguration<OrchestrationRun>

    public void Configure(EntityTypeBuilder<OrchestrationRun> builder)
    
        builder.Property(e => e.MetaData).HasConversion(
            jObject => jObject != null ? jObject.ToString(Formatting.None) : null,
            json => string.IsNullOrWhiteSpace(json) ? null : JObject.Parse(json)
        );

        builder.Property(e => e.SystemMetaData).HasConversion(
             jObject => jObject != null ? jObject.ToString(Formatting.None): null,
             json => string.IsNullOrWhiteSpace(json) ? null : JObject.Parse(json)
         );
    

我正在尝试执行的查询是...

var dbResponse = (from or in this.dbContext.OrchestrationRun
where MyDbContext.JsonValue(or.MetaData,"$.Product.ProductCategoryName") == "EXAMPLE"
select new
       
          Id = or.Id,
          CategoryId = "EXAMPLE"
       

    ).ToList();

注意:异常发生在 DbContext 实例化。因此查询永远不会被调用。

抛出的异常是……

System.InvalidOperationException:DbFunction 'MyDbContext.JsonValue' 的参数 'column' 具有无效类型 'JObject'。确保参数类型可以被当前提供者映射。 在 Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.ValidateDbFunctions(IModel 模型) 在 Microsoft.EntityFrameworkCore.Internal.SqlServerModelValidator.Validate(IModel 模型) 在 Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ValidatingConvention.Apply(InternalModelBuilder modelBuilder)

【问题讨论】:

看起来这两个功能,数据库函数和值转换根本不能以这种潜在有用的方式一起工作。我会在 EF Core 上创建一个问题:github.com/aspnet/EntityFrameworkCore/issues 你能把“CREATE FUNCTION Json_Value...”的SQL代码贴出来吗?您在那里定义了哪种数据类型? JSON_VALUE 是 SQL Server 的内置函数 【参考方案1】:

以下代码适用于完全相同的场景..

            var jsonValueMethod = typeof(MyDbContext).GetMethod(nameof(MyDbContext.JsonValue));

            builder.HasDbFunction(jsonValueMethod)
                .HasTranslation(args => 
                    return SqlFunctionExpression.Create("JSON_VALUE", args, jsonValueMethod.ReturnType, null);
                )
                .HasParameter("column").Metadata.TypeMapping = new StringTypeMapping("NVARCHAR(MAX)");

下面的行将JObject 列转换为NVARCHAR(MAX) 或任何您的字符串数据类型。

更新:这是 EF Core 5 及更高版本的语法。

        protected override void OnModelCreating(ModelBuilder builder)
        
            ....

            var jsonValueMethod = typeof(QueryExtentions).GetMethod(nameof(QueryExtentions.JsonValue));
            var stringTypeMapping = new StringTypeMapping("NVARCHAR(MAX)");

            builder
                .HasDbFunction(jsonValueMethod)
                .HasTranslation(args => new SqlFunctionExpression("JSON_VALUE", args, nullable: true, argumentsPropagateNullability: new[]  false, false , jsonValueMethod.ReturnType, stringTypeMapping))
                .HasParameter("column").Metadata.TypeMapping = stringTypeMapping;
            .....

【讨论】:

我稍后会测试它。谢谢 @Raghu SqlFunctionExpression.Create() 有效,但它在 .NET 5 中被标记为过时。无法弄清楚如何重写它以使其工作。有什么想法吗? @Nexus .HasTranslation(args => new SqlFunctionExpression("JSON_VALUE", args, nullable: true, argumentsPropagateNullability: new[] false, false , typeof(string), null))跨度> 【参考方案2】:

感谢 Raghu,您的回答对我帮助很大。对于可能来到这里并希望将转换与 json 函数混合并使用最新的 EF Core 5.0 更新 Raghu 的答案的用户:

jsonvalue函数:

public static class JsonExtensions

    public static string JsonValue(object expression, string path) => throw new InvalidOperationException($"nameof(JsonValue) cannot be called client side");

在 DbContext 中 OnModelCreating:

protected override void OnModelCreating(ModelBuilder modelBuilder)

            var jsonValueMethodInfo = typeof(JsonExtensions).GetRuntimeMethod(nameof(JsonExtensions.JsonValue), new[]  typeof(string), typeof(string) );

            modelBuilder
                .HasDbFunction(jsonValueMethodInfo)
                .HasTranslation(args => new SqlFunctionExpression("JSON_VALUE", args, nullable: true, argumentsPropagateNullability: new[]  false, false , typeof(string), null))
                .HasParameter("expression").Metadata.TypeMapping = new StringTypeMapping("NVARCHAR(MAX)"); // conversion

[...]

// example of conversion of a json property
                entity.Property(e => e.AdditionalProperties)
                    .HasColumnName("AdditionalJson")
                    .HasConversion(
                        v => Newtonsoft.Json.JsonConvert.SerializeObject(v, new Newtonsoft.Json.JsonSerializerSettings()  ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore ),
                        v => Newtonsoft.Json.JsonConvert.DeserializeObject<AdditionalUserProperties>(v)
                     );

然后您就可以使用具有转换属性的扩展方法...示例:

    var testId = (from u in this.Users
                     join e in this.Employees.IgnoreQueryFilters() on JsonExtensions.JsonValue(u.AdditionalProperties, "$." + nameof(AdditionalUserProperties.PersonalId)) equals e.PersonalId
                     where u.Id == userId
                     select e.Id).FirstOrDefault();

生成的sql:

[...][e] ON JSON_VALUE([u].[AdditionalJson], N'$.PersonalId') = [e].[PersonalId][...]

【讨论】:

以上是关于为 JObject 属性混合 EF Core Convensions 和 DBFunction的主要内容,如果未能解决你的问题,请参考以下文章

EF Core 5 - 支持字段:找不到属性的指定字段

Blazor 服务器:将 EF Core DbContextFactory 与 DbContext 混合

还在纠结Dapper或者EF Core?不妨试试“混合ORM”--RepoDb

EF Core 数据库迁移(Migration)

EF Core 复杂的 where 子句

EF Core 并发控制