@ -1,4 +1,6 @@ |
|||||
# Identity Management Module |
# Identity Management Module |
||||
|
|
||||
|
Identity module is used to manage [organization units](Organization-Units.md), roles, users and their permissions, based on the Microsoft Identity library. |
||||
|
|
||||
See [the source code](https://github.com/abpframework/abp/tree/dev/modules/identity). Documentation will come soon... |
See [the source code](https://github.com/abpframework/abp/tree/dev/modules/identity). Documentation will come soon... |
||||
|
|
||||
|
|||||
@ -0,0 +1,47 @@ |
|||||
|
# Organization Unit Management |
||||
|
|
||||
|
Organization units (OU) is a part of **Identity Module** and can be used to **hierarchically group users and entities**. |
||||
|
|
||||
|
### OrganizationUnit Entity |
||||
|
|
||||
|
An OU is represented by the **OrganizationUnit** entity. The fundamental properties of this entity are: |
||||
|
|
||||
|
- **TenantId**: Tenant's Id of this OU. Can be null for host OUs. |
||||
|
- **ParentId**: Parent OU's Id. Can be null if this is a root OU. |
||||
|
- **Code**: A hierarchical string code that is unique for a tenant. |
||||
|
- **DisplayName**: Shown name of the OU. |
||||
|
|
||||
|
The OrganizationUnit entity's primary key (Id) is a **Guid** type and it derives from the [**FullAuditedAggregateRoot**](../Entities.md) class. |
||||
|
|
||||
|
#### Organization Tree |
||||
|
|
||||
|
Since an OU can have a parent, all OUs of a tenant are in a **tree** structure. There are some rules for this tree; |
||||
|
|
||||
|
- There can be more than one root (where the `ParentId` is `null`). |
||||
|
- There is a limit for the first-level children count of an OU (because of the fixed OU Code unit length explained below). |
||||
|
|
||||
|
#### OU Code |
||||
|
|
||||
|
OU code is automatically generated and maintained by the OrganizationUnit Manager. It's a string that looks something like this: |
||||
|
|
||||
|
"**00001.00042.00005**" |
||||
|
|
||||
|
This code can be used to easily query the database for all the children of an OU (recursively). There are some rules for this code: |
||||
|
|
||||
|
- It must be **unique** for a [tenant](../Multi-Tenancy.md). |
||||
|
- All the children of the same OU have codes that **start with the parent OU's code**. |
||||
|
- It's **fixed length** and based on the level of the OU in the tree, as shown in the sample. |
||||
|
- While the OU code is unique, it can be **changeable** if you move an OU. |
||||
|
- You must reference an OU by Id, not Code. |
||||
|
|
||||
|
### OrganizationUnit Manager |
||||
|
|
||||
|
The **OrganizationUnitManager** class can be [injected](../Dependency-Injection.md) and used to manage OUs. Common use cases are: |
||||
|
|
||||
|
- Create, Update or Delete an OU |
||||
|
- Move an OU in the OU tree. |
||||
|
- Getting information about the OU tree and its items. |
||||
|
|
||||
|
#### Multi-Tenancy |
||||
|
|
||||
|
The `OrganizationUnitManager` is designed to work for a **single tenant** at a time. It works for the **current tenant** by default. |
||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 56 KiB |
@ -0,0 +1,370 @@ |
|||||
|
# v2.9 to v3.0 Angular UI Migration Guide |
||||
|
|
||||
|
## What Changed in v3.0? |
||||
|
|
||||
|
### Angular 10 |
||||
|
|
||||
|
The new ABP Angular UI is based on Angular 10 and TypeScript 3.9, and we have dropped support for Angular 8. Nevertheless, ABP modules will keep working with Angular 9. Therefore, if your project is Angular 9, you do not need to update to Angular 10. The update is usually very easy though. |
||||
|
|
||||
|
#### What to Do When Migrating? |
||||
|
|
||||
|
Open a terminal at your root folder and run the following command: |
||||
|
|
||||
|
```sh |
||||
|
yarn ng update @angular/cli @angular/core --force |
||||
|
``` |
||||
|
|
||||
|
This will make the following modifications: |
||||
|
|
||||
|
- Update your package.json and install new packages |
||||
|
- Revise tsconfig.json files to create a "Solution Style" configuration |
||||
|
- Rename `browserlist` as `.browserlistrc` |
||||
|
|
||||
|
On the other hand, it would be better if you check which packages to update first with `yarn ng update` command alone. Angular will give you a list of packages to update. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
When Angular reports the packages above, your command would look like this: |
||||
|
|
||||
|
```sh |
||||
|
yarn ng update @angular/cli @angular/core ng-zorro-antd --force |
||||
|
``` |
||||
|
|
||||
|
> If Angular complains about uncommited changes in your repo, you can either commit/stash them or add `--allow-dirty` parameter to the command. |
||||
|
|
||||
|
|
||||
|
### Config Modules |
||||
|
|
||||
|
In ABP v2.x, every lazy loaded module had a config module available via a separate package and module configuration was as follows: |
||||
|
|
||||
|
```js |
||||
|
import { AccountConfigModule } from '@abp/ng.account.config'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [ |
||||
|
// other imports |
||||
|
AccountConfigModule.forRoot({ redirectUrl: '/' }), |
||||
|
], |
||||
|
// providers, declarations, and bootstrap |
||||
|
}) |
||||
|
export class AppModule {} |
||||
|
``` |
||||
|
|
||||
|
...and in app-routing.module.ts... |
||||
|
|
||||
|
```js |
||||
|
const routes: Routes = [ |
||||
|
// other route configuration |
||||
|
{ |
||||
|
path: 'account', |
||||
|
loadChildren: () => import( |
||||
|
'./lazy-libs/account-wrapper.module' |
||||
|
).then(m => m.AccountWrapperModule), |
||||
|
}, |
||||
|
]; |
||||
|
``` |
||||
|
|
||||
|
Although working, this had a few disadvantages: |
||||
|
|
||||
|
- Every module came in two independent packages, but in reality, those packages were interdependent. |
||||
|
- Configuring lazy loaded modules required a wrapper module. |
||||
|
- ABP Commercial had extensibility system and configuring extensible modules at the root module was increasing the bundle size. |
||||
|
|
||||
|
In ABP v3.0, we have introduced a secondary entry points for each config module as well as a new way to configure lazy loaded modules without the wrappers. Now, the module configuration looks like this: |
||||
|
|
||||
|
```js |
||||
|
import { AccountConfigModule } from '@abp/ng.account/config'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [ |
||||
|
// other imports |
||||
|
AccountConfigModule.forRoot(), |
||||
|
], |
||||
|
// providers, declarations, and bootstrap |
||||
|
}) |
||||
|
export class AppModule {} |
||||
|
``` |
||||
|
|
||||
|
...and in app-routing.module.ts... |
||||
|
|
||||
|
```js |
||||
|
const routes: Routes = [ |
||||
|
// other route configuration |
||||
|
{ |
||||
|
path: 'account', |
||||
|
loadChildren: () => import('@abp/ng.account') |
||||
|
.then(m => m.AccountModule.forLazy({ redirectUrl: '/' })), |
||||
|
}, |
||||
|
]; |
||||
|
``` |
||||
|
|
||||
|
This change helped us reduce bundle size and build times substantially. We believe you will notice the difference in your apps. |
||||
|
|
||||
|
#### A Better Example |
||||
|
|
||||
|
AppModule: |
||||
|
|
||||
|
```js |
||||
|
import { AccountConfigModule } from '@abp/ng.account/config'; |
||||
|
import { CoreModule } from '@abp/ng.core'; |
||||
|
import { IdentityConfigModule } from '@abp/ng.identity/config'; |
||||
|
import { SettingManagementConfigModule } from '@abp/ng.setting-management/config'; |
||||
|
import { TenantManagementConfigModule } from '@abp/ng.tenant-management/config'; |
||||
|
import { ThemeBasicModule } from '@abp/ng.theme.basic'; |
||||
|
import { ThemeSharedModule } from '@abp/ng.theme.shared'; |
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { BrowserModule } from '@angular/platform-browser'; |
||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; |
||||
|
import { NgxsModule } from '@ngxs/store'; |
||||
|
import { environment } from '../environments/environment'; |
||||
|
import { AppRoutingModule } from './app-routing.module'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [ |
||||
|
BrowserModule, |
||||
|
BrowserAnimationsModule, |
||||
|
AppRoutingModule, |
||||
|
CoreModule.forRoot({ |
||||
|
environment, |
||||
|
sendNullsAsQueryParam: false, |
||||
|
skipGetAppConfiguration: false, |
||||
|
}), |
||||
|
ThemeSharedModule.forRoot(), |
||||
|
AccountConfigModule.forRoot(), |
||||
|
IdentityConfigModule.forRoot(), |
||||
|
TenantManagementConfigModule.forRoot(), |
||||
|
SettingManagementConfigModule.forRoot(), |
||||
|
ThemeBasicModule.forRoot(), |
||||
|
NgxsModule.forRoot(), |
||||
|
], |
||||
|
// providers, declarations, and bootstrap |
||||
|
}) |
||||
|
export class AppModule {} |
||||
|
``` |
||||
|
|
||||
|
AppRoutingModule: |
||||
|
|
||||
|
```js |
||||
|
import { DynamicLayoutComponent } from '@abp/ng.core'; |
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { RouterModule, Routes } from '@angular/router'; |
||||
|
|
||||
|
const routes: Routes = [ |
||||
|
{ |
||||
|
path: '', |
||||
|
component: DynamicLayoutComponent, |
||||
|
children: [ |
||||
|
{ |
||||
|
path: '', |
||||
|
pathMatch: 'full', |
||||
|
loadChildren: () => import('./home/home.module') |
||||
|
.then(m => m.HomeModule), |
||||
|
}, |
||||
|
{ |
||||
|
path: 'account', |
||||
|
loadChildren: () => import('@abp/ng.account') |
||||
|
.then(m => m.AccountModule.forLazy({ redirectUrl: '/' })), |
||||
|
}, |
||||
|
{ |
||||
|
path: 'identity', |
||||
|
loadChildren: () => import('@abp/ng.identity') |
||||
|
.then(m => m.IdentityModule.forLazy()), |
||||
|
}, |
||||
|
{ |
||||
|
path: 'tenant-management', |
||||
|
loadChildren: () => import('@abp/ng.tenant-management') |
||||
|
.then(m => m.TenantManagementModule.forLazy()), |
||||
|
}, |
||||
|
{ |
||||
|
path: 'setting-management', |
||||
|
loadChildren: () => import('@abp/ng.setting-management') |
||||
|
.then(m => m.SettingManagementModule.forLazy()), |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [RouterModule.forRoot(routes)], |
||||
|
exports: [RouterModule], |
||||
|
}) |
||||
|
export class AppRoutingModule {} |
||||
|
``` |
||||
|
|
||||
|
> You may have noticed that we used `DynamicLayoutComponent` at top level route component. We made this change in order to avoid unnecessary renders and flickering. It is not mandatory, but we recommend doing the same in your app routing. |
||||
|
|
||||
|
#### What to Do When Migrating? |
||||
|
|
||||
|
- Remove config packages from your project using `yarn remove`. |
||||
|
- Import config modules from secondary entry points (e.g. `@abp/ng.identity/config`). |
||||
|
- Call static `forRoot` method of all new config modules, even if a configuration is not passed. |
||||
|
- Call static `forRoot` method of `ThemeBasicModule` (or `ThemeLeptonModule` if commercial) and remove `SharedModule` from imports (unless you have added anything that is necessary for your root module in it). |
||||
|
- Import lazy ABP modules directly in app routing module (e.g. `() => import('@abp/ng.identity').then(...)`). |
||||
|
- Call static `forLazy` method of all lazy modules inside `then`, even if a configuration is not passed. |
||||
|
- [OPTIONAL] Add an empty parent route with `DynamicLayoutComponent` for better performance and UX. |
||||
|
|
||||
|
|
||||
|
### RoutesService |
||||
|
|
||||
|
In ABP v2.x, adding a route to the menu was done by one of two ways: |
||||
|
|
||||
|
- [Via `routes` Property in `AppRoutingModule`](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-routes-property-in-approutingmodule) |
||||
|
- [Via ConfigState](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-configstate) |
||||
|
|
||||
|
As of v3.0, we have changed how routes are added and modified. We are no longer storing routes in `ConfigState` (breaking change). Instead, there is a new service called `RoutesService` which is used for adding, patching, or removing menu items. Please check [the documentation](./Modifying-the-Menu.md) for details. |
||||
|
|
||||
|
#### What to Do When Migrating? |
||||
|
|
||||
|
- Check if you have ever used `ConfigState` or `ConfigStateService` to add any routes. If so, replace them with `add` method of `RoutesService`. |
||||
|
- Check if you have ever patched a route. If so, convert them to `patch` method of `RoutesService`. |
||||
|
- Double-check that you are using absolute paths and providing a `parentName` instead of `children` property for sub-menu items in `add` or `patch` method calls. |
||||
|
|
||||
|
|
||||
|
### NavItemsService |
||||
|
|
||||
|
In ABP v2.x, adding a navigation element was done [via LayoutStateService](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#how-to-add-an-element-to-right-part-of-the-menu) |
||||
|
|
||||
|
As of v3.0, we have changed how navigation items are added and modified and previous method of doing so is no longer available (breaking change). Please check [the documentation](./Modifying-the-Menu.md) for details. |
||||
|
|
||||
|
#### What to Do When Migrating? |
||||
|
|
||||
|
- Replace all `dispatchAddNavigationElement` calls with `addItems` method of `NavItemsService`. |
||||
|
|
||||
|
|
||||
|
### ngx-datatable |
||||
|
|
||||
|
Until v3, we had been using a custom component, `abp-table`, as the default table. However, data grids are complicated components and implementing a fully-featured one requires considerable effort, which we are planning to put in to other features and issues. |
||||
|
|
||||
|
As of ABP v3, we have switched to a battle-tested, well-executed data grid: [ngx-datatable](https://github.com/swimlane/ngx-datatable). All ABP modules will come with ngx-datatable already implemented in them. `ThemeSharedModule` already exports `NgxDatatableModule`. So, if you install the package by running `yarn add @swimlane/ngx-datatable` in your terminal, it will be available for use in all modules of your app. |
||||
|
|
||||
|
For proper styling, you need to add the following in the styles section of your angular.json file: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"input": "node_modules/@swimlane/ngx-datatable/index.css", |
||||
|
"inject": true, |
||||
|
"bundleName": "ngx-datatable-index" |
||||
|
}, |
||||
|
{ |
||||
|
"input": "node_modules/@swimlane/ngx-datatable/assets/icons.css", |
||||
|
"inject": true, |
||||
|
"bundleName": "ngx-datatable-icons" |
||||
|
}, |
||||
|
{ |
||||
|
"input": "node_modules/@swimlane/ngx-datatable/themes/material.css", |
||||
|
"inject": true, |
||||
|
"bundleName": "ngx-datatable-material" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Since `abp-table` is not dropped yet, modules previously built by ABP v2.x will not suddenly lose all their tables. Yet, they will look and feel different from built-in ABP v3 modules. Therefore, you will probably want to convert the tables in those modules to ngx-datatable. In order to decrease the amount of work required to convert an abp-table into ngx-datatable, we have modified the [ListService](./List-Service.md) to work well with ngx-datatable and [introduced](https://volosoft.com/blog/attribute-directives-to-avoid-repetition-in-angular-templates) two new directives: `NgxDatatableListDirective` and `NgxDatatableDefaultDirective`. |
||||
|
|
||||
|
The usage of those directives is rather simple: |
||||
|
|
||||
|
```js |
||||
|
@Component({ |
||||
|
providers: [ListService], |
||||
|
}) |
||||
|
export class SomeComponent { |
||||
|
data$ = this.list.hookToQuery( |
||||
|
query => this.dataService.get(query) |
||||
|
); |
||||
|
|
||||
|
constructor( |
||||
|
public readonly list: ListService, |
||||
|
public readonly dataService: SomeDataService, |
||||
|
) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
...and in component template... |
||||
|
|
||||
|
```html |
||||
|
<ngx-datatable |
||||
|
[rows]="(data$ | async)?.items || []" |
||||
|
[count]="(data$ | async)?.totalCount || 0" |
||||
|
[list]="list" |
||||
|
default |
||||
|
> |
||||
|
<!-- column templates here --> |
||||
|
</ngx-datatable> |
||||
|
``` |
||||
|
|
||||
|
Once you bind the injected `ListService` instance through `NgxDatatableListDirective`, you no longer need to worry about pagination or sorting. Similarly, `NgxDatatableDefaultDirective` gets rid of several property bindings to make ngx-datatable fit our styles. |
||||
|
|
||||
|
#### A Better Example |
||||
|
|
||||
|
```html |
||||
|
<ngx-datatable |
||||
|
[rows]="items" |
||||
|
[count]="count" |
||||
|
[list]="list" |
||||
|
default |
||||
|
> |
||||
|
<!-- the grid actions column --> |
||||
|
<ngx-datatable-column |
||||
|
name="" |
||||
|
[maxWidth]="150" |
||||
|
[width]="150" |
||||
|
[sortable]="false" |
||||
|
> |
||||
|
<ng-template |
||||
|
ngx-datatable-cell-template |
||||
|
let-row="row" |
||||
|
let-i="rowIndex" |
||||
|
> |
||||
|
<abp-grid-actions |
||||
|
[index]="i" |
||||
|
[record]="row" |
||||
|
text="AbpUi::Actions" |
||||
|
></abp-grid-actions> |
||||
|
</ng-template> |
||||
|
</ngx-datatable-column> |
||||
|
|
||||
|
<!-- a basic column --> |
||||
|
<ngx-datatable-column |
||||
|
prop="someProp" |
||||
|
[name]="'::SomeProp' | abpLocalization" |
||||
|
[width]="200" |
||||
|
></ngx-datatable-column> |
||||
|
|
||||
|
<!-- a column with a custom template --> |
||||
|
<ngx-datatable-column |
||||
|
prop="someOtherProp" |
||||
|
[name]="'::SomeOtherProp' | abpLocalization" |
||||
|
[width]="250" |
||||
|
> |
||||
|
<ng-template |
||||
|
ngx-datatable-cell-template |
||||
|
let-row="row" |
||||
|
let-i="index" |
||||
|
> |
||||
|
<div abpEllipsis>{%{{{ row.someOtherProp }}}%}</div> |
||||
|
</ng-template> |
||||
|
</ngx-datatable-column> |
||||
|
</ngx-datatable> |
||||
|
``` |
||||
|
|
||||
|
#### What to Do When Migrating? |
||||
|
|
||||
|
- Install `@swimlane/ngx-datatable` package. |
||||
|
- Add ngx-datatable styles in the angular.json file. |
||||
|
- If you can, update your modules according to the example above. |
||||
|
- If you have to do that later and are planning to keep abp-table for a while, make sure you update your pagination according to the [breaking change described here](./List-Service.md). |
||||
|
|
||||
|
**Important Note:** The `abp-table` is not removed, but is deprecated and will be removed in the future. Please consider switching to ngx-datatable. |
||||
|
|
||||
|
|
||||
|
### Deprecated Interfaces |
||||
|
|
||||
|
Some interfaces have long been marked as deprecated and now they are removed. |
||||
|
|
||||
|
#### What to Do When Migrating? |
||||
|
|
||||
|
- Please check if you are still using [anything listed in this issue](https://github.com/abpframework/abp/issues/4281) |
||||
|
|
||||
|
|
||||
|
## What's Next? |
||||
|
|
||||
|
* [Service Proxies](./Service-Proxies.md) |
||||
|
|
||||
|
After Width: | Height: | Size: 152 KiB |
@ -0,0 +1,60 @@ |
|||||
|
# 切换到 EF Core Oracle Devart 提供程序 |
||||
|
|
||||
|
本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **Oracle** 数据库提供程序 |
||||
|
|
||||
|
> 本文档使用[Devart](https://www.devart.com/dotconnect/oracle/)公司的付费库,有关其他选项,请参见[文档](Entity-Framework-Core-Oracle.md). |
||||
|
|
||||
|
## 替换Volo.Abp.EntityFrameworkCore.SqlServer包 |
||||
|
|
||||
|
解决方案中的 `.EntityFrameworkCore` 项目依赖于 [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet包. 删除这个包并且添加相同版本的[Volo.Abp.EntityFrameworkCore.Oracle.Devart](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) 包. |
||||
|
|
||||
|
## 替换模块依赖项 |
||||
|
|
||||
|
在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreOracleDevartModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.Oracle.Devart;`). |
||||
|
|
||||
|
## UseOracle() |
||||
|
|
||||
|
查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseOracle()`. 检查下列文件: |
||||
|
|
||||
|
* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. |
||||
|
* `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. |
||||
|
|
||||
|
找到 *YourProjectName*MigrationsDbContextFactory.cs 的 `CreateDbContext()` 方法,将以下代码块 |
||||
|
|
||||
|
```csharp |
||||
|
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>() |
||||
|
.UseSqlServer(configuration.GetConnectionString("Default")); |
||||
|
``` |
||||
|
|
||||
|
替换为: |
||||
|
|
||||
|
```csharp |
||||
|
var builder = (DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>) |
||||
|
new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>().UseOracle |
||||
|
( |
||||
|
configuration.GetConnectionString("Default") |
||||
|
); |
||||
|
``` |
||||
|
|
||||
|
> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. |
||||
|
|
||||
|
## 更改连接字符串 |
||||
|
|
||||
|
Oracle连接字符串与SQL Server连接字符串不同. 所以检查你的解决方案中所有的 `appsettings.json` 文件,更改其中的连接字符串. 有关Oracle连接字符串选项的详细内容请参见[connectionstrings.com](https://www.connectionstrings.com/oracle/). |
||||
|
|
||||
|
通常需要更改 `.DbMigrator` 和 `.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构. |
||||
|
|
||||
|
## 重新生成迁移 |
||||
|
|
||||
|
启动模板使用[Entity Framework Core的Code First迁移](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). EF Core迁移取决于所选的DBMS提供程序. 因此更改DBMS提供程序会导致迁移失败. |
||||
|
|
||||
|
* 删除 `.EntityFrameworkCore.DbMigrations` 项目下的Migrations文件夹,并重新生成解决方案. |
||||
|
* 在包管理控制台中运行 `Add-Migration "Initial"`(在解决方案资源管理器选择 `.DbMigrator` (或 `.Web`) 做为启动项目并且选择 `.EntityFrameworkCore.DbMigrations` 做为默认项目). |
||||
|
|
||||
|
这将创建一个配置所有数据库对象(表)的数据库迁移. |
||||
|
|
||||
|
运行 `.DbMigrator` 项目创建数据库和初始种子数据. |
||||
|
|
||||
|
## 运行应用程序 |
||||
|
|
||||
|
它已准备就绪, 只需要运行该应用程序与享受编码. |
||||
@ -0,0 +1,67 @@ |
|||||
|
# 切换到EF Core Oracle提供程序 |
||||
|
|
||||
|
本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **Oracle** 数据库提供程序 |
||||
|
|
||||
|
> 本文档使用[Devart](https://www.devart.com/dotconnect/oracle/)公司的付费库,因为它是oracle唯一支持EF Core 3.x的库 |
||||
|
|
||||
|
## 替换Volo.Abp.EntityFrameworkCore.SqlServer包 |
||||
|
|
||||
|
解决方案中的 `.EntityFrameworkCore` 项目依赖于 [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet包. 删除这个包并且添加相同版本的 [Volo.Abp.EntityFrameworkCore.Oracle.Devart](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) 包. |
||||
|
|
||||
|
## 替换模块依赖项 |
||||
|
|
||||
|
在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreOracleDevartModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.Oracle.Devart;`). |
||||
|
|
||||
|
## UseOracle() |
||||
|
|
||||
|
Find `UseSqlServer()` calls in your solution, replace with `UseOracle()`. Check the following files: |
||||
|
|
||||
|
* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project. |
||||
|
* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project. |
||||
|
|
||||
|
In the `CreateDbContext()` method of the *YourProjectName*MigrationsDbContextFactory.cs, replace the following code block |
||||
|
|
||||
|
查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseOracle()`. 检查下列文件: |
||||
|
|
||||
|
* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. |
||||
|
* `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. |
||||
|
|
||||
|
使用以下代码替换*YourProjectName*MigrationsDbContextFactory.cs中的 `CreateDbContext()` 方法: |
||||
|
|
||||
|
```csharp |
||||
|
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>() |
||||
|
.UseSqlServer(configuration.GetConnectionString("Default")); |
||||
|
``` |
||||
|
|
||||
|
与这个 |
||||
|
|
||||
|
```csharp |
||||
|
var builder = (DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>) |
||||
|
new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>().UseOracle |
||||
|
( |
||||
|
configuration.GetConnectionString("Default") |
||||
|
); |
||||
|
``` |
||||
|
|
||||
|
> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. |
||||
|
|
||||
|
## 更改连接字符串 |
||||
|
|
||||
|
Oracle连接字符串与SQL Server连接字符串不同. 所以检查你的解决方案中所有的 `appsettings.json` 文件,更改其中的连接字符串. 有关oracle连接字符串选项的详细内容请参见[connectionstrings.com](https://www.connectionstrings.com/oracle/). |
||||
|
|
||||
|
通常需要更改 `.DbMigrator` 和 `.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构. |
||||
|
|
||||
|
## 重新生成迁移 |
||||
|
|
||||
|
启动模板使用[Entity Framework Core的Code First迁移](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). EF Core迁移取决于所选的DBMS提供程序. 因此更改DBMS提供程序会导致迁移失败. |
||||
|
|
||||
|
* 删除 `.EntityFrameworkCore.DbMigrations` 项目下的Migrations文件夹,并重新生成解决方案. |
||||
|
* 在包管理控制台中运行 `Add-Migration "Initial"`(在解决方案资源管理器选择 `.DbMigrator` (或 `.Web`) 做为启动项目并且选择 `.EntityFrameworkCore.DbMigrations` 做为默认项目). |
||||
|
|
||||
|
这将创建一个配置所有数据库对象(表)的数据库迁移. |
||||
|
|
||||
|
运行 `.DbMigrator` 项目创建数据库和初始种子数据. |
||||
|
|
||||
|
## 运行应用程序 |
||||
|
|
||||
|
它已准备就绪, 只需要运行该应用程序与享受编码. |
||||
@ -1,67 +1,10 @@ |
|||||
# 切换到EF Core Oracle提供程序 |
# 切换到EF Core Oracle 提供程序 |
||||
|
|
||||
本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **Oracle** 数据库提供程序 |
本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **Oracle** 数据库提供程序 |
||||
|
|
||||
> 本文档使用[Devart](https://www.devart.com/dotconnect/oracle/)公司的付费库,因为它是oracle唯一支持EF Core 3.x的库 |
ABP框架提供了两种不同的Oracle包集成. 你可以选择以下其中一个: |
||||
|
|
||||
## 替换Volo.Abp.EntityFrameworkCore.SqlServer包 |
* **[Volo.Abp.EntityFrameworkCore.Oracle](Entity-Framework-Core-Oracle-Official.md)** 使用官方 & 免费的oracle驱动 ( **当前处于 beta**). |
||||
|
* **[Volo.Abp.EntityFrameworkCore.Oracle.Devart](Entity-Framework-Core-Oracle-Devart.md)** 使用[Devart](https://www.devart.com/)公司提供的商业(付费)驱动. |
||||
|
|
||||
解决方案中的 `.EntityFrameworkCore` 项目依赖于 [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet包. 删除这个包并且添加相同版本的 [Volo.Abp.EntityFrameworkCore.Oracle.Devart](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) 包. |
> 你可以选择一个你想要的包,如果你不知道它们之间的区别,请在网站上进行搜索. ABP框架仅提供集成,不提供第三库类库的支持. |
||||
|
|
||||
## 替换模块依赖项 |
|
||||
|
|
||||
在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreOracleDevartModule)` (或者替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.Oracle.Devart;`). |
|
||||
|
|
||||
## UseOracle() |
|
||||
|
|
||||
Find `UseSqlServer()` calls in your solution, replace with `UseOracle()`. Check the following files: |
|
||||
|
|
||||
* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project. |
|
||||
* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project. |
|
||||
|
|
||||
In the `CreateDbContext()` method of the *YourProjectName*MigrationsDbContextFactory.cs, replace the following code block |
|
||||
|
|
||||
查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseOracle()`. 检查下列文件: |
|
||||
|
|
||||
* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. |
|
||||
* `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. |
|
||||
|
|
||||
使用以下代码替换*YourProjectName*MigrationsDbContextFactory.cs中的 `CreateDbContext()` 方法: |
|
||||
|
|
||||
```csharp |
|
||||
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>() |
|
||||
.UseSqlServer(configuration.GetConnectionString("Default")); |
|
||||
``` |
|
||||
|
|
||||
与这个 |
|
||||
|
|
||||
```csharp |
|
||||
var builder = (DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>) |
|
||||
new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>().UseOracle |
|
||||
( |
|
||||
configuration.GetConnectionString("Default") |
|
||||
); |
|
||||
``` |
|
||||
|
|
||||
> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. |
|
||||
|
|
||||
## 更改连接字符串 |
|
||||
|
|
||||
Oracle连接字符串与SQL Server连接字符串不同. 所以检查你的解决方案中所有的 `appsettings.json` 文件,更改其中的连接字符串. 有关oracle连接字符串选项的详细内容请参见[connectionstrings.com](https://www.connectionstrings.com/oracle/). |
|
||||
|
|
||||
通常需要更改 `.DbMigrator` 和 `.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构. |
|
||||
|
|
||||
## 重新生成迁移 |
|
||||
|
|
||||
启动模板使用[Entity Framework Core的Code First迁移](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). EF Core迁移取决于所选的DBMS提供程序. 因此更改DBMS提供程序会导致迁移失败. |
|
||||
|
|
||||
* 删除 `.EntityFrameworkCore.DbMigrations` 项目下的Migrations文件夹,并重新生成解决方案. |
|
||||
* 在包管理控制台中运行 `Add-Migration "Initial"`(在解决方案资源管理器选择 `.DbMigrator` (或 `.Web`) 做为启动项目并且选择 `.EntityFrameworkCore.DbMigrations` 做为默认项目). |
|
||||
|
|
||||
这将创建一个配置所有数据库对象(表)的数据库迁移. |
|
||||
|
|
||||
运行 `.DbMigrator` 项目创建数据库和初始种子数据. |
|
||||
|
|
||||
## 运行应用程序 |
|
||||
|
|
||||
它已准备就绪, 只需要运行该应用程序与享受编码. |
|
||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 56 KiB |
@ -0,0 +1,363 @@ |
|||||
|
# Angular UI v2.9 迁移到 v3.0 指南 |
||||
|
|
||||
|
## 在v3.0改变了什么? |
||||
|
|
||||
|
### Angular 10 |
||||
|
|
||||
|
新的ABP Angular UI基于Angular 10和TypeScript 3.9,我们已经放弃了对Angular 8的支持. 不过ABP模块将继续与Angular 9兼容使用. 因此如果你的项目是Angular 9,则无需更新为 Angular10. 更新通常很容易. |
||||
|
|
||||
|
#### 如何迁移? |
||||
|
|
||||
|
在你的根文件夹中打开一个终端,然后运行以下命令: |
||||
|
|
||||
|
```sh |
||||
|
yarn ng update @angular/cli @angular/core --force |
||||
|
``` |
||||
|
|
||||
|
这会做如下修改: |
||||
|
|
||||
|
- 更新你的package.json并安装新的软件包 |
||||
|
- 修改tsconfig.json文件创建一个"Solution Style"配置 |
||||
|
- 重命名 `browserlist` 为 `.browserlistrc` |
||||
|
|
||||
|
另一方面,如果你单独使用 `yarn ng update` 命令检查首先要更新哪些包会更好. Angular会给你一个要更新的包列表. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
当Angular报告上面的包后,运行命令: |
||||
|
|
||||
|
```sh |
||||
|
yarn ng update @angular/cli @angular/core ng-zorro-antd --force |
||||
|
``` |
||||
|
|
||||
|
> 如果Angular提示你的仓库有中未提交的更改,可以提交/存储它,也可以在命令中添加 `--allow-dirty` 参数. |
||||
|
|
||||
|
### 配置模块 |
||||
|
|
||||
|
在ABP v2.x中,每个延迟加载的模块都有一个可通过单独的程序包使用的配置模块,模块配置如下: |
||||
|
|
||||
|
```js |
||||
|
import { AccountConfigModule } from '@abp/ng.account.config'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [ |
||||
|
// other imports |
||||
|
AccountConfigModule.forRoot({ redirectUrl: '/' }), |
||||
|
], |
||||
|
// providers, declarations, and bootstrap |
||||
|
}) |
||||
|
export class AppModule {} |
||||
|
``` |
||||
|
|
||||
|
...在app-routing.module.ts... |
||||
|
|
||||
|
```js |
||||
|
const routes: Routes = [ |
||||
|
// other route configuration |
||||
|
{ |
||||
|
path: 'account', |
||||
|
loadChildren: () => import( |
||||
|
'./lazy-libs/account-wrapper.module' |
||||
|
).then(m => m.AccountWrapperModule), |
||||
|
}, |
||||
|
]; |
||||
|
``` |
||||
|
|
||||
|
虽然有效,但有一些缺点: |
||||
|
|
||||
|
- 每个模块都有两个独立的程序包,但实际上这些程序包是相互依赖的. |
||||
|
- 配置延迟加载的模块需要包装模块. |
||||
|
- ABP Commercial具有可扩展性系统,在根模块上配置可扩展模块会增加 bundle 的大小. |
||||
|
|
||||
|
在ABP v3.0中,我们为每个配置模块引入了辅助入口点,并且提供了一种在没有包装的情况下配置延迟加载的模块的新方法. 现在模块配置如下所示: |
||||
|
|
||||
|
```js |
||||
|
import { AccountConfigModule } from '@abp/ng.account/config'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [ |
||||
|
// other imports |
||||
|
AccountConfigModule.forRoot(), |
||||
|
], |
||||
|
// providers, declarations, and bootstrap |
||||
|
}) |
||||
|
export class AppModule {} |
||||
|
``` |
||||
|
|
||||
|
... 在app-routing.module.ts... |
||||
|
|
||||
|
```js |
||||
|
const routes: Routes = [ |
||||
|
// other route configuration |
||||
|
{ |
||||
|
path: 'account', |
||||
|
loadChildren: () => import('@abp/ng.account') |
||||
|
.then(m => m.AccountModule.forLazy({ redirectUrl: '/' })), |
||||
|
}, |
||||
|
]; |
||||
|
``` |
||||
|
|
||||
|
这项更改帮助我们减少了捆绑包的大小并大大缩短了构建时间. 我们相信你会注意到你的应用程序有所不同. |
||||
|
|
||||
|
#### 一个更好的例子 |
||||
|
|
||||
|
AppModule: |
||||
|
|
||||
|
```js |
||||
|
import { AccountConfigModule } from '@abp/ng.account/config'; |
||||
|
import { CoreModule } from '@abp/ng.core'; |
||||
|
import { IdentityConfigModule } from '@abp/ng.identity/config'; |
||||
|
import { SettingManagementConfigModule } from '@abp/ng.setting-management/config'; |
||||
|
import { TenantManagementConfigModule } from '@abp/ng.tenant-management/config'; |
||||
|
import { ThemeBasicModule } from '@abp/ng.theme.basic'; |
||||
|
import { ThemeSharedModule } from '@abp/ng.theme.shared'; |
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { BrowserModule } from '@angular/platform-browser'; |
||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; |
||||
|
import { NgxsModule } from '@ngxs/store'; |
||||
|
import { environment } from '../environments/environment'; |
||||
|
import { AppRoutingModule } from './app-routing.module'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [ |
||||
|
BrowserModule, |
||||
|
BrowserAnimationsModule, |
||||
|
AppRoutingModule, |
||||
|
CoreModule.forRoot({ |
||||
|
environment, |
||||
|
sendNullsAsQueryParam: false, |
||||
|
skipGetAppConfiguration: false, |
||||
|
}), |
||||
|
ThemeSharedModule.forRoot(), |
||||
|
AccountConfigModule.forRoot(), |
||||
|
IdentityConfigModule.forRoot(), |
||||
|
TenantManagementConfigModule.forRoot(), |
||||
|
SettingManagementConfigModule.forRoot(), |
||||
|
ThemeBasicModule.forRoot(), |
||||
|
NgxsModule.forRoot(), |
||||
|
], |
||||
|
// providers, declarations, and bootstrap |
||||
|
}) |
||||
|
export class AppModule {} |
||||
|
``` |
||||
|
|
||||
|
AppRoutingModule: |
||||
|
|
||||
|
```js |
||||
|
import { DynamicLayoutComponent } from '@abp/ng.core'; |
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { RouterModule, Routes } from '@angular/router'; |
||||
|
|
||||
|
const routes: Routes = [ |
||||
|
{ |
||||
|
path: '', |
||||
|
component: DynamicLayoutComponent, |
||||
|
children: [ |
||||
|
{ |
||||
|
path: '', |
||||
|
pathMatch: 'full', |
||||
|
loadChildren: () => import('./home/home.module') |
||||
|
.then(m => m.HomeModule), |
||||
|
}, |
||||
|
{ |
||||
|
path: 'account', |
||||
|
loadChildren: () => import('@abp/ng.account') |
||||
|
.then(m => m.AccountModule.forLazy({ redirectUrl: '/' })), |
||||
|
}, |
||||
|
{ |
||||
|
path: 'identity', |
||||
|
loadChildren: () => import('@abp/ng.identity') |
||||
|
.then(m => m.IdentityModule.forLazy()), |
||||
|
}, |
||||
|
{ |
||||
|
path: 'tenant-management', |
||||
|
loadChildren: () => import('@abp/ng.tenant-management') |
||||
|
.then(m => m.TenantManagementModule.forLazy()), |
||||
|
}, |
||||
|
{ |
||||
|
path: 'setting-management', |
||||
|
loadChildren: () => import('@abp/ng.setting-management') |
||||
|
.then(m => m.SettingManagementModule.forLazy()), |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [RouterModule.forRoot(routes)], |
||||
|
exports: [RouterModule], |
||||
|
}) |
||||
|
export class AppRoutingModule {} |
||||
|
``` |
||||
|
|
||||
|
> 你可能已经注意到我们在top级别路由组件上使用了 `DynamicLayoutComponent`. 我们这样做是为了避免不必要的渲染和闪烁. 这不是强制的,但是我们建议在你的应用程序路由中做同样的事情. |
||||
|
|
||||
|
#### 如何迁移? |
||||
|
|
||||
|
- 使用 `yarn remove` 删除你的项目的配置包. |
||||
|
- 从辅助入口点(例如`@abp/ng.identity/config`)导入配置模块. |
||||
|
- 调用所有新配置模块的静态 `forRoot`方法,即使配置没有被传递. |
||||
|
- 调用 `ThemeBasicModule` 的静态 `forRoot` 方法(或商业上的 `ThemeLeptonModule`),并从导入中删除 `SharedModule`(除非已在其中添加了根模块所需的任何内容). |
||||
|
- 在app路由模块中直接导入延迟ABP模块 (如 `() => import('@abp/ng.identity').then(...)`). |
||||
|
- 在所有延迟模块 `then` 中调用的静态 `forLazy` 方法,即使配置没有被传递. |
||||
|
- [可选]使用 `DynamicLayoutComponent` 添加空的父路由,获得更好的性能和UX. |
||||
|
|
||||
|
### RoutesService |
||||
|
|
||||
|
在ABP v2.x中,通过以下两种方式之一将路由添加到菜单: |
||||
|
|
||||
|
- [通过 `AppRoutingModule` 的 `routes` 属性](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-routes-property-in-approutingmodule) |
||||
|
- [通过 ConfigState](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-configstate) |
||||
|
|
||||
|
从v3.0开始,我们更改了添加和修改路由的方式. 我们不再将路由存储在 `ConfigState`中(破坏性更改). 而是有一个名为 `RoutesService` 的新服务,该服务用于添加,修补或删除菜单项. 详情请查看[文档](Modifying-the-Menu.md). |
||||
|
|
||||
|
#### 如何迁移? |
||||
|
|
||||
|
- 检查你是否曾经使用 `ConfigState` 或 `ConfigStateService` 添加路由. 请用 `RoutesService` 的 `add` 方法替换它们. |
||||
|
- 检查你是否曾经修补的路由. 将其替换为 `RoutesService` 的 `patch` 方法. |
||||
|
- 仔细检查你是否使用绝对路径,并在 `add` 或 `patch` 方法调用中为子菜单项提供 `parentName` 而不是 `children` 属性. |
||||
|
|
||||
|
### NavItemsService |
||||
|
|
||||
|
在ABP v2.x中,[通过LayoutStateService](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#how-to-add-an-element-to-right-part-of-the-menu)添加导航元素. |
||||
|
|
||||
|
从v3.0开始,我们改变了添加和修改导航项的方式,以前的方法不再可用(破坏性更改). 详情请查看[文档](Modifying-the-Menu.md). |
||||
|
|
||||
|
#### 如何迁移? |
||||
|
|
||||
|
- 用 `NavItemsService` 的 `addItems` 方法替换所有 `dispatchAddNavigationElement` 调用. |
||||
|
|
||||
|
### ngx-datatable |
||||
|
|
||||
|
在v3之前,我们一直使用自定义组件 `abp-table` 作为默认表. 但是数据表是复杂的组件,要实现功能齐全的数据表需要大量的精力,我们计划将其引入其他功能. |
||||
|
|
||||
|
从ABP v3开始,我们已切换到经过严格测试,执行良好的数据表格:[ngx-datatable](https://github.com/swimlane/ngx-datatable). 所有的ABP模块都已经实现了ngx-datatable. `ThemeSharedModule` 已经导出了 `NgxDatatableModule`. 因此如果你在终端运行 `yarn add @swimlane/ngx-datatable` 来安装这个包,它将在你的应用的所有模块中可用. |
||||
|
|
||||
|
为了正确设置样式,你需要在angular.json文件的样式部分中添加以下内容: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"input": "node_modules/@swimlane/ngx-datatable/index.css", |
||||
|
"inject": true, |
||||
|
"bundleName": "ngx-datatable-index" |
||||
|
}, |
||||
|
{ |
||||
|
"input": "node_modules/@swimlane/ngx-datatable/assets/icons.css", |
||||
|
"inject": true, |
||||
|
"bundleName": "ngx-datatable-icons" |
||||
|
}, |
||||
|
{ |
||||
|
"input": "node_modules/@swimlane/ngx-datatable/themes/material.css", |
||||
|
"inject": true, |
||||
|
"bundleName": "ngx-datatable-material" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
由于尚未删除 `abp-table`, 因此以前由ABP v2.x构建的模块不会突然丢失所有. 但是它们的外观与内置ABP v3模块有所不同, 因此你可能希望将这些模块中的表转换为ngx-datatable. 为了减少将abp-table转换为ngx-datatable所需的工作量,我们修改了 `ListService` 以使其与 `ngx-datatable` 一起很好地工作,并引入了两个新指令: `NgxDatatableListDirective` 和 `NgxDatatableDefaultDirective`. |
||||
|
|
||||
|
这些指令的用法很简单: |
||||
|
|
||||
|
```js |
||||
|
@Component({ |
||||
|
providers: [ListService], |
||||
|
}) |
||||
|
export class SomeComponent { |
||||
|
data$ = this.list.hookToQuery( |
||||
|
query => this.dataService.get(query) |
||||
|
); |
||||
|
|
||||
|
constructor( |
||||
|
public readonly list: ListService, |
||||
|
public readonly dataService: SomeDataService, |
||||
|
) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
...在组件模板... |
||||
|
|
||||
|
```html |
||||
|
<ngx-datatable |
||||
|
[rows]="(data$ | async)?.items || []" |
||||
|
[count]="(data$ | async)?.totalCount || 0" |
||||
|
[list]="list" |
||||
|
default |
||||
|
> |
||||
|
<!-- column templates here --> |
||||
|
</ngx-datatable> |
||||
|
``` |
||||
|
|
||||
|
通过 `NgxDatatableListDirective` 绑定注入的 `ListService` 实例后,你不再需要担心分页或排序. 同样 `NgxDatatableDefaultDirective` 去除了几个属性绑定,以使ngx-datatable适合我们的样式. |
||||
|
|
||||
|
#### 一个更好的例子 |
||||
|
|
||||
|
```html |
||||
|
<ngx-datatable |
||||
|
[rows]="items" |
||||
|
[count]="count" |
||||
|
[list]="list" |
||||
|
default |
||||
|
> |
||||
|
<!-- the grid actions column --> |
||||
|
<ngx-datatable-column |
||||
|
name="" |
||||
|
[maxWidth]="150" |
||||
|
[width]="150" |
||||
|
[sortable]="false" |
||||
|
> |
||||
|
<ng-template |
||||
|
ngx-datatable-cell-template |
||||
|
let-row="row" |
||||
|
let-i="rowIndex" |
||||
|
> |
||||
|
<abp-grid-actions |
||||
|
[index]="i" |
||||
|
[record]="row" |
||||
|
text="AbpUi::Actions" |
||||
|
></abp-grid-actions> |
||||
|
</ng-template> |
||||
|
</ngx-datatable-column> |
||||
|
|
||||
|
<!-- a basic column --> |
||||
|
<ngx-datatable-column |
||||
|
prop="someProp" |
||||
|
[name]="'::SomeProp' | abpLocalization" |
||||
|
[width]="200" |
||||
|
></ngx-datatable-column> |
||||
|
|
||||
|
<!-- a column with a custom template --> |
||||
|
<ngx-datatable-column |
||||
|
prop="someOtherProp" |
||||
|
[name]="'::SomeOtherProp' | abpLocalization" |
||||
|
[width]="250" |
||||
|
> |
||||
|
<ng-template |
||||
|
ngx-datatable-cell-template |
||||
|
let-row="row" |
||||
|
let-i="index" |
||||
|
> |
||||
|
<div abpEllipsis>{%{{{ row.someOtherProp }}}%}</div> |
||||
|
</ng-template> |
||||
|
</ngx-datatable-column> |
||||
|
</ngx-datatable> |
||||
|
``` |
||||
|
|
||||
|
#### 如何迁移? |
||||
|
|
||||
|
- 安装 `@swimlane/ngx-datatable` 包. |
||||
|
- 添加ngx-datatable样式到angular.json文件. |
||||
|
- 如果可以的话,根据上面的例子更新你的模. |
||||
|
- 如果你稍后需要这样做,并且打算保留abp-table一段时间,请确保根据此处描述的[破坏性更改](List-Service.md)更新分页. |
||||
|
|
||||
|
**重要说明:**abp-table没有被删除,但已被弃用并在以后的版本中删除. 请考虑切换到ngx-datatable。 |
||||
|
|
||||
|
### 过时的接口 |
||||
|
|
||||
|
某些接口早已被标记为已弃用,现在已将其删除. |
||||
|
|
||||
|
#### 如何迁移? |
||||
|
|
||||
|
请检查你是否仍在使用[Issue中列出的任何内容](https://github.com/abpframework/abp/issues/4281). |
||||
|
|
||||
|
## 下一步是什么? |
||||
|
|
||||
|
* [服务代理](Service-Proxies.md) |
||||
|
After Width: | Height: | Size: 152 KiB |
@ -1,3 +1,277 @@ |
|||||
## Dynamic Forms |
# 动态表单 |
||||
|
|
||||
目前还没有文档. 你现在可以看到[组件演示](http://bootstrap-taghelpers.abp.io/Components/DynamicForms). |
`提示:` 在开始阅读本文档之前,请确保你已经看过并理解了[abp表单元素](Form-elements.md)文档. |
||||
|
|
||||
|
## 介绍 |
||||
|
|
||||
|
`abp-dynamic-form` 为给定c#模型创建bootstrap表单. |
||||
|
|
||||
|
基本用法: |
||||
|
|
||||
|
````xml |
||||
|
<abp-dynamic-form abp-model="@Model.MyDetailedModel"/> |
||||
|
```` |
||||
|
|
||||
|
Model: |
||||
|
|
||||
|
````csharp |
||||
|
public class DynamicFormsModel : PageModel |
||||
|
{ |
||||
|
[BindProperty] |
||||
|
public DetailedModel MyDetailedModel { get; set; } |
||||
|
|
||||
|
public List<SelectListItem> CountryList { get; set; } = new List<SelectListItem> |
||||
|
{ |
||||
|
new SelectListItem { Value = "CA", Text = "Canada"}, |
||||
|
new SelectListItem { Value = "US", Text = "USA"}, |
||||
|
new SelectListItem { Value = "UK", Text = "United Kingdom"}, |
||||
|
new SelectListItem { Value = "RU", Text = "Russia"} |
||||
|
}; |
||||
|
|
||||
|
public void OnGet() |
||||
|
{ |
||||
|
MyDetailedModel = new DetailedModel |
||||
|
{ |
||||
|
Name = "", |
||||
|
Description = "Lorem ipsum dolor sit amet.", |
||||
|
IsActive = true, |
||||
|
Age = 65, |
||||
|
Day = DateTime.Now, |
||||
|
MyCarType = CarType.Coupe, |
||||
|
YourCarType = CarType.Sedan, |
||||
|
Country = "RU", |
||||
|
NeighborCountries = new List<string>() { "UK", "CA" } |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public class DetailedModel |
||||
|
{ |
||||
|
[Required] |
||||
|
[Placeholder("Enter your name...")] |
||||
|
[Display(Name = "Name")] |
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
[TextArea(Rows = 4)] |
||||
|
[Display(Name = "Description")] |
||||
|
[InputInfoText("Describe Yourself")] |
||||
|
public string Description { get; set; } |
||||
|
|
||||
|
[Required] |
||||
|
[DataType(DataType.Password)] |
||||
|
[Display(Name = "Password")] |
||||
|
public string Password { get; set; } |
||||
|
|
||||
|
[Display(Name = "Is Active")] |
||||
|
public bool IsActive { get; set; } |
||||
|
|
||||
|
[Required] |
||||
|
[Display(Name = "Age")] |
||||
|
public int Age { get; set; } |
||||
|
|
||||
|
[Required] |
||||
|
[Display(Name = "My Car Type")] |
||||
|
public CarType MyCarType { get; set; } |
||||
|
|
||||
|
[Required] |
||||
|
[AbpRadioButton(Inline = true)] |
||||
|
[Display(Name = "Your Car Type")] |
||||
|
public CarType YourCarType { get; set; } |
||||
|
|
||||
|
[DataType(DataType.Date)] |
||||
|
[Display(Name = "Day")] |
||||
|
public DateTime Day { get; set; } |
||||
|
|
||||
|
[SelectItems(nameof(CountryList))] |
||||
|
[Display(Name = "Country")] |
||||
|
public string Country { get; set; } |
||||
|
|
||||
|
[SelectItems(nameof(CountryList))] |
||||
|
[Display(Name = "Neighbor Countries")] |
||||
|
public List<string> NeighborCountries { get; set; } |
||||
|
} |
||||
|
|
||||
|
public enum CarType |
||||
|
{ |
||||
|
Sedan, |
||||
|
Hatchback, |
||||
|
StationWagon, |
||||
|
Coupe |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
## Demo |
||||
|
|
||||
|
参阅 [动态表单demo页面](https://bootstrap-taghelpers.abp.io/Components/Dropdowns)查看示例. |
||||
|
|
||||
|
## Attributes |
||||
|
|
||||
|
### abp-model |
||||
|
|
||||
|
为动态表单设置c#模型,模型的属性以表单形式转化为输入. |
||||
|
|
||||
|
### submit-button |
||||
|
|
||||
|
可以为 `True` 或 `False`. |
||||
|
|
||||
|
如果为 `True`,则会在表单底部生成一个提交按钮. |
||||
|
|
||||
|
默认值是 `False`. |
||||
|
|
||||
|
### required-symbols |
||||
|
|
||||
|
可以为 `True` 或 `False`. |
||||
|
|
||||
|
如果为 `True`,则必需的输入将带有一个符号(*),表示它们是必需的. |
||||
|
|
||||
|
默认值是 `True`. |
||||
|
|
||||
|
## 表单内容布局 |
||||
|
|
||||
|
默认情况下,“`abp-dynamic-form` 会清除内部html并将inputs放入自身. 如果要向动态表单添加其他内容或将inputs放置到某些特定区域,可以使用`<abp-form-content />`标签. 这个标签将被表单内容替换, 而 `abp-dynamic-form` 标签的内部html的其余部分将保持不变. |
||||
|
|
||||
|
用法: |
||||
|
|
||||
|
````xml |
||||
|
<abp-dynamic-form abp-model="@Model.MyExampleModel"> |
||||
|
<div> |
||||
|
Some content.... |
||||
|
</div> |
||||
|
<div class="input-area"> |
||||
|
<abp-form-content /> |
||||
|
</div> |
||||
|
<div> |
||||
|
Some more content.... |
||||
|
</div> |
||||
|
</abp-dynamic-form> |
||||
|
```` |
||||
|
|
||||
|
## 输入排序 |
||||
|
|
||||
|
`abp-dynamic-form` 通过 `DisplayOrder` attribute对属性进行排序,然后按模型类中的属性顺序进行排序. |
||||
|
|
||||
|
默认每个属性的 `DisplayOrder` attribute值是10000. |
||||
|
|
||||
|
参见以下示例: |
||||
|
|
||||
|
````csharp |
||||
|
public class OrderExampleModel |
||||
|
{ |
||||
|
[DisplayOrder(10004)] |
||||
|
public string Name{ get; set; } |
||||
|
|
||||
|
[DisplayOrder(10005)] |
||||
|
public string Surname{ get; set; } |
||||
|
|
||||
|
//Default 10000 |
||||
|
public string EmailAddress { get; set; } |
||||
|
|
||||
|
[DisplayOrder(10003)] |
||||
|
public string PhoneNumber { get; set; } |
||||
|
|
||||
|
[DisplayOrder(9999)] |
||||
|
public string City { get; set; } |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
在这个示例中,inputs字段顺序为: `City` > `EmailAddress` > `PhoneNumber` > `Name` > `Surname`. |
||||
|
|
||||
|
## 忽略属性 |
||||
|
|
||||
|
默认情况下, `abp-dynamic-form` 会为模型类中的每个属性生成输入. 如果要忽略属性请使用 `DynamicFormIgnore` attribute. |
||||
|
|
||||
|
参见以下示例: |
||||
|
|
||||
|
````csharp |
||||
|
public class MyModel |
||||
|
{ |
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
[DynamicFormIgnore] |
||||
|
public string Surname { get; set; } |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
在这个示例中,不会为 `Surname` 属性生成输入. |
||||
|
|
||||
|
## 指示文本框,单选按钮组和组合框 |
||||
|
|
||||
|
如果你已经阅读了[表单元素文档](Form-elements.md),你会注意到在c#模型上 `abp-radio` 和 `abp-select` 标签非常相. 我们必须使用 `[AbpRadioButton()]` attribute来告诉 `abp-dynamic-form` 你的哪些属性是单选按钮组,哪些属性是组合框. |
||||
|
|
||||
|
参见以下示例: |
||||
|
|
||||
|
````xml |
||||
|
<abp-dynamic-form abp-model="@Model.MyDetailedModel"/> |
||||
|
```` |
||||
|
|
||||
|
Model: |
||||
|
|
||||
|
````csharp |
||||
|
public class DynamicFormsModel : PageModel |
||||
|
{ |
||||
|
[BindProperty] |
||||
|
public DetailedModel MyDetailedModel { get; set; } |
||||
|
|
||||
|
public List<SelectListItem> CountryList { get; set; } = new List<SelectListItem> |
||||
|
{ |
||||
|
new SelectListItem { Value = "CA", Text = "Canada"}, |
||||
|
new SelectListItem { Value = "US", Text = "USA"}, |
||||
|
new SelectListItem { Value = "UK", Text = "United Kingdom"}, |
||||
|
new SelectListItem { Value = "RU", Text = "Russia"} |
||||
|
}; |
||||
|
|
||||
|
public void OnGet() |
||||
|
{ |
||||
|
MyDetailedModel = new DetailedModel |
||||
|
{ |
||||
|
ComboCarType = CarType.Coupe, |
||||
|
RadioCarType = CarType.Sedan, |
||||
|
ComboCountry = "RU", |
||||
|
RadioCountry = "UK" |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public class DetailedModel |
||||
|
{ |
||||
|
public CarType ComboCarType { get; set; } |
||||
|
|
||||
|
[AbpRadioButton(Inline = true)] |
||||
|
public CarType RadioCarType { get; set; } |
||||
|
|
||||
|
[SelectItems(nameof(CountryList))] |
||||
|
public string ComboCountry { get; set; } |
||||
|
|
||||
|
[AbpRadioButton()] |
||||
|
[SelectItems(nameof(CountryList))] |
||||
|
public string RadioCountry { get; set; } |
||||
|
} |
||||
|
|
||||
|
public enum CarType |
||||
|
{ |
||||
|
Sedan, |
||||
|
Hatchback, |
||||
|
StationWagon, |
||||
|
Coupe |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
正如你上面的例子中看到: |
||||
|
|
||||
|
* 如果在**Enum**属性上使用 `[AbpRadioButton()]`,它将是一个单选按钮组. 否则它是组合框. |
||||
|
* 如果在属性上使用 `[SelectItems()]` 和 `[AbpRadioButton()]`,那么它将是一个单选按钮组. |
||||
|
* 如果只在属性上使用 `[SelectItems()]`,它将是一个组合框. |
||||
|
* 如果一个属性没有使用这些属性,它将是一个文本框. |
||||
|
|
||||
|
## 本地化 |
||||
|
|
||||
|
`abp-dynamic-form` 会处理本地化. |
||||
|
|
||||
|
默认情况下, 它将尝试查找 "DisplayName:{PropertyName}" 或 "{PropertyName}" 定位本地化键,并将定位值设置为label. |
||||
|
|
||||
|
你可以使用Asp.Net Core的 `[Display()]` attribute自行设置. 可以在此属性中使用本地化密钥. 请参阅以下示例: |
||||
|
|
||||
|
````csharp |
||||
|
[Display(Name = "Name")] |
||||
|
public string Name { get; set; } |
||||
|
```` |
||||
@ -1,27 +0,0 @@ |
|||||
{ |
|
||||
"iisSettings": { |
|
||||
"windowsAuthentication": false, |
|
||||
"anonymousAuthentication": true, |
|
||||
"iisExpress": { |
|
||||
"applicationUrl": "http://localhost:53450/", |
|
||||
"sslPort": 0 |
|
||||
} |
|
||||
}, |
|
||||
"profiles": { |
|
||||
"IIS Express": { |
|
||||
"commandName": "IISExpress", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
} |
|
||||
}, |
|
||||
"Volo.Abp.AspNetCore.Authentication.OAuth": { |
|
||||
"commandName": "Project", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
}, |
|
||||
"applicationUrl": "http://localhost:53451/" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,27 +0,0 @@ |
|||||
{ |
|
||||
"iisSettings": { |
|
||||
"windowsAuthentication": false, |
|
||||
"anonymousAuthentication": true, |
|
||||
"iisExpress": { |
|
||||
"applicationUrl": "http://localhost:53760/", |
|
||||
"sslPort": 0 |
|
||||
} |
|
||||
}, |
|
||||
"profiles": { |
|
||||
"IIS Express": { |
|
||||
"commandName": "IISExpress", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
} |
|
||||
}, |
|
||||
"Volo.Abp.AspNetCore.MultiTenancy": { |
|
||||
"commandName": "Project", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
}, |
|
||||
"applicationUrl": "http://localhost:53763/" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,27 +0,0 @@ |
|||||
{ |
|
||||
"iisSettings": { |
|
||||
"windowsAuthentication": false, |
|
||||
"anonymousAuthentication": true, |
|
||||
"iisExpress": { |
|
||||
"applicationUrl": "http://localhost:52302/", |
|
||||
"sslPort": 0 |
|
||||
} |
|
||||
}, |
|
||||
"profiles": { |
|
||||
"IIS Express": { |
|
||||
"commandName": "IISExpress", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
} |
|
||||
}, |
|
||||
"Volo.Abp.AspNetCore.Mvc.Client": { |
|
||||
"commandName": "Project", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
}, |
|
||||
"applicationUrl": "http://localhost:52303/" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,27 +0,0 @@ |
|||||
{ |
|
||||
"iisSettings": { |
|
||||
"windowsAuthentication": false, |
|
||||
"anonymousAuthentication": true, |
|
||||
"iisExpress": { |
|
||||
"applicationUrl": "http://localhost:61183/", |
|
||||
"sslPort": 0 |
|
||||
} |
|
||||
}, |
|
||||
"profiles": { |
|
||||
"IIS Express": { |
|
||||
"commandName": "IISExpress", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
} |
|
||||
}, |
|
||||
"Volo.Abp.AspNetCore.Mvc.UI.Bootstrap": { |
|
||||
"commandName": "Project", |
|
||||
"launchBrowser": true, |
|
||||
"launchUrl": "http://localhost:5000", |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,27 +0,0 @@ |
|||||
{ |
|
||||
"iisSettings": { |
|
||||
"windowsAuthentication": false, |
|
||||
"anonymousAuthentication": true, |
|
||||
"iisExpress": { |
|
||||
"applicationUrl": "http://localhost:53762/", |
|
||||
"sslPort": 0 |
|
||||
} |
|
||||
}, |
|
||||
"profiles": { |
|
||||
"IIS Express": { |
|
||||
"commandName": "IISExpress", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
} |
|
||||
}, |
|
||||
"Volo.Abp.AspNetCore.Mvc.UI.Bundling": { |
|
||||
"commandName": "Project", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
}, |
|
||||
"applicationUrl": "http://localhost:53765/" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,27 +0,0 @@ |
|||||
{ |
|
||||
"iisSettings": { |
|
||||
"windowsAuthentication": false, |
|
||||
"anonymousAuthentication": true, |
|
||||
"iisExpress": { |
|
||||
"applicationUrl": "http://localhost:56918/", |
|
||||
"sslPort": 0 |
|
||||
} |
|
||||
}, |
|
||||
"profiles": { |
|
||||
"IIS Express": { |
|
||||
"commandName": "IISExpress", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
} |
|
||||
}, |
|
||||
"Volo.Abp.AspNetCore.Mvc.UI.TenantSwitch": { |
|
||||
"commandName": "Project", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
}, |
|
||||
"applicationUrl": "http://localhost:56919/" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,27 +0,0 @@ |
|||||
{ |
|
||||
"iisSettings": { |
|
||||
"windowsAuthentication": false, |
|
||||
"anonymousAuthentication": true, |
|
||||
"iisExpress": { |
|
||||
"applicationUrl": "http://localhost:53766/", |
|
||||
"sslPort": 0 |
|
||||
} |
|
||||
}, |
|
||||
"profiles": { |
|
||||
"IIS Express": { |
|
||||
"commandName": "IISExpress", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
} |
|
||||
}, |
|
||||
"Volo.Abp.AspNetCore.Mvc.UI.Packages": { |
|
||||
"commandName": "Project", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
}, |
|
||||
"applicationUrl": "http://localhost:53768/" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,13 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Uppy |
||||
|
{ |
||||
|
public class UppyScriptContributor : BundleContributor |
||||
|
{ |
||||
|
public override void ConfigureBundle(BundleConfigurationContext context) |
||||
|
{ |
||||
|
context.Files.AddIfNotContains("/libs/uppy/uppy.min.js"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Uppy |
||||
|
{ |
||||
|
public class UppyStyleContributor : BundleContributor |
||||
|
{ |
||||
|
public override void ConfigureBundle(BundleConfigurationContext context) |
||||
|
{ |
||||
|
context.Files.AddIfNotContains("/libs/uppy/uppy.min.css"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,27 +0,0 @@ |
|||||
{ |
|
||||
"iisSettings": { |
|
||||
"windowsAuthentication": false, |
|
||||
"anonymousAuthentication": true, |
|
||||
"iisExpress": { |
|
||||
"applicationUrl": "http://localhost:50398/", |
|
||||
"sslPort": 0 |
|
||||
} |
|
||||
}, |
|
||||
"profiles": { |
|
||||
"IIS Express": { |
|
||||
"commandName": "IISExpress", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
} |
|
||||
}, |
|
||||
"Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic": { |
|
||||
"commandName": "Project", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
}, |
|
||||
"applicationUrl": "http://localhost:50399/" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,13 +1,17 @@ |
|||||
@using Localization.Resources.AbpUi |
@using Localization.Resources.AbpUi |
||||
@using Microsoft.Extensions.Localization |
@using Microsoft.Extensions.Localization |
||||
|
|
||||
@inject IStringLocalizer<AbpUiResource> L |
@inject IStringLocalizer<AbpUiResource> L |
||||
|
|
||||
<form method="post" class="page-search-form"> |
<form method="post" class="page-search-form"> |
||||
<div class="input-group"> |
<div class="input-group"> |
||||
<input class="form-control page-search-filter-text" placeholder="@L["Search"].Value"> |
<input class="form-control page-search-filter-text" placeholder="@L["Search"].Value" /> |
||||
<div class="input-group-append"> |
<div class="input-group-append"> |
||||
<abp-button button-type="Primary" type="submit"> |
<abp-button button-type="Primary" type="submit"> |
||||
<i class="fa fa-search" aria-hidden="true" style="margin-left: 10%; margin-top:50%;" ></i> |
<i class="fa fa-search" aria-hidden="true"></i> |
||||
</abp-button> |
</abp-button> |
||||
</div> |
</div> |
||||
</div> |
</div> |
||||
</form> |
</form> |
||||
|
|
||||
|
|
||||
|
|||||
@ -0,0 +1,4 @@ |
|||||
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
||||
|
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI |
||||
|
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap |
||||
|
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling |
||||
@ -1,27 +0,0 @@ |
|||||
{ |
|
||||
"iisSettings": { |
|
||||
"windowsAuthentication": false, |
|
||||
"anonymousAuthentication": true, |
|
||||
"iisExpress": { |
|
||||
"applicationUrl": "http://localhost:51544/", |
|
||||
"sslPort": 0 |
|
||||
} |
|
||||
}, |
|
||||
"profiles": { |
|
||||
"IIS Express": { |
|
||||
"commandName": "IISExpress", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
} |
|
||||
}, |
|
||||
"Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared": { |
|
||||
"commandName": "Project", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
}, |
|
||||
"applicationUrl": "http://localhost:51545/" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,27 +0,0 @@ |
|||||
{ |
|
||||
"iisSettings": { |
|
||||
"windowsAuthentication": false, |
|
||||
"anonymousAuthentication": true, |
|
||||
"iisExpress": { |
|
||||
"applicationUrl": "http://localhost:53761/", |
|
||||
"sslPort": 0 |
|
||||
} |
|
||||
}, |
|
||||
"profiles": { |
|
||||
"IIS Express": { |
|
||||
"commandName": "IISExpress", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
} |
|
||||
}, |
|
||||
"Volo.Abp.AspNetCore.Mvc.UI.Widgets": { |
|
||||
"commandName": "Project", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
}, |
|
||||
"applicationUrl": "http://localhost:53764/" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,27 +0,0 @@ |
|||||
{ |
|
||||
"iisSettings": { |
|
||||
"windowsAuthentication": false, |
|
||||
"anonymousAuthentication": true, |
|
||||
"iisExpress": { |
|
||||
"applicationUrl": "http://localhost:55913/", |
|
||||
"sslPort": 0 |
|
||||
} |
|
||||
}, |
|
||||
"profiles": { |
|
||||
"IIS Express": { |
|
||||
"commandName": "IISExpress", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
} |
|
||||
}, |
|
||||
"Volo.Abp.AspNetCore.Mvc.UI": { |
|
||||
"commandName": "Project", |
|
||||
"launchBrowser": true, |
|
||||
"launchUrl": "http://localhost:5000", |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,27 +0,0 @@ |
|||||
{ |
|
||||
"iisSettings": { |
|
||||
"windowsAuthentication": false, |
|
||||
"anonymousAuthentication": true, |
|
||||
"iisExpress": { |
|
||||
"applicationUrl": "http://localhost:53767/", |
|
||||
"sslPort": 0 |
|
||||
} |
|
||||
}, |
|
||||
"profiles": { |
|
||||
"IIS Express": { |
|
||||
"commandName": "IISExpress", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
} |
|
||||
}, |
|
||||
"Volo.Abp.AspNetCore.Mvc": { |
|
||||
"commandName": "Project", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
}, |
|
||||
"applicationUrl": "http://localhost:53769/" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,34 @@ |
|||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
using Microsoft.AspNetCore.Mvc.DataAnnotations; |
||||
|
using Microsoft.Extensions.Localization; |
||||
|
using Volo.Abp.Validation; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.DataAnnotations |
||||
|
{ |
||||
|
public class AbpValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider |
||||
|
{ |
||||
|
private readonly ValidationAttributeAdapterProvider _defaultAdapter; |
||||
|
|
||||
|
public AbpValidationAttributeAdapterProvider(ValidationAttributeAdapterProvider defaultAdapter) |
||||
|
{ |
||||
|
_defaultAdapter = defaultAdapter; |
||||
|
} |
||||
|
|
||||
|
public virtual IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer) |
||||
|
{ |
||||
|
var type = attribute.GetType(); |
||||
|
|
||||
|
if (type == typeof(DynamicStringLengthAttribute)) |
||||
|
{ |
||||
|
return new DynamicStringLengthAttributeAdapter((DynamicStringLengthAttribute) attribute, stringLocalizer); |
||||
|
} |
||||
|
|
||||
|
if (type == typeof(DynamicMaxLengthAttribute)) |
||||
|
{ |
||||
|
return new DynamicMaxLengthAttributeAdapter((DynamicMaxLengthAttribute) attribute, stringLocalizer); |
||||
|
} |
||||
|
|
||||
|
return _defaultAdapter.GetAttributeAdapter(attribute, stringLocalizer); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
using System.Globalization; |
||||
|
using Microsoft.AspNetCore.Mvc.DataAnnotations; |
||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; |
||||
|
using Microsoft.Extensions.Localization; |
||||
|
using Volo.Abp.Validation; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.DataAnnotations |
||||
|
{ |
||||
|
public class DynamicMaxLengthAttributeAdapter : AttributeAdapterBase<DynamicMaxLengthAttribute> |
||||
|
{ |
||||
|
private readonly string _max; |
||||
|
|
||||
|
public DynamicMaxLengthAttributeAdapter( |
||||
|
DynamicMaxLengthAttribute attribute, |
||||
|
IStringLocalizer stringLocalizer) |
||||
|
: base(attribute, stringLocalizer) |
||||
|
{ |
||||
|
_max = Attribute.Length.ToString(CultureInfo.InvariantCulture); |
||||
|
} |
||||
|
|
||||
|
public override string GetErrorMessage(ModelValidationContextBase validationContext) |
||||
|
{ |
||||
|
Check.NotNull(validationContext, nameof(validationContext)); |
||||
|
|
||||
|
return GetErrorMessage( |
||||
|
validationContext.ModelMetadata, |
||||
|
validationContext.ModelMetadata.GetDisplayName(), |
||||
|
Attribute.Length |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public override void AddValidation(ClientModelValidationContext context) |
||||
|
{ |
||||
|
Check.NotNull(context, nameof(context)); |
||||
|
|
||||
|
MergeAttribute(context.Attributes, "data-val", "true"); |
||||
|
MergeAttribute(context.Attributes, "data-val-maxlength", GetErrorMessage(context)); |
||||
|
MergeAttribute(context.Attributes, "data-val-maxlength-max", _max); |
||||
|
|
||||
|
if (Attribute.Length != int.MaxValue) |
||||
|
{ |
||||
|
MergeAttribute(context.Attributes, "data-val-maxlength-max", _max); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
using Microsoft.AspNetCore.Mvc.DataAnnotations; |
||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; |
||||
|
using Microsoft.Extensions.Localization; |
||||
|
using Volo.Abp.Validation; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.DataAnnotations |
||||
|
{ |
||||
|
public class DynamicStringLengthAttributeAdapter : AttributeAdapterBase<DynamicStringLengthAttribute> |
||||
|
{ |
||||
|
private readonly string _max; |
||||
|
private readonly string _min; |
||||
|
|
||||
|
public DynamicStringLengthAttributeAdapter( |
||||
|
DynamicStringLengthAttribute attribute, |
||||
|
IStringLocalizer stringLocalizer) |
||||
|
: base(attribute, stringLocalizer) |
||||
|
{ |
||||
|
_max = Attribute.MaximumLength.ToString(CultureInfo.InvariantCulture); |
||||
|
_min = Attribute.MinimumLength.ToString(CultureInfo.InvariantCulture); |
||||
|
} |
||||
|
|
||||
|
public override void AddValidation(ClientModelValidationContext context) |
||||
|
{ |
||||
|
Check.NotNull(context, nameof(context)); |
||||
|
|
||||
|
MergeAttribute(context.Attributes, "data-val", "true"); |
||||
|
MergeAttribute(context.Attributes, "data-val-length", GetErrorMessage(context)); |
||||
|
|
||||
|
if (Attribute.MaximumLength != int.MaxValue) |
||||
|
{ |
||||
|
MergeAttribute(context.Attributes, "data-val-length-max", _max); |
||||
|
} |
||||
|
|
||||
|
if (Attribute.MinimumLength != 0) |
||||
|
{ |
||||
|
MergeAttribute(context.Attributes, "data-val-length-min", _min); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override string GetErrorMessage(ModelValidationContextBase validationContext) |
||||
|
{ |
||||
|
Check.NotNull(validationContext, nameof(validationContext)); |
||||
|
|
||||
|
return GetErrorMessage( |
||||
|
validationContext.ModelMetadata, |
||||
|
validationContext.ModelMetadata.GetDisplayName(), |
||||
|
Attribute.MaximumLength, |
||||
|
Attribute.MinimumLength |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,27 +0,0 @@ |
|||||
{ |
|
||||
"iisSettings": { |
|
||||
"windowsAuthentication": false, |
|
||||
"anonymousAuthentication": true, |
|
||||
"iisExpress": { |
|
||||
"applicationUrl": "http://localhost:61851/", |
|
||||
"sslPort": 44301 |
|
||||
} |
|
||||
}, |
|
||||
"profiles": { |
|
||||
"IIS Express": { |
|
||||
"commandName": "IISExpress", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
} |
|
||||
}, |
|
||||
"Volo.Abp.AspNetCore.Serilog": { |
|
||||
"commandName": "Project", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
}, |
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,27 +0,0 @@ |
|||||
{ |
|
||||
"iisSettings": { |
|
||||
"windowsAuthentication": false, |
|
||||
"anonymousAuthentication": true, |
|
||||
"iisExpress": { |
|
||||
"applicationUrl": "http://localhost:53900/", |
|
||||
"sslPort": 44362 |
|
||||
} |
|
||||
}, |
|
||||
"profiles": { |
|
||||
"IIS Express": { |
|
||||
"commandName": "IISExpress", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
} |
|
||||
}, |
|
||||
"Volo.Abp.SignalR": { |
|
||||
"commandName": "Project", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
}, |
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,27 +0,0 @@ |
|||||
{ |
|
||||
"iisSettings": { |
|
||||
"windowsAuthentication": false, |
|
||||
"anonymousAuthentication": true, |
|
||||
"iisExpress": { |
|
||||
"applicationUrl": "http://localhost:53783/", |
|
||||
"sslPort": 0 |
|
||||
} |
|
||||
}, |
|
||||
"profiles": { |
|
||||
"IIS Express": { |
|
||||
"commandName": "IISExpress", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
} |
|
||||
}, |
|
||||
"Volo.Abp.AspNetCore.TestBase": { |
|
||||
"commandName": "Project", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
}, |
|
||||
"applicationUrl": "http://localhost:53784/" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,27 +0,0 @@ |
|||||
{ |
|
||||
"iisSettings": { |
|
||||
"windowsAuthentication": false, |
|
||||
"anonymousAuthentication": true, |
|
||||
"iisExpress": { |
|
||||
"applicationUrl": "http://localhost:53374/", |
|
||||
"sslPort": 0 |
|
||||
} |
|
||||
}, |
|
||||
"profiles": { |
|
||||
"IIS Express": { |
|
||||
"commandName": "IISExpress", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
} |
|
||||
}, |
|
||||
"Volo.Abp.AspNetCore": { |
|
||||
"commandName": "Project", |
|
||||
"launchBrowser": true, |
|
||||
"environmentVariables": { |
|
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
|
||||
}, |
|
||||
"applicationUrl": "http://localhost:53375/" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,7 @@ |
|||||
|
namespace Volo.Abp.BackgroundWorkers.Quartz |
||||
|
{ |
||||
|
public interface IQuartzBackgroundWorkerAdapter : IQuartzBackgroundWorker |
||||
|
{ |
||||
|
void BuildWorker(IBackgroundWorker worker); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,88 @@ |
|||||
|
using System; |
||||
|
using System.Reflection; |
||||
|
using System.Threading.Tasks; |
||||
|
using Quartz; |
||||
|
using Volo.Abp.Threading; |
||||
|
|
||||
|
namespace Volo.Abp.BackgroundWorkers.Quartz |
||||
|
{ |
||||
|
public class QuartzPeriodicBackgroundWorkerAdapter<TWorker> : QuartzBackgroundWorkerBase, |
||||
|
IQuartzBackgroundWorkerAdapter |
||||
|
where TWorker : IBackgroundWorker |
||||
|
{ |
||||
|
private readonly MethodInfo _doWorkAsyncMethod; |
||||
|
private readonly MethodInfo _doWorkMethod; |
||||
|
|
||||
|
public QuartzPeriodicBackgroundWorkerAdapter() |
||||
|
{ |
||||
|
AutoRegister = false; |
||||
|
|
||||
|
_doWorkAsyncMethod = typeof(TWorker).GetMethod("DoWorkAsync", BindingFlags.Instance | BindingFlags.NonPublic); |
||||
|
_doWorkMethod = typeof(TWorker).GetMethod("DoWork", BindingFlags.Instance | BindingFlags.NonPublic); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public void BuildWorker(IBackgroundWorker worker) |
||||
|
{ |
||||
|
int? period; |
||||
|
var workerType = worker.GetType(); |
||||
|
|
||||
|
if (worker is AsyncPeriodicBackgroundWorkerBase || worker is PeriodicBackgroundWorkerBase) |
||||
|
{ |
||||
|
if (typeof(TWorker) != worker.GetType()) |
||||
|
{ |
||||
|
throw new ArgumentException($"{nameof(worker)} type is different from the generic type"); |
||||
|
} |
||||
|
|
||||
|
var timer = (AbpTimer) worker.GetType().GetProperty("Timer", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(worker); |
||||
|
period = timer?.Period; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (period == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
JobDetail = JobBuilder |
||||
|
.Create<QuartzPeriodicBackgroundWorkerAdapter<TWorker>>() |
||||
|
.WithIdentity(workerType.FullName) |
||||
|
.Build(); |
||||
|
Trigger = TriggerBuilder.Create() |
||||
|
.WithIdentity(workerType.FullName) |
||||
|
.WithSimpleSchedule(builder => builder.WithInterval(TimeSpan.FromMilliseconds(period.Value)).RepeatForever()) |
||||
|
.Build(); |
||||
|
} |
||||
|
|
||||
|
public override async Task Execute(IJobExecutionContext context) |
||||
|
{ |
||||
|
var worker = (IBackgroundWorker) ServiceProvider.GetService(typeof(TWorker)); |
||||
|
var workerContext = new PeriodicBackgroundWorkerContext(ServiceProvider); |
||||
|
|
||||
|
switch (worker) |
||||
|
{ |
||||
|
case AsyncPeriodicBackgroundWorkerBase asyncWorker: |
||||
|
{ |
||||
|
if (_doWorkAsyncMethod != null) |
||||
|
{ |
||||
|
await (Task) _doWorkAsyncMethod.Invoke(asyncWorker, new object[] {workerContext}); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
case PeriodicBackgroundWorkerBase syncWorker: |
||||
|
{ |
||||
|
if (_doWorkMethod != null) |
||||
|
{ |
||||
|
_doWorkMethod.Invoke(syncWorker, new object[] {workerContext}); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
using System.Text.RegularExpressions; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.BlobStoring.Azure |
||||
|
{ |
||||
|
public class AzureBlobNamingNormalizer : IBlobNamingNormalizer, ITransientDependency |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
///https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names
|
||||
|
/// </summary>
|
||||
|
public virtual string NormalizeContainerName(string containerName) |
||||
|
{ |
||||
|
// All letters in a container name must be lowercase.
|
||||
|
containerName = containerName.ToLower(); |
||||
|
|
||||
|
// Container names can contain only letters, numbers, and the dash (-) character.
|
||||
|
containerName = Regex.Replace(containerName, "[^a-z0-9-]", string.Empty); |
||||
|
|
||||
|
// Every dash (-) character must be immediately preceded and followed by a letter or number;
|
||||
|
// consecutive dashes are not permitted in container names.
|
||||
|
// Container names must start or end with a letter or number
|
||||
|
containerName = Regex.Replace(containerName, "-{2,}", "-"); |
||||
|
containerName = Regex.Replace(containerName, "^-", string.Empty); |
||||
|
containerName = Regex.Replace(containerName, "-$", string.Empty); |
||||
|
|
||||
|
// Container names must be from 3 through 63 characters long.
|
||||
|
if (containerName.Length < 3) |
||||
|
{ |
||||
|
var length = containerName.Length; |
||||
|
for (var i = 0; i < 3 - length; i++) |
||||
|
{ |
||||
|
containerName += "0"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (containerName.Length > 63) |
||||
|
{ |
||||
|
containerName = containerName.Substring(0, 63); |
||||
|
} |
||||
|
|
||||
|
return containerName; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
///https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#blob-names
|
||||
|
/// </summary>
|
||||
|
public virtual string NormalizeBlobName(string blobName) |
||||
|
{ |
||||
|
return blobName; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
using System; |
||||
|
using System.Runtime; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Text.RegularExpressions; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.BlobStoring.FileSystem |
||||
|
{ |
||||
|
public class FileSystemBlobNamingNormalizer : IBlobNamingNormalizer, ITransientDependency |
||||
|
{ |
||||
|
private readonly IOSPlatformProvider _iosPlatformProvider; |
||||
|
|
||||
|
public FileSystemBlobNamingNormalizer(IOSPlatformProvider iosPlatformProvider) |
||||
|
{ |
||||
|
_iosPlatformProvider = iosPlatformProvider; |
||||
|
} |
||||
|
|
||||
|
public virtual string NormalizeContainerName(string containerName) |
||||
|
{ |
||||
|
return Normalize(containerName); |
||||
|
} |
||||
|
|
||||
|
public virtual string NormalizeBlobName(string blobName) |
||||
|
{ |
||||
|
return Normalize(blobName); |
||||
|
} |
||||
|
|
||||
|
protected virtual string Normalize(string fileName) |
||||
|
{ |
||||
|
var os = _iosPlatformProvider.GetCurrentOSPlatform(); |
||||
|
if (os == OSPlatform.Windows) |
||||
|
{ |
||||
|
// A filename cannot contain any of the following characters: \ / : * ? " < > |
|
||||
|
// In order to support the directory included in the blob name, remove / and \
|
||||
|
fileName = Regex.Replace(fileName, "[:\\*\\?\"<>\\|]", string.Empty); |
||||
|
} |
||||
|
|
||||
|
return fileName; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
namespace Volo.Abp.BlobStoring |
||||
|
{ |
||||
|
public interface IBlobNamingNormalizer |
||||
|
{ |
||||
|
string NormalizeContainerName(string containerName); |
||||
|
|
||||
|
string NormalizeBlobName(string blobName); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,25 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
<AssemblyName>Volo.Abp.Caching.StackExchangeRedis</AssemblyName> |
||||
|
<PackageId>Volo.Abp.Caching.StackExchangeRedis</PackageId> |
||||
|
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback> |
||||
|
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
||||
|
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
||||
|
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\Volo.Abp.Caching\Volo.Abp.Caching.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="3.1.5" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,30 @@ |
|||||
|
using System; |
||||
|
using Microsoft.Extensions.Caching.Distributed; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.DependencyInjection.Extensions; |
||||
|
using Volo.Abp.Modularity; |
||||
|
|
||||
|
namespace Volo.Abp.Caching.StackExchangeRedis |
||||
|
{ |
||||
|
[DependsOn( |
||||
|
typeof(AbpCachingModule) |
||||
|
)] |
||||
|
public class AbpCachingStackExchangeRedisModule : AbpModule |
||||
|
{ |
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
var configuration = context.Services.GetConfiguration(); |
||||
|
|
||||
|
context.Services.AddStackExchangeRedisCache(options => |
||||
|
{ |
||||
|
var redisConfiguration = configuration["Redis:Configuration"]; |
||||
|
if (!redisConfiguration.IsNullOrEmpty()) |
||||
|
{ |
||||
|
options.Configuration = configuration["Redis:Configuration"]; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
context.Services.Replace(ServiceDescriptor.Singleton<IDistributedCache, AbpRedisCache>()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,294 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Reflection; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Caching.Distributed; |
||||
|
using Microsoft.Extensions.Caching.StackExchangeRedis; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using StackExchange.Redis; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.Caching.StackExchangeRedis |
||||
|
{ |
||||
|
[DisableConventionalRegistration] |
||||
|
public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems |
||||
|
{ |
||||
|
protected static readonly string SetScript; |
||||
|
protected static readonly string AbsoluteExpirationKey; |
||||
|
protected static readonly string SlidingExpirationKey; |
||||
|
protected static readonly string DataKey; |
||||
|
protected static readonly long NotPresent; |
||||
|
|
||||
|
private static readonly FieldInfo RedisDatabaseField; |
||||
|
private static readonly MethodInfo ConnectMethod; |
||||
|
private static readonly MethodInfo ConnectAsyncMethod; |
||||
|
private static readonly MethodInfo MapMetadataMethod; |
||||
|
private static readonly MethodInfo GetAbsoluteExpirationMethod; |
||||
|
private static readonly MethodInfo GetExpirationInSecondsMethod; |
||||
|
|
||||
|
protected IDatabase RedisDatabase => GetRedisDatabase(); |
||||
|
private IDatabase _redisDatabase; |
||||
|
|
||||
|
protected string Instance { get; } |
||||
|
|
||||
|
static AbpRedisCache() |
||||
|
{ |
||||
|
var type = typeof(RedisCache); |
||||
|
|
||||
|
RedisDatabaseField = type.GetField("_cache", BindingFlags.Instance | BindingFlags.NonPublic); |
||||
|
|
||||
|
ConnectMethod = type.GetMethod("Connect", BindingFlags.Instance | BindingFlags.NonPublic); |
||||
|
|
||||
|
ConnectAsyncMethod = type.GetMethod("ConnectAsync", BindingFlags.Instance | BindingFlags.NonPublic); |
||||
|
|
||||
|
MapMetadataMethod = type.GetMethod("MapMetadata", BindingFlags.Instance | BindingFlags.NonPublic); |
||||
|
|
||||
|
GetAbsoluteExpirationMethod = type.GetMethod("GetAbsoluteExpiration", BindingFlags.Static | BindingFlags.NonPublic); |
||||
|
|
||||
|
GetExpirationInSecondsMethod = type.GetMethod("GetExpirationInSeconds", BindingFlags.Static | BindingFlags.NonPublic); |
||||
|
|
||||
|
SetScript = type.GetField("SetScript", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString(); |
||||
|
|
||||
|
AbsoluteExpirationKey = type.GetField("AbsoluteExpirationKey", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString(); |
||||
|
|
||||
|
SlidingExpirationKey = type.GetField("SlidingExpirationKey", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString(); |
||||
|
|
||||
|
DataKey = type.GetField("DataKey", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString(); |
||||
|
|
||||
|
NotPresent = type.GetField("NotPresent", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).To<int>(); |
||||
|
} |
||||
|
|
||||
|
public AbpRedisCache(IOptions<RedisCacheOptions> optionsAccessor) |
||||
|
: base(optionsAccessor) |
||||
|
{ |
||||
|
Instance = optionsAccessor.Value.InstanceName ?? string.Empty; |
||||
|
} |
||||
|
|
||||
|
protected virtual void Connect() |
||||
|
{ |
||||
|
if (GetRedisDatabase() != null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
ConnectMethod.Invoke(this, Array.Empty<object>()); |
||||
|
} |
||||
|
|
||||
|
protected virtual Task ConnectAsync(CancellationToken token = default) |
||||
|
{ |
||||
|
if (GetRedisDatabase() != null) |
||||
|
{ |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
return (Task) ConnectAsyncMethod.Invoke(this, new object[] {token}); |
||||
|
} |
||||
|
|
||||
|
public byte[][] GetMany( |
||||
|
IEnumerable<string> keys) |
||||
|
{ |
||||
|
keys = Check.NotNull(keys, nameof(keys)); |
||||
|
|
||||
|
return GetAndRefreshMany(keys, true); |
||||
|
} |
||||
|
|
||||
|
public async Task<byte[][]> GetManyAsync( |
||||
|
IEnumerable<string> keys, |
||||
|
CancellationToken token = default) |
||||
|
{ |
||||
|
keys = Check.NotNull(keys, nameof(keys)); |
||||
|
|
||||
|
return await GetAndRefreshManyAsync(keys, true, token); |
||||
|
} |
||||
|
|
||||
|
public void SetMany( |
||||
|
IEnumerable<KeyValuePair<string, byte[]>> items, |
||||
|
DistributedCacheEntryOptions options) |
||||
|
{ |
||||
|
Connect(); |
||||
|
|
||||
|
Task.WaitAll(PipelineSetMany(items, options)); |
||||
|
} |
||||
|
|
||||
|
public async Task SetManyAsync( |
||||
|
IEnumerable<KeyValuePair<string, byte[]>> items, |
||||
|
DistributedCacheEntryOptions options, |
||||
|
CancellationToken token = default) |
||||
|
{ |
||||
|
token.ThrowIfCancellationRequested(); |
||||
|
|
||||
|
await ConnectAsync(token); |
||||
|
|
||||
|
await Task.WhenAll(PipelineSetMany(items, options)); |
||||
|
} |
||||
|
|
||||
|
protected virtual byte[][] GetAndRefreshMany( |
||||
|
IEnumerable<string> keys, |
||||
|
bool getData) |
||||
|
{ |
||||
|
Connect(); |
||||
|
|
||||
|
var keyArray = keys.Select(key => Instance + key).ToArray(); |
||||
|
RedisValue[][] results; |
||||
|
|
||||
|
if (getData) |
||||
|
{ |
||||
|
results = RedisDatabase.HashMemberGetMany(keyArray, AbsoluteExpirationKey, |
||||
|
SlidingExpirationKey, DataKey); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
results = RedisDatabase.HashMemberGetMany(keyArray, AbsoluteExpirationKey, |
||||
|
SlidingExpirationKey); |
||||
|
} |
||||
|
|
||||
|
Task.WaitAll(PipelineRefreshManyAndOutData(keyArray, results, out var bytes)); |
||||
|
|
||||
|
return bytes; |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<byte[][]> GetAndRefreshManyAsync( |
||||
|
IEnumerable<string> keys, |
||||
|
bool getData, |
||||
|
CancellationToken token = default) |
||||
|
{ |
||||
|
token.ThrowIfCancellationRequested(); |
||||
|
|
||||
|
await ConnectAsync(token); |
||||
|
|
||||
|
var keyArray = keys.Select(key => Instance + key).ToArray(); |
||||
|
RedisValue[][] results; |
||||
|
|
||||
|
if (getData) |
||||
|
{ |
||||
|
results = await RedisDatabase.HashMemberGetManyAsync(keyArray, AbsoluteExpirationKey, |
||||
|
SlidingExpirationKey, DataKey); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
results = await RedisDatabase.HashMemberGetManyAsync(keyArray, AbsoluteExpirationKey, |
||||
|
SlidingExpirationKey); |
||||
|
} |
||||
|
|
||||
|
await Task.WhenAll(PipelineRefreshManyAndOutData(keyArray, results, out var bytes)); |
||||
|
|
||||
|
return bytes; |
||||
|
} |
||||
|
|
||||
|
protected virtual Task[] PipelineRefreshManyAndOutData( |
||||
|
string[] keys, |
||||
|
RedisValue[][] results, |
||||
|
out byte[][] bytes) |
||||
|
{ |
||||
|
bytes = new byte[keys.Length][]; |
||||
|
var tasks = new Task[keys.Length]; |
||||
|
|
||||
|
for (var i = 0; i < keys.Length; i++) |
||||
|
{ |
||||
|
if (results[i].Length >= 2) |
||||
|
{ |
||||
|
MapMetadata(results[i], out DateTimeOffset? absExpr, out TimeSpan? sldExpr); |
||||
|
|
||||
|
if (sldExpr.HasValue) |
||||
|
{ |
||||
|
TimeSpan? expr; |
||||
|
|
||||
|
if (absExpr.HasValue) |
||||
|
{ |
||||
|
var relExpr = absExpr.Value - DateTimeOffset.Now; |
||||
|
expr = relExpr <= sldExpr.Value ? relExpr : sldExpr; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
expr = sldExpr; |
||||
|
} |
||||
|
|
||||
|
tasks[i] = RedisDatabase.KeyExpireAsync(keys[i], expr); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
tasks[i] = Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (results[i].Length >= 3 && results[i][2].HasValue) |
||||
|
{ |
||||
|
bytes[i] = results[i][2]; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
bytes[i] = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return tasks; |
||||
|
} |
||||
|
|
||||
|
protected virtual Task[] PipelineSetMany( |
||||
|
IEnumerable<KeyValuePair<string, byte[]>> items, |
||||
|
DistributedCacheEntryOptions options) |
||||
|
{ |
||||
|
items = Check.NotNull(items, nameof(items)); |
||||
|
options = Check.NotNull(options, nameof(options)); |
||||
|
|
||||
|
var itemArray = items.ToArray(); |
||||
|
var tasks = new Task[itemArray.Length]; |
||||
|
var creationTime = DateTimeOffset.UtcNow; |
||||
|
var absoluteExpiration = GetAbsoluteExpiration(creationTime, options); |
||||
|
|
||||
|
for (var i = 0; i < itemArray.Length; i++) |
||||
|
{ |
||||
|
tasks[i] = RedisDatabase.ScriptEvaluateAsync(SetScript, new RedisKey[] {Instance + itemArray[i].Key}, |
||||
|
new RedisValue[] |
||||
|
{ |
||||
|
absoluteExpiration?.Ticks ?? NotPresent, |
||||
|
options.SlidingExpiration?.Ticks ?? NotPresent, |
||||
|
GetExpirationInSeconds(creationTime, absoluteExpiration, options) ?? NotPresent, |
||||
|
itemArray[i].Value |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return tasks; |
||||
|
} |
||||
|
|
||||
|
protected virtual void MapMetadata( |
||||
|
RedisValue[] results, |
||||
|
out DateTimeOffset? absoluteExpiration, |
||||
|
out TimeSpan? slidingExpiration) |
||||
|
{ |
||||
|
var parameters = new object[] {results, null, null}; |
||||
|
MapMetadataMethod.Invoke(this, parameters); |
||||
|
|
||||
|
absoluteExpiration = (DateTimeOffset?) parameters[1]; |
||||
|
slidingExpiration = (TimeSpan?) parameters[2]; |
||||
|
} |
||||
|
|
||||
|
protected virtual long? GetExpirationInSeconds( |
||||
|
DateTimeOffset creationTime, |
||||
|
DateTimeOffset? absoluteExpiration, |
||||
|
DistributedCacheEntryOptions options) |
||||
|
{ |
||||
|
return (long?) GetExpirationInSecondsMethod.Invoke(null, |
||||
|
new object[] {creationTime, absoluteExpiration, options}); |
||||
|
} |
||||
|
|
||||
|
protected virtual DateTimeOffset? GetAbsoluteExpiration( |
||||
|
DateTimeOffset creationTime, |
||||
|
DistributedCacheEntryOptions options) |
||||
|
{ |
||||
|
return (DateTimeOffset?) GetAbsoluteExpirationMethod.Invoke(null, new object[] {creationTime, options}); |
||||
|
} |
||||
|
|
||||
|
private IDatabase GetRedisDatabase() |
||||
|
{ |
||||
|
if (_redisDatabase == null) |
||||
|
{ |
||||
|
_redisDatabase = RedisDatabaseField.GetValue(this) as IDatabase; |
||||
|
} |
||||
|
|
||||
|
return _redisDatabase; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using StackExchange.Redis; |
||||
|
|
||||
|
namespace Volo.Abp.Caching.StackExchangeRedis |
||||
|
{ |
||||
|
public static class AbpRedisExtensions |
||||
|
{ |
||||
|
public static RedisValue[][] HashMemberGetMany( |
||||
|
this IDatabase cache, |
||||
|
string[] keys, |
||||
|
params string[] members) |
||||
|
{ |
||||
|
var tasks = new Task<RedisValue[]>[keys.Length]; |
||||
|
var fields = members.Select(member => (RedisValue) member).ToArray(); |
||||
|
var results = new RedisValue[keys.Length][]; |
||||
|
|
||||
|
for (var i = 0; i < keys.Length; i++) |
||||
|
{ |
||||
|
tasks[i] = cache.HashGetAsync((RedisKey) keys[i], fields); |
||||
|
} |
||||
|
|
||||
|
for (var i = 0; i < tasks.Length; i++) |
||||
|
{ |
||||
|
results[i] = cache.Wait(tasks[i]); |
||||
|
} |
||||
|
|
||||
|
return results; |
||||
|
} |
||||
|
|
||||
|
public static async Task<RedisValue[][]> HashMemberGetManyAsync( |
||||
|
this IDatabase cache, |
||||
|
string[] keys, |
||||
|
params string[] members) |
||||
|
{ |
||||
|
var tasks = new Task<RedisValue[]>[keys.Length]; |
||||
|
var fields = members.Select(member => (RedisValue) member).ToArray(); |
||||
|
|
||||
|
for (var i = 0; i < keys.Length; i++) |
||||
|
{ |
||||
|
tasks[i] = cache.HashGetAsync((RedisKey) keys[i], fields); |
||||
|
} |
||||
|
|
||||
|
return await Task.WhenAll(tasks); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Caching.Distributed; |
||||
|
|
||||
|
namespace Volo.Abp.Caching |
||||
|
{ |
||||
|
public interface ICacheSupportsMultipleItems |
||||
|
{ |
||||
|
byte[][] GetMany( |
||||
|
IEnumerable<string> keys |
||||
|
); |
||||
|
|
||||
|
Task<byte[][]> GetManyAsync( |
||||
|
IEnumerable<string> keys, |
||||
|
CancellationToken token = default |
||||
|
); |
||||
|
|
||||
|
void SetMany( |
||||
|
IEnumerable<KeyValuePair<string, byte[]>> items, |
||||
|
DistributedCacheEntryOptions options |
||||
|
); |
||||
|
|
||||
|
Task SetManyAsync( |
||||
|
IEnumerable<KeyValuePair<string, byte[]>> items, |
||||
|
DistributedCacheEntryOptions options, |
||||
|
CancellationToken token = default |
||||
|
); |
||||
|
} |
||||
|
} |
||||