diff --git a/src/EFCore.Design/Design/Internal/CSharpHelper.cs b/src/EFCore.Design/Design/Internal/CSharpHelper.cs index 878df46c822..67edc61b5c0 100644 --- a/src/EFCore.Design/Design/Internal/CSharpHelper.cs +++ b/src/EFCore.Design/Design/Internal/CSharpHelper.cs @@ -1358,8 +1358,7 @@ private static bool IsIdentifierPartCharacter(char ch) return ch < 'A' ? ch >= '0' && ch <= '9' - : ch <= 'Z' - || ch == '_'; + : ch <= 'Z'; } if (ch <= 'z') diff --git a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs index b3b5753082a..4329a6da959 100644 --- a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs @@ -35,7 +35,8 @@ public class AnnotationCodeGenerator : IAnnotationCodeGenerator RelationalAnnotationNames.InsertStoredProcedure, RelationalAnnotationNames.UpdateStoredProcedure, RelationalAnnotationNames.MappingFragments, - RelationalAnnotationNames.RelationalOverrides + RelationalAnnotationNames.RelationalOverrides, + RelationalAnnotationNames.ParameterDirection }; #region MethodInfos diff --git a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs index 1ea54b25765..fc24006ed32 100644 --- a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs @@ -310,6 +310,9 @@ public override void Generate(IEntityType entityType, CSharpRuntimeAnnotationCod annotations.Remove(RelationalAnnotationNames.ViewMappings); annotations.Remove(RelationalAnnotationNames.SqlQueryMappings); annotations.Remove(RelationalAnnotationNames.FunctionMappings); + annotations.Remove(RelationalAnnotationNames.InsertStoredProcedureMappings); + annotations.Remove(RelationalAnnotationNames.DeleteStoredProcedureMappings); + annotations.Remove(RelationalAnnotationNames.UpdateStoredProcedureMappings); annotations.Remove(RelationalAnnotationNames.DefaultMappings); } else @@ -360,6 +363,42 @@ public override void Generate(IEntityType entityType, CSharpRuntimeAnnotationCod GenerateSimpleAnnotation(RelationalAnnotationNames.Triggers, triggersVariable, parameters); } + + if (annotations.TryGetAndRemove( + RelationalAnnotationNames.InsertStoredProcedure, + out StoredProcedure insertStoredProcedure)) + { + var sprocVariable = Dependencies.CSharpHelper.Identifier("insertSproc", parameters.ScopeVariables, capitalize: false); + + Create(insertStoredProcedure, sprocVariable, parameters); + + GenerateSimpleAnnotation(RelationalAnnotationNames.InsertStoredProcedure, sprocVariable, parameters); + parameters.MainBuilder.AppendLine(); + } + + if (annotations.TryGetAndRemove( + RelationalAnnotationNames.DeleteStoredProcedure, + out StoredProcedure deleteStoredProcedure)) + { + var sprocVariable = Dependencies.CSharpHelper.Identifier("deleteSproc", parameters.ScopeVariables, capitalize: false); + + Create(deleteStoredProcedure, sprocVariable, parameters); + + GenerateSimpleAnnotation(RelationalAnnotationNames.DeleteStoredProcedure, sprocVariable, parameters); + parameters.MainBuilder.AppendLine(); + } + + if (annotations.TryGetAndRemove( + RelationalAnnotationNames.UpdateStoredProcedure, + out StoredProcedure updateStoredProcedure)) + { + var sprocVariable = Dependencies.CSharpHelper.Identifier("updateSproc", parameters.ScopeVariables, capitalize: false); + + Create(updateStoredProcedure, sprocVariable, parameters); + + GenerateSimpleAnnotation(RelationalAnnotationNames.UpdateStoredProcedure, sprocVariable, parameters); + parameters.MainBuilder.AppendLine(); + } } base.Generate(entityType, parameters); @@ -439,6 +478,49 @@ private void Create(ITrigger trigger, string triggersVariable, CSharpRuntimeAnno public virtual void Generate(ITrigger trigger, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) => GenerateSimpleAnnotations(parameters); + private void Create(IStoredProcedure storedProcedure, string sprocVariable, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + AddNamespace(typeof(RuntimeStoredProcedure), parameters.Namespaces); + var code = Dependencies.CSharpHelper; + var mainBuilder = parameters.MainBuilder; + mainBuilder + .Append("var ").Append(sprocVariable).AppendLine(" = new RuntimeStoredProcedure(").IncrementIndent() + .Append(parameters.TargetName).AppendLine(",") + .Append(code.Literal(storedProcedure.Name)).AppendLine(",") + .Append(code.Literal(storedProcedure.Schema)).AppendLine(",") + .Append(code.Literal(storedProcedure.AreTransactionsSuppressed)) + .AppendLine(");") + .DecrementIndent() + .AppendLine(); + + foreach (var parameter in storedProcedure.Parameters) + { + mainBuilder.Append(sprocVariable).Append(".AddParameter(") + .Append(code.Literal(parameter)) + .AppendLine(");"); + } + + foreach (var resultColumn in storedProcedure.ResultColumns) + { + mainBuilder.Append(sprocVariable).Append(".AddResultColumn(") + .Append(code.Literal(resultColumn)) + .AppendLine(");"); + } + + CreateAnnotations( + storedProcedure, + Generate, + parameters with { TargetName = sprocVariable }); + } + + /// + /// Generates code to create the given annotations. + /// + /// The stored procedure to which the annotations are applied. + /// Additional parameters used during code generation. + public virtual void Generate(IStoredProcedure storedProcedure, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + => GenerateSimpleAnnotations(parameters); + /// /// Generates code to create the given annotations. /// @@ -457,6 +539,11 @@ public override void Generate(IProperty property, CSharpRuntimeAnnotationCodeGen annotations.Remove(RelationalAnnotationNames.ViewColumnMappings); annotations.Remove(RelationalAnnotationNames.SqlQueryColumnMappings); annotations.Remove(RelationalAnnotationNames.FunctionColumnMappings); + annotations.Remove(RelationalAnnotationNames.InsertStoredProcedureParameterMappings); + annotations.Remove(RelationalAnnotationNames.InsertStoredProcedureResultColumnMappings); + annotations.Remove(RelationalAnnotationNames.DeleteStoredProcedureParameterMappings); + annotations.Remove(RelationalAnnotationNames.UpdateStoredProcedureParameterMappings); + annotations.Remove(RelationalAnnotationNames.UpdateStoredProcedureResultColumnMappings); annotations.Remove(RelationalAnnotationNames.DefaultColumnMappings); } else @@ -472,7 +559,7 @@ public override void Generate(IProperty property, CSharpRuntimeAnnotationCodeGen AddNamespace(typeof(StoreObjectDictionary), parameters.Namespaces); AddNamespace(typeof(StoreObjectIdentifier), parameters.Namespaces); var overridesVariable = Dependencies.CSharpHelper.Identifier("overrides", parameters.ScopeVariables, capitalize: false); - parameters.MainBuilder + parameters.MainBuilder.AppendLine() .Append("var ").Append(overridesVariable).AppendLine(" = new StoreObjectDictionary();"); foreach (var overrides in tableOverrides.GetValues()) @@ -481,6 +568,7 @@ public override void Generate(IProperty property, CSharpRuntimeAnnotationCodeGen } GenerateSimpleAnnotation(RelationalAnnotationNames.RelationalOverrides, overridesVariable, parameters); + parameters.MainBuilder.AppendLine(); } } @@ -617,6 +705,21 @@ private static void AppendLiteral(StoreObjectIdentifier storeObject, IndentedStr builder .Append("DbFunction(").Append(code.Literal(storeObject.Name)).Append(")"); break; + case StoreObjectType.InsertStoredProcedure: + builder + .Append("InsertStoredProcedure(").Append(code.Literal(storeObject.Name)) + .Append(", ").Append(code.Literal(storeObject.Schema)).Append(")"); + break; + case StoreObjectType.DeleteStoredProcedure: + builder + .Append("DeleteStoredProcedure(").Append(code.Literal(storeObject.Name)) + .Append(", ").Append(code.Literal(storeObject.Schema)).Append(")"); + break; + case StoreObjectType.UpdateStoredProcedure: + builder + .Append("UpdateStoredProcedure(").Append(code.Literal(storeObject.Name)) + .Append(", ").Append(code.Literal(storeObject.Schema)).Append(")"); + break; default: Check.DebugFail("Unexpected StoreObjectType: " + storeObject.StoreObjectType); break; diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.ToTable.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.ToTable.cs index 904d53978a2..81d3639ca0a 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.ToTable.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.ToTable.cs @@ -138,16 +138,7 @@ public static EntityTypeBuilder ToTable( string name, Action> buildAction) where TEntity : class - { - Check.NotNull(name, nameof(name)); - Check.NotNull(buildAction, nameof(buildAction)); - - entityTypeBuilder.Metadata.SetTableName(name); - entityTypeBuilder.Metadata.SetSchema(null); - buildAction(new (StoreObjectIdentifier.Table(name, null), entityTypeBuilder)); - - return entityTypeBuilder; - } + => ToTable(entityTypeBuilder, name, null, buildAction); /// /// Configures the table that the entity type maps to when targeting a relational database. @@ -195,7 +186,7 @@ public static EntityTypeBuilder ToTable( entityTypeBuilder.Metadata.SetTableName(name); entityTypeBuilder.Metadata.SetSchema(schema); - buildAction(new (StoreObjectIdentifier.Table(name, schema), entityTypeBuilder)); + buildAction(new(StoreObjectIdentifier.Table(name, entityTypeBuilder.Metadata.GetSchema()), entityTypeBuilder)); return entityTypeBuilder; } @@ -243,7 +234,7 @@ public static EntityTypeBuilder ToTable( entityTypeBuilder.Metadata.SetTableName(name); entityTypeBuilder.Metadata.SetSchema(schema); - buildAction(new (StoreObjectIdentifier.Table(name, schema), entityTypeBuilder)); + buildAction(new(StoreObjectIdentifier.Table(name, entityTypeBuilder.Metadata.GetSchema()), entityTypeBuilder)); return entityTypeBuilder; } @@ -374,16 +365,7 @@ public static OwnedNavigationBuilder ToTable> buildAction) where TOwnerEntity : class where TDependentEntity : class - { - Check.NotNull(name, nameof(name)); - Check.NotNull(buildAction, nameof(buildAction)); - - ownedNavigationBuilder.OwnedEntityType.SetTableName(name); - ownedNavigationBuilder.OwnedEntityType.SetSchema(null); - buildAction(new (StoreObjectIdentifier.Table(name, null), ownedNavigationBuilder)); - - return ownedNavigationBuilder; - } + => ToTable(ownedNavigationBuilder, name, null, buildAction); /// /// Configures the table that the entity type maps to when targeting a relational database. @@ -432,7 +414,7 @@ public static OwnedNavigationBuilder ToTable( ownedNavigationBuilder.OwnedEntityType.SetTableName(name); ownedNavigationBuilder.OwnedEntityType.SetSchema(schema); - buildAction(new (StoreObjectIdentifier.Table(name, schema), ownedNavigationBuilder)); + buildAction(new(StoreObjectIdentifier.Table(name, ownedNavigationBuilder.OwnedEntityType.GetSchema()), ownedNavigationBuilder)); return ownedNavigationBuilder; } @@ -485,7 +467,7 @@ public static OwnedNavigationBuilder ToTable SplitToTable( Check.NullButNotEmpty(schema, nameof(schema)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction(new(StoreObjectIdentifier.Table(name, schema), entityTypeBuilder)); + buildAction(new(StoreObjectIdentifier.Table(name, schema ?? entityTypeBuilder.Metadata.GetDefaultSchema()), entityTypeBuilder)); return entityTypeBuilder; } @@ -642,7 +624,7 @@ public static OwnedNavigationBuilder SplitToTable( Check.NullButNotEmpty(schema, nameof(schema)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction(new (StoreObjectIdentifier.Table(name, schema), ownedNavigationBuilder)); + buildAction(new(StoreObjectIdentifier.Table(name, schema ?? ownedNavigationBuilder.OwnedEntityType.GetDefaultSchema()), ownedNavigationBuilder)); return ownedNavigationBuilder; } @@ -673,7 +655,7 @@ public static OwnedNavigationBuilder SplitToTabl Check.NullButNotEmpty(schema, nameof(schema)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction(new (StoreObjectIdentifier.Table(name, schema), ownedNavigationBuilder)); + buildAction(new(StoreObjectIdentifier.Table(name, schema ?? ownedNavigationBuilder.OwnedEntityType.GetDefaultSchema()), ownedNavigationBuilder)); return ownedNavigationBuilder; } diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.ToView.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.ToView.cs index 0f1981767f2..ca5a2f51261 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.ToView.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.ToView.cs @@ -145,7 +145,7 @@ public static EntityTypeBuilder ToView( entityTypeBuilder.Metadata.SetViewName(name); entityTypeBuilder.Metadata.SetViewSchema(schema); entityTypeBuilder.Metadata.SetAnnotation(RelationalAnnotationNames.ViewDefinitionSql, null); - buildAction(new(StoreObjectIdentifier.View(name, schema), entityTypeBuilder)); + buildAction(new(StoreObjectIdentifier.View(name, entityTypeBuilder.Metadata.GetViewSchema()), entityTypeBuilder)); return entityTypeBuilder; } @@ -175,7 +175,7 @@ public static EntityTypeBuilder ToView( entityTypeBuilder.Metadata.SetViewName(name); entityTypeBuilder.Metadata.SetViewSchema(schema); entityTypeBuilder.Metadata.SetAnnotation(RelationalAnnotationNames.ViewDefinitionSql, null); - buildAction(new(StoreObjectIdentifier.View(name, schema), entityTypeBuilder)); + buildAction(new(StoreObjectIdentifier.View(name, entityTypeBuilder.Metadata.GetViewSchema()), entityTypeBuilder)); return entityTypeBuilder; } @@ -317,7 +317,7 @@ public static OwnedNavigationBuilder ToView( ownedNavigationBuilder.OwnedEntityType.SetViewName(name); ownedNavigationBuilder.OwnedEntityType.SetViewSchema(schema); ownedNavigationBuilder.OwnedEntityType.SetAnnotation(RelationalAnnotationNames.ViewDefinitionSql, null); - buildAction(new(StoreObjectIdentifier.View(name, schema), ownedNavigationBuilder)); + buildAction(new(StoreObjectIdentifier.View(name, ownedNavigationBuilder.OwnedEntityType.GetViewSchema()), ownedNavigationBuilder)); return ownedNavigationBuilder; } @@ -349,7 +349,7 @@ public static OwnedNavigationBuilder ToView SplitToView( /// /// See Modeling entity types and relationships for more information and examples. /// - /// The builder for the entity type being configured. + /// The builder for the entity type being configured. /// The name of the view. /// The schema of the view. /// An action that performs configuration of the view. /// The same builder instance so that multiple calls can be chained. public static EntityTypeBuilder SplitToView( - this EntityTypeBuilder ownedNavigationBuilder, + this EntityTypeBuilder entityTypeBuilder, string name, string? schema, Action buildAction) @@ -412,9 +412,9 @@ public static EntityTypeBuilder SplitToView( Check.NullButNotEmpty(schema, nameof(schema)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction(new(StoreObjectIdentifier.View(name, schema), ownedNavigationBuilder)); + buildAction(new(StoreObjectIdentifier.View(name, schema ?? entityTypeBuilder.Metadata.GetDefaultViewSchema()), entityTypeBuilder)); - return ownedNavigationBuilder; + return entityTypeBuilder; } /// @@ -441,7 +441,7 @@ public static EntityTypeBuilder SplitToView( Check.NullButNotEmpty(schema, nameof(schema)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction(new(StoreObjectIdentifier.View(name, schema), entityTypeBuilder)); + buildAction(new(StoreObjectIdentifier.View(name, schema ?? entityTypeBuilder.Metadata.GetDefaultViewSchema()), entityTypeBuilder)); return entityTypeBuilder; } @@ -506,7 +506,7 @@ public static OwnedNavigationBuilder SplitToView( Check.NullButNotEmpty(schema, nameof(schema)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction(new (StoreObjectIdentifier.View(name, schema), ownedNavigationBuilder)); + buildAction(new(StoreObjectIdentifier.View(name, schema ?? ownedNavigationBuilder.OwnedEntityType.GetDefaultViewSchema()), ownedNavigationBuilder)); return ownedNavigationBuilder; } @@ -537,7 +537,7 @@ public static OwnedNavigationBuilder SplitToView Check.NullButNotEmpty(schema, nameof(schema)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction(new (StoreObjectIdentifier.View(name, schema), ownedNavigationBuilder)); + buildAction(new(StoreObjectIdentifier.View(name, schema ?? ownedNavigationBuilder.OwnedEntityType.GetDefaultViewSchema()), ownedNavigationBuilder)); return ownedNavigationBuilder; } diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.UseSproc.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.UseSproc.cs index b07449295a9..d90fa8d75a8 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.UseSproc.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.UseSproc.cs @@ -14,7 +14,7 @@ namespace Microsoft.EntityFrameworkCore; public static partial class RelationalEntityTypeBuilderExtensions { /// - /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// Configures the stored procedure that the entity type uses for updates when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -34,7 +34,7 @@ public static EntityTypeBuilder UpdateUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// Configures the stored procedure that the entity type uses for updates when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -56,7 +56,7 @@ public static EntityTypeBuilder UpdateUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// Configures the stored procedure that the entity type uses for updates when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -80,7 +80,7 @@ public static EntityTypeBuilder UpdateUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// Configures the stored procedure that the entity type uses for updates when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -102,7 +102,7 @@ public static EntityTypeBuilder UpdateUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// Configures the stored procedure that the entity type uses for updates when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -126,7 +126,7 @@ public static EntityTypeBuilder UpdateUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// Configures the stored procedure that the entity type uses for updates when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -152,7 +152,7 @@ public static EntityTypeBuilder UpdateUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// Configures the stored procedure that the entity type uses for updates when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -172,7 +172,7 @@ public static OwnedNavigationBuilder UpdateUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// Configures the stored procedure that the entity type uses for updates when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -194,7 +194,7 @@ public static OwnedNavigationBuilder UpdateUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// Configures the stored procedure that the entity type uses for updates when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -218,7 +218,7 @@ public static OwnedNavigationBuilder UpdateUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// Configures the stored procedure that the entity type uses for updates when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -242,7 +242,7 @@ public static OwnedNavigationBuilder UpdateUsing buildAction); /// - /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// Configures the stored procedure that the entity type uses for updates when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -268,7 +268,7 @@ public static OwnedNavigationBuilder UpdateUsing buildAction); /// - /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// Configures the stored procedure that the entity type uses for updates when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -296,7 +296,7 @@ public static OwnedNavigationBuilder UpdateUsing buildAction); /// - /// Configures the stored procedure that the entity type would use for updates when targeting a relational database. + /// Configures the stored procedure that the entity type uses 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. @@ -310,7 +310,7 @@ public static OwnedNavigationBuilder UpdateUsing entityTypeBuilder.Metadata, StoreObjectType.UpdateStoredProcedure, fromDataAnnotation); /// - /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// Configures the stored procedure that the entity type uses for deletes when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -330,7 +330,7 @@ public static EntityTypeBuilder DeleteUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// Configures the stored procedure that the entity type uses for deletes when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -352,7 +352,7 @@ public static EntityTypeBuilder DeleteUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// Configures the stored procedure that the entity type uses for deletes when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -376,7 +376,7 @@ public static EntityTypeBuilder DeleteUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// Configures the stored procedure that the entity type uses for deletes when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -398,7 +398,7 @@ public static EntityTypeBuilder DeleteUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// Configures the stored procedure that the entity type uses for deletes when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -422,7 +422,7 @@ public static EntityTypeBuilder DeleteUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// Configures the stored procedure that the entity type uses for deletes when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -448,7 +448,7 @@ public static EntityTypeBuilder DeleteUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// Configures the stored procedure that the entity type uses for deletes when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -468,7 +468,7 @@ public static OwnedNavigationBuilder DeleteUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// Configures the stored procedure that the entity type uses for deletes when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -490,7 +490,7 @@ public static OwnedNavigationBuilder DeleteUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// Configures the stored procedure that the entity type uses for deletes when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -514,7 +514,7 @@ public static OwnedNavigationBuilder DeleteUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// Configures the stored procedure that the entity type uses for deletes when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -538,7 +538,7 @@ public static OwnedNavigationBuilder DeleteUsing buildAction); /// - /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// Configures the stored procedure that the entity type uses for deletes when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -564,7 +564,7 @@ public static OwnedNavigationBuilder DeleteUsing buildAction); /// - /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// Configures the stored procedure that the entity type uses for deletes when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -592,7 +592,7 @@ public static OwnedNavigationBuilder DeleteUsing buildAction); /// - /// Configures the stored procedure that the entity type would use for deletes when targeting a relational database. + /// Configures the stored procedure that the entity type uses 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. @@ -606,7 +606,7 @@ public static OwnedNavigationBuilder DeleteUsing entityTypeBuilder.Metadata, StoreObjectType.DeleteStoredProcedure, fromDataAnnotation); /// - /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// Configures the stored procedure that the entity type uses for inserts when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -626,7 +626,7 @@ public static EntityTypeBuilder InsertUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// Configures the stored procedure that the entity type uses for inserts when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -648,7 +648,7 @@ public static EntityTypeBuilder InsertUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// Configures the stored procedure that the entity type uses for inserts when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -672,7 +672,7 @@ public static EntityTypeBuilder InsertUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// Configures the stored procedure that the entity type uses for inserts when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -694,7 +694,7 @@ public static EntityTypeBuilder InsertUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// Configures the stored procedure that the entity type uses for inserts when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -718,7 +718,7 @@ public static EntityTypeBuilder InsertUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// Configures the stored procedure that the entity type uses for inserts when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -744,7 +744,7 @@ public static EntityTypeBuilder InsertUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// Configures the stored procedure that the entity type uses for inserts when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -764,7 +764,7 @@ public static OwnedNavigationBuilder InsertUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// Configures the stored procedure that the entity type uses for inserts when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -786,7 +786,7 @@ public static OwnedNavigationBuilder InsertUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// Configures the stored procedure that the entity type uses for inserts when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -810,7 +810,7 @@ public static OwnedNavigationBuilder InsertUsingStoredProcedure( buildAction); /// - /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// Configures the stored procedure that the entity type uses for inserts when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -834,7 +834,7 @@ public static OwnedNavigationBuilder InsertUsing buildAction); /// - /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// Configures the stored procedure that the entity type uses for inserts when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -860,7 +860,7 @@ public static OwnedNavigationBuilder InsertUsing buildAction); /// - /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// Configures the stored procedure that the entity type uses for inserts when targeting a relational database. /// /// /// See Modeling entity types and relationships and @@ -888,7 +888,7 @@ public static OwnedNavigationBuilder InsertUsing buildAction); /// - /// Configures the stored procedure that the entity type would use for inserts when targeting a relational database. + /// Configures the stored procedure that the entity type uses 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. @@ -927,8 +927,16 @@ private static EntityTypeBuilder UseStoredProcedure( { Check.NotNull(buildAction, nameof(buildAction)); + var entityType = entityTypeBuilder.Metadata; + if (entityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy + && !entityType.ClrType.IsInstantiable()) + { + throw new InvalidOperationException( + RelationalStrings.AbstractTpc(entityType.DisplayName(), name ?? sprocType.ToString())); + } + var sprocBuilder = InternalStoredProcedureBuilder.HasStoredProcedure( - entityTypeBuilder.Metadata, sprocType, name, schema); + entityType, sprocType, name, schema); buildAction(new(sprocBuilder.Metadata, entityTypeBuilder)); return entityTypeBuilder; diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs index c68b51e8a07..68ec8c0e4db 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs @@ -54,7 +54,7 @@ public static class RelationalEntityTypeExtensions /// The default name of the table to which the entity type would be mapped. public static string? GetDefaultTableName(this IReadOnlyEntityType entityType, bool truncate = true) { - if (entityType.GetDiscriminatorPropertyName() != null + if ((entityType.GetMappingStrategy() ?? RelationalAnnotationNames.TphMappingStrategy) == RelationalAnnotationNames.TphMappingStrategy && entityType.BaseType != null) { return entityType.GetRootType().GetTableName(); @@ -67,7 +67,7 @@ public static class RelationalEntityTypeExtensions return ownership.PrincipalEntityType.GetTableName(); } - var name = entityType.ShortName(); + var name = entityType.HasSharedClrType ? entityType.ShortName() : entityType.ClrType.ShortDisplayName(); if (entityType.HasSharedClrType && ownership != null #pragma warning disable EF1001 // Internal EF Core API usage. @@ -593,7 +593,7 @@ public static IEnumerable GetFunctionMappings(this IEntityType /// 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); + => (IMutableStoredProcedure?)StoredProcedure.FindStoredProcedure(entityType, StoreObjectType.DeleteStoredProcedure); /// /// Returns the stored procedure to which the entity type is mapped for deletes @@ -602,7 +602,7 @@ public static IEnumerable GetFunctionMappings(this IEntityType /// 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); + => (IConventionStoredProcedure?)StoredProcedure.FindStoredProcedure(entityType, StoreObjectType.DeleteStoredProcedure); /// /// Returns the stored procedure to which the entity type is mapped for deletes @@ -672,7 +672,7 @@ public static IMutableStoredProcedure SetDeleteStoredProcedure(this IMutableEnti /// 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); + => (IMutableStoredProcedure?)StoredProcedure.FindStoredProcedure(entityType, StoreObjectType.InsertStoredProcedure); /// /// Returns the stored procedure to which the entity type is mapped for inserts @@ -681,7 +681,7 @@ public static IMutableStoredProcedure SetDeleteStoredProcedure(this IMutableEnti /// 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); + => (IConventionStoredProcedure?)StoredProcedure.FindStoredProcedure(entityType, StoreObjectType.InsertStoredProcedure); /// /// Returns the stored procedure to which the entity type is mapped for inserts @@ -751,7 +751,7 @@ public static IMutableStoredProcedure SetInsertStoredProcedure(this IMutableEnti /// 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); + => (IMutableStoredProcedure?)StoredProcedure.FindStoredProcedure(entityType, StoreObjectType.UpdateStoredProcedure); /// /// Returns the stored procedure to which the entity type is mapped for updates @@ -760,7 +760,7 @@ public static IMutableStoredProcedure SetInsertStoredProcedure(this IMutableEnti /// 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); + => (IConventionStoredProcedure?)StoredProcedure.FindStoredProcedure(entityType, StoreObjectType.UpdateStoredProcedure); /// /// Returns the stored procedure to which the entity type is mapped for updates @@ -814,6 +814,36 @@ public static IMutableStoredProcedure SetUpdateStoredProcedure(this IMutableEnti public static ConfigurationSource? GetUpdateStoredProcedureConfigurationSource(this IConventionEntityType entityType) => StoredProcedure.GetStoredProcedureConfigurationSource(entityType, StoreObjectType.UpdateStoredProcedure); + /// + /// Returns the insert stored procedures to which the entity type is mapped. + /// + /// The entity type. + /// The insert stored procedures to which the entity type is mapped. + public static IEnumerable GetInsertStoredProcedureMappings(this IEntityType entityType) + => (IEnumerable?)entityType.FindRuntimeAnnotationValue( + RelationalAnnotationNames.InsertStoredProcedureMappings) + ?? Enumerable.Empty(); + + /// + /// Returns the delete stored procedures to which the entity type is mapped. + /// + /// The entity type. + /// The delete stored procedures to which the entity type is mapped. + public static IEnumerable GetDeleteStoredProcedureMappings(this IEntityType entityType) + => (IEnumerable?)entityType.FindRuntimeAnnotationValue( + RelationalAnnotationNames.DeleteStoredProcedureMappings) + ?? Enumerable.Empty(); + + /// + /// Returns the update stored procedures to which the entity type is mapped. + /// + /// The entity type. + /// The update stored procedures to which the entity type is mapped. + public static IEnumerable GetUpdateStoredProcedureMappings(this IEntityType entityType) + => (IEnumerable?)entityType.FindRuntimeAnnotationValue( + RelationalAnnotationNames.UpdateStoredProcedureMappings) + ?? Enumerable.Empty(); + #endregion #region Check constraint diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs index 50a4276b20b..0aa7b023fe9 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Data; using Microsoft.EntityFrameworkCore.Metadata.Internal; // ReSharper disable once CheckNamespace @@ -357,6 +358,54 @@ public static bool CanSetIsFixedLength( bool fromDataAnnotation = false) => propertyBuilder.CanSetAnnotation(RelationalAnnotationNames.IsFixedLength, fixedLength, fromDataAnnotation); + /// + /// Sets the direction of the stored procedure parameter. + /// + /// The builder for the property being configured. + /// The direction. + /// The identifier of the stored procedure. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + public static IConventionPropertyBuilder? HasDirection( + this IConventionPropertyBuilder propertyBuilder, + ParameterDirection direction, + in StoreObjectIdentifier storeObject, + bool fromDataAnnotation = false) + { + if (!propertyBuilder.CanSetDirection(direction, storeObject, fromDataAnnotation)) + { + return null; + } + + propertyBuilder.Metadata.SetDirection(direction, storeObject, fromDataAnnotation); + return propertyBuilder; + } + + /// + /// Returns a value indicating whether the given direction can be configured on the corresponding stored procedure parameter. + /// + /// The builder for the property being configured. + /// The direction. + /// The identifier of the stored procedure. + /// Indicates whether the configuration was specified using a data annotation. + /// if the property can be mapped to the given column. + public static bool CanSetDirection( + this IConventionPropertyBuilder propertyBuilder, + ParameterDirection direction, + in StoreObjectIdentifier storeObject, + bool fromDataAnnotation = false) + { + var overrides = (IConventionRelationalPropertyOverrides?)RelationalPropertyOverrides.Find( + propertyBuilder.Metadata, storeObject); + return overrides == null + || (fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) + .Overrides(overrides.GetDirectionConfigurationSource()) + || overrides.Direction == direction; + } + /// /// Configures the default value expression for the column that the property maps to when targeting a /// relational database. diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs index 921b60e5cc7..1467e0e1479 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Runtime.CompilerServices; using System.Text; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; // ReSharper disable once CheckNamespace @@ -508,6 +509,56 @@ public static IEnumerable GetFunctionColumnMappings(this RelationalAnnotationNames.FunctionColumnMappings) ?? Enumerable.Empty(); + /// + /// Returns the insert stored procedure result columns to which the property is mapped. + /// + /// The property. + /// The insert stored procedure result columns to which the property is mapped. + public static IEnumerable GetInsertStoredProcedureResultColumnMappings(this IProperty property) + => (IEnumerable?)property.FindRuntimeAnnotationValue( + RelationalAnnotationNames.InsertStoredProcedureResultColumnMappings) + ?? Enumerable.Empty(); + + /// + /// Returns the insert stored procedure parameters to which the property is mapped. + /// + /// The property. + /// The insert stored procedure parameters to which the property is mapped. + public static IEnumerable GetInsertStoredProcedureParameterMappings(this IProperty property) + => (IEnumerable?)property.FindRuntimeAnnotationValue( + RelationalAnnotationNames.InsertStoredProcedureParameterMappings) + ?? Enumerable.Empty(); + + /// + /// Returns the delete stored procedure parameters to which the property is mapped. + /// + /// The property. + /// The delete stored procedure parameters to which the property is mapped. + public static IEnumerable GetDeleteStoredProcedureParameterMappings(this IProperty property) + => (IEnumerable?)property.FindRuntimeAnnotationValue( + RelationalAnnotationNames.DeleteStoredProcedureParameterMappings) + ?? Enumerable.Empty(); + + /// + /// Returns the update stored procedure result columns to which the property is mapped. + /// + /// The property. + /// The update stored procedure result columns to which the property is mapped. + public static IEnumerable GetUpdateStoredProcedureResultColumnMappings(this IProperty property) + => (IEnumerable?)property.FindRuntimeAnnotationValue( + RelationalAnnotationNames.UpdateStoredProcedureResultColumnMappings) + ?? Enumerable.Empty(); + + /// + /// Returns the update stored procedure parameters to which the property is mapped. + /// + /// The property. + /// The update stored procedure parameters to which the property is mapped. + public static IEnumerable GetUpdateStoredProcedureParameterMappings(this IProperty property) + => (IEnumerable?)property.FindRuntimeAnnotationValue( + RelationalAnnotationNames.UpdateStoredProcedureParameterMappings) + ?? Enumerable.Empty(); + /// /// Returns the column corresponding to this property if it's mapped to the given table-like store object. /// @@ -557,6 +608,26 @@ public static IEnumerable GetFunctionColumnMappings(this } } + return null; + case StoreObjectType.InsertStoredProcedure: + foreach (var mapping in property.GetInsertStoredProcedureResultColumnMappings()) + { + if (mapping.TableMapping.Table.Name == storeObject.Name && mapping.TableMapping.Table.Schema == storeObject.Schema) + { + return mapping.Column; + } + } + + return null; + case StoreObjectType.UpdateStoredProcedure: + foreach (var mapping in property.GetUpdateStoredProcedureResultColumnMappings()) + { + if (mapping.TableMapping.Table.Name == storeObject.Name && mapping.TableMapping.Table.Schema == storeObject.Schema) + { + return mapping.Column; + } + } + return null; default: throw new NotSupportedException(storeObject.StoreObjectType.ToString()); @@ -1100,6 +1171,53 @@ private static bool IsOptionalSharingDependent( return optional ?? (entityType.BaseType != null && entityType.FindDiscriminatorProperty() != null); } + /// + /// Gets the direction of the corresponding stored procedure parameter. + /// + /// The property. + /// The identifier of the stored procedure containing the parameter. + /// + /// The direction of the corresponding stored procedure parameter. + /// + public static System.Data.ParameterDirection GetDirection(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + => property.FindOverrides(storeObject)?.Direction ?? System.Data.ParameterDirection.Input; + + /// + /// Sets the direction of the corresponding stored procedure parameter. + /// + /// The property. + /// The direction to set. + /// The identifier of the stored procedure containing the parameter. + public static void SetDirection( + this IMutableProperty property, + System.Data.ParameterDirection? direction, + in StoreObjectIdentifier storeObject) + => property.GetOrCreateOverrides(storeObject).Direction = direction; + + /// + /// Sets the direction of the corresponding stored procedure parameter. + /// + /// The property. + /// The direction to set. + /// The identifier of the stored procedure containing the parameter. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static System.Data.ParameterDirection? SetDirection( + this IConventionProperty property, + System.Data.ParameterDirection? direction, + in StoreObjectIdentifier storeObject, + bool fromDataAnnotation = false) + => property.GetOrCreateOverrides(storeObject, fromDataAnnotation).SetDirection(direction, fromDataAnnotation); + + /// + /// Gets the for the stored procedure parameter direction. + /// + /// The property. + /// The identifier of the stored procedure containing the parameter. + /// The for the stored procedure parameter direction. + public static ConfigurationSource? GetDirectionConfigurationSource(this IConventionProperty property, in StoreObjectIdentifier storeObject) + => property.FindOverrides(storeObject)?.GetDirectionConfigurationSource(); + /// /// Returns the comment for the column this property is mapped to. /// diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index 53b5bd3d93a..0b8cb90315f 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Data; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; @@ -276,7 +277,7 @@ static void AddSproc( private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy) { var entityType = sproc.EntityType; - var storeObjectIdentifier = ((StoredProcedure)sproc).CreateIdentifier()!.Value; + var storeObjectIdentifier = sproc.GetStoreIdentifier(); var primaryKey = entityType.FindPrimaryKey(); if (primaryKey == null) @@ -311,18 +312,43 @@ private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy } else if (mappingStrategy == RelationalAnnotationNames.TptMappingStrategy) { - if (entityType.BaseType != null) + var baseType = entityType.BaseType; + if (baseType != null) { foreach (var property in primaryKey.Properties) { properties.Add(property.Name, property); } + + while (baseType != null && baseType.IsAbstract()) + { + if (StoredProcedure.FindDeclaredStoredProcedure(baseType, storeObjectIdentifier.StoreObjectType) != null) + { + break; + } + + foreach (var property in baseType.GetDeclaredProperties()) + { + properties.Add(property.Name, property); + } + + baseType = baseType.BaseType; + } } } + var storeGeneratedProperties = storeObjectIdentifier.StoreObjectType switch + { + StoreObjectType.InsertStoredProcedure + => properties.Where(p => (p.Value.ValueGenerated & ValueGenerated.OnAdd) != 0).ToDictionary(p => p.Key, p => p.Value), + StoreObjectType.UpdateStoredProcedure + => properties.Where(p => (p.Value.ValueGenerated & ValueGenerated.OnUpdate) != 0).ToDictionary(p => p.Key, p => p.Value), + _ => new Dictionary() + }; + foreach (var resultColumn in sproc.ResultColumns) { - if (!properties!.TryGetValue(resultColumn, out var property)) + if (!properties.TryGetValue(resultColumn, out var property)) { throw new InvalidOperationException( RelationalStrings.StoredProcedureResultColumnNotFound( @@ -332,7 +358,8 @@ private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy switch (storeObjectIdentifier.StoreObjectType) { case StoreObjectType.InsertStoredProcedure: - if ((property.ValueGenerated & ValueGenerated.OnAdd) == 0) + case StoreObjectType.UpdateStoredProcedure: + if (!storeGeneratedProperties.Remove(property.Name)) { throw new InvalidOperationException( RelationalStrings.StoredProcedureResultColumnNotGenerated( @@ -344,44 +371,65 @@ private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy 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())); - } - + default: + Check.DebugFail("Unexpected stored procedure type: " + storeObjectIdentifier.StoreObjectType); break; } } foreach (var parameter in sproc.Parameters) { - if (!properties!.TryGetAndRemove(parameter, out IProperty property)) + 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) + switch (storeObjectIdentifier.StoreObjectType) { - throw new InvalidOperationException( - RelationalStrings.StoredProcedureDeleteNonKeyProperty( - entityType.DisplayName(), parameter, storeObjectIdentifier.DisplayName())); + case StoreObjectType.InsertStoredProcedure: + case StoreObjectType.UpdateStoredProcedure: + if (property.GetDirection(storeObjectIdentifier) != ParameterDirection.Input + && !storeGeneratedProperties.Remove(property.Name)) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureOutputParameterNotGenerated( + entityType.DisplayName(), parameter, storeObjectIdentifier.DisplayName())); + } + + break; + case StoreObjectType.DeleteStoredProcedure: + if (!property.IsPrimaryKey() + && !property.IsConcurrencyToken) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureDeleteNonKeyProperty( + entityType.DisplayName(), parameter, storeObjectIdentifier.DisplayName())); + } + + break; + default: + Check.DebugFail("Unexpected stored procedure type: " + storeObjectIdentifier.StoreObjectType); + break; } } + + if (storeGeneratedProperties.Count > 0) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureGeneratedPropertiesNotMapped( + entityType.DisplayName(), + storeObjectIdentifier.DisplayName(), + storeGeneratedProperties.Values.Format())); + } foreach (var resultColumn in sproc.ResultColumns) { - properties!.Remove(resultColumn); + properties.Remove(resultColumn); } - if (properties != null - && properties.Count > 0) + if (properties.Count > 0) { foreach (var property in properties.Values.ToList()) { @@ -1729,7 +1777,7 @@ private static void ValidateTphMapping(IEntityType rootEntityType, StoreObjectTy var isSproc = storeObjectType == StoreObjectType.DeleteStoredProcedure || storeObjectType == StoreObjectType.InsertStoredProcedure || storeObjectType == StoreObjectType.UpdateStoredProcedure; - var rootSproc = isSproc ? StoredProcedure.GetDeclaredStoredProcedure(rootEntityType, storeObjectType) : null; + var rootSproc = isSproc ? StoredProcedure.FindDeclaredStoredProcedure(rootEntityType, storeObjectType) : null; var rootId = StoreObjectIdentifier.Create(rootEntityType, storeObjectType); foreach (var entityType in rootEntityType.GetDerivedTypes()) { @@ -1743,7 +1791,7 @@ private static void ValidateTphMapping(IEntityType rootEntityType, StoreObjectTy { if (rootSproc != null) { - var sproc = StoredProcedure.GetDeclaredStoredProcedure(entityType, storeObjectType); + var sproc = StoredProcedure.FindDeclaredStoredProcedure(entityType, storeObjectType); if (sproc != null && sproc != rootSproc) { diff --git a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationSplitViewBuilder.cs b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationSplitViewBuilder.cs index 4c02be8b994..c817902a03c 100644 --- a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationSplitViewBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationSplitViewBuilder.cs @@ -65,6 +65,22 @@ public virtual ViewColumnBuilder Property(string propertyName) public virtual ViewColumnBuilder Property(string propertyName) => new(MappingFragment.StoreObject, OwnedNavigationBuilder.Property(propertyName)); + /// + /// Adds or updates an annotation on the view. 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 OwnedNavigationSplitViewBuilder HasAnnotation(string annotation, object? value) + { + Check.NotEmpty(annotation, nameof(annotation)); + + ((EntityTypeMappingFragment)MappingFragment).Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); + + return this; + } + OwnedNavigationBuilder IInfrastructure.Instance => OwnedNavigationBuilder; #region Hidden System.Object members diff --git a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationSplitViewBuilder``.cs b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationSplitViewBuilder``.cs index bb27021503c..bd186465668 100644 --- a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationSplitViewBuilder``.cs +++ b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationSplitViewBuilder``.cs @@ -44,6 +44,17 @@ private OwnedNavigationBuilder OwnedNavigationBu public virtual ViewColumnBuilder Property(Expression> propertyExpression) => new(MappingFragment.StoreObject, OwnedNavigationBuilder.Property(propertyExpression)); + /// + /// Adds or updates an annotation on the view. 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 OwnedNavigationSplitViewBuilder HasAnnotation( + string annotation, object? value) + => (OwnedNavigationSplitViewBuilder)base.HasAnnotation(annotation, value); + OwnedNavigationBuilder IInfrastructure>.Instance => OwnedNavigationBuilder; } diff --git a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder.cs b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder.cs index 9de2c4a730d..52f8d56ef16 100644 --- a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder.cs @@ -70,7 +70,7 @@ public virtual OwnedNavigationStoredProcedureBuilder HasParameter(string propert public virtual OwnedNavigationStoredProcedureBuilder HasParameter(string propertyName, Action buildAction) { Builder.HasParameter(propertyName, ConfigurationSource.Explicit); - buildAction(new(((StoredProcedure)Metadata).CreateIdentifier()!.Value, CreatePropertyBuilder(propertyName))); + buildAction(new(Metadata.GetStoreIdentifier()!.Value, CreatePropertyBuilder(propertyName))); return this; } @@ -128,7 +128,7 @@ public virtual OwnedNavigationStoredProcedureBuilder HasResultColumn( string propertyName, Action buildAction) { Builder.HasResultColumn(propertyName, ConfigurationSource.Explicit); - buildAction(new(((StoredProcedure)Metadata).CreateIdentifier()!.Value, CreatePropertyBuilder(propertyName))); + buildAction(new(Metadata.GetStoreIdentifier()!.Value, CreatePropertyBuilder(propertyName))); return this; } diff --git a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder``.cs b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder``.cs index 1db167f39b9..9ef38f2d803 100644 --- a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder``.cs +++ b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder``.cs @@ -80,7 +80,7 @@ public virtual OwnedNavigationStoredProcedureBuilder buildAction) { Builder.HasParameter(propertyExpression, ConfigurationSource.Explicit); - buildAction(new(((StoredProcedure)Metadata).CreateIdentifier()!.Value, CreatePropertyBuilder(propertyExpression))); + buildAction(new(Metadata.GetStoreIdentifier()!.Value, CreatePropertyBuilder(propertyExpression))); return this; } @@ -131,7 +131,7 @@ public virtual OwnedNavigationStoredProcedureBuilder buildAction) { Builder.HasResultColumn(propertyExpression, ConfigurationSource.Explicit); - buildAction(new(((StoredProcedure)Metadata).CreateIdentifier()!.Value, CreatePropertyBuilder(propertyExpression))); + buildAction(new(Metadata.GetStoreIdentifier()!.Value, CreatePropertyBuilder(propertyExpression))); return this; } diff --git a/src/EFCore.Relational/Metadata/Builders/SplitViewBuilder.cs b/src/EFCore.Relational/Metadata/Builders/SplitViewBuilder.cs index 00e9f14a992..7b7d950a196 100644 --- a/src/EFCore.Relational/Metadata/Builders/SplitViewBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/SplitViewBuilder.cs @@ -65,6 +65,22 @@ public virtual ViewColumnBuilder Property(string propertyName) public virtual ViewColumnBuilder Property(string propertyName) => new(MappingFragment.StoreObject, EntityTypeBuilder.Property(propertyName)); + /// + /// Adds or updates an annotation on the view. 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 SplitViewBuilder HasAnnotation(string annotation, object? value) + { + Check.NotEmpty(annotation, nameof(annotation)); + + ((EntityTypeMappingFragment)MappingFragment).Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); + + return this; + } + EntityTypeBuilder IInfrastructure.Instance => EntityTypeBuilder; #region Hidden System.Object members diff --git a/src/EFCore.Relational/Metadata/Builders/SplitViewBuilder`.cs b/src/EFCore.Relational/Metadata/Builders/SplitViewBuilder`.cs index d260bd10490..6e25474ce5f 100644 --- a/src/EFCore.Relational/Metadata/Builders/SplitViewBuilder`.cs +++ b/src/EFCore.Relational/Metadata/Builders/SplitViewBuilder`.cs @@ -37,5 +37,15 @@ private EntityTypeBuilder EntityTypeBuilder public virtual ViewColumnBuilder Property(Expression> propertyExpression) => new(MappingFragment.StoreObject, EntityTypeBuilder.Property(propertyExpression)); + /// + /// Adds or updates an annotation on the view. 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 SplitViewBuilder HasAnnotation(string annotation, object? value) + => (SplitViewBuilder)base.HasAnnotation(annotation, value); + EntityTypeBuilder IInfrastructure>.Instance => EntityTypeBuilder; } diff --git a/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder.cs b/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder.cs index 880ad9aef4c..2862c434d98 100644 --- a/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder.cs @@ -68,7 +68,7 @@ public virtual StoredProcedureBuilder HasParameter( string propertyName, Action buildAction) { Builder.HasParameter(propertyName, ConfigurationSource.Explicit); - buildAction(new(((StoredProcedure)Metadata).CreateIdentifier()!.Value, CreatePropertyBuilder(propertyName))); + buildAction(new(Metadata.GetStoreIdentifier()!.Value, CreatePropertyBuilder(propertyName))); return this; } @@ -144,7 +144,7 @@ public virtual StoredProcedureBuilder HasResultColumn( string propertyName, Action buildAction) { Builder.HasResultColumn(propertyName, ConfigurationSource.Explicit); - buildAction(new(((StoredProcedure)Metadata).CreateIdentifier()!.Value, CreatePropertyBuilder(propertyName))); + buildAction(new(Metadata.GetStoreIdentifier()!.Value, CreatePropertyBuilder(propertyName))); return this; } diff --git a/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder`.cs b/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder`.cs index c918f6ef75d..f5a22972b9f 100644 --- a/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder`.cs +++ b/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder`.cs @@ -104,7 +104,7 @@ public virtual StoredProcedureBuilder HasParameter HasResultColumn + /// Configures the stored procedure parameter as both an input and output parameter. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The same builder instance so that further configuration calls can be chained. + public virtual StoredProcedureParameterBuilder IsInputOutput() + { + ((IMutableRelationalPropertyOverrides)InternalOverrides).Direction = ParameterDirection.InputOutput; + + return this; + } + + /// + /// Configures the stored procedure parameter as an output parameter. + /// + /// + /// See Modeling entity types and relationships and + /// Saving data with EF Core for more information and examples. + /// + /// The same builder instance so that further configuration calls can be chained. + public virtual StoredProcedureParameterBuilder IsOutput() + { + ((IMutableRelationalPropertyOverrides)InternalOverrides).Direction = ParameterDirection.Output; + + return this; + } + /// /// Adds or updates an annotation on the property for a specific stored procedure. /// If an annotation with the key specified in diff --git a/src/EFCore.Relational/Metadata/Builders/ViewColumnBuilder.cs b/src/EFCore.Relational/Metadata/Builders/ViewColumnBuilder.cs index dcf250de03e..78dc79f57da 100644 --- a/src/EFCore.Relational/Metadata/Builders/ViewColumnBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/ViewColumnBuilder.cs @@ -62,6 +62,23 @@ public virtual ViewColumnBuilder HasColumnName(string? name) return this; } + /// + /// Adds or updates an annotation on the property for a specific view. + /// 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 ViewColumnBuilder 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 diff --git a/src/EFCore.Relational/Metadata/Builders/ViewColumnBuilder`.cs b/src/EFCore.Relational/Metadata/Builders/ViewColumnBuilder`.cs index 7617aaca701..535e59b3b2d 100644 --- a/src/EFCore.Relational/Metadata/Builders/ViewColumnBuilder`.cs +++ b/src/EFCore.Relational/Metadata/Builders/ViewColumnBuilder`.cs @@ -35,5 +35,16 @@ private PropertyBuilder PropertyBuilder public new virtual ViewColumnBuilder HasColumnName(string? name) => (ViewColumnBuilder)base.HasColumnName(name); + /// + /// Adds or updates an annotation on the property for a specific view. + /// 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 ViewColumnBuilder HasAnnotation(string annotation, object? value) + => (ViewColumnBuilder)base.HasAnnotation(annotation, value); + PropertyBuilder IInfrastructure>.Instance => PropertyBuilder; } diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs index 2f145d18fad..3e14e8158f8 100644 --- a/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs @@ -122,6 +122,9 @@ protected override void ProcessEntityTypeAnnotations( annotations.Remove(RelationalAnnotationNames.ViewMappings); annotations.Remove(RelationalAnnotationNames.SqlQueryMappings); annotations.Remove(RelationalAnnotationNames.FunctionMappings); + annotations.Remove(RelationalAnnotationNames.InsertStoredProcedureMappings); + annotations.Remove(RelationalAnnotationNames.DeleteStoredProcedureMappings); + annotations.Remove(RelationalAnnotationNames.UpdateStoredProcedureMappings); annotations.Remove(RelationalAnnotationNames.DefaultMappings); } else @@ -171,6 +174,42 @@ protected override void ProcessEntityTypeAnnotations( annotations[RelationalAnnotationNames.Triggers] = runtimeTriggers; } + + if (annotations.TryGetValue(RelationalAnnotationNames.InsertStoredProcedure, out var insertStoredProcedure)) + { + var runtimeSproc = Create((IStoredProcedure)insertStoredProcedure!, runtimeEntityType); + + CreateAnnotations( + (IStoredProcedure)insertStoredProcedure!, runtimeSproc, + static (convention, annotations, source, target, runtime) + => convention.ProcessStoredProcedureAnnotations(annotations, source, target, runtime)); + + annotations[RelationalAnnotationNames.InsertStoredProcedure] = runtimeSproc; + } + + if (annotations.TryGetValue(RelationalAnnotationNames.DeleteStoredProcedure, out var deleteStoredProcedure)) + { + var runtimeSproc = Create((IStoredProcedure)deleteStoredProcedure!, runtimeEntityType); + + CreateAnnotations( + (IStoredProcedure)deleteStoredProcedure!, runtimeSproc, + static (convention, annotations, source, target, runtime) + => convention.ProcessStoredProcedureAnnotations(annotations, source, target, runtime)); + + annotations[RelationalAnnotationNames.DeleteStoredProcedure] = runtimeSproc; + } + + if (annotations.TryGetValue(RelationalAnnotationNames.UpdateStoredProcedure, out var updateStoredProcedure)) + { + var runtimeSproc = Create((IStoredProcedure)updateStoredProcedure!, runtimeEntityType); + + CreateAnnotations( + (IStoredProcedure)updateStoredProcedure!, runtimeSproc, + static (convention, annotations, source, target, runtime) + => convention.ProcessStoredProcedureAnnotations(annotations, source, target, runtime)); + + annotations[RelationalAnnotationNames.UpdateStoredProcedure] = runtimeSproc; + } } } @@ -315,6 +354,11 @@ protected override void ProcessPropertyAnnotations( annotations.Remove(RelationalAnnotationNames.ViewColumnMappings); annotations.Remove(RelationalAnnotationNames.SqlQueryColumnMappings); annotations.Remove(RelationalAnnotationNames.FunctionColumnMappings); + annotations.Remove(RelationalAnnotationNames.InsertStoredProcedureParameterMappings); + annotations.Remove(RelationalAnnotationNames.InsertStoredProcedureResultColumnMappings); + annotations.Remove(RelationalAnnotationNames.DeleteStoredProcedureParameterMappings); + annotations.Remove(RelationalAnnotationNames.UpdateStoredProcedureParameterMappings); + annotations.Remove(RelationalAnnotationNames.UpdateStoredProcedureResultColumnMappings); annotations.Remove(RelationalAnnotationNames.DefaultColumnMappings); } else @@ -430,7 +474,7 @@ private static RuntimeTrigger Create(ITrigger trigger, RuntimeEntityType runtime => new(runtimeEntityType, trigger.ModelName, trigger.Name, trigger.TableName, trigger.TableSchema); /// - /// Updates the function annotations that will be set on the read-only object. + /// Updates the trigger annotations that will be set on the read-only object. /// /// The annotations to be processed. /// The source trigger. @@ -443,4 +487,25 @@ protected virtual void ProcessTriggerAnnotations( bool runtime) { } + + private static RuntimeStoredProcedure Create(IStoredProcedure storedProcedure, RuntimeEntityType runtimeEntityType) + => new(runtimeEntityType, + storedProcedure.Name, + storedProcedure.Schema, + storedProcedure.AreTransactionsSuppressed); + + /// + /// Updates the stored procedure annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source stored procedure. + /// The target stored procedure that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected virtual void ProcessStoredProcedureAnnotations( + Dictionary annotations, + IStoredProcedure storedProcedure, + RuntimeStoredProcedure runtimeStoredProcedure, + bool runtime) + { + } } diff --git a/src/EFCore.Relational/Metadata/IColumn.cs b/src/EFCore.Relational/Metadata/IColumn.cs index 99685f72dc3..dd54cc67a9f 100644 --- a/src/EFCore.Relational/Metadata/IColumn.cs +++ b/src/EFCore.Relational/Metadata/IColumn.cs @@ -160,19 +160,8 @@ public virtual ValueComparer ProviderValueComparer /// /// An entity type. /// The property mapping or if not found. - public virtual IColumnMapping? FindColumnMapping(IReadOnlyEntityType entityType) - { - for (var i = 0; i < PropertyMappings.Count; i++) - { - var mapping = PropertyMappings[i]; - if (mapping.Property.DeclaringEntityType.IsAssignableFrom(entityType)) - { - return mapping; - } - } - - return null; - } + new IColumnMapping? FindColumnMapping(IReadOnlyEntityType entityType) + => (IColumnMapping?)((IColumnBase)this).FindColumnMapping(entityType); /// /// diff --git a/src/EFCore.Relational/Metadata/IColumnBase.cs b/src/EFCore.Relational/Metadata/IColumnBase.cs index 4588f10abcd..565bee56ee4 100644 --- a/src/EFCore.Relational/Metadata/IColumnBase.cs +++ b/src/EFCore.Relational/Metadata/IColumnBase.cs @@ -40,4 +40,23 @@ public interface IColumnBase : IAnnotatable /// Gets the property mappings. /// IReadOnlyList PropertyMappings { get; } + + /// + /// Returns the property mapping for the given entity type. + /// + /// An entity type. + /// The property mapping or if not found. + public virtual IColumnMappingBase? FindColumnMapping(IReadOnlyEntityType entityType) + { + for (var i = 0; i < PropertyMappings.Count; i++) + { + var mapping = PropertyMappings[i]; + if (mapping.Property.DeclaringEntityType.IsAssignableFrom(entityType)) + { + return mapping; + } + } + + return null; + } } diff --git a/src/EFCore.Relational/Metadata/IConventionRelationalPropertyOverrides.cs b/src/EFCore.Relational/Metadata/IConventionRelationalPropertyOverrides.cs index fff6a25c68a..687b4e490b7 100644 --- a/src/EFCore.Relational/Metadata/IConventionRelationalPropertyOverrides.cs +++ b/src/EFCore.Relational/Metadata/IConventionRelationalPropertyOverrides.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Data; + namespace Microsoft.EntityFrameworkCore.Metadata; /// @@ -48,4 +50,20 @@ public interface IConventionRelationalPropertyOverrides : IReadOnlyRelationalPro /// /// The configuration source for . ConfigurationSource? GetColumnNameConfigurationSource(); + + /// + /// Sets the direction of the stored procedure parameter. + /// + /// The direction. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + ParameterDirection? SetDirection(ParameterDirection? direction, bool fromDataAnnotation = false) + => ((ParameterDirection?)SetAnnotation(RelationalAnnotationNames.ParameterDirection, direction, fromDataAnnotation)?.Value); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetDirectionConfigurationSource() + => FindAnnotation(RelationalAnnotationNames.ParameterDirection)?.GetConfigurationSource(); } diff --git a/src/EFCore.Relational/Metadata/IConventionStoredProcedure.cs b/src/EFCore.Relational/Metadata/IConventionStoredProcedure.cs index 2c83f9883ac..1e83d739f97 100644 --- a/src/EFCore.Relational/Metadata/IConventionStoredProcedure.cs +++ b/src/EFCore.Relational/Metadata/IConventionStoredProcedure.cs @@ -6,30 +6,30 @@ namespace Microsoft.EntityFrameworkCore.Metadata; /// -/// Represents a relational database function in a model in +/// Represents a stored procedure 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. + /// Gets the entity type in which this stored procedure is defined. /// new IConventionEntityType EntityType { get; } /// - /// Gets the builder that can be used to configure this function. + /// Gets the builder that can be used to configure this stored procedure. /// /// If the function has been removed from the model. new IConventionStoredProcedureBuilder Builder { get; } /// - /// Gets the configuration source for this function. + /// Gets the configuration source for this stored procedure. /// /// The configuration source for this function. ConfigurationSource GetConfigurationSource(); /// - /// Sets the name of the function in the database. + /// Sets the name of the stored procedure in the database. /// /// The name of the function in the database. /// Indicates whether the configuration was specified using a data annotation. @@ -43,7 +43,7 @@ public interface IConventionStoredProcedure : IReadOnlyStoredProcedure, IConvent ConfigurationSource? GetNameConfigurationSource(); /// - /// Sets the schema of the function in the database. + /// Sets the schema of the stored procedure in the database. /// /// The schema of the function in the database. /// Indicates whether the configuration was specified using a data annotation. diff --git a/src/EFCore.Relational/Metadata/IFunctionColumn.cs b/src/EFCore.Relational/Metadata/IFunctionColumn.cs index 6ff09a72e94..7c6608e6a9a 100644 --- a/src/EFCore.Relational/Metadata/IFunctionColumn.cs +++ b/src/EFCore.Relational/Metadata/IFunctionColumn.cs @@ -23,6 +23,14 @@ public interface IFunctionColumn : IColumnBase /// new IReadOnlyList PropertyMappings { get; } + /// + /// Returns the property mapping for the given entity type. + /// + /// An entity type. + /// The property mapping or if not found. + new IFunctionColumnMapping? FindColumnMapping(IReadOnlyEntityType entityType) + => (IFunctionColumnMapping?)((IColumnBase)this).FindColumnMapping(entityType); + /// /// /// Creates a human-readable representation of the given metadata. diff --git a/src/EFCore.Relational/Metadata/IFunctionMapping.cs b/src/EFCore.Relational/Metadata/IFunctionMapping.cs index e6560eac15c..9925059082c 100644 --- a/src/EFCore.Relational/Metadata/IFunctionMapping.cs +++ b/src/EFCore.Relational/Metadata/IFunctionMapping.cs @@ -59,9 +59,9 @@ string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOpt builder.Append("FunctionMapping: "); } - builder.Append(EntityType.Name).Append(" - "); + builder.Append(EntityType.DisplayName()).Append(" - "); - builder.Append(Table.Name); + builder.Append(StoreFunction.Name); if (IsDefaultFunctionMapping) { diff --git a/src/EFCore.Relational/Metadata/IMutableRelationalPropertyOverrides.cs b/src/EFCore.Relational/Metadata/IMutableRelationalPropertyOverrides.cs index 45063bdce75..badb6b3eb2f 100644 --- a/src/EFCore.Relational/Metadata/IMutableRelationalPropertyOverrides.cs +++ b/src/EFCore.Relational/Metadata/IMutableRelationalPropertyOverrides.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Data; + namespace Microsoft.EntityFrameworkCore.Metadata; /// @@ -21,6 +23,15 @@ public interface IMutableRelationalPropertyOverrides : IReadOnlyRelationalProper /// new string? ColumnName { get; set; } + /// + /// Gets or sets the direction of the stored procedure parameter. + /// + new ParameterDirection? Direction + { + get => ((ParameterDirection?)this[RelationalAnnotationNames.ParameterDirection]); + set => SetAnnotation(RelationalAnnotationNames.ParameterDirection, value); + } + /// /// Removes the column name override. /// diff --git a/src/EFCore.Relational/Metadata/IMutableStoredProcedure.cs b/src/EFCore.Relational/Metadata/IMutableStoredProcedure.cs index 093d6f2a66b..25643f7622c 100644 --- a/src/EFCore.Relational/Metadata/IMutableStoredProcedure.cs +++ b/src/EFCore.Relational/Metadata/IMutableStoredProcedure.cs @@ -6,23 +6,23 @@ namespace Microsoft.EntityFrameworkCore.Metadata; /// -/// Represents a relational database function in an model in +/// Represents a stored procedure in a 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. + /// Gets or sets the name of the stored procedure in the database. /// new string? Name { get; [param: NotNull] set; } /// - /// Gets or sets the schema of the function in the database. + /// Gets or sets the schema of the stored procedure in the database. /// new string? Schema { get; set; } /// - /// Gets the entity type in which this function is defined. + /// Gets the entity type in which this stored procedure is defined. /// new IMutableEntityType EntityType { get; } diff --git a/src/EFCore.Relational/Metadata/IReadOnlyRelationalPropertyOverrides.cs b/src/EFCore.Relational/Metadata/IReadOnlyRelationalPropertyOverrides.cs index 07dad4656a7..3d5cf0bb282 100644 --- a/src/EFCore.Relational/Metadata/IReadOnlyRelationalPropertyOverrides.cs +++ b/src/EFCore.Relational/Metadata/IReadOnlyRelationalPropertyOverrides.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Data; using System.Text; namespace Microsoft.EntityFrameworkCore.Metadata; @@ -33,6 +34,12 @@ public interface IReadOnlyRelationalPropertyOverrides : IReadOnlyAnnotatable /// bool IsColumnNameOverridden { get; } + /// + /// Gets the direction of the stored procedure parameter. + /// + ParameterDirection? Direction + => ((ParameterDirection?)this[RelationalAnnotationNames.ParameterDirection]); + /// /// /// Creates a human-readable representation of the given metadata. diff --git a/src/EFCore.Relational/Metadata/IReadOnlyStoredProcedure.cs b/src/EFCore.Relational/Metadata/IReadOnlyStoredProcedure.cs index 23739096e18..ba911ba0411 100644 --- a/src/EFCore.Relational/Metadata/IReadOnlyStoredProcedure.cs +++ b/src/EFCore.Relational/Metadata/IReadOnlyStoredProcedure.cs @@ -31,6 +31,36 @@ public interface IReadOnlyStoredProcedure : IReadOnlyAnnotatable /// The configured value. bool AreTransactionsSuppressed { get; } + /// + /// Returns the store identifier of this stored procedure. + /// + /// The store identifier. if there is no corresponding store object. + StoreObjectIdentifier? GetStoreIdentifier() + { + 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; + } + /// /// Gets the names of properties mapped to parameters for this stored procedure. /// diff --git a/src/EFCore.Relational/Metadata/IRelationalAnnotationProvider.cs b/src/EFCore.Relational/Metadata/IRelationalAnnotationProvider.cs index 6e8cdb7a00e..024c470f1e2 100644 --- a/src/EFCore.Relational/Metadata/IRelationalAnnotationProvider.cs +++ b/src/EFCore.Relational/Metadata/IRelationalAnnotationProvider.cs @@ -92,6 +92,30 @@ public interface IRelationalAnnotationProvider /// Whether the model should contain design-time configuration. IEnumerable For(IFunctionColumn column, bool designTime); + /// + /// Gets provider-specific annotations for the given . + /// + /// The stored procedure. + /// The annotations. + /// Whether the model should contain design-time configuration. + IEnumerable For(IStoreStoredProcedure storedProcedure, bool designTime); + + /// + /// Gets provider-specific annotations for the given . + /// + /// The parameter. + /// The annotations. + /// Whether the model should contain design-time configuration. + IEnumerable For(IStoreStoredProcedureParameter parameter, bool designTime); + + /// + /// Gets provider-specific annotations for the given . + /// + /// The result column. + /// The annotations. + /// Whether the model should contain design-time configuration. + IEnumerable For(IStoreStoredProcedureResultColumn column, bool designTime); + /// /// Gets provider-specific annotations for the given . /// diff --git a/src/EFCore.Relational/Metadata/IRelationalModel.cs b/src/EFCore.Relational/Metadata/IRelationalModel.cs index 2d1afdb1cd6..befc25d803a 100644 --- a/src/EFCore.Relational/Metadata/IRelationalModel.cs +++ b/src/EFCore.Relational/Metadata/IRelationalModel.cs @@ -46,6 +46,11 @@ IEnumerable Sequences /// IEnumerable Functions { get; } + /// + /// Returns all stored procedures contained in the model. + /// + IEnumerable StoredProcedures { get; } + /// /// Returns the database collation. /// @@ -93,9 +98,17 @@ IEnumerable Sequences /// The name of the function. /// The schema of the function. /// A list of parameter types. - /// The or if no function with the given name was defined. + /// The or if no function with the given name was found. IStoreFunction? FindFunction(string name, string? schema, IReadOnlyList parameters); + /// + /// Finds a with the name. + /// + /// The name of the stored procedure. + /// The schema of the stored procedure. + /// The or if no stored procedure with the given name was found. + IStoreStoredProcedure? FindStoredProcedure(string name, string? schema); + /// /// /// Creates a human-readable representation of the given metadata. diff --git a/src/EFCore.Relational/Metadata/ISqlQueryColumn.cs b/src/EFCore.Relational/Metadata/ISqlQueryColumn.cs index e07d84a5b66..022c86733a7 100644 --- a/src/EFCore.Relational/Metadata/ISqlQueryColumn.cs +++ b/src/EFCore.Relational/Metadata/ISqlQueryColumn.cs @@ -24,6 +24,14 @@ public interface ISqlQueryColumn : IColumnBase /// new IReadOnlyList PropertyMappings { get; } + /// + /// Returns the property mapping for the given entity type. + /// + /// An entity type. + /// The property mapping or if not found. + new ISqlQueryColumnMapping? FindColumnMapping(IReadOnlyEntityType entityType) + => (ISqlQueryColumnMapping?)((IColumnBase)this).FindColumnMapping(entityType); + /// /// /// Creates a human-readable representation of the given metadata. diff --git a/src/EFCore.Relational/Metadata/IStoreFunction.cs b/src/EFCore.Relational/Metadata/IStoreFunction.cs index 280ae3e5f63..3f875d38189 100644 --- a/src/EFCore.Relational/Metadata/IStoreFunction.cs +++ b/src/EFCore.Relational/Metadata/IStoreFunction.cs @@ -14,7 +14,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata; public interface IStoreFunction : ITableBase { /// - /// Gets the associated s. + /// Gets the associated model functions. /// IEnumerable DbFunctions { get; } diff --git a/src/EFCore.Relational/Metadata/IStoreStoredProcedure.cs b/src/EFCore.Relational/Metadata/IStoreStoredProcedure.cs new file mode 100644 index 00000000000..b78c5b8c360 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IStoreStoredProcedure.cs @@ -0,0 +1,134 @@ +// 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 database. +/// +public interface IStoreStoredProcedure : ITableBase +{ + /// + /// Gets the associated model stored procedures. + /// + IEnumerable StoredProcedures { get; } + + /// + /// Gets the entity type mappings. + /// + new IEnumerable EntityTypeMappings { get; } + + /// + /// Gets the parameters for this stored procedures. + /// + IEnumerable Parameters { get; } + + /// + /// Gets the parameter with the given name. Returns + /// if no parameter with the given name is defined for the returned row set. + /// + IStoreStoredProcedureParameter? FindParameter(string name); + + /// + /// Gets the parameter mapped to the given property. Returns + /// if no parameter is mapped to the given property. + /// + IStoreStoredProcedureParameter? FindParameter(IProperty property); + + /// + /// Gets the columns defined for the returned row set. + /// + IEnumerable ResultColumns { get; } + + /// + /// Gets the result column with the given name. Returns + /// if no result column with the given name is defined for the returned row set. + /// + IStoreStoredProcedureResultColumn? FindResultColumn(string name); + + /// + /// Gets the result column mapped to the given property. Returns + /// if no result column is mapped to the given property. + /// + IStoreStoredProcedureResultColumn? FindResultColumn(IProperty property); + + /// + /// + /// 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); + + try + { + builder + .Append(indentString) + .Append("StoreStoredProcedure: "); + + 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.ToDebugString(options, indent + 4)); + } + } + + var resultColumns = ResultColumns.ToList(); + if (resultColumns.Count != 0) + { + builder.AppendLine().Append(indentString).Append(" ResultColumns: "); + foreach (var column in resultColumns) + { + builder.AppendLine().Append(column.ToDebugString(options, indent + 4)); + } + } + + var mappings = EntityTypeMappings.ToList(); + if (mappings.Count != 0) + { + builder.AppendLine().Append(indentString).Append(" EntityTypeMappings: "); + foreach (var mapping in mappings) + { + builder.AppendLine().Append(mapping.ToDebugString(options, indent + 4)); + } + } + + if ((options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(AnnotationsToDebugString(indent: indent + 2)); + } + } + } + catch (Exception exception) + { + builder.AppendLine().AppendLine(CoreStrings.DebugViewError(exception.Message)); + } + + return builder.ToString(); + } +} diff --git a/src/EFCore.Relational/Metadata/IStoreStoredProcedureParameter.cs b/src/EFCore.Relational/Metadata/IStoreStoredProcedureParameter.cs new file mode 100644 index 00000000000..de157b328e4 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IStoreStoredProcedureParameter.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Data; +using System.Text; + +namespace Microsoft.EntityFrameworkCore.Metadata; + +/// +/// Represents a parameter in a stored procedure. +/// +public interface IStoreStoredProcedureParameter : IColumnBase +{ + /// + /// Gets the containing stored procedure. + /// + IStoreStoredProcedure StoredProcedure { get; } + + /// + /// Gets the property mappings. + /// + new IReadOnlyList PropertyMappings { get; } + + /// + /// Gets the direction of the parameter. + /// + ParameterDirection Direction { get; } + + /// + /// Returns the property mapping for the given entity type. + /// + /// An entity type. + /// The property mapping or if not found. + IStoredProcedureParameterMapping? FindParameterMapping(IReadOnlyEntityType entityType) + => (IStoredProcedureParameterMapping?)FindColumnMapping(entityType); + + /// + /// + /// 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); + + var singleLine = (options & MetadataDebugStringOptions.SingleLine) != 0; + if (singleLine) + { + builder.Append($"StoredProcedureParameter: {Table.Name}."); + } + + builder.Append(Name).Append(" ("); + builder.Append(StoreType).Append(')'); + builder.Append(IsNullable ? " Nullable" : " NonNullable"); + builder.Append(')'); + + if (Direction != ParameterDirection.Input) + { + builder.Append(" ").Append(Direction); + } + + if (!singleLine && (options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(AnnotationsToDebugString(indent + 2)); + } + + return builder.ToString(); + } +} diff --git a/src/EFCore.Relational/Metadata/IStoreStoredProcedureResultColumn.cs b/src/EFCore.Relational/Metadata/IStoreStoredProcedureResultColumn.cs new file mode 100644 index 00000000000..4efca1fcbc9 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IStoreStoredProcedureResultColumn.cs @@ -0,0 +1,68 @@ +// 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 result column in a stored procedure. +/// +public interface IStoreStoredProcedureResultColumn : IColumnBase +{ + /// + /// Gets the containing stored procedure. + /// + IStoreStoredProcedure StoredProcedure { get; } + + /// + /// Gets the property mappings. + /// + new IReadOnlyList PropertyMappings { get; } + + /// + /// Returns the property mapping for the given entity type. + /// + /// An entity type. + /// The property mapping or if not found. + new IStoredProcedureResultColumnMapping? FindColumnMapping(IReadOnlyEntityType entityType) + => (IStoredProcedureResultColumnMapping?)((IColumnBase)this).FindColumnMapping(entityType); + + /// + /// + /// 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); + + var singleLine = (options & MetadataDebugStringOptions.SingleLine) != 0; + if (singleLine) + { + builder.Append($"StoredProcedureResultColumn: {Table.Name}."); + } + + builder.Append(Name).Append(" ("); + builder.Append(StoreType).Append(')'); + builder.Append(IsNullable ? " Nullable" : " NonNullable"); + builder.Append(')'); + + if (!singleLine && (options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(AnnotationsToDebugString(indent + 2)); + } + + return builder.ToString(); + } +} diff --git a/src/EFCore.Relational/Metadata/IStoredProcedure.cs b/src/EFCore.Relational/Metadata/IStoredProcedure.cs index 2e571045add..3b0cf61e6cf 100644 --- a/src/EFCore.Relational/Metadata/IStoredProcedure.cs +++ b/src/EFCore.Relational/Metadata/IStoredProcedure.cs @@ -4,25 +4,30 @@ namespace Microsoft.EntityFrameworkCore.Metadata; /// -/// Represents a relational database function in a model. +/// Represents a stored procedure 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. + /// Gets the entity type in which this stored procedure is defined. /// new IEntityType EntityType { get; } - ///// - ///// Gets the associated . - ///// - //IStoreFunction StoreFunction { get; } + /// + /// Gets the associated database stored procedure. + /// + IStoreStoredProcedure StoreStoredProcedure { get; } + + /// + /// Returns the store identifier of this stored procedure. + /// + /// The store identifier. + new StoreObjectIdentifier GetStoreIdentifier() + => ((IReadOnlyStoredProcedure)this).GetStoreIdentifier()!.Value; + } diff --git a/src/EFCore.Relational/Metadata/IStoredProcedureMapping.cs b/src/EFCore.Relational/Metadata/IStoredProcedureMapping.cs new file mode 100644 index 00000000000..5a29707021a --- /dev/null +++ b/src/EFCore.Relational/Metadata/IStoredProcedureMapping.cs @@ -0,0 +1,81 @@ +// 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 entity type mapping to a stored procedure. +/// +public interface IStoredProcedureMapping : ITableMappingBase +{ + /// + /// Gets the target stored procedure in the database. + /// + IStoreStoredProcedure StoreStoredProcedure { get; } + + /// + /// Gets the target stored procedure in the model. + /// + IStoredProcedure StoredProcedure { get; } + + /// + /// Gets the stored procedure identifier including whether it's used for insert, delete or update. + /// + StoreObjectIdentifier StoredProcedureIdentifier { get; } + + /// + /// Gets the parameter mappings corresponding to the target stored procedure. + /// + IEnumerable ParameterMappings { get; } + + /// + /// Gets the result column mappings corresponding to the target stored procedure. + /// + IEnumerable ResultColumnMappings { get; } + + /// + /// + /// 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); + + var singleLine = (options & MetadataDebugStringOptions.SingleLine) != 0; + if (singleLine) + { + builder.Append("StoredProcedureMapping: "); + } + + builder.Append(EntityType.DisplayName()).Append(" - "); + + builder.Append(StoreStoredProcedure.Name); + + builder.Append(" Type:").Append(StoredProcedureIdentifier.StoreObjectType); + + if (IncludesDerivedTypes) + { + builder.Append(" IncludesDerivedTypes"); + } + + if (!singleLine && (options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(AnnotationsToDebugString(indent + 2)); + } + + return builder.ToString(); + } +} diff --git a/src/EFCore.Relational/Metadata/IStoredProcedureParameterMapping.cs b/src/EFCore.Relational/Metadata/IStoredProcedureParameterMapping.cs new file mode 100644 index 00000000000..f3f38d5817c --- /dev/null +++ b/src/EFCore.Relational/Metadata/IStoredProcedureParameterMapping.cs @@ -0,0 +1,59 @@ +// 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 property mapping to a stored procedure parameter. +/// +public interface IStoredProcedureParameterMapping : IColumnMappingBase +{ + /// + /// Gets the target parameter. + /// + IStoreStoredProcedureParameter Parameter { get; } + + /// + /// Gets the containing stored procedure mapping. + /// + IStoredProcedureMapping StoredProcedureMapping { get; } + + /// + /// + /// 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); + + var singleLine = (options & MetadataDebugStringOptions.SingleLine) != 0; + if (singleLine) + { + builder.Append("StoredProcedureParameterMapping: "); + } + + builder.Append(Property.Name).Append(" - "); + + builder.Append(Column.Name); + + if (!singleLine && (options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(AnnotationsToDebugString(indent + 2)); + } + + return builder.ToString(); + } +} diff --git a/src/EFCore.Relational/Metadata/IStoredProcedureResultColumnMapping.cs b/src/EFCore.Relational/Metadata/IStoredProcedureResultColumnMapping.cs new file mode 100644 index 00000000000..fc4ce0ed160 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IStoredProcedureResultColumnMapping.cs @@ -0,0 +1,59 @@ +// 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 property mapping to a stored procedure result column. +/// +public interface IStoredProcedureResultColumnMapping : IColumnMappingBase +{ + /// + /// Gets the target column. + /// + new IStoreStoredProcedureResultColumn Column { get; } + + /// + /// Gets the containing stored procedure mapping. + /// + IStoredProcedureMapping StoredProcedureMapping { get; } + + /// + /// + /// 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); + + var singleLine = (options & MetadataDebugStringOptions.SingleLine) != 0; + if (singleLine) + { + builder.Append("StoredProcedureResultColumnMapping: "); + } + + builder.Append(Property.Name).Append(" - "); + + builder.Append(Column.Name); + + if (!singleLine && (options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(AnnotationsToDebugString(indent + 2)); + } + + return builder.ToString(); + } +} diff --git a/src/EFCore.Relational/Metadata/IViewColumn.cs b/src/EFCore.Relational/Metadata/IViewColumn.cs index a43bb1e76b8..40a82aa2b20 100644 --- a/src/EFCore.Relational/Metadata/IViewColumn.cs +++ b/src/EFCore.Relational/Metadata/IViewColumn.cs @@ -23,6 +23,14 @@ public interface IViewColumn : IColumnBase /// new IReadOnlyList PropertyMappings { get; } + /// + /// Returns the property mapping for the given entity type. + /// + /// An entity type. + /// The property mapping or if not found. + new IViewColumnMapping? FindColumnMapping(IReadOnlyEntityType entityType) + => (IViewColumnMapping?)((IColumnBase)this).FindColumnMapping(entityType); + /// /// /// Creates a human-readable representation of the given metadata. diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs index 0e6f337e43c..b1878d8a0ae 100644 --- a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs +++ b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs @@ -25,6 +25,7 @@ public class DbFunction : ConventionAnnotatable, IMutableDbFunction, IConvention private RelationalTypeMapping? _typeMapping; private Func, SqlExpression>? _translation; private InternalDbFunctionBuilder? _builder; + private IStoreFunction? _storeFunction; private ConfigurationSource _configurationSource; private ConfigurationSource? _schemaConfigurationSource; @@ -642,15 +643,6 @@ public virtual IReadOnlyList Parameters 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 - /// 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 @@ -767,11 +759,11 @@ bool IConventionDbFunction.SetIsNullable(bool nullable, bool fromDataAnnotation) /// IStoreFunction IDbFunction.StoreFunction - => StoreFunction!; // Relational model creation ensures StoreFunction is populated + => _storeFunction!; // Relational model creation ensures StoreFunction is populated IStoreFunction IRuntimeDbFunction.StoreFunction { - get => StoreFunction!; - set => StoreFunction = value; + get => _storeFunction!; + set => _storeFunction = value; } } diff --git a/src/EFCore.Relational/Metadata/Internal/FunctionColumnMapping.cs b/src/EFCore.Relational/Metadata/Internal/FunctionColumnMapping.cs index ec3c11cb122..43770ef97d0 100644 --- a/src/EFCore.Relational/Metadata/Internal/FunctionColumnMapping.cs +++ b/src/EFCore.Relational/Metadata/Internal/FunctionColumnMapping.cs @@ -20,8 +20,8 @@ public class FunctionColumnMapping : ColumnMappingBase, IFunctionColumnMapping public FunctionColumnMapping( IProperty property, FunctionColumn column, - FunctionMapping viewMapping) - : base(property, column, viewMapping) + FunctionMapping functionMapping) + : base(property, column, functionMapping) { } diff --git a/src/EFCore.Relational/Metadata/Internal/IRuntimeStoredProcedure.cs b/src/EFCore.Relational/Metadata/Internal/IRuntimeStoredProcedure.cs new file mode 100644 index 00000000000..29c667a548e --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/IRuntimeStoredProcedure.cs @@ -0,0 +1,21 @@ +// 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 interface IRuntimeStoredProcedure : IStoredProcedure +{ + /// + /// 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. + /// + new IStoreStoredProcedure StoreStoredProcedure { get; set; } +} diff --git a/src/EFCore.Relational/Metadata/Internal/InternalStoredProcedureBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalStoredProcedureBuilder.cs index 834f6d9f868..b4bebbfdef0 100644 --- a/src/EFCore.Relational/Metadata/Internal/InternalStoredProcedureBuilder.cs +++ b/src/EFCore.Relational/Metadata/Internal/InternalStoredProcedureBuilder.cs @@ -38,7 +38,7 @@ public static InternalStoredProcedureBuilder HasStoredProcedure( string? name = null, string? schema = null) { - var sproc = StoredProcedure.GetDeclaredStoredProcedure(entityType, sprocType); + var sproc = (StoredProcedure?)StoredProcedure.FindDeclaredStoredProcedure(entityType, sprocType); if (sproc == null) { sproc = name == null @@ -69,7 +69,7 @@ public static InternalStoredProcedureBuilder HasStoredProcedure( StoreObjectType sprocType, bool fromDataAnnotation) { - var sproc = StoredProcedure.GetDeclaredStoredProcedure(entityType, sprocType); + var sproc = (StoredProcedure?)StoredProcedure.FindDeclaredStoredProcedure(entityType, sprocType); if (sproc == null) { sproc = StoredProcedure.SetStoredProcedure(entityType, sprocType, fromDataAnnotation); @@ -156,57 +156,6 @@ public virtual bool CanSetName(string? name, ConfigurationSource configurationSo 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 diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index d01f84c0462..534d3160ffc 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -81,6 +81,15 @@ public override bool IsReadOnly public virtual SortedDictionary<(string, string?, IReadOnlyList), StoreFunction> Functions { get; } = new(NamedListComparer.Instance); + /// + /// 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 SortedDictionary<(string, string?), StoreStoredProcedure> StoredProcedures { get; } + = new(); + /// public virtual ITable? FindTable(string name, string? schema) => Tables.TryGetValue((name, schema), out var table) @@ -105,6 +114,12 @@ public override bool IsReadOnly ? function : null; + /// + public virtual IStoreStoredProcedure? FindStoredProcedure(string name, string? schema) + => StoredProcedures.TryGetValue((name, schema), out var storedProcedure) + ? storedProcedure + : 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 @@ -144,6 +159,8 @@ public static IRelationalModel Create( AddSqlQueries(databaseModel, entityType); AddMappedFunctions(databaseModel, entityType); + + AddStoredProcedures(databaseModel, entityType); } AddTvfs(databaseModel); @@ -215,9 +232,9 @@ public static IRelationalModel Create( } } - foreach (var query in databaseModel.Queries.Values) + if (relationalAnnotationProvider != null) { - if (relationalAnnotationProvider != null) + foreach (var query in databaseModel.Queries.Values) { foreach (SqlQueryColumn queryColumn in query.Columns.Values) { @@ -226,11 +243,8 @@ public static IRelationalModel Create( query.AddAnnotations(relationalAnnotationProvider.For(query, designTime)); } - } - foreach (var function in databaseModel.Functions.Values) - { - if (relationalAnnotationProvider != null) + foreach (var function in databaseModel.Functions.Values) { foreach (FunctionColumn functionColumn in function.Columns.Values) { @@ -239,10 +253,22 @@ public static IRelationalModel Create( function.AddAnnotations(relationalAnnotationProvider.For(function, designTime)); } - } - if (relationalAnnotationProvider != null) - { + foreach (var storedProcedure in databaseModel.StoredProcedures.Values) + { + foreach (StoreStoredProcedureParameter parameter in storedProcedure.Parameters) + { + parameter.AddAnnotations(relationalAnnotationProvider.For(parameter, designTime)); + } + + foreach (StoreStoredProcedureResultColumn resultColumn in storedProcedure.ResultColumns) + { + resultColumn.AddAnnotations(relationalAnnotationProvider.For(resultColumn, designTime)); + } + + storedProcedure.AddAnnotations(relationalAnnotationProvider.For(storedProcedure, designTime)); + } + foreach (var sequence in ((IRelationalModel)databaseModel).Sequences) { ((AnnotatableBase)sequence).AddAnnotations(relationalAnnotationProvider.For(sequence, designTime)); @@ -273,7 +299,8 @@ private static void AddDefaultMappings(RelationalModel databaseModel, IEntityTyp databaseModel.DefaultTables.Add(mappedTableName, defaultTable); } - var tableMapping = new TableMappingBase(entityType, defaultTable, includesDerivedTypes: !isTpc && mappedType == entityType); + var tableMapping = new TableMappingBase( + entityType, defaultTable, includesDerivedTypes: !isTpc && mappedType == entityType); foreach (var property in entityType.GetProperties()) { @@ -353,7 +380,7 @@ private static void AddTables(RelationalModel databaseModel, IEntityType entityT if (mappedTableName == null) { - if (isTpc) + if (isTpc || mappingStrategy == RelationalAnnotationNames.TphMappingStrategy) { break; } @@ -394,7 +421,7 @@ private static void AddTables(RelationalModel databaseModel, IEntityType entityT tableMappings.Reverse(); } - private static TableMapping CreateTableMapping( + private static void CreateTableMapping( IEntityType entityType, IEntityType mappedType, StoreObjectIdentifier mappedTable, @@ -413,7 +440,7 @@ private static TableMapping CreateTableMapping( { IsSplitEntityTypePrincipal = isSplitEntityTypePrincipal }; - + foreach (var property in mappedType.GetProperties()) { var columnName = property.GetColumnName(mappedTable); @@ -456,8 +483,6 @@ private static TableMapping CreateTableMapping( tableMappings.Add(tableMapping); table.EntityTypeMappings.Add(tableMapping); } - - return tableMapping; } private static void AddViews(RelationalModel databaseModel, IEntityType entityType) @@ -482,7 +507,7 @@ private static void AddViews(RelationalModel databaseModel, IEntityType entityTy if (mappedViewName == null) { - if (isTpc) + if (isTpc || mappingStrategy == RelationalAnnotationNames.TphMappingStrategy) { break; } @@ -630,10 +655,7 @@ private static void AddSqlQueries(RelationalModel databaseModel, IEntityType ent databaseModel.Queries.Add(mappedQuery.Name, sqlQuery); } - var queryMapping = new SqlQueryMapping(entityType, sqlQuery, includesDerivedTypes: true) - { - IsDefaultSqlQueryMapping = true - }; + var queryMapping = new SqlQueryMapping(entityType, sqlQuery, includesDerivedTypes: true) { IsDefaultSqlQueryMapping = true }; foreach (var property in mappedType.GetProperties()) { @@ -793,8 +815,10 @@ private static FunctionMapping CreateFunctionMapping( var column = (FunctionColumn?)storeFunction.FindColumn(columnName); if (column == null) { - column = new FunctionColumn(columnName, property.GetColumnType(mappedFunction), storeFunction); - column.IsNullable = property.IsColumnNullable(mappedFunction); + column = new FunctionColumn(columnName, property.GetColumnType(mappedFunction), storeFunction) + { + IsNullable = property.IsColumnNullable(mappedFunction) + }; storeFunction.Columns.Add(columnName, column); } else if (!property.IsColumnNullable(mappedFunction)) @@ -846,6 +870,284 @@ private static StoreFunction GetOrCreateStoreFunction(IRuntimeDbFunction dbFunct return storeFunction; } + private static void AddStoredProcedures(RelationalModel databaseModel, IEntityType entityType) + { + var mappedType = entityType; + + Check.DebugAssert( + entityType.FindRuntimeAnnotationValue(RelationalAnnotationNames.InsertStoredProcedureMappings) == null, "not null"); + var insertStoredProcedureMappings = new List(); + + Check.DebugAssert( + entityType.FindRuntimeAnnotationValue(RelationalAnnotationNames.DeleteStoredProcedureMappings) == null, "not null"); + var deleteStoredProcedureMappings = new List(); + + Check.DebugAssert( + entityType.FindRuntimeAnnotationValue(RelationalAnnotationNames.UpdateStoredProcedureMappings) == null, "not null"); + var updateStoredProcedureMappings = new List(); + + var mappingStrategy = entityType.GetMappingStrategy(); + var isTpc = mappingStrategy == RelationalAnnotationNames.TpcMappingStrategy; + while (mappedType != null) + { + var insertSproc = (IRuntimeStoredProcedure?)mappedType.GetInsertStoredProcedure(); + if (insertSproc != null + && insertStoredProcedureMappings != null) + { + CreateStoredProcedureMapping( + entityType, + mappedType, + insertSproc, + databaseModel, + insertStoredProcedureMappings, + includesDerivedTypes: !isTpc && mappedType == entityType); + } + else if (entityType == mappedType) + { + insertStoredProcedureMappings = null; + } + + var deleteSproc = (IRuntimeStoredProcedure?)mappedType.GetDeleteStoredProcedure(); + if (deleteSproc != null + && deleteStoredProcedureMappings != null) + { + CreateStoredProcedureMapping( + entityType, + mappedType, + deleteSproc, + databaseModel, + deleteStoredProcedureMappings, + includesDerivedTypes: !isTpc && mappedType == entityType); + } + else if (entityType == mappedType) + { + deleteStoredProcedureMappings = null; + } + + var updateSproc = (IRuntimeStoredProcedure?)mappedType.GetUpdateStoredProcedure(); + if (updateSproc != null + && updateStoredProcedureMappings != null) + { + CreateStoredProcedureMapping( + entityType, + mappedType, + updateSproc, + databaseModel, + updateStoredProcedureMappings, + includesDerivedTypes: !isTpc && mappedType == entityType); + } + else if (entityType == mappedType) + { + updateStoredProcedureMappings = null; + } + + if (isTpc || mappingStrategy == RelationalAnnotationNames.TphMappingStrategy) + { + break; + } + + mappedType = mappedType.BaseType; + } + + if (insertStoredProcedureMappings?.Count > 0) + { + insertStoredProcedureMappings.Reverse(); + entityType.SetRuntimeAnnotation(RelationalAnnotationNames.InsertStoredProcedureMappings, insertStoredProcedureMappings); + } + + if (deleteStoredProcedureMappings?.Count > 0) + { + deleteStoredProcedureMappings.Reverse(); + entityType.SetRuntimeAnnotation(RelationalAnnotationNames.DeleteStoredProcedureMappings, deleteStoredProcedureMappings); + } + + if (updateStoredProcedureMappings?.Count > 0) + { + updateStoredProcedureMappings.Reverse(); + entityType.SetRuntimeAnnotation(RelationalAnnotationNames.UpdateStoredProcedureMappings, updateStoredProcedureMappings); + } + } + + private static void CreateStoredProcedureMapping( + IEntityType entityType, + IEntityType mappedType, + IRuntimeStoredProcedure storedProcedure, + RelationalModel model, + List storedProcedureMappings, + bool includesDerivedTypes) + { + var storeStoredProcedure = GetOrCreateStoreStoredProcedure(storedProcedure, model); + + var identifier = storedProcedure.GetStoreIdentifier(); + var storedProcedureMapping = new StoredProcedureMapping( + entityType, storeStoredProcedure, storedProcedure, includesDerivedTypes); + var (parameterMappingAnnotationName, columnMappingAnnotationName) = identifier.StoreObjectType switch + { + StoreObjectType.InsertStoredProcedure + => (RelationalAnnotationNames.InsertStoredProcedureParameterMappings, + RelationalAnnotationNames.InsertStoredProcedureResultColumnMappings), + StoreObjectType.DeleteStoredProcedure + => (RelationalAnnotationNames.DeleteStoredProcedureParameterMappings, ""), + StoreObjectType.UpdateStoredProcedure + => (RelationalAnnotationNames.UpdateStoredProcedureParameterMappings, + RelationalAnnotationNames.UpdateStoredProcedureResultColumnMappings), + _ => throw new Exception("Unexpected stored procedure type: " + identifier.StoreObjectType) + }; + + foreach (var parameter in storedProcedure.Parameters) + { + var property = mappedType.FindProperty(parameter); + if (property == null) + { + Check.DebugAssert( + entityType.GetMappingStrategy() == RelationalAnnotationNames.TphMappingStrategy, + "Expected TPH for " + entityType.DisplayName()); + + if (entityType.BaseType == null) + { + foreach (var derivedProperty in entityType.GetDerivedProperties()) + { + if (derivedProperty.Name == parameter) + { + GetOrCreateStoreStoredProcedureParameter(derivedProperty, storeStoredProcedure, identifier); + break; + } + } + } + + continue; + } + + var storeParameter = GetOrCreateStoreStoredProcedureParameter(property, storeStoredProcedure, identifier); + + var columnMapping = new StoredProcedureParameterMapping(property, storeParameter, storedProcedureMapping); + storedProcedureMapping.AddParameterMapping(columnMapping); + storeParameter.AddPropertyMapping(columnMapping); + + if (property.FindRuntimeAnnotationValue(parameterMappingAnnotationName) + is not SortedSet columnMappings) + { + columnMappings = new SortedSet(ColumnMappingBaseComparer.Instance); + property.AddRuntimeAnnotation(parameterMappingAnnotationName, columnMappings); + } + + columnMappings.Add(columnMapping); + } + + foreach (var resultColumn in storedProcedure.ResultColumns) + { + var property = mappedType.FindProperty(resultColumn); + if (property == null) + { + Check.DebugAssert( + entityType.GetMappingStrategy() == RelationalAnnotationNames.TphMappingStrategy, + "Expected TPH for " + entityType.DisplayName()); + + if (entityType.BaseType == null) + { + foreach (var derivedProperty in entityType.GetDerivedProperties()) + { + if (derivedProperty.Name == resultColumn) + { + GetOrCreateStoreStoredProcedureResultColumn(derivedProperty, storeStoredProcedure, identifier); + break; + } + } + } + + continue; + } + + var column = GetOrCreateStoreStoredProcedureResultColumn(property, storeStoredProcedure, identifier); + + var columnMapping = new StoredProcedureResultColumnMapping(property, column, storedProcedureMapping); + storedProcedureMapping.AddColumnMapping(columnMapping); + column.AddPropertyMapping(columnMapping); + + if (property.FindRuntimeAnnotationValue(columnMappingAnnotationName) + is not SortedSet columnMappings) + { + columnMappings = new SortedSet(ColumnMappingBaseComparer.Instance); + property.AddRuntimeAnnotation(columnMappingAnnotationName, columnMappings); + } + + columnMappings.Add(columnMapping); + } + + storedProcedureMappings.Add(storedProcedureMapping); + storeStoredProcedure.EntityTypeMappings.Add(storedProcedureMapping); + + static StoreStoredProcedure GetOrCreateStoreStoredProcedure( + IRuntimeStoredProcedure storedProcedure, + RelationalModel model) + { + var storeStoredProcedure = (StoreStoredProcedure?)storedProcedure.StoreStoredProcedure; + if (storeStoredProcedure == null) + { + storeStoredProcedure = (StoreStoredProcedure?)model.FindStoredProcedure(storedProcedure.Name, storedProcedure.Schema); + if (storeStoredProcedure == null) + { + storeStoredProcedure = new StoreStoredProcedure(storedProcedure, model); + model.StoredProcedures.Add((storeStoredProcedure.Name, storeStoredProcedure.Schema), storeStoredProcedure); + } + else + { + storedProcedure.StoreStoredProcedure = storeStoredProcedure; + storeStoredProcedure.StoredProcedures.Add(storedProcedure); + } + } + + return storeStoredProcedure; + } + + static StoreStoredProcedureParameter GetOrCreateStoreStoredProcedureParameter( + IProperty property, + StoreStoredProcedure storeStoredProcedure, + StoreObjectIdentifier identifier) + { + var columnName = property.GetColumnName(identifier)!; + var storeParameter = (StoreStoredProcedureParameter?)storeStoredProcedure.FindParameter(columnName); + if (storeParameter == null) + { + storeParameter = new StoreStoredProcedureParameter( + columnName, + property.GetColumnType(identifier), + storeStoredProcedure, + property.GetDirection(identifier)) { IsNullable = property.IsColumnNullable(identifier) }; + storeStoredProcedure.AddParameter(storeParameter); + } + else if (!property.IsColumnNullable(identifier)) + { + storeParameter.IsNullable = false; + } + + return storeParameter; + } + + static StoreStoredProcedureResultColumn GetOrCreateStoreStoredProcedureResultColumn( + IProperty property, + StoreStoredProcedure storeStoredProcedure, + StoreObjectIdentifier identifier) + { + var columnName = property.GetColumnName(identifier)!; + var column = (StoreStoredProcedureResultColumn?)storeStoredProcedure.FindResultColumn(columnName); + if (column == null) + { + column = new StoreStoredProcedureResultColumn(columnName, property.GetColumnType(identifier), storeStoredProcedure) + { + IsNullable = property.IsColumnNullable(identifier) + }; + storeStoredProcedure.AddResultColumn(column); + } + else if (!property.IsColumnNullable(identifier)) + { + column.IsNullable = false; + } + + return column; + } + } + private static void PopulateTableConfiguration(Table table, bool designTime) { var storeObject = StoreObjectIdentifier.Table(table.Name, table.Schema); @@ -1003,7 +1305,7 @@ private static void PopulateRowInternalForeignKeys(TableBase tab { entityTypeMapping.IsSharedTablePrincipal = false; } - + var entityType = entityTypeMapping.EntityType; mappedEntityTypes.Add(entityType); var primaryKey = entityType.FindPrimaryKey(); @@ -1295,6 +1597,12 @@ IEnumerable IRelationalModel.Functions get => Functions.Values; } + IEnumerable IRelationalModel.StoredProcedures + { + [DebuggerStepThrough] + get => StoredProcedures.Values; + } + IEnumerable IRelationalModel.Queries { [DebuggerStepThrough] diff --git a/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedure.cs b/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedure.cs new file mode 100644 index 00000000000..74132283939 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedure.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. + +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 StoreStoredProcedure : TableBase, IStoreStoredProcedure +{ + private readonly SortedDictionary _parametersSet; + + /// + /// 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 StoreStoredProcedure(IRuntimeStoredProcedure sproc, RelationalModel model) + : base(sproc.Name, sproc.Schema, model) + { + StoredProcedures = new(StoredProcedureComparer.Instance) { { sproc } }; + + sproc.StoreStoredProcedure = this; + + _parametersSet = new(StringComparer.Ordinal); + } + + /// + /// 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 SortedSet StoredProcedures { 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. + /// + public virtual List Parameters { get; protected set; } + = new(); + + /// + /// 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 AddParameter(IStoreStoredProcedureParameter parameter) + { + _parametersSet[parameter.Name] = parameter; + Parameters.Add(parameter); + } + + /// + /// 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 IStoreStoredProcedureParameter? FindParameter(string name) + => _parametersSet.TryGetValue(name, out var parameter) + ? parameter + : 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 IStoreStoredProcedureParameter? FindParameter(IProperty property) + => property.GetInsertStoredProcedureParameterMappings() + .Concat(property.GetDeleteStoredProcedureParameterMappings()) + .Concat(property.GetUpdateStoredProcedureParameterMappings()) + .FirstOrDefault(cm => cm.StoredProcedureMapping.StoreStoredProcedure == this) + ?.Parameter; + + /// + /// 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 List ResultColumns { get; protected set; } + = new(); + + /// + /// 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 AddResultColumn(IStoreStoredProcedureResultColumn column) + { + Columns[column.Name] = column; + ResultColumns.Add(column); + } + + /// + public override IColumnBase? FindColumn(IProperty property) + => property.GetInsertStoredProcedureResultColumnMappings() + .Concat(property.GetUpdateStoredProcedureResultColumnMappings()) + .FirstOrDefault(cm => cm.StoredProcedureMapping.StoreStoredProcedure == this) + ?.Column; + + /// + [DebuggerStepThrough] + public virtual IStoreStoredProcedureResultColumn? FindResultColumn(string name) + => (IStoreStoredProcedureResultColumn?)base.FindColumn(name); + + /// + [DebuggerStepThrough] + public virtual IStoreStoredProcedureResultColumn? FindResultColumn(IProperty property) + => (IStoreStoredProcedureResultColumn?)FindColumn(property); + + /// + /// 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() + => ((IStoreStoredProcedure)this).ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + + /// + IEnumerable IStoreStoredProcedure.StoredProcedures + { + [DebuggerStepThrough] + get => StoredProcedures; + } + + /// + IEnumerable IStoreStoredProcedure.EntityTypeMappings + { + [DebuggerStepThrough] + get => EntityTypeMappings.Cast(); + } + + /// + IEnumerable IStoreStoredProcedure.Parameters + { + [DebuggerStepThrough] + get => Parameters; + } + + /// + IEnumerable IStoreStoredProcedure.ResultColumns + { + [DebuggerStepThrough] + get => ResultColumns; + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedureParameter.cs b/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedureParameter.cs new file mode 100644 index 00000000000..ddfa4297778 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedureParameter.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Data; + +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 StoreStoredProcedureParameter + : ColumnBase, IStoreStoredProcedureParameter +{ + /// + /// 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 StoreStoredProcedureParameter( + string name, + string type, + StoreStoredProcedure storedProcedure, + ParameterDirection direction) + : base(name, type, storedProcedure) + { + Direction = direction; + } + + /// + /// 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 StoreStoredProcedure StoredProcedure + => (StoreStoredProcedure)Table; + + /// + public virtual ParameterDirection Direction { 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. + /// + public override string ToString() + => ((IStoreStoredProcedureParameter)this).ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + + /// + IStoreStoredProcedure IStoreStoredProcedureParameter.StoredProcedure + { + [DebuggerStepThrough] + get => StoredProcedure; + } + + /// + IReadOnlyList IStoreStoredProcedureParameter.PropertyMappings + { + [DebuggerStepThrough] + get => PropertyMappings; + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedureResultColumn.cs b/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedureResultColumn.cs new file mode 100644 index 00000000000..f1f16053304 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedureResultColumn.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +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 StoreStoredProcedureResultColumn + : ColumnBase, IStoreStoredProcedureResultColumn +{ + /// + /// 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 StoreStoredProcedureResultColumn(string name, string type, StoreStoredProcedure storedProcedure) + : base(name, type, storedProcedure) + { + } + + /// + /// 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 StoreStoredProcedure StoredProcedure + => (StoreStoredProcedure)Table; + + /// + /// 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() + => ((IStoreStoredProcedureResultColumn)this).ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + + /// + IStoreStoredProcedure IStoreStoredProcedureResultColumn.StoredProcedure + { + [DebuggerStepThrough] + get => StoredProcedure; + } + + /// + IReadOnlyList IStoreStoredProcedureResultColumn.PropertyMappings + { + [DebuggerStepThrough] + get => PropertyMappings; + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/StoredProcedure.cs b/src/EFCore.Relational/Metadata/Internal/StoredProcedure.cs index 07c0ccb556f..6841e9ed6b4 100644 --- a/src/EFCore.Relational/Metadata/Internal/StoredProcedure.cs +++ b/src/EFCore.Relational/Metadata/Internal/StoredProcedure.cs @@ -10,7 +10,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal; /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public class StoredProcedure : - ConventionAnnotatable, IStoredProcedure, IMutableStoredProcedure, IConventionStoredProcedure + ConventionAnnotatable, IRuntimeStoredProcedure, IMutableStoredProcedure, IConventionStoredProcedure { private readonly List _parameters = new(); private readonly HashSet _parametersSet = new(); @@ -20,6 +20,7 @@ public class StoredProcedure : private string? _name; private InternalStoredProcedureBuilder? _builder; private bool _areTransactionsSuppressed; + private IStoreStoredProcedure? _storeStoredProcedure; private ConfigurationSource _configurationSource; private ConfigurationSource? _schemaConfigurationSource; @@ -86,11 +87,11 @@ public override bool IsReadOnly /// 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( + public static IStoredProcedure? FindStoredProcedure( IReadOnlyEntityType entityType, StoreObjectType sprocType) { - var storedProcedure = GetDeclaredStoredProcedure(entityType, sprocType); + var storedProcedure = FindDeclaredStoredProcedure(entityType, sprocType); if (storedProcedure != null) { return storedProcedure; @@ -112,12 +113,12 @@ public override bool IsReadOnly /// 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( + public static IStoredProcedure? FindDeclaredStoredProcedure( IReadOnlyEntityType entityType, StoreObjectType sprocType) { var sprocAnnotation = entityType.FindAnnotation(GetAnnotationName(sprocType)); - return sprocAnnotation != null ? (StoredProcedure?)sprocAnnotation.Value : null; + return sprocAnnotation != null ? (IStoredProcedure?)sprocAnnotation.Value : null; } /// @@ -130,13 +131,13 @@ public static StoredProcedure SetStoredProcedure( IMutableEntityType entityType, StoreObjectType sprocType) { - var oldId = GetDeclaredStoredProcedure(entityType, sprocType)?.CreateIdentifier(); + var oldId = FindDeclaredStoredProcedure(entityType, sprocType)?.GetStoreIdentifier(); var sproc = new StoredProcedure(entityType, ConfigurationSource.Explicit); entityType.SetAnnotation(GetAnnotationName(sprocType), sproc); if (oldId != null) { - UpdateOverrides(oldId.Value, sproc.CreateIdentifier(), (IConventionEntityType)entityType); + UpdateOverrides(oldId.Value, ((IReadOnlyStoredProcedure)sproc).GetStoreIdentifier(), (IConventionEntityType)entityType); } return sproc; @@ -154,14 +155,14 @@ public static StoredProcedure SetStoredProcedure( string name, string? schema) { - var oldId = GetDeclaredStoredProcedure(entityType, sprocType)?.CreateIdentifier(); + var oldId = FindDeclaredStoredProcedure(entityType, sprocType)?.GetStoreIdentifier(); 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); + UpdateOverrides(oldId.Value, ((IReadOnlyStoredProcedure)sproc).GetStoreIdentifier(), (IConventionEntityType)entityType); } return sproc; @@ -178,7 +179,7 @@ public static StoredProcedure SetStoredProcedure( StoreObjectType sprocType, bool fromDataAnnotation) { - var oldId = GetDeclaredStoredProcedure(entityType, sprocType)?.CreateIdentifier(); + var oldId = FindDeclaredStoredProcedure(entityType, sprocType)?.GetStoreIdentifier(); var sproc = new StoredProcedure( (IMutableEntityType)entityType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); @@ -187,7 +188,7 @@ public static StoredProcedure SetStoredProcedure( if (oldId != null && sproc != null) { - UpdateOverrides(oldId.Value, sproc.CreateIdentifier(), entityType); + UpdateOverrides(oldId.Value, ((IReadOnlyStoredProcedure)sproc).GetStoreIdentifier(), entityType); } return sproc; @@ -206,7 +207,7 @@ public static StoredProcedure SetStoredProcedure( string? schema, bool fromDataAnnotation) { - var oldId = GetDeclaredStoredProcedure(entityType, sprocType)?.CreateIdentifier(); + var oldId = FindDeclaredStoredProcedure(entityType, sprocType)?.GetStoreIdentifier(); var configurationSource = fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention; var sproc = new StoredProcedure((IMutableEntityType)entityType, configurationSource); sproc = (StoredProcedure?)entityType.SetAnnotation(GetAnnotationName(sprocType), sproc)?.Value; @@ -216,7 +217,7 @@ public static StoredProcedure SetStoredProcedure( if (oldId != null && sproc != null) { - UpdateOverrides(oldId.Value, sproc.CreateIdentifier(), entityType); + UpdateOverrides(oldId.Value, ((IReadOnlyStoredProcedure)sproc).GetStoreIdentifier(), entityType); } return sproc; @@ -230,7 +231,7 @@ public static StoredProcedure SetStoredProcedure( /// public static IMutableStoredProcedure? RemoveStoredProcedure(IMutableEntityType entityType, StoreObjectType sprocType) { - var oldId = GetDeclaredStoredProcedure(entityType, sprocType)?.CreateIdentifier(); + var oldId = FindDeclaredStoredProcedure(entityType, sprocType)?.GetStoreIdentifier(); var sproc = (IMutableStoredProcedure?)entityType.RemoveAnnotation(GetAnnotationName(sprocType))?.Value; if (oldId != null @@ -250,7 +251,7 @@ public static StoredProcedure SetStoredProcedure( /// public static IConventionStoredProcedure? RemoveStoredProcedure(IConventionEntityType entityType, StoreObjectType sprocType) { - var oldId = GetDeclaredStoredProcedure(entityType, sprocType)?.CreateIdentifier(); + var oldId = FindDeclaredStoredProcedure(entityType, sprocType)?.GetStoreIdentifier(); var sproc = (IConventionStoredProcedure?)entityType.RemoveAnnotation(GetAnnotationName(sprocType))?.Value; if (oldId != null @@ -282,38 +283,6 @@ private static string GetAnnotationName(StoreObjectType sprocType) _ => 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() @@ -346,7 +315,7 @@ public virtual string? Schema { EnsureMutable(); - var oldId = CreateIdentifier(); + var oldId = ((IReadOnlyStoredProcedure)this).GetStoreIdentifier(); _schema = schema; @@ -354,7 +323,7 @@ public virtual string? Schema if (oldId != null) { - UpdateOverrides(oldId.Value, CreateIdentifier(), (IConventionEntityType)EntityType); + UpdateOverrides(oldId.Value, ((IReadOnlyStoredProcedure)this).GetStoreIdentifier(), (IConventionEntityType)EntityType); } return schema; @@ -378,6 +347,12 @@ public virtual string? Name private string? GetDefaultName() { + var tableName = EntityType.GetTableName() ?? EntityType.GetDefaultTableName(); + if (tableName == null) + { + return null; + } + string? suffix; if (EntityType.GetInsertStoredProcedure() == this) { @@ -396,12 +371,6 @@ public virtual string? Name return null; } - var tableName = EntityType.GetDefaultTableName(); - if (tableName == null) - { - return null; - } - return tableName + suffix; } @@ -415,7 +384,7 @@ public virtual string? Name { EnsureMutable(); - var oldId = CreateIdentifier(); + var oldId = ((IReadOnlyStoredProcedure)this).GetStoreIdentifier(); _name = name; @@ -425,7 +394,7 @@ public virtual string? Name if (oldId != null) { - UpdateOverrides(oldId.Value, CreateIdentifier(), (IConventionEntityType)EntityType); + UpdateOverrides(oldId.Value, ((IReadOnlyStoredProcedure)this).GetStoreIdentifier(), (IConventionEntityType)EntityType); } return name; @@ -441,7 +410,7 @@ public virtual void SetName(string? name, string? schema, ConfigurationSource co { EnsureMutable(); - var oldId = CreateIdentifier(); + var oldId = ((IReadOnlyStoredProcedure)this).GetStoreIdentifier(); _name = name; @@ -456,7 +425,7 @@ public virtual void SetName(string? name, string? schema, ConfigurationSource co if (!skipOverrides && oldId != null) { - UpdateOverrides(oldId.Value, CreateIdentifier(), (IConventionEntityType)EntityType); + UpdateOverrides(oldId.Value, ((IReadOnlyStoredProcedure)this).GetStoreIdentifier(), (IConventionEntityType)EntityType); } } @@ -568,15 +537,6 @@ public virtual bool AddResultColumn(string propertyName) 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 @@ -633,9 +593,16 @@ IEntityType IStoredProcedure.EntityType get => (IEntityType)EntityType; } - ///// - //IStoreFunction IDbFunction.StoreFunction - // => StoreFunction!; // Relational model creation ensures StoreFunction is populated + /// + IStoreStoredProcedure IStoredProcedure.StoreStoredProcedure + => _storeStoredProcedure!; // Relational model creation ensures StoreStoredProcedure is populated + + /// + IStoreStoredProcedure IRuntimeStoredProcedure.StoreStoredProcedure + { + get => _storeStoredProcedure!; + set => _storeStoredProcedure = value; + } /// [DebuggerStepThrough] diff --git a/src/EFCore.Relational/Metadata/Internal/StoredProcedureComparer.cs b/src/EFCore.Relational/Metadata/Internal/StoredProcedureComparer.cs new file mode 100644 index 00000000000..edb4e5d6be6 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/StoredProcedureComparer.cs @@ -0,0 +1,118 @@ +// 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. +/// +// Sealed for perf +public sealed class StoredProcedureComparer : IEqualityComparer, IComparer +{ + private StoredProcedureComparer() + { + } + + /// + /// 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 readonly StoredProcedureComparer Instance = new(); + + /// + /// 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 int Compare(IStoredProcedure? x, IStoredProcedure? y) + { + if (ReferenceEquals(x, y)) + { + return 0; + } + + if (x is null) + { + return -1; + } + + if (y is null) + { + return 1; + } + + var xId = x.GetStoreIdentifier(); + var yId = y.GetStoreIdentifier(); + + var result = 0; + result = xId.StoreObjectType.CompareTo(yId.StoreObjectType); + if (result != 0) + { + return result; + } + + result = EntityTypeFullNameComparer.Instance.Compare(x.EntityType, y.EntityType); + if (result != 0) + { + return result; + } + + result = StringComparer.Ordinal.Compare(xId.Name, yId.Name); + if (result != 0) + { + return result; + } + + result = StringComparer.Ordinal.Compare(xId.Schema, yId.Schema); + if (result != 0) + { + return result; + } + + result = x.Parameters.Count().CompareTo(y.Parameters.Count()); + if (result != 0) + { + return result; + } + + result = x.Parameters.Zip(y.Parameters, (xc, yc) => StringComparer.Ordinal.Compare(xc, yc)) + .FirstOrDefault(r => r != 0); + if (result != 0) + { + return result; + } + + return x.ResultColumns.Zip(y.ResultColumns, (xc, yc) => StringComparer.Ordinal.Compare(xc, yc)) + .FirstOrDefault(r => r != 0); + } + + /// + /// 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 bool Equals(IStoredProcedure? x, IStoredProcedure? y) + => ReferenceEquals(x, y) + || (x is not null + && y is not null + && x.EntityType == y.EntityType + && x.GetStoreIdentifier() == y.GetStoreIdentifier() + && x.Parameters.SequenceEqual(y.Parameters) + && x.ResultColumns.SequenceEqual(y.ResultColumns)); + + /// + /// 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 int GetHashCode(IStoredProcedure obj) + => obj.GetStoreIdentifier().GetHashCode(); +} diff --git a/src/EFCore.Relational/Metadata/Internal/StoredProcedureMapping.cs b/src/EFCore.Relational/Metadata/Internal/StoredProcedureMapping.cs new file mode 100644 index 00000000000..f6f38ca14f7 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/StoredProcedureMapping.cs @@ -0,0 +1,91 @@ +// 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 StoredProcedureMapping : TableMappingBase, IStoredProcedureMapping +{ + /// + /// 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 StoredProcedureMapping( + IEntityType entityType, + StoreStoredProcedure storeStoredProcedure, + IStoredProcedure storedProcedure, + bool includesDerivedTypes) + : base(entityType, storeStoredProcedure, includesDerivedTypes) + { + StoredProcedure = storedProcedure; + StoredProcedureIdentifier = storedProcedure.GetStoreIdentifier(); + } + + /// + public virtual IStoreStoredProcedure StoreStoredProcedure + => (StoreStoredProcedure)base.Table; + + /// + public virtual IStoredProcedure StoredProcedure { get; } + + /// + public virtual StoreObjectIdentifier StoredProcedureIdentifier { 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. + /// + public virtual List ParameterMappings { get; } + = new(); + + /// + /// 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 AddParameterMapping(IStoredProcedureParameterMapping parameterMapping) + { + if (ParameterMappings.IndexOf(parameterMapping, ColumnMappingBaseComparer.Instance) != -1) + { + return false; + } + + ParameterMappings.Add(parameterMapping); + ParameterMappings.Sort(ColumnMappingBaseComparer.Instance); + + return true; + } + + /// + /// 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() + => ((IStoredProcedureMapping)this).ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + + /// + IEnumerable IStoredProcedureMapping.ResultColumnMappings + { + [DebuggerStepThrough] + get => ColumnMappings; + } + + /// + IEnumerable IStoredProcedureMapping.ParameterMappings + { + [DebuggerStepThrough] + get => ParameterMappings; + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/StoredProcedureParameterMapping.cs b/src/EFCore.Relational/Metadata/Internal/StoredProcedureParameterMapping.cs new file mode 100644 index 00000000000..6c69bc8382c --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/StoredProcedureParameterMapping.cs @@ -0,0 +1,56 @@ +// 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 StoredProcedureParameterMapping : ColumnMappingBase, IStoredProcedureParameterMapping +{ + /// + /// 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 StoredProcedureParameterMapping( + IProperty property, + StoreStoredProcedureParameter parameter, + StoredProcedureMapping storedProcedureMapping) + : base(property, parameter, storedProcedureMapping) + { + } + + /// + public virtual IStoredProcedureMapping StoredProcedureMapping + => (IStoredProcedureMapping)TableMapping; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override RelationalTypeMapping GetTypeMapping() + => Property.FindRelationalTypeMapping(StoredProcedureMapping.StoredProcedureIdentifier)!; + + /// + /// 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() + => ((IStoredProcedureParameterMapping)this).ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + + /// + IStoreStoredProcedureParameter IStoredProcedureParameterMapping.Parameter + { + [DebuggerStepThrough] + get => (IStoreStoredProcedureParameter)Column; + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/StoredProcedureResultColumnMapping.cs b/src/EFCore.Relational/Metadata/Internal/StoredProcedureResultColumnMapping.cs new file mode 100644 index 00000000000..c64a612185a --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/StoredProcedureResultColumnMapping.cs @@ -0,0 +1,56 @@ +// 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 StoredProcedureResultColumnMapping : ColumnMappingBase, IStoredProcedureResultColumnMapping +{ + /// + /// 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 StoredProcedureResultColumnMapping( + IProperty property, + StoreStoredProcedureResultColumn column, + StoredProcedureMapping storedProcedureMapping) + : base(property, column, storedProcedureMapping) + { + } + + /// + public virtual IStoredProcedureMapping StoredProcedureMapping + => (IStoredProcedureMapping)TableMapping; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override RelationalTypeMapping GetTypeMapping() + => Property.FindRelationalTypeMapping(StoredProcedureMapping.StoredProcedureIdentifier)!; + + /// + /// 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() + => ((IStoredProcedureResultColumnMapping)this).ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + + /// + IStoreStoredProcedureResultColumn IStoredProcedureResultColumnMapping.Column + { + [DebuggerStepThrough] + get => (IStoreStoredProcedureResultColumn)Column; + } +} diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs index c060f520d0c..e2e520f71e3 100644 --- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs +++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs @@ -92,6 +92,11 @@ public static class RelationalAnnotationNames /// public const string UpdateStoredProcedure = Prefix + "UpdateStoredProcedure"; + /// + /// The name for mapped function name annotations. + /// + public const string ParameterDirection = Prefix + "ParameterDirection"; + /// /// The name for mapped sql query annotations. /// @@ -234,6 +239,46 @@ public static class RelationalAnnotationNames /// public const string FunctionColumnMappings = Prefix + "FunctionColumnMappings"; + /// + /// The name for insert stored procedure mappings annotations. + /// + public const string InsertStoredProcedureMappings = Prefix + "InsertStoredProcedureMappings"; + + /// + /// The name for insert stored procedure result column mappings annotations. + /// + public const string InsertStoredProcedureResultColumnMappings = Prefix + "InsertStoredProcedureResultColumnMappings"; + + /// + /// The name for insert stored procedure parameter mappings annotations. + /// + public const string InsertStoredProcedureParameterMappings = Prefix + "InsertStoredProcedureParameterMappings"; + + /// + /// The name for delete stored procedure mappings annotations. + /// + public const string DeleteStoredProcedureMappings = Prefix + "DeleteStoredProcedureMappings"; + + /// + /// The name for delete stored procedure parameter mappings annotations. + /// + public const string DeleteStoredProcedureParameterMappings = Prefix + "DeleteStoredProcedureParameterMappings"; + + /// + /// The name for update stored procedure mappings annotations. + /// + public const string UpdateStoredProcedureMappings = Prefix + "UpdateStoredProcedureMappings"; + + /// + /// The name for update stored procedure result column mappings annotations. + /// + public const string UpdateStoredProcedureResultColumnMappings = Prefix + "UpdateStoredProcedureResultColumnMappings"; + + /// + /// The name for update stored procedure parameter mappings annotations. + /// + public const string UpdateStoredProcedureParameterMappings = Prefix + "UpdateStoredProcedureParameterMappings"; + /// /// The name for sql query mappings annotations. /// diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationProvider.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationProvider.cs index 09c879111ad..fc7e95eab3e 100644 --- a/src/EFCore.Relational/Metadata/RelationalAnnotationProvider.cs +++ b/src/EFCore.Relational/Metadata/RelationalAnnotationProvider.cs @@ -69,6 +69,18 @@ public virtual IEnumerable For(IStoreFunction function, bool design /// public virtual IEnumerable For(IFunctionColumn column, bool designTime) => Enumerable.Empty(); + + /// + public virtual IEnumerable For(IStoreStoredProcedure storedProcedure, bool designTime) + => Enumerable.Empty(); + + /// + public virtual IEnumerable For(IStoreStoredProcedureParameter parameter, bool designTime) + => Enumerable.Empty(); + + /// + public virtual IEnumerable For(IStoreStoredProcedureResultColumn column, bool designTime) + => Enumerable.Empty(); /// public virtual IEnumerable For(IForeignKeyConstraint foreignKey, bool designTime) diff --git a/src/EFCore.Relational/Metadata/RuntimeStoredProcedure.cs b/src/EFCore.Relational/Metadata/RuntimeStoredProcedure.cs new file mode 100644 index 00000000000..fcb5e845c37 --- /dev/null +++ b/src/EFCore.Relational/Metadata/RuntimeStoredProcedure.cs @@ -0,0 +1,164 @@ +// 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; + +/// +/// 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 RuntimeStoredProcedure : AnnotatableBase, IRuntimeStoredProcedure +{ + private readonly List _parameters = new(); + private readonly List _resultColumns = new(); + private readonly string? _schema; + private readonly string _name; + private readonly bool _areTransactionsSuppressed; + private IStoreStoredProcedure? _storeStoredProcedure; + + /// + /// Initializes a new instance of the class. + /// + /// The mapped entity type. + /// The name. + /// The schema. + /// Whether the automatic transactions are surpressed. + public RuntimeStoredProcedure( + RuntimeEntityType entityType, + string name, + string? schema, + bool areTransactionsSuppressed) + { + EntityType = entityType; + _name = name; + _schema = schema; + _areTransactionsSuppressed = areTransactionsSuppressed; + } + + /// + /// Gets the entity type in which this stored procedure is defined. + /// + public virtual RuntimeEntityType EntityType { get; set; } + + /// + /// Adds a new parameter mapped to the property with the given name. + /// + /// The name of the corresponding property. + public virtual void AddParameter(string propertyName) + { + _parameters.Add(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. + public virtual void AddResultColumn(string propertyName) + { + _resultColumns.Add(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 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)); + + /// + IReadOnlyEntityType IReadOnlyStoredProcedure.EntityType + { + [DebuggerStepThrough] + get => EntityType; + } + + /// + IEntityType IStoredProcedure.EntityType + { + [DebuggerStepThrough] + get => EntityType; + } + + /// + string? IReadOnlyStoredProcedure.Name + { + [DebuggerStepThrough] + get => _name; + } + + /// + string IStoredProcedure.Name + { + [DebuggerStepThrough] + get => _name; + } + + /// + string? IReadOnlyStoredProcedure.Schema + { + [DebuggerStepThrough] + get => _schema; + } + + /// + bool IReadOnlyStoredProcedure.AreTransactionsSuppressed + { + [DebuggerStepThrough] + get => _areTransactionsSuppressed; + } + + /// + IReadOnlyList IReadOnlyStoredProcedure.Parameters + { + [DebuggerStepThrough] + get => _parameters; + } + + /// + bool IReadOnlyStoredProcedure.ContainsParameter(string propertyName) + => _parameters.Contains(propertyName); + + /// + IReadOnlyList IReadOnlyStoredProcedure.ResultColumns + { + [DebuggerStepThrough] + get => _resultColumns; + } + + /// + bool IReadOnlyStoredProcedure.ContainsResultColumn(string propertyName) + => _resultColumns.Contains(propertyName); + + + /// + IStoreStoredProcedure IStoredProcedure.StoreStoredProcedure + { + [DebuggerStepThrough] + get => _storeStoredProcedure!; + } + + /// + IStoreStoredProcedure IRuntimeStoredProcedure.StoreStoredProcedure + { + get => _storeStoredProcedure!; + set => _storeStoredProcedure = value; + } +} diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index eed94e6bbd5..6b5756384b1 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1230,13 +1230,21 @@ public static string SqlQueryOverrideMismatch(object? propertySpecification, obj 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. + /// The property '{entityType}.{property}' is mapped to a parameter of the stored procedure '{sproc}', but only concurrency token and 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 entity type '{entityType}' is mapped to the stored procedure '{sproc}', however the store-generated properties {properties} are not mapped to any output parameter or result column. + /// + public static string StoredProcedureGeneratedPropertiesNotMapped(object? entityType, object? sproc, object? properties) + => string.Format( + GetString("StoredProcedureGeneratedPropertiesNotMapped", nameof(entityType), nameof(sproc), nameof(properties)), + entityType, sproc, properties); + /// /// 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. /// @@ -1253,6 +1261,14 @@ public static string StoredProcedureNoName(object? entityType, object? sproc) GetString("StoredProcedureNoName", nameof(entityType), nameof(sproc)), entityType, sproc); + /// + /// The property '{entityType}.{property}' is mapped to an output parameter of the stored procedure '{sproc}', but it is not configured as store-generated. Either configure it as store-generated or don't configure the parameter as output. + /// + public static string StoredProcedureOutputParameterNotGenerated(object? entityType, object? property, object? sproc) + => string.Format( + GetString("StoredProcedureOutputParameterNotGenerated", nameof(entityType), nameof(property), nameof(sproc)), + entityType, property, 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}'. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 5a38d8f7d7a..e804bc3e0f0 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -862,7 +862,10 @@ 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 property '{entityType}.{property}' is mapped to a parameter of the stored procedure '{sproc}', but only concurrency token and key properties are supported for Delete stored procedures. + + + The entity type '{entityType}' is mapped to the stored procedure '{sproc}', however the store-generated properties {properties} are not mapped to any output parameter or result column. 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. @@ -870,6 +873,9 @@ The entity type '{entityType}' was configured to use '{sproc}', but the store name was not specified. Configure the stored procedure name explicitly. + + The property '{entityType}.{property}' is mapped to an output parameter of the stored procedure '{sproc}', but it is not configured as store-generated. Either configure it as store-generated or don't configure the parameter as output. + 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}'. diff --git a/src/EFCore.SqlServer/Extensions/SqlServerPropertyExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerPropertyExtensions.cs index ec60455e0e6..d731eb391d6 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerPropertyExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerPropertyExtensions.cs @@ -846,6 +846,11 @@ private static bool IsCompatibleWithValueGeneration( in StoreObjectIdentifier storeObject, ITypeMappingSource? typeMappingSource) { + if (storeObject.StoreObjectType != StoreObjectType.Table) + { + return false; + } + var valueConverter = property.GetValueConverter() ?? (property.FindRelationalTypeMapping(storeObject) ?? typeMappingSource?.FindMapping((IProperty)property))?.Converter; diff --git a/src/EFCore/Metadata/Conventions/DiscriminatorConvention.cs b/src/EFCore/Metadata/Conventions/DiscriminatorConvention.cs index 93daf7dd161..ec66c77eb03 100644 --- a/src/EFCore/Metadata/Conventions/DiscriminatorConvention.cs +++ b/src/EFCore/Metadata/Conventions/DiscriminatorConvention.cs @@ -72,13 +72,13 @@ public virtual void ProcessEntityTypeBaseTypeChanged( if (newBaseType.BaseType == null) { - discriminator?.HasValue(newBaseType, newBaseType.ShortName()); + discriminator?.HasValue(newBaseType, newBaseType.GetDefaultDiscriminatorValue()); } } if (discriminator != null) { - discriminator.HasValue(entityTypeBuilder.Metadata, entityTypeBuilder.Metadata.ShortName()); + discriminator.HasValue(entityTypeBuilder.Metadata, entityTypeBuilder.Metadata.GetDefaultDiscriminatorValue()); SetDefaultDiscriminatorValues(derivedEntityTypes, discriminator); } } @@ -115,7 +115,7 @@ protected virtual void SetDefaultDiscriminatorValues( { foreach (var entityType in entityTypes) { - discriminatorBuilder.HasValue(entityType, entityType.ShortName()); + discriminatorBuilder.HasValue(entityType, entityType.GetDefaultDiscriminatorValue()); } } } diff --git a/src/EFCore/Metadata/Conventions/ManyToManyJoinEntityTypeConvention.cs b/src/EFCore/Metadata/Conventions/ManyToManyJoinEntityTypeConvention.cs index 58bdf89a38e..98f904eea22 100644 --- a/src/EFCore/Metadata/Conventions/ManyToManyJoinEntityTypeConvention.cs +++ b/src/EFCore/Metadata/Conventions/ManyToManyJoinEntityTypeConvention.cs @@ -119,8 +119,10 @@ protected virtual string GenerateJoinTypeName(IConventionSkipNavigation skipNavi var declaringEntityType = skipNavigation.DeclaringEntityType; var inverseEntityType = inverseSkipNavigation.DeclaringEntityType; var model = declaringEntityType.Model; - var joinEntityTypeName = declaringEntityType.ShortName(); - var inverseName = inverseEntityType.ShortName(); + var joinEntityTypeName = !declaringEntityType.HasSharedClrType + ? declaringEntityType.ClrType.ShortDisplayName() : declaringEntityType.ShortName(); + var inverseName = !inverseEntityType.HasSharedClrType + ? inverseEntityType.ClrType.ShortDisplayName() : inverseEntityType.ShortName(); joinEntityTypeName = StringComparer.Ordinal.Compare(joinEntityTypeName, inverseName) < 0 ? joinEntityTypeName + inverseName : inverseName + joinEntityTypeName; diff --git a/src/EFCore/Metadata/IReadOnlyEntityType.cs b/src/EFCore/Metadata/IReadOnlyEntityType.cs index cd07f87b88b..43c89b6e3f2 100644 --- a/src/EFCore/Metadata/IReadOnlyEntityType.cs +++ b/src/EFCore/Metadata/IReadOnlyEntityType.cs @@ -77,8 +77,15 @@ bool GetIsDiscriminatorMappingComplete() : !ClrType.IsInstantiable() || (BaseType == null && GetDirectlyDerivedTypes().Count() == 0) ? null - : (object)ShortName(); + : (object?)GetDefaultDiscriminatorValue(); } + + /// + /// Returns the default discriminator value that would be used for this entity type. + /// + /// The default discriminator value for this entity type. + string GetDefaultDiscriminatorValue() + => !HasSharedClrType ? ClrType.ShortDisplayName() : ShortName(); /// /// Gets all types in the model from which a given entity type derives, starting with the root. diff --git a/src/EFCore/Metadata/IReadOnlyTypeBase.cs b/src/EFCore/Metadata/IReadOnlyTypeBase.cs index ec411b10016..f60e522cfad 100644 --- a/src/EFCore/Metadata/IReadOnlyTypeBase.cs +++ b/src/EFCore/Metadata/IReadOnlyTypeBase.cs @@ -113,7 +113,14 @@ string ShortName() { if (!HasSharedClrType) { - return ClrType.ShortDisplayName(); + var name = ClrType.ShortDisplayName(); + var lessIndex = name.IndexOf("<", StringComparison.Ordinal); + if (lessIndex == -1) + { + return name; + } + + return name[..lessIndex]; } var hashIndex = Name.LastIndexOf("#", StringComparison.Ordinal); diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs index af84e2eb2f5..f65ae0b71ed 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs @@ -49,10 +49,18 @@ public void Test_new_annotations_handled_for_entity_types() RelationalAnnotationNames.ViewColumnMappings, RelationalAnnotationNames.SqlQueryColumnMappings, RelationalAnnotationNames.FunctionColumnMappings, + RelationalAnnotationNames.InsertStoredProcedureParameterMappings, + RelationalAnnotationNames.InsertStoredProcedureResultColumnMappings, + RelationalAnnotationNames.DeleteStoredProcedureParameterMappings, + RelationalAnnotationNames.UpdateStoredProcedureParameterMappings, + RelationalAnnotationNames.UpdateStoredProcedureResultColumnMappings, RelationalAnnotationNames.DefaultColumnMappings, RelationalAnnotationNames.TableMappings, RelationalAnnotationNames.ViewMappings, RelationalAnnotationNames.FunctionMappings, + RelationalAnnotationNames.InsertStoredProcedureMappings, + RelationalAnnotationNames.DeleteStoredProcedureMappings, + RelationalAnnotationNames.UpdateStoredProcedureMappings, RelationalAnnotationNames.SqlQueryMappings, RelationalAnnotationNames.DefaultMappings, RelationalAnnotationNames.ForeignKeyMappings, @@ -75,12 +83,12 @@ public void Test_new_annotations_handled_for_entity_types() RelationalAnnotationNames.IsFixedLength, RelationalAnnotationNames.Collation, RelationalAnnotationNames.IsStored, + RelationalAnnotationNames.ParameterDirection, RelationalAnnotationNames.TpcMappingStrategy, RelationalAnnotationNames.TphMappingStrategy, RelationalAnnotationNames.TptMappingStrategy, RelationalAnnotationNames.RelationalModel, RelationalAnnotationNames.ModelDependencies, - RelationalAnnotationNames.Triggers, // Appears on entity but requires provider-specific support RelationalAnnotationNames.FieldValueGetter }; @@ -199,10 +207,18 @@ public void Test_new_annotations_handled_for_properties() RelationalAnnotationNames.ViewColumnMappings, RelationalAnnotationNames.SqlQueryColumnMappings, RelationalAnnotationNames.FunctionColumnMappings, + RelationalAnnotationNames.InsertStoredProcedureParameterMappings, + RelationalAnnotationNames.InsertStoredProcedureResultColumnMappings, + RelationalAnnotationNames.DeleteStoredProcedureParameterMappings, + RelationalAnnotationNames.UpdateStoredProcedureParameterMappings, + RelationalAnnotationNames.UpdateStoredProcedureResultColumnMappings, RelationalAnnotationNames.DefaultColumnMappings, RelationalAnnotationNames.TableMappings, RelationalAnnotationNames.ViewMappings, RelationalAnnotationNames.FunctionMappings, + RelationalAnnotationNames.InsertStoredProcedureMappings, + RelationalAnnotationNames.DeleteStoredProcedureMappings, + RelationalAnnotationNames.UpdateStoredProcedureMappings, RelationalAnnotationNames.SqlQueryMappings, RelationalAnnotationNames.ForeignKeyMappings, RelationalAnnotationNames.TableIndexMappings, diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs index eb9e5cc0938..3aa528f3320 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs @@ -905,33 +905,33 @@ public partial class BigContextModel { partial void Initialize() { - var dependentBasebyte = DependentBasebyteEntityType.Create(this); + var dependentBase = DependentBaseEntityType.Create(this); var principalBase = PrincipalBaseEntityType.Create(this); var ownedType = OwnedTypeEntityType.Create(this); var ownedType0 = OwnedType0EntityType.Create(this); var principalBasePrincipalDerivedDependentBasebyte = PrincipalBasePrincipalDerivedDependentBasebyteEntityType.Create(this); - var dependentDerivedbyte = DependentDerivedbyteEntityType.Create(this, dependentBasebyte); - var principalDerivedDependentBasebyte = PrincipalDerivedDependentBasebyteEntityType.Create(this, principalBase); + var dependentDerived = DependentDerivedEntityType.Create(this, dependentBase); + var principalDerived = PrincipalDerivedEntityType.Create(this, principalBase); - DependentBasebyteEntityType.CreateForeignKey1(dependentBasebyte, principalBase); - DependentBasebyteEntityType.CreateForeignKey2(dependentBasebyte, principalDerivedDependentBasebyte); + DependentBaseEntityType.CreateForeignKey1(dependentBase, principalBase); + DependentBaseEntityType.CreateForeignKey2(dependentBase, principalDerived); OwnedTypeEntityType.CreateForeignKey1(ownedType, principalBase); OwnedTypeEntityType.CreateForeignKey2(ownedType, ownedType); - OwnedType0EntityType.CreateForeignKey1(ownedType0, principalDerivedDependentBasebyte); - PrincipalBasePrincipalDerivedDependentBasebyteEntityType.CreateForeignKey1(principalBasePrincipalDerivedDependentBasebyte, principalDerivedDependentBasebyte); + OwnedType0EntityType.CreateForeignKey1(ownedType0, principalDerived); + PrincipalBasePrincipalDerivedDependentBasebyteEntityType.CreateForeignKey1(principalBasePrincipalDerivedDependentBasebyte, principalDerived); PrincipalBasePrincipalDerivedDependentBasebyteEntityType.CreateForeignKey2(principalBasePrincipalDerivedDependentBasebyte, principalBase); - PrincipalDerivedDependentBasebyteEntityType.CreateForeignKey1(principalDerivedDependentBasebyte, principalBase); + PrincipalDerivedEntityType.CreateForeignKey1(principalDerived, principalBase); - PrincipalBaseEntityType.CreateSkipNavigation1(principalBase, principalDerivedDependentBasebyte, principalBasePrincipalDerivedDependentBasebyte); - PrincipalDerivedDependentBasebyteEntityType.CreateSkipNavigation1(principalDerivedDependentBasebyte, principalBase, principalBasePrincipalDerivedDependentBasebyte); + PrincipalBaseEntityType.CreateSkipNavigation1(principalBase, principalDerived, principalBasePrincipalDerivedDependentBasebyte); + PrincipalDerivedEntityType.CreateSkipNavigation1(principalDerived, principalBase, principalBasePrincipalDerivedDependentBasebyte); - DependentBasebyteEntityType.CreateAnnotations(dependentBasebyte); + DependentBaseEntityType.CreateAnnotations(dependentBase); PrincipalBaseEntityType.CreateAnnotations(principalBase); OwnedTypeEntityType.CreateAnnotations(ownedType); OwnedType0EntityType.CreateAnnotations(ownedType0); PrincipalBasePrincipalDerivedDependentBasebyteEntityType.CreateAnnotations(principalBasePrincipalDerivedDependentBasebyte); - DependentDerivedbyteEntityType.CreateAnnotations(dependentDerivedbyte); - PrincipalDerivedDependentBasebyteEntityType.CreateAnnotations(principalDerivedDependentBasebyte); + DependentDerivedEntityType.CreateAnnotations(dependentDerived); + PrincipalDerivedEntityType.CreateAnnotations(principalDerived); AddAnnotation(""Relational:MaxIdentifierLength"", 128); AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn); @@ -940,7 +940,7 @@ partial void Initialize() } ", c), c => AssertFileContents( - "DependentBasebyteEntityType.cs", @"// + "DependentBaseEntityType.cs", @"// using System; using System.Reflection; using Microsoft.EntityFrameworkCore; @@ -955,7 +955,7 @@ partial void Initialize() namespace TestNamespace { - internal partial class DependentBasebyteEntityType + internal partial class DependentBaseEntityType { public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? baseEntityType = null) { @@ -975,7 +975,6 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba var principalAlternateId = runtimeEntityType.AddProperty( ""PrincipalAlternateId"", typeof(Point), - valueGenerated: ValueGenerated.OnAdd, afterSaveBehavior: PropertySaveBehavior.Throw); principalAlternateId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); @@ -1051,7 +1050,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) runtimeEntityType.AddAnnotation(""Relational:MappingStrategy"", ""TPH""); runtimeEntityType.AddAnnotation(""Relational:Schema"", null); runtimeEntityType.AddAnnotation(""Relational:SqlQuery"", null); - runtimeEntityType.AddAnnotation(""Relational:TableName"", ""PrincipalDerived""); + runtimeEntityType.AddAnnotation(""Relational:TableName"", ""DependentBase""); runtimeEntityType.AddAnnotation(""Relational:ViewName"", null); runtimeEntityType.AddAnnotation(""Relational:ViewSchema"", null); @@ -1095,6 +1094,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalBase).GetField(""k__BackingField"", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), valueGenerated: ValueGenerated.OnAdd, afterSaveBehavior: PropertySaveBehavior.Throw); + var overrides = new StoreObjectDictionary(); var idPrincipalDerived = new RuntimeRelationalPropertyOverrides( id, @@ -1103,6 +1103,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba ""DerivedId""); overrides.Add(StoreObjectIdentifier.Table(""PrincipalDerived"", null), idPrincipalDerived); id.AddAnnotation(""Relational:RelationalOverrides"", overrides); + id.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn); var alternateId = runtimeEntityType.AddProperty( @@ -1212,6 +1213,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba propertyAccessMode: PropertyAccessMode.Field, valueGenerated: ValueGenerated.OnAdd, afterSaveBehavior: PropertySaveBehavior.Throw); + var overrides = new StoreObjectDictionary(); var principalBaseIdPrincipalBase = new RuntimeRelationalPropertyOverrides( principalBaseId, @@ -1221,6 +1223,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba principalBaseIdPrincipalBase.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn); overrides.Add(StoreObjectIdentifier.Table(""PrincipalBase"", ""mySchema""), principalBaseIdPrincipalBase); principalBaseId.AddAnnotation(""Relational:RelationalOverrides"", overrides); + principalBaseId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn); var principalBaseAlternateId = runtimeEntityType.AddProperty( @@ -1238,6 +1241,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.OwnedType).GetField(""
k__BackingField"", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), propertyAccessMode: PropertyAccessMode.Field, nullable: true); + var overrides0 = new StoreObjectDictionary(); var detailsDetails = new RuntimeRelationalPropertyOverrides( details, @@ -1246,6 +1250,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba null); overrides0.Add(StoreObjectIdentifier.Table(""Details"", null), detailsDetails); details.AddAnnotation(""Relational:RelationalOverrides"", overrides0); + details.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); var number = runtimeEntityType.AddProperty( @@ -1351,17 +1356,17 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba baseEntityType, sharedClrType: true); - var principalDerivedDependentBasebyteId = runtimeEntityType.AddProperty( - ""PrincipalDerived>Id"", + var principalDerivedId = runtimeEntityType.AddProperty( + ""PrincipalDerivedId"", typeof(long), afterSaveBehavior: PropertySaveBehavior.Throw); - principalDerivedDependentBasebyteId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); + principalDerivedId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); - var principalDerivedDependentBasebyteAlternateId = runtimeEntityType.AddProperty( - ""PrincipalDerived>AlternateId"", + var principalDerivedAlternateId = runtimeEntityType.AddProperty( + ""PrincipalDerivedAlternateId"", typeof(Point), afterSaveBehavior: PropertySaveBehavior.Throw); - principalDerivedDependentBasebyteAlternateId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); + principalDerivedAlternateId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); var id = runtimeEntityType.AddProperty( ""Id"", @@ -1390,7 +1395,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba propertyInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.OwnedType).GetProperty(""Context"", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)); var key = runtimeEntityType.AddKey( - new[] { principalDerivedDependentBasebyteId, principalDerivedDependentBasebyteAlternateId, id }); + new[] { principalDerivedId, principalDerivedAlternateId, id }); runtimeEntityType.SetPrimaryKey(key); return runtimeEntityType; @@ -1398,7 +1403,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba public static RuntimeForeignKey CreateForeignKey1(RuntimeEntityType declaringEntityType, RuntimeEntityType principalEntityType) { - var runtimeForeignKey = declaringEntityType.AddForeignKey(new[] { declaringEntityType.FindProperty(""PrincipalDerived>Id"")!, declaringEntityType.FindProperty(""PrincipalDerived>AlternateId"")! }, + var runtimeForeignKey = declaringEntityType.AddForeignKey(new[] { declaringEntityType.FindProperty(""PrincipalDerivedId"")!, declaringEntityType.FindProperty(""PrincipalDerivedAlternateId"")! }, principalEntityType.FindKey(new[] { principalEntityType.FindProperty(""Id"")!, principalEntityType.FindProperty(""AlternateId"")! })!, principalEntityType, deleteBehavior: DeleteBehavior.Cascade, @@ -1546,7 +1551,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) } ", c), c => AssertFileContents( - "DependentDerivedbyteEntityType.cs", @"// + "DependentDerivedEntityType.cs", @"// using System; using System.Reflection; using Microsoft.EntityFrameworkCore.Metadata; @@ -1558,7 +1563,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) namespace TestNamespace { - internal partial class DependentDerivedbyteEntityType + internal partial class DependentDerivedEntityType { public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? baseEntityType = null) { @@ -1595,7 +1600,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) runtimeEntityType.AddAnnotation(""Relational:FunctionName"", null); runtimeEntityType.AddAnnotation(""Relational:Schema"", null); runtimeEntityType.AddAnnotation(""Relational:SqlQuery"", null); - runtimeEntityType.AddAnnotation(""Relational:TableName"", ""PrincipalDerived""); + runtimeEntityType.AddAnnotation(""Relational:TableName"", ""DependentBase""); runtimeEntityType.AddAnnotation(""Relational:ViewName"", null); runtimeEntityType.AddAnnotation(""Relational:ViewSchema"", null); @@ -1607,7 +1612,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) } ", c), c => AssertFileContents( - "PrincipalDerivedDependentBasebyteEntityType.cs", @"// + "PrincipalDerivedEntityType.cs", @"// using System; using System.Collections.Generic; using System.Reflection; @@ -1620,7 +1625,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) namespace TestNamespace { - internal partial class PrincipalDerivedDependentBasebyteEntityType + internal partial class PrincipalDerivedEntityType { public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? baseEntityType = null) { @@ -2279,8 +2284,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey>("PrincipalId") .HasPrincipalKey(e => e.Id); - eb.ToTable("PrincipalDerived"); - eb.HasDiscriminator("EnumDiscriminator") .HasValue(Enum1.One) .HasValue>(Enum1.Two) @@ -2357,17 +2360,17 @@ public partial class TpcContextModel { partial void Initialize() { - var dependentBasebyte = DependentBasebyteEntityType.Create(this); + var dependentBase = DependentBaseEntityType.Create(this); var principalBase = PrincipalBaseEntityType.Create(this); - var principalDerivedDependentBasebyte = PrincipalDerivedDependentBasebyteEntityType.Create(this, principalBase); + var principalDerived = PrincipalDerivedEntityType.Create(this, principalBase); - DependentBasebyteEntityType.CreateForeignKey1(dependentBasebyte, principalDerivedDependentBasebyte); + DependentBaseEntityType.CreateForeignKey1(dependentBase, principalDerived); PrincipalBaseEntityType.CreateForeignKey1(principalBase, principalBase); - PrincipalBaseEntityType.CreateForeignKey2(principalBase, principalDerivedDependentBasebyte); + PrincipalBaseEntityType.CreateForeignKey2(principalBase, principalDerived); - DependentBasebyteEntityType.CreateAnnotations(dependentBasebyte); + DependentBaseEntityType.CreateAnnotations(dependentBase); PrincipalBaseEntityType.CreateAnnotations(principalBase); - PrincipalDerivedDependentBasebyteEntityType.CreateAnnotations(principalDerivedDependentBasebyte); + PrincipalDerivedEntityType.CreateAnnotations(principalDerived); AddAnnotation(""Relational:DefaultSchema"", ""TPC""); AddAnnotation(""Relational:MaxIdentifierLength"", 128); @@ -2377,7 +2380,7 @@ partial void Initialize() } ", c), c => AssertFileContents( - "DependentBasebyteEntityType.cs", @"// + "DependentBaseEntityType.cs", @"// using System; using System.Reflection; using Microsoft.EntityFrameworkCore; @@ -2389,7 +2392,7 @@ partial void Initialize() namespace TestNamespace { - internal partial class DependentBasebyteEntityType + internal partial class DependentBaseEntityType { public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? baseEntityType = null) { @@ -2469,6 +2472,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) "PrincipalBaseEntityType.cs", @"// using System; using System.Collections.Generic; +using System.Data; using System.Reflection; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Scaffolding.Internal; @@ -2495,6 +2499,32 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalBase).GetField(""k__BackingField"", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), valueGenerated: ValueGenerated.OnAdd, afterSaveBehavior: PropertySaveBehavior.Throw); + + var overrides = new StoreObjectDictionary(); + var idDerivedInsert = new RuntimeRelationalPropertyOverrides( + id, + StoreObjectIdentifier.InsertStoredProcedure(""Derived_Insert"", ""TPC""), + true, + ""DerivedId""); + idDerivedInsert.AddAnnotation(""foo"", ""bar3""); + overrides.Add(StoreObjectIdentifier.InsertStoredProcedure(""Derived_Insert"", ""TPC""), idDerivedInsert); + var idPrincipalBaseView = new RuntimeRelationalPropertyOverrides( + id, + StoreObjectIdentifier.View(""PrincipalBaseView"", ""TPC""), + false, + null); + idPrincipalBaseView.AddAnnotation(""foo"", ""bar2""); + overrides.Add(StoreObjectIdentifier.View(""PrincipalBaseView"", ""TPC""), idPrincipalBaseView); + var idPrincipalBaseInsert = new RuntimeRelationalPropertyOverrides( + id, + StoreObjectIdentifier.InsertStoredProcedure(""PrincipalBase_Insert"", ""TPC""), + true, + ""BaseId""); + idPrincipalBaseInsert.AddAnnotation(""foo"", ""bar""); + idPrincipalBaseInsert.AddAnnotation(""Relational:ParameterDirection"", ParameterDirection.Output); + overrides.Add(StoreObjectIdentifier.InsertStoredProcedure(""PrincipalBase_Insert"", ""TPC""), idPrincipalBaseInsert); + id.AddAnnotation(""Relational:RelationalOverrides"", overrides); + id.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn); var principalBaseId = runtimeEntityType.AddProperty( @@ -2503,11 +2533,11 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba nullable: true); principalBaseId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); - var principalDerivedDependentBasebyteId = runtimeEntityType.AddProperty( - ""PrincipalDerived>Id"", + var principalDerivedId = runtimeEntityType.AddProperty( + ""PrincipalDerivedId"", typeof(long?), nullable: true); - principalDerivedDependentBasebyteId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); + principalDerivedId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); var key = runtimeEntityType.AddKey( new[] { id }); @@ -2517,7 +2547,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba new[] { principalBaseId }); var index0 = runtimeEntityType.AddIndex( - new[] { principalDerivedDependentBasebyteId }); + new[] { principalDerivedId }); return runtimeEntityType; } @@ -2540,7 +2570,7 @@ public static RuntimeForeignKey CreateForeignKey1(RuntimeEntityType declaringEnt public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEntityType, RuntimeEntityType principalEntityType) { - var runtimeForeignKey = declaringEntityType.AddForeignKey(new[] { declaringEntityType.FindProperty(""PrincipalDerived>Id"")! }, + var runtimeForeignKey = declaringEntityType.AddForeignKey(new[] { declaringEntityType.FindProperty(""PrincipalDerivedId"")! }, principalEntityType.FindKey(new[] { principalEntityType.FindProperty(""Id"")! })!, principalEntityType); @@ -2556,6 +2586,38 @@ public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEnt public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) { + var insertSproc = new RuntimeStoredProcedure( + runtimeEntityType, + ""PrincipalBase_Insert"", + ""TPC"", + true); + + insertSproc.AddParameter(""PrincipalBaseId""); + insertSproc.AddParameter(""PrincipalDerivedId""); + insertSproc.AddParameter(""Id""); + insertSproc.AddAnnotation(""foo"", ""bar1""); + runtimeEntityType.AddAnnotation(""Relational:InsertStoredProcedure"", insertSproc); + + var deleteSproc = new RuntimeStoredProcedure( + runtimeEntityType, + ""PrincipalBase_Delete"", + ""TPC"", + false); + + deleteSproc.AddParameter(""Id""); + runtimeEntityType.AddAnnotation(""Relational:DeleteStoredProcedure"", deleteSproc); + + var updateSproc = new RuntimeStoredProcedure( + runtimeEntityType, + ""PrincipalBase_Update"", + ""TPC"", + false); + + updateSproc.AddParameter(""PrincipalBaseId""); + updateSproc.AddParameter(""PrincipalDerivedId""); + updateSproc.AddParameter(""Id""); + runtimeEntityType.AddAnnotation(""Relational:UpdateStoredProcedure"", updateSproc); + runtimeEntityType.AddAnnotation(""Relational:FunctionName"", null); runtimeEntityType.AddAnnotation(""Relational:MappingStrategy"", ""TPC""); runtimeEntityType.AddAnnotation(""Relational:Schema"", ""TPC""); @@ -2573,7 +2635,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) } ", c), c => AssertFileContents( - "PrincipalDerivedDependentBasebyteEntityType.cs", @"// + "PrincipalDerivedEntityType.cs", @"// using System; using System.Reflection; using Microsoft.EntityFrameworkCore.Metadata; @@ -2584,7 +2646,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) namespace TestNamespace { - internal partial class PrincipalDerivedDependentBasebyteEntityType + internal partial class PrincipalDerivedEntityType { public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? baseEntityType = null) { @@ -2599,6 +2661,37 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) { + var insertSproc = new RuntimeStoredProcedure( + runtimeEntityType, + ""Derived_Insert"", + ""TPC"", + false); + + insertSproc.AddParameter(""PrincipalBaseId""); + insertSproc.AddParameter(""PrincipalDerivedId""); + insertSproc.AddResultColumn(""Id""); + runtimeEntityType.AddAnnotation(""Relational:InsertStoredProcedure"", insertSproc); + + var deleteSproc = new RuntimeStoredProcedure( + runtimeEntityType, + ""Derived_Delete"", + ""TPC"", + false); + + deleteSproc.AddParameter(""Id""); + runtimeEntityType.AddAnnotation(""Relational:DeleteStoredProcedure"", deleteSproc); + + var updateSproc = new RuntimeStoredProcedure( + runtimeEntityType, + ""Derived_Update"", + ""Derived"", + false); + + updateSproc.AddParameter(""PrincipalBaseId""); + updateSproc.AddParameter(""PrincipalDerivedId""); + updateSproc.AddParameter(""Id""); + runtimeEntityType.AddAnnotation(""Relational:UpdateStoredProcedure"", updateSproc); + runtimeEntityType.AddAnnotation(""Relational:FunctionName"", null); runtimeEntityType.AddAnnotation(""Relational:Schema"", ""TPC""); runtimeEntityType.AddAnnotation(""Relational:SqlQuery"", null); @@ -2619,10 +2712,51 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) Assert.Equal("TPC", model.GetDefaultSchema()); var principalBase = model.FindEntityType(typeof(PrincipalBase)); + var id = principalBase.FindProperty("Id"); + + Assert.Equal("Id", id.GetColumnName()); Assert.Equal("PrincipalBase", principalBase.GetTableName()); Assert.Equal("TPC", principalBase.GetSchema()); + Assert.Equal("Id", id.GetColumnName(StoreObjectIdentifier.Create(principalBase, StoreObjectType.Table).Value)); + Assert.Null(id.FindOverrides(StoreObjectIdentifier.Create(principalBase, StoreObjectType.Table).Value)); + Assert.Equal("PrincipalBaseView", principalBase.GetViewName()); Assert.Equal("TPC", principalBase.GetViewSchema()); + Assert.Equal("Id", id.GetColumnName(StoreObjectIdentifier.Create(principalBase, StoreObjectType.View).Value)); + Assert.Equal("bar2", + id.FindOverrides(StoreObjectIdentifier.Create(principalBase, StoreObjectType.View).Value)["foo"]); + + var insertSproc = principalBase.GetInsertStoredProcedure()!; + Assert.Equal("PrincipalBase_Insert", insertSproc.Name); + Assert.Equal("TPC", insertSproc.Schema); + Assert.Equal(new[] { "PrincipalBaseId", "PrincipalDerivedId", "Id" }, insertSproc.Parameters); + Assert.Empty(insertSproc.ResultColumns); + Assert.True(insertSproc.AreTransactionsSuppressed); + Assert.Equal("bar1", insertSproc["foo"]); + Assert.Same(principalBase, insertSproc.EntityType); + Assert.Equal("BaseId", id.GetColumnName(StoreObjectIdentifier.Create(principalBase, StoreObjectType.InsertStoredProcedure).Value)); + Assert.Equal("bar", + id.FindOverrides(StoreObjectIdentifier.Create(principalBase, StoreObjectType.InsertStoredProcedure).Value)["foo"]); + + var updateSproc = principalBase.GetUpdateStoredProcedure()!; + Assert.Equal("PrincipalBase_Update", updateSproc.Name); + Assert.Equal("TPC", updateSproc.Schema); + Assert.Equal(new[] { "PrincipalBaseId", "PrincipalDerivedId", "Id" }, updateSproc.Parameters); + Assert.Empty(updateSproc.ResultColumns); + Assert.False(updateSproc.AreTransactionsSuppressed); + Assert.Empty(updateSproc.GetAnnotations()); + Assert.Same(principalBase, updateSproc.EntityType); + Assert.Equal("Id", id.GetColumnName(StoreObjectIdentifier.Create(principalBase, StoreObjectType.UpdateStoredProcedure).Value)); + Assert.Null(id.FindOverrides(StoreObjectIdentifier.Create(principalBase, StoreObjectType.UpdateStoredProcedure).Value)); + + var deleteSproc = principalBase.GetDeleteStoredProcedure()!; + Assert.Equal("PrincipalBase_Delete", deleteSproc.Name); + Assert.Equal("TPC", deleteSproc.Schema); + Assert.Equal(new[] { "Id" }, deleteSproc.Parameters); + Assert.Empty(deleteSproc.ResultColumns); + Assert.Same(principalBase, deleteSproc.EntityType); + Assert.Equal("Id", id.GetColumnName(StoreObjectIdentifier.Create(principalBase, StoreObjectType.DeleteStoredProcedure).Value)); + Assert.Null(id.FindOverrides(StoreObjectIdentifier.Create(principalBase, StoreObjectType.DeleteStoredProcedure).Value)); Assert.Equal("PrincipalBase", principalBase.GetDiscriminatorValue()); Assert.Null(principalBase.FindDiscriminatorProperty()); @@ -2643,6 +2777,39 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) Assert.Equal("PrincipalDerivedView", principalDerived.GetViewName()); Assert.Equal("TPC", principalBase.GetViewSchema()); + insertSproc = principalDerived.GetInsertStoredProcedure()!; + Assert.Equal("Derived_Insert", insertSproc.Name); + Assert.Equal("TPC", insertSproc.Schema); + Assert.Equal(new[] { "PrincipalBaseId", "PrincipalDerivedId" }, insertSproc.Parameters); + Assert.Equal(new[] { "Id" }, insertSproc.ResultColumns); + Assert.False(insertSproc.AreTransactionsSuppressed); + Assert.Null(insertSproc["foo"]); + Assert.Same(principalDerived, insertSproc.EntityType); + Assert.Equal("DerivedId", id.GetColumnName(StoreObjectIdentifier.Create(principalDerived, StoreObjectType.InsertStoredProcedure).Value)); + Assert.Equal("bar3", + id.FindOverrides(StoreObjectIdentifier.Create(principalDerived, StoreObjectType.InsertStoredProcedure).Value)["foo"]); + + updateSproc = principalDerived.GetUpdateStoredProcedure()!; + Assert.Equal("Derived_Update", updateSproc.Name); + Assert.Equal("Derived", updateSproc.Schema); + Assert.Equal(new[] { "PrincipalBaseId", "PrincipalDerivedId", "Id" }, updateSproc.Parameters); + Assert.Empty(updateSproc.ResultColumns); + Assert.False(updateSproc.AreTransactionsSuppressed); + Assert.Empty(updateSproc.GetAnnotations()); + Assert.Same(principalDerived, updateSproc.EntityType); + Assert.Equal("Id", id.GetColumnName(StoreObjectIdentifier.Create(principalDerived, StoreObjectType.UpdateStoredProcedure).Value)); + Assert.Null(id.FindOverrides(StoreObjectIdentifier.Create(principalDerived, StoreObjectType.UpdateStoredProcedure).Value)); + + deleteSproc = principalDerived.GetDeleteStoredProcedure()!; + Assert.Equal("Derived_Delete", deleteSproc.Name); + Assert.Equal("TPC", deleteSproc.Schema); + Assert.Equal(new[] { "Id" }, deleteSproc.Parameters); + Assert.Empty(deleteSproc.ResultColumns); + Assert.False(deleteSproc.AreTransactionsSuppressed); + Assert.Same(principalDerived, deleteSproc.EntityType); + Assert.Equal("Id", id.GetColumnName(StoreObjectIdentifier.Create(principalDerived, StoreObjectType.DeleteStoredProcedure).Value)); + Assert.Null(id.FindOverrides(StoreObjectIdentifier.Create(principalDerived, StoreObjectType.DeleteStoredProcedure).Value)); + Assert.Equal("PrincipalDerived>", principalDerived.GetDiscriminatorValue()); Assert.Null(principalDerived.FindDiscriminatorProperty()); Assert.Equal("TPC", principalDerived.GetMappingStrategy()); @@ -2708,8 +2875,21 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) eb.Ignore(e => e.Owned); eb.UseTpcMappingStrategy(); + eb.ToTable("PrincipalBase"); - eb.ToView("PrincipalBaseView"); + eb.ToView("PrincipalBaseView", tb => tb.Property(e => e.Id).HasAnnotation("foo", "bar2")); + + eb.InsertUsingStoredProcedure(s => s.SuppressTransactions() + .HasParameter("PrincipalBaseId") + .HasParameter("PrincipalDerivedId") + .HasParameter(p => p.Id, pb => pb.HasName("BaseId").IsOutput().HasAnnotation("foo", "bar")) + .HasAnnotation("foo", "bar1")); + eb.UpdateUsingStoredProcedure(s => s + .HasParameter("PrincipalBaseId") + .HasParameter("PrincipalDerivedId") + .HasParameter(p => p.Id)); + eb.DeleteUsingStoredProcedure(s => s + .HasParameter(p => p.Id)); }); modelBuilder.Entity>>( @@ -2723,6 +2903,17 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) eb.ToTable("PrincipalDerived"); eb.ToView("PrincipalDerivedView"); + + eb.InsertUsingStoredProcedure("Derived_Insert", s => s + .HasParameter("PrincipalBaseId") + .HasParameter("PrincipalDerivedId") + .HasResultColumn(p => p.Id, pb => pb.HasName("DerivedId").HasAnnotation("foo", "bar3"))); + eb.UpdateUsingStoredProcedure("Derived_Update", "Derived", s => s + .HasParameter("PrincipalBaseId") + .HasParameter("PrincipalDerivedId") + .HasParameter(p => p.Id)); + eb.DeleteUsingStoredProcedure("Derived_Delete", s => s + .HasParameter(p => p.Id)); }); modelBuilder.Entity>( @@ -4119,23 +4310,23 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas nullable: true); blob.AddAnnotation(""Cosmos:PropertyName"", ""JsonBlob""); - var __id = runtimeEntityType.AddProperty( + var id0 = runtimeEntityType.AddProperty( ""__id"", typeof(string), afterSaveBehavior: PropertySaveBehavior.Throw, valueGeneratorFactory: new IdValueGeneratorFactory().Create); - __id.AddAnnotation(""Cosmos:PropertyName"", ""id""); + id0.AddAnnotation(""Cosmos:PropertyName"", ""id""); - var __jObject = runtimeEntityType.AddProperty( + var jObject = runtimeEntityType.AddProperty( ""__jObject"", typeof(JObject), nullable: true, valueGenerated: ValueGenerated.OnAddOrUpdate, beforeSaveBehavior: PropertySaveBehavior.Ignore, afterSaveBehavior: PropertySaveBehavior.Ignore); - __jObject.AddAnnotation(""Cosmos:PropertyName"", """"); + jObject.AddAnnotation(""Cosmos:PropertyName"", """"); - var _etag = runtimeEntityType.AddProperty( + var etag = runtimeEntityType.AddProperty( ""_etag"", typeof(string), nullable: true, @@ -4149,7 +4340,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas runtimeEntityType.SetPrimaryKey(key); var key0 = runtimeEntityType.AddKey( - new[] { __id, partitionId }); + new[] { id0, partitionId }); return runtimeEntityType; } diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 574fb3bdb5c..42320146305 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -2735,6 +2735,33 @@ public virtual void Detects_non_generated_update_stored_procedure_result_columns modelBuilder); } + [ConditionalFact] + public virtual void Detects_non_generated_insert_stored_procedure_output_parameter_in_TPC() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .InsertUsingStoredProcedure(s => s.HasParameter(a => a.Name, p => p.IsOutput())) + .UseTpcMappingStrategy(); + modelBuilder.Entity(); + + VerifyError( + RelationalStrings.StoredProcedureOutputParameterNotGenerated(nameof(Animal), nameof(Animal.Name), "Animal_Insert"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_non_generated_update_stored_procedure_input_output_parameter() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .Ignore(a => a.FavoritePerson) + .UpdateUsingStoredProcedure(s => s.HasParameter(a => a.Id).HasParameter(a => a.Name, p => p.IsInputOutput())); + + VerifyError( + RelationalStrings.StoredProcedureOutputParameterNotGenerated(nameof(Animal), nameof(Animal.Name), "Animal_Update"), + modelBuilder); + } + [ConditionalFact] public virtual void Detects_delete_stored_procedure_result_columns_in_TPH() { @@ -2852,6 +2879,24 @@ public virtual void Detects_InsertUsingStoredProcedure_without_a_name() modelBuilder); } + [ConditionalFact] + public virtual void Detects_missing_generated_stored_procedure_parameters() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .UpdateUsingStoredProcedure("Update", s => s + .HasParameter(a => a.Id, p => p.HasName("MyId")) + .HasParameter(a => a.Name) + .HasParameter("FavoritePersonId") + .HasParameter(a => a.Name)) + .Property(a => a.Name).ValueGeneratedOnUpdate(); + + VerifyError( + RelationalStrings.StoredProcedureGeneratedPropertiesNotMapped(nameof(Animal), + "Update", "{'Name'}"), + modelBuilder); + } + [ConditionalFact] public virtual void Detects_missing_stored_procedure_parameters_in_TPC() { @@ -2865,7 +2910,10 @@ public virtual void Detects_missing_stored_procedure_parameters_in_TPC() .HasResultColumn(a => a.Name)) .Property(a => a.Name).ValueGeneratedOnUpdate(); modelBuilder.Entity() - .UpdateUsingStoredProcedure(s => s.HasParameter(c => c.Breed).HasParameter(a => a.Name)); + .UpdateUsingStoredProcedure(s => s + .HasResultColumn(a => a.Name) + .HasParameter(c => c.Breed) + .HasParameter(a => a.Name)); VerifyError( RelationalStrings.StoredProcedurePropertiesNotMapped(nameof(Cat), "Cat_Update", "{'Identity', 'Id', 'FavoritePersonId'}"), diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs index 5afc5bb785c..827b0e86125 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Data; using Microsoft.EntityFrameworkCore.Metadata.Internal; using NameSpace1; @@ -51,7 +52,7 @@ public void Can_use_relational_model_with_tables(bool useExplicitMapping, Mappin [InlineData(Mapping.TPC)] public void Can_use_relational_model_with_views(Mapping mapping) { - var model = CreateTestModel(mapToTables: false, mapToViews: true, mapping); + var model = CreateTestModel(mapToTables: false, mapToViews: true, mapping: mapping); Assert.Equal(11, model.Model.GetEntityTypes().Count()); Assert.Equal(mapping switch @@ -67,13 +68,47 @@ public void Can_use_relational_model_with_views(Mapping mapping) AssertViews(model, mapping); } + [ConditionalTheory] + [InlineData(true, Mapping.TPH)] + [InlineData(true, Mapping.TPT)] + [InlineData(true, Mapping.TPC)] + [InlineData(false, Mapping.TPH)] + [InlineData(false, Mapping.TPT)] + [InlineData(false, Mapping.TPC)] + public void Can_use_relational_model_with_sprocs(bool mapToTables, Mapping mapping) + { + var model = CreateTestModel(mapToTables: mapToTables, mapToSprocs:true, mapping: mapping); + + Assert.Equal(11, model.Model.GetEntityTypes().Count()); + Assert.Equal( + mapping switch + { + Mapping.TPC => 5, + Mapping.TPH => 3, + _ => 6 + }, model.Tables.Count()); + + Assert.Equal(mapping switch + { + Mapping.TPC => 24, + Mapping.TPH => 18, + _ => 27 + }, model.StoredProcedures.Count()); + + Assert.Empty(model.Views); + Assert.True(model.Model.GetEntityTypes().All(et => !et.GetViewMappings().Any())); + + AssertDefaultMappings(model, mapping); + AssertSprocs(model, mapping); + } + [ConditionalTheory] [InlineData(Mapping.TPH)] [InlineData(Mapping.TPT)] [InlineData(Mapping.TPC)] - public void Can_use_relational_model_with_views_and_tables(Mapping mapping) + public void Can_use_relational_model_with_tables_and_views(Mapping mapping) { - var model = CreateTestModel(mapToTables: true, mapToViews: true, mapping); + var model = CreateTestModel(mapToTables: true, mapToViews: true, mapping: mapping); Assert.Equal(11, model.Model.GetEntityTypes().Count()); Assert.Equal(mapping switch @@ -82,6 +117,7 @@ public void Can_use_relational_model_with_views_and_tables(Mapping mapping) Mapping.TPH => 3, _ => 6 }, model.Tables.Count()); + Assert.Equal(mapping switch { Mapping.TPC => 5, @@ -641,12 +677,12 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) var specialCustomerTable = specialCustomerType.GetTableMappings().Select(t => t.Table).Last(); - var SpecialtyCK = specialCustomerType.GetCheckConstraints().Single(); - Assert.Equal("Specialty", SpecialtyCK.Name); - Assert.Equal("Specialty", SpecialtyCK.GetName( + var specialtyCk = specialCustomerType.GetCheckConstraints().Single(); + Assert.Equal("Specialty", specialtyCk.Name); + Assert.Equal("Specialty", specialtyCk.GetName( StoreObjectIdentifier.Table(specialCustomerTable.Name, specialCustomerTable.Schema))); - Assert.Equal("Specialty", SpecialtyCK.GetDefaultName()); - Assert.Equal("Specialty", SpecialtyCK.GetDefaultName( + Assert.Equal("Specialty", specialtyCk.GetDefaultName()); + Assert.Equal("Specialty", specialtyCk.GetDefaultName( StoreObjectIdentifier.Table(specialCustomerTable.Name, specialCustomerTable.Schema))); var customerTable = customerType.GetTableMappings().Last().Table; @@ -722,7 +758,7 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) extraSpecialCustomerType.GetTableMappings().Select(t => t.Table).First(t => t.Name == "ExtraSpecialCustomer"); Assert.Empty(customerTable.CheckConstraints); - Assert.Same(SpecialtyCK, specialCustomerTable.CheckConstraints.Single()); + Assert.Same(specialtyCk, specialCustomerTable.CheckConstraints.Single()); Assert.Empty(extraSpecialCustomerTable.CheckConstraints); Assert.Equal(4, customerPk.GetMappedConstraints().Count()); @@ -845,8 +881,8 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) Assert.True(addressColumn.IsNullable); var abstractStringColumn = specialCustomerTable.Columns.Single(c => c.Name == nameof(AbstractCustomer.AbstractString)); - Assert.True(specialtyColumn.IsNullable); - Assert.Equal(2, specialtyColumn.PropertyMappings.Count); + Assert.True(abstractStringColumn.IsNullable); + Assert.Equal(3, abstractStringColumn.PropertyMappings.Count); var abstractStringProperty = abstractStringColumn.PropertyMappings.First().Property; Assert.Equal(3, abstractStringProperty.GetTableColumnMappings().Count()); @@ -902,8 +938,6 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) Assert.Equal("IX_AbstractBase_RelatedCustomerSpecialty", specialCustomerDbIndex.Name); Assert.Equal("IX_AbstractBase_AnotherRelatedCustomerId", anotherSpecialCustomerDbIndex.Name); - - Assert.Equal(5, idProperty.GetTableColumnMappings().Count()); } else { @@ -974,8 +1008,6 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) Assert.Equal("IX_SpecialCustomer_RelatedCustomerSpecialty", specialCustomerDbIndex.Name); Assert.Equal("IX_SpecialCustomer_AnotherRelatedCustomerId", anotherSpecialCustomerDbIndex.Name); - - Assert.Equal(3, idProperty.GetTableColumnMappings().Count()); } Assert.Same(specialCustomerPkConstraint.MappedKeys.First(), customerPk); @@ -987,7 +1019,833 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) } } - private IRelationalModel CreateTestModel(bool mapToTables = false, bool mapToViews = false, Mapping mapping = Mapping.TPH) + private static void AssertSprocs(IRelationalModel model, Mapping mapping) + { + var orderType = model.Model.FindEntityType(typeof(Order)); + var orderInsertMapping = orderType.GetInsertStoredProcedureMappings().Single(); + Assert.True(orderInsertMapping.IncludesDerivedTypes); + Assert.Same(orderType.GetInsertStoredProcedure(), orderInsertMapping.StoredProcedure); + + Assert.Equal( + new[] { nameof(Order.AlternateId), nameof(Order.CustomerId), nameof(Order.OrderDate) }, + orderInsertMapping.ParameterMappings.Select(m => m.Property.Name)); + + Assert.Equal( + new[] { nameof(Order.Id) }, + orderInsertMapping.ResultColumnMappings.Select(m => m.Property.Name)); + Assert.Equal(orderInsertMapping.ResultColumnMappings, orderInsertMapping.ColumnMappings); + + var ordersInsertSproc = orderInsertMapping.StoreStoredProcedure; + Assert.Same(ordersInsertSproc, orderInsertMapping.Table); + Assert.Equal("Order_Insert", ordersInsertSproc.Name); + Assert.Null(ordersInsertSproc.Schema); + Assert.False(ordersInsertSproc.IsShared); + Assert.Same(ordersInsertSproc, model.FindStoredProcedure(ordersInsertSproc.Name, ordersInsertSproc.Schema)); + Assert.Equal( + new[] { nameof(Order) }, + ordersInsertSproc.EntityTypeMappings.Select(m => m.EntityType.DisplayName())); + + Assert.Equal( + new[] { nameof(Order.AlternateId), nameof(Order.CustomerId), nameof(Order.OrderDate) }, + ordersInsertSproc.Parameters.Select(m => m.Name)); + + Assert.Equal( + new[] { nameof(Order.Id) }, + ordersInsertSproc.ResultColumns.Select(m => m.Name)); + Assert.Equal(ordersInsertSproc.ResultColumns, ordersInsertSproc.Columns); + + var orderDate = orderType.FindProperty(nameof(Order.OrderDate)); + + var orderDateInsertMapping = orderDate.GetInsertStoredProcedureParameterMappings().Single(); + Assert.NotNull(orderDateInsertMapping.TypeMapping); + Assert.Equal("default_datetime_mapping", orderDateInsertMapping.TypeMapping.StoreType); + Assert.Same(orderInsertMapping, orderDateInsertMapping.TableMapping); + + var orderDateColumn = orderDateInsertMapping.Parameter; + Assert.Same(orderDateInsertMapping.Parameter, orderDateInsertMapping.Column); + Assert.Same(orderDateColumn, ordersInsertSproc.FindParameter("OrderDate")); + Assert.Same(orderDateColumn, ordersInsertSproc.FindParameter(orderDate)); + Assert.Equal("OrderDate", orderDateColumn.Name); + Assert.Equal("default_datetime_mapping", orderDateColumn.StoreType); + Assert.False(orderDateColumn.IsNullable); + Assert.Equal(ParameterDirection.Input, orderDateColumn.Direction); + Assert.Same(ordersInsertSproc, orderDateColumn.StoredProcedure); + Assert.Same(orderDateColumn.StoredProcedure, orderDateColumn.Table); + Assert.Same(orderDateInsertMapping, orderDateColumn.FindParameterMapping(orderType)); + + var abstractBaseType = model.Model.FindEntityType(typeof(AbstractBase)); + var abstractCustomerType = model.Model.FindEntityType(typeof(AbstractCustomer)); + var customerType = model.Model.FindEntityType(typeof(Customer)); + var specialCustomerType = model.Model.FindEntityType(typeof(SpecialCustomer)); + var extraSpecialCustomerType = model.Model.FindEntityType(typeof(ExtraSpecialCustomer)); + var orderDetailsOwnership = orderType.FindNavigation(nameof(Order.Details)).ForeignKey; + var orderDetailsType = orderDetailsOwnership.DeclaringEntityType; + + Assert.Empty(ordersInsertSproc.GetReferencingRowInternalForeignKeys(orderType)); + Assert.Equal( + RelationalStrings.TableNotMappedEntityType(nameof(SpecialCustomer), ordersInsertSproc.Name), + Assert.Throws( + () => ordersInsertSproc.GetReferencingRowInternalForeignKeys(specialCustomerType)).Message); + Assert.Equal( + RelationalStrings.TableNotMappedEntityType(nameof(SpecialCustomer), ordersInsertSproc.Name), + Assert.Throws( + () => ordersInsertSproc.GetRowInternalForeignKeys(specialCustomerType)).Message); + Assert.False(ordersInsertSproc.IsOptional(orderType)); + Assert.Equal( + RelationalStrings.TableNotMappedEntityType(nameof(OrderDetails), ordersInsertSproc.Name), + Assert.Throws( + () => ordersInsertSproc.IsOptional(orderDetailsType)).Message); + Assert.Equal( + RelationalStrings.TableNotMappedEntityType(nameof(SpecialCustomer), ordersInsertSproc.Name), + Assert.Throws( + () => ordersInsertSproc.IsOptional(specialCustomerType)).Message); + + var billingAddressOwnership = orderDetailsType.FindNavigation(nameof(OrderDetails.BillingAddress)).ForeignKey; + Assert.True(billingAddressOwnership.IsRequiredDependent); + + var billingAddressType = billingAddressOwnership.DeclaringEntityType; + + var shippingAddressOwnership = orderDetailsType.FindNavigation(nameof(OrderDetails.ShippingAddress)).ForeignKey; + Assert.True(shippingAddressOwnership.IsRequiredDependent); + + var billingAddressInsertMapping = billingAddressType.GetInsertStoredProcedureMappings().Single(); + Assert.Same(billingAddressType.GetInsertStoredProcedure(), billingAddressInsertMapping.StoredProcedure); + Assert.Same(billingAddressType, billingAddressInsertMapping.StoredProcedure.EntityType); + + var billingAddressInsertSproc = billingAddressInsertMapping.StoreStoredProcedure; + Assert.Equal("BillingAddress_Insert", billingAddressInsertSproc.Name); + Assert.Null(billingAddressInsertSproc.Schema); + Assert.Equal( + new[] { "OrderDetails.BillingAddress#Address" }, + billingAddressInsertSproc.EntityTypeMappings.Select(m => m.EntityType.DisplayName())); + + Assert.Equal( + new[] { nameof(Address.City), nameof(Address.Street), "OrderDetailsOrderId" }, + billingAddressInsertSproc.Parameters.Select(m => m.Name)); + + Assert.Empty(billingAddressInsertSproc.ResultColumns.Select(m => m.Name)); + + var billingAddressUpdateMapping = billingAddressType.GetUpdateStoredProcedureMappings().Single(); + Assert.Same(billingAddressType.GetUpdateStoredProcedure(), billingAddressUpdateMapping.StoredProcedure); + Assert.Same(billingAddressType, billingAddressUpdateMapping.StoredProcedure.EntityType); + + var billingAddressUpdateSproc = billingAddressUpdateMapping.StoreStoredProcedure; + Assert.Equal("BillingAddress_Update", billingAddressUpdateSproc.Name); + Assert.Null(billingAddressUpdateSproc.Schema); + Assert.Equal( + new[] { "OrderDetails.BillingAddress#Address" }, + billingAddressUpdateSproc.EntityTypeMappings.Select(m => m.EntityType.DisplayName())); + + Assert.Equal( + new[] { nameof(Address.City), nameof(Address.Street), "OrderDetailsOrderId" }, + billingAddressUpdateSproc.Parameters.Select(m => m.Name)); + + Assert.Empty(billingAddressUpdateSproc.ResultColumns.Select(m => m.Name)); + + var billingAddressDeleteMapping = billingAddressType.GetDeleteStoredProcedureMappings().Single(); + Assert.Same(billingAddressType.GetDeleteStoredProcedure(), billingAddressDeleteMapping.StoredProcedure); + Assert.Same(billingAddressType, billingAddressDeleteMapping.StoredProcedure.EntityType); + + var billingAddressDeleteSproc = billingAddressDeleteMapping.StoreStoredProcedure; + Assert.Equal("BillingAddress_Delete", billingAddressDeleteSproc.Name); + Assert.Null(billingAddressDeleteSproc.Schema); + Assert.Equal( + new[] { "OrderDetails.BillingAddress#Address" }, + billingAddressDeleteSproc.EntityTypeMappings.Select(m => m.EntityType.DisplayName())); + + Assert.Equal( + new[] { "OrderDetailsOrderId" }, + billingAddressDeleteSproc.Parameters.Select(m => m.Name)); + + Assert.Empty(billingAddressDeleteSproc.ResultColumns.Select(m => m.Name)); + + Assert.Equal(new[] { orderDate }, orderDateColumn.PropertyMappings.Select(m => m.Property)); + + var specialCustomerInsertSproc = + specialCustomerType.GetInsertStoredProcedureMappings().Last().StoreStoredProcedure; + var specialCustomerUpdateSproc = + specialCustomerType.GetUpdateStoredProcedureMappings().Last().StoreStoredProcedure; + var specialCustomerDeleteSproc = + specialCustomerType.GetDeleteStoredProcedureMappings().Last().StoreStoredProcedure; + + var customerInsertSproc = customerType.GetInsertStoredProcedureMappings().Last().StoreStoredProcedure; + Assert.False(customerInsertSproc.IsOptional(customerType)); + if (mapping == Mapping.TPC) + { + Assert.Equal( + RelationalStrings.TableNotMappedEntityType(nameof(SpecialCustomer), customerInsertSproc.Name), + Assert.Throws( + () => customerInsertSproc.IsOptional(specialCustomerType)).Message); + } + else + { + Assert.False(customerInsertSproc.IsOptional(specialCustomerType)); + Assert.False(customerInsertSproc.IsOptional(extraSpecialCustomerType)); + } + + var customerUpdateSproc = customerType.GetUpdateStoredProcedureMappings().Last().StoreStoredProcedure; + Assert.False(customerUpdateSproc.IsOptional(customerType)); + if (mapping == Mapping.TPC) + { + Assert.Equal( + RelationalStrings.TableNotMappedEntityType(nameof(SpecialCustomer), customerUpdateSproc.Name), + Assert.Throws( + () => customerUpdateSproc.IsOptional(specialCustomerType)).Message); + } + else + { + Assert.False(customerUpdateSproc.IsOptional(specialCustomerType)); + Assert.False(customerUpdateSproc.IsOptional(extraSpecialCustomerType)); + } + + var customerDeleteSproc = customerType.GetDeleteStoredProcedureMappings().Last().StoreStoredProcedure; + Assert.False(customerDeleteSproc.IsOptional(customerType)); + if (mapping == Mapping.TPC) + { + Assert.Equal( + RelationalStrings.TableNotMappedEntityType(nameof(SpecialCustomer), customerDeleteSproc.Name), + Assert.Throws( + () => customerDeleteSproc.IsOptional(specialCustomerType)).Message); + } + else + { + Assert.False(customerDeleteSproc.IsOptional(specialCustomerType)); + Assert.False(customerDeleteSproc.IsOptional(extraSpecialCustomerType)); + } + + var customerPk = specialCustomerType.FindPrimaryKey(); + var idProperty = customerPk.Properties.Single(); + + if (mapping == Mapping.TPT) + { + var baseInsertMapping = abstractBaseType.GetInsertStoredProcedureMappings().Single(); + Assert.True(baseInsertMapping.IncludesDerivedTypes); + Assert.Same(abstractBaseType.GetInsertStoredProcedure(), baseInsertMapping.StoredProcedure); + + Assert.Equal( + new[] { nameof(AbstractBase.Id), "SpecialtyAk" }, + baseInsertMapping.ParameterMappings.Select(m => m.Property.Name)); + + Assert.Empty(baseInsertMapping.ResultColumnMappings.Select(m => m.Property.Name)); + Assert.Equal(baseInsertMapping.ResultColumnMappings, baseInsertMapping.ColumnMappings); + + var baseInsertSproc = baseInsertMapping.StoreStoredProcedure; + Assert.Equal("AbstractBase_Insert", baseInsertSproc.Name); + Assert.Equal("Customer_Insert", customerInsertSproc.Name); + Assert.Empty(abstractCustomerType.GetInsertStoredProcedureMappings().Where(m => m.IncludesDerivedTypes)); + Assert.Equal( + "SpecialCustomer_Insert", + specialCustomerType.GetInsertStoredProcedureMappings().Single(m => m.IncludesDerivedTypes).StoreStoredProcedure.Name); + Assert.Null(baseInsertSproc.Schema); + Assert.Equal( + new[] { nameof(AbstractBase), nameof(Customer), nameof(ExtraSpecialCustomer), nameof(SpecialCustomer) }, + baseInsertSproc.EntityTypeMappings.Select(m => m.EntityType.DisplayName())); + + Assert.Equal( + new[] { "InsertId", "SpecialtyAk" }, + baseInsertSproc.Parameters.Select(m => m.Name)); + + Assert.Empty(baseInsertSproc.ResultColumns.Select(m => m.Name)); + Assert.Equal(baseInsertSproc.ResultColumns, baseInsertSproc.Columns); + + var baseUpdateMapping = abstractBaseType.GetUpdateStoredProcedureMappings().Single(); + Assert.True(baseUpdateMapping.IncludesDerivedTypes); + Assert.Same(abstractBaseType.GetUpdateStoredProcedure(), baseUpdateMapping.StoredProcedure); + + Assert.Equal( + new[] { nameof(AbstractBase.Id), "SpecialtyAk" }, + baseUpdateMapping.ParameterMappings.Select(m => m.Property.Name)); + + Assert.Empty( + baseUpdateMapping.ResultColumnMappings.Select(m => m.Property.Name)); + Assert.Equal(baseUpdateMapping.ResultColumnMappings, baseUpdateMapping.ColumnMappings); + + var baseUpdateSproc = baseUpdateMapping.StoreStoredProcedure; + Assert.Equal("AbstractBase_Update", baseUpdateSproc.Name); + Assert.Equal("Customer_Update", customerUpdateSproc.Name); + Assert.Empty(abstractCustomerType.GetUpdateStoredProcedureMappings().Where(m => m.IncludesDerivedTypes)); + Assert.Equal( + "SpecialCustomer_Update", + specialCustomerType.GetUpdateStoredProcedureMappings().Single(m => m.IncludesDerivedTypes).StoreStoredProcedure.Name); + + Assert.Null(baseUpdateSproc.Schema); + Assert.Equal( + new[] { nameof(AbstractBase), nameof(Customer), nameof(ExtraSpecialCustomer), nameof(SpecialCustomer) }, + baseUpdateSproc.EntityTypeMappings.Select(m => m.EntityType.DisplayName())); + + Assert.Equal( + new[] { "UpdateId", "SpecialtyAk" }, + baseUpdateSproc.Parameters.Select(m => m.Name)); + + Assert.Empty(baseUpdateSproc.ResultColumns.Select(m => m.Name)); + Assert.Equal(baseUpdateSproc.ResultColumns, baseUpdateSproc.Columns); + + var baseDeleteMapping = abstractBaseType.GetDeleteStoredProcedureMappings().Single(); + Assert.True(baseDeleteMapping.IncludesDerivedTypes); + Assert.Same(abstractBaseType.GetDeleteStoredProcedure(), baseDeleteMapping.StoredProcedure); + + Assert.Equal( + new[] { nameof(AbstractBase.Id) }, + baseDeleteMapping.ParameterMappings.Select(m => m.Property.Name)); + + Assert.Empty( + baseDeleteMapping.ResultColumnMappings.Select(m => m.Property.Name)); + Assert.Equal(baseDeleteMapping.ResultColumnMappings, baseDeleteMapping.ColumnMappings); + + var baseDeleteSproc = baseDeleteMapping.StoreStoredProcedure; + Assert.Equal("AbstractBase_Delete", baseDeleteSproc.Name); + Assert.Equal("Customer_Delete", customerDeleteSproc.Name); + Assert.Empty(abstractCustomerType.GetDeleteStoredProcedureMappings().Where(m => m.IncludesDerivedTypes)); + Assert.Equal( + "SpecialCustomer_Delete", + specialCustomerType.GetDeleteStoredProcedureMappings().Single(m => m.IncludesDerivedTypes).StoreStoredProcedure.Name); + + Assert.Null(baseDeleteSproc.Schema); + Assert.Equal( + new[] { nameof(AbstractBase), nameof(Customer), nameof(ExtraSpecialCustomer), nameof(SpecialCustomer) }, + baseDeleteSproc.EntityTypeMappings.Select(m => m.EntityType.DisplayName())); + + Assert.Equal( + new[] { "DeleteId" }, + baseDeleteSproc.Parameters.Select(m => m.Name)); + + Assert.Empty(baseDeleteSproc.ResultColumns.Select(m => m.Name)); + Assert.Equal(baseDeleteSproc.ResultColumns, baseDeleteSproc.Columns); + + Assert.Equal(3, specialCustomerType.GetInsertStoredProcedureMappings().Count()); + Assert.Null(specialCustomerType.GetInsertStoredProcedureMappings().First().IsSplitEntityTypePrincipal); + Assert.False(specialCustomerType.GetInsertStoredProcedureMappings().First().IncludesDerivedTypes); + Assert.Null(specialCustomerType.GetInsertStoredProcedureMappings().Last().IsSplitEntityTypePrincipal); + Assert.True(specialCustomerType.GetTableMappings().Last().IncludesDerivedTypes); + + Assert.Equal("SpecialCustomer_Insert", specialCustomerInsertSproc.Name); + Assert.Single(specialCustomerInsertSproc.ResultColumns); + Assert.Equal(4, specialCustomerInsertSproc.Parameters.Count()); + + Assert.Null( + specialCustomerInsertSproc.EntityTypeMappings.Single(m => m.EntityType == specialCustomerType).IsSharedTablePrincipal); + + var specialtyInsertParameter = + specialCustomerInsertSproc.Parameters.Single(c => c.Name == nameof(SpecialCustomer.Specialty)); + + Assert.False(specialtyInsertParameter.IsNullable); + + var specialtyProperty = specialtyInsertParameter.PropertyMappings.First().Property; + + Assert.Equal( + RelationalStrings.PropertyNotMappedToTable( + nameof(SpecialCustomer.Specialty), nameof(SpecialCustomer), "Customer_Insert"), + Assert.Throws( + () => + specialtyProperty.IsColumnNullable( + StoreObjectIdentifier.InsertStoredProcedure(customerInsertSproc.Name, customerInsertSproc.Schema))) + .Message); + + var abstractStringParameter = + specialCustomerInsertSproc.Parameters.Single(c => c.Name == nameof(AbstractCustomer.AbstractString)); + Assert.False(abstractStringParameter.IsNullable); + Assert.Equal(2, abstractStringParameter.PropertyMappings.Count); + + var abstractStringProperty = abstractStringParameter.PropertyMappings.First().Property; + Assert.Equal(2, abstractStringProperty.GetInsertStoredProcedureParameterMappings().Count()); + Assert.Equal( + new[] + { + StoreObjectIdentifier.InsertStoredProcedure(specialCustomerInsertSproc.Name, specialCustomerInsertSproc.Schema) + }, + abstractStringProperty.GetMappedStoreObjects(StoreObjectType.InsertStoredProcedure)); + + var extraSpecialCustomerInsertSproc = + extraSpecialCustomerType.GetInsertStoredProcedureMappings().Select(t => t.StoreStoredProcedure) + .First(t => t.Name == "ExtraSpecialCustomer_Insert"); + + var idPropertyInsertParameter = baseInsertSproc.FindParameter(idProperty)!; + var idPropertyInsertParameterMapping = idProperty.GetInsertStoredProcedureParameterMappings().First(); + Assert.Same(idPropertyInsertParameter, baseInsertSproc.FindParameter("InsertId")); + Assert.Same(idPropertyInsertParameter, idPropertyInsertParameterMapping.Column); + Assert.Equal("InsertId", idPropertyInsertParameter.Name); + Assert.Equal("default_int_mapping", idPropertyInsertParameter.StoreType); + Assert.False(idPropertyInsertParameter.IsNullable); + Assert.Same(baseInsertSproc, idPropertyInsertParameter.StoredProcedure); + Assert.Same(idPropertyInsertParameter.StoredProcedure, idPropertyInsertParameter.Table); + Assert.Same(idPropertyInsertParameterMapping, idPropertyInsertParameter.FindParameterMapping(abstractBaseType)); + + Assert.Equal(3, idProperty.GetInsertStoredProcedureResultColumnMappings().Count()); + Assert.Equal(7, idProperty.GetInsertStoredProcedureParameterMappings().Count()); + Assert.Equal( + new[] + { + StoreObjectIdentifier.InsertStoredProcedure(baseInsertSproc.Name, baseInsertSproc.Schema), + StoreObjectIdentifier.InsertStoredProcedure(customerInsertSproc.Name, customerInsertSproc.Schema), + StoreObjectIdentifier.InsertStoredProcedure(specialCustomerInsertSproc.Name, specialCustomerInsertSproc.Schema), + StoreObjectIdentifier.InsertStoredProcedure( + extraSpecialCustomerInsertSproc.Name, extraSpecialCustomerInsertSproc.Schema) + }, + idProperty.GetMappedStoreObjects(StoreObjectType.InsertStoredProcedure)); + + var extraSpecialCustomerUpdateSproc = + extraSpecialCustomerType.GetUpdateStoredProcedureMappings().Select(t => t.StoreStoredProcedure) + .First(t => t.Name == "ExtraSpecialCustomer_Update"); + + var idPropertyUpdateParameter = baseUpdateSproc.FindParameter(idProperty)!; + var idPropertyUpdateParameterMapping = idProperty.GetUpdateStoredProcedureParameterMappings().First(); + Assert.Same(idPropertyUpdateParameter, baseUpdateSproc.FindParameter("UpdateId")); + Assert.Same(idPropertyUpdateParameter, idPropertyUpdateParameterMapping.Parameter); + Assert.Equal("UpdateId", idPropertyUpdateParameter.Name); + Assert.Equal("default_int_mapping", idPropertyUpdateParameter.StoreType); + Assert.Equal(ParameterDirection.Input, idPropertyUpdateParameter.Direction); + Assert.False(idPropertyUpdateParameter.IsNullable); + Assert.Same(baseUpdateSproc, idPropertyUpdateParameter.StoredProcedure); + Assert.Same(idPropertyUpdateParameter.StoredProcedure, idPropertyUpdateParameter.Table); + Assert.Same(idPropertyUpdateParameterMapping, idPropertyUpdateParameter.FindParameterMapping(abstractBaseType)); + + Assert.Empty(idProperty.GetUpdateStoredProcedureResultColumnMappings()); + Assert.Equal(10, idProperty.GetUpdateStoredProcedureParameterMappings().Count()); + Assert.Equal( + new[] + { + StoreObjectIdentifier.UpdateStoredProcedure(baseUpdateSproc.Name, baseUpdateSproc.Schema), + StoreObjectIdentifier.UpdateStoredProcedure(customerUpdateSproc.Name, customerUpdateSproc.Schema), + StoreObjectIdentifier.UpdateStoredProcedure(specialCustomerUpdateSproc.Name, specialCustomerUpdateSproc.Schema), + StoreObjectIdentifier.UpdateStoredProcedure( + extraSpecialCustomerUpdateSproc.Name, extraSpecialCustomerUpdateSproc.Schema) + }, + idProperty.GetMappedStoreObjects(StoreObjectType.UpdateStoredProcedure)); + + var extraSpecialCustomerDeleteSproc = + extraSpecialCustomerType.GetDeleteStoredProcedureMappings().Select(t => t.StoreStoredProcedure) + .First(t => t.Name == "ExtraSpecialCustomer_Delete"); + + var idPropertyDeleteParameter = baseDeleteSproc.FindParameter(idProperty)!; + var idPropertyDeleteParameterMapping = idProperty.GetDeleteStoredProcedureParameterMappings().First(); + Assert.Same(idPropertyDeleteParameter, baseDeleteSproc.FindParameter("DeleteId")); + Assert.Same(idPropertyDeleteParameter, idPropertyDeleteParameterMapping.Parameter); + Assert.Equal("DeleteId", idPropertyDeleteParameter.Name); + Assert.Equal("default_int_mapping", idPropertyDeleteParameter.StoreType); + Assert.Equal(ParameterDirection.Input, idPropertyDeleteParameter.Direction); + Assert.False(idPropertyDeleteParameter.IsNullable); + Assert.Same(baseDeleteSproc, idPropertyDeleteParameter.StoredProcedure); + Assert.Same(idPropertyDeleteParameter.StoredProcedure, idPropertyDeleteParameter.Table); + Assert.Same(idPropertyDeleteParameterMapping, idPropertyDeleteParameter.FindParameterMapping(abstractBaseType)); + + Assert.Equal(10, idProperty.GetDeleteStoredProcedureParameterMappings().Count()); + Assert.Equal( + new[] + { + StoreObjectIdentifier.DeleteStoredProcedure(baseDeleteSproc.Name, baseDeleteSproc.Schema), + StoreObjectIdentifier.DeleteStoredProcedure(customerDeleteSproc.Name, customerDeleteSproc.Schema), + StoreObjectIdentifier.DeleteStoredProcedure(specialCustomerDeleteSproc.Name, specialCustomerDeleteSproc.Schema), + StoreObjectIdentifier.DeleteStoredProcedure( + extraSpecialCustomerDeleteSproc.Name, extraSpecialCustomerDeleteSproc.Schema) + }, + idProperty.GetMappedStoreObjects(StoreObjectType.DeleteStoredProcedure)); + } + else + { + var specialCustomerInsertMapping = specialCustomerType.GetInsertStoredProcedureMappings().Single(); + Assert.Null(specialCustomerInsertMapping.IsSplitEntityTypePrincipal); + + var specialtyParameter = specialCustomerInsertSproc.Parameters.Single(c => c.Name == nameof(SpecialCustomer.Specialty)); + + if (mapping == Mapping.TPH) + { + var baseInsertMapping = abstractBaseType.GetInsertStoredProcedureMappings().Single(); + Assert.True(baseInsertMapping.IncludesDerivedTypes); + Assert.Same(abstractBaseType.GetInsertStoredProcedure(), baseInsertMapping.StoredProcedure); + + Assert.Equal( + new[] { "Discriminator", "SpecialtyAk" }, + baseInsertMapping.ParameterMappings.Select(m => m.Property.Name)); + + Assert.Equal( + new[] { nameof(AbstractBase.Id) }, + baseInsertMapping.ResultColumnMappings.Select(m => m.Property.Name)); + Assert.Equal(baseInsertMapping.ResultColumnMappings, baseInsertMapping.ColumnMappings); + + var baseInsertSproc = baseInsertMapping.StoreStoredProcedure; + Assert.Equal("AbstractBase_Insert", baseInsertSproc.Name); + Assert.Same(baseInsertSproc, customerInsertSproc); + Assert.Same(baseInsertSproc, abstractBaseType.GetInsertStoredProcedureMappings().Single().StoreStoredProcedure); + Assert.Same(baseInsertSproc, abstractCustomerType.GetInsertStoredProcedureMappings().Single().StoreStoredProcedure); + Assert.Same(baseInsertSproc, specialCustomerType.GetInsertStoredProcedureMappings().Single().StoreStoredProcedure); + Assert.Same(baseInsertSproc, baseInsertMapping.Table); + Assert.Null(baseInsertSproc.Schema); + Assert.Equal( + new[] + { + nameof(AbstractBase), + nameof(AbstractCustomer), + nameof(Customer), + nameof(ExtraSpecialCustomer), + nameof(SpecialCustomer) + }, + baseInsertSproc.EntityTypeMappings.Select(m => m.EntityType.DisplayName())); + + Assert.Equal( + new[] + { + "Discriminator", + nameof(SpecialCustomer.Specialty), + nameof(SpecialCustomer.RelatedCustomerSpecialty), + "SpecialtyAk", + "AnotherRelatedCustomerId", + nameof(Customer.EnumValue), + nameof(Customer.Name), + nameof(Customer.SomeShort), + nameof(AbstractCustomer.AbstractString), + }, + baseInsertSproc.Parameters.Select(m => m.Name)); + + Assert.Equal(new[] { "InsertId" }, baseInsertSproc.ResultColumns.Select(m => m.Name)); + Assert.Equal(baseInsertSproc.ResultColumns, baseInsertSproc.Columns); + + Assert.True(specialCustomerInsertMapping.IncludesDerivedTypes); + Assert.Same(customerUpdateSproc, specialCustomerUpdateSproc); + + Assert.Equal(5, specialCustomerInsertSproc.EntityTypeMappings.Count()); + Assert.Null(specialCustomerInsertSproc.EntityTypeMappings.First().IsSharedTablePrincipal); + Assert.Null(specialCustomerInsertSproc.EntityTypeMappings.Last().IsSharedTablePrincipal); + + Assert.Single(specialCustomerInsertSproc.Columns); + Assert.Equal(9, specialCustomerInsertSproc.Parameters.Count()); + + var baseUpdateMapping = abstractBaseType.GetUpdateStoredProcedureMappings().Single(); + Assert.True(baseUpdateMapping.IncludesDerivedTypes); + Assert.Same(abstractBaseType.GetUpdateStoredProcedure(), baseUpdateMapping.StoredProcedure); + + Assert.Equal( + new[] { nameof(AbstractBase.Id) }, + baseUpdateMapping.ParameterMappings.Select(m => m.Property.Name)); + + Assert.Empty( + baseUpdateMapping.ResultColumnMappings.Select(m => m.Property.Name)); + Assert.Equal(baseUpdateMapping.ResultColumnMappings, baseUpdateMapping.ColumnMappings); + + var baseUpdateSproc = baseUpdateMapping.StoreStoredProcedure; + Assert.Equal("AbstractBase_Update", baseUpdateSproc.Name); + Assert.Same(baseUpdateSproc, customerUpdateSproc); + Assert.Same(baseUpdateSproc, abstractBaseType.GetUpdateStoredProcedureMappings().Single().StoreStoredProcedure); + Assert.Same(baseUpdateSproc, abstractCustomerType.GetUpdateStoredProcedureMappings().Single().StoreStoredProcedure); + Assert.Same(baseUpdateSproc, specialCustomerType.GetUpdateStoredProcedureMappings().Single().StoreStoredProcedure); + Assert.Same(baseUpdateSproc, baseUpdateMapping.Table); + Assert.Null(baseUpdateSproc.Schema); + Assert.Equal( + new[] + { + nameof(AbstractBase), + nameof(AbstractCustomer), + nameof(Customer), + nameof(ExtraSpecialCustomer), + nameof(SpecialCustomer) + }, + baseUpdateSproc.EntityTypeMappings.Select(m => m.EntityType.DisplayName())); + + Assert.Equal( + new[] + { + "UpdateId", + nameof(SpecialCustomer.Specialty), + nameof(SpecialCustomer.RelatedCustomerSpecialty), + "AnotherRelatedCustomerId", + nameof(Customer.EnumValue), + nameof(Customer.Name), + nameof(Customer.SomeShort), + nameof(AbstractCustomer.AbstractString), + }, + baseUpdateSproc.Parameters.Select(m => m.Name)); + + Assert.Empty(baseUpdateSproc.ResultColumns.Select(m => m.Name)); + Assert.Equal(baseUpdateSproc.ResultColumns, baseUpdateSproc.Columns); + + var baseDeleteMapping = abstractBaseType.GetDeleteStoredProcedureMappings().Single(); + Assert.True(baseDeleteMapping.IncludesDerivedTypes); + Assert.Same(abstractBaseType.GetDeleteStoredProcedure(), baseDeleteMapping.StoredProcedure); + + Assert.Equal( + new[] { nameof(AbstractBase.Id) }, + baseDeleteMapping.ParameterMappings.Select(m => m.Property.Name)); + + Assert.Empty( + baseDeleteMapping.ResultColumnMappings.Select(m => m.Property.Name)); + Assert.Equal(baseDeleteMapping.ResultColumnMappings, baseDeleteMapping.ColumnMappings); + + var baseDeleteSproc = baseDeleteMapping.StoreStoredProcedure; + Assert.Equal("AbstractBase_Delete", baseDeleteSproc.Name); + Assert.Same(baseDeleteSproc, customerDeleteSproc); + Assert.Same(baseDeleteSproc, abstractBaseType.GetDeleteStoredProcedureMappings().Single().StoreStoredProcedure); + Assert.Same(baseDeleteSproc, abstractCustomerType.GetDeleteStoredProcedureMappings().Single().StoreStoredProcedure); + Assert.Same(baseDeleteSproc, specialCustomerType.GetDeleteStoredProcedureMappings().Single().StoreStoredProcedure); + Assert.Same(baseDeleteSproc, baseDeleteMapping.Table); + Assert.Null(baseDeleteSproc.Schema); + Assert.Equal( + new[] + { + nameof(AbstractBase), + nameof(AbstractCustomer), + nameof(Customer), + nameof(ExtraSpecialCustomer), + nameof(SpecialCustomer) + }, + baseDeleteSproc.EntityTypeMappings.Select(m => m.EntityType.DisplayName())); + + Assert.Equal( + new[] { "DeleteId" }, + baseDeleteSproc.Parameters.Select(m => m.Name)); + + Assert.Empty(baseDeleteSproc.ResultColumns.Select(m => m.Name)); + Assert.Equal(baseDeleteSproc.ResultColumns, baseDeleteSproc.Columns); + + Assert.True(specialCustomerInsertMapping.IncludesDerivedTypes); + Assert.Same(customerInsertSproc, specialCustomerInsertSproc); + + Assert.Equal(5, specialCustomerInsertSproc.EntityTypeMappings.Count()); + Assert.Null(specialCustomerInsertSproc.EntityTypeMappings.First().IsSharedTablePrincipal); + Assert.Null(specialCustomerInsertSproc.EntityTypeMappings.Last().IsSharedTablePrincipal); + + Assert.Single(specialCustomerInsertSproc.Columns); + Assert.Equal(9, specialCustomerInsertSproc.Parameters.Count()); + + Assert.True(specialtyParameter.IsNullable); + + var abstractStringColumn = + specialCustomerInsertSproc.Parameters.Single(c => c.Name == nameof(AbstractCustomer.AbstractString)); + Assert.True(specialtyParameter.IsNullable); + Assert.Equal(2, specialtyParameter.PropertyMappings.Count); + + var abstractStringProperty = abstractStringColumn.PropertyMappings.First().Property; + Assert.Equal(3, abstractStringProperty.GetInsertStoredProcedureParameterMappings().Count()); + Assert.Equal( + new[] { StoreObjectIdentifier.InsertStoredProcedure(customerInsertSproc.Name, customerInsertSproc.Schema) }, + abstractStringProperty.GetMappedStoreObjects(StoreObjectType.InsertStoredProcedure)); + + var idPropertyInsertColumn = baseInsertSproc.FindResultColumn(idProperty)!; + var idPropertyInsertColumnMapping = idProperty.GetInsertStoredProcedureResultColumnMappings().First(); + Assert.Same(idPropertyInsertColumn, baseInsertSproc.FindResultColumn("InsertId")); + Assert.Same(idPropertyInsertColumn, idPropertyInsertColumnMapping.Column); + Assert.Equal("InsertId", idPropertyInsertColumn.Name); + Assert.Equal("default_int_mapping", idPropertyInsertColumn.StoreType); + Assert.False(idPropertyInsertColumn.IsNullable); + Assert.Same(baseInsertSproc, idPropertyInsertColumn.StoredProcedure); + Assert.Same(idPropertyInsertColumn.StoredProcedure, idPropertyInsertColumn.Table); + Assert.Same(idPropertyInsertColumnMapping, idPropertyInsertColumn.FindColumnMapping(abstractBaseType)); + + Assert.Empty(idProperty.GetInsertStoredProcedureParameterMappings()); + Assert.Equal(5, idProperty.GetInsertStoredProcedureResultColumnMappings().Count()); + Assert.Equal( + new[] { StoreObjectIdentifier.InsertStoredProcedure(customerInsertSproc.Name, customerInsertSproc.Schema) }, + idProperty.GetMappedStoreObjects(StoreObjectType.InsertStoredProcedure)); + + var idPropertyUpdateParameter = baseUpdateSproc.FindParameter(idProperty)!; + var idPropertyUpdateParameterMapping = idProperty.GetUpdateStoredProcedureParameterMappings().First(); + Assert.Same(idPropertyUpdateParameter, baseUpdateSproc.FindParameter("UpdateId")); + Assert.Same(idPropertyUpdateParameter, idPropertyUpdateParameterMapping.Parameter); + Assert.Equal("UpdateId", idPropertyUpdateParameter.Name); + Assert.Equal("default_int_mapping", idPropertyUpdateParameter.StoreType); + Assert.Equal(ParameterDirection.Input, idPropertyUpdateParameter.Direction); + Assert.False(idPropertyUpdateParameter.IsNullable); + Assert.Same(baseUpdateSproc, idPropertyUpdateParameter.StoredProcedure); + Assert.Same(idPropertyUpdateParameter.StoredProcedure, idPropertyUpdateParameter.Table); + Assert.Same(idPropertyUpdateParameterMapping, idPropertyUpdateParameter.FindParameterMapping(abstractBaseType)); + + Assert.Empty(idProperty.GetUpdateStoredProcedureResultColumnMappings()); + Assert.Equal(5, idProperty.GetUpdateStoredProcedureParameterMappings().Count()); + Assert.Equal( + new[] { StoreObjectIdentifier.UpdateStoredProcedure(customerUpdateSproc.Name, customerUpdateSproc.Schema) }, + idProperty.GetMappedStoreObjects(StoreObjectType.UpdateStoredProcedure)); + + var idPropertyDeleteParameter = baseDeleteSproc.FindParameter(idProperty)!; + var idPropertyDeleteParameterMapping = idProperty.GetDeleteStoredProcedureParameterMappings().First(); + Assert.Same(idPropertyDeleteParameter, baseDeleteSproc.FindParameter("DeleteId")); + Assert.Same(idPropertyDeleteParameter, idPropertyDeleteParameterMapping.Parameter); + Assert.Equal("DeleteId", idPropertyDeleteParameter.Name); + Assert.Equal("default_int_mapping", idPropertyDeleteParameter.StoreType); + Assert.Equal(ParameterDirection.Input, idPropertyDeleteParameter.Direction); + Assert.False(idPropertyDeleteParameter.IsNullable); + Assert.Same(baseDeleteSproc, idPropertyDeleteParameter.StoredProcedure); + Assert.Same(idPropertyDeleteParameter.StoredProcedure, idPropertyDeleteParameter.Table); + Assert.Same(idPropertyDeleteParameterMapping, idPropertyDeleteParameter.FindParameterMapping(abstractBaseType)); + + Assert.Equal(5, idProperty.GetDeleteStoredProcedureParameterMappings().Count()); + Assert.Equal( + new[] { StoreObjectIdentifier.DeleteStoredProcedure(customerDeleteSproc.Name, customerDeleteSproc.Schema) }, + idProperty.GetMappedStoreObjects(StoreObjectType.DeleteStoredProcedure)); + } + else + { + Assert.Null(abstractBaseType.GetInsertStoredProcedure()); + Assert.Null(abstractBaseType.GetUpdateStoredProcedure()); + Assert.Null(abstractBaseType.GetDeleteStoredProcedure()); + + Assert.Equal("Customer_Insert", customerInsertSproc.Name); + Assert.Null(abstractCustomerType.GetInsertStoredProcedure()); + Assert.Equal("SpecialCustomer_Insert", specialCustomerType.GetInsertStoredProcedure().Name); + + Assert.False(specialCustomerInsertMapping.IncludesDerivedTypes); + Assert.NotSame(customerInsertSproc, specialCustomerInsertSproc); + + Assert.Equal("Customer_Insert", customerInsertSproc.Name); + Assert.Empty(abstractCustomerType.GetInsertStoredProcedureMappings()); + Assert.Equal( + "SpecialCustomer_Insert", + specialCustomerType.GetInsertStoredProcedureMappings().Single().StoreStoredProcedure + .Name); + + Assert.Null(customerInsertSproc.Schema); + Assert.Equal( + new[] { nameof(Customer) }, + customerInsertSproc.EntityTypeMappings.Select(m => m.EntityType.DisplayName())); + Assert.Null(customerInsertSproc.EntityTypeMappings.Single().IsSharedTablePrincipal); + Assert.Null(customerInsertSproc.EntityTypeMappings.Single().IsSplitEntityTypePrincipal); + + Assert.Equal( + new[] { "InsertId", nameof(Customer.EnumValue), nameof(Customer.Name), nameof(Customer.SomeShort), "SpecialtyAk" }, + customerInsertSproc.Parameters.Select(m => m.Name)); + + Assert.Empty(customerInsertSproc.ResultColumns.Select(m => m.Name)); + + Assert.Equal("Customer_Update", customerUpdateSproc.Name); + Assert.Empty(abstractCustomerType.GetUpdateStoredProcedureMappings()); + Assert.Equal( + "SpecialCustomer_Update", + specialCustomerType.GetUpdateStoredProcedureMappings().Single().StoreStoredProcedure + .Name); + Assert.Null(customerUpdateSproc.Schema); + Assert.Equal( + new[] { nameof(Customer) }, + customerUpdateSproc.EntityTypeMappings.Select(m => m.EntityType.DisplayName())); + Assert.Null(customerUpdateSproc.EntityTypeMappings.Single().IsSharedTablePrincipal); + Assert.Null(customerUpdateSproc.EntityTypeMappings.Single().IsSplitEntityTypePrincipal); + + Assert.Equal( + new[] { "UpdateId", nameof(Customer.EnumValue), nameof(Customer.Name), nameof(Customer.SomeShort) }, + customerUpdateSproc.Parameters.Select(m => m.Name)); + + Assert.Empty(customerUpdateSproc.ResultColumns.Select(m => m.Name)); + + Assert.Equal("Customer_Delete", customerDeleteSproc.Name); + Assert.Empty(abstractCustomerType.GetDeleteStoredProcedureMappings()); + Assert.Equal( + "SpecialCustomer_Delete", + specialCustomerType.GetDeleteStoredProcedureMappings().Single().StoreStoredProcedure + .Name); + Assert.Null(customerDeleteSproc.Schema); + Assert.Equal( + new[] { nameof(Customer) }, + customerDeleteSproc.EntityTypeMappings.Select(m => m.EntityType.DisplayName())); + Assert.Null(customerDeleteSproc.EntityTypeMappings.Single().IsSharedTablePrincipal); + Assert.Null(customerDeleteSproc.EntityTypeMappings.Single().IsSplitEntityTypePrincipal); + + Assert.Equal( + new[] { "DeleteId" }, + customerDeleteSproc.Parameters.Select(m => m.Name)); + + Assert.Empty(customerDeleteSproc.ResultColumns.Select(m => m.Name)); + + Assert.Single(specialCustomerInsertSproc.EntityTypeMappings); + + var abstractStringInsertParameter = specialCustomerInsertSproc.Parameters + .Single(c => c.Name == nameof(AbstractCustomer.AbstractString)); + Assert.False(specialtyParameter.IsNullable); + + var extraSpecialCustomerInsertSproc = + extraSpecialCustomerType.GetInsertStoredProcedureMappings().Select(t => t.StoreStoredProcedure) + .First(t => t.Name == "ExtraSpecialCustomer_Insert"); + + Assert.Single(extraSpecialCustomerInsertSproc.EntityTypeMappings); + + var idPropertyInsertColumn = customerInsertSproc.FindParameter(idProperty)!; + var idPropertyInsertColumnMapping = idProperty.GetInsertStoredProcedureParameterMappings().First(); + Assert.Same(idPropertyInsertColumn, customerInsertSproc.FindParameter("InsertId")); + Assert.Same(idPropertyInsertColumn, idPropertyInsertColumnMapping.Column); + Assert.Equal("InsertId", idPropertyInsertColumn.Name); + Assert.Equal("default_int_mapping", idPropertyInsertColumn.StoreType); + Assert.False(idPropertyInsertColumn.IsNullable); + Assert.Same(customerInsertSproc, idPropertyInsertColumn.StoredProcedure); + Assert.Same(idPropertyInsertColumn.StoredProcedure, idPropertyInsertColumn.Table); + Assert.Same(idPropertyInsertColumnMapping, idPropertyInsertColumn.FindColumnMapping(abstractBaseType)); + + Assert.Empty(idProperty.GetInsertStoredProcedureResultColumnMappings()); + Assert.Equal(3, idProperty.GetInsertStoredProcedureParameterMappings().Count()); + Assert.Equal( + new[] + { + StoreObjectIdentifier.InsertStoredProcedure(customerInsertSproc.Name, customerInsertSproc.Schema), + StoreObjectIdentifier.InsertStoredProcedure( + specialCustomerInsertSproc.Name, specialCustomerInsertSproc.Schema), + StoreObjectIdentifier.InsertStoredProcedure( + extraSpecialCustomerInsertSproc.Name, extraSpecialCustomerInsertSproc.Schema) + }, + idProperty.GetMappedStoreObjects(StoreObjectType.InsertStoredProcedure)); + + var extraSpecialCustomerUpdateSproc = + extraSpecialCustomerType.GetUpdateStoredProcedureMappings().Select(t => t.StoreStoredProcedure) + .First(t => t.Name == "ExtraSpecialCustomer_Update"); + + Assert.Single(extraSpecialCustomerUpdateSproc.EntityTypeMappings); + + var idPropertyUpdateParameter = customerUpdateSproc.FindParameter(idProperty)!; + var idPropertyUpdateParameterMapping = idProperty.GetUpdateStoredProcedureParameterMappings().First(); + Assert.Same(idPropertyUpdateParameter, customerUpdateSproc.FindParameter("UpdateId")); + Assert.Same(idPropertyUpdateParameter, idPropertyUpdateParameterMapping.Parameter); + Assert.Equal("UpdateId", idPropertyUpdateParameter.Name); + Assert.Equal("default_int_mapping", idPropertyUpdateParameter.StoreType); + Assert.Equal(ParameterDirection.Input, idPropertyUpdateParameter.Direction); + Assert.False(idPropertyUpdateParameter.IsNullable); + Assert.Same(customerUpdateSproc, idPropertyUpdateParameter.StoredProcedure); + Assert.Same(idPropertyUpdateParameter.StoredProcedure, idPropertyUpdateParameter.Table); + Assert.Same(idPropertyUpdateParameterMapping, idPropertyUpdateParameter.FindParameterMapping(abstractBaseType)); + + Assert.Empty(idProperty.GetUpdateStoredProcedureResultColumnMappings()); + Assert.Equal(3, idProperty.GetUpdateStoredProcedureParameterMappings().Count()); + Assert.Equal( + new[] + { + StoreObjectIdentifier.UpdateStoredProcedure(customerUpdateSproc.Name, customerUpdateSproc.Schema), + StoreObjectIdentifier.UpdateStoredProcedure( + specialCustomerUpdateSproc.Name, specialCustomerUpdateSproc.Schema), + StoreObjectIdentifier.UpdateStoredProcedure( + extraSpecialCustomerUpdateSproc.Name, extraSpecialCustomerUpdateSproc.Schema) + }, + idProperty.GetMappedStoreObjects(StoreObjectType.UpdateStoredProcedure)); + + var extraSpecialCustomerDeleteSproc = + extraSpecialCustomerType.GetDeleteStoredProcedureMappings().Select(t => t.StoreStoredProcedure) + .First(t => t.Name == "ExtraSpecialCustomer_Delete"); + + Assert.Single(extraSpecialCustomerDeleteSproc.EntityTypeMappings); + + var idPropertyDeleteParameter = customerDeleteSproc.FindParameter(idProperty)!; + var idPropertyDeleteParameterMapping = idProperty.GetDeleteStoredProcedureParameterMappings().First(); + Assert.Same(idPropertyDeleteParameter, customerDeleteSproc.FindParameter("DeleteId")); + Assert.Same(idPropertyDeleteParameter, idPropertyDeleteParameterMapping.Parameter); + Assert.Equal("DeleteId", idPropertyDeleteParameter.Name); + Assert.Equal("default_int_mapping", idPropertyDeleteParameter.StoreType); + Assert.Equal(ParameterDirection.Input, idPropertyDeleteParameter.Direction); + Assert.False(idPropertyDeleteParameter.IsNullable); + Assert.Same(customerDeleteSproc, idPropertyDeleteParameter.StoredProcedure); + Assert.Same(idPropertyDeleteParameter.StoredProcedure, idPropertyDeleteParameter.Table); + Assert.Same(idPropertyDeleteParameterMapping, idPropertyDeleteParameter.FindParameterMapping(abstractBaseType)); + + Assert.Equal(3, idProperty.GetDeleteStoredProcedureParameterMappings().Count()); + Assert.Equal( + new[] + { + StoreObjectIdentifier.DeleteStoredProcedure(customerDeleteSproc.Name, customerDeleteSproc.Schema), + StoreObjectIdentifier.DeleteStoredProcedure( + specialCustomerDeleteSproc.Name, specialCustomerDeleteSproc.Schema), + StoreObjectIdentifier.DeleteStoredProcedure( + extraSpecialCustomerDeleteSproc.Name, extraSpecialCustomerDeleteSproc.Schema) + }, + idProperty.GetMappedStoreObjects(StoreObjectType.DeleteStoredProcedure)); + } + } + } + + private IRelationalModel CreateTestModel( + bool mapToTables = false, + bool mapToViews = false, + bool mapToSprocs = false, + Mapping mapping = Mapping.TPH) { var modelBuilder = CreateConventionModelBuilder(); @@ -1005,6 +1863,50 @@ private IRelationalModel CreateTestModel(bool mapToTables = false, bool mapToVie { cb.ToTable(_ => { }); } + + if (mapToSprocs) + { + if (mapping == Mapping.TPH) + { + cb + .InsertUsingStoredProcedure( + s => s + .HasResultColumn(b => b.Id, p => p.HasName("InsertId")) + .HasParameter("Discriminator") + .HasParameter((SpecialCustomer c) => c.Specialty) + .HasParameter((SpecialCustomer c) => c.RelatedCustomerSpecialty) + .HasParameter("SpecialtyAk") + .HasParameter("AnotherRelatedCustomerId") + .HasParameter((Customer c) => c.EnumValue) + .HasParameter((Customer c) => c.Name) + .HasParameter((Customer c) => c.SomeShort) + .HasParameter((AbstractCustomer c) => c.AbstractString)) + .UpdateUsingStoredProcedure( + s => s.HasParameter(b => b.Id, p => p.HasName("UpdateId")) + .HasParameter((SpecialCustomer c) => c.Specialty) + .HasParameter((SpecialCustomer c) => c.RelatedCustomerSpecialty) + .HasParameter("AnotherRelatedCustomerId") + .HasParameter((Customer c) => c.EnumValue) + .HasParameter((Customer c) => c.Name) + .HasParameter((Customer c) => c.SomeShort) + .HasParameter((AbstractCustomer c) => c.AbstractString)) + .DeleteUsingStoredProcedure( + s => s.HasParameter(b => b.Id, p => p.HasName("DeleteId"))); + } + else + { + cb + .InsertUsingStoredProcedure( + s => s + .HasParameter(b => b.Id, p => p.IsOutput().HasName("InsertId")) + .HasParameter("SpecialtyAk")) + .UpdateUsingStoredProcedure( + s => s.HasParameter(b => b.Id, p => p.HasName("UpdateId")) + .HasParameter("SpecialtyAk")) + .DeleteUsingStoredProcedure( + s => s.HasParameter(b => b.Id, p => p.HasName("DeleteId"))); + } + } } if (mapping == Mapping.TPC) @@ -1036,6 +1938,35 @@ private IRelationalModel CreateTestModel(bool mapToTables = false, bool mapToVie { cb.ToTable("Customer"); } + + if (mapToSprocs) + { + cb + .InsertUsingStoredProcedure( + s => s + .HasParameter(c => c.Id, p => p.HasName("InsertId")) + .HasParameter(c => c.EnumValue) + .HasParameter(c => c.Name) + .HasParameter(c => c.SomeShort)) + .UpdateUsingStoredProcedure( + s => s + .HasParameter(b => b.Id, p => p.HasName("UpdateId")) + .HasParameter(c => c.EnumValue) + .HasParameter(c => c.Name) + .HasParameter(c => c.SomeShort)) + .DeleteUsingStoredProcedure( + s => s.HasParameter(b => b.Id, p => p.HasName("DeleteId"))); + + if (mapping == Mapping.TPC) + { + cb.InsertUsingStoredProcedure(s => s.HasParameter("SpecialtyAk")); + } + else + { + cb.InsertUsingStoredProcedure( + s => s.HasParameter(c => c.Id, p => p.IsOutput())); + } + } } }); @@ -1064,6 +1995,56 @@ private IRelationalModel CreateTestModel(bool mapToTables = false, bool mapToVie cb.ToTable("SpecialCustomer", "SpecialSchema"); } } + + if (mapToSprocs) + { + if (mapping == Mapping.TPC) + { + cb + .InsertUsingStoredProcedure( + s => s + .HasParameter(b => b.Id, p => p.HasName("InsertId")) + .HasParameter(c => c.Specialty) + .HasParameter(c => c.RelatedCustomerSpecialty) + .HasParameter("AnotherRelatedCustomerId") + .HasParameter("SpecialtyAk") + .HasParameter(c => c.EnumValue) + .HasParameter(c => c.Name) + .HasParameter(c => c.SomeShort) + .HasParameter(c => c.AbstractString)) + .UpdateUsingStoredProcedure( + s => s.HasParameter(b => b.Id, p => p.HasName("UpdateId")) + .HasParameter(c => c.Specialty) + .HasParameter(c => c.RelatedCustomerSpecialty) + .HasParameter("AnotherRelatedCustomerId") + .HasParameter(c => c.EnumValue) + .HasParameter(c => c.Name) + .HasParameter(c => c.SomeShort) + .HasParameter(c => c.AbstractString)) + .DeleteUsingStoredProcedure( + s => s.HasParameter(b => b.Id, p => p.HasName("DeleteId"))); + } + else if (mapping == Mapping.TPT) + { + cb + .InsertUsingStoredProcedure( + s => s + .HasResultColumn(b => b.Id, p => p.HasName("InsertId")) + .HasParameter(c => c.Specialty) + .HasParameter(c => c.RelatedCustomerSpecialty) + .HasParameter("AnotherRelatedCustomerId") + .HasParameter(c => c.AbstractString)) + .UpdateUsingStoredProcedure( + s => s.HasParameter(b => b.Id, p => p.HasName("UpdateId")) + .HasParameter(c => c.Specialty) + .HasParameter(c => c.RelatedCustomerSpecialty) + .HasParameter("AnotherRelatedCustomerId") + .HasParameter(c => c.AbstractString)) + .DeleteUsingStoredProcedure( + s => s.HasParameter(b => b.Id, p => p.HasName("DeleteId"))); + } + } + cb.HasCheckConstraint("Specialty", "[Specialty] IN ('Specialist', 'Generalist')"); cb.Property(s => s.Specialty).IsRequired(); @@ -1096,6 +2077,22 @@ private IRelationalModel CreateTestModel(bool mapToTables = false, bool mapToVie cb.OwnsOne(c => c.Details, cdb => cdb.ToTable("SpecialCustomer", "SpecialSchema")); } } + + if (mapToSprocs) + { + cb.OwnsOne( + c => c.Details, cdb => cdb + .InsertUsingStoredProcedure("CustomerDetailsInsert", s => s + .HasParameter("SpecialCustomerId") + .HasParameter(b => b.BirthDay) + .HasParameter(b => b.Address)) + .UpdateUsingStoredProcedure("CustomerDetailsUpdate", s => s + .HasParameter("SpecialCustomerId") + .HasParameter(b => b.BirthDay) + .HasParameter(b => b.Address)) + .DeleteUsingStoredProcedure("CustomerDetailsDelete", s => s + .HasParameter("SpecialCustomerId"))); + } } }); @@ -1113,6 +2110,43 @@ private IRelationalModel CreateTestModel(bool mapToTables = false, bool mapToVie { cb.ToTable("ExtraSpecialCustomer", "ExtraSpecialSchema"); } + + if (mapToSprocs) + { + if (mapping == Mapping.TPC) + { + cb + .InsertUsingStoredProcedure( + s => s + .HasParameter(b => b.Id, p => p.HasName("InsertId")) + .HasParameter(c => c.Specialty) + .HasParameter(c => c.RelatedCustomerSpecialty) + .HasParameter("AnotherRelatedCustomerId") + .HasParameter("SpecialtyAk") + .HasParameter(c => c.EnumValue) + .HasParameter(c => c.Name) + .HasParameter(c => c.SomeShort) + .HasParameter(c => c.AbstractString)) + .UpdateUsingStoredProcedure( + s => s.HasParameter(b => b.Id, p => p.HasName("UpdateId")) + .HasParameter(c => c.Specialty) + .HasParameter(c => c.RelatedCustomerSpecialty) + .HasParameter("AnotherRelatedCustomerId") + .HasParameter(c => c.EnumValue) + .HasParameter(c => c.Name) + .HasParameter(c => c.SomeShort) + .HasParameter(c => c.AbstractString)) + .DeleteUsingStoredProcedure( + s => s.HasParameter(b => b.Id, p => p.HasName("DeleteId"))); + } + else if (mapping == Mapping.TPT) + { + cb + .InsertUsingStoredProcedure(s => s.HasResultColumn(b => b.Id)) + .UpdateUsingStoredProcedure(s => s.HasParameter(b => b.Id)) + .DeleteUsingStoredProcedure(s => s.HasParameter(b => b.Id)); + } + } } if (mapping == Mapping.TPC) @@ -1129,6 +2163,25 @@ private IRelationalModel CreateTestModel(bool mapToTables = false, bool mapToVie { cb.OwnsOne(c => c.Details, cdb => cdb.ToTable("ExtraSpecialCustomer", "ExtraSpecialSchema")); } + + if (mapToSprocs) + { + cb.OwnsOne( + c => c.Details, cdb => cdb + .InsertUsingStoredProcedure( + "CustomerDetailsInsert", s => s + .HasParameter("ExtraSpecialCustomerId") + .HasParameter(b => b.BirthDay) + .HasParameter(b => b.Address)) + .UpdateUsingStoredProcedure( + "CustomerDetailsUpdate", s => s + .HasParameter("ExtraSpecialCustomerId") + .HasParameter(b => b.BirthDay) + .HasParameter(b => b.Address)) + .DeleteUsingStoredProcedure( + "CustomerDetailsDelete", s => s + .HasParameter("ExtraSpecialCustomerId"))); + } } }); @@ -1143,10 +2196,25 @@ private IRelationalModel CreateTestModel(bool mapToTables = false, bool mapToVie .HasForeignKey(o => o.OrderDate).HasPrincipalKey(o => o.Date) .HasConstraintName("FK_DateDetails"); - // Note: the below is resetting the name of the anonymous index - // created in HasForeignKey() above, not creating a new index. ob.HasIndex(o => o.OrderDate).HasDatabaseName("IX_OrderDate"); + if (mapToSprocs) + { + ob + .InsertUsingStoredProcedure( + s => s + .HasResultColumn(c => c.Id) + .HasParameter(c => c.AlternateId) + .HasParameter(c => c.CustomerId) + .HasParameter(c => c.OrderDate)) + .UpdateUsingStoredProcedure( + s => s + .HasParameter(c => c.Id) + .HasParameter(c => c.CustomerId) + .HasParameter(c => c.OrderDate)) + .DeleteUsingStoredProcedure(s => s.HasParameter(b => b.Id)); + } + ob.OwnsOne( o => o.Details, odb => { @@ -1159,8 +2227,61 @@ private IRelationalModel CreateTestModel(bool mapToTables = false, bool mapToVie odb.HasOne(od => od.DateDetails).WithOne() .HasForeignKey(o => o.OrderDate).HasPrincipalKey(o => o.Date); - odb.OwnsOne(od => od.BillingAddress); - odb.OwnsOne(od => od.ShippingAddress); + if (mapToSprocs) + { + odb + .InsertUsingStoredProcedure( + "OrderDetails_Insert", s => s + .HasParameter(c => c.OrderId) + .HasParameter(c => c.AlternateId) + .HasParameter(c => c.Active) + .HasParameter(c => c.OrderDate)) + .UpdateUsingStoredProcedure( + "OrderDetails_Update", s => s + .HasParameter(c => c.OrderId) + .HasParameter(c => c.Active) + .HasParameter(c => c.OrderDate)) + .DeleteUsingStoredProcedure( + "OrderDetails_Delete", s => s + .HasParameter(b => b.OrderId)); + + odb.OwnsOne( + od => od.BillingAddress, bab => bab + .InsertUsingStoredProcedure( + "BillingAddress_Insert", s => s + .HasParameter(c => c.City) + .HasParameter(c => c.Street) + .HasParameter("OrderDetailsOrderId")) + .UpdateUsingStoredProcedure( + "BillingAddress_Update", s => s + .HasParameter(c => c.City) + .HasParameter(c => c.Street) + .HasParameter("OrderDetailsOrderId")) + .DeleteUsingStoredProcedure( + "BillingAddress_Delete", s => s + .HasParameter("OrderDetailsOrderId"))); + + odb.OwnsOne( + od => od.ShippingAddress, sab => sab + .InsertUsingStoredProcedure( + "ShippingAddress_Insert", s => s + .HasParameter("OrderDetailsOrderId") + .HasParameter(c => c.City) + .HasParameter(c => c.Street)) + .UpdateUsingStoredProcedure( + "ShippingAddress_Update", s => s + .HasParameter("OrderDetailsOrderId") + .HasParameter(c => c.City) + .HasParameter(c => c.Street)) + .DeleteUsingStoredProcedure( + "ShippingAddress_Delete", s => s + .HasParameter("OrderDetailsOrderId"))); + } + else + { + odb.OwnsOne(od => od.BillingAddress); + odb.OwnsOne(od => od.ShippingAddress); + } odb.Navigation(od => od.BillingAddress).IsRequired(); odb.Navigation(od => od.ShippingAddress).IsRequired(); }); diff --git a/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderTest.cs b/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderTest.cs index df861550b26..ba7efa3b850 100644 --- a/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderTest.cs +++ b/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderTest.cs @@ -14,6 +14,8 @@ public abstract class RelationalNonRelationshipTestBase : NonRelationshipTestBas public virtual void Can_use_table_splitting() { var modelBuilder = CreateModelBuilder(); + modelBuilder.HasDefaultSchema("dbo"); + modelBuilder.Entity().SplitToTable( "OrderDetails", s => { @@ -40,15 +42,15 @@ public virtual void Can_use_table_splitting() 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"))); + Assert.False(entity.IsTableExcludedFromMigrations(StoreObjectIdentifier.Table("Order", "dbo"))); + Assert.True(entity.IsTableExcludedFromMigrations(StoreObjectIdentifier.Table("OrderDetails", "dbo"))); + Assert.Same(entity.GetMappingFragments().Single(), entity.FindMappingFragment(StoreObjectIdentifier.Table("OrderDetails", "dbo"))); 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"))); + Assert.Null(customerId.GetColumnName(StoreObjectIdentifier.Table("Order", "dbo"))); + Assert.Equal("id", customerId.GetColumnName(StoreObjectIdentifier.Table("OrderDetails", "dbo"))); + Assert.Same(customerId.GetOverrides().Single(), customerId.FindOverrides(StoreObjectIdentifier.Table("OrderDetails", "dbo"))); } [ConditionalFact] @@ -201,6 +203,7 @@ public virtual void Can_use_table_splitting() public virtual void Sproc_overrides_update_when_renamed_in_TPH() { var modelBuilder = CreateModelBuilder(); + modelBuilder.HasDefaultSchema("mySchema"); modelBuilder.Ignore(); modelBuilder.Ignore(); @@ -246,9 +249,13 @@ public virtual void Sproc_overrides_update_when_renamed_in_TPH() var bookLabel = model.FindEntityType(typeof(BookLabel))!; var insertSproc = bookLabel.GetInsertStoredProcedure()!; Assert.Equal("Insert", insertSproc.Name); - Assert.Null(insertSproc.Schema); + Assert.Equal("mySchema", insertSproc.Schema); Assert.Equal(new[] { "BookId", "Discriminator" }, insertSproc.Parameters); Assert.Equal(new[] { "Id" }, insertSproc.ResultColumns); + Assert.True(insertSproc.ContainsParameter("Discriminator")); + Assert.False(insertSproc.ContainsParameter("Id")); + Assert.False(insertSproc.ContainsResultColumn("Discriminator")); + Assert.True(insertSproc.ContainsResultColumn("Id")); Assert.True(insertSproc.AreTransactionsSuppressed); Assert.Equal("bar1", insertSproc["foo"]); Assert.Same(bookLabel, insertSproc.EntityType); @@ -264,7 +271,7 @@ public virtual void Sproc_overrides_update_when_renamed_in_TPH() var deleteSproc = bookLabel.GetDeleteStoredProcedure()!; Assert.Equal("BookLabel_Delete", deleteSproc.Name); - Assert.Null(deleteSproc.Schema); + Assert.Equal("mySchema", deleteSproc.Schema); Assert.Equal(new[] { "Id" }, deleteSproc.Parameters); Assert.Empty(deleteSproc.ResultColumns); Assert.True(deleteSproc.AreTransactionsSuppressed); @@ -772,6 +779,10 @@ public abstract class TestSplitTableBuilder public abstract TestColumnBuilder Property(string propertyName); public abstract TestColumnBuilder Property(Expression> propertyExpression); + + public abstract TestSplitTableBuilder HasAnnotation( + string annotation, + object? value); } public class GenericTestSplitTableBuilder : TestSplitTableBuilder, IInfrastructure> @@ -807,6 +818,9 @@ public override TestColumnBuilder Property(string property public override TestColumnBuilder Property(Expression> propertyExpression) => new GenericTestColumnBuilder(TableBuilder.Property(propertyExpression)); + + public override TestSplitTableBuilder HasAnnotation(string annotation, object? value) + => Wrap(TableBuilder.HasAnnotation(annotation, value)); } public class NonGenericTestSplitTableBuilder : TestSplitTableBuilder, IInfrastructure @@ -842,6 +856,9 @@ public override TestColumnBuilder Property(string property public override TestColumnBuilder Property(Expression> propertyExpression) => new NonGenericTestColumnBuilder(TableBuilder.Property(propertyExpression.GetPropertyAccess().Name)); + + public override TestSplitTableBuilder HasAnnotation(string annotation, object? value) + => Wrap(TableBuilder.HasAnnotation(annotation, value)); } public abstract class TestOwnedNavigationSplitTableBuilder @@ -857,6 +874,10 @@ public abstract class TestOwnedNavigationSplitTableBuilder Property(string propertyName); public abstract TestColumnBuilder Property(Expression> propertyExpression); + + public abstract TestOwnedNavigationSplitTableBuilder HasAnnotation( + string annotation, + object? value); } public class GenericTestOwnedNavigationSplitTableBuilder : @@ -894,6 +915,9 @@ public override TestColumnBuilder Property(string property public override TestColumnBuilder Property(Expression> propertyExpression) => new GenericTestColumnBuilder(TableBuilder.Property(propertyExpression)); + + public override TestOwnedNavigationSplitTableBuilder HasAnnotation(string annotation, object? value) + => Wrap(TableBuilder.HasAnnotation(annotation, value)); } public class NonGenericTestOwnedNavigationSplitTableBuilder : @@ -930,11 +954,18 @@ public override TestColumnBuilder Property(string property public override TestColumnBuilder Property(Expression> propertyExpression) => new GenericTestColumnBuilder(TableBuilder.Property(propertyExpression.GetPropertyAccess().Name)); + + public override TestOwnedNavigationSplitTableBuilder HasAnnotation(string annotation, object? value) + => Wrap(TableBuilder.HasAnnotation(annotation, value)); } public abstract class TestColumnBuilder { public abstract TestColumnBuilder HasColumnName(string? name); + + public abstract TestColumnBuilder HasAnnotation( + string annotation, + object? value); } public class GenericTestColumnBuilder : TestColumnBuilder, IInfrastructure> @@ -954,6 +985,11 @@ protected virtual TestColumnBuilder Wrap(ColumnBuilder col public override TestColumnBuilder HasColumnName(string? name) => Wrap(ColumnBuilder.HasColumnName(name)); + + public override TestColumnBuilder HasAnnotation( + string annotation, + object? value) + => Wrap(ColumnBuilder.HasAnnotation(annotation, value)); } public class NonGenericTestColumnBuilder : TestColumnBuilder, IInfrastructure @@ -973,6 +1009,11 @@ protected virtual TestColumnBuilder Wrap(ColumnBuilder tableBuilder) public override TestColumnBuilder HasColumnName(string? name) => Wrap(ColumnBuilder.HasColumnName(name)); + + public override TestColumnBuilder HasAnnotation( + string annotation, + object? value) + => Wrap(ColumnBuilder.HasAnnotation(annotation, value)); } public abstract class TestViewBuilder @@ -1137,6 +1178,10 @@ public abstract class TestSplitViewBuilder public abstract TestViewColumnBuilder Property(string propertyName); public abstract TestViewColumnBuilder Property(Expression> propertyExpression); + + public abstract TestSplitViewBuilder HasAnnotation( + string annotation, + object? value); } public class GenericTestSplitViewBuilder : TestSplitViewBuilder, IInfrastructure> @@ -1166,6 +1211,9 @@ public override TestViewColumnBuilder Property(string prop public override TestViewColumnBuilder Property(Expression> propertyExpression) => new GenericTestViewColumnBuilder(ViewBuilder.Property(propertyExpression)); + + public override TestSplitViewBuilder HasAnnotation(string annotation, object? value) + => Wrap(ViewBuilder.HasAnnotation(annotation, value)); } public class NonGenericTestSplitViewBuilder : TestSplitViewBuilder, IInfrastructure @@ -1195,6 +1243,9 @@ public override TestViewColumnBuilder Property(string prop public override TestViewColumnBuilder Property(Expression> propertyExpression) => new NonGenericTestViewColumnBuilder(ViewBuilder.Property(propertyExpression.GetPropertyAccess().Name)); + + public override TestSplitViewBuilder HasAnnotation(string annotation, object? value) + => Wrap(ViewBuilder.HasAnnotation(annotation, value)); } public abstract class TestOwnedNavigationSplitViewBuilder @@ -1209,6 +1260,10 @@ public abstract class TestOwnedNavigationSplitViewBuilder Property( Expression> propertyExpression); + + public abstract TestOwnedNavigationSplitViewBuilder HasAnnotation( + string annotation, + object? value); } public class GenericTestOwnedNavigationSplitViewBuilder : @@ -1244,6 +1299,9 @@ public override TestViewColumnBuilder Property(string prop public override TestViewColumnBuilder Property( Expression> propertyExpression) => new GenericTestViewColumnBuilder(ViewBuilder.Property(propertyExpression.GetPropertyAccess().Name)); + + public override TestOwnedNavigationSplitViewBuilder HasAnnotation(string annotation, object? value) + => Wrap(ViewBuilder.HasAnnotation(annotation, value)); } public class NonGenericTestOwnedNavigationSplitViewBuilder : @@ -1278,11 +1336,18 @@ public override TestViewColumnBuilder Property(string prop public override TestViewColumnBuilder Property( Expression> propertyExpression) => new GenericTestViewColumnBuilder(ViewBuilder.Property(propertyExpression.GetPropertyAccess().Name)); + + public override TestOwnedNavigationSplitViewBuilder HasAnnotation(string annotation, object? value) + => Wrap(ViewBuilder.HasAnnotation(annotation, value)); } public abstract class TestViewColumnBuilder { public abstract TestViewColumnBuilder HasColumnName(string? name); + + public abstract TestViewColumnBuilder HasAnnotation( + string annotation, + object? value); } public class GenericTestViewColumnBuilder : TestViewColumnBuilder, IInfrastructure> @@ -1302,6 +1367,11 @@ protected virtual TestViewColumnBuilder Wrap(ViewColumnBuilder HasColumnName(string? name) => Wrap(ViewColumnBuilder.HasColumnName(name)); + + public override TestViewColumnBuilder HasAnnotation( + string annotation, + object? value) + => Wrap(ViewColumnBuilder.HasAnnotation(annotation, value)); } public class NonGenericTestViewColumnBuilder @@ -1322,6 +1392,11 @@ protected virtual TestViewColumnBuilder Wrap(ViewColumnBuilder viewCo public override TestViewColumnBuilder HasColumnName(string? name) => Wrap(ViewColumnBuilder.HasColumnName(name)); + + public override TestViewColumnBuilder HasAnnotation( + string annotation, + object? value) + => Wrap(ViewColumnBuilder.HasAnnotation(annotation, value)); } public abstract class TestStoredProcedureBuilder @@ -1737,12 +1812,21 @@ StoredProcedureParameterBuilder IInfrastructure => StoredProcedureParameterBuilder; protected virtual TestStoredProcedureParameterBuilder Wrap(StoredProcedureParameterBuilder storedProcedureParameterBuilder) - => new TestStoredProcedureParameterBuilder(storedProcedureParameterBuilder); + => new(storedProcedureParameterBuilder); - public TestStoredProcedureParameterBuilder HasName(string? name) + public virtual TestStoredProcedureParameterBuilder HasName(string? name) => Wrap(StoredProcedureParameterBuilder.HasName(name)); - //public TestStoredProcedureParameterBuilder IsOutput(bool isOutput = true); + public virtual TestStoredProcedureParameterBuilder IsOutput() + => Wrap(StoredProcedureParameterBuilder.IsOutput()); + + public virtual TestStoredProcedureParameterBuilder IsInputOutput() + => Wrap(StoredProcedureParameterBuilder.IsInputOutput()); + + public virtual TestStoredProcedureParameterBuilder HasAnnotation( + string annotation, + object? value) + => Wrap(StoredProcedureParameterBuilder.HasAnnotation(annotation, value)); } public class TestStoredProcedureResultColumnBuilder : IInfrastructure @@ -1760,37 +1844,33 @@ StoredProcedureResultColumnBuilder IInfrastructure new TestStoredProcedureResultColumnBuilder(storedProcedureResultColumnBuilder); - public TestStoredProcedureResultColumnBuilder HasName(string? name) + public virtual TestStoredProcedureResultColumnBuilder HasName(string? name) => Wrap(StoredProcedureResultColumnBuilder.HasName(name)); + + public virtual TestStoredProcedureResultColumnBuilder HasAnnotation( + string annotation, + object? value) + => Wrap(StoredProcedureResultColumnBuilder.HasAnnotation(annotation, value)); } - //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 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 { diff --git a/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs b/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs index 5571e43a974..560891d0d3d 100644 --- a/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs +++ b/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs @@ -7,6 +7,24 @@ namespace Microsoft.EntityFrameworkCore.ModelBuilding; public static class RelationalTestModelBuilderExtensions { + public static ModelBuilderTest.TestModelBuilder HasDefaultSchema( + this ModelBuilderTest.TestModelBuilder modelBuilder, + string? schema) + { + modelBuilder.GetInfrastructure().HasDefaultSchema(schema); + + return modelBuilder; + } + + public static ModelBuilderTest.TestModelBuilder UseCollation( + this ModelBuilderTest.TestModelBuilder modelBuilder, + string? collation) + { + modelBuilder.GetInfrastructure().UseCollation(collation); + + return modelBuilder; + } + public static ModelBuilderTest.TestPropertyBuilder HasColumnName( this ModelBuilderTest.TestPropertyBuilder builder, string? name) @@ -758,6 +776,15 @@ public static ModelBuilderTest.TestOwnedNavigationBuilder ToFunction( + this ModelBuilderTest.TestEntityTypeBuilder builder, + string name, + Action> buildAction) + where TEntity : class + { + return builder; + } + public static ModelBuilderTest.TestEntityTypeBuilder UpdateUsingStoredProcedure( this ModelBuilderTest.TestEntityTypeBuilder builder, Action> buildAction) diff --git a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs index 36d578920d2..b5e96b531d1 100644 --- a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs +++ b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs @@ -47,7 +47,7 @@ public override void Passes_for_ForeignKey_on_inherited_generated_composite_key_ .Metadata.SetValueGenerationStrategy(SqlServerValueGenerationStrategy.None); modelBuilder.Entity().HasAlternateKey("SomeId", "SomeOtherId"); modelBuilder.Entity>().HasOne().WithOne().HasForeignKey>("SomeId"); - modelBuilder.Entity>(); + modelBuilder.Entity>().Metadata.SetDiscriminatorValue("GenericString"); Validate(modelBuilder); } diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs index 8c98a8f88b5..2fa187fea19 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs @@ -970,7 +970,7 @@ public virtual void Detects_ForeignKey_on_inherited_generated_key_property() modelBuilder.Entity().Property("SomeId").ValueGeneratedOnAdd(); modelBuilder.Entity().HasAlternateKey("SomeId"); modelBuilder.Entity>().HasOne().WithOne().HasForeignKey>("SomeId"); - modelBuilder.Entity>(); + modelBuilder.Entity>().Metadata.SetDiscriminatorValue("GenericString"); VerifyError( CoreStrings.ForeignKeyPropertyInKey( diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs index 5cffaf9c89e..bd7ee7ac3d5 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs @@ -102,7 +102,7 @@ protected abstract TestModelBuilder CreateTestModelBuilder( Action? configure); } - public abstract class TestModelBuilder + public abstract class TestModelBuilder : IInfrastructure { protected TestModelBuilder(TestHelpers testHelpers, Action? configure) { @@ -133,7 +133,7 @@ protected TestModelBuilder(TestHelpers testHelpers, Action ModelBuilder.Model; - public TestHelpers.TestModelBuilder ModelBuilder { get; } + protected TestHelpers.TestModelBuilder ModelBuilder { get; } public ListLoggerFactory ValidationLoggerFactory { get; } public ListLoggerFactory ModelLoggerFactory { get; } protected virtual DiagnosticsLogger ValidationLogger { get; } @@ -174,6 +174,9 @@ public virtual TestModelBuilder UsePropertyAccessMode(PropertyAccessMode propert return this; } + + ModelBuilder IInfrastructure.Instance + => ModelBuilder; } public abstract class TestEntityTypeBuilder