@ -0,0 +1,67 @@ |
|||
# ABP.IO Platform 8.2 Final Has Been Released! |
|||
|
|||
[ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) 8.2 versions have been released today. |
|||
|
|||
## What's New With Version 8.2? |
|||
|
|||
All the new features were explained in detail in the [8.2 RC Announcement Post](https://blog.abp.io/abp/announcing-abp-8-2-release-candidate), so there is no need to review them again. You can check it out for more details. |
|||
|
|||
## Getting Started with 8.2 |
|||
|
|||
### Creating New Solutions |
|||
|
|||
You can create a new solution with the ABP Framework version 8.2 by either using the `abp new` command or generating the CLI command on the [get started page](https://abp.io/get-started). |
|||
|
|||
> See the [getting started document](https://docs.abp.io/en/abp/latest/Getting-Started) for more. |
|||
|
|||
### How to Upgrade an Existing Solution |
|||
|
|||
#### Install/Update the ABP CLI |
|||
|
|||
First, install the ABP CLI or upgrade it to the latest version. |
|||
|
|||
If you haven't installed it yet: |
|||
|
|||
```bash |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
``` |
|||
|
|||
To update the existing CLI: |
|||
|
|||
```bash |
|||
dotnet tool update -g Volo.Abp.Cli |
|||
``` |
|||
|
|||
#### Upgrading Existing Solutions with the ABP Update Command |
|||
|
|||
[ABP CLI](https://docs.abp.io/en/abp/latest/CLI) provides a handy command to update all the ABP related NuGet and NPM packages in your solution with a single command: |
|||
|
|||
```bash |
|||
abp update |
|||
``` |
|||
|
|||
Run this command in the root folder of your solution. |
|||
|
|||
## Migration Guides |
|||
|
|||
There are a few breaking changes in this version that may affect your application. |
|||
Please see the following migration documents, if you are upgrading from v8.x or earlier: |
|||
|
|||
* [ABP Framework 8.x to 8.2 Migration Guide](https://docs.abp.io/en/abp/8.2/Migration-Guides/Abp-8_2) |
|||
* [ABP Commercial 8.x to 8.2 Migration Guide](https://docs.abp.io/en/commercial/8.2/migration-guides/v8_2) |
|||
|
|||
## Community News |
|||
|
|||
### New ABP Community Posts |
|||
|
|||
As always, exciting articles have been contributed by the ABP community. I will highlight some of them here: |
|||
|
|||
* [How to use Angular Material with Form Validation on ABP](https://community.abp.io/posts/how-to-use-angular-material-with-form-validation-on-abp-jtheajj3) by [Mahmut Gündoğdu](https://x.com/mahmutgundogdu) |
|||
* [Tunnel your local host address to a public URL with ngrok](https://community.abp.io/posts/tunnel-your-local-host-address-to-a-public-url-with-ngrok-4cywnocj) by [Bart Van Hoey](https://github.com/bartvanhoey) |
|||
* [Antiforgery Token Validation When Angular and HTTP API Runs on the Same Server](https://community.abp.io/posts/antiforgery-token-validation-when-angular-and-http-api-runs-on-the-same-server-mzf5ppdq) by [dignite](https://x.com/dignite_adu) |
|||
|
|||
Thanks to the ABP Community for all the content they have published. You can also [post your ABP-related (text or video) content](https://community.abp.io/articles/submit) to the ABP Community. |
|||
|
|||
## About the Next Version |
|||
|
|||
The next feature version will be 8.3. You can follow the [release planning here](https://github.com/abpframework/abp/milestones). Please [submit an issue](https://github.com/abpframework/abp/issues/new) if you have any problems with this version. |
|||
|
After Width: | Height: | Size: 1.9 MiB |
@ -0,0 +1,302 @@ |
|||
# How to use .NET Aspire with ABP framework |
|||
|
|||
[.NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview) is an opinionated, cloud-ready stack designed for building observable, production-ready, and distributed applications. On the other hand, the [ABP framework](https://docs.abp.io/en/abp/latest) offers a complete, modular and layered software architecture based on Domain Driven Design principles and patterns. This guide explores how to combine .NET Aspire with ABP, enabling developers to create observable, and feature-rich applications. |
|||
|
|||
## When to Use .NET Aspire? |
|||
|
|||
Using .NET Aspire with the ABP framework can be beneficial in various scenarios where you need to combine the strengths of both technologies. Here are some situations when using .NET Aspire with ABP can be advantageous: |
|||
|
|||
- **Enterprise Web Applications:** ABP is well-suited for building enterprise web applications with its opinionated architecture and best practices. When combined with .NET Aspire, you can leverage ABP's features for rapid development of user interfaces, backend services, and business logic while benefiting from .NET Aspire's cloud-native capabilities and observability features. |
|||
- **Observability and Monitoring:** .NET Aspire's emphasis on observability, including logging, monitoring, and tracing, can enhance ABP applications by providing deeper insights into system behavior, performance metrics, and diagnostics, which is key for maintaining and optimizing enterprise-grade applications. |
|||
|
|||
## Creating a new ABP Solution |
|||
|
|||
To demonstrate the usage of .NET Aspire with the ABP framework, I've created an ABP solution. If you want to create the same solution from scratch, follow the steps below: |
|||
|
|||
Install the ABP CLI if you haven't installed it before: |
|||
|
|||
```bash |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
``` |
|||
|
|||
Create a new solution with the ABP framework's Application Startup Template with Tiered MVC UI and EF Core database: |
|||
|
|||
```bash |
|||
abp new AspirationalAbp -u mvc --database-provider ef -dbms PostgreSQL --csf --tiered |
|||
``` |
|||
|
|||
> The startup template selection matters for this article. I chose these options so that the demo solution can cover complex scenarios. |
|||
|
|||
**Disclaimer-I:** This article is based on version `8.0.1` of .NET Aspire and version `8.2.0` of ABP Framework. |
|||
|
|||
**Disclaimer-II:** ABP and .NET Aspire may not be fully compatible in some respects. This article aims to explain how these two technologies can be used together in the simplest way possible, even if they are not fully compatible. |
|||
## Add .NET Aspire |
|||
|
|||
After creating the solution, run the following commands in the `src` folder of your solution to add .NET Aspire: |
|||
|
|||
```bash |
|||
// Adding AppHost |
|||
dotnet new aspire-apphost -n AspirationalAbp.AppHost |
|||
dotnet sln ../AspirationalAbp.sln add ./AspirationalAbp.AppHost/AspirationalAbp.AppHost.csproj |
|||
|
|||
// Adding ServiceDefaults |
|||
dotnet new aspire-servicedefaults -n AspirationalAbp.ServiceDefaults |
|||
dotnet sln ../AspirationalAbp.sln add ./AspirationalAbp.ServiceDefaults/AspirationalAbp.ServiceDefaults.csproj |
|||
``` |
|||
|
|||
These commands add two new projects to the solution: |
|||
- **AspirationalAbp.AppHost**: An orchestrator project designed to connect and configure the different projects and services of your app. |
|||
- **AspirationalAbp.ServiceDefaults**: A .NET Aspire shared project to manage configurations that are reused across the projects in your solution related to [resilience](https://learn.microsoft.com/en-us/dotnet/core/resilience/http-resilience), [service discovery](https://learn.microsoft.com/en-us/dotnet/aspire/service-discovery/overview), and [telemetry](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/telemetry). |
|||
|
|||
We have added .NET Aspire to our ABP based solution, but we have not registered our projects in the .NET Aspire orchestration. Now, let's enroll our projects, which implement the db migrator, web user interface, API, and auth, in .NET Aspire orchestration. |
|||
|
|||
## Registering projects to .NET Aspire orchestration |
|||
|
|||
First of all, we need to add the reference of related projects to the `AspirationalAbp.AppHost` project. For this, add the following `ItemGroups` to the `AspirationalAbp.AppHost/AspirationalAbp.AppHost.csproj` file: |
|||
|
|||
```csharp |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\AspirationalAbp.AuthServer\AspirationalAbp.AuthServer.csproj" /> |
|||
<ProjectReference Include="..\AspirationalAbp.HttpApi.Host\AspirationalAbp.HttpApi.Host.csproj" /> |
|||
<ProjectReference Include="..\AspirationalAbp.Web\AspirationalAbp.Web.csproj"/> |
|||
<ProjectReference Include="..\AspirationalAbp.DbMigrator\AspirationalAbp.DbMigrator.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="8.0.1" /> |
|||
<PackageReference Include="Aspire.Hosting.Redis" Version="8.0.1" /> |
|||
</ItemGroup> |
|||
``` |
|||
|
|||
With the first `ItemGroup`, we added the references of `AuthServer`, `HttpApi.Host`, `Web`, and `DbMigrator` projects to the app host project. So, we can orchestrate them within the app model. |
|||
|
|||
With the second `ItemGroup`, to model the **PostgreSQL** server resource and **Redis** resource in the app host, installed the `Aspire.Hosting.PostgreSQL` and `Aspire.Hosting.Redis` |
|||
|
|||
Now let's update the `Program` class of the `AspirationalAbp.AppHost` project as follows: |
|||
|
|||
```csharp |
|||
using Microsoft.Extensions.Hosting; |
|||
|
|||
var builder = DistributedApplication.CreateBuilder(args); |
|||
|
|||
var postgres = builder |
|||
.AddPostgres("postgres") |
|||
.AddDatabase("AspirationalAbp"); |
|||
|
|||
var redis = builder.AddRedis("redis"); |
|||
|
|||
// DbMigrator |
|||
if (builder.Environment.IsDevelopment()) |
|||
{ |
|||
builder |
|||
.AddProject<Projects.AspirationalAbp_DbMigrator>("dbMigrator") |
|||
.WithReference(postgres, "Default") |
|||
.WithReference(redis, "Redis") |
|||
.WithReplicas(1); |
|||
} |
|||
|
|||
// AuthServer |
|||
var authServerLaunchProfile = "AspirationalAbp.AuthServer"; |
|||
builder |
|||
.AddProject<Projects.AspirationalAbp_AuthServer>("authserver", launchProfileName: authServerLaunchProfile) |
|||
.WithExternalHttpEndpoints() |
|||
.WithReference(postgres, "Default") |
|||
.WithReference(redis); |
|||
|
|||
// HttpApi.Host |
|||
var httpApiHostLaunchProfile = "AspirationalAbp.HttpApi.Host"; |
|||
builder |
|||
.AddProject<Projects.AspirationalAbp_HttpApi_Host>("httpapihost", launchProfileName: httpApiHostLaunchProfile) |
|||
.WithExternalHttpEndpoints() |
|||
.WithReference(postgres, "Default") |
|||
.WithReference(redis); |
|||
|
|||
// Web |
|||
builder |
|||
.AddProject<Projects.AspirationalAbp_Web>("web", "AspirationalAbp.Web") |
|||
.WithReference(redis); |
|||
|
|||
builder.Build().Run(); |
|||
``` |
|||
|
|||
With the code above, the following operations were performed below: |
|||
|
|||
1. Creates an `IDistributedApplicationBuilder` instance by calling `DistributedApplication.CreateBuilder(args)`. |
|||
2. Adds PostgreSQL and the `AspirationalAbp` database. |
|||
3. Adds Redis. |
|||
4. Adds the `DbMigrator` project with references to PostgreSQL and Redis, ensuring one replica in development. |
|||
5. Adds the `AuthServer` project with external HTTP endpoints, referencing PostgreSQL and Redis. |
|||
6. Adds the `HttpApi.Host` project with external HTTP endpoints, referencing PostgreSQL and Redis. |
|||
7. Adds the `Web` project, referencing Redis. |
|||
8. Builds and runs the application. |
|||
|
|||
Now let's make the projects we added to the app host compatible with .NET Aspire. |
|||
|
|||
## Configuring Projects for Aspire |
|||
|
|||
To make the `AspirationalAbp.DbMigrator`, `AspirationalAbp.AuthServer`, `AspirationalAbp.HttpApi.Host`, and `AspirationalAbp.Web` projects compatible with .NET Aspire, we need to add and configure several packages. For that, we need to add the `Aspire.StackExchange.Redis` package to all these projects and the `Aspire.Npgsql.EntityFrameworkCore.PostgreSQL` package to the `AspirationalAbp.EntityFrameworkCore` project. Additionally, we will add the `AspirationalAbp.ServiceDefaults` reference to host projects except `AspirationalAbp.DbMigrator`. Also, we need to convert [Serilog](https://serilog.net/) events into [OpenTelemetry](https://opentelemetry.io/) `LogRecord`s, for that we will add a `Serilog.Sinks.OpenTelemetry` reference to host projects. Let's begin with configuring `AspirationalAbp.DbMigrator`. |
|||
|
|||
### AspirationalAbp.DbMigrator |
|||
|
|||
First, let's add the `Aspire.StackExchange.Redis`, and `Serilog.Sinks.OpenTelemetry` packages to the `AspirationalAbp.DbMigrator` project. For this, let's run the following .NET CLI command inside the `AspirationalAbp.DbMigrator` project: |
|||
|
|||
```bash |
|||
dotnet add package Aspire.StackExchange.Redis --version 8.0.1 |
|||
dotnet add package Serilog.Sinks.OpenTelemetry --version 4.0.0-dev-00313 |
|||
``` |
|||
|
|||
Then let's override the `PreConfigureServices` method in `AspirationalAbpDbMigratorModule` as below: |
|||
|
|||
```csharp |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
var configuration = context.Services.GetConfiguration(); |
|||
configuration["Redis:Configuration"] = configuration["ConnectionStrings:Redis"]; |
|||
} |
|||
``` |
|||
|
|||
To use the **OpenTelemetry** sink we have installed the `Serilog.Sinks.OpenTelemetry` package and now let's enable the sink. For this, let's write the following code block just before calling the `CreateLogger` method in the logger configuration in `Program.cs`: |
|||
|
|||
```csharp |
|||
/// .WriteTo.Async(c => c.Console()) |
|||
.WriteTo.Async(c => c.OpenTelemetry()) |
|||
/// .CreateLogger(); |
|||
``` |
|||
|
|||
Now let's continue with `AspirationalAbp.EntityFrameworkCore`. |
|||
|
|||
### AspirationalAbp.EntityFrameworkCore |
|||
|
|||
Now let's add the `Aspire.Npgsql.EntityFrameworkCore.PostgreSQL` package to the `AspirationalAbp.EntityFrameworkCore` project. For this, you can run the following command in the `AspirationalAbp.EntityFrameworkCore` project: |
|||
|
|||
```bash |
|||
dotnet add package Aspire.Npgsql.EntityFrameworkCore.PostgreSQL --version 8.0.1 |
|||
``` |
|||
|
|||
Now let's continue with `AspirationalAbp.AuthServer`. |
|||
|
|||
### AspirationalAbp.AuthServer |
|||
|
|||
First, let's add the `Serilog.Sinks.OpenTelemetry`, `Aspire.StackExchange.Redis` and `AspirationalAbp.ServiceDefaults` packages to the `AspirationalAbp.AuthServer` project. For this, let's run the following .NET CLI command inside the `AspirationalAbp.AuthServer` project: |
|||
|
|||
```bash |
|||
dotnet add package Aspire.StackExchange.Redis --version 8.0.1 |
|||
dotnet add reference ../AspirationalAbp.ServiceDefaults/AspirationalAbp.ServiceDefaults.csproj |
|||
dotnet add package Serilog.Sinks.OpenTelemetry --version 4.0.0-dev-00313 |
|||
``` |
|||
|
|||
Then add the following code block after defining the builder variable in `Program.cs`: |
|||
|
|||
```csharp |
|||
builder.AddServiceDefaults(); |
|||
builder.AddRedisClient("redis"); |
|||
builder.AddNpgsqlDbContext<AspirationalAbpDbContext>("Default", |
|||
options => |
|||
{ |
|||
options.DisableRetry = true; |
|||
}); |
|||
``` |
|||
|
|||
Then add the following code to the `PreConfigureServices` method in the `AspirationalAbpAuthServerModule` class: |
|||
|
|||
```csharp |
|||
configuration["Redis:Configuration"] = configuration["ConnectionStrings:Redis"]; |
|||
``` |
|||
|
|||
To use the **OpenTelemetry** sink we have installed the `Serilog.Sinks.OpenTelemetry` package and now let's enable the sink. For this, let's write the following code block just before calling the `CreateLogger` method in logger configuration in `Program.cs`: |
|||
|
|||
```csharp |
|||
/// .WriteTo.Async(c => c.Console()) |
|||
.WriteTo.Async(c => c.OpenTelemetry()) |
|||
/// .CreateLogger(); |
|||
``` |
|||
|
|||
So far we have made `AspirationalAbp.DbMigrator`, `AspirationalAbp.EntityFrameworkCore`, and `AspirationalAbp.AuthServer` compatible with .NET Aspire. Now let's continue with `AspirationalAbp.HttpApi.Host`. |
|||
|
|||
### AspirationalAbp.HttpApi.Host |
|||
|
|||
First, let's add the `Serilog.Sinks.OpenTelemetry`, `Aspire.StackExchange.Redis` and `AspirationalAbp.ServiceDefaults` packages to the `AspirationalAbp.HttpApi.Host` project. For this, let's run the following .NET CLI command inside the `AspirationalAbp.HttpApi.Host` project: |
|||
|
|||
```bash |
|||
dotnet add package Aspire.StackExchange.Redis --version 8.0.1 |
|||
dotnet add reference ../AspirationalAbp.ServiceDefaults/AspirationalAbp.ServiceDefaults.csproj |
|||
dotnet add package Serilog.Sinks.OpenTelemetry --version 4.0.0-dev-00313 |
|||
``` |
|||
|
|||
Then add the following code block after defining the builder variable in `Program.cs`: |
|||
|
|||
```csharp |
|||
builder.AddServiceDefaults(); |
|||
builder.AddRedisClient("redis"); |
|||
builder.AddNpgsqlDbContext<AspirationalAbpDbContext>("Default", |
|||
options => |
|||
{ |
|||
options.DisableRetry = true; |
|||
}); |
|||
``` |
|||
|
|||
Then let's override the `PreConfigureServices` method in `AspirationalAbpHttpApiHostModule` as below: |
|||
|
|||
```csharp |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
var configuration = context.Services.GetConfiguration(); |
|||
configuration["Redis:Configuration"] = configuration["ConnectionStrings:Redis"]; |
|||
} |
|||
``` |
|||
|
|||
To use the **OpenTelemetry** sink we have installed the `Serilog.Sinks.OpenTelemetry` package and now let's enable the sink. For this, let's write the following code block just before calling the `CreateLogger` method in the logger configuration in `Program.cs`: |
|||
|
|||
```csharp |
|||
/// .WriteTo.Async(c => c.Console()) |
|||
.WriteTo.Async(c => c.OpenTelemetry()) |
|||
/// .CreateLogger(); |
|||
``` |
|||
|
|||
Finally, let's make `AspirationalAbp.Web` compatible with .NET Aspire. |
|||
|
|||
### AspirationalAbp.Web |
|||
|
|||
First, let's add the `Serilog.Sinks.OpenTelemetry`, `Aspire.StackExchange.Redis` and `AspirationalAbp.ServiceDefaults` packages to the `AspirationalAbp.Web` project. For this, let's run the following .NET CLI command inside the `AspirationalAbp.Web` project: |
|||
|
|||
```bash |
|||
dotnet add package Aspire.StackExchange.Redis --version 8.0.1 |
|||
dotnet add reference ../AspirationalAbp.ServiceDefaults/AspirationalAbp.ServiceDefaults.csproj |
|||
dotnet add package Serilog.Sinks.OpenTelemetry --version 4.0.0-dev-00313 |
|||
``` |
|||
|
|||
Then add the following code block after defining the builder variable in `Program.cs`: |
|||
|
|||
```csharp |
|||
builder.AddServiceDefaults(); |
|||
builder.AddRedisClient("redis"); |
|||
``` |
|||
|
|||
Then add the following code to the `PreConfigureServices` method in the `AspirationalAbpWebModule` class: |
|||
|
|||
```bash |
|||
var configuration = context.Services.GetConfiguration(); |
|||
configuration["Redis:Configuration"] = configuration["ConnectionStrings:Redis"]; |
|||
``` |
|||
|
|||
To use the **OpenTelemetry** sink we have installed the `Serilog.Sinks.OpenTelemetry` package and now let's enable the sink. For this, let's write the following code block just before calling the `CreateLogger` method in logger configuration in `Program.cs`: |
|||
|
|||
```csharp |
|||
/// .WriteTo.Async(c => c.Console()) |
|||
.WriteTo.Async(c => c.OpenTelemetry()) |
|||
/// .CreateLogger(); |
|||
``` |
|||
|
|||
After making all our changes, we can run the `AspirationalAbp.AppHost` project. |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
## Conclusion |
|||
|
|||
Combining .NET Aspire with the ABP framework creates a powerful setup for building robust, observable, and feature-rich applications. By integrating Aspire's observability and cloud capabilities with ABP's approach of focusing on your business without repeating yourself, you can develop feature-rich, scalable applications with enhanced monitoring and seamless cloud integration. This guide provides a clear path to set up and configure these technologies, ensuring your applications are well-structured, maintainable, and ready for modern cloud environments. |
|||
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 73 KiB |
@ -0,0 +1,130 @@ |
|||
# Use User-Defined Function Mapping for Global Filter |
|||
|
|||
## Introduction |
|||
|
|||
ABP provides data filters that can filter queries automatically based on some rules. This feature is useful for implementing multi-tenancy, soft delete, and other global filters. It uses [EF Core's Global Query Filters system](https://learn.microsoft.com/en-us/ef/core/querying/filters) for the EF Core Integration. |
|||
|
|||
EF Core Global Query Filters generate filter conditions and apply them to SQL queries. ABP controls whether this filter condition takes effect through a variable. However, this variable may cause performance losses in some scenarios. |
|||
|
|||
## The Filter Condition Variable |
|||
|
|||
Think of a scenario with a global filter `IIsActive`, which filters out inactive entities: |
|||
|
|||
```csharp |
|||
public class Book : IIsActive |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public bool IsActive { get; set; } |
|||
} |
|||
``` |
|||
|
|||
The SQL generated by the [EF Core Global Query Filters](https://learn.microsoft.com/en-us/ef/core/querying/filters) is as follows: |
|||
|
|||
```SQL |
|||
SELECT * FROM [AppBooks] AS [a] |
|||
WHERE (@__ef_filter__p_0 = CAST(1 AS bit) OR [a].[IsActive] = CAST(1 AS bit)) |
|||
``` |
|||
|
|||
> The `__ef_filter__p_0` variable controls whether the filter condition takes effect. |
|||
|
|||
The generated SQL is not optimal, and some databases do not optimize it well. |
|||
|
|||
## Using User-defined function mapping for global filters |
|||
|
|||
In the [upcoming preview version of ABP, v8.3.0-rc.1](https://github.com/abpframework/abp/pull/20065), we start the [User-defined function mapping](https://learn.microsoft.com/en-us/ef/core/querying/user-defined-function-mapping) to implement global filters more efficiently. This feature is enabled by default, so you don't need to make any changes if you create a new solution and start from scratch. Otherwise, you can enable it easily by following the instructions below. |
|||
|
|||
To use this new feature for your custom global filters, you need to change your `DbContext` as follows: |
|||
|
|||
````csharp |
|||
protected bool IsActiveFilterEnabled => DataFilter?.IsEnabled<IIsActive>() ?? false; |
|||
|
|||
protected override bool ShouldFilterEntity<TEntity>(IMutableEntityType entityType) |
|||
{ |
|||
if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
return base.ShouldFilterEntity<TEntity>(entityType); |
|||
} |
|||
|
|||
protected override Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>(ModelBuilder modelBuilder) |
|||
{ |
|||
var expression = base.CreateFilterExpression<TEntity>(modelBuilder); |
|||
|
|||
if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))) |
|||
{ |
|||
Expression<Func<TEntity, bool>> isActiveFilter = e => !IsActiveFilterEnabled || EF.Property<bool>(e, "IsActive"); |
|||
|
|||
if (UseDbFunction()) |
|||
{ |
|||
isActiveFilter = e => IsActiveFilter(((IIsActive)e).IsActive, true); |
|||
|
|||
var abpEfCoreCurrentDbContext = this.GetService<AbpEfCoreCurrentDbContext>(); |
|||
modelBuilder.HasDbFunction(typeof(MyProjectNameDbContext).GetMethod(nameof(IsActiveFilter))!) |
|||
.HasTranslation(args => |
|||
{ |
|||
// (bool isActive, bool boolParam) |
|||
var isActive = args[0]; |
|||
var boolParam = args[1]; |
|||
|
|||
if (abpEfCoreCurrentDbContext.Context?.DataFilter.IsEnabled<IIsActive>() == true) |
|||
{ |
|||
// isActive == true |
|||
return new SqlBinaryExpression( |
|||
ExpressionType.Equal, |
|||
isActive, |
|||
new SqlConstantExpression(Expression.Constant(true), boolParam.TypeMapping), |
|||
boolParam.Type, |
|||
boolParam.TypeMapping); |
|||
} |
|||
|
|||
// empty where sql |
|||
return new SqlConstantExpression(Expression.Constant(true), boolParam.TypeMapping); |
|||
}); |
|||
} |
|||
|
|||
expression = expression == null ? isActiveFilter : QueryFilterExpressionHelper.CombineExpressions(expression, isActiveFilter); |
|||
} |
|||
|
|||
return expression; |
|||
} |
|||
|
|||
public static bool IsActiveFilter(bool isActive, bool boolParam) |
|||
{ |
|||
throw new NotSupportedException(AbpEfCoreDataFilterDbFunctionMethods.NotSupportedExceptionMessage); |
|||
} |
|||
|
|||
public override string GetCompiledQueryCacheKey() |
|||
{ |
|||
return $"{base.GetCompiledQueryCacheKey()}:{IsActiveFilterEnabled}"; |
|||
} |
|||
```` |
|||
|
|||
After these changes, the SQL generated by the EF Core Global Query Filters will be as follows: |
|||
|
|||
Enabling the `IIsActive` filter: |
|||
|
|||
```SQL |
|||
SELECT * FROM [AppBooks] AS [a] WHERE |
|||
[a].[IsActive] = CAST(1 AS bit) |
|||
``` |
|||
|
|||
Disabling the `IIsActive` filter: |
|||
|
|||
```SQL |
|||
SELECT * FROM [AppBooks] AS [a] |
|||
``` |
|||
|
|||
## Conclusion |
|||
|
|||
We have implemented global filters using [User-defined function mapping](https://learn.microsoft.com/en-us/ef/core/querying/user-defined-function-mapping), which can generate more efficient SQL and thus improve performance. |
|||
|
|||
Upgrade to the latest ABP version and enjoy the performance improvement! |
|||
|
|||
## References |
|||
|
|||
- [ABP Framework Data Filtering](https://docs.abp.io/en/abp/latest/Data-Filtering) |
|||
- [EF Core's Global Query Filters system](https://learn.microsoft.com/en-us/ef/core/querying/filters) |
|||
- [User-defined function mapping](https://learn.microsoft.com/en-us/ef/core/querying/user-defined-function-mapping) |
|||
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 41 KiB |
@ -0,0 +1,12 @@ |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Authentication; |
|||
|
|||
public class AbpAspNetCoreTokenUnauthorizedErrorInfo : IScopedDependency |
|||
{ |
|||
public string? Error { get; set; } |
|||
|
|||
public string? ErrorDescription { get; set; } |
|||
|
|||
public string? ErrorUri { get; set; } |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using Microsoft.EntityFrameworkCore; |
|||
using Microsoft.EntityFrameworkCore.Infrastructure; |
|||
using Volo.Abp.EntityFrameworkCore; |
|||
|
|||
namespace Microsoft.Extensions.DependencyInjection; |
|||
|
|||
public static class AbpEfCoreDbContextOptionsBuilderExtensions |
|||
{ |
|||
public static DbContextOptionsBuilder AddAbpDbContextOptionsExtension(this DbContextOptionsBuilder optionsBuilder) |
|||
{ |
|||
((IDbContextOptionsBuilderInfrastructure) optionsBuilder).AddOrUpdateExtension(new AbpDbContextOptionsExtension()); |
|||
return optionsBuilder; |
|||
} |
|||
|
|||
public static DbContextOptionsBuilder<TContext> AddAbpDbContextOptionsExtension<TContext>(this DbContextOptionsBuilder<TContext> optionsBuilder) |
|||
where TContext : DbContext |
|||
{ |
|||
((IDbContextOptionsBuilderInfrastructure) optionsBuilder).AddOrUpdateExtension(new AbpDbContextOptionsExtension()); |
|||
return optionsBuilder; |
|||
} |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
using System.Linq.Expressions; |
|||
using System.Reflection; |
|||
using Microsoft.EntityFrameworkCore; |
|||
using Microsoft.EntityFrameworkCore.Query.SqlExpressions; |
|||
using Volo.Abp; |
|||
using Volo.Abp.EntityFrameworkCore; |
|||
using Volo.Abp.EntityFrameworkCore.GlobalFilters; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace Microsoft.Extensions.DependencyInjection; |
|||
|
|||
public static class AbpEfCoreModelBuilderExtensions |
|||
{ |
|||
public static ModelBuilder ConfigureSoftDeleteDbFunction(this ModelBuilder modelBuilder, MethodInfo methodInfo, AbpEfCoreCurrentDbContext abpEfCoreCurrentDbContext) |
|||
{ |
|||
modelBuilder.HasDbFunction(methodInfo) |
|||
.HasTranslation(args => |
|||
{ |
|||
// (bool isDeleted, bool boolParam)
|
|||
var isDeleted = args[0]; |
|||
var boolParam = args[1]; |
|||
|
|||
if (abpEfCoreCurrentDbContext.Context?.DataFilter.IsEnabled<ISoftDelete>() == true) |
|||
{ |
|||
// IsDeleted == false
|
|||
return new SqlBinaryExpression( |
|||
ExpressionType.Equal, |
|||
isDeleted, |
|||
new SqlConstantExpression(Expression.Constant(false), boolParam.TypeMapping), |
|||
boolParam.Type, |
|||
boolParam.TypeMapping); |
|||
} |
|||
|
|||
// empty where sql
|
|||
return new SqlConstantExpression(Expression.Constant(true), boolParam.TypeMapping); |
|||
}); |
|||
|
|||
return modelBuilder; |
|||
} |
|||
|
|||
public static ModelBuilder ConfigureMultiTenantDbFunction(this ModelBuilder modelBuilder, MethodInfo methodInfo, AbpEfCoreCurrentDbContext abpEfCoreCurrentDbContext) |
|||
{ |
|||
modelBuilder.HasDbFunction(methodInfo) |
|||
.HasTranslation(args => |
|||
{ |
|||
// (Guid? tenantId, int? currentTenantId)
|
|||
var tenantId = args[0]; |
|||
var currentTenantId = args[1]; |
|||
var boolParam = args[2]; |
|||
|
|||
if (abpEfCoreCurrentDbContext.Context?.DataFilter.IsEnabled<IMultiTenant>() == true) |
|||
{ |
|||
// TenantId == CurrentTenantId
|
|||
return new SqlBinaryExpression( |
|||
ExpressionType.Equal, |
|||
tenantId, |
|||
currentTenantId, |
|||
boolParam.Type, |
|||
boolParam.TypeMapping); |
|||
} |
|||
|
|||
// empty where sql
|
|||
return new SqlConstantExpression(Expression.Constant(true), boolParam.TypeMapping); |
|||
}); |
|||
|
|||
return modelBuilder; |
|||
} |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Microsoft.EntityFrameworkCore.Infrastructure; |
|||
using Microsoft.EntityFrameworkCore.Query; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.DependencyInjection.Extensions; |
|||
using Volo.Abp.EntityFrameworkCore.GlobalFilters; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore; |
|||
|
|||
public class AbpDbContextOptionsExtension : IDbContextOptionsExtension |
|||
{ |
|||
public void ApplyServices(IServiceCollection services) |
|||
{ |
|||
var serviceDescriptor = services.FirstOrDefault(x => x.ServiceType == typeof(ICompiledQueryCacheKeyGenerator)); |
|||
if (serviceDescriptor != null && serviceDescriptor.ImplementationType != null) |
|||
{ |
|||
services.Remove(serviceDescriptor); |
|||
services.AddScoped(serviceDescriptor.ImplementationType); |
|||
services.Add(ServiceDescriptor.Scoped<ICompiledQueryCacheKeyGenerator>(provider => |
|||
ActivatorUtilities.CreateInstance<AbpCompiledQueryCacheKeyGenerator>(provider, |
|||
provider.GetRequiredService(serviceDescriptor.ImplementationType) |
|||
.As<ICompiledQueryCacheKeyGenerator>()))); |
|||
} |
|||
|
|||
services.Replace(ServiceDescriptor.Scoped<IAsyncQueryProvider, AbpEntityQueryProvider>()); |
|||
services.AddSingleton(typeof(AbpEfCoreCurrentDbContext)); |
|||
} |
|||
|
|||
public void Validate(IDbContextOptions options) |
|||
{ |
|||
} |
|||
|
|||
public DbContextOptionsExtensionInfo Info => new AbpOptionsExtensionInfo(this); |
|||
|
|||
private class AbpOptionsExtensionInfo : DbContextOptionsExtensionInfo |
|||
{ |
|||
public AbpOptionsExtensionInfo(IDbContextOptionsExtension extension) |
|||
: base(extension) |
|||
{ |
|||
} |
|||
|
|||
public override bool IsDatabaseProvider => false; |
|||
|
|||
public override int GetServiceProviderHashCode() |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) |
|||
{ |
|||
return other is AbpOptionsExtensionInfo; |
|||
} |
|||
|
|||
public override void PopulateDebugInfo(IDictionary<string, string> debugInfo) |
|||
{ |
|||
} |
|||
|
|||
public override string LogFragment => "AbpOptionsExtension"; |
|||
} |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
using System.Linq.Expressions; |
|||
using System.Threading; |
|||
using Microsoft.EntityFrameworkCore.Infrastructure; |
|||
using Microsoft.EntityFrameworkCore.Query.Internal; |
|||
using Volo.Abp.EntityFrameworkCore.GlobalFilters; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore; |
|||
|
|||
#pragma warning disable EF1001
|
|||
public class AbpEntityQueryProvider : EntityQueryProvider |
|||
{ |
|||
protected AbpEfCoreCurrentDbContext AbpEfCoreCurrentDbContext { get; } |
|||
protected ICurrentDbContext CurrentDbContext { get; } |
|||
|
|||
public AbpEntityQueryProvider( |
|||
IQueryCompiler queryCompiler, |
|||
AbpEfCoreCurrentDbContext abpEfCoreCurrentDbContext, |
|||
ICurrentDbContext currentDbContext) |
|||
: base(queryCompiler) |
|||
{ |
|||
AbpEfCoreCurrentDbContext = abpEfCoreCurrentDbContext; |
|||
CurrentDbContext = currentDbContext; |
|||
} |
|||
|
|||
public override object Execute(Expression expression) |
|||
{ |
|||
using (AbpEfCoreCurrentDbContext.Use(CurrentDbContext.Context as IAbpEfCoreDbFunctionContext)) |
|||
{ |
|||
return base.Execute(expression); |
|||
} |
|||
} |
|||
|
|||
public override TResult Execute<TResult>(Expression expression) |
|||
{ |
|||
using (AbpEfCoreCurrentDbContext.Use(CurrentDbContext.Context as IAbpEfCoreDbFunctionContext)) |
|||
{ |
|||
return base.Execute<TResult>(expression); |
|||
} |
|||
} |
|||
|
|||
public override TResult ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken = new CancellationToken()) |
|||
{ |
|||
using (AbpEfCoreCurrentDbContext.Use(CurrentDbContext.Context as IAbpEfCoreDbFunctionContext)) |
|||
{ |
|||
return base.ExecuteAsync<TResult>(expression, cancellationToken); |
|||
} |
|||
} |
|||
} |
|||
#pragma warning restore EF1001
|
|||
@ -0,0 +1,59 @@ |
|||
using System; |
|||
using System.Linq.Expressions; |
|||
using Microsoft.EntityFrameworkCore.Infrastructure; |
|||
using Microsoft.EntityFrameworkCore.Query; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore.GlobalFilters; |
|||
|
|||
public class AbpCompiledQueryCacheKeyGenerator : ICompiledQueryCacheKeyGenerator |
|||
{ |
|||
protected ICompiledQueryCacheKeyGenerator InnerCompiledQueryCacheKeyGenerator { get; } |
|||
protected ICurrentDbContext CurrentContext { get; } |
|||
|
|||
public AbpCompiledQueryCacheKeyGenerator( |
|||
ICompiledQueryCacheKeyGenerator innerCompiledQueryCacheKeyGenerator, |
|||
ICurrentDbContext currentContext) |
|||
{ |
|||
InnerCompiledQueryCacheKeyGenerator = innerCompiledQueryCacheKeyGenerator; |
|||
CurrentContext = currentContext; |
|||
} |
|||
|
|||
public virtual object GenerateCacheKey(Expression query, bool async) |
|||
{ |
|||
var cacheKey = InnerCompiledQueryCacheKeyGenerator.GenerateCacheKey(query, async); |
|||
if (CurrentContext.Context is IAbpEfCoreDbFunctionContext abpEfCoreDbFunctionContext) |
|||
{ |
|||
return new AbpCompiledQueryCacheKey(cacheKey, abpEfCoreDbFunctionContext.GetCompiledQueryCacheKey()); |
|||
} |
|||
|
|||
return cacheKey; |
|||
} |
|||
|
|||
private readonly struct AbpCompiledQueryCacheKey : IEquatable<AbpCompiledQueryCacheKey> |
|||
{ |
|||
private readonly object _compiledQueryCacheKey; |
|||
private readonly string _currentFilterCacheKey; |
|||
|
|||
public AbpCompiledQueryCacheKey(object compiledQueryCacheKey, string currentFilterCacheKey) |
|||
{ |
|||
_compiledQueryCacheKey = compiledQueryCacheKey; |
|||
_currentFilterCacheKey = currentFilterCacheKey; |
|||
} |
|||
|
|||
public override bool Equals(object? obj) |
|||
{ |
|||
return obj is AbpCompiledQueryCacheKey key && Equals(key); |
|||
} |
|||
|
|||
public bool Equals(AbpCompiledQueryCacheKey other) |
|||
{ |
|||
return _compiledQueryCacheKey.Equals(other._compiledQueryCacheKey) && |
|||
_currentFilterCacheKey == other._currentFilterCacheKey; |
|||
} |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
return HashCode.Combine(_compiledQueryCacheKey, _currentFilterCacheKey); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using System; |
|||
using System.Threading; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore.GlobalFilters; |
|||
|
|||
public class AbpEfCoreCurrentDbContext |
|||
{ |
|||
private readonly AsyncLocal<IAbpEfCoreDbFunctionContext?> _current = new AsyncLocal<IAbpEfCoreDbFunctionContext?>(); |
|||
|
|||
public IAbpEfCoreDbFunctionContext? Context => _current.Value; |
|||
|
|||
public IDisposable Use(IAbpEfCoreDbFunctionContext? context) |
|||
{ |
|||
var previousValue = Context; |
|||
_current.Value = context; |
|||
return new DisposeAction(() => |
|||
{ |
|||
_current.Value = previousValue; |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using System; |
|||
using System.Reflection; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore.GlobalFilters; |
|||
|
|||
public static class AbpEfCoreDataFilterDbFunctionMethods |
|||
{ |
|||
public const string NotSupportedExceptionMessage = "Your EF Core database provider does not support 'User-defined function mapping'." + |
|||
"Please set 'UseDbFunction' of 'AbpEfCoreGlobalFilterOptions' to false to disable it." + |
|||
"See https://learn.microsoft.com/en-us/ef/core/querying/user-defined-function-mapping for more information." ; |
|||
|
|||
public static bool SoftDeleteFilter(bool isDeleted, bool boolParam) |
|||
{ |
|||
throw new NotSupportedException(NotSupportedExceptionMessage); |
|||
} |
|||
|
|||
public static MethodInfo SoftDeleteFilterMethodInfo => typeof(AbpEfCoreDataFilterDbFunctionMethods).GetMethod(nameof(SoftDeleteFilter))!; |
|||
|
|||
public static bool MultiTenantFilter(Guid? tenantId, Guid? currentTenantId, bool boolParam) |
|||
{ |
|||
throw new NotSupportedException(NotSupportedExceptionMessage); |
|||
} |
|||
|
|||
public static MethodInfo MultiTenantFilterMethodInfo => typeof(AbpEfCoreDataFilterDbFunctionMethods).GetMethod(nameof(MultiTenantFilter))!; |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
namespace Volo.Abp.EntityFrameworkCore.GlobalFilters; |
|||
|
|||
public class AbpEfCoreGlobalFilterOptions |
|||
{ |
|||
/// <summary>
|
|||
/// Use User-defined function mapping to filter data.
|
|||
/// https://learn.microsoft.com/en-us/ef/core/querying/user-defined-function-mapping
|
|||
/// </summary>
|
|||
public bool UseDbFunction { get; set; } |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using Volo.Abp.Data; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore.GlobalFilters; |
|||
|
|||
public interface IAbpEfCoreDbFunctionContext |
|||
{ |
|||
IAbpLazyServiceProvider LazyServiceProvider { get; set; } |
|||
|
|||
ICurrentTenant CurrentTenant { get; } |
|||
|
|||
IDataFilter DataFilter { get; } |
|||
|
|||
string GetCompiledQueryCacheKey(); |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
namespace Volo.Abp.ExceptionHandling; |
|||
|
|||
public static class AbpExceptionHandlingConsts |
|||
{ |
|||
public const string Unauthorized = "Unauthorized"; |
|||
public const string InvalidToken = "invalid_token"; |
|||
public const string SessionExpired = "SessionExpired"; |
|||
} |
|||