diff --git a/src/EFCore.Abstractions/Properties/AbstractionsStrings.Designer.cs b/src/EFCore.Abstractions/Properties/AbstractionsStrings.Designer.cs index 24e8ce10ae1..45d990e16fe 100644 --- a/src/EFCore.Abstractions/Properties/AbstractionsStrings.Designer.cs +++ b/src/EFCore.Abstractions/Properties/AbstractionsStrings.Designer.cs @@ -1,9 +1,6 @@ // -using System; -using System.Reflection; using System.Resources; -using JetBrains.Annotations; #nullable enable diff --git a/src/EFCore.Cosmos/Design/Internal/CosmosCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.Cosmos/Design/Internal/CosmosCSharpRuntimeAnnotationCodeGenerator.cs new file mode 100644 index 00000000000..5904acbfc37 --- /dev/null +++ b/src/EFCore.Cosmos/Design/Internal/CosmosCSharpRuntimeAnnotationCodeGenerator.cs @@ -0,0 +1,57 @@ +// 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.Internal; +using Microsoft.EntityFrameworkCore.Design.Internal; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Design.Internal +{ + /// + /// 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. + /// +#pragma warning disable EF1001 // Internal EF Core API usage. + public class CosmosCSharpRuntimeAnnotationCodeGenerator : CSharpRuntimeAnnotationCodeGenerator + { + /// + /// 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 CosmosCSharpRuntimeAnnotationCodeGenerator( + CSharpRuntimeAnnotationCodeGeneratorDependencies dependencies) + : base(dependencies) + { + } + + /// + public override void Generate(IModel model, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + var annotations = parameters.Annotations; + if (!parameters.IsRuntime) + { + annotations.Remove(CosmosAnnotationNames.Throughput); + } + + base.Generate(model, parameters); + } + + /// + public override void Generate(IEntityType entityType, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + var annotations = parameters.Annotations; + if (!parameters.IsRuntime) + { + annotations.Remove(CosmosAnnotationNames.AnalyticalStoreTimeToLive); + annotations.Remove(CosmosAnnotationNames.DefaultTimeToLive); + annotations.Remove(CosmosAnnotationNames.Throughput); + } + + base.Generate(entityType, parameters); + } + } +} diff --git a/src/EFCore.Cosmos/Design/Internal/CosmosDesignTimeServices.cs b/src/EFCore.Cosmos/Design/Internal/CosmosDesignTimeServices.cs index 1dd9203dea5..9c72b13aa09 100644 --- a/src/EFCore.Cosmos/Design/Internal/CosmosDesignTimeServices.cs +++ b/src/EFCore.Cosmos/Design/Internal/CosmosDesignTimeServices.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.EntityFrameworkCore.Design; +using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.Extensions.DependencyInjection; [assembly: DesignTimeProviderServices("Microsoft.EntityFrameworkCore.Cosmos.Design.Internal.CosmosDesignTimeServices")] @@ -25,7 +26,10 @@ public class CosmosDesignTimeServices : IDesignTimeServices public virtual void ConfigureDesignTimeServices(IServiceCollection serviceCollection) { serviceCollection.AddEntityFrameworkCosmos(); +#pragma warning disable EF1001 // Internal EF Core API usage. new EntityFrameworkDesignServicesBuilder(serviceCollection) + .TryAdd() +#pragma warning restore EF1001 // Internal EF Core API usage. .TryAddCoreServices(); } } diff --git a/src/EFCore.Cosmos/Extensions/CosmosEntityTypeBuilderExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosEntityTypeBuilderExtensions.cs index a6aafb041ad..7e92cde073d 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosEntityTypeBuilderExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosEntityTypeBuilderExtensions.cs @@ -4,8 +4,10 @@ using System; using System.Linq.Expressions; using System.Reflection; +using Microsoft.Azure.Cosmos; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Utilities; @@ -288,5 +290,258 @@ public static EntityTypeBuilder UseETagConcurrency(this Entity UseETagConcurrency((EntityTypeBuilder)entityTypeBuilder); return entityTypeBuilder; } + + /// + /// Configures the time to live for analytical store in seconds at container scope. + /// + /// The builder for the entity type being configured. + /// The time to live. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder HasAnalyticalStoreTimeToLive( + this EntityTypeBuilder entityTypeBuilder, + int? seconds) + { + entityTypeBuilder.Metadata.SetAnalyticalStoreTimeToLive(seconds); + + return entityTypeBuilder; + } + + /// + /// Configures the time to live for analytical store in seconds at container scope. + /// + /// The builder for the entity type being configured. + /// The time to live. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder HasAnalyticalStoreTimeToLive( + this EntityTypeBuilder entityTypeBuilder, + int? seconds) + where TEntity : class + { + entityTypeBuilder.Metadata.SetAnalyticalStoreTimeToLive(seconds); + + return entityTypeBuilder; + } + + /// + /// Configures the time to live for analytical store in seconds at container scope. + /// + /// The builder for the entity type being configured. + /// The time to live. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + public static IConventionEntityTypeBuilder? HasAnalyticalStoreTimeToLive( + this IConventionEntityTypeBuilder entityTypeBuilder, + int? seconds, + bool fromDataAnnotation = false) + { + if (!entityTypeBuilder.CanSetAnalyticalStoreTimeToLive(seconds, fromDataAnnotation)) + { + return null; + } + + entityTypeBuilder.Metadata.SetAnalyticalStoreTimeToLive(seconds, fromDataAnnotation); + + return entityTypeBuilder; + } + + /// + /// Returns a value indicating whether the time to live for analytical store can be set + /// from the current configuration source + /// + /// The builder for the entity type being configured. + /// The time to live. + /// Indicates whether the configuration was specified using a data annotation. + /// if the configuration can be applied. + public static bool CanSetAnalyticalStoreTimeToLive( + this IConventionEntityTypeBuilder entityTypeBuilder, + int? seconds, + bool fromDataAnnotation = false) + { + Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); + + return entityTypeBuilder.CanSetAnnotation(CosmosAnnotationNames.AnalyticalStoreTimeToLive, seconds, fromDataAnnotation); + } + + /// + /// Configures the default time to live in seconds at container scope. + /// + /// The builder for the entity type being configured. + /// The time to live. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder HasDefaultTimeToLive( + this EntityTypeBuilder entityTypeBuilder, + int? seconds) + { + entityTypeBuilder.Metadata.SetDefaultTimeToLive(seconds); + + return entityTypeBuilder; + } + + /// + /// Configures the default time to live in seconds at container scope. + /// + /// The builder for the entity type being configured. + /// The time to live. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder HasDefaultTimeToLive( + this EntityTypeBuilder entityTypeBuilder, + int? seconds) + where TEntity : class + { + entityTypeBuilder.Metadata.SetDefaultTimeToLive(seconds); + + return entityTypeBuilder; + } + + /// + /// Configures the default time to live in seconds at container scope. + /// + /// The builder for the entity type being configured. + /// The time to live. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + public static IConventionEntityTypeBuilder? HasDefaultTimeToLive( + this IConventionEntityTypeBuilder entityTypeBuilder, + int? seconds, + bool fromDataAnnotation = false) + { + if (!entityTypeBuilder.CanSetDefaultTimeToLive(seconds, fromDataAnnotation)) + { + return null; + } + + entityTypeBuilder.Metadata.SetDefaultTimeToLive(seconds, fromDataAnnotation); + + return entityTypeBuilder; + } + + /// + /// Returns a value indicating whether the default time to live can be set + /// from the current configuration source + /// + /// The builder for the entity type being configured. + /// The time to live. + /// Indicates whether the configuration was specified using a data annotation. + /// if the configuration can be applied. + public static bool CanSetDefaultTimeToLive( + this IConventionEntityTypeBuilder entityTypeBuilder, + int? seconds, + bool fromDataAnnotation = false) + { + Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); + + return entityTypeBuilder.CanSetAnnotation(CosmosAnnotationNames.DefaultTimeToLive, seconds, fromDataAnnotation); + } + + /// + /// Configures the manual provisioned throughput offering. + /// + /// The builder for the entity type being configured. + /// The throughput to set. + public static EntityTypeBuilder HasManualThroughput(this EntityTypeBuilder entityTypeBuilder, int? throughput) + { + entityTypeBuilder.Metadata.SetThroughput(throughput, autoscale: false); + + return entityTypeBuilder; + } + + /// + /// Configures the manual provisioned throughput offering. + /// + /// The builder for the entity type being configured. + /// The throughput to set. + public static EntityTypeBuilder HasManualThroughput(this EntityTypeBuilder entityTypeBuilder, int? throughput) + where TEntity : class + { + entityTypeBuilder.Metadata.SetThroughput(throughput, autoscale: false); + + return entityTypeBuilder; + } + + /// + /// Configures the autoscale provisioned throughput offering. + /// + /// The builder for the entity type being configured. + /// The throughput to set. + public static EntityTypeBuilder HasAutoscaleThroughput(this EntityTypeBuilder entityTypeBuilder, int? throughput) + { + entityTypeBuilder.Metadata.SetThroughput(throughput, autoscale: true); + + return entityTypeBuilder; + } + + /// + /// Configures the autoscale provisioned throughput offering. + /// + /// The builder for the entity type being configured. + /// The throughput to set. + public static EntityTypeBuilder HasAutoscaleThroughput(this EntityTypeBuilder entityTypeBuilder, int? throughput) + where TEntity : class + { + entityTypeBuilder.Metadata.SetThroughput(throughput, autoscale: true); + + return entityTypeBuilder; + } + + /// + /// Configures the provisioned throughput. + /// + /// The builder for the entity type being configured. + /// The throughput to set. + /// Whether autoscale is enabled. + /// Indicates whether the configuration was specified using a data annotation. + public static IConventionEntityTypeBuilder? HasThroughput( + this IConventionEntityTypeBuilder entityTypeBuilder, + int? throughput, + bool autoscale, + bool fromDataAnnotation = false) + { + if (!entityTypeBuilder.CanSetThroughput(throughput, autoscale, fromDataAnnotation)) + { + return null; + } + + entityTypeBuilder.Metadata.SetThroughput(throughput, autoscale, fromDataAnnotation); + + return entityTypeBuilder; + } + + /// + /// Returns a value indicating whether the given throughput can be set. + /// + /// The builder for the entity type being configured. + /// The throughput to set. + /// Whether autoscale is enabled. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given container name can be set as default. + public static bool CanSetThroughput( + this IConventionEntityTypeBuilder entityTypeBuilder, + int? throughput, + bool autoscale, + bool fromDataAnnotation = false) + { + var existingAnnotation = entityTypeBuilder.Metadata.FindAnnotation(CosmosAnnotationNames.Throughput); + if (existingAnnotation == null) + { + return true; + } + + var configurationSource = fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention; + if (configurationSource.Overrides(existingAnnotation.GetConfigurationSource())) + { + return true; + } + + var existingThroughput = (ThroughputProperties?)existingAnnotation.Value; + return autoscale + ? existingThroughput?.Throughput == throughput + : existingThroughput?.AutoscaleMaxThroughput == throughput; + } } } diff --git a/src/EFCore.Cosmos/Extensions/CosmosEntityTypeExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosEntityTypeExtensions.cs index cb5f588e2e8..e9125995a27 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosEntityTypeExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosEntityTypeExtensions.cs @@ -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.Azure.Cosmos; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Utilities; @@ -215,5 +216,148 @@ public static void SetETagPropertyName( /// The property mapped to etag, or if no property is mapped to ETag. public static IProperty? GetETagProperty(this IEntityType entityType) => (IProperty?)((IReadOnlyEntityType)entityType).GetETagProperty(); + + /// + /// Returns the time to live for analytical store in seconds at container scope. + /// + /// The entity type. + /// The time to live. + public static int? GetAnalyticalStoreTimeToLive(this IReadOnlyEntityType entityType) + => entityType.BaseType != null + ? entityType.GetRootType().GetAnalyticalStoreTimeToLive() + : (int?)entityType[CosmosAnnotationNames.AnalyticalStoreTimeToLive]; + + /// + /// Sets the time to live for analytical store in seconds at container scope. + /// + /// The entity type. + /// The time to live to set. + public static void SetAnalyticalStoreTimeToLive(this IMutableEntityType entityType, int? seconds) + => entityType.SetOrRemoveAnnotation( + CosmosAnnotationNames.AnalyticalStoreTimeToLive, + seconds); + + /// + /// Sets the time to live for analytical store in seconds at container scope. + /// + /// The entity type. + /// The time to live to set. + /// Indicates whether the configuration was specified using a data annotation. + public static void SetAnalyticalStoreTimeToLive( + this IConventionEntityType entityType, + int? seconds, + bool fromDataAnnotation = false) + => entityType.SetOrRemoveAnnotation( + CosmosAnnotationNames.AnalyticalStoreTimeToLive, + seconds, + fromDataAnnotation); + + /// + /// Gets the for the time to live for analytical store in seconds at container scope. + /// + /// The entity typer. + /// The for the time to live for analytical store. + public static ConfigurationSource? GetAnalyticalStoreTimeToLiveConfigurationSource(this IConventionEntityType entityType) + => entityType.FindAnnotation(CosmosAnnotationNames.AnalyticalStoreTimeToLive) + ?.GetConfigurationSource(); + + /// + /// Returns the default time to live in seconds at container scope. + /// + /// The entity type. + /// The time to live. + public static int? GetDefaultTimeToLive(this IReadOnlyEntityType entityType) + => entityType.BaseType != null + ? entityType.GetRootType().GetDefaultTimeToLive() + : (int?)entityType[CosmosAnnotationNames.DefaultTimeToLive]; + + /// + /// Sets the default time to live in seconds at container scope. + /// + /// The entity type. + /// The time to live to set. + public static void SetDefaultTimeToLive(this IMutableEntityType entityType, int? seconds) + => entityType.SetOrRemoveAnnotation( + CosmosAnnotationNames.DefaultTimeToLive, + seconds); + + /// + /// Sets the default time to live in seconds at container scope. + /// + /// The entity type. + /// The time to live to set. + /// Indicates whether the configuration was specified using a data annotation. + public static void SetDefaultTimeToLive( + this IConventionEntityType entityType, + int? seconds, + bool fromDataAnnotation = false) + => entityType.SetOrRemoveAnnotation( + CosmosAnnotationNames.DefaultTimeToLive, + seconds, + fromDataAnnotation); + + /// + /// Gets the for the default time to live in seconds at container scope. + /// + /// The entity type to find configuration source for. + /// The for the default time to live. + public static ConfigurationSource? GetDefaultTimeToLiveConfigurationSource(this IConventionEntityType entityType) + => entityType.FindAnnotation(CosmosAnnotationNames.DefaultTimeToLive) + ?.GetConfigurationSource(); + + /// + /// Returns the provisioned throughput at container scope. + /// + /// The entity type. + /// The throughput. + public static ThroughputProperties? GetThroughput(this IReadOnlyEntityType entityType) + => entityType.BaseType != null + ? entityType.GetRootType().GetThroughput() + : (ThroughputProperties?)entityType[CosmosAnnotationNames.Throughput]; + + /// + /// Sets the provisioned throughput at container scope. + /// + /// The entity type. + /// The throughput to set. + /// Whether autoscale is enabled. + public static void SetThroughput(this IMutableEntityType entityType, int? throughput, bool? autoscale) + => entityType.SetOrRemoveAnnotation( + CosmosAnnotationNames.Throughput, + throughput == null || autoscale == null + ? null + : autoscale.Value + ? ThroughputProperties.CreateAutoscaleThroughput(throughput.Value) + : ThroughputProperties.CreateManualThroughput(throughput.Value)); + + /// + /// Sets the provisioned throughput at container scope. + /// + /// The entity type. + /// The throughput to set. + /// Whether autoscale is enabled. + /// Indicates whether the configuration was specified using a data annotation. + public static void SetThroughput( + this IConventionEntityType entityType, + int? throughput, + bool? autoscale, + bool fromDataAnnotation = false) + => entityType.SetOrRemoveAnnotation( + CosmosAnnotationNames.Throughput, + throughput == null || autoscale == null + ? null + : autoscale.Value + ? ThroughputProperties.CreateAutoscaleThroughput(throughput.Value) + : ThroughputProperties.CreateManualThroughput(throughput.Value), + fromDataAnnotation); + + /// + /// Gets the for the provisioned throughput at container scope. + /// + /// The entity type to find configuration source for. + /// The for the throughput. + public static ConfigurationSource? GetThroughputConfigurationSource(this IConventionEntityType entityType) + => entityType.FindAnnotation(CosmosAnnotationNames.Throughput) + ?.GetConfigurationSource(); } } diff --git a/src/EFCore.Cosmos/Extensions/CosmosModelBuilderExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosModelBuilderExtensions.cs index 6a2cff5897e..029958b5015 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosModelBuilderExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosModelBuilderExtensions.cs @@ -1,9 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Azure.Cosmos; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Utilities; +using Newtonsoft.Json.Linq; // ReSharper disable once CheckNamespace namespace Microsoft.EntityFrameworkCore @@ -70,10 +73,88 @@ public static bool CanSetDefaultContainer( string? name, bool fromDataAnnotation = false) { - Check.NotNull(modelBuilder, nameof(modelBuilder)); Check.NullButNotEmpty(name, nameof(name)); return modelBuilder.CanSetAnnotation(CosmosAnnotationNames.ContainerName, name, fromDataAnnotation); } + + /// + /// Configures the manual provisioned throughput offering. + /// + /// The model builder. + /// The throughput to set. + public static ModelBuilder HasManualThroughput(this ModelBuilder modelBuilder, int? throughput) + { + modelBuilder.Model.SetThroughput(throughput, autoscale: false); + + return modelBuilder; + } + + /// + /// Configures the autoscale provisioned throughput offering. + /// + /// The model builder. + /// The throughput to set. + public static ModelBuilder HasAutoscaleThroughput(this ModelBuilder modelBuilder, int? throughput) + { + modelBuilder.Model.SetThroughput(throughput, autoscale: true); + + return modelBuilder; + } + + /// + /// Configures the provisioned throughput. + /// + /// The model builder. + /// The throughput to set. + /// Whether autoscale is enabled. + /// Indicates whether the configuration was specified using a data annotation. + public static IConventionModelBuilder? HasThroughput( + this IConventionModelBuilder modelBuilder, + int? throughput, + bool autoscale, + bool fromDataAnnotation = false) + { + if (!modelBuilder.CanSetThroughput(throughput, autoscale, fromDataAnnotation)) + { + return null; + } + + modelBuilder.Metadata.SetThroughput(throughput, autoscale, fromDataAnnotation); + + return modelBuilder; + } + + /// + /// Returns a value indicating whether the given throughput can be set. + /// + /// The model builder. + /// The throughput to set. + /// Whether autoscale is enabled. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given container name can be set as default. + public static bool CanSetThroughput( + this IConventionModelBuilder modelBuilder, + int? throughput, + bool autoscale, + bool fromDataAnnotation = false) + { + var existingAnnotation = modelBuilder.Metadata.FindAnnotation(CosmosAnnotationNames.Throughput); + if (existingAnnotation == null) + { + return true; + } + + var configurationSource = fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention; + if (configurationSource.Overrides(existingAnnotation.GetConfigurationSource())) + { + return true; + } + + var existingThroughput = (ThroughputProperties?)existingAnnotation.Value; + return autoscale + ? existingThroughput?.Throughput == throughput + : existingThroughput?.AutoscaleMaxThroughput == throughput; + } } } diff --git a/src/EFCore.Cosmos/Extensions/CosmosModelExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosModelExtensions.cs index 65f0f89693a..a5bdb09902b 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosModelExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosModelExtensions.cs @@ -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.Azure.Cosmos; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Utilities; @@ -58,5 +59,61 @@ public static void SetDefaultContainer(this IMutableModel model, string? name) /// The configuration source for the default container name. public static ConfigurationSource? GetDefaultContainerConfigurationSource(this IConventionModel model) => model.FindAnnotation(CosmosAnnotationNames.ContainerName)?.GetConfigurationSource(); + + /// + /// Returns the provisioned throughput at database scope. + /// + /// The model. + /// The throughput. + public static ThroughputProperties? GetThroughput(this IReadOnlyModel model) + => (ThroughputProperties?)model[CosmosAnnotationNames.Throughput]; + + /// + /// Sets the provisioned throughput at database scope. + /// + /// The model. + /// The throughput to set. + /// Whether autoscale is enabled. + public static void SetThroughput(this IMutableModel model, int? throughput, bool? autoscale) + => model.SetOrRemoveAnnotation( + CosmosAnnotationNames.Throughput, + throughput == null || autoscale == null + ? null + : autoscale.Value + ? ThroughputProperties.CreateAutoscaleThroughput(throughput.Value) + : ThroughputProperties.CreateManualThroughput(throughput.Value)); + + /// + /// Sets the provisioned throughput at database scope. + /// + /// The model. + /// The throughput to set. + /// Whether autoscale is enabled. + /// Indicates whether the configuration was specified using a data annotation. + public static int? SetThroughput( + this IConventionModel model, + int? throughput, + bool? autoscale, + bool fromDataAnnotation = false) + { + var valueSet = (ThroughputProperties?)model.SetOrRemoveAnnotation( + CosmosAnnotationNames.Throughput, + throughput == null || autoscale == null + ? null + : autoscale.Value + ? ThroughputProperties.CreateAutoscaleThroughput(throughput.Value) + : ThroughputProperties.CreateManualThroughput(throughput.Value), + fromDataAnnotation)?.Value; + return valueSet?.AutoscaleMaxThroughput ?? valueSet?.Throughput; + } + + /// + /// Gets the for the provisioned throughput at database scope. + /// + /// The model. + /// The for the throughput. + public static ConfigurationSource? GetThroughputConfigurationSource(this IConventionModel model) + => model.FindAnnotation(CosmosAnnotationNames.Throughput) + ?.GetConfigurationSource(); } } diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs index feae50c3492..e74a263fd20 100644 --- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs +++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs @@ -1,9 +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 System; -using System.Collections.Generic; -using System.Linq; +using Microsoft.Azure.Cosmos; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -98,6 +96,9 @@ protected virtual void ValidateSharedContainerCompatibility( { var discriminatorValues = new Dictionary(); IProperty? partitionKey = null; + int? analyticalTTL = null; + int? defaultTTL = null; + ThroughputProperties? throughput = null; IEntityType? firstEntityType = null; foreach (var entityType in mappedTypes) { @@ -163,6 +164,72 @@ protected virtual void ValidateSharedContainerCompatibility( discriminatorValues[discriminatorValue] = entityType; } + + var currentAnalyticalTTL = entityType.GetAnalyticalStoreTimeToLive(); + if (currentAnalyticalTTL != null) + { + if (analyticalTTL == null) + { + analyticalTTL = currentAnalyticalTTL; + } + else if (analyticalTTL != currentAnalyticalTTL) + { + var conflictingEntityType = mappedTypes.First(et => et.GetAnalyticalStoreTimeToLive() != null); + throw new InvalidOperationException( + CosmosStrings.AnalyticalTTLMismatch( + analyticalTTL, conflictingEntityType.DisplayName(), entityType.DisplayName(), currentAnalyticalTTL, container)); + } + } + + var currentDefaultTTL = entityType.GetDefaultTimeToLive(); + if (currentDefaultTTL != null) + { + if (defaultTTL == null) + { + defaultTTL = currentDefaultTTL; + } + else if (defaultTTL != currentDefaultTTL) + { + var conflictingEntityType = mappedTypes.First(et => et.GetDefaultTimeToLive() != null); + throw new InvalidOperationException( + CosmosStrings.DefaultTTLMismatch( + defaultTTL, conflictingEntityType.DisplayName(), entityType.DisplayName(), currentDefaultTTL, container)); + } + } + + var currentThroughput = entityType.GetThroughput(); + if (currentThroughput != null) + { + if (throughput == null) + { + throughput = currentThroughput; + } + else if ((throughput.AutoscaleMaxThroughput ?? throughput.Throughput) + != (currentThroughput.AutoscaleMaxThroughput ?? currentThroughput.Throughput)) + { + var conflictingEntityType = mappedTypes.First(et => et.GetThroughput() != null); + throw new InvalidOperationException( + CosmosStrings.ThroughputMismatch( + throughput.AutoscaleMaxThroughput ?? throughput.Throughput, conflictingEntityType.DisplayName(), + entityType.DisplayName(), currentThroughput.AutoscaleMaxThroughput ?? currentThroughput.Throughput, + container)); + } + else if ((throughput.AutoscaleMaxThroughput == null) + != (currentThroughput.AutoscaleMaxThroughput == null)) + { + var conflictingEntityType = mappedTypes.First(et => et.GetThroughput() != null); + var autoscaleType = throughput.AutoscaleMaxThroughput == null + ? entityType + : conflictingEntityType; + var manualType = throughput.AutoscaleMaxThroughput != null + ? entityType + : conflictingEntityType; + + throw new InvalidOperationException( + CosmosStrings.ThroughputTypeMismatch( + manualType.DisplayName(), autoscaleType.DisplayName(), container)); + } + } } } diff --git a/src/EFCore.Cosmos/Metadata/Conventions/CosmosRuntimeModelConvention.cs b/src/EFCore.Cosmos/Metadata/Conventions/CosmosRuntimeModelConvention.cs new file mode 100644 index 00000000000..3b2df200575 --- /dev/null +++ b/src/EFCore.Cosmos/Metadata/Conventions/CosmosRuntimeModelConvention.cs @@ -0,0 +1,65 @@ +// 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.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; + +#nullable enable + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions +{ + /// + /// A convention that creates an optimized copy of the mutable model. This convention is typically + /// implemented by database providers to update provider annotations when creating a read-only model. + /// + public class CosmosRuntimeModelConvention : RuntimeModelConvention + { + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + public CosmosRuntimeModelConvention( + ProviderConventionSetBuilderDependencies dependencies) + : base(dependencies) + { + } + + /// + /// Updates the model annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source model. + /// The target model that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected override void ProcessModelAnnotations( + Dictionary annotations, IModel model, RuntimeModel runtimeModel, bool runtime) + { + base.ProcessModelAnnotations(annotations, model, runtimeModel, runtime); + + if (!runtime) + { + annotations.Remove(CosmosAnnotationNames.Throughput); + } + } + + /// + /// Updates the entity type annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source entity type. + /// The target entity type that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected override void ProcessEntityTypeAnnotations( + IDictionary annotations, IEntityType entityType, RuntimeEntityType runtimeEntityType, bool runtime) + { + base.ProcessEntityTypeAnnotations(annotations, entityType, runtimeEntityType, runtime); + + if (!runtime) + { + annotations.Remove(CosmosAnnotationNames.AnalyticalStoreTimeToLive); + annotations.Remove(CosmosAnnotationNames.DefaultTimeToLive); + annotations.Remove(CosmosAnnotationNames.Throughput); + } + } + } +} diff --git a/src/EFCore.Cosmos/Metadata/Conventions/Internal/CosmosConventionSetBuilder.cs b/src/EFCore.Cosmos/Metadata/Conventions/Internal/CosmosConventionSetBuilder.cs index ba7e79bb488..5371cfbb05a 100644 --- a/src/EFCore.Cosmos/Metadata/Conventions/Internal/CosmosConventionSetBuilder.cs +++ b/src/EFCore.Cosmos/Metadata/Conventions/Internal/CosmosConventionSetBuilder.cs @@ -110,6 +110,10 @@ public override ConventionSet CreateConventionSet() ReplaceConvention(conventionSet.ModelFinalizingConventions, inversePropertyAttributeConvention); + ReplaceConvention( + conventionSet.ModelFinalizedConventions, + (RuntimeModelConvention)new CosmosRuntimeModelConvention(Dependencies)); + return conventionSet; } diff --git a/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs b/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs index 409323a3692..f58138e3be7 100644 --- a/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs +++ b/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs @@ -50,5 +50,29 @@ public static class CosmosAnnotationNames /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public const string ETagName = Prefix + "ETagName"; + + /// + /// 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 const string AnalyticalStoreTimeToLive = Prefix + "AnalyticalStoreTimeToLive"; + + /// + /// 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 const string DefaultTimeToLive = Prefix + "DefaultTimeToLive"; + + /// + /// 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 const string Throughput = Prefix + "Throughput"; } } diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs index b75dd679806..3758de9d63a 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs @@ -1,9 +1,6 @@ // -using System; -using System.Reflection; using System.Resources; -using System.Threading; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.Extensions.Logging; @@ -23,6 +20,14 @@ public static class CosmosStrings private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.EntityFrameworkCore.Cosmos.Properties.CosmosStrings", typeof(CosmosStrings).Assembly); + /// + /// The time to live for analytical store was configured to '{ttl1}' on '{entityType1}', but on '{entityType2}' it was configured to '{ttl2}'. All entity types mapped to the same container '{container}' must be configured with the same time to live for analytical store. + /// + public static string AnalyticalTTLMismatch(object? ttl1, object? entityType1, object? entityType2, object? ttl2, object? container) + => string.Format( + GetString("AnalyticalTTLMismatch", nameof(ttl1), nameof(entityType1), nameof(entityType2), nameof(ttl2), nameof(container)), + ttl1, entityType1, entityType2, ttl2, container); + /// /// The Cosmos database does not support 'CanConnect' or 'CanConnectAsync'. /// @@ -41,6 +46,14 @@ public static string ConnectionStringConflictingConfiguration public static string CosmosNotInUse => GetString("CosmosNotInUse"); + /// + /// The default time to live was configured to '{ttl1}' on '{entityType1}', but on '{entityType2}' it was configured to '{ttl2}'. All entity types mapped to the same container '{container}' must be configured with the same default time to live. + /// + public static string DefaultTTLMismatch(object? ttl1, object? entityType1, object? entityType2, object? ttl2, object? container) + => string.Format( + GetString("DefaultTTLMismatch", nameof(ttl1), nameof(entityType1), nameof(entityType2), nameof(ttl2), nameof(container)), + ttl1, entityType1, entityType2, ttl2, container); + /// /// The discriminator value for '{entityType1}' is '{discriminatorValue}' which is the same for '{entityType2}'. Every concrete entity type mapped to the container '{container}' must have a unique discriminator value. /// @@ -253,6 +266,22 @@ public static string ResourceIdMissing public static string ReverseAfterSkipTakeNotSupported => GetString("ReverseAfterSkipTakeNotSupported"); + /// + /// The provisioned throughput was configured to '{throughput1}' on '{entityType1}', but on '{entityType2}' it was configured to '{throughput2}'. All entity types mapped to the same container '{container}' must be configured with the same provisioned throughput. + /// + public static string ThroughputMismatch(object? throughput1, object? entityType1, object? entityType2, object? throughput2, object? container) + => string.Format( + GetString("ThroughputMismatch", nameof(throughput1), nameof(entityType1), nameof(entityType2), nameof(throughput2), nameof(container)), + throughput1, entityType1, entityType2, throughput2, container); + + /// + /// The provisioned throughput was configured as manual on '{manualEntityType}', but on '{autoscaleEntityType}' it was configured as autoscale. All entity types mapped to the same container '{container}' must be configured with the same provisioned throughput type. + /// + public static string ThroughputTypeMismatch(object? manualEntityType, object? autoscaleEntityType, object? container) + => string.Format( + GetString("ThroughputTypeMismatch", nameof(manualEntityType), nameof(autoscaleEntityType), nameof(container)), + manualEntityType, autoscaleEntityType, container); + /// /// The Cosmos database provider does not support transactions. /// diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.resx b/src/EFCore.Cosmos/Properties/CosmosStrings.resx index 2e0458794b9..c5f72982bde 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.resx +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + The time to live for analytical store was configured to '{ttl1}' on '{entityType1}', but on '{entityType2}' it was configured to '{ttl2}'. All entity types mapped to the same container '{container}' must be configured with the same time to live for analytical store. + The Cosmos database does not support 'CanConnect' or 'CanConnectAsync'. @@ -126,6 +129,9 @@ Cosmos-specific methods can only be used when the context is using the Cosmos provider. + + The default time to live was configured to '{ttl1}' on '{entityType1}', but on '{entityType2}' it was configured to '{ttl2}'. All entity types mapped to the same container '{container}' must be configured with the same default time to live. + The discriminator value for '{entityType1}' is '{discriminatorValue}' which is the same for '{entityType2}'. Every concrete entity type mapped to the container '{container}' must have a unique discriminator value. @@ -218,6 +224,12 @@ Reversing the ordering is not supported when limit or offset are already applied. + + The provisioned throughput was configured to '{throughput1}' on '{entityType1}', but on '{entityType2}' it was configured to '{throughput2}'. All entity types mapped to the same container '{container}' must be configured with the same provisioned throughput. + + + The provisioned throughput was configured as manual on '{manualEntityType}', but on '{autoscaleEntityType}' it was configured as autoscale. All entity types mapped to the same container '{container}' must be configured with the same provisioned throughput type. + The Cosmos database provider does not support transactions. diff --git a/src/EFCore.Cosmos/Storage/Internal/ContainerProperties.cs b/src/EFCore.Cosmos/Storage/Internal/ContainerProperties.cs new file mode 100644 index 00000000000..a1ee7e5a284 --- /dev/null +++ b/src/EFCore.Cosmos/Storage/Internal/ContainerProperties.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Azure.Cosmos; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal +{ + /// + /// 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 record struct ContainerProperties + { + /// + /// 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 readonly string Id; + + /// + /// 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 readonly string PartitionKey; + + /// + /// 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 readonly int? AnalyticalStoreTimeToLiveInSeconds; + + /// + /// 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 readonly int? DefaultTimeToLive; + + /// + /// 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 readonly ThroughputProperties? Throughput; + + /// + /// 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 ContainerProperties(string containerId, string partitionKey, int? analyticalTTL, int? defaultTTL, ThroughputProperties? throughput) + { + Id = containerId; + PartitionKey = partitionKey; + AnalyticalStoreTimeToLiveInSeconds = analyticalTTL; + DefaultTimeToLive = defaultTTL; + Throughput = throughput; + } + + /// + /// 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 void Deconstruct(out string containerId, out string partitionKey, out int? analyticalTTL, out int? defaultTTL, out ThroughputProperties? throughput) + { + containerId = Id; + partitionKey = PartitionKey; + analyticalTTL = AnalyticalStoreTimeToLiveInSeconds; + defaultTTL = DefaultTimeToLive; + throughput = Throughput; + } + } +} diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs index f7a523b0400..14d0270d2a2 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs @@ -102,9 +102,9 @@ private CosmosClient Client /// 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 virtual bool CreateDatabaseIfNotExists() + public virtual bool CreateDatabaseIfNotExists(ThroughputProperties? throughput) => _executionStrategyFactory.Create().Execute( - (object?)null, CreateDatabaseIfNotExistsOnce, null); + throughput, CreateDatabaseIfNotExistsOnce, null); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -114,8 +114,8 @@ public virtual bool CreateDatabaseIfNotExists() /// public virtual bool CreateDatabaseIfNotExistsOnce( DbContext? context, - object? state) - => CreateDatabaseIfNotExistsOnceAsync(context, state).GetAwaiter().GetResult(); + ThroughputProperties? throughput) + => CreateDatabaseIfNotExistsOnceAsync(context, throughput).GetAwaiter().GetResult(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -124,9 +124,10 @@ public virtual bool CreateDatabaseIfNotExistsOnce( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual Task CreateDatabaseIfNotExistsAsync( + ThroughputProperties? throughput, CancellationToken cancellationToken = default) => _executionStrategyFactory.Create().ExecuteAsync( - (object?)null, CreateDatabaseIfNotExistsOnceAsync, null, cancellationToken); + throughput, CreateDatabaseIfNotExistsOnceAsync, null, cancellationToken); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -136,10 +137,10 @@ public virtual Task CreateDatabaseIfNotExistsAsync( /// public virtual async Task CreateDatabaseIfNotExistsOnceAsync( DbContext? _, - object? __, + ThroughputProperties? throughput, CancellationToken cancellationToken = default) { - var response = await Client.CreateDatabaseIfNotExistsAsync(_databaseId, cancellationToken: cancellationToken) + var response = await Client.CreateDatabaseIfNotExistsAsync(_databaseId, throughput, cancellationToken: cancellationToken) .ConfigureAwait(false); return response.StatusCode == HttpStatusCode.Created; @@ -204,15 +205,13 @@ public virtual async Task DeleteDatabaseOnceAsync( /// 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 virtual bool CreateContainerIfNotExists( - string containerId, - string partitionKey) + public virtual bool CreateContainerIfNotExists(ContainerProperties properties) => _executionStrategyFactory.Create().Execute( - (containerId, partitionKey), CreateContainerIfNotExistsOnce, null); + properties, CreateContainerIfNotExistsOnce, null); private bool CreateContainerIfNotExistsOnce( DbContext context, - (string ContainerId, string PartitionKey) parameters) + ContainerProperties parameters) => CreateContainerIfNotExistsOnceAsync(context, parameters).GetAwaiter().GetResult(); /// @@ -221,24 +220,24 @@ private bool CreateContainerIfNotExistsOnce( /// 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 virtual Task CreateContainerIfNotExistsAsync( - string containerId, - string partitionKey, - CancellationToken cancellationToken = default) + public virtual Task CreateContainerIfNotExistsAsync(ContainerProperties properties, CancellationToken cancellationToken = default) => _executionStrategyFactory.Create().ExecuteAsync( - (containerId, partitionKey), CreateContainerIfNotExistsOnceAsync, null, cancellationToken); + properties, CreateContainerIfNotExistsOnceAsync, null, cancellationToken); private async Task CreateContainerIfNotExistsOnceAsync( DbContext _, - (string ContainerId, string PartitionKey) parameters, + ContainerProperties parameters, CancellationToken cancellationToken = default) { using var response = await Client.GetDatabase(_databaseId).CreateContainerStreamAsync( - new ContainerProperties(parameters.ContainerId, "/" + parameters.PartitionKey) - { - PartitionKeyDefinitionVersion = PartitionKeyDefinitionVersion.V2 - }, - cancellationToken: cancellationToken) + new Azure.Cosmos.ContainerProperties(parameters.Id, "/" + parameters.PartitionKey) + { + PartitionKeyDefinitionVersion = PartitionKeyDefinitionVersion.V2, + DefaultTimeToLive = parameters.DefaultTimeToLive, + AnalyticalStoreTimeToLiveInSeconds = parameters.AnalyticalStoreTimeToLiveInSeconds, + }, + parameters.Throughput, + cancellationToken: cancellationToken) .ConfigureAwait(false); if (response.StatusCode == HttpStatusCode.Conflict) { diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs index ea7bfef7685..edc7be38c63 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs @@ -1,9 +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 System; -using System.Threading; -using System.Threading.Tasks; +using Microsoft.Azure.Cosmos; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage; @@ -50,16 +48,12 @@ public CosmosDatabaseCreator( /// public virtual bool EnsureCreated() { - var created = _cosmosClient.CreateDatabaseIfNotExists(); - foreach (var entityType in _designTimeModel.Model.GetEntityTypes()) + var model = _designTimeModel.Model; + var created = _cosmosClient.CreateDatabaseIfNotExists(model.GetThroughput()); + + foreach (var container in GetContainersToCreate(model)) { - var containerName = entityType.GetContainer(); - if (containerName != null) - { - created |= _cosmosClient.CreateContainerIfNotExists( - containerName, - GetPartitionKeyStoreName(entityType)); - } + created |= _cosmosClient.CreateContainerIfNotExists(container); } if (created) @@ -78,19 +72,14 @@ public virtual bool EnsureCreated() /// public virtual async Task EnsureCreatedAsync(CancellationToken cancellationToken = default) { - var created = await _cosmosClient.CreateDatabaseIfNotExistsAsync(cancellationToken) + var model = _designTimeModel.Model; + var created = await _cosmosClient.CreateDatabaseIfNotExistsAsync(model.GetThroughput(), cancellationToken) .ConfigureAwait(false); - foreach (var entityType in _designTimeModel.Model.GetEntityTypes()) + + foreach (var container in GetContainersToCreate(model)) { - var containerName = entityType.GetContainer(); - if (containerName != null) - { - created |= await _cosmosClient.CreateContainerIfNotExistsAsync( - containerName, - GetPartitionKeyStoreName(entityType), - cancellationToken) - .ConfigureAwait(false); - } + created |= await _cosmosClient.CreateContainerIfNotExistsAsync(container, cancellationToken) + .ConfigureAwait(false); } if (created) @@ -101,6 +90,52 @@ public virtual async Task EnsureCreatedAsync(CancellationToken cancellatio return created; } + private IEnumerable GetContainersToCreate(IModel model) + { + var containers = new Dictionary>(); + foreach (var entityType in model.GetEntityTypes().Where(et => et.FindPrimaryKey() != null)) + { + var container = entityType.GetContainer(); + if (container == null) + { + continue; + } + + if (!containers.TryGetValue(container, out var mappedTypes)) + { + mappedTypes = new List(); + containers[container] = mappedTypes; + } + + mappedTypes.Add(entityType); + } + + foreach (var containerMapping in containers) + { + var mappedTypes = containerMapping.Value; + var containerName = containerMapping.Key; + string? partitionKey = null; + int? analyticalTTL = null; + int? defaultTTL = null; + ThroughputProperties? throughput = null; + + foreach (var entityType in mappedTypes) + { + partitionKey ??= GetPartitionKeyStoreName(entityType); + analyticalTTL ??= entityType.GetAnalyticalStoreTimeToLive(); + defaultTTL ??= entityType.GetDefaultTimeToLive(); + throughput ??= entityType.GetThroughput(); + } + + yield return new ContainerProperties( + containerName, + partitionKey!, + analyticalTTL, + defaultTTL, + throughput); + } + } + /// /// 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.Cosmos/Storage/Internal/ICosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/ICosmosClientWrapper.cs index 142184dcd72..45b2529b1d0 100644 --- a/src/EFCore.Cosmos/Storage/Internal/ICosmosClientWrapper.cs +++ b/src/EFCore.Cosmos/Storage/Internal/ICosmosClientWrapper.cs @@ -24,7 +24,7 @@ public interface ICosmosClientWrapper /// 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. /// - Task CreateDatabaseIfNotExistsAsync(CancellationToken cancellationToken = default); + bool CreateDatabaseIfNotExists(ThroughputProperties? throughput); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -32,7 +32,7 @@ public interface ICosmosClientWrapper /// 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. /// - bool CreateDatabaseIfNotExists(); + Task CreateDatabaseIfNotExistsAsync(ThroughputProperties? throughput, CancellationToken cancellationToken = default); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -40,7 +40,7 @@ public interface ICosmosClientWrapper /// 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. /// - bool CreateContainerIfNotExists(string containerId, string partitionKey); + bool CreateContainerIfNotExists(ContainerProperties properties); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -48,10 +48,7 @@ public interface ICosmosClientWrapper /// 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. /// - Task CreateContainerIfNotExistsAsync( - string containerId, - string partitionKey, - CancellationToken cancellationToken = default); + Task CreateContainerIfNotExistsAsync(ContainerProperties properties, CancellationToken cancellationToken = default); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Design/Properties/DesignStrings.Designer.cs b/src/EFCore.Design/Properties/DesignStrings.Designer.cs index 701fd8f27e5..fbbb59da74a 100644 --- a/src/EFCore.Design/Properties/DesignStrings.Designer.cs +++ b/src/EFCore.Design/Properties/DesignStrings.Designer.cs @@ -1,7 +1,5 @@ // -using System; -using System.Reflection; using System.Resources; #nullable enable diff --git a/src/EFCore.InMemory/Properties/InMemoryStrings.Designer.cs b/src/EFCore.InMemory/Properties/InMemoryStrings.Designer.cs index 21cc04b226b..8fbd20b82b3 100644 --- a/src/EFCore.InMemory/Properties/InMemoryStrings.Designer.cs +++ b/src/EFCore.InMemory/Properties/InMemoryStrings.Designer.cs @@ -1,9 +1,6 @@ // -using System; -using System.Reflection; using System.Resources; -using System.Threading; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.Extensions.Logging; diff --git a/src/EFCore.Proxies/Properties/ProxiesStrings.Designer.cs b/src/EFCore.Proxies/Properties/ProxiesStrings.Designer.cs index b86a7de06fb..d4aeda2556c 100644 --- a/src/EFCore.Proxies/Properties/ProxiesStrings.Designer.cs +++ b/src/EFCore.Proxies/Properties/ProxiesStrings.Designer.cs @@ -1,9 +1,6 @@ // -using System; -using System.Reflection; using System.Resources; -using JetBrains.Annotations; #nullable enable diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index eb95cd991a4..e3ebc2a1faf 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1,9 +1,6 @@ // -using System; -using System.Reflection; using System.Resources; -using System.Threading; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.Extensions.Logging; diff --git a/src/EFCore.SqlServer.NTS/Properties/SqlServerNTSStrings.Designer.cs b/src/EFCore.SqlServer.NTS/Properties/SqlServerNTSStrings.Designer.cs index 411a5dee086..cb2e78ad6cf 100644 --- a/src/EFCore.SqlServer.NTS/Properties/SqlServerNTSStrings.Designer.cs +++ b/src/EFCore.SqlServer.NTS/Properties/SqlServerNTSStrings.Designer.cs @@ -1,7 +1,5 @@ // -using System; -using System.Reflection; using System.Resources; #nullable enable diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs index dae43f18bcf..80f340df44d 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs @@ -1,9 +1,6 @@ // -using System; -using System.Reflection; using System.Resources; -using System.Threading; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.Extensions.Logging; @@ -220,27 +217,37 @@ public static string SequenceBadType(object? property, object? entityType, objec property, entityType, propertyType); /// - /// An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure' to the 'UseSqlServer' call. + /// Entity type '{entityType}' mapped to temporal table does not contain the expected period property: '{propertyName}'. /// - public static string TransientExceptionDetected - => GetString("TransientExceptionDetected"); + public static string TemporalExpectedPeriodPropertyNotFound(object? entityType, object? propertyName) + => string.Format( + GetString("TemporalExpectedPeriodPropertyNotFound", nameof(entityType), nameof(propertyName)), + entityType, propertyName); /// - /// Only root entity type should be marked as temporal. Entity type: '{entityType}'. + /// Entity type '{entityType}' mapped to temporal table must have a period start and a period end property. /// - public static string TemporalOnlyOnRoot(object? entityType) + public static string TemporalMustDefinePeriodProperties(object? entityType) => string.Format( - GetString("TemporalOnlyOnRoot", nameof(entityType)), + GetString("TemporalMustDefinePeriodProperties", nameof(entityType)), entityType); /// - /// Temporal tables are only supported for entities using Table-Per-Hierarchy inheritance mapping. Entity type: '{entityType}'. + /// Temporal query is trying to use navigation to an entity '{entityType}' which itself doesn't map to temporal table. Either map the entity to temporal table or use join manually to access it. /// - public static string TemporalOnlySupportedForTPH(object? entityType) + public static string TemporalNavigationExpansionBetweenTemporalAndNonTemporal(object? entityType) => string.Format( - GetString("TemporalOnlySupportedForTPH", nameof(entityType)), + GetString("TemporalNavigationExpansionBetweenTemporalAndNonTemporal", nameof(entityType)), entityType); + /// + /// Navigation expansion is only supported for '{operationName}' temporal operation. For other operations use join manually. + /// + public static string TemporalNavigationExpansionOnlySupportedForAsOf(object? operationName) + => string.Format( + GetString("TemporalNavigationExpansionOnlySupportedForAsOf", nameof(operationName)), + operationName); + /// /// Temporal tables are not supported for table splitting scenario. Table: '{table}'. /// @@ -250,36 +257,36 @@ public static string TemporalNotSupportedForTableSplitting(object? table) table); /// - /// Entity type '{entityType}' mapped to temporal table must have a period start and a period end property. + /// Only root entity type should be marked as temporal. Entity type: '{entityType}'. /// - public static string TemporalMustDefinePeriodProperties(object? entityType) + public static string TemporalOnlyOnRoot(object? entityType) => string.Format( - GetString("TemporalMustDefinePeriodProperties", nameof(entityType)), + GetString("TemporalOnlyOnRoot", nameof(entityType)), entityType); /// - /// Entity type '{entityType}' mapped to temporal table does not contain the expected period property: '{propertyName}'. + /// Temporal tables are only supported for entities using Table-Per-Hierarchy inheritance mapping. Entity type: '{entityType}'. /// - public static string TemporalExpectedPeriodPropertyNotFound(object? entityType, object? propertyName) + public static string TemporalOnlySupportedForTPH(object? entityType) => string.Format( - GetString("TemporalExpectedPeriodPropertyNotFound", nameof(entityType), nameof(propertyName)), - entityType, propertyName); + GetString("TemporalOnlySupportedForTPH", nameof(entityType)), + entityType); /// - /// Period property '{entityType}.{propertyName}' must be a shadow property. + /// Period property '{entityType}.{propertyName}' can't have a default value specified. /// - public static string TemporalPeriodPropertyMustBeInShadowState(object? entityType, object? propertyName) + public static string TemporalPeriodPropertyCantHaveDefaultValue(object? entityType, object? propertyName) => string.Format( - GetString("TemporalPeriodPropertyMustBeInShadowState", nameof(entityType), nameof(propertyName)), + GetString("TemporalPeriodPropertyCantHaveDefaultValue", nameof(entityType), nameof(propertyName)), entityType, propertyName); /// - /// Period property '{entityType}.{propertyName}' must be non-nullable and of type '{dateTimeType}'. + /// Period property '{entityType}.{propertyName}' must be a shadow property. /// - public static string TemporalPeriodPropertyMustBeNonNullableDateTime(object? entityType, object? propertyName, object? dateTimeType) + public static string TemporalPeriodPropertyMustBeInShadowState(object? entityType, object? propertyName) => string.Format( - GetString("TemporalPeriodPropertyMustBeNonNullableDateTime", nameof(entityType), nameof(propertyName), nameof(dateTimeType)), - entityType, propertyName, dateTimeType); + GetString("TemporalPeriodPropertyMustBeInShadowState", nameof(entityType), nameof(propertyName)), + entityType, propertyName); /// /// Period property '{entityType}.{propertyName}' must be mapped to a column of type '{columnType}'. @@ -290,20 +297,12 @@ public static string TemporalPeriodPropertyMustBeMappedToDatetime2(object? entit entityType, propertyName, columnType); /// - /// Period property '{entityType}.{propertyName}' can't have a default value specified. - /// - public static string TemporalPeriodPropertyCantHaveDefaultValue(object? entityType, object? propertyName) - => string.Format( - GetString("TemporalPeriodPropertyCantHaveDefaultValue", nameof(entityType), nameof(propertyName)), - entityType, propertyName); - - /// - /// Property '{entityType}.{propertyName}' is mapped to the period column and must have ValueGenerated set to '{valueGeneratedValue}'. + /// Period property '{entityType}.{propertyName}' must be non-nullable and of type '{dateTimeType}'. /// - public static string TemporalPropertyMappedToPeriodColumnMustBeValueGeneratedOnAddOrUpdate(object? entityType, object? propertyName, object? valueGeneratedValue) + public static string TemporalPeriodPropertyMustBeNonNullableDateTime(object? entityType, object? propertyName, object? dateTimeType) => string.Format( - GetString("TemporalPropertyMappedToPeriodColumnMustBeValueGeneratedOnAddOrUpdate", nameof(entityType), nameof(propertyName), nameof(valueGeneratedValue)), - entityType, propertyName, valueGeneratedValue); + GetString("TemporalPeriodPropertyMustBeNonNullableDateTime", nameof(entityType), nameof(propertyName), nameof(dateTimeType)), + entityType, propertyName, dateTimeType); /// /// Property '{entityType}.{propertyName}' is mapped to the period column and can't have default value specified. @@ -314,20 +313,12 @@ public static string TemporalPropertyMappedToPeriodColumnCantHaveDefaultValue(ob entityType, propertyName); /// - /// Temporal query is trying to use navigation to an entity '{entityType}' which itself doesn't map to temporal table. Either map the entity to temporal table or use join manually to access it. - /// - public static string TemporalNavigationExpansionBetweenTemporalAndNonTemporal(object? entityType) - => string.Format( - GetString("TemporalNavigationExpansionBetweenTemporalAndNonTemporal", nameof(entityType)), - entityType); - - /// - /// Navigation expansion is only supported for '{operationName}' temporal operation. For other operations use join manually. + /// Property '{entityType}.{propertyName}' is mapped to the period column and must have ValueGenerated set to '{valueGeneratedValue}'. /// - public static string TemporalNavigationExpansionOnlySupportedForAsOf(object? operationName) + public static string TemporalPropertyMappedToPeriodColumnMustBeValueGeneratedOnAddOrUpdate(object? entityType, object? propertyName, object? valueGeneratedValue) => string.Format( - GetString("TemporalNavigationExpansionOnlySupportedForAsOf", nameof(operationName)), - operationName); + GetString("TemporalPropertyMappedToPeriodColumnMustBeValueGeneratedOnAddOrUpdate", nameof(entityType), nameof(propertyName), nameof(valueGeneratedValue)), + entityType, propertyName, valueGeneratedValue); /// /// Set operation can't be applied on entity '{entityType}' because temporal operations on both arguments don't match. @@ -337,6 +328,12 @@ public static string TemporalSetOperationOnMismatchedSources(object? entityType) GetString("TemporalSetOperationOnMismatchedSources", nameof(entityType)), entityType); + /// + /// An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure' to the 'UseSqlServer' call. + /// + public static string TransientExceptionDetected + => GetString("TransientExceptionDetected"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name)!; diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx index 0def370989f..cc45f62a28b 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx @@ -271,64 +271,49 @@ SQL Server sequences cannot be used to generate values for the property '{property}' on entity type '{entityType}' because the property type is '{propertyType}'. Sequences can only be used with integer properties. - - An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure' to the 'UseSqlServer' call. + + Entity type '{entityType}' mapped to temporal table does not contain the expected period property: '{propertyName}'. - - - Only root entity type should be marked as temporal. Entity type: '{entityType}'. + + Entity type '{entityType}' mapped to temporal table must have a period start and a period end property. - - - Temporal tables are only supported for entities using Table-Per-Hierarchy inheritance mapping. Entity type: '{entityType}'. + + Temporal query is trying to use navigation to an entity '{entityType}' which itself doesn't map to temporal table. Either map the entity to temporal table or use join manually to access it. + + + Navigation expansion is only supported for '{operationName}' temporal operation. For other operations use join manually. - Temporal tables are not supported for table splitting scenario. Table: '{table}'. - - - Entity type '{entityType}' mapped to temporal table must have a period start and a period end property. + + Only root entity type should be marked as temporal. Entity type: '{entityType}'. - - - Entity type '{entityType}' mapped to temporal table does not contain the expected period property: '{propertyName}'. + + Temporal tables are only supported for entities using Table-Per-Hierarchy inheritance mapping. Entity type: '{entityType}'. + + + Period property '{entityType}.{propertyName}' can't have a default value specified. - Period property '{entityType}.{propertyName}' must be a shadow property. - - - Period property '{entityType}.{propertyName}' must be non-nullable and of type '{dateTimeType}'. - - Period property '{entityType}.{propertyName}' must be mapped to a column of type '{columnType}'. - - - Period property '{entityType}.{propertyName}' can't have a default value specified. - - - - Property '{entityType}.{propertyName}' is mapped to the period column and must have ValueGenerated set to '{valueGeneratedValue}'. + + Period property '{entityType}.{propertyName}' must be non-nullable and of type '{dateTimeType}'. - Property '{entityType}.{propertyName}' is mapped to the period column and can't have default value specified. - - - Temporal query is trying to use navigation to an entity '{entityType}' which itself doesn't map to temporal table. Either map the entity to temporal table or use join manually to access it. - - - - Navigation expansion is only supported for '{operationName}' temporal operation. For other operations use join manually. + + Property '{entityType}.{propertyName}' is mapped to the period column and must have ValueGenerated set to '{valueGeneratedValue}'. - Set operation can't be applied on entity '{entityType}' because temporal operations on both arguments don't match. - + + An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure' to the 'UseSqlServer' call. + \ No newline at end of file diff --git a/src/EFCore.Sqlite.Core/Properties/SqliteStrings.Designer.cs b/src/EFCore.Sqlite.Core/Properties/SqliteStrings.Designer.cs index c42e826b3f7..0c4c6a120d0 100644 --- a/src/EFCore.Sqlite.Core/Properties/SqliteStrings.Designer.cs +++ b/src/EFCore.Sqlite.Core/Properties/SqliteStrings.Designer.cs @@ -1,9 +1,6 @@ // -using System; -using System.Reflection; using System.Resources; -using System.Threading; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.Extensions.Logging; diff --git a/src/EFCore.Sqlite.NTS/Properties/SqliteNTSStrings.Designer.cs b/src/EFCore.Sqlite.NTS/Properties/SqliteNTSStrings.Designer.cs index 1e2bae3ff6b..270f5a8c453 100644 --- a/src/EFCore.Sqlite.NTS/Properties/SqliteNTSStrings.Designer.cs +++ b/src/EFCore.Sqlite.NTS/Properties/SqliteNTSStrings.Designer.cs @@ -1,9 +1,6 @@ // -using System; -using System.Reflection; using System.Resources; -using JetBrains.Annotations; #nullable enable diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 8762199149f..a676e9a448e 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -1,9 +1,6 @@ // -using System; -using System.Reflection; using System.Resources; -using System.Threading; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.Extensions.Logging; @@ -1146,6 +1143,12 @@ public static string IncludeWithCycle(object? navigationName, object? inverseNav GetString("IncludeWithCycle", nameof(navigationName), nameof(inverseNavigationName)), navigationName, inverseNavigationName); + /// + /// Incompatible sources used for set operation. + /// + public static string IncompatibleSourcesForSetOperation + => GetString("IncompatibleSourcesForSetOperation"); + /// /// The entity type '{entityType}' is configured as derived from '{baseEntityType}', however according to the hierarchy of the corresponding CLR types it should derive from '{clrBaseEntityType}'. Configure '{entityType}' having either '{baseEntityType}' or 'null' as the base type. /// @@ -2550,12 +2553,6 @@ public static string ServiceProviderConfigRemoved(object? key) public static string SetOperationWithDifferentIncludesInOperands => GetString("SetOperationWithDifferentIncludesInOperands"); - /// - /// Incompatible sources used for set operation. - /// - public static string IncompatibleSourcesForSetOperation - => GetString("IncompatibleSourcesForSetOperation"); - /// /// The entity type '{entityType}' is in shadow state. A valid model requires all entity types to have a corresponding CLR type. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index aa4bbbd191f..cbc83190133 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -551,6 +551,9 @@ The Include path '{navigationName}->{inverseNavigationName}' results in a cycle. Cycles are not allowed in no-tracking queries; either use a tracking query or remove the cycle. + + Incompatible sources used for set operation. + The entity type '{entityType}' is configured as derived from '{baseEntityType}', however according to the hierarchy of the corresponding CLR types it should derive from '{clrBaseEntityType}'. Configure '{entityType}' having either '{baseEntityType}' or 'null' as the base type. @@ -1411,9 +1414,6 @@ Unable to translate set operation since both operands have different 'Include' operations. Consider having same 'Include' applied on both sides. - - Incompatible sources used for set operation. - The entity type '{entityType}' is in shadow state. A valid model requires all entity types to have a corresponding CLR type. Obsolete diff --git a/src/Microsoft.Data.Sqlite.Core/Properties/Resources.Designer.cs b/src/Microsoft.Data.Sqlite.Core/Properties/Resources.Designer.cs index 1ff29363ff4..9ea463d6fe8 100644 --- a/src/Microsoft.Data.Sqlite.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.Data.Sqlite.Core/Properties/Resources.Designer.cs @@ -1,6 +1,5 @@ // -using System.Reflection; using System.Resources; #nullable enable diff --git a/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs index 5bf10eddfa8..5f19ec038c0 100644 --- a/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs @@ -1,9 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Threading.Tasks; using Microsoft.Azure.Cosmos; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Conventions.Internal; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; @@ -127,10 +126,13 @@ public async Task Should_throw_if_specified_connection_mode_is_wrong() }); } - private DbContextOptions CreateOptions(CosmosTestStore testDatabase) - => Fixture.AddOptions(testDatabase.AddProviderOptions(new DbContextOptionsBuilder())) - .EnableDetailedErrors() - .Options; + private DbContextOptions CreateOptions(CosmosTestStore testDatabase, Action configure = null) + { + var builder = Fixture.AddOptions(testDatabase.AddProviderOptions(new DbContextOptionsBuilder())) + .EnableDetailedErrors(); + configure?.Invoke(builder); + return builder.Options; + } private class Customer { diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index 7f54c78efb2..0714176da40 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -232,14 +232,14 @@ public override void Clean(DbContext context) public override async Task CleanAsync(DbContext context) { var cosmosClientWrapper = context.GetService(); - var created = await cosmosClientWrapper.CreateDatabaseIfNotExistsAsync(); + var created = await cosmosClientWrapper.CreateDatabaseIfNotExistsAsync(null); try { if (!created) { var cosmosClient = context.Database.GetCosmosClient(); var database = cosmosClient.GetDatabase(Name); - var containerIterator = database.GetContainerQueryIterator(); + var containerIterator = database.GetContainerQueryIterator(); while (containerIterator.HasMoreResults) { foreach (var containerProperties in await containerIterator.ReadNextAsync()) diff --git a/test/EFCore.Cosmos.Tests/Infrastructure/CosmosModelValidatorTest.cs b/test/EFCore.Cosmos.Tests/Infrastructure/CosmosModelValidatorTest.cs index 82833590d29..5c6d799b434 100644 --- a/test/EFCore.Cosmos.Tests/Infrastructure/CosmosModelValidatorTest.cs +++ b/test/EFCore.Cosmos.Tests/Infrastructure/CosmosModelValidatorTest.cs @@ -90,7 +90,10 @@ public virtual void Detects_non_string_id_property() public virtual void Passes_on_valid_partition_keys() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().ToContainer("Orders").HasPartitionKey(c => c.PartitionId); + modelBuilder.Entity().ToContainer("Orders").HasPartitionKey(c => c.PartitionId) + .HasAnalyticalStoreTimeToLive(-1) + .HasDefaultTimeToLive(100) + .HasAutoscaleThroughput(200); modelBuilder.Entity().ToContainer("Orders").HasPartitionKey(o => o.PartitionId) .Property(o => o.PartitionId).HasConversion(); @@ -190,6 +193,54 @@ public virtual void Detects_partition_key_of_different_type() nameof(Customer.PartitionId), typeof(Order).Name, "int"), modelBuilder); } + [ConditionalFact] + public virtual void Detects_conflicting_analytical_ttl() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().ToContainer("Orders") + .HasAnalyticalStoreTimeToLive(-1); + modelBuilder.Entity().ToContainer("Orders") + .HasAnalyticalStoreTimeToLive(60); + + VerifyError(CosmosStrings.AnalyticalTTLMismatch(-1, typeof(Customer).Name, typeof(Order).Name, 60, "Orders"), modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_conflicting_default_ttl() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().ToContainer("Orders") + .HasDefaultTimeToLive(100); + modelBuilder.Entity().ToContainer("Orders") + .HasDefaultTimeToLive(60); + + VerifyError(CosmosStrings.DefaultTTLMismatch(100, typeof(Customer).Name, typeof(Order).Name, 60, "Orders"), modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_conflicting_throughput() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().ToContainer("Orders") + .HasAutoscaleThroughput(200); + modelBuilder.Entity().ToContainer("Orders") + .HasAutoscaleThroughput(60); + + VerifyError(CosmosStrings.ThroughputMismatch(200, typeof(Customer).Name, typeof(Order).Name, 60, "Orders"), modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_conflicting_throughput_type() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().ToContainer("Orders") + .HasManualThroughput(200); + modelBuilder.Entity().ToContainer("Orders") + .HasAutoscaleThroughput(200); + + VerifyError(CosmosStrings.ThroughputTypeMismatch(typeof(Customer).Name, typeof(Order).Name, "Orders"), modelBuilder); + } + [ConditionalFact] public virtual void Detects_properties_mapped_to_same_property() { diff --git a/tools/Resources.tt b/tools/Resources.tt index cc13eebc7ba..413f4fedce1 100644 --- a/tools/Resources.tt +++ b/tools/Resources.tt @@ -1,5 +1,4 @@ <#@ template hostspecific="true" #> -<#@ assembly name="EnvDTE" #> <#@ assembly name="Microsoft.VisualStudio.Interop" #> <#@ assembly name="System.Core" #> <#@ assembly name="System.Windows.Forms" #> @@ -16,14 +15,11 @@ #> // -using System; -using System.Reflection; using System.Resources; <# if (!model.NoDiagnostics) { #> -using System.Threading; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.Extensions.Logging; diff --git a/tools/SqliteResources.tt b/tools/SqliteResources.tt index 97e04ca8b08..3069b8db653 100644 --- a/tools/SqliteResources.tt +++ b/tools/SqliteResources.tt @@ -1,5 +1,5 @@ <#@ template hostspecific="true" #> -<#@ assembly name="EnvDTE" #> +<#@ assembly name="Microsoft.VisualStudio.Interop" #> <#@ assembly name="System.Core" #> <#@ assembly name="System.Windows.Forms" #> <#@ import namespace="System.Collections" #> @@ -14,7 +14,6 @@ #> // -using System.Reflection; using System.Resources; #nullable enable