diff --git a/src/EFCore.Cosmos/Metadata/Conventions/CosmosRelationshipDiscoveryConvention.cs b/src/EFCore.Cosmos/Metadata/Conventions/CosmosRelationshipDiscoveryConvention.cs index b409aa58f01..2063d27add5 100644 --- a/src/EFCore.Cosmos/Metadata/Conventions/CosmosRelationshipDiscoveryConvention.cs +++ b/src/EFCore.Cosmos/Metadata/Conventions/CosmosRelationshipDiscoveryConvention.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - - // ReSharper disable once CheckNamespace namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; diff --git a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs index 07409031d2f..63c0d25dcbc 100644 --- a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs @@ -35,8 +35,7 @@ public class AnnotationCodeGenerator : IAnnotationCodeGenerator RelationalAnnotationNames.InsertStoredProcedure, RelationalAnnotationNames.UpdateStoredProcedure, RelationalAnnotationNames.MappingFragments, - RelationalAnnotationNames.RelationalOverrides, - RelationalAnnotationNames.ParameterDirection + RelationalAnnotationNames.RelationalOverrides }; #region MethodInfos diff --git a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs index fc24006ed32..b2b7d5e5618 100644 --- a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.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; namespace Microsoft.EntityFrameworkCore.Design.Internal; @@ -166,16 +167,16 @@ private void Create( mainBuilder.AppendLine(");").DecrementIndent() .AppendLine(); - var parameterParameters = parameters with { TargetName = functionVariable }; + parameters = parameters with { TargetName = functionVariable }; foreach (var parameter in function.Parameters) { - Create(parameter, parameterParameters); + Create(parameter, parameters); } CreateAnnotations( function, Generate, - parameters with { TargetName = functionVariable }); + parameters); mainBuilder .Append(functionsVariable).Append("[").Append(code.Literal(function.ModelName)).Append("] = ").Append(functionVariable) @@ -481,6 +482,8 @@ public virtual void Generate(ITrigger trigger, CSharpRuntimeAnnotationCodeGenera private void Create(IStoredProcedure storedProcedure, string sprocVariable, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) { AddNamespace(typeof(RuntimeStoredProcedure), parameters.Namespaces); + AddNamespace(typeof(ParameterDirection), parameters.Namespaces); + var code = Dependencies.CSharpHelper; var mainBuilder = parameters.MainBuilder; mainBuilder @@ -488,29 +491,27 @@ private void Create(IStoredProcedure storedProcedure, string sprocVariable, CSha .Append(parameters.TargetName).AppendLine(",") .Append(code.Literal(storedProcedure.Name)).AppendLine(",") .Append(code.Literal(storedProcedure.Schema)).AppendLine(",") + .Append(code.Literal(storedProcedure.AreRowsAffectedReturned)).AppendLine(",") .Append(code.Literal(storedProcedure.AreTransactionsSuppressed)) .AppendLine(");") .DecrementIndent() .AppendLine(); - + + parameters = parameters with { TargetName = sprocVariable }; foreach (var parameter in storedProcedure.Parameters) { - mainBuilder.Append(sprocVariable).Append(".AddParameter(") - .Append(code.Literal(parameter)) - .AppendLine(");"); + Create(parameter, parameters); } foreach (var resultColumn in storedProcedure.ResultColumns) { - mainBuilder.Append(sprocVariable).Append(".AddResultColumn(") - .Append(code.Literal(resultColumn)) - .AppendLine(");"); + Create(resultColumn, parameters); } CreateAnnotations( storedProcedure, Generate, - parameters with { TargetName = sprocVariable }); + parameters); } /// @@ -521,6 +522,64 @@ private void Create(IStoredProcedure storedProcedure, string sprocVariable, CSha public virtual void Generate(IStoredProcedure storedProcedure, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) => GenerateSimpleAnnotations(parameters); + private void Create(IStoredProcedureParameter parameter, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + var code = Dependencies.CSharpHelper; + var mainBuilder = parameters.MainBuilder; + var parameterVariable = code.Identifier(parameter.Name, parameters.ScopeVariables, capitalize: false); + + mainBuilder + .Append("var ").Append(parameterVariable).Append(" = ") + .Append(parameters.TargetName).AppendLine(".AddParameter(").IncrementIndent() + .Append(code.Literal(parameter.Name)).Append(", ") + .Append(code.Literal(parameter.Direction)).Append(", ") + .Append(code.Literal(parameter.ForRowsAffected)).Append(", ") + .Append(code.Literal(parameter.PropertyName!)).Append(", ") + .Append(code.Literal(parameter.ForOriginalValue)) + .AppendLine(");").DecrementIndent(); + + CreateAnnotations( + parameter, + Generate, + parameters with { TargetName = parameterVariable }); + } + + /// + /// 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(IStoredProcedureParameter storedProcedure, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + => GenerateSimpleAnnotations(parameters); + + private void Create(IStoredProcedureResultColumn resultColumn, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + var code = Dependencies.CSharpHelper; + var mainBuilder = parameters.MainBuilder; + var resultColumnVariable = code.Identifier(resultColumn.Name, parameters.ScopeVariables, capitalize: false); + + mainBuilder + .Append("var ").Append(resultColumnVariable).Append(" = ") + .Append(parameters.TargetName).AppendLine(".AddResultColumn(").IncrementIndent() + .Append(code.Literal(resultColumn.Name)).Append(", ") + .Append(code.Literal(resultColumn.ForRowsAffected)).Append(", ") + .Append(code.Literal(resultColumn.PropertyName!)) + .AppendLine(");").DecrementIndent(); + + CreateAnnotations( + resultColumn, + Generate, + parameters with { TargetName = resultColumnVariable }); + } + + /// + /// 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(IStoredProcedureResultColumn storedProcedure, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + => GenerateSimpleAnnotations(parameters); + /// /// Generates code to create the given annotations. /// diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs index 0aa7b023fe9..1547bc19942 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs @@ -357,54 +357,6 @@ public static bool CanSetIsFixedLength( bool? fixedLength, 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 diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs index 1467e0e1479..ac1baec11b5 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs @@ -4,7 +4,6 @@ using System.Globalization; using System.Runtime.CompilerServices; using System.Text; -using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; // ReSharper disable once CheckNamespace @@ -1170,53 +1169,6 @@ 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/RelationalModelRuntimeInitializer.cs b/src/EFCore.Relational/Infrastructure/RelationalModelRuntimeInitializer.cs index 773eb861576..d83b18b92fd 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelRuntimeInitializer.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelRuntimeInitializer.cs @@ -62,7 +62,11 @@ protected override void InitializeModel(IModel model, bool designTime, bool prev } else { - RelationalModel.Add(model, RelationalDependencies.RelationalAnnotationProvider, designTime); + RelationalModel.Add( + model, + RelationalDependencies.RelationalAnnotationProvider, + (IRelationalTypeMappingSource)Dependencies.ModelDependencies.TypeMappingSource, + designTime); } } } diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index b1da78717b9..04e6106aa95 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -214,11 +214,14 @@ protected virtual void ValidateStoredProcedures( foreach (var entityType in model.GetEntityTypes()) { var mappingStrategy = entityType.GetMappingStrategy() ?? RelationalAnnotationNames.TphMappingStrategy; + + var sprocCount = 0; var deleteStoredProcedure = entityType.GetDeleteStoredProcedure(); if (deleteStoredProcedure != null) { AddSproc(StoreObjectType.DeleteStoredProcedure, entityType, storedProcedures); ValidateSproc(deleteStoredProcedure, mappingStrategy); + sprocCount++; } var insertStoredProcedure = entityType.GetInsertStoredProcedure(); @@ -226,6 +229,7 @@ protected virtual void ValidateStoredProcedures( { AddSproc(StoreObjectType.InsertStoredProcedure, entityType, storedProcedures); ValidateSproc(insertStoredProcedure, mappingStrategy); + sprocCount++; } var updateStoredProcedure = entityType.GetUpdateStoredProcedure(); @@ -233,6 +237,14 @@ protected virtual void ValidateStoredProcedures( { AddSproc(StoreObjectType.UpdateStoredProcedure, entityType, storedProcedures); ValidateSproc(updateStoredProcedure, mappingStrategy); + sprocCount++; + } + + if (sprocCount > 0 + && sprocCount < 3 + && entityType.GetTableName() == null) + { + throw new InvalidOperationException(RelationalStrings.StoredProcedureUnmapped(entityType.DisplayName())); } } @@ -337,6 +349,8 @@ private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy } } + var originalValueProperties = new Dictionary(properties); + var storeGeneratedProperties = storeObjectIdentifier.StoreObjectType switch { StoreObjectType.InsertStoredProcedure @@ -346,56 +360,106 @@ private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy _ => new Dictionary() }; + var resultColumnNames = new HashSet(); foreach (var resultColumn in sproc.ResultColumns) { - if (!properties.TryGetValue(resultColumn, out var property)) + IProperty? property = null!; + if (resultColumn.PropertyName != null + && !properties.TryGetValue(resultColumn.PropertyName, out property)) { throw new InvalidOperationException( RelationalStrings.StoredProcedureResultColumnNotFound( - resultColumn, entityType.DisplayName(), storeObjectIdentifier.DisplayName())); + resultColumn.PropertyName, entityType.DisplayName(), storeObjectIdentifier.DisplayName())); + } + + if (!resultColumnNames.Add(resultColumn.Name)) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureDuplicateResultColumnName( + resultColumn.Name, storeObjectIdentifier.DisplayName())); + } + + if (resultColumn.PropertyName == null) + { + continue; } switch (storeObjectIdentifier.StoreObjectType) { case StoreObjectType.InsertStoredProcedure: case StoreObjectType.UpdateStoredProcedure: - if (!storeGeneratedProperties.Remove(property.Name)) + if (!storeGeneratedProperties.ContainsKey(property.Name)) { throw new InvalidOperationException( RelationalStrings.StoredProcedureResultColumnNotGenerated( - entityType.DisplayName(), resultColumn, storeObjectIdentifier.DisplayName())); + entityType.DisplayName(), resultColumn.PropertyName, storeObjectIdentifier.DisplayName())); } break; case StoreObjectType.DeleteStoredProcedure: throw new InvalidOperationException( RelationalStrings.StoredProcedureResultColumnDelete( - entityType.DisplayName(), resultColumn, storeObjectIdentifier.DisplayName())); + entityType.DisplayName(), resultColumn.PropertyName, storeObjectIdentifier.DisplayName())); default: Check.DebugFail("Unexpected stored procedure type: " + storeObjectIdentifier.StoreObjectType); break; } } - + + var parameterNames = new HashSet(); foreach (var parameter in sproc.Parameters) { - if (!properties.TryGetAndRemove(parameter, out IProperty property)) + IProperty property = null!; + if (parameter.PropertyName != null) + { + if (parameter.ForOriginalValue == true) + { + if (!originalValueProperties.TryGetAndRemove(parameter.PropertyName, out property)) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureParameterNotFound( + parameter.PropertyName, entityType.DisplayName(), storeObjectIdentifier.DisplayName())); + } + } + else if (!properties.TryGetAndRemove(parameter.PropertyName, out property)) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureParameterNotFound( + parameter.PropertyName, entityType.DisplayName(), storeObjectIdentifier.DisplayName())); + } + } + + if (!parameterNames.Add(parameter.Name)) { throw new InvalidOperationException( - RelationalStrings.StoredProcedureParameterNotFound( - parameter, entityType.DisplayName(), storeObjectIdentifier.DisplayName())); + RelationalStrings.StoredProcedureDuplicateParameterName( + parameter.Name, storeObjectIdentifier.DisplayName())); + } + + if (parameter.PropertyName == null) + { + continue; } switch (storeObjectIdentifier.StoreObjectType) { case StoreObjectType.InsertStoredProcedure: case StoreObjectType.UpdateStoredProcedure: - if (property.GetDirection(storeObjectIdentifier) != ParameterDirection.Input + if (parameter.Direction != ParameterDirection.Input && !storeGeneratedProperties.Remove(property.Name)) { + if (sproc.Parameters.Any(p => p.PropertyName == property.Name + && p.ForOriginalValue != parameter.ForOriginalValue + && p.Direction != ParameterDirection.Input)) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureOutputParameterConflict( + entityType.DisplayName(), parameter.PropertyName, storeObjectIdentifier.DisplayName())); + } + throw new InvalidOperationException( RelationalStrings.StoredProcedureOutputParameterNotGenerated( - entityType.DisplayName(), parameter, storeObjectIdentifier.DisplayName())); + entityType.DisplayName(), parameter.PropertyName, storeObjectIdentifier.DisplayName())); } break; @@ -405,7 +469,7 @@ private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy { throw new InvalidOperationException( RelationalStrings.StoredProcedureDeleteNonKeyProperty( - entityType.DisplayName(), parameter, storeObjectIdentifier.DisplayName())); + entityType.DisplayName(), parameter.PropertyName, storeObjectIdentifier.DisplayName())); } break; @@ -414,6 +478,23 @@ private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy break; } } + + foreach (var resultColumn in sproc.ResultColumns) + { + if (resultColumn.PropertyName == null) + { + continue; + } + + properties.Remove(resultColumn.PropertyName); + + if (!storeGeneratedProperties.Remove(resultColumn.PropertyName)) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureResultColumnParameterConflict( + entityType.DisplayName(), resultColumn.PropertyName, storeObjectIdentifier.DisplayName())); + } + } if (storeGeneratedProperties.Count > 0) { @@ -424,11 +505,6 @@ private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy storeGeneratedProperties.Values.Format())); } - foreach (var resultColumn in sproc.ResultColumns) - { - properties.Remove(resultColumn); - } - if (properties.Count > 0) { foreach (var property in properties.Values.ToList()) @@ -464,6 +540,14 @@ private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy } } + foreach (var property in properties.Keys.ToList()) + { + if (!originalValueProperties.ContainsKey(property)) + { + properties.Remove(property); + } + } + if (properties.Count > 0) { throw new InvalidOperationException( @@ -473,6 +557,20 @@ private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy properties.Values.Format(false))); } } + + var missedConcurrencyToken = originalValueProperties.Values.FirstOrDefault(p => p.IsConcurrencyToken); + if (missedConcurrencyToken != null + && storeObjectIdentifier.StoreObjectType != StoreObjectType.InsertStoredProcedure + && (sproc.AreRowsAffectedReturned + || sproc.FindRowsAffectedParameter() != null + || sproc.FindRowsAffectedResultColumn() != null)) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureConcurrencyTokenNotMapped( + entityType.DisplayName(), + storeObjectIdentifier.DisplayName(), + missedConcurrencyToken.Name)); + } } /// diff --git a/src/EFCore.Relational/Metadata/Builders/IConventionStoredProcedureBuilder.cs b/src/EFCore.Relational/Metadata/Builders/IConventionStoredProcedureBuilder.cs index e120adc1c51..f335504e7db 100644 --- a/src/EFCore.Relational/Metadata/Builders/IConventionStoredProcedureBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/IConventionStoredProcedureBuilder.cs @@ -75,7 +75,7 @@ public interface IConventionStoredProcedureBuilder : IConventionAnnotatableBuild /// The same builder instance if the configuration was applied, /// otherwise. /// - IConventionStoredProcedureBuilder? HasParameter(string propertyName, bool fromDataAnnotation = false); + IConventionStoredProcedureParameterBuilder? HasParameter(string propertyName, bool fromDataAnnotation = false); /// /// Returns a value indicating whether a parameter mapped to the given property can be used for stored procedure. @@ -85,16 +85,51 @@ public interface IConventionStoredProcedureBuilder : IConventionAnnotatableBuild /// if the parameter can be used for the stored procedure. bool CanHaveParameter(string propertyName, bool fromDataAnnotation = false); + /// + /// Configures a new parameter that holds the original value of the property with the given name + /// if no parameter mapped to the given property exists. + /// + /// The property name. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The builder instance if the configuration was applied, otherwise. + /// + IConventionStoredProcedureParameterBuilder? HasOriginalValueParameter(string propertyName, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether a parameter holds the original value of the mapped property + /// can be used for stored procedure. + /// + /// The property name. + /// Indicates whether the configuration was specified using a data annotation. + /// if the parameter can be used for the stored procedure. + bool CanHaveOriginalValueParameter(string propertyName, bool fromDataAnnotation = false); + + /// + /// Configures a new parameter that returns the rows affected if no such parameter exists. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The builder instance if the configuration was applied, otherwise. + /// + IConventionStoredProcedureParameterBuilder? HasRowsAffectedParameter(bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether a parameter that returns the rows affected can be used for stored procedure. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// if the parameter can be used for the stored procedure. + bool CanHaveRowsAffectedParameter(bool fromDataAnnotation = false); + /// /// Configures a new column of the result for this stored procedure. This is used for database generated columns. /// /// The property name. /// Indicates whether the configuration was specified using a data annotation. /// - /// The same builder instance if the configuration was applied, - /// otherwise. + /// The builder instance if the configuration was applied, otherwise. /// - IConventionStoredProcedureBuilder? HasResultColumn(string propertyName, bool fromDataAnnotation = false); + IConventionStoredProcedureResultColumnBuilder? HasResultColumn(string propertyName, bool fromDataAnnotation = false); /// /// Returns a value indicating whether a column of the result mapped to the given property can be used for stored procedure. @@ -104,6 +139,24 @@ public interface IConventionStoredProcedureBuilder : IConventionAnnotatableBuild /// if the column of the result can be used for the stored procedure. bool CanHaveResultColumn(string propertyName, bool fromDataAnnotation = false); + /// + /// Configures a new column that contains the rows affected for this stored procedure if no such column exists. + /// + /// The property name. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The builder instance if the configuration was applied, otherwise. + /// + IConventionStoredProcedureResultColumnBuilder? HasRowsAffectedResultColumn(string propertyName, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether a column that contains the rows affected can be used for stored procedure. + /// + /// The property name. + /// Indicates whether the configuration was specified using a data annotation. + /// if the column of the result can be used for the stored procedure. + bool CanHaveRowsAffectedResultColumn(string propertyName, bool fromDataAnnotation = false); + /// /// Prevents automatically creating a transaction when executing this stored procedure. /// diff --git a/src/EFCore.Relational/Metadata/Builders/IConventionStoredProcedureParameterBuilder.cs b/src/EFCore.Relational/Metadata/Builders/IConventionStoredProcedureParameterBuilder.cs new file mode 100644 index 00000000000..978d7c778a6 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Builders/IConventionStoredProcedureParameterBuilder.cs @@ -0,0 +1,64 @@ +// 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.Builders; + +/// +/// Provides a simple API for configuring a . +/// +/// +/// See Model building conventions for more information and examples. +/// +public interface IConventionStoredProcedureParameterBuilder : IConventionAnnotatableBuilder +{ + /// + /// The stored procedure parameter metadata that is being built. + /// + new IConventionStoredProcedureParameter Metadata { get; } + + /// + /// Configures the parameter name. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The name of the parameter. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionStoredProcedureParameterBuilder? HasName(string name, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given parameter name can be set. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The name of the parameter. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given parameter name can be set. + bool CanSetName(string? name, bool fromDataAnnotation = false); + + /// + /// Sets the direction of the stored procedure parameter. + /// + /// The direction. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionStoredProcedureParameterBuilder? HasDirection(ParameterDirection direction, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given direction can be configured on the corresponding stored procedure parameter. + /// + /// The direction. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given direction can be configured. + bool CanSetDirection(ParameterDirection direction, bool fromDataAnnotation = false); +} diff --git a/src/EFCore.Relational/Metadata/Builders/IConventionStoredProcedureResultColumnBuilder.cs b/src/EFCore.Relational/Metadata/Builders/IConventionStoredProcedureResultColumnBuilder.cs new file mode 100644 index 00000000000..3fb0d22c6e2 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Builders/IConventionStoredProcedureResultColumnBuilder.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders; + +/// +/// Provides a simple API for configuring a . +/// +/// +/// See Model building conventions for more information and examples. +/// +public interface IConventionStoredProcedureResultColumnBuilder : IConventionAnnotatableBuilder +{ + /// + /// The stored procedure result column metadata that is being built. + /// + new IConventionStoredProcedureResultColumn Metadata { get; } + + /// + /// Configures the result column name. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The name of the result column. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionStoredProcedureResultColumnBuilder? HasName(string name, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given result column name can be set. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The name of the result column. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given result column name can be set. + bool CanSetName(string? name, bool fromDataAnnotation = false); +} diff --git a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder.cs b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder.cs index 52f8d56ef16..c60ace6e276 100644 --- a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder.cs @@ -67,10 +67,59 @@ public virtual OwnedNavigationStoredProcedureBuilder HasParameter(string propert /// The parameter name. /// An action that performs configuration of the parameter. /// The same builder instance so that multiple configuration calls can be chained. - public virtual OwnedNavigationStoredProcedureBuilder HasParameter(string propertyName, Action buildAction) + public virtual OwnedNavigationStoredProcedureBuilder HasParameter( + string propertyName, Action buildAction) { - Builder.HasParameter(propertyName, ConfigurationSource.Explicit); - buildAction(new(Metadata.GetStoreIdentifier()!.Value, CreatePropertyBuilder(propertyName))); + var parameterBuilder = Builder.HasParameter(propertyName, ConfigurationSource.Explicit)!; + buildAction(new(parameterBuilder, CreatePropertyBuilder(propertyName))); + return this; + } + + /// + /// Configures a new parameter that holds the original value if no parameter mapped to the given property exists. + /// + /// The property name. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationStoredProcedureBuilder HasOriginalValueParameter(string propertyName) + { + Builder.HasOriginalValueParameter(propertyName, ConfigurationSource.Explicit); + return this; + } + + /// + /// Configures a new parameter that holds the original value if no parameter mapped to the given property exists. + /// + /// The parameter name. + /// An action that performs configuration of the parameter. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationStoredProcedureBuilder HasOriginalValueParameter( + string propertyName, Action buildAction) + { + var parameterBuilder = Builder.HasOriginalValueParameter(propertyName, ConfigurationSource.Explicit)!; + buildAction(new(parameterBuilder, CreatePropertyBuilder(propertyName))); + return this; + } + + /// + /// Configures a new parameter that returns the rows affected if no such parameter exists. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationStoredProcedureBuilder HasRowsAffectedParameter() + { + Builder.HasRowsAffectedParameter(ConfigurationSource.Explicit); + return this; + } + + /// + /// Configures a new parameter that returns the rows affected if no such parameter exists. + /// + /// An action that performs configuration of the parameter. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationStoredProcedureBuilder HasRowsAffectedParameter( + Action buildAction) + { + var parameterBuilder = Builder.HasRowsAffectedParameter(ConfigurationSource.Explicit)!; + buildAction(new(parameterBuilder, null)); return this; } @@ -127,8 +176,46 @@ public virtual OwnedNavigationStoredProcedureBuilder HasResultColumn(string prop public virtual OwnedNavigationStoredProcedureBuilder HasResultColumn( string propertyName, Action buildAction) { - Builder.HasResultColumn(propertyName, ConfigurationSource.Explicit); - buildAction(new(Metadata.GetStoreIdentifier()!.Value, CreatePropertyBuilder(propertyName))); + var resultColumnBuilder = Builder.HasResultColumn(propertyName, ConfigurationSource.Explicit)!; + buildAction(new(resultColumnBuilder, CreatePropertyBuilder(propertyName))); + return this; + } + + /// + /// Configures a new column of the result that returns the rows affected for this stored procedure + /// if no such column exists. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationStoredProcedureBuilder HasRowsAffectedResultColumn() + { + Builder.HasRowsAffectedResultColumn(ConfigurationSource.Explicit); + return this; + } + + /// + /// Configures a new column of the result that returns the rows affected for this stored procedure + /// if no such column exists. + /// + /// An action that performs configuration of the column. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationStoredProcedureBuilder HasRowsAffectedResultColumn( + Action buildAction) + { + var resultColumnBuilder = Builder.HasRowsAffectedResultColumn(ConfigurationSource.Explicit)!; + buildAction(new(resultColumnBuilder, null)); + return this; + } + + /// + /// Configures the result of this stored procedure to be the number of rows affected. + /// + /// + /// A value indicating whether this stored procedure returns the number of rows affected. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationStoredProcedureBuilder HasRowsAffectedReturnValue(bool rowsAffectedReturned = true) + { + Builder.HasRowsAffectedReturn(rowsAffectedReturned, ConfigurationSource.Explicit); return this; } diff --git a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder``.cs b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder``.cs index 9ef38f2d803..4c866fbfa68 100644 --- a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder``.cs +++ b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationStoredProcedureBuilder``.cs @@ -79,11 +79,79 @@ public virtual OwnedNavigationStoredProcedureBuilder> propertyExpression, Action buildAction) { - Builder.HasParameter(propertyExpression, ConfigurationSource.Explicit); - buildAction(new(Metadata.GetStoreIdentifier()!.Value, CreatePropertyBuilder(propertyExpression))); + var parameterBuilder = Builder.HasParameter(propertyExpression, ConfigurationSource.Explicit)!; + buildAction(new(parameterBuilder, CreatePropertyBuilder(propertyExpression))); return this; } + /// + /// Configures a new parameter that holds the original value if no parameter mapped to the given property exists. + /// + /// The property name. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual OwnedNavigationStoredProcedureBuilder HasOriginalValueParameter( + string propertyName) + => (OwnedNavigationStoredProcedureBuilder)base.HasOriginalValueParameter(propertyName); + + /// + /// Configures a new parameter that holds the original value if no parameter mapped to the given property exists. + /// + /// The parameter name. + /// An action that performs configuration of the parameter. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual OwnedNavigationStoredProcedureBuilder HasOriginalValueParameter( + string propertyName, Action buildAction) + => (OwnedNavigationStoredProcedureBuilder)base.HasOriginalValueParameter(propertyName, buildAction); + + /// + /// Configures a new parameter that holds the original value if no parameter mapped to the given property exists. + /// + /// The property type. + /// + /// A lambda expression representing the property to be configured (blog => blog.Url). + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression) + { + Builder.HasOriginalValueParameter(propertyExpression, ConfigurationSource.Explicit); + return this; + } + + /// + /// Configures a new parameter that holds the original value if no parameter mapped to the given property exists. + /// + /// The property type. + /// + /// A lambda expression representing the property to be configured (blog => blog.Url). + /// + /// An action that performs configuration of the parameter. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression, + Action buildAction) + { + var parameterBuilder = Builder.HasOriginalValueParameter(propertyExpression, ConfigurationSource.Explicit)!; + buildAction(new(parameterBuilder, CreatePropertyBuilder(propertyExpression))); + return this; + } + + /// + /// Configures a new parameter that returns the rows affected if no such parameter exists. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual OwnedNavigationStoredProcedureBuilder HasRowsAffectedParameter() + => (OwnedNavigationStoredProcedureBuilder)base.HasRowsAffectedParameter(); + + /// + /// Configures a new parameter that returns the rows affected if no such parameter exists. + /// + /// An action that performs configuration of the parameter. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual OwnedNavigationStoredProcedureBuilder HasRowsAffectedParameter( + Action buildAction) + => (OwnedNavigationStoredProcedureBuilder)base.HasRowsAffectedParameter(buildAction); + /// /// Configures a new column of the result for this stored procedure. This is used for database generated columns. /// @@ -130,10 +198,38 @@ public virtual OwnedNavigationStoredProcedureBuilder> propertyExpression, Action buildAction) { - Builder.HasResultColumn(propertyExpression, ConfigurationSource.Explicit); - buildAction(new(Metadata.GetStoreIdentifier()!.Value, CreatePropertyBuilder(propertyExpression))); + var resultColumnBuilder = Builder.HasResultColumn(propertyExpression, ConfigurationSource.Explicit)!; + buildAction(new(resultColumnBuilder, CreatePropertyBuilder(propertyExpression))); return this; } + + /// + /// Configures a new column of the result that returns the rows affected for this stored procedure + /// if no such column exists. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual OwnedNavigationStoredProcedureBuilder HasRowsAffectedResultColumn() + => (OwnedNavigationStoredProcedureBuilder)base.HasRowsAffectedResultColumn(); + + /// + /// Configures a new column of the result that returns the rows affected for this stored procedure + /// if no such column exists. + /// + /// An action that performs configuration of the column. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual OwnedNavigationStoredProcedureBuilder HasRowsAffectedResultColumn( + Action buildAction) + => (OwnedNavigationStoredProcedureBuilder)base.HasRowsAffectedResultColumn(buildAction); + + /// + /// Configures the result of this stored procedure to be the number of rows affected. + /// + /// + /// A value indicating whether this stored procedure returns the number of rows affected. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual OwnedNavigationStoredProcedureBuilder HasRowsAffectedReturnValue(bool rowsAffectedReturned = true) + => (OwnedNavigationStoredProcedureBuilder)base.HasRowsAffectedReturnValue(rowsAffectedReturned); /// /// Prevents automatically creating a transaction when executing this stored procedure. diff --git a/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder.cs b/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder.cs index 2862c434d98..5ab996b3ec1 100644 --- a/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder.cs @@ -67,60 +67,57 @@ public virtual StoredProcedureBuilder HasParameter(string propertyName) public virtual StoredProcedureBuilder HasParameter( string propertyName, Action buildAction) { - Builder.HasParameter(propertyName, ConfigurationSource.Explicit); - buildAction(new(Metadata.GetStoreIdentifier()!.Value, CreatePropertyBuilder(propertyName))); + var parameterBuilder = Builder.HasParameter(propertyName, ConfigurationSource.Explicit)!; + buildAction(new(parameterBuilder, CreatePropertyBuilder(propertyName))); return this; } /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// Configures a new parameter that holds the original value if no parameter mapped to the given property exists. /// - [EntityFrameworkInternal] - protected virtual PropertyBuilder CreatePropertyBuilder(string propertyName) + /// The property name. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasOriginalValueParameter(string propertyName) { - var entityType = EntityTypeBuilder.Metadata; - var property = entityType.FindProperty(propertyName); - if (property == null) - { - property = entityType.GetDerivedTypes().SelectMany(et => et.GetDeclaredProperties()) - .FirstOrDefault(p => p.Name == propertyName); - } - - if (property == null) - { - throw new InvalidOperationException(CoreStrings.PropertyNotFound(propertyName, entityType.DisplayName())); - } + Builder.HasOriginalValueParameter(propertyName, ConfigurationSource.Explicit); + return this; + } -#pragma warning disable EF1001 // Internal EF Core API usage. - return new ModelBuilder(entityType.Model) -#pragma warning restore EF1001 // Internal EF Core API usage. - .Entity(property.DeclaringEntityType.Name) - .Property(property.ClrType, propertyName); + /// + /// Configures a new parameter that holds the original value if no parameter mapped to the given property exists. + /// + /// The parameter name. + /// An action that performs configuration of the parameter. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasOriginalValueParameter( + string propertyName, Action buildAction) + { + var parameterBuilder = Builder.HasOriginalValueParameter(propertyName, ConfigurationSource.Explicit)!; + buildAction(new(parameterBuilder, CreatePropertyBuilder(propertyName))); + return this; } /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// Configures a new parameter that returns the rows affected if no such parameter exists. /// - [EntityFrameworkInternal] - protected virtual PropertyBuilder CreatePropertyBuilder( - Expression> propertyExpression) - where TDerivedEntity : class + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasRowsAffectedParameter() { - var memberInfo = propertyExpression.GetMemberAccess(); - var entityType = EntityTypeBuilder.Metadata; - var entityTypeBuilder = entityType.ClrType == typeof(TDerivedEntity) - ? EntityTypeBuilder -#pragma warning disable EF1001 // Internal EF Core API usage. - : new ModelBuilder(entityType.Model).Entity(typeof(TDerivedEntity)); -#pragma warning restore EF1001 // Internal EF Core API usage. + Builder.HasRowsAffectedParameter(ConfigurationSource.Explicit); + return this; + } - return entityTypeBuilder.Property(memberInfo.GetMemberType(), memberInfo.Name); + /// + /// Configures a new parameter that returns the rows affected if no such parameter exists. + /// + /// An action that performs configuration of the parameter. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasRowsAffectedParameter( + Action buildAction) + { + var parameterBuilder = Builder.HasRowsAffectedParameter(ConfigurationSource.Explicit)!; + buildAction(new(parameterBuilder, null)); + return this; } /// @@ -143,8 +140,45 @@ public virtual StoredProcedureBuilder HasResultColumn(string propertyName) public virtual StoredProcedureBuilder HasResultColumn( string propertyName, Action buildAction) { - Builder.HasResultColumn(propertyName, ConfigurationSource.Explicit); - buildAction(new(Metadata.GetStoreIdentifier()!.Value, CreatePropertyBuilder(propertyName))); + var resultColumnBuilder = Builder.HasResultColumn(propertyName, ConfigurationSource.Explicit)!; + buildAction(new(resultColumnBuilder, CreatePropertyBuilder(propertyName))); + return this; + } + + /// + /// Configures a new column of the result that returns the rows affected for this stored procedure + /// if no such column exists. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasRowsAffectedResultColumn() + { + Builder.HasRowsAffectedResultColumn(ConfigurationSource.Explicit); + return this; + } + + /// + /// Configures a new column of the result that returns the rows affected for this stored procedure + /// if no such column exists. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasRowsAffectedResultColumn( + Action buildAction) + { + var resultColumnBuilder = Builder.HasRowsAffectedResultColumn(ConfigurationSource.Explicit)!; + buildAction(new(resultColumnBuilder, null)); + return this; + } + + /// + /// Configures the result of this stored procedure to be the number of rows affected. + /// + /// + /// A value indicating whether this stored procedure returns the number of rows affected. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasRowsAffectedReturnValue(bool rowsAffectedReturned = true) + { + Builder.HasRowsAffectedReturn(rowsAffectedReturned, ConfigurationSource.Explicit); return this; } @@ -175,5 +209,56 @@ public virtual StoredProcedureBuilder HasAnnotation(string annotation, object? v return this; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + protected virtual PropertyBuilder CreatePropertyBuilder(string propertyName) + { + var entityType = EntityTypeBuilder.Metadata; + var property = entityType.FindProperty(propertyName); + if (property == null) + { + property = entityType.GetDerivedTypes().SelectMany(et => et.GetDeclaredProperties()) + .FirstOrDefault(p => p.Name == propertyName); + } + + if (property == null) + { + throw new InvalidOperationException(CoreStrings.PropertyNotFound(propertyName, entityType.DisplayName())); + } + +#pragma warning disable EF1001 // Internal EF Core API usage. + return new ModelBuilder(entityType.Model) +#pragma warning restore EF1001 // Internal EF Core API usage. + .Entity(property.DeclaringEntityType.Name) + .Property(property.ClrType, propertyName); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + protected virtual PropertyBuilder CreatePropertyBuilder( + Expression> propertyExpression) + where TDerivedEntity : class + { + var memberInfo = propertyExpression.GetMemberAccess(); + var entityType = EntityTypeBuilder.Metadata; + var entityTypeBuilder = entityType.ClrType == typeof(TDerivedEntity) + ? EntityTypeBuilder +#pragma warning disable EF1001 // Internal EF Core API usage. + : new ModelBuilder(entityType.Model).Entity(typeof(TDerivedEntity)); +#pragma warning restore EF1001 // Internal EF Core API usage. + + return entityTypeBuilder.Property(memberInfo.GetMemberType(), memberInfo.Name); + } + EntityTypeBuilder IInfrastructure.Instance => EntityTypeBuilder; } diff --git a/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder`.cs b/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder`.cs index f5a22972b9f..03fab03faf0 100644 --- a/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder`.cs +++ b/src/EFCore.Relational/Metadata/Builders/StoredProcedureBuilder`.cs @@ -103,11 +103,108 @@ public virtual StoredProcedureBuilder HasParameter buildAction) where TDerivedEntity : class, TEntity { - Builder.HasParameter(propertyExpression, ConfigurationSource.Explicit); - buildAction(new(Metadata.GetStoreIdentifier()!.Value, CreatePropertyBuilder(propertyExpression))); + var parameterBuilder = Builder.HasParameter(propertyExpression, ConfigurationSource.Explicit)!; + buildAction(new(parameterBuilder, CreatePropertyBuilder(propertyExpression))); return this; } + /// + /// Configures a new parameter that holds the original value if no parameter mapped to the given property exists. + /// + /// The property name. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual StoredProcedureBuilder HasOriginalValueParameter(string propertyName) + => (StoredProcedureBuilder)base.HasOriginalValueParameter(propertyName); + + /// + /// Configures a new parameter that holds the original value if no parameter mapped to the given property exists. + /// + /// The parameter name. + /// An action that performs configuration of the parameter. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual StoredProcedureBuilder HasOriginalValueParameter( + string propertyName, Action buildAction) + => (StoredProcedureBuilder)base.HasOriginalValueParameter(propertyName, buildAction); + + /// + /// Configures a new parameter that holds the original value if no parameter mapped to the given property exists. + /// + /// The property type. + /// + /// A lambda expression representing the property to be configured (blog => blog.Url). + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression) + => HasOriginalValueParameter(propertyExpression); + + /// + /// Configures a new parameter that holds the original value if no parameter mapped to the given property exists. + /// + /// The same or a derived entity type. + /// The property type. + /// + /// A lambda expression representing the property to be configured (blog => blog.Url). + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression) + where TDerivedEntity : class, TEntity + { + Builder.HasOriginalValueParameter(propertyExpression, ConfigurationSource.Explicit); + return this; + } + + /// + /// Configures a new parameter that holds the original value if no parameter mapped to the given property exists. + /// + /// The property type. + /// + /// A lambda expression representing the property to be configured (blog => blog.Url). + /// + /// An action that performs configuration of the parameter. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression, + Action buildAction) + => HasOriginalValueParameter(propertyExpression, buildAction); + + /// + /// Configures a new parameter that holds the original value if no parameter mapped to the given property exists. + /// + /// The same or a derived entity type. + /// The property type. + /// + /// A lambda expression representing the property to be configured (blog => blog.Url). + /// + /// An action that performs configuration of the parameter. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual StoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression, + Action buildAction) + where TDerivedEntity : class, TEntity + { + var parameterBuilder = Builder.HasOriginalValueParameter(propertyExpression, ConfigurationSource.Explicit)!; + buildAction(new(parameterBuilder, CreatePropertyBuilder(propertyExpression))); + return this; + } + + /// + /// Configures a new parameter that returns the rows affected if no such parameter exists. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual StoredProcedureBuilder HasRowsAffectedParameter() + => (StoredProcedureBuilder)base.HasRowsAffectedParameter(); + + /// + /// Configures a new parameter that returns the rows affected if no such parameter exists. + /// + /// An action that performs configuration of the parameter. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual StoredProcedureBuilder HasRowsAffectedParameter( + Action buildAction) + => (StoredProcedureBuilder)base.HasRowsAffectedParameter(buildAction); + /// /// Configures a new column of the result for this stored procedure. This is used for database generated columns. /// @@ -184,10 +281,38 @@ public virtual StoredProcedureBuilder HasResultColumn buildAction) where TDerivedEntity : class, TEntity { - Builder.HasResultColumn(propertyExpression, ConfigurationSource.Explicit); - buildAction(new(Metadata.GetStoreIdentifier()!.Value, CreatePropertyBuilder(propertyExpression))); + var resultColumnBuilder = Builder.HasResultColumn(propertyExpression, ConfigurationSource.Explicit)!; + buildAction(new(resultColumnBuilder, CreatePropertyBuilder(propertyExpression))); return this; } + + /// + /// Configures a new column of the result that returns the rows affected for this stored procedure + /// if no such column exists. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual StoredProcedureBuilder HasRowsAffectedResultColumn() + => (StoredProcedureBuilder)base.HasRowsAffectedResultColumn(); + + /// + /// Configures a new column of the result that returns the rows affected for this stored procedure + /// if no such column exists. + /// + /// An action that performs configuration of the column. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual StoredProcedureBuilder HasRowsAffectedResultColumn( + Action buildAction) + => (StoredProcedureBuilder)base.HasRowsAffectedResultColumn(buildAction); + + /// + /// Configures the result of this stored procedure to be the number of rows affected. + /// + /// + /// A value indicating whether this stored procedure returns the number of rows affected. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual StoredProcedureBuilder HasRowsAffectedReturnValue(bool rowsAffectedReturned = true) + => (StoredProcedureBuilder)base.HasRowsAffectedReturnValue(rowsAffectedReturned); /// /// Prevents automatically creating a transaction when executing this stored procedure. diff --git a/src/EFCore.Relational/Metadata/Builders/StoredProcedureParameterBuilder.cs b/src/EFCore.Relational/Metadata/Builders/StoredProcedureParameterBuilder.cs index 3103bc18c6f..0d276a1b3cf 100644 --- a/src/EFCore.Relational/Metadata/Builders/StoredProcedureParameterBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/StoredProcedureParameterBuilder.cs @@ -14,7 +14,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Builders; /// Instances of this class are returned from methods when using the API /// and it is not designed to be directly constructed in your application code. /// -public class StoredProcedureParameterBuilder : IInfrastructure +public class StoredProcedureParameterBuilder : IInfrastructure { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -23,23 +23,19 @@ public class StoredProcedureParameterBuilder : IInfrastructure /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - public StoredProcedureParameterBuilder(in StoreObjectIdentifier storeObject, PropertyBuilder propertyBuilder) + public StoredProcedureParameterBuilder( + InternalStoredProcedureParameterBuilder builder, PropertyBuilder? propertyBuilder) { - Check.DebugAssert(storeObject.StoreObjectType == StoreObjectType.InsertStoredProcedure - || storeObject.StoreObjectType == StoreObjectType.DeleteStoredProcedure - || storeObject.StoreObjectType == StoreObjectType.UpdateStoredProcedure, - "StoreObjectType should be StoredProcedure, not " + storeObject.StoreObjectType); - - InternalOverrides = RelationalPropertyOverrides.GetOrCreate( - propertyBuilder.Metadata, storeObject, ConfigurationSource.Explicit); + Builder = builder; PropertyBuilder = propertyBuilder; } /// - /// The stored procedure-specific overrides being configured. + /// The stored procedure parameter being configured. /// - public virtual IMutableRelationalPropertyOverrides Overrides => InternalOverrides; - + public virtual IMutableStoredProcedureParameter Metadata + => Builder.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 @@ -47,9 +43,9 @@ public StoredProcedureParameterBuilder(in StoreObjectIdentifier storeObject, Pro /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - protected virtual RelationalPropertyOverrides InternalOverrides { get; } + protected virtual InternalStoredProcedureParameterBuilder Builder { get; } - private PropertyBuilder PropertyBuilder { get; } + private PropertyBuilder? PropertyBuilder { get; } /// /// Sets the name of the stored procedure parameter. @@ -60,11 +56,11 @@ public StoredProcedureParameterBuilder(in StoreObjectIdentifier storeObject, Pro /// /// The store type of the function parameter in the database. /// The same builder instance so that further configuration calls can be chained. - public virtual StoredProcedureParameterBuilder HasName(string? name) + public virtual StoredProcedureParameterBuilder HasName(string name) { - Check.NullButNotEmpty(name, nameof(name)); + Check.NotNull(name, nameof(name)); - InternalOverrides.SetColumnName(name, ConfigurationSource.Explicit); + Builder.HasName(name, ConfigurationSource.Explicit); return this; } @@ -79,7 +75,7 @@ public virtual StoredProcedureParameterBuilder HasName(string? name) /// The same builder instance so that further configuration calls can be chained. public virtual StoredProcedureParameterBuilder IsInputOutput() { - ((IMutableRelationalPropertyOverrides)InternalOverrides).Direction = ParameterDirection.InputOutput; + Builder.HasDirection(ParameterDirection.InputOutput, ConfigurationSource.Explicit); return this; } @@ -94,7 +90,7 @@ public virtual StoredProcedureParameterBuilder IsInputOutput() /// The same builder instance so that further configuration calls can be chained. public virtual StoredProcedureParameterBuilder IsOutput() { - ((IMutableRelationalPropertyOverrides)InternalOverrides).Direction = ParameterDirection.Output; + Builder.HasDirection(ParameterDirection.Output, ConfigurationSource.Explicit); return this; } @@ -111,12 +107,12 @@ public virtual StoredProcedureParameterBuilder HasAnnotation(string annotation, { Check.NotEmpty(annotation, nameof(annotation)); - InternalOverrides.Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); + Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); return this; } - PropertyBuilder IInfrastructure.Instance => PropertyBuilder; + PropertyBuilder? IInfrastructure.Instance => PropertyBuilder; #region Hidden System.Object members diff --git a/src/EFCore.Relational/Metadata/Builders/StoredProcedureResultColumnBuilder.cs b/src/EFCore.Relational/Metadata/Builders/StoredProcedureResultColumnBuilder.cs index b83fc3da30e..e8fffca8776 100644 --- a/src/EFCore.Relational/Metadata/Builders/StoredProcedureResultColumnBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/StoredProcedureResultColumnBuilder.cs @@ -13,7 +13,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Builders; /// Instances of this class are returned from methods when using the API /// and it is not designed to be directly constructed in your application code. /// -public class StoredProcedureResultColumnBuilder : IInfrastructure +public class StoredProcedureResultColumnBuilder : IInfrastructure { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -22,22 +22,18 @@ public class StoredProcedureResultColumnBuilder : IInfrastructure [EntityFrameworkInternal] - public StoredProcedureResultColumnBuilder(in StoreObjectIdentifier storeObject, PropertyBuilder propertyBuilder) + public StoredProcedureResultColumnBuilder( + InternalStoredProcedureResultColumnBuilder builder, PropertyBuilder? propertyBuilder) { - Check.DebugAssert(storeObject.StoreObjectType == StoreObjectType.InsertStoredProcedure - || storeObject.StoreObjectType == StoreObjectType.DeleteStoredProcedure - || storeObject.StoreObjectType == StoreObjectType.UpdateStoredProcedure, - "StoreObjectType should be StoredProcedure, not " + storeObject.StoreObjectType); - - InternalOverrides = RelationalPropertyOverrides.GetOrCreate( - propertyBuilder.Metadata, storeObject, ConfigurationSource.Explicit); + Builder = builder; PropertyBuilder = propertyBuilder; } /// - /// The stored procedure-specific overrides being configured. + /// The stored procedure result column being configured. /// - public virtual IMutableRelationalPropertyOverrides Overrides => InternalOverrides; + public virtual IMutableStoredProcedureResultColumn Metadata + => Builder.Metadata; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -46,9 +42,9 @@ public StoredProcedureResultColumnBuilder(in StoreObjectIdentifier storeObject, /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - protected virtual RelationalPropertyOverrides InternalOverrides { get; } + protected virtual InternalStoredProcedureResultColumnBuilder Builder { get; } - private PropertyBuilder PropertyBuilder { get; } + private PropertyBuilder? PropertyBuilder { get; } /// /// Sets the name of the stored procedure result column. @@ -59,11 +55,11 @@ public StoredProcedureResultColumnBuilder(in StoreObjectIdentifier storeObject, /// /// The store type of the function parameter in the database. /// The same builder instance so that further configuration calls can be chained. - public virtual StoredProcedureResultColumnBuilder HasName(string? name) + public virtual StoredProcedureResultColumnBuilder HasName(string name) { Check.NullButNotEmpty(name, nameof(name)); - InternalOverrides.SetColumnName(name, ConfigurationSource.Explicit); + Builder.HasName(name, ConfigurationSource.Explicit); return this; } @@ -80,12 +76,12 @@ public virtual StoredProcedureResultColumnBuilder HasAnnotation(string annotatio { Check.NotEmpty(annotation, nameof(annotation)); - InternalOverrides.Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); + Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); return this; } - PropertyBuilder IInfrastructure.Instance => PropertyBuilder; + PropertyBuilder? IInfrastructure.Instance => PropertyBuilder; #region Hidden System.Object members diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs index 3e14e8158f8..a628a28c48a 100644 --- a/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs @@ -52,7 +52,11 @@ protected override void ProcessModelAnnotations( if (runtime) { annotations[RelationalAnnotationNames.RelationalModel] = - RelationalModel.Create(runtimeModel, RelationalDependencies.RelationalAnnotationProvider, designTime: false); + RelationalModel.Create( + runtimeModel, + RelationalDependencies.RelationalAnnotationProvider, + (IRelationalTypeMappingSource)Dependencies.TypeMappingSource, + designTime: false); } else { @@ -488,12 +492,50 @@ protected virtual void ProcessTriggerAnnotations( { } - private static RuntimeStoredProcedure Create(IStoredProcedure storedProcedure, RuntimeEntityType runtimeEntityType) - => new(runtimeEntityType, + private RuntimeStoredProcedure Create(IStoredProcedure storedProcedure, RuntimeEntityType runtimeEntityType) + { + var runtimeStoredProcedure = new RuntimeStoredProcedure( + runtimeEntityType, storedProcedure.Name, storedProcedure.Schema, + storedProcedure.AreRowsAffectedReturned, storedProcedure.AreTransactionsSuppressed); + foreach (var parameter in storedProcedure.Parameters) + { + var runtimeParameter = Create(parameter, runtimeStoredProcedure); + CreateAnnotations( + parameter, runtimeParameter, static (convention, annotations, source, target, runtime) => + convention.ProcessStoredProcedureParameterAnnotations(annotations, source, target, runtime)); + } + + foreach (var resultColumn in storedProcedure.ResultColumns) + { + var runtimeResultColumn = Create(resultColumn, runtimeStoredProcedure); + CreateAnnotations( + resultColumn, runtimeResultColumn, static (convention, annotations, source, target, runtime) => + convention.ProcessStoredProcedureResultColumnAnnotations(annotations, source, target, runtime)); + } + + return runtimeStoredProcedure; + } + + private RuntimeStoredProcedureParameter Create( + IStoredProcedureParameter parameter, RuntimeStoredProcedure runtimeStoredProcedure) + => runtimeStoredProcedure.AddParameter( + parameter.Name, + parameter.Direction, + parameter.ForRowsAffected, + parameter.PropertyName, + parameter.ForOriginalValue); + + private RuntimeStoredProcedureResultColumn Create( + IStoredProcedureResultColumn resultColumn, RuntimeStoredProcedure runtimeStoredProcedure) + => runtimeStoredProcedure.AddResultColumn( + resultColumn.Name, + resultColumn.ForRowsAffected, + resultColumn.PropertyName); + /// /// Updates the stored procedure annotations that will be set on the read-only object. /// @@ -508,4 +550,34 @@ protected virtual void ProcessStoredProcedureAnnotations( bool runtime) { } + + /// + /// Updates the stored procedure parameter annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source stored procedure parameter. + /// The target stored procedure parameter that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected virtual void ProcessStoredProcedureParameterAnnotations( + Dictionary annotations, + IStoredProcedureParameter parameter, + RuntimeStoredProcedureParameter runtimeParameter, + bool runtime) + { + } + + /// + /// Updates the stored procedure result column annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source fstored procedure result column. + /// The target stored procedure result column that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected virtual void ProcessStoredProcedureResultColumnAnnotations( + Dictionary annotations, + IStoredProcedureResultColumn resultColumn, + RuntimeStoredProcedureResultColumn runtimeResultColumn, + bool runtime) + { + } } diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalValueGenerationConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalValueGenerationConvention.cs index d22c6816cd5..214367c5dc5 100644 --- a/src/EFCore.Relational/Metadata/Conventions/RelationalValueGenerationConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalValueGenerationConvention.cs @@ -76,82 +76,73 @@ public virtual void ProcessEntityTypeAnnotationChanged( IConventionAnnotation? oldAnnotation, IConventionContext context) { - if (name == RelationalAnnotationNames.ViewName - || name == RelationalAnnotationNames.FunctionName - || name == RelationalAnnotationNames.SqlQuery) + var entityType = entityTypeBuilder.Metadata; + switch (name) { - if (annotation?.Value != null - && oldAnnotation?.Value == null - && entityTypeBuilder.Metadata.GetTableName() == null) - { + case RelationalAnnotationNames.ViewName: + case RelationalAnnotationNames.FunctionName: + case RelationalAnnotationNames.SqlQuery: + case RelationalAnnotationNames.InsertStoredProcedure: + if (annotation?.Value != null + && oldAnnotation?.Value == null + && entityType.GetTableName() == null) + { + ProcessTableChanged( + entityTypeBuilder, + entityType.GetDefaultTableName(), + entityType.GetDefaultSchema(), + null, + null); + } + break; + + case RelationalAnnotationNames.TableName: + var schema = entityType.GetSchema(); ProcessTableChanged( entityTypeBuilder, - entityTypeBuilder.Metadata.GetDefaultTableName(), - entityTypeBuilder.Metadata.GetDefaultSchema(), - null, - null); - } - } - else if (name == RelationalAnnotationNames.TableName) - { - var schema = entityTypeBuilder.Metadata.GetSchema(); - ProcessTableChanged( - entityTypeBuilder, - (string?)oldAnnotation?.Value ?? entityTypeBuilder.Metadata.GetDefaultTableName(), - schema, - entityTypeBuilder.Metadata.GetTableName(), - schema); - } - else if (name == RelationalAnnotationNames.Schema) - { - var tableName = entityTypeBuilder.Metadata.GetTableName(); - ProcessTableChanged( - entityTypeBuilder, - tableName, - (string?)oldAnnotation?.Value ?? entityTypeBuilder.Metadata.GetDefaultSchema(), - tableName, - entityTypeBuilder.Metadata.GetSchema()); - } - else if (name == RelationalAnnotationNames.MappingStrategy) - { - var primaryKey = entityTypeBuilder.Metadata.FindPrimaryKey(); - if (primaryKey == null) - { - return; - } + (string?)oldAnnotation?.Value ?? entityType.GetDefaultTableName(), + schema, + entityType.GetTableName(), + schema); + break; + + case RelationalAnnotationNames.Schema: + var tableName = entityType.GetTableName(); + ProcessTableChanged( + entityTypeBuilder, + tableName, + (string?)oldAnnotation?.Value ?? entityType.GetDefaultSchema(), + tableName, + entityTypeBuilder.Metadata.GetSchema()); + break; + + case RelationalAnnotationNames.MappingStrategy: + var primaryKey = entityTypeBuilder.Metadata.FindPrimaryKey(); + if (primaryKey == null) + { + return; + } - foreach (var property in primaryKey.Properties) - { - property.Builder.ValueGenerated(GetValueGenerated(property)); - } + foreach (var property in primaryKey.Properties) + { + property.Builder.ValueGenerated(GetValueGenerated(property)); + } + break; } } - private static void ProcessTableChanged( + private void ProcessTableChanged( IConventionEntityTypeBuilder entityTypeBuilder, string? oldTable, string? oldSchema, string? newTable, string? newSchema) { - if (newTable == null) + if (newTable == null || oldTable == null) { - if (entityTypeBuilder.Metadata.GetDerivedTypes().All(e => e.GetTableName() == null)) + foreach (var property in entityTypeBuilder.Metadata.GetDeclaredProperties()) { - foreach (var property in entityTypeBuilder.Metadata.GetProperties()) - { - property.Builder.ValueGenerated(null); - } - } - - return; - } - - if (oldTable == null) - { - foreach (var property in entityTypeBuilder.Metadata.GetProperties()) - { - property.Builder.ValueGenerated(GetValueGenerated(property, StoreObjectIdentifier.Table(newTable, newSchema))); + property.Builder.ValueGenerated(GetValueGenerated(property)); } return; @@ -174,7 +165,7 @@ private static void ProcessTableChanged( foreach (var property in primaryKey.Properties) { - property.Builder.ValueGenerated(GetValueGenerated(property, StoreObjectIdentifier.Table(newTable, newSchema))); + property.Builder.ValueGenerated(GetValueGenerated(property)); } } @@ -185,15 +176,16 @@ private static void ProcessTableChanged( /// The store value generation strategy to set for the given property. protected override ValueGenerated? GetValueGenerated(IConventionProperty property) { - var tableName = property.DeclaringEntityType.GetTableName(); - - return tableName == null + var table = property.GetMappedStoreObjects(StoreObjectType.Table).FirstOrDefault(); + return !MappingStrategyAllowsValueGeneration(property, property.DeclaringEntityType.GetMappingStrategy()) ? null - : !MappingStrategyAllowsValueGeneration(property, property.DeclaringEntityType.GetMappingStrategy()) - ? null - : GetValueGenerated(property, StoreObjectIdentifier.Table(tableName, property.DeclaringEntityType.GetSchema())); + : table.Name != null + ? GetValueGenerated(property, table) + : property.GetMappedStoreObjects(StoreObjectType.InsertStoredProcedure).Any() + ? GetValueGenerated((IReadOnlyProperty)property) + : null; } - + /// /// Checks whether or not the mapping strategy and property allow value generation by convention. /// diff --git a/src/EFCore.Relational/Metadata/IColumnBase.cs b/src/EFCore.Relational/Metadata/IColumnBase.cs index af8da463f5a..725636e9e08 100644 --- a/src/EFCore.Relational/Metadata/IColumnBase.cs +++ b/src/EFCore.Relational/Metadata/IColumnBase.cs @@ -26,6 +26,11 @@ public interface IColumnBase : IAnnotatable /// Type ProviderClrType { get; } + /// + /// Gets the type mapping for the column-like object. + /// + RelationalTypeMapping StoreTypeMapping { get; } + /// /// Gets the value indicating whether the column can contain NULL. /// diff --git a/src/EFCore.Relational/Metadata/IConventionDbFunctionParameter.cs b/src/EFCore.Relational/Metadata/IConventionDbFunctionParameter.cs index 2e3ad31db5e..de0fdb3a07f 100644 --- a/src/EFCore.Relational/Metadata/IConventionDbFunctionParameter.cs +++ b/src/EFCore.Relational/Metadata/IConventionDbFunctionParameter.cs @@ -4,15 +4,15 @@ namespace Microsoft.EntityFrameworkCore.Metadata; /// -/// Represents a parameter. +/// Represents a function parameter. /// /// /// See Database functions for more information and examples. /// -public interface IConventionDbFunctionParameter : IConventionAnnotatable, IReadOnlyDbFunctionParameter +public interface IConventionDbFunctionParameter : IReadOnlyDbFunctionParameter, IConventionAnnotatable { /// - /// The to which this parameter belongs. + /// The function to which this parameter belongs. /// new IConventionDbFunction Function { get; } diff --git a/src/EFCore.Relational/Metadata/IConventionRelationalPropertyOverrides.cs b/src/EFCore.Relational/Metadata/IConventionRelationalPropertyOverrides.cs index 687b4e490b7..6bd4ebd0d16 100644 --- a/src/EFCore.Relational/Metadata/IConventionRelationalPropertyOverrides.cs +++ b/src/EFCore.Relational/Metadata/IConventionRelationalPropertyOverrides.cs @@ -50,20 +50,4 @@ 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 1e83d739f97..e50a9e60e21 100644 --- a/src/EFCore.Relational/Metadata/IConventionStoredProcedure.cs +++ b/src/EFCore.Relational/Metadata/IConventionStoredProcedure.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.EntityFrameworkCore.Query.SqlExpressions; - namespace Microsoft.EntityFrameworkCore.Metadata; /// @@ -19,19 +17,19 @@ public interface IConventionStoredProcedure : IReadOnlyStoredProcedure, IConvent /// /// Gets the builder that can be used to configure this stored procedure. /// - /// If the function has been removed from the model. + /// If the stored procedure has been removed from the model. new IConventionStoredProcedureBuilder Builder { get; } /// /// Gets the configuration source for this stored procedure. /// - /// The configuration source for this function. + /// The configuration source for this stored procedure. ConfigurationSource GetConfigurationSource(); /// /// Sets the name of the stored procedure in the database. /// - /// The name of the function in the database. + /// The name of the stored procedure in the database. /// Indicates whether the configuration was specified using a data annotation. /// The configured value. string? SetName(string? name, bool fromDataAnnotation = false); @@ -45,7 +43,7 @@ public interface IConventionStoredProcedure : IReadOnlyStoredProcedure, IConvent /// /// Sets the schema of the stored procedure in the database. /// - /// The schema of the function in the database. + /// The schema of the stored procedure in the database. /// Indicates whether the configuration was specified using a data annotation. /// The configured value. string? SetSchema(string? schema, bool fromDataAnnotation = false); @@ -56,21 +54,98 @@ public interface IConventionStoredProcedure : IReadOnlyStoredProcedure, IConvent /// The configuration source for . ConfigurationSource? GetSchemaConfigurationSource(); + /// + /// Gets the parameters for this stored procedure. + /// + new IReadOnlyList Parameters { get; } + + /// + /// Returns the parameter corresponding to the given property. + /// + /// The name of a property. + /// The parameter corresponding to the given property if found; otherwise. + new IConventionStoredProcedureParameter? FindParameter(string propertyName); + /// /// Adds a new parameter mapped to the property with the given name. /// /// The name of the corresponding property. /// Indicates whether the configuration was specified using a data annotation. - /// The configured value. - string? AddParameter(string propertyName, bool fromDataAnnotation = false); + /// The added parameter. + IConventionStoredProcedureParameter? AddParameter(string propertyName, bool fromDataAnnotation = false); + + /// + /// Returns the original value parameter corresponding to the given property. + /// + /// The name of a property. + /// + /// The original value parameter corresponding to the given property if found; otherwise. + /// + new IConventionStoredProcedureParameter? FindOriginalValueParameter(string propertyName); + + /// + /// Adds a new parameter that will hold the original value of the property with the given name. + /// + /// The name of the corresponding property. + /// Indicates whether the configuration was specified using a data annotation. + /// The added parameter. + IConventionStoredProcedureParameter? AddOriginalValueParameter(string propertyName, bool fromDataAnnotation = false); + + /// + /// Returns the rows affected parameter. + /// + /// + /// The rows affected parameter if found; otherwise. + /// + new IConventionStoredProcedureParameter? FindRowsAffectedParameter(); + + /// + /// Adds an output parameter that returns the rows affected by this stored procedure. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// The added parameter. + IConventionStoredProcedureParameter? AddRowsAffectedParameter(bool fromDataAnnotation = false); + + /// + /// Gets the columns of the result for this stored procedure. + /// + new IReadOnlyList ResultColumns { get; } + + /// + /// Returns the result column corresponding to the given property. + /// + /// The name of a property. + /// The result column corresponding to the given property if found; otherwise. + new IConventionStoredProcedureResultColumn? FindResultColumn(string propertyName); /// /// Adds a new column of the result for this stored procedure mapped to the property with the given name /// /// The name of the corresponding property. /// Indicates whether the configuration was specified using a data annotation. + /// The added column. + IConventionStoredProcedureResultColumn? AddResultColumn(string propertyName, bool fromDataAnnotation = false); + + /// + /// Returns the rows affected result column. + /// + /// The rows affected result column if found; otherwise. + new IConventionStoredProcedureResultColumn? FindRowsAffectedResultColumn(); + + /// + /// Adds a new column of the result that contains the rows affected by this stored procedure. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// The added column. + IConventionStoredProcedureResultColumn? AddRowsAffectedResultColumn(bool fromDataAnnotation = false); + + /// + /// Configures whether this stored procedure returns the number of rows affected. + /// + /// A value indicating whether the number of rows affected is returned. + /// Indicates whether the configuration was specified using a data annotation. /// The configured value. - string? AddResultColumn(string propertyName, bool fromDataAnnotation = false); + bool SetAreRowsAffectedReturned(bool rowsAffectedReturned, bool fromDataAnnotation = false); /// /// Prevents automatically creating a transaction when executing this stored procedure. diff --git a/src/EFCore.Relational/Metadata/IConventionStoredProcedureParameter.cs b/src/EFCore.Relational/Metadata/IConventionStoredProcedureParameter.cs new file mode 100644 index 00000000000..b33d2619f3f --- /dev/null +++ b/src/EFCore.Relational/Metadata/IConventionStoredProcedureParameter.cs @@ -0,0 +1,49 @@ +// 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; + +/// +/// Represents a stored procedure parameter. +/// +public interface IConventionStoredProcedureParameter : IReadOnlyStoredProcedureParameter, IConventionAnnotatable +{ + /// + /// Gets the stored procedure to which this parameter belongs. + /// + new IConventionStoredProcedure StoredProcedure { get; } + + /// + /// Gets the builder that can be used to configure this stored procedure parameter. + /// + /// If the stored procedure parameter has been removed from the model. + new IConventionStoredProcedureParameterBuilder Builder { get; } + + /// + /// Sets the parameter name. + /// + /// The parameter name. + /// Indicates whether the configuration was specified using a data annotation. + string SetName(string name, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetNameConfigurationSource(); + + /// + /// Sets the direction of the parameter. + /// + /// The direction of the parameter. + /// Indicates whether the configuration was specified using a data annotation. + ParameterDirection SetDirection(ParameterDirection direction, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetDirectionConfigurationSource(); +} diff --git a/src/EFCore.Relational/Metadata/IConventionStoredProcedureResultColumn.cs b/src/EFCore.Relational/Metadata/IConventionStoredProcedureResultColumn.cs new file mode 100644 index 00000000000..1f7a51ad6bb --- /dev/null +++ b/src/EFCore.Relational/Metadata/IConventionStoredProcedureResultColumn.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Metadata; + +/// +/// Represents a stored procedure result column. +/// +public interface IConventionStoredProcedureResultColumn : IReadOnlyStoredProcedureResultColumn, IConventionAnnotatable +{ + /// + /// Gets the stored procedure to which this result column belongs. + /// + new IConventionStoredProcedure StoredProcedure { get; } + + /// + /// Gets the builder that can be used to configure this result column. + /// + /// If the stored procedure result column has been removed from the model. + new IConventionStoredProcedureResultColumnBuilder Builder { get; } + + /// + /// Sets the result column name. + /// + /// The result column name. + /// Indicates whether the configuration was specified using a data annotation. + string? SetName(string name, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetNameConfigurationSource(); +} diff --git a/src/EFCore.Relational/Metadata/IMutableDbFunctionParameter.cs b/src/EFCore.Relational/Metadata/IMutableDbFunctionParameter.cs index 75d96e6f473..8fa52d850f7 100644 --- a/src/EFCore.Relational/Metadata/IMutableDbFunctionParameter.cs +++ b/src/EFCore.Relational/Metadata/IMutableDbFunctionParameter.cs @@ -4,7 +4,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata; /// -/// Represents a parameter. +/// Represents a function parameter. /// /// /// See Database functions for more information and examples. @@ -12,7 +12,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata; public interface IMutableDbFunctionParameter : IReadOnlyDbFunctionParameter, IMutableAnnotatable { /// - /// Gets the to which this parameter belongs. + /// Gets the function to which this parameter belongs. /// new IMutableDbFunction Function { get; } diff --git a/src/EFCore.Relational/Metadata/IMutableRelationalPropertyOverrides.cs b/src/EFCore.Relational/Metadata/IMutableRelationalPropertyOverrides.cs index badb6b3eb2f..24055085c2d 100644 --- a/src/EFCore.Relational/Metadata/IMutableRelationalPropertyOverrides.cs +++ b/src/EFCore.Relational/Metadata/IMutableRelationalPropertyOverrides.cs @@ -23,15 +23,6 @@ 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 25643f7622c..f2779df60f4 100644 --- a/src/EFCore.Relational/Metadata/IMutableStoredProcedure.cs +++ b/src/EFCore.Relational/Metadata/IMutableStoredProcedure.cs @@ -32,17 +32,88 @@ public interface IMutableStoredProcedure : IReadOnlyStoredProcedure, IMutableAnn /// The configured value. new bool AreTransactionsSuppressed { get; set; } + /// + /// Gets or sets a value indicating whether this stored procedure returns the number of rows affected. + /// + new bool AreRowsAffectedReturned { get; set; } + + /// + /// Gets the parameters for this stored procedure. + /// + new IReadOnlyList Parameters { get; } + + /// + /// Returns the parameter corresponding to the given property. + /// + /// The name of a property. + /// The parameter corresponding to the given property if found; otherwise. + new IMutableStoredProcedureParameter? FindParameter(string propertyName); + /// /// Adds a new parameter mapped to the property with the given name. /// /// The name of the corresponding property. - /// if a parameter was added. - bool AddParameter(string propertyName); + /// The added parameter. + IMutableStoredProcedureParameter AddParameter(string propertyName); + + /// + /// Returns the original value parameter corresponding to the given property. + /// + /// The name of a property. + /// + /// The original value parameter corresponding to the given property if found; otherwise. + /// + new IMutableStoredProcedureParameter? FindOriginalValueParameter(string propertyName); + + /// + /// Adds a new parameter that holds the original value of the property with the given name. + /// + /// The name of the corresponding property. + /// The added parameter. + IMutableStoredProcedureParameter AddOriginalValueParameter(string propertyName); + + /// + /// Returns the rows affected parameter. + /// + /// + /// The rows affected parameter if found; otherwise. + /// + new IMutableStoredProcedureParameter? FindRowsAffectedParameter(); + + /// + /// Adds an output parameter that returns the rows affected by this stored procedure. + /// + /// The added parameter. + IMutableStoredProcedureParameter AddRowsAffectedParameter(); + + /// + /// Gets the columns of the result for this stored procedure. + /// + new IReadOnlyList ResultColumns { get; } + + /// + /// Returns the result column corresponding to the given property. + /// + /// The name of a property. + /// The result column corresponding to the given property if found; otherwise. + new IMutableStoredProcedureResultColumn? FindResultColumn(string propertyName); /// /// Adds a new column of the result for this stored procedure mapped to the property with the given name /// /// The name of the corresponding property. - /// if a column was added. - bool AddResultColumn(string propertyName); + /// The added column. + IMutableStoredProcedureResultColumn AddResultColumn(string propertyName); + + /// + /// Returns the rows affected result column. + /// + /// The rows affected result column if found; otherwise. + new IMutableStoredProcedureResultColumn? FindRowsAffectedResultColumn(); + + /// + /// Adds a new column of the result that contains the rows affected by this stored procedure. + /// + /// The added column. + IMutableStoredProcedureResultColumn AddRowsAffectedResultColumn(); } diff --git a/src/EFCore.Relational/Metadata/IMutableStoredProcedureParameter.cs b/src/EFCore.Relational/Metadata/IMutableStoredProcedureParameter.cs new file mode 100644 index 00000000000..06a4a32a9c8 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IMutableStoredProcedureParameter.cs @@ -0,0 +1,27 @@ +// 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; + +/// +/// Represents a stored procedure parameter. +/// +public interface IMutableStoredProcedureParameter : IReadOnlyStoredProcedureParameter, IMutableAnnotatable +{ + /// + /// Gets the stored procedure to which this parameter belongs. + /// + new IMutableStoredProcedure StoredProcedure { get; } + + /// + /// Gets or sets the parameter name. + /// + new string Name { get; set; } + + /// + /// Gets or sets the direction of the parameter. + /// + new ParameterDirection Direction { get; set; } +} diff --git a/src/EFCore.Relational/Metadata/IMutableStoredProcedureResultColumn.cs b/src/EFCore.Relational/Metadata/IMutableStoredProcedureResultColumn.cs new file mode 100644 index 00000000000..038b42a086c --- /dev/null +++ b/src/EFCore.Relational/Metadata/IMutableStoredProcedureResultColumn.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Metadata; + +/// +/// Represents a stored procedure result column. +/// +public interface IMutableStoredProcedureResultColumn : IReadOnlyStoredProcedureResultColumn, IMutableAnnotatable +{ + /// + /// Gets the stored procedure to which this result column belongs. + /// + new IMutableStoredProcedure StoredProcedure { get; } + + /// + /// Gets or sets the result column name. + /// + new string Name { get; set; } +} diff --git a/src/EFCore.Relational/Metadata/IReadOnlyDbFunctionParameter.cs b/src/EFCore.Relational/Metadata/IReadOnlyDbFunctionParameter.cs index 855e388f035..dff4400a1f6 100644 --- a/src/EFCore.Relational/Metadata/IReadOnlyDbFunctionParameter.cs +++ b/src/EFCore.Relational/Metadata/IReadOnlyDbFunctionParameter.cs @@ -14,7 +14,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata; public interface IReadOnlyDbFunctionParameter : IReadOnlyAnnotatable { /// - /// Gets the to which this parameter belongs. + /// Gets the function to which this parameter belongs. /// IReadOnlyDbFunction Function { get; } diff --git a/src/EFCore.Relational/Metadata/IReadOnlyRelationalPropertyOverrides.cs b/src/EFCore.Relational/Metadata/IReadOnlyRelationalPropertyOverrides.cs index 3d5cf0bb282..9a82eec8229 100644 --- a/src/EFCore.Relational/Metadata/IReadOnlyRelationalPropertyOverrides.cs +++ b/src/EFCore.Relational/Metadata/IReadOnlyRelationalPropertyOverrides.cs @@ -34,12 +34,6 @@ 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 ba911ba0411..b8a30347cd4 100644 --- a/src/EFCore.Relational/Metadata/IReadOnlyStoredProcedure.cs +++ b/src/EFCore.Relational/Metadata/IReadOnlyStoredProcedure.cs @@ -28,8 +28,12 @@ public interface IReadOnlyStoredProcedure : IReadOnlyAnnotatable /// /// Returns a value indicating whether automatic creation of transactions is disabled when executing this stored procedure. /// - /// The configured value. bool AreTransactionsSuppressed { get; } + + /// + /// Gets a value indicating whether this stored procedure returns the number of rows affected. + /// + bool AreRowsAffectedReturned { get; } /// /// Returns the store identifier of this stored procedure. @@ -62,28 +66,51 @@ public interface IReadOnlyStoredProcedure : IReadOnlyAnnotatable } /// - /// Gets the names of properties mapped to parameters for this stored procedure. + /// Gets the parameters for this stored procedure. + /// + IReadOnlyList Parameters { get; } + + /// + /// Returns the parameter corresponding to the given property. /// - IReadOnlyList Parameters { get; } + /// The name of a property. + /// The parameter corresponding to the given property if found; otherwise. + IReadOnlyStoredProcedureParameter? FindParameter(string propertyName); /// - /// Returns a value indicating whether there is a parameter corresponding to the given property. + /// Returns the original value parameter corresponding to the given property. /// /// The name of a property. - /// if a parameter corresponding to the given property is found. - bool ContainsParameter(string propertyName); + /// + /// The original value parameter corresponding to the given property if found; otherwise. + /// + IReadOnlyStoredProcedureParameter? FindOriginalValueParameter(string propertyName); /// - /// Gets the names of properties mapped to columns of the result for this stored procedure. + /// Returns the rows affected parameter. /// - IReadOnlyList ResultColumns { get; } + /// + /// The rows affected parameter if found; otherwise. + /// + IReadOnlyStoredProcedureParameter? FindRowsAffectedParameter(); /// - /// Returns a value indicating whether there is a column of the result corresponding to the given property. + /// Gets the columns of the result for this stored procedure. + /// + IReadOnlyList ResultColumns { get; } + + /// + /// Returns the result column corresponding to the given property. /// /// The name of a property. - /// if a columns of the result corresponding to the given property is found. - bool ContainsResultColumn(string propertyName); + /// The result column corresponding to the given property if found; otherwise. + IReadOnlyStoredProcedureResultColumn? FindResultColumn(string propertyName); + + /// + /// Returns the rows affected result column. + /// + /// The rows affected result column if found; otherwise. + IReadOnlyStoredProcedureResultColumn? FindRowsAffectedResultColumn(); /// /// Returns the name of the stored procedure prepended by the schema diff --git a/src/EFCore.Relational/Metadata/IReadOnlyStoredProcedureParameter.cs b/src/EFCore.Relational/Metadata/IReadOnlyStoredProcedureParameter.cs new file mode 100644 index 00000000000..e6448b55ec9 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IReadOnlyStoredProcedureParameter.cs @@ -0,0 +1,93 @@ +// 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 stored procedure parameter. +/// +public interface IReadOnlyStoredProcedureParameter : IReadOnlyAnnotatable +{ + /// + /// Gets the stored procedure to which this parameter belongs. + /// + IReadOnlyStoredProcedure StoredProcedure { get; } + + /// + /// Gets the parameter name. + /// + string Name { get; } + + /// + /// Gets the name of property mapped to this parameter. + /// + string? PropertyName { get; } + + /// + /// Gets the direction of the parameter. + /// + ParameterDirection Direction { get; } + + /// + /// Gets a value indicating whether the parameter holds the original or the current property value. + /// + bool? ForOriginalValue { get; } + + /// + /// Gets a value indicating whether the parameter holds the rows affected by the stored procedure. + /// + bool ForRowsAffected { 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) + .Append("StoredProcedureParameter: "); + + builder.Append(Name); + + if (Direction != ParameterDirection.Input) + { + builder.Append(' ') + .Append(Direction); + } + + if (ForOriginalValue == true) + { + builder.Append(" ForOriginalValue"); + } + + if (ForRowsAffected) + { + builder.Append(" ForRowsAffected"); + } + + if ((options & MetadataDebugStringOptions.SingleLine) == 0) + { + if ((options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(AnnotationsToDebugString(indent + 2)); + } + } + + return builder.ToString(); + } +} diff --git a/src/EFCore.Relational/Metadata/IReadOnlyStoredProcedureResultColumn.cs b/src/EFCore.Relational/Metadata/IReadOnlyStoredProcedureResultColumn.cs new file mode 100644 index 00000000000..e4b43ff4fc5 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IReadOnlyStoredProcedureResultColumn.cs @@ -0,0 +1,66 @@ +// 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 result column. +/// +public interface IReadOnlyStoredProcedureResultColumn : IReadOnlyAnnotatable +{ + /// + /// Gets the stored procedure to which this result column belongs. + /// + IReadOnlyStoredProcedure StoredProcedure { get; } + + /// + /// Gets the result column name. + /// + string Name { get; } + + /// + /// Gets the name of property mapped to this result column. + /// + string? PropertyName { get; } + + /// + /// Gets a value indicating whether the result column will hold the rows affected by the stored procedure. + /// + bool ForRowsAffected { 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) + .Append("StoredProcedureResultColumn: "); + + builder.Append(Name); + + if ((options & MetadataDebugStringOptions.SingleLine) == 0) + { + if ((options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(AnnotationsToDebugString(indent + 2)); + } + } + + return builder.ToString(); + } +} diff --git a/src/EFCore.Relational/Metadata/IStoreStoredProcedure.cs b/src/EFCore.Relational/Metadata/IStoreStoredProcedure.cs index b78c5b8c360..c4f8c692c6a 100644 --- a/src/EFCore.Relational/Metadata/IStoreStoredProcedure.cs +++ b/src/EFCore.Relational/Metadata/IStoreStoredProcedure.cs @@ -19,11 +19,16 @@ public interface IStoreStoredProcedure : ITableBase /// Gets the entity type mappings. /// new IEnumerable EntityTypeMappings { get; } - + + /// + /// Gets the return for this stored procedure. + /// + IStoreStoredProcedureReturn? Return { get; } + /// - /// Gets the parameters for this stored procedures. + /// Gets the parameters for this stored procedure. /// - IEnumerable Parameters { get; } + IReadOnlyList Parameters { get; } /// /// Gets the parameter with the given name. Returns diff --git a/src/EFCore.Relational/Metadata/IStoreStoredProcedureParameter.cs b/src/EFCore.Relational/Metadata/IStoreStoredProcedureParameter.cs index de157b328e4..e2c3b992ae8 100644 --- a/src/EFCore.Relational/Metadata/IStoreStoredProcedureParameter.cs +++ b/src/EFCore.Relational/Metadata/IStoreStoredProcedureParameter.cs @@ -26,6 +26,11 @@ public interface IStoreStoredProcedureParameter : IColumnBase /// ParameterDirection Direction { get; } + /// + /// Gets the 0-based position of the parameter in the declaring stored procedure. + /// + int Position { get; } + /// /// Returns the property mapping for the given entity type. /// diff --git a/src/EFCore.Relational/Metadata/IStoreStoredProcedureReturn.cs b/src/EFCore.Relational/Metadata/IStoreStoredProcedureReturn.cs new file mode 100644 index 00000000000..6674dcfde6c --- /dev/null +++ b/src/EFCore.Relational/Metadata/IStoreStoredProcedureReturn.cs @@ -0,0 +1,55 @@ +// 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 the return value of a stored procedure. +/// +public interface IStoreStoredProcedureReturn : IColumnBase +{ + /// + /// Gets the containing stored procedure. + /// + IStoreStoredProcedure StoredProcedure { 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($"StoreStoredProcedureReturn: {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 3b0cf61e6cf..12d36a6ec54 100644 --- a/src/EFCore.Relational/Metadata/IStoredProcedure.cs +++ b/src/EFCore.Relational/Metadata/IStoredProcedure.cs @@ -23,11 +23,57 @@ public interface IStoredProcedure : IReadOnlyStoredProcedure, IAnnotatable /// IStoreStoredProcedure StoreStoredProcedure { get; } + /// + /// Gets the parameters for this stored procedure. + /// + new IReadOnlyList Parameters { get; } + + /// + /// Returns the parameter corresponding to the given property. + /// + /// The name of a property. + /// The parameter corresponding to the given property if found; otherwise. + new IStoredProcedureParameter? FindParameter(string propertyName); + + /// + /// Returns the original value parameter corresponding to the given property. + /// + /// The name of a property. + /// + /// The original value parameter corresponding to the given property if found; otherwise. + /// + new IStoredProcedureParameter? FindOriginalValueParameter(string propertyName); + + /// + /// Returns the rows affected parameter. + /// + /// + /// The rows affected parameter if found; otherwise. + /// + new IStoredProcedureParameter? FindRowsAffectedParameter(); + + /// + /// Gets the columns of the result for this stored procedure. + /// + new IReadOnlyList ResultColumns { get; } + + /// + /// Returns the result column corresponding to the given property. + /// + /// The name of a property. + /// The result column corresponding to the given property if found; otherwise. + new IStoredProcedureResultColumn? FindResultColumn(string propertyName); + + /// + /// Returns the rows affected result column. + /// > + /// The rows affected result column if found; otherwise. + new IStoredProcedureResultColumn? FindRowsAffectedResultColumn(); + /// /// 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 index 5a29707021a..d50c7540d63 100644 --- a/src/EFCore.Relational/Metadata/IStoredProcedureMapping.cs +++ b/src/EFCore.Relational/Metadata/IStoredProcedureMapping.cs @@ -25,6 +25,11 @@ public interface IStoredProcedureMapping : ITableMappingBase /// StoreObjectIdentifier StoredProcedureIdentifier { get; } + /// + /// Gets the corresponding table mapping if it exists. + /// + ITableMapping? TableMapping { get; } + /// /// Gets the parameter mappings corresponding to the target stored procedure. /// diff --git a/src/EFCore.Relational/Metadata/IStoredProcedureParameter.cs b/src/EFCore.Relational/Metadata/IStoredProcedureParameter.cs new file mode 100644 index 00000000000..9c7e591bfc6 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IStoredProcedureParameter.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Metadata; + +/// +/// Represents a stored procedure parameter. +/// +public interface IStoredProcedureParameter : IReadOnlyStoredProcedureParameter, IAnnotatable +{ + /// + /// Gets the stored procedure to which this parameter belongs. + /// + new IStoredProcedure StoredProcedure { get; } + + /// + /// Gets the associated database stored procedure parameter. + /// + IStoreStoredProcedureParameter StoreParameter { get; } +} diff --git a/src/EFCore.Relational/Metadata/IStoredProcedureParameterMapping.cs b/src/EFCore.Relational/Metadata/IStoredProcedureParameterMapping.cs index f3f38d5817c..1f2d367e4bb 100644 --- a/src/EFCore.Relational/Metadata/IStoredProcedureParameterMapping.cs +++ b/src/EFCore.Relational/Metadata/IStoredProcedureParameterMapping.cs @@ -13,7 +13,12 @@ public interface IStoredProcedureParameterMapping : IColumnMappingBase /// /// Gets the target parameter. /// - IStoreStoredProcedureParameter Parameter { get; } + IStoreStoredProcedureParameter StoreParameter { get; } + + /// + /// Gets the associated stored procedure parameter. + /// + IStoredProcedureParameter Parameter { get; } /// /// Gets the containing stored procedure mapping. diff --git a/src/EFCore.Relational/Metadata/IStoredProcedureResultColumn.cs b/src/EFCore.Relational/Metadata/IStoredProcedureResultColumn.cs new file mode 100644 index 00000000000..1882b19d977 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IStoredProcedureResultColumn.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Metadata; + +/// +/// Represents a stored procedure result column. +/// +public interface IStoredProcedureResultColumn : IReadOnlyStoredProcedureResultColumn, IAnnotatable +{ + /// + /// Gets the stored procedure to which this parameter belongs. + /// + new IStoredProcedure StoredProcedure { get; } + + /// + /// Gets the associated database stored procedure result column. + /// + IStoreStoredProcedureResultColumn StoreResultColumn { get; } +} diff --git a/src/EFCore.Relational/Metadata/IStoredProcedureResultColumnMapping.cs b/src/EFCore.Relational/Metadata/IStoredProcedureResultColumnMapping.cs index fc4ce0ed160..e10ff97bfa0 100644 --- a/src/EFCore.Relational/Metadata/IStoredProcedureResultColumnMapping.cs +++ b/src/EFCore.Relational/Metadata/IStoredProcedureResultColumnMapping.cs @@ -13,7 +13,12 @@ public interface IStoredProcedureResultColumnMapping : IColumnMappingBase /// /// Gets the target column. /// - new IStoreStoredProcedureResultColumn Column { get; } + IStoreStoredProcedureResultColumn StoreResultColumn { get; } + + /// + /// Gets the associated stored procedure result column. + /// + IStoredProcedureResultColumn ResultColumn { get; } /// /// Gets the containing stored procedure mapping. diff --git a/src/EFCore.Relational/Metadata/ITableMapping.cs b/src/EFCore.Relational/Metadata/ITableMapping.cs index 732c08d3756..3605b5f8d00 100644 --- a/src/EFCore.Relational/Metadata/ITableMapping.cs +++ b/src/EFCore.Relational/Metadata/ITableMapping.cs @@ -22,6 +22,21 @@ public interface ITableMapping : ITableMappingBase /// Gets the properties mapped to columns on the target table. /// new IEnumerable ColumnMappings { get; } + + /// + /// Gets the corresponding insert stored procedure mapping if it exists. + /// + IStoredProcedureMapping? InsertStoredProcedureMapping { get; } + + /// + /// Gets the corresponding insert stored procedure mapping if it exists. + /// + IStoredProcedureMapping? DeleteStoredProcedureMapping { get; } + + /// + /// Gets the corresponding insert stored procedure mapping if it exists. + /// + IStoredProcedureMapping? UpdateStoredProcedureMapping { get; } /// /// diff --git a/src/EFCore.Relational/Metadata/Internal/ColumnBase.cs b/src/EFCore.Relational/Metadata/Internal/ColumnBase.cs index 8e59938a61b..42ebec03d57 100644 --- a/src/EFCore.Relational/Metadata/Internal/ColumnBase.cs +++ b/src/EFCore.Relational/Metadata/Internal/ColumnBase.cs @@ -83,12 +83,21 @@ public virtual Type ProviderClrType return _providerClrType; } - var typeMapping = PropertyMappings.First().TypeMapping; + var typeMapping = StoreTypeMapping; var providerType = typeMapping.Converter?.ProviderClrType ?? typeMapping.ClrType; return _providerClrType = providerType; } } + + /// + /// 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 RelationalTypeMapping StoreTypeMapping + => PropertyMappings.First().TypeMapping; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs index b1878d8a0ae..54c6c4e1494 100644 --- a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs +++ b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs @@ -139,8 +139,13 @@ public static string GetFunctionName(MethodInfo methodInfo) return builder.ToString(); } - - /// + + /// + /// 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 IMutableModel Model { get; } /// @@ -174,7 +179,10 @@ public virtual void SetRemovedFromModel() => _builder = null; /// - /// Indicates whether the function is read-only. + /// 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 bool IsReadOnly => ((Annotatable)Model).IsReadOnly; diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs b/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs index a26f7aa3df7..e3b489f64fe 100644 --- a/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs +++ b/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs @@ -40,7 +40,7 @@ public DbFunctionParameter( Name = name; Function = function; ClrType = clrType; - _builder = new InternalDbFunctionParameterBuilder(this, function.Builder.ModelBuilder); + _builder = new(this, function.Builder.ModelBuilder); } /// @@ -74,7 +74,10 @@ public virtual void SetRemovedFromModel() => _builder = null; /// - /// Indicates whether the function parameter is read-only. + /// 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 bool IsReadOnly => ((Annotatable)Function.Model).IsReadOnly; @@ -86,19 +89,39 @@ public override bool IsReadOnly /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual DbFunction Function { 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 string Name { 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 Type ClrType { 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. + /// [DebuggerStepThrough] public virtual ConfigurationSource GetConfigurationSource() => Function.GetConfigurationSource(); - - /// + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// public virtual IStoreFunctionParameter StoreFunctionParameter { get; set; } = default!; /// diff --git a/src/EFCore.Relational/Metadata/Internal/IRuntimeStoredProcedureParameter.cs b/src/EFCore.Relational/Metadata/Internal/IRuntimeStoredProcedureParameter.cs new file mode 100644 index 00000000000..eb3590d3989 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/IRuntimeStoredProcedureParameter.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 IRuntimeStoredProcedureParameter : IStoredProcedureParameter +{ + /// + /// 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 IStoreStoredProcedureParameter StoreParameter { get; set; } +} diff --git a/src/EFCore.Relational/Metadata/Internal/IRuntimeStoredProcedureResultColumn.cs b/src/EFCore.Relational/Metadata/Internal/IRuntimeStoredProcedureResultColumn.cs new file mode 100644 index 00000000000..c0246ca0f2c --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/IRuntimeStoredProcedureResultColumn.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 IRuntimeStoredProcedureResultColumn : IStoredProcedureResultColumn +{ + /// + /// 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 IStoreStoredProcedureResultColumn StoreResultColumn { get; set; } +} diff --git a/src/EFCore.Relational/Metadata/Internal/InternalStoredProcedureBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalStoredProcedureBuilder.cs index b4bebbfdef0..a734c2cb3a4 100644 --- a/src/EFCore.Relational/Metadata/Internal/InternalStoredProcedureBuilder.cs +++ b/src/EFCore.Relational/Metadata/Internal/InternalStoredProcedureBuilder.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.EntityFrameworkCore.Internal; - namespace Microsoft.EntityFrameworkCore.Metadata.Internal; /// @@ -163,21 +161,22 @@ public virtual bool CanSetSchema(string? schema, ConfigurationSource configurati /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual InternalStoredProcedureBuilder? HasParameter( + public virtual InternalStoredProcedureParameterBuilder? HasParameter( string propertyName, ConfigurationSource configurationSource) { - if (!Metadata.ContainsParameter(propertyName)) + var parameter = Metadata.FindParameter(propertyName); + if (parameter == null) { if (!configurationSource.Overrides(Metadata.GetConfigurationSource())) { return null; } - Metadata.AddParameter(propertyName); + parameter = Metadata.AddParameter(propertyName); } Metadata.UpdateConfigurationSource(configurationSource); - return this; + return parameter.Builder; } /// @@ -186,7 +185,7 @@ public virtual bool CanSetSchema(string? schema, ConfigurationSource configurati /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual InternalStoredProcedureBuilder? HasParameter( + public virtual InternalStoredProcedureParameterBuilder? HasParameter( Expression> propertyExpression, ConfigurationSource configurationSource) where TDerivedEntity : class @@ -199,30 +198,122 @@ public virtual bool CanSetSchema(string? schema, ConfigurationSource configurati /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual bool CanHaveParameter(string propertyName, ConfigurationSource configurationSource) - => Metadata.ContainsParameter(propertyName) + => Metadata.FindParameter(propertyName) != null || configurationSource.Overrides(Metadata.GetConfigurationSource()); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalStoredProcedureParameterBuilder? HasOriginalValueParameter( + string propertyName, ConfigurationSource configurationSource) + { + var parameter = Metadata.FindOriginalValueParameter(propertyName); + if (parameter == null) + { + if (!configurationSource.Overrides(Metadata.GetConfigurationSource())) + { + return null; + } + + parameter = Metadata.AddOriginalValueParameter(propertyName); + } + Metadata.UpdateConfigurationSource(configurationSource); + return parameter.Builder; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalStoredProcedureParameterBuilder? HasOriginalValueParameter( + Expression> propertyExpression, + ConfigurationSource configurationSource) + where TDerivedEntity : class + => HasOriginalValueParameter(propertyExpression.GetMemberAccess().Name, configurationSource); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanHaveOriginalValueParameter(string propertyName, ConfigurationSource configurationSource) + => Metadata.FindOriginalValueParameter(propertyName) != null + || configurationSource.Overrides(Metadata.GetConfigurationSource()); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual InternalStoredProcedureBuilder? HasResultColumn( + public virtual InternalStoredProcedureParameterBuilder? HasRowsAffectedParameter( + ConfigurationSource configurationSource) + { + var parameter = Metadata.FindRowsAffectedParameter(); + if (parameter == null) + { + if (!configurationSource.Overrides(Metadata.GetConfigurationSource())) + { + return null; + } + + parameter = Metadata.AddRowsAffectedParameter(); + } + + Metadata.UpdateConfigurationSource(configurationSource); + return parameter.Builder; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalStoredProcedureParameterBuilder? HasRowsAffectedParameter( + ConfigurationSource configurationSource) + where TDerivedEntity : class + => HasRowsAffectedParameter(configurationSource); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanHaveRowsAffectedParameter(ConfigurationSource configurationSource) + => Metadata.FindRowsAffectedParameter() != null + || configurationSource.Overrides(Metadata.GetConfigurationSource()); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalStoredProcedureResultColumnBuilder? HasResultColumn( string propertyName, ConfigurationSource configurationSource) { - if (!Metadata.ContainsResultColumn(propertyName)) + var resultColumn = Metadata.FindResultColumn(propertyName); + if (resultColumn == null) { if (!configurationSource.Overrides(Metadata.GetConfigurationSource())) { return null; } - Metadata.AddResultColumn(propertyName); + resultColumn = Metadata.AddResultColumn(propertyName); } Metadata.UpdateConfigurationSource(configurationSource); - return this; + return resultColumn.Builder; } /// @@ -231,7 +322,7 @@ public virtual bool CanHaveParameter(string propertyName, ConfigurationSource co /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual InternalStoredProcedureBuilder? HasResultColumn( + public virtual InternalStoredProcedureResultColumnBuilder? HasResultColumn( Expression> propertyExpression, ConfigurationSource configurationSource) where TDerivedEntity : class @@ -244,7 +335,79 @@ public virtual bool CanHaveParameter(string propertyName, ConfigurationSource co /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual bool CanHaveResultColumn(string propertyName, ConfigurationSource configurationSource) - => Metadata.ContainsResultColumn(propertyName) + => Metadata.FindResultColumn(propertyName) != null + || configurationSource.Overrides(Metadata.GetConfigurationSource()); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalStoredProcedureResultColumnBuilder? HasRowsAffectedResultColumn( + ConfigurationSource configurationSource) + { + var resultColumn = Metadata.FindRowsAffectedResultColumn(); + if (resultColumn == null) + { + if (!configurationSource.Overrides(Metadata.GetConfigurationSource())) + { + return null; + } + + resultColumn = Metadata.AddRowsAffectedResultColumn(); + } + + Metadata.UpdateConfigurationSource(configurationSource); + return resultColumn.Builder; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalStoredProcedureResultColumnBuilder? HasRowsAffectedResultColumn( + ConfigurationSource configurationSource) + where TDerivedEntity : class + => HasRowsAffectedResultColumn(configurationSource); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanHaveRowsAffectedResultColumn(ConfigurationSource configurationSource) + => Metadata.FindRowsAffectedResultColumn() != null + || configurationSource.Overrides(Metadata.GetConfigurationSource()); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalStoredProcedureBuilder? HasRowsAffectedReturn(bool rowsAffectedReturned, ConfigurationSource configurationSource) + { + if (!CanHaveRowsAffectedReturn(rowsAffectedReturned, configurationSource)) + { + return null; + } + + Metadata.SetAreRowsAffectedReturned(rowsAffectedReturned); + return this; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanHaveRowsAffectedReturn(bool rowsAffectedReturned, ConfigurationSource configurationSource) + => Metadata.AreRowsAffectedReturned == rowsAffectedReturned || configurationSource.Overrides(Metadata.GetConfigurationSource()); /// @@ -308,7 +471,7 @@ bool IConventionStoredProcedureBuilder.CanSetSchema(string? schema, bool fromDat /// [DebuggerStepThrough] - IConventionStoredProcedureBuilder? IConventionStoredProcedureBuilder.HasParameter(string propertyName, bool fromDataAnnotation) + IConventionStoredProcedureParameterBuilder? IConventionStoredProcedureBuilder.HasParameter(string propertyName, bool fromDataAnnotation) => HasParameter(propertyName, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// @@ -316,10 +479,33 @@ bool IConventionStoredProcedureBuilder.CanSetSchema(string? schema, bool fromDat bool IConventionStoredProcedureBuilder.CanHaveParameter(string propertyName, bool fromDataAnnotation) => CanHaveParameter(propertyName, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionStoredProcedureParameterBuilder? IConventionStoredProcedureBuilder.HasOriginalValueParameter( + string propertyName, bool fromDataAnnotation) + => HasOriginalValueParameter(propertyName, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionStoredProcedureBuilder.CanHaveOriginalValueParameter(string propertyName, bool fromDataAnnotation) + => CanHaveOriginalValueParameter(propertyName, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionStoredProcedureParameterBuilder? IConventionStoredProcedureBuilder.HasRowsAffectedParameter(bool fromDataAnnotation) + => HasRowsAffectedParameter( + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// [DebuggerStepThrough] - IConventionStoredProcedureBuilder? IConventionStoredProcedureBuilder.HasResultColumn(string propertyName, bool fromDataAnnotation) + bool IConventionStoredProcedureBuilder.CanHaveRowsAffectedParameter(bool fromDataAnnotation) + => CanHaveRowsAffectedParameter(fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionStoredProcedureResultColumnBuilder? IConventionStoredProcedureBuilder.HasResultColumn(string propertyName, bool fromDataAnnotation) => HasResultColumn(propertyName, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// @@ -327,6 +513,17 @@ bool IConventionStoredProcedureBuilder.CanHaveParameter(string propertyName, boo bool IConventionStoredProcedureBuilder.CanHaveResultColumn(string propertyName, bool fromDataAnnotation) => CanHaveResultColumn(propertyName, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionStoredProcedureResultColumnBuilder? IConventionStoredProcedureBuilder.HasRowsAffectedResultColumn( + string propertyName, bool fromDataAnnotation) + => HasRowsAffectedResultColumn(fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionStoredProcedureBuilder.CanHaveRowsAffectedResultColumn(string propertyName, bool fromDataAnnotation) + => CanHaveRowsAffectedResultColumn(fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// [DebuggerStepThrough] diff --git a/src/EFCore.Relational/Metadata/Internal/InternalStoredProcedureParameterBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalStoredProcedureParameterBuilder.cs new file mode 100644 index 00000000000..f6bcac84a17 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/InternalStoredProcedureParameterBuilder.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +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 InternalStoredProcedureParameterBuilder : + AnnotatableBuilder, + IConventionStoredProcedureParameterBuilder +{ + /// + /// 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 InternalStoredProcedureParameterBuilder( + StoredProcedureParameter parameter, IConventionModelBuilder modelBuilder) + : base(parameter, modelBuilder) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalStoredProcedureParameterBuilder? HasName( + string name, + ConfigurationSource configurationSource) + { + if (!CanSetName(name, configurationSource)) + { + return null; + } + + Metadata.SetName(name, configurationSource); + + return this; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanSetName( + string? name, + ConfigurationSource configurationSource) + => configurationSource.Overrides(Metadata.GetNameConfigurationSource()) + || Metadata.Name == name; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalStoredProcedureParameterBuilder? HasDirection( + ParameterDirection direction, + ConfigurationSource configurationSource) + { + if (!CanSetDirection(direction, configurationSource)) + { + return null; + } + + Metadata.SetDirection(direction, configurationSource); + + return this; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanSetDirection( + ParameterDirection direction, + ConfigurationSource configurationSource) + => configurationSource == ConfigurationSource.Explicit + || Metadata.Direction == direction + || (configurationSource.Overrides(Metadata.GetDirectionConfigurationSource()) && Metadata.IsValid(direction)); + + /// + IConventionStoredProcedureParameter IConventionStoredProcedureParameterBuilder.Metadata + { + [DebuggerStepThrough] + get => Metadata; + } + + /// + [DebuggerStepThrough] + IConventionStoredProcedureParameterBuilder? IConventionStoredProcedureParameterBuilder.HasName(string name, bool fromDataAnnotation) + => HasName(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionStoredProcedureParameterBuilder.CanSetName(string? name, bool fromDataAnnotation) + => CanSetName(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionStoredProcedureParameterBuilder? IConventionStoredProcedureParameterBuilder.HasDirection( + ParameterDirection direction, bool fromDataAnnotation) + => HasDirection(direction, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionStoredProcedureParameterBuilder.CanSetDirection(ParameterDirection direction, bool fromDataAnnotation) + => CanSetDirection(direction, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); +} diff --git a/src/EFCore.Relational/Metadata/Internal/InternalStoredProcedureResultColumnBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalStoredProcedureResultColumnBuilder.cs new file mode 100644 index 00000000000..897ad864ea4 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/InternalStoredProcedureResultColumnBuilder.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +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 InternalStoredProcedureResultColumnBuilder : + AnnotatableBuilder, + IConventionStoredProcedureResultColumnBuilder +{ + /// + /// 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 InternalStoredProcedureResultColumnBuilder( + StoredProcedureResultColumn resultColumn, IConventionModelBuilder modelBuilder) + : base(resultColumn, modelBuilder) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalStoredProcedureResultColumnBuilder? HasName( + string name, + ConfigurationSource configurationSource) + { + if (!CanSetName(name, configurationSource)) + { + return null; + } + + Metadata.SetName(name, configurationSource); + + return this; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanSetName( + string? name, + ConfigurationSource configurationSource) + => configurationSource.Overrides(Metadata.GetNameConfigurationSource()) + || Metadata.Name == name; + + /// + IConventionStoredProcedureResultColumn IConventionStoredProcedureResultColumnBuilder.Metadata + { + [DebuggerStepThrough] + get => Metadata; + } + + /// + [DebuggerStepThrough] + IConventionStoredProcedureResultColumnBuilder? IConventionStoredProcedureResultColumnBuilder.HasName(string name, bool fromDataAnnotation) + => HasName(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionStoredProcedureResultColumnBuilder.CanSetName(string? name, bool fromDataAnnotation) + => CanSetName(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); +} diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index 534d3160ffc..2b25ebee5e0 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -128,10 +128,17 @@ public override bool IsReadOnly /// public static IModel Add( IModel model, - IRelationalAnnotationProvider? relationalAnnotationProvider, + IRelationalAnnotationProvider relationalAnnotationProvider, + IRelationalTypeMappingSource relationalTypeMappingSource, bool designTime) { - model.AddRuntimeAnnotation(RelationalAnnotationNames.RelationalModel, Create(model, relationalAnnotationProvider, designTime)); + model.AddRuntimeAnnotation( + RelationalAnnotationNames.RelationalModel, + Create( + model, + relationalAnnotationProvider, + relationalTypeMappingSource, + designTime)); return model; } @@ -143,7 +150,8 @@ public static IModel Add( /// public static IRelationalModel Create( IModel model, - IRelationalAnnotationProvider? relationalAnnotationProvider, + IRelationalAnnotationProvider relationalAnnotationProvider, + IRelationalTypeMappingSource relationalTypeMappingSource, bool designTime) { var databaseModel = new RelationalModel(model); @@ -160,7 +168,7 @@ public static IRelationalModel Create( AddMappedFunctions(databaseModel, entityType); - AddStoredProcedures(databaseModel, entityType); + AddStoredProcedures(databaseModel, entityType, relationalTypeMappingSource); } AddTvfs(databaseModel); @@ -870,7 +878,10 @@ private static StoreFunction GetOrCreateStoreFunction(IRuntimeDbFunction dbFunct return storeFunction; } - private static void AddStoredProcedures(RelationalModel databaseModel, IEntityType entityType) + private static void AddStoredProcedures( + RelationalModel databaseModel, + IEntityType entityType, + IRelationalTypeMappingSource relationalTypeMappingSource) { var mappedType = entityType; @@ -890,17 +901,35 @@ private static void AddStoredProcedures(RelationalModel databaseModel, IEntityTy var isTpc = mappingStrategy == RelationalAnnotationNames.TpcMappingStrategy; while (mappedType != null) { + var includesDerivedTypes = !isTpc && mappedType == entityType; + + var tableMappings = entityType.GetTableMappings().Where(m + => m.Table.Name == mappedType.GetTableName() + && m.Table.Schema == mappedType.GetSchema() + && m.IsSplitEntityTypePrincipal != false + && m.IncludesDerivedTypes == includesDerivedTypes); + var tableMapping = (TableMapping?)tableMappings.FirstOrDefault(); + + Check.DebugAssert(tableMapping == null || tableMappings.Count() == 1, "Expected table mapping to be unique"); + var insertSproc = (IRuntimeStoredProcedure?)mappedType.GetInsertStoredProcedure(); if (insertSproc != null && insertStoredProcedureMappings != null) { - CreateStoredProcedureMapping( + var insertProcedureMapping = CreateStoredProcedureMapping( entityType, mappedType, insertSproc, + tableMapping, databaseModel, insertStoredProcedureMappings, - includesDerivedTypes: !isTpc && mappedType == entityType); + includesDerivedTypes, + relationalTypeMappingSource); + + if (tableMapping != null) + { + tableMapping.InsertStoredProcedureMapping = insertProcedureMapping; + } } else if (entityType == mappedType) { @@ -911,13 +940,20 @@ private static void AddStoredProcedures(RelationalModel databaseModel, IEntityTy if (deleteSproc != null && deleteStoredProcedureMappings != null) { - CreateStoredProcedureMapping( + var deleteProcedureMapping = CreateStoredProcedureMapping( entityType, mappedType, deleteSproc, + tableMapping, databaseModel, deleteStoredProcedureMappings, - includesDerivedTypes: !isTpc && mappedType == entityType); + includesDerivedTypes, + relationalTypeMappingSource); + + if (tableMapping != null) + { + tableMapping.DeleteStoredProcedureMapping = deleteProcedureMapping; + } } else if (entityType == mappedType) { @@ -928,13 +964,20 @@ private static void AddStoredProcedures(RelationalModel databaseModel, IEntityTy if (updateSproc != null && updateStoredProcedureMappings != null) { - CreateStoredProcedureMapping( + var updateProcedureMapping = CreateStoredProcedureMapping( entityType, mappedType, updateSproc, + tableMapping, databaseModel, updateStoredProcedureMappings, - includesDerivedTypes: !isTpc && mappedType == entityType); + includesDerivedTypes, + relationalTypeMappingSource); + + if (tableMapping != null) + { + tableMapping.UpdateStoredProcedureMapping = updateProcedureMapping; + } } else if (entityType == mappedType) { @@ -968,19 +1011,21 @@ private static void AddStoredProcedures(RelationalModel databaseModel, IEntityTy } } - private static void CreateStoredProcedureMapping( + private static StoredProcedureMapping CreateStoredProcedureMapping( IEntityType entityType, IEntityType mappedType, IRuntimeStoredProcedure storedProcedure, + ITableMapping? tableMapping, RelationalModel model, List storedProcedureMappings, - bool includesDerivedTypes) + bool includesDerivedTypes, + IRelationalTypeMappingSource relationalTypeMappingSource) { - var storeStoredProcedure = GetOrCreateStoreStoredProcedure(storedProcedure, model); + var storeStoredProcedure = GetOrCreateStoreStoredProcedure(storedProcedure, model, relationalTypeMappingSource); var identifier = storedProcedure.GetStoreIdentifier(); var storedProcedureMapping = new StoredProcedureMapping( - entityType, storeStoredProcedure, storedProcedure, includesDerivedTypes); + entityType, storeStoredProcedure, storedProcedure, tableMapping, includesDerivedTypes); var (parameterMappingAnnotationName, columnMappingAnnotationName) = identifier.StoreObjectType switch { StoreObjectType.InsertStoredProcedure @@ -994,9 +1039,24 @@ private static void CreateStoredProcedureMapping( _ => throw new Exception("Unexpected stored procedure type: " + identifier.StoreObjectType) }; + var position = -1; foreach (var parameter in storedProcedure.Parameters) { - var property = mappedType.FindProperty(parameter); + position++; + if (parameter.PropertyName == null) + { + GetOrCreateStoreStoredProcedureParameter( + parameter, + null, + position, + storeStoredProcedure, + identifier, + relationalTypeMappingSource); + + continue; + } + + var property = mappedType.FindProperty(parameter.PropertyName); if (property == null) { Check.DebugAssert( @@ -1007,9 +1067,15 @@ private static void CreateStoredProcedureMapping( { foreach (var derivedProperty in entityType.GetDerivedProperties()) { - if (derivedProperty.Name == parameter) + if (derivedProperty.Name == parameter.PropertyName) { - GetOrCreateStoreStoredProcedureParameter(derivedProperty, storeStoredProcedure, identifier); + GetOrCreateStoreStoredProcedureParameter( + parameter, + derivedProperty, + position, + storeStoredProcedure, + identifier, + relationalTypeMappingSource); break; } } @@ -1018,9 +1084,16 @@ private static void CreateStoredProcedureMapping( continue; } - var storeParameter = GetOrCreateStoreStoredProcedureParameter(property, storeStoredProcedure, identifier); + var storeParameter = GetOrCreateStoreStoredProcedureParameter( + parameter, + property, + position, + storeStoredProcedure, + identifier, + relationalTypeMappingSource); - var columnMapping = new StoredProcedureParameterMapping(property, storeParameter, storedProcedureMapping); + var columnMapping = new StoredProcedureParameterMapping( + property, parameter, storeParameter, storedProcedureMapping); storedProcedureMapping.AddParameterMapping(columnMapping); storeParameter.AddPropertyMapping(columnMapping); @@ -1036,7 +1109,19 @@ private static void CreateStoredProcedureMapping( foreach (var resultColumn in storedProcedure.ResultColumns) { - var property = mappedType.FindProperty(resultColumn); + if (resultColumn.PropertyName == null) + { + GetOrCreateStoreStoredProcedureResultColumn( + resultColumn, + null, + storeStoredProcedure, + identifier, + relationalTypeMappingSource); + + continue; + } + + var property = mappedType.FindProperty(resultColumn.PropertyName); if (property == null) { Check.DebugAssert( @@ -1047,9 +1132,14 @@ private static void CreateStoredProcedureMapping( { foreach (var derivedProperty in entityType.GetDerivedProperties()) { - if (derivedProperty.Name == resultColumn) + if (derivedProperty.Name == resultColumn.PropertyName) { - GetOrCreateStoreStoredProcedureResultColumn(derivedProperty, storeStoredProcedure, identifier); + GetOrCreateStoreStoredProcedureResultColumn( + resultColumn, + derivedProperty, + storeStoredProcedure, + identifier, + relationalTypeMappingSource); break; } } @@ -1058,9 +1148,15 @@ private static void CreateStoredProcedureMapping( continue; } - var column = GetOrCreateStoreStoredProcedureResultColumn(property, storeStoredProcedure, identifier); + var column = GetOrCreateStoreStoredProcedureResultColumn( + resultColumn, + property, + storeStoredProcedure, + identifier, + relationalTypeMappingSource); - var columnMapping = new StoredProcedureResultColumnMapping(property, column, storedProcedureMapping); + var columnMapping = new StoredProcedureResultColumnMapping( + property, resultColumn, column, storedProcedureMapping); storedProcedureMapping.AddColumnMapping(columnMapping); column.AddPropertyMapping(columnMapping); @@ -1077,9 +1173,12 @@ private static void CreateStoredProcedureMapping( storedProcedureMappings.Add(storedProcedureMapping); storeStoredProcedure.EntityTypeMappings.Add(storedProcedureMapping); + return storedProcedureMapping; + static StoreStoredProcedure GetOrCreateStoreStoredProcedure( IRuntimeStoredProcedure storedProcedure, - RelationalModel model) + RelationalModel model, + IRelationalTypeMappingSource relationalTypeMappingSource) { var storeStoredProcedure = (StoreStoredProcedure?)storedProcedure.StoreStoredProcedure; if (storeStoredProcedure == null) @@ -1088,6 +1187,15 @@ static StoreStoredProcedure GetOrCreateStoreStoredProcedure( if (storeStoredProcedure == null) { storeStoredProcedure = new StoreStoredProcedure(storedProcedure, model); + if (storedProcedure.AreRowsAffectedReturned) + { + var typeMapping = relationalTypeMappingSource.FindMapping(typeof(int))!; + storeStoredProcedure.Return = new StoreStoredProcedureReturn( + "", + typeMapping.StoreType, + storeStoredProcedure, + typeMapping); + } model.StoredProcedures.Add((storeStoredProcedure.Name, storeStoredProcedure.Schema), storeStoredProcedure); } else @@ -1101,49 +1209,90 @@ static StoreStoredProcedure GetOrCreateStoreStoredProcedure( } static StoreStoredProcedureParameter GetOrCreateStoreStoredProcedureParameter( - IProperty property, + IStoredProcedureParameter parameter, + IProperty? property, + int position, StoreStoredProcedure storeStoredProcedure, - StoreObjectIdentifier identifier) + StoreObjectIdentifier identifier, + IRelationalTypeMappingSource relationalTypeMappingSource) { - var columnName = property.GetColumnName(identifier)!; - var storeParameter = (StoreStoredProcedureParameter?)storeStoredProcedure.FindParameter(columnName); + var name = parameter.Name; + var storeParameter = (StoreStoredProcedureParameter?)storeStoredProcedure.FindParameter(name); if (storeParameter == null) { - storeParameter = new StoreStoredProcedureParameter( - columnName, - property.GetColumnType(identifier), - storeStoredProcedure, - property.GetDirection(identifier)) { IsNullable = property.IsColumnNullable(identifier) }; + if (property == null) + { + var typeMapping = relationalTypeMappingSource.FindMapping(typeof(int))!; + storeParameter = new StoreStoredProcedureParameter( + name, + typeMapping.StoreType, + position, + storeStoredProcedure, + parameter.Direction, + typeMapping); + } + else + { + storeParameter = new StoreStoredProcedureParameter( + name, + property.GetColumnType(identifier), + position, + storeStoredProcedure, + parameter.Direction) + { + IsNullable = property.IsColumnNullable(identifier) + }; + } + storeStoredProcedure.AddParameter(storeParameter); } - else if (!property.IsColumnNullable(identifier)) + else if (property?.IsColumnNullable(identifier) == false) { storeParameter.IsNullable = false; } - + + ((IRuntimeStoredProcedureParameter)parameter).StoreParameter = storeParameter; return storeParameter; } static StoreStoredProcedureResultColumn GetOrCreateStoreStoredProcedureResultColumn( - IProperty property, + IStoredProcedureResultColumn resultColumn, + IProperty? property, StoreStoredProcedure storeStoredProcedure, - StoreObjectIdentifier identifier) + StoreObjectIdentifier identifier, + IRelationalTypeMappingSource relationalTypeMappingSource) { - var columnName = property.GetColumnName(identifier)!; - var column = (StoreStoredProcedureResultColumn?)storeStoredProcedure.FindResultColumn(columnName); + var name = resultColumn.Name; + var column = (StoreStoredProcedureResultColumn?)storeStoredProcedure.FindResultColumn(name); if (column == null) { - column = new StoreStoredProcedureResultColumn(columnName, property.GetColumnType(identifier), storeStoredProcedure) + if (property == null) { - IsNullable = property.IsColumnNullable(identifier) - }; + var typeMapping = relationalTypeMappingSource.FindMapping(typeof(int))!; + column = new StoreStoredProcedureResultColumn( + name, + typeMapping.StoreType, + storeStoredProcedure); + } + else + { + column = new StoreStoredProcedureResultColumn( + name, + property.GetColumnType(identifier), + storeStoredProcedure) + { + IsNullable = property.IsColumnNullable(identifier) + }; + } + storeStoredProcedure.AddResultColumn(column); } - else if (!property.IsColumnNullable(identifier)) + else if (property?.IsColumnNullable(identifier) == false) { column.IsNullable = false; } + ((IRuntimeStoredProcedureResultColumn)resultColumn).StoreResultColumn = column; return column; } } diff --git a/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedure.cs b/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedure.cs index 74132283939..3f23ac7f4d1 100644 --- a/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedure.cs +++ b/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedure.cs @@ -36,6 +36,14 @@ public StoreStoredProcedure(IRuntimeStoredProcedure sproc, RelationalModel model /// 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 IStoreStoredProcedureReturn? Return { get; set; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -80,7 +88,7 @@ public virtual void AddParameter(IStoreStoredProcedureParameter parameter) .Concat(property.GetDeleteStoredProcedureParameterMappings()) .Concat(property.GetUpdateStoredProcedureParameterMappings()) .FirstOrDefault(cm => cm.StoredProcedureMapping.StoreStoredProcedure == this) - ?.Parameter; + ?.StoreParameter; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -129,6 +137,18 @@ public virtual void AddResultColumn(IStoreStoredProcedureResultColumn column) public override string ToString() => ((IStoreStoredProcedure)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( + () => ((IStoreStoredProcedure)this).ToDebugString(), + () => ((IStoreStoredProcedure)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + /// IEnumerable IStoreStoredProcedure.StoredProcedures { @@ -144,7 +164,7 @@ IEnumerable IStoreStoredProcedure.EntityTypeMappings } /// - IEnumerable IStoreStoredProcedure.Parameters + IReadOnlyList IStoreStoredProcedure.Parameters { [DebuggerStepThrough] get => Parameters; diff --git a/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedureParameter.cs b/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedureParameter.cs index ddfa4297778..7bba844d50f 100644 --- a/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedureParameter.cs +++ b/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedureParameter.cs @@ -14,6 +14,8 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal; public class StoreStoredProcedureParameter : ColumnBase, IStoreStoredProcedureParameter { + private readonly RelationalTypeMapping? _storeTypeMapping; + /// /// 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 @@ -23,11 +25,15 @@ public class StoreStoredProcedureParameter public StoreStoredProcedureParameter( string name, string type, + int position, StoreStoredProcedure storedProcedure, - ParameterDirection direction) + ParameterDirection direction, + RelationalTypeMapping? storeTypeMapping = null) : base(name, type, storedProcedure) { + Position = position; Direction = direction; + _storeTypeMapping = storeTypeMapping; } /// @@ -38,9 +44,31 @@ public StoreStoredProcedureParameter( /// 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 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 virtual int Position { 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 RelationalTypeMapping StoreTypeMapping + => _storeTypeMapping ?? base.StoreTypeMapping; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -51,6 +79,18 @@ public virtual StoreStoredProcedure StoredProcedure public override string ToString() => ((IStoreStoredProcedureParameter)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( + () => ((IStoreStoredProcedureParameter)this).ToDebugString(), + () => ((IStoreStoredProcedureParameter)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + /// IStoreStoredProcedure IStoreStoredProcedureParameter.StoredProcedure { diff --git a/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedureResultColumn.cs b/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedureResultColumn.cs index f1f16053304..aaab2217f96 100644 --- a/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedureResultColumn.cs +++ b/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedureResultColumn.cs @@ -12,15 +12,22 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal; public class StoreStoredProcedureResultColumn : ColumnBase, IStoreStoredProcedureResultColumn { + private readonly RelationalTypeMapping? _storeTypeMapping; + /// /// 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) + public StoreStoredProcedureResultColumn( + string name, + string type, + StoreStoredProcedure storedProcedure, + RelationalTypeMapping? storeTypeMapping = null) : base(name, type, storedProcedure) { + _storeTypeMapping = storeTypeMapping; } /// @@ -31,6 +38,15 @@ public StoreStoredProcedureResultColumn(string name, string type, StoreStoredPro /// 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 RelationalTypeMapping StoreTypeMapping + => _storeTypeMapping ?? base.StoreTypeMapping; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -41,6 +57,18 @@ public virtual StoreStoredProcedure StoredProcedure public override string ToString() => ((IStoreStoredProcedureResultColumn)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( + () => ((IStoreStoredProcedureResultColumn)this).ToDebugString(), + () => ((IStoreStoredProcedureResultColumn)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + /// IStoreStoredProcedure IStoreStoredProcedureResultColumn.StoredProcedure { diff --git a/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedureReturn.cs b/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedureReturn.cs new file mode 100644 index 00000000000..cfac9fdf733 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/StoreStoredProcedureReturn.cs @@ -0,0 +1,77 @@ +// 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 StoreStoredProcedureReturn : ColumnBase, IStoreStoredProcedureReturn +{ + private readonly RelationalTypeMapping? _storeTypeMapping; + + /// + /// 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 StoreStoredProcedureReturn( + string name, + string type, + StoreStoredProcedure storedProcedure, + RelationalTypeMapping? storeTypeMapping = null) + : base(name, type, storedProcedure) + { + _storeTypeMapping = storeTypeMapping; + } + + /// + /// 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 RelationalTypeMapping StoreTypeMapping + => _storeTypeMapping ?? base.StoreTypeMapping; + + /// + /// 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() + => ((IStoreStoredProcedureReturn)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( + () => ((IStoreStoredProcedureReturn)this).ToDebugString(), + () => ((IStoreStoredProcedureReturn)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + + /// + IStoreStoredProcedure IStoreStoredProcedureReturn.StoredProcedure + { + [DebuggerStepThrough] + get => StoredProcedure; + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/StoredProcedure.cs b/src/EFCore.Relational/Metadata/Internal/StoredProcedure.cs index 6841e9ed6b4..cb99405a6d5 100644 --- a/src/EFCore.Relational/Metadata/Internal/StoredProcedure.cs +++ b/src/EFCore.Relational/Metadata/Internal/StoredProcedure.cs @@ -12,14 +12,18 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal; public class StoredProcedure : ConventionAnnotatable, IRuntimeStoredProcedure, IMutableStoredProcedure, IConventionStoredProcedure { - private readonly List _parameters = new(); - private readonly HashSet _parametersSet = new(); - private readonly List _resultColumns = new(); - private readonly HashSet _resultColumnsSet = new(); + private readonly List _parameters = new(); + private readonly Dictionary _currentValueParameters = new(); + private readonly Dictionary _originalValueParameters = new(); + private StoredProcedureParameter? _rowsAffectedParameter; + private readonly List _resultColumns = new(); + private StoredProcedureResultColumn? _rowsAffectedResultColumn; + private readonly Dictionary _propertyResultColumns = new(); private string? _schema; private string? _name; private InternalStoredProcedureBuilder? _builder; private bool _areTransactionsSuppressed; + private bool _areRowsAffectedReturned; private IStoreStoredProcedure? _storeStoredProcedure; private ConfigurationSource _configurationSource; @@ -350,7 +354,12 @@ public virtual string? Name var tableName = EntityType.GetTableName() ?? EntityType.GetDefaultTableName(); if (tableName == null) { - return null; + if (_configurationSource == ConfigurationSource.Convention) + { + return null; + } + + tableName = Uniquifier.Truncate(EntityType.ShortName(), EntityType.Model.GetMaxIdentifierLength()); } string? suffix; @@ -439,7 +448,7 @@ public virtual bool AreTransactionsSuppressed get => _areTransactionsSuppressed; set => SetAreTransactionsSuppressed(value, ConfigurationSource.Explicit); } - + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -457,7 +466,45 @@ public virtual bool SetAreTransactionsSuppressed(bool areTransactionsSuppressed, return areTransactionsSuppressed; } - /// + /// + /// 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 AreRowsAffectedReturned + { + get => _areRowsAffectedReturned; + set => SetAreRowsAffectedReturned(value); + } + + /// + /// 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 SetAreRowsAffectedReturned(bool areRowsAffectedReturned) + { + EnsureMutable(); + + if (_rowsAffectedParameter != null || _rowsAffectedResultColumn != null) + { + throw new InvalidOperationException(RelationalStrings.StoredProcedureRowsAffectedReturnConflictingParameter( + ((IReadOnlyStoredProcedure)this).GetStoreIdentifier()?.DisplayName())); + } + + _areRowsAffectedReturned = areRowsAffectedReturned; + + return areRowsAffectedReturned; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// public virtual ConfigurationSource? GetAreTransactionsSuppressedConfigurationSource() => _areTransactionsSuppressedConfigurationSource; @@ -486,55 +533,189 @@ private static void UpdateOverrides( } } } - - /// - public virtual IReadOnlyList Parameters + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IReadOnlyList Parameters { [DebuggerStepThrough] get => _parameters; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual StoredProcedureParameter? FindParameter(string propertyName) + => _currentValueParameters.TryGetValue(propertyName, out var parameter) + ? parameter + : null; - /// - public virtual bool ContainsParameter(string propertyName) - => _parametersSet.Contains(propertyName); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual StoredProcedureParameter AddParameter(string propertyName) + { + if (_currentValueParameters.ContainsKey(propertyName)) + { + throw new InvalidOperationException(RelationalStrings.StoredProcedureDuplicateParameter( + propertyName, ((IReadOnlyStoredProcedure)this).GetStoreIdentifier()?.DisplayName())); + } - /// - public virtual bool AddParameter(string propertyName) + var parameter = new StoredProcedureParameter(this, rowsAffected: false, propertyName, originalValue: false); + _parameters.Add(parameter); + _currentValueParameters.Add(propertyName, parameter); + + return 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 StoredProcedureParameter? FindOriginalValueParameter(string propertyName) + => _originalValueParameters.TryGetValue(propertyName, 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 StoredProcedureParameter AddOriginalValueParameter(string propertyName) { - if (!_parametersSet.Contains(propertyName)) + if (_originalValueParameters.ContainsKey(propertyName)) { - _parameters.Add(propertyName); - _parametersSet.Add(propertyName); + throw new InvalidOperationException(RelationalStrings.StoredProcedureDuplicateOriginalValueParameter( + propertyName, ((IReadOnlyStoredProcedure)this).GetStoreIdentifier()?.DisplayName())); + } + + var parameter = new StoredProcedureParameter(this, rowsAffected: false, propertyName, originalValue: true); + _parameters.Add(parameter); + _originalValueParameters.Add(propertyName, parameter); - return true; + return 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 StoredProcedureParameter? FindRowsAffectedParameter() + => _rowsAffectedParameter; + + /// + /// 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 StoredProcedureParameter AddRowsAffectedParameter() + { + if (_rowsAffectedParameter != null + || _rowsAffectedResultColumn != null + || _areRowsAffectedReturned) + { + throw new InvalidOperationException(RelationalStrings.StoredProcedureDuplicateRowsAffectedParameter( + ((IReadOnlyStoredProcedure)this).GetStoreIdentifier()?.DisplayName())); } + + var parameter = new StoredProcedureParameter(this, rowsAffected: true, propertyName: null, originalValue: null); + _parameters.Add(parameter); + _rowsAffectedParameter = parameter; - return false; + return parameter; } - /// - public virtual IReadOnlyList 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 virtual IReadOnlyList ResultColumns { [DebuggerStepThrough] get => _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 virtual StoredProcedureResultColumn? FindResultColumn(string propertyName) + => _propertyResultColumns.TryGetValue(propertyName, out var resultColumn) + ? resultColumn + : null; - /// - public virtual bool ContainsResultColumn(string propertyName) - => _resultColumnsSet.Contains(propertyName); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual StoredProcedureResultColumn AddResultColumn(string propertyName) + { + if (_propertyResultColumns.ContainsKey(propertyName)) + { + throw new InvalidOperationException(RelationalStrings.StoredProcedureDuplicateResultColumn( + propertyName, ((IReadOnlyStoredProcedure)this).GetStoreIdentifier()?.DisplayName())); + } - /// - public virtual bool AddResultColumn(string propertyName) + var resultColumn = new StoredProcedureResultColumn(this, forRowsAffected: false, propertyName); + _resultColumns.Add(resultColumn); + _propertyResultColumns.Add(propertyName, resultColumn); + + return resultColumn; + } + + /// + /// 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 StoredProcedureResultColumn? FindRowsAffectedResultColumn() + => _rowsAffectedResultColumn; + + /// + /// 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 StoredProcedureResultColumn AddRowsAffectedResultColumn() { - if (!_resultColumnsSet.Contains(propertyName)) + if (_rowsAffectedResultColumn != null + || _rowsAffectedParameter != null + || _areRowsAffectedReturned) { - _resultColumns.Add(propertyName); - _resultColumnsSet.Add(propertyName); - - return true; + throw new InvalidOperationException(RelationalStrings.StoredProcedureDuplicateRowsAffectedResultColumn( + ((IReadOnlyStoredProcedure)this).GetStoreIdentifier()?.DisplayName())); } - return false; + var resultColumn = new StoredProcedureResultColumn(this, forRowsAffected: true, propertyName: null); + _resultColumns.Add(resultColumn); + _rowsAffectedResultColumn = resultColumn; + + return resultColumn; } /// @@ -603,6 +784,62 @@ IStoreStoredProcedure IRuntimeStoredProcedure.StoreStoredProcedure get => _storeStoredProcedure!; set => _storeStoredProcedure = value; } + + /// + IReadOnlyList IReadOnlyStoredProcedure.Parameters + { + [DebuggerStepThrough] + get => Parameters; + } + + /// + IReadOnlyList IMutableStoredProcedure.Parameters + { + [DebuggerStepThrough] + get => Parameters; + } + + /// + IReadOnlyList IConventionStoredProcedure.Parameters + { + [DebuggerStepThrough] + get => Parameters; + } + + /// + IReadOnlyList IStoredProcedure.Parameters + { + [DebuggerStepThrough] + get => Parameters; + } + + /// + IReadOnlyList IReadOnlyStoredProcedure.ResultColumns + { + [DebuggerStepThrough] + get => ResultColumns; + } + + /// + IReadOnlyList IMutableStoredProcedure.ResultColumns + { + [DebuggerStepThrough] + get => ResultColumns; + } + + /// + IReadOnlyList IConventionStoredProcedure.ResultColumns + { + [DebuggerStepThrough] + get => ResultColumns; + } + + /// + IReadOnlyList IStoredProcedure.ResultColumns + { + [DebuggerStepThrough] + get => ResultColumns; + } /// [DebuggerStepThrough] @@ -616,17 +853,164 @@ IStoreStoredProcedure IRuntimeStoredProcedure.StoreStoredProcedure /// [DebuggerStepThrough] - string? IConventionStoredProcedure.AddParameter(string propertyName, bool fromDataAnnotation) - => AddParameter(propertyName) ? propertyName : null; + bool IConventionStoredProcedure.SetAreTransactionsSuppressed(bool areTransactionsSuppressed, bool fromDataAnnotation) + => SetAreTransactionsSuppressed( + areTransactionsSuppressed, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionStoredProcedure.SetAreRowsAffectedReturned(bool rowsAffectedReturned, bool fromDataAnnotation) + => SetAreRowsAffectedReturned(rowsAffectedReturned); + + /// + [DebuggerStepThrough] + IReadOnlyStoredProcedureParameter? IReadOnlyStoredProcedure.FindParameter(string propertyName) + => FindParameter(propertyName); + + /// + [DebuggerStepThrough] + IMutableStoredProcedureParameter? IMutableStoredProcedure.FindParameter(string propertyName) + => FindParameter(propertyName); + + /// + [DebuggerStepThrough] + IConventionStoredProcedureParameter? IConventionStoredProcedure.FindParameter(string propertyName) + => FindParameter(propertyName); + + /// + [DebuggerStepThrough] + IStoredProcedureParameter? IStoredProcedure.FindParameter(string propertyName) + => FindParameter(propertyName); + + /// + [DebuggerStepThrough] + IConventionStoredProcedureParameter? IConventionStoredProcedure.AddParameter(string propertyName, bool fromDataAnnotation) + => AddParameter(propertyName); + + /// + [DebuggerStepThrough] + IMutableStoredProcedureParameter IMutableStoredProcedure.AddParameter(string propertyName) + => AddParameter(propertyName); + + /// + [DebuggerStepThrough] + IReadOnlyStoredProcedureParameter? IReadOnlyStoredProcedure.FindOriginalValueParameter(string propertyName) + => FindOriginalValueParameter(propertyName); + + /// + [DebuggerStepThrough] + IMutableStoredProcedureParameter? IMutableStoredProcedure.FindOriginalValueParameter(string propertyName) + => FindOriginalValueParameter(propertyName); /// [DebuggerStepThrough] - string? IConventionStoredProcedure.AddResultColumn(string propertyName, bool fromDataAnnotation) - => AddResultColumn(propertyName) ? propertyName : null; + IConventionStoredProcedureParameter? IConventionStoredProcedure.FindOriginalValueParameter(string propertyName) + => FindOriginalValueParameter(propertyName); + + /// + [DebuggerStepThrough] + IStoredProcedureParameter? IStoredProcedure.FindOriginalValueParameter(string propertyName) + => FindOriginalValueParameter(propertyName); + + /// + [DebuggerStepThrough] + IMutableStoredProcedureParameter IMutableStoredProcedure.AddOriginalValueParameter(string propertyName) + => AddOriginalValueParameter(propertyName); + + /// + [DebuggerStepThrough] + IConventionStoredProcedureParameter? IConventionStoredProcedure.AddOriginalValueParameter( + string propertyName, bool fromDataAnnotation) + => AddOriginalValueParameter(propertyName); + + /// + [DebuggerStepThrough] + IReadOnlyStoredProcedureParameter? IReadOnlyStoredProcedure.FindRowsAffectedParameter() + => FindRowsAffectedParameter(); + + /// + [DebuggerStepThrough] + IMutableStoredProcedureParameter? IMutableStoredProcedure.FindRowsAffectedParameter() + => FindRowsAffectedParameter(); + + /// + [DebuggerStepThrough] + IConventionStoredProcedureParameter? IConventionStoredProcedure.FindRowsAffectedParameter() + => FindRowsAffectedParameter(); + + /// + [DebuggerStepThrough] + IStoredProcedureParameter? IStoredProcedure.FindRowsAffectedParameter() + => FindRowsAffectedParameter(); + + /// + [DebuggerStepThrough] + IMutableStoredProcedureParameter IMutableStoredProcedure.AddRowsAffectedParameter() + => AddRowsAffectedParameter(); + + /// + [DebuggerStepThrough] + IConventionStoredProcedureParameter? IConventionStoredProcedure.AddRowsAffectedParameter(bool fromDataAnnotation) + => AddRowsAffectedParameter(); + + /// + [DebuggerStepThrough] + IReadOnlyStoredProcedureResultColumn? IReadOnlyStoredProcedure.FindResultColumn(string propertyName) + => FindResultColumn(propertyName); + + /// + [DebuggerStepThrough] + IMutableStoredProcedureResultColumn? IMutableStoredProcedure.FindResultColumn(string propertyName) + => FindResultColumn(propertyName); + + /// + [DebuggerStepThrough] + IConventionStoredProcedureResultColumn? IConventionStoredProcedure.FindResultColumn(string propertyName) + => FindResultColumn(propertyName); + + /// + [DebuggerStepThrough] + IStoredProcedureResultColumn? IStoredProcedure.FindResultColumn(string propertyName) + => FindResultColumn(propertyName); + + /// + [DebuggerStepThrough] + IMutableStoredProcedureResultColumn IMutableStoredProcedure.AddResultColumn(string propertyName) + => AddResultColumn(propertyName); + + /// + [DebuggerStepThrough] + IConventionStoredProcedureResultColumn? IConventionStoredProcedure.AddResultColumn( + string propertyName, bool fromDataAnnotation) + => AddResultColumn(propertyName); /// [DebuggerStepThrough] - bool IConventionStoredProcedure.SetAreTransactionsSuppressed(bool areTransactionsSuppressed, bool fromDataAnnotation) - => SetAreTransactionsSuppressed( - areTransactionsSuppressed, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + IReadOnlyStoredProcedureResultColumn? IReadOnlyStoredProcedure.FindRowsAffectedResultColumn() + => FindRowsAffectedResultColumn(); + + /// + [DebuggerStepThrough] + IMutableStoredProcedureResultColumn? IMutableStoredProcedure.FindRowsAffectedResultColumn() + => FindRowsAffectedResultColumn(); + + /// + [DebuggerStepThrough] + IConventionStoredProcedureResultColumn? IConventionStoredProcedure.FindRowsAffectedResultColumn() + => FindRowsAffectedResultColumn(); + + /// + [DebuggerStepThrough] + IStoredProcedureResultColumn? IStoredProcedure.FindRowsAffectedResultColumn() + => FindRowsAffectedResultColumn(); + + /// + [DebuggerStepThrough] + IMutableStoredProcedureResultColumn IMutableStoredProcedure.AddRowsAffectedResultColumn() + => AddRowsAffectedResultColumn(); + + /// + [DebuggerStepThrough] + IConventionStoredProcedureResultColumn? IConventionStoredProcedure.AddRowsAffectedResultColumn(bool fromDataAnnotation) + => AddRowsAffectedResultColumn(); } diff --git a/src/EFCore.Relational/Metadata/Internal/StoredProcedureMapping.cs b/src/EFCore.Relational/Metadata/Internal/StoredProcedureMapping.cs index f6f38ca14f7..16baa9ecb62 100644 --- a/src/EFCore.Relational/Metadata/Internal/StoredProcedureMapping.cs +++ b/src/EFCore.Relational/Metadata/Internal/StoredProcedureMapping.cs @@ -21,11 +21,13 @@ public StoredProcedureMapping( IEntityType entityType, StoreStoredProcedure storeStoredProcedure, IStoredProcedure storedProcedure, + ITableMapping? tableMapping, bool includesDerivedTypes) : base(entityType, storeStoredProcedure, includesDerivedTypes) { StoredProcedure = storedProcedure; StoredProcedureIdentifier = storedProcedure.GetStoreIdentifier(); + TableMapping = tableMapping; } /// @@ -37,6 +39,9 @@ public virtual IStoreStoredProcedure StoreStoredProcedure /// public virtual StoreObjectIdentifier StoredProcedureIdentifier { get; } + + /// + public virtual ITableMapping? TableMapping { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -75,6 +80,18 @@ public virtual bool AddParameterMapping(IStoredProcedureParameterMapping paramet public override string ToString() => ((IStoredProcedureMapping)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( + () => ((IStoredProcedureMapping)this).ToDebugString(), + () => ((IStoredProcedureMapping)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + /// IEnumerable IStoredProcedureMapping.ResultColumnMappings { diff --git a/src/EFCore.Relational/Metadata/Internal/StoredProcedureParameter.cs b/src/EFCore.Relational/Metadata/Internal/StoredProcedureParameter.cs new file mode 100644 index 00000000000..9980b1681d7 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/StoredProcedureParameter.cs @@ -0,0 +1,299 @@ +// 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 StoredProcedureParameter : + ConventionAnnotatable, + IMutableStoredProcedureParameter, + IConventionStoredProcedureParameter, + IRuntimeStoredProcedureParameter +{ + private string? _name; + private ParameterDirection? _direction; + + private ConfigurationSource? _nameConfigurationSource; + private ConfigurationSource? _directionConfigurationSource; + private InternalStoredProcedureParameterBuilder? _builder; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public StoredProcedureParameter( + StoredProcedure storedProcedure, + bool rowsAffected, + string? propertyName, + bool? originalValue) + { + StoredProcedure = storedProcedure; + ForRowsAffected = rowsAffected; + PropertyName = propertyName; + ForOriginalValue = originalValue; + if (rowsAffected) + { + _direction = ParameterDirection.Output; + _directionConfigurationSource = ConfigurationSource.Explicit; + } + _builder = new(this, storedProcedure.Builder.ModelBuilder); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalStoredProcedureParameterBuilder Builder + { + [DebuggerStepThrough] + get => _builder ?? throw new InvalidOperationException(CoreStrings.ObjectRemovedFromModel); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool IsInModel + => _builder is not null; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual void SetRemovedFromModel() + => _builder = null; + + /// + /// 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 bool IsReadOnly + => ((Annotatable)StoredProcedure.EntityType).IsReadOnly; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual StoredProcedure StoredProcedure { 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 IStoreStoredProcedureParameter StoreParameter { get; set; } = default!; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual string? PropertyName { 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 bool? ForOriginalValue { 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 bool ForRowsAffected { 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 string Name + { + get => _name ?? (ForRowsAffected + ? "RowsAffected" + : GetProperty().GetDefaultColumnName( + ((IReadOnlyStoredProcedure)StoredProcedure).GetStoreIdentifier()!.Value)); + set => SetName(value, ConfigurationSource.Explicit); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual string SetName(string name, ConfigurationSource configurationSource) + { + _name = name; + + _nameConfigurationSource = configurationSource.Max(_nameConfigurationSource); + + return name; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ConfigurationSource? GetNameConfigurationSource() + => _nameConfigurationSource; + + /// + /// 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 ParameterDirection Direction + { + get => _direction ?? ParameterDirection.Input; + set => SetDirection(value, ConfigurationSource.Explicit); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ParameterDirection SetDirection(ParameterDirection direction, ConfigurationSource configurationSource) + { + if (ForRowsAffected) + { + throw new InvalidOperationException( + RelationalStrings.StoredProcedureParameterInvalidConfiguration( + nameof(Direction), Name, ((IReadOnlyStoredProcedure)StoredProcedure).GetStoreIdentifier()?.DisplayName())); + } + + if (!IsValid(direction)) + { + throw new InvalidOperationException(RelationalStrings.StoredProcedureParameterInvalidDirection( + direction, Name, ((IReadOnlyStoredProcedure)StoredProcedure).GetStoreIdentifier()?.DisplayName())); + } + + _direction = direction; + + _directionConfigurationSource = configurationSource.Max(_directionConfigurationSource); + + return 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 bool IsValid(ParameterDirection direction) => direction switch + { + ParameterDirection.Output => ForOriginalValue != true, + ParameterDirection.ReturnValue => false, + _ => 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 virtual ConfigurationSource? GetDirectionConfigurationSource() + => _directionConfigurationSource; + + private IMutableProperty GetProperty() + => StoredProcedure.EntityType.FindProperty(PropertyName!) + ?? StoredProcedure.EntityType.GetDerivedTypes().Select(t => t.FindDeclaredProperty(PropertyName!)!) + .First(n => n != 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 override string ToString() + => ((IStoredProcedureParameter)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( + () => ((IStoredProcedureParameter)this).ToDebugString(), + () => ((IStoredProcedureParameter)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + + /// + IReadOnlyStoredProcedure IReadOnlyStoredProcedureParameter.StoredProcedure + { + [DebuggerStepThrough] + get => StoredProcedure; + } + + /// + IMutableStoredProcedure IMutableStoredProcedureParameter.StoredProcedure + { + [DebuggerStepThrough] + get => StoredProcedure; + } + + /// + IConventionStoredProcedure IConventionStoredProcedureParameter.StoredProcedure + { + [DebuggerStepThrough] + get => StoredProcedure; + } + + /// + IStoredProcedure IStoredProcedureParameter.StoredProcedure + { + [DebuggerStepThrough] + get => StoredProcedure; + } + + /// + IConventionStoredProcedureParameterBuilder IConventionStoredProcedureParameter.Builder + { + [DebuggerStepThrough] + get => Builder; + } + + /// + string IConventionStoredProcedureParameter.SetName(string name, bool fromDataAnnotation) + => SetName(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + ParameterDirection IConventionStoredProcedureParameter.SetDirection(ParameterDirection direction, bool fromDataAnnotation) + => SetDirection(direction, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); +} diff --git a/src/EFCore.Relational/Metadata/Internal/StoredProcedureParameterMapping.cs b/src/EFCore.Relational/Metadata/Internal/StoredProcedureParameterMapping.cs index 6c69bc8382c..7f2e62b9335 100644 --- a/src/EFCore.Relational/Metadata/Internal/StoredProcedureParameterMapping.cs +++ b/src/EFCore.Relational/Metadata/Internal/StoredProcedureParameterMapping.cs @@ -19,13 +19,28 @@ public class StoredProcedureParameterMapping : ColumnMappingBase, IStoredProcedu /// public StoredProcedureParameterMapping( IProperty property, - StoreStoredProcedureParameter parameter, + IStoredProcedureParameter parameter, + StoreStoredProcedureParameter storeParameter, StoredProcedureMapping storedProcedureMapping) - : base(property, parameter, storedProcedureMapping) + : base(property, storeParameter, storedProcedureMapping) { - } + Parameter = 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 IStoredProcedureParameter Parameter { 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 IStoredProcedureMapping StoredProcedureMapping => (IStoredProcedureMapping)TableMapping; @@ -47,8 +62,20 @@ protected override RelationalTypeMapping GetTypeMapping() public override string ToString() => ((IStoredProcedureParameterMapping)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( + () => ((IStoredProcedureParameterMapping)this).ToDebugString(), + () => ((IStoredProcedureParameterMapping)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + /// - IStoreStoredProcedureParameter IStoredProcedureParameterMapping.Parameter + IStoreStoredProcedureParameter IStoredProcedureParameterMapping.StoreParameter { [DebuggerStepThrough] get => (IStoreStoredProcedureParameter)Column; diff --git a/src/EFCore.Relational/Metadata/Internal/StoredProcedureResultColumn.cs b/src/EFCore.Relational/Metadata/Internal/StoredProcedureResultColumn.cs new file mode 100644 index 00000000000..626edeea3a2 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/StoredProcedureResultColumn.cs @@ -0,0 +1,230 @@ +// 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 StoredProcedureResultColumn : + ConventionAnnotatable, + IMutableStoredProcedureResultColumn, + IConventionStoredProcedureResultColumn, + IRuntimeStoredProcedureResultColumn +{ + private string _name = "RowsAffected"; + + private ConfigurationSource? _nameConfigurationSource; + private InternalStoredProcedureResultColumnBuilder? _builder; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public StoredProcedureResultColumn( + StoredProcedure storedProcedure, + bool forRowsAffected, + string? propertyName) + { + StoredProcedure = storedProcedure; + ForRowsAffected = forRowsAffected; + PropertyName = propertyName; + _builder = new(this, storedProcedure.Builder.ModelBuilder); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalStoredProcedureResultColumnBuilder Builder + { + [DebuggerStepThrough] + get => _builder ?? throw new InvalidOperationException(CoreStrings.ObjectRemovedFromModel); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool IsInModel + => _builder is not null; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual void SetRemovedFromModel() + => _builder = null; + + /// + /// 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 bool IsReadOnly + => ((Annotatable)StoredProcedure.EntityType).IsReadOnly; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual StoredProcedure StoredProcedure { 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 IStoreStoredProcedureResultColumn StoreResultColumn { get; set; } = default!; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual string? PropertyName { 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 bool ForRowsAffected { 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 string Name + { + get => ForRowsAffected + ? _name + : GetProperty().GetColumnName( + ((IReadOnlyStoredProcedure)StoredProcedure).GetStoreIdentifier()!.Value)!; + set => SetName(value, ConfigurationSource.Explicit); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual string? SetName(string name, ConfigurationSource configurationSource) + { + if (ForRowsAffected) + { + _name = name; + + _nameConfigurationSource = configurationSource.Max(_nameConfigurationSource); + + return name; + } + + if (configurationSource == ConfigurationSource.Explicit) + { + GetProperty().SetColumnName(name, ((IReadOnlyStoredProcedure)StoredProcedure).GetStoreIdentifier()!.Value); + return name; + } + + return ((IConventionProperty)GetProperty()).SetColumnName( + name, ((IReadOnlyStoredProcedure)StoredProcedure).GetStoreIdentifier()!.Value, + fromDataAnnotation: configurationSource == ConfigurationSource.DataAnnotation); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ConfigurationSource? GetNameConfigurationSource() + => ForRowsAffected + ? _nameConfigurationSource + : ((IConventionProperty)GetProperty()!) + .GetColumnNameConfigurationSource(((IReadOnlyStoredProcedure)StoredProcedure).GetStoreIdentifier()!.Value); + + private IMutableProperty GetProperty() + => StoredProcedure.EntityType.FindProperty(PropertyName!) + ?? StoredProcedure.EntityType.GetDerivedTypes().Select(t => t.FindDeclaredProperty(PropertyName!)!) + .First(n => n != 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 override string ToString() + => ((IStoredProcedureResultColumn)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( + () => ((IStoredProcedureResultColumn)this).ToDebugString(), + () => ((IStoredProcedureResultColumn)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + + /// + IReadOnlyStoredProcedure IReadOnlyStoredProcedureResultColumn.StoredProcedure + { + [DebuggerStepThrough] + get => StoredProcedure; + } + + /// + IMutableStoredProcedure IMutableStoredProcedureResultColumn.StoredProcedure + { + [DebuggerStepThrough] + get => StoredProcedure; + } + + /// + IConventionStoredProcedure IConventionStoredProcedureResultColumn.StoredProcedure + { + [DebuggerStepThrough] + get => StoredProcedure; + } + + /// + IStoredProcedure IStoredProcedureResultColumn.StoredProcedure + { + [DebuggerStepThrough] + get => StoredProcedure; + } + + /// + IConventionStoredProcedureResultColumnBuilder IConventionStoredProcedureResultColumn.Builder + { + [DebuggerStepThrough] + get => Builder; + } + + /// + string? IConventionStoredProcedureResultColumn.SetName(string name, bool fromDataAnnotation) + => SetName(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); +} diff --git a/src/EFCore.Relational/Metadata/Internal/StoredProcedureResultColumnMapping.cs b/src/EFCore.Relational/Metadata/Internal/StoredProcedureResultColumnMapping.cs index c64a612185a..a8c300038b8 100644 --- a/src/EFCore.Relational/Metadata/Internal/StoredProcedureResultColumnMapping.cs +++ b/src/EFCore.Relational/Metadata/Internal/StoredProcedureResultColumnMapping.cs @@ -19,13 +19,28 @@ public class StoredProcedureResultColumnMapping : ColumnMappingBase, IStoredProc /// public StoredProcedureResultColumnMapping( IProperty property, - StoreStoredProcedureResultColumn column, + IStoredProcedureResultColumn resultColumn, + StoreStoredProcedureResultColumn storeResultColumn, StoredProcedureMapping storedProcedureMapping) - : base(property, column, storedProcedureMapping) + : base(property, storeResultColumn, storedProcedureMapping) { + ResultColumn = resultColumn; } - /// + /// + /// 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 IStoredProcedureResultColumn ResultColumn { 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 IStoredProcedureMapping StoredProcedureMapping => (IStoredProcedureMapping)TableMapping; @@ -47,8 +62,20 @@ protected override RelationalTypeMapping GetTypeMapping() public override string ToString() => ((IStoredProcedureResultColumnMapping)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( + () => ((IStoredProcedureResultColumnMapping)this).ToDebugString(), + () => ((IStoredProcedureResultColumnMapping)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + /// - IStoreStoredProcedureResultColumn IStoredProcedureResultColumnMapping.Column + IStoreStoredProcedureResultColumn IStoredProcedureResultColumnMapping.StoreResultColumn { [DebuggerStepThrough] get => (IStoreStoredProcedureResultColumn)Column; diff --git a/src/EFCore.Relational/Metadata/Internal/TableMapping.cs b/src/EFCore.Relational/Metadata/Internal/TableMapping.cs index 09c8e2a2ec2..a0bb67ba981 100644 --- a/src/EFCore.Relational/Metadata/Internal/TableMapping.cs +++ b/src/EFCore.Relational/Metadata/Internal/TableMapping.cs @@ -24,10 +24,39 @@ public TableMapping( : base(entityType, table, includesDerivedTypes) { } - - /// + + /// + /// 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 new virtual ITable Table => (ITable)base.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 virtual IStoredProcedureMapping? InsertStoredProcedureMapping { get; set; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IStoredProcedureMapping? DeleteStoredProcedureMapping { get; set; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IStoredProcedureMapping? UpdateStoredProcedureMapping { get; set; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs index e2e520f71e3..09b3a35b107 100644 --- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs +++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs @@ -92,11 +92,6 @@ 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. /// diff --git a/src/EFCore.Relational/Metadata/RuntimeStoredProcedure.cs b/src/EFCore.Relational/Metadata/RuntimeStoredProcedure.cs index fcb5e845c37..b60a8c5c1ee 100644 --- a/src/EFCore.Relational/Metadata/RuntimeStoredProcedure.cs +++ b/src/EFCore.Relational/Metadata/RuntimeStoredProcedure.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; namespace Microsoft.EntityFrameworkCore.Metadata; @@ -13,10 +14,11 @@ namespace Microsoft.EntityFrameworkCore.Metadata; /// public class RuntimeStoredProcedure : AnnotatableBase, IRuntimeStoredProcedure { - private readonly List _parameters = new(); - private readonly List _resultColumns = new(); + private readonly List _parameters = new(); + private readonly List _resultColumns = new(); private readonly string? _schema; private readonly string _name; + private readonly bool _areRowsAffectedReturned; private readonly bool _areTransactionsSuppressed; private IStoreStoredProcedure? _storeStoredProcedure; @@ -26,16 +28,19 @@ public class RuntimeStoredProcedure : AnnotatableBase, IRuntimeStoredProcedure /// The mapped entity type. /// The name. /// The schema. + /// Whether this stored procedure returns the number of rows affected. /// Whether the automatic transactions are surpressed. public RuntimeStoredProcedure( RuntimeEntityType entityType, string name, string? schema, + bool areRowsAffectedReturned, bool areTransactionsSuppressed) { EntityType = entityType; _name = name; _schema = schema; + _areRowsAffectedReturned = areRowsAffectedReturned; _areTransactionsSuppressed = areTransactionsSuppressed; } @@ -47,19 +52,47 @@ public RuntimeStoredProcedure( /// /// Adds a new parameter mapped to the property with the given name. /// + /// The name of the parameter. + /// The direction. + /// Whether the parameter holds the rows affected. /// The name of the corresponding property. - public virtual void AddParameter(string propertyName) + /// Whether the parameter holds the original value. + public virtual RuntimeStoredProcedureParameter AddParameter( + string name, + ParameterDirection direction, + bool forRowsAffected, + string? propertyName, + bool? forOriginalValue) { - _parameters.Add(propertyName); + var parameter = new RuntimeStoredProcedureParameter( + this, + name, + direction, + forRowsAffected, + propertyName, + forOriginalValue); + _parameters.Add(parameter); + return parameter; } /// /// Adds a new column of the result for this stored procedure mapped to the property with the given name /// + /// The name of the result column. + /// Whether the column holds the rows affected. /// The name of the corresponding property. - public virtual void AddResultColumn(string propertyName) + public virtual RuntimeStoredProcedureResultColumn AddResultColumn( + string name, + bool forRowsAffected, + string? propertyName) { - _resultColumns.Add(propertyName); + var resultColumn = new RuntimeStoredProcedureResultColumn( + this, + name, + forRowsAffected, + propertyName); + _resultColumns.Add(resultColumn); + return resultColumn; } /// @@ -124,30 +157,88 @@ bool IReadOnlyStoredProcedure.AreTransactionsSuppressed [DebuggerStepThrough] get => _areTransactionsSuppressed; } + + /// + bool IReadOnlyStoredProcedure.AreRowsAffectedReturned + { + [DebuggerStepThrough] + get => _areRowsAffectedReturned; + } /// - IReadOnlyList IReadOnlyStoredProcedure.Parameters + IReadOnlyList IReadOnlyStoredProcedure.Parameters { [DebuggerStepThrough] get => _parameters; } + + /// + IReadOnlyList IStoredProcedure.Parameters + { + [DebuggerStepThrough] + get => _parameters; + } + + /// + IReadOnlyStoredProcedureParameter? IReadOnlyStoredProcedure.FindParameter(string propertyName) + => _parameters.FirstOrDefault((IReadOnlyStoredProcedureParameter p) + => p.ForOriginalValue == false && p.PropertyName == propertyName); + + /// + [DebuggerStepThrough] + IStoredProcedureParameter? IStoredProcedure.FindParameter(string propertyName) + => (IStoredProcedureParameter?)((IReadOnlyStoredProcedure)this).FindParameter(propertyName); + + /// + IReadOnlyStoredProcedureParameter? IReadOnlyStoredProcedure.FindOriginalValueParameter(string propertyName) + => _parameters.FirstOrDefault((IReadOnlyStoredProcedureParameter p) + => p.ForOriginalValue == true && p.PropertyName == propertyName); /// - bool IReadOnlyStoredProcedure.ContainsParameter(string propertyName) - => _parameters.Contains(propertyName); + IStoredProcedureParameter? IStoredProcedure.FindOriginalValueParameter(string propertyName) + => (IStoredProcedureParameter?)((IReadOnlyStoredProcedure)this).FindOriginalValueParameter(propertyName); + + /// + IReadOnlyStoredProcedureParameter? IReadOnlyStoredProcedure.FindRowsAffectedParameter() + => _parameters.FirstOrDefault((IStoredProcedureParameter p) + => p.ForRowsAffected); + + /// + IStoredProcedureParameter? IStoredProcedure.FindRowsAffectedParameter() + => (IStoredProcedureParameter?)((IReadOnlyStoredProcedure)this).FindRowsAffectedParameter(); /// - IReadOnlyList IReadOnlyStoredProcedure.ResultColumns + IReadOnlyList IReadOnlyStoredProcedure.ResultColumns + { + [DebuggerStepThrough] + get => _resultColumns; + } + + /// + IReadOnlyList IStoredProcedure.ResultColumns { [DebuggerStepThrough] get => _resultColumns; } /// - bool IReadOnlyStoredProcedure.ContainsResultColumn(string propertyName) - => _resultColumns.Contains(propertyName); + IReadOnlyStoredProcedureResultColumn? IReadOnlyStoredProcedure.FindResultColumn(string propertyName) + => _resultColumns.FirstOrDefault((IReadOnlyStoredProcedureResultColumn c) + => c.PropertyName == propertyName); + + /// + IStoredProcedureResultColumn? IStoredProcedure.FindResultColumn(string propertyName) + => (IStoredProcedureResultColumn?)((IReadOnlyStoredProcedure)this).FindResultColumn(propertyName); + /// + IReadOnlyStoredProcedureResultColumn? IReadOnlyStoredProcedure.FindRowsAffectedResultColumn() + => _resultColumns.FirstOrDefault((IReadOnlyStoredProcedureResultColumn c) + => c.ForRowsAffected); + /// + IStoredProcedureResultColumn? IStoredProcedure.FindRowsAffectedResultColumn() + => (IStoredProcedureResultColumn?)((IReadOnlyStoredProcedure)this).FindRowsAffectedResultColumn(); + /// IStoreStoredProcedure IStoredProcedure.StoreStoredProcedure { diff --git a/src/EFCore.Relational/Metadata/RuntimeStoredProcedureParameter.cs b/src/EFCore.Relational/Metadata/RuntimeStoredProcedureParameter.cs new file mode 100644 index 00000000000..786611dd15d --- /dev/null +++ b/src/EFCore.Relational/Metadata/RuntimeStoredProcedureParameter.cs @@ -0,0 +1,132 @@ +// 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; + +/// +/// Represents a stored procedure parameter. +/// +public class RuntimeStoredProcedureParameter : AnnotatableBase, IRuntimeStoredProcedureParameter +{ + private IStoreStoredProcedureParameter? _storeParameter; + private readonly string? _propertyName; + private readonly bool _forRowsAffected; + private readonly bool? _forOriginalValue; + private readonly string _name; + private readonly ParameterDirection _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. + /// + [EntityFrameworkInternal] + public RuntimeStoredProcedureParameter( + RuntimeStoredProcedure storedProcedure, + string name, + ParameterDirection direction, + bool forRowsAffected, + string? propertyName, + bool? forOriginalValue) + { + StoredProcedure = storedProcedure; + _propertyName = propertyName; + _forOriginalValue = forOriginalValue; + _forRowsAffected = forRowsAffected; + _name = name; + _direction = direction; + } + + /// + /// Gets the stored procedure to which this parameter belongs. + /// + public virtual RuntimeStoredProcedure StoredProcedure { 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() + => ((IStoredProcedureParameter)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( + () => ((IStoredProcedureParameter)this).ToDebugString(), + () => ((IStoredProcedureParameter)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + + /// + IReadOnlyStoredProcedure IReadOnlyStoredProcedureParameter.StoredProcedure + { + [DebuggerStepThrough] + get => StoredProcedure; + } + + /// + IStoredProcedure IStoredProcedureParameter.StoredProcedure + { + [DebuggerStepThrough] + get => StoredProcedure; + } + + /// + string IReadOnlyStoredProcedureParameter.Name + { + [DebuggerStepThrough] + get => _name; + } + + /// + string? IReadOnlyStoredProcedureParameter.PropertyName + { + [DebuggerStepThrough] + get => _propertyName; + } + + /// + ParameterDirection IReadOnlyStoredProcedureParameter.Direction + { + [DebuggerStepThrough] + get => _direction; + } + + /// + bool? IReadOnlyStoredProcedureParameter.ForOriginalValue + { + [DebuggerStepThrough] + get => _forOriginalValue; + } + + /// + bool IReadOnlyStoredProcedureParameter.ForRowsAffected + { + [DebuggerStepThrough] + get => _forRowsAffected; + } + + /// + IStoreStoredProcedureParameter IStoredProcedureParameter.StoreParameter + { + [DebuggerStepThrough] + get => _storeParameter!; + } + + /// + IStoreStoredProcedureParameter IRuntimeStoredProcedureParameter.StoreParameter + { + [DebuggerStepThrough] + get => _storeParameter!; + set => _storeParameter = value; + } +} diff --git a/src/EFCore.Relational/Metadata/RuntimeStoredProcedureResultColumn.cs b/src/EFCore.Relational/Metadata/RuntimeStoredProcedureResultColumn.cs new file mode 100644 index 00000000000..cb2aedc7621 --- /dev/null +++ b/src/EFCore.Relational/Metadata/RuntimeStoredProcedureResultColumn.cs @@ -0,0 +1,112 @@ +// 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; + +/// +/// Represents a stored procedure result column. +/// +public class RuntimeStoredProcedureResultColumn : AnnotatableBase, IRuntimeStoredProcedureResultColumn +{ + private IStoreStoredProcedureResultColumn? _storeResultColumn; + private readonly string? _propertyName; + private readonly bool _forRowsAffected; + private readonly string _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. + /// + [EntityFrameworkInternal] + public RuntimeStoredProcedureResultColumn( + RuntimeStoredProcedure storedProcedure, + string name, + bool forRowsAffected, + string? propertyName) + { + StoredProcedure = storedProcedure; + _propertyName = propertyName; + _forRowsAffected = forRowsAffected; + _name = name; + } + + /// + /// Gets the stored procedure to which this parameter belongs. + /// + public virtual RuntimeStoredProcedure StoredProcedure { 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() + => ((IStoredProcedureResultColumn)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( + () => ((IStoredProcedureResultColumn)this).ToDebugString(), + () => ((IStoredProcedureResultColumn)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + + /// + IReadOnlyStoredProcedure IReadOnlyStoredProcedureResultColumn.StoredProcedure + { + [DebuggerStepThrough] + get => StoredProcedure; + } + + /// + IStoredProcedure IStoredProcedureResultColumn.StoredProcedure + { + [DebuggerStepThrough] + get => StoredProcedure; + } + + /// + string IReadOnlyStoredProcedureResultColumn.Name + { + [DebuggerStepThrough] + get => _name; + } + + /// + string? IReadOnlyStoredProcedureResultColumn.PropertyName + { + [DebuggerStepThrough] + get => _propertyName; + } + + /// + bool IReadOnlyStoredProcedureResultColumn.ForRowsAffected + { + [DebuggerStepThrough] + get => _forRowsAffected; + } + + /// + IStoreStoredProcedureResultColumn IStoredProcedureResultColumn.StoreResultColumn + { + [DebuggerStepThrough] + get => _storeResultColumn!; + } + + /// + IStoreStoredProcedureResultColumn IRuntimeStoredProcedureResultColumn.StoreResultColumn + { + [DebuggerStepThrough] + get => _storeResultColumn!; + set => _storeResultColumn = value; + } +} diff --git a/src/EFCore.Relational/Metadata/RuntimeTrigger.cs b/src/EFCore.Relational/Metadata/RuntimeTrigger.cs index e5ad7097b4b..41397d4325a 100644 --- a/src/EFCore.Relational/Metadata/RuntimeTrigger.cs +++ b/src/EFCore.Relational/Metadata/RuntimeTrigger.cs @@ -12,11 +12,13 @@ namespace Microsoft.EntityFrameworkCore.Metadata; public class RuntimeTrigger : AnnotatableBase, ITrigger { /// - /// 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. + /// Initializes a new instance of the class. /// + /// The entity type. + /// The name in the model. + /// The name in the database. + /// The name of the table. + /// The schema of the table. public RuntimeTrigger( RuntimeEntityType entityType, string modelName, @@ -33,10 +35,8 @@ public RuntimeTrigger( /// public virtual string ModelName { get; } - - /// - /// Gets the database name of the trigger. - /// + + /// public virtual string Name { get; } /// diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs index 41a6029ba1a..bf74375f8a2 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs @@ -1011,8 +1011,8 @@ protected virtual IEnumerable Diff( IsDestructiveChange = isDestructiveChange }; - var sourceTypeMapping = source.PropertyMappings.First().TypeMapping; - var targetTypeMapping = target.PropertyMappings.First().TypeMapping; + var sourceTypeMapping = source.StoreTypeMapping; + var targetTypeMapping = target.StoreTypeMapping; Initialize( alterColumnOperation, target, targetTypeMapping, @@ -1059,8 +1059,7 @@ protected virtual IEnumerable Add( Name = target.Name }; - var targetMapping = target.PropertyMappings.First(); - var targetTypeMapping = targetMapping.TypeMapping; + var targetTypeMapping = target.StoreTypeMapping; Initialize( operation, target, targetTypeMapping, target.IsNullable, diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 2ce1e55662f..1200a7e5f32 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1229,6 +1229,14 @@ public static string SqlQueryOverrideMismatch(object? propertySpecification, obj GetString("SqlQueryOverrideMismatch", nameof(propertySpecification), nameof(query)), propertySpecification, query); + /// + /// The entity type '{entityType}' is mapped to the stored procedure '{sproc}', but the concurrency token '{token}' is not mapped to any original value parameter. + /// + public static string StoredProcedureConcurrencyTokenNotMapped(object? entityType, object? sproc, object? token) + => string.Format( + GetString("StoredProcedureConcurrencyTokenNotMapped", nameof(entityType), nameof(sproc), nameof(token)), + entityType, sproc, token); + /// /// The property '{entityType}.{property}' is mapped to a parameter of the stored procedure '{sproc}', but only concurrency token and key properties are supported for Delete stored procedures. /// @@ -1237,6 +1245,62 @@ public static string StoredProcedureDeleteNonKeyProperty(object? entityType, obj GetString("StoredProcedureDeleteNonKeyProperty", nameof(entityType), nameof(property), nameof(sproc)), entityType, property, sproc); + /// + /// The original value parameter for the property '{property}' cannot be added to the stored procedure '{sproc}' because another original value parameter for this property already exists. + /// + public static string StoredProcedureDuplicateOriginalValueParameter(object? property, object? sproc) + => string.Format( + GetString("StoredProcedureDuplicateOriginalValueParameter", nameof(property), nameof(sproc)), + property, sproc); + + /// + /// The parameter for the property '{property}' cannot be added to the stored procedure '{sproc}' because another parameter for this property already exists. + /// + public static string StoredProcedureDuplicateParameter(object? property, object? sproc) + => string.Format( + GetString("StoredProcedureDuplicateParameter", nameof(property), nameof(sproc)), + property, sproc); + + /// + /// The parameter '{parameter}' cannot be added to the stored procedure '{sproc}' because another parameter with this name already exists. + /// + public static string StoredProcedureDuplicateParameterName(object? parameter, object? sproc) + => string.Format( + GetString("StoredProcedureDuplicateParameterName", nameof(parameter), nameof(sproc)), + parameter, sproc); + + /// + /// The result column for the property '{property}' cannot be added to the stored procedure '{sproc}' because another result column for this property already exists. + /// + public static string StoredProcedureDuplicateResultColumn(object? property, object? sproc) + => string.Format( + GetString("StoredProcedureDuplicateResultColumn", nameof(property), nameof(sproc)), + property, sproc); + + /// + /// The result column '{column}' cannot be added to the stored procedure '{sproc}' because another result column with this name already exists. + /// + public static string StoredProcedureDuplicateResultColumnName(object? column, object? sproc) + => string.Format( + GetString("StoredProcedureDuplicateResultColumnName", nameof(column), nameof(sproc)), + column, sproc); + + /// + /// The rows affected parameter cannot be added to the stored procedure '{sproc}' because the rows affected are already returned via another parameter, via the stored procedure return value or via a result column. + /// + public static string StoredProcedureDuplicateRowsAffectedParameter(object? sproc) + => string.Format( + GetString("StoredProcedureDuplicateRowsAffectedParameter", nameof(sproc)), + sproc); + + /// + /// The rows affected result column cannot be added to the stored procedure '{sproc}' because the rows affected are already returned via another column, via a parameter or via the stored procedure return value. + /// + public static string StoredProcedureDuplicateRowsAffectedResultColumn(object? sproc) + => string.Format( + GetString("StoredProcedureDuplicateRowsAffectedResultColumn", nameof(sproc)), + 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. /// @@ -1261,6 +1325,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 also mapped to an output original value output parameter. A store-generated property can only be mapped to one output parameter. + /// + public static string StoredProcedureOutputParameterConflict(object? entityType, object? property, object? sproc) + => string.Format( + GetString("StoredProcedureOutputParameterConflict", nameof(entityType), nameof(property), nameof(sproc)), + entityType, property, 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. /// @@ -1277,6 +1349,22 @@ public static string StoredProcedureOverrideMismatch(object? propertySpecificati GetString("StoredProcedureOverrideMismatch", nameof(propertySpecification), nameof(sproc)), propertySpecification, sproc); + /// + /// '{facet}' cannot be configured for the parameter '{parameter}' of the stored procedure '{sproc}'. + /// + public static string StoredProcedureParameterInvalidConfiguration(object? facet, object? parameter, object? sproc) + => string.Format( + GetString("StoredProcedureParameterInvalidConfiguration", nameof(facet), nameof(parameter), nameof(sproc)), + facet, parameter, sproc); + + /// + /// Unsupported direction '{direction}' was specified for the parameter '{parameter}' of the stored procedure '{sproc}'. + /// + public static string StoredProcedureParameterInvalidDirection(object? direction, object? parameter, object? sproc) + => string.Format( + GetString("StoredProcedureParameterInvalidDirection", nameof(direction), nameof(parameter), nameof(sproc)), + direction, parameter, sproc); + /// /// No property named '{property}' found on the entity type '{entityType}' corresponding to the parameter on the stored procedure '{sproc}' /// @@ -1317,6 +1405,22 @@ public static string StoredProcedureResultColumnNotGenerated(object? entityType, GetString("StoredProcedureResultColumnNotGenerated", nameof(entityType), nameof(property), nameof(sproc)), entityType, property, sproc); + /// + /// The property '{entityType}.{property}' is mapped to a result column of the stored procedure '{sproc}', but it is also mapped to an output parameter. A store-generated property can only be mapped to one of these. + /// + public static string StoredProcedureResultColumnParameterConflict(object? entityType, object? property, object? sproc) + => string.Format( + GetString("StoredProcedureResultColumnParameterConflict", nameof(entityType), nameof(property), nameof(sproc)), + entityType, property, sproc); + + /// + /// The stored procedure '{sproc}' cannot be configured to return the rows affected because a rows affected parameter or a rows affected result column for this stored procedure already exists. + /// + public static string StoredProcedureRowsAffectedReturnConflictingParameter(object? sproc) + => string.Format( + GetString("StoredProcedureRowsAffectedReturnConflictingParameter", nameof(sproc)), + sproc); + /// /// Both entity type '{entityType1}' and '{entityType2}' were configured to use '{sproc}', stored procedure sharing is not supported. Specify different names for the corresponding stored procedures. /// @@ -1333,6 +1437,14 @@ public static string StoredProcedureTphDuplicate(object? entityType, object? oth GetString("StoredProcedureTphDuplicate", nameof(entityType), nameof(otherEntityType), nameof(sproc)), entityType, otherEntityType, sproc); + /// + /// The entity type '{entityType}' was configured to use some stored procedures and is not mapped to any table. An entity type that isn't mapped to a table must be mapped to insert, update and delete stored procedures. + /// + public static string StoredProcedureUnmapped(object? entityType) + => string.Format( + GetString("StoredProcedureUnmapped", nameof(entityType)), + entityType); + /// /// The entity type '{entityType}' is not mapped to the store object '{table}'. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index cb446cd6249..5ea22837654 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -861,9 +861,33 @@ The property '{propertySpecification}' has specific configuration for the SQL query '{query}', but isn't mapped to a column on that query. Remove the specific configuration, or map an entity type that contains this property to '{query}'. + + The entity type '{entityType}' is mapped to the stored procedure '{sproc}', but the concurrency token '{token}' is not mapped to any original value parameter. + The property '{entityType}.{property}' is mapped to a parameter of the stored procedure '{sproc}', but only concurrency token and key properties are supported for Delete stored procedures. + + The original value parameter for the property '{property}' cannot be added to the stored procedure '{sproc}' because another original value parameter for this property already exists. + + + The parameter for the property '{property}' cannot be added to the stored procedure '{sproc}' because another parameter for this property already exists. + + + The parameter '{parameter}' cannot be added to the stored procedure '{sproc}' because another parameter with this name already exists. + + + The result column for the property '{property}' cannot be added to the stored procedure '{sproc}' because another result column for this property already exists. + + + The result column '{column}' cannot be added to the stored procedure '{sproc}' because another result column with this name already exists. + + + The rows affected parameter cannot be added to the stored procedure '{sproc}' because the rows affected are already returned via another parameter, via the stored procedure return value or via a result column. + + + The rows affected result column cannot be added to the stored procedure '{sproc}' because the rows affected are already returned via another column, via a parameter or via the stored procedure return value. + 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. @@ -873,12 +897,21 @@ 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 also mapped to an output original value output parameter. A store-generated property can only be mapped to one output parameter. + 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}'. + + '{facet}' cannot be configured for the parameter '{parameter}' of the stored procedure '{sproc}'. + + + Unsupported direction '{direction}' was specified for the parameter '{parameter}' of the stored procedure '{sproc}'. + No property named '{property}' found on the entity type '{entityType}' corresponding to the parameter on the stored procedure '{sproc}' @@ -894,12 +927,21 @@ The property '{entityType}.{property}' is mapped to a result column of the stored procedure '{sproc}', but it is not configured as store-generated. + + The property '{entityType}.{property}' is mapped to a result column of the stored procedure '{sproc}', but it is also mapped to an output parameter. A store-generated property can only be mapped to one of these. + + + The stored procedure '{sproc}' cannot be configured to return the rows affected because a rows affected parameter or a rows affected result column for this stored procedure already exists. + Both entity type '{entityType1}' and '{entityType2}' were configured to use '{sproc}', stored procedure sharing is not supported. Specify different names for the corresponding stored procedures. Both '{entityType}' and '{otherEntityType}' are explicitly mapped to the stored procedure '{sproc}' using the 'TPH' mapping strategy. Configure the stored procedure mapping on the root entity type, including all parameters for the derived types. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. + + The entity type '{entityType}' was configured to use some stored procedures and is not mapped to any table. An entity type that isn't mapped to a table must be mapped to insert, update and delete stored procedures. + The entity type '{entityType}' is not mapped to the store object '{table}'. diff --git a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerOnDeleteConvention.cs b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerOnDeleteConvention.cs index 19d6c35ca06..56e1ba5eb72 100644 --- a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerOnDeleteConvention.cs +++ b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerOnDeleteConvention.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - - // ReSharper disable once CheckNamespace namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; diff --git a/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs b/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs index 8b26227b643..d6697ef782d 100644 --- a/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs +++ b/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs @@ -382,7 +382,8 @@ public static bool IsStrictlyDerivedFrom(this EntityType entityType, IReadOnlyEn public static IEnumerable FindDerivedNavigations( this IReadOnlyEntityType entityType, string navigationName) - => entityType.GetDerivedNavigations().Where(navigation => navigationName == navigation.Name); + => entityType.GetDerivedTypes().Select(t => t.FindDeclaredNavigation(navigationName)!) + .Where(n => n != null); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs index f65ae0b71ed..6ed728dec13 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs @@ -83,7 +83,6 @@ public void Test_new_annotations_handled_for_entity_types() RelationalAnnotationNames.IsFixedLength, RelationalAnnotationNames.Collation, RelationalAnnotationNames.IsStored, - RelationalAnnotationNames.ParameterDirection, RelationalAnnotationNames.TpcMappingStrategy, RelationalAnnotationNames.TphMappingStrategy, RelationalAnnotationNames.TptMappingStrategy, @@ -347,7 +346,7 @@ private static void MissingAnnotationCheck( if (!invalidAnnotations.Contains(annotationName)) { - var modelBuilder = RelationalTestHelpers.Instance.CreateConventionBuilder(); + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); var metadataItem = createMetadataItem(modelBuilder); metadataItem.SetAnnotation( annotationName, validAnnotations.ContainsKey(annotationName) @@ -443,7 +442,7 @@ public void Snapshot_with_enum_discriminator_uses_converted_values() new CSharpSnapshotGeneratorDependencies( codeHelper, sqlServerTypeMappingSource, sqlServerAnnotationCodeGenerator)))); - var modelBuilder = RelationalTestHelpers.Instance.CreateConventionBuilder(); + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); modelBuilder.Model.RemoveAnnotation(CoreAnnotationNames.ProductVersion); modelBuilder.Entity( eb => @@ -470,7 +469,7 @@ public void Snapshot_with_enum_discriminator_uses_converted_values() private static void AssertConverter(ValueConverter valueConverter, string expected) { - var modelBuilder = RelationalTestHelpers.Instance.CreateConventionBuilder(); + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); var property = modelBuilder.Entity().Property(e => e.Id).Metadata; property.SetMaxLength(1000); property.SetValueConverter(valueConverter); @@ -698,7 +697,7 @@ public void Snapshots_compile() { var generator = CreateMigrationsCodeGenerator(); - var modelBuilder = RelationalTestHelpers.Instance.CreateConventionBuilder(); + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); modelBuilder.Model.RemoveAnnotation(CoreAnnotationNames.ProductVersion); modelBuilder.Entity( x => @@ -794,7 +793,7 @@ public void Snapshot_with_default_values_are_round_tripped() { var generator = CreateMigrationsCodeGenerator(); - var modelBuilder = RelationalTestHelpers.Instance.CreateConventionBuilder(); + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); modelBuilder.Entity( eb => { diff --git a/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs b/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs index 9e878820428..fb9bccaaf17 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs @@ -71,7 +71,7 @@ var migrationAssembly new FakeDiagnosticsLogger()); var historyRepository = new MockHistoryRepository(); - var services = RelationalTestHelpers.Instance.CreateContextServices(); + var services = FakeRelationalTestHelpers.Instance.CreateContextServices(); var model = new Model().FinalizeModel(); model.AddRuntimeAnnotation(RelationalAnnotationNames.RelationalModel, new RelationalModel(model)); diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpModelGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpModelGeneratorTest.cs index 9023d6205a3..3efc541ed9c 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpModelGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpModelGeneratorTest.cs @@ -22,7 +22,7 @@ public void Language_works() public void WriteCode_works() { var generator = CreateGenerator(); - var modelBuilder = RelationalTestHelpers.Instance.CreateConventionBuilder(); + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); modelBuilder.Entity("TestEntity").Property("Id"); var result = generator.GenerateModel( diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs index 9786af15cd1..32081cbbe29 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs @@ -2516,7 +2516,6 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba StoreObjectIdentifier.InsertStoredProcedure(""Derived_Insert"", ""TPC""), true, ""DerivedId""); - idDerivedInsert.AddAnnotation(""foo"", ""bar3""); overrides.Add(StoreObjectIdentifier.InsertStoredProcedure(""Derived_Insert"", ""TPC""), idDerivedInsert); var idPrincipalBaseView = new RuntimeRelationalPropertyOverrides( id, @@ -2525,14 +2524,6 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba 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(""Relational:DefaultValueSql"", ""NEXT VALUE FOR [TPC].[PrincipalBaseSequence]""); @@ -2601,11 +2592,16 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) runtimeEntityType, ""PrincipalBase_Insert"", ""TPC"", + false, true); - insertSproc.AddParameter(""PrincipalBaseId""); - insertSproc.AddParameter(""PrincipalDerivedId""); - insertSproc.AddParameter(""Id""); + var principalBaseId = insertSproc.AddParameter( + ""PrincipalBaseId"", ParameterDirection.Input, false, ""PrincipalBaseId"", false); + var principalDerivedId = insertSproc.AddParameter( + ""PrincipalDerivedId"", ParameterDirection.Input, false, ""PrincipalDerivedId"", false); + var baseId = insertSproc.AddParameter( + ""BaseId"", ParameterDirection.Output, false, ""Id"", false); + baseId.AddAnnotation(""foo"", ""bar""); insertSproc.AddAnnotation(""foo"", ""bar1""); runtimeEntityType.AddAnnotation(""Relational:InsertStoredProcedure"", insertSproc); @@ -2613,20 +2609,26 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) runtimeEntityType, ""PrincipalBase_Delete"", ""TPC"", + true, false); - deleteSproc.AddParameter(""Id""); + var id = deleteSproc.AddParameter( + ""Id"", ParameterDirection.Input, false, ""Id"", false); runtimeEntityType.AddAnnotation(""Relational:DeleteStoredProcedure"", deleteSproc); var updateSproc = new RuntimeStoredProcedure( runtimeEntityType, ""PrincipalBase_Update"", ""TPC"", + false, false); - updateSproc.AddParameter(""PrincipalBaseId""); - updateSproc.AddParameter(""PrincipalDerivedId""); - updateSproc.AddParameter(""Id""); + var principalBaseId0 = updateSproc.AddParameter( + ""PrincipalBaseId"", ParameterDirection.Input, false, ""PrincipalBaseId"", false); + var principalDerivedId0 = updateSproc.AddParameter( + ""PrincipalDerivedId"", ParameterDirection.Input, false, ""PrincipalDerivedId"", false); + var id0 = updateSproc.AddParameter( + ""Id"", ParameterDirection.Input, false, ""Id"", false); runtimeEntityType.AddAnnotation(""Relational:UpdateStoredProcedure"", updateSproc); runtimeEntityType.AddAnnotation(""Relational:FunctionName"", null); @@ -2648,6 +2650,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) c => AssertFileContents( "PrincipalDerivedEntityType.cs", @"// using System; +using System.Data; using System.Reflection; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Scaffolding.Internal; @@ -2676,31 +2679,42 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) runtimeEntityType, ""Derived_Insert"", ""TPC"", + false, false); - insertSproc.AddParameter(""PrincipalBaseId""); - insertSproc.AddParameter(""PrincipalDerivedId""); - insertSproc.AddResultColumn(""Id""); + var principalBaseId = insertSproc.AddParameter( + ""PrincipalBaseId"", ParameterDirection.Input, false, ""PrincipalBaseId"", false); + var principalDerivedId = insertSproc.AddParameter( + ""PrincipalDerivedId"", ParameterDirection.Input, false, ""PrincipalDerivedId"", false); + var derivedId = insertSproc.AddResultColumn( + ""DerivedId"", false, ""Id""); + derivedId.AddAnnotation(""foo"", ""bar3""); runtimeEntityType.AddAnnotation(""Relational:InsertStoredProcedure"", insertSproc); var deleteSproc = new RuntimeStoredProcedure( runtimeEntityType, ""Derived_Delete"", ""TPC"", + false, false); - deleteSproc.AddParameter(""Id""); + var id = deleteSproc.AddParameter( + ""Id"", ParameterDirection.Input, false, ""Id"", false); runtimeEntityType.AddAnnotation(""Relational:DeleteStoredProcedure"", deleteSproc); var updateSproc = new RuntimeStoredProcedure( runtimeEntityType, ""Derived_Update"", ""Derived"", + false, false); - updateSproc.AddParameter(""PrincipalBaseId""); - updateSproc.AddParameter(""PrincipalDerivedId""); - updateSproc.AddParameter(""Id""); + var principalBaseId0 = updateSproc.AddParameter( + ""PrincipalBaseId"", ParameterDirection.Input, false, ""PrincipalBaseId"", false); + var principalDerivedId0 = updateSproc.AddParameter( + ""PrincipalDerivedId"", ParameterDirection.Input, false, ""PrincipalDerivedId"", false); + var id0 = updateSproc.AddParameter( + ""Id"", ParameterDirection.Input, false, ""Id"", false); runtimeEntityType.AddAnnotation(""Relational:UpdateStoredProcedure"", updateSproc); runtimeEntityType.AddAnnotation(""Relational:FunctionName"", null); @@ -2740,33 +2754,37 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) var insertSproc = principalBase.GetInsertStoredProcedure()!; Assert.Equal("PrincipalBase_Insert", insertSproc.Name); Assert.Equal("TPC", insertSproc.Schema); - Assert.Equal(new[] { "PrincipalBaseId", "PrincipalDerivedId", "Id" }, insertSproc.Parameters); + Assert.Equal(new[] { "PrincipalBaseId", "PrincipalDerivedId", "Id" }, insertSproc.Parameters.Select(p => p.PropertyName)); Assert.Empty(insertSproc.ResultColumns); Assert.True(insertSproc.AreTransactionsSuppressed); + Assert.False(insertSproc.AreRowsAffectedReturned); 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"]); + Assert.Equal("BaseId", insertSproc.Parameters.Last().Name); + Assert.Equal("bar", insertSproc.Parameters.Last()["foo"]); + Assert.Null(id.FindOverrides(StoreObjectIdentifier.Create(principalBase, StoreObjectType.InsertStoredProcedure).Value)); var updateSproc = principalBase.GetUpdateStoredProcedure()!; Assert.Equal("PrincipalBase_Update", updateSproc.Name); Assert.Equal("TPC", updateSproc.Schema); - Assert.Equal(new[] { "PrincipalBaseId", "PrincipalDerivedId", "Id" }, updateSproc.Parameters); + Assert.Equal(new[] { "PrincipalBaseId", "PrincipalDerivedId", "Id" }, updateSproc.Parameters.Select(p => p.PropertyName)); Assert.Empty(updateSproc.ResultColumns); Assert.False(updateSproc.AreTransactionsSuppressed); + Assert.False(updateSproc.AreRowsAffectedReturned); Assert.Empty(updateSproc.GetAnnotations()); Assert.Same(principalBase, updateSproc.EntityType); - Assert.Equal("Id", id.GetColumnName(StoreObjectIdentifier.Create(principalBase, StoreObjectType.UpdateStoredProcedure).Value)); + Assert.Equal("Id", updateSproc.Parameters.Last().Name); 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.Equal(new[] { "Id" }, deleteSproc.Parameters.Select(p => p.Name)); Assert.Empty(deleteSproc.ResultColumns); + Assert.False(deleteSproc.AreTransactionsSuppressed); + Assert.True(deleteSproc.AreRowsAffectedReturned); Assert.Same(principalBase, deleteSproc.EntityType); - Assert.Equal("Id", id.GetColumnName(StoreObjectIdentifier.Create(principalBase, StoreObjectType.DeleteStoredProcedure).Value)); + Assert.Equal("Id", deleteSproc.Parameters.Last().Name); Assert.Null(id.FindOverrides(StoreObjectIdentifier.Create(principalBase, StoreObjectType.DeleteStoredProcedure).Value)); Assert.Equal("PrincipalBase", principalBase.GetDiscriminatorValue()); @@ -2791,34 +2809,35 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) 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.Equal(new[] { "PrincipalBaseId", "PrincipalDerivedId" }, insertSproc.Parameters.Select(p => p.PropertyName)); + Assert.Equal(new[] { "Id" }, insertSproc.ResultColumns.Select(p => p.PropertyName)); Assert.False(insertSproc.AreTransactionsSuppressed); Assert.Null(insertSproc["foo"]); Assert.Same(principalDerived, insertSproc.EntityType); + Assert.Equal("DerivedId", insertSproc.ResultColumns.Last().Name); Assert.Equal("DerivedId", id.GetColumnName(StoreObjectIdentifier.Create(principalDerived, StoreObjectType.InsertStoredProcedure).Value)); - Assert.Equal("bar3", - id.FindOverrides(StoreObjectIdentifier.Create(principalDerived, StoreObjectType.InsertStoredProcedure).Value)["foo"]); + Assert.Equal("bar3", insertSproc.ResultColumns.Last()["foo"]); + Assert.Null(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.Equal(new[] { "PrincipalBaseId", "PrincipalDerivedId", "Id" }, updateSproc.Parameters.Select(p => p.PropertyName)); 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.Equal("Id", updateSproc.Parameters.Last().Name); 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.Equal(new[] { "Id" }, deleteSproc.Parameters.Select(p => p.PropertyName)); 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.Equal("Id", deleteSproc.Parameters.Last().Name); Assert.Null(id.FindOverrides(StoreObjectIdentifier.Create(principalDerived, StoreObjectType.DeleteStoredProcedure).Value)); Assert.Equal("PrincipalDerived>", principalDerived.GetDiscriminatorValue()); @@ -2900,6 +2919,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasParameter("PrincipalDerivedId") .HasParameter(p => p.Id)); eb.DeleteUsingStoredProcedure(s => s + .HasRowsAffectedReturnValue() .HasParameter(p => p.Id)); }); diff --git a/test/EFCore.Design.Tests/TestUtilities/DesignTestHelpers.cs b/test/EFCore.Design.Tests/TestUtilities/DesignTestHelpers.cs index a36b1eab231..a65dc172213 100644 --- a/test/EFCore.Design.Tests/TestUtilities/DesignTestHelpers.cs +++ b/test/EFCore.Design.Tests/TestUtilities/DesignTestHelpers.cs @@ -5,7 +5,7 @@ namespace Microsoft.EntityFrameworkCore.TestUtilities; -public class DesignTestHelpers : TestHelpers +public class DesignTestHelpers : RelationalTestHelpers { protected DesignTestHelpers() { diff --git a/test/EFCore.Relational.Specification.Tests/DataAnnotationRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/DataAnnotationRelationalTestBase.cs index 33b6d67345f..a11bdc374b6 100644 --- a/test/EFCore.Relational.Specification.Tests/DataAnnotationRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/DataAnnotationRelationalTestBase.cs @@ -14,6 +14,54 @@ protected DataAnnotationRelationalTestBase(TFixture fixture) { } + [ConditionalFact] + public virtual void ForeignKey_to_ForeignKey_on_many_to_many() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + entity => + { + entity.HasMany(d => d.Profile16s) + .WithMany(p => p.Login16s) + .UsingEntity>( + "Login16Profile16", + l => l.HasOne().WithMany().HasForeignKey("Profile16Id"), + r => r.HasOne().WithMany().HasForeignKey("Login16Id"), + j => + { + j.HasKey("Login16Id", "Profile16Id"); + + j.ToTable("Login16Profile16"); + }); + }); + + var model = Validate(modelBuilder); + + var login = modelBuilder.Model.FindEntityType(typeof(Login16)); + var logins = login.FindSkipNavigation(nameof(Login16.Profile16s)); + var join = logins.JoinEntityType; + Assert.Equal(2, join.GetProperties().Count()); + Assert.False(GetProperty(model, "Login16Id").IsForeignKey()); + Assert.False(GetProperty(model, "Profile16Id").IsForeignKey()); + } + + public class Login16 + { + public int Login16Id { get; set; } + + [ForeignKey("Login16Id")] + public virtual ICollection Profile16s { get; set; } + } + + public class Profile16 + { + public int Profile16Id { get; set; } + + [ForeignKey("Profile16Id")] + public virtual ICollection Login16s { get; set; } + } + [ConditionalFact] public virtual void Table_can_configure_TPT_with_Owned() => ExecuteWithStrategyInTransaction( diff --git a/test/EFCore.Relational.Specification.Tests/EFCore.Relational.Specification.Tests.csproj b/test/EFCore.Relational.Specification.Tests/EFCore.Relational.Specification.Tests.csproj index b45763d16c6..cba1f1ace6a 100644 --- a/test/EFCore.Relational.Specification.Tests/EFCore.Relational.Specification.Tests.csproj +++ b/test/EFCore.Relational.Specification.Tests/EFCore.Relational.Specification.Tests.csproj @@ -53,6 +53,7 @@ + diff --git a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs index e9c185dc0ad..5a94fb821c8 100644 --- a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs @@ -2008,7 +2008,7 @@ protected virtual void AssertSql(params string[] expected) public abstract class MigrationsFixtureBase : SharedStoreFixtureBase { - public abstract TestHelpers TestHelpers { get; } + public abstract RelationalTestHelpers TestHelpers { get; } public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; diff --git a/test/EFCore.Relational.Specification.Tests/Query/OwnedEntityQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/OwnedEntityQueryRelationalTestBase.cs index 92a9a0369d9..8945f7a2930 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/OwnedEntityQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/OwnedEntityQueryRelationalTestBase.cs @@ -230,6 +230,97 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Owned_entity_with_all_null_properties_materializes_when_not_containing_another_owned_entity(bool async) + { + var contextFactory = await InitializeAsync(seed: c => c.Seed()); + + using var context = contextFactory.CreateContext(); + var query = context.RotRutCases.OrderBy(e => e.Buyer); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Collection(result, + t => + { + Assert.Equal("Buyer1", t.Buyer); + Assert.NotNull(t.Rot); + Assert.Equal(1, t.Rot.ServiceType); + Assert.Equal("1", t.Rot.ApartmentNo); + Assert.NotNull(t.Rut); + Assert.Equal(1, t.Rut.Value); + }, + t => + { + Assert.Equal("Buyer2", t.Buyer); + // Cannot verify owned entities here since they differ between relational/in-memory + }); + } + + protected class MyContext28247 : DbContext + { + public MyContext28247(DbContextOptions options) + : base(options) + { + } + + public DbSet RotRutCases { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(b => + { + b.ToTable("RotRutCases"); + + b.OwnsOne(e => e.Rot); + b.OwnsOne(e => e.Rut); + }); + } + + public void Seed() + { + Add( + new RotRutCase + { + Buyer = "Buyer1", + Rot = new Rot { ServiceType = 1, ApartmentNo = "1" }, + Rut = new Rut { Value = 1 } + }); + + Add( + new RotRutCase + { + Buyer = "Buyer2", + Rot = new Rot { ServiceType = null, ApartmentNo = null }, + Rut = new Rut { Value = null } + }); + + SaveChanges(); + } + } + + public class RotRutCase + { + public int Id { get; set; } + public string Buyer { get; set; } + public Rot Rot { get; set; } + public Rut Rut { get; set; } + } + + public class Rot + { + public int? ServiceType { get; set; } + public string ApartmentNo { get; set; } + } + + public class Rut + { + public int? Value { get; set; } + } + protected override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) { return base.AddOptions(builder).ConfigureWarnings( diff --git a/test/EFCore.Relational.Specification.Tests/Query/SimpleQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/SimpleQueryRelationalTestBase.cs index 5331a46ad70..e0cc9149aa5 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/SimpleQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/SimpleQueryRelationalTestBase.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.ComponentModel.DataAnnotations.Schema; using NameSpace1; diff --git a/test/EFCore.Relational.Specification.Tests/TestUtilities/RelationalTestHelpers.cs b/test/EFCore.Relational.Specification.Tests/TestUtilities/RelationalTestHelpers.cs new file mode 100644 index 00000000000..3371c004dee --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TestUtilities/RelationalTestHelpers.cs @@ -0,0 +1,77 @@ +// 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.Design.Internal; +using Microsoft.EntityFrameworkCore.Internal; + +namespace Microsoft.EntityFrameworkCore.TestUtilities; + +public abstract class RelationalTestHelpers : TestHelpers +{ + protected virtual EntityFrameworkDesignServicesBuilder CreateEntityFrameworkDesignServicesBuilder(IServiceCollection services) + => new(services); + + public IServiceProvider CreateDesignServiceProvider( + IServiceCollection customServices = null, + Action replaceServices = null, + Type additionalDesignTimeServices = null, + IOperationReporter reporter = null) + => CreateDesignServiceProvider( + CreateContext().GetService().Name, + customServices, + replaceServices, + additionalDesignTimeServices, + reporter); + + public IServiceProvider CreateDesignServiceProvider( + string provider, + IServiceCollection customServices = null, + Action replaceServices = null, + Type additionalDesignTimeServices = null, + IOperationReporter reporter = null) + => CreateServiceProvider( + customServices, services => + { + if (replaceServices != null) + { + var builder = CreateEntityFrameworkDesignServicesBuilder(services); + replaceServices(builder); + } + + if (additionalDesignTimeServices != null) + { + ConfigureDesignTimeServices(additionalDesignTimeServices, services); + } + + ConfigureProviderServices(provider, services); + services.AddEntityFrameworkDesignTimeServices(reporter); + + return services; + }); + + private void ConfigureProviderServices(string provider, IServiceCollection services) + { + var providerAssembly = Assembly.Load(new AssemblyName(provider)); + + var providerServicesAttribute = providerAssembly.GetCustomAttribute(); + if (providerServicesAttribute == null) + { + throw new InvalidOperationException(DesignStrings.CannotFindDesignTimeProviderAssemblyAttribute(provider)); + } + + var designTimeServicesType = providerAssembly.GetType( + providerServicesAttribute.TypeName, + throwOnError: true, + ignoreCase: false)!; + + ConfigureDesignTimeServices(designTimeServicesType, services); + } + + private static void ConfigureDesignTimeServices( + Type designTimeServicesType, + IServiceCollection services) + { + var designTimeServices = (IDesignTimeServices)Activator.CreateInstance(designTimeServicesType)!; + designTimeServices.ConfigureDesignTimeServices(services); + } +} diff --git a/test/EFCore.Specification.Tests/TestUtilities/TestOperationReporter.cs b/test/EFCore.Relational.Specification.Tests/TestUtilities/TestOperationReporter.cs similarity index 100% rename from test/EFCore.Specification.Tests/TestUtilities/TestOperationReporter.cs rename to test/EFCore.Relational.Specification.Tests/TestUtilities/TestOperationReporter.cs diff --git a/test/EFCore.Relational.Tests/Design/AnnotationCodeGeneratorTest.cs b/test/EFCore.Relational.Tests/Design/AnnotationCodeGeneratorTest.cs index bb371017233..d390013a875 100644 --- a/test/EFCore.Relational.Tests/Design/AnnotationCodeGeneratorTest.cs +++ b/test/EFCore.Relational.Tests/Design/AnnotationCodeGeneratorTest.cs @@ -45,7 +45,7 @@ public void GenerateFluentApi_IProperty_works_with_collation() } private ModelBuilder CreateModelBuilder() - => RelationalTestHelpers.Instance.CreateConventionBuilder(); + => FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); private AnnotationCodeGenerator CreateGenerator() => new( diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 75e0aa1899f..2ef844b692b 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -2619,6 +2619,22 @@ public virtual void Detects_keyless_entity_type_mapped_to_a_stored_procedure() RelationalStrings.StoredProcedureKeyless(nameof(Animal), "Animal_Insert"), modelBuilder); } + + [ConditionalFact] + public virtual void Detects_tableless_entity_type_mapped_to_some_stored_procedures() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .Ignore(a => a.FavoritePerson) + .ToTable((string)null) + .InsertUsingStoredProcedure(s => s.HasParameter(c => c.Id).HasParameter(c => c.Name)) + .UpdateUsingStoredProcedure(s => s.HasParameter(c => c.Id).HasParameter(c => c.Name)) + .Property(a => a.Id).ValueGeneratedNever(); + + VerifyError( + RelationalStrings.StoredProcedureUnmapped(nameof(Animal)), + modelBuilder); + } [ConditionalFact] public virtual void Detects_derived_entity_type_mapped_to_a_stored_procedure_in_TPH() @@ -2713,6 +2729,36 @@ public virtual void Detects_unmatched_stored_procedure_result_columns_in_TPH() RelationalStrings.StoredProcedureResultColumnNotFound("Missing", nameof(Animal), "dbo.Update"), modelBuilder); } + + [ConditionalFact] + public virtual void Detects_duplicate_parameter() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .InsertUsingStoredProcedure(s => s + .HasParameter(a => a.Id, p => p.IsOutput()) + .HasRowsAffectedParameter(c => c.HasName("Id")) + .HasParameter("FavoritePersonId")); + + VerifyError( + RelationalStrings.StoredProcedureDuplicateParameterName("Id", "Animal_Insert"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_duplicate_result_column() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .InsertUsingStoredProcedure(s => s + .HasResultColumn(a => a.Id, c => c.HasName("Id")) + .HasRowsAffectedResultColumn(c => c.HasName("Id")) + .HasParameter("FavoritePersonId")); + + VerifyError( + RelationalStrings.StoredProcedureDuplicateResultColumnName("Id", "Animal_Insert"), + modelBuilder); + } [ConditionalFact] public virtual void Detects_non_generated_insert_stored_procedure_result_columns_in_TPH() @@ -2782,6 +2828,55 @@ public virtual void Detects_delete_stored_procedure_result_columns_in_TPH() RelationalStrings.StoredProcedureResultColumnDelete(nameof(Animal), nameof(Animal.Name), "Delete"), modelBuilder); } + + [ConditionalFact] + public virtual void Detects_generated_properties_mapped_to_result_and_parameter() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .UpdateUsingStoredProcedure(s => s + .HasParameter(a => a.Id) + .HasParameter(a => a.Name, p => p.IsInputOutput()) + .HasResultColumn(a => a.Name)) + .Property(a => a.Name).ValueGeneratedOnUpdate(); + + VerifyError( + RelationalStrings.StoredProcedureResultColumnParameterConflict(nameof(Animal), nameof(Animal.Name), "Animal_Update"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_generated_properties_mapped_to_original_and_current_parameter() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .UpdateUsingStoredProcedure(s => s + .HasParameter(a => a.Id) + .HasParameter(a => a.Name, p => p.IsOutput()) + .HasOriginalValueParameter(a => a.Name, p => p.IsInputOutput().HasName("OriginalName"))) + .Property(a => a.Name).ValueGeneratedOnUpdate(); + + VerifyError( + RelationalStrings.StoredProcedureOutputParameterConflict(nameof(Animal), nameof(Animal.Name), "Animal_Update"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_unmapped_concurrency_token() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity() + .UpdateUsingStoredProcedure(s => s + .HasParameter(a => a.Id) + .HasParameter("FavoritePersonId") + .HasParameter(a => a.Name, p => p.IsOutput()) + .HasRowsAffectedReturnValue()) + .Property(a => a.Name).IsRowVersion(); + + VerifyError( + RelationalStrings.StoredProcedureConcurrencyTokenNotMapped(nameof(Animal), "Animal_Update", nameof(Animal.Name)), + modelBuilder); + } [ConditionalFact] public virtual void Passes_on_valid_UsingDeleteStoredProcedure_in_TPT() @@ -2853,7 +2948,7 @@ public virtual void Detects_unmatched_stored_procedure_parameters_in_TPT() var modelBuilder = CreateConventionModelBuilder(); modelBuilder.Entity() .UseTptMappingStrategy() - .UpdateUsingStoredProcedure("Update", s => s.HasParameter((Cat c) => c.Breed)); + .UpdateUsingStoredProcedure("Update", s => s.HasOriginalValueParameter((Cat c) => c.Breed)); modelBuilder.Entity(); VerifyError( @@ -2879,8 +2974,9 @@ public virtual void Detects_unmatched_stored_procedure_result_columns_in_TPT() public virtual void Detects_InsertUsingStoredProcedure_without_a_name() { var modelBuilder = CreateConventionModelBuilder(); - modelBuilder.Entity().InsertUsingStoredProcedure(s => { }).UseTpcMappingStrategy(); + var entityType = (IConventionEntityType)modelBuilder.Entity().UseTpcMappingStrategy().Metadata; modelBuilder.Entity>(); + entityType.SetInsertStoredProcedure(); VerifyError( RelationalStrings.StoredProcedureNoName(nameof(Abstract), "InsertStoredProcedure"), @@ -3340,5 +3436,5 @@ protected virtual TestHelpers.TestModelBuilder CreateModelBuilderWithoutConventi typeof(T))); protected override TestHelpers TestHelpers - => RelationalTestHelpers.Instance; + => FakeRelationalTestHelpers.Instance; } diff --git a/test/EFCore.Relational.Tests/Metadata/Conventions/Internal/TableValuedDbFunctionConventionTest.cs b/test/EFCore.Relational.Tests/Metadata/Conventions/Internal/TableValuedDbFunctionConventionTest.cs index c52cdfcb20f..66bd0ea6c1f 100644 --- a/test/EFCore.Relational.Tests/Metadata/Conventions/Internal/TableValuedDbFunctionConventionTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/Conventions/Internal/TableValuedDbFunctionConventionTest.cs @@ -95,7 +95,7 @@ public void Throws_when_adding_a_function_returning_a_scalar() } private static TestHelpers.TestModelBuilder CreateModelBuilder() - => RelationalTestHelpers.Instance.CreateConventionBuilder(); + => FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); private static IModel Finalize(TestHelpers.TestModelBuilder modelBuilder) => modelBuilder.FinalizeModel(); diff --git a/test/EFCore.Relational.Tests/Metadata/Conventions/SequenceUniquificationConventionTest.cs b/test/EFCore.Relational.Tests/Metadata/Conventions/SequenceUniquificationConventionTest.cs index 081d520258e..14b97a8fad0 100644 --- a/test/EFCore.Relational.Tests/Metadata/Conventions/SequenceUniquificationConventionTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/Conventions/SequenceUniquificationConventionTest.cs @@ -73,8 +73,8 @@ private ModelBuilder GetModelBuilder() } private ProviderConventionSetBuilderDependencies CreateDependencies() - => RelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); + => FakeRelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); private RelationalConventionSetBuilderDependencies CreateRelationalDependencies() - => RelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); + => FakeRelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); } diff --git a/test/EFCore.Relational.Tests/Metadata/Conventions/TableSharingConcurrencyTokenConventionTest.cs b/test/EFCore.Relational.Tests/Metadata/Conventions/TableSharingConcurrencyTokenConventionTest.cs index 2ed3b5c64d7..b567393708e 100644 --- a/test/EFCore.Relational.Tests/Metadata/Conventions/TableSharingConcurrencyTokenConventionTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/Conventions/TableSharingConcurrencyTokenConventionTest.cs @@ -202,8 +202,8 @@ private ModelBuilder GetModelBuilder(DbContext dbContext = null) } private ProviderConventionSetBuilderDependencies CreateDependencies() - => RelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); + => FakeRelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); private RelationalConventionSetBuilderDependencies CreateRelationalDependencies() - => RelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); + => FakeRelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); } diff --git a/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs b/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs index a60a116d921..d5d03790cc5 100644 --- a/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs +++ b/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs @@ -841,5 +841,5 @@ public void DbFunction_Queryable_custom_translation() } private TestHelpers.TestModelBuilder GetModelBuilder() - => RelationalTestHelpers.Instance.CreateConventionBuilder(); + => FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); } diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs index 82a36d3e519..68916ff69f0 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs @@ -1538,7 +1538,7 @@ private void AssertIsGeneric(ReferenceReferenceBuilder _) } protected virtual ModelBuilder CreateConventionModelBuilder() - => RelationalTestHelpers.Instance.CreateConventionBuilder(); + => FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); private InternalModelBuilder CreateBuilder() => (InternalModelBuilder)CreateConventionModelBuilder().GetInfrastructure(); diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalEntityTypeAttributeConventionTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalEntityTypeAttributeConventionTest.cs index ab859f9a276..a69f060b9bd 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalEntityTypeAttributeConventionTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalEntityTypeAttributeConventionTest.cs @@ -106,13 +106,13 @@ private InternalEntityTypeBuilder CreateInternalEntityTypeBuilder() } private ProviderConventionSetBuilderDependencies CreateDependencies() - => RelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); + => FakeRelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); private RelationalConventionSetBuilderDependencies CreateRelationalDependencies() - => RelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); + => FakeRelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); protected virtual ModelBuilder CreateConventionalModelBuilder() - => RelationalTestHelpers.Instance.CreateConventionBuilder(); + => FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); [Table("MyTable", Schema = "MySchema")] [Comment("Test table comment")] diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalIndexTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalIndexTest.cs index 816b25b308b..164ee26a200 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalIndexTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalIndexTest.cs @@ -51,7 +51,7 @@ public void IndexAttribute_database_name_can_be_overriden_using_fluent_api() } protected virtual ModelBuilder CreateConventionModelBuilder() - => RelationalTestHelpers.Instance.CreateConventionBuilder(); + => FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); [Index(nameof(A), nameof(B), Name = "IndexOnAAndB", IsUnique = true)] [Index(nameof(B), nameof(C), Name = "IndexOnBAndC")] diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs index 4561c23cf1c..f12ded84343 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs @@ -618,7 +618,7 @@ public void RemoveCheckConstraint_returns_null_when_constraint_is_missing() } protected virtual ModelBuilder CreateConventionModelBuilder() - => RelationalTestHelpers.Instance.CreateConventionBuilder(); + => FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); private enum MyEnum : byte { diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs index 827b0e86125..f481a54fdba 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs @@ -77,7 +77,7 @@ public void Can_use_relational_model_with_views(Mapping mapping) [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); + var model = CreateTestModel(mapToTables: mapToTables, mapToSprocs: true, mapping: mapping); Assert.Equal(11, model.Model.GetEntityTypes().Count()); Assert.Equal( @@ -99,6 +99,37 @@ public void Can_use_relational_model_with_sprocs(bool mapToTables, Mapping mappi Assert.True(model.Model.GetEntityTypes().All(et => !et.GetViewMappings().Any())); AssertDefaultMappings(model, mapping); + AssertTables(model, mapping); + AssertSprocs(model, mapping, mappedToTables: true); + } + + [ConditionalTheory] + [InlineData(Mapping.TPH)] + [InlineData(Mapping.TPT)] + [InlineData(Mapping.TPC)] + public void Can_use_relational_model_with_sprocs_and_views(Mapping mapping) + { + var model = CreateTestModel(mapToViews: true, mapToSprocs: true, mapping: mapping); + + Assert.Equal(11, model.Model.GetEntityTypes().Count()); + + Assert.Equal( + mapping switch + { + Mapping.TPC => 5, + Mapping.TPH => 3, + _ => 6 + }, model.Views.Count()); + + Assert.Equal(mapping switch + { + Mapping.TPC => 24, + Mapping.TPH => 18, + _ => 27 + }, model.StoredProcedures.Count()); + + AssertDefaultMappings(model, mapping); + AssertViews(model, mapping); AssertSprocs(model, mapping); } @@ -633,12 +664,12 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) 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; Assert.Equal( @@ -667,7 +698,7 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) Assert.Equal("IX_Order_CustomerId", ordersCustomerIndex.GetDefaultDatabaseName()); Assert.Equal("IX_Order_CustomerId", ordersCustomerIndex.GetDefaultDatabaseName( StoreObjectIdentifier.Table(ordersTable.Name, ordersTable.Schema))); - + Assert.Equal("PK_Order", orderPk.GetName()); Assert.Equal("PK_Order", orderPk.GetName( StoreObjectIdentifier.Table(ordersTable.Name, ordersTable.Schema))); @@ -1019,7 +1050,7 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) } } - private static void AssertSprocs(IRelationalModel model, Mapping mapping) + private static void AssertSprocs(IRelationalModel model, Mapping mapping, bool mappedToTables = false) { var orderType = model.Model.FindEntityType(typeof(Order)); var orderInsertMapping = orderType.GetInsertStoredProcedureMappings().Single(); @@ -1048,6 +1079,10 @@ private static void AssertSprocs(IRelationalModel model, Mapping mapping) Assert.Equal( new[] { nameof(Order.AlternateId), nameof(Order.CustomerId), nameof(Order.OrderDate) }, ordersInsertSproc.Parameters.Select(m => m.Name)); + + Assert.Equal( + new[] { 0, 1, 2 }, + ordersInsertSproc.Parameters.Select(m => m.Position)); Assert.Equal( new[] { nameof(Order.Id) }, @@ -1061,17 +1096,17 @@ private static void AssertSprocs(IRelationalModel model, Mapping mapping) 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 orderDateParameter = orderDateInsertMapping.StoreParameter; + Assert.Same(orderDateInsertMapping.StoreParameter, orderDateInsertMapping.Column); + Assert.Same(orderDateParameter, ordersInsertSproc.FindParameter("OrderDate")); + Assert.Same(orderDateParameter, ordersInsertSproc.FindParameter(orderDate)); + Assert.Equal("OrderDate", orderDateParameter.Name); + Assert.Equal("default_datetime_mapping", orderDateParameter.StoreType); + Assert.False(orderDateParameter.IsNullable); + Assert.Equal(ParameterDirection.Input, orderDateParameter.Direction); + Assert.Same(ordersInsertSproc, orderDateParameter.StoredProcedure); + Assert.Same(orderDateParameter.StoredProcedure, orderDateParameter.Table); + Assert.Same(orderDateInsertMapping, orderDateParameter.FindParameterMapping(orderType)); var abstractBaseType = model.Model.FindEntityType(typeof(AbstractBase)); var abstractCustomerType = model.Model.FindEntityType(typeof(AbstractCustomer)); @@ -1099,6 +1134,17 @@ private static void AssertSprocs(IRelationalModel model, Mapping mapping) RelationalStrings.TableNotMappedEntityType(nameof(SpecialCustomer), ordersInsertSproc.Name), Assert.Throws( () => ordersInsertSproc.IsOptional(specialCustomerType)).Message); + + var tableMapping = orderInsertMapping.TableMapping; + if (mappedToTables) + { + Assert.Equal("Order", tableMapping.Table.Name); + Assert.Same(orderInsertMapping, tableMapping.InsertStoredProcedureMapping); + } + else + { + Assert.Null(tableMapping); + } var billingAddressOwnership = orderDetailsType.FindNavigation(nameof(OrderDetails.BillingAddress)).ForeignKey; Assert.True(billingAddressOwnership.IsRequiredDependent); @@ -1159,7 +1205,7 @@ private static void AssertSprocs(IRelationalModel model, Mapping mapping) Assert.Empty(billingAddressDeleteSproc.ResultColumns.Select(m => m.Name)); - Assert.Equal(new[] { orderDate }, orderDateColumn.PropertyMappings.Select(m => m.Property)); + Assert.Equal(new[] { orderDate }, orderDateParameter.PropertyMappings.Select(m => m.Property)); var specialCustomerInsertSproc = specialCustomerType.GetInsertStoredProcedureMappings().Last().StoreStoredProcedure; @@ -1316,7 +1362,7 @@ private static void AssertSprocs(IRelationalModel model, Mapping mapping) 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.True(specialCustomerType.GetInsertStoredProcedureMappings().Last().IncludesDerivedTypes); Assert.Equal("SpecialCustomer_Insert", specialCustomerInsertSproc.Name); Assert.Single(specialCustomerInsertSproc.ResultColumns); @@ -1390,7 +1436,7 @@ private static void AssertSprocs(IRelationalModel model, Mapping mapping) var idPropertyUpdateParameter = baseUpdateSproc.FindParameter(idProperty)!; var idPropertyUpdateParameterMapping = idProperty.GetUpdateStoredProcedureParameterMappings().First(); Assert.Same(idPropertyUpdateParameter, baseUpdateSproc.FindParameter("UpdateId")); - Assert.Same(idPropertyUpdateParameter, idPropertyUpdateParameterMapping.Parameter); + Assert.Same(idPropertyUpdateParameter, idPropertyUpdateParameterMapping.StoreParameter); Assert.Equal("UpdateId", idPropertyUpdateParameter.Name); Assert.Equal("default_int_mapping", idPropertyUpdateParameter.StoreType); Assert.Equal(ParameterDirection.Input, idPropertyUpdateParameter.Direction); @@ -1419,7 +1465,7 @@ private static void AssertSprocs(IRelationalModel model, Mapping mapping) var idPropertyDeleteParameter = baseDeleteSproc.FindParameter(idProperty)!; var idPropertyDeleteParameterMapping = idProperty.GetDeleteStoredProcedureParameterMappings().First(); Assert.Same(idPropertyDeleteParameter, baseDeleteSproc.FindParameter("DeleteId")); - Assert.Same(idPropertyDeleteParameter, idPropertyDeleteParameterMapping.Parameter); + Assert.Same(idPropertyDeleteParameter, idPropertyDeleteParameterMapping.StoreParameter); Assert.Equal("DeleteId", idPropertyDeleteParameter.Name); Assert.Equal("default_int_mapping", idPropertyDeleteParameter.StoreType); Assert.Equal(ParameterDirection.Input, idPropertyDeleteParameter.Direction); @@ -1638,7 +1684,7 @@ private static void AssertSprocs(IRelationalModel model, Mapping mapping) var idPropertyUpdateParameter = baseUpdateSproc.FindParameter(idProperty)!; var idPropertyUpdateParameterMapping = idProperty.GetUpdateStoredProcedureParameterMappings().First(); Assert.Same(idPropertyUpdateParameter, baseUpdateSproc.FindParameter("UpdateId")); - Assert.Same(idPropertyUpdateParameter, idPropertyUpdateParameterMapping.Parameter); + Assert.Same(idPropertyUpdateParameter, idPropertyUpdateParameterMapping.StoreParameter); Assert.Equal("UpdateId", idPropertyUpdateParameter.Name); Assert.Equal("default_int_mapping", idPropertyUpdateParameter.StoreType); Assert.Equal(ParameterDirection.Input, idPropertyUpdateParameter.Direction); @@ -1656,7 +1702,7 @@ private static void AssertSprocs(IRelationalModel model, Mapping mapping) var idPropertyDeleteParameter = baseDeleteSproc.FindParameter(idProperty)!; var idPropertyDeleteParameterMapping = idProperty.GetDeleteStoredProcedureParameterMappings().First(); Assert.Same(idPropertyDeleteParameter, baseDeleteSproc.FindParameter("DeleteId")); - Assert.Same(idPropertyDeleteParameter, idPropertyDeleteParameterMapping.Parameter); + Assert.Same(idPropertyDeleteParameter, idPropertyDeleteParameterMapping.StoreParameter); Assert.Equal("DeleteId", idPropertyDeleteParameter.Name); Assert.Equal("default_int_mapping", idPropertyDeleteParameter.StoreType); Assert.Equal(ParameterDirection.Input, idPropertyDeleteParameter.Direction); @@ -1786,7 +1832,7 @@ private static void AssertSprocs(IRelationalModel model, Mapping mapping) var idPropertyUpdateParameter = customerUpdateSproc.FindParameter(idProperty)!; var idPropertyUpdateParameterMapping = idProperty.GetUpdateStoredProcedureParameterMappings().First(); Assert.Same(idPropertyUpdateParameter, customerUpdateSproc.FindParameter("UpdateId")); - Assert.Same(idPropertyUpdateParameter, idPropertyUpdateParameterMapping.Parameter); + Assert.Same(idPropertyUpdateParameter, idPropertyUpdateParameterMapping.StoreParameter); Assert.Equal("UpdateId", idPropertyUpdateParameter.Name); Assert.Equal("default_int_mapping", idPropertyUpdateParameter.StoreType); Assert.Equal(ParameterDirection.Input, idPropertyUpdateParameter.Direction); @@ -1817,7 +1863,7 @@ private static void AssertSprocs(IRelationalModel model, Mapping mapping) var idPropertyDeleteParameter = customerDeleteSproc.FindParameter(idProperty)!; var idPropertyDeleteParameterMapping = idProperty.GetDeleteStoredProcedureParameterMappings().First(); Assert.Same(idPropertyDeleteParameter, customerDeleteSproc.FindParameter("DeleteId")); - Assert.Same(idPropertyDeleteParameter, idPropertyDeleteParameterMapping.Parameter); + Assert.Same(idPropertyDeleteParameter, idPropertyDeleteParameterMapping.StoreParameter); Assert.Equal("DeleteId", idPropertyDeleteParameter.Name); Assert.Equal("default_int_mapping", idPropertyDeleteParameter.StoreType); Assert.Equal(ParameterDirection.Input, idPropertyDeleteParameter.Direction); @@ -1938,7 +1984,7 @@ private IRelationalModel CreateTestModel( { cb.ToTable("Customer"); } - + if (mapToSprocs) { cb @@ -1956,7 +2002,7 @@ private IRelationalModel CreateTestModel( .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")); @@ -2077,7 +2123,7 @@ private IRelationalModel CreateTestModel( cb.OwnsOne(c => c.Details, cdb => cdb.ToTable("SpecialCustomer", "SpecialSchema")); } } - + if (mapToSprocs) { cb.OwnsOne( @@ -2163,7 +2209,7 @@ private IRelationalModel CreateTestModel( { cb.OwnsOne(c => c.Details, cdb => cdb.ToTable("ExtraSpecialCustomer", "ExtraSpecialSchema")); } - + if (mapToSprocs) { cb.OwnsOne( @@ -2336,7 +2382,7 @@ public void Can_use_relational_model_with_entity_splitting_and_table_splitting_o { cb.Ignore(c => c.Orders); cb.Ignore(c => c.RelatedCustomer); - + if (mapToViews) { cb.ToView("CustomerView", tb => @@ -2357,7 +2403,7 @@ public void Can_use_relational_model_with_entity_splitting_and_table_splitting_o { tb.Property(c => c.AbstractString); }); - + cb.SplitToTable("CustomerDetails", tb => { tb.Property(c => c.AbstractString); @@ -2371,7 +2417,7 @@ public void Can_use_relational_model_with_entity_splitting_and_table_splitting_o if (mapToViews) { db.ToView("CustomerView"); - + db.SplitToView("CustomerDetailsView", tb => { tb.Property(d => d.BirthDay); @@ -2402,9 +2448,9 @@ public void Can_use_relational_model_with_entity_splitting_and_table_splitting_o Assert.Equal(2, model.Views.Count()); var customerView = model.Views.Single(t => t.Name == "CustomerView"); - + Assert.Equal(2, customerView.EntityTypeMappings.Count()); - + var customerMapping = customerView.EntityTypeMappings.First(); Assert.True(customerMapping.IsSharedTablePrincipal); Assert.True(customerMapping.IsSplitEntityTypePrincipal); @@ -2452,7 +2498,7 @@ public void Can_use_relational_model_with_entity_splitting_and_table_splitting_o Assert.True(detailsMapping.IsSplitEntityTypePrincipal); var customerDetailsTable = model.Tables.Single(t => t.Name == "CustomerDetails"); - + Assert.Equal(new[] { customerTable, customerDetailsTable }, customerType.GetTableMappings().Select(m => m.Table)); @@ -2463,7 +2509,7 @@ public void Can_use_relational_model_with_entity_splitting_and_table_splitting_o Assert.False(customerSplitMapping.IsSplitEntityTypePrincipal); var detailsSplitMapping = customerDetailsTable.EntityTypeMappings.Last(); Assert.False(detailsSplitMapping.IsSharedTablePrincipal); - Assert.False(detailsSplitMapping.IsSplitEntityTypePrincipal); + Assert.False(detailsSplitMapping.IsSplitEntityTypePrincipal); Assert.Equal(new[] { customerTable, customerDetailsTable }, detailsType.GetTableMappings().Select(m => m.Table)); @@ -2562,7 +2608,7 @@ public void Can_use_relational_model_with_entity_splitting_and_table_splitting_o Assert.Equal( new[] { customerTable, customerDetailsTable }, detailsType.GetTableMappings().Select(m => m.Table)); - + var detailsSplitMapping = customerDetailsTable.EntityTypeMappings.Single(); Assert.Null(detailsSplitMapping.IsSharedTablePrincipal); Assert.False(detailsSplitMapping.IsSplitEntityTypePrincipal); @@ -2647,7 +2693,7 @@ public void Can_use_relational_model_with_entity_splitting_and_table_splitting_o c => c.Details, db => { db.ToTable("CustomerDetails"); - + db.SplitToTable( "Details", tb => { @@ -2731,7 +2777,7 @@ public void Can_use_relational_model_with_entity_splitting_and_table_splitting_o Assert.True(customerDetailsFk.IsRequired); Assert.True(customerDetailsFk.IsRequiredDependent); Assert.Same(detailsType, customerDetailsFk.DeclaringEntityType); - + Assert.Single(detailsTable.UniqueConstraints); var detailsFkConstraint = detailsTable.ForeignKeyConstraints.Single(); Assert.Empty(detailsTable.Indexes); @@ -2992,7 +3038,7 @@ private static IRelationalModel Finalize(TestHelpers.TestModelBuilder modelBuild => modelBuilder.FinalizeModel(designTime: true).GetRelationalModel(); protected virtual TestHelpers.TestModelBuilder CreateConventionModelBuilder() - => RelationalTestHelpers.Instance.CreateConventionBuilder( + => FakeRelationalTestHelpers.Instance.CreateConventionBuilder( configureContext: b => b.ConfigureWarnings(w => w.Default(WarningBehavior.Throw) diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalPropertyAttributeConventionTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalPropertyAttributeConventionTest.cs index 3507b167d20..ba7436d7b8d 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalPropertyAttributeConventionTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalPropertyAttributeConventionTest.cs @@ -146,13 +146,13 @@ private InternalEntityTypeBuilder CreateInternalEntityTypeBuilder() } private ProviderConventionSetBuilderDependencies CreateDependencies() - => RelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); + => FakeRelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); private RelationalConventionSetBuilderDependencies CreateRelationalDependencies() - => RelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); + => FakeRelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); protected virtual ModelBuilder CreateConventionalModelBuilder() - => RelationalTestHelpers.Instance.CreateConventionBuilder(); + => FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); private class A { diff --git a/test/EFCore.Relational.Tests/Metadata/TriggerTest.cs b/test/EFCore.Relational.Tests/Metadata/TriggerTest.cs index 19accbc96e3..093c34be647 100644 --- a/test/EFCore.Relational.Tests/Metadata/TriggerTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/TriggerTest.cs @@ -93,7 +93,7 @@ public void RemoveTrigger_returns_null_when_trigger_is_missing() } protected virtual ModelBuilder CreateConventionModelBuilder() - => RelationalTestHelpers.Instance.CreateConventionBuilder(); + => FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); private class Customer { diff --git a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs index d68920617b0..47c449c5160 100644 --- a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs +++ b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs @@ -11986,5 +11986,5 @@ public void Model_differ_does_not_detect_entity_type_mapped_to_TVF() skipSourceConventions: true); protected override TestHelpers TestHelpers - => RelationalTestHelpers.Instance; + => FakeRelationalTestHelpers.Instance; } diff --git a/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderTest.cs b/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderTest.cs index ba7efa3b850..3589c1e1527 100644 --- a/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderTest.cs +++ b/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderTest.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; + #nullable enable // ReSharper disable InconsistentNaming @@ -154,6 +156,186 @@ public virtual void Can_use_view_splitting_with_schema() Assert.Same(customerId.GetOverrides().Single(), customerId.FindOverrides(StoreObjectIdentifier.View("OrderDetails", "sch"))); Assert.Null(customerId.GetColumnName(StoreObjectIdentifier.View("Order"))); } + + [ConditionalFact] + public virtual void Conflicting_sproc_rows_affected_return_and_parameter_throw() + { + var modelBuilder = CreateModelBuilder(); + + Assert.Equal( + RelationalStrings.StoredProcedureRowsAffectedReturnConflictingParameter("BookLabel_Update"), + Assert.Throws( + () => modelBuilder.Entity() + .UpdateUsingStoredProcedure( + s => s.HasRowsAffectedParameter() + .HasRowsAffectedReturnValue())) + .Message); + } + + [ConditionalFact] + public virtual void Conflicting_sproc_rows_affected_return_and_result_column_throw() + { + var modelBuilder = CreateModelBuilder(); + + Assert.Equal( + RelationalStrings.StoredProcedureRowsAffectedReturnConflictingParameter("BookLabel_Update"), + Assert.Throws( + () => modelBuilder.Entity() + .UpdateUsingStoredProcedure( + s => s.HasRowsAffectedResultColumn() + .HasRowsAffectedReturnValue())) + .Message); + } + + [ConditionalFact] + public virtual void Conflicting_sproc_rows_affected_parameter_and_return_throw() + { + var modelBuilder = CreateModelBuilder(); + + Assert.Equal( + RelationalStrings.StoredProcedureDuplicateRowsAffectedParameter("BookLabel_Update"), + Assert.Throws( + () => modelBuilder.Entity() + .UpdateUsingStoredProcedure( + s => s.HasRowsAffectedReturnValue() + .HasRowsAffectedParameter())) + .Message); + } + + [ConditionalFact] + public virtual void Conflicting_sproc_rows_affected_result_column_and_return_throw() + { + var modelBuilder = CreateModelBuilder(); + + Assert.Equal( + RelationalStrings.StoredProcedureDuplicateRowsAffectedResultColumn("BookLabel_Update"), + Assert.Throws( + () => modelBuilder.Entity() + .UpdateUsingStoredProcedure( + s => s.HasRowsAffectedReturnValue() + .HasRowsAffectedResultColumn())) + .Message); + } + + [ConditionalFact] + public virtual void Conflicting_sproc_rows_affected_result_column_and_parameter_throw() + { + var modelBuilder = CreateModelBuilder(); + + Assert.Equal( + RelationalStrings.StoredProcedureDuplicateRowsAffectedResultColumn("BookLabel_Update"), + Assert.Throws( + () => modelBuilder.Entity() + .UpdateUsingStoredProcedure( + s => s.HasRowsAffectedParameter() + .HasRowsAffectedResultColumn())) + .Message); + } + + [ConditionalFact] + public virtual void Duplicate_sproc_rows_affected_result_column_throws() + { + var modelBuilder = CreateModelBuilder(); + + var sproc = modelBuilder.Entity() + .UpdateUsingStoredProcedure( + s => s.HasRowsAffectedResultColumn()).Metadata.GetUpdateStoredProcedure()!; + + Assert.Equal( + RelationalStrings.StoredProcedureDuplicateRowsAffectedResultColumn("BookLabel_Update"), + Assert.Throws(() => sproc.AddRowsAffectedResultColumn()) + .Message); + } + + [ConditionalFact] + public virtual void Conflicting_sproc_rows_affected_parameter_and_result_column_throw() + { + var modelBuilder = CreateModelBuilder(); + + Assert.Equal( + RelationalStrings.StoredProcedureDuplicateRowsAffectedParameter("BookLabel_Update"), + Assert.Throws( + () => modelBuilder.Entity() + .UpdateUsingStoredProcedure( + s => s.HasRowsAffectedResultColumn() + .HasRowsAffectedParameter())) + .Message); + } + + [ConditionalFact] + public virtual void Duplicate_sproc_rows_affected_parameter_throws() + { + var modelBuilder = CreateModelBuilder(); + + var sproc = modelBuilder.Entity() + .UpdateUsingStoredProcedure( + s => s.HasRowsAffectedParameter()).Metadata.GetUpdateStoredProcedure()!; + + Assert.Equal( + RelationalStrings.StoredProcedureDuplicateRowsAffectedParameter("BookLabel_Update"), + Assert.Throws(() => sproc.AddRowsAffectedParameter()) + .Message); + } + + [ConditionalFact] + public virtual void Duplicate_sproc_parameter_throws() + { + var modelBuilder = CreateModelBuilder(); + + var sproc = modelBuilder.Entity() + .InsertUsingStoredProcedure( + s => s.HasParameter(b => b.Id)).Metadata.GetInsertStoredProcedure()!; + + Assert.Equal( + RelationalStrings.StoredProcedureDuplicateParameter("Id", "BookLabel_Insert"), + Assert.Throws(() => sproc.AddParameter("Id")) + .Message); + } + + [ConditionalFact] + public virtual void Duplicate_sproc_original_value_parameter_throws() + { + var modelBuilder = CreateModelBuilder(); + + var sproc = modelBuilder.Entity() + .InsertUsingStoredProcedure( + s => s.HasOriginalValueParameter(b => b.Id)).Metadata.GetInsertStoredProcedure()!; + + Assert.Equal( + RelationalStrings.StoredProcedureDuplicateOriginalValueParameter("Id", "BookLabel_Insert"), + Assert.Throws(() => sproc.AddOriginalValueParameter("Id")) + .Message); + } + + [ConditionalFact] + public virtual void Duplicate_sproc_result_column_throws() + { + var modelBuilder = CreateModelBuilder(); + + var sproc = modelBuilder.Entity() + .InsertUsingStoredProcedure( + s => s.HasResultColumn(b => b.Id)).Metadata.GetInsertStoredProcedure()!; + + Assert.Equal( + RelationalStrings.StoredProcedureDuplicateResultColumn("Id", "BookLabel_Insert"), + Assert.Throws(() => sproc.AddResultColumn("Id")) + .Message); + } + + [ConditionalFact] + public virtual void Configuring_direction_on_RowsAffectedParameter_throws() + { + var modelBuilder = CreateModelBuilder(); + + var param = modelBuilder.Entity() + .InsertUsingStoredProcedure( + s => s.HasRowsAffectedParameter()).Metadata.GetInsertStoredProcedure()!.Parameters.Single(); + + Assert.Equal( + RelationalStrings.StoredProcedureParameterInvalidConfiguration("Direction", "RowsAffected", "BookLabel_Insert"), + Assert.Throws(() => param.Direction = ParameterDirection.Input) + .Message); + } } public abstract class RelationalInheritanceTestBase : InheritanceTestBase @@ -208,6 +390,7 @@ public virtual void Sproc_overrides_update_when_renamed_in_TPH() modelBuilder.Ignore(); modelBuilder.Ignore(); + modelBuilder.Entity().Property("RowVersion").IsRowVersion(); modelBuilder.Entity() .Ignore(s => s.SpecialBookLabel) .InsertUsingStoredProcedure( @@ -220,7 +403,9 @@ public virtual void Sproc_overrides_update_when_renamed_in_TPH() var resultColumnBuilder = p.HasName("InsertId"); var nonGenericBuilder = (IInfrastructure)resultColumnBuilder; Assert.IsAssignableFrom(nonGenericBuilder.Instance.GetInfrastructure()); - })) + }) + .HasResultColumn("RowVersion") + .HasRowsAffectedReturnValue()) .UpdateUsingStoredProcedure( s => s.SuppressTransactions().HasAnnotation("foo", "bar2") .HasParameter( @@ -230,10 +415,15 @@ public virtual void Sproc_overrides_update_when_renamed_in_TPH() var nonGenericBuilder = (IInfrastructure)parameterBuilder; Assert.IsAssignableFrom(nonGenericBuilder.Instance.GetInfrastructure()); }) - .HasParameter(b => b.BookId)) + .HasParameter(b => b.BookId) + .HasRowsAffectedParameter() + .HasOriginalValueParameter("RowVersion", p => p.HasName("OriginalRowVersion")) + .HasParameter("RowVersion", p => p.IsOutput())) .DeleteUsingStoredProcedure( s => s.SuppressTransactions().HasAnnotation("foo", "bar3") - .HasParameter(b => b.Id, p => p.HasName("DeleteId"))); + .HasParameter(b => b.Id, p => p.HasName("DeleteId")) + .HasRowsAffectedResultColumn() + .HasOriginalValueParameter("RowVersion")); modelBuilder.Entity() .Ignore(s => s.BookLabel); @@ -250,44 +440,69 @@ public virtual void Sproc_overrides_update_when_renamed_in_TPH() var insertSproc = bookLabel.GetInsertStoredProcedure()!; Assert.Equal("Insert", insertSproc.Name); 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.Equal(new[] { "BookId", "Discriminator" }, insertSproc.Parameters.Select(p => p.PropertyName)); + Assert.Equal(new[] { "Id", "RowVersion" }, insertSproc.ResultColumns.Select(p => p.PropertyName)); + Assert.False(insertSproc.FindParameter("Discriminator")!.ForOriginalValue); + Assert.Null(insertSproc.FindParameter("Id")); + Assert.Null(insertSproc.FindResultColumn("Discriminator")); + Assert.False(insertSproc.FindResultColumn("Id")!.ForRowsAffected); Assert.True(insertSproc.AreTransactionsSuppressed); + Assert.True(insertSproc.AreRowsAffectedReturned); Assert.Equal("bar1", insertSproc["foo"]); Assert.Same(bookLabel, insertSproc.EntityType); var updateSproc = bookLabel.GetUpdateStoredProcedure()!; Assert.Equal("Update", updateSproc.Name); Assert.Equal("dbo", updateSproc.Schema); - Assert.Equal(new[] { "Id", "BookId" }, updateSproc.Parameters); + Assert.Equal(new[] { "Id", "BookId", null, "RowVersion", "RowVersion" }, updateSproc.Parameters.Select(p => p.PropertyName)); Assert.Empty(updateSproc.ResultColumns); Assert.True(updateSproc.AreTransactionsSuppressed); Assert.Equal("bar2", updateSproc["foo"]); Assert.Same(bookLabel, updateSproc.EntityType); + Assert.False(updateSproc.AreRowsAffectedReturned); + + var rowsAffectedParameter = updateSproc.Parameters[2]; + Assert.Null(rowsAffectedParameter.ForOriginalValue); + Assert.True(rowsAffectedParameter.ForRowsAffected); + Assert.Equal(ParameterDirection.Output, rowsAffectedParameter.Direction); + Assert.Same(updateSproc, rowsAffectedParameter.StoredProcedure); + + var originalRowVersionParameter = updateSproc.Parameters[3]; + Assert.True(originalRowVersionParameter.ForOriginalValue); + Assert.False(originalRowVersionParameter.ForRowsAffected); + Assert.Equal(ParameterDirection.Input, originalRowVersionParameter.Direction); + Assert.Same(updateSproc, originalRowVersionParameter.StoredProcedure); + + var currentRowVersionParameter = updateSproc.Parameters[4]; + Assert.False(currentRowVersionParameter.ForOriginalValue); + Assert.False(currentRowVersionParameter.ForRowsAffected); + Assert.Equal(ParameterDirection.Output, currentRowVersionParameter.Direction); + Assert.Same(updateSproc, currentRowVersionParameter.StoredProcedure); var deleteSproc = bookLabel.GetDeleteStoredProcedure()!; Assert.Equal("BookLabel_Delete", deleteSproc.Name); Assert.Equal("mySchema", deleteSproc.Schema); - Assert.Equal(new[] { "Id" }, deleteSproc.Parameters); - Assert.Empty(deleteSproc.ResultColumns); + Assert.Equal(new[] { "Id", "RowVersion" }, deleteSproc.Parameters.Select(p => p.PropertyName)); + Assert.Equal(new[] { "RowsAffected" }, deleteSproc.ResultColumns.Select(p => p.Name)); Assert.True(deleteSproc.AreTransactionsSuppressed); Assert.Equal("bar3", deleteSproc["foo"]); Assert.Same(bookLabel, deleteSproc.EntityType); + Assert.False(deleteSproc.AreRowsAffectedReturned); + + var rowsAffectedResultColumn = deleteSproc.ResultColumns[0]; + Assert.True(rowsAffectedResultColumn.ForRowsAffected); + Assert.Same(deleteSproc, rowsAffectedResultColumn.StoredProcedure); var id = bookLabel.FindProperty(nameof(BookLabel.Id))!; - Assert.Equal(3, id.GetOverrides().Count()); + Assert.Single(id.GetOverrides()); Assert.Equal( "InsertId", id.GetColumnName(StoreObjectIdentifier.Create(bookLabel, StoreObjectType.InsertStoredProcedure)!.Value)); Assert.Equal( - "UpdateId", + "Id", id.GetColumnName(StoreObjectIdentifier.Create(bookLabel, StoreObjectType.UpdateStoredProcedure)!.Value)); Assert.Equal( - "DeleteId", + "Id", id.GetColumnName(StoreObjectIdentifier.Create(bookLabel, StoreObjectType.DeleteStoredProcedure)!.Value)); var specialBookLabel = model.FindEntityType(typeof(SpecialBookLabel))!; @@ -550,7 +765,7 @@ public virtual void Can_use_sproc_mapping_with_owned_reference() var insertSproc = bookOwnership1.DeclaringEntityType.GetInsertStoredProcedure()!; Assert.Equal("Insert", insertSproc.Name); Assert.Null(insertSproc.Schema); - Assert.Equal(new[] { "Id", "BookId" }, insertSproc.Parameters); + Assert.Equal(new[] { "Id", "BookId" }, insertSproc.Parameters.Select(p => p.PropertyName)); Assert.Empty(insertSproc.ResultColumns); Assert.True(insertSproc.AreTransactionsSuppressed); Assert.Equal("bar1", insertSproc["foo"]); @@ -559,8 +774,8 @@ public virtual void Can_use_sproc_mapping_with_owned_reference() var updateSproc = bookOwnership1.DeclaringEntityType.GetUpdateStoredProcedure()!; Assert.Equal("Update", updateSproc.Name); Assert.Equal("dbo", updateSproc.Schema); - Assert.Equal(new[] { "Id", "BookId" }, updateSproc.Parameters); - Assert.Equal(new[] { "Id" }, updateSproc.ResultColumns); + Assert.Equal(new[] { "Id", "BookId" }, updateSproc.Parameters.Select(p => p.PropertyName)); + Assert.Equal(new[] { "Id" }, updateSproc.ResultColumns.Select(p => p.Name)); Assert.True(updateSproc.AreTransactionsSuppressed); Assert.Equal("bar2", updateSproc["foo"]); Assert.Same(bookOwnership1.DeclaringEntityType, updateSproc.EntityType); @@ -568,22 +783,22 @@ public virtual void Can_use_sproc_mapping_with_owned_reference() var deleteSproc = bookOwnership1.DeclaringEntityType.GetDeleteStoredProcedure()!; Assert.Equal("BookLabel_Delete", deleteSproc.Name); Assert.Null(deleteSproc.Schema); - Assert.Equal(new[] { "BookId" }, deleteSproc.Parameters); + Assert.Equal(new[] { "BookId" }, deleteSproc.Parameters.Select(p => p.PropertyName)); Assert.Empty(deleteSproc.ResultColumns); Assert.True(deleteSproc.AreTransactionsSuppressed); Assert.Equal("bar3", deleteSproc["foo"]); Assert.Same(bookOwnership1.DeclaringEntityType, deleteSproc.EntityType); var bookId = bookOwnership1.DeclaringEntityType.FindProperty(nameof(BookLabel.BookId))!; - Assert.Equal(3, bookId.GetOverrides().Count()); + Assert.Empty(bookId.GetOverrides()); Assert.Equal( - "InsertId", + "BookId", bookId.GetColumnName(StoreObjectIdentifier.Create(bookOwnership1.DeclaringEntityType, StoreObjectType.InsertStoredProcedure)!.Value)); Assert.Equal( - "UpdateId", + "BookId", bookId.GetColumnName(StoreObjectIdentifier.Create(bookOwnership1.DeclaringEntityType, StoreObjectType.UpdateStoredProcedure)!.Value)); Assert.Equal( - "DeleteId", + "BookId", bookId.GetColumnName(StoreObjectIdentifier.Create(bookOwnership1.DeclaringEntityType, StoreObjectType.DeleteStoredProcedure)!.Value)); Assert.Null(bookOwnership2.DeclaringEntityType.GetInsertStoredProcedure()); @@ -1424,6 +1639,34 @@ public abstract TestStoredProcedureBuilder HasParameter> propertyExpression, Action buildAction) where TDerivedEntity : class, TEntity; + + public abstract TestStoredProcedureBuilder HasOriginalValueParameter( + string propertyName); + + public abstract TestStoredProcedureBuilder HasOriginalValueParameter( + string propertyName, + Action buildAction); + + public abstract TestStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression); + + public abstract TestStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression, + Action buildAction); + + public abstract TestStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression) + where TDerivedEntity : class, TEntity; + + public abstract TestStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression, + Action buildAction) + where TDerivedEntity : class, TEntity; + + public abstract TestStoredProcedureBuilder HasRowsAffectedParameter(); + + public abstract TestStoredProcedureBuilder HasRowsAffectedParameter( + Action buildAction); public abstract TestStoredProcedureBuilder HasResultColumn( string propertyName); @@ -1448,10 +1691,14 @@ public abstract TestStoredProcedureBuilder HasResultColumn buildAction) where TDerivedEntity : class, TEntity; - public abstract TestStoredProcedureBuilder SuppressTransactions(bool suppress = true); + public abstract TestStoredProcedureBuilder HasRowsAffectedResultColumn(); - //public abstract StoredProcedureBuilder HasRowsAffectedParameter( - // Action> buildAction); + public abstract TestStoredProcedureBuilder HasRowsAffectedResultColumn( + Action buildAction); + + public abstract TestStoredProcedureBuilder HasRowsAffectedReturnValue(bool rowsAffectedReturned = true); + + public abstract TestStoredProcedureBuilder SuppressTransactions(bool suppress = true); public abstract TestStoredProcedureBuilder HasAnnotation(string annotation, object? value); } @@ -1499,6 +1746,40 @@ public override TestStoredProcedureBuilder HasParameter> propertyExpression, Action buildAction) => Wrap(StoredProcedureBuilder.HasParameter(propertyExpression, s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder HasOriginalValueParameter( + string propertyName) + => Wrap(StoredProcedureBuilder.HasOriginalValueParameter(propertyName)); + + public override TestStoredProcedureBuilder HasOriginalValueParameter( + string propertyName, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasOriginalValueParameter(propertyName, s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression) + => Wrap(StoredProcedureBuilder.HasOriginalValueParameter(propertyExpression)); + + public override TestStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasOriginalValueParameter(propertyExpression, s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression) + => Wrap(StoredProcedureBuilder.HasOriginalValueParameter(propertyExpression)); + + public override TestStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasOriginalValueParameter(propertyExpression, s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder HasRowsAffectedParameter() + => Wrap(StoredProcedureBuilder.HasRowsAffectedParameter()); + + public override TestStoredProcedureBuilder HasRowsAffectedParameter( + Action buildAction) + => Wrap(StoredProcedureBuilder.HasRowsAffectedParameter(s => buildAction(new(s)))); public override TestStoredProcedureBuilder HasResultColumn( string propertyName) @@ -1527,6 +1808,16 @@ public override TestStoredProcedureBuilder HasResultColumn buildAction) => Wrap(StoredProcedureBuilder.HasResultColumn(propertyExpression, s => buildAction(new(s)))); + public override TestStoredProcedureBuilder HasRowsAffectedResultColumn() + => Wrap(StoredProcedureBuilder.HasRowsAffectedResultColumn()); + + public override TestStoredProcedureBuilder HasRowsAffectedResultColumn( + Action buildAction) + => Wrap(StoredProcedureBuilder.HasRowsAffectedResultColumn(s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder HasRowsAffectedReturnValue(bool rowsAffectedReturned) + => Wrap(StoredProcedureBuilder.HasRowsAffectedReturnValue(rowsAffectedReturned)); + public override TestStoredProcedureBuilder SuppressTransactions(bool suppress) => Wrap(StoredProcedureBuilder.SuppressTransactions(suppress)); @@ -1582,6 +1873,44 @@ public override TestStoredProcedureBuilder HasParameter buildAction(new(s)))); + public override TestStoredProcedureBuilder HasOriginalValueParameter( + string propertyName) + => Wrap(StoredProcedureBuilder.HasOriginalValueParameter(propertyName)); + + public override TestStoredProcedureBuilder HasOriginalValueParameter( + string propertyName, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasOriginalValueParameter(propertyName, s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression) + => Wrap(StoredProcedureBuilder.HasOriginalValueParameter(propertyExpression.GetMemberAccess().Name)); + + public override TestStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression, + Action buildAction) + => Wrap( + StoredProcedureBuilder.HasOriginalValueParameter( + propertyExpression.GetMemberAccess().Name, s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression) + => Wrap(StoredProcedureBuilder.HasOriginalValueParameter(propertyExpression.GetMemberAccess().Name)); + + public override TestStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression, + Action buildAction) + => Wrap( + StoredProcedureBuilder.HasOriginalValueParameter( + propertyExpression.GetMemberAccess().Name, s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder HasRowsAffectedParameter() + => Wrap(StoredProcedureBuilder.HasRowsAffectedParameter()); + + public override TestStoredProcedureBuilder HasRowsAffectedParameter( + Action buildAction) + => Wrap(StoredProcedureBuilder.HasRowsAffectedParameter(s => buildAction(new(s)))); + public override TestStoredProcedureBuilder HasResultColumn( string propertyName) => Wrap(StoredProcedureBuilder.HasResultColumn(propertyName)); @@ -1613,6 +1942,16 @@ public override TestStoredProcedureBuilder HasResultColumn buildAction(new(s)))); + public override TestStoredProcedureBuilder HasRowsAffectedResultColumn() + => Wrap(StoredProcedureBuilder.HasRowsAffectedResultColumn()); + + public override TestStoredProcedureBuilder HasRowsAffectedResultColumn( + Action buildAction) + => Wrap(StoredProcedureBuilder.HasRowsAffectedResultColumn(s => buildAction(new(s)))); + + public override TestStoredProcedureBuilder HasRowsAffectedReturnValue(bool rowsAffectedReturned) + => Wrap(StoredProcedureBuilder.HasRowsAffectedReturnValue(rowsAffectedReturned)); + public override TestStoredProcedureBuilder SuppressTransactions(bool suppress) => Wrap(StoredProcedureBuilder.SuppressTransactions(suppress)); @@ -1637,6 +1976,25 @@ public abstract TestOwnedNavigationStoredProcedureBuilder HasParameter( Expression> propertyExpression, Action buildAction); + + public abstract TestOwnedNavigationStoredProcedureBuilder HasOriginalValueParameter( + string propertyName); + + public abstract TestOwnedNavigationStoredProcedureBuilder HasOriginalValueParameter( + string propertyName, + Action buildAction); + + public abstract TestOwnedNavigationStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression); + + public abstract TestOwnedNavigationStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression, + Action buildAction); + + public abstract TestOwnedNavigationStoredProcedureBuilder HasRowsAffectedParameter(); + + public abstract TestOwnedNavigationStoredProcedureBuilder HasRowsAffectedParameter( + Action buildAction); public abstract TestOwnedNavigationStoredProcedureBuilder HasResultColumn( string propertyName); @@ -1652,12 +2010,17 @@ public abstract TestOwnedNavigationStoredProcedureBuilder> propertyExpression, Action buildAction); + public abstract TestOwnedNavigationStoredProcedureBuilder HasRowsAffectedResultColumn(); + + public abstract TestOwnedNavigationStoredProcedureBuilder HasRowsAffectedResultColumn( + Action buildAction); + + public abstract TestOwnedNavigationStoredProcedureBuilder + HasRowsAffectedReturnValue(bool rowsAffectedReturned = true); + public abstract TestOwnedNavigationStoredProcedureBuilder SuppressTransactions(bool suppress = true); - //public abstract StoredProcedureBuilder HasRowsAffectedParameter( - // Action> buildAction); - public abstract TestOwnedNavigationStoredProcedureBuilder HasAnnotation( string annotation, object? value); @@ -1702,7 +2065,32 @@ public override TestOwnedNavigationStoredProcedureBuilder> propertyExpression, Action buildAction) => Wrap(StoredProcedureBuilder.HasParameter(propertyExpression, s => buildAction(new(s)))); + + public override TestOwnedNavigationStoredProcedureBuilder HasOriginalValueParameter( + string propertyName) + => Wrap(StoredProcedureBuilder.HasOriginalValueParameter(propertyName)); + public override TestOwnedNavigationStoredProcedureBuilder HasOriginalValueParameter( + string propertyName, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasOriginalValueParameter(propertyName, s => buildAction(new(s)))); + + public override TestOwnedNavigationStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression) + => Wrap(StoredProcedureBuilder.HasOriginalValueParameter(propertyExpression)); + + public override TestOwnedNavigationStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasOriginalValueParameter(propertyExpression, s => buildAction(new(s)))); + + public override TestOwnedNavigationStoredProcedureBuilder HasRowsAffectedParameter() + => Wrap(StoredProcedureBuilder.HasRowsAffectedParameter()); + + public override TestOwnedNavigationStoredProcedureBuilder HasRowsAffectedParameter( + Action buildAction) + => Wrap(StoredProcedureBuilder.HasRowsAffectedParameter(s => buildAction(new(s)))); + public override TestOwnedNavigationStoredProcedureBuilder HasResultColumn( string propertyName) => Wrap(StoredProcedureBuilder.HasResultColumn(propertyName)); @@ -1714,12 +2102,22 @@ public override TestOwnedNavigationStoredProcedureBuilder HasResultColumn( Expression> propertyExpression) - => Wrap(StoredProcedureBuilder.HasResultColumn(propertyExpression)); + => Wrap(StoredProcedureBuilder.HasResultColumn(propertyExpression)); public override TestOwnedNavigationStoredProcedureBuilder HasResultColumn( Expression> propertyExpression, Action buildAction) - => Wrap(StoredProcedureBuilder.HasResultColumn(propertyExpression, s => buildAction(new(s)))); + => Wrap(StoredProcedureBuilder.HasResultColumn(propertyExpression, s => buildAction(new(s)))); + + public override TestOwnedNavigationStoredProcedureBuilder HasRowsAffectedResultColumn() + => Wrap(StoredProcedureBuilder.HasRowsAffectedResultColumn()); + + public override TestOwnedNavigationStoredProcedureBuilder HasRowsAffectedResultColumn( + Action buildAction) + => Wrap(StoredProcedureBuilder.HasRowsAffectedResultColumn(s => buildAction(new(s)))); + + public override TestOwnedNavigationStoredProcedureBuilder HasRowsAffectedReturnValue(bool rowsAffectedReturned) + => Wrap(StoredProcedureBuilder.HasRowsAffectedReturnValue(rowsAffectedReturned)); public override TestOwnedNavigationStoredProcedureBuilder SuppressTransactions(bool suppress) => Wrap(StoredProcedureBuilder.SuppressTransactions(suppress)); @@ -1769,6 +2167,33 @@ public override TestOwnedNavigationStoredProcedureBuilder Wrap( StoredProcedureBuilder.HasParameter( propertyExpression.GetMemberAccess().Name, s => buildAction(new(s)))); + + public override TestOwnedNavigationStoredProcedureBuilder HasOriginalValueParameter( + string propertyName) + => Wrap(StoredProcedureBuilder.HasOriginalValueParameter(propertyName)); + + public override TestOwnedNavigationStoredProcedureBuilder HasOriginalValueParameter( + string propertyName, + Action buildAction) + => Wrap(StoredProcedureBuilder.HasOriginalValueParameter(propertyName, s => buildAction(new(s)))); + + public override TestOwnedNavigationStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression) + => Wrap(StoredProcedureBuilder.HasOriginalValueParameter(propertyExpression.GetMemberAccess().Name)); + + public override TestOwnedNavigationStoredProcedureBuilder HasOriginalValueParameter( + Expression> propertyExpression, + Action buildAction) + => Wrap( + StoredProcedureBuilder.HasOriginalValueParameter( + propertyExpression.GetMemberAccess().Name, s => buildAction(new(s)))); + + public override TestOwnedNavigationStoredProcedureBuilder HasRowsAffectedParameter() + => Wrap(StoredProcedureBuilder.HasRowsAffectedParameter()); + + public override TestOwnedNavigationStoredProcedureBuilder HasRowsAffectedParameter( + Action buildAction) + => Wrap(StoredProcedureBuilder.HasRowsAffectedParameter(s => buildAction(new(s)))); public override TestOwnedNavigationStoredProcedureBuilder HasResultColumn( string propertyName) @@ -1789,6 +2214,16 @@ public override TestOwnedNavigationStoredProcedureBuilder Wrap( StoredProcedureBuilder.HasResultColumn( propertyExpression.GetMemberAccess().Name, s => buildAction(new(s)))); + + public override TestOwnedNavigationStoredProcedureBuilder HasRowsAffectedResultColumn() + => Wrap(StoredProcedureBuilder.HasRowsAffectedResultColumn()); + + public override TestOwnedNavigationStoredProcedureBuilder HasRowsAffectedResultColumn( + Action buildAction) + => Wrap(StoredProcedureBuilder.HasRowsAffectedResultColumn(s => buildAction(new(s)))); + + public override TestOwnedNavigationStoredProcedureBuilder HasRowsAffectedReturnValue(bool rowsAffectedReturned) + => Wrap(StoredProcedureBuilder.HasRowsAffectedReturnValue(rowsAffectedReturned)); public override TestOwnedNavigationStoredProcedureBuilder SuppressTransactions(bool suppress) => Wrap(StoredProcedureBuilder.SuppressTransactions(suppress)); @@ -1814,7 +2249,7 @@ StoredProcedureParameterBuilder IInfrastructure protected virtual TestStoredProcedureParameterBuilder Wrap(StoredProcedureParameterBuilder storedProcedureParameterBuilder) => new(storedProcedureParameterBuilder); - public virtual TestStoredProcedureParameterBuilder HasName(string? name) + public virtual TestStoredProcedureParameterBuilder HasName(string name) => Wrap(StoredProcedureParameterBuilder.HasName(name)); public virtual TestStoredProcedureParameterBuilder IsOutput() @@ -1844,7 +2279,7 @@ StoredProcedureResultColumnBuilder IInfrastructure new TestStoredProcedureResultColumnBuilder(storedProcedureResultColumnBuilder); - public virtual TestStoredProcedureResultColumnBuilder HasName(string? name) + public virtual TestStoredProcedureResultColumnBuilder HasName(string name) => Wrap(StoredProcedureResultColumnBuilder.HasName(name)); public virtual TestStoredProcedureResultColumnBuilder HasAnnotation( diff --git a/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs b/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs index 8a8f77c70f6..29fecbd5d3f 100644 --- a/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs +++ b/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs @@ -69,6 +69,20 @@ public class RelationalApiConsistencyFixture : ApiConsistencyFixtureBase typeof(IConventionStoredProcedureBuilder), typeof(IStoredProcedure)) }, + { + typeof(IReadOnlyStoredProcedureParameter), + (typeof(IMutableStoredProcedureParameter), + typeof(IConventionStoredProcedureParameter), + typeof(IConventionStoredProcedureParameterBuilder), + typeof(IStoredProcedureParameter)) + }, + { + typeof(IReadOnlyStoredProcedureResultColumn), + (typeof(IMutableStoredProcedureResultColumn), + typeof(IConventionStoredProcedureResultColumn), + typeof(IConventionStoredProcedureResultColumnBuilder), + typeof(IStoredProcedureResultColumn)) + }, { typeof(IReadOnlySequence), (typeof(IMutableSequence), @@ -113,15 +127,19 @@ public class RelationalApiConsistencyFixture : ApiConsistencyFixtureBase typeof(ITable), typeof(IView), typeof(IStoreFunction), + typeof(IStoreStoredProcedure), typeof(ITableMappingBase), typeof(ITableMapping), typeof(IViewMapping), typeof(IFunctionMapping), + typeof(IStoredProcedureMapping), typeof(IColumnBase), typeof(IColumn), typeof(IViewColumn), typeof(IFunctionColumn), typeof(IStoreFunctionParameter), + typeof(IStoreStoredProcedureParameter), + typeof(IStoreStoredProcedureResultColumn), typeof(IFunctionColumnMapping), typeof(IColumnMappingBase), typeof(IColumnMapping), @@ -130,7 +148,6 @@ public class RelationalApiConsistencyFixture : ApiConsistencyFixtureBase typeof(IForeignKeyConstraint), typeof(IUniqueConstraint), typeof(ITrigger), - typeof(IEntityTypeMappingFragment), typeof(IRelationalPropertyOverrides) }; @@ -281,6 +298,23 @@ public override typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(methodTypes[0], methodTypes[1])), typeof(Action) }), + GetMethod( + typeof(StoredProcedureBuilder<>), + nameof(StoredProcedureBuilder.HasOriginalValueParameter), + genericParameterCount: 2, + (typeTypes, methodTypes) => new[] + { + typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(methodTypes[0], methodTypes[1])) + }), + GetMethod( + typeof(StoredProcedureBuilder<>), + nameof(StoredProcedureBuilder.HasOriginalValueParameter), + genericParameterCount: 2, + (typeTypes, methodTypes) => new[] + { + typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(methodTypes[0], methodTypes[1])), + typeof(Action) + }), GetMethod( typeof(StoredProcedureBuilder<>), nameof(StoredProcedureBuilder.HasResultColumn), @@ -297,7 +331,10 @@ public override { typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(methodTypes[0], methodTypes[1])), typeof(Action) - }) + }), + typeof(IConventionStoredProcedure).GetMethod( + nameof(IConventionStoredProcedure.SetAreRowsAffectedReturned), + new[] { typeof(bool), typeof(bool) }) }; public override HashSet AsyncMethodExceptions { get; } = new() diff --git a/test/EFCore.Relational.Tests/RelationalDatabaseFacadeExtensionsTest.cs b/test/EFCore.Relational.Tests/RelationalDatabaseFacadeExtensionsTest.cs index 3ff9d43feb3..228fc1c45c2 100644 --- a/test/EFCore.Relational.Tests/RelationalDatabaseFacadeExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/RelationalDatabaseFacadeExtensionsTest.cs @@ -13,7 +13,7 @@ public class RelationalDatabaseFacadeExtensionsTest [ConditionalFact] public void Return_true_if_relational() { - using var context = RelationalTestHelpers.Instance.CreateContext(); + using var context = FakeRelationalTestHelpers.Instance.CreateContext(); Assert.True(context.Database.IsRelational()); } @@ -28,7 +28,7 @@ public void Return_false_if_inMemory() public void GetDbConnection_returns_the_current_connection() { var dbConnection = new FakeDbConnection("A=B"); - var context = RelationalTestHelpers.Instance.CreateContext(); + var context = FakeRelationalTestHelpers.Instance.CreateContext(); ((FakeRelationalConnection)context.GetService()).UseConnection(dbConnection); @@ -55,7 +55,7 @@ public void Relational_specific_methods_throws_when_non_relational_provider_is_i public async Task Can_open_the_underlying_connection(bool async) { var dbConnection = new FakeDbConnection("A=B"); - var context = RelationalTestHelpers.Instance.CreateContext(); + var context = FakeRelationalTestHelpers.Instance.CreateContext(); ((FakeRelationalConnection)context.GetService()).UseConnection(dbConnection); @@ -77,7 +77,7 @@ public async Task Can_open_the_underlying_connection(bool async) public async Task Can_close_the_underlying_connection(bool async) { var dbConnection = new FakeDbConnection("A=B"); - var context = RelationalTestHelpers.Instance.CreateContext(); + var context = FakeRelationalTestHelpers.Instance.CreateContext(); ((FakeRelationalConnection)context.GetService()).UseConnection(dbConnection); @@ -101,7 +101,7 @@ public async Task Can_close_the_underlying_connection(bool async) public async Task Can_begin_transaction_with_isolation_level(bool async) { var dbConnection = new FakeDbConnection("A=B"); - var context = RelationalTestHelpers.Instance.CreateContext(); + var context = FakeRelationalTestHelpers.Instance.CreateContext(); ((FakeRelationalConnection)context.GetService()).UseConnection(dbConnection); var transaction = async @@ -116,7 +116,7 @@ public async Task Can_begin_transaction_with_isolation_level(bool async) public void Can_use_transaction() { var dbConnection = new FakeDbConnection("A=B"); - var context = RelationalTestHelpers.Instance.CreateContext(); + var context = FakeRelationalTestHelpers.Instance.CreateContext(); ((FakeRelationalConnection)context.GetService()).UseConnection(dbConnection); var transaction = new FakeDbTransaction(dbConnection, IsolationLevel.Chaos); @@ -211,7 +211,7 @@ public void GetMigrations_works() var migrationsAssembly = new FakeIMigrationsAssembly { Migrations = migrations.ToDictionary(x => x, x => default(TypeInfo)) }; - var db = RelationalTestHelpers.Instance.CreateContext( + var db = FakeRelationalTestHelpers.Instance.CreateContext( new ServiceCollection().AddSingleton(migrationsAssembly)); Assert.Equal(migrations, db.Database.GetMigrations()); @@ -239,7 +239,7 @@ public async Task GetAppliedMigrations_works(bool async) var repository = new FakeHistoryRepository { AppliedMigrations = migrations.Select(id => new HistoryRow(id, "1.1.0")).ToList() }; - var context = RelationalTestHelpers.Instance.CreateContext( + var context = FakeRelationalTestHelpers.Instance.CreateContext( new ServiceCollection().AddSingleton(repository)); Assert.Equal( @@ -303,7 +303,7 @@ public async Task GetPendingMigrations_works(bool async) AppliedMigrations = appliedMigrations.Select(id => new HistoryRow(id, "1.1.0")).ToList() }; - var context = RelationalTestHelpers.Instance.CreateContext( + var context = FakeRelationalTestHelpers.Instance.CreateContext( new ServiceCollection() .AddSingleton(repository) .AddSingleton(migrationsAssembly)); diff --git a/test/EFCore.Relational.Tests/RelationalEventIdTest.cs b/test/EFCore.Relational.Tests/RelationalEventIdTest.cs index 6dd8397a957..470afda0018 100644 --- a/test/EFCore.Relational.Tests/RelationalEventIdTest.cs +++ b/test/EFCore.Relational.Tests/RelationalEventIdTest.cs @@ -26,7 +26,7 @@ public void Every_eventId_has_a_logger_method_and_logs_when_level_enabled() var key = entityType.AddKey(property, ConfigurationSource.Convention); var foreignKey = new ForeignKey(new List { property }, key, entityType, entityType, ConfigurationSource.Convention); var index = new Index(new List { property }, "IndexName", entityType, ConfigurationSource.Convention); - var contextServices = RelationalTestHelpers.Instance.CreateContextServices(model.FinalizeModel()); + var contextServices = FakeRelationalTestHelpers.Instance.CreateContextServices(model.FinalizeModel()); var updateEntry = new InternalEntityEntry(contextServices.GetRequiredService(), entityType, new object()); var columnOperation = new AddColumnOperation { diff --git a/test/EFCore.Relational.Tests/Storage/RelationalDatabaseFacadeExtensionsTest.cs b/test/EFCore.Relational.Tests/Storage/RelationalDatabaseFacadeExtensionsTest.cs index b8c172f69df..677513f7a7b 100644 --- a/test/EFCore.Relational.Tests/Storage/RelationalDatabaseFacadeExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Storage/RelationalDatabaseFacadeExtensionsTest.cs @@ -253,8 +253,8 @@ private class ThudContext : DbContext { public ThudContext() : base( - RelationalTestHelpers.Instance.CreateOptions( - RelationalTestHelpers.Instance.CreateServiceProvider( + FakeRelationalTestHelpers.Instance.CreateOptions( + FakeRelationalTestHelpers.Instance.CreateServiceProvider( new ServiceCollection() .AddScoped()))) { diff --git a/test/EFCore.Relational.Tests/Storage/RelationalParameterBuilderTest.cs b/test/EFCore.Relational.Tests/Storage/RelationalParameterBuilderTest.cs index d13992fb384..47bdee99c40 100644 --- a/test/EFCore.Relational.Tests/Storage/RelationalParameterBuilderTest.cs +++ b/test/EFCore.Relational.Tests/Storage/RelationalParameterBuilderTest.cs @@ -49,7 +49,7 @@ public void Can_add_type_mapped_parameter_by_property(bool nullable) TestServiceFactory.Instance.Create(), TestServiceFactory.Instance.Create()); - var modelBuilder = RelationalTestHelpers.Instance.CreateConventionBuilder(); + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); modelBuilder.Entity("MyType").Property("MyProp").IsRequired(!nullable); diff --git a/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTest.cs b/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTest.cs index 46ad3f4e069..e79abd11916 100644 --- a/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTest.cs +++ b/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTest.cs @@ -345,5 +345,5 @@ public static RelationalTypeMapping GetMapping( => typeMappingSource.FindMapping(property); protected override ModelBuilder CreateModelBuilder(Action configure = null) - => RelationalTestHelpers.Instance.CreateConventionBuilder(configureModel: configure); + => FakeRelationalTestHelpers.Instance.CreateConventionBuilder(configureModel: configure); } diff --git a/test/EFCore.Relational.Tests/TestUtilities/RelationalTestHelpers.cs b/test/EFCore.Relational.Tests/TestUtilities/FakeRelationalTestHelpers.cs similarity index 75% rename from test/EFCore.Relational.Tests/TestUtilities/RelationalTestHelpers.cs rename to test/EFCore.Relational.Tests/TestUtilities/FakeRelationalTestHelpers.cs index 400de7b963e..0b4703206ea 100644 --- a/test/EFCore.Relational.Tests/TestUtilities/RelationalTestHelpers.cs +++ b/test/EFCore.Relational.Tests/TestUtilities/FakeRelationalTestHelpers.cs @@ -5,15 +5,15 @@ namespace Microsoft.EntityFrameworkCore.TestUtilities; -public class RelationalTestHelpers : TestHelpers +public class FakeRelationalTestHelpers : TestHelpers { - protected RelationalTestHelpers() + protected FakeRelationalTestHelpers() { } - public static RelationalTestHelpers Instance { get; } = new(); + public static FakeRelationalTestHelpers Instance { get; } = new(); - protected override EntityFrameworkDesignServicesBuilder CreateEntityFrameworkDesignServicesBuilder( + protected virtual EntityFrameworkDesignServicesBuilder CreateEntityFrameworkDesignServicesBuilder( IServiceCollection services) => new EntityFrameworkRelationalDesignServicesBuilder(services); diff --git a/test/EFCore.Relational.Tests/TestUtilities/TestRelationalConventionSetBuilder.cs b/test/EFCore.Relational.Tests/TestUtilities/TestRelationalConventionSetBuilder.cs index 16f0247e0d1..99df9f0fd93 100644 --- a/test/EFCore.Relational.Tests/TestUtilities/TestRelationalConventionSetBuilder.cs +++ b/test/EFCore.Relational.Tests/TestUtilities/TestRelationalConventionSetBuilder.cs @@ -13,5 +13,5 @@ public TestRelationalConventionSetBuilder( } public static ConventionSet Build() - => ConventionSet.CreateConventionSet(RelationalTestHelpers.Instance.CreateContext()); + => ConventionSet.CreateConventionSet(FakeRelationalTestHelpers.Instance.CreateContext()); } diff --git a/test/EFCore.Relational.Tests/Update/BatchExecutorTest.cs b/test/EFCore.Relational.Tests/Update/BatchExecutorTest.cs index 4e5cbc3f560..e166f9a05ce 100644 --- a/test/EFCore.Relational.Tests/Update/BatchExecutorTest.cs +++ b/test/EFCore.Relational.Tests/Update/BatchExecutorTest.cs @@ -78,7 +78,7 @@ private static readonly IServiceProvider _serviceProvider .BuildServiceProvider(validateScopes: true); public TestContext() - : base(RelationalTestHelpers.Instance.CreateOptions(_serviceProvider)) + : base(FakeRelationalTestHelpers.Instance.CreateOptions(_serviceProvider)) { } diff --git a/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs b/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs index 5b4fd46cda7..8d19a4998ba 100644 --- a/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs +++ b/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs @@ -339,7 +339,7 @@ public void BatchCommands_sorts_entities_while_reassigning_child_tree() [ConditionalFact] public void BatchCommands_creates_batches_lazily() { - var configuration = RelationalTestHelpers.Instance.CreateContextServices( + var configuration = FakeRelationalTestHelpers.Instance.CreateContextServices( new ServiceCollection().AddScoped(), CreateFKOneToManyModelWithGeneratedIds()); @@ -996,7 +996,7 @@ public void BatchCommands_creates_batch_on_incomplete_updates_for_shared_table_n } private static IServiceProvider CreateContextServices(IModel model) - => RelationalTestHelpers.Instance.CreateContextServices(model); + => FakeRelationalTestHelpers.Instance.CreateContextServices(model); public List CreateBatches( IUpdateEntry[] entries, @@ -1012,7 +1012,7 @@ public ICommandBatchPreparer CreateCommandBatchPreparer( bool sensitiveLogging = false) { modificationCommandBatchFactory ??= - RelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); + FakeRelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); var loggingOptions = new LoggingOptions(); if (sensitiveLogging) @@ -1033,7 +1033,7 @@ public ICommandBatchPreparer CreateCommandBatchPreparer( private static IModel CreateSimpleFKModel() { - var modelBuilder = RelationalTestHelpers.Instance.CreateConventionBuilder(); + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); modelBuilder.Entity( b => @@ -1055,7 +1055,7 @@ private static IModel CreateSimpleFKModel() private static IModel CreateFKOneToManyModelWithGeneratedIds() { - var modelBuilder = RelationalTestHelpers.Instance.CreateConventionBuilder(); + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); modelBuilder.Entity( b => @@ -1077,7 +1077,7 @@ private static IModel CreateFKOneToManyModelWithGeneratedIds() private static IModel CreateCyclicFKModel() { - var modelBuilder = RelationalTestHelpers.Instance.CreateConventionBuilder(); + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); modelBuilder.Entity( b => @@ -1105,7 +1105,7 @@ private static IModel CreateCyclicFKModel() private static IModel CreateCyclicFkWithTailModel() { - var modelBuilder = RelationalTestHelpers.Instance.CreateConventionBuilder(); + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); modelBuilder.Entity( b => @@ -1141,7 +1141,7 @@ private static IModel CreateCyclicFkWithTailModel() private static IModel CreateTwoLevelFKModel() { - var modelBuilder = RelationalTestHelpers.Instance.CreateConventionBuilder(); + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); modelBuilder.Entity(); @@ -1166,7 +1166,7 @@ private static IModel CreateTwoLevelFKModel() private static IModel CreateSharedTableModel() { - var modelBuilder = RelationalTestHelpers.Instance.CreateConventionBuilder(); + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); modelBuilder.Entity( b => diff --git a/test/EFCore.Relational.Tests/Update/ModificationCommandComparerTest.cs b/test/EFCore.Relational.Tests/Update/ModificationCommandComparerTest.cs index 6de424935d8..cdc176bb4a3 100644 --- a/test/EFCore.Relational.Tests/Update/ModificationCommandComparerTest.cs +++ b/test/EFCore.Relational.Tests/Update/ModificationCommandComparerTest.cs @@ -12,7 +12,7 @@ public class ModificationCommandComparerTest [ConditionalFact] public void Compare_returns_0_only_for_commands_that_are_equal() { - var modelBuilder = RelationalTestHelpers.Instance.CreateConventionBuilder(); + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); var entityType = modelBuilder.Model.AddEntityType(typeof(object)); var key = entityType.AddProperty("Id", typeof(int)); entityType.SetPrimaryKey(key); @@ -163,7 +163,7 @@ public void Compare_returns_0_only_for_entries_that_have_same_key_values() private void Compare_returns_0_only_for_entries_that_have_same_key_values_generic(T value1, T value2) { - var modelBuilder = RelationalTestHelpers.Instance.CreateConventionBuilder(); + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); var entityType = modelBuilder.Model.AddEntityType(typeof(object)); var keyProperty = entityType.AddProperty("Id", typeof(T)); diff --git a/test/EFCore.Relational.Tests/Update/ModificationCommandTest.cs b/test/EFCore.Relational.Tests/Update/ModificationCommandTest.cs index c1e05c40b2a..13047fcda55 100644 --- a/test/EFCore.Relational.Tests/Update/ModificationCommandTest.cs +++ b/test/EFCore.Relational.Tests/Update/ModificationCommandTest.cs @@ -442,7 +442,7 @@ private class T1 private static IModel BuildModel(bool generateKeyValues, bool computeNonKeyValue) { - var modelBuilder = RelationalTestHelpers.Instance.CreateConventionBuilder(); + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); var model = modelBuilder.Model; var entityType = model.AddEntityType(typeof(T1)); @@ -473,7 +473,7 @@ private static InternalEntityEntry CreateEntry( { var model = BuildModel(generateKeyValues, computeNonKeyValue); - return RelationalTestHelpers.Instance.CreateInternalEntry( + return FakeRelationalTestHelpers.Instance.CreateInternalEntry( model, entityState, new diff --git a/test/EFCore.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs b/test/EFCore.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs index 4e065f16c7b..b89d2879671 100644 --- a/test/EFCore.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs +++ b/test/EFCore.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs @@ -628,7 +628,7 @@ private class T1 private static IModel BuildModel(bool generateKeyValues, bool computeNonKeyValue) { - var modelBuilder = RelationalTestHelpers.Instance.CreateConventionBuilder(); + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); var entityType = modelBuilder.Entity(); entityType.Property(t => t.Id).HasColumnName("Col1"); @@ -654,7 +654,7 @@ private static InternalEntityEntry CreateEntry( { var model = BuildModel(generateKeyValues, computeNonKeyValue); - return RelationalTestHelpers.Instance.CreateInternalEntry( + return FakeRelationalTestHelpers.Instance.CreateInternalEntry( model, entityState, new T1 { Id = overrideKeyValues ? 1 : default, @@ -692,7 +692,7 @@ private static ModificationCommandBatchFactoryDependencies CreateDependencies( var logger = new FakeRelationalCommandDiagnosticsLogger(); sqlGenerator ??= new FakeSqlGenerator( - RelationalTestHelpers.Instance.CreateContextServices() + FakeRelationalTestHelpers.Instance.CreateContextServices() .GetRequiredService()); return new ModificationCommandBatchFactoryDependencies( diff --git a/test/EFCore.Relational.Tests/Update/UpdateSqlGeneratorTest.cs b/test/EFCore.Relational.Tests/Update/UpdateSqlGeneratorTest.cs index 34c232b0733..37d1b2a81eb 100644 --- a/test/EFCore.Relational.Tests/Update/UpdateSqlGeneratorTest.cs +++ b/test/EFCore.Relational.Tests/Update/UpdateSqlGeneratorTest.cs @@ -107,7 +107,7 @@ protected override void AppendUpdateOperation_for_computed_property_verification stringBuilder.ToString()); protected override TestHelpers TestHelpers - => RelationalTestHelpers.Instance; + => FakeRelationalTestHelpers.Instance; protected override string RowsAffected => "provider_specific_rowcount()"; diff --git a/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs b/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs index cbacc4a29b6..30e19e30d12 100644 --- a/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs +++ b/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs @@ -1563,54 +1563,6 @@ protected class Profile15 public virtual Profile15 Profile4 { get; set; } } - [ConditionalFact] - public virtual void ForeignKey_to_ForeignKey_on_many_to_many() - { - var modelBuilder = CreateModelBuilder(); - - modelBuilder.Entity( - entity => - { - entity.HasMany(d => d.Profile16s) - .WithMany(p => p.Login16s) - .UsingEntity>( - "Login16Profile16", - l => l.HasOne().WithMany().HasForeignKey("Profile16Id"), - r => r.HasOne().WithMany().HasForeignKey("Login16Id"), - j => - { - j.HasKey("Login16Id", "Profile16Id"); - - j.ToTable("Login16Profile16"); - }); - }); - - var model = Validate(modelBuilder); - - var login = modelBuilder.Model.FindEntityType(typeof(Login16)); - var logins = login.FindSkipNavigation(nameof(Login16.Profile16s)); - var join = logins.JoinEntityType; - Assert.Equal(2, join.GetProperties().Count()); - Assert.False(GetProperty(model, "Login16Id").IsForeignKey()); - Assert.False(GetProperty(model, "Profile16Id").IsForeignKey()); - } - - public class Login16 - { - public int Login16Id { get; set; } - - [ForeignKey("Login16Id")] - public virtual ICollection Profile16s { get; set; } - } - - public class Profile16 - { - public int Profile16Id { get; set; } - - [ForeignKey("Profile16Id")] - public virtual ICollection Login16s { get; set; } - } - [ConditionalFact] public virtual void ForeignKeyAttribute_configures_relationships_when_inverse_on_derived() { diff --git a/test/EFCore.Specification.Tests/EFCore.Specification.Tests.csproj b/test/EFCore.Specification.Tests/EFCore.Specification.Tests.csproj index c2ee41f6bfa..ca96bd27d4f 100644 --- a/test/EFCore.Specification.Tests/EFCore.Specification.Tests.csproj +++ b/test/EFCore.Specification.Tests/EFCore.Specification.Tests.csproj @@ -45,7 +45,6 @@ - diff --git a/test/EFCore.Specification.Tests/Query/OwnedEntityQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/OwnedEntityQueryTestBase.cs index 47095868bce..5293339a155 100644 --- a/test/EFCore.Specification.Tests/Query/OwnedEntityQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/OwnedEntityQueryTestBase.cs @@ -402,95 +402,4 @@ protected class IntermediateOwnedEntity public CustomerData CustomerData { get; set; } public SupplierData SupplierData { get; set; } } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task Owned_entity_with_all_null_properties_materializes_when_not_containing_another_owned_entity(bool async) - { - var contextFactory = await InitializeAsync(seed: c => c.Seed()); - - using var context = contextFactory.CreateContext(); - var query = context.RotRutCases.OrderBy(e => e.Buyer); - - var result = async - ? await query.ToListAsync() - : query.ToList(); - - Assert.Collection(result, - t => - { - Assert.Equal("Buyer1", t.Buyer); - Assert.NotNull(t.Rot); - Assert.Equal(1, t.Rot.ServiceType); - Assert.Equal("1", t.Rot.ApartmentNo); - Assert.NotNull(t.Rut); - Assert.Equal(1, t.Rut.Value); - }, - t => - { - Assert.Equal("Buyer2", t.Buyer); - // Cannot verify owned entities here since they differ between relational/in-memory - }); - } - - protected class MyContext28247 : DbContext - { - public MyContext28247(DbContextOptions options) - : base(options) - { - } - - public DbSet RotRutCases { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity(b => - { - b.ToTable("RotRutCases"); - - b.OwnsOne(e => e.Rot); - b.OwnsOne(e => e.Rut); - }); - } - - public void Seed() - { - Add( - new RotRutCase - { - Buyer = "Buyer1", - Rot = new Rot { ServiceType = 1, ApartmentNo = "1" }, - Rut = new Rut { Value = 1 } - }); - - Add( - new RotRutCase - { - Buyer = "Buyer2", - Rot = new Rot { ServiceType = null, ApartmentNo = null }, - Rut = new Rut { Value = null } - }); - - SaveChanges(); - } - } - - public class RotRutCase - { - public int Id { get; set; } - public string Buyer { get; set; } - public Rot Rot { get; set; } - public Rut Rut { get; set; } - } - - public class Rot - { - public int? ServiceType { get; set; } - public string ApartmentNo { get; set; } - } - - public class Rut - { - public int? Value { get; set; } - } } diff --git a/test/EFCore.Specification.Tests/QueryExpressionInterceptionTestBase.cs b/test/EFCore.Specification.Tests/QueryExpressionInterceptionTestBase.cs index 410da6cef9f..91a3babf548 100644 --- a/test/EFCore.Specification.Tests/QueryExpressionInterceptionTestBase.cs +++ b/test/EFCore.Specification.Tests/QueryExpressionInterceptionTestBase.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.EntityFrameworkCore.Query.SqlExpressions; - namespace Microsoft.EntityFrameworkCore; public abstract class QueryExpressionInterceptionTestBase : InterceptionTestBase diff --git a/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs b/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs index 75a320006b4..4e893901ec9 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs @@ -34,7 +34,7 @@ public DbContextOptions CreateOptions(IServiceProvider serviceProvider = null) public IServiceProvider CreateServiceProvider(IServiceCollection customServices = null) => CreateServiceProvider(customServices, AddProviderServices); - private static IServiceProvider CreateServiceProvider( + protected static IServiceProvider CreateServiceProvider( IServiceCollection customServices, Func addProviderServices) { @@ -52,73 +52,6 @@ private static IServiceProvider CreateServiceProvider( return services.BuildServiceProvider(); // No scope validation; test doubles violate scopes, but only resolved once. } - protected virtual EntityFrameworkDesignServicesBuilder CreateEntityFrameworkDesignServicesBuilder(IServiceCollection services) - => new(services); - - public IServiceProvider CreateDesignServiceProvider( - IServiceCollection customServices = null, - Action replaceServices = null, - Type additionalDesignTimeServices = null, - IOperationReporter reporter = null) - => CreateDesignServiceProvider( - CreateContext().GetService().Name, - customServices, - replaceServices, - additionalDesignTimeServices, - reporter); - - public IServiceProvider CreateDesignServiceProvider( - string provider, - IServiceCollection customServices = null, - Action replaceServices = null, - Type additionalDesignTimeServices = null, - IOperationReporter reporter = null) - => CreateServiceProvider( - customServices, services => - { - if (replaceServices != null) - { - var builder = CreateEntityFrameworkDesignServicesBuilder(services); - replaceServices(builder); - } - - if (additionalDesignTimeServices != null) - { - ConfigureDesignTimeServices(additionalDesignTimeServices, services); - } - - ConfigureProviderServices(provider, services); - services.AddEntityFrameworkDesignTimeServices(reporter); - - return services; - }); - - private void ConfigureProviderServices(string provider, IServiceCollection services) - { - var providerAssembly = Assembly.Load(new AssemblyName(provider)); - - var providerServicesAttribute = providerAssembly.GetCustomAttribute(); - if (providerServicesAttribute == null) - { - throw new InvalidOperationException(DesignStrings.CannotFindDesignTimeProviderAssemblyAttribute(provider)); - } - - var designTimeServicesType = providerAssembly.GetType( - providerServicesAttribute.TypeName, - throwOnError: true, - ignoreCase: false)!; - - ConfigureDesignTimeServices(designTimeServicesType, services); - } - - private static void ConfigureDesignTimeServices( - Type designTimeServicesType, - IServiceCollection services) - { - var designTimeServices = (IDesignTimeServices)Activator.CreateInstance(designTimeServicesType)!; - designTimeServices.ConfigureDesignTimeServices(services); - } - public abstract IServiceCollection AddProviderServices(IServiceCollection services); public DbContextOptionsBuilder AddProviderOptions(DbContextOptionsBuilder optionsBuilder) diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs index c5f29cab1e3..dd27b926947 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs @@ -6980,7 +6980,7 @@ protected override string StoreName protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; - public override TestHelpers TestHelpers + public override RelationalTestHelpers TestHelpers => SqlServerTestHelpers.Instance; protected override IServiceCollection AddServices(IServiceCollection serviceCollection) diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerTestHelpers.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerTestHelpers.cs index 2f4d2624ed9..b7b7b7329d2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerTestHelpers.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerTestHelpers.cs @@ -6,7 +6,7 @@ namespace Microsoft.EntityFrameworkCore.TestUtilities; -public class SqlServerTestHelpers : TestHelpers +public class SqlServerTestHelpers : RelationalTestHelpers { protected SqlServerTestHelpers() { diff --git a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs index 5c737f568bd..5f8e1ade98f 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs @@ -1218,7 +1218,7 @@ protected override string StoreName protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; - public override TestHelpers TestHelpers + public override RelationalTestHelpers TestHelpers => SqliteTestHelpers.Instance; protected override IServiceCollection AddServices(IServiceCollection serviceCollection) diff --git a/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteTestHelpers.cs b/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteTestHelpers.cs index 1b0386ab51b..76a591602d0 100644 --- a/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteTestHelpers.cs +++ b/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteTestHelpers.cs @@ -6,7 +6,7 @@ namespace Microsoft.EntityFrameworkCore.TestUtilities; -public class SqliteTestHelpers : TestHelpers +public class SqliteTestHelpers : RelationalTestHelpers { protected SqliteTestHelpers() {