From 32bfb11c6f0b734e6b2764f1fe1932b8c1b40f93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 23:14:15 +0000 Subject: [PATCH 1/6] Initial plan From 8e96d0ba19458e883493b543c6ebc309e78f78b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 23:19:38 +0000 Subject: [PATCH 2/6] Document user-defined function mapping options Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../querying/user-defined-function-mapping.md | 46 ++++++++++++++++--- .../UserDefinedFunctionMapping/Model.cs | 31 ++++++++++++- .../UserDefinedFunctionMapping/Program.cs | 5 ++ 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/entity-framework/core/querying/user-defined-function-mapping.md b/entity-framework/core/querying/user-defined-function-mapping.md index e76dbd7ef6..782f2ef31d 100644 --- a/entity-framework/core/querying/user-defined-function-mapping.md +++ b/entity-framework/core/querying/user-defined-function-mapping.md @@ -2,7 +2,7 @@ title: User-defined function mapping - EF Core description: Mapping user-defined functions to database functions author: SamMonoRT -ms.date: 11/23/2020 +ms.date: 08/19/2026 uid: core/querying/user-defined-function-mapping --- # User-defined function mapping @@ -46,11 +46,13 @@ The body of the CLR method is not important. The method will not be invoked clie > [!NOTE] > In the example, the method is defined on `DbContext`, but it can also be defined as a static method inside other classes. -This function definition can now be associated with user-defined function in the model configuration: +This function definition can now be associated with a user-defined function in the model configuration: [!code-csharp[Main](../../../samples/core/Querying/UserDefinedFunctionMapping/Model.cs#BasicFunctionConfiguration)] -By default, EF Core tries to map CLR function to a user-defined function with the same name. If the names differ, we can use `HasName` to provide the correct name for the user-defined function we want to map to. +The lambda overload of avoids manually looking up the `MethodInfo`. The `default` argument values are only used to identify the method; they are never sent to the database. + +By default, EF Core maps the CLR method to a database function with the same name in the default schema. Use and when the name or schema differs. Now, executing the following query: @@ -64,9 +66,36 @@ FROM [Blogs] AS [b] WHERE [dbo].[CommentedPostCountForBlog]([b].[BlogId]) > 1 ``` +## Mapping a built-in function + +A static method on the context can also be mapped by applying . The attribute's `Name`, `Schema`, `IsBuiltIn`, and `IsNullable` properties configure the corresponding characteristics of the database function. The fluent API methods `HasName`, `HasSchema`, `IsBuiltIn`, and `IsNullable` provide the same configuration. + +For example, the following method maps SQL Server's built-in `JSON_VALUE` function. Because `IsBuiltIn` is `true`, EF Core emits the function name without a schema. + +[!code-csharp[Main](../../../samples/core/Querying/UserDefinedFunctionMapping/Model.cs#JsonFunctionDefinition)] + +### Configuring store types + +Use to configure a function's return store type and to configure a parameter's store type. This is particularly useful when the CLR parameter type has no native database mapping. + +In this example, `JsonEntity.Metadata` is a dictionary stored as `nvarchar(max)` through a value converter. The `json` function parameter has the same store type, while the result uses the `nvarchar(4000)` type returned by `JSON_VALUE`: + +[!code-csharp[Main](../../../samples/core/Querying/UserDefinedFunctionMapping/Model.cs#JsonFunctionConfiguration)] + +The function can then be used with the converted property: + +[!code-csharp[Main](../../../samples/core/Querying/UserDefinedFunctionMapping/Program.cs#JsonFunctionQuery)] + +```sql +SELECT JSON_VALUE([j].[Metadata], N'$.Filter') +FROM [JsonEntities] AS [j] +``` + +The value converter is taken from the expression passed as the function argument. Therefore, this pattern works for a mapped property such as `JsonEntity.Metadata`, but configuring the parameter store type does not make arbitrary dictionary values translatable. Convert in-memory values to the provider type before passing them as parameters. + ## Mapping a method to a custom SQL -EF Core also allows for user-defined functions that get converted to a specific SQL. The SQL expression is provided using `HasTranslation` method during user-defined function configuration. +EF Core also allows a CLR method to be translated directly to a SQL expression rather than a database function. The SQL expression is provided using during function configuration. In the example below, we'll create a function that computes percentage difference between two integers. @@ -89,9 +118,12 @@ SELECT 100 * (ABS(CAST([p].[BlogId] AS float) - 3) / ((CAST([p].[BlogId] AS floa FROM [Posts] AS [p] ``` +> [!CAUTION] +> `HasTranslation` works with the SQL expression tree, not SQL text. The translation must construct valid objects with the correct type mappings, nullability, and argument nullability propagation. Incorrect metadata can produce invalid SQL or incorrect query results, and the expression types used by a translation may be specific to a database provider. Use this low-level API only after understanding the provider's SQL expression tree; prefer a regular function mapping or an existing provider translation when possible. + ## Configuring nullability of user-defined function based on its arguments -If the user-defined function can only return `null` when one or more of its arguments are `null`, EFCore provides way to specify that, resulting in more performant SQL. It can be done by adding a `PropagatesNullability()` call to the relevant function parameters model configuration. +If nullability propagates from a function argument—that is, the function returns `null` whenever that argument is `null`—EF Core can generate more efficient SQL. Configure this by calling for the relevant parameters. For more information about how EF Core compensates for SQL's three-valued logic, see [Query null semantics](xref:core/querying/null-comparisons). To illustrate this, define user function `ConcatStrings`: @@ -133,7 +165,7 @@ WHERE ([dbo].[ConcatStrings]([b].[Url], CONVERT(VARCHAR(11), [b].[Rating])) <> N The second query doesn't need to re-evaluate the function itself to test its nullability. > [!NOTE] -> This optimization should only be used if the function can only return `null` when it's parameters are `null`. +> Only configure nullability propagation when the function can return `null` solely because one or more of the configured parameters are `null`. ## Mapping a queryable function to a table-valued function @@ -168,7 +200,7 @@ And below is the mapping: [!code-csharp[Main](../../../samples/core/Querying/UserDefinedFunctionMapping/Model.cs#QueryableFunctionConfigurationHasDbFunction)] > [!NOTE] -> A queryable function must be mapped to a table-valued function and can't make use of `HasTranslation`. +> A queryable function must be mapped to a table-valued function. `HasTranslation` supports scalar functions only and can't be used for a table-valued function. When the function is mapped, the following query: diff --git a/samples/core/Querying/UserDefinedFunctionMapping/Model.cs b/samples/core/Querying/UserDefinedFunctionMapping/Model.cs index d689449518..5c2edfa6b2 100644 --- a/samples/core/Querying/UserDefinedFunctionMapping/Model.cs +++ b/samples/core/Querying/UserDefinedFunctionMapping/Model.cs @@ -3,6 +3,7 @@ using System.Data; using System.Linq; using System.Linq.Expressions; +using System.Text.Json; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; @@ -42,17 +43,30 @@ public class Comment } #endregion +public class JsonEntity +{ + public int Id { get; set; } + public Dictionary Metadata { get; set; } +} + public class BloggingContext : DbContext { public DbSet Blogs { get; set; } public DbSet Posts { get; set; } public DbSet Comments { get; set; } + public DbSet JsonEntities { get; set; } #region BasicFunctionDefinition public int ActivePostCountForBlog(int blogId) => throw new NotSupportedException(); #endregion + #region JsonFunctionDefinition + [DbFunction(Name = "JSON_VALUE", IsBuiltIn = true, IsNullable = true)] + public static string JsonValue(Dictionary json, string path) + => throw new NotSupportedException(); + #endregion + #region HasTranslationFunctionDefinition public double PercentageDifference(double first, int second) => throw new NotSupportedException(); @@ -139,8 +153,21 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) new Comment { CommentId = 6, PostId = 3, Text = "I couldn't agree with you more", Likes = 2 }); #region BasicFunctionConfiguration - modelBuilder.HasDbFunction(typeof(BloggingContext).GetMethod(nameof(ActivePostCountForBlog), [typeof(int)])) - .HasName("CommentedPostCountForBlog"); + modelBuilder.HasDbFunction(() => ActivePostCountForBlog(default)) + .HasName("CommentedPostCountForBlog") + .HasSchema("dbo"); + #endregion + + #region JsonFunctionConfiguration + modelBuilder.Entity() + .Property(e => e.Metadata) + .HasConversion( + value => JsonSerializer.Serialize(value, (JsonSerializerOptions)null), + value => JsonSerializer.Deserialize>(value, (JsonSerializerOptions)null)); + + var jsonValueFunction = modelBuilder.HasDbFunction(() => JsonValue(default, default)); + jsonValueFunction.HasStoreType("nvarchar(4000)"); + jsonValueFunction.HasParameter("json").HasStoreType("nvarchar(max)"); #endregion #region HasTranslationFunctionConfiguration diff --git a/samples/core/Querying/UserDefinedFunctionMapping/Program.cs b/samples/core/Querying/UserDefinedFunctionMapping/Program.cs index 00ad3e3d38..47af5cd3ac 100644 --- a/samples/core/Querying/UserDefinedFunctionMapping/Program.cs +++ b/samples/core/Querying/UserDefinedFunctionMapping/Program.cs @@ -54,6 +54,11 @@ where context.ActivePostCountForBlog(b.BlogId) > 1 #endregion var result1 = await query1.ToListAsync(); + #region JsonFunctionQuery + var jsonQuery = context.JsonEntities.Select(e => BloggingContext.JsonValue(e.Metadata, "$.Filter")); + #endregion + var jsonResults = await jsonQuery.ToListAsync(); + #region HasTranslationQuery var query2 = from p in context.Posts select context.PercentageDifference(p.BlogId, 3); From f71d4100b673fbad46ad01d3c99ce4005e702f61 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 23:26:06 +0000 Subject: [PATCH 3/6] Clarify converted function arguments Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- entity-framework/core/querying/user-defined-function-mapping.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entity-framework/core/querying/user-defined-function-mapping.md b/entity-framework/core/querying/user-defined-function-mapping.md index 782f2ef31d..60e8220eb5 100644 --- a/entity-framework/core/querying/user-defined-function-mapping.md +++ b/entity-framework/core/querying/user-defined-function-mapping.md @@ -91,7 +91,7 @@ SELECT JSON_VALUE([j].[Metadata], N'$.Filter') FROM [JsonEntities] AS [j] ``` -The value converter is taken from the expression passed as the function argument. Therefore, this pattern works for a mapped property such as `JsonEntity.Metadata`, but configuring the parameter store type does not make arbitrary dictionary values translatable. Convert in-memory values to the provider type before passing them as parameters. +The value converter is taken from the expression passed as the function argument. Therefore, this pattern works for a mapped property such as `JsonEntity.Metadata`, but configuring the parameter store type does not make arbitrary dictionary values translatable. To use an in-memory dictionary, serialize it and pass the resulting string to a separately mapped method whose CLR parameter is `string`. ## Mapping a method to a custom SQL From 10f37e12ce0f50d22d0f50eb98aadd4d918488ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Aug 2026 01:22:00 +0000 Subject: [PATCH 4/6] Add ValueComparer for JsonEntity.Metadata dictionary Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- samples/core/Querying/UserDefinedFunctionMapping/Model.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/samples/core/Querying/UserDefinedFunctionMapping/Model.cs b/samples/core/Querying/UserDefinedFunctionMapping/Model.cs index 5c2edfa6b2..6d078fc0da 100644 --- a/samples/core/Querying/UserDefinedFunctionMapping/Model.cs +++ b/samples/core/Querying/UserDefinedFunctionMapping/Model.cs @@ -5,6 +5,7 @@ using System.Linq.Expressions; using System.Text.Json; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; @@ -163,7 +164,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .Property(e => e.Metadata) .HasConversion( value => JsonSerializer.Serialize(value, (JsonSerializerOptions)null), - value => JsonSerializer.Deserialize>(value, (JsonSerializerOptions)null)); + value => JsonSerializer.Deserialize>(value, (JsonSerializerOptions)null), + new ValueComparer>( + (c1, c2) => c1.Count == c2.Count && !c1.Except(c2).Any(), + c => c.Aggregate(0, (a, kvp) => a ^ HashCode.Combine(kvp.Key, kvp.Value)), + c => c.ToDictionary(kvp => kvp.Key, kvp => kvp.Value))); var jsonValueFunction = modelBuilder.HasDbFunction(() => JsonValue(default, default)); jsonValueFunction.HasStoreType("nvarchar(4000)"); From b95629a162a24b26d4a7d1ecf63164a90324d162 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 02:43:43 +0000 Subject: [PATCH 5/6] Refocus section on DbFunctionAttribute rather than IsBuiltIn Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../core/querying/user-defined-function-mapping.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/entity-framework/core/querying/user-defined-function-mapping.md b/entity-framework/core/querying/user-defined-function-mapping.md index 60e8220eb5..a5f129b328 100644 --- a/entity-framework/core/querying/user-defined-function-mapping.md +++ b/entity-framework/core/querying/user-defined-function-mapping.md @@ -66,11 +66,11 @@ FROM [Blogs] AS [b] WHERE [dbo].[CommentedPostCountForBlog]([b].[BlogId]) > 1 ``` -## Mapping a built-in function +## Mapping a function using DbFunctionAttribute -A static method on the context can also be mapped by applying . The attribute's `Name`, `Schema`, `IsBuiltIn`, and `IsNullable` properties configure the corresponding characteristics of the database function. The fluent API methods `HasName`, `HasSchema`, `IsBuiltIn`, and `IsNullable` provide the same configuration. +Instead of registering a function in `OnModelCreating`, a static method can be mapped directly by applying . The attribute's `Name`, `Schema`, `IsBuiltIn`, and `IsNullable` properties configure the corresponding characteristics of the database function; these are the same characteristics configured by the `HasName`, `HasSchema`, `IsBuiltIn`, and `IsNullable` fluent API methods when using `HasDbFunction`. Methods annotated with `DbFunctionAttribute` are discovered and registered automatically, so no call to `HasDbFunction` is required. -For example, the following method maps SQL Server's built-in `JSON_VALUE` function. Because `IsBuiltIn` is `true`, EF Core emits the function name without a schema. +For example, the following method uses `DbFunctionAttribute` to map SQL Server's built-in `JSON_VALUE` function. Because `IsBuiltIn` is `true`, EF Core emits the function name without a schema. [!code-csharp[Main](../../../samples/core/Querying/UserDefinedFunctionMapping/Model.cs#JsonFunctionDefinition)] From 735b8e2d291a4cf3c33068cb96a9c292e07c8c06 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Thu, 20 Aug 2026 20:57:23 -0700 Subject: [PATCH 6/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- entity-framework/core/querying/user-defined-function-mapping.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entity-framework/core/querying/user-defined-function-mapping.md b/entity-framework/core/querying/user-defined-function-mapping.md index a5f129b328..7adccc2487 100644 --- a/entity-framework/core/querying/user-defined-function-mapping.md +++ b/entity-framework/core/querying/user-defined-function-mapping.md @@ -68,7 +68,7 @@ WHERE [dbo].[CommentedPostCountForBlog]([b].[BlogId]) > 1 ## Mapping a function using DbFunctionAttribute -Instead of registering a function in `OnModelCreating`, a static method can be mapped directly by applying . The attribute's `Name`, `Schema`, `IsBuiltIn`, and `IsNullable` properties configure the corresponding characteristics of the database function; these are the same characteristics configured by the `HasName`, `HasSchema`, `IsBuiltIn`, and `IsNullable` fluent API methods when using `HasDbFunction`. Methods annotated with `DbFunctionAttribute` are discovered and registered automatically, so no call to `HasDbFunction` is required. +Instead of registering a function in `OnModelCreating`, a static method declared on the `DbContext` can be mapped directly by applying . The attribute's `Name`, `Schema`, `IsBuiltIn`, and `IsNullable` properties configure the corresponding characteristics of the database function; these are the same characteristics configured by the `HasName`, `HasSchema`, `IsBuiltIn`, and `IsNullable` fluent API methods when using `HasDbFunction`. Attributed methods on the context are discovered and registered automatically; attributed methods on other classes must still be registered with `HasDbFunction`. Call `HasDbFunction` for an automatically registered method only when a builder is needed for additional fluent configuration, as in the store-type example below. For example, the following method uses `DbFunctionAttribute` to map SQL Server's built-in `JSON_VALUE` function. Because `IsBuiltIn` is `true`, EF Core emits the function name without a schema.