-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding documentation for custom function mapping
Fixes #500
- Loading branch information
Showing
11 changed files
with
520 additions
and
7 deletions.
There are no files selected for viewing
202 changes: 202 additions & 0 deletions
202
entity-framework/core/modeling/custom-function-mapping.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
--- | ||
title: Custom function mapping - EF Core | ||
description: Mapping user-defined functions to database functions | ||
author: maumar | ||
ms.date: 11/23/2020 | ||
uid: core/modeling/custom-function-mapping | ||
--- | ||
# Custom function mapping | ||
|
||
EF Core allows for using user-defined SQL functions in queries. To do that, the functions need to be mapped to a CLR method during model configuration. When translating the LINQ query to SQL, user-defined function will be called instead of the CLR function it has been mapped to. | ||
|
||
## Mapping method to a SQL function | ||
|
||
To illustrate the custom function mapping, lets define the following entities: | ||
|
||
[!code-csharp[Main](../../../samples/core/Modeling/CustomFunctionMapping/Blog.cs#Entity)] | ||
|
||
[!code-csharp[Main](../../../samples/core/Modeling/CustomFunctionMapping/Post.cs#Entity)] | ||
|
||
[!code-csharp[Main](../../../samples/core/Modeling/CustomFunctionMapping/Tag.cs#Entity)] | ||
|
||
And the following model configuration: | ||
|
||
[!code-csharp[Main](../../../samples/core/Modeling/CustomFunctionMapping/BloggingContext.cs#EntityConfiguration)] | ||
|
||
Blog can have many posts, each post can be tagged with multiple tags, and each tag can be associated with multiple posts: many-to-many relationship. | ||
|
||
Next, create the user-defined function `DistinctTagsCountForBlogPosts`, which returns the count of unique tags associated with all the posts of a given blog, based on the blog `Id`: | ||
|
||
```sql | ||
CREATE FUNCTION dbo.DistinctTagsCountForBlogPosts(@id int) | ||
RETURNS int | ||
AS | ||
BEGIN | ||
RETURN (SELECT COUNT(*) FROM( | ||
SELECT DISTINCT t.TagId FROM dbo.Tags AS t | ||
JOIN dbo.PostTag AS pt ON t.TagId = pt.TagId | ||
JOIN dbo.Posts AS p ON p.PostId = pt.PostId | ||
JOIN dbo.Blogs AS b ON b.BlogId = p.BlogId | ||
WHERE b.BlogId = @id) AS subquery); | ||
END | ||
``` | ||
|
||
To use it in EF Core, define the following CLR method, which we will map to the user-defined function: | ||
|
||
[!code-csharp[Main](../../../samples/core/Modeling/CustomFunctionMapping/BloggingContext.cs#BasicFunctionDefinition)] | ||
|
||
In the example, the method is defined on `DbContext`, but it can also be defined in other places. | ||
|
||
> [!NOTE] | ||
> Body of the CLR method is not important. EF Core only looks at the method signature. | ||
This function definition can be associated with user-defined function in the model configuration: | ||
|
||
[!code-csharp[Main](../../../samples/core/Modeling/CustomFunctionMapping/BloggingContext.cs#BasicFunctionConfiguration)] | ||
|
||
> [!NOTE] | ||
> By default EF Core tries to map CLR function to a user-defined function with the same name. If the names are different, we can use `HasName` to select the correct name for the user-defined function we want to map to. | ||
Now, executing the following query: | ||
|
||
[!code-csharp[Main](../../../samples/core/Modeling/CustomFunctionMapping/Program.cs#BasicQuery)] | ||
|
||
Will produce this SQL: | ||
|
||
```sql | ||
SELECT [b].[BlogId], [b].[Rating], [b].[Url] | ||
FROM [Blogs] AS [b] | ||
WHERE [dbo].[DistinctTagsCountForBlogPosts]([b].[BlogId]) > 2 | ||
``` | ||
|
||
## Mapping method to function defined in the model | ||
|
||
EF Core also allows for user-defined functions that don't map to the corresponding function in the database. It can be done by specifying the function body using the [Microsoft.EntityFrameworkCore.Query.SqlExpressions](dotnet/api/microsoft.entityframeworkcore.query.sqlexpressions) API. Function body is provided using `HasTranslation` method during user-defined function configuration. | ||
|
||
In the example below, we'll create a function that computes difference between two integers. | ||
|
||
CLR method is as follows: | ||
|
||
[!code-csharp[Main](../../../samples/core/Modeling/CustomFunctionMapping/BloggingContext.cs#HasTranslationFunctionDefinition)] | ||
|
||
The function definition is as follows: | ||
|
||
[!code-csharp[Main](../../../samples/core/Modeling/CustomFunctionMapping/BloggingContext.cs#HasTranslationFunctionConfiguration)] | ||
|
||
[SqlExpressionFactory](dotnet/api/microsoft.entityframeworkcore.query.sqlexpressionfactory) can be used to construct `SqlExpression` tree. | ||
|
||
Once we define the function, it can be used in the query. Instead of calling database function, EF Core will translate the method body directly into SQL. | ||
|
||
[!code-csharp[Main](../../../samples/core/Modeling/CustomFunctionMapping/Program.cs#HasTranslationQuery)] | ||
|
||
Produces the following SQL: | ||
|
||
```sql | ||
SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title] | ||
FROM [Posts] AS [p] | ||
WHERE [p].[PostId] < ABS([p].[BlogId] - 3) | ||
``` | ||
|
||
## Mapping DbSet to a Table-Valued Function | ||
|
||
It's also possible to map a `DbSet` of entities to a Table-Valued function instead of a table in the database. To illustrate this lets define another entity that represents blog with multiple posts. In the example, the entity is [keyless](keyless-entity-types), but it doesn't have to be. | ||
|
||
[!code-csharp[Main](../../../samples/core/Modeling/CustomFunctionMapping/BlogWithMultiplePosts.cs#Entity)] | ||
|
||
Next, create the following Table-Valued Function on the database, which returns only blogs with multiple posts as well as the number of posts associated with each of these blogs: | ||
|
||
```sql | ||
CREATE FUNCTION dbo.BlogsWithMultiplePosts() | ||
RETURNS @blogs TABLE | ||
( | ||
Rating int, | ||
Url nvarchar(max), | ||
PostCount int not null | ||
) | ||
AS | ||
BEGIN | ||
INSERT INTO @blogs | ||
SELECT b.Rating, b.Url, COUNT(p.BlogId) | ||
FROM Blogs AS b | ||
JOIN Posts AS p ON b.BlogId = p.BlogId | ||
GROUP BY b.Rating, b.Url | ||
HAVING COUNT(p.BlogId) > 1 | ||
|
||
RETURN | ||
END | ||
``` | ||
|
||
Now, the `DbSet<BlogWithMultiplePost>` can be mapped to this function in a following way: | ||
|
||
[!code-csharp[Main](../../../samples/core/Modeling/CustomFunctionMapping/BloggingContext.cs#QueryableFunctionConfigurationToFunction)] | ||
|
||
> [!NOTE] | ||
> In order to map DbSet to a Table-Valued Function the function must be parameterless. Also, names of the entity properties should match the names of the columns returned by the TVF. Any discrepancies can be configured using `HasColumnName` method, just like mapping to a regular table. | ||
When the set is mapped to a Table-Valued function, the query: | ||
|
||
[!code-csharp[Main](../../../samples/core/Modeling/CustomFunctionMapping/Program.cs#ToFunctionQuery)] | ||
|
||
Produces the following SQL: | ||
|
||
```sql | ||
SELECT [b].[Url], [b].[PostCount] | ||
FROM [dbo].[BlogsWithMultiplePosts]() AS [b] | ||
WHERE [b].[Rating] > 3 | ||
``` | ||
|
||
## Mapping Queryable function to a Table-Valued Function | ||
|
||
EF Core also supports mapping to Table-Valued Function using a user-defined CLR function returning `IQueryable` of entity types. This allows EF Core to use Table-Valued Function with parameters. Process is similar to mapping a scalar user-defined function to a SQL function. We need a Table-Valued function on the database, CLR function that will be used in the LINQ queries and mapping between the two. | ||
|
||
As an example we'll use a Table-Valued Function that returns all posts marked with a specific tag: | ||
|
||
```sql | ||
CREATE FUNCTION dbo.PostsTaggedWith(@tag varchar(max)) | ||
RETURNS @posts TABLE | ||
( | ||
PostId int not null, | ||
BlogId int not null, | ||
Content nvarchar(max), | ||
Rating int not null, | ||
Title nvarchar(max) | ||
) | ||
AS | ||
BEGIN | ||
INSERT INTO @posts | ||
SELECT p.PostId, p.BlogId, p.Content, p.Rating, p.Title | ||
FROM Posts AS p | ||
WHERE EXISTS ( | ||
SELECT 1 | ||
FROM PostTag AS pt | ||
INNER JOIN Tags AS t ON pt.TagId = t.TagId | ||
WHERE (p.PostId = pt.PostId) AND (t.TagId = @tag)) | ||
|
||
RETURN | ||
END | ||
``` | ||
|
||
CLR function signature is as follows: | ||
|
||
[!code-csharp[Main](../../../samples/core/Modeling/CustomFunctionMapping/BloggingContext.cs#QueryableFunctionDefinition)] | ||
|
||
And below is the mapping: | ||
|
||
[!code-csharp[Main](../../../samples/core/Modeling/CustomFunctionMapping/BloggingContext.cs#QueryableFunctionConfigurationHasDbFunction)] | ||
|
||
> [!CAUTION] | ||
> Mapping to queryable of entity types overrides the default mapping to a table for this set. If necessary (for example when the entity is not keyless) mapping to the table must be specified explicitly using `ToTable` method. | ||
When the function is mapped, the following query: | ||
|
||
[!code-csharp[Main](../../../samples/core/Modeling/CustomFunctionMapping/Program.cs#TableValuedFunctionQuery)] | ||
|
||
will produce: | ||
|
||
```sql | ||
SELECT [t].[TagId], [p].[PostId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title] | ||
FROM [Tags] AS [t] | ||
OUTER APPLY [dbo].[PostsTaggedWith]([t].[TagId]) AS [p] | ||
WHERE CAST(LEN([t].[TagId]) AS int) < 10 | ||
ORDER BY [t].[TagId], [p].[PostId] | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
using System.Collections.Generic; | ||
|
||
namespace EFModeling.CustomFunctionMapping | ||
{ | ||
#region Entity | ||
public class Blog | ||
{ | ||
public int BlogId { get; set; } | ||
public string Url { get; set; } | ||
public int? Rating { get; set; } | ||
|
||
public List<Post> Posts { get; set; } | ||
} | ||
#endregion | ||
} |
11 changes: 11 additions & 0 deletions
11
samples/core/Modeling/CustomFunctionMapping/BlogWithMultiplePosts.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
namespace EFModeling.CustomFunctionMapping | ||
{ | ||
#region Entity | ||
public class BlogWithMultiplePosts | ||
{ | ||
public string Url { get; set; } | ||
public int? Rating { get; set; } | ||
public int PostCount { get; set; } | ||
} | ||
#endregion | ||
} |
129 changes: 129 additions & 0 deletions
129
samples/core/Modeling/CustomFunctionMapping/BloggingContext.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
using Microsoft.EntityFrameworkCore; | ||
using Microsoft.EntityFrameworkCore.Infrastructure; | ||
using Microsoft.EntityFrameworkCore.Query; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
|
||
namespace EFModeling.CustomFunctionMapping | ||
{ | ||
public class BloggingContext : DbContext | ||
{ | ||
public DbSet<Blog> Blogs { get; set; } | ||
public DbSet<Post> Posts { get; set; } | ||
public DbSet<Tag> Tags { get; set; } | ||
|
||
#region BasicFunctionDefinition | ||
public int UniqueTagsCountForBlogPosts(int blogId) | ||
=> throw new NotSupportedException(); | ||
#endregion | ||
|
||
#region HasTranslationFunctionDefinition | ||
public int Difference(int first, int second) | ||
=> throw new NotSupportedException(); | ||
#endregion | ||
|
||
#region QueryableFunctionDefinition | ||
public IQueryable<Post> PostsTaggedWith(string tag) | ||
=> throw new NotSupportedException(); | ||
#endregion | ||
|
||
protected override void OnModelCreating(ModelBuilder modelBuilder) | ||
{ | ||
var noSeeding = false; | ||
if (noSeeding) | ||
{ | ||
#region EntityConfiguration | ||
modelBuilder.Entity<Blog>() | ||
.HasMany(b => b.Posts) | ||
.WithOne(p => p.Blog) | ||
.OnDelete(DeleteBehavior.NoAction); | ||
|
||
modelBuilder.Entity<Post>() | ||
.HasMany(p => p.Tags) | ||
.WithMany(t => t.Posts); | ||
#endregion | ||
} | ||
|
||
modelBuilder.Entity<Blog>() | ||
.HasMany(b => b.Posts) | ||
.WithOne(p => p.Blog) | ||
.OnDelete(DeleteBehavior.NoAction); | ||
|
||
modelBuilder.Entity<Post>() | ||
.HasMany(p => p.Tags) | ||
.WithMany(t => t.Posts) | ||
.UsingEntity<Dictionary<string, object>>( | ||
"PostTag", | ||
r => r.HasOne<Tag>().WithMany().HasForeignKey("TagId"), | ||
l => l.HasOne<Post>().WithMany().HasForeignKey("PostId"), | ||
je => | ||
{ | ||
je.HasKey("PostId", "TagId"); | ||
je.HasData( | ||
new { PostId = 1, TagId = "general" }, | ||
new { PostId = 1, TagId = "informative" }, | ||
new { PostId = 2, TagId = "classic" }, | ||
new { PostId = 3, TagId = "opinion" }, | ||
new { PostId = 4, TagId = "opinion" }, | ||
new { PostId = 4, TagId = "informative" }); | ||
}); | ||
|
||
modelBuilder.Entity<Blog>() | ||
.HasData( | ||
new Blog { BlogId = 1, Url = @"https://devblogs.microsoft.com/dotnet", Rating = 5 }, | ||
new Blog { BlogId = 2, Url = @"https://mytravelblog.com/", Rating = 4 }); | ||
|
||
modelBuilder.Entity<Post>() | ||
.HasData( | ||
new Post { PostId = 1, BlogId = 1, Title = "What's new", Content = "Lorem ipsum dolor sit amet", Rating = 5 }, | ||
new Post { PostId = 2, BlogId = 2, Title = "Around the World in Eighty Days", Content = "consectetur adipiscing elit", Rating = 5 }, | ||
new Post { PostId = 3, BlogId = 2, Title = "Glamping *is* the way", Content = "sed do eiusmod tempor incididunt", Rating = 4 }, | ||
new Post { PostId = 4, BlogId = 2, Title = "Travel in the time of pandemic", Content = "ut labore et dolore magna aliqua", Rating = 3 }); | ||
|
||
modelBuilder.Entity<Tag>() | ||
.HasData( | ||
new Tag { TagId = "general" }, | ||
new Tag { TagId = "classic" }, | ||
new Tag { TagId = "opinion" }, | ||
new Tag { TagId = "informative" }); | ||
|
||
#region BasicFunctionConfiguration | ||
modelBuilder.HasDbFunction(typeof(BloggingContext).GetMethod(nameof(UniqueTagsCountForBlogPosts), new[] { typeof(int) })) | ||
.HasName("DistinctTagsCountForBlogPosts"); | ||
#endregion | ||
|
||
#region HasTranslationFunctionConfiguration | ||
var sqlExpressionFactory = this.GetService<ISqlExpressionFactory>(); | ||
|
||
// ABS(first - second) | ||
modelBuilder.HasDbFunction(typeof(BloggingContext).GetMethod(nameof(Difference), new[] { typeof(int), typeof(int) })) | ||
.HasTranslation(args => sqlExpressionFactory.Function( | ||
name: "ABS", | ||
arguments: new[] | ||
{ | ||
sqlExpressionFactory.Subtract( | ||
args.First(), | ||
args.Skip(1).First()) | ||
}, | ||
nullable: false, | ||
argumentsPropagateNullability: new[] { false, false }, | ||
returnType: typeof(int))); | ||
#endregion | ||
|
||
#region QueryableFunctionConfigurationToFunction | ||
modelBuilder.Entity<BlogWithMultiplePosts>().HasNoKey().ToFunction("BlogsWithMultiplePosts"); | ||
#endregion | ||
|
||
#region QueryableFunctionConfigurationHasDbFunction | ||
modelBuilder.Entity<Post>().ToTable("Posts"); | ||
modelBuilder.HasDbFunction(typeof(BloggingContext).GetMethod(nameof(PostsTaggedWith), new[] { typeof(string) })); | ||
#endregion | ||
} | ||
|
||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||
{ | ||
optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFModeling.CustomFunctionMapping;Trusted_Connection=True;ConnectRetryCount=0"); | ||
} | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
samples/core/Modeling/CustomFunctionMapping/CustomFunctionMapping.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>netcoreapp3.1</TargetFramework> | ||
<RootNamespace>EFModeling.CustomFunctionMapping</RootNamespace> | ||
<AssemblyName>EFModeling.CustomFunctionMapping</AssemblyName> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.0" /> | ||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.0" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Oops, something went wrong.