From 13d404ff6919e55c3b917fb3a2fcf85becc9a167 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Tue, 19 Jul 2022 11:11:38 -0700 Subject: [PATCH] Add sproc support for Output and InputOutput parameters Add sproc support to runtime and compiled models Add sproc support to relational model Fixed property overrides not taking the default schema into account Changed EntityType.ShortName() to not include the generic argument Part of #245 --- .../Design/Internal/CSharpHelper.cs | 3 +- .../Design/AnnotationCodeGenerator.cs | 3 +- ...nalCSharpRuntimeAnnotationCodeGenerator.cs | 105 +- ...onalEntityTypeBuilderExtensions.ToTable.cs | 38 +- ...ionalEntityTypeBuilderExtensions.ToView.cs | 22 +- ...nalEntityTypeBuilderExtensions.UseSproc.cs | 10 +- .../RelationalEntityTypeExtensions.cs | 46 +- .../RelationalPropertyBuilderExtensions.cs | 49 + .../RelationalPropertyExtensions.cs | 118 ++ .../RelationalModelValidator.cs | 99 +- .../OwnedNavigationSplitViewBuilder.cs | 16 + .../OwnedNavigationSplitViewBuilder``.cs | 11 + .../OwnedNavigationStoredProcedureBuilder.cs | 4 +- ...OwnedNavigationStoredProcedureBuilder``.cs | 4 +- .../Metadata/Builders/SplitViewBuilder.cs | 16 + .../Metadata/Builders/SplitViewBuilder`.cs | 10 + .../Builders/StoredProcedureBuilder.cs | 4 +- .../Builders/StoredProcedureBuilder`.cs | 4 +- .../StoredProcedureParameterBuilder.cs | 31 + .../Metadata/Builders/ViewColumnBuilder.cs | 17 + .../Metadata/Builders/ViewColumnBuilder`.cs | 11 + .../RelationalRuntimeModelConvention.cs | 67 +- src/EFCore.Relational/Metadata/IColumn.cs | 15 +- src/EFCore.Relational/Metadata/IColumnBase.cs | 19 + .../IConventionRelationalPropertyOverrides.cs | 18 + .../Metadata/IConventionStoredProcedure.cs | 12 +- .../Metadata/IFunctionColumn.cs | 8 + .../Metadata/IFunctionMapping.cs | 4 +- .../IMutableRelationalPropertyOverrides.cs | 11 + .../Metadata/IMutableStoredProcedure.cs | 8 +- .../IReadOnlyRelationalPropertyOverrides.cs | 7 + .../Metadata/IReadOnlyStoredProcedure.cs | 30 + .../Metadata/IRelationalAnnotationProvider.cs | 24 + .../Metadata/IRelationalModel.cs | 15 +- .../Metadata/ISqlQueryColumn.cs | 8 + .../Metadata/IStoreFunction.cs | 2 +- .../Metadata/IStoreStoredProcedure.cs | 134 ++ .../IStoreStoredProcedureParameter.cs | 79 ++ .../IStoreStoredProcedureResultColumn.cs | 68 + .../Metadata/IStoredProcedure.cs | 25 +- .../Metadata/IStoredProcedureMapping.cs | 81 ++ .../IStoredProcedureParameterMapping.cs | 59 + .../IStoredProcedureResultColumnMapping.cs | 59 + src/EFCore.Relational/Metadata/IViewColumn.cs | 8 + .../Metadata/Internal/DbFunction.cs | 16 +- .../Internal/FunctionColumnMapping.cs | 4 +- .../Internal/IRuntimeStoredProcedure.cs | 21 + .../InternalStoredProcedureBuilder.cs | 4 +- .../Metadata/Internal/RelationalModel.cs | 362 ++++- .../Metadata/Internal/StoreStoredProcedure.cs | 159 +++ .../Internal/StoreStoredProcedureParameter.cs | 67 + .../StoreStoredProcedureResultColumn.cs | 57 + .../Metadata/Internal/StoredProcedure.cs | 109 +- .../Internal/StoredProcedureComparer.cs | 118 ++ .../Internal/StoredProcedureMapping.cs | 91 ++ .../StoredProcedureParameterMapping.cs | 56 + .../StoredProcedureResultColumnMapping.cs | 56 + .../Metadata/RelationalAnnotationNames.cs | 45 + .../Metadata/RelationalAnnotationProvider.cs | 12 + .../Metadata/RuntimeStoredProcedure.cs | 164 +++ .../Properties/RelationalStrings.Designer.cs | 16 + .../Properties/RelationalStrings.resx | 6 + .../Extensions/SqlServerPropertyExtensions.cs | 5 + .../Conventions/DiscriminatorConvention.cs | 6 +- .../ManyToManyJoinEntityTypeConvention.cs | 6 +- src/EFCore/Metadata/IReadOnlyEntityType.cs | 9 +- src/EFCore/Metadata/IReadOnlyTypeBase.cs | 9 +- .../Design/CSharpMigrationsGeneratorTest.cs | 18 +- .../CSharpRuntimeModelCodeGeneratorTest.cs | 299 +++- .../RelationalModelValidatorTest.cs | 50 +- .../Metadata/RelationalModelTest.cs | 1205 ++++++++++++++++- .../RelationalModelBuilderTest.cs | 158 ++- .../RelationalTestModelBuilderExtensions.cs | 27 + .../SqlServerModelValidatorTest.cs | 2 +- .../Infrastructure/ModelValidatorTest.cs | 2 +- .../ModelBuilding/ModelBuilderTestBase.cs | 7 +- 76 files changed, 4197 insertions(+), 351 deletions(-) create mode 100644 src/EFCore.Relational/Metadata/IStoreStoredProcedure.cs create mode 100644 src/EFCore.Relational/Metadata/IStoreStoredProcedureParameter.cs create mode 100644 src/EFCore.Relational/Metadata/IStoreStoredProcedureResultColumn.cs create mode 100644 src/EFCore.Relational/Metadata/IStoredProcedureMapping.cs create mode 100644 src/EFCore.Relational/Metadata/IStoredProcedureParameterMapping.cs create mode 100644 src/EFCore.Relational/Metadata/IStoredProcedureResultColumnMapping.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/IRuntimeStoredProcedure.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/StoreStoredProcedure.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/StoreStoredProcedureParameter.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/StoreStoredProcedureResultColumn.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/StoredProcedureComparer.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/StoredProcedureMapping.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/StoredProcedureParameterMapping.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/StoredProcedureResultColumnMapping.cs create mode 100644 src/EFCore.Relational/Metadata/RuntimeStoredProcedure.cs 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..f859a9fb92c 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.UseSproc.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.UseSproc.cs @@ -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 5240fc3fc87..08b10cc76d0 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..7cf88b0ab4d 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,49 @@ 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 = new Dictionary(); + switch (storeObjectIdentifier.StoreObjectType) + { + case StoreObjectType.InsertStoredProcedure: + storeGeneratedProperties = properties.Where(p => (p.Value.ValueGenerated & ValueGenerated.OnAdd) != 0) + .ToDictionary(p => p.Key, p => p.Value); + + break; + case StoreObjectType.UpdateStoredProcedure: + storeGeneratedProperties = properties.Where(p => (p.Value.ValueGenerated & ValueGenerated.OnUpdate) != 0) + .ToDictionary(p => p.Key, p => p.Value); + + break; + } + 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 +364,7 @@ private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy switch (storeObjectIdentifier.StoreObjectType) { case StoreObjectType.InsertStoredProcedure: - if ((property.ValueGenerated & ValueGenerated.OnAdd) == 0) + if (!storeGeneratedProperties.Remove(property.Name)) { throw new InvalidOperationException( RelationalStrings.StoredProcedureResultColumnNotGenerated( @@ -345,7 +377,7 @@ private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy RelationalStrings.StoredProcedureResultColumnDelete( entityType.DisplayName(), resultColumn, storeObjectIdentifier.DisplayName())); case StoreObjectType.UpdateStoredProcedure: - if ((property.ValueGenerated & ValueGenerated.OnUpdate) == 0) + if (!storeGeneratedProperties.Remove(property.Name)) { throw new InvalidOperationException( RelationalStrings.StoredProcedureResultColumnNotGenerated( @@ -358,30 +390,63 @@ private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy 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: + 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; + case StoreObjectType.UpdateStoredProcedure: + if (property.GetDirection(storeObjectIdentifier) != ParameterDirection.Input + && !storeGeneratedProperties.Remove(property.Name)) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureOutputParameterNotGenerated( + entityType.DisplayName(), parameter, storeObjectIdentifier.DisplayName())); + } + + break; } } + + if (storeGeneratedProperties.Count > 0) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureGeneratedPropertiesNotMapped( + entityType.DisplayName(), + storeObjectIdentifier.DisplayName(), + storeGeneratedProperties.Values.Format(false))); + } 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 +1794,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 +1808,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..e50cfe27803 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 485f567dd59..2e82e8a4760 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..c8e194d9a97 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); diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index d01f84c0462..68628c06c97 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,292 @@ 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) != 0) + { + insertStoredProcedureMappings!.Reverse(); + entityType.SetRuntimeAnnotation(RelationalAnnotationNames.InsertStoredProcedureMappings, insertStoredProcedureMappings); + } + + if ((deleteStoredProcedureMappings?.Count ?? 0) != 0) + { + deleteStoredProcedureMappings!.Reverse(); + entityType.SetRuntimeAnnotation(RelationalAnnotationNames.DeleteStoredProcedureMappings, deleteStoredProcedureMappings); + } + + if ((updateStoredProcedureMappings?.Count ?? 0) != 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 = ""; + var columnMappingAnnotationName = ""; + + switch (identifier.StoreObjectType) + { + case StoreObjectType.InsertStoredProcedure: + parameterMappingAnnotationName = RelationalAnnotationNames.InsertStoredProcedureParameterMappings; + columnMappingAnnotationName = RelationalAnnotationNames.InsertStoredProcedureResultColumnMappings; + break; + case StoreObjectType.DeleteStoredProcedure: + parameterMappingAnnotationName = RelationalAnnotationNames.DeleteStoredProcedureParameterMappings; + break; + case StoreObjectType.UpdateStoredProcedure: + parameterMappingAnnotationName = RelationalAnnotationNames.UpdateStoredProcedureParameterMappings; + columnMappingAnnotationName = RelationalAnnotationNames.UpdateStoredProcedureResultColumnMappings; + break; + default: + Check.DebugFail("Unexpected stored procedure type: " + identifier.StoreObjectType); + break; + } + + 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); + } + + private 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; + } + + private 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 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; + } + private static void PopulateTableConfiguration(Table table, bool designTime) { var storeObject = StoreObjectIdentifier.Table(table.Name, table.Schema); @@ -1003,7 +1313,7 @@ private static void PopulateRowInternalForeignKeys(TableBase tab { entityTypeMapping.IsSharedTablePrincipal = false; } - + var entityType = entityTypeMapping.EntityType; mappedEntityTypes.Add(entityType); var primaryKey = entityType.FindPrimaryKey(); @@ -1295,6 +1605,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 a6999562df6..a2a5b5803bd 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..4b1d6a87e46 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1237,6 +1237,14 @@ public static string StoredProcedureDeleteNonKeyProperty(object? entityType, obj 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..73339281843 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -864,12 +864,18 @@ 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 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. 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 09cb794579e..dbc3cbc9f56 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.GetReaderFieldValue }; @@ -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..1e57b231e49 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 => 9, + // Mapping.TPH => 3, + // _ => 10 + //}, 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); @@ -986,8 +1018,878 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) Assert.NotNull(specialCustomerDbIndex.MappedIndexes.Single()); } } + + 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 shippingAddressType = shippingAddressOwnership.DeclaringEntityType; + + 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, Mapping mapping = Mapping.TPH) + private IRelationalModel CreateTestModel( + bool mapToTables = false, + bool mapToViews = false, + bool mapToSprocs = false, + Mapping mapping = Mapping.TPH) { var modelBuilder = CreateConventionModelBuilder(); @@ -1005,6 +1907,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 +1982,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 +2039,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 +2121,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 +2154,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 +2207,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 +2240,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 +2271,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