diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index bf7f36a7b6b..11144e254e8 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -923,7 +923,7 @@ private void GenerateTableMapping( if (hasOverrides) { - GenerateOverrides("t", entityType, table!.Value, stringBuilder); + GeneratePropertyOverrides("t", entityType, table!.Value, stringBuilder); } } @@ -959,7 +959,7 @@ private void GenerateSplitTableMapping( using (stringBuilder.Indent()) { GenerateTriggers("t", entityType, table.Name, table.Schema, stringBuilder); - GenerateOverrides("t", entityType, table, stringBuilder); + GeneratePropertyOverrides("t", entityType, table, stringBuilder); GenerateEntityTypeMappingFragmentAnnotations("t", fragment, stringBuilder); } @@ -1023,7 +1023,7 @@ private void GenerateViewMapping(string entityTypeBuilderName, IEntityType entit using (stringBuilder.Indent()) { - GenerateOverrides("v", entityType, view!.Value, stringBuilder); + GeneratePropertyOverrides("v", entityType, view!.Value, stringBuilder); } stringBuilder.Append("}"); @@ -1055,7 +1055,7 @@ private void GenerateSplitViewMapping( using (stringBuilder.Indent()) { - GenerateOverrides("v", entityType, fragment.StoreObject, stringBuilder); + GeneratePropertyOverrides("v", entityType, fragment.StoreObject, stringBuilder); GenerateEntityTypeMappingFragmentAnnotations("v", fragment, stringBuilder); } @@ -1266,7 +1266,7 @@ protected virtual void GenerateTriggerAnnotations( /// The entity type. /// The store object identifier. /// The builder code is added to. - protected virtual void GenerateOverrides( + protected virtual void GeneratePropertyOverrides( string tableBuilderName, IEntityType entityType, StoreObjectIdentifier storeObject, @@ -1277,7 +1277,7 @@ protected virtual void GenerateOverrides( var overrides = property.FindOverrides(storeObject); if (overrides != null) { - GenerateOverride(tableBuilderName, overrides, stringBuilder); + GeneratePropertyOverride(tableBuilderName, overrides, stringBuilder); } } } @@ -1288,7 +1288,7 @@ protected virtual void GenerateOverrides( /// The name of the table builder variable. /// The overrides. /// The builder code is added to. - protected virtual void GenerateOverride( + protected virtual void GeneratePropertyOverride( string tableBuilderName, IRelationalPropertyOverrides overrides, IndentedStringBuilder stringBuilder) @@ -1301,7 +1301,7 @@ protected virtual void GenerateOverride( // Note that GenerateAnnotations below does the corresponding decrement stringBuilder.IncrementIndent(); - if (overrides.ColumnNameOverridden) + if (overrides.IsColumnNameOverridden) { stringBuilder .AppendLine() diff --git a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs index 42aaa626e22..b3b5753082a 100644 --- a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs @@ -31,6 +31,9 @@ public class AnnotationCodeGenerator : IAnnotationCodeGenerator RelationalAnnotationNames.Triggers, RelationalAnnotationNames.Sequences, RelationalAnnotationNames.DbFunctions, + RelationalAnnotationNames.DeleteStoredProcedure, + RelationalAnnotationNames.InsertStoredProcedure, + RelationalAnnotationNames.UpdateStoredProcedure, RelationalAnnotationNames.MappingFragments, RelationalAnnotationNames.RelationalOverrides }; diff --git a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs index 99f4681d8c7..1ea54b25765 100644 --- a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs @@ -503,7 +503,7 @@ private void Create( AppendLiteral(storeObject, mainBuilder, code); mainBuilder.AppendLine(",") - .Append(code.Literal(overrides.ColumnNameOverridden)).AppendLine(",") + .Append(code.Literal(overrides.IsColumnNameOverridden)).AppendLine(",") .Append(code.Literal(overrides.ColumnName)).AppendLine(");").DecrementIndent(); CreateAnnotations( diff --git a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs index 904163e6c40..d3ef709703b 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs @@ -97,7 +97,7 @@ private enum Id DuplicateColumnOrders, ForeignKeyTpcPrincipalWarning, TpcStoreGeneratedIdentityWarning, - KeyUnmappedProperties, + KeyPropertiesNotMappedToTable, // Update events BatchReadyForExecution = CoreEventId.RelationalBaseId + 700, @@ -825,8 +825,8 @@ private static EventId MakeValidationId(Id id) /// This event uses the payload when used with a . /// /// - public static readonly EventId KeyUnmappedProperties = - MakeValidationId(Id.KeyUnmappedProperties); + public static readonly EventId KeyPropertiesNotMappedToTable = + MakeValidationId(Id.KeyPropertiesNotMappedToTable); /// /// A foreign key specifies properties which don't map to the related tables. diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs index 93bf91d2cb6..e01d2271d76 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs @@ -2775,15 +2775,15 @@ private static string NamedIndexPropertiesMappedToNonOverlappingTables(EventDefi } /// - /// Logs the event. + /// Logs the event. /// /// The diagnostics logger to use. /// The foreign key. - public static void KeyUnmappedProperties( + public static void KeyPropertiesNotMappedToTable( this IDiagnosticsLogger diagnostics, IKey key) { - var definition = RelationalResources.LogKeyUnmappedProperties(diagnostics); + var definition = RelationalResources.LogKeyPropertiesNotMappedToTable(diagnostics); if (diagnostics.ShouldLog(definition)) { @@ -2798,14 +2798,14 @@ public static void KeyUnmappedProperties( { var eventData = new KeyEventData( definition, - KeyUnmappedProperties, + KeyPropertiesNotMappedToTable, key); diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); } } - private static string KeyUnmappedProperties(EventDefinitionBase definition, EventData payload) + private static string KeyPropertiesNotMappedToTable(EventDefinitionBase definition, EventData payload) { var d = (EventDefinition)definition; var p = (KeyEventData)payload; diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs index 56787f2dafe..5a928d4c42f 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs @@ -536,7 +536,7 @@ public abstract class RelationalLoggingDefinitions : LoggingDefinitions /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - public EventDefinitionBase? LogKeyUnmappedProperties; + public EventDefinitionBase? LogKeyPropertiesNotMappedToTable; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.UseSproc.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.UseSproc.cs new file mode 100644 index 00000000000..b07449295a9 --- /dev/null +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.UseSproc.cs @@ -0,0 +1,970 @@ +// 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.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore; + +/// +/// Relational database specific extension methods for . +/// +/// +/// See Modeling entity types and relationships for more information and examples. +/// +public static partial class RelationalEntityTypeBuilderExtensions +{ + /// + /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The builder for the entity type being configured. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder UpdateUsingStoredProcedure( + this EntityTypeBuilder entityTypeBuilder, + Action buildAction) + => UseStoredProcedure( + entityTypeBuilder, + null, + null, + StoreObjectType.UpdateStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder UpdateUsingStoredProcedure( + this EntityTypeBuilder entityTypeBuilder, + string name, + Action buildAction) + => UseStoredProcedure( + entityTypeBuilder, + name, + null, + StoreObjectType.UpdateStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// The schema of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder UpdateUsingStoredProcedure( + this EntityTypeBuilder entityTypeBuilder, + string name, + string? schema, + Action buildAction) + => UseStoredProcedure( + entityTypeBuilder, + name, + schema, + StoreObjectType.UpdateStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The entity type being configured. + /// The builder for the entity type being configured. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder UpdateUsingStoredProcedure( + this EntityTypeBuilder entityTypeBuilder, + Action> buildAction) + where TEntity : class + => UseStoredProcedure( + entityTypeBuilder, + null, + null, + StoreObjectType.UpdateStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The entity type being configured. + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder UpdateUsingStoredProcedure( + this EntityTypeBuilder entityTypeBuilder, + string name, + Action> buildAction) + where TEntity : class + => UseStoredProcedure( + entityTypeBuilder, + name, + null, + StoreObjectType.UpdateStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The entity type being configured. + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// The schema of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder UpdateUsingStoredProcedure( + this EntityTypeBuilder entityTypeBuilder, + string name, + string? schema, + Action> buildAction) + where TEntity : class + => UseStoredProcedure( + entityTypeBuilder, + name, + schema, + StoreObjectType.UpdateStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The builder for the entity type being configured. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder UpdateUsingStoredProcedure( + this OwnedNavigationBuilder ownedNavigationBuilder, + Action buildAction) + => UseStoredProcedure( + ownedNavigationBuilder, + null, + null, + StoreObjectType.UpdateStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder UpdateUsingStoredProcedure( + this OwnedNavigationBuilder ownedNavigationBuilder, + string name, + Action buildAction) + => UseStoredProcedure( + ownedNavigationBuilder, + name, + null, + StoreObjectType.UpdateStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// The schema of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder UpdateUsingStoredProcedure( + this OwnedNavigationBuilder ownedNavigationBuilder, + string name, + string? schema, + Action buildAction) + => UseStoredProcedure( + ownedNavigationBuilder, + name, + schema, + StoreObjectType.UpdateStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The entity type owning the relationship. + /// The dependent entity type of the relationship. + /// The builder for the entity type being configured. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder UpdateUsingStoredProcedure( + this OwnedNavigationBuilder ownedNavigationBuilder, + Action> buildAction) + where TOwnerEntity : class + where TDependentEntity : class + => UseStoredProcedure( + ownedNavigationBuilder, + null, + null, + StoreObjectType.UpdateStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The entity type owning the relationship. + /// The dependent entity type of the relationship. + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder UpdateUsingStoredProcedure( + this OwnedNavigationBuilder ownedNavigationBuilder, + string name, + Action> buildAction) + where TOwnerEntity : class + where TDependentEntity : class + => UseStoredProcedure( + ownedNavigationBuilder, + name, + null, + StoreObjectType.UpdateStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The entity type owning the relationship. + /// The dependent entity type of the relationship. + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// The schema of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder UpdateUsingStoredProcedure( + this OwnedNavigationBuilder ownedNavigationBuilder, + string name, + string? schema, + Action> buildAction) + where TOwnerEntity : class + where TDependentEntity : class + => UseStoredProcedure( + ownedNavigationBuilder, + name, + schema, + StoreObjectType.UpdateStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// + /// The builder for the entity type being configured. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The builder instance if the configuration was applied, otherwise. + /// + public static IConventionStoredProcedureBuilder? UpdateUsingStoredProcedure( + this IConventionEntityTypeBuilder entityTypeBuilder, + bool fromDataAnnotation = false) + => InternalStoredProcedureBuilder.HasStoredProcedure( + entityTypeBuilder.Metadata, StoreObjectType.UpdateStoredProcedure, fromDataAnnotation); + + /// + /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The builder for the entity type being configured. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder DeleteUsingStoredProcedure( + this EntityTypeBuilder entityTypeBuilder, + Action buildAction) + => UseStoredProcedure( + entityTypeBuilder, + null, + null, + StoreObjectType.DeleteStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder DeleteUsingStoredProcedure( + this EntityTypeBuilder entityTypeBuilder, + string name, + Action buildAction) + => UseStoredProcedure( + entityTypeBuilder, + name, + null, + StoreObjectType.DeleteStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// The schema of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder DeleteUsingStoredProcedure( + this EntityTypeBuilder entityTypeBuilder, + string name, + string? schema, + Action buildAction) + => UseStoredProcedure( + entityTypeBuilder, + name, + schema, + StoreObjectType.DeleteStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The entity type being configured. + /// The builder for the entity type being configured. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder DeleteUsingStoredProcedure( + this EntityTypeBuilder entityTypeBuilder, + Action> buildAction) + where TEntity : class + => UseStoredProcedure( + entityTypeBuilder, + null, + null, + StoreObjectType.DeleteStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The entity type being configured. + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder DeleteUsingStoredProcedure( + this EntityTypeBuilder entityTypeBuilder, + string name, + Action> buildAction) + where TEntity : class + => UseStoredProcedure( + entityTypeBuilder, + name, + null, + StoreObjectType.DeleteStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The entity type being configured. + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// The schema of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder DeleteUsingStoredProcedure( + this EntityTypeBuilder entityTypeBuilder, + string name, + string? schema, + Action> buildAction) + where TEntity : class + => UseStoredProcedure( + entityTypeBuilder, + name, + schema, + StoreObjectType.DeleteStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The builder for the entity type being configured. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder DeleteUsingStoredProcedure( + this OwnedNavigationBuilder ownedNavigationBuilder, + Action buildAction) + => UseStoredProcedure( + ownedNavigationBuilder, + null, + null, + StoreObjectType.DeleteStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder DeleteUsingStoredProcedure( + this OwnedNavigationBuilder ownedNavigationBuilder, + string name, + Action buildAction) + => UseStoredProcedure( + ownedNavigationBuilder, + name, + null, + StoreObjectType.DeleteStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// The schema of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder DeleteUsingStoredProcedure( + this OwnedNavigationBuilder ownedNavigationBuilder, + string name, + string? schema, + Action buildAction) + => UseStoredProcedure( + ownedNavigationBuilder, + name, + schema, + StoreObjectType.DeleteStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The entity type owning the relationship. + /// The dependent entity type of the relationship. + /// The builder for the entity type being configured. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder DeleteUsingStoredProcedure( + this OwnedNavigationBuilder ownedNavigationBuilder, + Action> buildAction) + where TOwnerEntity : class + where TDependentEntity : class + => UseStoredProcedure( + ownedNavigationBuilder, + null, + null, + StoreObjectType.DeleteStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The entity type owning the relationship. + /// The dependent entity type of the relationship. + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder DeleteUsingStoredProcedure( + this OwnedNavigationBuilder ownedNavigationBuilder, + string name, + Action> buildAction) + where TOwnerEntity : class + where TDependentEntity : class + => UseStoredProcedure( + ownedNavigationBuilder, + name, + null, + StoreObjectType.DeleteStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The entity type owning the relationship. + /// The dependent entity type of the relationship. + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// The schema of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder DeleteUsingStoredProcedure( + this OwnedNavigationBuilder ownedNavigationBuilder, + string name, + string? schema, + Action> buildAction) + where TOwnerEntity : class + where TDependentEntity : class + => UseStoredProcedure( + ownedNavigationBuilder, + name, + schema, + StoreObjectType.DeleteStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// + /// The builder for the entity type being configured. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The builder instance if the configuration was applied, otherwise. + /// + public static IConventionStoredProcedureBuilder? DeleteUsingStoredProcedure( + this IConventionEntityTypeBuilder entityTypeBuilder, + bool fromDataAnnotation = false) + => InternalStoredProcedureBuilder.HasStoredProcedure( + entityTypeBuilder.Metadata, StoreObjectType.DeleteStoredProcedure, fromDataAnnotation); + + /// + /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The builder for the entity type being configured. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder InsertUsingStoredProcedure( + this EntityTypeBuilder entityTypeBuilder, + Action buildAction) + => UseStoredProcedure( + entityTypeBuilder, + null, + null, + StoreObjectType.InsertStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder InsertUsingStoredProcedure( + this EntityTypeBuilder entityTypeBuilder, + string name, + Action buildAction) + => UseStoredProcedure( + entityTypeBuilder, + name, + null, + StoreObjectType.InsertStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// The schema of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder InsertUsingStoredProcedure( + this EntityTypeBuilder entityTypeBuilder, + string name, + string? schema, + Action buildAction) + => UseStoredProcedure( + entityTypeBuilder, + name, + schema, + StoreObjectType.InsertStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The entity type being configured. + /// The builder for the entity type being configured. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder InsertUsingStoredProcedure( + this EntityTypeBuilder entityTypeBuilder, + Action> buildAction) + where TEntity : class + => UseStoredProcedure( + entityTypeBuilder, + null, + null, + StoreObjectType.InsertStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The entity type being configured. + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder InsertUsingStoredProcedure( + this EntityTypeBuilder entityTypeBuilder, + string name, + Action> buildAction) + where TEntity : class + => UseStoredProcedure( + entityTypeBuilder, + name, + null, + StoreObjectType.InsertStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The entity type being configured. + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// The schema of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder InsertUsingStoredProcedure( + this EntityTypeBuilder entityTypeBuilder, + string name, + string? schema, + Action> buildAction) + where TEntity : class + => UseStoredProcedure( + entityTypeBuilder, + name, + schema, + StoreObjectType.InsertStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The builder for the entity type being configured. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder InsertUsingStoredProcedure( + this OwnedNavigationBuilder ownedNavigationBuilder, + Action buildAction) + => UseStoredProcedure( + ownedNavigationBuilder, + null, + null, + StoreObjectType.InsertStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder InsertUsingStoredProcedure( + this OwnedNavigationBuilder ownedNavigationBuilder, + string name, + Action buildAction) + => UseStoredProcedure( + ownedNavigationBuilder, + name, + null, + StoreObjectType.InsertStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// The schema of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder InsertUsingStoredProcedure( + this OwnedNavigationBuilder ownedNavigationBuilder, + string name, + string? schema, + Action buildAction) + => UseStoredProcedure( + ownedNavigationBuilder, + name, + schema, + StoreObjectType.InsertStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The entity type owning the relationship. + /// The dependent entity type of the relationship. + /// The builder for the entity type being configured. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder InsertUsingStoredProcedure( + this OwnedNavigationBuilder ownedNavigationBuilder, + Action> buildAction) + where TOwnerEntity : class + where TDependentEntity : class + => UseStoredProcedure( + ownedNavigationBuilder, + null, + null, + StoreObjectType.InsertStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The entity type owning the relationship. + /// The dependent entity type of the relationship. + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder InsertUsingStoredProcedure( + this OwnedNavigationBuilder ownedNavigationBuilder, + string name, + Action> buildAction) + where TOwnerEntity : class + where TDependentEntity : class + => UseStoredProcedure( + ownedNavigationBuilder, + name, + null, + StoreObjectType.InsertStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The entity type owning the relationship. + /// The dependent entity type of the relationship. + /// The builder for the entity type being configured. + /// The name of the stored procedure in the database. + /// The schema of the stored procedure in the database. + /// An action that performs configuration of the stored procedure. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder InsertUsingStoredProcedure( + this OwnedNavigationBuilder ownedNavigationBuilder, + string name, + string? schema, + Action> buildAction) + where TOwnerEntity : class + where TDependentEntity : class + => UseStoredProcedure( + ownedNavigationBuilder, + name, + schema, + StoreObjectType.InsertStoredProcedure, + buildAction); + + /// + /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// + /// The builder for the entity type being configured. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The builder instance if the configuration was applied, otherwise. + /// + public static IConventionStoredProcedureBuilder? InsertUsingStoredProcedure( + this IConventionEntityTypeBuilder entityTypeBuilder, + bool fromDataAnnotation = false) + => InternalStoredProcedureBuilder.HasStoredProcedure( + entityTypeBuilder.Metadata, StoreObjectType.InsertStoredProcedure, fromDataAnnotation); + + private static EntityTypeBuilder UseStoredProcedure( + EntityTypeBuilder entityTypeBuilder, + string? name, + string? schema, + StoreObjectType sprocType, + Action buildAction) + { + Check.NotNull(buildAction, nameof(buildAction)); + + var sprocBuilder = InternalStoredProcedureBuilder.HasStoredProcedure( + entityTypeBuilder.Metadata, sprocType, name, schema); + buildAction(new(sprocBuilder.Metadata, entityTypeBuilder)); + + return entityTypeBuilder; + } + + private static EntityTypeBuilder UseStoredProcedure( + EntityTypeBuilder entityTypeBuilder, + string? name, + string? schema, + StoreObjectType sprocType, + Action> buildAction) + where TEntity : class + { + Check.NotNull(buildAction, nameof(buildAction)); + + var sprocBuilder = InternalStoredProcedureBuilder.HasStoredProcedure( + entityTypeBuilder.Metadata, sprocType, name, schema); + buildAction(new(sprocBuilder.Metadata, entityTypeBuilder)); + + return entityTypeBuilder; + } + + private static OwnedNavigationBuilder UseStoredProcedure( + OwnedNavigationBuilder ownedNavigationBuilder, + string? name, + string? schema, + StoreObjectType sprocType, + Action buildAction) + { + Check.NotNull(buildAction, nameof(buildAction)); + + var sprocBuilder = InternalStoredProcedureBuilder.HasStoredProcedure( + ownedNavigationBuilder.OwnedEntityType, sprocType, name, schema); + buildAction(new(sprocBuilder.Metadata, ownedNavigationBuilder)); + + return ownedNavigationBuilder; + } + + private static OwnedNavigationBuilder UseStoredProcedure( + OwnedNavigationBuilder ownedNavigationBuilder, + string? name, + string? schema, + StoreObjectType sprocType, + Action> buildAction) + where TOwnerEntity : class + where TDependentEntity : class + { + Check.NotNull(buildAction, nameof(buildAction)); + + var sprocBuilder = InternalStoredProcedureBuilder.HasStoredProcedure( + ownedNavigationBuilder.OwnedEntityType, sprocType, name, schema); + buildAction(new(sprocBuilder.Metadata, ownedNavigationBuilder)); + + return ownedNavigationBuilder; + } +} diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs index c17b3127d68..c68b51e8a07 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs @@ -241,8 +241,8 @@ public static void SetSchema(this IMutableEntityType entityType, string? value) /// Returns the name of the view to which the entity type is mapped prepended by the schema /// or if not mapped to a view. /// - /// The entity type to get the table name for. - /// The name of the table to which the entity type is mapped prepended by the schema. + /// The entity type to get the view name for. + /// The name of the view to which the entity type is mapped prepended by the schema. public static string? GetSchemaQualifiedViewName(this IReadOnlyEntityType entityType) { var viewName = entityType.GetViewName(); @@ -506,8 +506,8 @@ public static void SetSqlQuery(this IMutableEntityType entityType, string? name) /// /// Returns the SQL string mappings. /// - /// The entity type to get the function mappings for. - /// The functions to which the entity type is mapped. + /// The entity type to get the SQL string mappings for. + /// The SQL string to which the entity type is mapped. public static IEnumerable GetSqlQueryMappings(this IEntityType entityType) => (IEnumerable?)entityType.FindRuntimeAnnotationValue( RelationalAnnotationNames.SqlQueryMappings) @@ -573,7 +573,248 @@ public static IEnumerable GetFunctionMappings(this IEntityType RelationalAnnotationNames.FunctionMappings) ?? Enumerable.Empty(); - #endregion Function mapping + #endregion + + #region SProc mapping + + /// + /// Returns the stored procedure to which the entity type is mapped for deletes + /// or if not mapped to a stored procedure. + /// + /// The entity type. + /// The stored procedure to which the entity type is mapped. + public static IReadOnlyStoredProcedure? GetDeleteStoredProcedure(this IReadOnlyEntityType entityType) + => StoredProcedure.FindStoredProcedure(entityType, StoreObjectType.DeleteStoredProcedure); + + /// + /// Returns the stored procedure to which the entity type is mapped for deletes + /// or if not mapped to a stored procedure. + /// + /// The entity type. + /// The stored procedure to which the entity type is mapped. + public static IMutableStoredProcedure? GetDeleteStoredProcedure(this IMutableEntityType entityType) + => StoredProcedure.FindStoredProcedure(entityType, StoreObjectType.DeleteStoredProcedure); + + /// + /// Returns the stored procedure to which the entity type is mapped for deletes + /// or if not mapped to a stored procedure. + /// + /// The entity type. + /// The stored procedure to which the entity type is mapped. + public static IConventionStoredProcedure? GetDeleteStoredProcedure(this IConventionEntityType entityType) + => StoredProcedure.FindStoredProcedure(entityType, StoreObjectType.DeleteStoredProcedure); + + /// + /// Returns the stored procedure to which the entity type is mapped for deletes + /// or if not mapped to a stored procedure. + /// + /// The entity type. + /// The stored procedure to which the entity type is mapped. + public static IStoredProcedure? GetDeleteStoredProcedure(this IEntityType entityType) + => StoredProcedure.FindStoredProcedure(entityType, StoreObjectType.DeleteStoredProcedure); + + /// + /// Maps the entity type to a stored procedure for deletes. + /// + /// The entity type. + /// The new stored procedure. + public static IMutableStoredProcedure SetDeleteStoredProcedure(this IMutableEntityType entityType) + => StoredProcedure.SetStoredProcedure(entityType, StoreObjectType.DeleteStoredProcedure); + + /// + /// Maps the entity type to a stored procedure for deletes. + /// + /// The entity type. + /// Indicates whether the configuration was specified using a data annotation. + /// The new stored procedure. + public static IConventionStoredProcedure? SetDeleteStoredProcedure( + this IConventionEntityType entityType, + bool fromDataAnnotation = false) + => StoredProcedure.SetStoredProcedure(entityType, StoreObjectType.DeleteStoredProcedure, fromDataAnnotation); + + /// + /// Removes the mapped delete stored procedure for this entity type. + /// + /// The entity type. + /// The removed stored procedure. + public static IMutableStoredProcedure? RemoveDeleteStoredProcedure(this IMutableEntityType entityType) + => StoredProcedure.RemoveStoredProcedure(entityType, StoreObjectType.DeleteStoredProcedure); + + /// + /// Removes the mapped delete stored procedure for this entity type. + /// + /// The entity type. + /// The removed stored procedure. + public static IConventionStoredProcedure? RemoveDeleteStoredProcedure(this IConventionEntityType entityType) + => StoredProcedure.RemoveStoredProcedure(entityType, StoreObjectType.DeleteStoredProcedure); + + /// + /// Gets the for the delete stored procedure. + /// + /// The entity type to find configuration source for. + /// The for the delete stored procedure. + public static ConfigurationSource? GetDeleteStoredProcedureConfigurationSource(this IConventionEntityType entityType) + => StoredProcedure.GetStoredProcedureConfigurationSource(entityType, StoreObjectType.DeleteStoredProcedure); + + /// + /// Returns the stored procedure to which the entity type is mapped for inserts + /// or if not mapped to a stored procedure. + /// + /// The entity type. + /// The stored procedure to which the entity type is mapped. + public static IReadOnlyStoredProcedure? GetInsertStoredProcedure(this IReadOnlyEntityType entityType) + => StoredProcedure.FindStoredProcedure(entityType, StoreObjectType.InsertStoredProcedure); + + /// + /// Returns the stored procedure to which the entity type is mapped for inserts + /// or if not mapped to a stored procedure. + /// + /// The entity type. + /// The stored procedure to which the entity type is mapped. + public static IMutableStoredProcedure? GetInsertStoredProcedure(this IMutableEntityType entityType) + => StoredProcedure.FindStoredProcedure(entityType, StoreObjectType.InsertStoredProcedure); + + /// + /// Returns the stored procedure to which the entity type is mapped for inserts + /// or if not mapped to a stored procedure. + /// + /// The entity type. + /// The stored procedure to which the entity type is mapped. + public static IConventionStoredProcedure? GetInsertStoredProcedure(this IConventionEntityType entityType) + => StoredProcedure.FindStoredProcedure(entityType, StoreObjectType.InsertStoredProcedure); + + /// + /// Returns the stored procedure to which the entity type is mapped for inserts + /// or if not mapped to a stored procedure. + /// + /// The entity type. + /// The stored procedure to which the entity type is mapped. + public static IStoredProcedure? GetInsertStoredProcedure(this IEntityType entityType) + => StoredProcedure.FindStoredProcedure(entityType, StoreObjectType.InsertStoredProcedure); + + /// + /// Maps the entity type to a stored procedure for inserts. + /// + /// The entity type. + /// The new stored procedure. + public static IMutableStoredProcedure SetInsertStoredProcedure(this IMutableEntityType entityType) + => StoredProcedure.SetStoredProcedure(entityType, StoreObjectType.InsertStoredProcedure); + + /// + /// Maps the entity type to a stored procedure for inserts. + /// + /// The entity type. + /// Indicates whether the configuration was specified using a data annotation. + /// The new stored procedure. + public static IConventionStoredProcedure? SetInsertStoredProcedure( + this IConventionEntityType entityType, + bool fromDataAnnotation = false) + => StoredProcedure.SetStoredProcedure(entityType, StoreObjectType.InsertStoredProcedure, fromDataAnnotation); + + /// + /// Removes the mapped insert stored procedure for this entity type. + /// + /// The entity type. + /// The removed stored procedure. + public static IMutableStoredProcedure? RemoveInsertStoredProcedure(this IMutableEntityType entityType) + => StoredProcedure.RemoveStoredProcedure(entityType, StoreObjectType.InsertStoredProcedure); + + /// + /// Removes the mapped insert stored procedure for this entity type. + /// + /// The entity type. + /// The removed stored procedure. + public static IConventionStoredProcedure? RemoveInsertStoredProcedure(this IConventionEntityType entityType) + => StoredProcedure.RemoveStoredProcedure(entityType, StoreObjectType.InsertStoredProcedure); + + /// + /// Gets the for the insert stored procedure. + /// + /// The entity type. + /// The for the insert stored procedure. + public static ConfigurationSource? GetInsertStoredProcedureConfigurationSource(this IConventionEntityType entityType) + => StoredProcedure.GetStoredProcedureConfigurationSource(entityType, StoreObjectType.InsertStoredProcedure); + + /// + /// Returns the stored procedure to which the entity type is mapped for updates + /// or if not mapped to a stored procedure. + /// + /// The entity type. + /// The stored procedure to which the entity type is mapped. + public static IReadOnlyStoredProcedure? GetUpdateStoredProcedure(this IReadOnlyEntityType entityType) + => StoredProcedure.FindStoredProcedure(entityType, StoreObjectType.UpdateStoredProcedure); + + /// + /// Returns the stored procedure to which the entity type is mapped for updates + /// or if not mapped to a stored procedure. + /// + /// The entity type. + /// The stored procedure to which the entity type is mapped. + public static IMutableStoredProcedure? GetUpdateStoredProcedure(this IMutableEntityType entityType) + => StoredProcedure.FindStoredProcedure(entityType, StoreObjectType.UpdateStoredProcedure); + + /// + /// Returns the stored procedure to which the entity type is mapped for updates + /// or if not mapped to a stored procedure. + /// + /// The entity type. + /// The stored procedure to which the entity type is mapped. + public static IConventionStoredProcedure? GetUpdateStoredProcedure(this IConventionEntityType entityType) + => StoredProcedure.FindStoredProcedure(entityType, StoreObjectType.UpdateStoredProcedure); + + /// + /// Returns the stored procedure to which the entity type is mapped for updates + /// or if not mapped to a stored procedure. + /// + /// The entity type. + /// The stored procedure to which the entity type is mapped. + public static IStoredProcedure? GetUpdateStoredProcedure(this IEntityType entityType) + => StoredProcedure.FindStoredProcedure(entityType, StoreObjectType.UpdateStoredProcedure); + + /// + /// Maps the entity type to a stored procedure for updates. + /// + /// The entity type. + /// The new stored procedure. + public static IMutableStoredProcedure SetUpdateStoredProcedure(this IMutableEntityType entityType) + => StoredProcedure.SetStoredProcedure(entityType, StoreObjectType.UpdateStoredProcedure); + + /// + /// Maps the entity type to a stored procedure for updates. + /// + /// The entity type. + /// Indicates whether the configuration was specified using a data annotation. + /// The new stored procedure. + public static IConventionStoredProcedure? SetUpdateStoredProcedure( + this IConventionEntityType entityType, + bool fromDataAnnotation = false) + => StoredProcedure.SetStoredProcedure(entityType, StoreObjectType.UpdateStoredProcedure, fromDataAnnotation); + + /// + /// Removes the mapped update stored procedure for this entity type. + /// + /// The entity type. + /// The removed stored procedure. + public static IMutableStoredProcedure? RemoveUpdateStoredProcedure(this IMutableEntityType entityType) + => StoredProcedure.RemoveStoredProcedure(entityType, StoreObjectType.UpdateStoredProcedure); + + /// + /// Removes the mapped update stored procedure for this entity type. + /// + /// The entity type. + /// The removed stored procedure. + public static IConventionStoredProcedure? RemoveUpdateStoredProcedure(this IConventionEntityType entityType) + => StoredProcedure.RemoveStoredProcedure(entityType, StoreObjectType.UpdateStoredProcedure); + + /// + /// Gets the for the update stored procedure. + /// + /// The entity type to find configuration source for. + /// The for the update stored procedure. + public static ConfigurationSource? GetUpdateStoredProcedureConfigurationSource(this IConventionEntityType entityType) + => StoredProcedure.GetStoredProcedureConfigurationSource(entityType, StoreObjectType.UpdateStoredProcedure); + + #endregion #region Check constraint @@ -828,6 +1069,8 @@ public static void SetComment(this IMutableEntityType entityType, string? commen #endregion Comment + #region Mapping Fragments + /// /// /// Returns all configured entity type mapping fragments. @@ -1097,6 +1340,10 @@ public static IConventionEntityTypeMappingFragment GetOrCreateMappingFragment( in StoreObjectIdentifier storeObject) => EntityTypeMappingFragment.Remove((IMutableEntityType)entityType, storeObject); + #endregion + + #region Table sharing + /// /// Gets the foreign keys for the given entity type that point to other entity types /// sharing the same table-like store object. @@ -1169,6 +1416,8 @@ public static IEnumerable FindRowInternalForeignKeys( // ReSharper disable once RedundantCast => ((IReadOnlyEntityType)entityType).FindRowInternalForeignKeys(storeObject).Cast(); + #endregion + #region IsTableExcludedFromMigrations /// diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs index d17904d8d4e..921b60e5cc7 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs @@ -52,7 +52,7 @@ public static string GetColumnName(this IReadOnlyProperty property) public static string? GetColumnName(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) { var overrides = property.FindOverrides(storeObject); - if (overrides?.ColumnNameOverridden == true) + if (overrides?.IsColumnNameOverridden == true) { return overrides.ColumnName; } diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index d8bb74f0d5a..53b5bd3d93a 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -53,6 +53,7 @@ public override void Validate(IModel model, IDiagnosticsLogger + /// Validates the mapping/configuration of stored procedures in the model. + /// + /// The model to validate. + /// The logger to use. + protected virtual void ValidateStoredProcedures( + IModel model, + IDiagnosticsLogger logger) + { + var storedProcedures = new Dictionary>(); + foreach (var entityType in model.GetEntityTypes()) + { + var mappingStrategy = entityType.GetMappingStrategy() ?? RelationalAnnotationNames.TphMappingStrategy; + var deleteStoredProcedure = entityType.GetDeleteStoredProcedure(); + if (deleteStoredProcedure != null) + { + AddSproc(StoreObjectType.DeleteStoredProcedure, entityType, storedProcedures); + ValidateSproc(deleteStoredProcedure, mappingStrategy); + } + + var insertStoredProcedure = entityType.GetInsertStoredProcedure(); + if (insertStoredProcedure != null) + { + AddSproc(StoreObjectType.InsertStoredProcedure, entityType, storedProcedures); + ValidateSproc(insertStoredProcedure, mappingStrategy); + } + + var updateStoredProcedure = entityType.GetUpdateStoredProcedure(); + if (updateStoredProcedure != null) + { + AddSproc(StoreObjectType.UpdateStoredProcedure, entityType, storedProcedures); + ValidateSproc(updateStoredProcedure, mappingStrategy); + } + } + + foreach (var (sproc, mappedTypes) in storedProcedures) + { + foreach (var mappedType in mappedTypes) + { + if (mappedTypes[0].GetRootType() != mappedType.GetRootType()) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureTableSharing( + mappedTypes[0].DisplayName(), + mappedType.DisplayName(), + sproc.DisplayName())); + } + } + } + + static void AddSproc( + StoreObjectType storedProcedureType, + IEntityType entityType, + Dictionary> storedProcedures) + { + var sprocId = StoreObjectIdentifier.Create(entityType, storedProcedureType); + if (sprocId == null) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureNoName( + entityType.DisplayName(), storedProcedureType)); + } + + if (!storedProcedures.TryGetValue(sprocId.Value, out var mappedTypes)) + { + mappedTypes = new List(); + storedProcedures[sprocId.Value] = mappedTypes; + } + + mappedTypes.Add(entityType); + } + } + + private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy) + { + var entityType = sproc.EntityType; + var storeObjectIdentifier = ((StoredProcedure)sproc).CreateIdentifier()!.Value; + + var primaryKey = entityType.FindPrimaryKey(); + if (primaryKey == null) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureKeyless( + entityType.DisplayName(), storeObjectIdentifier.DisplayName())); + } + + var properties = entityType.GetDeclaredProperties().ToDictionary(p => p.Name); + if (mappingStrategy == RelationalAnnotationNames.TphMappingStrategy) + { + if (entityType.BaseType != null) + { + return; + } + + foreach (var property in entityType.GetDerivedProperties()) + { + properties.Add(property.Name, property); + } + } + else if (mappingStrategy == RelationalAnnotationNames.TpcMappingStrategy) + { + if (entityType.BaseType != null) + { + foreach (var property in entityType.BaseType.GetProperties()) + { + properties.Add(property.Name, property); + } + } + } + else if (mappingStrategy == RelationalAnnotationNames.TptMappingStrategy) + { + if (entityType.BaseType != null) + { + foreach (var property in primaryKey.Properties) + { + properties.Add(property.Name, property); + } + } + } + + foreach (var resultColumn in sproc.ResultColumns) + { + if (!properties!.TryGetValue(resultColumn, out var property)) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureResultColumnNotFound( + resultColumn, entityType.DisplayName(), storeObjectIdentifier.DisplayName())); + } + + switch (storeObjectIdentifier.StoreObjectType) + { + case StoreObjectType.InsertStoredProcedure: + if ((property.ValueGenerated & ValueGenerated.OnAdd) == 0) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureResultColumnNotGenerated( + entityType.DisplayName(), resultColumn, storeObjectIdentifier.DisplayName())); + } + + break; + case StoreObjectType.DeleteStoredProcedure: + throw new InvalidOperationException( + RelationalStrings.StoredProcedureResultColumnDelete( + entityType.DisplayName(), resultColumn, storeObjectIdentifier.DisplayName())); + case StoreObjectType.UpdateStoredProcedure: + if ((property.ValueGenerated & ValueGenerated.OnUpdate) == 0) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureResultColumnNotGenerated( + entityType.DisplayName(), resultColumn, storeObjectIdentifier.DisplayName())); + } + + break; + } + } + + foreach (var parameter in sproc.Parameters) + { + if (!properties!.TryGetAndRemove(parameter, out IProperty property)) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureParameterNotFound( + parameter, entityType.DisplayName(), storeObjectIdentifier.DisplayName())); + } + + if (storeObjectIdentifier.StoreObjectType == StoreObjectType.DeleteStoredProcedure + && !property.IsPrimaryKey() + && !property.IsConcurrencyToken) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureDeleteNonKeyProperty( + entityType.DisplayName(), parameter, storeObjectIdentifier.DisplayName())); + } + } + + foreach (var resultColumn in sproc.ResultColumns) + { + properties!.Remove(resultColumn); + } + + if (properties != null + && properties.Count > 0) + { + foreach (var property in properties.Values.ToList()) + { + switch (storeObjectIdentifier.StoreObjectType) + { + case StoreObjectType.InsertStoredProcedure: + if ((property.ValueGenerated & ValueGenerated.OnAdd) == 0 + && property.GetBeforeSaveBehavior() != PropertySaveBehavior.Save) + { + properties.Remove(property.Name); + } + + break; + case StoreObjectType.DeleteStoredProcedure: + if (!property.IsPrimaryKey() + && !property.IsConcurrencyToken) + { + properties.Remove(property.Name); + } + + break; + case StoreObjectType.UpdateStoredProcedure: + if (!property.IsPrimaryKey() + && !property.IsConcurrencyToken + && (property.ValueGenerated & ValueGenerated.OnUpdate) == 0 + && property.GetAfterSaveBehavior() != PropertySaveBehavior.Save) + { + properties.Remove(property.Name); + } + + break; + } + } + + if (properties.Count > 0) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedurePropertiesNotMapped( + entityType.DisplayName(), + storeObjectIdentifier.DisplayName(), + properties.Values.Format(false))); + } + } + } + /// /// Validates the mapping/configuration of properties in the model. /// @@ -1306,7 +1534,11 @@ protected override void ValidateInheritanceMapping( ValidateMappingStrategy(entityType, mappingStrategy); var storeObject = entityType.GetSchemaQualifiedTableName() ?? entityType.GetSchemaQualifiedViewName() - ?? entityType.GetFunctionName(); + ?? entityType.GetFunctionName() + ?? entityType.GetSqlQuery() + ?? entityType.GetInsertStoredProcedure()?.GetSchemaQualifiedName() + ?? entityType.GetDeleteStoredProcedure()?.GetSchemaQualifiedName() + ?? entityType.GetUpdateStoredProcedure()?.GetSchemaQualifiedName(); if (mappingStrategy == RelationalAnnotationNames.TpcMappingStrategy && !entityType.ClrType.IsInstantiable() && storeObject != null) @@ -1343,8 +1575,13 @@ protected override void ValidateInheritanceMapping( RelationalStrings.NonTphMappingStrategy(mappingStrategy, entityType.DisplayName())); } - ValidateTphMapping(entityType, forTables: false); - ValidateTphMapping(entityType, forTables: true); + ValidateTphMapping(entityType, StoreObjectType.Table); + ValidateTphMapping(entityType, StoreObjectType.View); + ValidateTphMapping(entityType, StoreObjectType.Function); + ValidateTphMapping(entityType, StoreObjectType.InsertStoredProcedure); + ValidateTphMapping(entityType, StoreObjectType.DeleteStoredProcedure); + ValidateTphMapping(entityType, StoreObjectType.UpdateStoredProcedure); + ValidateDiscriminatorValues(entityType); } else @@ -1365,8 +1602,11 @@ protected override void ValidateInheritanceMapping( RelationalStrings.KeylessMappingStrategy(mappingStrategy ?? RelationalAnnotationNames.TptMappingStrategy, entityType.DisplayName())); } - ValidateNonTphMapping(entityType, forTables: false); - ValidateNonTphMapping(entityType, forTables: true); + ValidateNonTphMapping(entityType, StoreObjectType.Table); + ValidateNonTphMapping(entityType, StoreObjectType.View); + ValidateNonTphMapping(entityType, StoreObjectType.InsertStoredProcedure); + ValidateNonTphMapping(entityType, StoreObjectType.DeleteStoredProcedure); + ValidateNonTphMapping(entityType, StoreObjectType.UpdateStoredProcedure); var derivedTypes = entityType.GetDerivedTypesInclusive().ToList(); var discriminatorValues = new Dictionary(); @@ -1376,6 +1616,7 @@ protected override void ValidateInheritanceMapping( { continue; } + var discriminatorValue = derivedType.GetDiscriminatorValue(); if (discriminatorValue is not string valueString) { @@ -1414,32 +1655,41 @@ protected virtual void ValidateMappingStrategy(IEntityType entityType, string? m }; } - private static void ValidateNonTphMapping(IEntityType rootEntityType, bool forTables) + private static void ValidateNonTphMapping(IEntityType rootEntityType, StoreObjectType storeObjectType) { var isTpc = rootEntityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy; - var derivedTypes = new Dictionary<(string, string?), IEntityType>(); + var derivedTypes = new Dictionary(); foreach (var entityType in rootEntityType.GetDerivedTypesInclusive()) { - var name = forTables ? entityType.GetTableName() : entityType.GetViewName(); - if (name == null) + var storeObject = StoreObjectIdentifier.Create(entityType, storeObjectType); + if (storeObject == null) { continue; } - var schema = forTables ? entityType.GetSchema() : entityType.GetViewSchema(); - if (derivedTypes.TryGetValue((name, schema), out var otherType)) + if (derivedTypes.TryGetValue(storeObject.Value, out var otherType)) { - throw new InvalidOperationException( - forTables - ? RelationalStrings.NonTphTableClash( - entityType.DisplayName(), otherType.DisplayName(), entityType.GetSchemaQualifiedTableName()) - : RelationalStrings.NonTphViewClash( - entityType.DisplayName(), otherType.DisplayName(), entityType.GetSchemaQualifiedViewName())); + switch (storeObjectType) + { + case StoreObjectType.Table: + throw new InvalidOperationException( + RelationalStrings.NonTphTableClash( + entityType.DisplayName(), otherType.DisplayName(), storeObject.Value.DisplayName())); + case StoreObjectType.View: + throw new InvalidOperationException( + RelationalStrings.NonTphViewClash( + entityType.DisplayName(), otherType.DisplayName(), storeObject.Value.DisplayName())); + case StoreObjectType.InsertStoredProcedure: + case StoreObjectType.DeleteStoredProcedure: + case StoreObjectType.UpdateStoredProcedure: + throw new InvalidOperationException( + RelationalStrings.NonTphStoredProcedureClash( + entityType.DisplayName(), otherType.DisplayName(), storeObject.Value.DisplayName())); + } } if (isTpc) { - var storeObject = StoreObjectIdentifier.Create(entityType, forTables ? StoreObjectType.Table : StoreObjectType.View)!; var rowInternalFk = entityType.FindDeclaredReferencingRowInternalForeignKeys(storeObject.Value) .FirstOrDefault(); if (rowInternalFk != null @@ -1452,10 +1702,10 @@ private static void ValidateNonTphMapping(IEntityType rootEntityType, bool forTa } } - derivedTypes[(name, schema)] = entityType; + derivedTypes[storeObject.Value] = entityType; } - var rootStoreObject = StoreObjectIdentifier.Create(rootEntityType, forTables ? StoreObjectType.Table : StoreObjectType.View); + var rootStoreObject = StoreObjectIdentifier.Create(rootEntityType, storeObjectType); if (rootStoreObject == null) { return; @@ -1466,47 +1716,69 @@ private static void ValidateNonTphMapping(IEntityType rootEntityType, bool forTa && rootEntityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy) { var derivedTypePair = derivedTypes.First(kv => kv.Value != rootEntityType); - var (derivedName, derivedSchema) = derivedTypePair.Key; throw new InvalidOperationException(RelationalStrings.TpcTableSharingDependent( rootEntityType.DisplayName(), rootStoreObject.Value.DisplayName(), derivedTypePair.Value.DisplayName(), - derivedSchema == null ? derivedName : $"{derivedSchema}.{derivedName}")); + derivedTypePair.Key.DisplayName())); } } - private static void ValidateTphMapping(IEntityType rootEntityType, bool forTables) + private static void ValidateTphMapping(IEntityType rootEntityType, StoreObjectType storeObjectType) { - string? firstName = null; - string? firstSchema = null; - IEntityType? firstType = null; - foreach (var entityType in rootEntityType.GetDerivedTypesInclusive()) + var isSproc = storeObjectType == StoreObjectType.DeleteStoredProcedure + || storeObjectType == StoreObjectType.InsertStoredProcedure + || storeObjectType == StoreObjectType.UpdateStoredProcedure; + var rootSproc = isSproc ? StoredProcedure.GetDeclaredStoredProcedure(rootEntityType, storeObjectType) : null; + var rootId = StoreObjectIdentifier.Create(rootEntityType, storeObjectType); + foreach (var entityType in rootEntityType.GetDerivedTypes()) { - var name = forTables ? entityType.GetTableName() : entityType.GetViewName(); - if (name == null) + var entityId = StoreObjectIdentifier.Create(entityType, storeObjectType); + if (entityId == null) { continue; } - if (firstType == null) + if (rootId == entityId) { - firstType = entityType; - firstName = forTables ? firstType.GetTableName() : firstType.GetViewName(); - firstSchema = forTables ? firstType.GetSchema() : firstType.GetViewSchema(); + if (rootSproc != null) + { + var sproc = StoredProcedure.GetDeclaredStoredProcedure(entityType, storeObjectType); + if (sproc != null + && sproc != rootSproc) + { + throw new InvalidOperationException(RelationalStrings.StoredProcedureTphDuplicate( + entityType.DisplayName(), rootEntityType.DisplayName(), rootId?.DisplayName())); + } + } + continue; } - var schema = forTables ? entityType.GetSchema() : entityType.GetViewSchema(); - if (name != firstName || schema != firstSchema) + switch (storeObjectType) { - throw new InvalidOperationException( - forTables - ? RelationalStrings.TphTableMismatch( - entityType.DisplayName(), entityType.GetSchemaQualifiedTableName(), - firstType.DisplayName(), firstType.GetSchemaQualifiedTableName()) - : RelationalStrings.TphViewMismatch( - entityType.DisplayName(), entityType.GetSchemaQualifiedViewName(), - firstType.DisplayName(), firstType.GetSchemaQualifiedViewName())); + case StoreObjectType.Table: + throw new InvalidOperationException( + RelationalStrings.TphTableMismatch( + entityType.DisplayName(), entityId.Value.DisplayName(), + rootEntityType.DisplayName(), rootId?.DisplayName())); + case StoreObjectType.View: + throw new InvalidOperationException( + RelationalStrings.TphViewMismatch( + entityType.DisplayName(), entityId.Value.DisplayName(), + rootEntityType.DisplayName(), rootId?.DisplayName())); + case StoreObjectType.Function: + throw new InvalidOperationException( + RelationalStrings.TphDbFunctionMismatch( + entityType.DisplayName(), entityId.Value.DisplayName(), + rootEntityType.DisplayName(), rootId?.DisplayName())); + case StoreObjectType.InsertStoredProcedure: + case StoreObjectType.DeleteStoredProcedure: + case StoreObjectType.UpdateStoredProcedure: + throw new InvalidOperationException( + RelationalStrings.TphStoredProcedureMismatch( + entityType.DisplayName(), entityId.Value.DisplayName(), + rootEntityType.DisplayName(), rootId?.DisplayName())); } } } @@ -1701,6 +1973,13 @@ protected virtual void ValidatePropertyOverrides( RelationalStrings.FunctionOverrideMismatch( entityType.DisplayName() + "." + property.Name, storeObjectOverride.StoreObject.DisplayName())); + case StoreObjectType.InsertStoredProcedure: + case StoreObjectType.DeleteStoredProcedure: + case StoreObjectType.UpdateStoredProcedure: + throw new InvalidOperationException( + RelationalStrings.StoredProcedureOverrideMismatch( + entityType.DisplayName() + "." + property.Name, + storeObjectOverride.StoreObject.DisplayName())); default: throw new NotSupportedException(storeObject.StoreObjectType.ToString()); } diff --git a/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs b/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs index 7a2c4097c23..8901db37e9f 100644 --- a/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs @@ -105,4 +105,34 @@ public virtual DbFunctionBuilder HasTranslation(Func + /// Returns an object that can be used to configure a parameter with the given name. + /// If no parameter with the given name exists, then a new parameter will be added. + /// + /// + /// See Database functions for more information and examples. + /// + /// The parameter name. + /// An action that performs configuration of the parameter. + /// The builder to use for further parameter configuration. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual DbFunctionBuilder HasParameter(string name, Action buildAction) + => (DbFunctionBuilder)base.HasParameter(name, buildAction); + + /// + /// Adds or updates an annotation on the database function. If an annotation with the key specified in + /// already exists, its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual DbFunctionBuilder HasAnnotation(string annotation, object? value) + { + Check.NotEmpty(annotation, nameof(annotation)); + + Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); + + return this; + } } diff --git a/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilderBase.cs b/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilderBase.cs index 3e07be59518..ffaa6126514 100644 --- a/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilderBase.cs +++ b/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilderBase.cs @@ -107,6 +107,23 @@ public virtual DbFunctionBuilderBase IsBuiltIn(bool builtIn = true) public virtual DbFunctionParameterBuilder HasParameter(string name) => new(Builder.HasParameter(name, ConfigurationSource.Explicit).Metadata); + /// + /// Returns an object that can be used to configure a parameter with the given name. + /// If no parameter with the given name exists, then a new parameter will be added. + /// + /// + /// See Database functions for more information and examples. + /// + /// The parameter name. + /// An action that performs configuration of the parameter. + /// The builder to use for further parameter configuration. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual DbFunctionBuilderBase HasParameter(string name, Action buildAction) + { + buildAction(HasParameter(name)); + return this; + } + #region Hidden System.Object members /// diff --git a/src/EFCore.Relational/Metadata/Builders/IConventionStoredProcedureBuilder.cs b/src/EFCore.Relational/Metadata/Builders/IConventionStoredProcedureBuilder.cs new file mode 100644 index 00000000000..e120adc1c51 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Builders/IConventionStoredProcedureBuilder.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders; + +/// +/// Provides a simple API for configuring a . +/// +/// +/// See Model building conventions for more information and examples. +/// +public interface IConventionStoredProcedureBuilder : IConventionAnnotatableBuilder +{ + /// + /// The function being configured. + /// + new IConventionStoredProcedure Metadata { get; } + + /// + /// Sets the name of the stored procedure. + /// + /// The name of the stored procedure in the database. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionStoredProcedureBuilder? HasName(string? name, bool fromDataAnnotation = false); + + /// + /// Sets the name and schema of the stored procedure. + /// + /// The name of the stored procedure in the database. + /// The schema of the stored procedure in the database. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionStoredProcedureBuilder? HasName(string? name, string? schema, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given name can be set for the stored procedure. + /// + /// The name of the stored procedure in the database. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given name can be set for the stored procedure. + bool CanSetName(string? name, bool fromDataAnnotation = false); + + /// + /// Sets the schema of the stored procedure. + /// + /// The schema of the stored procedure in the database. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionStoredProcedureBuilder? HasSchema(string? schema, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given schema can be set for the stored procedure. + /// + /// The schema of the stored procedure in the database. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given schema can be set for the database function. + bool CanSetSchema(string? schema, bool fromDataAnnotation = false); + + /// + /// Configures a new parameter if no parameter mapped to the given property exists. + /// + /// The property name. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionStoredProcedureBuilder? HasParameter(string propertyName, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether a parameter mapped to the given property can be used for stored procedure. + /// + /// The property name. + /// Indicates whether the configuration was specified using a data annotation. + /// if the parameter can be used for the stored procedure. + bool CanHaveParameter(string propertyName, bool fromDataAnnotation = false); + + /// + /// Configures a new column of the result for this stored procedure. This is used for database generated columns. + /// + /// The property name. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionStoredProcedureBuilder? HasResultColumn(string propertyName, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether a column of the result mapped to the given property can be used for stored procedure. + /// + /// The property name. + /// Indicates whether the configuration was specified using a data annotation. + /// if the column of the result can be used for the stored procedure. + bool CanHaveResultColumn(string propertyName, bool fromDataAnnotation = false); + + /// + /// Prevents automatically creating a transaction when executing this stored procedure. + /// + /// A value indicating whether the automatic transactions should be prevented. + /// Indicates whether the configuration was specified using a data annotation. + /// The same builder instance so that multiple configuration calls can be chained. + IConventionStoredProcedureBuilder? SuppressTransactions(bool suppress, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the transaction suppression can be set for stored procedure. + /// + /// A value indicating whether the automatic transactions should be prevented. + /// Indicates whether the configuration was specified using a data annotation. + /// if the column of the result can be used for the stored procedure. + bool CanSetSuppressTransactions(bool suppress, bool fromDataAnnotation = false); +} diff --git a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder.cs b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder.cs new file mode 100644 index 00000000000..9de2c4a730d --- /dev/null +++ b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder.cs @@ -0,0 +1,163 @@ +// 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.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders; + +/// +/// Provides a simple API for configuring a that an entity type is mapped to. +/// +public class OwnedNavigationStoredProcedureBuilder : + IInfrastructure, IInfrastructure +{ + /// + /// 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. + /// + [EntityFrameworkInternal] + public OwnedNavigationStoredProcedureBuilder( + IMutableStoredProcedure sproc, + OwnedNavigationBuilder ownedNavigationBuilder) + { + Builder = ((StoredProcedure)sproc).Builder; + OwnedNavigationBuilder = ownedNavigationBuilder; + } + + private OwnedNavigationBuilder OwnedNavigationBuilder { get; } + + /// + /// 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. + /// + [EntityFrameworkInternal] + protected virtual InternalStoredProcedureBuilder Builder { [DebuggerStepThrough] get; } + + /// + IConventionStoredProcedureBuilder IInfrastructure.Instance + { + [DebuggerStepThrough] + get => Builder; + } + + /// + /// The stored procedure being configured. + /// + public virtual IMutableStoredProcedure Metadata + => Builder.Metadata; + + /// + /// Configures a new parameter if no parameter mapped to the given property exists. + /// + /// The property name. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationStoredProcedureBuilder HasParameter(string propertyName) + { + Builder.HasParameter(propertyName, ConfigurationSource.Explicit); + return this; + } + + /// + /// Configures a new parameter if no parameter mapped to the given property exists. + /// + /// The parameter name. + /// An action that performs configuration of the parameter. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationStoredProcedureBuilder HasParameter(string propertyName, Action buildAction) + { + Builder.HasParameter(propertyName, ConfigurationSource.Explicit); + buildAction(new(((StoredProcedure)Metadata).CreateIdentifier()!.Value, CreatePropertyBuilder(propertyName))); + return this; + } + + /// + /// 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. + /// + [EntityFrameworkInternal] + protected virtual PropertyBuilder CreatePropertyBuilder(string propertyName) + { + var entityType = OwnedNavigationBuilder.OwnedEntityType; + var property = entityType.FindProperty(propertyName); + if (property == null) + { + throw new InvalidOperationException(CoreStrings.PropertyNotFound(propertyName, entityType.DisplayName())); + } + + return OwnedNavigationBuilder.Property(property.ClrType, propertyName); + } + + /// + /// 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. + /// + [EntityFrameworkInternal] + protected virtual PropertyBuilder CreatePropertyBuilder( + Expression> propertyExpression) + { + var memberInfo = propertyExpression.GetMemberAccess(); + return OwnedNavigationBuilder.Property(memberInfo.GetMemberType(), memberInfo.Name); + } + + /// + /// Configures a new column of the result for this stored procedure. This is used for database generated columns. + /// + /// The property name. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationStoredProcedureBuilder HasResultColumn(string propertyName) + { + Builder.HasResultColumn(propertyName, ConfigurationSource.Explicit); + return this; + } + + /// + /// Configures a new column of the result for this stored procedure. This is used for database generated columns. + /// + /// The property name. + /// An action that performs configuration of the column. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationStoredProcedureBuilder HasResultColumn( + string propertyName, Action buildAction) + { + Builder.HasResultColumn(propertyName, ConfigurationSource.Explicit); + buildAction(new(((StoredProcedure)Metadata).CreateIdentifier()!.Value, CreatePropertyBuilder(propertyName))); + return this; + } + + /// + /// Prevents automatically creating a transaction when executing this stored procedure. + /// + /// A value indicating whether the automatic transactions should be prevented. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationStoredProcedureBuilder SuppressTransactions(bool suppress = true) + { + Builder.SuppressTransactions(suppress, ConfigurationSource.Explicit); + return this; + } + + /// + /// Adds or updates an annotation on the stored procedure. If an annotation with the key specified in + /// already exists, its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationStoredProcedureBuilder HasAnnotation(string annotation, object? value) + { + Check.NotEmpty(annotation, nameof(annotation)); + + Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); + + return this; + } + + OwnedNavigationBuilder IInfrastructure.Instance => OwnedNavigationBuilder; +} diff --git a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder``.cs b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder``.cs new file mode 100644 index 00000000000..1db167f39b9 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder``.cs @@ -0,0 +1,159 @@ +// 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.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders; + +/// +/// Provides a simple API for configuring a that an entity type is mapped to. +/// +/// The entity type owning the relationship. +/// The dependent entity type of the relationship. +public class OwnedNavigationStoredProcedureBuilder : + OwnedNavigationStoredProcedureBuilder, IInfrastructure> + where TOwnerEntity : class + where TDependentEntity : class +{ + /// + /// 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. + /// + [EntityFrameworkInternal] + public OwnedNavigationStoredProcedureBuilder( + IMutableStoredProcedure sproc, + OwnedNavigationBuilder ownedNavigationBuilder) + : base(sproc, ownedNavigationBuilder) + { + } + + private OwnedNavigationBuilder OwnedNavigationBuilder + => (OwnedNavigationBuilder)((IInfrastructure)this) + .GetInfrastructure(); + + /// + /// Configures a new parameter if no parameter mapped to the given property exists. + /// + /// The property name. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual OwnedNavigationStoredProcedureBuilder HasParameter(string propertyName) + => (OwnedNavigationStoredProcedureBuilder)base.HasParameter(propertyName); + + /// + /// Configures a new parameter if no parameter mapped to the given property exists. + /// + /// The parameter name. + /// An action that performs configuration of the parameter. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual OwnedNavigationStoredProcedureBuilder HasParameter( + string propertyName, Action buildAction) + => (OwnedNavigationStoredProcedureBuilder)base.HasParameter(propertyName, buildAction); + + /// + /// Configures a new parameter if no parameter mapped to the given property exists. + /// + /// The property type. + /// + /// A lambda expression representing the property to be configured (blog => blog.Url). + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationStoredProcedureBuilder HasParameter( + Expression> propertyExpression) + { + Builder.HasParameter(propertyExpression, ConfigurationSource.Explicit); + return this; + } + + /// + /// Configures a new parameter if no parameter mapped to the given property exists. + /// + /// The property type. + /// + /// A lambda expression representing the property to be configured (blog => blog.Url). + /// + /// An action that performs configuration of the parameter. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationStoredProcedureBuilder HasParameter( + Expression> propertyExpression, + Action buildAction) + { + Builder.HasParameter(propertyExpression, ConfigurationSource.Explicit); + buildAction(new(((StoredProcedure)Metadata).CreateIdentifier()!.Value, CreatePropertyBuilder(propertyExpression))); + return this; + } + + /// + /// Configures a new column of the result for this stored procedure. This is used for database generated columns. + /// + /// The property name. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual OwnedNavigationStoredProcedureBuilder HasResultColumn(string propertyName) + => (OwnedNavigationStoredProcedureBuilder)base.HasResultColumn(propertyName); + + /// + /// Configures a new column of the result for this stored procedure. This is used for database generated columns. + /// + /// The property name. + /// An action that performs configuration of the column. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual OwnedNavigationStoredProcedureBuilder HasResultColumn( + string propertyName, Action buildAction) + => (OwnedNavigationStoredProcedureBuilder)base.HasResultColumn(propertyName, buildAction); + + /// + /// Configures a new column of the result for this stored procedure. This is used for database generated columns. + /// + /// The property type. + /// + /// A lambda expression representing the property to be configured (blog => blog.Url). + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression) + { + Builder.HasResultColumn(propertyExpression, ConfigurationSource.Explicit); + return this; + } + + /// + /// Configures a new column of the result for this stored procedure. This is used for database generated columns. + /// + /// The property type. + /// + /// A lambda expression representing the property to be configured (blog => blog.Url). + /// + /// An action that performs configuration of the column. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression, + Action buildAction) + { + Builder.HasResultColumn(propertyExpression, ConfigurationSource.Explicit); + buildAction(new(((StoredProcedure)Metadata).CreateIdentifier()!.Value, CreatePropertyBuilder(propertyExpression))); + return this; + } + + /// + /// Prevents automatically creating a transaction when executing this stored procedure. + /// + /// A value indicating whether the automatic transactions should be prevented. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual OwnedNavigationStoredProcedureBuilder SuppressTransactions(bool suppress = true) + => (OwnedNavigationStoredProcedureBuilder)base.SuppressTransactions(suppress); + + /// + /// Adds or updates an annotation on the stored procedure. If an annotation with the key specified in + /// already exists, its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual OwnedNavigationStoredProcedureBuilder HasAnnotation( + string annotation, object? value) + => (OwnedNavigationStoredProcedureBuilder)base.HasAnnotation(annotation, value); + + OwnedNavigationBuilder IInfrastructure>.Instance + => OwnedNavigationBuilder; +} diff --git a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationTableValuedFunctionBuilder.cs b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationTableValuedFunctionBuilder.cs index 1dd69f117c4..d3da28e53db 100644 --- a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationTableValuedFunctionBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationTableValuedFunctionBuilder.cs @@ -58,5 +58,35 @@ public OwnedNavigationTableValuedFunctionBuilder( public new virtual OwnedNavigationTableValuedFunctionBuilder IsBuiltIn(bool builtIn = true) => (OwnedNavigationTableValuedFunctionBuilder)base.IsBuiltIn(builtIn); + /// + /// Returns an object that can be used to configure a parameter with the given name. + /// If no parameter with the given name exists, then a new parameter will be added. + /// + /// + /// See Database functions for more information and examples. + /// + /// The parameter name. + /// An action that performs configuration of the parameter. + /// The builder to use for further parameter configuration. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual OwnedNavigationTableValuedFunctionBuilder HasParameter(string name, Action buildAction) + => (OwnedNavigationTableValuedFunctionBuilder)base.HasParameter(name, buildAction); + + /// + /// Adds or updates an annotation on the database function. If an annotation with the key specified in + /// already exists, its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationTableValuedFunctionBuilder HasAnnotation(string annotation, object? value) + { + Check.NotEmpty(annotation, nameof(annotation)); + + Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); + + return this; + } + OwnedNavigationBuilder IInfrastructure.Instance => OwnedNavigationBuilder; } diff --git a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationTableValuedFunctionBuilder``.cs b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationTableValuedFunctionBuilder``.cs index 47a2041eb0a..1967edb424c 100644 --- a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationTableValuedFunctionBuilder``.cs +++ b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationTableValuedFunctionBuilder``.cs @@ -65,6 +65,30 @@ private OwnedNavigationBuilder OwnedNavigationBu public new virtual OwnedNavigationTableValuedFunctionBuilder IsBuiltIn(bool builtIn = true) => (OwnedNavigationTableValuedFunctionBuilder)base.IsBuiltIn(builtIn); + /// + /// Returns an object that can be used to configure a parameter with the given name. + /// If no parameter with the given name exists, then a new parameter will be added. + /// + /// + /// See Database functions for more information and examples. + /// + /// The parameter name. + /// An action that performs configuration of the parameter. + /// The builder to use for further parameter configuration. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual OwnedNavigationTableValuedFunctionBuilder HasParameter(string name, Action buildAction) + => (OwnedNavigationTableValuedFunctionBuilder)base.HasParameter(name, buildAction); + + /// + /// Adds or updates an annotation on the database function. If an annotation with the key specified in + /// already exists, its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual OwnedNavigationTableValuedFunctionBuilder HasAnnotation(string annotation, object? value) + => (OwnedNavigationTableValuedFunctionBuilder)base.HasAnnotation(annotation, value); + OwnedNavigationBuilder IInfrastructure>.Instance => OwnedNavigationBuilder; } diff --git a/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder.cs b/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder.cs new file mode 100644 index 00000000000..880ad9aef4c --- /dev/null +++ b/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder.cs @@ -0,0 +1,179 @@ +// 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.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders; + +/// +/// Provides a simple API for configuring a that an entity type is mapped to. +/// +public class StoredProcedureBuilder : IInfrastructure, IInfrastructure +{ + /// + /// 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. + /// + [EntityFrameworkInternal] + public StoredProcedureBuilder(IMutableStoredProcedure sproc, EntityTypeBuilder entityTypeBuilder) + { + Builder = ((StoredProcedure)sproc).Builder; + EntityTypeBuilder = entityTypeBuilder; + } + + private EntityTypeBuilder EntityTypeBuilder { get; } + + /// + /// 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. + /// + [EntityFrameworkInternal] + protected virtual InternalStoredProcedureBuilder Builder { [DebuggerStepThrough] get; } + + /// + IConventionStoredProcedureBuilder IInfrastructure.Instance + { + [DebuggerStepThrough] + get => Builder; + } + + /// + /// The stored procedure being configured. + /// + public virtual IMutableStoredProcedure Metadata + => Builder.Metadata; + + /// + /// Configures a new parameter if no parameter mapped to the given property exists. + /// + /// The property name. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasParameter(string propertyName) + { + Builder.HasParameter(propertyName, ConfigurationSource.Explicit); + return this; + } + + /// + /// Configures a new parameter if no parameter mapped to the given property exists. + /// + /// The parameter name. + /// An action that performs configuration of the parameter. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasParameter( + string propertyName, Action buildAction) + { + Builder.HasParameter(propertyName, ConfigurationSource.Explicit); + buildAction(new(((StoredProcedure)Metadata).CreateIdentifier()!.Value, CreatePropertyBuilder(propertyName))); + return this; + } + + /// + /// 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. + /// + [EntityFrameworkInternal] + protected virtual PropertyBuilder CreatePropertyBuilder(string propertyName) + { + var entityType = EntityTypeBuilder.Metadata; + var property = entityType.FindProperty(propertyName); + if (property == null) + { + property = entityType.GetDerivedTypes().SelectMany(et => et.GetDeclaredProperties()) + .FirstOrDefault(p => p.Name == propertyName); + } + + if (property == null) + { + throw new InvalidOperationException(CoreStrings.PropertyNotFound(propertyName, entityType.DisplayName())); + } + +#pragma warning disable EF1001 // Internal EF Core API usage. + return new ModelBuilder(entityType.Model) +#pragma warning restore EF1001 // Internal EF Core API usage. + .Entity(property.DeclaringEntityType.Name) + .Property(property.ClrType, propertyName); + } + + /// + /// 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. + /// + [EntityFrameworkInternal] + protected virtual PropertyBuilder CreatePropertyBuilder( + Expression> propertyExpression) + where TDerivedEntity : class + { + var memberInfo = propertyExpression.GetMemberAccess(); + var entityType = EntityTypeBuilder.Metadata; + var entityTypeBuilder = entityType.ClrType == typeof(TDerivedEntity) + ? EntityTypeBuilder +#pragma warning disable EF1001 // Internal EF Core API usage. + : new ModelBuilder(entityType.Model).Entity(typeof(TDerivedEntity)); +#pragma warning restore EF1001 // Internal EF Core API usage. + + return entityTypeBuilder.Property(memberInfo.GetMemberType(), memberInfo.Name); + } + + /// + /// Configures a new column of the result for this stored procedure. This is used for database generated columns. + /// + /// The property name. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasResultColumn(string propertyName) + { + Builder.HasResultColumn(propertyName, ConfigurationSource.Explicit); + return this; + } + + /// + /// Configures a new column of the result for this stored procedure. This is used for database generated columns. + /// + /// The property name. + /// An action that performs configuration of the column. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasResultColumn( + string propertyName, Action buildAction) + { + Builder.HasResultColumn(propertyName, ConfigurationSource.Explicit); + buildAction(new(((StoredProcedure)Metadata).CreateIdentifier()!.Value, CreatePropertyBuilder(propertyName))); + return this; + } + + /// + /// Prevents automatically creating a transaction when executing this stored procedure. + /// + /// A value indicating whether the automatic transactions should be prevented. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder SuppressTransactions(bool suppress = true) + { + Builder.SuppressTransactions(suppress, ConfigurationSource.Explicit); + return this; + } + + /// + /// Adds or updates an annotation on the stored procedure. If an annotation with the key specified in + /// already exists, its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasAnnotation(string annotation, object? value) + { + Check.NotEmpty(annotation, nameof(annotation)); + + Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); + + return this; + } + + EntityTypeBuilder IInfrastructure.Instance => EntityTypeBuilder; +} diff --git a/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder`.cs b/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder`.cs new file mode 100644 index 00000000000..c918f6ef75d --- /dev/null +++ b/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder`.cs @@ -0,0 +1,211 @@ +// 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.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders; + +/// +/// Provides a simple API for configuring a that an entity type is mapped to. +/// +/// The entity type being configured. +public class StoredProcedureBuilder : StoredProcedureBuilder, IInfrastructure> + where TEntity : class +{ + /// + /// 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. + /// + [EntityFrameworkInternal] + public StoredProcedureBuilder(IMutableStoredProcedure sproc, EntityTypeBuilder entityTypeBuilder) + : base(sproc, entityTypeBuilder) + { + } + + private EntityTypeBuilder EntityTypeBuilder + => (EntityTypeBuilder)((IInfrastructure)this).Instance; + + /// + /// Configures a new parameter if no parameter mapped to the given property exists. + /// + /// The property name. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual StoredProcedureBuilder HasParameter(string propertyName) + => (StoredProcedureBuilder)base.HasParameter(propertyName); + + /// + /// Configures a new parameter if no parameter mapped to the given property exists. + /// + /// The parameter name. + /// An action that performs configuration of the parameter. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual StoredProcedureBuilder HasParameter( + string propertyName, Action buildAction) + => (StoredProcedureBuilder)base.HasParameter(propertyName, buildAction); + + /// + /// Configures a new parameter if no parameter mapped to the given property exists. + /// + /// The property type. + /// + /// A lambda expression representing the property to be configured (blog => blog.Url). + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasParameter( + Expression> propertyExpression) + => HasParameter(propertyExpression); + + /// + /// Configures a new parameter if no parameter mapped to the given property exists. + /// + /// The same or a derived entity type. + /// The property type. + /// + /// A lambda expression representing the property to be configured (blog => blog.Url). + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasParameter( + Expression> propertyExpression) + where TDerivedEntity : class, TEntity + { + Builder.HasParameter(propertyExpression, ConfigurationSource.Explicit); + return this; + } + + /// + /// Configures a new parameter if no parameter mapped to the given property exists. + /// + /// The property type. + /// + /// A lambda expression representing the property to be configured (blog => blog.Url). + /// + /// An action that performs configuration of the parameter. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasParameter( + Expression> propertyExpression, + Action buildAction) + => HasParameter(propertyExpression, buildAction); + + /// + /// Configures a new parameter if no parameter mapped to the given property exists. + /// + /// The same or a derived entity type. + /// The property type. + /// + /// A lambda expression representing the property to be configured (blog => blog.Url). + /// + /// An action that performs configuration of the parameter. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasParameter( + Expression> propertyExpression, + Action buildAction) + where TDerivedEntity : class, TEntity + { + Builder.HasParameter(propertyExpression, ConfigurationSource.Explicit); + buildAction(new(((StoredProcedure)Metadata).CreateIdentifier()!.Value, CreatePropertyBuilder(propertyExpression))); + return this; + } + + /// + /// Configures a new column of the result for this stored procedure. This is used for database generated columns. + /// + /// The property name. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual StoredProcedureBuilder HasResultColumn(string propertyName) + => (StoredProcedureBuilder)base.HasResultColumn(propertyName); + + /// + /// Configures a new column of the result for this stored procedure. This is used for database generated columns. + /// + /// The property name. + /// An action that performs configuration of the column. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual StoredProcedureBuilder HasResultColumn( + string propertyName, Action buildAction) + => (StoredProcedureBuilder)base.HasResultColumn(propertyName, buildAction); + + /// + /// Configures a new column of the result for this stored procedure. This is used for database generated columns. + /// + /// The property type. + /// + /// A lambda expression representing the property to be configured (blog => blog.Url). + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasResultColumn( + Expression> propertyExpression) + => HasResultColumn(propertyExpression); + + /// + /// Configures a new column of the result for this stored procedure. This is used for database generated columns. + /// + /// The same or a derived entity type. + /// The property type. + /// + /// A lambda expression representing the property to be configured (blog => blog.Url). + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasResultColumn( + Expression> propertyExpression) + where TDerivedEntity : class, TEntity + { + Builder.HasResultColumn(propertyExpression, ConfigurationSource.Explicit); + return this; + } + + /// + /// Configures a new column of the result for this stored procedure. This is used for database generated columns. + /// + /// The property type. + /// + /// A lambda expression representing the property to be configured (blog => blog.Url). + /// + /// An action that performs configuration of the column. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasResultColumn( + Expression> propertyExpression, + Action buildAction) + => HasResultColumn(propertyExpression, buildAction); + + /// + /// Configures a new column of the result for this stored procedure. This is used for database generated columns. + /// + /// The same or a derived entity type. + /// The property type. + /// + /// A lambda expression representing the property to be configured (blog => blog.Url). + /// + /// An action that performs configuration of the column. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasResultColumn( + Expression> propertyExpression, + Action buildAction) + where TDerivedEntity : class, TEntity + { + Builder.HasResultColumn(propertyExpression, ConfigurationSource.Explicit); + buildAction(new(((StoredProcedure)Metadata).CreateIdentifier()!.Value, CreatePropertyBuilder(propertyExpression))); + return this; + } + + /// + /// Prevents automatically creating a transaction when executing this stored procedure. + /// + /// A value indicating whether the automatic transactions should be prevented. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual StoredProcedureBuilder SuppressTransactions(bool suppress = true) + => (StoredProcedureBuilder)base.SuppressTransactions(suppress); + + /// + /// Adds or updates an annotation on the stored procedure. If an annotation with the key specified in + /// already exists, its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual StoredProcedureBuilder HasAnnotation(string annotation, object? value) + => (StoredProcedureBuilder)base.HasAnnotation(annotation, value); + + EntityTypeBuilder IInfrastructure>.Instance => EntityTypeBuilder; +} diff --git a/src/EFCore.Relational/Metadata/Builders/StoredProcedureParameterBuilder.cs b/src/EFCore.Relational/Metadata/Builders/StoredProcedureParameterBuilder.cs new file mode 100644 index 00000000000..19c0702076a --- /dev/null +++ b/src/EFCore.Relational/Metadata/Builders/StoredProcedureParameterBuilder.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders; + +/// +/// Provides a simple API for configuring a parameter. +/// +/// +/// Instances of this class are returned from methods when using the API +/// and it is not designed to be directly constructed in your application code. +/// +public class StoredProcedureParameterBuilder : IInfrastructure +{ + /// + /// 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. + /// + [EntityFrameworkInternal] + public StoredProcedureParameterBuilder(in StoreObjectIdentifier storeObject, PropertyBuilder propertyBuilder) + { + Check.DebugAssert(storeObject.StoreObjectType == StoreObjectType.InsertStoredProcedure + || storeObject.StoreObjectType == StoreObjectType.DeleteStoredProcedure + || storeObject.StoreObjectType == StoreObjectType.UpdateStoredProcedure, + "StoreObjectType should be StoredProcedure, not " + storeObject.StoreObjectType); + + InternalOverrides = RelationalPropertyOverrides.GetOrCreate( + propertyBuilder.Metadata, storeObject, ConfigurationSource.Explicit); + PropertyBuilder = propertyBuilder; + } + + /// + /// The stored procedure-specific overrides being configured. + /// + public virtual IMutableRelationalPropertyOverrides Overrides => InternalOverrides; + + /// + /// 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. + /// + [EntityFrameworkInternal] + protected virtual RelationalPropertyOverrides InternalOverrides { get; } + + private PropertyBuilder PropertyBuilder { get; } + + /// + /// Sets the name of the stored procedure parameter. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The store type of the function parameter in the database. + /// The same builder instance so that further configuration calls can be chained. + public virtual StoredProcedureParameterBuilder HasName(string? name) + { + Check.NullButNotEmpty(name, nameof(name)); + + InternalOverrides.SetColumnName(name, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Adds or updates an annotation on the property for a specific stored procedure. + /// If an annotation with the key specified in + /// already exists, its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureParameterBuilder HasAnnotation(string annotation, object? value) + { + Check.NotEmpty(annotation, nameof(annotation)); + + InternalOverrides.Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); + + return this; + } + + PropertyBuilder IInfrastructure.Instance => PropertyBuilder; + + #region Hidden System.Object members + + /// + /// Returns a string that represents the current object. + /// + /// A string that represents the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + public override string? ToString() + => base.ToString(); + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// if the specified object is equal to the current object; otherwise, . + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectEqualsIsObjectEquals + public override bool Equals(object? obj) + => base.Equals(obj); + + /// + /// Serves as the default hash function. + /// + /// A hash code for the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode + public override int GetHashCode() + => base.GetHashCode(); + + #endregion +} diff --git a/src/EFCore.Relational/Metadata/Builders/StoredProcedureResultColumnBuilder.cs b/src/EFCore.Relational/Metadata/Builders/StoredProcedureResultColumnBuilder.cs new file mode 100644 index 00000000000..b83fc3da30e --- /dev/null +++ b/src/EFCore.Relational/Metadata/Builders/StoredProcedureResultColumnBuilder.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders; + +/// +/// Provides a simple API for configuring a result column. +/// +/// +/// Instances of this class are returned from methods when using the API +/// and it is not designed to be directly constructed in your application code. +/// +public class StoredProcedureResultColumnBuilder : IInfrastructure +{ + /// + /// 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. + /// + [EntityFrameworkInternal] + public StoredProcedureResultColumnBuilder(in StoreObjectIdentifier storeObject, PropertyBuilder propertyBuilder) + { + Check.DebugAssert(storeObject.StoreObjectType == StoreObjectType.InsertStoredProcedure + || storeObject.StoreObjectType == StoreObjectType.DeleteStoredProcedure + || storeObject.StoreObjectType == StoreObjectType.UpdateStoredProcedure, + "StoreObjectType should be StoredProcedure, not " + storeObject.StoreObjectType); + + InternalOverrides = RelationalPropertyOverrides.GetOrCreate( + propertyBuilder.Metadata, storeObject, ConfigurationSource.Explicit); + PropertyBuilder = propertyBuilder; + } + + /// + /// The stored procedure-specific overrides being configured. + /// + public virtual IMutableRelationalPropertyOverrides Overrides => InternalOverrides; + + /// + /// 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. + /// + [EntityFrameworkInternal] + protected virtual RelationalPropertyOverrides InternalOverrides { get; } + + private PropertyBuilder PropertyBuilder { get; } + + /// + /// Sets the name of the stored procedure result column. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The store type of the function parameter in the database. + /// The same builder instance so that further configuration calls can be chained. + public virtual StoredProcedureResultColumnBuilder HasName(string? name) + { + Check.NullButNotEmpty(name, nameof(name)); + + InternalOverrides.SetColumnName(name, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Adds or updates an annotation on the property for a specific stored procedure. + /// If an annotation with the key specified in + /// already exists, its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureResultColumnBuilder HasAnnotation(string annotation, object? value) + { + Check.NotEmpty(annotation, nameof(annotation)); + + InternalOverrides.Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); + + return this; + } + + PropertyBuilder IInfrastructure.Instance => PropertyBuilder; + + #region Hidden System.Object members + + /// + /// Returns a string that represents the current object. + /// + /// A string that represents the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + public override string? ToString() + => base.ToString(); + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// if the specified object is equal to the current object; otherwise, . + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectEqualsIsObjectEquals + public override bool Equals(object? obj) + => base.Equals(obj); + + /// + /// Serves as the default hash function. + /// + /// A hash code for the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode + public override int GetHashCode() + => base.GetHashCode(); + + #endregion +} diff --git a/src/EFCore.Relational/Metadata/Builders/TableValuedFunctionBuilder.cs b/src/EFCore.Relational/Metadata/Builders/TableValuedFunctionBuilder.cs index 502431374d7..b441ba64b50 100644 --- a/src/EFCore.Relational/Metadata/Builders/TableValuedFunctionBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/TableValuedFunctionBuilder.cs @@ -56,5 +56,35 @@ public TableValuedFunctionBuilder(IMutableDbFunction function, EntityTypeBuilder public new virtual TableValuedFunctionBuilder IsBuiltIn(bool builtIn = true) => (TableValuedFunctionBuilder)base.IsBuiltIn(builtIn); + /// + /// Returns an object that can be used to configure a parameter with the given name. + /// If no parameter with the given name exists, then a new parameter will be added. + /// + /// + /// See Database functions for more information and examples. + /// + /// The parameter name. + /// An action that performs configuration of the parameter. + /// The builder to use for further parameter configuration. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual TableValuedFunctionBuilder HasParameter(string name, Action buildAction) + => (TableValuedFunctionBuilder)base.HasParameter(name, buildAction); + + /// + /// Adds or updates an annotation on the database function. If an annotation with the key specified in + /// already exists, its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual TableValuedFunctionBuilder HasAnnotation(string annotation, object? value) + { + Check.NotEmpty(annotation, nameof(annotation)); + + Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); + + return this; + } + EntityTypeBuilder IInfrastructure.Instance => EntityTypeBuilder; } diff --git a/src/EFCore.Relational/Metadata/Builders/TableValuedFunctionBuilder`.cs b/src/EFCore.Relational/Metadata/Builders/TableValuedFunctionBuilder`.cs index 013f8633fcb..22dd17ec6c3 100644 --- a/src/EFCore.Relational/Metadata/Builders/TableValuedFunctionBuilder`.cs +++ b/src/EFCore.Relational/Metadata/Builders/TableValuedFunctionBuilder`.cs @@ -58,5 +58,29 @@ private EntityTypeBuilder EntityTypeBuilder public new virtual TableValuedFunctionBuilder IsBuiltIn(bool builtIn = true) => (TableValuedFunctionBuilder)base.IsBuiltIn(builtIn); + /// + /// Returns an object that can be used to configure a parameter with the given name. + /// If no parameter with the given name exists, then a new parameter will be added. + /// + /// + /// See Database functions for more information and examples. + /// + /// The parameter name. + /// An action that performs configuration of the parameter. + /// The builder to use for further parameter configuration. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual TableValuedFunctionBuilder HasParameter(string name, Action buildAction) + => (TableValuedFunctionBuilder)base.HasParameter(name, buildAction); + + /// + /// Adds or updates an annotation on the database function. If an annotation with the key specified in + /// already exists, its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual TableValuedFunctionBuilder HasAnnotation(string annotation, object? value) + => (TableValuedFunctionBuilder)base.HasAnnotation(annotation, value); + EntityTypeBuilder IInfrastructure>.Instance => EntityTypeBuilder; } diff --git a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs index e3812030b50..267884d06b0 100644 --- a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs +++ b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs @@ -75,6 +75,7 @@ public override ConventionSet CreateConventionSet() conventionSet.EntityTypeAddedConventions.Add(entitySplittingConvention); conventionSet.EntityTypeAddedConventions.Add(checkConstraintConvention); conventionSet.EntityTypeAddedConventions.Add(triggerConvention); + conventionSet.EntityTypeAddedConventions.Add(new StoredProcedureConvention(Dependencies, RelationalDependencies)); ValueGenerationConvention valueGenerationConvention = new RelationalValueGenerationConvention(Dependencies, RelationalDependencies); diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs index 2f59b82bf21..2f145d18fad 100644 --- a/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs @@ -129,7 +129,7 @@ protected override void ProcessEntityTypeAnnotations( annotations.Remove(RelationalAnnotationNames.CheckConstraints); annotations.Remove(RelationalAnnotationNames.Comment); annotations.Remove(RelationalAnnotationNames.IsTableExcludedFromMigrations); - + // These need to be set explicitly to prevent default values from being generated annotations[RelationalAnnotationNames.TableName] = entityType.GetTableName(); annotations[RelationalAnnotationNames.Schema] = entityType.GetSchema(); @@ -348,7 +348,7 @@ private static RuntimeRelationalPropertyOverrides Create( => new( runtimeProperty, propertyOverrides.StoreObject, - propertyOverrides.ColumnNameOverridden, + propertyOverrides.IsColumnNameOverridden, propertyOverrides.ColumnName); /// diff --git a/src/EFCore.Relational/Metadata/Conventions/StoredProcedureConvention.cs b/src/EFCore.Relational/Metadata/Conventions/StoredProcedureConvention.cs new file mode 100644 index 00000000000..46b913b9e3e --- /dev/null +++ b/src/EFCore.Relational/Metadata/Conventions/StoredProcedureConvention.cs @@ -0,0 +1,76 @@ +// 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.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; + +/// +/// A convention that ensures that the entity type is current for the stored procedures. +/// +/// +/// See Model building conventions and +/// Entity type hierarchy mapping for more information and examples. +/// +public class StoredProcedureConvention : IEntityTypeAddedConvention +{ + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + /// Parameter object containing relational dependencies for this convention. + public StoredProcedureConvention( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies) + { + Dependencies = dependencies; + RelationalDependencies = relationalDependencies; + } + + /// + /// Dependencies for this service. + /// + protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } + + /// + /// Relational provider-specific dependencies for this service. + /// + protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; } + + /// + /// Called after an entity type is added to the model. + /// + /// The builder for the entity type. + /// Additional information associated with convention execution. + public virtual void ProcessEntityTypeAdded( + IConventionEntityTypeBuilder entityTypeBuilder, + IConventionContext context) + { + var entityType = entityTypeBuilder.Metadata; + if (!entityType.HasSharedClrType) + { + return; + } + + var sproc = (StoredProcedure?)entityType.GetDeleteStoredProcedure(); + if (sproc != null + && sproc.EntityType != entityType) + { + sproc.EntityType = (IMutableEntityType)entityType; + } + + sproc = (StoredProcedure?)entityType.GetInsertStoredProcedure(); + if (sproc != null + && sproc.EntityType != entityType) + { + sproc.EntityType = (IMutableEntityType)entityType; + } + + sproc = (StoredProcedure?)entityType.GetUpdateStoredProcedure(); + if (sproc != null + && sproc.EntityType != entityType) + { + sproc.EntityType = (IMutableEntityType)entityType; + } + } +} diff --git a/src/EFCore.Relational/Metadata/IConventionDbFunction.cs b/src/EFCore.Relational/Metadata/IConventionDbFunction.cs index 592d137e0c3..07c8afaca66 100644 --- a/src/EFCore.Relational/Metadata/IConventionDbFunction.cs +++ b/src/EFCore.Relational/Metadata/IConventionDbFunction.cs @@ -15,7 +15,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata; public interface IConventionDbFunction : IReadOnlyDbFunction, IConventionAnnotatable { /// - /// Gets the in which this function is defined. + /// Gets the model in which this function is defined. /// new IConventionModel Model { get; } diff --git a/src/EFCore.Relational/Metadata/IConventionStoredProcedure.cs b/src/EFCore.Relational/Metadata/IConventionStoredProcedure.cs new file mode 100644 index 00000000000..2c83f9883ac --- /dev/null +++ b/src/EFCore.Relational/Metadata/IConventionStoredProcedure.cs @@ -0,0 +1,88 @@ +// 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.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.Metadata; + +/// +/// Represents a relational database function in a model in +/// the form that can be mutated while the model is being built. +/// +public interface IConventionStoredProcedure : IReadOnlyStoredProcedure, IConventionAnnotatable +{ + /// + /// Gets the entity type in which this function is defined. + /// + new IConventionEntityType EntityType { get; } + + /// + /// Gets the builder that can be used to configure this function. + /// + /// If the function has been removed from the model. + new IConventionStoredProcedureBuilder Builder { get; } + + /// + /// Gets the configuration source for this function. + /// + /// The configuration source for this function. + ConfigurationSource GetConfigurationSource(); + + /// + /// Sets the name of the function in the database. + /// + /// The name of the function in the database. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + string? SetName(string? name, bool fromDataAnnotation = false); + + /// + /// Gets the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetNameConfigurationSource(); + + /// + /// Sets the schema of the function in the database. + /// + /// The schema of the function in the database. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + string? SetSchema(string? schema, bool fromDataAnnotation = false); + + /// + /// Gets the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetSchemaConfigurationSource(); + + /// + /// Adds a new parameter mapped to the property with the given name. + /// + /// The name of the corresponding property. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + string? AddParameter(string propertyName, bool fromDataAnnotation = false); + + /// + /// Adds a new column of the result for this stored procedure mapped to the property with the given name + /// + /// The name of the corresponding property. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + string? AddResultColumn(string propertyName, bool fromDataAnnotation = false); + + /// + /// Prevents automatically creating a transaction when executing this stored procedure. + /// + /// A value indicating whether the automatic transactions should be prevented. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + bool SetAreTransactionsSuppressed(bool areTransactionsSuppressed, bool fromDataAnnotation = false); + + /// + /// Gets the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetAreTransactionsSuppressedConfigurationSource(); +} diff --git a/src/EFCore.Relational/Metadata/IMutableDbFunction.cs b/src/EFCore.Relational/Metadata/IMutableDbFunction.cs index ddacfc6da15..ef7f3258294 100644 --- a/src/EFCore.Relational/Metadata/IMutableDbFunction.cs +++ b/src/EFCore.Relational/Metadata/IMutableDbFunction.cs @@ -45,7 +45,7 @@ public interface IMutableDbFunction : IReadOnlyDbFunction, IMutableAnnotatable new RelationalTypeMapping? TypeMapping { get; set; } /// - /// Gets the in which this function is defined. + /// Gets the model in which this function is defined. /// new IMutableModel Model { get; } diff --git a/src/EFCore.Relational/Metadata/IMutableStoredProcedure.cs b/src/EFCore.Relational/Metadata/IMutableStoredProcedure.cs new file mode 100644 index 00000000000..093d6f2a66b --- /dev/null +++ b/src/EFCore.Relational/Metadata/IMutableStoredProcedure.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.EntityFrameworkCore.Metadata; + +/// +/// Represents a relational database function in an model in +/// the form that can be mutated while the model is being built. +/// +public interface IMutableStoredProcedure : IReadOnlyStoredProcedure, IMutableAnnotatable +{ + /// + /// Gets or sets the name of the function in the database. + /// + new string? Name { get; [param: NotNull] set; } + + /// + /// Gets or sets the schema of the function in the database. + /// + new string? Schema { get; set; } + + /// + /// Gets the entity type in which this function is defined. + /// + new IMutableEntityType EntityType { get; } + + /// + /// Returns a value indicating whether automatic creation of transactions is disabled when executing this stored procedure. + /// + /// The configured value. + new bool AreTransactionsSuppressed { get; set; } + + /// + /// Adds a new parameter mapped to the property with the given name. + /// + /// The name of the corresponding property. + /// if a parameter was added. + bool AddParameter(string propertyName); + + /// + /// Adds a new column of the result for this stored procedure mapped to the property with the given name + /// + /// The name of the corresponding property. + /// if a column was added. + bool AddResultColumn(string propertyName); +} diff --git a/src/EFCore.Relational/Metadata/IReadOnlyRelationalPropertyOverrides.cs b/src/EFCore.Relational/Metadata/IReadOnlyRelationalPropertyOverrides.cs index 5fe8987bf95..07dad4656a7 100644 --- a/src/EFCore.Relational/Metadata/IReadOnlyRelationalPropertyOverrides.cs +++ b/src/EFCore.Relational/Metadata/IReadOnlyRelationalPropertyOverrides.cs @@ -31,7 +31,7 @@ public interface IReadOnlyRelationalPropertyOverrides : IReadOnlyAnnotatable /// /// Gets a value indicating whether the column name is overriden. /// - bool ColumnNameOverridden { get; } + bool IsColumnNameOverridden { get; } /// /// @@ -55,7 +55,7 @@ string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOpt .Append("Override: ") .Append(StoreObject.DisplayName()); - if (ColumnNameOverridden) + if (IsColumnNameOverridden) { builder.Append(" ColumnName: ") .Append(ColumnName); diff --git a/src/EFCore.Relational/Metadata/IReadOnlyStoredProcedure.cs b/src/EFCore.Relational/Metadata/IReadOnlyStoredProcedure.cs new file mode 100644 index 00000000000..23739096e18 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IReadOnlyStoredProcedure.cs @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; + +namespace Microsoft.EntityFrameworkCore.Metadata; + +/// +/// Represents a stored procedure in a model. +/// +public interface IReadOnlyStoredProcedure : IReadOnlyAnnotatable +{ + /// + /// Gets the name of the stored procedure in the database. + /// + string? Name { get; } + + /// + /// Gets the schema of the stored procedure in the database. + /// + string? Schema { get; } + + /// + /// Gets the entity type in which this stored procedure is defined. + /// + IReadOnlyEntityType EntityType { get; } + + /// + /// Returns a value indicating whether automatic creation of transactions is disabled when executing this stored procedure. + /// + /// The configured value. + bool AreTransactionsSuppressed { get; } + + /// + /// Gets the names of properties mapped to parameters for this stored procedure. + /// + IReadOnlyList Parameters { get; } + + /// + /// Returns a value indicating whether there is a parameter corresponding to the given property. + /// + /// The name of a property. + /// if a parameter corresponding to the given property is found. + bool ContainsParameter(string propertyName); + + /// + /// Gets the names of properties mapped to columns of the result for this stored procedure. + /// + IReadOnlyList ResultColumns { get; } + + /// + /// Returns a value indicating whether there is a column of the result corresponding to the given property. + /// + /// The name of a property. + /// if a columns of the result corresponding to the given property is found. + bool ContainsResultColumn(string propertyName); + + /// + /// Returns the name of the stored procedure prepended by the schema + /// or if not mapped. + /// + /// The name of the stored procedure prepended by the schema. + string? GetSchemaQualifiedName() + { + var name = Name; + if (name == null) + { + return null; + } + + var schema = Schema; + return (string.IsNullOrEmpty(schema) ? "" : schema + ".") + name; + } + + /// + /// + /// Creates a human-readable representation of the given metadata. + /// + /// + /// Warning: Do not rely on the format of the returned string. + /// It is designed for debugging only and may change arbitrarily between releases. + /// + /// + /// Options for generating the string. + /// The number of indent spaces to use before each new line. + /// A human-readable representation. + string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOptions.ShortDefault, int indent = 0) + { + var builder = new StringBuilder(); + var indentString = new string(' ', indent); + + builder + .Append(indentString) + .Append("StoredProcedure: "); + + if (Schema != null) + { + builder + .Append(Schema) + .Append('.'); + } + + builder.Append(Name); + + if ((options & MetadataDebugStringOptions.SingleLine) == 0) + { + var parameters = Parameters.ToList(); + if (parameters.Count != 0) + { + builder.AppendLine().Append(indentString).Append(" Parameters: "); + foreach (var parameter in parameters) + { + builder.AppendLine().Append(parameter); + } + } + + if ((options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(AnnotationsToDebugString(indent: indent + 2)); + } + } + + return builder.ToString(); + } +} diff --git a/src/EFCore.Relational/Metadata/IStoredProcedure.cs b/src/EFCore.Relational/Metadata/IStoredProcedure.cs new file mode 100644 index 00000000000..2e571045add --- /dev/null +++ b/src/EFCore.Relational/Metadata/IStoredProcedure.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Metadata; + +/// +/// Represents a relational database function in a model. +/// +/// +/// See Database functions for more information and examples. +/// +public interface IStoredProcedure : IReadOnlyStoredProcedure, IAnnotatable +{ + /// + /// Gets the name of the stored procedure in the database. + /// + new string Name { get; } + + /// + /// Gets the entity type in which this function is defined. + /// + new IEntityType EntityType { get; } + + ///// + ///// Gets the associated . + ///// + //IStoreFunction StoreFunction { get; } +} diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs index 2c76de5e509..0e6f337e43c 100644 --- a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs +++ b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs @@ -99,9 +99,9 @@ public DbFunction( ReturnType = returnType; Model = model; _configurationSource = configurationSource; - _builder = new InternalDbFunctionBuilder(this, ((IConventionModel)model).Builder); + _builder = new(this, ((IConventionModel)model).Builder); _parameters = parameters == null - ? new List() + ? new() : parameters .Select(p => new DbFunctionParameter(this, p.Name, p.Type)) .ToList(); @@ -633,6 +633,15 @@ public virtual IReadOnlyList Parameters get => _parameters; } + /// + /// 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 virtual DbFunctionParameter? FindParameter(string name) + => Parameters.SingleOrDefault(p => p.Name == name); + /// /// 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 @@ -719,15 +728,6 @@ IReadOnlyList IDbFunction.Parameters get => _parameters; } - /// - /// 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 virtual DbFunctionParameter? FindParameter(string name) - => Parameters.SingleOrDefault(p => p.Name == name); - /// [DebuggerStepThrough] string? IConventionDbFunction.SetName(string? name, bool fromDataAnnotation) diff --git a/src/EFCore.Relational/Metadata/Internal/InternalStoredProcedureBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalStoredProcedureBuilder.cs new file mode 100644 index 00000000000..834f6d9f868 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/InternalStoredProcedureBuilder.cs @@ -0,0 +1,392 @@ +// 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.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.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 class InternalStoredProcedureBuilder : + AnnotatableBuilder, + IConventionStoredProcedureBuilder +{ + /// + /// 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 InternalStoredProcedureBuilder(StoredProcedure storedProcedure, IConventionModelBuilder modelBuilder) + : base(storedProcedure, modelBuilder) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static InternalStoredProcedureBuilder HasStoredProcedure( + IMutableEntityType entityType, + StoreObjectType sprocType, + string? name = null, + string? schema = null) + { + var sproc = StoredProcedure.GetDeclaredStoredProcedure(entityType, sprocType); + if (sproc == null) + { + sproc = name == null + ? StoredProcedure.SetStoredProcedure(entityType, sprocType) + : StoredProcedure.SetStoredProcedure(entityType, sprocType, name, schema); + } + else + { + if (name != null) + { + sproc.SetName(name, schema, ConfigurationSource.Explicit); + } + + sproc.UpdateConfigurationSource(ConfigurationSource.Explicit); + } + + return sproc.Builder; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static InternalStoredProcedureBuilder? HasStoredProcedure( + IConventionEntityType entityType, + StoreObjectType sprocType, + bool fromDataAnnotation) + { + var sproc = StoredProcedure.GetDeclaredStoredProcedure(entityType, sprocType); + if (sproc == null) + { + sproc = StoredProcedure.SetStoredProcedure(entityType, sprocType, fromDataAnnotation); + } + else + { + sproc.UpdateConfigurationSource(fromDataAnnotation + ? ConfigurationSource.DataAnnotation + : ConfigurationSource.Convention); + } + + return sproc?.Builder; + } + + /// + /// 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 virtual InternalStoredProcedureBuilder? HasName(string? name, ConfigurationSource configurationSource) + { + if (CanSetName(name, configurationSource)) + { + Metadata.SetName(name, configurationSource); + return this; + } + + return null; + } + + /// + /// 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 virtual InternalStoredProcedureBuilder? HasName(string? name, string? schema, ConfigurationSource configurationSource) + { + if (CanSetName(name, configurationSource) + && CanSetSchema(schema, configurationSource)) + { + Metadata.SetName(name, schema, configurationSource); + return this; + } + + return null; + } + + /// + /// 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 virtual bool CanSetName(string? name, ConfigurationSource configurationSource) + => (name != "" || configurationSource == ConfigurationSource.Explicit) + && (configurationSource.Overrides(Metadata.GetNameConfigurationSource()) + || Metadata.Name == name); + + /// + /// 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 virtual InternalStoredProcedureBuilder? HasSchema(string? schema, ConfigurationSource configurationSource) + { + if (CanSetSchema(schema, configurationSource)) + { + Metadata.SetSchema(schema, configurationSource); + return this; + } + + return null; + } + + /// + /// 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 virtual bool CanSetSchema(string? schema, ConfigurationSource configurationSource) + => configurationSource.Overrides(Metadata.GetSchemaConfigurationSource()) + || Metadata.Schema == schema; + + /// + /// 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 virtual PropertyBuilder CreatePropertyBuilder(EntityTypeBuilder entityTypeBuilder, string propertyName) + { + var entityType = entityTypeBuilder.Metadata; + var property = entityType.FindProperty(propertyName); + if (property == null) + { + property = entityType.GetDerivedTypes().SelectMany(et => et.GetDeclaredProperties()) + .FirstOrDefault(p => p.Name == propertyName); + } + + if (property == null) + { + throw new InvalidOperationException(CoreStrings.PropertyNotFound(propertyName, entityType.DisplayName())); + } + +#pragma warning disable EF1001 // Internal EF Core API usage. + return new ModelBuilder(entityType.Model) +#pragma warning restore EF1001 // Internal EF Core API usage. + .Entity(property.DeclaringEntityType.Name) + .Property(property.ClrType, propertyName); + } + + /// + /// 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 virtual PropertyBuilder CreatePropertyBuilder( + EntityTypeBuilder entityTypeBuilder, + Expression> propertyExpression) + where TDerivedEntity : class + { + var memberInfo = propertyExpression.GetMemberAccess(); + var entityType = entityTypeBuilder.Metadata; + if (entityType.ClrType != typeof(TDerivedEntity)) + { +#pragma warning disable EF1001 // Internal EF Core API usage. + entityTypeBuilder = new ModelBuilder(entityType.Model).Entity(typeof(TDerivedEntity)); +#pragma warning restore EF1001 // Internal EF Core API usage. + } + + return entityTypeBuilder.Property(memberInfo.GetMemberType(), memberInfo.Name); + } + + /// + /// 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 virtual InternalStoredProcedureBuilder? HasParameter( + string propertyName, ConfigurationSource configurationSource) + { + if (!Metadata.ContainsParameter(propertyName)) + { + if (!configurationSource.Overrides(Metadata.GetConfigurationSource())) + { + return null; + } + + Metadata.AddParameter(propertyName); + } + + Metadata.UpdateConfigurationSource(configurationSource); + return this; + } + + /// + /// 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 virtual InternalStoredProcedureBuilder? HasParameter( + Expression> propertyExpression, + ConfigurationSource configurationSource) + where TDerivedEntity : class + => HasParameter(propertyExpression.GetMemberAccess().Name, configurationSource); + + /// + /// 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 virtual bool CanHaveParameter(string propertyName, ConfigurationSource configurationSource) + => Metadata.ContainsParameter(propertyName) + || configurationSource.Overrides(Metadata.GetConfigurationSource()); + + /// + /// 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 virtual InternalStoredProcedureBuilder? HasResultColumn( + string propertyName, ConfigurationSource configurationSource) + { + if (!Metadata.ContainsResultColumn(propertyName)) + { + if (!configurationSource.Overrides(Metadata.GetConfigurationSource())) + { + return null; + } + + Metadata.AddResultColumn(propertyName); + } + + Metadata.UpdateConfigurationSource(configurationSource); + return this; + } + + /// + /// 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 virtual InternalStoredProcedureBuilder? HasResultColumn( + Expression> propertyExpression, + ConfigurationSource configurationSource) + where TDerivedEntity : class + => HasResultColumn(propertyExpression.GetMemberAccess().Name, configurationSource); + + /// + /// 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 virtual bool CanHaveResultColumn(string propertyName, ConfigurationSource configurationSource) + => Metadata.ContainsResultColumn(propertyName) + || configurationSource.Overrides(Metadata.GetConfigurationSource()); + + /// + /// 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 virtual InternalStoredProcedureBuilder? SuppressTransactions(bool suppress, ConfigurationSource configurationSource) + { + if (!CanSuppressTransactions(suppress, configurationSource)) + { + return null; + } + + Metadata.SetAreTransactionsSuppressed(suppress, configurationSource); + return this; + } + + /// + /// 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 virtual bool CanSuppressTransactions(bool suppress, ConfigurationSource configurationSource) + => Metadata.AreTransactionsSuppressed == suppress + || configurationSource.Overrides(Metadata.GetAreTransactionsSuppressedConfigurationSource()); + + IConventionStoredProcedure IConventionStoredProcedureBuilder.Metadata + { + [DebuggerStepThrough] + get => Metadata; + } + + /// + [DebuggerStepThrough] + IConventionStoredProcedureBuilder? IConventionStoredProcedureBuilder.HasName(string? name, bool fromDataAnnotation) + => HasName(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionStoredProcedureBuilder? IConventionStoredProcedureBuilder.HasName(string? name, string? schema, bool fromDataAnnotation) + + => HasName(name, schema, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionStoredProcedureBuilder.CanSetName(string? name, bool fromDataAnnotation) + => CanSetName(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionStoredProcedureBuilder? IConventionStoredProcedureBuilder.HasSchema(string? schema, bool fromDataAnnotation) + => HasSchema(schema, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionStoredProcedureBuilder.CanSetSchema(string? schema, bool fromDataAnnotation) + => CanSetSchema(schema, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionStoredProcedureBuilder? IConventionStoredProcedureBuilder.HasParameter(string propertyName, bool fromDataAnnotation) + => HasParameter(propertyName, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionStoredProcedureBuilder.CanHaveParameter(string propertyName, bool fromDataAnnotation) + => CanHaveParameter(propertyName, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionStoredProcedureBuilder? IConventionStoredProcedureBuilder.HasResultColumn(string propertyName, bool fromDataAnnotation) + => HasResultColumn(propertyName, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionStoredProcedureBuilder.CanHaveResultColumn(string propertyName, bool fromDataAnnotation) + => CanHaveResultColumn(propertyName, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionStoredProcedureBuilder? IConventionStoredProcedureBuilder.SuppressTransactions(bool suppress, bool fromDataAnnotation) + => SuppressTransactions(suppress, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionStoredProcedureBuilder.CanSetSuppressTransactions(bool suppress, bool fromDataAnnotation) + => CanSuppressTransactions(suppress, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); +} diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalKeyExtensions.cs b/src/EFCore.Relational/Metadata/Internal/RelationalKeyExtensions.cs index a50faacc0d4..f437487aaa7 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalKeyExtensions.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalKeyExtensions.cs @@ -171,7 +171,7 @@ public static bool AreCompatible( return null; } - logger.KeyUnmappedProperties((IKey)key); + logger.KeyPropertiesNotMappedToTable((IKey)key); } return null; diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalPropertyOverrides.cs b/src/EFCore.Relational/Metadata/Internal/RelationalPropertyOverrides.cs index aeec0461e23..f0a5006c667 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalPropertyOverrides.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalPropertyOverrides.cs @@ -101,10 +101,22 @@ public virtual void SetRemovedFromModel() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public static void Attach(IConventionProperty property, IConventionRelationalPropertyOverrides detachedOverrides) + => Attach(property, detachedOverrides, detachedOverrides.StoreObject); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static void Attach( + IConventionProperty property, + IConventionRelationalPropertyOverrides detachedOverrides, + StoreObjectIdentifier newStoreObject) { var newOverrides = GetOrCreate( (IMutableProperty)property, - detachedOverrides.StoreObject, + newStoreObject, detachedOverrides.GetConfigurationSource()); MergeInto(detachedOverrides, newOverrides); @@ -184,12 +196,12 @@ public virtual string? ColumnName /// 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 ColumnNameOverridden + public virtual bool IsColumnNameOverridden => _columnNameConfigurationSource != null; private bool RemoveColumnNameOverride(ConfigurationSource configurationSource) { - if (!ColumnNameOverridden) + if (!IsColumnNameOverridden) { return true; } diff --git a/src/EFCore.Relational/Metadata/Internal/StoredProcedure.cs b/src/EFCore.Relational/Metadata/Internal/StoredProcedure.cs new file mode 100644 index 00000000000..07c0ccb556f --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/StoredProcedure.cs @@ -0,0 +1,665 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Metadata.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 class StoredProcedure : + ConventionAnnotatable, IStoredProcedure, IMutableStoredProcedure, IConventionStoredProcedure +{ + private readonly List _parameters = new(); + private readonly HashSet _parametersSet = new(); + private readonly List _resultColumns = new(); + private readonly HashSet _resultColumnsSet = new(); + private string? _schema; + private string? _name; + private InternalStoredProcedureBuilder? _builder; + private bool _areTransactionsSuppressed; + + private ConfigurationSource _configurationSource; + private ConfigurationSource? _schemaConfigurationSource; + private ConfigurationSource? _nameConfigurationSource; + private ConfigurationSource? _areTransactionsSuppressedConfigurationSource; + + /// + /// 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 StoredProcedure( + IMutableEntityType entityType, + ConfigurationSource configurationSource) + { + EntityType = entityType; + _configurationSource = configurationSource; + _builder = new(this, ((IConventionEntityType)entityType).Model.Builder); + } + + /// + public virtual IMutableEntityType EntityType { get; set; } + + /// + /// 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 virtual InternalStoredProcedureBuilder Builder + { + [DebuggerStepThrough] + get => _builder ?? throw new InvalidOperationException(CoreStrings.ObjectRemovedFromModel); + } + + /// + /// 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 virtual bool IsInModel + => _builder is not null; + + /// + /// 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 virtual void SetRemovedFromModel() + => _builder = null; + + /// + /// Indicates whether the function is read-only. + /// + public override bool IsReadOnly + => ((Annotatable)EntityType).IsReadOnly; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static StoredProcedure? FindStoredProcedure( + IReadOnlyEntityType entityType, + StoreObjectType sprocType) + { + var storedProcedure = GetDeclaredStoredProcedure(entityType, sprocType); + if (storedProcedure != null) + { + return storedProcedure; + } + + if ((entityType.GetMappingStrategy() ?? RelationalAnnotationNames.TphMappingStrategy) + == RelationalAnnotationNames.TphMappingStrategy + && entityType.BaseType != null) + { + return FindStoredProcedure(entityType.GetRootType(), sprocType); + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static StoredProcedure? GetDeclaredStoredProcedure( + IReadOnlyEntityType entityType, + StoreObjectType sprocType) + { + var sprocAnnotation = entityType.FindAnnotation(GetAnnotationName(sprocType)); + return sprocAnnotation != null ? (StoredProcedure?)sprocAnnotation.Value : null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static StoredProcedure SetStoredProcedure( + IMutableEntityType entityType, + StoreObjectType sprocType) + { + var oldId = GetDeclaredStoredProcedure(entityType, sprocType)?.CreateIdentifier(); + var sproc = new StoredProcedure(entityType, ConfigurationSource.Explicit); + entityType.SetAnnotation(GetAnnotationName(sprocType), sproc); + + if (oldId != null) + { + UpdateOverrides(oldId.Value, sproc.CreateIdentifier(), (IConventionEntityType)entityType); + } + + return sproc; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static StoredProcedure SetStoredProcedure( + IMutableEntityType entityType, + StoreObjectType sprocType, + string name, + string? schema) + { + var oldId = GetDeclaredStoredProcedure(entityType, sprocType)?.CreateIdentifier(); + var sproc = new StoredProcedure(entityType, ConfigurationSource.Explicit); + entityType.SetAnnotation(GetAnnotationName(sprocType), sproc); + sproc.SetName(name, schema, ConfigurationSource.Explicit, skipOverrides: true); + + if (oldId != null) + { + UpdateOverrides(oldId.Value, sproc.CreateIdentifier(), (IConventionEntityType)entityType); + } + + return sproc; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static StoredProcedure? SetStoredProcedure( + IConventionEntityType entityType, + StoreObjectType sprocType, + bool fromDataAnnotation) + { + var oldId = GetDeclaredStoredProcedure(entityType, sprocType)?.CreateIdentifier(); + var sproc = new StoredProcedure( + (IMutableEntityType)entityType, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + sproc = (StoredProcedure?)entityType.SetAnnotation(GetAnnotationName(sprocType), sproc)?.Value; + + if (oldId != null + && sproc != null) + { + UpdateOverrides(oldId.Value, sproc.CreateIdentifier(), entityType); + } + + return sproc; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static StoredProcedure? SetStoredProcedure( + IConventionEntityType entityType, + StoreObjectType sprocType, + string name, + string? schema, + bool fromDataAnnotation) + { + var oldId = GetDeclaredStoredProcedure(entityType, sprocType)?.CreateIdentifier(); + var configurationSource = fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention; + var sproc = new StoredProcedure((IMutableEntityType)entityType, configurationSource); + sproc = (StoredProcedure?)entityType.SetAnnotation(GetAnnotationName(sprocType), sproc)?.Value; + + sproc?.SetName(name, schema, configurationSource, skipOverrides: true); + + if (oldId != null + && sproc != null) + { + UpdateOverrides(oldId.Value, sproc.CreateIdentifier(), entityType); + } + + return sproc; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static IMutableStoredProcedure? RemoveStoredProcedure(IMutableEntityType entityType, StoreObjectType sprocType) + { + var oldId = GetDeclaredStoredProcedure(entityType, sprocType)?.CreateIdentifier(); + var sproc = (IMutableStoredProcedure?)entityType.RemoveAnnotation(GetAnnotationName(sprocType))?.Value; + + if (oldId != null + && sproc != null) + { + UpdateOverrides(oldId.Value, null, (IConventionEntityType)entityType); + } + + return sproc; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static IConventionStoredProcedure? RemoveStoredProcedure(IConventionEntityType entityType, StoreObjectType sprocType) + { + var oldId = GetDeclaredStoredProcedure(entityType, sprocType)?.CreateIdentifier(); + var sproc = (IConventionStoredProcedure?)entityType.RemoveAnnotation(GetAnnotationName(sprocType))?.Value; + + if (oldId != null + && sproc != null) + { + UpdateOverrides(oldId.Value, null, entityType); + } + + return sproc; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static ConfigurationSource? GetStoredProcedureConfigurationSource( + IConventionEntityType entityType, StoreObjectType sprocType) + => entityType.FindAnnotation(GetAnnotationName(sprocType)) + ?.GetConfigurationSource(); + + private static string GetAnnotationName(StoreObjectType sprocType) + => sprocType switch + { + StoreObjectType.InsertStoredProcedure => RelationalAnnotationNames.InsertStoredProcedure, + StoreObjectType.DeleteStoredProcedure => RelationalAnnotationNames.DeleteStoredProcedure, + StoreObjectType.UpdateStoredProcedure => RelationalAnnotationNames.UpdateStoredProcedure, + _ => throw new InvalidOperationException("Unsopported sproc type " + sprocType) + }; + + /// + /// 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 virtual StoreObjectIdentifier? CreateIdentifier() + { + var name = Name; + if (name == null) + { + return null; + } + + if (EntityType.GetInsertStoredProcedure() == this) + { + return StoreObjectIdentifier.InsertStoredProcedure(name, Schema); + } + + if (EntityType.GetDeleteStoredProcedure() == this) + { + return StoreObjectIdentifier.DeleteStoredProcedure(name, Schema); + } + + if (EntityType.GetUpdateStoredProcedure() == this) + { + return StoreObjectIdentifier.UpdateStoredProcedure(name, Schema); + } + + return null; + } + + /// + [DebuggerStepThrough] + public virtual ConfigurationSource GetConfigurationSource() + => _configurationSource; + + /// + /// 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. + /// + [DebuggerStepThrough] + public virtual void UpdateConfigurationSource(ConfigurationSource configurationSource) + => _configurationSource = configurationSource.Max(_configurationSource); + + /// + public virtual string? Schema + { + get => _schema ?? EntityType.GetDefaultSchema(); + set => SetSchema(value, ConfigurationSource.Explicit); + } + + /// + /// 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 virtual string? SetSchema(string? schema, ConfigurationSource configurationSource) + { + EnsureMutable(); + + var oldId = CreateIdentifier(); + + _schema = schema; + + _schemaConfigurationSource = configurationSource.Max(_schemaConfigurationSource); + + if (oldId != null) + { + UpdateOverrides(oldId.Value, CreateIdentifier(), (IConventionEntityType)EntityType); + } + + return schema; + } + + /// + /// 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 virtual ConfigurationSource? GetSchemaConfigurationSource() + => _schemaConfigurationSource; + + /// + public virtual string? Name + { + get => _name ?? GetDefaultName(); + set => SetName(value, ConfigurationSource.Explicit); + } + + private string? GetDefaultName() + { + string? suffix; + if (EntityType.GetInsertStoredProcedure() == this) + { + suffix = "_Insert"; + } + else if (EntityType.GetDeleteStoredProcedure() == this) + { + suffix = "_Delete"; + } + else if (EntityType.GetUpdateStoredProcedure() == this) + { + suffix = "_Update"; + } + else + { + return null; + } + + var tableName = EntityType.GetDefaultTableName(); + if (tableName == null) + { + return null; + } + + return tableName + suffix; + } + + /// + /// 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 virtual string? SetName(string? name, ConfigurationSource configurationSource) + { + EnsureMutable(); + + var oldId = CreateIdentifier(); + + _name = name; + + _nameConfigurationSource = name == null + ? null + : configurationSource.Max(_nameConfigurationSource); + + if (oldId != null) + { + UpdateOverrides(oldId.Value, CreateIdentifier(), (IConventionEntityType)EntityType); + } + + return name; + } + + /// + /// 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 virtual void SetName(string? name, string? schema, ConfigurationSource configurationSource, bool skipOverrides = false) + { + EnsureMutable(); + + var oldId = CreateIdentifier(); + + _name = name; + + _nameConfigurationSource = name == null + ? null + : configurationSource.Max(_nameConfigurationSource); + + _schema = schema; + + _schemaConfigurationSource = configurationSource.Max(_schemaConfigurationSource); + + if (!skipOverrides + && oldId != null) + { + UpdateOverrides(oldId.Value, CreateIdentifier(), (IConventionEntityType)EntityType); + } + } + + /// + public virtual ConfigurationSource? GetNameConfigurationSource() + => _nameConfigurationSource; + + /// + public virtual bool AreTransactionsSuppressed + { + get => _areTransactionsSuppressed; + set => SetAreTransactionsSuppressed(value, ConfigurationSource.Explicit); + } + + /// + /// 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 virtual bool SetAreTransactionsSuppressed(bool areTransactionsSuppressed, ConfigurationSource configurationSource) + { + EnsureMutable(); + + _areTransactionsSuppressed = areTransactionsSuppressed; + + _areTransactionsSuppressedConfigurationSource = configurationSource.Max(_areTransactionsSuppressedConfigurationSource); + + return areTransactionsSuppressed; + } + + /// + public virtual ConfigurationSource? GetAreTransactionsSuppressedConfigurationSource() + => _areTransactionsSuppressedConfigurationSource; + + private static void UpdateOverrides( + StoreObjectIdentifier oldId, + StoreObjectIdentifier? newId, + IConventionEntityType entityType) + { + if (oldId == newId) + { + return; + } + + var properties = (entityType.GetMappingStrategy() ?? RelationalAnnotationNames.TphMappingStrategy) + == RelationalAnnotationNames.TphMappingStrategy + ? entityType.GetProperties().Concat(entityType.GetDerivedProperties()) + : entityType.GetProperties(); + + foreach (var property in properties) + { + var removedOverrides = property.RemoveOverrides(oldId); + if (removedOverrides != null + && newId != null) + { + RelationalPropertyOverrides.Attach(property, removedOverrides, newId.Value); + } + } + } + + /// + public virtual IReadOnlyList Parameters + { + [DebuggerStepThrough] + get => _parameters; + } + + /// + public virtual bool ContainsParameter(string propertyName) + => _parametersSet.Contains(propertyName); + + /// + public virtual bool AddParameter(string propertyName) + { + if (!_parametersSet.Contains(propertyName)) + { + _parameters.Add(propertyName); + _parametersSet.Add(propertyName); + + return true; + } + + return false; + } + + /// + public virtual IReadOnlyList ResultColumns + { + [DebuggerStepThrough] + get => _resultColumns; + } + + /// + public virtual bool ContainsResultColumn(string propertyName) + => _resultColumnsSet.Contains(propertyName); + + /// + public virtual bool AddResultColumn(string propertyName) + { + if (!_resultColumnsSet.Contains(propertyName)) + { + _resultColumns.Add(propertyName); + _resultColumnsSet.Add(propertyName); + + return true; + } + + return false; + } + + ///// + ///// 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. + ///// + //[DisallowNull] + //public virtual IStoreFunction? StoreFunction { get; set; } + + /// + /// 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 override string ToString() + => ((IStoredProcedure)this).ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + + /// + /// 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. + /// + [EntityFrameworkInternal] + public virtual DebugView DebugView + => new( + () => ((IStoredProcedure)this).ToDebugString(), + () => ((IStoredProcedure)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + + /// + IConventionStoredProcedureBuilder IConventionStoredProcedure.Builder + { + [DebuggerStepThrough] + get => Builder; + } + + /// + string IStoredProcedure.Name + { + [DebuggerStepThrough] + get => Name!; + } + + /// + IReadOnlyEntityType IReadOnlyStoredProcedure.EntityType + { + [DebuggerStepThrough] + get => (IConventionEntityType)EntityType; + } + + /// + IConventionEntityType IConventionStoredProcedure.EntityType + { + [DebuggerStepThrough] + get => (IConventionEntityType)EntityType; + } + + /// + IEntityType IStoredProcedure.EntityType + { + [DebuggerStepThrough] + get => (IEntityType)EntityType; + } + + ///// + //IStoreFunction IDbFunction.StoreFunction + // => StoreFunction!; // Relational model creation ensures StoreFunction is populated + + /// + [DebuggerStepThrough] + string? IConventionStoredProcedure.SetName(string? name, bool fromDataAnnotation) + => SetName(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + string? IConventionStoredProcedure.SetSchema(string? schema, bool fromDataAnnotation) + => SetSchema(schema, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + string? IConventionStoredProcedure.AddParameter(string propertyName, bool fromDataAnnotation) + => AddParameter(propertyName) ? propertyName : null; + + /// + [DebuggerStepThrough] + string? IConventionStoredProcedure.AddResultColumn(string propertyName, bool fromDataAnnotation) + => AddResultColumn(propertyName) ? propertyName : null; + + /// + [DebuggerStepThrough] + bool IConventionStoredProcedure.SetAreTransactionsSuppressed(bool areTransactionsSuppressed, bool fromDataAnnotation) + => SetAreTransactionsSuppressed( + areTransactionsSuppressed, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); +} diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs index e6cd3da5c2f..c060f520d0c 100644 --- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs +++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs @@ -77,6 +77,21 @@ public static class RelationalAnnotationNames /// public const string FunctionName = Prefix + "FunctionName"; + /// + /// The name for mapped delete stored procedure annotations. + /// + public const string DeleteStoredProcedure = Prefix + "DeleteStoredProcedure"; + + /// + /// The name for mapped insert stored procedure annotations. + /// + public const string InsertStoredProcedure = Prefix + "InsertStoredProcedure"; + + /// + /// The name for mapped update stored procedure annotations. + /// + public const string UpdateStoredProcedure = Prefix + "UpdateStoredProcedure"; + /// /// The name for mapped sql query annotations. /// diff --git a/src/EFCore.Relational/Metadata/RuntimeRelationalPropertyOverrides.cs b/src/EFCore.Relational/Metadata/RuntimeRelationalPropertyOverrides.cs index 43f8fb11100..bee9445fcf0 100644 --- a/src/EFCore.Relational/Metadata/RuntimeRelationalPropertyOverrides.cs +++ b/src/EFCore.Relational/Metadata/RuntimeRelationalPropertyOverrides.cs @@ -78,7 +78,7 @@ IReadOnlyProperty IReadOnlyRelationalPropertyOverrides.Property } /// - bool IReadOnlyRelationalPropertyOverrides.ColumnNameOverridden + bool IReadOnlyRelationalPropertyOverrides.IsColumnNameOverridden { [DebuggerStepThrough] get => FindAnnotation(RelationalAnnotationNames.ColumnName) != null; diff --git a/src/EFCore.Relational/Metadata/StoreObjectIdentifier.cs b/src/EFCore.Relational/Metadata/StoreObjectIdentifier.cs index a8f555f16a8..070aa49b81c 100644 --- a/src/EFCore.Relational/Metadata/StoreObjectIdentifier.cs +++ b/src/EFCore.Relational/Metadata/StoreObjectIdentifier.cs @@ -42,6 +42,21 @@ private StoreObjectIdentifier(StoreObjectType storeObjectType, string name, stri case StoreObjectType.Function: var functionName = entityType.GetFunctionName(); return functionName == null ? null : DbFunction(functionName); + case StoreObjectType.InsertStoredProcedure: + var insertStoredProcedure = entityType.GetInsertStoredProcedure(); + return insertStoredProcedure == null || insertStoredProcedure.Name == null + ? null + : InsertStoredProcedure(insertStoredProcedure.Name, insertStoredProcedure.Schema); + case StoreObjectType.DeleteStoredProcedure: + var deleteStoredProcedure = entityType.GetDeleteStoredProcedure(); + return deleteStoredProcedure == null || deleteStoredProcedure.Name == null + ? null + : DeleteStoredProcedure(deleteStoredProcedure.Name, deleteStoredProcedure.Schema); + case StoreObjectType.UpdateStoredProcedure: + var updateStoredProcedure = entityType.GetUpdateStoredProcedure(); + return updateStoredProcedure == null || updateStoredProcedure.Name == null + ? null + : UpdateStoredProcedure(updateStoredProcedure.Name, updateStoredProcedure.Schema); default: return null; } @@ -109,6 +124,45 @@ public static StoreObjectIdentifier DbFunction(string modelName) return new StoreObjectIdentifier(StoreObjectType.Function, modelName); } + /// + /// Creates an insert stored procedure id. + /// + /// The stored procedure name. + /// The stored procedure schema. + /// The stored procedure id. + public static StoreObjectIdentifier InsertStoredProcedure(string name, string? schema = null) + { + Check.NotNull(name, nameof(name)); + + return new StoreObjectIdentifier(StoreObjectType.InsertStoredProcedure, name, schema); + } + + /// + /// Creates a delete stored procedure id. + /// + /// The stored procedure name. + /// The stored procedure schema. + /// The stored procedure id. + public static StoreObjectIdentifier DeleteStoredProcedure(string name, string? schema = null) + { + Check.NotNull(name, nameof(name)); + + return new StoreObjectIdentifier(StoreObjectType.DeleteStoredProcedure, name, schema); + } + + /// + /// Creates an update stored procedure id. + /// + /// The stored procedure name. + /// The stored procedure schema. + /// The stored procedure id. + public static StoreObjectIdentifier UpdateStoredProcedure(string name, string? schema = null) + { + Check.NotNull(name, nameof(name)); + + return new StoreObjectIdentifier(StoreObjectType.UpdateStoredProcedure, name, schema); + } + /// /// Gets the table-like store object type. /// diff --git a/src/EFCore.Relational/Metadata/StoreObjectType.cs b/src/EFCore.Relational/Metadata/StoreObjectType.cs index b268a35b848..b10ea103509 100644 --- a/src/EFCore.Relational/Metadata/StoreObjectType.cs +++ b/src/EFCore.Relational/Metadata/StoreObjectType.cs @@ -29,5 +29,20 @@ public enum StoreObjectType /// /// A table-valued function. /// - Function + Function, + + /// + /// An insert stored procedure. + /// + InsertStoredProcedure, + + /// + /// A delete stored procedure. + /// + DeleteStoredProcedure, + + /// + /// An update stored procedure. + /// + UpdateStoredProcedure, } diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index edd1c6549b5..eed94e6bbd5 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1091,6 +1091,14 @@ public static string NonTphMappingStrategy(object? mappingStrategy, object? enti GetString("NonTphMappingStrategy", nameof(mappingStrategy), nameof(entityType)), mappingStrategy, entityType); + /// + /// Both '{entityType}' and '{otherEntityType}' are mapped to the stored procedure '{sproc}'. All the entity types in a non-TPH hierarchy (one that doesn't have a discriminator) must be mapped to different stored procedures. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. + /// + public static string NonTphStoredProcedureClash(object? entityType, object? otherEntityType, object? sproc) + => string.Format( + GetString("NonTphStoredProcedureClash", nameof(entityType), nameof(otherEntityType), nameof(sproc)), + entityType, otherEntityType, sproc); + /// /// Both '{entityType}' and '{otherEntityType}' are mapped to the table '{table}'. All the entity types in a non-TPH hierarchy (one that doesn't have a discriminator) must be mapped to different tables. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. /// @@ -1221,6 +1229,94 @@ public static string SqlQueryOverrideMismatch(object? propertySpecification, obj GetString("SqlQueryOverrideMismatch", nameof(propertySpecification), nameof(query)), propertySpecification, query); + /// + /// The non-key property '{entityType}.{property}' is mapped to a parameter of the stored procedure '{sproc}', but only key properties are supported for Delete stored procedures. + /// + public static string StoredProcedureDeleteNonKeyProperty(object? entityType, object? property, object? sproc) + => string.Format( + GetString("StoredProcedureDeleteNonKeyProperty", nameof(entityType), nameof(property), nameof(sproc)), + entityType, property, sproc); + + /// + /// The keyless entity type '{entityType}' was configured to use '{sproc}'. An entity type requires a primary key to be able to be mapped to a stored procedure. + /// + public static string StoredProcedureKeyless(object? entityType, object? sproc) + => string.Format( + GetString("StoredProcedureKeyless", nameof(entityType), nameof(sproc)), + entityType, sproc); + + /// + /// The entity type '{entityType}' was configured to use '{sproc}', but the store name was not specified. Configure the stored procedure name explicitly. + /// + public static string StoredProcedureNoName(object? entityType, object? sproc) + => string.Format( + GetString("StoredProcedureNoName", nameof(entityType), nameof(sproc)), + entityType, sproc); + + /// + /// The property '{propertySpecification}' has specific configuration for the stored procedure '{sproc}', but it isn't mapped to a parameter or a result column on that stored procedure. Remove the specific configuration, or map an entity type that contains this property to '{sproc}'. + /// + public static string StoredProcedureOverrideMismatch(object? propertySpecification, object? sproc) + => string.Format( + GetString("StoredProcedureOverrideMismatch", nameof(propertySpecification), nameof(sproc)), + propertySpecification, sproc); + + /// + /// No property named '{property}' found on the entity type '{entityType}' corresponding to the parameter on the stored procedure '{sproc}' + /// + public static string StoredProcedureParameterNotFound(object? property, object? entityType, object? sproc) + => string.Format( + GetString("StoredProcedureParameterNotFound", nameof(property), nameof(entityType), nameof(sproc)), + property, entityType, sproc); + + /// + /// The entity type '{entityType}' is mapped to the stored procedure '{sproc}', however the properties {properties} are not mapped to any parameter or result column. + /// + public static string StoredProcedurePropertiesNotMapped(object? entityType, object? sproc, object? properties) + => string.Format( + GetString("StoredProcedurePropertiesNotMapped", nameof(entityType), nameof(sproc), nameof(properties)), + entityType, sproc, properties); + + /// + /// The property '{entityType}.{property}' is mapped to a result column of the stored procedure '{sproc}', but store-generated values are not supported for Delete stored procedures. + /// + public static string StoredProcedureResultColumnDelete(object? entityType, object? property, object? sproc) + => string.Format( + GetString("StoredProcedureResultColumnDelete", nameof(entityType), nameof(property), nameof(sproc)), + entityType, property, sproc); + + /// + /// No property named '{property}' found on the entity type '{entityType}' corresponding to the result column on the stored procedure '{sproc}' + /// + public static string StoredProcedureResultColumnNotFound(object? property, object? entityType, object? sproc) + => string.Format( + GetString("StoredProcedureResultColumnNotFound", nameof(property), nameof(entityType), nameof(sproc)), + property, entityType, sproc); + + /// + /// The property '{entityType}.{property}' is mapped to a result column of the stored procedure '{sproc}', but it is not configured as store-generated. + /// + public static string StoredProcedureResultColumnNotGenerated(object? entityType, object? property, object? sproc) + => string.Format( + GetString("StoredProcedureResultColumnNotGenerated", nameof(entityType), nameof(property), nameof(sproc)), + entityType, property, sproc); + + /// + /// Both entity type '{entityType1}' and '{entityType2}' were configured to use '{sproc}', stored procedure sharing is not supported. Specify different names for the corresponding stored procedures. + /// + public static string StoredProcedureTableSharing(object? entityType1, object? entityType2, object? sproc) + => string.Format( + GetString("StoredProcedureTableSharing", nameof(entityType1), nameof(entityType2), nameof(sproc)), + entityType1, entityType2, sproc); + + /// + /// Both '{entityType}' and '{otherEntityType}' are explicitly mapped to the stored procedure '{sproc}' using the 'TPH' mapping strategy. Configure the stored procedure mapping on the root entity type, including all parameters for the derived types. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. + /// + public static string StoredProcedureTphDuplicate(object? entityType, object? otherEntityType, object? sproc) + => string.Format( + GetString("StoredProcedureTphDuplicate", nameof(entityType), nameof(otherEntityType), nameof(sproc)), + entityType, otherEntityType, sproc); + /// /// The entity type '{entityType}' is not mapped to the store object '{table}'. /// @@ -1238,7 +1334,7 @@ public static string TableOverrideMismatch(object? propertySpecification, object propertySpecification, table); /// - /// The element type of the result of '{dbFunction}' is mapped to '{entityType}'. This is not supported since '{entityType}' is part of hierarchy and does not contain a discriminator property. + /// The element type of the result of '{dbFunction}' is mapped to '{entityType}'. This is not supported since '{entityType}' is part of hierarchy but does not contain a discriminator property. Only TPH hierarchies can be mapped to a TVF. /// public static string TableValuedFunctionNonTph(object? dbFunction, object? entityType) => string.Format( @@ -1270,7 +1366,7 @@ public static string TooFewReaderFields(object? expected, object? actual) expected, actual); /// - /// The entity type '{dependentType}' is mapped to '{storeObject}'. However the principal entity type '{principalEntityType}' is also mapped to '{storeObject}' and it's using the TPC mapping strategy. Only leaf entity types in a TPC hierarchy can use table-sharing. + /// The entity type '{dependentType}' is mapped to '{storeObject}'. However the principal entity type '{principalEntityType}' is also mapped to '{storeObject}' and it's using the TPC mapping strategy. Entity types in a TPC hierarchy can use table-sharing only if they have no derived types. /// public static string TpcTableSharing(object? dependentType, object? storeObject, object? principalEntityType) => string.Format( @@ -1285,6 +1381,22 @@ public static string TpcTableSharingDependent(object? dependentType, object? sto GetString("TpcTableSharingDependent", nameof(dependentType), nameof(storeObject), nameof(derivedType), nameof(otherStoreObject)), dependentType, storeObject, derivedType, otherStoreObject); + /// + /// '{entityType}' is mapped to the database function '{function}' while '{otherEntityType}' is mapped to the database function '{otherFunction}'. Map all the entity types in the hierarchy to the same database function. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. + /// + public static string TphDbFunctionMismatch(object? entityType, object? function, object? otherEntityType, object? otherFunction) + => string.Format( + GetString("TphDbFunctionMismatch", nameof(entityType), nameof(function), nameof(otherEntityType), nameof(otherFunction)), + entityType, function, otherEntityType, otherFunction); + + /// + /// '{entityType}' is mapped to the stored procedure '{sproc}' while '{otherEntityType}' is mapped to the stored procedure '{otherSproc}'. Map all the entity types in the hierarchy to the same stored procedure, or remove the discriminator and map them all to different stored procedures. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. + /// + public static string TphStoredProcedureMismatch(object? entityType, object? sproc, object? otherEntityType, object? otherSproc) + => string.Format( + GetString("TphStoredProcedureMismatch", nameof(entityType), nameof(sproc), nameof(otherEntityType), nameof(otherSproc)), + entityType, sproc, otherEntityType, otherSproc); + /// /// '{entityType}' is mapped to the table '{table}' while '{otherEntityType}' is mapped to the table '{otherTable}'. Map all the entity types in the hierarchy to the same table, or remove the discriminator and map them all to different tables. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. /// @@ -2506,23 +2618,23 @@ public static EventDefinition LogKeyHasDefaultValue(IDiagnostics /// /// The key {keyProperties} on the entity type '{entityType}' cannot be represented in the database. Either all or some of the properties aren't mapped to table '{table}'. All key properties must be mapped to a single table for the unique constraint to be created. /// - public static EventDefinition LogKeyUnmappedProperties(IDiagnosticsLogger logger) + public static EventDefinition LogKeyPropertiesNotMappedToTable(IDiagnosticsLogger logger) { - var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogKeyUnmappedProperties; + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogKeyPropertiesNotMappedToTable; if (definition == null) { definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((RelationalLoggingDefinitions)logger.Definitions).LogKeyUnmappedProperties, + ref ((RelationalLoggingDefinitions)logger.Definitions).LogKeyPropertiesNotMappedToTable, logger, static logger => new EventDefinition( logger.Options, - RelationalEventId.KeyUnmappedProperties, + RelationalEventId.KeyPropertiesNotMappedToTable, LogLevel.Error, - "RelationalEventId.KeyUnmappedProperties", + "RelationalEventId.KeyPropertiesNotMappedToTable", level => LoggerMessage.Define( level, - RelationalEventId.KeyUnmappedProperties, - _resourceManager.GetString("LogKeyUnmappedProperties")!))); + RelationalEventId.KeyPropertiesNotMappedToTable, + _resourceManager.GetString("LogKeyPropertiesNotMappedToTable")!))); } return (EventDefinition)definition; diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 4d202036ce2..5a38d8f7d7a 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -623,9 +623,9 @@ Property '{property}' on entity type '{entityType}' is part of a primary or alternate key, but has a constant default value set. Constant default values are not useful for primary or alternate keys since these properties must always have non-null unique values. Warning RelationalEventId.ModelValidationKeyDefaultValueWarning string string - + The key {keyProperties} on the entity type '{entityType}' cannot be represented in the database. Either all or some of the properties aren't mapped to table '{table}'. All key properties must be mapped to a single table for the unique constraint to be created. - Error RelationalEventId.KeyUnmappedProperties string string string + Error RelationalEventId.KeyPropertiesNotMappedToTable string string string Migrating using database '{database}' on server '{dataSource}'. @@ -804,6 +804,9 @@ The mapping strategy '{mappingStrategy}' specified on '{entityType}' is not supported for entity types with a discriminator. + + Both '{entityType}' and '{otherEntityType}' are mapped to the stored procedure '{sproc}'. All the entity types in a non-TPH hierarchy (one that doesn't have a discriminator) must be mapped to different stored procedures. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. + Both '{entityType}' and '{otherEntityType}' are mapped to the table '{table}'. All the entity types in a non-TPH hierarchy (one that doesn't have a discriminator) must be mapped to different tables. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. @@ -858,6 +861,39 @@ The property '{propertySpecification}' has specific configuration for the SQL query '{query}', but isn't mapped to a column on that query. Remove the specific configuration, or map an entity type that contains this property to '{query}'. + + The non-key property '{entityType}.{property}' is mapped to a parameter of the stored procedure '{sproc}', but only key properties are supported for Delete stored procedures. + + + The keyless entity type '{entityType}' was configured to use '{sproc}'. An entity type requires a primary key to be able to be mapped to a stored procedure. + + + The entity type '{entityType}' was configured to use '{sproc}', but the store name was not specified. Configure the stored procedure name explicitly. + + + The property '{propertySpecification}' has specific configuration for the stored procedure '{sproc}', but it isn't mapped to a parameter or a result column on that stored procedure. Remove the specific configuration, or map an entity type that contains this property to '{sproc}'. + + + No property named '{property}' found on the entity type '{entityType}' corresponding to the parameter on the stored procedure '{sproc}' + + + The entity type '{entityType}' is mapped to the stored procedure '{sproc}', however the properties {properties} are not mapped to any parameter or result column. + + + The property '{entityType}.{property}' is mapped to a result column of the stored procedure '{sproc}', but store-generated values are not supported for Delete stored procedures. + + + No property named '{property}' found on the entity type '{entityType}' corresponding to the result column on the stored procedure '{sproc}' + + + The property '{entityType}.{property}' is mapped to a result column of the stored procedure '{sproc}', but it is not configured as store-generated. + + + Both entity type '{entityType1}' and '{entityType2}' were configured to use '{sproc}', stored procedure sharing is not supported. Specify different names for the corresponding stored procedures. + + + Both '{entityType}' and '{otherEntityType}' are explicitly mapped to the stored procedure '{sproc}' using the 'TPH' mapping strategy. Configure the stored procedure mapping on the root entity type, including all parameters for the derived types. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. + The entity type '{entityType}' is not mapped to the store object '{table}'. @@ -865,7 +901,7 @@ The property '{propertySpecification}' has specific configuration for the table '{table}', but isn't mapped to a column on that table. Remove the specific configuration, or map an entity type that contains this property to '{table}'. - The element type of the result of '{dbFunction}' is mapped to '{entityType}'. This is not supported since '{entityType}' is part of hierarchy and does not contain a discriminator property. + The element type of the result of '{dbFunction}' is mapped to '{entityType}'. This is not supported since '{entityType}' is part of hierarchy but does not contain a discriminator property. Only TPH hierarchies can be mapped to a TVF. Timeout must be less than or equal to Int32.MaxValue (2147483647) seconds. Provided timeout: {seconds} seconds. @@ -877,11 +913,17 @@ The underlying reader doesn't have as many fields as expected. Expected: {expected}, actual: {actual}. - The entity type '{dependentType}' is mapped to '{storeObject}'. However the principal entity type '{principalEntityType}' is also mapped to '{storeObject}' and it's using the TPC mapping strategy. Only leaf entity types in a TPC hierarchy can use table-sharing. + The entity type '{dependentType}' is mapped to '{storeObject}'. However the principal entity type '{principalEntityType}' is also mapped to '{storeObject}' and it's using the TPC mapping strategy. Entity types in a TPC hierarchy can use table-sharing only if they have no derived types. The entity type '{dependentType}' is mapped to '{storeObject}'. However one of its derived types '{derivedType}' is mapped to '{otherStoreObject}'. Hierarchies using table-sharing cannot be mapped using the TPC mapping strategy. + + '{entityType}' is mapped to the database function '{function}' while '{otherEntityType}' is mapped to the database function '{otherFunction}'. Map all the entity types in the hierarchy to the same database function. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. + + + '{entityType}' is mapped to the stored procedure '{sproc}' while '{otherEntityType}' is mapped to the stored procedure '{otherSproc}'. Map all the entity types in the hierarchy to the same stored procedure, or remove the discriminator and map them all to different stored procedures. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. + '{entityType}' is mapped to the table '{table}' while '{otherEntityType}' is mapped to the table '{otherTable}'. Map all the entity types in the hierarchy to the same table, or remove the discriminator and map them all to different tables. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. @@ -960,4 +1002,4 @@ 'VisitChildren' must be overridden in the class deriving from 'SqlExpression'. - + \ No newline at end of file diff --git a/src/EFCore/Infrastructure/ConventionAnnotatable.cs b/src/EFCore/Infrastructure/ConventionAnnotatable.cs index 188128ad3c7..b48bd3cf6ba 100644 --- a/src/EFCore/Infrastructure/ConventionAnnotatable.cs +++ b/src/EFCore/Infrastructure/ConventionAnnotatable.cs @@ -133,21 +133,6 @@ public override void SetAnnotation(string name, object? value) public new virtual ConventionAnnotation? FindAnnotation(string name) => (ConventionAnnotation?)base.FindAnnotation(name); - /// - /// Removes the given annotation from this object. - /// - /// The annotation to remove. - /// The configuration source of the annotation to be removed. - /// The annotation that was removed. - public virtual ConventionAnnotation? RemoveAnnotation(string name, ConfigurationSource configurationSource) - { - var annotation = FindAnnotation(name); - return annotation == null - || !configurationSource.Overrides(annotation.GetConfigurationSource()) - ? null - : (ConventionAnnotation?)base.RemoveAnnotation(name); - } - /// protected override Annotation CreateAnnotation(string name, object? value) => CreateAnnotation(name, value, ConfigurationSource.Explicit); @@ -212,8 +197,8 @@ IConventionAnnotation IConventionAnnotatable.AddAnnotation(string name, object? /// [DebuggerStepThrough] - IConventionAnnotation? IConventionAnnotatable.RemoveAnnotation(string name, bool fromDataAnnotation) - => RemoveAnnotation(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + IConventionAnnotation? IConventionAnnotatable.RemoveAnnotation(string name) + => (IConventionAnnotation?)RemoveAnnotation(name); /// [DebuggerStepThrough] diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index 08e42a2cc90..4902eac65d0 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -604,7 +604,8 @@ protected virtual void ValidateDiscriminatorValues(IEntityType rootEntityType) if (!discriminatorProperty.ClrType.IsInstanceOfType(discriminatorValue)) { throw new InvalidOperationException( - CoreStrings.DiscriminatorValueIncompatible(discriminatorValue, discriminatorProperty.Name, discriminatorProperty.ClrType)); + CoreStrings.DiscriminatorValueIncompatible( + discriminatorValue, derivedType.DisplayName(), discriminatorProperty.ClrType.DisplayName())); } if (discriminatorValues.TryGetValue(discriminatorValue, out var duplicateEntityType)) diff --git a/src/EFCore/Metadata/IConventionAnnotatable.cs b/src/EFCore/Metadata/IConventionAnnotatable.cs index 52c89ffd1b5..71c8243bda6 100644 --- a/src/EFCore/Metadata/IConventionAnnotatable.cs +++ b/src/EFCore/Metadata/IConventionAnnotatable.cs @@ -66,9 +66,8 @@ public interface IConventionAnnotatable : IReadOnlyAnnotatable /// Removes the annotation with the given name from this object. /// /// The name of the annotation to remove. - /// Indicates whether the configuration was specified using a data annotation. /// The annotation that was removed. - IConventionAnnotation? RemoveAnnotation(string name, bool fromDataAnnotation = false); + IConventionAnnotation? RemoveAnnotation(string name); /// /// Gets the annotation with the given name, throwing if it does not exist. diff --git a/src/EFCore/Metadata/IConventionEntityType.cs b/src/EFCore/Metadata/IConventionEntityType.cs index 396cf991faa..4279dccbd3b 100644 --- a/src/EFCore/Metadata/IConventionEntityType.cs +++ b/src/EFCore/Metadata/IConventionEntityType.cs @@ -126,10 +126,9 @@ public interface IConventionEntityType : IReadOnlyEntityType, IConventionTypeBas /// /// Removes the discriminator value for this entity type. /// - /// Indicates whether the configuration was specified using a data annotation. /// The removed discriminator value. - object? RemoveDiscriminatorValue(bool fromDataAnnotation = false) - => RemoveAnnotation(CoreAnnotationNames.DiscriminatorValue, fromDataAnnotation)?.Value; + object? RemoveDiscriminatorValue() + => RemoveAnnotation(CoreAnnotationNames.DiscriminatorValue)?.Value; /// /// Gets the for the discriminator value. diff --git a/src/EFCore/Metadata/Internal/EntityType.cs b/src/EFCore/Metadata/Internal/EntityType.cs index b0ef33240dc..73db23e0735 100644 --- a/src/EFCore/Metadata/Internal/EntityType.cs +++ b/src/EFCore/Metadata/Internal/EntityType.cs @@ -3310,35 +3310,10 @@ public virtual ChangeTrackingStrategy GetChangeTrackingStrategy() { CheckDiscriminatorProperty(property); - if (((property == null && BaseType == null) - || (property != null && !property.ClrType.IsInstanceOfType(((IReadOnlyEntityType)this).GetDiscriminatorValue())))) - { - RemoveDiscriminatorValue(this, configurationSource); - if (BaseType == null) - { - foreach (var derivedType in GetDerivedTypes()) - { - RemoveDiscriminatorValue(derivedType, configurationSource); - } - } - } - return ((string?)SetAnnotation(CoreAnnotationNames.DiscriminatorProperty, property?.Name, configurationSource)?.Value) == property?.Name ? property : (Property?)((IReadOnlyEntityType)this).FindDiscriminatorProperty(); - - static void RemoveDiscriminatorValue(IReadOnlyEntityType entityType, ConfigurationSource configurationSource) - { - if (configurationSource is ConfigurationSource.Convention or ConfigurationSource.DataAnnotation) - { - ((IConventionEntityType)entityType).RemoveDiscriminatorValue(configurationSource == ConfigurationSource.DataAnnotation); - } - else - { - ((IMutableEntityType)entityType).RemoveDiscriminatorValue(); - } - } } private void CheckDiscriminatorProperty(Property? property) diff --git a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs index e62abe7e9fe..5978cfcd461 100644 --- a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs @@ -4854,6 +4854,9 @@ public virtual bool CanSetServiceOnlyConstructorBinding( RemoveUnusedDiscriminatorProperty(discriminatorProperty, configurationSource); rootTypeBuilder.Metadata.SetDiscriminatorProperty(discriminatorProperty, configurationSource); + + RemoveIncompatibleDiscriminatorValues(Metadata, discriminatorProperty, configurationSource); + discriminatorPropertyBuilder.IsRequired(true, ConfigurationSource.Convention); discriminatorPropertyBuilder.HasValueGeneratorFactory( typeof(DiscriminatorValueGeneratorFactory), ConfigurationSource.Convention); @@ -4861,6 +4864,35 @@ public virtual bool CanSetServiceOnlyConstructorBinding( return new DiscriminatorBuilder(Metadata); } + private void RemoveIncompatibleDiscriminatorValues( + EntityType entityType, + Property? newDiscriminatorProperty, + ConfigurationSource configurationSource) + { + if ((newDiscriminatorProperty != null || entityType.BaseType != null) + && (newDiscriminatorProperty == null + || newDiscriminatorProperty.ClrType.IsInstanceOfType(((IReadOnlyEntityType)entityType).GetDiscriminatorValue()))) + { + return; + } + + if (configurationSource.Overrides(((IConventionEntityType)entityType).GetDiscriminatorValueConfigurationSource())) + { + ((IMutableEntityType)entityType).RemoveDiscriminatorValue(); + } + + if (entityType.BaseType == null) + { + foreach (var derivedType in entityType.GetDerivedTypes()) + { + if (configurationSource.Overrides(((IConventionEntityType)derivedType).GetDiscriminatorValueConfigurationSource())) + { + ((IMutableEntityType)derivedType).RemoveDiscriminatorValue(); + } + } + } + } + /// /// 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 @@ -4881,6 +4913,8 @@ public virtual bool CanSetServiceOnlyConstructorBinding( } Metadata.SetDiscriminatorProperty(null, configurationSource); + + RemoveIncompatibleDiscriminatorValues(Metadata, null, configurationSource); if (configurationSource == ConfigurationSource.Explicit) { diff --git a/src/EFCore/ModelBuilder.cs b/src/EFCore/ModelBuilder.cs index 0c7562029c3..f1207ee42cd 100644 --- a/src/EFCore/ModelBuilder.cs +++ b/src/EFCore/ModelBuilder.cs @@ -88,6 +88,18 @@ public ModelBuilder() _builder = new Model().Builder; } + /// + /// 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. + /// + [EntityFrameworkInternal] + public ModelBuilder(IMutableModel model) + { + _builder = ((Model)model).Builder; + } + /// /// The model being configured. /// diff --git a/test/EFCore.Cosmos.FunctionalTests/CustomConvertersCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/CustomConvertersCosmosTest.cs index 085d5e6b97d..8b422d97c29 100644 --- a/test/EFCore.Cosmos.FunctionalTests/CustomConvertersCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/CustomConvertersCosmosTest.cs @@ -13,11 +13,6 @@ public CustomConvertersCosmosTest(CustomConvertersCosmosFixture fixture) Fixture.TestSqlLoggerFactory.Clear(); } - public override void Can_perform_query_with_max_length() - { - // Over the 2Mb document limit - } - [ConditionalTheory(Skip = "Issue #17246 No Explicit Convert")] public override Task Can_filter_projection_with_inline_enum_variable(bool async) => base.Can_filter_projection_with_inline_enum_variable(async); diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs index 5cba542dca9..f816e4b7468 100644 --- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs +++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs @@ -1094,7 +1094,8 @@ public void Unmapped_entity_types_are_stored_in_the_model_snapshot() builder => { builder.HasDefaultSchema("default"); - builder.Entity().Ignore(e => e.EntityWithTwoProperties).ToTable((string)null); + builder.Entity().Ignore(e => e.EntityWithTwoProperties).ToTable((string)null) + .UpdateUsingStoredProcedure("Update", "sproc", p => p.HasParameter(e => e.Id)); }, AddBoilerPlate( @" diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 99afb33690f..574fb3bdb5c 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -797,7 +797,7 @@ public void Detects_unnamed_key_properties_mapped_to_different_fragments_in_enti modelBuilder.Entity().HasAlternateKey(nameof(Animal.Name), nameof(Cat.Identity)); var definition = RelationalResources - .LogKeyUnmappedProperties(new TestLogger()); + .LogKeyPropertiesNotMappedToTable(new TestLogger()); VerifyWarning( definition.GenerateMessage( "{'Name', 'Identity'}", @@ -1795,6 +1795,18 @@ public virtual void Passes_for_missing_discriminator_value_for_abstract_class() Validate(modelBuilder); } + [ConditionalFact] + public virtual void Detects_derived_entity_type_mapped_to_a_different_SQL_query() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity().ToSqlQuery("sql"); + modelBuilder.Entity().ToSqlQuery("sql2"); + + VerifyError( + RelationalStrings.InvalidMappedSqlQueryDerivedType(nameof(Cat), nameof(Animal)), + modelBuilder); + } + [ConditionalFact] public virtual void Passes_for_TPT() { @@ -2031,7 +2043,6 @@ public virtual void Detects_ToTable_for_abstract_class_TPC() { var modelBuilder = CreateConventionModelBuilder(); modelBuilder.Entity().ToTable("Abstract", "dbo").UseTpcMappingStrategy(); - modelBuilder.Entity(); modelBuilder.Entity>(); VerifyError( @@ -2044,7 +2055,6 @@ public virtual void Detects_ToView_for_abstract_class_TPC() { var modelBuilder = CreateConventionModelBuilder(); modelBuilder.Entity().ToView("Abstract").UseTpcMappingStrategy(); - modelBuilder.Entity(); modelBuilder.Entity>(); VerifyError( @@ -2057,7 +2067,6 @@ public virtual void Detects_ToFunction_for_abstract_class_TPC() { var modelBuilder = CreateConventionModelBuilder(); modelBuilder.Entity().ToFunction("Abstract").UseTpcMappingStrategy(); - modelBuilder.Entity(); modelBuilder.Entity>(); VerifyError( @@ -2065,6 +2074,19 @@ public virtual void Detects_ToFunction_for_abstract_class_TPC() modelBuilder); } + [ConditionalFact] + public virtual void Detects_UsingStoredProcedure_for_abstract_class_TPC() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity().InsertUsingStoredProcedure("Insert", s => s.HasParameter(s => s.Id)) + .UseTpcMappingStrategy(); + modelBuilder.Entity>(); + + VerifyError( + RelationalStrings.AbstractTpc(nameof(Abstract), "Insert"), + modelBuilder); + } + [ConditionalFact] public virtual void Detects_clashing_entity_types_in_views_TPC() { @@ -2334,6 +2356,18 @@ public virtual void Detects_invalid_sql_query_overrides() RelationalStrings.SqlQueryOverrideMismatch("Animal.Name", "Dog"), modelBuilder); } + + [ConditionalFact] + public virtual void Detects_invalid_stored_procedure_overrides() + { + var modelBuilder = CreateConventionModelBuilder(); + var property = modelBuilder.Entity().Property(a => a.Name).GetInfrastructure(); + property.HasColumnName("DogName", StoreObjectIdentifier.InsertStoredProcedure("Dog")); + + VerifyError( + RelationalStrings.StoredProcedureOverrideMismatch("Animal.Name", "Dog"), + modelBuilder); + } [ConditionalFact] public virtual void Detects_invalid_function_overrides() @@ -2506,19 +2540,366 @@ public void Detects_derived_entity_type_mapped_to_a_function() { var modelBuilder = CreateConventionModelBuilder(); - var function = modelBuilder.HasDbFunction(TestMethods.MethodAMi).Metadata; + var function = modelBuilder.HasDbFunction(BaseTestMethods.MethodAMi).Metadata; - modelBuilder.Entity().HasNoKey(); + modelBuilder.Entity().ToFunction(function.ModelName).HasNoKey(); modelBuilder.Entity().ToFunction(function.ModelName); VerifyError( RelationalStrings.InvalidMappedFunctionDerivedType( nameof(TestMethods), - "Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidatorTest+TestMethods.MethodA()", + "Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidatorTest+BaseTestMethods.MethodA()", nameof(BaseTestMethods)), modelBuilder); } + [ConditionalFact] + public void Detects_derived_entity_type_mapped_to_a_different_function() + { + var modelBuilder = CreateConventionModelBuilder(); + + var function = modelBuilder.HasDbFunction(BaseTestMethods.MethodAMi).Metadata; + var function2 = modelBuilder.HasDbFunction(TestMethods.MethodAMi).Metadata; + + modelBuilder.Entity().ToFunction(function.ModelName).HasNoKey(); + modelBuilder.Entity().ToFunction(function2.ModelName); + + VerifyError( + RelationalStrings.TphDbFunctionMismatch( + nameof(TestMethods), + "Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidatorTest+TestMethods.MethodA()", + nameof(BaseTestMethods), + "Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidatorTest+BaseTestMethods.MethodA()"), + modelBuilder); + } + + [ConditionalFact] + public void Detects_multiple_entity_types_mapped_to_the_same_stored_procedure() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity( + db => + { + db.HasBaseType((string)null); + db.OwnsOne(d => d.SomeTestMethods).DeleteUsingStoredProcedure("Delete", + s => s.HasParameter("DerivedTestMethodsId")); + db.OwnsOne(d => d.OtherTestMethods).DeleteUsingStoredProcedure("Delete", + s => s.HasParameter("DerivedTestMethodsId")); + }); + + VerifyError( + RelationalStrings.StoredProcedureTableSharing( + "DerivedTestMethods.OtherTestMethods#TestMethods", + "DerivedTestMethods.SomeTestMethods#TestMethods", + "Delete"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_keyless_entity_type_mapped_to_a_stored_procedure() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .Ignore(a => a.FavoritePerson) + .HasNoKey() + .InsertUsingStoredProcedure(s => s + .HasParameter(c => c.Id) + .HasParameter(c => c.Name)); + + VerifyError( + RelationalStrings.StoredProcedureKeyless(nameof(Animal), "Animal_Insert"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_derived_entity_type_mapped_to_a_stored_procedure_in_TPH() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity().HasDiscriminator("Discriminator"); + modelBuilder.Entity().UpdateUsingStoredProcedure("Update", s => s.HasParameter(c => c.Breed)); + + VerifyError( + RelationalStrings.TphStoredProcedureMismatch(nameof(Cat), "Update", nameof(Animal), ""), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_derived_entity_type_mapped_to_a_different_stored_procedure_in_TPH() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity().UpdateUsingStoredProcedure("Update", s => s.HasParameter(c => c.Id)) + .HasDiscriminator("Discriminator"); + modelBuilder.Entity().UpdateUsingStoredProcedure("Update2", s => s.HasParameter(c => c.Breed)); + + VerifyError( + RelationalStrings.TphStoredProcedureMismatch(nameof(Cat), "Update2", nameof(Animal), "Update"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_derived_entity_type_mapped_to_a_different_stored_procedure_instance_in_TPH() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity().UpdateUsingStoredProcedure("Update", s => s.HasParameter(c => c.Id)); + modelBuilder.Entity().UpdateUsingStoredProcedure("Update", s => s.HasParameter(c => c.Breed)); + + VerifyError( + RelationalStrings.StoredProcedureTphDuplicate(nameof(Cat), nameof(Animal), "Update"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_missing_stored_procedure_parameters_in_TPH() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .UpdateUsingStoredProcedure("Update", s => s + .HasParameter(a => a.Id, p => p.HasName("MyId")) + .HasParameter(a => a.Name) + .HasParameter((Cat c) => c.Breed) + .HasResultColumn(a => a.Name)) + .Property(a => a.Name).ValueGeneratedOnUpdate(); + modelBuilder.Entity(); + + VerifyError( + RelationalStrings.StoredProcedurePropertiesNotMapped(nameof(Animal), "Update", "{'FavoritePersonId', 'Identity'}"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_unmatched_stored_procedure_parameters_in_TPH() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .UpdateUsingStoredProcedure("Update", s => s.HasParameter("Missing")) + .Property(a => a.Name).ValueGeneratedOnUpdate(); + + VerifyError( + RelationalStrings.StoredProcedureParameterNotFound("Missing", nameof(Animal), "Update"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_non_key_delete_stored_procedure_params_in_TPH() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .DeleteUsingStoredProcedure(s => s.HasParameter(a => a.Id) + .HasParameter(a => a.Name)) + .Property(a => a.Name).ValueGeneratedOnUpdate(); + + VerifyError( + RelationalStrings.StoredProcedureDeleteNonKeyProperty(nameof(Animal), nameof(Animal.Name), "Animal_Delete"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_unmatched_stored_procedure_result_columns_in_TPH() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .UpdateUsingStoredProcedure("Update", "dbo", s => s.HasResultColumn("Missing")); + + VerifyError( + RelationalStrings.StoredProcedureResultColumnNotFound("Missing", nameof(Animal), "dbo.Update"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_non_generated_insert_stored_procedure_result_columns_in_TPH() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .InsertUsingStoredProcedure(s => s.HasResultColumn(a => a.Name)); + modelBuilder.Entity(); + + VerifyError( + RelationalStrings.StoredProcedureResultColumnNotGenerated(nameof(Animal), nameof(Animal.Name), "Animal_Insert"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_non_generated_update_stored_procedure_result_columns_in_TPT() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .Ignore(a => a.FavoritePerson) + .UpdateUsingStoredProcedure(s => s.HasParameter(a => a.Id).HasParameter(a => a.Name)) + .UseTptMappingStrategy(); + modelBuilder.Entity() + .UpdateUsingStoredProcedure("Update", s => s.HasResultColumn(c => c.Breed)); + + VerifyError( + RelationalStrings.StoredProcedureResultColumnNotGenerated(nameof(Cat), nameof(Cat.Breed), "Update"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_delete_stored_procedure_result_columns_in_TPH() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .DeleteUsingStoredProcedure("Delete", s => s.HasResultColumn(a => a.Name)) + .Property(a => a.Name).ValueGeneratedOnUpdate(); + + VerifyError( + RelationalStrings.StoredProcedureResultColumnDelete(nameof(Animal), nameof(Animal.Name), "Delete"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Passes_on_valid_UsingDeleteStoredProcedure_in_TPT() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .UseTptMappingStrategy() + .DeleteUsingStoredProcedure("Delete", s => s.HasParameter(a => a.Id)) + .Property(a => a.Name).ValueGeneratedOnUpdate(); + modelBuilder.Entity() + .DeleteUsingStoredProcedure(s => s.HasParameter(a => a.Id)); + + Validate(modelBuilder); + } + + [ConditionalFact] + public virtual void Passes_on_derived_entity_type_mapped_to_a_stored_procedure_in_TPT() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity().UseTptMappingStrategy(); + modelBuilder.Entity().UpdateUsingStoredProcedure("Update", s => s + .HasParameter(c => c.Id) + .HasParameter(c => c.Breed) + .HasParameter(c => c.Identity)); + + Validate(modelBuilder); + } + + [ConditionalFact] + public virtual void Passes_on_derived_entity_type_not_mapped_to_a_stored_procedure_in_TPT() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .UseTptMappingStrategy() + .UpdateUsingStoredProcedure("Update", s => s + .HasParameter(a => a.Id, p => p.HasName("MyId")) + .HasParameter(a => a.Name) + .HasParameter("FavoritePersonId") + .HasResultColumn(a => a.Name)) + .Property(a => a.Name).ValueGeneratedOnUpdate(); + modelBuilder.Entity(); + + Validate(modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_missing_stored_procedure_parameters_in_TPT() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .UseTptMappingStrategy() + .UpdateUsingStoredProcedure("Update", s => s + .HasParameter(a => a.Id, p => p.HasName("MyId")) + .HasParameter(a => a.Name) + .HasParameter("FavoritePersonId") + .HasResultColumn(a => a.Name)) + .Property(a => a.Name).ValueGeneratedOnUpdate(); + modelBuilder.Entity() + .UpdateUsingStoredProcedure(s => s.HasParameter(c => c.Breed)); + + VerifyError( + RelationalStrings.StoredProcedurePropertiesNotMapped(nameof(Cat), "Cat_Update", "{'Identity', 'Id'}"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_unmatched_stored_procedure_parameters_in_TPT() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .UseTptMappingStrategy() + .UpdateUsingStoredProcedure("Update", s => s.HasParameter((Cat c) => c.Breed)); + modelBuilder.Entity(); + + VerifyError( + RelationalStrings.StoredProcedureParameterNotFound(nameof(Cat.Breed), nameof(Animal), "Update"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_unmatched_stored_procedure_result_columns_in_TPT() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .UpdateUsingStoredProcedure("Update", "dbo", s => s.HasResultColumn((Cat c) => c.Breed)) + .UseTptMappingStrategy(); + modelBuilder.Entity(); + + VerifyError( + RelationalStrings.StoredProcedureResultColumnNotFound(nameof(Cat.Breed), nameof(Animal), "dbo.Update"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_InsertUsingStoredProcedure_without_a_name() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity().InsertUsingStoredProcedure(s => { }).UseTpcMappingStrategy(); + modelBuilder.Entity>(); + + VerifyError( + RelationalStrings.StoredProcedureNoName(nameof(Abstract), "InsertStoredProcedure"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_missing_stored_procedure_parameters_in_TPC() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .UseTpcMappingStrategy() + .UpdateUsingStoredProcedure("Update", s => s + .HasParameter(a => a.Id, p => p.HasName("MyId")) + .HasParameter(a => a.Name) + .HasParameter("FavoritePersonId") + .HasResultColumn(a => a.Name)) + .Property(a => a.Name).ValueGeneratedOnUpdate(); + modelBuilder.Entity() + .UpdateUsingStoredProcedure(s => s.HasParameter(c => c.Breed).HasParameter(a => a.Name)); + + VerifyError( + RelationalStrings.StoredProcedurePropertiesNotMapped(nameof(Cat), "Cat_Update", "{'Identity', 'Id', 'FavoritePersonId'}"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_unmatched_stored_procedure_parameters_in_TPC() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .UseTpcMappingStrategy() + .UpdateUsingStoredProcedure("Update", s => s.HasParameter((Cat c) => c.Breed)); + modelBuilder.Entity(); + + VerifyError( + RelationalStrings.StoredProcedureParameterNotFound(nameof(Cat.Breed), nameof(Animal), "Update"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_unmatched_stored_procedure_result_columns_in_TPC() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .UpdateUsingStoredProcedure("Update", "dbo", s => s.HasResultColumn((Cat c) => c.Breed)) + .UseTpcMappingStrategy(); + modelBuilder.Entity(); + + VerifyError( + RelationalStrings.StoredProcedureResultColumnNotFound(nameof(Cat.Breed), nameof(Animal), "dbo.Update"), + modelBuilder); + } + [ConditionalFact] public void Passes_for_unnamed_index_with_all_properties_not_mapped_to_any_table() { @@ -2832,6 +3213,10 @@ public TestDecimalToDecimalConverter() private class BaseTestMethods { + public static readonly MethodInfo MethodAMi = typeof(BaseTestMethods).GetTypeInfo().GetDeclaredMethod(nameof(MethodA)); + + public static IQueryable MethodA() + => throw new NotImplementedException(); } private class DerivedTestMethods : TestMethods @@ -2843,14 +3228,14 @@ private class DerivedTestMethods : TestMethods private class TestMethods : BaseTestMethods { - public static readonly MethodInfo MethodAMi = typeof(TestMethods).GetTypeInfo().GetDeclaredMethod(nameof(MethodA)); + public static readonly new MethodInfo MethodAMi = typeof(TestMethods).GetTypeInfo().GetDeclaredMethod(nameof(MethodA)); public static readonly MethodInfo MethodBMi = typeof(TestMethods).GetTypeInfo().GetDeclaredMethod(nameof(MethodB)); public static readonly MethodInfo MethodCMi = typeof(TestMethods).GetTypeInfo().GetDeclaredMethod(nameof(MethodC)); public static readonly MethodInfo MethodDMi = typeof(TestMethods).GetTypeInfo().GetDeclaredMethod(nameof(MethodD)); public static readonly MethodInfo MethodEMi = typeof(TestMethods).GetTypeInfo().GetDeclaredMethod(nameof(MethodE)); public static readonly MethodInfo MethodFMi = typeof(TestMethods).GetTypeInfo().GetDeclaredMethod(nameof(MethodF)); - public static IQueryable MethodA() + public static new IQueryable MethodA() => throw new NotImplementedException(); public static IQueryable MethodB(int id) diff --git a/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderTest.cs b/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderTest.cs index b9c4b25fcc1..df861550b26 100644 --- a/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderTest.cs +++ b/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderTest.cs @@ -14,23 +14,24 @@ public abstract class RelationalNonRelationshipTestBase : NonRelationshipTestBas public virtual void Can_use_table_splitting() { var modelBuilder = CreateModelBuilder(); - modelBuilder.Entity().SplitToTable("OrderDetails", s => - { - s.ExcludeFromMigrations(); - var propertyBuilder = s.Property(o => o.CustomerId); - var columnBuilder = propertyBuilder.HasColumnName("id"); - if (columnBuilder is IInfrastructure> genericBuilder) + modelBuilder.Entity().SplitToTable( + "OrderDetails", s => { - Assert.IsType>(genericBuilder.Instance.GetInfrastructure>()); - Assert.IsAssignableFrom(genericBuilder.GetInfrastructure().Overrides); - } - else - { - var nonGenericBuilder = (IInfrastructure)columnBuilder; - Assert.IsAssignableFrom(nonGenericBuilder.Instance.GetInfrastructure()); - Assert.IsAssignableFrom(nonGenericBuilder.Instance.Overrides); - } - }); + s.ExcludeFromMigrations(); + var propertyBuilder = s.Property(o => o.CustomerId); + var columnBuilder = propertyBuilder.HasColumnName("id"); + if (columnBuilder is IInfrastructure> genericBuilder) + { + Assert.IsType>(genericBuilder.Instance.GetInfrastructure>()); + Assert.IsAssignableFrom(genericBuilder.GetInfrastructure().Overrides); + } + else + { + var nonGenericBuilder = (IInfrastructure)columnBuilder; + Assert.IsAssignableFrom(nonGenericBuilder.Instance.GetInfrastructure()); + Assert.IsAssignableFrom(nonGenericBuilder.Instance.Overrides); + } + }); modelBuilder.Ignore(); modelBuilder.Ignore(); @@ -55,9 +56,10 @@ public virtual void Can_use_table_splitting_with_schema() { var modelBuilder = CreateModelBuilder(); modelBuilder.Entity().ToTable("Order", "dbo") - .SplitToTable("OrderDetails", "sch", s => - s.ExcludeFromMigrations() - .Property(o => o.CustomerId).HasColumnName("id")); + .SplitToTable( + "OrderDetails", "sch", s => + s.ExcludeFromMigrations() + .Property(o => o.CustomerId).HasColumnName("id")); modelBuilder.Ignore(); modelBuilder.Ignore(); @@ -68,9 +70,12 @@ public virtual void Can_use_table_splitting_with_schema() Assert.False(entity.IsTableExcludedFromMigrations()); Assert.False(entity.IsTableExcludedFromMigrations(StoreObjectIdentifier.Table("Order", "dbo"))); Assert.True(entity.IsTableExcludedFromMigrations(StoreObjectIdentifier.Table("OrderDetails", "sch"))); - Assert.Same(entity.GetMappingFragments().Single(), entity.FindMappingFragment(StoreObjectIdentifier.Table("OrderDetails", "sch"))); - Assert.Equal(RelationalStrings.TableNotMappedEntityType(nameof(Order), "Order"), - Assert.Throws(() => entity.IsTableExcludedFromMigrations(StoreObjectIdentifier.Table("Order"))).Message); + Assert.Same( + entity.GetMappingFragments().Single(), entity.FindMappingFragment(StoreObjectIdentifier.Table("OrderDetails", "sch"))); + Assert.Equal( + RelationalStrings.TableNotMappedEntityType(nameof(Order), "Order"), + Assert.Throws(() => entity.IsTableExcludedFromMigrations(StoreObjectIdentifier.Table("Order"))) + .Message); var customerId = entity.FindProperty(nameof(Order.CustomerId))!; Assert.Equal("CustomerId", customerId.GetColumnName()); @@ -85,22 +90,23 @@ public virtual void Can_use_view_splitting() { var modelBuilder = CreateModelBuilder(); modelBuilder.Entity().ToView("Order") - .SplitToView("OrderDetails", s => - { - var propertyBuilder = s.Property(o => o.CustomerId); - var columnBuilder = propertyBuilder.HasColumnName("id"); - if (columnBuilder is IInfrastructure> genericBuilder) - { - Assert.IsType>(genericBuilder.Instance.GetInfrastructure>()); - Assert.IsAssignableFrom(genericBuilder.GetInfrastructure().Overrides); - } - else - { - var nonGenericBuilder = (IInfrastructure)columnBuilder; - Assert.IsAssignableFrom(nonGenericBuilder.Instance.GetInfrastructure()); - Assert.IsAssignableFrom(nonGenericBuilder.Instance.Overrides); - } - }); + .SplitToView( + "OrderDetails", s => + { + var propertyBuilder = s.Property(o => o.CustomerId); + var columnBuilder = propertyBuilder.HasColumnName("id"); + if (columnBuilder is IInfrastructure> genericBuilder) + { + Assert.IsType>(genericBuilder.Instance.GetInfrastructure>()); + Assert.IsAssignableFrom(genericBuilder.GetInfrastructure().Overrides); + } + else + { + var nonGenericBuilder = (IInfrastructure)columnBuilder; + Assert.IsAssignableFrom(nonGenericBuilder.Instance.GetInfrastructure()); + Assert.IsAssignableFrom(nonGenericBuilder.Instance.Overrides); + } + }); modelBuilder.Ignore(); modelBuilder.Ignore(); @@ -122,8 +128,9 @@ public virtual void Can_use_view_splitting_with_schema() { var modelBuilder = CreateModelBuilder(); modelBuilder.Entity().ToView("Order", "dbo") - .SplitToView("OrderDetails", "sch", s => - s.Property(o => o.CustomerId).HasColumnName("id")); + .SplitToView( + "OrderDetails", "sch", s => + s.Property(o => o.CustomerId).HasColumnName("id")); modelBuilder.Ignore(); modelBuilder.Ignore(); @@ -131,9 +138,12 @@ public virtual void Can_use_view_splitting_with_schema() var entity = model.FindEntityType(typeof(Order))!; - Assert.Same(entity.GetMappingFragments().Single(), entity.FindMappingFragment(StoreObjectIdentifier.View("OrderDetails", "sch"))); - Assert.Equal(RelationalStrings.TableNotMappedEntityType(nameof(Order), "Order"), - Assert.Throws(() => entity.IsTableExcludedFromMigrations(StoreObjectIdentifier.View("Order"))).Message); + Assert.Same( + entity.GetMappingFragments().Single(), entity.FindMappingFragment(StoreObjectIdentifier.View("OrderDetails", "sch"))); + Assert.Equal( + RelationalStrings.TableNotMappedEntityType(nameof(Order), "Order"), + Assert.Throws(() => entity.IsTableExcludedFromMigrations(StoreObjectIdentifier.View("Order"))) + .Message); var customerId = entity.FindProperty(nameof(Order.CustomerId))!; Assert.Equal("CustomerId", customerId.GetColumnName()); @@ -146,6 +156,180 @@ public virtual void Can_use_view_splitting_with_schema() public abstract class RelationalInheritanceTestBase : InheritanceTestBase { + [ConditionalFact] + public virtual void Can_use_table_splitting() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.Entity().SplitToTable( + "OrderDetails", s => + { + s.ExcludeFromMigrations(); + var propertyBuilder = s.Property(o => o.CustomerId); + var columnBuilder = propertyBuilder.HasColumnName("id"); + if (columnBuilder is IInfrastructure> genericBuilder) + { + Assert.IsType>(genericBuilder.Instance.GetInfrastructure>()); + Assert.IsAssignableFrom(genericBuilder.GetInfrastructure().Overrides); + } + else + { + var nonGenericBuilder = (IInfrastructure)columnBuilder; + Assert.IsAssignableFrom(nonGenericBuilder.Instance.GetInfrastructure()); + Assert.IsAssignableFrom(nonGenericBuilder.Instance.Overrides); + } + }); + modelBuilder.Ignore(); + modelBuilder.Ignore(); + + var model = modelBuilder.FinalizeModel(); + + var entity = model.FindEntityType(typeof(Order))!; + + Assert.False(entity.IsTableExcludedFromMigrations()); + Assert.False(entity.IsTableExcludedFromMigrations(StoreObjectIdentifier.Table("Order"))); + Assert.True(entity.IsTableExcludedFromMigrations(StoreObjectIdentifier.Table("OrderDetails"))); + Assert.Same(entity.GetMappingFragments().Single(), entity.FindMappingFragment(StoreObjectIdentifier.Table("OrderDetails"))); + + var customerId = entity.FindProperty(nameof(Order.CustomerId))!; + Assert.Equal("CustomerId", customerId.GetColumnName()); + Assert.Null(customerId.GetColumnName(StoreObjectIdentifier.Table("Order"))); + Assert.Equal("id", customerId.GetColumnName(StoreObjectIdentifier.Table("OrderDetails"))); + Assert.Same(customerId.GetOverrides().Single(), customerId.FindOverrides(StoreObjectIdentifier.Table("OrderDetails"))); + } + + [ConditionalFact] + public virtual void Sproc_overrides_update_when_renamed_in_TPH() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Ignore(); + modelBuilder.Ignore(); + + modelBuilder.Entity() + .Ignore(s => s.SpecialBookLabel) + .InsertUsingStoredProcedure( + s => s.SuppressTransactions().HasAnnotation("foo", "bar1") + .HasParameter(b => b.BookId) + .HasParameter("Discriminator") + .HasResultColumn( + b => b.Id, p => + { + var resultColumnBuilder = p.HasName("InsertId"); + var nonGenericBuilder = (IInfrastructure)resultColumnBuilder; + Assert.IsAssignableFrom(nonGenericBuilder.Instance.GetInfrastructure()); + })) + .UpdateUsingStoredProcedure( + s => s.SuppressTransactions().HasAnnotation("foo", "bar2") + .HasParameter( + b => b.Id, p => + { + var parameterBuilder = p.HasName("UpdateId"); + var nonGenericBuilder = (IInfrastructure)parameterBuilder; + Assert.IsAssignableFrom(nonGenericBuilder.Instance.GetInfrastructure()); + }) + .HasParameter(b => b.BookId)) + .DeleteUsingStoredProcedure( + s => s.SuppressTransactions().HasAnnotation("foo", "bar3") + .HasParameter(b => b.Id, p => p.HasName("DeleteId"))); + + modelBuilder.Entity() + .Ignore(s => s.BookLabel); + modelBuilder.Entity(); + + modelBuilder.Entity() + .InsertUsingStoredProcedure("Insert", s => { }) + .UpdateUsingStoredProcedure("Update", "dbo", s => { }) + .DeleteUsingStoredProcedure("BookLabel_Delete", s => { }); + + var model = modelBuilder.FinalizeModel(); + + var bookLabel = model.FindEntityType(typeof(BookLabel))!; + var insertSproc = bookLabel.GetInsertStoredProcedure()!; + Assert.Equal("Insert", insertSproc.Name); + Assert.Null(insertSproc.Schema); + Assert.Equal(new[] { "BookId", "Discriminator" }, insertSproc.Parameters); + Assert.Equal(new[] { "Id" }, insertSproc.ResultColumns); + Assert.True(insertSproc.AreTransactionsSuppressed); + Assert.Equal("bar1", insertSproc["foo"]); + Assert.Same(bookLabel, insertSproc.EntityType); + + var updateSproc = bookLabel.GetUpdateStoredProcedure()!; + Assert.Equal("Update", updateSproc.Name); + Assert.Equal("dbo", updateSproc.Schema); + Assert.Equal(new[] { "Id", "BookId" }, updateSproc.Parameters); + Assert.Empty(updateSproc.ResultColumns); + Assert.True(updateSproc.AreTransactionsSuppressed); + Assert.Equal("bar2", updateSproc["foo"]); + Assert.Same(bookLabel, updateSproc.EntityType); + + var deleteSproc = bookLabel.GetDeleteStoredProcedure()!; + Assert.Equal("BookLabel_Delete", deleteSproc.Name); + Assert.Null(deleteSproc.Schema); + Assert.Equal(new[] { "Id" }, deleteSproc.Parameters); + Assert.Empty(deleteSproc.ResultColumns); + Assert.True(deleteSproc.AreTransactionsSuppressed); + Assert.Equal("bar3", deleteSproc["foo"]); + Assert.Same(bookLabel, deleteSproc.EntityType); + + var id = bookLabel.FindProperty(nameof(BookLabel.Id))!; + Assert.Equal(3, id.GetOverrides().Count()); + Assert.Equal( + "InsertId", + id.GetColumnName(StoreObjectIdentifier.Create(bookLabel, StoreObjectType.InsertStoredProcedure)!.Value)); + Assert.Equal( + "UpdateId", + id.GetColumnName(StoreObjectIdentifier.Create(bookLabel, StoreObjectType.UpdateStoredProcedure)!.Value)); + Assert.Equal( + "DeleteId", + id.GetColumnName(StoreObjectIdentifier.Create(bookLabel, StoreObjectType.DeleteStoredProcedure)!.Value)); + + var specialBookLabel = model.FindEntityType(typeof(SpecialBookLabel))!; + Assert.Same(insertSproc, specialBookLabel.GetInsertStoredProcedure()); + Assert.Same(updateSproc, specialBookLabel.GetUpdateStoredProcedure()); + Assert.Same(deleteSproc, specialBookLabel.GetDeleteStoredProcedure()); + + var extraSpecialBookLabel = model.FindEntityType(typeof(ExtraSpecialBookLabel))!; + Assert.Same(insertSproc, extraSpecialBookLabel.GetInsertStoredProcedure()); + Assert.Same(updateSproc, extraSpecialBookLabel.GetUpdateStoredProcedure()); + Assert.Same(deleteSproc, extraSpecialBookLabel.GetDeleteStoredProcedure()); + } + + [ConditionalFact] + public virtual void Sproc_overrides_are_removed_with_sproc() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Ignore(); + modelBuilder.Ignore(); + modelBuilder.Ignore(); + + modelBuilder.Entity() + .Ignore(s => s.SpecialBookLabel) + .InsertUsingStoredProcedure( + s => s.HasParameter(b => b.BookId) + .HasResultColumn(b => b.Id, p => p.HasName("InsertId"))) + .UpdateUsingStoredProcedure( + s => s.HasParameter(b => b.Id, p => p.HasName("UpdateId")) + .HasParameter(b => b.BookId)) + .DeleteUsingStoredProcedure( + s => s.HasParameter(b => b.Id, p => p.HasName("DeleteId"))); + + var bookLabelEntityType = modelBuilder.Entity().Metadata; + + bookLabelEntityType.RemoveInsertStoredProcedure(); + bookLabelEntityType.RemoveUpdateStoredProcedure(); + bookLabelEntityType.RemoveDeleteStoredProcedure(); + + var model = modelBuilder.FinalizeModel(); + + var bookLabel = model.FindEntityType(typeof(BookLabel))!; + Assert.Null(bookLabel.GetInsertStoredProcedure()); + Assert.Null(bookLabel.GetUpdateStoredProcedure()); + Assert.Null(bookLabel.GetDeleteStoredProcedure()); + + var id = bookLabel.FindProperty(nameof(BookLabel.Id))!; + Assert.Empty(id.GetOverrides()); + } } public abstract class RelationalOneToManyTestBase : OneToManyTestBase @@ -180,23 +364,24 @@ public virtual void Can_use_table_splitting_with_owned_reference() { lb.Ignore(l => l.Book); lb.Property("ShadowProp"); - - lb.SplitToTable("BookLabelDetails", s => - { - var propertyBuilder = s.Property(o => o.Id); - var columnBuilder = propertyBuilder.HasColumnName("bid"); - if (columnBuilder is IInfrastructure> genericBuilder) - { - Assert.IsType>(genericBuilder.Instance.GetInfrastructure>()); - Assert.IsAssignableFrom(genericBuilder.GetInfrastructure().Overrides); - } - else + + lb.SplitToTable( + "BookLabelDetails", s => { - var nonGenericBuilder = (IInfrastructure)columnBuilder; - Assert.IsAssignableFrom(nonGenericBuilder.Instance.GetInfrastructure()); - Assert.IsAssignableFrom(nonGenericBuilder.Instance.Overrides); - } - }); + var propertyBuilder = s.Property(o => o.Id); + var columnBuilder = propertyBuilder.HasColumnName("bid"); + if (columnBuilder is IInfrastructure> genericBuilder) + { + Assert.IsType>(genericBuilder.Instance.GetInfrastructure>()); + Assert.IsAssignableFrom(genericBuilder.GetInfrastructure().Overrides); + } + else + { + var nonGenericBuilder = (IInfrastructure)columnBuilder; + Assert.IsAssignableFrom(nonGenericBuilder.Instance.GetInfrastructure()); + Assert.IsAssignableFrom(nonGenericBuilder.Instance.Overrides); + } + }); }); modelBuilder.Entity() .OwnsOne(b => b.AlternateLabel); @@ -250,22 +435,23 @@ public virtual void Can_use_view_splitting_with_owned_collection() r.Property("ShadowProp"); r.ToView("Order"); - r.SplitToView("OrderDetails", s => - { - var propertyBuilder = s.Property(o => o.AnotherCustomerId); - var columnBuilder = propertyBuilder.HasColumnName("cid"); - if (columnBuilder is IInfrastructure> genericBuilder) - { - Assert.IsType>(genericBuilder.Instance.GetInfrastructure>()); - Assert.IsAssignableFrom(genericBuilder.GetInfrastructure().Overrides); - } - else + r.SplitToView( + "OrderDetails", s => { - var nonGenericBuilder = (IInfrastructure)columnBuilder; - Assert.IsAssignableFrom(nonGenericBuilder.Instance.GetInfrastructure()); - Assert.IsAssignableFrom(nonGenericBuilder.Instance.Overrides); - } - }); + var propertyBuilder = s.Property(o => o.AnotherCustomerId); + var columnBuilder = propertyBuilder.HasColumnName("cid"); + if (columnBuilder is IInfrastructure> genericBuilder) + { + Assert.IsType>(genericBuilder.Instance.GetInfrastructure>()); + Assert.IsAssignableFrom(genericBuilder.GetInfrastructure().Overrides); + } + else + { + var nonGenericBuilder = (IInfrastructure)columnBuilder; + Assert.IsAssignableFrom(nonGenericBuilder.Instance.GetInfrastructure()); + Assert.IsAssignableFrom(nonGenericBuilder.Instance.Overrides); + } + }); }); var model = modelBuilder.FinalizeModel(); @@ -292,6 +478,111 @@ public virtual void Can_use_view_splitting_with_owned_collection() Assert.True(((IConventionRelationalPropertyOverrides)overrides).IsInModel); Assert.Same(anotherCustomerId, overrides.Property); } + + [ConditionalFact] + public virtual void Can_use_sproc_mapping_with_owned_reference() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Ignore(); + modelBuilder.Ignore(); + modelBuilder.Ignore(); + + modelBuilder.Entity().OwnsOne( + b => b.Label, lb => + { + lb.Ignore(l => l.Book); + lb.Ignore(s => s.SpecialBookLabel); + + lb.Property(l => l.Id).ValueGeneratedOnUpdate(); + + lb.InsertUsingStoredProcedure( + s => s.SuppressTransactions().HasAnnotation("foo", "bar1") + .HasParameter(b => b.Id) + .HasParameter(b => b.BookId, p => p.HasName("InsertId"))) + .UpdateUsingStoredProcedure( + s => s.SuppressTransactions().HasAnnotation("foo", "bar2") + .HasParameter(b => b.Id) + .HasParameter(b => b.BookId, p => + { + var parameterBuilder = p.HasName("UpdateId"); + var nonGenericBuilder = (IInfrastructure)parameterBuilder; + Assert.IsAssignableFrom(nonGenericBuilder.Instance.GetInfrastructure()); + }) + .HasResultColumn( + b => b.Id, p => + { + var nonGenericBuilder = (IInfrastructure)p; + Assert.IsAssignableFrom(nonGenericBuilder.Instance.GetInfrastructure()); + })) + .DeleteUsingStoredProcedure( + s => s.SuppressTransactions().HasAnnotation("foo", "bar3") + .HasParameter(b => b.BookId, p => p.HasName("DeleteId"))); + }); + modelBuilder.Entity() + .OwnsOne(b => b.AlternateLabel); + + modelBuilder.Entity().OwnsOne( + b => b.Label, lb => + { + lb.InsertUsingStoredProcedure("Insert", s => { }); + lb.UpdateUsingStoredProcedure("Update", "dbo", s => { }); + lb.DeleteUsingStoredProcedure("BookLabel_Delete", s => { }); + }); + + var model = modelBuilder.FinalizeModel(); + + Assert.Equal(2, model.GetEntityTypes().Count(e => e.ClrType == typeof(BookLabel))); + Assert.Equal(3, model.GetEntityTypes().Count()); + + var book = model.FindEntityType(typeof(Book))!; + var bookOwnership1 = book.FindNavigation(nameof(Book.Label))!.ForeignKey; + var bookOwnership2 = book.FindNavigation(nameof(Book.AlternateLabel))!.ForeignKey; + Assert.NotSame(bookOwnership1.DeclaringEntityType, bookOwnership2.DeclaringEntityType); + + var insertSproc = bookOwnership1.DeclaringEntityType.GetInsertStoredProcedure()!; + Assert.Equal("Insert", insertSproc.Name); + Assert.Null(insertSproc.Schema); + Assert.Equal(new[] { "Id", "BookId" }, insertSproc.Parameters); + Assert.Empty(insertSproc.ResultColumns); + Assert.True(insertSproc.AreTransactionsSuppressed); + Assert.Equal("bar1", insertSproc["foo"]); + Assert.Same(bookOwnership1.DeclaringEntityType, insertSproc.EntityType); + + var updateSproc = bookOwnership1.DeclaringEntityType.GetUpdateStoredProcedure()!; + Assert.Equal("Update", updateSproc.Name); + Assert.Equal("dbo", updateSproc.Schema); + Assert.Equal(new[] { "Id", "BookId" }, updateSproc.Parameters); + Assert.Equal(new[] { "Id" }, updateSproc.ResultColumns); + Assert.True(updateSproc.AreTransactionsSuppressed); + Assert.Equal("bar2", updateSproc["foo"]); + Assert.Same(bookOwnership1.DeclaringEntityType, updateSproc.EntityType); + + var deleteSproc = bookOwnership1.DeclaringEntityType.GetDeleteStoredProcedure()!; + Assert.Equal("BookLabel_Delete", deleteSproc.Name); + Assert.Null(deleteSproc.Schema); + Assert.Equal(new[] { "BookId" }, deleteSproc.Parameters); + Assert.Empty(deleteSproc.ResultColumns); + Assert.True(deleteSproc.AreTransactionsSuppressed); + Assert.Equal("bar3", deleteSproc["foo"]); + Assert.Same(bookOwnership1.DeclaringEntityType, deleteSproc.EntityType); + + var bookId = bookOwnership1.DeclaringEntityType.FindProperty(nameof(BookLabel.BookId))!; + Assert.Equal(3, bookId.GetOverrides().Count()); + Assert.Equal( + "InsertId", + bookId.GetColumnName(StoreObjectIdentifier.Create(bookOwnership1.DeclaringEntityType, StoreObjectType.InsertStoredProcedure)!.Value)); + Assert.Equal( + "UpdateId", + bookId.GetColumnName(StoreObjectIdentifier.Create(bookOwnership1.DeclaringEntityType, StoreObjectType.UpdateStoredProcedure)!.Value)); + Assert.Equal( + "DeleteId", + bookId.GetColumnName(StoreObjectIdentifier.Create(bookOwnership1.DeclaringEntityType, StoreObjectType.DeleteStoredProcedure)!.Value)); + + Assert.Null(bookOwnership2.DeclaringEntityType.GetInsertStoredProcedure()); + Assert.Null(bookOwnership2.DeclaringEntityType.GetUpdateStoredProcedure()); + Assert.Null(bookOwnership2.DeclaringEntityType.GetDeleteStoredProcedure()); + } } public abstract class TestTableBuilder @@ -414,7 +705,8 @@ public override string? Name public override string? Schema => TableBuilder.Schema; - OwnedNavigationTableBuilder IInfrastructure>.Instance + OwnedNavigationTableBuilder + IInfrastructure>.Instance => TableBuilder; protected virtual TestOwnedNavigationTableBuilder Wrap( @@ -432,7 +724,8 @@ public override TestColumnBuilder Property(Expression : - TestOwnedNavigationTableBuilder, IInfrastructure + TestOwnedNavigationTableBuilder, + IInfrastructure where TOwnerEntity : class where TDependentEntity : class { @@ -585,7 +878,8 @@ public override string? Name public override string? Schema => TableBuilder.Schema; - OwnedNavigationSplitTableBuilder IInfrastructure>.Instance + OwnedNavigationSplitTableBuilder + IInfrastructure>.Instance => TableBuilder; protected virtual TestOwnedNavigationSplitTableBuilder Wrap( @@ -603,7 +897,8 @@ public override TestColumnBuilder Property(Expression : - TestOwnedNavigationSplitTableBuilder, IInfrastructure + TestOwnedNavigationSplitTableBuilder, + IInfrastructure where TOwnerEntity : class where TDependentEntity : class { @@ -623,7 +918,8 @@ public override string? Schema OwnedNavigationSplitTableBuilder IInfrastructure.Instance => TableBuilder; - protected virtual TestOwnedNavigationSplitTableBuilder Wrap(OwnedNavigationSplitTableBuilder tableBuilder) + protected virtual TestOwnedNavigationSplitTableBuilder Wrap( + OwnedNavigationSplitTableBuilder tableBuilder) => new NonGenericTestOwnedNavigationSplitTableBuilder(tableBuilder); public override TestOwnedNavigationSplitTableBuilder ExcludeFromMigrations(bool excluded = true) @@ -759,7 +1055,8 @@ public abstract class TestOwnedNavigationViewBuilder Property(string propertyName); - public abstract TestViewColumnBuilder Property(Expression> propertyExpression); + public abstract TestViewColumnBuilder Property( + Expression> propertyExpression); } public class GenericTestOwnedNavigationViewBuilder : @@ -781,7 +1078,8 @@ public override string? Name public override string? Schema => ViewBuilder.Schema; - OwnedNavigationViewBuilder IInfrastructure>.Instance + OwnedNavigationViewBuilder + IInfrastructure>.Instance => ViewBuilder; protected virtual TestOwnedNavigationViewBuilder Wrap( @@ -791,12 +1089,14 @@ protected virtual TestOwnedNavigationViewBuilder public override TestViewColumnBuilder Property(string propertyName) => new NonGenericTestViewColumnBuilder(ViewBuilder.Property(propertyName)); - public override TestViewColumnBuilder Property(Expression> propertyExpression) + public override TestViewColumnBuilder Property( + Expression> propertyExpression) => new GenericTestViewColumnBuilder(ViewBuilder.Property(propertyExpression.GetPropertyAccess().Name)); } public class NonGenericTestOwnedNavigationViewBuilder : - TestOwnedNavigationViewBuilder, IInfrastructure + TestOwnedNavigationViewBuilder, + IInfrastructure where TOwnerEntity : class where TDependentEntity : class { @@ -822,7 +1122,8 @@ protected virtual TestOwnedNavigationViewBuilder public override TestViewColumnBuilder Property(string propertyName) => new NonGenericTestViewColumnBuilder(ViewBuilder.Property(propertyName)); - public override TestViewColumnBuilder Property(Expression> propertyExpression) + public override TestViewColumnBuilder Property( + Expression> propertyExpression) => new GenericTestViewColumnBuilder(ViewBuilder.Property(propertyExpression.GetPropertyAccess().Name)); } @@ -906,7 +1207,8 @@ public abstract class TestOwnedNavigationSplitViewBuilder Property(string propertyName); - public abstract TestViewColumnBuilder Property(Expression> propertyExpression); + public abstract TestViewColumnBuilder Property( + Expression> propertyExpression); } public class GenericTestOwnedNavigationSplitViewBuilder : @@ -928,7 +1230,8 @@ public override string? Name public override string? Schema => ViewBuilder.Schema; - OwnedNavigationSplitViewBuilder IInfrastructure>.Instance + OwnedNavigationSplitViewBuilder + IInfrastructure>.Instance => ViewBuilder; protected virtual TestOwnedNavigationSplitViewBuilder Wrap( @@ -938,12 +1241,14 @@ protected virtual TestOwnedNavigationSplitViewBuilder Property(string propertyName) => new NonGenericTestViewColumnBuilder(ViewBuilder.Property(propertyName)); - public override TestViewColumnBuilder Property(Expression> propertyExpression) + public override TestViewColumnBuilder Property( + Expression> propertyExpression) => new GenericTestViewColumnBuilder(ViewBuilder.Property(propertyExpression.GetPropertyAccess().Name)); } public class NonGenericTestOwnedNavigationSplitViewBuilder : - TestOwnedNavigationSplitViewBuilder, IInfrastructure + TestOwnedNavigationSplitViewBuilder, + IInfrastructure where TOwnerEntity : class where TDependentEntity : class { @@ -963,13 +1268,15 @@ public override string? Schema OwnedNavigationSplitViewBuilder IInfrastructure.Instance => ViewBuilder; - protected virtual TestOwnedNavigationSplitViewBuilder Wrap(OwnedNavigationSplitViewBuilder tableBuilder) + protected virtual TestOwnedNavigationSplitViewBuilder Wrap( + OwnedNavigationSplitViewBuilder tableBuilder) => new NonGenericTestOwnedNavigationSplitViewBuilder(tableBuilder); public override TestViewColumnBuilder Property(string propertyName) => new NonGenericTestViewColumnBuilder(ViewBuilder.Property(propertyName)); - public override TestViewColumnBuilder Property(Expression> propertyExpression) + public override TestViewColumnBuilder Property( + Expression> propertyExpression) => new GenericTestViewColumnBuilder(ViewBuilder.Property(propertyExpression.GetPropertyAccess().Name)); } @@ -997,7 +1304,8 @@ public override TestViewColumnBuilder HasColumnName(string? name) => Wrap(ViewColumnBuilder.HasColumnName(name)); } - public class NonGenericTestViewColumnBuilder : TestViewColumnBuilder, IInfrastructure + public class NonGenericTestViewColumnBuilder + : TestViewColumnBuilder, IInfrastructure { public NonGenericTestViewColumnBuilder(ViewColumnBuilder tableBuilder) { @@ -1009,13 +1317,481 @@ public NonGenericTestViewColumnBuilder(ViewColumnBuilder tableBuilder) ViewColumnBuilder IInfrastructure.Instance => ViewColumnBuilder; - protected virtual TestViewColumnBuilder Wrap(ViewColumnBuilder tableBuilder) - => new NonGenericTestViewColumnBuilder(tableBuilder); + protected virtual TestViewColumnBuilder Wrap(ViewColumnBuilder viewColumnBuilder) + => new NonGenericTestViewColumnBuilder(viewColumnBuilder); public override TestViewColumnBuilder HasColumnName(string? name) => Wrap(ViewColumnBuilder.HasColumnName(name)); } + public abstract class TestStoredProcedureBuilder + where TEntity : class + { + public abstract TestStoredProcedureBuilder HasParameter( + string propertyName); + + public abstract TestStoredProcedureBuilder HasParameter( + string propertyName, + Action buildAction); + + public abstract TestStoredProcedureBuilder HasParameter( + Expression> propertyExpression); + + public abstract TestStoredProcedureBuilder HasParameter( + Expression> propertyExpression, + Action buildAction); + + public abstract TestStoredProcedureBuilder HasParameter( + Expression> propertyExpression) + where TDerivedEntity : class, TEntity; + + public abstract TestStoredProcedureBuilder HasParameter( + Expression> propertyExpression, + Action buildAction) + where TDerivedEntity : class, TEntity; + + public abstract TestStoredProcedureBuilder HasResultColumn( + string propertyName); + + public abstract TestStoredProcedureBuilder HasResultColumn( + string propertyName, + Action buildAction); + + public abstract TestStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression); + + public abstract TestStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression, + Action buildAction); + + public abstract TestStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression) + where TDerivedEntity : class, TEntity; + + public abstract TestStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression, + Action buildAction) + where TDerivedEntity : class, TEntity; + + public abstract TestStoredProcedureBuilder SuppressTransactions(bool suppress = true); + + //public abstract StoredProcedureBuilder HasRowsAffectedParameter( + // Action> buildAction); + + public abstract TestStoredProcedureBuilder HasAnnotation(string annotation, object? value); + } + + public class GenericTestStoredProcedureBuilder + : TestStoredProcedureBuilder, IInfrastructure> + where TEntity : class + { + public GenericTestStoredProcedureBuilder(StoredProcedureBuilder storedProcedureBuilder) + { + StoredProcedureBuilder = storedProcedureBuilder; + } + + private StoredProcedureBuilder StoredProcedureBuilder { get; } + + StoredProcedureBuilder IInfrastructure>.Instance + => StoredProcedureBuilder; + + protected virtual TestStoredProcedureBuilder Wrap(StoredProcedureBuilder storedProcedureBuilder) + => new GenericTestStoredProcedureBuilder(storedProcedureBuilder); + + public override TestStoredProcedureBuilder HasParameter( + string propertyName) + => Wrap(StoredProcedureBuilder.HasParameter(propertyName)); + + public override TestStoredProcedureBuilder HasParameter( + string propertyName, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasParameter(propertyName, s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder HasParameter( + Expression> propertyExpression) + => Wrap(StoredProcedureBuilder.HasParameter(propertyExpression)); + + public override TestStoredProcedureBuilder HasParameter( + Expression> propertyExpression, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasParameter(propertyExpression, s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder HasParameter( + Expression> propertyExpression) + => Wrap(StoredProcedureBuilder.HasParameter(propertyExpression)); + + public override TestStoredProcedureBuilder HasParameter( + Expression> propertyExpression, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasParameter(propertyExpression, s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder HasResultColumn( + string propertyName) + => Wrap(StoredProcedureBuilder.HasResultColumn(propertyName)); + + public override TestStoredProcedureBuilder HasResultColumn( + string propertyName, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasResultColumn(propertyName, s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression) + => Wrap(StoredProcedureBuilder.HasResultColumn(propertyExpression)); + + public override TestStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasResultColumn(propertyExpression, s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression) + => Wrap(StoredProcedureBuilder.HasResultColumn(propertyExpression)); + + public override TestStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasResultColumn(propertyExpression, s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder SuppressTransactions(bool suppress) + => Wrap(StoredProcedureBuilder.SuppressTransactions(suppress)); + + public override TestStoredProcedureBuilder HasAnnotation(string annotation, object? value) + => Wrap(StoredProcedureBuilder.HasAnnotation(annotation, value)); + } + + public class NonGenericTestStoredProcedureBuilder + : TestStoredProcedureBuilder, IInfrastructure + where TEntity : class + { + public NonGenericTestStoredProcedureBuilder(StoredProcedureBuilder storedProcedureBuilder) + { + StoredProcedureBuilder = storedProcedureBuilder; + } + + private StoredProcedureBuilder StoredProcedureBuilder { get; } + + StoredProcedureBuilder IInfrastructure.Instance + => StoredProcedureBuilder; + + protected virtual TestStoredProcedureBuilder Wrap(StoredProcedureBuilder storedProcedureBuilder) + => new NonGenericTestStoredProcedureBuilder(storedProcedureBuilder); + + public override TestStoredProcedureBuilder HasParameter( + string propertyName) + => Wrap(StoredProcedureBuilder.HasParameter(propertyName)); + + public override TestStoredProcedureBuilder HasParameter( + string propertyName, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasParameter(propertyName, s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder HasParameter( + Expression> propertyExpression) + => Wrap(StoredProcedureBuilder.HasParameter(propertyExpression.GetMemberAccess().Name)); + + public override TestStoredProcedureBuilder HasParameter( + Expression> propertyExpression, + Action buildAction) + => Wrap( + StoredProcedureBuilder.HasParameter( + propertyExpression.GetMemberAccess().Name, s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder HasParameter( + Expression> propertyExpression) + => Wrap(StoredProcedureBuilder.HasParameter(propertyExpression.GetMemberAccess().Name)); + + public override TestStoredProcedureBuilder HasParameter( + Expression> propertyExpression, + Action buildAction) + => Wrap( + StoredProcedureBuilder.HasParameter( + propertyExpression.GetMemberAccess().Name, s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder HasResultColumn( + string propertyName) + => Wrap(StoredProcedureBuilder.HasResultColumn(propertyName)); + + public override TestStoredProcedureBuilder HasResultColumn( + string propertyName, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasResultColumn(propertyName, s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression) + => Wrap(StoredProcedureBuilder.HasResultColumn(propertyExpression.GetMemberAccess().Name)); + + public override TestStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression, + Action buildAction) + => Wrap( + StoredProcedureBuilder.HasResultColumn( + propertyExpression.GetMemberAccess().Name, s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression) + => Wrap(StoredProcedureBuilder.HasResultColumn(propertyExpression.GetMemberAccess().Name)); + + public override TestStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression, + Action buildAction) + => Wrap( + StoredProcedureBuilder.HasResultColumn( + propertyExpression.GetMemberAccess().Name, s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder SuppressTransactions(bool suppress) + => Wrap(StoredProcedureBuilder.SuppressTransactions(suppress)); + + public override TestStoredProcedureBuilder HasAnnotation(string annotation, object? value) + => Wrap(StoredProcedureBuilder.HasAnnotation(annotation, value)); + } + + public abstract class TestOwnedNavigationStoredProcedureBuilder + where TOwnerEntity : class + where TDependentEntity : class + { + public abstract TestOwnedNavigationStoredProcedureBuilder HasParameter( + string propertyName); + + public abstract TestOwnedNavigationStoredProcedureBuilder HasParameter( + string propertyName, + Action buildAction); + + public abstract TestOwnedNavigationStoredProcedureBuilder HasParameter( + Expression> propertyExpression); + + public abstract TestOwnedNavigationStoredProcedureBuilder HasParameter( + Expression> propertyExpression, + Action buildAction); + + public abstract TestOwnedNavigationStoredProcedureBuilder HasResultColumn( + string propertyName); + + public abstract TestOwnedNavigationStoredProcedureBuilder HasResultColumn( + string propertyName, + Action buildAction); + + public abstract TestOwnedNavigationStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression); + + public abstract TestOwnedNavigationStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression, + Action buildAction); + + public abstract TestOwnedNavigationStoredProcedureBuilder + SuppressTransactions(bool suppress = true); + + //public abstract StoredProcedureBuilder HasRowsAffectedParameter( + // Action> buildAction); + + public abstract TestOwnedNavigationStoredProcedureBuilder HasAnnotation( + string annotation, + object? value); + } + + public class GenericTestOwnedNavigationStoredProcedureBuilder + : TestOwnedNavigationStoredProcedureBuilder, + IInfrastructure> + where TOwnerEntity : class + where TDependentEntity : class + { + public GenericTestOwnedNavigationStoredProcedureBuilder( + OwnedNavigationStoredProcedureBuilder storedProcedureBuilder) + { + StoredProcedureBuilder = storedProcedureBuilder; + } + + private OwnedNavigationStoredProcedureBuilder StoredProcedureBuilder { get; } + + OwnedNavigationStoredProcedureBuilder + IInfrastructure>.Instance + => StoredProcedureBuilder; + + protected virtual TestOwnedNavigationStoredProcedureBuilder Wrap( + OwnedNavigationStoredProcedureBuilder storedProcedureBuilder) + => new GenericTestOwnedNavigationStoredProcedureBuilder(storedProcedureBuilder); + + public override TestOwnedNavigationStoredProcedureBuilder HasParameter( + string propertyName) + => Wrap(StoredProcedureBuilder.HasParameter(propertyName)); + + public override TestOwnedNavigationStoredProcedureBuilder HasParameter( + string propertyName, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasParameter(propertyName, s => buildAction(new(s)))); + + public override TestOwnedNavigationStoredProcedureBuilder HasParameter( + Expression> propertyExpression) + => Wrap(StoredProcedureBuilder.HasParameter(propertyExpression)); + + public override TestOwnedNavigationStoredProcedureBuilder HasParameter( + Expression> propertyExpression, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasParameter(propertyExpression, s => buildAction(new(s)))); + + public override TestOwnedNavigationStoredProcedureBuilder HasResultColumn( + string propertyName) + => Wrap(StoredProcedureBuilder.HasResultColumn(propertyName)); + + public override TestOwnedNavigationStoredProcedureBuilder HasResultColumn( + string propertyName, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasResultColumn(propertyName, s => buildAction(new(s)))); + + public override TestOwnedNavigationStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression) + => Wrap(StoredProcedureBuilder.HasResultColumn(propertyExpression)); + + public override TestOwnedNavigationStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasResultColumn(propertyExpression, s => buildAction(new(s)))); + + public override TestOwnedNavigationStoredProcedureBuilder SuppressTransactions(bool suppress) + => Wrap(StoredProcedureBuilder.SuppressTransactions(suppress)); + + public override TestOwnedNavigationStoredProcedureBuilder HasAnnotation( + string annotation, + object? value) + => Wrap(StoredProcedureBuilder.HasAnnotation(annotation, value)); + } + + public class NonGenericTestOwnedNavigationStoredProcedureBuilder + : TestOwnedNavigationStoredProcedureBuilder, + IInfrastructure + where TOwnerEntity : class + where TDependentEntity : class + { + public NonGenericTestOwnedNavigationStoredProcedureBuilder(OwnedNavigationStoredProcedureBuilder storedProcedureBuilder) + { + StoredProcedureBuilder = storedProcedureBuilder; + } + + private OwnedNavigationStoredProcedureBuilder StoredProcedureBuilder { get; } + + OwnedNavigationStoredProcedureBuilder IInfrastructure.Instance + => StoredProcedureBuilder; + + protected virtual TestOwnedNavigationStoredProcedureBuilder Wrap( + OwnedNavigationStoredProcedureBuilder storedProcedureBuilder) + => new NonGenericTestOwnedNavigationStoredProcedureBuilder(storedProcedureBuilder); + + public override TestOwnedNavigationStoredProcedureBuilder HasParameter( + string propertyName) + => Wrap(StoredProcedureBuilder.HasParameter(propertyName)); + + public override TestOwnedNavigationStoredProcedureBuilder HasParameter( + string propertyName, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasParameter(propertyName, s => buildAction(new(s)))); + + public override TestOwnedNavigationStoredProcedureBuilder HasParameter( + Expression> propertyExpression) + => Wrap(StoredProcedureBuilder.HasParameter(propertyExpression.GetMemberAccess().Name)); + + public override TestOwnedNavigationStoredProcedureBuilder HasParameter( + Expression> propertyExpression, + Action buildAction) + => Wrap( + StoredProcedureBuilder.HasParameter( + propertyExpression.GetMemberAccess().Name, s => buildAction(new(s)))); + + public override TestOwnedNavigationStoredProcedureBuilder HasResultColumn( + string propertyName) + => Wrap(StoredProcedureBuilder.HasResultColumn(propertyName)); + + public override TestOwnedNavigationStoredProcedureBuilder HasResultColumn( + string propertyName, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasResultColumn(propertyName, s => buildAction(new(s)))); + + public override TestOwnedNavigationStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression) + => Wrap(StoredProcedureBuilder.HasResultColumn(propertyExpression.GetMemberAccess().Name)); + + public override TestOwnedNavigationStoredProcedureBuilder HasResultColumn( + Expression> propertyExpression, + Action buildAction) + => Wrap( + StoredProcedureBuilder.HasResultColumn( + propertyExpression.GetMemberAccess().Name, s => buildAction(new(s)))); + + public override TestOwnedNavigationStoredProcedureBuilder SuppressTransactions(bool suppress) + => Wrap(StoredProcedureBuilder.SuppressTransactions(suppress)); + + public override TestOwnedNavigationStoredProcedureBuilder HasAnnotation( + string annotation, + object? value) + => Wrap(StoredProcedureBuilder.HasAnnotation(annotation, value)); + } + + public class TestStoredProcedureParameterBuilder : IInfrastructure + { + public TestStoredProcedureParameterBuilder(StoredProcedureParameterBuilder storedProcedureParameterBuilder) + { + StoredProcedureParameterBuilder = storedProcedureParameterBuilder; + } + + private StoredProcedureParameterBuilder StoredProcedureParameterBuilder { get; } + + StoredProcedureParameterBuilder IInfrastructure.Instance + => StoredProcedureParameterBuilder; + + protected virtual TestStoredProcedureParameterBuilder Wrap(StoredProcedureParameterBuilder storedProcedureParameterBuilder) + => new TestStoredProcedureParameterBuilder(storedProcedureParameterBuilder); + + public TestStoredProcedureParameterBuilder HasName(string? name) + => Wrap(StoredProcedureParameterBuilder.HasName(name)); + + //public TestStoredProcedureParameterBuilder IsOutput(bool isOutput = true); + } + + public class TestStoredProcedureResultColumnBuilder : IInfrastructure + { + public TestStoredProcedureResultColumnBuilder(StoredProcedureResultColumnBuilder storedProcedureResultColumnBuilder) + { + StoredProcedureResultColumnBuilder = storedProcedureResultColumnBuilder; + } + + private StoredProcedureResultColumnBuilder StoredProcedureResultColumnBuilder { get; } + + StoredProcedureResultColumnBuilder IInfrastructure.Instance + => StoredProcedureResultColumnBuilder; + + protected virtual TestStoredProcedureResultColumnBuilder Wrap(StoredProcedureResultColumnBuilder storedProcedureResultColumnBuilder) + => new TestStoredProcedureResultColumnBuilder(storedProcedureResultColumnBuilder); + + public TestStoredProcedureResultColumnBuilder HasName(string? name) + => Wrap(StoredProcedureResultColumnBuilder.HasName(name)); + } + + //public static ModelBuilderTest.TestEntityTypeBuilder ToFunction( + // this ModelBuilderTest.TestEntityTypeBuilder builder, + // string name, + // Action> buildAction) + // where TEntity : class + //{ + // return builder; + //} + + //public abstract class TestTableValuedFunctionBuilder : DbFunctionBuilderBase + // where TEntity : class + //{ + // protected TestTableValuedFunctionBuilder(IMutableDbFunction function) : base(function) + // { + // } + + // public new abstract TestTableValuedFunctionBuilder HasName(string name); + + // public new abstract TestTableValuedFunctionBuilder HasSchema(string? schema); + + // public abstract TestTableValuedFunctionBuilder HasParameter( + // Expression> propertyExpression); + + // public abstract TestTableValuedFunctionBuilder HasParameter( + // Expression> propertyExpression, + // Action> buildAction); + //} + public abstract class TestTriggerBuilder { public abstract TestTriggerBuilder HasName(string name); diff --git a/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs b/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs index 093ed314b8d..5571e43a974 100644 --- a/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs +++ b/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs @@ -524,12 +524,14 @@ public static ModelBuilderTest.TestEntityTypeBuilder ToView( switch (builder) { case IInfrastructure> genericBuilder: - genericBuilder.Instance.ToView(name, + genericBuilder.Instance.ToView( + name, b => buildAction( new RelationalModelBuilderTest.GenericTestViewBuilder(b))); break; case IInfrastructure nonGenericBuilder: - nonGenericBuilder.Instance.ToView(name, + nonGenericBuilder.Instance.ToView( + name, b => buildAction( new RelationalModelBuilderTest.NonGenericTestViewBuilder(b))); break; @@ -548,11 +550,13 @@ public static ModelBuilderTest.TestEntityTypeBuilder ToView( switch (builder) { case IInfrastructure> genericBuilder: - genericBuilder.Instance.ToView(name, schema, + genericBuilder.Instance.ToView( + name, schema, b => buildAction(new RelationalModelBuilderTest.GenericTestViewBuilder(b))); break; case IInfrastructure nonGenericBuilder: - nonGenericBuilder.Instance.ToView(name, schema, + nonGenericBuilder.Instance.ToView( + name, schema, b => buildAction(new RelationalModelBuilderTest.NonGenericTestViewBuilder(b))); break; } @@ -609,12 +613,14 @@ public static ModelBuilderTest.TestOwnedNavigationBuilder> genericBuilder: - genericBuilder.Instance.ToView(name, + genericBuilder.Instance.ToView( + name, b => buildAction( new RelationalModelBuilderTest.GenericTestOwnedNavigationViewBuilder(b))); break; case IInfrastructure nonGenericBuilder: - nonGenericBuilder.Instance.ToView(name, + nonGenericBuilder.Instance.ToView( + name, b => buildAction( new RelationalModelBuilderTest.NonGenericTestOwnedNavigationViewBuilder(b))); break; @@ -634,12 +640,14 @@ public static ModelBuilderTest.TestOwnedNavigationBuilder> genericBuilder: - genericBuilder.Instance.ToView(name, schema, + genericBuilder.Instance.ToView( + name, schema, b => buildAction( new RelationalModelBuilderTest.GenericTestOwnedNavigationViewBuilder(b))); break; case IInfrastructure nonGenericBuilder: - nonGenericBuilder.Instance.ToView(name, schema, + nonGenericBuilder.Instance.ToView( + name, schema, b => buildAction( new RelationalModelBuilderTest.NonGenericTestOwnedNavigationViewBuilder(b))); break; @@ -657,12 +665,14 @@ public static ModelBuilderTest.TestEntityTypeBuilder SplitToView> genericBuilder: - genericBuilder.Instance.SplitToView(name, + genericBuilder.Instance.SplitToView( + name, b => buildAction( new RelationalModelBuilderTest.GenericTestSplitViewBuilder(b))); break; case IInfrastructure nonGenericBuilder: - nonGenericBuilder.Instance.SplitToView(name, + nonGenericBuilder.Instance.SplitToView( + name, b => buildAction( new RelationalModelBuilderTest.NonGenericTestSplitViewBuilder(b))); break; @@ -681,11 +691,13 @@ public static ModelBuilderTest.TestEntityTypeBuilder SplitToView> genericBuilder: - genericBuilder.Instance.SplitToView(name, schema, + genericBuilder.Instance.SplitToView( + name, schema, b => buildAction(new RelationalModelBuilderTest.GenericTestSplitViewBuilder(b))); break; case IInfrastructure nonGenericBuilder: - nonGenericBuilder.Instance.SplitToView(name, schema, + nonGenericBuilder.Instance.SplitToView( + name, schema, b => buildAction(new RelationalModelBuilderTest.NonGenericTestSplitViewBuilder(b))); break; } @@ -703,12 +715,14 @@ public static ModelBuilderTest.TestOwnedNavigationBuilder> genericBuilder: - genericBuilder.Instance.SplitToView(name, + genericBuilder.Instance.SplitToView( + name, b => buildAction( new RelationalModelBuilderTest.GenericTestOwnedNavigationSplitViewBuilder(b))); break; case IInfrastructure nonGenericBuilder: - nonGenericBuilder.Instance.SplitToView(name, + nonGenericBuilder.Instance.SplitToView( + name, b => buildAction( new RelationalModelBuilderTest.NonGenericTestOwnedNavigationSplitViewBuilder(b))); break; @@ -728,12 +742,14 @@ public static ModelBuilderTest.TestOwnedNavigationBuilder> genericBuilder: - genericBuilder.Instance.SplitToView(name, schema, + genericBuilder.Instance.SplitToView( + name, schema, b => buildAction( new RelationalModelBuilderTest.GenericTestOwnedNavigationSplitViewBuilder(b))); break; case IInfrastructure nonGenericBuilder: - nonGenericBuilder.Instance.SplitToView(name, schema, + nonGenericBuilder.Instance.SplitToView( + name, schema, b => buildAction( new RelationalModelBuilderTest.NonGenericTestOwnedNavigationSplitViewBuilder(b))); break; @@ -742,6 +758,462 @@ public static ModelBuilderTest.TestOwnedNavigationBuilder UpdateUsingStoredProcedure( + this ModelBuilderTest.TestEntityTypeBuilder builder, + Action> buildAction) + where TEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.UpdateUsingStoredProcedure( + b => buildAction(new RelationalModelBuilderTest.GenericTestStoredProcedureBuilder(b))); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.UpdateUsingStoredProcedure( + b => buildAction(new RelationalModelBuilderTest.NonGenericTestStoredProcedureBuilder(b))); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestEntityTypeBuilder UpdateUsingStoredProcedure( + this ModelBuilderTest.TestEntityTypeBuilder builder, + string name, + Action> buildAction) + where TEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.UpdateUsingStoredProcedure( + name, + b => buildAction(new RelationalModelBuilderTest.GenericTestStoredProcedureBuilder(b))); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.UpdateUsingStoredProcedure( + name, + b => buildAction(new RelationalModelBuilderTest.NonGenericTestStoredProcedureBuilder(b))); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestEntityTypeBuilder UpdateUsingStoredProcedure( + this ModelBuilderTest.TestEntityTypeBuilder builder, + string name, + string? schema, + Action> buildAction) + where TEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.UpdateUsingStoredProcedure( + name, schema, + b => buildAction(new RelationalModelBuilderTest.GenericTestStoredProcedureBuilder(b))); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.UpdateUsingStoredProcedure( + name, schema, + b => buildAction(new RelationalModelBuilderTest.NonGenericTestStoredProcedureBuilder(b))); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestOwnedNavigationBuilder UpdateUsingStoredProcedure( + this ModelBuilderTest.TestOwnedNavigationBuilder builder, + Action> buildAction) + where TOwnerEntity : class + where TDependentEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.UpdateUsingStoredProcedure( + b => buildAction( + new RelationalModelBuilderTest.GenericTestOwnedNavigationStoredProcedureBuilder( + b))); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.UpdateUsingStoredProcedure( + b => buildAction( + new RelationalModelBuilderTest.NonGenericTestOwnedNavigationStoredProcedureBuilder( + b))); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestOwnedNavigationBuilder UpdateUsingStoredProcedure( + this ModelBuilderTest.TestOwnedNavigationBuilder builder, + string name, + Action> buildAction) + where TOwnerEntity : class + where TDependentEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.UpdateUsingStoredProcedure( + name, + b => buildAction( + new RelationalModelBuilderTest.GenericTestOwnedNavigationStoredProcedureBuilder( + b))); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.UpdateUsingStoredProcedure( + name, + b => buildAction( + new RelationalModelBuilderTest.NonGenericTestOwnedNavigationStoredProcedureBuilder( + b))); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestOwnedNavigationBuilder UpdateUsingStoredProcedure( + this ModelBuilderTest.TestOwnedNavigationBuilder builder, + string name, + string? schema, + Action> buildAction) + where TOwnerEntity : class + where TDependentEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.UpdateUsingStoredProcedure( + name, schema, + b => buildAction( + new RelationalModelBuilderTest.GenericTestOwnedNavigationStoredProcedureBuilder( + b))); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.UpdateUsingStoredProcedure( + name, schema, + b => buildAction( + new RelationalModelBuilderTest.NonGenericTestOwnedNavigationStoredProcedureBuilder( + b))); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestEntityTypeBuilder InsertUsingStoredProcedure( + this ModelBuilderTest.TestEntityTypeBuilder builder, + Action> buildAction) + where TEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.InsertUsingStoredProcedure( + b => buildAction(new RelationalModelBuilderTest.GenericTestStoredProcedureBuilder(b))); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.InsertUsingStoredProcedure( + b => buildAction(new RelationalModelBuilderTest.NonGenericTestStoredProcedureBuilder(b))); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestEntityTypeBuilder InsertUsingStoredProcedure( + this ModelBuilderTest.TestEntityTypeBuilder builder, + string name, + Action> buildAction) + where TEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.InsertUsingStoredProcedure( + name, + b => buildAction(new RelationalModelBuilderTest.GenericTestStoredProcedureBuilder(b))); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.InsertUsingStoredProcedure( + name, + b => buildAction(new RelationalModelBuilderTest.NonGenericTestStoredProcedureBuilder(b))); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestEntityTypeBuilder InsertUsingStoredProcedure( + this ModelBuilderTest.TestEntityTypeBuilder builder, + string name, + string? schema, + Action> buildAction) + where TEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.InsertUsingStoredProcedure( + name, schema, + b => buildAction(new RelationalModelBuilderTest.GenericTestStoredProcedureBuilder(b))); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.InsertUsingStoredProcedure( + name, schema, + b => buildAction(new RelationalModelBuilderTest.NonGenericTestStoredProcedureBuilder(b))); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestOwnedNavigationBuilder InsertUsingStoredProcedure( + this ModelBuilderTest.TestOwnedNavigationBuilder builder, + Action> buildAction) + where TOwnerEntity : class + where TDependentEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.InsertUsingStoredProcedure( + b => buildAction( + new RelationalModelBuilderTest.GenericTestOwnedNavigationStoredProcedureBuilder( + b))); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.InsertUsingStoredProcedure( + b => buildAction( + new RelationalModelBuilderTest.NonGenericTestOwnedNavigationStoredProcedureBuilder( + b))); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestOwnedNavigationBuilder InsertUsingStoredProcedure( + this ModelBuilderTest.TestOwnedNavigationBuilder builder, + string name, + Action> buildAction) + where TOwnerEntity : class + where TDependentEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.InsertUsingStoredProcedure( + name, + b => buildAction( + new RelationalModelBuilderTest.GenericTestOwnedNavigationStoredProcedureBuilder( + b))); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.InsertUsingStoredProcedure( + name, + b => buildAction( + new RelationalModelBuilderTest.NonGenericTestOwnedNavigationStoredProcedureBuilder( + b))); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestOwnedNavigationBuilder InsertUsingStoredProcedure( + this ModelBuilderTest.TestOwnedNavigationBuilder builder, + string name, + string? schema, + Action> buildAction) + where TOwnerEntity : class + where TDependentEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.InsertUsingStoredProcedure( + name, schema, + b => buildAction( + new RelationalModelBuilderTest.GenericTestOwnedNavigationStoredProcedureBuilder( + b))); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.InsertUsingStoredProcedure( + name, schema, + b => buildAction( + new RelationalModelBuilderTest.NonGenericTestOwnedNavigationStoredProcedureBuilder( + b))); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestEntityTypeBuilder DeleteUsingStoredProcedure( + this ModelBuilderTest.TestEntityTypeBuilder builder, + Action> buildAction) + where TEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.DeleteUsingStoredProcedure( + b => buildAction(new RelationalModelBuilderTest.GenericTestStoredProcedureBuilder(b))); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.DeleteUsingStoredProcedure( + b => buildAction(new RelationalModelBuilderTest.NonGenericTestStoredProcedureBuilder(b))); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestEntityTypeBuilder DeleteUsingStoredProcedure( + this ModelBuilderTest.TestEntityTypeBuilder builder, + string name, + Action> buildAction) + where TEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.DeleteUsingStoredProcedure( + name, + b => buildAction(new RelationalModelBuilderTest.GenericTestStoredProcedureBuilder(b))); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.DeleteUsingStoredProcedure( + name, + b => buildAction(new RelationalModelBuilderTest.NonGenericTestStoredProcedureBuilder(b))); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestEntityTypeBuilder DeleteUsingStoredProcedure( + this ModelBuilderTest.TestEntityTypeBuilder builder, + string name, + string? schema, + Action> buildAction) + where TEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.DeleteUsingStoredProcedure( + name, schema, + b => buildAction(new RelationalModelBuilderTest.GenericTestStoredProcedureBuilder(b))); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.DeleteUsingStoredProcedure( + name, schema, + b => buildAction(new RelationalModelBuilderTest.NonGenericTestStoredProcedureBuilder(b))); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestOwnedNavigationBuilder DeleteUsingStoredProcedure( + this ModelBuilderTest.TestOwnedNavigationBuilder builder, + Action> buildAction) + where TOwnerEntity : class + where TDependentEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.DeleteUsingStoredProcedure( + b => buildAction( + new RelationalModelBuilderTest.GenericTestOwnedNavigationStoredProcedureBuilder( + b))); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.DeleteUsingStoredProcedure( + b => buildAction( + new RelationalModelBuilderTest.NonGenericTestOwnedNavigationStoredProcedureBuilder( + b))); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestOwnedNavigationBuilder DeleteUsingStoredProcedure( + this ModelBuilderTest.TestOwnedNavigationBuilder builder, + string name, + Action> buildAction) + where TOwnerEntity : class + where TDependentEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.DeleteUsingStoredProcedure( + name, + b => buildAction( + new RelationalModelBuilderTest.GenericTestOwnedNavigationStoredProcedureBuilder( + b))); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.DeleteUsingStoredProcedure( + name, + b => buildAction( + new RelationalModelBuilderTest.NonGenericTestOwnedNavigationStoredProcedureBuilder( + b))); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestOwnedNavigationBuilder DeleteUsingStoredProcedure( + this ModelBuilderTest.TestOwnedNavigationBuilder builder, + string name, + string? schema, + Action> buildAction) + where TOwnerEntity : class + where TDependentEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.DeleteUsingStoredProcedure( + name, schema, + b => buildAction( + new RelationalModelBuilderTest.GenericTestOwnedNavigationStoredProcedureBuilder( + b))); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.DeleteUsingStoredProcedure( + name, schema, + b => buildAction( + new RelationalModelBuilderTest.NonGenericTestOwnedNavigationStoredProcedureBuilder( + b))); + break; + } + + return builder; + } + public static ModelBuilderTest.TestEntityTypeBuilder HasCheckConstraint( this ModelBuilderTest.TestEntityTypeBuilder builder, string name, @@ -787,9 +1259,9 @@ public static ModelBuilderTest.TestEntityTypeBuilder HasCheckConstraint public static ModelBuilderTest.TestOwnedNavigationBuilder HasCheckConstraint ( - this ModelBuilderTest.TestOwnedNavigationBuilder builder, - string name, - string? sql) + this ModelBuilderTest.TestOwnedNavigationBuilder builder, + string name, + string? sql) where TOwnerEntity : class where TDependentEntity : class { @@ -807,11 +1279,11 @@ public static ModelBuilderTest.TestOwnedNavigationBuilder HasCheckConstraint - ( - this ModelBuilderTest.TestOwnedNavigationBuilder builder, - string name, - string sql, - Action buildAction) + ( + this ModelBuilderTest.TestOwnedNavigationBuilder builder, + string name, + string sql, + Action buildAction) where TOwnerEntity : class where TDependentEntity : class { @@ -853,8 +1325,8 @@ public static ModelBuilderTest.TestOwnershipBuilder HasConstraintName ( - this ModelBuilderTest.TestReferenceReferenceBuilder builder, - string name) + this ModelBuilderTest.TestReferenceReferenceBuilder builder, + string name) where TOwnerEntity : class where TDependentEntity : class { @@ -873,8 +1345,8 @@ public static ModelBuilderTest.TestReferenceReferenceBuilder HasConstraintName ( - this ModelBuilderTest.TestReferenceCollectionBuilder builder, - string name) + this ModelBuilderTest.TestReferenceCollectionBuilder builder, + string name) where TOwnerEntity : class where TDependentEntity : class { diff --git a/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs b/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs index 2ea5e22e0d4..fb491b4f501 100644 --- a/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs +++ b/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs @@ -52,6 +52,13 @@ public class RelationalApiConsistencyFixture : ApiConsistencyFixtureBase typeof(IConventionDbFunctionParameterBuilder), typeof(IDbFunctionParameter)) }, + { + typeof(IReadOnlyStoredProcedure), + (typeof(IMutableStoredProcedure), + typeof(IConventionStoredProcedure), + typeof(IConventionStoredProcedureBuilder), + typeof(IStoredProcedure)) + }, { typeof(IReadOnlySequence), (typeof(IMutableSequence), @@ -95,12 +102,17 @@ public class RelationalApiConsistencyFixture : ApiConsistencyFixtureBase typeof(ITableBase), typeof(ITable), typeof(IView), + typeof(IStoreFunction), typeof(ITableMappingBase), typeof(ITableMapping), typeof(IViewMapping), + typeof(IFunctionMapping), typeof(IColumnBase), typeof(IColumn), typeof(IViewColumn), + typeof(IFunctionColumn), + typeof(IStoreFunctionParameter), + typeof(IFunctionColumnMapping), typeof(IColumnMappingBase), typeof(IColumnMapping), typeof(IViewColumnMapping), @@ -142,10 +154,16 @@ public class RelationalApiConsistencyFixture : ApiConsistencyFixtureBase typeof(TableValuedFunctionBuilder<>), typeof(OwnedNavigationTableValuedFunctionBuilder), typeof(OwnedNavigationTableValuedFunctionBuilder<,>), + typeof(StoredProcedureBuilder), + typeof(StoredProcedureBuilder<>), + typeof(OwnedNavigationStoredProcedureBuilder), + typeof(OwnedNavigationStoredProcedureBuilder<,>), typeof(ColumnBuilder), typeof(ColumnBuilder<>), typeof(ViewColumnBuilder), typeof(ViewColumnBuilder<>), + typeof(StoredProcedureParameterBuilder), + typeof(StoredProcedureResultColumnBuilder), typeof(SequenceBuilder), typeof(MigrationBuilder), typeof(AlterOperationBuilder<>), @@ -235,7 +253,41 @@ public override new[] { typeof(IReadOnlyProperty), typeof(StoreObjectIdentifier).MakeByRefType() }), typeof(RelationalPropertyExtensions).GetMethod( nameof(RelationalPropertyExtensions.GetOverrides), - new[] { typeof(IReadOnlyProperty) }) + new[] { typeof(IReadOnlyProperty) }), + GetMethod( + typeof(StoredProcedureBuilder<>), + nameof(StoredProcedureBuilder.HasParameter), + genericParameterCount: 2, + (typeTypes, methodTypes) => new[] + { + typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(methodTypes[0], methodTypes[1])) + }), + GetMethod( + typeof(StoredProcedureBuilder<>), + nameof(StoredProcedureBuilder.HasParameter), + genericParameterCount: 2, + (typeTypes, methodTypes) => new[] + { + typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(methodTypes[0], methodTypes[1])), + typeof(Action) + }), + GetMethod( + typeof(StoredProcedureBuilder<>), + nameof(StoredProcedureBuilder.HasResultColumn), + genericParameterCount: 2, + (typeTypes, methodTypes) => new[] + { + typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(methodTypes[0], methodTypes[1])) + }), + GetMethod( + typeof(StoredProcedureBuilder<>), + nameof(StoredProcedureBuilder.HasResultColumn), + genericParameterCount: 2, + (typeTypes, methodTypes) => new[] + { + typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(methodTypes[0], methodTypes[1])), + typeof(Action) + }) }; public override HashSet AsyncMethodExceptions { get; } = new() @@ -267,6 +319,12 @@ public override typeof(RelationalConnectionDiagnosticsLogger).GetMethod( nameof(IRelationalConnectionDiagnosticsLogger.ConnectionDisposedAsync)) }; + + public override HashSet MetadataMethodExceptions { get; } = new() + { + typeof(IMutableStoredProcedure).GetMethod(nameof(IMutableStoredProcedure.AddParameter)), + typeof(IMutableStoredProcedure).GetMethod(nameof(IMutableStoredProcedure.AddResultColumn)) + }; public List> RelationalMetadataMethods { get; } = new(); @@ -295,6 +353,8 @@ protected override void Initialize() GenericFluentApiTypes.Add(typeof(OwnedNavigationSplitViewBuilder), typeof(OwnedNavigationSplitViewBuilder<,>)); GenericFluentApiTypes.Add(typeof(TableValuedFunctionBuilder), typeof(TableValuedFunctionBuilder<>)); GenericFluentApiTypes.Add(typeof(OwnedNavigationTableValuedFunctionBuilder), typeof(OwnedNavigationTableValuedFunctionBuilder<,>)); + GenericFluentApiTypes.Add(typeof(StoredProcedureBuilder), typeof(StoredProcedureBuilder<>)); + GenericFluentApiTypes.Add(typeof(OwnedNavigationStoredProcedureBuilder), typeof(OwnedNavigationStoredProcedureBuilder<,>)); GenericFluentApiTypes.Add(typeof(ColumnBuilder), typeof(ColumnBuilder<>)); GenericFluentApiTypes.Add(typeof(ViewColumnBuilder), typeof(ViewColumnBuilder<>)); @@ -307,7 +367,9 @@ protected override void Initialize() MirrorTypes.Add(typeof(SplitViewBuilder), typeof(OwnedNavigationSplitViewBuilder)); MirrorTypes.Add(typeof(SplitViewBuilder<>), typeof(OwnedNavigationSplitViewBuilder<,>)); MirrorTypes.Add(typeof(TableValuedFunctionBuilder), typeof(OwnedNavigationTableValuedFunctionBuilder)); - MirrorTypes.Add(typeof(TableValuedFunctionBuilder<>), typeof(OwnedNavigationTableValuedFunctionBuilder<,>)); + MirrorTypes.Add(typeof(TableValuedFunctionBuilder<>), typeof(OwnedNavigationTableValuedFunctionBuilder<,>)); + MirrorTypes.Add(typeof(StoredProcedureBuilder), typeof(OwnedNavigationStoredProcedureBuilder)); + MirrorTypes.Add(typeof(StoredProcedureBuilder<>), typeof(OwnedNavigationStoredProcedureBuilder<,>)); base.Initialize(); } diff --git a/test/EFCore.Specification.Tests/ApiConsistencyTestBase.cs b/test/EFCore.Specification.Tests/ApiConsistencyTestBase.cs index 883cfe20799..0cdd96c7052 100644 --- a/test/EFCore.Specification.Tests/ApiConsistencyTestBase.cs +++ b/test/EFCore.Specification.Tests/ApiConsistencyTestBase.cs @@ -539,7 +539,18 @@ private string ValidateConventionBuilderMethods(IReadOnlyList method if (!methodLookup.TryGetValue(expectedName, out var canSetMethod)) { - return $"{declaringType.Name} expected to have a {expectedName} method"; + if (method.Name.StartsWith("Has", StringComparison.Ordinal)) + { + var otherExpectedName = "CanHave" + method.Name[3..]; + if (!methodLookup.TryGetValue(otherExpectedName, out canSetMethod)) + { + return $"{declaringType.Name} expected to have a {expectedName} or {otherExpectedName} method"; + } + } + else + { + return $"{declaringType.Name} expected to have a {expectedName} method"; + } } var parameterIndex = method.IsStatic ? 1 : 0; @@ -1177,6 +1188,18 @@ public virtual IReadOnlyList Runtime)> MetadataMethods { get; } = new(); + protected static MethodInfo GetMethod(Type type, string name, int genericParameterCount, + Func parameterGenerator) + => type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Single( + mi => mi.Name == name + && ((genericParameterCount == 0 && !mi.IsGenericMethod) + || (mi.IsGenericMethod && mi.GetGenericArguments().Length == genericParameterCount)) + && mi.GetParameters().Select(e => e.ParameterType).SequenceEqual( + parameterGenerator( + type.IsGenericType ? type.GenericTypeArguments : Array.Empty(), + mi.IsGenericMethod ? mi.GetGenericArguments() : Array.Empty()))); + protected virtual void Initialize() { foreach (var typeTuple in MetadataTypes.Values) diff --git a/test/EFCore.Specification.Tests/QueryExpressionInterceptionTestBase.cs b/test/EFCore.Specification.Tests/QueryExpressionInterceptionTestBase.cs index 83340269734..69f515048d8 100644 --- a/test/EFCore.Specification.Tests/QueryExpressionInterceptionTestBase.cs +++ b/test/EFCore.Specification.Tests/QueryExpressionInterceptionTestBase.cs @@ -26,7 +26,7 @@ public virtual async Task Intercept_query_passively(bool async, bool inject) var query = context.Set().Where(e => e.Type == "Black Hole"); var results = async ? await query.ToListAsync() : query.ToList(); - Assert.Equal(1, results.Count); + Assert.Single(results); Assert.Equal("Black Hole", results[0].Type); AssertNormalOutcome(context, interceptor); @@ -55,7 +55,7 @@ public virtual async Task Intercept_query_with_multiple_interceptors(bool async, var query = context.Set().Where(e => e.Type == "Bing Bang"); var results = async ? await query.ToListAsync() : query.ToList(); - Assert.Equal(1, results.Count); + Assert.Single(results); Assert.Equal("Bing Bang", results[0].Type); AssertNormalOutcome(context, interceptor1); @@ -90,7 +90,7 @@ public virtual async Task Intercept_to_change_query_expression(bool async, bool var query = context.Set().Where(e => e.Type == "Black Hole"); var results = async ? await query.ToListAsync() : query.ToList(); - Assert.Equal(1, results.Count); + Assert.Single(results); Assert.Equal("Bing Bang", results[0].Type); AssertNormalOutcome(context, interceptor); diff --git a/test/EFCore.Tests/ApiConsistencyTest.cs b/test/EFCore.Tests/ApiConsistencyTest.cs index 2248ee79d0c..270bcf54576 100644 --- a/test/EFCore.Tests/ApiConsistencyTest.cs +++ b/test/EFCore.Tests/ApiConsistencyTest.cs @@ -174,8 +174,7 @@ protected override void Initialize() typeof(IMutableModel).GetMethod(nameof(IMutableModel.AddOwned)), typeof(IMutableModel).GetMethod(nameof(IMutableModel.AddShared)), typeof(IMutableEntityType).GetMethod(nameof(IMutableEntityType.AddData)), - typeof(IConventionEntityType).GetMethod(nameof(IConventionEntityType.LeastDerivedType)), - typeof(IConventionEntityType).GetMethod(nameof(IConventionEntityType.RemoveDiscriminatorValue)) + typeof(IConventionEntityType).GetMethod(nameof(IConventionEntityType.LeastDerivedType)) }; } } diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs index a284fb03765..8c98a8f88b5 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs @@ -1376,6 +1376,27 @@ public virtual void Detects_missing_discriminator_property() VerifyError(CoreStrings.NoDiscriminatorProperty(entityA.DisplayName()), modelBuilder); } + [ConditionalFact] + public virtual void Detects_incompatible_discriminator_value() + { + var modelBuilder = CreateConventionlessModelBuilder(); + var model = modelBuilder.Model; + + var entityA = model.AddEntityType(typeof(A)); + SetPrimaryKey(entityA); + AddProperties(entityA); + + var entityC = model.AddEntityType(typeof(C)); + SetBaseType(entityC, entityA); + + entityA.SetDiscriminatorProperty(entityA.AddProperty("D", typeof(int))); + entityA.SetDiscriminatorValue("1"); + + entityC.SetDiscriminatorValue(1); + + VerifyError(CoreStrings.DiscriminatorValueIncompatible("1", nameof(A), "int"), modelBuilder); + } + [ConditionalFact] public virtual void Detects_missing_discriminator_value_on_base() { @@ -1390,6 +1411,8 @@ public virtual void Detects_missing_discriminator_value_on_base() SetBaseType(entityC, entityA); entityA.SetDiscriminatorProperty(entityA.AddProperty("D", typeof(int))); + entityA.RemoveDiscriminatorValue(); + entityC.SetDiscriminatorValue(1); VerifyError(CoreStrings.NoDiscriminatorValue(entityA.DisplayName()), modelBuilder); @@ -1410,6 +1433,8 @@ public virtual void Detects_missing_discriminator_value_on_leaf() entityAbstract.SetDiscriminatorProperty(entityAbstract.AddProperty("D", typeof(int))); entityAbstract.SetDiscriminatorValue(0); + + entityGeneric.RemoveDiscriminatorValue(); VerifyError(CoreStrings.NoDiscriminatorValue(entityGeneric.DisplayName()), modelBuilder); }