From 3d869fb6bc0d43d3840b1016b9645bdca1fcf1b4 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 16 Nov 2022 20:38:38 +0100 Subject: [PATCH] Detect DbFunction returning a nullable value expression (#29588) Closes #29585 --- .../Properties/RelationalStrings.Designer.cs | 8 ++++++ .../Properties/RelationalStrings.resx | 3 ++ .../RelationalMethodCallTranslatorProvider.cs | 11 +++++++- .../Query/UdfDbFunctionTestBase.cs | 28 +++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index c412f98f98d..b7ef50f00fa 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -279,6 +279,14 @@ public static string DbFunctionNonScalarCustomTranslation(object? function) GetString("DbFunctionNonScalarCustomTranslation", nameof(function)), function); + /// + /// The DbFunction '{function}' returns a SqlExpression of type '{type}', which is a nullable value type. DbFunctions must return expressions with non-nullable value types, even when they may return null. + /// + public static string DbFunctionNullableValueReturnType(object? function, object? type) + => string.Format( + GetString("DbFunctionNullableValueReturnType", nameof(function), nameof(type)), + function, type); + /// /// The default value SQL has not been specified for the column '{table}.{column}'. Specify the SQL before using Entity Framework to create the database schema. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 2b06d916915..d10d59b928c 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -217,6 +217,9 @@ Cannot set custom translation on the DbFunction '{function}' since it is not a scalar function. + + The DbFunction '{function}' returns a SqlExpression of type '{type}', which is a nullable value type. DbFunctions must return expressions with non-nullable value types, even when they may return null. + The default value SQL has not been specified for the column '{table}.{column}'. Specify the SQL before using Entity Framework to create the database schema. diff --git a/src/EFCore.Relational/Query/RelationalMethodCallTranslatorProvider.cs b/src/EFCore.Relational/Query/RelationalMethodCallTranslatorProvider.cs index 566f3d12bf8..1de2f16a1f3 100644 --- a/src/EFCore.Relational/Query/RelationalMethodCallTranslatorProvider.cs +++ b/src/EFCore.Relational/Query/RelationalMethodCallTranslatorProvider.cs @@ -60,8 +60,17 @@ public RelationalMethodCallTranslatorProvider(RelationalMethodCallTranslatorProv { if (dbFunction.Translation != null) { - return dbFunction.Translation.Invoke( + var translation = dbFunction.Translation.Invoke( arguments.Select(e => _sqlExpressionFactory.ApplyDefaultTypeMapping(e)).ToList()); + + if (translation.Type.IsNullableValueType()) + { + throw new InvalidOperationException( + RelationalStrings.DbFunctionNullableValueReturnType( + dbFunction.ModelName, dbFunction.ReturnType.ShortDisplayName())); + } + + return translation; } var argumentsPropagateNullability = dbFunction.Parameters.Select(p => p.PropagatesNullability); diff --git a/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs index 9f524110d90..ce2cc778e72 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs @@ -224,6 +224,9 @@ public static string IdentityStringNonNullable(string s) public static string IdentityStringNonNullableFluent(string s) => throw new Exception(); + public static int? NullableValueReturnType() + => throw new NotImplementedException(); + public string StringLength(string s) => throw new Exception(); @@ -310,6 +313,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) negated: false, typeMapping: null)); + modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(NullableValueReturnType), Array.Empty())) + .HasTranslation( + _ => new SqlFunctionExpression( + "foo", + nullable: true, + typeof(int?), + typeMapping: null)); + //Instance modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(CustomerOrderCountInstance))) .HasName("CustomerOrderCount"); @@ -1009,6 +1020,23 @@ public virtual void Scalar_Function_with_nested_InExpression_translation() Assert.Equal(4, query.Count); } +#if RELEASE + [ConditionalFact] + public virtual void Scalar_Function_with_nullable_value_return_type_throws() + { + using var context = CreateContext(); + + var exception = Assert.Throws( + () => context.Customers.Where(c => c.Id == UDFSqlContext.NullableValueReturnType()).ToList()); + + Assert.Equal( + RelationalStrings.DbFunctionNullableValueReturnType( + "Microsoft.EntityFrameworkCore.Query.UdfDbFunctionTestBase+UDFSqlContext.NullableValueReturnType()", + "int?"), + exception.Message); + } +#endif + #endregion #region Instance