dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.65k stars 3.15k forks source link

JArray should be a supported type #31366

Open Supernectar opened 1 year ago

Supernectar commented 1 year ago

File a bug

When using the Microsoft.EntityFrameworkCore.Cosmos database provider, there is an issue with setting a model property's type as JArray. Attempting to do so results in the following exception being thrown:

System.InvalidOperationException
  HResult=0x80131509
  Message=The 'JArray' property 'MyModel.Data' could not be mapped because the database provider does not support this type. Consider converting the property value to a type supported by the database using a value converter. See https://aka.ms/efcore-docs-value-converters for more information. Alternately, exclude the property from the model using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.

Include your code

To reproduce this bug, you can clone this repo https://github.com/Supernectar/EFCosmosJArrayBug.git. Simply building and running the project should trigger the exception mentioned above.

As a workaround, modifiying MyDbContext.cs to include a ValueConverter stops the exception from being thrown

using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;

public class MyDbContext : DbContext
{
    public MyDbContext() { }

    public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) { }
    public virtual DbSet<MyModel>? MyModels { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<MyModel>()
            .HasKey(o => o.Data);

        modelBuilder.Entity<MyModel>() # Adding this block stops Exception from being thrown
            .Property(e => e.Data)
            .HasConversion(
                v => v.ToString(),
                v => JArray.Parse(v)
            );
    }
}

However, this is not the expected solution, since this way data is stored as a stringified json instead of an actual json array. Cosmosdb supports storing actual json objects/arrays.

Also changing the object type to JObject doesn't throw the error, and I'm able to save it properly as a json object in cosmosdb, which leads me to think that it is a supported datatype. So why is the JObject supported and not the JArray, although they come from the same library?

Include stack traces

System.InvalidOperationException
  HResult=0x80131509
  Message=The 'JArray' property 'MyModel.Data' could not be mapped because the database provider does not support this type. Consider converting the property value to a type supported by the database using a value converter. See https://aka.ms/efcore-docs-value-converters for more information. Alternately, exclude the property from the model using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
  Source=Microsoft.EntityFrameworkCore
  StackTrace:
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.ThrowPropertyNotMappedException(String propertyType, IConventionEntityType entityType, IConventionProperty unmappedProperty) in /_/src/EFCore/Infrastructure/ModelValidator.cs:line 275
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.ValidatePropertyMapping(IModel model, IDiagnosticsLogger`1 logger) in /_/src/EFCore/Infrastructure/ModelValidator.cs:line 153
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger) in /_/src/EFCore/Infrastructure/ModelValidator.cs:line 49
   at Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal.CosmosModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelRuntimeInitializer.Initialize(IModel model, Boolean designTime, IDiagnosticsLogger`1 validationLogger) in /_/src/EFCore/Infrastructure/ModelRuntimeInitializer.cs:line 87
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.GetModel(DbContext context, ModelCreationDependencies modelCreationDependencies, Boolean designTime) in /_/src/EFCore/Infrastructure/ModelSource.cs:line 80
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel(Boolean designTime) in /_/src/EFCore/Internal/DbContextServices.cs:line 86
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model() in /_/src/EFCore/Internal/DbContextServices.cs:line 113
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType) in /_/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ServiceProviderServiceExtensions.cs:line 46
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider) in /_/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ServiceProviderServiceExtensions.cs:line 63
   at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies() in /_/src/EFCore/DbContext.cs:line 467
   at Microsoft.EntityFrameworkCore.DbContext.get_ContextServices() in /_/src/EFCore/DbContext.cs:line 447
   at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies() in /_/src/EFCore/DbContext.cs:line 467
   at Microsoft.EntityFrameworkCore.DbContext.Microsoft.EntityFrameworkCore.Internal.IDbContextDependencies.get_StateManager() in /_/src/EFCore/DbContext.cs:line 217
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.EntryWithoutDetectChanges(TEntity entity) in /_/src/EFCore/Internal/InternalDbSet.cs:line 539
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.<AddAsync>d__19.MoveNext() in /_/src/EFCore/Internal/InternalDbSet.cs:line 206
   at System.Threading.Tasks.ValueTask`1.get_Result() in /_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs:line 785
   at System.Runtime.CompilerServices.ValueTaskAwaiter`1.GetResult() in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.cs:line 126
   at Program.<RunAsync>d__1.MoveNext() in C:\Users\GPATACA\source\WebApplication1\Program.cs:line 36
   at Program.<Main>d__0.MoveNext() in C:\Users\GPATACA\source\WebApplication1\Program.cs:line 21
   at Program.<Main>(String[] args)

Include provider and version information

EF Core version: 7.0.9 Database provider: Microsoft.EntityFrameworkCore.Cosmos Target framework: .NET 6.0 Operating system: Windows 11 IDE: Visual Studio 2022 17.6.5

ajcvickers commented 1 year ago

@Supernectar JObject is supported as an implementation detail and may not be supported in future releases. What is your motivation for using JArray or JObject properties in your entity types?

Supernectar commented 1 year ago

@ajcvickers By excluding support for JArray or JObject, we are essentially limiting the flexibility for users to store objects containing custom fields. This limitation forces developers to create a separate model for each potential body input the user might provide, which is clearly impractical and not a viable solution.

roji commented 1 year ago

Duplicate of #28871 and/or #29825.

The current support generally indeed requires a strongly-typed model. We do have plans to allow for weakly-typed/dynamic models as you'd like. However, note that if that's done via JArray/JObject, that ties you to the specific JSON package used by the provider - that's currently Newtonsoft.Json but should switch to System.Text.Json at some point (and that would be a breaking change). Because of this, simply allow non-JSON .NET Dictionary/List is probably a better approach here.