diff --git a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs index 9f72d0b44c4..f77bf697d50 100644 --- a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs @@ -243,6 +243,18 @@ public virtual IReadOnlyList GenerateFluentApiCalls( #pragma warning restore CS0618 } + if (annotations.TryGetValue(RelationalAnnotationNames.ContainerColumnType, out var containerColumnTypeAnnotation) + && containerColumnTypeAnnotation is { Value: string containerColumnType } + && entityType.IsOwned()) + { + methodCallCodeFragments.Add( + new MethodCallCodeFragment( + nameof(RelationalOwnedNavigationBuilderExtensions.HasColumnType), + containerColumnType)); + + annotations.Remove(RelationalAnnotationNames.ContainerColumnType); + } + methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(entityType, annotations, GenerateFluentApi)); return methodCallCodeFragments; diff --git a/src/EFCore.Relational/Extensions/RelationalOwnedNavigationBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalOwnedNavigationBuilderExtensions.cs index 158c3993cfc..17952095f08 100644 --- a/src/EFCore.Relational/Extensions/RelationalOwnedNavigationBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalOwnedNavigationBuilderExtensions.cs @@ -58,7 +58,7 @@ public static OwnedNavigationBuilder ToJson builder.ToJson(jsonColumnName, null); + => (OwnedNavigationBuilder)((OwnedNavigationBuilder)builder).ToJson(jsonColumnName); /// /// Configures a relationship where this entity type and the entities that it owns are mapped to a JSON column in the database. @@ -74,47 +74,42 @@ public static OwnedNavigationBuilder ToJson builder.ToJson(jsonColumnName, null); + { + builder.OwnedEntityType.SetContainerColumnName(jsonColumnName); + + return builder; + } /// - /// Configures a relationship where this entity type and the entities that it owns are mapped to a JSON column in the database. + /// Set the relational database column type to be used to store the document represented by this owned entity. /// /// - /// This method should only be specified for the outer-most owned entity in the given ownership structure. - /// All entities owned by this will be automatically mapped to the same JSON column. - /// The ownerships must still be explicitly defined. + /// This method should only be specified for the outer-most owned entity in the given ownership structure and + /// only when mapping the column to a database document type. /// /// The builder for the owned navigation being configured. - /// JSON column name to use. - /// The database type for the JSON column, or to use the database default. + /// The database type for the column, or to use the database default. /// The same builder instance so that multiple calls can be chained. - public static OwnedNavigationBuilder ToJson( + public static OwnedNavigationBuilder HasColumnType( this OwnedNavigationBuilder builder, - string? jsonColumnName, - string? jsonColumnType) + string? columnType) where TOwnerEntity : class where TDependentEntity : class - => (OwnedNavigationBuilder)((OwnedNavigationBuilder)builder).ToJson(jsonColumnName, jsonColumnType); + => (OwnedNavigationBuilder)((OwnedNavigationBuilder)builder).HasColumnType(columnType); /// - /// Configures a relationship where this entity type and the entities that it owns are mapped to a JSON column in the database. + /// Set the relational database column type to be used to store the document represented by this owned entity. /// /// - /// This method should only be specified for the outer-most owned entity in the given ownership structure. - /// All entities owned by this will be automatically mapped to the same JSON column. - /// The ownerships must still be explicitly defined. + /// This method should only be specified for the outer-most owned entity in the given ownership structure and + /// only when mapping the column to a database document type. /// /// The builder for the owned navigation being configured. - /// JSON column name to use. - /// The database type for the JSON column, or to use the database default. + /// The database type for the column, or to use the database default. /// The same builder instance so that multiple calls can be chained. - public static OwnedNavigationBuilder ToJson( - this OwnedNavigationBuilder builder, - string? jsonColumnName, - string? jsonColumnType) + public static OwnedNavigationBuilder HasColumnType(this OwnedNavigationBuilder builder, string? columnType) { - builder.OwnedEntityType.SetContainerColumnName(jsonColumnName); - builder.OwnedEntityType.SetContainerColumnType(jsonColumnType); + builder.OwnedEntityType.SetContainerColumnType(columnType); return builder; } diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index 4c5f8b5db56..cbc0cf877a4 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -2592,6 +2592,23 @@ protected virtual void ValidateJsonEntities( IModel model, IDiagnosticsLogger logger) { + foreach (var entityType in model.GetEntityTypes()) + { + if (entityType[RelationalAnnotationNames.ContainerColumnType] != null) + { + if (entityType.FindOwnership()?.PrincipalEntityType.IsOwned() == true) + { + throw new InvalidOperationException(RelationalStrings.ContainerTypeOnNonRoot(entityType.DisplayName())); + } + + if (!entityType.IsOwned() + || entityType.GetContainerColumnName() == null) + { + throw new InvalidOperationException(RelationalStrings.ContainerTypeOnNonContainer(entityType.DisplayName())); + } + } + } + var tables = BuildSharedTableEntityMap(model.GetEntityTypes()); foreach (var (table, mappedTypes) in tables) { diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs index e0337d77f04..5910fd347d5 100644 --- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs +++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs @@ -413,6 +413,7 @@ public static class RelationalAnnotationNames ModelDependencies, FieldValueGetter, ContainerColumnName, + ContainerColumnType, #pragma warning disable CS0618 // Type or member is obsolete ContainerColumnTypeMapping, #pragma warning restore CS0618 // Type or member is obsolete diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 3b7b9c4c49e..0fcc8691b24 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -195,6 +195,22 @@ public static string ConflictingTypeMappingsInferredForColumn(object? column) GetString("ConflictingTypeMappingsInferredForColumn", nameof(column)), column); + /// + /// The entity type '{entityType}' has a container column type configured, but is nested in another owned type. The container column type can only be specified on a top-level owned type mapped to a container. + /// + public static string ContainerTypeOnNonRoot(object? entityType) + => string.Format( + GetString("ContainerTypeOnNonRoot", nameof(entityType)), + entityType); + + /// + /// The entity type '{entityType}' has a container column type configured, but is not mapped to a container column, such as for JSON. The container column type can only be specified on a top-level owned type mapped to a container. + /// + public static string ContainerTypeOnNonContainer(object? entityType) + => string.Format( + GetString("ContainerTypeOnNonContainer", nameof(entityType)), + entityType); + /// /// {numSortOrderProperties} values were provided in CreateIndexOperations.IsDescending, but the operation has {numColumns} columns. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 7099418d2e3..8ad50733a0d 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -187,6 +187,12 @@ Conflicting type mappings were inferred for column '{column}'. + + The entity type '{entityType}' has a container column type configured, but is nested in another owned type. The container column type can only be specified on a top-level owned type mapped to a container. + + + The entity type '{entityType}' has a container column type configured, but is not mapped to a container column, such as for JSON. The container column type can only be specified on a top-level owned type mapped to a container. + {numSortOrderProperties} values were provided in CreateIndexOperations.IsDescending, but the operation has {numColumns} columns. diff --git a/src/EFCore.SqlServer/Diagnostics/Internal/SqlServerLoggingDefinitions.cs b/src/EFCore.SqlServer/Diagnostics/Internal/SqlServerLoggingDefinitions.cs index 39aeb7ee552..a81a6df2ebd 100644 --- a/src/EFCore.SqlServer/Diagnostics/Internal/SqlServerLoggingDefinitions.cs +++ b/src/EFCore.SqlServer/Diagnostics/Internal/SqlServerLoggingDefinitions.cs @@ -194,4 +194,12 @@ public class SqlServerLoggingDefinitions : RelationalLoggingDefinitions /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public EventDefinitionBase? LogMissingViewDefinitionRights; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public EventDefinitionBase? LogJsonTypeExperimental; } diff --git a/src/EFCore.SqlServer/Diagnostics/SqlServerEventId.cs b/src/EFCore.SqlServer/Diagnostics/SqlServerEventId.cs index 2fd40c196ef..5940679dcf3 100644 --- a/src/EFCore.SqlServer/Diagnostics/SqlServerEventId.cs +++ b/src/EFCore.SqlServer/Diagnostics/SqlServerEventId.cs @@ -25,14 +25,14 @@ public static class SqlServerEventId // Try to use naming and be consistent with existing names. private enum Id { - // Model validation events + // All events + // Don't insert or delete anything in the middle of this section! DecimalTypeDefaultWarning = CoreEventId.ProviderBaseId, ByteIdentityColumnWarning, ConflictingValueGenerationStrategiesWarning, DecimalTypeKeyWarning, - - // Transaction events SavepointsDisabledBecauseOfMARS, + JsonTypeExperimental, // Scaffolding events ColumnFound = CoreEventId.ProviderDesignBaseId, @@ -115,6 +115,20 @@ private static EventId MakeValidationId(Id id) /// public static readonly EventId ByteIdentityColumnWarning = MakeValidationId(Id.ByteIdentityColumnWarning); + /// + /// An entity type makes use of the SQL Server native 'json' type. Please note that support for this type in EF Core 9 is + /// experimental and may change in future releases. + /// + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId JsonTypeExperimental = MakeValidationId(Id.JsonTypeExperimental); + /// /// There are conflicting value generation methods for a property. /// diff --git a/src/EFCore.SqlServer/Extensions/Internal/SqlServerLoggerExtensions.cs b/src/EFCore.SqlServer/Extensions/Internal/SqlServerLoggerExtensions.cs index 64642e4a680..1be0de40b3a 100644 --- a/src/EFCore.SqlServer/Extensions/Internal/SqlServerLoggerExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/Internal/SqlServerLoggerExtensions.cs @@ -124,6 +124,32 @@ private static string ByteIdentityColumnWarning(EventDefinitionBase definition, p.Property.DeclaringType.DisplayName()); } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static void JsonTypeExperimental( + this IDiagnosticsLogger diagnostics, + IEntityType entityType) + { + var definition = SqlServerResources.LogJsonTypeExperimental(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, entityType.DisplayName()); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new EntityTypeEventData(definition, (d, p) + => ((EventDefinition)d).GenerateMessage(((EntityTypeEventData)p).EntityType.DisplayName()), entityType); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs index 68cc2d344c5..b0dde8c6af6 100644 --- a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs +++ b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs @@ -45,6 +45,27 @@ public override void Validate(IModel model, IDiagnosticsLogger + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected virtual void ValidateUseOfJsonType( + IModel model, + IDiagnosticsLogger logger) + { + foreach (var entityType in model.GetEntityTypes()) + { + if (string.Equals(entityType.GetContainerColumnType(), "json", StringComparison.OrdinalIgnoreCase) + || entityType.GetProperties().Any(p => string.Equals(p.GetColumnType(), "json", StringComparison.OrdinalIgnoreCase))) + { + logger.JsonTypeExperimental(entityType); + } + } } /// diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs index a2394614e7c..00b27a4de1a 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs @@ -818,6 +818,31 @@ public static EventDefinition LogFoundUniqueConstraint(IDiagnost return (EventDefinition)definition; } + /// + /// The entity type '{entityType}' makes use of the SQL Server native 'json' type. Please note that support for this type in EF Core 9 is experimental and may change in future releases. + /// + public static EventDefinition LogJsonTypeExperimental(IDiagnosticsLogger logger) + { + var definition = ((Diagnostics.Internal.SqlServerLoggingDefinitions)logger.Definitions).LogJsonTypeExperimental; + if (definition == null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((Diagnostics.Internal.SqlServerLoggingDefinitions)logger.Definitions).LogJsonTypeExperimental, + logger, + static logger => new EventDefinition( + logger.Options, + SqlServerEventId.JsonTypeExperimental, + LogLevel.Warning, + "SqlServerEventId.JsonTypeExperimental", + level => LoggerMessage.Define( + level, + SqlServerEventId.JsonTypeExperimental, + _resourceManager.GetString("LogJsonTypeExperimental")!))); + } + + return (EventDefinition)definition; + } + /// /// Unable to find a schema in the database matching the selected schema '{schema}'. /// diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx index 5b734aebd29..2f8726af548 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx @@ -1,17 +1,17 @@  - @@ -264,6 +264,10 @@ Found unique constraint on table '{tableName}' with name '{uniqueConstraintName}'. Debug SqlServerEventId.UniqueConstraintFound string string + + The entity type '{entityType}' makes use of the SQL Server native 'json' type. Please note that support for this type in EF Core 9 is experimental and may change in future releases. + Warning SqlServerEventId.JsonTypeExperimental string + Unable to find a schema in the database matching the selected schema '{schema}'. Warning SqlServerEventId.MissingSchemaWarning string? diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerJsonPostprocessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerJsonPostprocessor.cs index 9d301e6deb3..1d6a007f43a 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerJsonPostprocessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerJsonPostprocessor.cs @@ -49,6 +49,29 @@ public Expression Process(Expression expression) return Visit(expression); } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitExtension(Expression expression) + => expression switch + { + SqlServerOpenJsonExpression openJsonExpression + => openJsonExpression is { JsonExpression.TypeMapping: SqlServerStringTypeMapping { StoreType: "json" } } or + { JsonExpression.TypeMapping: SqlServerJsonElementTypeMapping { StoreType: "json" } } + ? openJsonExpression.Update( + new SqlUnaryExpression( + ExpressionType.Convert, + (SqlExpression)Visit(openJsonExpression.JsonExpression), + typeof(string), + typeMappingSource.FindMapping(typeof(string))!)) + : openJsonExpression, + + _ => base.VisitExtension(expression) + }; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerTypeMappingPostprocessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerTypeMappingPostprocessor.cs index 3cfad0135ab..1aa6607bd87 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerTypeMappingPostprocessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerTypeMappingPostprocessor.cs @@ -43,7 +43,8 @@ protected override Expression VisitExtension(Expression expression) => expression switch { SqlServerOpenJsonExpression openJsonExpression - => ApplyTypeMappingsOnOpenJsonExpression(openJsonExpression), + when TryGetInferredTypeMapping(openJsonExpression.Alias, "value", out var typeMapping) + => ApplyTypeMappingsOnOpenJsonExpression(openJsonExpression, new[] { typeMapping }), _ => base.VisitExtension(expression) }; @@ -54,21 +55,12 @@ SqlServerOpenJsonExpression openJsonExpression /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected virtual SqlServerOpenJsonExpression ApplyTypeMappingsOnOpenJsonExpression(SqlServerOpenJsonExpression openJsonExpression) + protected virtual SqlServerOpenJsonExpression ApplyTypeMappingsOnOpenJsonExpression( + SqlServerOpenJsonExpression openJsonExpression, + IReadOnlyList typeMappings) { - if (openJsonExpression is { JsonExpression.TypeMapping: SqlServerStringTypeMapping { StoreType: "json" } } or - { JsonExpression.TypeMapping: SqlServerJsonElementTypeMapping { StoreType: "json" } }) - { - openJsonExpression = openJsonExpression.Update( - new SqlUnaryExpression( - ExpressionType.Convert, (SqlExpression)Visit(openJsonExpression.JsonExpression), typeof(string), - _typeMappingSource.FindMapping(typeof(string))!)); - } - - if (!TryGetInferredTypeMapping(openJsonExpression.Alias, "value", out var elementTypeMapping)) - { - return openJsonExpression; - } + Check.DebugAssert(typeMappings.Count == 1, "typeMappings.Count == 1"); + var elementTypeMapping = typeMappings[0]; // Constant queryables are translated to VALUES, no need for JSON. // Column queryables have their type mapping from the model, so we don't ever need to apply an inferred mapping on them. diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerJsonElementTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerJsonElementTypeMapping.cs index 461783c45f8..b855dd253c5 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerJsonElementTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerJsonElementTypeMapping.cs @@ -124,7 +124,7 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p /// protected override void ConfigureParameter(DbParameter parameter) { - if ("json".Equals(StoreType, StringComparison.OrdinalIgnoreCase) + if (StoreType == "json" && parameter is SqlParameter sqlParameter) // To avoid crashing wrapping providers { sqlParameter.SqlDbType = ((SqlDbType)35); diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs index a0de9806e61..771f6c844d4 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs @@ -241,7 +241,7 @@ public SqlServerTypeMappingSource( if (clrType == typeof(JsonElement)) { - return "json".Equals(storeTypeName, StringComparison.OrdinalIgnoreCase) + return storeTypeName == "json" ? SqlServerJsonElementTypeMapping.JsonTypeDefault : SqlServerJsonElementTypeMapping.Default; } diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs index 70997837f36..c044391de2c 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs @@ -95,6 +95,7 @@ public void Test_new_annotations_handled_for_entity_types() RelationalAnnotationNames.JsonPropertyName, // Appears on entity type but requires specific model (i.e. owned types that can map to json, otherwise validation throws) RelationalAnnotationNames.ContainerColumnName, + RelationalAnnotationNames.ContainerColumnType, #pragma warning disable CS0618 RelationalAnnotationNames.ContainerColumnTypeMapping, #pragma warning restore CS0618 @@ -262,6 +263,7 @@ public void Test_new_annotations_handled_for_properties() RelationalAnnotationNames.ModelDependencies, RelationalAnnotationNames.FieldValueGetter, RelationalAnnotationNames.ContainerColumnName, + RelationalAnnotationNames.ContainerColumnType, #pragma warning disable CS0618 RelationalAnnotationNames.ContainerColumnTypeMapping, #pragma warning restore CS0618 diff --git a/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryTestBase.cs index 758b378007a..888fe994cbb 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryTestBase.cs @@ -10,6 +10,10 @@ public abstract class AdHocJsonQueryTestBase : NonSharedModelTestBase protected override string StoreName => "AdHocJsonQueryTest"; + protected virtual void ConfigureWarnings(WarningsConfigurationBuilder builder) + { + } + #region 32310 [ConditionalTheory] @@ -17,7 +21,8 @@ protected override string StoreName public virtual async Task Contains_on_nested_collection_with_init_only_navigation(bool async) { var contextFactory = await InitializeAsync( - onModelCreating: b => b.Entity().OwnsOne(e => e.Visits).ToJson("Visits", JsonColumnType), + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: b => b.Entity().OwnsOne(e => e.Visits).ToJson().HasColumnType(JsonColumnType), seed: Seed32310); await using var context = contextFactory.CreateContext(); @@ -62,7 +67,10 @@ public class Visits32310 [MemberData(nameof(IsAsyncData))] public virtual async Task Optional_json_properties_materialized_as_null_when_the_element_in_json_is_not_present(bool async) { - var contextFactory = await InitializeAsync(BuildModel29219, seed: Seed29219); + var contextFactory = await InitializeAsync( + onModelCreating: BuildModel29219, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: Seed29219); using (var context = contextFactory.CreateContext()) { @@ -82,7 +90,10 @@ public virtual async Task Optional_json_properties_materialized_as_null_when_the [MemberData(nameof(IsAsyncData))] public virtual async Task Can_project_nullable_json_property_when_the_element_in_json_is_not_present(bool async) { - var contextFactory = await InitializeAsync(BuildModel29219, seed: Seed29219); + var contextFactory = await InitializeAsync( + onModelCreating: BuildModel29219, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: Seed29219); using (var context = contextFactory.CreateContext()) { @@ -105,8 +116,8 @@ protected void BuildModel29219(ModelBuilder modelBuilder) { b.ToTable("Entities"); b.Property(x => x.Id).ValueGeneratedNever(); - b.OwnsOne(x => x.Reference).ToJson("Reference", JsonColumnType); - b.OwnsMany(x => x.Collection).ToJson("Collection", JsonColumnType); + b.OwnsOne(x => x.Reference).ToJson().HasColumnType(JsonColumnType); + b.OwnsMany(x => x.Collection).ToJson().HasColumnType(JsonColumnType); }); protected abstract Task Seed29219(DbContext ctx); @@ -139,7 +150,7 @@ protected virtual void BuildModel30028(ModelBuilder modelBuilder) b.OwnsOne( x => x.Json, nb => { - nb.ToJson("Json", JsonColumnType); + nb.ToJson().HasColumnType(JsonColumnType); nb.OwnsMany(x => x.Collection, nnb => nnb.OwnsOne(x => x.Nested)); nb.OwnsOne(x => x.OptionalReference, nnb => nnb.OwnsOne(x => x.Nested)); nb.OwnsOne(x => x.RequiredReference, nnb => nnb.OwnsOne(x => x.Nested)); @@ -176,7 +187,11 @@ public class MyJsonLeafEntity30028 [MemberData(nameof(IsAsyncData))] public virtual async Task Accessing_missing_navigation_works(bool async) { - var contextFactory = await InitializeAsync(BuildModel30028, seed: Seed30028); + var contextFactory = await InitializeAsync( + onModelCreating: BuildModel30028, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: Seed30028); + using (var context = contextFactory.CreateContext()) { var result = context.Set().OrderBy(x => x.Id).ToList(); @@ -203,7 +218,11 @@ public virtual async Task Accessing_missing_navigation_works(bool async) [MemberData(nameof(IsAsyncData))] public virtual async Task Missing_navigation_works_with_deduplication(bool async) { - var contextFactory = await InitializeAsync(BuildModel30028, seed: Seed30028); + var contextFactory = await InitializeAsync( + onModelCreating: BuildModel30028, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: Seed30028); + using (var context = contextFactory.CreateContext()) { var result = context.Set().OrderBy(x => x.Id).Select( @@ -252,7 +271,11 @@ public virtual async Task Missing_navigation_works_with_deduplication(bool async [ConditionalFact] public virtual async Task Project_json_with_no_properties() { - var contextFactory = await InitializeAsync(BuildModel32939, seed: Seed32939); + var contextFactory = await InitializeAsync( + onModelCreating: BuildModel32939, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: Seed32939); + using var context = contextFactory.CreateContext(); context.Set().ToList(); } @@ -272,8 +295,8 @@ protected Task Seed32939(DbContext ctx) protected virtual void BuildModel32939(ModelBuilder modelBuilder) { modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); - modelBuilder.Entity().OwnsOne(x => x.Empty, b => b.ToJson("Empty", JsonColumnType)); - modelBuilder.Entity().OwnsOne(x => x.FieldOnly, b => b.ToJson("FieldOnly", JsonColumnType)); + modelBuilder.Entity().OwnsOne(x => x.Empty, b => b.ToJson().HasColumnType(JsonColumnType)); + modelBuilder.Entity().OwnsOne(x => x.FieldOnly, b => b.ToJson().HasColumnType(JsonColumnType)); } public class Entity32939 @@ -302,7 +325,11 @@ public class JsonFieldOnly32939 [ConditionalFact] public virtual async Task Query_with_nested_json_collection_mapped_to_private_field_via_IReadOnlyList() { - var contextFactory = await InitializeAsync(BuildModel33046, seed: Seed33046); + var contextFactory = await InitializeAsync( + onModelCreating: BuildModel33046, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: Seed33046); + using var context = contextFactory.CreateContext(); var query = context.Set().ToList(); Assert.Equal(1, query.Count); @@ -317,7 +344,7 @@ protected virtual void BuildModel33046(ModelBuilder modelBuilder) b.OwnsMany( x => x.Rounds, ownedBuilder => { - ownedBuilder.ToJson("Rounds", JsonColumnType); + ownedBuilder.ToJson().HasColumnType(JsonColumnType); ownedBuilder.OwnsMany(r => r.SubRounds); }); }); @@ -357,7 +384,10 @@ public class SubRound [MemberData(nameof(IsAsyncData))] public virtual async Task Project_json_array_of_primitives_on_reference(bool async) { - var contextFactory = await InitializeAsync(BuildModelArrayOfPrimitives, seed: SeedArrayOfPrimitives); + var contextFactory = await InitializeAsync( + onModelCreating: BuildModelArrayOfPrimitives, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedArrayOfPrimitives); using (var context = contextFactory.CreateContext()) { @@ -379,7 +409,10 @@ public virtual async Task Project_json_array_of_primitives_on_reference(bool asy [MemberData(nameof(IsAsyncData))] public virtual async Task Project_json_array_of_primitives_on_collection(bool async) { - var contextFactory = await InitializeAsync(BuildModelArrayOfPrimitives, seed: SeedArrayOfPrimitives); + var contextFactory = await InitializeAsync( + onModelCreating: BuildModelArrayOfPrimitives, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedArrayOfPrimitives); using (var context = contextFactory.CreateContext()) { @@ -401,7 +434,10 @@ public virtual async Task Project_json_array_of_primitives_on_collection(bool as [MemberData(nameof(IsAsyncData))] public virtual async Task Project_element_of_json_array_of_primitives(bool async) { - var contextFactory = await InitializeAsync(BuildModelArrayOfPrimitives, seed: SeedArrayOfPrimitives); + var contextFactory = await InitializeAsync( + onModelCreating: BuildModelArrayOfPrimitives, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedArrayOfPrimitives); using (var context = contextFactory.CreateContext()) { @@ -418,7 +454,10 @@ public virtual async Task Project_element_of_json_array_of_primitives(bool async [MemberData(nameof(IsAsyncData))] public virtual async Task Predicate_based_on_element_of_json_array_of_primitives1(bool async) { - var contextFactory = await InitializeAsync(BuildModelArrayOfPrimitives, seed: SeedArrayOfPrimitives); + var contextFactory = await InitializeAsync( + onModelCreating: BuildModelArrayOfPrimitives, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedArrayOfPrimitives); using (var context = contextFactory.CreateContext()) { @@ -437,7 +476,10 @@ public virtual async Task Predicate_based_on_element_of_json_array_of_primitives [MemberData(nameof(IsAsyncData))] public virtual async Task Predicate_based_on_element_of_json_array_of_primitives2(bool async) { - var contextFactory = await InitializeAsync(BuildModelArrayOfPrimitives, seed: SeedArrayOfPrimitives); + var contextFactory = await InitializeAsync( + onModelCreating: BuildModelArrayOfPrimitives, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedArrayOfPrimitives); using (var context = contextFactory.CreateContext()) { @@ -456,7 +498,10 @@ public virtual async Task Predicate_based_on_element_of_json_array_of_primitives [MemberData(nameof(IsAsyncData))] public virtual async Task Predicate_based_on_element_of_json_array_of_primitives3(bool async) { - var contextFactory = await InitializeAsync(BuildModelArrayOfPrimitives, seed: SeedArrayOfPrimitives); + var contextFactory = await InitializeAsync( + onModelCreating: BuildModelArrayOfPrimitives, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedArrayOfPrimitives); using (var context = contextFactory.CreateContext()) { @@ -481,10 +526,10 @@ protected virtual void BuildModelArrayOfPrimitives(ModelBuilder modelBuilder) { modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); modelBuilder.Entity().OwnsOne( - x => x.Reference, b => b.ToJson("Reference", JsonColumnType)); + x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); modelBuilder.Entity().OwnsMany( - x => x.Collection, b => b.ToJson("Collection", JsonColumnType)); + x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); } public class MyEntityArrayOfPrimitives @@ -508,7 +553,10 @@ public class MyJsonEntityArrayOfPrimitives [MemberData(nameof(IsAsyncData))] public virtual async Task Junk_in_json_basic_tracking(bool async) { - var contextFactory = await InitializeAsync(BuildModelJunkInJson, seed: SeedJunkInJson); + var contextFactory = await InitializeAsync( + onModelCreating: BuildModelJunkInJson, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedJunkInJson); using (var context = contextFactory.CreateContext()) { @@ -532,7 +580,10 @@ public virtual async Task Junk_in_json_basic_tracking(bool async) [MemberData(nameof(IsAsyncData))] public virtual async Task Junk_in_json_basic_no_tracking(bool async) { - var contextFactory = await InitializeAsync(BuildModelJunkInJson, seed: SeedJunkInJson); + var contextFactory = await InitializeAsync( + onModelCreating: BuildModelJunkInJson, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedJunkInJson); using (var context = contextFactory.CreateContext()) { @@ -564,7 +615,7 @@ protected virtual void BuildModelJunkInJson(ModelBuilder modelBuilder) b.OwnsOne( x => x.Reference, b => { - b.ToJson("Reference", JsonColumnType); + b.ToJson().HasColumnType(JsonColumnType); b.OwnsOne(x => x.NestedReference); b.OwnsMany(x => x.NestedCollection); }); @@ -572,7 +623,7 @@ protected virtual void BuildModelJunkInJson(ModelBuilder modelBuilder) b.OwnsOne( x => x.ReferenceWithCtor, b => { - b.ToJson("ReferenceWithCtor", JsonColumnType); + b.ToJson().HasColumnType(JsonColumnType); b.OwnsOne(x => x.NestedReference); b.OwnsMany(x => x.NestedCollection); }); @@ -580,7 +631,7 @@ protected virtual void BuildModelJunkInJson(ModelBuilder modelBuilder) b.OwnsMany( x => x.Collection, b => { - b.ToJson("Collection", JsonColumnType); + b.ToJson().HasColumnType(JsonColumnType); b.OwnsOne(x => x.NestedReference); b.OwnsMany(x => x.NestedCollection); }); @@ -588,7 +639,7 @@ protected virtual void BuildModelJunkInJson(ModelBuilder modelBuilder) b.OwnsMany( x => x.CollectionWithCtor, b => { - b.ToJson("CollectionWithCtor", JsonColumnType); + b.ToJson().HasColumnType(JsonColumnType); b.OwnsOne(x => x.NestedReference); b.OwnsMany(x => x.NestedCollection); }); @@ -639,7 +690,10 @@ public class MyJsonEntityJunkInJsonWithCtorNested(DateTime doB) [MemberData(nameof(IsAsyncData))] public virtual async Task Tricky_buffering_basic(bool async) { - var contextFactory = await InitializeAsync(BuildModelTrickyBuffering, seed: SeedTrickyBuffering); + var contextFactory = await InitializeAsync( + onModelCreating: BuildModelTrickyBuffering, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedTrickyBuffering); using (var context = contextFactory.CreateContext()) { @@ -668,7 +722,7 @@ protected virtual void BuildModelTrickyBuffering(ModelBuilder modelBuilder) b.OwnsOne( x => x.Reference, b => { - b.ToJson("Reference", JsonColumnType); + b.ToJson().HasColumnType(JsonColumnType); b.OwnsOne(x => x.NestedReference); b.OwnsMany(x => x.NestedCollection); }); @@ -701,7 +755,10 @@ public class MyJsonEntityTrickyBufferingNested [MemberData(nameof(IsAsyncData))] public virtual async Task Shadow_properties_basic_tracking(bool async) { - var contextFactory = await InitializeAsync(BuildModelShadowProperties, seed: SeedShadowProperties); + var contextFactory = await InitializeAsync( + onModelCreating: BuildModelShadowProperties, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedShadowProperties); using (var context = contextFactory.CreateContext()) { @@ -739,7 +796,10 @@ public virtual async Task Shadow_properties_basic_tracking(bool async) [MemberData(nameof(IsAsyncData))] public virtual async Task Shadow_properties_basic_no_tracking(bool async) { - var contextFactory = await InitializeAsync(BuildModelShadowProperties, seed: SeedShadowProperties); + var contextFactory = await InitializeAsync( + onModelCreating: BuildModelShadowProperties, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedShadowProperties); using (var context = contextFactory.CreateContext()) { @@ -761,7 +821,10 @@ public virtual async Task Shadow_properties_basic_no_tracking(bool async) [MemberData(nameof(IsAsyncData))] public virtual async Task Project_shadow_properties_from_json_entity(bool async) { - var contextFactory = await InitializeAsync(BuildModelShadowProperties, seed: SeedShadowProperties); + var contextFactory = await InitializeAsync( + onModelCreating: BuildModelShadowProperties, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedShadowProperties); using (var context = contextFactory.CreateContext()) { @@ -795,28 +858,28 @@ protected virtual void BuildModelShadowProperties(ModelBuilder modelBuilder) b.OwnsOne( x => x.Reference, b => { - b.ToJson("Reference", JsonColumnType); + b.ToJson().HasColumnType(JsonColumnType); b.Property("ShadowString"); }); b.OwnsOne( x => x.ReferenceWithCtor, b => { - b.ToJson("ReferenceWithCtor", JsonColumnType); + b.ToJson().HasColumnType(JsonColumnType); b.Property("Shadow_Int").HasJsonPropertyName("ShadowInt"); }); b.OwnsMany( x => x.Collection, b => { - b.ToJson("Collection", JsonColumnType); + b.ToJson().HasColumnType(JsonColumnType); b.Property("ShadowDouble"); }); b.OwnsMany( x => x.CollectionWithCtor, b => { - b.ToJson("CollectionWithCtor", JsonColumnType); + b.ToJson().HasColumnType(JsonColumnType); b.Property("ShadowNullableByte"); }); }); @@ -854,7 +917,11 @@ public virtual async Task Project_proxies_entity_with_json(bool async) var contextFactory = await InitializeAsync( onModelCreating: BuildModelLazyLoadingProxies, seed: SeedLazyLoadingProxies, - onConfiguring: OnConfiguringLazyLoadingProxies, + onConfiguring: b => + { + b = b.ConfigureWarnings(ConfigureWarnings); + OnConfiguringLazyLoadingProxies(b); + }, addServices: AddServicesLazyLoadingProxies); using (var context = contextFactory.CreateContext()) @@ -914,8 +981,8 @@ private Task SeedLazyLoadingProxies(DbContext ctx) protected virtual void BuildModelLazyLoadingProxies(ModelBuilder modelBuilder) { modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); - modelBuilder.Entity().OwnsOne(x => x.Reference, b => b.ToJson("Reference", JsonColumnType)); - modelBuilder.Entity().OwnsMany(x => x.Collection, b => b.ToJson("Collection", JsonColumnType)); + modelBuilder.Entity().OwnsOne(x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); + modelBuilder.Entity().OwnsMany(x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); } public class MyEntityLazyLoadingProxies @@ -947,7 +1014,10 @@ public class MyJsonEntityLazyLoadingProxies [MemberData(nameof(IsAsyncData))] public virtual async Task Not_ICollection_basic_projection(bool async) { - var contextFactory = await InitializeAsync(BuildModelNotICollection, seed: SeedNotICollection); + var contextFactory = await InitializeAsync( + onModelCreating: BuildModelNotICollection, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedNotICollection); using (var context = contextFactory.CreateContext()) { @@ -993,7 +1063,7 @@ protected virtual void BuildModelNotICollection(ModelBuilder modelBuilder) b.OwnsOne( cr => cr.Json, nb => { - nb.ToJson("Json", JsonColumnType); + nb.ToJson().HasColumnType(JsonColumnType); nb.OwnsMany(x => x.Collection); }); }); diff --git a/test/EFCore.Relational.Specification.Tests/TestUtilities/RelationalModelAsserter.cs b/test/EFCore.Relational.Specification.Tests/TestUtilities/RelationalModelAsserter.cs index e76efee1a10..d096745b403 100644 --- a/test/EFCore.Relational.Specification.Tests/TestUtilities/RelationalModelAsserter.cs +++ b/test/EFCore.Relational.Specification.Tests/TestUtilities/RelationalModelAsserter.cs @@ -205,6 +205,7 @@ public override bool AssertEqual( Assert.Multiple( () => Assert.Equal(expected.GetDbSetName(), actual.GetDbSetName()), () => Assert.Equal(expected.GetContainerColumnName(), actual.GetContainerColumnName()), + () => Assert.Equal(expected.GetContainerColumnType(), actual.GetContainerColumnType()), () => Assert.Equal(expected.GetJsonPropertyName(), actual.GetJsonPropertyName()), () => { @@ -543,6 +544,7 @@ public override bool AssertEqual( Assert.Multiple( () => Assert.Equal(expected.GetContainerColumnName(), actual.GetContainerColumnName()), + () => Assert.Equal(expected.GetContainerColumnType(), actual.GetContainerColumnType()), () => Assert.Equal(expectedStructuralType.GetJsonPropertyName(), actualStructuralType.GetJsonPropertyName()), () => Assert.Equal(expectedStructuralType.GetTableName(), actualStructuralType.GetTableName()), () => Assert.Equal(expectedStructuralType.GetViewName(), actualStructuralType.GetViewName()), diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.Json.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.Json.cs index 203b04f5174..f713efd6f47 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.Json.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.Json.cs @@ -5,6 +5,50 @@ namespace Microsoft.EntityFrameworkCore.Infrastructure; public partial class RelationalModelValidatorTest { + [ConditionalFact] + public void Throw_when_non_json_entity_has_column_type_set() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity( + b => + { + b.OwnsOne( + x => x.OwnedReference, bb => + { + bb.HasColumnType("nvarchar(2000)"); + bb.Ignore(x => x.NestedCollection); + bb.Ignore(x => x.NestedReference); + }); + b.Ignore(x => x.OwnedCollection); + }); + + VerifyError( + RelationalStrings.ContainerTypeOnNonContainer(nameof(ValidatorJsonOwnedRoot)), + modelBuilder); + } + + [ConditionalFact] + public void Throw_when_non_root_json_entity_has_column_type_set() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity( + b => + { + b.OwnsOne( + x => x.OwnedReference, bb => + { + bb.ToJson(); + bb.Ignore(x => x.NestedCollection); + bb.OwnsOne(x => x.NestedReference, bbb => bbb.HasColumnType("nvarchar(2000)")); + }); + b.Ignore(x => x.OwnedCollection); + }); + + VerifyError( + RelationalStrings.ContainerTypeOnNonRoot(nameof(ValidatorJsonOwnedBranch)), + modelBuilder); + } + [ConditionalFact] public void Throw_when_non_json_entity_is_the_owner_of_json_entity_ref_ref() { @@ -614,26 +658,26 @@ public void SeedData_on_entity_with_json_navigation_throws_meaningful_exception( modelBuilder); } - private class ValidatorJsonEntityBasic + protected class ValidatorJsonEntityBasic { public int Id { get; set; } public ValidatorJsonOwnedRoot OwnedReference { get; set; } public List OwnedCollection { get; set; } } - private abstract class ValidatorJsonEntityInheritanceAbstract : ValidatorJsonEntityInheritanceBase + protected abstract class ValidatorJsonEntityInheritanceAbstract : ValidatorJsonEntityInheritanceBase { public Guid Guid { get; set; } } - private class ValidatorJsonEntityInheritanceBase + protected class ValidatorJsonEntityInheritanceBase { public int Id { get; set; } public string Name { get; set; } public ValidatorJsonOwnedBranch ReferenceOnBase { get; set; } } - private class ValidatorJsonEntityInheritanceDerived : ValidatorJsonEntityInheritanceAbstract + protected class ValidatorJsonEntityInheritanceDerived : ValidatorJsonEntityInheritanceAbstract { public bool Switch { get; set; } @@ -642,7 +686,7 @@ private class ValidatorJsonEntityInheritanceDerived : ValidatorJsonEntityInherit public List CollectionOnDerived { get; set; } } - private class ValidatorJsonOwnedRoot + protected class ValidatorJsonOwnedRoot { public string Name { get; set; } public int Number { get; } @@ -651,12 +695,12 @@ private class ValidatorJsonOwnedRoot public List NestedCollection { get; } } - private class ValidatorJsonOwnedBranch + protected class ValidatorJsonOwnedBranch { public double Number { get; set; } } - private class ValidatorJsonEntityExplicitOrdinal + protected class ValidatorJsonEntityExplicitOrdinal { public int Id { get; set; } @@ -665,19 +709,19 @@ private class ValidatorJsonEntityExplicitOrdinal public List OwnedCollection { get; set; } } - private class ValidatorJsonOwnedExplicitOrdinal + protected class ValidatorJsonOwnedExplicitOrdinal { public int Ordinal { get; set; } public DateTime Date { get; set; } } - private class ValidatorJsonEntityJsonReferencingRegularEntity + protected class ValidatorJsonEntityJsonReferencingRegularEntity { public int Id { get; set; } public ValidatorJsonOwnedReferencingRegularEntity Owned { get; set; } } - private class ValidatorJsonOwnedReferencingRegularEntity + protected class ValidatorJsonOwnedReferencingRegularEntity { public string Foo { get; set; } @@ -685,13 +729,13 @@ private class ValidatorJsonOwnedReferencingRegularEntity public ValidatorJsonEntityReferencedEntity Reference { get; } } - private class ValidatorJsonEntityReferencedEntity + protected class ValidatorJsonEntityReferencedEntity { public int Id { get; set; } public DateTime Date { get; set; } } - private class ValidatorJsonEntitySideBySide + protected class ValidatorJsonEntitySideBySide { public int Id { get; set; } public string Name { get; set; } @@ -701,7 +745,7 @@ private class ValidatorJsonEntitySideBySide public List Collection2 { get; set; } } - private class ValidatorJsonEntityTableSplitting + protected class ValidatorJsonEntityTableSplitting { public int Id { get; set; } public ValidatorJsonEntityBasic Link { get; set; } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs index 651cd8b8b08..97c07f97af5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs @@ -13,6 +13,13 @@ public abstract class AdHocJsonQuerySqlServerTestBase : AdHocJsonQueryTestBase protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; + protected override void ConfigureWarnings(WarningsConfigurationBuilder builder) + { + base.ConfigureWarnings(builder); + + builder.Log(CoreEventId.StringEnumValueInJson, SqlServerEventId.JsonTypeExperimental); + } + protected override async Task Seed29219(DbContext ctx) { var entity1 = new MyEntity29219 @@ -196,7 +203,7 @@ public virtual async Task Read_enum_property_with_legacy_values(bool async) { var contextFactory = await InitializeAsync( onModelCreating: BuildModelEnumLegacyValues, - onConfiguring: b => b.ConfigureWarnings(b => b.Log(CoreEventId.StringEnumValueInJson)), + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), seed: SeedEnumLegacyValues); using (var context = contextFactory.CreateContext()) @@ -225,7 +232,7 @@ public virtual async Task Read_json_entity_with_enum_properties_with_legacy_valu { var contextFactory = await InitializeAsync( onModelCreating: BuildModelEnumLegacyValues, - onConfiguring: b => b.ConfigureWarnings(b => b.Log(CoreEventId.StringEnumValueInJson)), + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), seed: SeedEnumLegacyValues, shouldLogCategory: c => c == DbLoggerCategory.Query.Name); @@ -266,7 +273,7 @@ public virtual async Task Read_json_entity_collection_with_enum_properties_with_ { var contextFactory = await InitializeAsync( onModelCreating: BuildModelEnumLegacyValues, - onConfiguring: b => b.ConfigureWarnings(b => b.Log(CoreEventId.StringEnumValueInJson)), + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), seed: SeedEnumLegacyValues, shouldLogCategory: c => c == DbLoggerCategory.Query.Name); @@ -324,8 +331,8 @@ protected virtual void BuildModelEnumLegacyValues(ModelBuilder modelBuilder) { b.ToTable("Entities"); b.Property(x => x.Id).ValueGeneratedNever(); - b.OwnsOne(x => x.Reference, b => b.ToJson("Reference", JsonColumnType)); - b.OwnsMany(x => x.Collection, b => b.ToJson("Collection", JsonColumnType)); + b.OwnsOne(x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); + b.OwnsMany(x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); }); private class MyEntityEnumLegacyValues diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryJsonTypeSqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryJsonTypeSqlServerFixture.cs index 4a8f92bbe3c..e59e6565b88 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryJsonTypeSqlServerFixture.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryJsonTypeSqlServerFixture.cs @@ -18,39 +18,39 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con modelBuilder.Entity( b => { - b.OwnsOne(x => x.OwnedReferenceRoot).ToJson("OwnedReferenceRoot", "json"); - b.OwnsMany(x => x.OwnedCollectionRoot).ToJson("OwnedCollectionRoot", "json"); + b.OwnsOne(x => x.OwnedReferenceRoot).ToJson().HasColumnType("json"); + b.OwnsMany(x => x.OwnedCollectionRoot).ToJson().HasColumnType("json"); }); modelBuilder.Entity( b => { - b.OwnsOne(x => x.OwnedReferenceRoot).ToJson("json_reference_custom_naming", "json"); - b.OwnsMany(x => x.OwnedCollectionRoot).ToJson("json_collection_custom_naming", "json"); + b.OwnsOne(x => x.OwnedReferenceRoot).ToJson("json_reference_custom_naming").HasColumnType("json");; + b.OwnsMany(x => x.OwnedCollectionRoot).HasColumnType("json").ToJson("json_collection_custom_naming"); }); - modelBuilder.Entity().OwnsMany(x => x.OwnedCollection).ToJson("OwnedCollection", "json"); + modelBuilder.Entity().OwnsMany(x => x.OwnedCollection).ToJson().HasColumnType("json"); modelBuilder.Entity( b => { - b.OwnsOne(x => x.ReferenceOnBase).ToJson("ReferenceOnBase", "json"); - b.OwnsMany(x => x.CollectionOnBase).ToJson("CollectionOnBase", "json"); + b.OwnsOne(x => x.ReferenceOnBase).ToJson().HasColumnType("json"); + b.OwnsMany(x => x.CollectionOnBase).ToJson().HasColumnType("json"); }); modelBuilder.Entity( b => { b.HasBaseType(); - b.OwnsOne(x => x.ReferenceOnDerived).ToJson("ReferenceOnDerived", "json"); - b.OwnsMany(x => x.CollectionOnDerived).ToJson("CollectionOnDerived", "json"); + b.OwnsOne(x => x.ReferenceOnDerived).ToJson().HasColumnType("json"); + b.OwnsMany(x => x.CollectionOnDerived).ToJson().HasColumnType("json"); }); modelBuilder.Entity( b => { - b.OwnsOne(x => x.Reference).ToJson("Reference", "json"); - b.OwnsMany(x => x.Collection).ToJson("Collection", "json"); + b.OwnsOne(x => x.Reference).ToJson().HasColumnType("json"); + b.OwnsMany(x => x.Collection).ToJson().HasColumnType("json"); b.PrimitiveCollection(e => e.TestDefaultStringCollection).HasColumnType("json").IsRequired(); b.PrimitiveCollection(e => e.TestMaxLengthStringCollection).HasColumnType("json").IsRequired(); b.PrimitiveCollection(e => e.TestInt16Collection).HasColumnType("json").IsRequired(); @@ -78,6 +78,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b.PrimitiveCollection(e => e.TestNullableEnumWithConverterThatHandlesNullsCollection).HasColumnType("json"); }); - modelBuilder.Entity().OwnsOne(x => x.Reference).ToJson("Reference", "json"); + modelBuilder.Entity().OwnsOne(x => x.Reference).ToJson().HasColumnType("json"); } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerFixture.cs index 21d51690775..49e5c0660fc 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerFixture.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerFixture.cs @@ -12,6 +12,9 @@ public class JsonQuerySqlServerFixture : JsonQueryRelationalFixture protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder).ConfigureWarnings(e => e.Log(SqlServerEventId.JsonTypeExperimental)); + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { base.OnModelCreating(modelBuilder, context); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs index 55b4707c46a..ce821d8b95a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs @@ -17,6 +17,83 @@ public PrimitiveCollectionsQuerySqlServerJsonTypeTest(PrimitiveCollectionsQueryS Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + public override async Task Inline_collection_with_single_parameter_element_Contains(bool async) + { + await base.Inline_collection_with_single_parameter_element_Contains(async); + + AssertSql( + """ +@__i_0='2' + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Id] = @__i_0 +"""); + } + + public override async Task Inline_collection_with_single_parameter_element_Count(bool async) + { + await base.Inline_collection_with_single_parameter_element_Count(async); + + AssertSql( + """ +@__i_0='2' + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT COUNT(*) + FROM (VALUES (CAST(@__i_0 AS int))) AS [v]([Value]) + WHERE [v].[Value] > [p].[Id]) = 1 +"""); + } + + public override async Task Parameter_collection_Contains_with_EF_Constant(bool async) + { + await base.Parameter_collection_Contains_with_EF_Constant(async); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Id] IN (2, 999, 1000) +"""); + } + + public override async Task Parameter_collection_Where_with_EF_Constant_Where_Any(bool async) + { + await base.Parameter_collection_Where_with_EF_Constant_Where_Any(async); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE EXISTS ( + SELECT 1 + FROM (VALUES (2), (999), (1000)) AS [i]([Value]) + WHERE [i].[Value] > 0) +"""); + } + + public override async Task Parameter_collection_Count_with_column_predicate_with_EF_Constant(bool async) + { + await base.Parameter_collection_Count_with_column_predicate_with_EF_Constant(async); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT COUNT(*) + FROM (VALUES (2), (999), (1000)) AS [i]([Value]) + WHERE [i].[Value] > [p].[Id]) = 2 +"""); + } + public override async Task Inline_collection_of_ints_Contains(bool async) { await base.Inline_collection_of_ints_Contains(async); @@ -1966,10 +2043,6 @@ FROM OPENJSON(@__strings_1) WITH ([value] nvarchar(max) '$') AS [s] """); } - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); @@ -1988,7 +2061,9 @@ protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - => base.AddOptions(builder).UseSqlServer(b => b.UseCompatibilityLevel(160)); + => base.AddOptions(builder) + .UseSqlServer(b => b.UseCompatibilityLevel(160)) + .ConfigureWarnings(e => e.Log(SqlServerEventId.JsonTypeExperimental)); protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs index e3d56a4901f..20573fc9b3c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs @@ -2453,6 +2453,45 @@ nationalCharacterVaryingMaxColumn national char varying(max) NULL }, "DROP TABLE MaxColumns;"); + [ConditionalFact (Skip = "TODO:SQLJSON")] + public void Handles_native_JSON_type() + => Test( + @" +CREATE TABLE JsonColumns ( + Id int, + jsonTypeColumn json NULL +);", + Enumerable.Empty(), + Enumerable.Empty(), + (dbModel, scaffoldingFactory) => + { + var columns = dbModel.Tables.Single().Columns; + + Assert.Equal("json", columns.Single(c => c.Name == "jsonTypeColumn").StoreType); + + var model = scaffoldingFactory.Create(dbModel, new()); + + Assert.Collection( + model.GetEntityTypes(), + e => + { + Assert.Equal("JsonColumn", e.Name); + Assert.Collection( + e.GetProperties(), + p => Assert.Equal("Id", p.Name), + p => + { + Assert.Equal("JsonTypeColumn", p.Name); + Assert.Same(typeof(string), p.ClrType); + Assert.Null(p.GetMaxLength()); + }); + Assert.Empty(e.GetForeignKeys()); + Assert.Empty(e.GetSkipNavigations()); + Assert.Empty(e.GetNavigations()); + }); + }, + "DROP TABLE JsonColumns;"); + [ConditionalFact] public void Specific_max_length_are_add_to_store_type() => Test( diff --git a/test/EFCore.SqlServer.FunctionalTests/SqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/SqlServerFixture.cs index 57f63ac3a54..27015232920 100644 --- a/test/EFCore.SqlServer.FunctionalTests/SqlServerFixture.cs +++ b/test/EFCore.SqlServer.FunctionalTests/SqlServerFixture.cs @@ -21,6 +21,7 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build w => { w.Log(SqlServerEventId.ByteIdentityColumnWarning); + w.Log(SqlServerEventId.JsonTypeExperimental); w.Log(SqlServerEventId.DecimalTypeKeyWarning); }); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateJsonTypeSqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateJsonTypeSqlServerFixture.cs index 5f570de2d4d..840544363e4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateJsonTypeSqlServerFixture.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateJsonTypeSqlServerFixture.cs @@ -18,30 +18,30 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con modelBuilder.Entity( b => { - b.OwnsOne(x => x.OwnedReferenceRoot).ToJson("OwnedReferenceRoot", "json"); - b.OwnsMany(x => x.OwnedCollectionRoot).ToJson("OwnedCollectionRoot", "json"); + b.OwnsOne(x => x.OwnedReferenceRoot).ToJson().HasColumnType("json"); + b.OwnsMany(x => x.OwnedCollectionRoot).ToJson().HasColumnType("json"); }); modelBuilder.Entity( b => { - b.OwnsOne(x => x.ReferenceOnBase).ToJson("ReferenceOnBase", "json"); - b.OwnsMany(x => x.CollectionOnBase).ToJson("CollectionOnBase", "json"); + b.OwnsOne(x => x.ReferenceOnBase).ToJson().HasColumnType("json"); + b.OwnsMany(x => x.CollectionOnBase).ToJson().HasColumnType("json"); }); modelBuilder.Entity( b => { b.HasBaseType(); - b.OwnsOne(x => x.ReferenceOnDerived).ToJson("ReferenceOnDerived", "json"); - b.OwnsMany(x => x.CollectionOnDerived).ToJson("CollectionOnDerived", "json"); + b.OwnsOne(x => x.ReferenceOnDerived).ToJson().HasColumnType("json"); + b.OwnsMany(x => x.CollectionOnDerived).ToJson().HasColumnType("json"); }); modelBuilder.Entity( b => { - b.OwnsOne(x => x.Reference).ToJson("Reference", "json"); - b.OwnsMany(x => x.Collection).ToJson("Collection", "json"); + b.OwnsOne(x => x.Reference).ToJson().HasColumnType("json"); + b.OwnsMany(x => x.Collection).ToJson().HasColumnType("json"); b.PrimitiveCollection(e => e.TestDefaultStringCollection).HasColumnType("json"); b.PrimitiveCollection(e => e.TestMaxLengthStringCollection).HasColumnType("json"); b.PrimitiveCollection(e => e.TestInt16Collection).HasColumnType("json"); @@ -69,6 +69,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b.PrimitiveCollection(e => e.TestNullableEnumWithConverterThatHandlesNullsCollection).HasColumnType("json"); }); - modelBuilder.Entity().OwnsOne(x => x.Reference).ToJson("Reference", "json"); + modelBuilder.Entity().OwnsOne(x => x.Reference).ToJson().HasColumnType("json"); } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerFixture.cs index 719cbced154..fb12560e0c8 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerFixture.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerFixture.cs @@ -12,6 +12,9 @@ public class JsonUpdateSqlServerFixture : JsonUpdateFixtureBase protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder).ConfigureWarnings(e => e.Log(SqlServerEventId.JsonTypeExperimental)); + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { base.OnModelCreating(modelBuilder, context); diff --git a/test/EFCore.SqlServer.Tests/Diagnostics/SqlServerEventIdTest.cs b/test/EFCore.SqlServer.Tests/Diagnostics/SqlServerEventIdTest.cs index 58ecf35c6cd..cabca35ad48 100644 --- a/test/EFCore.SqlServer.Tests/Diagnostics/SqlServerEventIdTest.cs +++ b/test/EFCore.SqlServer.Tests/Diagnostics/SqlServerEventIdTest.cs @@ -22,6 +22,7 @@ public void Every_eventId_has_a_logger_method_and_logs_when_level_enabled() { { typeof(IList), () => new List { "Fake1", "Fake2" } }, { typeof(IProperty), () => property }, + { typeof(IEntityType), () => entityType }, { typeof(IReadOnlyProperty), () => property }, { typeof(string), () => "Fake" } }; diff --git a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs index f28a1271469..e7459ba5e8b 100644 --- a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs +++ b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs @@ -11,6 +11,43 @@ namespace Microsoft.EntityFrameworkCore.Infrastructure; public class SqlServerModelValidatorTest : RelationalModelValidatorTest { + [ConditionalFact] + public void Detects_use_of_json_column() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity( + b => + { + b.Property(e => e.Name).HasColumnType("json"); + }); + + VerifyWarning( + SqlServerResources.LogJsonTypeExperimental(new TestLogger()) + .GenerateMessage("Cheese"), modelBuilder); + } + + [ConditionalFact] + public void Detects_use_of_json_column_for_container() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity( + b => + { + b.OwnsOne( + x => x.OwnedReference, bb => + { + bb.ToJson().HasColumnType("json"); + bb.Ignore(x => x.NestedCollection); + bb.Ignore(x => x.NestedReference); + }); + b.Ignore(x => x.OwnedCollection); + }); + + VerifyWarning( + SqlServerResources.LogJsonTypeExperimental(new TestLogger()) + .GenerateMessage(nameof(ValidatorJsonOwnedRoot)), modelBuilder); + } + [ConditionalFact] // Issue #34324 public virtual void Throws_for_nested_primitive_collections() {