mirror of https://github.com/abpframework/abp.git
217 changed files with 3603 additions and 2157 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,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 |
|||
{ |
|||
|
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using System.Reflection; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Http; |
|||
using Microsoft.AspNetCore.Mvc.Controllers; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Middleware; |
|||
|
|||
public abstract class AbpMiddlewareBase : IMiddleware |
|||
{ |
|||
protected Task<bool> ShouldSkipAsync(HttpContext context, RequestDelegate next) |
|||
{ |
|||
var endpoint = context.GetEndpoint(); |
|||
var controllerActionDescriptor = endpoint?.Metadata.GetMetadata<ControllerActionDescriptor>(); |
|||
var disableAbpFeaturesAttribute = controllerActionDescriptor?.ControllerTypeInfo.GetCustomAttribute<DisableAbpFeaturesAttribute>(); |
|||
return Task.FromResult(disableAbpFeaturesAttribute != null && disableAbpFeaturesAttribute.DisableMiddleware); |
|||
} |
|||
|
|||
public abstract Task InvokeAsync(HttpContext context, RequestDelegate next); |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp; |
|||
|
|||
[AttributeUsage(AttributeTargets.Class)] |
|||
public class DisableAbpFeaturesAttribute : Attribute |
|||
{ |
|||
/// <summary>
|
|||
/// The framework will not register any interceptors for the class.
|
|||
/// This will cause the all features that depend on interceptors to not work.
|
|||
/// </summary>
|
|||
public bool DisableInterceptors { get; set; } = true; |
|||
|
|||
/// <summary>
|
|||
/// The framework middleware will skip the class.
|
|||
/// This will cause the all features that depend on middleware to not work.
|
|||
/// </summary>
|
|||
public bool DisableMiddleware { get; set; } = true; |
|||
|
|||
/// <summary>
|
|||
/// The framework will not remove all built-in filters for the class.
|
|||
/// This will cause the all features that depend on filters to not work.
|
|||
/// </summary>
|
|||
public bool DisableMvcFilters { get; set; } = true; |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Features; |
|||
|
|||
public class FeatureValueProviderManager : IFeatureValueProviderManager, ISingletonDependency |
|||
{ |
|||
public IReadOnlyList<IFeatureValueProvider> ValueProviders => _lazyProviders.Value; |
|||
private readonly Lazy<List<IFeatureValueProvider>> _lazyProviders; |
|||
|
|||
protected AbpFeatureOptions Options { get; } |
|||
protected IServiceProvider ServiceProvider { get; } |
|||
|
|||
public FeatureValueProviderManager( |
|||
IServiceProvider serviceProvider, |
|||
IOptions<AbpFeatureOptions> options) |
|||
{ |
|||
Options = options.Value; |
|||
ServiceProvider = serviceProvider; |
|||
|
|||
_lazyProviders = new Lazy<List<IFeatureValueProvider>>(GetProviders, true); |
|||
} |
|||
|
|||
protected virtual List<IFeatureValueProvider> GetProviders() |
|||
{ |
|||
var providers = Options |
|||
.ValueProviders |
|||
.Select(type => (ServiceProvider.GetRequiredService(type) as IFeatureValueProvider)!) |
|||
.ToList(); |
|||
|
|||
var multipleProviders = providers.GroupBy(p => p.Name).FirstOrDefault(x => x.Count() > 1); |
|||
if(multipleProviders != null) |
|||
{ |
|||
throw new AbpException($"Duplicate feature value provider name detected: {multipleProviders.Key}. Providers:{Environment.NewLine}{multipleProviders.Select(p => p.GetType().FullName!).JoinAsString(Environment.NewLine)}"); |
|||
} |
|||
|
|||
return providers; |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.Abp.Features; |
|||
|
|||
public interface IFeatureValueProviderManager |
|||
{ |
|||
IReadOnlyList<IFeatureValueProvider> ValueProviders { get; } |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace Volo.Abp.MultiTenancy; |
|||
|
|||
public interface ITenantNormalizer |
|||
{ |
|||
string? NormalizeName(string? name); |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.MultiTenancy; |
|||
|
|||
public class UpperInvariantTenantNormalizer : ITenantNormalizer, ITransientDependency |
|||
{ |
|||
public virtual string? NormalizeName(string? name) |
|||
{ |
|||
return name?.Normalize().ToUpperInvariant(); |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue