Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cosmos: allow for concurrency token on '_etag' property #19581

Merged
merged 13 commits into from
Feb 5, 2020
28 changes: 28 additions & 0 deletions src/EFCore.Cosmos/Extensions/CosmosEntityTypeBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -255,5 +255,33 @@ public static bool CanSetPartitionKey(

return entityTypeBuilder.CanSetAnnotation(CosmosAnnotationNames.PartitionKeyName, name, fromDataAnnotation);
}

/// <summary>
/// Configures this entity to use CosmosDb etag concurrency checks.
/// </summary>
/// <param name="entityTypeBuilder"> The builder for the entity type being configured. </param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public static EntityTypeBuilder UseEtagConcurrency([NotNull] this EntityTypeBuilder entityTypeBuilder)
{
Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder));

entityTypeBuilder.Property<string>("_etag")
.ValueGeneratedOnAddOrUpdate()
.IsConcurrencyToken();
return entityTypeBuilder;
}

/// <summary>
/// Configures this entity to use CosmosDb etag concurrency checks.
/// </summary>
/// <param name="entityTypeBuilder"> The builder for the entity type being configured. </param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public static EntityTypeBuilder<TEntity> UseEtagConcurrency<TEntity>([NotNull] this EntityTypeBuilder<TEntity> entityTypeBuilder)
where TEntity : class
{
Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder));
UseEtagConcurrency((EntityTypeBuilder)entityTypeBuilder);
return entityTypeBuilder;
}
}
}
52 changes: 52 additions & 0 deletions src/EFCore.Cosmos/Extensions/CosmosEntityTypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,5 +146,57 @@ public static void SetPartitionKeyPropertyName(
public static ConfigurationSource? GetPartitionKeyPropertyNameConfigurationSource([NotNull] this IConventionEntityType entityType)
=> entityType.FindAnnotation(CosmosAnnotationNames.PartitionKeyName)
?.GetConfigurationSource();

/// <summary>
/// Returns the name of the property that is used to store the etag.
/// </summary>
/// <param name="entityType"> The entity type to get the etag property name for. </param>
/// <returns> The name of the etag property. </returns>
public static string GetETagPropertyName([NotNull] this IEntityType entityType)
=> entityType[CosmosAnnotationNames.ETagName] as string;

/// <summary>
/// Sets the name of the property that is used to store the etag key.
/// </summary>
/// <param name="entityType"> The entity type to set the etag property name for. </param>
/// <param name="name"> The name to set. </param>
public static void SetETagPropertyName([NotNull] this IMutableEntityType entityType, [CanBeNull] string name)
=> entityType.SetOrRemoveAnnotation(
CosmosAnnotationNames.ETagName,
Check.NullButNotEmpty(name, nameof(name)));

/// <summary>
/// Sets the name of the property that is used to store the etag.
/// </summary>
/// <param name="entityType"> The entity type to set the etag property name for. </param>
/// <param name="name"> The name to set. </param>
/// <param name="fromDataAnnotation"> Indicates whether the configuration was specified using a data annotation. </param>
public static void SetETagPropertyName(
[NotNull] this IConventionEntityType entityType, [CanBeNull] string name, bool fromDataAnnotation = false)
=> entityType.SetOrRemoveAnnotation(
CosmosAnnotationNames.ETagName,
Check.NullButNotEmpty(name, nameof(name)),
fromDataAnnotation);

/// <summary>
/// Gets the <see cref="ConfigurationSource" /> for the property that is used to store the etag.
/// </summary>
/// <param name="entityType"> The entity type to find configuration source for. </param>
/// <returns> The <see cref="ConfigurationSource" /> for the etag property. </returns>
public static ConfigurationSource? GetETagPropertyNameConfigurationSource([NotNull] this IConventionEntityType entityType)
jviau marked this conversation as resolved.
Show resolved Hide resolved
=> entityType.FindAnnotation(CosmosAnnotationNames.ETagName)
?.GetConfigurationSource();

/// <summary>
/// Gets the <see cref="IProperty"/> on this entity that is mapped to cosmos etag, if it exists.
/// </summary>
/// <param name="entityType"> The entity type to get the etag property for. </param>
/// <returns> The <see cref="IProperty"/> mapped to etag, or null if no property is mapped to etag. </returns>
public static IProperty GetETagProperty([NotNull] this IEntityType entityType)
{
Check.NotNull(entityType, nameof(entityType));
var etagPropertyName = entityType.GetETagPropertyName();
return !string.IsNullOrEmpty(etagPropertyName) ? entityType.FindProperty(etagPropertyName) : null;
}
}
}
25 changes: 25 additions & 0 deletions src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,30 @@ public static bool CanSetJsonProperty(
[CanBeNull] string name,
bool fromDataAnnotation = false)
=> propertyBuilder.CanSetAnnotation(CosmosAnnotationNames.PropertyName, name, fromDataAnnotation);

/// <summary>
/// Configures this property to be the etag concurrency token.
/// </summary>
/// <param name="propertyBuilder"> The builder for the property being configured. </param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public static PropertyBuilder IsEtagConcurrency([NotNull] this PropertyBuilder propertyBuilder)
{
Check.NotNull(propertyBuilder, nameof(propertyBuilder));
propertyBuilder
.IsConcurrencyToken()
.ToJsonProperty("_etag")
.ValueGeneratedOnAddOrUpdate();
return propertyBuilder;
}

/// <summary>
/// Configures this property to be the etag concurrency token.
/// </summary>
/// <typeparam name="TProperty"> The type of the property being configured. </typeparam>
/// <param name="propertyBuilder"> The builder for the property being configured. </param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public static PropertyBuilder<TProperty> IsEtagConcurrency<TProperty>(
[NotNull] this PropertyBuilder<TProperty> propertyBuilder)
=> (PropertyBuilder<TProperty>)IsEtagConcurrency((PropertyBuilder)propertyBuilder);
}
}
36 changes: 36 additions & 0 deletions src/EFCore.Cosmos/Internal/CosmosModelValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public override void Validate(IModel model, IDiagnosticsLogger<DbLoggerCategory.
base.Validate(model, logger);

ValidateSharedContainerCompatibility(model, logger);
ValidateOnlyEtagConcurrencyToken(model, logger);
}

/// <summary>
Expand Down Expand Up @@ -173,5 +174,40 @@ protected virtual void ValidateSharedContainerCompatibility(
}
}
}

/// <summary>
/// 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.
/// </summary>
protected virtual void ValidateOnlyEtagConcurrencyToken(
[NotNull] IModel model,
[NotNull] IDiagnosticsLogger<DbLoggerCategory.Model.Validation> logger)
{
foreach (var entityType in model.GetEntityTypes())
{
foreach (var property in entityType.GetDeclaredProperties())
{
if (property.IsConcurrencyToken)
{
var storeName = property.GetJsonPropertyName();
if (storeName != "_etag")
{
throw new InvalidOperationException(
CosmosStrings.NonEtagConcurrencyToken(entityType.DisplayName(), storeName));
}

var etagType = property.GetTypeMapping().Converter?.ProviderClrType ?? property.ClrType;
if (etagType != typeof(string))
{
throw new InvalidOperationException(
CosmosStrings.ETagNonStringStoreType(
property.Name, entityType.DisplayName(), etagType.ShortDisplayName()));
}
}
}
}
}
}
}
35 changes: 35 additions & 0 deletions src/EFCore.Cosmos/Metadata/Conventions/ETagPropertyConvention.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Metadata.Conventions;

namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.Conventions
{
/// <summary>
/// A convention that adds etag metadata on the concurrency token, if present.
/// </summary>
public class ETagPropertyConvention : IModelFinalizedConvention
jviau marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Called after a model is finalized.
/// </summary>
/// <param name="modelBuilder"> The builder for the model. </param>
/// <param name="context"> Additional information associated with convention execution. </param>
public virtual void ProcessModelFinalized(
IConventionModelBuilder modelBuilder,
IConventionContext<IConventionModelBuilder> context)
{
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
{
foreach (var property in entityType.GetDeclaredProperties())
{
if (property.IsConcurrencyToken)
{
entityType.SetETagPropertyName(property.Name);
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ public override ConventionSet CreateConventionSet()
var conventionSet = base.CreateConventionSet();

conventionSet.ModelInitializedConventions.Add(new ContextContainerConvention(Dependencies));
ConventionSet.AddBefore(
conventionSet.ModelFinalizedConventions,
new ETagPropertyConvention(),
typeof(ValidatingConvention));

var discriminatorConvention = new CosmosDiscriminatorConvention(Dependencies);
var storeKeyConvention = new StoreKeyConvention(Dependencies);
Expand Down
8 changes: 8 additions & 0 deletions src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,13 @@ public static class CosmosAnnotationNames
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public const string PartitionKeyName = Prefix + "PartitionKeyName";

/// <summary>
/// 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.
/// </summary>
public const string ETagName = Prefix + "EtagName";
}
}
24 changes: 24 additions & 0 deletions src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/EFCore.Cosmos/Properties/CosmosStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,18 @@
<data name="DuplicateDiscriminatorValue" xml:space="preserve">
<value>The discriminator value for '{entityType1}' is '{discriminatorValue}' which is the same for '{entityType2}'. Every concrete entity type mapped to the container '{container}' needs to have a unique discriminator value.</value>
</data>
<data name="ETagNonStringStoreType" xml:space="preserve">
<value>The type of the etag property '{property}' on '{entityType}' is '{propertyType}'. All etag properties need to be strings or have a string converter.</value>
</data>
<data name="NoDiscriminatorProperty" xml:space="preserve">
<value>The entity type '{entityType}' is sharing the container '{container}' with other types, but does not have a discriminator property configured.</value>
</data>
<data name="NoDiscriminatorValue" xml:space="preserve">
<value>The entity type '{entityType}' is sharing the container '{container}' with other types, but does not have a discriminator value configured.</value>
</data>
<data name="NonEtagConcurrencyToken" xml:space="preserve">
<value>The entity type '{entityType}' has property '{property}' as its concurrency token, but only '_etag' is supported. Consider using 'EntityTypeBuilder.UseEtagConcurrency'.</value>
</data>
<data name="NoPartitionKey" xml:space="preserve">
<value>The entity type '{entityType}' does not have a partition key set, but it is mapped to the container '{container}' shared by entity types with partition keys.</value>
</data>
Expand All @@ -147,4 +153,7 @@
<data name="PartitionKeyStoreNameMismatch" xml:space="preserve">
<value>The partition key property '{property1}' on '{entityType1}' is mapped as '{storeName1}', but the partition key property '{property2}' on '{entityType2}' is mapped as '{storeName2}'. All partition key properties need to be mapped to the same store property.</value>
</data>
<data name="UpdateConflict" xml:space="preserve">
<value>Conflicts were detected for item with id '{itemId}'.</value>
</data>
</root>
Loading