Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 39 additions & 7 deletions entity-framework/core/querying/user-defined-function-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <xref:Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions.HasDbFunction*> 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 <xref:Microsoft.EntityFrameworkCore.Metadata.Builders.DbFunctionBuilderBase.HasName*> and <xref:Microsoft.EntityFrameworkCore.Metadata.Builders.DbFunctionBuilderBase.HasSchema*> when the name or schema differs.

Now, executing the following query:

Expand All @@ -64,9 +66,36 @@ FROM [Blogs] AS [b]
WHERE [dbo].[CommentedPostCountForBlog]([b].[BlogId]) > 1
```

## Mapping a built-in function
Comment thread
AndriySvyryd marked this conversation as resolved.
Outdated

A static method on the context can also be mapped by applying <xref:Microsoft.EntityFrameworkCore.DbFunctionAttribute>. 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 <xref:Microsoft.EntityFrameworkCore.Metadata.Builders.DbFunctionBuilder.HasStoreType*> to configure a function's return store type and <xref:Microsoft.EntityFrameworkCore.Metadata.Builders.DbFunctionParameterBuilder.HasStoreType*> 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. 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

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 <xref:Microsoft.EntityFrameworkCore.Metadata.Builders.DbFunctionBuilder.HasTranslation*> during function configuration.

In the example below, we'll create a function that computes percentage difference between two integers.

Expand All @@ -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 <xref:Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlExpression> 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 <xref:Microsoft.EntityFrameworkCore.Metadata.Builders.DbFunctionParameterBuilder.PropagatesNullability*> 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`:

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:

Expand Down
31 changes: 29 additions & 2 deletions samples/core/Querying/UserDefinedFunctionMapping/Model.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -42,17 +43,30 @@ public class Comment
}
#endregion

public class JsonEntity
{
public int Id { get; set; }
public Dictionary<string, string> Metadata { get; set; }
}
Comment thread
AndriySvyryd marked this conversation as resolved.

public class BloggingContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
public DbSet<Comment> Comments { get; set; }
public DbSet<JsonEntity> 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<string, string> json, string path)
=> throw new NotSupportedException();
#endregion

#region HasTranslationFunctionDefinition
public double PercentageDifference(double first, int second)
=> throw new NotSupportedException();
Expand Down Expand Up @@ -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<JsonEntity>()
.Property(e => e.Metadata)
.HasConversion(
value => JsonSerializer.Serialize(value, (JsonSerializerOptions)null),
value => JsonSerializer.Deserialize<Dictionary<string, string>>(value, (JsonSerializerOptions)null));
Comment thread
AndriySvyryd marked this conversation as resolved.
Outdated

var jsonValueFunction = modelBuilder.HasDbFunction(() => JsonValue(default, default));
jsonValueFunction.HasStoreType("nvarchar(4000)");
jsonValueFunction.HasParameter("json").HasStoreType("nvarchar(max)");
#endregion

#region HasTranslationFunctionConfiguration
Expand Down
5 changes: 5 additions & 0 deletions samples/core/Querying/UserDefinedFunctionMapping/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading