From fcc947b7eac08188a51b49e36df17ad79d7833f3 Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Tue, 23 Jun 2020 15:03:17 -0700 Subject: [PATCH] Query: Make it easy to map built-in functions Resolves #17268 - Introduces new API `IsBuiltIn` on DbFunctionBuilder which marks the function as built-in and we translate it accordingly. - Add `DbFunctionAttribute.IsBuiltIn` and convention to set the flag appropriately. --- .../DbFunctionAttribute.cs | 14 ++++++- .../Metadata/Builders/DbFunctionBuilder.cs | 12 ++++++ .../Builders/IConventionDbFunctionBuilder.cs | 19 +++++++++ ...RelationalDbFunctionAttributeConvention.cs | 5 +++ .../Metadata/IConventionDbFunction.cs | 14 +++++++ src/EFCore.Relational/Metadata/IDbFunction.cs | 5 +++ .../Metadata/IMutableDbFunction.cs | 5 +++ .../Metadata/Internal/DbFunction.cs | 41 +++++++++++++++++++ .../Internal/InternalDbFunctionBuilder.cs | 37 +++++++++++++++++ .../RelationalMethodCallTranslatorProvider.cs | 22 ++++++++-- .../SqlServerDbFunctionConvention.cs | 6 ++- .../Query/UdfDbFunctionTestBase.cs | 29 ++----------- 12 files changed, 177 insertions(+), 32 deletions(-) diff --git a/src/EFCore.Abstractions/DbFunctionAttribute.cs b/src/EFCore.Abstractions/DbFunctionAttribute.cs index c5367469d64..e913a6c4c98 100644 --- a/src/EFCore.Abstractions/DbFunctionAttribute.cs +++ b/src/EFCore.Abstractions/DbFunctionAttribute.cs @@ -19,6 +19,7 @@ public class DbFunctionAttribute : Attribute { private string _name; private string _schema; + private bool _builtIn; /// /// Initializes a new instance of the class. @@ -32,12 +33,14 @@ public DbFunctionAttribute() /// /// The name of the function in the database. /// The schema of the function in the database. - public DbFunctionAttribute([NotNull] string name, [CanBeNull] string schema = null) + /// The value indicating whether the database function is built-in or not. + public DbFunctionAttribute([NotNull] string name, [CanBeNull] string schema = null, bool builtIn = false) { Check.NotEmpty(name, nameof(name)); _name = name; _schema = schema; + _builtIn = builtIn; } /// @@ -63,5 +66,14 @@ public virtual string Schema get => _schema; [param: CanBeNull] set => _schema = value; } + + /// + /// The value indicating wheather the database function is built-in or not. + /// + public virtual bool IsBuiltIn + { + get => _builtIn; + set => _builtIn = value; + } } } diff --git a/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs b/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs index a97ef3c7192..8c848fdfcc7 100644 --- a/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs @@ -70,6 +70,18 @@ public virtual DbFunctionBuilder HasSchema([CanBeNull] string schema) return this; } + /// + /// Marks whether the database function is built-in or not. + /// + /// The value indicating wheather the database function is built-in or not. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual DbFunctionBuilder IsBuiltIn(bool builtIn = true) + { + Builder.IsBuiltIn(builtIn, ConfigurationSource.Explicit); + + return this; + } + /// /// Sets the store type of the database function. /// diff --git a/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionBuilder.cs b/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionBuilder.cs index e7b7409702c..0d586eebd6d 100644 --- a/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionBuilder.cs @@ -57,6 +57,25 @@ public interface IConventionDbFunctionBuilder : IConventionAnnotatableBuilder /// if the given schema can be set for the database function. bool CanSetSchema([CanBeNull] string schema, bool fromDataAnnotation = false); + /// + /// Sets the value indicating wheather the database function is built-in or not. + /// + /// The value indicating whether the database function is built-in or not. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionDbFunctionBuilder IsBuiltIn(bool builtIn, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given built-in can be set for the database function. + /// + /// The value indicating whether the database function is built-in or not. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given schema can be set for the database function. + bool CanSetIsBuiltIn(bool builtIn, bool fromDataAnnotation = false); + /// /// Sets the store type of the function in the database. /// diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalDbFunctionAttributeConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalDbFunctionAttributeConvention.cs index 7865e588e91..b4b4ab5205b 100644 --- a/src/EFCore.Relational/Metadata/Conventions/RelationalDbFunctionAttributeConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalDbFunctionAttributeConvention.cs @@ -87,6 +87,11 @@ protected virtual void ProcessDbFunctionAdded( { dbFunctionBuilder.HasSchema(dbFunctionAttribute.Schema, fromDataAnnotation: true); } + + if (dbFunctionAttribute.IsBuiltIn) + { + dbFunctionBuilder.IsBuiltIn(dbFunctionAttribute.IsBuiltIn, fromDataAnnotation: true); + } } } } diff --git a/src/EFCore.Relational/Metadata/IConventionDbFunction.cs b/src/EFCore.Relational/Metadata/IConventionDbFunction.cs index 9d22b34fba4..c14b125607a 100644 --- a/src/EFCore.Relational/Metadata/IConventionDbFunction.cs +++ b/src/EFCore.Relational/Metadata/IConventionDbFunction.cs @@ -60,6 +60,20 @@ public interface IConventionDbFunction : IConventionAnnotatable, IDbFunction /// The configuration source for . ConfigurationSource? GetSchemaConfigurationSource(); + /// + /// Sets the value indicating wheather the database function is built-in or not. + /// + /// The value indicating whether the database function is built-in or not. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + bool SetIsBuiltIn(bool builtIn, bool fromDataAnnotation = false); + + /// + /// Gets the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetIsBuiltInConfigurationSource(); + /// /// Sets the store type of the function in the database. /// diff --git a/src/EFCore.Relational/Metadata/IDbFunction.cs b/src/EFCore.Relational/Metadata/IDbFunction.cs index 95138747755..9b4f5f13ddf 100644 --- a/src/EFCore.Relational/Metadata/IDbFunction.cs +++ b/src/EFCore.Relational/Metadata/IDbFunction.cs @@ -25,6 +25,11 @@ public interface IDbFunction : IAnnotatable /// string Schema { get; } + /// + /// Gets the value indicating wheather the database function is built-in or not. + /// + bool IsBuiltIn { get; } + /// /// Gets the name of the function in the model. /// diff --git a/src/EFCore.Relational/Metadata/IMutableDbFunction.cs b/src/EFCore.Relational/Metadata/IMutableDbFunction.cs index a1f71ca2f56..dc2e2ea5f09 100644 --- a/src/EFCore.Relational/Metadata/IMutableDbFunction.cs +++ b/src/EFCore.Relational/Metadata/IMutableDbFunction.cs @@ -25,6 +25,11 @@ public interface IMutableDbFunction : IMutableAnnotatable, IDbFunction /// new string Schema { get; [param: CanBeNull] set; } + /// + /// Gets or sets the value indicating wheather the database function is built-in or not. + /// + new bool IsBuiltIn { get; set; } + /// /// Gets or sets the store type of the function in the database. /// diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs index 0da8bc64bb1..b25461c98bc 100644 --- a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs +++ b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs @@ -29,6 +29,7 @@ public class DbFunction : ConventionAnnotatable, IMutableDbFunction, IConvention private readonly List _parameters; private string _schema; private string _name; + private bool _builtIn; private string _storeType; private RelationalTypeMapping _typeMapping; private Func, SqlExpression> _translation; @@ -36,6 +37,7 @@ public class DbFunction : ConventionAnnotatable, IMutableDbFunction, IConvention private ConfigurationSource _configurationSource; private ConfigurationSource? _schemaConfigurationSource; private ConfigurationSource? _nameConfigurationSource; + private ConfigurationSource? _builtInConfigurationSource; private ConfigurationSource? _storeTypeConfigurationSource; private ConfigurationSource? _typeMappingConfigurationSource; private ConfigurationSource? _translationConfigurationSource; @@ -373,6 +375,40 @@ public virtual string SetName([CanBeNull] string name, ConfigurationSource confi /// 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 bool IsBuiltIn + { + get => _builtIn; + set => SetIsBuiltIn(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 SetIsBuiltIn(bool builtIn, ConfigurationSource configurationSource) + { + _builtIn = builtIn; + _builtInConfigurationSource = configurationSource.Max(_builtInConfigurationSource); + + return builtIn; + } + + /// + /// 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? GetIsBuiltInConfigurationSource() => _builtInConfigurationSource; + /// /// 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 @@ -582,6 +618,11 @@ string IConventionDbFunction.SetName(string name, bool fromDataAnnotation) string IConventionDbFunction.SetSchema(string schema, bool fromDataAnnotation) => SetSchema(schema, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// + [DebuggerStepThrough] + bool IConventionDbFunction.SetIsBuiltIn(bool builtIn, bool fromDataAnnotation) + => SetIsBuiltIn(builtIn, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// [DebuggerStepThrough] string IConventionDbFunction.SetStoreType(string storeType, bool fromDataAnnotation) diff --git a/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs index f13d7a09e51..e22612d781b 100644 --- a/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs +++ b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs @@ -89,6 +89,33 @@ public virtual bool CanSetSchema([CanBeNull] string schema, ConfigurationSource => configurationSource.Overrides(Metadata.GetSchemaConfigurationSource()) || Metadata.Schema == schema; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IConventionDbFunctionBuilder IsBuiltIn(bool builtIn, ConfigurationSource configurationSource) + { + if (CanSetIsBuiltIn(builtIn, configurationSource)) + { + Metadata.SetIsBuiltIn(builtIn, configurationSource); + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanSetIsBuiltIn(bool builtIn, ConfigurationSource configurationSource) + => configurationSource.Overrides(Metadata.GetIsBuiltInConfigurationSource()) + || Metadata.IsBuiltIn == builtIn; + /// /// 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 @@ -218,6 +245,16 @@ IConventionDbFunctionBuilder IConventionDbFunctionBuilder.HasSchema(string schem bool IConventionDbFunctionBuilder.CanSetSchema(string schema, bool fromDataAnnotation) => CanSetSchema(schema, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// + [DebuggerStepThrough] + IConventionDbFunctionBuilder IConventionDbFunctionBuilder.IsBuiltIn(bool builtIn, bool fromDataAnnotation) + => IsBuiltIn(builtIn, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionDbFunctionBuilder.CanSetIsBuiltIn(bool builtIn, bool fromDataAnnotation) + => CanSetIsBuiltIn(builtIn, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// [DebuggerStepThrough] IConventionDbFunctionBuilder IConventionDbFunctionBuilder.HasStoreType(string storeType, bool fromDataAnnotation) diff --git a/src/EFCore.Relational/Query/RelationalMethodCallTranslatorProvider.cs b/src/EFCore.Relational/Query/RelationalMethodCallTranslatorProvider.cs index d39f873987b..445b5eb09c5 100644 --- a/src/EFCore.Relational/Query/RelationalMethodCallTranslatorProvider.cs +++ b/src/EFCore.Relational/Query/RelationalMethodCallTranslatorProvider.cs @@ -70,15 +70,29 @@ public virtual SqlExpression Translate( var dbFunction = model.FindDbFunction(method); if (dbFunction != null) { - return dbFunction.Translation?.Invoke( - arguments.Select(e => _sqlExpressionFactory.ApplyDefaultTypeMapping(e)).ToList()) - ?? _sqlExpressionFactory.Function( - dbFunction.Schema, + if (dbFunction.Translation != null) + { + return dbFunction.Translation.Invoke( + arguments.Select(e => _sqlExpressionFactory.ApplyDefaultTypeMapping(e)).ToList()); + } + + if (dbFunction.IsBuiltIn) + { + return _sqlExpressionFactory.Function( dbFunction.Name, arguments, nullable: true, argumentsPropagateNullability: arguments.Select(a => false).ToList(), method.ReturnType); + } + + return _sqlExpressionFactory.Function( + dbFunction.Schema, + dbFunction.Name, + arguments, + nullable: true, + argumentsPropagateNullability: arguments.Select(a => false).ToList(), + method.ReturnType); } return _plugins.Concat(_translators) diff --git a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerDbFunctionConvention.cs b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerDbFunctionConvention.cs index 74236bc0762..1b1917d2ef0 100644 --- a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerDbFunctionConvention.cs +++ b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerDbFunctionConvention.cs @@ -10,7 +10,8 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions { /// - /// A convention that ensures that is populated for database functions which are not built-in. + /// A convention that ensures that is populated for database functions which + /// have flag set to false. /// public class SqlServerDbFunctionConvention : IModelFinalizingConvention { @@ -39,7 +40,8 @@ public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, { foreach (var dbFunction in modelBuilder.Metadata.GetDbFunctions()) { - if (string.IsNullOrEmpty(dbFunction.Schema)) + if (!dbFunction.IsBuiltIn + && string.IsNullOrEmpty(dbFunction.Schema)) { dbFunction.SetSchema("dbo"); } diff --git a/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs index 845b85f3d45..80e204df312 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs @@ -94,6 +94,7 @@ public enum ReportingPeriod Fall } + [DbFunction(Name = "len", IsBuiltIn = true)] public static long MyCustomLengthStatic(string s) => throw new Exception(); public static bool IsDateStatic(string date) => throw new Exception(); public static int AddOneStatic(int num) => num + 1; @@ -220,15 +221,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(GetSqlFragmentStatic))) .HasTranslation(args => new SqlFragmentExpression("'Two'")); var isDateMethodInfo = typeof(UDFSqlContext).GetMethod(nameof(IsDateStatic)); - modelBuilder.HasDbFunction(isDateMethodInfo) - .HasTranslation(args => new SqlFunctionExpression( - "IsDate", args, nullable: true, argumentsPropagateNullability: args.Select(a => true).ToList(), isDateMethodInfo.ReturnType, null)); - - var methodInfo = typeof(UDFSqlContext).GetMethod(nameof(MyCustomLengthStatic)); - - modelBuilder.HasDbFunction(methodInfo) - .HasTranslation(args => new SqlFunctionExpression( - "len", args, nullable: true, argumentsPropagateNullability: args.Select(a => true).ToList(), methodInfo.ReturnType, null)); + modelBuilder.HasDbFunction(isDateMethodInfo).HasName("IsDate").IsBuiltIn(); modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(AddValues), new[] { typeof(int), typeof(int) })); @@ -244,27 +237,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(GetReportingPeriodStartDateInstance))) .HasName("GetReportingPeriodStartDate"); var isDateMethodInfo2 = typeof(UDFSqlContext).GetMethod(nameof(IsDateInstance)); - modelBuilder.HasDbFunction(isDateMethodInfo2) - .HasTranslation(args => new SqlFunctionExpression( - "IsDate", - args, - nullable: true, - argumentsPropagateNullability: args.Select(a => true).ToList(), - isDateMethodInfo2.ReturnType, - null)); + modelBuilder.HasDbFunction(isDateMethodInfo2).HasName("IsDate").IsBuiltIn(); modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(DollarValueInstance))).HasName("DollarValue"); var methodInfo2 = typeof(UDFSqlContext).GetMethod(nameof(MyCustomLengthInstance)); - modelBuilder.HasDbFunction(methodInfo2) - .HasTranslation(args => new SqlFunctionExpression( - "len", - args, - nullable: true, - argumentsPropagateNullability: args.Select(a => true).ToList(), - methodInfo2.ReturnType, - null)); + modelBuilder.HasDbFunction(methodInfo2).HasName("len").IsBuiltIn(); modelBuilder.Entity().ToTable("MultProductOrders").HasKey(mpo => mpo.OrderId);