Skip to content

Commit

Permalink
Query: Make it easy to map built-in functions
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
smitpatel committed Jun 23, 2020
1 parent 2213f95 commit d65d0ed
Show file tree
Hide file tree
Showing 13 changed files with 193 additions and 32 deletions.
14 changes: 13 additions & 1 deletion src/EFCore.Abstractions/DbFunctionAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class DbFunctionAttribute : Attribute
{
private string _name;
private string _schema;
private bool _builtIn;

/// <summary>
/// Initializes a new instance of the <see cref="DbFunctionAttribute" /> class.
Expand All @@ -32,12 +33,14 @@ public DbFunctionAttribute()
/// </summary>
/// <param name="name">The name of the function in the database.</param>
/// <param name="schema">The schema of the function in the database.</param>
public DbFunctionAttribute([NotNull] string name, [CanBeNull] string schema = null)
/// <param name="builtIn"> The value indicating whether the database function is built-in or not. </param>
public DbFunctionAttribute([NotNull] string name, [CanBeNull] string schema = null, bool builtIn = false)
{
Check.NotEmpty(name, nameof(name));

_name = name;
_schema = schema;
_builtIn = builtIn;
}

/// <summary>
Expand All @@ -63,5 +66,14 @@ public virtual string Schema
get => _schema;
[param: CanBeNull] set => _schema = value;
}

/// <summary>
/// The value indicating wheather the database function is built-in or not.
/// </summary>
public virtual bool IsBuiltIn
{
get => _builtIn;
set => _builtIn = value;
}
}
}
12 changes: 12 additions & 0 deletions src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ public virtual DbFunctionBuilder HasSchema([CanBeNull] string schema)
return this;
}

/// <summary>
/// Marks whether the database function is built-in or not.
/// </summary>
/// <param name="builtIn"> The value indicating wheather the database function is built-in or not. </param>
/// <returns> The same builder instance so that multiple configuration calls can be chained. </returns>
public virtual DbFunctionBuilder IsBuiltIn(bool builtIn = true)
{
Builder.IsBuiltIn(builtIn, ConfigurationSource.Explicit);

return this;
}

/// <summary>
/// Sets the store type of the database function.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,25 @@ public interface IConventionDbFunctionBuilder : IConventionAnnotatableBuilder
/// <returns> <see langword="true" /> if the given schema can be set for the database function. </returns>
bool CanSetSchema([CanBeNull] string schema, bool fromDataAnnotation = false);

/// <summary>
/// Sets the value indicating wheather the database function is built-in or not.
/// </summary>
/// <param name="builtIn"> The value indicating whether the database function is built-in or not. </param>
/// <param name="fromDataAnnotation"> Indicates whether the configuration was specified using a data annotation. </param>
/// <returns>
/// The same builder instance if the configuration was applied,
/// <see langword="null" /> otherwise.
/// </returns>
IConventionDbFunctionBuilder IsBuiltIn(bool builtIn, bool fromDataAnnotation = false);

/// <summary>
/// Returns a value indicating whether the given built-in can be set for the database function.
/// </summary>
/// <param name="builtIn"> The value indicating whether the database function is built-in or not. </param>
/// <param name="fromDataAnnotation"> Indicates whether the configuration was specified using a data annotation. </param>
/// <returns> <see langword="true" /> if the given schema can be set for the database function. </returns>
bool CanSetIsBuiltIn(bool builtIn, bool fromDataAnnotation = false);

/// <summary>
/// Sets the store type of the function in the database.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ protected virtual void ProcessDbFunctionAdded(
{
dbFunctionBuilder.HasSchema(dbFunctionAttribute.Schema, fromDataAnnotation: true);
}

if (dbFunctionAttribute.IsBuiltIn)
{
dbFunctionBuilder.IsBuiltIn(dbFunctionAttribute.IsBuiltIn, fromDataAnnotation: true);
}
}
}
}
Expand Down
14 changes: 14 additions & 0 deletions src/EFCore.Relational/Metadata/IConventionDbFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ public interface IConventionDbFunction : IConventionAnnotatable, IDbFunction
/// <returns> The configuration source for <see cref="IDbFunction.Schema" />. </returns>
ConfigurationSource? GetSchemaConfigurationSource();

/// <summary>
/// Sets the value indicating wheather the database function is built-in or not.
/// </summary>
/// <param name="builtIn"> The value indicating whether the database function is built-in or not. </param>
/// <param name="fromDataAnnotation"> Indicates whether the configuration was specified using a data annotation. </param>
/// <returns> The configured value. </returns>
bool SetIsBuiltIn(bool builtIn, bool fromDataAnnotation = false);

/// <summary>
/// Gets the configuration source for <see cref="IDbFunction.IsBuiltIn" />.
/// </summary>
/// <returns> The configuration source for <see cref="IDbFunction.IsBuiltIn" />. </returns>
ConfigurationSource? GetIsBuiltInConfigurationSource();

/// <summary>
/// Sets the store type of the function in the database.
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/EFCore.Relational/Metadata/IDbFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ public interface IDbFunction : IAnnotatable
/// </summary>
string Schema { get; }

/// <summary>
/// Gets the value indicating wheather the database function is built-in or not.
/// </summary>
bool IsBuiltIn { get; }

/// <summary>
/// Gets the name of the function in the model.
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/EFCore.Relational/Metadata/IMutableDbFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ public interface IMutableDbFunction : IMutableAnnotatable, IDbFunction
/// </summary>
new string Schema { get; [param: CanBeNull] set; }

/// <summary>
/// Gets or sets the value indicating wheather the database function is built-in or not.
/// </summary>
new bool IsBuiltIn { get; set; }

/// <summary>
/// Gets or sets the store type of the function in the database.
/// </summary>
Expand Down
41 changes: 41 additions & 0 deletions src/EFCore.Relational/Metadata/Internal/DbFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ public class DbFunction : ConventionAnnotatable, IMutableDbFunction, IConvention
private readonly List<DbFunctionParameter> _parameters;
private string _schema;
private string _name;
private bool _builtIn;
private string _storeType;
private RelationalTypeMapping _typeMapping;
private Func<IReadOnlyCollection<SqlExpression>, SqlExpression> _translation;

private ConfigurationSource _configurationSource;
private ConfigurationSource? _schemaConfigurationSource;
private ConfigurationSource? _nameConfigurationSource;
private ConfigurationSource? _builtInConfigurationSource;
private ConfigurationSource? _storeTypeConfigurationSource;
private ConfigurationSource? _typeMappingConfigurationSource;
private ConfigurationSource? _translationConfigurationSource;
Expand Down Expand Up @@ -373,6 +375,40 @@ public virtual string SetName([CanBeNull] string name, ConfigurationSource confi
/// </summary>
public virtual ConfigurationSource? GetNameConfigurationSource() => _nameConfigurationSource;

/// <summary>
/// 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.
/// </summary>
public virtual bool IsBuiltIn
{
get => _builtIn;
set => SetIsBuiltIn(value, ConfigurationSource.Explicit);
}

/// <summary>
/// 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.
/// </summary>
public virtual bool SetIsBuiltIn(bool builtIn, ConfigurationSource configurationSource)
{
_builtIn = builtIn;
_builtInConfigurationSource = configurationSource.Max(_builtInConfigurationSource);

return builtIn;
}

/// <summary>
/// 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.
/// </summary>
public virtual ConfigurationSource? GetIsBuiltInConfigurationSource() => _builtInConfigurationSource;

/// <summary>
/// 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
Expand Down Expand Up @@ -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);

/// <inheritdoc />
[DebuggerStepThrough]
bool IConventionDbFunction.SetIsBuiltIn(bool builtIn, bool fromDataAnnotation)
=> SetIsBuiltIn(builtIn, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention);

/// <inheritdoc />
[DebuggerStepThrough]
string IConventionDbFunction.SetStoreType(string storeType, bool fromDataAnnotation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,33 @@ public virtual bool CanSetSchema([CanBeNull] string schema, ConfigurationSource
=> configurationSource.Overrides(Metadata.GetSchemaConfigurationSource())
|| Metadata.Schema == schema;

/// <summary>
/// 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.
/// </summary>
public virtual IConventionDbFunctionBuilder IsBuiltIn(bool builtIn, ConfigurationSource configurationSource)
{
if (CanSetIsBuiltIn(builtIn, configurationSource))
{
Metadata.SetIsBuiltIn(builtIn, configurationSource);
return this;
}

return null;
}

/// <summary>
/// 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.
/// </summary>
public virtual bool CanSetIsBuiltIn(bool builtIn, ConfigurationSource configurationSource)
=> configurationSource.Overrides(Metadata.GetIsBuiltInConfigurationSource())
|| Metadata.IsBuiltIn == builtIn;

/// <summary>
/// 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
Expand Down Expand Up @@ -218,6 +245,16 @@ IConventionDbFunctionBuilder IConventionDbFunctionBuilder.HasSchema(string schem
bool IConventionDbFunctionBuilder.CanSetSchema(string schema, bool fromDataAnnotation)
=> CanSetSchema(schema, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention);

/// <inheritdoc />
[DebuggerStepThrough]
IConventionDbFunctionBuilder IConventionDbFunctionBuilder.IsBuiltIn(bool builtIn, bool fromDataAnnotation)
=> IsBuiltIn(builtIn, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention);

/// <inheritdoc />
[DebuggerStepThrough]
bool IConventionDbFunctionBuilder.CanSetIsBuiltIn(bool builtIn, bool fromDataAnnotation)
=> CanSetIsBuiltIn(builtIn, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention);

/// <inheritdoc />
[DebuggerStepThrough]
IConventionDbFunctionBuilder IConventionDbFunctionBuilder.HasStoreType(string storeType, bool fromDataAnnotation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
namespace Microsoft.EntityFrameworkCore.Metadata.Conventions
{
/// <summary>
/// A convention that ensures that <see cref="IDbFunction.Schema"/> is populated for database functions which are not built-in.
/// A convention that ensures that <see cref="IDbFunction.Schema"/> is populated for database functions which
/// have <see cref="IDbFunction.IsBuiltIn"/> flag set to <see langword="false"/>.
/// </summary>
public class SqlServerDbFunctionConvention : IModelFinalizingConvention
{
Expand Down Expand Up @@ -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");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) }));

Expand All @@ -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<MultProductOrders>().ToTable("MultProductOrders").HasKey(mpo => mpo.OrderId);

Expand Down
16 changes: 16 additions & 0 deletions test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,22 @@ public void DbFunction_HasName()
Assert.NotEqual(funcA.Metadata.Name, funcB.Metadata.Name);
}

[ConditionalFact]
public void DbFunction_IsBuiltIn()
{
var modelBuilder = GetModelBuilder();

var methodA = typeof(OuterA.Inner).GetMethod(nameof(OuterA.Inner.Min));

var funcA = modelBuilder.HasDbFunction(methodA);

Assert.False(funcA.Metadata.IsBuiltIn);

funcA.IsBuiltIn();

Assert.True(funcA.Metadata.IsBuiltIn);
}

[ConditionalFact]
public virtual void Set_empty_function_name_throws()
{
Expand Down

0 comments on commit d65d0ed

Please sign in to comment.