diff --git a/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs b/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs index fd26fb76755..a96b8e804d6 100644 --- a/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs @@ -1,6 +1,7 @@ // 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.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -300,21 +301,21 @@ public static IConventionDbFunction RemoveDbFunction([NotNull] this IConventionM /// Returns all s contained in the model. /// /// The model to get the functions in. - public static IReadOnlyList GetDbFunctions([NotNull] this IModel model) - => DbFunction.GetDbFunctions(model).ToList(); + public static IEnumerable GetDbFunctions([NotNull] this IModel model) + => DbFunction.GetDbFunctions((Model)model); /// /// Returns all s contained in the model. /// /// The model to get the functions in. - public static IReadOnlyList GetDbFunctions([NotNull] this IMutableModel model) - => (IReadOnlyList)((IModel)model).GetDbFunctions(); + public static IEnumerable GetDbFunctions([NotNull] this IMutableModel model) + => DbFunction.GetDbFunctions((Model)model); /// /// Returns all s contained in the model. /// /// The model to get the functions in. - public static IReadOnlyList GetDbFunctions([NotNull] this IConventionModel model) - => (IReadOnlyList)((IModel)model).GetDbFunctions(); + public static IEnumerable GetDbFunctions([NotNull] this IConventionModel model) + => DbFunction.GetDbFunctions((Model)model); } } diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index 33b313d8add..ffb336591ff 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -80,26 +80,23 @@ protected virtual void ValidateDbFunctions([NotNull] IModel model, [NotNull] IDi RelationalStrings.DbFunctionNameEmpty(methodInfo.DisplayName())); } - if (dbFunction.Translation == null) + if (dbFunction.TypeMapping == null) { - if (RelationalDependencies.TypeMappingSource.FindMapping(methodInfo.ReturnType) == null) + throw new InvalidOperationException( + RelationalStrings.DbFunctionInvalidReturnType( + methodInfo.DisplayName(), + methodInfo.ReturnType.ShortDisplayName())); + } + + foreach (var parameter in dbFunction.Parameters) + { + if(parameter.TypeMapping == null) { throw new InvalidOperationException( - RelationalStrings.DbFunctionInvalidReturnType( + RelationalStrings.DbFunctionInvalidParameterType( + parameter.Name, methodInfo.DisplayName(), - methodInfo.ReturnType.ShortDisplayName())); - } - - foreach (var parameter in methodInfo.GetParameters()) - { - if (RelationalDependencies.TypeMappingSource.FindMapping(parameter.ParameterType) == null) - { - throw new InvalidOperationException( - RelationalStrings.DbFunctionInvalidParameterType( - parameter.Name, - methodInfo.DisplayName(), - parameter.ParameterType.ShortDisplayName())); - } + parameter.ClrType.ShortDisplayName())); } } } diff --git a/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs b/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs index 66630d134e3..84b00fb732a 100644 --- a/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs @@ -4,11 +4,15 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; using Microsoft.EntityFrameworkCore.Utilities; +using Microsoft.EntityFrameworkCore.Storage; namespace Microsoft.EntityFrameworkCore.Metadata.Builders { @@ -98,6 +102,53 @@ bool IConventionDbFunctionBuilder.CanSetSchema(string schema, bool fromDataAnnot => Overrides(fromDataAnnotation, _function.GetSchemaConfigurationSource()) || _function.Schema == schema; + /// + /// Sets the store type of the database function. + /// + /// The store type of the function in the database. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual DbFunctionBuilder HasStoreType([CanBeNull] string storeType) + { + _function.StoreType = storeType; + + return this; + } + + /// + IConventionDbFunctionBuilder IConventionDbFunctionBuilder.HasStoreType(string storeType, bool fromDataAnnotation) + { + if (((IConventionDbFunctionBuilder)this).CanSetStoreType(storeType, fromDataAnnotation)) + { + ((IConventionDbFunction)_function).SetStoreType(storeType, fromDataAnnotation); + return this; + } + + return null; + } + + /// + bool IConventionDbFunctionBuilder.CanSetStoreType(string storeType, bool fromDataAnnotation) + => Overrides(fromDataAnnotation, _function.GetStoreTypeConfigurationSource()) + || _function.StoreType == storeType; + + /// + IConventionDbFunctionBuilder IConventionDbFunctionBuilder.HasTypeMapping( + RelationalTypeMapping returnTypeMapping, bool fromDataAnnotation) + { + if (((IConventionDbFunctionBuilder)this).CanSetTypeMapping(returnTypeMapping, fromDataAnnotation)) + { + ((IConventionDbFunction)_function).SetTypeMapping(returnTypeMapping, fromDataAnnotation); + return this; + } + + return null; + } + + /// + bool IConventionDbFunctionBuilder.CanSetTypeMapping(RelationalTypeMapping returnTypeMapping, bool fromDataAnnotation) + => Overrides(fromDataAnnotation, _function.GetTypeMappingConfigurationSource()) + || _function.TypeMapping == returnTypeMapping; + /// /// /// Sets a callback that will be invoked to perform custom translation of this @@ -136,6 +187,25 @@ IConventionDbFunctionBuilder IConventionDbFunctionBuilder.HasTranslation( return null; } + public virtual DbFunctionParameterBuilder HasParameter([NotNull] string name) + { + return new DbFunctionParameterBuilder((DbFunctionParameter)FindParameter(name)); + } + + private IDbFunctionParameter FindParameter(string name) + { + var parameter = Metadata.Parameters.SingleOrDefault( + funcParam => string.Compare(funcParam.Name, name, StringComparison.OrdinalIgnoreCase) == 0); + + if (parameter == null) + { + throw new ArgumentException( + RelationalStrings.DbFunctionInvalidParameterName(name, Metadata.MethodInfo.DisplayName())); + } + + return parameter; + } + /// bool IConventionDbFunctionBuilder.CanSetTranslation( Func, SqlExpression> translation, bool fromDataAnnotation) diff --git a/src/EFCore.Relational/Metadata/Builders/DbFunctionParameterBuilder.cs b/src/EFCore.Relational/Metadata/Builders/DbFunctionParameterBuilder.cs new file mode 100644 index 00000000000..b2c8ef9db5e --- /dev/null +++ b/src/EFCore.Relational/Metadata/Builders/DbFunctionParameterBuilder.cs @@ -0,0 +1,150 @@ +// 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.Collections.Generic; +using System.ComponentModel; +using System.Text; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders +{ + /// + /// + /// Provides a simple API for configuring a . + /// + /// + /// 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 DbFunctionParameterBuilder : IConventionDbFunctionParameterBuilder + { + private readonly DbFunctionParameter _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. + /// + [EntityFrameworkInternal] + public DbFunctionParameterBuilder([NotNull] IMutableDbFunctionParameter parameter) + { + Check.NotNull(parameter, nameof(parameter)); + + _parameter = (DbFunctionParameter)parameter; + } + + public virtual IMutableDbFunctionParameter Metadata => _parameter; + + /// + IConventionDbFunctionParameter IConventionDbFunctionParameterBuilder.Metadata => _parameter; + + /// + /// Specify if this parametere supports Nullability Propagation + /// + public virtual DbFunctionParameterBuilder HasNullabilityPropagation(bool supportsNullabilityPropagation) + { + _parameter.SupportsNullPropagation = supportsNullabilityPropagation; + + return this; + } + + IConventionDbFunctionParameterBuilder IConventionDbFunctionParameterBuilder.HasNullPropagation( + bool supportsNullPropagation, bool fromDataAnnotation) + { + if (((IConventionDbFunctionParameterBuilder)this).CanSetSupportsNullPropagation(supportsNullPropagation, fromDataAnnotation)) + { + ((IConventionDbFunctionParameter)_parameter).SetSupportsNullPropagation(supportsNullPropagation, fromDataAnnotation); + return this; + } + + return null; + } + + bool IConventionDbFunctionParameterBuilder.CanSetSupportsNullPropagation(bool supportsNullPropagation, bool fromDataAnnotation) + { + return Overrides(fromDataAnnotation, _parameter.GetSupportsNullPropagationConfigurationSource()) + || _parameter.SupportsNullPropagation == supportsNullPropagation; + } + + public virtual DbFunctionParameterBuilder HasStoreType([CanBeNull] string storeType) + { + _parameter.StoreType = storeType; + + return this; + } + + IConventionDbFunctionParameterBuilder IConventionDbFunctionParameterBuilder.HasStoreType(string storeType, bool fromDataAnnotation) + { + if (((IConventionDbFunctionParameterBuilder)this).CanSetStoreType(storeType, fromDataAnnotation)) + { + ((IConventionDbFunctionParameter)_parameter).SetStoreType(storeType, fromDataAnnotation); + return this; + } + + return null; + } + + bool IConventionDbFunctionParameterBuilder.CanSetStoreType(string storeType, bool fromDataAnnotation) + { + return Overrides(fromDataAnnotation, _parameter.GetStoreTypeConfigurationSource()) + || _parameter.StoreType == storeType; + } + + IConventionDbFunctionParameterBuilder IConventionDbFunctionParameterBuilder.HasTypeMapping( + RelationalTypeMapping typeMapping, bool fromDataAnnotation) + { + if (((IConventionDbFunctionParameterBuilder)this).CanSetTypeMapping(typeMapping, fromDataAnnotation)) + { + ((IConventionDbFunctionParameter)_parameter).SetTypeMapping(typeMapping, fromDataAnnotation); + return this; + } + + return null; + } + + bool IConventionDbFunctionParameterBuilder.CanSetTypeMapping(RelationalTypeMapping typeMapping, bool fromDataAnnotation) + { + return Overrides(fromDataAnnotation, _parameter.GetTypeMappingConfigurationSource()) + || _parameter.TypeMapping == typeMapping; + } + + private bool Overrides(bool fromDataAnnotation, ConfigurationSource? configurationSource) + => (fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) + .Overrides(configurationSource); + + #region Hidden System.Object members + + /// + /// Returns a string that represents the current object. + /// + /// A string that represents the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + public override string ToString() => base.ToString(); + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false. + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectEqualsIsObjectEquals + public override bool Equals(object obj) => base.Equals(obj); + + /// + /// Serves as the default hash function. + /// + /// A hash code for the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode + public override int GetHashCode() => base.GetHashCode(); + + #endregion + } +} diff --git a/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionBuilder.cs b/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionBuilder.cs index b944d3ecc24..70cc4c6b0bd 100644 --- a/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionBuilder.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; namespace Microsoft.EntityFrameworkCore.Metadata.Builders { @@ -56,6 +57,44 @@ public interface IConventionDbFunctionBuilder /// true if the given schema can be set for the database function. bool CanSetSchema([CanBeNull] string schema, bool fromDataAnnotation = false); + /// + /// Sets the store type of the function in the database. + /// + /// The store type of the function in the database. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// null otherwise. + /// + IConventionDbFunctionBuilder HasStoreType([CanBeNull] string storeType, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given store type can be set for the database function. + /// + /// The store type of the function in the database. + /// Indicates whether the configuration was specified using a data annotation. + /// true if the given store type can be set for the database function. + bool CanSetStoreType([CanBeNull] string storeType, bool fromDataAnnotation = false); + + /// + /// Sets the return type mapping of the database function. + /// + /// The return type mapping of the function in the database. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// null otherwise. + /// + IConventionDbFunctionBuilder HasTypeMapping([CanBeNull] RelationalTypeMapping typeMapping, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given return type mapping can be set for the database function. + /// + /// The return type mapping of the function in the database. + /// Indicates whether the configuration was specified using a data annotation. + /// true if the given return type mapping can be set for the database function. + bool CanSetTypeMapping([CanBeNull] RelationalTypeMapping typeMapping, bool fromDataAnnotation = false); + /// /// /// Sets a callback that will be invoked to perform custom translation of this diff --git a/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionParameterBuilder.cs b/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionParameterBuilder.cs new file mode 100644 index 00000000000..5e2056de83a --- /dev/null +++ b/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionParameterBuilder.cs @@ -0,0 +1,28 @@ +// 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 JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Provides a simple API for configuring a . + /// + public interface IConventionDbFunctionParameterBuilder + { + IConventionDbFunctionParameter Metadata { get; } + + IConventionDbFunctionParameterBuilder HasNullPropagation(bool supportsNullPropagation, bool fromDataAnnotation = false); + + bool CanSetSupportsNullPropagation(bool supportsNullPropagation, bool fromDataAnnotation = false); + + IConventionDbFunctionParameterBuilder HasStoreType([CanBeNull] string storeType, bool fromDataAnnotation = false); + + bool CanSetStoreType([CanBeNull] string storeType, bool fromDataAnnotation = false); + + IConventionDbFunctionParameterBuilder HasTypeMapping([CanBeNull] RelationalTypeMapping typeMapping, bool fromDataAnnotation = false); + + bool CanSetTypeMapping([CanBeNull] RelationalTypeMapping typeMapping, bool fromDataAnnotation = false); + } +} diff --git a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs index f0b7ddd88b2..3182a76aef4 100644 --- a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs +++ b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs @@ -84,6 +84,7 @@ public override ConventionSet CreateConventionSet() var dbFunctionAttributeConvention = new RelationalDbFunctionAttributeConvention(Dependencies, RelationalDependencies); conventionSet.ModelInitializedConventions.Add(dbFunctionAttributeConvention); + conventionSet.ModelAnnotationChangedConventions.Add(dbFunctionAttributeConvention); var sharedTableConvention = new SharedTableConvention(Dependencies, RelationalDependencies); ConventionSet.AddBefore( @@ -95,7 +96,10 @@ public override ConventionSet CreateConventionSet() sharedTableConvention, typeof(ValidatingConvention)); - conventionSet.ModelAnnotationChangedConventions.Add(dbFunctionAttributeConvention); + ConventionSet.AddBefore( + conventionSet.ModelFinalizedConventions, + new DbFunctionTypeMappingConvention(Dependencies, RelationalDependencies), + typeof(ValidatingConvention)); return conventionSet; } diff --git a/src/EFCore.Relational/Metadata/Conventions/Internal/DbFunctionTypeMappingConvention.cs b/src/EFCore.Relational/Metadata/Conventions/Internal/DbFunctionTypeMappingConvention.cs new file mode 100644 index 00000000000..e5446532980 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Conventions/Internal/DbFunctionTypeMappingConvention.cs @@ -0,0 +1,62 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Text; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class DbFunctionTypeMappingConvention : IModelFinalizedConvention + { + private readonly IRelationalTypeMappingSource _relationalTypeMappingSource; + + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + /// Parameter object containing relational dependencies for this convention. + public DbFunctionTypeMappingConvention( + [NotNull] ProviderConventionSetBuilderDependencies dependencies, + [NotNull] RelationalConventionSetBuilderDependencies relationalDependencies) + { + _relationalTypeMappingSource = (IRelationalTypeMappingSource)dependencies.TypeMappingSource; + } + + /// + /// Called after a model is finalized. + /// + /// The builder for the model. + /// Additional information associated with convention execution. + public virtual void ProcessModelFinalized(IConventionModelBuilder modelBuilder, IConventionContext context) + { + foreach (var dbFunction in modelBuilder.Metadata.GetDbFunctions()) + { + var typeMapping = !string.IsNullOrEmpty(dbFunction.StoreType) + ? _relationalTypeMappingSource.FindMapping(dbFunction.StoreType) + : _relationalTypeMappingSource.FindMapping(dbFunction.MethodInfo.ReturnType); + + dbFunction.Builder.HasTypeMapping(typeMapping); + + foreach (var parameter in dbFunction.Parameters) + { + typeMapping = !string.IsNullOrEmpty(parameter.StoreType) + ? _relationalTypeMappingSource.FindMapping(parameter.StoreType) + : _relationalTypeMappingSource.FindMapping(parameter.ClrType); + + parameter.Builder.HasTypeMapping(typeMapping); + } + } + } + } +} diff --git a/src/EFCore.Relational/Metadata/IConventionDbFunction.cs b/src/EFCore.Relational/Metadata/IConventionDbFunction.cs index a1999b4f085..842ee0ced79 100644 --- a/src/EFCore.Relational/Metadata/IConventionDbFunction.cs +++ b/src/EFCore.Relational/Metadata/IConventionDbFunction.cs @@ -6,6 +6,7 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; namespace Microsoft.EntityFrameworkCore.Metadata { @@ -57,6 +58,32 @@ public interface IConventionDbFunction : IDbFunction /// The configuration source for . ConfigurationSource? GetSchemaConfigurationSource(); + /// + /// Sets the store type of the function in the database. + /// + /// The store type of the function in the database. + /// Indicates whether the configuration was specified using a data annotation. + void SetStoreType([CanBeNull] string storeType, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetStoreTypeConfigurationSource(); + + /// + /// Sets the type mapping of the function in the database. + /// + /// The type mapping of the function in the database. + /// Indicates whether the configuration was specified using a data annotation. + void SetTypeMapping([CanBeNull] RelationalTypeMapping typeMapping, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetTypeMappingConfigurationSource(); + /// /// Sets the translation callback for performing custom translation of the method call into a SQL expression fragment. /// @@ -71,5 +98,10 @@ public interface IConventionDbFunction : IDbFunction /// /// The configuration source for . ConfigurationSource? GetTranslationConfigurationSource(); + + /// + /// The parameters for this function + /// + new IReadOnlyList Parameters { get; } } } diff --git a/src/EFCore.Relational/Metadata/IConventionDbFunctionParameter.cs b/src/EFCore.Relational/Metadata/IConventionDbFunctionParameter.cs new file mode 100644 index 00000000000..73ce0dab3e4 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IConventionDbFunctionParameter.cs @@ -0,0 +1,55 @@ +// 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 JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + public interface IConventionDbFunctionParameter : IDbFunctionParameter + { + new IConventionDbFunction Parent { get; } + + IConventionDbFunctionParameterBuilder Builder { get; } + + /// + /// Sets the store type of the parameter in the database. + /// + /// The store type of the parameter in the database. + /// Indicates whether the configuration was specified using a data annotation. + void SetStoreType([CanBeNull] string storeType, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetStoreTypeConfigurationSource(); + + /// + /// Sets the type mapping of the parameter in the database. + /// + /// The type mapping of the parameter in the database. + /// Indicates whether the configuration was specified using a data annotation. + void SetTypeMapping([CanBeNull] RelationalTypeMapping typeMapping, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetTypeMappingConfigurationSource(); + + /// + /// Sets if the parameter propagates null in the database. + /// + /// The supports null propagation value. + /// Indicates whether the configuration was specified using a data annotation. + void SetSupportsNullPropagation(bool supportsNullPropagation, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetSupportsNullPropagationConfigurationSource(); + } +} diff --git a/src/EFCore.Relational/Metadata/IDbFunction.cs b/src/EFCore.Relational/Metadata/IDbFunction.cs index 353763942f5..67efc15e0ad 100644 --- a/src/EFCore.Relational/Metadata/IDbFunction.cs +++ b/src/EFCore.Relational/Metadata/IDbFunction.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Reflection; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; namespace Microsoft.EntityFrameworkCore.Metadata { @@ -33,6 +34,21 @@ public interface IDbFunction /// MethodInfo MethodInfo { get; } + /// + /// The configured store type string + /// + string StoreType { get; } + + /// + /// The type mapping for the function's return type + /// + RelationalTypeMapping TypeMapping { get; } + + /// + /// The parameters for this function + /// + IReadOnlyList Parameters { get; } + /// /// A translation callback for performing custom translation of the method call into a SQL expression fragment. /// diff --git a/src/EFCore.Relational/Metadata/IDbFunctionParameter.cs b/src/EFCore.Relational/Metadata/IDbFunctionParameter.cs new file mode 100644 index 00000000000..b763c9cb6c3 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IDbFunctionParameter.cs @@ -0,0 +1,26 @@ +// 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 Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Represents a db function parameter in an . + /// + public interface IDbFunctionParameter + { + IDbFunction Parent { get; } + + string Name { get; } + + Type ClrType { get; } + + bool SupportsNullPropagation { get; } + + string StoreType { get; } + + RelationalTypeMapping TypeMapping { get; } + } +} diff --git a/src/EFCore.Relational/Metadata/IMutableDbFunction.cs b/src/EFCore.Relational/Metadata/IMutableDbFunction.cs index 10c27724dec..9c16c624e49 100644 --- a/src/EFCore.Relational/Metadata/IMutableDbFunction.cs +++ b/src/EFCore.Relational/Metadata/IMutableDbFunction.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; namespace Microsoft.EntityFrameworkCore.Metadata { @@ -17,13 +18,23 @@ public interface IMutableDbFunction : IDbFunction /// /// The name of the function in the database. /// - new string FunctionName { get; [param: NotNull] set; } + new string FunctionName { get; [param: CanBeNull] set; } /// /// The schema of the function in the database. /// new string Schema { get; [param: CanBeNull] set; } + /// + /// The store type of the function in the database. + /// + new string StoreType { get; [param: CanBeNull] set; } + + /// + /// The type mapping of the function in the database. + /// + new RelationalTypeMapping TypeMapping { get; [param: CanBeNull] set; } + /// /// The in which this function is defined. /// diff --git a/src/EFCore.Relational/Metadata/IMutableDbFunctionParameter.cs b/src/EFCore.Relational/Metadata/IMutableDbFunctionParameter.cs new file mode 100644 index 00000000000..d0217bd99cb --- /dev/null +++ b/src/EFCore.Relational/Metadata/IMutableDbFunctionParameter.cs @@ -0,0 +1,23 @@ +// 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 JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Represents a db function parametere in an . + /// + public interface IMutableDbFunctionParameter : IDbFunctionParameter + { + new IMutableDbFunction Parent { get; } + + new bool SupportsNullPropagation { get; set; } + + new string StoreType { get; [param: CanBeNull] set; } + + new RelationalTypeMapping TypeMapping { get; [param: CanBeNull] set; } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs index 0e25e58e7dc..3f63200566d 100644 --- a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs +++ b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs @@ -11,6 +11,7 @@ using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Metadata.Internal @@ -25,13 +26,18 @@ public class DbFunction : IMutableDbFunction, IConventionDbFunction { private readonly IMutableModel _model; private readonly string _annotationName; + private readonly List _parameters; private string _schema; private string _functionName; + private string _storeType; + private RelationalTypeMapping _typeMapping; + private Func, SqlExpression> _translation; private ConfigurationSource? _schemaConfigurationSource; private ConfigurationSource? _functionNameConfigurationSource; + private ConfigurationSource? _storeTypeConfigurationSource; + private ConfigurationSource? _typeMappingConfigurationSource; private ConfigurationSource? _translationConfigurationSource; - private Func, SqlExpression> _translation; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -55,6 +61,7 @@ public DbFunction( if (!methodInfo.IsStatic && !typeof(DbContext).IsAssignableFrom(methodInfo.DeclaringType)) { + // ReSharper disable once AssignNullToNotNullAttribute throw new ArgumentException( RelationalStrings.DbFunctionInvalidInstanceType( methodInfo.DisplayName(), methodInfo.DeclaringType.ShortDisplayName())); @@ -71,6 +78,11 @@ public DbFunction( MethodInfo = methodInfo; _model = model; + + _parameters = methodInfo.GetParameters() + .Select((pi, i) => new DbFunctionParameter(this, pi.Name, pi.ParameterType)) + .ToList(); + _annotationName = BuildAnnotationName(methodInfo); if (configurationSource == ConfigurationSource.Explicit) { @@ -96,18 +108,19 @@ 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] IModel model) + public static IEnumerable GetDbFunctions([NotNull] Model model) { Check.NotNull(model, nameof(model)); return model.GetAnnotations() .Where(a => a.Name.StartsWith(RelationalAnnotationNames.DbFunction, StringComparison.Ordinal)) .Select(a => a.Value) - .Cast(); + .Cast(); } private static string BuildAnnotationName(MethodBase methodBase) => + // ReSharper disable once AssignNullToNotNullAttribute $"{RelationalAnnotationNames.DbFunction}{methodBase.DeclaringType.ShortDisplayName()}{methodBase.Name}({string.Join(",", methodBase.GetParameters().Select(p => p.ParameterType.Name))})"; /// @@ -180,7 +193,7 @@ private void UpdateSchemaConfigurationSource(ConfigurationSource configurationSo /// public virtual string FunctionName { - get => _functionName; + get => _functionName ?? MethodInfo.Name; set => SetFunctionName(value, ConfigurationSource.Explicit); } @@ -218,6 +231,80 @@ private void UpdateFunctionNameConfigurationSource(ConfigurationSource configura /// 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 + /// any release. You should only use it directly in 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 StoreType + { + get => _storeType; + set => SetStoreType(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 void SetStoreType([NotNull] string storeType, ConfigurationSource configurationSource) + { + Check.NotNull(storeType, nameof(storeType)); + + _storeType = storeType; + + UpdateStoreTypeConfigurationSource(configurationSource); + } + + 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 + /// any release. You should only use it directly in 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? GetStoreTypeConfigurationSource() => _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 + /// any release. You should only use it directly in 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 TypeMapping + { + get => _typeMapping; + set => SetTypeMapping(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 void SetTypeMapping([CanBeNull] RelationalTypeMapping typeMapping, ConfigurationSource configurationSource) + { + _typeMapping = typeMapping; + + UpdateTypeMappingConfigurationSource(configurationSource); + } + + 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 + /// any release. You should only use it directly in 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? GetTypeMappingConfigurationSource() => _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 @@ -290,8 +377,22 @@ void IConventionDbFunction.SetFunctionName(string name, bool fromDataAnnotation) void IConventionDbFunction.SetSchema(string schema, bool fromDataAnnotation) => SetSchema(schema, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + void IConventionDbFunction.SetStoreType(string storeType, bool fromDataAnnotation) + => SetStoreType(storeType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + void IConventionDbFunction.SetTypeMapping(RelationalTypeMapping returnTypeMapping, bool fromDataAnnotation) + => SetTypeMapping(returnTypeMapping, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + void IConventionDbFunction.SetTranslation( Func, SqlExpression> translation, bool fromDataAnnotation) => SetTranslation(translation, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual IReadOnlyList Parameters => _parameters; + + IReadOnlyList IConventionDbFunction.Parameters => _parameters; } } diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs b/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs new file mode 100644 index 00000000000..48e953b4825 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs @@ -0,0 +1,121 @@ +// 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 JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class DbFunctionParameter : IMutableDbFunctionParameter, IConventionDbFunctionParameter + { + private readonly IMutableDbFunction _parent; + private readonly string _name; + private readonly Type _clrType; + private bool _supportsNullPropagation; + private string _storeType; + private RelationalTypeMapping _typeMapping; + + private ConfigurationSource? _supportsNullPropagationConfigurationSource; + private ConfigurationSource? _storeTypeConfigurationSource; + private ConfigurationSource? _typeMappingConfigurationSource; + + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public DbFunctionParameter([NotNull] IMutableDbFunction parent, [NotNull] string name, [NotNull] Type clrType) + { + Check.NotEmpty(name, nameof(name)); + Check.NotNull(clrType, nameof(clrType)); + Check.NotNull(parent, nameof(parent)); + + _name = name; + _parent = parent; + _clrType = clrType; + } + + public virtual IConventionDbFunctionParameterBuilder Builder => new DbFunctionParameterBuilder(this); + + public virtual bool SupportsNullPropagation + { + get => _supportsNullPropagation; + set => SetSupportsNullPropagation(value, ConfigurationSource.Explicit); + } + + public virtual void SetSupportsNullPropagation(bool supportsNullPropagation, ConfigurationSource configurationSource) + { + _supportsNullPropagation = supportsNullPropagation; + + UpdateSupportsNullPropagationConfigurationSource(configurationSource); + } + + private void UpdateSupportsNullPropagationConfigurationSource(ConfigurationSource configurationSource) + => _supportsNullPropagationConfigurationSource = configurationSource.Max(_supportsNullPropagationConfigurationSource); + + public virtual ConfigurationSource? GetSupportsNullPropagationConfigurationSource() => _supportsNullPropagationConfigurationSource; + + public virtual string StoreType + { + get => _storeType; + set => SetStoreType(value, ConfigurationSource.Explicit); + } + + public virtual void SetStoreType([CanBeNull] string storeType, ConfigurationSource configurationSource) + { + _storeType = storeType; + + UpdateStoreTypeConfigurationSource(configurationSource); + } + + private void UpdateStoreTypeConfigurationSource(ConfigurationSource configurationSource) + => _storeTypeConfigurationSource = configurationSource.Max(_storeTypeConfigurationSource); + + public virtual ConfigurationSource? GetStoreTypeConfigurationSource() => _storeTypeConfigurationSource; + + public virtual RelationalTypeMapping TypeMapping + { + get => _typeMapping; + set => SetTypeMapping(value, ConfigurationSource.Explicit); + } + + public virtual void SetTypeMapping(RelationalTypeMapping typeMapping, ConfigurationSource configurationSource) + { + _typeMapping = typeMapping; + + UpdateTypeMappingConfigurationSource(configurationSource); + } + + private void UpdateTypeMappingConfigurationSource(ConfigurationSource configurationSource) + => _typeMappingConfigurationSource = configurationSource.Max(_typeMappingConfigurationSource); + + public virtual ConfigurationSource? GetTypeMappingConfigurationSource() => _typeMappingConfigurationSource; + + IConventionDbFunction IConventionDbFunctionParameter.Parent => (IConventionDbFunction)_parent; + + IDbFunction IDbFunctionParameter.Parent => _parent; + + IMutableDbFunction IMutableDbFunctionParameter.Parent => _parent; + + string IDbFunctionParameter.Name => _name; + + Type IDbFunctionParameter.ClrType => _clrType; + + void IConventionDbFunctionParameter.SetStoreType(string storeType, bool fromDataAnnotation) + => SetStoreType(storeType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + void IConventionDbFunctionParameter.SetSupportsNullPropagation(bool supportsNullPropagation, bool fromDataAnnotation) + => SetSupportsNullPropagation(supportsNullPropagation, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + void IConventionDbFunctionParameter.SetTypeMapping(RelationalTypeMapping typeMapping, bool fromDataAnnotation) + => SetTypeMapping(typeMapping, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + } +} diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index d471c519e94..f01dbab60ae 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1,10 +1,11 @@ -// +// using System; using System.Reflection; using System.Resources; using System.Threading; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging; namespace Microsoft.EntityFrameworkCore.Diagnostics @@ -341,7 +342,9 @@ public static string ConflictingColumnServerGeneration([CanBeNull] object confli /// The check constraint '{checkConstraint}' cannot be added to the entity type '{entityType}' because another check constraint with the same name already exists. /// public static string DuplicateCheckConstraint([CanBeNull] object checkConstraint, [CanBeNull] object entityType) - => string.Format(GetString("DuplicateCheckConstraint", nameof(checkConstraint), nameof(entityType)), checkConstraint, entityType); + => string.Format( + GetString("DuplicateCheckConstraint", nameof(checkConstraint), nameof(entityType)), + checkConstraint, entityType); /// /// The foreign keys {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{foreignKeyName}' but use different columns ({columnNames1} and {columnNames2}). @@ -461,6 +464,14 @@ public static string DbFunctionInvalidParameterType([CanBeNull] object parameter GetString("DbFunctionInvalidParameterType", nameof(parameter), nameof(function), nameof(type)), parameter, function, type); + /// + /// The DbFunction '{function}' does not have a parameter named '{parameter}'. + /// + public static string DbFunctionInvalidParameterName([CanBeNull] object function, [CanBeNull] object parameter) + => string.Format( + GetString("DbFunctionInvalidParameterName", nameof(function), nameof(parameter)), + function, parameter); + /// /// The DbFunction '{function}' is generic. Generic methods are not supported. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 813a2d54ded..d18607a8580 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -410,6 +410,9 @@ The parameter '{parameter}' for the DbFunction '{function}' has an invalid type '{type}'. Ensure the parameter type can be mapped by the current provider. + + The DbFunction '{function}' does not have a parameter named '{parameter}'. + The DbFunction '{function}' is generic. Generic methods are not supported. diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 172406f8512..bf3ac493bd8 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -1079,43 +1079,6 @@ var methodInfo modelBuilder.Model); } - [ConditionalFact] - public virtual void Detects_function_with_invalid_parameter_type_but_translate_callback_does_not_throw() - { - var modelBuilder = CreateConventionalModelBuilder(); - - var methodInfo - = typeof(DbFunctionMetadataTests.TestMethods) - .GetRuntimeMethod( - nameof(DbFunctionMetadataTests.TestMethods.MethodF), - new[] { typeof(DbFunctionMetadataTests.MyBaseContext) }); - - var dbFuncBuilder = modelBuilder.HasDbFunction(methodInfo); - - dbFuncBuilder.HasTranslation(parameters => null); - - Validate(modelBuilder.Model); - } - - [ConditionalFact] - public virtual void Detects_function_with_invalid_parameter_type_but_no_translate_callback_throws() - { - var modelBuilder = CreateConventionalModelBuilder(); - - var methodInfo - = typeof(DbFunctionMetadataTests.TestMethods) - .GetRuntimeMethod( - nameof(DbFunctionMetadataTests.TestMethods.MethodF), - new[] { typeof(DbFunctionMetadataTests.MyBaseContext) }); - - modelBuilder.HasDbFunction(methodInfo); - - VerifyError( - RelationalStrings.DbFunctionInvalidParameterType( - "context", methodInfo.DisplayName(), typeof(DbFunctionMetadataTests.MyBaseContext).ShortDisplayName()), - modelBuilder.Model); - } - private static void GenerateMapping(IMutableProperty property) => property[CoreAnnotationNames.TypeMapping] = new TestRelationalTypeMappingSource( diff --git a/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs b/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs index a0b2ed6b9e3..5faa67913e8 100644 --- a/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs +++ b/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs @@ -168,6 +168,9 @@ public static int DuplicateNameTest() public static MethodInfo MethodBmi = typeof(TestMethods).GetRuntimeMethod( nameof(TestMethods.MethodB), new[] { typeof(string), typeof(int) }); + public static MethodInfo MethodImi = typeof(TestMethods).GetRuntimeMethod( + nameof(TestMethods.MethodI), new Type[] { }); + public static MethodInfo MethodHmi = typeof(TestMethods).GetTypeInfo().GetDeclaredMethod(nameof(TestMethods.MethodH)); public class TestMethods @@ -203,6 +206,11 @@ public static int MethodH(T a, string b) { throw new Exception(); } + + public static int MethodI() + { + throw new Exception(); + } } [ConditionalFact] @@ -424,6 +432,16 @@ public void Adding_method_with_relational_schema() Assert.Equal("dbo", dbFuncBuilder.Metadata.Schema); } + [ConditionalFact] + public void Adding_method_with_store_type() + { + var modelBuilder = GetModelBuilder(); + + var dbFuncBuilder = modelBuilder.HasDbFunction(MethodAmi).HasStoreType("int(8)"); + + Assert.Equal("int(8)", dbFuncBuilder.Metadata.StoreType); + } + [ConditionalFact] public void Adding_method_with_relational_schema_fluent_overrides() { @@ -485,15 +503,134 @@ public virtual void Set_empty_function_name_throws() expectedMessage, Assert.Throws(() => modelBuilder.HasDbFunction(MethodAmi).HasName("")).Message); } + [ConditionalFact] + public void DbParameters_load_no_parameters() + { + var modelBuilder = GetModelBuilder(); + + var dbFuncBuilder = modelBuilder.HasDbFunction(MethodImi); + var dbFunc = dbFuncBuilder.Metadata; + + Assert.Equal(0, dbFunc.Parameters.Count); + } + + [ConditionalFact] + public void DbParameters_invalid_parameter_name_throws() + { + var modelBuilder = GetModelBuilder(); + + var dbFuncBuilder = modelBuilder.HasDbFunction(MethodBmi); + + Assert.Equal( + RelationalStrings.DbFunctionInvalidParameterName("q", dbFuncBuilder.Metadata.MethodInfo.DisplayName()), + Assert.Throws(() => dbFuncBuilder.HasParameter("q")).Message); + } + + [ConditionalFact] + public void DbParameters_load_with_parameters() + { + var modelBuilder = GetModelBuilder(); + + var dbFuncBuilder = modelBuilder.HasDbFunction(MethodBmi); + var dbFunc = dbFuncBuilder.Metadata; + + Assert.Equal(2, dbFunc.Parameters.Count); + + Assert.Equal("c", dbFunc.Parameters[0].Name); + Assert.Equal(typeof(string), dbFunc.Parameters[0].ClrType); + + Assert.Equal("d", dbFunc.Parameters[1].Name); + Assert.Equal(typeof(int), dbFunc.Parameters[1].ClrType); + } + + [ConditionalFact] + public void DbParameters_dbfunctionType() + { + var modelBuilder = GetModelBuilder(); + + var dbFuncBuilder = modelBuilder.HasDbFunction(MethodBmi); + var dbFunc = dbFuncBuilder.Metadata; + + dbFuncBuilder.HasParameter("c"); + + Assert.Equal(2, dbFunc.Parameters.Count); + + Assert.Equal("c", dbFunc.Parameters[0].Name); + Assert.Equal(typeof(string), dbFunc.Parameters[0].ClrType); + + Assert.Equal("d", dbFunc.Parameters[1].Name); + Assert.Equal(typeof(int), dbFunc.Parameters[1].ClrType); + } + + [ConditionalFact] + public void DbParameters_name() + { + var modelBuilder = GetModelBuilder(); + + var dbFuncBuilder = modelBuilder.HasDbFunction(MethodBmi); + var dbFunc = dbFuncBuilder.Metadata; + + dbFuncBuilder.HasParameter("c"); + + Assert.Equal(2, dbFunc.Parameters.Count); + + Assert.Equal("c", dbFunc.Parameters[0].Name); + Assert.Equal(typeof(string), dbFunc.Parameters[0].ClrType); + + Assert.Equal("d", dbFunc.Parameters[1].Name); + Assert.Equal(typeof(int), dbFunc.Parameters[1].ClrType); + } + + [ConditionalFact] + public void DbParameters_NullabilityPropagation() + { + var modelBuilder = GetModelBuilder(); + + var dbFuncBuilder = modelBuilder.HasDbFunction(MethodBmi); + var dbFunc = dbFuncBuilder.Metadata; + + dbFuncBuilder.HasParameter("c").HasNullabilityPropagation(true); + + Assert.Equal(2, dbFunc.Parameters.Count); + + Assert.Equal("c", dbFunc.Parameters[0].Name); + Assert.Equal(typeof(string), dbFunc.Parameters[0].ClrType); + Assert.Equal(true, dbFunc.Parameters[0].SupportsNullPropagation); + + Assert.Equal("d", dbFunc.Parameters[1].Name); + Assert.Equal(typeof(int), dbFunc.Parameters[1].ClrType); + } + + [ConditionalFact] + public void DbParameters_StoreType() + { + var modelBuilder = GetModelBuilder(); + + var dbFuncBuilder = modelBuilder.HasDbFunction(MethodBmi); + var dbFunc = dbFuncBuilder.Metadata; + + dbFuncBuilder.HasParameter("c").HasStoreType("varchar(max)"); + + Assert.Equal(2, dbFunc.Parameters.Count); + + Assert.Equal("c", dbFunc.Parameters[0].Name); + Assert.Equal(typeof(string), dbFunc.Parameters[0].ClrType); + Assert.Equal("varchar(max)", dbFunc.Parameters[0].StoreType); + + Assert.Equal("d", dbFunc.Parameters[1].Name); + Assert.Equal(typeof(int), dbFunc.Parameters[1].ClrType); + } + private ModelBuilder GetModelBuilder(DbContext dbContext = null) { var conventionSet = new ConventionSet(); - var dbFunctionAttributeConvention =new RelationalDbFunctionAttributeConvention( - CreateDependencies().With(new CurrentDbContext (dbContext ?? new DbContext(new DbContextOptions()))), - CreateRelationalDependencies()); + var dependencies = CreateDependencies().With(new CurrentDbContext(dbContext ?? new DbContext(new DbContextOptions()))); + var relationalDependencies = CreateRelationalDependencies(); + var dbFunctionAttributeConvention = new RelationalDbFunctionAttributeConvention(dependencies, relationalDependencies); conventionSet.ModelInitializedConventions.Add(dbFunctionAttributeConvention); conventionSet.ModelAnnotationChangedConventions.Add(dbFunctionAttributeConvention); + conventionSet.ModelFinalizedConventions.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 6d9d64de789..e00c57ca23f 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs @@ -382,7 +382,7 @@ public void Can_get_and_set_dbfunction() var dbFunc = model.AddDbFunction(testMethod); Assert.NotNull(dbFunc); - Assert.Null(dbFunc.FunctionName); + Assert.NotNull(dbFunc.FunctionName); Assert.Null(dbFunc.Schema); }