diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index ecd60227ecb..5a1406e09f4 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -55,7 +55,6 @@ public virtual void Generate(string builderName, IModel model, IndentedStringBui var annotations = model.GetAnnotations().ToList(); - IgnoreAnnotationTypes(annotations, RelationalAnnotationNames.DbFunction); IgnoreAnnotations( annotations, ChangeDetector.SkipDetectChangesAnnotation, @@ -63,6 +62,7 @@ public virtual void Generate(string builderName, IModel model, IndentedStringBui CoreAnnotationNames.OwnedTypes, RelationalAnnotationNames.CheckConstraints, RelationalAnnotationNames.Sequences, + RelationalAnnotationNames.DbFunctions, RelationalAnnotationNames.Tables, RelationalAnnotationNames.Views); diff --git a/src/EFCore.Design/Migrations/Design/MigrationsCodeGenerator.cs b/src/EFCore.Design/Migrations/Design/MigrationsCodeGenerator.cs index e525186dcdd..66273516c6e 100644 --- a/src/EFCore.Design/Migrations/Design/MigrationsCodeGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/MigrationsCodeGenerator.cs @@ -243,6 +243,7 @@ private IEnumerable GetAnnotationNamespaces(IEnumerable it CoreAnnotationNames.QueryFilter, RelationalAnnotationNames.CheckConstraints, RelationalAnnotationNames.Sequences, + RelationalAnnotationNames.DbFunctions, RelationalAnnotationNames.Tables, RelationalAnnotationNames.TableMappings, RelationalAnnotationNames.TableColumnMappings, @@ -251,18 +252,12 @@ private IEnumerable GetAnnotationNamespaces(IEnumerable it RelationalAnnotationNames.ViewColumnMappings }; - var ignoredAnnotationTypes = new List - { - RelationalAnnotationNames.DbFunction - }; - return items.SelectMany( i => i.GetAnnotations().Select( a => new { Annotatable = i, Annotation = a }) .Where( a => a.Annotation.Value != null - && !ignoredAnnotations.Contains(a.Annotation.Name) - && !ignoredAnnotationTypes.Any(p => a.Annotation.Name.StartsWith(p, StringComparison.Ordinal))) + && !ignoredAnnotations.Contains(a.Annotation.Name)) .SelectMany(a => GetProviderType(a.Annotatable, a.Annotation.Value.GetType()).GetNamespaces())); } diff --git a/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs b/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs index c32f12aefb2..47ee7d33d25 100644 --- a/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs @@ -219,7 +219,7 @@ public static IConventionSequence AddSequence( /// public static IMutableSequence RemoveSequence( [NotNull] this IMutableModel model, [NotNull] string name, [CanBeNull] string schema = null) - => Sequence.RemoveSequence(model, name, schema); + => Sequence.RemoveSequence(Check.NotNull(model, nameof(model)), name, schema); /// /// Removes the with the given name. @@ -233,28 +233,28 @@ public static IMutableSequence RemoveSequence( /// public static IConventionSequence RemoveSequence( [NotNull] this IConventionModel model, [NotNull] string name, [CanBeNull] string schema = null) - => (IConventionSequence)((IMutableModel)model).RemoveSequence(name, schema); + => Sequence.RemoveSequence((IMutableModel)Check.NotNull(model, nameof(model)), name, schema); /// /// Returns all s contained in the model. /// /// The model to get the sequences in. public static IEnumerable GetSequences([NotNull] this IModel model) - => Sequence.GetSequences(model); + => Sequence.GetSequences(Check.NotNull(model, nameof(model))); /// /// Returns all s contained in the model. /// /// The model to get the sequences in. public static IEnumerable GetSequences([NotNull] this IMutableModel model) - => (IEnumerable)((IModel)model).GetSequences(); + => Sequence.GetSequences(Check.NotNull(model, nameof(model))); /// /// Returns all s contained in the model. /// /// The model to get the sequences in. public static IEnumerable GetSequences([NotNull] this IConventionModel model) - => (IEnumerable)((IModel)model).GetSequences(); + => Sequence.GetSequences(Check.NotNull(model, nameof(model))); /// /// Finds a that is mapped to the method represented by the given . @@ -263,14 +263,9 @@ public static IEnumerable GetSequences([NotNull] this IConv /// The for the method that is mapped to the function. /// The or null if the method is not mapped. public static IDbFunction FindDbFunction([NotNull] this IModel model, [NotNull] MethodInfo method) - { - Check.NotNull(model, nameof(model)); - Check.NotNull(method, nameof(method)); - - return DbFunction.FindDbFunction( + => DbFunction.FindDbFunction( Check.NotNull(model, nameof(model)), Check.NotNull(method, nameof(method))); - } /// /// Finds a that is mapped to the method represented by the given . @@ -290,6 +285,35 @@ public static IMutableDbFunction FindDbFunction([NotNull] this IMutableModel mod public static IConventionDbFunction FindDbFunction([NotNull] this IConventionModel model, [NotNull] MethodInfo method) => (IConventionDbFunction)((IModel)model).FindDbFunction(method); + /// + /// Finds a that is mapped to the method represented by the given . + /// + /// The model to find the function in. + /// The model name of the function. + /// The or null if the method is not mapped. + public static IDbFunction FindDbFunction([NotNull] this IModel model, [NotNull] string name) + => DbFunction.FindDbFunction( + Check.NotNull(model, nameof(model)), + Check.NotNull(name, nameof(name))); + + /// + /// Finds a that is mapped to the method represented by the given . + /// + /// The model to find the function in. + /// The model name of the function. + /// The or null if the method is not mapped. + public static IMutableDbFunction FindDbFunction([NotNull] this IMutableModel model, [NotNull] string name) + => (IMutableDbFunction)((IModel)model).FindDbFunction(name); + + /// + /// Finds a that is mapped to the method represented by the given . + /// + /// The model to find the function in. + /// The model name of the function. + /// The or null if the method is not mapped. + public static IConventionDbFunction FindDbFunction([NotNull] this IConventionModel model, [NotNull] string name) + => (IConventionDbFunction)((IModel)model).FindDbFunction(name); + /// /// Either returns the existing mapped to the given method /// or creates a new function mapped to the method. @@ -298,8 +322,8 @@ public static IConventionDbFunction FindDbFunction([NotNull] this IConventionMod /// The for the method that is mapped to the function. /// The . public static DbFunction AddDbFunction([NotNull] this IMutableModel model, [NotNull] MethodInfo methodInfo) - => new DbFunction( - Check.NotNull(methodInfo, nameof(methodInfo)), model, ConfigurationSource.Explicit); + => DbFunction.AddDbFunction( + model, Check.NotNull(methodInfo, nameof(methodInfo)), ConfigurationSource.Explicit); /// /// Either returns the existing mapped to the given method @@ -311,9 +335,34 @@ public static DbFunction AddDbFunction([NotNull] this IMutableModel model, [NotN /// The . public static IConventionDbFunction AddDbFunction( [NotNull] this IConventionModel model, [NotNull] MethodInfo methodInfo, bool fromDataAnnotation = false) - => new DbFunction( - Check.NotNull(methodInfo, nameof(methodInfo)), (IMutableModel)model, - fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + => DbFunction.AddDbFunction( + (IMutableModel)model, Check.NotNull(methodInfo, nameof(methodInfo)), + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// Either returns the existing mapped to the given method + /// or creates a new function mapped to the method. + /// + /// The model to add the function to. + /// The model name of the function. + /// The . + public static DbFunction AddDbFunction([NotNull] this IMutableModel model, [NotNull] string name) + => DbFunction.AddDbFunction( + model, Check.NotNull(name, nameof(name)), ConfigurationSource.Explicit); + + /// + /// Either returns the existing mapped to the given method + /// or creates a new function mapped to the method. + /// + /// The model to add the function to. + /// The model name of the function. + /// Indicates whether the configuration was specified using a data annotation. + /// The . + public static IConventionDbFunction AddDbFunction( + [NotNull] this IConventionModel model, [NotNull] string name, bool fromDataAnnotation = false) + => DbFunction.AddDbFunction( + (IMutableModel)model, Check.NotNull(name, nameof(name)), + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// /// Removes the that is mapped to the method represented by the given @@ -323,14 +372,9 @@ public static IConventionDbFunction AddDbFunction( /// The for the method that is mapped to the function. /// The removed or null if the method is not mapped. public static IMutableDbFunction RemoveDbFunction([NotNull] this IMutableModel model, [NotNull] MethodInfo method) - { - Check.NotNull(model, nameof(model)); - Check.NotNull(method, nameof(method)); - - return DbFunction.RemoveDbFunction( + => DbFunction.RemoveDbFunction( Check.NotNull(model, nameof(model)), Check.NotNull(method, nameof(method))); - } /// /// Removes the that is mapped to the method represented by the given @@ -342,12 +386,34 @@ public static IMutableDbFunction RemoveDbFunction([NotNull] this IMutableModel m public static IConventionDbFunction RemoveDbFunction([NotNull] this IConventionModel model, [NotNull] MethodInfo method) => (IConventionDbFunction)((IMutableModel)model).RemoveDbFunction(method); + /// + /// Removes the that is mapped to the method represented by the given + /// . + /// + /// The model to find the function in. + /// The model name of the function. + /// The removed or null if the method is not mapped. + public static IMutableDbFunction RemoveDbFunction([NotNull] this IMutableModel model, [NotNull] string name) + => DbFunction.RemoveDbFunction( + Check.NotNull(model, nameof(model)), + Check.NotNull(name, nameof(name))); + + /// + /// Removes the that is mapped to the method represented by the given + /// . + /// + /// The model to find the function in. + /// The model name of the function. + /// The removed or null if the method is not mapped. + public static IConventionDbFunction RemoveDbFunction([NotNull] this IConventionModel model, [NotNull] string name) + => (IConventionDbFunction)((IMutableModel)model).RemoveDbFunction(name); + /// /// Returns all s contained in the model. /// /// The model to get the functions in. public static IEnumerable GetDbFunctions([NotNull] this IModel model) - => DbFunction.GetDbFunctions(model.AsModel()); + => DbFunction.GetDbFunctions(model); /// /// Returns all s contained in the model. diff --git a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs index f651dc690d9..c4e41b7a835 100644 --- a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs +++ b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs @@ -87,8 +87,8 @@ public override ConventionSet CreateConventionSet() var dbFunctionAttributeConvention = new RelationalDbFunctionAttributeConvention(Dependencies, RelationalDependencies); conventionSet.ModelInitializedConventions.Add(dbFunctionAttributeConvention); - conventionSet.ModelAnnotationChangedConventions.Add(dbFunctionAttributeConvention); + conventionSet.ModelFinalizingConventions.Add(dbFunctionAttributeConvention); conventionSet.ModelFinalizingConventions.Add(tableNameFromDbSetConvention); conventionSet.ModelFinalizingConventions.Add(storeGenerationConvention); conventionSet.ModelFinalizingConventions.Add(new SharedTableConvention(Dependencies, RelationalDependencies)); diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalDbFunctionAttributeConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalDbFunctionAttributeConvention.cs index e702e012e8f..b6ab6f2cb5d 100644 --- a/src/EFCore.Relational/Metadata/Conventions/RelationalDbFunctionAttributeConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalDbFunctionAttributeConvention.cs @@ -1,13 +1,11 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Linq; using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; -using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Metadata.Conventions { @@ -15,7 +13,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions /// A convention that configures model function mappings based on public static methods on the context marked with /// . /// - public class RelationalDbFunctionAttributeConvention : IModelInitializedConvention, IModelAnnotationChangedConvention + public class RelationalDbFunctionAttributeConvention : IModelInitializedConvention, IModelFinalizingConvention { /// /// Creates a new instance of . @@ -63,29 +61,12 @@ public virtual void ProcessModelInitialized( } } - /// - /// Called after an annotation is changed on an model. - /// - /// The builder for the model. - /// The annotation name. - /// The new annotation. - /// The old annotation. - /// Additional information associated with convention execution. - public virtual void ProcessModelAnnotationChanged( - IConventionModelBuilder modelBuilder, - string name, - IConventionAnnotation annotation, - IConventionAnnotation oldAnnotation, - IConventionContext context) + /// + public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - Check.NotNull(name, nameof(name)); - - if (name.StartsWith(RelationalAnnotationNames.DbFunction, StringComparison.Ordinal) - && annotation?.Value != null - && oldAnnotation == null) + foreach (var function in modelBuilder.Metadata.GetDbFunctions()) { - ProcessDbFunctionAdded(new DbFunctionBuilder((IMutableDbFunction)annotation.Value), context); + ProcessDbFunctionAdded(function.Builder, context); } } @@ -98,10 +79,15 @@ protected virtual void ProcessDbFunctionAdded( [NotNull] IConventionDbFunctionBuilder dbFunctionBuilder, [NotNull] IConventionContext context) { var methodInfo = dbFunctionBuilder.Metadata.MethodInfo; - var dbFunctionAttribute = methodInfo.GetCustomAttributes().SingleOrDefault(); - - dbFunctionBuilder.HasName(dbFunctionAttribute?.Name ?? methodInfo.Name); - dbFunctionBuilder.HasSchema(dbFunctionAttribute?.Schema); + var dbFunctionAttribute = methodInfo?.GetCustomAttributes().SingleOrDefault(); + if (dbFunctionAttribute != null) + { + dbFunctionBuilder.HasName(dbFunctionAttribute.Name, fromDataAnnotation: true); + if (dbFunctionAttribute.Schema != null) + { + dbFunctionBuilder.HasSchema(dbFunctionAttribute.Schema, fromDataAnnotation: true); + } + } } } } diff --git a/src/EFCore.Relational/Metadata/IDbFunction.cs b/src/EFCore.Relational/Metadata/IDbFunction.cs index 8b129911289..201f5611cb5 100644 --- a/src/EFCore.Relational/Metadata/IDbFunction.cs +++ b/src/EFCore.Relational/Metadata/IDbFunction.cs @@ -24,6 +24,11 @@ public interface IDbFunction /// string Schema { get; } + /// + /// The name of the function in the model. + /// + string ModelName { get; } + /// /// The in which this function is defined. /// diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs index 9a503831b20..c31d0ae5231 100644 --- a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs +++ b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs @@ -26,7 +26,6 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal public class DbFunction : IMutableDbFunction, IConventionDbFunction { private readonly IMutableModel _model; - private readonly string _annotationName; private readonly List _parameters; private string _schema; private string _name; @@ -34,6 +33,7 @@ public class DbFunction : IMutableDbFunction, IConventionDbFunction private RelationalTypeMapping _typeMapping; private Func, SqlExpression> _translation; + private ConfigurationSource _configurationSource; private ConfigurationSource? _schemaConfigurationSource; private ConfigurationSource? _nameConfigurationSource; private ConfigurationSource? _storeTypeConfigurationSource; @@ -51,9 +51,6 @@ public DbFunction( [NotNull] IMutableModel model, ConfigurationSource configurationSource) { - Check.NotNull(methodInfo, nameof(methodInfo)); - Check.NotNull(model, nameof(model)); - if (methodInfo.IsGenericMethod) { throw new ArgumentException(RelationalStrings.DbFunctionGenericMethodNotSupported(methodInfo.DisplayName())); @@ -91,30 +88,115 @@ public DbFunction( MethodInfo = methodInfo; - _model = model; + var parameters = methodInfo.GetParameters(); - _parameters = methodInfo.GetParameters() + _parameters = parameters .Select((pi, i) => new DbFunctionParameter(this, pi.Name, pi.ParameterType)) .ToList(); - _annotationName = BuildAnnotationName(methodInfo); - if (configurationSource == ConfigurationSource.Explicit) + ModelName = GetFunctionName(methodInfo, parameters); + + _model = model; + _configurationSource = configurationSource; + Builder = new DbFunctionBuilder(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 DbFunction( + [NotNull] string name, + [NotNull] IMutableModel model, + ConfigurationSource configurationSource) + { + ModelName = name; + _parameters = new List(); + _model = model; + _configurationSource = configurationSource; + Builder = new DbFunctionBuilder(this); + } + + private static string GetFunctionName(MethodInfo methodInfo, ParameterInfo[] parameters) + => methodInfo.DeclaringType.FullName + "." + methodInfo.Name + + "(" + string.Join(",", parameters.Select(p => p.ParameterType.FullName)) + ")"; + + /// + /// The builder that can be used to configure this function. + /// + public virtual DbFunctionBuilder Builder { get; private 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 static IEnumerable GetDbFunctions([NotNull] IModel model) + => ((SortedDictionary)model[RelationalAnnotationNames.DbFunctions]) + ?.Values ?? Enumerable.Empty(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static DbFunction FindDbFunction( + [NotNull] IModel model, + [NotNull] MethodInfo methodInfo) + { + var functions = (SortedDictionary)model[RelationalAnnotationNames.DbFunctions]; + if (functions == null) { - _model.AddAnnotation(_annotationName, this); + return null; } - else + + var parameters = methodInfo.GetParameters(); + var name = GetFunctionName(methodInfo, parameters); + + if (functions.TryGetValue(name, out var dbFunction)) { - ((IConventionModel)_model).AddAnnotation( - _annotationName, - this, - configurationSource == ConfigurationSource.DataAnnotation); + return dbFunction; } + + if (parameters.Any(p => p.ParameterType.IsGenericType && p.ParameterType.GetGenericTypeDefinition() == typeof(Expression<>))) + { + var parameterTypes = parameters.Select(p => p.ParameterType.IsGenericType + && p.ParameterType.GetGenericTypeDefinition() == typeof(Expression<>) + && p.ParameterType.GetGenericArguments()[0].GetGenericTypeDefinition() == typeof(Func<>) + ? p.ParameterType.GetGenericArguments()[0].GetGenericArguments()[0] + : p.ParameterType).ToArray(); + + var nonExpressionMethod = methodInfo.DeclaringType.GetMethod(methodInfo.Name, parameterTypes); + + if (functions.TryGetValue(GetFunctionName(nonExpressionMethod, nonExpressionMethod.GetParameters()), out dbFunction)) + { + return dbFunction; + } + } + + return null; } /// - /// The builder that can be used to configure this function. + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 IConventionDbFunctionBuilder Builder => new DbFunctionBuilder(this); + public static DbFunction FindDbFunction( + [NotNull] IModel model, + [NotNull] string name) + { + var functions = (SortedDictionary)model[RelationalAnnotationNames.DbFunctions]; + return functions == null + || !functions.TryGetValue(name, out var dbFunction) + ? null + : dbFunction; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -122,21 +204,110 @@ public DbFunction( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public static IEnumerable GetDbFunctions([NotNull] Model model) + public static DbFunction AddDbFunction( + [NotNull] IMutableModel model, [NotNull] MethodInfo methodInfo, ConfigurationSource configurationSource) { - Check.NotNull(model, nameof(model)); + var function = new DbFunction(methodInfo, model, configurationSource); - return model.GetAnnotations() - .Where(a => a.Name.StartsWith(RelationalAnnotationNames.DbFunction, StringComparison.Ordinal)) - .Select(a => a.Value) - .Cast(); + GetOrCreateFunctions(model).Add(function.ModelName, function); + return function; } - private static string BuildAnnotationName(MethodBase methodBase) - => - // ReSharper disable once AssignNullToNotNullAttribute - // ReSharper disable once PossibleNullReferenceException - $"{RelationalAnnotationNames.DbFunction}{methodBase.DeclaringType.FullName}{methodBase.Name}({string.Join(",", methodBase.GetParameters().Select(p => p.ParameterType.FullName))})"; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static DbFunction AddDbFunction( + [NotNull] IMutableModel model, + [NotNull] string name, + ConfigurationSource configurationSource) + { + var function = new DbFunction(name, model, configurationSource); + + GetOrCreateFunctions(model).Add(name, function); + return function; + } + + private static SortedDictionary GetOrCreateFunctions(IMutableModel model) + { + var functions = (SortedDictionary)model[RelationalAnnotationNames.DbFunctions]; + if (functions == null) + { + functions = new SortedDictionary(); + model[RelationalAnnotationNames.DbFunctions] = functions; + } + + return functions; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static DbFunction RemoveDbFunction( + [NotNull] IMutableModel model, + [NotNull] MethodInfo methodInfo) + { + var functions = (SortedDictionary)model[RelationalAnnotationNames.DbFunctions]; + if (functions == null) + { + return null; + } + + var name = GetFunctionName(methodInfo, methodInfo.GetParameters()); + if (!functions.TryGetValue(name, out var function)) + { + return null; + } + + functions.Remove(name); + function.Builder = null; + + return function; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static DbFunction RemoveDbFunction( + [NotNull] IMutableModel model, + [NotNull] string name) + { + var functions = (SortedDictionary)model[RelationalAnnotationNames.DbFunctions]; + if (functions == null + || !functions.TryGetValue(name, out var function)) + { + return null; + } + + functions.Remove(name); + function.Builder = null; + + return function; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 ModelName { get; private 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 MethodInfo MethodInfo { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -145,7 +316,7 @@ private static string BuildAnnotationName(MethodBase methodBase) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual ConfigurationSource GetConfigurationSource() - => ((IConventionModel)_model).FindAnnotation(_annotationName).GetConfigurationSource(); + => _configurationSource; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -154,7 +325,7 @@ public virtual ConfigurationSource GetConfigurationSource() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual void UpdateConfigurationSource(ConfigurationSource configurationSource) - => ((Model)_model).FindAnnotation(_annotationName).UpdateConfigurationSource(configurationSource); + => _configurationSource = configurationSource.Max(_configurationSource); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -178,12 +349,11 @@ public virtual void SetSchema([CanBeNull] string schema, ConfigurationSource con { _schema = schema; - UpdateSchemaConfigurationSource(configurationSource); + _schemaConfigurationSource = schema == null + ? (ConfigurationSource?)null + : configurationSource.Max(_schemaConfigurationSource); } - private void UpdateSchemaConfigurationSource(ConfigurationSource configurationSource) - => _schemaConfigurationSource = configurationSource.Max(_schemaConfigurationSource); - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -200,7 +370,7 @@ private void UpdateSchemaConfigurationSource(ConfigurationSource configurationSo /// public virtual string Name { - get => _name ?? MethodInfo.Name; + get => _name ?? MethodInfo?.Name ?? ModelName; set => SetName(value, ConfigurationSource.Explicit); } @@ -210,18 +380,15 @@ public virtual string Name /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual void SetName([NotNull] string name, ConfigurationSource configurationSource) + public virtual void SetName([CanBeNull] string name, ConfigurationSource configurationSource) { - Check.NotNull(name, nameof(name)); - _name = name; - UpdateNameConfigurationSource(configurationSource); + _nameConfigurationSource = name == null + ? (ConfigurationSource?)null + : configurationSource.Max(_nameConfigurationSource); } - private void UpdateNameConfigurationSource(ConfigurationSource configurationSource) - => _nameConfigurationSource = configurationSource.Max(_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 @@ -230,14 +397,6 @@ private void UpdateNameConfigurationSource(ConfigurationSource configurationSour /// 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 MethodInfo MethodInfo { 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 @@ -256,18 +415,15 @@ public virtual string StoreType /// any release. You should only use it directly in 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 SetStoreType([NotNull] string storeType, ConfigurationSource configurationSource) + public virtual void SetStoreType([CanBeNull] string storeType, ConfigurationSource configurationSource) { - Check.NotNull(storeType, nameof(storeType)); - _storeType = storeType; - UpdateStoreTypeConfigurationSource(configurationSource); + _storeTypeConfigurationSource = storeType == null + ? (ConfigurationSource?)null + : configurationSource.Max(_storeTypeConfigurationSource); } - private void UpdateStoreTypeConfigurationSource(ConfigurationSource configurationSource) - => _storeTypeConfigurationSource = configurationSource.Max(_storeTypeConfigurationSource); - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -298,12 +454,11 @@ public virtual void SetTypeMapping([CanBeNull] RelationalTypeMapping typeMapping { _typeMapping = typeMapping; - UpdateTypeMappingConfigurationSource(configurationSource); + _typeMappingConfigurationSource = typeMapping == null + ? (ConfigurationSource?)null + : configurationSource.Max(_typeMappingConfigurationSource); } - private void UpdateTypeMappingConfigurationSource(ConfigurationSource configurationSource) - => _typeMappingConfigurationSource = configurationSource.Max(_typeMappingConfigurationSource); - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -324,14 +479,6 @@ public virtual Func, SqlExpression> Translati set => SetTranslation(value, ConfigurationSource.Explicit); } - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual bool IsIQueryable { 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 @@ -344,12 +491,11 @@ public virtual void SetTranslation( { _translation = translation; - UpdateTranslationConfigurationSource(configurationSource); + _translationConfigurationSource = translation == null + ? (ConfigurationSource?)null + : configurationSource.Max(_translationConfigurationSource); } - private void UpdateTranslationConfigurationSource(ConfigurationSource configurationSource) - => _translationConfigurationSource = configurationSource.Max(_translationConfigurationSource); - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -364,30 +510,7 @@ private void UpdateTranslationConfigurationSource(ConfigurationSource configurat /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public static DbFunction FindDbFunction( - [NotNull] IModel model, - [NotNull] MethodInfo methodInfo) - { - var dbFunction = model[BuildAnnotationName(methodInfo)] as DbFunction; - - if (dbFunction == null - && methodInfo.GetParameters().Any(p => p.ParameterType.IsGenericType && p.ParameterType.GetGenericTypeDefinition() == typeof(Expression<>))) - { - var parameters = methodInfo.GetParameters().Select(p => p.ParameterType.IsGenericType - && p.ParameterType.GetGenericTypeDefinition() == typeof(Expression<>) - && p.ParameterType.GetGenericArguments()[0].GetGenericTypeDefinition() == typeof(Func<>) - ? p.ParameterType.GetGenericArguments()[0].GetGenericArguments()[0] - : p.ParameterType).ToArray(); - - var nonExpressionMethod = methodInfo.DeclaringType.GetMethod(methodInfo.Name, parameters); - - dbFunction = nonExpressionMethod != null - ? model[BuildAnnotationName(nonExpressionMethod)] as DbFunction - : null; - } - - return dbFunction; - } + public virtual bool IsIQueryable { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -395,10 +518,7 @@ public static DbFunction FindDbFunction( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public static DbFunction RemoveDbFunction( - [NotNull] IMutableModel model, - [NotNull] MethodInfo methodInfo) - => model.RemoveAnnotation(BuildAnnotationName(methodInfo))?.Value as DbFunction; + IConventionDbFunctionBuilder IConventionDbFunction.Builder => Builder; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs b/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs index b308570bd54..b84102dff3c 100644 --- a/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs +++ b/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs @@ -41,6 +41,7 @@ public DbFunctionParameter([NotNull] IMutableDbFunction function, [NotNull] stri _name = name; _function = function; _clrType = clrType; + Builder = new DbFunctionParameterBuilder(this); } /// @@ -49,7 +50,7 @@ public DbFunctionParameter([NotNull] IMutableDbFunction function, [NotNull] stri /// any release. You should only use it directly in 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 IConventionDbFunctionParameterBuilder Builder => new DbFunctionParameterBuilder(this); + public virtual IConventionDbFunctionParameterBuilder Builder { get; private set; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Metadata/Internal/Sequence.cs b/src/EFCore.Relational/Metadata/Internal/Sequence.cs index f88dde18f3b..1511924b375 100644 --- a/src/EFCore.Relational/Metadata/Internal/Sequence.cs +++ b/src/EFCore.Relational/Metadata/Internal/Sequence.cs @@ -23,14 +23,14 @@ public class Sequence : IMutableSequence, IConventionSequence { private readonly IModel _model; - private string _name { get; set; } - private string _schema { get; set; } - private long? _startValue { get; set; } - private int? _incrementBy { get; set; } - private long? _minValue { get; set; } - private long? _maxValue { get; set; } - private Type _clrType { get; set; } - private bool? _isCyclic { get; set; } + private readonly string _name; + private readonly string _schema; + private long? _startValue; + private int? _incrementBy; + private long? _minValue; + private long? _maxValue; + private Type _clrType; + private bool? _isCyclic; private ConfigurationSource _configurationSource; private ConfigurationSource? _startValueConfigurationSource; @@ -95,9 +95,9 @@ public class Sequence : IMutableSequence, IConventionSequence /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public Sequence( - [NotNull] IMutableModel model, [NotNull] string name, [CanBeNull] string schema, + [NotNull] IModel model, ConfigurationSource configurationSource) { Check.NotEmpty(name, nameof(name)); @@ -107,6 +107,7 @@ public Sequence( _name = name; _schema = schema; _configurationSource = configurationSource; + Builder = new SequenceBuilder(this); } /// @@ -133,6 +134,7 @@ public Sequence([NotNull] IModel model, [NotNull] string annotationName) _maxValue = data.MaxValue; _clrType = data.ClrType; _isCyclic = data.IsCyclic; + Builder = new SequenceBuilder(this); } /// @@ -172,7 +174,7 @@ public static Sequence FindSequence([NotNull] IModel model, [NotNull] string nam public static Sequence AddSequence( [NotNull] IMutableModel model, [NotNull] string name, [CanBeNull] string schema, ConfigurationSource configurationSource) { - var sequence = new Sequence(model, name, schema, configurationSource); + var sequence = new Sequence(name, schema, model, configurationSource); var sequences = (SortedDictionary<(string, string), Sequence>)model[RelationalAnnotationNames.Sequences]; if (sequences == null) { @@ -190,7 +192,7 @@ public static Sequence AddSequence( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public static IMutableSequence RemoveSequence([NotNull] IMutableModel model, [NotNull] string name, [CanBeNull] string schema) + public static Sequence RemoveSequence([NotNull] IMutableModel model, [NotNull] string name, [CanBeNull] string schema) { var sequences = (SortedDictionary<(string, string), Sequence>)model[RelationalAnnotationNames.Sequences]; if (sequences == null @@ -200,6 +202,7 @@ public static IMutableSequence RemoveSequence([NotNull] IMutableModel model, [No } sequences.Remove((name, schema)); + sequence.Builder = null; return sequence; } @@ -210,7 +213,7 @@ public static IMutableSequence RemoveSequence([NotNull] IMutableModel model, [No /// any release. You should only use it directly in 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 => (IMutableModel)_model; + public virtual SequenceBuilder Builder { get; private set; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -218,7 +221,7 @@ public static IMutableSequence RemoveSequence([NotNull] IMutableModel model, [No /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - IConventionSequenceBuilder IConventionSequence.Builder => new SequenceBuilder(this); + public virtual IModel Model => _model; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -491,7 +494,7 @@ public virtual void UpdateConfigurationSource(ConfigurationSource configurationS /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - IConventionModel IConventionSequence.Model => (IConventionModel)Model; + IConventionSequenceBuilder IConventionSequence.Builder => Builder; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -499,7 +502,15 @@ public virtual void UpdateConfigurationSource(ConfigurationSource configurationS /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - IModel ISequence.Model => _model; + IMutableModel IMutableSequence.Model => (IMutableModel)Model; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + IConventionModel IConventionSequence.Model => (IConventionModel)Model; /// /// 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 0dd7869292a..432e16386ac 100644 --- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs +++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs @@ -105,8 +105,14 @@ public static class RelationalAnnotationNames /// /// The name for DbFunction annotation. /// + [Obsolete("Use DbFunctions")] public const string DbFunction = Prefix + "DbFunction"; + /// + /// The name for functions annotation. + /// + public const string DbFunctions = Prefix + "DbFunctions"; + /// /// The name for the annotation containing the maximum length for database identifiers. /// diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs index af7ac45e953..17aa764d516 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs @@ -71,7 +71,7 @@ public void Test_new_annotations_handled_for_entity_types() RelationalAnnotationNames.CheckConstraints, RelationalAnnotationNames.DefaultSchema, RelationalAnnotationNames.Filter, - RelationalAnnotationNames.DbFunction, + RelationalAnnotationNames.DbFunctions, RelationalAnnotationNames.MaxIdentifierLength, RelationalAnnotationNames.IsFixedLength }; @@ -157,7 +157,7 @@ public void Test_new_annotations_handled_for_properties() RelationalAnnotationNames.Sequences, RelationalAnnotationNames.CheckConstraints, RelationalAnnotationNames.Filter, - RelationalAnnotationNames.DbFunction, + RelationalAnnotationNames.DbFunctions, RelationalAnnotationNames.MaxIdentifierLength }; diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 6e0f7dea40a..4e8f9001968 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -1013,7 +1013,7 @@ public virtual void Detects_ToTable_on_derived_entity_types() } [ConditionalFact] - public void Detects_function_with_invalid_return_type_throws() + public void Detects_function_with_invalid_return_type() { var modelBuilder = CreateConventionalModelBuilder(); diff --git a/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs b/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs index 98ed0c600a4..6329cc4dd6b 100644 --- a/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs +++ b/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs @@ -279,6 +279,8 @@ var dup2methodInfo var dbFunc1 = modelBuilder.HasDbFunction(dup1methodInfo).HasName("Dup1").Metadata; var dbFunc2 = modelBuilder.HasDbFunction(dup2methodInfo).HasName("Dup2").Metadata; + modelBuilder.FinalizeModel(); + Assert.Equal("Dup1", dbFunc1.Name); Assert.Equal("Dup2", dbFunc2.Name); } @@ -289,6 +291,8 @@ public virtual void Finds_dbFunctions_on_dbContext() var context = new MyDerivedContext(); var modelBuilder = GetModelBuilder(context); + modelBuilder.FinalizeModel(); + foreach (var function in MyBaseContext.FunctionNames) { Assert.NotNull( @@ -317,6 +321,8 @@ var methodInfo var dbFunc = modelBuilder.HasDbFunction(methodInfo).Metadata; + modelBuilder.FinalizeModel(); + Assert.Equal("InstancePublicBase", dbFunc.Name); Assert.Equal(typeof(int), dbFunc.MethodInfo.ReturnType); } @@ -355,6 +361,8 @@ public void Adding_method_fluent_only_convention_defaults() var dbFuncBuilder = modelBuilder.HasDbFunction(MethodAmi); var dbFunc = dbFuncBuilder.Metadata; + modelBuilder.FinalizeModel(); + Assert.Equal("MethodA", dbFunc.Name); Assert.Null(dbFunc.Schema); Assert.Equal(typeof(int), dbFunc.MethodInfo.ReturnType); @@ -368,6 +376,8 @@ public void Adding_method_fluent_only_convention_defaults_fluent_method_info() var dbFuncBuilder = modelBuilder.HasDbFunction(() => TestMethods.MethodA(null, default)); var dbFunc = dbFuncBuilder.Metadata; + modelBuilder.FinalizeModel(); + Assert.Equal("MethodA", dbFunc.Name); Assert.Null(dbFunc.Schema); Assert.Equal(typeof(int), dbFunc.MethodInfo.ReturnType); @@ -408,6 +418,8 @@ public void Adding_method_fluent_only_with_name_schema() var dbFunc = dbFuncBuilder.Metadata; + modelBuilder.FinalizeModel(); + Assert.Equal("foo", dbFunc.Name); Assert.Equal("bar", dbFunc.Schema); Assert.Equal(typeof(int), dbFunc.MethodInfo.ReturnType); @@ -422,6 +434,8 @@ public void Adding_method_fluent_only_with_builder() var dbFunc = modelBuilder.HasDbFunction(MethodAmi).Metadata; + modelBuilder.FinalizeModel(); + Assert.Equal("foo", dbFunc.Name); Assert.Equal("bar", dbFunc.Schema); Assert.Equal(typeof(int), dbFunc.MethodInfo.ReturnType); @@ -435,6 +449,8 @@ public void Adding_method_with_attribute_only() var dbFuncBuilder = modelBuilder.HasDbFunction(MethodBmi); var dbFunc = dbFuncBuilder.Metadata; + modelBuilder.FinalizeModel(); + Assert.Equal("MethodFoo", dbFunc.Name); Assert.Equal("bar", dbFunc.Schema); Assert.Equal(typeof(int), dbFunc.MethodInfo.ReturnType); @@ -451,6 +467,8 @@ public void Adding_method_with_attribute_and_fluent_api_configuration_source() var dbFunc = dbFuncBuilder.Metadata; + modelBuilder.FinalizeModel(); + Assert.Equal("foo", dbFunc.Name); Assert.Equal("bar", dbFunc.Schema); Assert.Equal(typeof(int), dbFunc.MethodInfo.ReturnType); @@ -465,6 +483,8 @@ public void Adding_method_with_attribute_and_fluent_configuration_source() var dbFunc = modelBuilder.HasDbFunction(MethodBmi).Metadata; + modelBuilder.FinalizeModel(); + Assert.Equal("foo", dbFunc.Name); Assert.Equal("bar", dbFunc.Schema); Assert.Equal(typeof(int), dbFunc.MethodInfo.ReturnType); @@ -479,6 +499,8 @@ public void Adding_method_with_relational_schema() var dbFuncBuilder = modelBuilder.HasDbFunction(MethodAmi); + modelBuilder.FinalizeModel(); + Assert.Equal("dbo", dbFuncBuilder.Metadata.Schema); } @@ -489,6 +511,8 @@ public void Adding_method_with_store_type() var dbFuncBuilder = modelBuilder.HasDbFunction(MethodAmi).HasStoreType("int(8)"); + modelBuilder.FinalizeModel(); + Assert.Equal("int(8)", dbFuncBuilder.Metadata.StoreType); } @@ -501,6 +525,8 @@ public void Adding_method_with_relational_schema_fluent_overrides() var dbFuncBuilder = modelBuilder.HasDbFunction(MethodAmi).HasSchema("bar"); + modelBuilder.FinalizeModel(); + Assert.Equal("bar", dbFuncBuilder.Metadata.Schema); } @@ -513,6 +539,8 @@ public void Adding_method_with_relational_schema_attribute_overrides() var dbFuncBuilder = modelBuilder.HasDbFunction(MethodBmi); + modelBuilder.FinalizeModel(); + Assert.Equal("bar", dbFuncBuilder.Metadata.Schema); } @@ -529,6 +557,8 @@ public void Changing_default_schema_is_detected_by_dbfunction() modelBuilder.HasDefaultSchema("xyz"); + modelBuilder.FinalizeModel(); + Assert.Equal("xyz", dbFuncBuilder.Metadata.Schema); } @@ -561,6 +591,8 @@ public void DbParameters_load_no_parameters() var dbFuncBuilder = modelBuilder.HasDbFunction(MethodImi); var dbFunc = dbFuncBuilder.Metadata; + modelBuilder.FinalizeModel(); + Assert.Equal(0, dbFunc.Parameters.Count); } @@ -584,6 +616,8 @@ public void DbParameters_load_with_parameters() var dbFuncBuilder = modelBuilder.HasDbFunction(MethodBmi); var dbFunc = dbFuncBuilder.Metadata; + modelBuilder.FinalizeModel(); + Assert.Equal(2, dbFunc.Parameters.Count); Assert.Equal("c", dbFunc.Parameters[0].Name); @@ -603,6 +637,8 @@ public void DbParameters_dbfunctionType() dbFuncBuilder.HasParameter("c"); + modelBuilder.FinalizeModel(); + Assert.Equal(2, dbFunc.Parameters.Count); Assert.Equal("c", dbFunc.Parameters[0].Name); @@ -622,6 +658,8 @@ public void DbParameters_name() dbFuncBuilder.HasParameter("c"); + modelBuilder.FinalizeModel(); + Assert.Equal(2, dbFunc.Parameters.Count); Assert.Equal("c", dbFunc.Parameters[0].Name); @@ -641,6 +679,8 @@ public void DbParameters_StoreType() dbFuncBuilder.HasParameter("c").HasStoreType("varchar(max)"); + modelBuilder.FinalizeModel(); + Assert.Equal(2, dbFunc.Parameters.Count); Assert.Equal("c", dbFunc.Parameters[0].Name); @@ -664,6 +704,8 @@ public void DbFunction_Annotation_FullName() funcA.HasName("MinA"); + modelBuilder.FinalizeModel(); + Assert.Equal("MinA", funcA.Metadata.Name); Assert.Equal("Min", funcB.Metadata.Name); Assert.NotEqual(funcA.Metadata.Name, funcB.Metadata.Name); @@ -677,6 +719,8 @@ public void Find_Queryable_Single_Expression_Overload() var funcA = modelBuilder.HasDbFunction(typeof(MyDerivedContext).GetMethod(nameof(MyDerivedContext.QueryableSingleParam), new Type[] { typeof(int) })); var funcB = modelBuilder.HasDbFunction(typeof(MyDerivedContext).GetMethod(nameof(MyDerivedContext.QueryableSingleParam), new Type[] { typeof(Expression>) })); + modelBuilder.FinalizeModel(); + Assert.Equal("QueryableSingleParam", funcA.Metadata.Name); Assert.Equal("QueryableSingleParam", funcB.Metadata.Name); Assert.Equal(funcA.Metadata, funcB.Metadata); @@ -691,6 +735,8 @@ public void Find_Queryable_Multiple_Expression_Overload() var funcB = modelBuilder.HasDbFunction(typeof(MyDerivedContext).GetMethod(nameof(MyDerivedContext.QueryableMultiParam), new Type[] { typeof(Expression>), typeof(double) })); var funcC = modelBuilder.HasDbFunction(typeof(MyDerivedContext).GetMethod(nameof(MyDerivedContext.QueryableMultiParam), new Type[] { typeof(Expression>), typeof(Expression>) })); + modelBuilder.FinalizeModel(); + Assert.Equal("QueryableMultiParam", funcA.Metadata.Name); Assert.Equal("QueryableMultiParam", funcB.Metadata.Name); Assert.Equal("QueryableMultiParam", funcC.Metadata.Name); @@ -709,7 +755,7 @@ private ModelBuilder GetModelBuilder(DbContext dbContext = null) var relationalDependencies = CreateRelationalDependencies(); var dbFunctionAttributeConvention = new RelationalDbFunctionAttributeConvention(dependencies, relationalDependencies); conventionSet.ModelInitializedConventions.Add(dbFunctionAttributeConvention); - conventionSet.ModelAnnotationChangedConventions.Add(dbFunctionAttributeConvention); + conventionSet.ModelFinalizingConventions.Add(dbFunctionAttributeConvention); conventionSet.ModelFinalizingConventions.Add(new DbFunctionTypeMappingConvention(dependencies, relationalDependencies)); return new ModelBuilder(conventionSet); diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs index 2c0a3698ba8..8534ad255f0 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs @@ -411,6 +411,11 @@ public void Can_get_and_set_dbfunction() Assert.NotNull(dbFunc); Assert.NotNull(dbFunc.Name); Assert.Null(dbFunc.Schema); + Assert.NotNull(((IConventionDbFunction)dbFunc).Builder); + + Assert.Same(dbFunc, model.RemoveDbFunction(testMethod)); + + Assert.Null(((IConventionDbFunction)dbFunc).Builder); } [ConditionalFact] @@ -432,8 +437,10 @@ public void Can_get_and_set_sequence() Assert.Null(sequence.MinValue); Assert.Null(sequence.MaxValue); Assert.Same(typeof(long), sequence.ClrType); + Assert.False(sequence.IsCyclic); + Assert.NotNull(((IConventionSequence)sequence).Builder); - var sequence2 = model.FindSequence("Foo"); + Assert.Same(sequence, model.FindSequence("Foo")); sequence.StartValue = 1729; sequence.IncrementBy = 11; @@ -449,13 +456,9 @@ public void Can_get_and_set_sequence() Assert.Equal(2010, sequence.MaxValue); Assert.Same(typeof(int), sequence.ClrType); - Assert.Equal(sequence2.Name, sequence.Name); - Assert.Equal(sequence2.Schema, sequence.Schema); - Assert.Equal(sequence2.IncrementBy, sequence.IncrementBy); - Assert.Equal(sequence2.StartValue, sequence.StartValue); - Assert.Equal(sequence2.MinValue, sequence.MinValue); - Assert.Equal(sequence2.MaxValue, sequence.MaxValue); - Assert.Same(sequence2.ClrType, sequence.ClrType); + Assert.Same(sequence, model.RemoveSequence("Foo")); + + Assert.Null(((IConventionSequence)sequence).Builder); } [ConditionalFact] @@ -479,7 +482,7 @@ public void Can_get_and_set_sequence_with_schema_name() Assert.Null(sequence.MaxValue); Assert.Same(typeof(long), sequence.ClrType); - var sequence2 = model.FindSequence("Foo", "Smoo"); + Assert.Same(sequence, model.FindSequence("Foo", "Smoo")); sequence.StartValue = 1729; sequence.IncrementBy = 11; @@ -494,14 +497,6 @@ public void Can_get_and_set_sequence_with_schema_name() Assert.Equal(2001, sequence.MinValue); Assert.Equal(2010, sequence.MaxValue); Assert.Same(typeof(int), sequence.ClrType); - - Assert.Equal(sequence2.Name, sequence.Name); - Assert.Equal(sequence2.Schema, sequence.Schema); - Assert.Equal(sequence2.IncrementBy, sequence.IncrementBy); - Assert.Equal(sequence2.StartValue, sequence.StartValue); - Assert.Equal(sequence2.MinValue, sequence.MinValue); - Assert.Equal(sequence2.MaxValue, sequence.MaxValue); - Assert.Same(sequence2.ClrType, sequence.ClrType); } [ConditionalFact] @@ -515,7 +510,7 @@ public void Sequence_is_in_model_schema_if_schema_not_specified() var sequence = model.AddSequence("Foo"); - Assert.Equal("Foo", model.FindSequence("Foo").Name); + Assert.Same(sequence, model.FindSequence("Foo")); Assert.Equal("Foo", sequence.Name); Assert.Equal("Smoo", sequence.Schema); @@ -536,7 +531,7 @@ public void Returns_same_sequence_if_schema_not_specified_explicitly() var sequence = model.AddSequence("Foo"); - Assert.Equal("Foo", model.FindSequence("Foo").Name); + Assert.Same(sequence, model.FindSequence("Foo")); Assert.Equal("Foo", sequence.Name); Assert.Null(sequence.Schema); @@ -548,7 +543,7 @@ public void Returns_same_sequence_if_schema_not_specified_explicitly() model.SetDefaultSchema("Smoo"); - var sequence2 = model.FindSequence("Foo"); + Assert.Same(sequence, model.FindSequence("Foo")); sequence.StartValue = 1729; sequence.IncrementBy = 11; @@ -563,14 +558,6 @@ public void Returns_same_sequence_if_schema_not_specified_explicitly() Assert.Equal(2001, sequence.MinValue); Assert.Equal(2010, sequence.MaxValue); Assert.Same(typeof(int), sequence.ClrType); - - Assert.Equal(sequence2.Name, sequence.Name); - Assert.Equal(sequence2.Schema, sequence.Schema); - Assert.Equal(sequence2.IncrementBy, sequence.IncrementBy); - Assert.Equal(sequence2.StartValue, sequence.StartValue); - Assert.Equal(sequence2.MinValue, sequence.MinValue); - Assert.Equal(sequence2.MaxValue, sequence.MaxValue); - Assert.Same(sequence2.ClrType, sequence.ClrType); } [ConditionalFact]