mirror of https://github.com/abpframework/abp.git
347 changed files with 4696 additions and 2577 deletions
@ -0,0 +1,72 @@ |
|||
# Use Deepl to translate localization files |
|||
|
|||
Translating localized text during the development of ABP modules is a boring job. For this reason, we have added the `translate` command to the CLI tool to translate the localization files, but it still requires manual translation of texts by the developer. |
|||
|
|||
Now, we introduce a new way to translate the target language, which is to use the [Deepl](https://www.deepl.com/translator) translation service to translate the target language. |
|||
|
|||
You can use the `translate --online` command in your module directory to translate the target language. |
|||
|
|||
For example, if you have added all `en` localization texts to your module, and you want to translate them to `zh-Hans`, you can run the following command: |
|||
|
|||
``` |
|||
abp translate -r en -c zh-Hans --online --deepl-auth-key your_auth_key |
|||
``` |
|||
|
|||
* `-r` parameter is used to specify the source language. It is usually a localized file that you have created. |
|||
* `-c` parameter is used to specify the target language. It is usually a language that you want to translate. |
|||
* `--online` parameter is used to indicate that the translation is performed from the Deepl service. |
|||
* `--deepl-auth-key` parameter is the API key of your Deepl account. You can get it from [here](https://support.deepl.com/hc/en-us/articles/360020695820-Authentication-Key). |
|||
|
|||
|
|||
The output of the above command is as follows: |
|||
|
|||
``` |
|||
ABP CLI 8.0.0 |
|||
Abp translate online... |
|||
Target culture: zh-Hans |
|||
Reference culture: en |
|||
Create translation: Settings => 设置 |
|||
Create translation: SuccessfullySaved => 成功保存 |
|||
Create translation: Permission:SettingManagement => 设置管理 |
|||
Create translation: Permission:Emailing => 发送电子邮件 |
|||
Create translation: Permission:EmailingTest => 电子邮件测试 |
|||
Create translation: Permission:TimeZone => 时区 |
|||
Create translation: SendTestEmail => 发送测试电子邮件 |
|||
Create translation: SenderEmailAddress => 发件人电子邮件地址 |
|||
Create translation: TargetEmailAddress => 目标电子邮件地址 |
|||
Create translation: Subject => 主题 |
|||
Create translation: Body => 正文 |
|||
Create translation: TestEmailSubject => 测试电子邮件 {0} |
|||
Create translation: TestEmailBody => 在此测试电子邮件正文信息 |
|||
Create translation: SuccessfullySent => 成功发送 |
|||
Create translation: Send => 发送 |
|||
Create translation: Menu:Emailing => 发送电子邮件 |
|||
Create translation: Menu:TimeZone => 时区 |
|||
Create translation: DisplayName:Timezone => 时区 |
|||
Create translation: TimezoneHelpText => 此设置用于应用程序范围或基于租户的设置。 |
|||
Create translation: SmtpHost => 主机 |
|||
Create translation: SmtpPort => 端口 |
|||
Create translation: SmtpUserName => 用户名 |
|||
Create translation: SmtpPassword => 密码 |
|||
Create translation: SmtpDomain => 域名 |
|||
Create translation: SmtpEnableSsl => 启用 ssl |
|||
Create translation: SmtpUseDefaultCredentials => 使用默认凭据 |
|||
Create translation: DefaultFromAddress => 默认地址 |
|||
Create translation: DefaultFromDisplayName => 显示名称的默认值 |
|||
Create translation: Feature:SettingManagementGroup => 设置管理 |
|||
Create translation: Feature:SettingManagementEnable => 启用设置管理 |
|||
Create translation: Feature:SettingManagementEnableDescription => 在应用程序中启用设置管理系统。 |
|||
Create translation: Feature:AllowChangingEmailSettings => 允许更改电子邮件设置。 |
|||
Create translation: Feature:AllowChangingEmailSettingsDescription => 允许更改电子邮件设置。 |
|||
Write translation json to setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hans.json. |
|||
``` |
|||
|
|||
The generated `zh-Hans.json` as follow: |
|||
|
|||
 |
|||
|
|||
In this example, It only translates one `en.json` to `zh-Hans.json`, but if there are multiple `en.json` files in the module, it will translate all `en.json` files to `zh-Hans.json`. |
|||
|
|||
Of course, the translation is not always correct, you can update the translation in the generated `zh-Hans.json` files. |
|||
|
|||
Enjoy it! |
|||
|
After Width: | Height: | Size: 456 KiB |
@ -0,0 +1,198 @@ |
|||
# ABP Now Supports Keyed Services! |
|||
|
|||
In this post, I describe the new **"keyed service"** support for the dependency injection container, which came with .NET 8.0. Then, I'll show you an example of usage within the ABP Framework. |
|||
|
|||
## What Are Keyed Services? |
|||
|
|||
ASP.NET Core ships with a built-in dependency injection container, which is a pretty basic DI container that supports minimal features a dependency injection container is supposed to have. For that reason, most of the .NET users use third-party containers like [Autofac](https://autofac.org/), or Ninject. |
|||
|
|||
> ABP Framework uses Autofac by default in startup templates, and it supports more features. For example, the built-in DI container does not natively support property injection but Autofac does. |
|||
|
|||
[**Keyed dependency injection (DI) services**](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-8.0#keyed-services) were added to the built-in DI container as a new feature with .NET 8.0. This is an important feature, which allows for registering and retrieving DI services using keys/names. |
|||
|
|||
Prior to this version, you could register multiple services as follows: |
|||
|
|||
```csharp |
|||
var builder = WebApplication.CreateBuilder(args); |
|||
|
|||
//service registrations |
|||
builder.Services.AddTransient<IMyService, MyServiceOne>(); |
|||
builder.Services.AddTransient<IMyService, MyServiceTwo>(); |
|||
builder.Services.AddTransient<IMyService, MyServiceThree>(); |
|||
``` |
|||
|
|||
But without keyed services, you could only retrieve all of the services like this: |
|||
|
|||
```csharp |
|||
public class BookService(IEnumerable<IMyService> services) {} |
|||
``` |
|||
|
|||
Or the last registered one (`MyServiceThree` in our case): |
|||
|
|||
```csharp |
|||
public class BookService(IMyService service) {} |
|||
``` |
|||
|
|||
There was not a simple way to retrieve a specific implementation type directly. With keyed services, now we can register services with a key and then resolve them by the key anywhere we want and retrieve the only one that we want to use. |
|||
|
|||
## ABP Framework Now Supports Keyed Services! |
|||
|
|||
Autofac was already supporting the named/keyed DI services for a long time and with v9.0.0, they made it compatible with the `Microsoft.Extensions.DependencyInjection` package (including the keyed service support). |
|||
|
|||
After the v9.0.0 was released for Autofac, then the ABP Framework Core team immediately updated the `Autofac.Extensions.DependencyInjection` package to `v9.0.0`, made the related changes in its own Autofac package and included in the v8.0.2 release. |
|||
|
|||
You can see the related changes in the following PRs: |
|||
|
|||
* https://github.com/abpframework/abp/pull/18775 |
|||
* https://github.com/abpframework/abp/pull/18777 |
|||
|
|||
## Using Keyed Services with ABP Framework |
|||
|
|||
After the quick background of the process and the feature itself, now let's see the keyed services in action. |
|||
|
|||
First, update the ABP CLI to v8.0.2 with the following command (if your CLI version is newer, no need to apply this): |
|||
|
|||
```bash |
|||
dotnet tool update -g Volo.Abp.Cli --version 8.0.2 |
|||
``` |
|||
|
|||
Then, we can create an application template (single-layer) with the following command (MVC as the UI option and EF Core as the DB provider): |
|||
|
|||
```bash |
|||
abp new KeyedServiceDemo -t app-nolayers -csf --version 8.0.2 |
|||
``` |
|||
|
|||
After the application is created, we can open it in an IDE and start developing... |
|||
|
|||
**Example**: |
|||
|
|||
Let's assume that we have an interface called `IMyService`, which has three implementation types `MyServiceOne`, `MyServiceTwo` and `MyServiceThree`: |
|||
|
|||
```csharp |
|||
public interface IMyService |
|||
{ |
|||
string GetMessage(); |
|||
} |
|||
|
|||
public class MyServiceOne : IMyService |
|||
{ |
|||
public string GetMessage() => "MyServiceOne"; |
|||
} |
|||
|
|||
public class MyServiceTwo : IMyService |
|||
{ |
|||
public string GetMessage() => "MyServiceTwo"; |
|||
} |
|||
|
|||
public class MyServiceThree : IMyService |
|||
{ |
|||
public string GetMessage() => "MyServiceThree"; |
|||
} |
|||
``` |
|||
|
|||
After creating our services, now we can register them in our module class. So, open the module class and add the following lines in the `ConfigureServices` method: |
|||
|
|||
```csharp |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
//code abbreviation for clarity... |
|||
|
|||
context.Services.AddKeyedTransient<IMyService, MyServiceOne>("myserviceone"); |
|||
context.Services.AddKeyedTransient<IMyService, MyServiceTwo>("myservicetwo"); |
|||
context.Services.AddKeyedTransient<IMyService, MyServiceThree>("myservicethree"); |
|||
} |
|||
``` |
|||
|
|||
* To register a keyed service, you can use one of the `AddKeyedTransient`, `AddKeyedScoped`, or `AddKeyedSingleton` overloads, and provide a key for the registration. |
|||
* In the example above, I used `AddKeyedTransient` overload, and registered all of these services as in the `Transient` lifetime. |
|||
|
|||
Then, when you want to retrieve a keyed service, you can use the `[FromKeyedServices(object key)]` attribute: |
|||
|
|||
```csharp |
|||
public class NotificationService([FromKeyedServices("myserviceone")] IMyService myServiceOne) |
|||
{ |
|||
public string Notify() => myServiceOne.GetMessage(); |
|||
} |
|||
``` |
|||
|
|||
With this kind of use, you can be certain that not the latest registered service is resolved and instead the keyed service will be resolved, and then you can use it. |
|||
|
|||
Also, you can use the `FromKeyedServicesAttribute` with minimal APIs as follows: |
|||
|
|||
```csharp |
|||
public override void OnApplicationInitialization(ApplicationInitializationContext context) |
|||
{ |
|||
//code abbreviation for clarity... |
|||
|
|||
app.UseConfiguredEndpoints(endpoints => |
|||
{ |
|||
endpoints.MapGet("/my-service", ([FromKeyedServices("myserviceone")] IMyService myservice) => myservice.GetMessage()); |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
That's how it's easy to register multiple services in a keyed fashion and resolve them by keys! |
|||
|
|||
## Advanced |
|||
|
|||
In this section, I want to briefly mention some advanced topics (relatively 😀), such as what happens if I register more than one service with the same key, or is there a way to get the keyed services from the `ServiceProvider` etc. |
|||
|
|||
### Registering Multiple Services with the Same Key |
|||
|
|||
Let's assume that you mistakenly registered multiple services with the same key: |
|||
|
|||
```csharp |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
//code abbreviation for clarity... |
|||
|
|||
context.Services.AddKeyedTransient<IMyService, MyServiceOne>("myserviceone"); |
|||
context.Services.AddKeyedTransient<IMyService, MyServiceTwo>("myserviceone"); |
|||
} |
|||
``` |
|||
|
|||
In that case, when you try to resolve the dependency with the key, then the last registered one will be used without having a problem: |
|||
|
|||
```csharp |
|||
//it will resolve the `MyServiceTwo` service! |
|||
endpoints.MapGet("/my-service", ([FromKeyedServices("myserviceone")] IMyService myservice) => myservice.GetMessage()); |
|||
``` |
|||
|
|||
Also, since you used the same key for multiple services, then you can inject the `IEnumerable<IMyService>` to get all services for a certain key and use them: |
|||
|
|||
```csharp |
|||
//it will resolve both `MyServiceOne` and `MyServiceTwo` services! |
|||
endpoints.MapGet("/my-service", ([FromKeyedServices("myserviceone")] IEnumerable<IMyService> myservices) => |
|||
{ |
|||
var sb = new StringBuilder(); |
|||
foreach (var myService in myservices) |
|||
{ |
|||
sb.AppendLine(myService.GetMessage()); |
|||
} |
|||
|
|||
return sb.ToString(); |
|||
}); |
|||
``` |
|||
|
|||
### Resolving Keyed Services from ServiceProvider & LazyServiceProvider |
|||
|
|||
You can resolve keyed services by using one of the extension methods (`.GetKeyedServices`, `.GetKeyedService<>`, `.GetRequiredKeyedService<>`, ...): |
|||
|
|||
```csharp |
|||
//uses the `MyServiceTwo` service! |
|||
var myService = ServiceProvider.GetRequiredKeyedService<IMyService>("myserviceone"); |
|||
``` |
|||
|
|||
On the other hand, resolving keyed services from `LazyServiceProvider` is not supported in v8.0.2, but there is a PR, which you can find at https://github.com/abpframework/abp/pull/18792, it will fix this problem and it will be included in the next version. |
|||
|
|||
### Automatically Registering Keyed Services |
|||
|
|||
Currently, if you want to register a keyed service, you need to do it manually as we see in the previous sections by using one of the overloads (`.AddKeyedTransient`, `.AddKeyedScoped` and `.AddKeyedSingleton`). |
|||
|
|||
It would be good if we could make this process automatically and not need to manually register services, and for that purpose, I have [created an issue](https://github.com/abpframework/abp/issues/18794) that aims to introduce an attribute, which allows us to automatically register multiple services as keyed services. |
|||
|
|||
You can [follow the issue](https://github.com/abpframework/abp/issues/18794) if you are considering using keyed services in your application and don't want to register them manually. |
|||
|
|||
## Summary |
|||
|
|||
In this post, I described the new **"keyed service"** support added to the built-in dependency injection container that was released in .NET 8, and wanted to announce it's being supported from v8.0.2 for ABP Framework. It's a really good addition to the built-in dependency injection container and can be pretty useful for certain use-cases. |
|||
|
After Width: | Height: | Size: 387 KiB |
@ -0,0 +1,85 @@ |
|||
# ABP Version 8.1 Migration Guide |
|||
|
|||
This document is a guide for upgrading ABP v8.0 solutions to ABP v8.1. There are some changes in this version that may affect your applications, please read it carefully and apply the necessary changes to your application. |
|||
|
|||
## Added `NormalizedName` property to `Tenant` |
|||
|
|||
The `Tenant` entity has a new property called `NormalizedName`. It is used to find/cache a tenant by its name in a case-insensitive way. |
|||
This property is automatically set when a tenant is created or updated. It gets the normalized name of the tenant name by `UpperInvariantTenantNormalizer(ITenantNormalizer)` service. You can implement this service to change the normalization logic. |
|||
|
|||
### `ITenantStore` |
|||
|
|||
The `ITenantStore` will use the `NormalizedName` parameter to get tenants, Please use the `ITenantNormalizer` to normalize the tenant name before calling the `ITenantStore` methods. |
|||
|
|||
### Update `NormalizedName` in `appsettings.json` |
|||
|
|||
If your tenants defined in the `appsettings.json` file, you should add the `NormalizedName` property to your tenants. |
|||
|
|||
````json |
|||
"Tenants": [ |
|||
{ |
|||
"Id": "446a5211-3d72-4339-9adc-845151f8ada0", |
|||
"Name": "tenant1", |
|||
"NormalizedName": "TENANT1" // <-- Add this property |
|||
}, |
|||
{ |
|||
"Id": "25388015-ef1c-4355-9c18-f6b6ddbaf89d", |
|||
"Name": "tenant2", |
|||
"NormalizedName": "TENANT2", // <-- Add this property |
|||
"ConnectionStrings": { |
|||
"Default": "...tenant2's db connection string here..." |
|||
} |
|||
} |
|||
] |
|||
```` |
|||
|
|||
### Update `NormalizedName` in the database |
|||
|
|||
Please add a sql script to your migration to set the `NormalizedName` property of the existing tenants. You can use the following script: |
|||
|
|||
> This script is for the SQL Server database. You can change it for your database. |
|||
|
|||
> The table name `SaasTenants` is used for ABP commercial Saas module. `AbpTenants` is for the ABP open-source Tenant Management module. |
|||
|
|||
```sql |
|||
UPDATE SaasTenants SET NormalizedName = UPPER(Name) WHERE NormalizedName IS NULL OR NormalizedName = '' |
|||
``` |
|||
|
|||
```csharp |
|||
/// <inheritdoc /> |
|||
public partial class Add_NormalizedName : Migration |
|||
{ |
|||
/// <inheritdoc /> |
|||
protected override void Up(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.AddColumn<string>( |
|||
name: "NormalizedName", |
|||
table: "SaasTenants", |
|||
type: "nvarchar(64)", |
|||
nullable: false, |
|||
defaultValue: ""); |
|||
|
|||
migrationBuilder.Sql("UPDATE SaasTenants SET NormalizedName = UPPER(Name) WHERE NormalizedName IS NULL OR NormalizedName = ''"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_SaasTenants_NormalizedName", |
|||
table: "SaasTenants", |
|||
column: "NormalizedName"); |
|||
} |
|||
|
|||
/// <inheritdoc /> |
|||
protected override void Down(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.DropIndex( |
|||
name: "IX_SaasTenants_NormalizedName", |
|||
table: "SaasTenants"); |
|||
|
|||
migrationBuilder.DropColumn( |
|||
name: "NormalizedName", |
|||
table: "SaasTenants"); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
See https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#adding-raw-sql to learn how to add raw SQL to migrations. |
|||
|
|||
@ -0,0 +1,197 @@ |
|||
# HTTP Error Handling |
|||
|
|||
When the `RestService` is used, all HTTP errors are reported to the [`HttpErrorReporterService`](./HTTP-Error-Reporter-Service), and then `ErrorHandler`, a service exposed by the `@abp/ng.theme.shared` package automatically handles the errors. |
|||
|
|||
## Custom HTTP Error Handler |
|||
|
|||
### Function Method `Deprecated` |
|||
|
|||
A custom HTTP error handler can be registered to an injection token named `HTTP_ERROR_HANDLER`. If a custom handler function is registered, the `ErrorHandler` executes that function. |
|||
|
|||
See an example: |
|||
|
|||
```ts |
|||
// http-error-handler.ts |
|||
import { ContentProjectionService, PROJECTION_STRATEGY } from '@abp/ng.core'; |
|||
import { ToasterService } from '@abp/ng.theme.shared'; |
|||
import { HttpErrorResponse } from '@angular/common/http'; |
|||
import { Injector } from '@angular/core'; |
|||
import { of, EMPTY } from 'rxjs'; |
|||
import { Error404Component } from './error404/error404.component'; |
|||
|
|||
export function handleHttpErrors(injector: Injector, httpError: HttpErrorResponse) { |
|||
if (httpError.status === 400) { |
|||
const toaster = injector.get(ToasterService); |
|||
toaster.error(httpError.error?.error?.message || 'Bad request!', '400'); |
|||
return EMPTY; |
|||
} |
|||
|
|||
if (httpError.status === 404) { |
|||
const contentProjection = injector.get(ContentProjectionService); |
|||
contentProjection.projectContent(PROJECTION_STRATEGY.AppendComponentToBody(Error404Component)); |
|||
return EMPTY; |
|||
} |
|||
|
|||
return of(httpError); |
|||
} |
|||
|
|||
// app.module.ts |
|||
import { Error404Component } from './error404/error404.component'; |
|||
import { handleHttpErrors } from './http-error-handling'; |
|||
import { HTTP_ERROR_HANDLER, ... } from '@abp/ng.theme.shared'; |
|||
|
|||
@NgModule({ |
|||
// ... |
|||
providers: [ |
|||
// ... |
|||
{ provide: HTTP_ERROR_HANDLER, useValue: handleHttpErrors } |
|||
], |
|||
declarations: [ |
|||
//... |
|||
Error404Component], |
|||
}) |
|||
export class AppModule {} |
|||
``` |
|||
|
|||
In the example above: |
|||
|
|||
- Created a function named `handleHttpErrors` and defined as value of the `HTTP_ERROR_HANDLER` provider in app.module. After this, the function executes when an HTTP error occurs. |
|||
- 400 bad request errors is handled. When a 400 error occurs. |
|||
|
|||
- Since `of(httpError)` is returned at bottom of the `handleHttpErrors`, the `ErrorHandler` will handle the HTTP errors except 400 and 404 errors. |
|||
|
|||
**Note 1:** If you put `return EMPTY` to next line of handling an error, default error handling will not work for that error. [EMPTY](https://rxjs.dev/api/index/const/EMPTY) can be imported from `rxjs`. |
|||
|
|||
```ts |
|||
export function handleHttpErrors( |
|||
injector: Injector, |
|||
httpError: HttpErrorResponse |
|||
) { |
|||
if (httpError.status === 403) { |
|||
// handle 403 errors here |
|||
return EMPTY; // put return EMPTY to skip default error handling |
|||
} |
|||
} |
|||
``` |
|||
|
|||
**Note 2:** If you put `return of(httpError)`, default error handling will work. |
|||
|
|||
- `of` is a function. It can be imported from `rxjs`. |
|||
- `httpError` is the second parameter of the error handler function which is registered to the `HTTP_ERROR_HANDLER` provider. Type of the `httpError` is `HttpErrorResponse`. |
|||
|
|||
```ts |
|||
import { of } from "rxjs"; |
|||
|
|||
export function handleHttpErrors( |
|||
injector: Injector, |
|||
httpError: HttpErrorResponse |
|||
) { |
|||
if (httpError.status === 500) { |
|||
// handle 500 errors here |
|||
} |
|||
|
|||
// you can return the of(httpError) at bottom of the function to run the default handler of ABP for HTTP errors that you didn't handle above. |
|||
return of(httpError); |
|||
} |
|||
``` |
|||
|
|||
### Service Method |
|||
|
|||
You can provide **more than one handler** with services, a custom HTTP error handler service can be registered with injection token named **`CUSTOM_ERROR_HANDLERS`**. ABP has some default [error handlers](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/theme-shared/src/lib/providers/error-handlers.provider.ts). |
|||
|
|||
### How To Add New Handler Service |
|||
|
|||
ABP error handler services are implements the interface of **CustomHttpErrorHandlerService**. |
|||
|
|||
**Interface of `CUSTOM_ERROR_HANDLERS`** |
|||
|
|||
```ts |
|||
interface CustomHttpErrorHandlerService { |
|||
readonly priority: number; |
|||
canHandle(error: unknown): boolean; |
|||
execute(): void; |
|||
} |
|||
``` |
|||
|
|||
- **`priority`** ABP sorts the services according to the number of the priority variable. Higher priority will be checked first. |
|||
- **`canHandle`** Check if the service can handle the error. Returns boolean. |
|||
- **`execute`** If the service can handle the error, then run the execute method. |
|||
|
|||
**In Summary** |
|||
|
|||
- Services are sorted by their priority number. |
|||
- Start from highest priority service and run canHandle() method. Pick the service if can handle the error, if not check next service. |
|||
- If the service found, run the execute method of a service. Done. |
|||
|
|||
See an example: |
|||
|
|||
```ts |
|||
// custom-error-handler.service.ts |
|||
import { inject, Injectable } from "@angular/core"; |
|||
import { HttpErrorResponse } from "@angular/common/http"; |
|||
import { CustomHttpErrorHandlerService } from "@abp/ng.theme.shared"; |
|||
import { CUSTOM_HTTP_ERROR_HANDLER_PRIORITY } from "@abp/ng.theme.shared"; |
|||
import { ToasterService } from "@abp/ng.theme.shared"; |
|||
|
|||
@Injectable({ providedIn: "root" }) |
|||
export class MyCustomErrorHandlerService |
|||
implements CustomHttpErrorHandlerService |
|||
{ |
|||
// You can write any number here, ex: 9999 |
|||
readonly priority = CUSTOM_HTTP_ERROR_HANDLER_PRIORITY.veryHigh; |
|||
protected readonly toaster = inject(ToasterService); |
|||
private error: HttpErrorResponse | undefined = undefined; |
|||
|
|||
// What kind of error should be handled by this service? You can decide it in this method. If error is suitable to your case then return true; otherwise return false. |
|||
canHandle(error: unknown): boolean { |
|||
if (error instanceof HttpErrorResponse && error.status === 400) { |
|||
this.error = error; |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
// If this service is picked from ErrorHandler, this execute method will be called. |
|||
execute() { |
|||
this.toaster.error( |
|||
this.error.error?.error?.message || "Bad request!", |
|||
"400" |
|||
); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
```ts |
|||
|
|||
// app.module.ts |
|||
import { CUSTOM_ERROR_HANDLERS, ... } from '@abp/ng.theme.shared'; |
|||
import { MyCustomErrorHandlerService } from './custom-error-handler.service'; |
|||
|
|||
@NgModule({ |
|||
// ... |
|||
providers: [ |
|||
// ... |
|||
{ |
|||
provide: CUSTOM_ERROR_HANDLERS, |
|||
useExisting: MyCustomErrorHandlerService, |
|||
multi: true, |
|||
} |
|||
] |
|||
}) |
|||
export class AppModule {} |
|||
``` |
|||
|
|||
In the example above: |
|||
|
|||
- Created a service named `MyCustomErrorHandlerService`, and provided via `useExisting` key because we dont want another instance of it. And set `multi` key to true because ABP default error handlers are also provided with **CUSTOM_ERROR_HANDLERS** injection token. |
|||
|
|||
- 400 errors are handled from custom `MyCustomErrorHandlerService`. When a 400 error occurs, backend error message will be displayed as shown below: |
|||
|
|||
 |
|||
|
|||
### Notes |
|||
|
|||
- If your service cannot handle the error. Then ABP will check the next Error Service. |
|||
- If none of the service handle the error. Then basic confirmation message about the error will be shown to the user. |
|||
- You can provide more than one service, with CUSTOM_ERROR_HANDLER injection token. |
|||
- If you want your custom service to be evaluated (checked) earlier, set the priority variable high. |
|||
@ -0,0 +1,31 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.AspNetCore.Mvc.Abstractions; |
|||
using Microsoft.AspNetCore.Mvc.Controllers; |
|||
using Volo.Abp.AspNetCore.Filters; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.ApplicationModels; |
|||
|
|||
public class AbpMvcActionDescriptorProvider : IActionDescriptorProvider |
|||
{ |
|||
public virtual int Order => -1000 + 10; |
|||
|
|||
public virtual void OnProvidersExecuting(ActionDescriptorProviderContext context) |
|||
{ |
|||
} |
|||
|
|||
public virtual void OnProvidersExecuted(ActionDescriptorProviderContext context) |
|||
{ |
|||
foreach (var action in context.Results.Where(x => x is ControllerActionDescriptor).Cast<ControllerActionDescriptor>()) |
|||
{ |
|||
var disableAbpFeaturesAttribute = action.ControllerTypeInfo.GetCustomAttribute<DisableAbpFeaturesAttribute>(true); |
|||
if (disableAbpFeaturesAttribute != null && disableAbpFeaturesAttribute.DisableMvcFilters) |
|||
{ |
|||
action.FilterDescriptors.RemoveAll(x => x.Filter is ServiceFilterAttribute serviceFilterAttribute && |
|||
typeof(IAbpFilter).IsAssignableFrom(serviceFilterAttribute.ServiceType)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace Volo.Abp.AspNetCore.Filters; |
|||
|
|||
public interface IAbpFilter |
|||
{ |
|||
|
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue