Skip to content

Commit

Permalink
Stop including the discriminator in the JSON id by default
Browse files Browse the repository at this point in the history
Fixes #34179

There are also new APIs on the entity type and model builders to facilitate optional behaviors, including reverting to the pre-9 behavior:

- Entity<Blog>().IncludeDiscriminatorInJsonId();
- Entity<Blog>().IncludeRootDiscriminatorInJsonId();
- Entity<Blog>().AlwaysCreateShadowIdProperty();

Note that this change requires regeneration of the Northwind database.
  • Loading branch information
roji authored and ajcvickers committed Jul 21, 2024
1 parent d41ba67 commit fff81d7
Show file tree
Hide file tree
Showing 66 changed files with 2,173 additions and 2,665 deletions.
319 changes: 319 additions & 0 deletions src/EFCore.Cosmos/Extensions/CosmosEntityTypeBuilderExtensions.cs

Large diffs are not rendered by default.

91 changes: 91 additions & 0 deletions src/EFCore.Cosmos/Extensions/CosmosEntityTypeExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.Cosmos.Metadata;
using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal;

// ReSharper disable once CheckNamespace
Expand Down Expand Up @@ -342,6 +343,96 @@ public static void SetETagPropertyName(this IMutableEntityType entityType, strin
public static IProperty? GetETagProperty(this IEntityType entityType)
=> (IProperty?)((IReadOnlyEntityType)entityType).GetETagProperty();

/// <summary>
/// Returns a value indicating whether model building will always create a "__id" shadow property mapped to the JSON "id".
/// This was the default behavior before EF Core 9.0.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <returns>
/// <see langword="true" /> to force __id creation, <see langword="false" /> to not force __id creation,
/// <see langword="null" /> to revert to the default setting.
/// .</returns>
public static bool? GetAlwaysCreateShadowIdProperty(this IReadOnlyEntityType entityType)
=> entityType.BaseType != null
? entityType.GetRootType().GetAlwaysCreateShadowIdProperty()
: (bool?)entityType[CosmosAnnotationNames.AlwaysCreateShadowIdProperty];

/// <summary>
/// Forces model building to always create a "__id" shadow property mapped to the JSON "id". This was the default
/// behavior before EF Core 9.0.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="alwaysCreate">
/// <see langword="true" /> to force __id creation, <see langword="false" /> to not force __id creation,
/// <see langword="null" /> to revert to the default setting.
/// </param>
public static void SetAlwaysCreateShadowIdProperty(this IMutableEntityType entityType, bool? alwaysCreate)
=> entityType.SetOrRemoveAnnotation(CosmosAnnotationNames.AlwaysCreateShadowIdProperty, alwaysCreate);

/// <summary>
/// Forces model building to always create a "__id" shadow property mapped to the JSON "id". This was the default
/// behavior before EF Core 9.0.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="alwaysCreate">
/// <see langword="true" /> to force __id creation, <see langword="false" /> to not force __id creation,
/// <see langword="null" /> to revert to the default setting.
/// </param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
public static bool? SetAlwaysCreateShadowIdProperty(
this IConventionEntityType entityType,
bool? alwaysCreate,
bool fromDataAnnotation = false)
=> (bool?)entityType.SetOrRemoveAnnotation(
CosmosAnnotationNames.AlwaysCreateShadowIdProperty, alwaysCreate, fromDataAnnotation)?.Value;

/// <summary>
/// Gets the <see cref="ConfigurationSource" /> for <see cref="GetAlwaysCreateShadowIdProperty"/>.
/// </summary>
/// <param name="entityType">The entity typer.</param>
/// <returns>The <see cref="ConfigurationSource" />.</returns>
public static ConfigurationSource? GetAlwaysCreateShadowIdPropertyConfigurationSource(this IConventionEntityType entityType)
=> entityType.FindAnnotation(CosmosAnnotationNames.AlwaysCreateShadowIdProperty)?.GetConfigurationSource();

/// <summary>
/// Returns a value indicating whether the entity type discriminator should be included in the JSON "id" value.
/// Prior to EF Core 9, it was always included. Starting with EF Core 9, it is not included by default.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <returns>The <see cref="DiscriminatorInKeyBehavior"/> or <see langword="null" /> if not set.</returns>
public static DiscriminatorInKeyBehavior? GetDiscriminatorInKey(this IReadOnlyEntityType entityType)
=> entityType.BaseType != null
? entityType.GetRootType().GetDiscriminatorInKey()
: (DiscriminatorInKeyBehavior?)entityType[CosmosAnnotationNames.DiscriminatorInKey];

/// <summary>
/// Includes the entity type discriminator in the JSON "id".
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="behavior">The behavior to use, or <see langword="null" /> to reset the behavior to the default.</param>
public static void SetDiscriminatorInKey(this IMutableEntityType entityType, DiscriminatorInKeyBehavior? behavior)
=> entityType.SetOrRemoveAnnotation(CosmosAnnotationNames.DiscriminatorInKey, behavior);

/// <summary>
/// Includes the entity type discriminator in the JSON "id".
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="behavior">The behavior to use, or <see langword="null" /> to reset the behavior to the default.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
public static DiscriminatorInKeyBehavior? SetDiscriminatorInKey(
this IConventionEntityType entityType, DiscriminatorInKeyBehavior? behavior, bool fromDataAnnotation = false)
=> (DiscriminatorInKeyBehavior?)entityType.SetOrRemoveAnnotation(
CosmosAnnotationNames.DiscriminatorInKey, behavior, fromDataAnnotation)?.Value;

/// <summary>
/// Gets the <see cref="ConfigurationSource" /> for <see cref="GetDiscriminatorInKey"/>.
/// </summary>
/// <param name="entityType">The entity typer.</param>
/// <returns>The <see cref="ConfigurationSource" />.</returns>
public static ConfigurationSource? GetDiscriminatorInKeyConfigurationSource(this IConventionEntityType entityType)
=> entityType.FindAnnotation(CosmosAnnotationNames.DiscriminatorInKey)
?.GetConfigurationSource();

/// <summary>
/// Returns the time to live for analytical store in seconds at container scope.
/// </summary>
Expand Down
76 changes: 76 additions & 0 deletions src/EFCore.Cosmos/Extensions/CosmosModelBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.Cosmos.Metadata;
using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal;

// ReSharper disable once CheckNamespace
Expand Down Expand Up @@ -104,6 +105,81 @@ public static ModelBuilder HasManualThroughput(this ModelBuilder modelBuilder, i
return modelBuilder;
}

/// <summary>
/// Forces model building to always create a "__id" shadow property mapped to the JSON "id". This was the default
/// behavior before EF Core 9.0.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see>, and
/// <see href="https://aka.ms/efcore-docs-cosmos">Accessing Azure Cosmos DB with EF Core</see> for more information and examples.
/// </remarks>
/// <param name="modelBuilder">The model builder.</param>
/// <param name="alwaysCreate">
/// <see langword="true" /> to force __id creation, <see langword="false" /> to not force __id creation,
/// <see langword="null" /> to revert to the default setting.
/// </param>
public static ModelBuilder AlwaysCreateShadowIdProperties(this ModelBuilder modelBuilder, bool? alwaysCreate = true)
{
modelBuilder.Model.SetAlwaysCreateShadowIdProperty(alwaysCreate);

return modelBuilder;
}

/// <summary>
/// Includes the discriminator value of the entity type in the JSON "id" value. This was the default behavior before EF Core 9.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see>, and
/// <see href="https://aka.ms/efcore-docs-cosmos">Accessing Azure Cosmos DB with EF Core</see> for more information and examples.
/// </remarks>
/// <param name="modelBuilder">The model builder.</param>
/// <param name="includeDiscriminator">
/// <see langword="true" /> to include the discriminator, <see langword="false" /> to not include the discriminator,
/// <see langword="null" /> to revert to the default setting.
/// </param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public static ModelBuilder IncludeDiscriminatorInJsonId(
this ModelBuilder modelBuilder,
bool? includeDiscriminator = true)
{
modelBuilder.Model.SetDiscriminatorInKey(
includeDiscriminator == null
? null
: includeDiscriminator.Value
? DiscriminatorInKeyBehavior.EntityTypeName
: DiscriminatorInKeyBehavior.None);

return modelBuilder;
}

/// <summary>
/// Includes the discriminator value of the root entity type in the JSON "id" value. This allows types with the same
/// primary key to be saved in the same container, while still allowing "ReadItem" to be used for lookups of an unknown type.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see>, and
/// <see href="https://aka.ms/efcore-docs-cosmos">Accessing Azure Cosmos DB with EF Core</see> for more information and examples.
/// </remarks>
/// <param name="modelBuilder">The model builder.</param>
/// <param name="includeDiscriminator">
/// <see langword="true" /> to include the discriminator, <see langword="false" /> to not include the discriminator,
/// <see langword="null" /> to revert to the default setting.
/// </param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public static ModelBuilder IncludeRootDiscriminatorInJsonId(
this ModelBuilder modelBuilder,
bool? includeDiscriminator = true)
{
modelBuilder.Model.SetDiscriminatorInKey(
includeDiscriminator == null
? null
: includeDiscriminator.Value
? DiscriminatorInKeyBehavior.RootEntityTypeName
: DiscriminatorInKeyBehavior.None);

return modelBuilder;
}

/// <summary>
/// Configures the autoscale provisioned throughput offering.
/// </summary>
Expand Down
86 changes: 86 additions & 0 deletions src/EFCore.Cosmos/Extensions/CosmosModelExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.Cosmos.Metadata;
using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal;

// ReSharper disable once CheckNamespace
Expand Down Expand Up @@ -57,6 +58,91 @@ public static void SetDefaultContainer(this IMutableModel model, string? name)
public static ConfigurationSource? GetDefaultContainerConfigurationSource(this IConventionModel model)
=> model.FindAnnotation(CosmosAnnotationNames.ContainerName)?.GetConfigurationSource();

/// <summary>
/// Returns a value indicating whether the setting for always creating the "__id" property can be set
/// from the current configuration source
/// </summary>
/// <param name="model">The model.</param>
/// <returns>The configured value.</returns>
public static bool? GetAlwaysCreateShadowIdProperty(this IReadOnlyModel model)
=> (bool?)model[CosmosAnnotationNames.AlwaysCreateShadowIdProperty];

/// <summary>
/// Forces model building to always create a "__id" shadow property mapped to the JSON "id". This was the default
/// behavior before EF Core 9.0.
/// </summary>
/// <param name="model">The model.</param>
/// <param name="alwaysCreate">
/// <see langword="true" /> to force __id creation, <see langword="false" /> to not force __id creation,
/// <see langword="null" /> to revert to the default setting.
/// </param>
public static void SetAlwaysCreateShadowIdProperty(this IMutableModel model, bool? alwaysCreate)
=> model.SetOrRemoveAnnotation(CosmosAnnotationNames.AlwaysCreateShadowIdProperty, alwaysCreate);

/// <summary>
/// Forces model building to always create a "__id" shadow property mapped to the JSON "id". This was the default
/// behavior before EF Core 9.0.
/// </summary>
/// <param name="model">The model.</param>
/// <param name="alwaysCreate">
/// <see langword="true" /> to force __id creation, <see langword="false" /> to not force __id creation,
/// <see langword="null" /> to revert to the default setting.
/// </param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
public static bool? SetAlwaysCreateShadowIdProperty(
this IConventionModel model,
bool? alwaysCreate,
bool fromDataAnnotation = false)
=> (bool?)model.SetOrRemoveAnnotation(CosmosAnnotationNames.AlwaysCreateShadowIdProperty, alwaysCreate, fromDataAnnotation)?.Value;

/// <summary>
/// Gets the <see cref="ConfigurationSource" />
/// for <see cref="GetAlwaysCreateShadowIdProperty(Microsoft.EntityFrameworkCore.Metadata.IReadOnlyModel)"/>.
/// </summary>
/// <param name="model">The model.</param>
/// <returns>The <see cref="ConfigurationSource" />.</returns>
public static ConfigurationSource? GetAlwaysCreateShadowIdPropertyConfigurationSource(this IConventionModel model)
=> model.FindAnnotation(CosmosAnnotationNames.AlwaysCreateShadowIdProperty)?.GetConfigurationSource();

/// <summary>
/// Returns a value indicating whether the entity type discriminator should be included in the JSON "id" value.
/// Prior to EF Core 9, it was always included. Starting with EF Core 9, it is not included by default.
/// </summary>
/// <param name="model">The model.</param>
/// <returns>The <see cref="DiscriminatorInKeyBehavior"/> or <see langword="null" /> if not set.</returns>
public static DiscriminatorInKeyBehavior? GetDiscriminatorInKey(this IReadOnlyModel model)
=> (DiscriminatorInKeyBehavior?)model[CosmosAnnotationNames.DiscriminatorInKey];

/// <summary>
/// Includes the entity type discriminator in the JSON "id".
/// </summary>
/// <param name="model">The model.</param>
/// <param name="behavior">The behavior to use, or <see langword="null" /> to reset the behavior to the default.</param>
public static void SetDiscriminatorInKey(this IMutableModel model, DiscriminatorInKeyBehavior? behavior)
=> model.SetOrRemoveAnnotation(CosmosAnnotationNames.DiscriminatorInKey, behavior);

/// <summary>
/// Includes the entity type discriminator in the JSON "id".
/// </summary>
/// <param name="model">The model.</param>
/// <param name="behavior">The behavior to use, or <see langword="null" /> to reset the behavior to the default.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
public static DiscriminatorInKeyBehavior? SetDiscriminatorInKey(
this IConventionModel model,
DiscriminatorInKeyBehavior? behavior,
bool fromDataAnnotation = false)
=> (DiscriminatorInKeyBehavior?)model.SetOrRemoveAnnotation(
CosmosAnnotationNames.DiscriminatorInKey, behavior, fromDataAnnotation)?.Value;

/// <summary>
/// Gets the <see cref="ConfigurationSource" />
/// for <see cref="GetAlwaysCreateShadowIdProperty(Microsoft.EntityFrameworkCore.Metadata.IReadOnlyModel)"/>.
/// </summary>
/// <param name="model">The model.</param>
/// <returns>The <see cref="ConfigurationSource" />.</returns>
public static ConfigurationSource? GetDiscriminatorInKeyConfigurationSource(this IConventionModel model)
=> model.FindAnnotation(CosmosAnnotationNames.DiscriminatorInKey)?.GetConfigurationSource();

/// <summary>
/// Returns the provisioned throughput at database scope.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ protected virtual void ValidateKeys(
}

var idProperty = entityType.GetProperties()
.FirstOrDefault(p => p.GetJsonPropertyName() == StoreKeyConvention.IdPropertyJsonName);
.FirstOrDefault(p => p.GetJsonPropertyName() == CosmosJsonIdConvention.IdPropertyJsonName);
if (idProperty == null)
{
throw new InvalidOperationException(CosmosStrings.NoIdProperty(entityType.DisplayName()));
Expand All @@ -333,11 +333,6 @@ protected virtual void ValidateKeys(
idProperty.Name, entityType.DisplayName(), idType.ShortDisplayName()));
}

if (!idProperty.IsKey())
{
throw new InvalidOperationException(CosmosStrings.NoIdKey(entityType.DisplayName(), idProperty.Name));
}

var partitionKeyPropertyNames = entityType.GetPartitionKeyPropertyNames();
if (partitionKeyPropertyNames.Count == 0)
{
Expand All @@ -364,13 +359,6 @@ protected virtual void ValidateKeys(
CosmosStrings.PartitionKeyBadStoreType(
partitionKeyPropertyName, entityType.DisplayName(), partitionKeyType.ShortDisplayName()));
}

if (!partitionKey.GetContainingKeys().Any(k => k.Properties.Contains(idProperty)))
{
throw new InvalidOperationException(
CosmosStrings.NoPartitionKeyKey(
entityType.DisplayName(), partitionKeyPropertyName, idProperty.Name));
}
}
}
}
Expand Down
Loading

0 comments on commit fff81d7

Please sign in to comment.