diff --git a/src/EFCore.Abstractions/DbFunctionAttribute.cs b/src/EFCore.Abstractions/DbFunctionAttribute.cs index e913a6c4c98..a64cbff5ff4 100644 --- a/src/EFCore.Abstractions/DbFunctionAttribute.cs +++ b/src/EFCore.Abstractions/DbFunctionAttribute.cs @@ -17,9 +17,12 @@ namespace Microsoft.EntityFrameworkCore public class DbFunctionAttribute : Attribute #pragma warning restore CA1813 // Avoid unsealed attributes { + private static readonly bool DefaultNullable = true; + private string _name; private string _schema; private bool _builtIn; + private bool? _nullable; /// /// Initializes a new instance of the class. @@ -68,12 +71,27 @@ public virtual string Schema } /// - /// The value indicating wheather the database function is built-in or not. + /// The value indicating whether the database function is built-in or not. /// public virtual bool IsBuiltIn { get => _builtIn; set => _builtIn = value; } + + /// + /// The value indicating whether the database function can return null result or not. + /// + public virtual bool IsNullable + { + get => _nullable ?? DefaultNullable; + set => _nullable = value; + } + + /// + /// Use this method if you want to know the nullability of + /// the database function or if it was not specified. + /// + public bool? GetIsNullable() => _nullable; } } diff --git a/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs b/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs index 1c078f2728f..05ec03d9d07 100644 --- a/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs @@ -45,11 +45,24 @@ public DbFunctionBuilder([NotNull] IMutableDbFunction function) /// /// Marks whether the database function is built-in. /// - /// The value indicating wheather the database function is built-in. + /// The value indicating whether the database function is built-in. /// The same builder instance so that multiple configuration calls can be chained. public new virtual DbFunctionBuilder IsBuiltIn(bool builtIn = true) => (DbFunctionBuilder)base.IsBuiltIn(builtIn); + + /// + /// Marks whether the database function can return null value. + /// + /// The value indicating whether the database function can return null. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual DbFunctionBuilderBase IsNullable(bool nullable = true) + { + Builder.IsNullable(nullable, ConfigurationSource.Explicit); + + return this; + } + /// /// Sets the return store type of the database function. /// diff --git a/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilderBase.cs b/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilderBase.cs index dbcc61d70b1..3d0244021de 100644 --- a/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilderBase.cs +++ b/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilderBase.cs @@ -77,7 +77,7 @@ public virtual DbFunctionBuilderBase HasSchema([CanBeNull] string schema) /// /// Marks whether the database function is built-in. /// - /// The value indicating wheather the database function is built-in. + /// The value indicating whether the database function is built-in. /// The same builder instance so that multiple configuration calls can be chained. public virtual DbFunctionBuilderBase IsBuiltIn(bool builtIn = true) { diff --git a/src/EFCore.Relational/Metadata/Builders/DbFunctionParameterBuilder.cs b/src/EFCore.Relational/Metadata/Builders/DbFunctionParameterBuilder.cs index dbf94a36903..3b0ddabe1a8 100644 --- a/src/EFCore.Relational/Metadata/Builders/DbFunctionParameterBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/DbFunctionParameterBuilder.cs @@ -62,6 +62,18 @@ public virtual DbFunctionParameterBuilder HasStoreType([CanBeNull] string storeT return this; } + /// + /// Indicates whether parameter propagates nullability, meaning if it's value is null the database function itself returns null. + /// + /// Value which indicates whether parameter propagates nullability. + /// The same builder instance so that further configuration calls can be chained. + public virtual DbFunctionParameterBuilder PropagatesNullability(bool propagatesNullability = true) + { + Builder.PropagatesNullability(propagatesNullability, ConfigurationSource.Explicit); + + return this; + } + #region Hidden System.Object members /// diff --git a/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionBuilder.cs b/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionBuilder.cs index 0d586eebd6d..ff297a31eee 100644 --- a/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionBuilder.cs @@ -58,7 +58,7 @@ public interface IConventionDbFunctionBuilder : IConventionAnnotatableBuilder bool CanSetSchema([CanBeNull] string schema, bool fromDataAnnotation = false); /// - /// Sets the value indicating wheather the database function is built-in or not. + /// Sets the value indicating whether 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. @@ -76,6 +76,25 @@ public interface IConventionDbFunctionBuilder : IConventionAnnotatableBuilder /// if the given schema can be set for the database function. bool CanSetIsBuiltIn(bool builtIn, bool fromDataAnnotation = false); + /// + /// Sets the value indicating whether the database function can return null value 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 IsNullable(bool nullable, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given nullable can be set for the database function. + /// + /// The value indicating whether the database function can return null value or not. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given schema can be set for the database function. + bool CanSetIsNullable(bool nullable, 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 b4b4ab5205b..07a8ff93796 100644 --- a/src/EFCore.Relational/Metadata/Conventions/RelationalDbFunctionAttributeConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalDbFunctionAttributeConvention.cs @@ -92,6 +92,11 @@ protected virtual void ProcessDbFunctionAdded( { dbFunctionBuilder.IsBuiltIn(dbFunctionAttribute.IsBuiltIn, fromDataAnnotation: true); } + + if (!dbFunctionAttribute.GetIsNullable() != null) + { + dbFunctionBuilder.IsNullable(dbFunctionAttribute.IsNullable, fromDataAnnotation: true); + } } } } diff --git a/src/EFCore.Relational/Metadata/IConventionDbFunction.cs b/src/EFCore.Relational/Metadata/IConventionDbFunction.cs index c14b125607a..2a02dd4fb9d 100644 --- a/src/EFCore.Relational/Metadata/IConventionDbFunction.cs +++ b/src/EFCore.Relational/Metadata/IConventionDbFunction.cs @@ -61,7 +61,7 @@ public interface IConventionDbFunction : IConventionAnnotatable, IDbFunction ConfigurationSource? GetSchemaConfigurationSource(); /// - /// Sets the value indicating wheather the database function is built-in or not. + /// Sets the value indicating whether 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. @@ -74,6 +74,20 @@ public interface IConventionDbFunction : IConventionAnnotatable, IDbFunction /// The configuration source for . ConfigurationSource? GetIsBuiltInConfigurationSource(); + /// + /// Sets the value indicating whether the database function can return null value or not. + /// + /// The value indicating whether the database function can return null value or not. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + bool SetIsNullable(bool nullable, bool fromDataAnnotation = false); + + /// + /// Gets the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetIsNullableConfigurationSource(); + /// /// 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 0e56baf139e..c015b9677ab 100644 --- a/src/EFCore.Relational/Metadata/IDbFunction.cs +++ b/src/EFCore.Relational/Metadata/IDbFunction.cs @@ -55,6 +55,11 @@ public interface IDbFunction : IAnnotatable /// bool IsAggregate { get; } + /// + /// Gets the value indicating whether the database function can return null. + /// + bool IsNullable { get; } + /// /// Gets the configured store type string. /// diff --git a/src/EFCore.Relational/Metadata/IMutableDbFunction.cs b/src/EFCore.Relational/Metadata/IMutableDbFunction.cs index dc2e2ea5f09..8b4e6b96938 100644 --- a/src/EFCore.Relational/Metadata/IMutableDbFunction.cs +++ b/src/EFCore.Relational/Metadata/IMutableDbFunction.cs @@ -26,10 +26,15 @@ 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. + /// Gets or sets the value indicating whether the database function is built-in or not. /// new bool IsBuiltIn { get; set; } + /// + /// Gets or sets the value indicating whether the database function can return null value or not. + /// + new bool IsNullable { 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 a44b4f46019..fc18071abd5 100644 --- a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs +++ b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs @@ -30,6 +30,7 @@ public class DbFunction : ConventionAnnotatable, IMutableDbFunction, IConvention private string _schema; private string _name; private bool _builtIn; + private bool _nullable; private string _storeType; private RelationalTypeMapping _typeMapping; private Func, SqlExpression> _translation; @@ -38,6 +39,7 @@ public class DbFunction : ConventionAnnotatable, IMutableDbFunction, IConvention private ConfigurationSource? _schemaConfigurationSource; private ConfigurationSource? _nameConfigurationSource; private ConfigurationSource? _builtInConfigurationSource; + private ConfigurationSource? _nullableConfigurationSource; private ConfigurationSource? _storeTypeConfigurationSource; private ConfigurationSource? _typeMappingConfigurationSource; private ConfigurationSource? _translationConfigurationSource; @@ -113,6 +115,8 @@ public DbFunction( : parameters .Select(p => new DbFunctionParameter(this, p.Name, p.Type)) .ToList(); + + _nullable = true; } private static string GetFunctionName(MethodInfo methodInfo, ParameterInfo[] parameters) @@ -386,6 +390,45 @@ public virtual bool SetIsBuiltIn(bool builtIn, ConfigurationSource configuration /// 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 + /// 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 IsNullable + { + get => _nullable; + set => SetIsNullable(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 SetIsNullable(bool nullable, ConfigurationSource configurationSource) + { + if (!IsScalar) + { + new InvalidOperationException(RelationalStrings.NullabilityInfoOnlyAllowedOnScalarFunctions); + } + + _nullable = nullable; + _nullableConfigurationSource = configurationSource.Max(_nullableConfigurationSource); + + return nullable; + } + + /// + /// 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? GetIsNullableConfigurationSource() => _nullableConfigurationSource; + /// /// 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 @@ -605,6 +648,11 @@ string IConventionDbFunction.SetSchema(string schema, bool fromDataAnnotation) bool IConventionDbFunction.SetIsBuiltIn(bool builtIn, bool fromDataAnnotation) => SetIsBuiltIn(builtIn, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// + [DebuggerStepThrough] + bool IConventionDbFunction.SetIsNullable(bool nullable, bool fromDataAnnotation) + => SetIsNullable(nullable, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// [DebuggerStepThrough] string IConventionDbFunction.SetStoreType(string storeType, bool fromDataAnnotation) diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs b/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs index f159443271d..6d586025ee0 100644 --- a/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs +++ b/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Builders.Internal; @@ -22,9 +23,11 @@ public class DbFunctionParameter : ConventionAnnotatable, IMutableDbFunctionPara { private string _storeType; private RelationalTypeMapping _typeMapping; + private bool _propagatesNullability; private ConfigurationSource? _storeTypeConfigurationSource; private ConfigurationSource? _typeMappingConfigurationSource; + private ConfigurationSource? _propagatesNullabilityConfigurationSource; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -177,6 +180,55 @@ public virtual RelationalTypeMapping SetTypeMapping( 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 bool PropagatesNullability + { + get => _propagatesNullability; + set => SetPropagatesNullability(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 SetPropagatesNullability(bool propagatesNullability, ConfigurationSource configurationSource) + { + if (!Function.IsScalar) + { + new InvalidOperationException(RelationalStrings.NullabilityInfoOnlyAllowedOnScalarFunctions); + } + + _propagatesNullability = propagatesNullability; + + UpdatePropagatesNullabilityConfigurationSource(configurationSource); + + return propagatesNullability; + } + + /// + /// 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. + /// + private void UpdatePropagatesNullabilityConfigurationSource(ConfigurationSource configurationSource) + => _propagatesNullabilityConfigurationSource = 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? GetPropagatesNullabilityConfigurationSource() => _propagatesNullabilityConfigurationSource; + /// /// 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 diff --git a/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs index e22612d781b..ad536d64bfc 100644 --- a/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs +++ b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs @@ -116,6 +116,33 @@ public virtual bool CanSetIsBuiltIn(bool builtIn, ConfigurationSource configurat => 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 + /// 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 IsNullable(bool nullable, ConfigurationSource configurationSource) + { + if (CanSetIsNullable(nullable, configurationSource)) + { + Metadata.SetIsNullable(nullable, 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 CanSetIsNullable(bool nullable, ConfigurationSource configurationSource) + => configurationSource.Overrides(Metadata.GetIsNullableConfigurationSource()) + || Metadata.IsNullable == nullable; + /// /// 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 @@ -255,6 +282,16 @@ IConventionDbFunctionBuilder IConventionDbFunctionBuilder.IsBuiltIn(bool builtIn bool IConventionDbFunctionBuilder.CanSetIsBuiltIn(bool builtIn, bool fromDataAnnotation) => CanSetIsBuiltIn(builtIn, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// + [DebuggerStepThrough] + IConventionDbFunctionBuilder IConventionDbFunctionBuilder.IsNullable(bool nullable, bool fromDataAnnotation) + => IsNullable(nullable, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionDbFunctionBuilder.CanSetIsNullable(bool nullable, bool fromDataAnnotation) + => CanSetIsNullable(nullable, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// [DebuggerStepThrough] IConventionDbFunctionBuilder IConventionDbFunctionBuilder.HasStoreType(string storeType, bool fromDataAnnotation) diff --git a/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionParameterBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionParameterBuilder.cs index 7ece80be473..914cfc3b2cc 100644 --- a/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionParameterBuilder.cs +++ b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionParameterBuilder.cs @@ -31,6 +31,7 @@ public InternalDbFunctionParameterBuilder([NotNull] DbFunctionParameter paramete : base(parameter, modelBuilder) { } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -87,6 +88,35 @@ public virtual bool CanSetTypeMapping([CanBeNull] RelationalTypeMapping typeMapp => configurationSource.Overrides(Metadata.GetTypeMappingConfigurationSource()) || Metadata.TypeMapping == typeMapping; + /// + /// 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 IConventionDbFunctionParameterBuilder PropagatesNullability( + bool propagatesNullability, + ConfigurationSource configurationSource) + { + if (CanSetPropagatesNullability(propagatesNullability, configurationSource)) + { + Metadata.SetPropagatesNullability(propagatesNullability, 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 CanSetPropagatesNullability(bool propagatesNullability, ConfigurationSource configurationSource) + => configurationSource.Overrides(Metadata.GetPropagatesNullabilityConfigurationSource()) + || Metadata.PropagatesNullability == propagatesNullability; + /// IConventionDbFunctionParameter IConventionDbFunctionParameterBuilder.Metadata { diff --git a/src/EFCore.Relational/Metadata/Internal/StoreFunction.cs b/src/EFCore.Relational/Metadata/Internal/StoreFunction.cs index e905df86fd4..e93bc6477f7 100644 --- a/src/EFCore.Relational/Metadata/Internal/StoreFunction.cs +++ b/src/EFCore.Relational/Metadata/Internal/StoreFunction.cs @@ -29,6 +29,7 @@ public StoreFunction([NotNull] DbFunction dbFunction, [NotNull] RelationalModel { DbFunctions = new SortedDictionary() { { dbFunction.ModelName, dbFunction } }; IsBuiltIn = dbFunction.IsBuiltIn; + IsNullable = dbFunction.IsNullable; ReturnType = dbFunction.StoreType; Parameters = new StoreFunctionParameter[dbFunction.Parameters.Count]; @@ -50,6 +51,14 @@ public StoreFunction([NotNull] DbFunction dbFunction, [NotNull] RelationalModel /// public virtual bool IsBuiltIn { get; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool IsNullable { get; } + /// public virtual string ReturnType { get; } diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 40adae121bc..d9a5fc25ae8 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1026,6 +1026,12 @@ public static string TableValuedFunctionNonTPH([CanBeNull] object dbFunction, [C public static string CustomQueryMappingOnOwner => GetString("CustomQueryMappingOnOwner"); + /// + /// Nullability information should only be specified for scalar database functions. + /// + public static string NullabilityInfoOnlyAllowedOnScalarFunctions + => GetString("NullabilityInfoOnlyAllowedOnScalarFunctions"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 61c0ba4df3f..8f2e54bcdc5 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -729,4 +729,7 @@ Using 'FromSqlRaw' or 'FromSqlInterpolated' on an entity type which has owned reference navigations sharing same table is not supported. + + Nullability information should only be specified for scalar database functions. + \ No newline at end of file diff --git a/src/EFCore.Relational/Query/RelationalMethodCallTranslatorProvider.cs b/src/EFCore.Relational/Query/RelationalMethodCallTranslatorProvider.cs index 71262f8f956..02cfdcd0501 100644 --- a/src/EFCore.Relational/Query/RelationalMethodCallTranslatorProvider.cs +++ b/src/EFCore.Relational/Query/RelationalMethodCallTranslatorProvider.cs @@ -8,6 +8,7 @@ using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Utilities; @@ -82,8 +83,9 @@ public virtual SqlExpression Translate( return _sqlExpressionFactory.Function( dbFunction.Name, arguments, - nullable: true, - argumentsPropagateNullability: arguments.Select(a => false).ToList(), + nullable: dbFunction.IsNullable, + argumentsPropagateNullability: dbFunction.Parameters.Select(p => p is DbFunctionParameter fp + && fp.PropagatesNullability), method.ReturnType.UnwrapNullableType()); } @@ -91,8 +93,9 @@ public virtual SqlExpression Translate( dbFunction.Schema, dbFunction.Name, arguments, - nullable: true, - argumentsPropagateNullability: arguments.Select(a => false).ToList(), + nullable: dbFunction.IsNullable, + argumentsPropagateNullability: dbFunction.Parameters.Select(p => p is DbFunctionParameter fp + && fp.PropagatesNullability), method.ReturnType.UnwrapNullableType()); } diff --git a/src/EFCore.Relational/Query/SqlExpressions/SqlFunctionExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SqlFunctionExpression.cs index 8c61fd032a6..4d8ac7c1e94 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SqlFunctionExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SqlFunctionExpression.cs @@ -203,30 +203,35 @@ private SqlFunctionExpression( /// The name of the function. /// public virtual string Name { get; } + /// /// The schema in which the function is defined, if any. /// public virtual string Schema { get; } + /// /// A bool value indicating if the function is niladic. /// public virtual bool IsNiladic { get; } + /// /// A bool value indicating if the function is built-in. /// public virtual bool IsBuiltIn { get; } + /// /// The list of arguments of this function. /// public virtual IReadOnlyList Arguments { get; } + /// /// The instance on which this function is applied. /// public virtual SqlExpression Instance { get; } + /// /// A bool value indicating if the function can return null result. /// - public virtual bool IsNullable { get; private set; } /// diff --git a/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs index fa52a9a434a..e86aa012eaf 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs @@ -172,6 +172,15 @@ public enum ReportingPeriod [DbFunction(Schema = "dbo")] public static string IdentityString(string s) => throw new Exception(); + public static string IdentityStringPropagateNull(string s) => throw new Exception(); + + [DbFunction(IsNullable = false)] + public static string IdentityStringNonNullable(string s) => throw new Exception(); + + public static string IdentityStringNonNullableFluent(string s) => throw new Exception(); + + public string StringLength(string s) => throw new Exception(); + public int AddValues(int a, int b) { throw new NotImplementedException(); @@ -242,6 +251,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(AddValues), new[] { typeof(int), typeof(int) })); + modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(IdentityStringPropagateNull), new[] { typeof(string) })) + .HasParameter("s").PropagatesNullability(); + + modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(IdentityStringNonNullableFluent), new[] { typeof(string) })) + .IsNullable(false); + //Instance modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(CustomerOrderCountInstance))) .HasName("CustomerOrderCount"); @@ -264,6 +279,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().ToTable("MultProductOrders").HasKey(mpo => mpo.OrderId); + modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(StringLength), new[] { typeof(string) })) + .HasParameter("s").PropagatesNullability(); + //Table modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(GetCustomerOrderCountByYear), new[] { typeof(int) })); modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(GetCustomerOrderCountByYearOnlyFrom2000), new[] { typeof(int), typeof(bool) })); @@ -820,6 +838,61 @@ public virtual void Nullable_navigation_property_access_preserves_schema_for_sql Assert.Equal("Customer", result); } + [ConditionalFact] + public virtual void Compare_function_without_null_propagation_to_null() + { + using var context = CreateContext(); + + var result = context.Customers + .OrderBy(c => c.Id) + .Where(c => UDFSqlContext.IdentityString(c.FirstName) != null) + .ToList(); + + Assert.Equal(4, result.Count); + } + + [ConditionalFact] + public virtual void Compare_function_with_null_propagation_to_null() + { + using var context = CreateContext(); + + var result = context.Customers + .OrderBy(c => c.Id) + .Where(c => UDFSqlContext.IdentityStringPropagateNull(c.FirstName) != null) + .ToList(); + + Assert.Equal(4, result.Count); + } + + [ConditionalFact] + public virtual void Compare_non_nullable_function_to_null_gets_optimized() + { + using var context = CreateContext(); + + var result = context.Customers + .OrderBy(c => c.Id) + .Where(c => UDFSqlContext.IdentityStringNonNullable(c.FirstName) != null + && UDFSqlContext.IdentityStringNonNullableFluent(c.FirstName) != null) + .ToList(); + + Assert.Equal(4, result.Count); + } + + [ConditionalFact] + public virtual void Compare_functions_returning_int_that_take_nullable_param_which_propagates_null() + { + using var context = CreateContext(); + + //var prm = default(string); + + var result = context.Customers + .OrderBy(c => c.Id) + .Where(c => context.StringLength(c.FirstName) != context.StringLength(c.LastName)) + .ToList(); + + Assert.Equal(4, result.Count); + } + [ConditionalFact] public virtual void Scalar_Function_SqlFragment_Static() { @@ -1947,8 +2020,8 @@ public virtual void TVF_backing_entity_type_mapped_to_view() using (var context = CreateContext()) { var customers = (from t in context.Set() - orderby t.FirstName - select t).ToList(); + orderby t.FirstName + select t).ToList(); Assert.Equal(4, customers.Count); } @@ -1961,9 +2034,9 @@ public virtual void Udf_with_argument_being_comparison_to_null_parameter() { var prm = default(string); var query = (from c in context.Customers - from r in context.GetCustomerOrderCountByYearOnlyFrom2000(c.Id, c.LastName != prm) - orderby r.Year - select r + from r in context.GetCustomerOrderCountByYearOnlyFrom2000(c.Id, c.LastName != prm) + orderby r.Year + select r ).ToList(); Assert.Equal(4, query.Count); @@ -1996,9 +2069,9 @@ select r ClearLog(); var query = (from a in context.Addresses - from r in context.GetCustomerOrderCountByYearOnlyFrom2000(1, a.City == a.State) - orderby a.Id, r.Year - select r + from r in context.GetCustomerOrderCountByYearOnlyFrom2000(1, a.City == a.State) + orderby a.Id, r.Year + select r ).ToList(); Assert.Equal(expected.Count, query.Count); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs b/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs index 0cdd309203c..a7736cafb89 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs @@ -219,6 +219,50 @@ FROM [Orders] AS [o] ORDER BY [o].[Id]"); } + public override void Compare_function_without_null_propagation_to_null() + { + base.Compare_function_without_null_propagation_to_null(); + + AssertSql( + @"SELECT [c].[Id], [c].[FirstName], [c].[LastName] +FROM [Customers] AS [c] +WHERE [dbo].[IdentityString]([c].[FirstName]) IS NOT NULL +ORDER BY [c].[Id]"); + } + + public override void Compare_function_with_null_propagation_to_null() + { + base.Compare_function_with_null_propagation_to_null(); + + AssertSql( + @"SELECT [c].[Id], [c].[FirstName], [c].[LastName] +FROM [Customers] AS [c] +WHERE [c].[FirstName] IS NOT NULL +ORDER BY [c].[Id]"); + } + + public override void Compare_non_nullable_function_to_null_gets_optimized() + { + base.Compare_non_nullable_function_to_null_gets_optimized(); + + AssertSql( + @"SELECT [c].[Id], [c].[FirstName], [c].[LastName] +FROM [Customers] AS [c] +ORDER BY [c].[Id]"); + } + + public override void Compare_functions_returning_int_that_take_nullable_param_which_propagates_null() + { + base.Compare_functions_returning_int_that_take_nullable_param_which_propagates_null(); + + AssertSql( + @"SELECT [c].[Id], [c].[FirstName], [c].[LastName] +FROM [Customers] AS [c] +WHERE (([dbo].[StringLength]([c].[FirstName]) <> [dbo].[StringLength]([c].[LastName])) OR ([c].[FirstName] IS NULL OR [c].[LastName] IS NULL)) AND ([c].[FirstName] IS NOT NULL OR [c].[LastName] IS NOT NULL) +ORDER BY [c].[Id]"); + } + + public override void Scalar_Function_SqlFragment_Static() { base.Scalar_Function_SqlFragment_Static(); @@ -890,11 +934,43 @@ returns bit end"); context.Database.ExecuteSqlRaw( - @"create function [dbo].[IdentityString] (@customerName nvarchar(max)) + @"create function [dbo].[IdentityString] (@s nvarchar(max)) + returns nvarchar(max) + as + begin + return @s; + end"); + + context.Database.ExecuteSqlRaw( + @"create function [dbo].[IdentityStringPropagatesNull] (@s nvarchar(max)) + returns nvarchar(max) + as + begin + return @s; + end"); + + context.Database.ExecuteSqlRaw( + @"create function [dbo].[IdentityStringNonNullable] (@s nvarchar(max)) + returns nvarchar(max) + as + begin + return COALESCE(@s, 'NULL'); + end"); + + context.Database.ExecuteSqlRaw( + @"create function [dbo].[IdentityStringNonNullableFluent] (@s nvarchar(max)) returns nvarchar(max) as begin - return @customerName; + return COALESCE(@s, 'NULL'); + end"); + + context.Database.ExecuteSqlRaw( + @"create function [dbo].[StringLength] (@s nvarchar(max)) + returns int + as + begin + return LEN(@s); end"); context.Database.ExecuteSqlRaw(