@ -0,0 +1,53 @@ |
|||
# ABP.IO Platform 4.2 Final Has Been Released! |
|||
|
|||
[ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) 4.2 versions have been released today. |
|||
|
|||
## What's New With 4.2? |
|||
|
|||
Since all the new features are already explained in details with the [4.2 RC Announcement Post](https://blog.abp.io/abp/ABP-IO-Platform-v4-2-RC-Has-Been-Released), I will not repeat all the details again. See the [RC Blog Post](https://blog.abp.io/abp/ABP-IO-Platform-v4-2-RC-Has-Been-Released) for all the features and enhancements. |
|||
|
|||
## Creating New Solutions |
|||
|
|||
You can create a new solution with the ABP Framework version 4.2 by either using the `abp new` command or using the **direct download** tab on the [get started page](https://abp.io/get-started). |
|||
|
|||
> See the [getting started document](https://docs.abp.io/en/abp/latest/Getting-Started) for details. |
|||
|
|||
## How to Upgrade an Existing Solution |
|||
|
|||
### Install/Update the ABP CLI |
|||
|
|||
First of all, install the ABP CLI or upgrade to the latest version. |
|||
|
|||
If you haven't installed yet: |
|||
|
|||
```bash |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
``` |
|||
|
|||
To update an existing installation: |
|||
|
|||
```bash |
|||
dotnet tool update -g Volo.Abp.Cli |
|||
``` |
|||
|
|||
### ABP UPDATE Command |
|||
|
|||
[ABP CLI](https://docs.abp.io/en/abp/latest/CLI) provides a handy command to update all the ABP related NuGet and NPM packages in your solution with a single command: |
|||
|
|||
```bash |
|||
abp update |
|||
``` |
|||
|
|||
Run this command in the root folder of your solution. |
|||
|
|||
## Migration Guide |
|||
|
|||
Check [the migration guide](https://docs.abp.io/en/abp/latest/Migration-Guides/Abp-4_2) for the applications with the version 4.x upgrading to the version 4.2. |
|||
|
|||
> It is strongly recommended to check the migration guide for this version. Especially, the new `IRepository.GetQueryableAsync()` method is a core change should be considered after upgrading the solution. |
|||
|
|||
## About the Next Version |
|||
|
|||
The next feature version will be 4.3. It is planned to release the 4.3 RC (Release Candidate) on March 11 and the final version on March 25, 2021. |
|||
|
|||
We decided to slow down the feature development for the [next milestone](https://github.com/abpframework/abp/milestone/49). We will continue to improve the existing features and introduce new ones, sure, but wanted to have more time for the planning, documentation, creating guides and improving the development experience. |
|||
@ -0,0 +1,248 @@ |
|||
# Modal |
|||
|
|||
`ModalComponent` is a pre-built component exposed by `@abp/ng.theme.shared` package to show modals. The component uses the [`ng-bootstrap`](https://ng-bootstrap.github.io/)'s modal service inside to render a modal. |
|||
|
|||
The `abp-modal` provides some additional benefits: |
|||
|
|||
- It is **flexible**. You can pass header, body, footer templates easily by adding the templates to the `abp-modal` content. It can also be implemented quickly. |
|||
- Provides several inputs be able to customize the modal and several outputs be able to listen to some events. |
|||
- Automatically detects the close button which has a `#abpClose` template variable and closes the modal when pressed this button. |
|||
- Automatically detects the `abp-button` and triggers its loading spinner when the `busy` input value of the modal component is true. |
|||
- Automatically checks if the form inside the modal **has changed, but not saved**. It warns the user by displaying a [confirmation popup](Confirmation-Service) in this case when a user tries to close the modal or refresh/close the tab of the browser. |
|||
|
|||
|
|||
> Note: A modal can also be rendered by using the `ng-bootstrap` modal. For further information, see [Modal doc](https://ng-bootstrap.github.io/#/components/modal) on the `ng-bootstrap` documentation. |
|||
|
|||
## Getting Started |
|||
|
|||
In order to use the `abp-modal` in an HTML template, the **`ThemeSharedModule`** should be imported into your module like this: |
|||
|
|||
```js |
|||
// ... |
|||
import { ThemeSharedModule } from '@abp/ng.theme.shared'; |
|||
|
|||
@NgModule({ |
|||
//... |
|||
imports: [..., ThemeSharedModule], |
|||
}) |
|||
export class MyFeatureModule {} |
|||
``` |
|||
|
|||
## Usage |
|||
|
|||
You can add the `abp-modal` to your component very quickly. See an example: |
|||
|
|||
```html |
|||
<!-- sample.component.html --> |
|||
|
|||
<button class="btn btn-primary" (click)="isModalOpen = true">Open modal</button> |
|||
|
|||
<abp-modal [(visible)]="isModalOpen"> |
|||
<ng-template #abpHeader> |
|||
<h3>Modal Title</h3> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpBody> |
|||
<p>Modal content</p> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpFooter> |
|||
<button type="button" class="btn btn-secondary" #abpClose>Close</button> |
|||
</ng-template> |
|||
</abp-modal> |
|||
``` |
|||
|
|||
```js |
|||
// sample.component.ts |
|||
|
|||
@Component(/* component metadata */) |
|||
export class SampleComponent { |
|||
isModelOpen = false |
|||
} |
|||
``` |
|||
|
|||
 |
|||
|
|||
|
|||
See an example form inside a modal: |
|||
|
|||
```html |
|||
<!-- book.component.ts --> |
|||
|
|||
<abp-modal [(visible)]="isModalOpen" [busy]="inProgress"> |
|||
<ng-template #abpHeader> |
|||
<h3>Book</h3> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpBody> |
|||
<form id="book-form" [formGroup]="form" (ngSubmit)="save()"> |
|||
<div class="form-group"> |
|||
<label for="book-name">Author</label><span> * </span> |
|||
<input type="text" id="author" class="form-control" formControlName="author" autofocus /> |
|||
</div> |
|||
|
|||
<div class="form-group"> |
|||
<label for="book-name">Name</label><span> * </span> |
|||
<input type="text" id="book-name" class="form-control" formControlName="name" /> |
|||
</div> |
|||
|
|||
<div class="form-group"> |
|||
<label for="book-price">Price</label><span> * </span> |
|||
<input type="number" id="book-price" class="form-control" formControlName="price" /> |
|||
</div> |
|||
|
|||
<div class="form-group"> |
|||
<label for="book-type">Type</label><span> * </span> |
|||
<select class="form-control" id="book-type" formControlName="type"> |
|||
<option [ngValue]="null">Select a book type</option> |
|||
<option [ngValue]="0">Undefined</option> |
|||
<option [ngValue]="1">Adventure</option> |
|||
<option [ngValue]="2">Biography</option> |
|||
<option [ngValue]="3">Fantastic</option> |
|||
<option [ngValue]="4">Science</option> |
|||
</select> |
|||
</div> |
|||
|
|||
<div class="form-group"> |
|||
<label for="book-publish-date">Publish date</label><span> * </span> |
|||
<input |
|||
id="book-publish-date" |
|||
formControlName="publishDate" |
|||
class="form-control" |
|||
type="date" |
|||
/> |
|||
</div> |
|||
</form> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpFooter> |
|||
<button type="button" class="btn btn-secondary" #abpClose> |
|||
Cancel |
|||
</button> |
|||
|
|||
<button form="book-form" class="btn btn-primary" [disabled]="form.invalid || form.pristine"> |
|||
<i class="fa fa-check mr-1"></i> |
|||
Save |
|||
</button> |
|||
</ng-template> |
|||
</abp-modal> |
|||
``` |
|||
|
|||
```ts |
|||
// book.component.ts |
|||
|
|||
import { Component } from '@angular/core'; |
|||
import { FormBuilder, Validators } from '@angular/forms'; |
|||
|
|||
@Component(/* component metadata */) |
|||
export class BookComponent { |
|||
form = this.fb.group({ |
|||
author: [null, [Validators.required]], |
|||
name: [null, [Validators.required]], |
|||
price: [null, [Validators.required, Validators.min(0)]], |
|||
type: [null, [Validators.required]], |
|||
publishDate: [null, [Validators.required]], |
|||
}); |
|||
|
|||
inProgress: boolean; |
|||
|
|||
isModalOpen: boolean; |
|||
|
|||
constructor(private fb: FormBuilder, private service: BookService) {} |
|||
|
|||
save() { |
|||
if (this.form.invalid) return; |
|||
|
|||
this.inProgress = true; |
|||
|
|||
this.service.save(this.form.value).subscribe(() => { |
|||
this.inProgress = false; |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
The modal with form looks like this: |
|||
|
|||
 |
|||
|
|||
## API |
|||
|
|||
### Inputs |
|||
|
|||
#### visible |
|||
|
|||
```js |
|||
@Input() visible: boolean |
|||
``` |
|||
|
|||
**`visible`** is a boolean input that determines whether the modal is open. It is also can be used two-way binding. |
|||
|
|||
#### busy |
|||
|
|||
```js |
|||
@Input() busy: boolean |
|||
``` |
|||
|
|||
**`busy`** is a boolean input that determines whether the busy status of the modal is true. When `busy` is true, the modal cannot be closed and the `abp-button` loading spinner is triggered. |
|||
|
|||
|
|||
#### options |
|||
|
|||
```js |
|||
@Input() options: NgbModalOptions |
|||
``` |
|||
|
|||
**`options`** is an input typed [NgbModalOptions](https://ng-bootstrap.github.io/#/components/modal/api#NgbModalOptions). It is configuration for the `ng-bootstrap` modal. |
|||
|
|||
#### suppressUnsavedChangesWarning |
|||
|
|||
```js |
|||
@Input() suppressUnsavedChangesWarning: boolean |
|||
``` |
|||
|
|||
**`suppressUnsavedChangesWarning`** is a boolean input that determines whether the confirmation popup triggering active or not. It can also be set globally as shown below: |
|||
|
|||
```ts |
|||
//app.module.ts |
|||
|
|||
// app.module.ts |
|||
|
|||
import { SUPPRESS_UNSAVED_CHANGES_WARNING } from '@abp/ng.theme.shared'; |
|||
|
|||
// ... |
|||
|
|||
@NgModule({ |
|||
// ... |
|||
providers: [{provide: SUPPRESS_UNSAVED_CHANGES_WARNING, useValue: true}] |
|||
}) |
|||
export class AppModule {} |
|||
``` |
|||
|
|||
Note: The `suppressUnsavedChangesWarning` input of `abp-modal` value overrides the `SUPPRESS_UNSAVED_CHANGES_WARNING` injection token value. |
|||
|
|||
### Outputs |
|||
|
|||
#### visibleChange |
|||
|
|||
```js |
|||
@Output() readonly visibleChange = new EventEmitter<boolean>(); |
|||
``` |
|||
|
|||
**`visibleChange`** is an event emitted when the modal visibility has changed. The event payload is a boolean. |
|||
|
|||
#### appear |
|||
|
|||
```js |
|||
@Output() readonly appear = new EventEmitter<void>(); |
|||
``` |
|||
|
|||
**`appear`** is an event emitted when the modal has opened. |
|||
|
|||
#### disappear |
|||
|
|||
```js |
|||
@Output() readonly disappear = new EventEmitter<void>(); |
|||
``` |
|||
|
|||
**`disappear`** is an event emitted when the modal has closed. |
|||
@ -0,0 +1,146 @@ |
|||
# Router Events Simplified |
|||
|
|||
`RouterEvents` is a utility service to provide an easy implementation for one of the most frequent needs in Angular templates: `TrackByFunction`. Please see [this page in Angular docs](https://angular.io/guide/template-syntax#ngfor-with-trackby) for its purpose. |
|||
|
|||
|
|||
|
|||
|
|||
## Benefit |
|||
|
|||
You can use router events directly and filter them as seen below: |
|||
|
|||
```js |
|||
import { |
|||
NavigationEnd, |
|||
NavigationError, |
|||
NavigationCancel, |
|||
Router, |
|||
} from '@angular/router'; |
|||
import { filter } from 'rxjs/operators'; |
|||
|
|||
@Injectable() |
|||
class SomeService { |
|||
navigationFinish$ = this.router.events.pipe( |
|||
filter( |
|||
event => |
|||
event instanceof NavigationEnd || |
|||
event instanceof NavigationError || |
|||
event instanceof NavigationCancel, |
|||
), |
|||
); |
|||
/* Observable<Event> */ |
|||
|
|||
constructor(private router: Router) {} |
|||
} |
|||
``` |
|||
|
|||
However, `RouterEvents` makes filtering router events easier. |
|||
|
|||
```js |
|||
import { RouterEvents } from '@abp/ng.core'; |
|||
|
|||
@Injectable() |
|||
class SomeService { |
|||
navigationFinish$ = this.routerEvents.getNavigationEvents('End', 'Error', 'Cancel'); |
|||
/* Observable<NavigationCancel | NavigationEnd | NavigationError> */ |
|||
|
|||
constructor(private routerEvents: RouterEvents) {} |
|||
} |
|||
``` |
|||
|
|||
`RouterEvents` also delivers improved type-safety. In the example above, `navigationFinish$` has inferred type of `Observable<NavigationCancel | NavigationEnd | NavigationError>` whereas it would have `Observable<Event>` when router events are filtered directly. |
|||
|
|||
|
|||
|
|||
|
|||
## Usage |
|||
|
|||
You do not have to provide `RouterEvents` at the module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components. |
|||
|
|||
|
|||
### How to Get Specific Navigation Events |
|||
|
|||
You can use `getNavigationEvents` to get a stream of navigation events matching given event keys. |
|||
|
|||
```js |
|||
import { RouterEvents } from '@abp/ng.core'; |
|||
import { merge } from 'rxjs'; |
|||
import { mapTo } from 'rxjs/operators'; |
|||
|
|||
@Injectable() |
|||
class SomeService { |
|||
navigationStart$ = this.routerEvents.getNavigationEvents('Start'); |
|||
/* Observable<NavigationStart> */ |
|||
|
|||
navigationFinish$ = this.routerEvents.getNavigationEvents('End', 'Error', 'Cancel'); |
|||
/* Observable<NavigationCancel | NavigationEnd | NavigationError> */ |
|||
|
|||
loading$ = merge( |
|||
this.navigationStart$.pipe(mapTo(true)), |
|||
this.navigationFinish$.pipe(mapTo(false)), |
|||
); |
|||
/* Observable<boolean> */ |
|||
|
|||
constructor(private routerEvents: RouterEvents) {} |
|||
} |
|||
``` |
|||
|
|||
|
|||
### How to Get All Navigation Events |
|||
|
|||
You can use `getAllNavigationEvents` to get a stream of all navigation events without passing any keys. |
|||
|
|||
```js |
|||
import { RouterEvents, NavigationStart } from '@abp/ng.core'; |
|||
import { map } from 'rxjs/operators'; |
|||
|
|||
@Injectable() |
|||
class SomeService { |
|||
navigationEvent$ = this.routerEvents.getAllNavigationEvents(); |
|||
/* Observable<NavigationCancel | NavigationEnd | NavigationError | NavigationStart> */ |
|||
|
|||
loading$ = this.navigationEvent$.pipe( |
|||
map(event => event instanceof NavigationStart), |
|||
); |
|||
/* Observable<boolean> */ |
|||
|
|||
constructor(private routerEvents: RouterEvents) {} |
|||
} |
|||
``` |
|||
|
|||
|
|||
### How to Get Specific Router Events |
|||
|
|||
You can use `getEvents` to get a stream of router events matching given event constructors. |
|||
|
|||
```js |
|||
import { RouterEvents } from '@abp/ng.core'; |
|||
import { ActivationEnd, ChildActivationEnd } from '@angular/router'; |
|||
|
|||
@Injectable() |
|||
class SomeService { |
|||
moduleActivation$ = this.routerEvents.getEvents(ActivationEnd, ChildActivationEnd); |
|||
/* Observable<ActivationEnd | ChildActivationEnd> */ |
|||
|
|||
constructor(private routerEvents: RouterEvents) {} |
|||
} |
|||
``` |
|||
|
|||
|
|||
### How to Get All Router Events |
|||
|
|||
You can use `getEvents` to get a stream of all router events without passing any event constructors. This is nothing different from accessing `events` property of `Router` and is added to the service just for convenience. |
|||
|
|||
```js |
|||
import { RouterEvents } from '@abp/ng.core'; |
|||
import { ActivationEnd, ChildActivationEnd } from '@angular/router'; |
|||
|
|||
@Injectable() |
|||
class SomeService { |
|||
routerEvent$ = this.routerEvents.getAllEvents(); |
|||
/* Observable<Event> */ |
|||
|
|||
constructor(private routerEvents: RouterEvents) {} |
|||
} |
|||
``` |
|||
|
|||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 108 KiB |
@ -0,0 +1,233 @@ |
|||
# 模块化插件 |
|||
|
|||
可以将[模块](Module-Development-Basics.md)加载为插件.这意味着你可能不需要在解决方案中引用模块的程序集,就可以像其它模块一样在启动应用时加载该模块. |
|||
|
|||
## 基本用法 |
|||
|
|||
`IServiceCollection.AddApplication<T>()` 扩展方法可以获取配置插件源的选项. |
|||
|
|||
**示例: 从文件夹加载插件** |
|||
|
|||
````csharp |
|||
using Microsoft.AspNetCore.Builder; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Modularity.PlugIns; |
|||
|
|||
namespace MyPlugInDemo.Web |
|||
{ |
|||
public class Startup |
|||
{ |
|||
public void ConfigureServices(IServiceCollection services) |
|||
{ |
|||
services.AddApplication<MyPlugInDemoWebModule>(options => |
|||
{ |
|||
options.PlugInSources.AddFolder(@"D:\Temp\MyPlugIns"); |
|||
}); |
|||
} |
|||
|
|||
public void Configure(IApplicationBuilder app) |
|||
{ |
|||
app.InitializeApplication(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* 这是典型的ASP.NET Core应用程序的`Startup`类. |
|||
* `PlugInSources.AddFolder`从指定的目录中加载程序集(通常为dll). |
|||
|
|||
就这样.ABP将在这个目录中发现这些模块,像其它常规一样配置和初始化它们. |
|||
|
|||
### 插件源 |
|||
|
|||
`options.PlugInSources`类实际上是`IPlugInSource`接口的一系列实现并且 `AddFolder`方法仅仅是以下表达式的便捷方法: |
|||
|
|||
````csharp |
|||
options.PlugInSources.Add(new FolderPlugInSource(@"D:\Temp\MyPlugIns")); |
|||
```` |
|||
|
|||
> `AddFolder()`方法仅在给定目录下查找程序集文件,而不在子目录中查找.你可以传递一个`SearchOption.AllDirectories`参数作为第二个参数,来递归地查找它的子目录. |
|||
|
|||
这里有两个内置插件源的示例: |
|||
|
|||
* `PlugInSources.AddFiles()`方法获取程序集(通常是dll)文件列表.这是使用`FilePlugInSource`类的快捷方式. |
|||
* `PlugInSources.AddTypes()`方法获取模块类类型的列表.如果实用化此方法,则需要自己加载模块的程序集,但是在需要时它提供了灵活性.这是使用`TypePlugInSource`类的快捷方式. |
|||
|
|||
如果需要,你可以创建自己的`IPlugInSource`的接口实现,并像其它方法一样添加到`options.PlugInSources`中. |
|||
|
|||
## 示例:创建一个简单的插件 |
|||
|
|||
在一个解决方案中创建一个简单的**类库项目** |
|||
|
|||
 |
|||
|
|||
你可以在模块中添加需要使用的ABP框架包.至少,你应该为这个项目添加包`Volo.Abp.Core`: |
|||
|
|||
```` |
|||
Install-Package Volo.Abp.Core |
|||
```` |
|||
|
|||
每个[模块](Module-Development-Basics.md)必须声明为一个继承自`AbpModule`的类.这里是一个简单的模块类,用于解析一个服务并在应用启动时对其初始化: |
|||
|
|||
````csharp |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace MyPlugIn |
|||
{ |
|||
public class MyPlungInModule : AbpModule |
|||
{ |
|||
public override void OnApplicationInitialization(ApplicationInitializationContext context) |
|||
{ |
|||
var myService = context.ServiceProvider |
|||
.GetRequiredService<MyService>(); |
|||
|
|||
myService.Initialize(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`MyService`可以是注册在[依赖注入](Dependency-Injection.md)系统中的任意类,如下所示: |
|||
|
|||
````csharp |
|||
using Microsoft.Extensions.Logging; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace MyPlugIn |
|||
{ |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly ILogger<MyService> _logger; |
|||
|
|||
public MyService(ILogger<MyService> logger) |
|||
{ |
|||
_logger = logger; |
|||
} |
|||
|
|||
public void Initialize() |
|||
{ |
|||
_logger.LogInformation("MyService has been initialized"); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
编译这个项目,打开build目录,找到`MyPlugIn.dll`: |
|||
|
|||
 |
|||
|
|||
将`MyPlugIn.dll`复制到到插件目录中(此实例为`D:\Temp\MyPlugIns`). |
|||
|
|||
如果你已经按照上述方式配置了主应用程序(参见“基础用法”部分),那么在应用程序启动时,你可以看到“MyService has been initialized(MyService已经初始化)的日志. |
|||
|
|||
## 示例:创建一个Razor Pages插件 |
|||
|
|||
创建内部带视图的插件需要更多的注意. |
|||
|
|||
> 这个示例假设你已经使用应用程序启动模板和MVC / Razor Pages UI[创建了一个新的Web应用程序](https://abp.io/get-started). |
|||
|
|||
在解决方案中创建一个新的**类库**项目: |
|||
|
|||
 |
|||
|
|||
编辑这个`.csproj`文件内容: |
|||
|
|||
````xml |
|||
<Project Sdk="Microsoft.NET.Sdk.Web"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net5.0</TargetFramework> |
|||
<OutputType>Library</OutputType> |
|||
<IsPackable>true</IsPackable> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared" Version="4.0.1" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
```` |
|||
|
|||
* 将`Sdk`修改为`Microsoft.NET.Sdk.Web`. |
|||
* 添加了`OutputType`和`IsPackable`属性. |
|||
* 添加了`Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared`NuGet包. |
|||
|
|||
> 不需要[Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared) 包.你可以引用更基础的程序包,例如[Volo.Abp.AspNetCore.Mvc](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc/). 但是,如果需要构建一个UI视图/组件,建议参考[Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared)程序包,因为它是最高级的程序包,不依赖于特定[theme](UI/AspNetCore/Theming.md).如果依赖特定主题没有问题,则可以直接引用该主题的程序包,以便能够使用插件中特定于主题的功能. |
|||
|
|||
接下来在插件中创建模块类: |
|||
|
|||
````csharp |
|||
using System.IO; |
|||
using System.Reflection; |
|||
using Microsoft.AspNetCore.Mvc.ApplicationParts; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace MyMvcUIPlugIn |
|||
{ |
|||
[DependsOn(typeof(AbpAspNetCoreMvcUiThemeSharedModule))] |
|||
public class MyMvcUIPlugInModule : AbpModule |
|||
{ |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
PreConfigure<IMvcBuilder>(mvcBuilder => |
|||
{ |
|||
// 添加插件程序集 |
|||
mvcBuilder.PartManager.ApplicationParts.Add(new AssemblyPart(typeof(MyMvcUIPlugInModule).Assembly)); |
|||
|
|||
// 添加视图程序集 |
|||
var viewDllPath = Path.Combine(Path.GetDirectoryName(typeof(MyMvcUIPlugInModule).Assembly.Location), "MyMvcUIPlugIn.Views.dll"); |
|||
var viewAssembly = new CompiledRazorAssemblyPart(Assembly.LoadFrom(viewDllPath)); |
|||
mvcBuilder.PartManager.ApplicationParts.Add(viewAssembly); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* 由于我们添加了相关的NuGet包,因此取决于`AbpAspNetCoreMvcUiThemeSharedModule`. |
|||
* 添加插件程序集到ASP.NET Core MVC的`PartManager`中.这是ASP.NET Core所必需的.否则,你插件中的控制器将无法正常工作. |
|||
* 添加插件的视图程序集到ASP.NET Core MVC的`PartManager`中.这是ASP.NET Core所必需的.否则,你在插件中的视图将不起作用. |
|||
|
|||
现在,你可以在`Pages`目录下添加一个razor页面,例如`MyPlugInPage.cshtml`: |
|||
|
|||
````html |
|||
@page |
|||
@model MyMvcUIPlugIn.Pages.MyPlugInPage |
|||
<h1>Welcome to my plug-in page</h1> |
|||
<p>This page is located inside a plug-in module! :)</p> |
|||
```` |
|||
|
|||
现在,你可以构建插件项目.它将产生以下输出: |
|||
|
|||
 |
|||
|
|||
将`MyMvcUIPlugIn.dll`和`MyMvcUIPlugIn.Views.dll`复制到到插件目录下(此示例中为`D:\Temp\MyPlugIns`). |
|||
|
|||
如果你已经按照上述方式配置了主应用程序(参见“基础用法”部分),那么在应用程序启动的时候,你应该能够访问`/MyPlugInPage`URL: |
|||
|
|||
 |
|||
|
|||
## 讨论 |
|||
|
|||
在现实世界中,你的插件可能具有一些外部依赖性.另外,你的应用程序可能被设计为支持插件.所有这些都是你自己的系统要求.ABP做的仅仅是在应用程序启动时加载模块.你在这些模块中执行什么操作由你决定. |
|||
|
|||
但是,我们可以为一些常见情况提供一些建议. |
|||
|
|||
### 库依赖 |
|||
|
|||
对于包/dll依赖,你可以将相关的dll复制到插件目录下.ABP会自动将所有程序集加载到该目录下,并且你的插件将按预期工作. |
|||
|
|||
> 请参见[Microsoft文档](https://docs.microsoft.com/zh-cn/dotnet/core/tutorials/creating-app-with-plugin-support#plugin-with-library-dependencies). |
|||
|
|||
### 数据库模式 |
|||
|
|||
如果你的模块使用关系型数据库和[Entity Framework Core](Entity-Framework-Core.md), 那么它需要在数据库中提供表.有多种不同的方法可确保在应用程序使用插件时创建表.一些例子; |
|||
|
|||
1. 插件可以检查数据库表是否存在,并在应用程序启动时创建表,或者如果插件已更新且需要进行某些架构更改时,则会迁移它们.你可以使用EF Core的迁移API来做到这一点. |
|||
2. 你可以改进`DbMigrator`应用程序,用于查找插件的迁移并执行它们. |
|||
|
|||
可能还有其它解决方案.例如,如果你的数据库管理员不允许你在应用程序代码中更改数据库模式,则可能需要手动将SQL文件发送给数据库管理员,以将其应用于数据库. |
|||
@ -0,0 +1 @@ |
|||
TODO... |
|||
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 25 KiB |
@ -0,0 +1,57 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using System.Text; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.AspNetCore.Mvc.Conventions; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc |
|||
{ |
|||
public class AspNetCoreApiDescriptionModelProviderOptions |
|||
{ |
|||
public Func<Type, ConventionalControllerSetting, string> ControllerNameGenerator { get; set; } |
|||
|
|||
public Func<MethodInfo, string> ActionNameGenerator { get; set; } |
|||
|
|||
public AspNetCoreApiDescriptionModelProviderOptions() |
|||
{ |
|||
ControllerNameGenerator = (controllerType, setting) => |
|||
{ |
|||
var controllerName = controllerType.Name.RemovePostFix("Controller") |
|||
.RemovePostFix(ApplicationService.CommonPostfixes); |
|||
|
|||
if (setting?.UrlControllerNameNormalizer != null) |
|||
{ |
|||
controllerName = |
|||
setting.UrlControllerNameNormalizer( |
|||
new UrlControllerNameNormalizerContext(setting.RootPath, controllerName)); |
|||
} |
|||
|
|||
return controllerName; |
|||
}; |
|||
|
|||
ActionNameGenerator = (method) => |
|||
{ |
|||
var methodNameBuilder = new StringBuilder(method.Name); |
|||
|
|||
var parameters = method.GetParameters(); |
|||
if (parameters.Any()) |
|||
{ |
|||
methodNameBuilder.Append("By"); |
|||
|
|||
for (var i = 0; i < parameters.Length; i++) |
|||
{ |
|||
if (i > 0) |
|||
{ |
|||
methodNameBuilder.Append("And"); |
|||
} |
|||
|
|||
methodNameBuilder.Append(parameters[i].Name.ToPascalCase()); |
|||
} |
|||
} |
|||
|
|||
return methodNameBuilder.ToString(); |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -1,25 +0,0 @@ |
|||
using System.IO; |
|||
using Microsoft.AspNetCore.Http; |
|||
using Volo.Abp.Content; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.Content |
|||
{ |
|||
internal class InternalRemoteStreamContent : IRemoteStreamContent |
|||
{ |
|||
private readonly HttpContext _httpContext; |
|||
|
|||
public InternalRemoteStreamContent(HttpContext httpContext) |
|||
{ |
|||
_httpContext = httpContext; |
|||
} |
|||
|
|||
public string ContentType => _httpContext.Request.ContentType; |
|||
|
|||
public long? ContentLength => _httpContext.Request.ContentLength; |
|||
|
|||
public Stream GetStream() |
|||
{ |
|||
return _httpContext.Request.Body; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using System; |
|||
using Microsoft.AspNetCore.Http; |
|||
using Microsoft.AspNetCore.Localization; |
|||
|
|||
namespace Microsoft.AspNetCore.RequestLocalization |
|||
{ |
|||
public static class AbpRequestCultureCookieHelper |
|||
{ |
|||
public static void SetCultureCookie( |
|||
HttpContext httpContext, |
|||
RequestCulture requestCulture) |
|||
{ |
|||
httpContext.Response.Cookies.Append( |
|||
CookieRequestCultureProvider.DefaultCookieName, |
|||
CookieRequestCultureProvider.MakeCookieValue(requestCulture), |
|||
new CookieOptions |
|||
{ |
|||
Expires = DateTime.Now.AddYears(2) |
|||
} |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -1,67 +0,0 @@ |
|||
// This software is part of the Autofac IoC container
|
|||
// Copyright © 2017 Autofac Contributors
|
|||
// http://autofac.org
|
|||
//
|
|||
// Permission is hereby granted, free of charge, to any person
|
|||
// obtaining a copy of this software and associated documentation
|
|||
// files (the "Software"), to deal in the Software without
|
|||
// restriction, including without limitation the rights to use,
|
|||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|||
// copies of the Software, and to permit persons to whom the
|
|||
// Software is furnished to do so, subject to the following
|
|||
// conditions:
|
|||
//
|
|||
// The above copyright notice and this permission notice shall be
|
|||
// included in all copies or substantial portions of the Software.
|
|||
//
|
|||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|||
// OTHER DEALINGS IN THE SOFTWARE.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using JetBrains.Annotations; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp; |
|||
|
|||
namespace Autofac.Extensions.DependencyInjection |
|||
{ |
|||
/// <summary>
|
|||
/// Extension methods on <see cref="IServiceCollection"/> to register the <see cref="IServiceProviderFactory{TContainerBuilder}"/>.
|
|||
/// </summary>
|
|||
public static class ServiceCollectionExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Adds the <see cref="AutofacServiceProviderFactory"/> to the service collection.
|
|||
/// </summary>
|
|||
/// <param name="services">The service collection to add the factory to.</param>
|
|||
/// <param name="configurationAction">Action on a <see cref="ContainerBuilder"/> that adds component registrations to the container.</param>
|
|||
/// <returns>The service collection.</returns>
|
|||
public static IServiceCollection AddAutofac(this IServiceCollection services, Action<ContainerBuilder> configurationAction = null) |
|||
{ |
|||
return services |
|||
.ClearServiceProviderFactories() |
|||
.AddSingleton<IServiceProviderFactory<ContainerBuilder>>(new AutofacServiceProviderFactory(configurationAction)); |
|||
} |
|||
|
|||
private static IServiceCollection ClearServiceProviderFactories([NotNull] this IServiceCollection services) |
|||
{ |
|||
Check.NotNull(services, nameof(services)); |
|||
|
|||
services.RemoveAll( |
|||
service => service.ImplementationInstance? |
|||
.GetType() |
|||
.GetInterfaces() |
|||
.Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IServiceProviderFactory<>)) == true |
|||
); |
|||
|
|||
return services; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
using System; |
|||
using System.Net.Http; |
|||
using System.Threading; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Threading; |
|||
|
|||
namespace Volo.Abp.Cli.Http |
|||
{ |
|||
public class CliHttpClientFactory : ISingletonDependency |
|||
{ |
|||
public static readonly TimeSpan DefaultTimeout = TimeSpan.FromMinutes(1); |
|||
|
|||
private readonly IHttpClientFactory _clientFactory; |
|||
private readonly ICancellationTokenProvider _cancellationTokenProvider; |
|||
|
|||
public CliHttpClientFactory(IHttpClientFactory clientFactory, ICancellationTokenProvider cancellationTokenProvider) |
|||
{ |
|||
_clientFactory = clientFactory; |
|||
_cancellationTokenProvider = cancellationTokenProvider; |
|||
} |
|||
|
|||
public HttpClient CreateClient(bool needsAuthentication = true, TimeSpan? timeout = null) |
|||
{ |
|||
var httpClient = _clientFactory.CreateClient(CliConsts.HttpClientName); |
|||
httpClient.Timeout = timeout ?? DefaultTimeout; |
|||
|
|||
if (needsAuthentication) |
|||
{ |
|||
httpClient.AddAbpAuthenticationToken(); |
|||
} |
|||
|
|||
return httpClient; |
|||
} |
|||
|
|||
public CancellationToken GetCancellationToken(TimeSpan? timeout = null) |
|||
{ |
|||
if (timeout == null) |
|||
{ |
|||
if (_cancellationTokenProvider == null) |
|||
{ |
|||
var cancellationTokenSource = new CancellationTokenSource(); |
|||
cancellationTokenSource.CancelAfter(DefaultTimeout); |
|||
return cancellationTokenSource.Token; |
|||
} |
|||
else |
|||
{ |
|||
return _cancellationTokenProvider.Token; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
var cancellationTokenSource = new CancellationTokenSource(); |
|||
cancellationTokenSource.CancelAfter(Convert.ToInt32(timeout.Value.TotalMilliseconds)); |
|||
return cancellationTokenSource.Token; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.Abp.Data |
|||
{ |
|||
public class AbpDatabaseInfo |
|||
{ |
|||
public string DatabaseName { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// List of connection names mapped to this database.
|
|||
/// </summary>
|
|||
public HashSet<string> MappedConnections { get; } |
|||
|
|||
/// <summary>
|
|||
/// Is this database used by tenants. Set this to true if this database
|
|||
/// can't owned by tenants.
|
|||
///
|
|||
/// Default: true.
|
|||
/// </summary>
|
|||
public bool IsUsedByTenants { get; set; } = true; |
|||
|
|||
internal AbpDatabaseInfo(string databaseName) |
|||
{ |
|||
DatabaseName = databaseName; |
|||
MappedConnections = new HashSet<string>(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.Data |
|||
{ |
|||
public class AbpDatabaseInfoDictionary : Dictionary<string, AbpDatabaseInfo> |
|||
{ |
|||
private Dictionary<string, AbpDatabaseInfo> ConnectionIndex { get; set; } |
|||
|
|||
public AbpDatabaseInfoDictionary() |
|||
{ |
|||
ConnectionIndex = new Dictionary<string, AbpDatabaseInfo>(); |
|||
} |
|||
|
|||
[CanBeNull] |
|||
public AbpDatabaseInfo GetMappedDatabaseOrNull(string connectionStringName) |
|||
{ |
|||
return ConnectionIndex.GetOrDefault(connectionStringName); |
|||
} |
|||
|
|||
public AbpDatabaseInfoDictionary Configure(string databaseName, Action<AbpDatabaseInfo> configureAction) |
|||
{ |
|||
var databaseInfo = this.GetOrAdd( |
|||
databaseName, |
|||
() => new AbpDatabaseInfo(databaseName) |
|||
); |
|||
|
|||
configureAction(databaseInfo); |
|||
|
|||
return this; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// This method should be called if this dictionary changes.
|
|||
/// It refreshes indexes for quick access to the connection informations.
|
|||
/// </summary>
|
|||
public void RefreshIndexes() |
|||
{ |
|||
ConnectionIndex = new Dictionary<string, AbpDatabaseInfo>(); |
|||
|
|||
foreach (var databaseInfo in Values) |
|||
{ |
|||
foreach (var mappedConnection in databaseInfo.MappedConnections) |
|||
{ |
|||
if (ConnectionIndex.ContainsKey(mappedConnection)) |
|||
{ |
|||
throw new AbpException( |
|||
$"A connection name can not map to multiple databases: {mappedConnection}." |
|||
); |
|||
} |
|||
|
|||
ConnectionIndex[mappedConnection] = databaseInfo; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,12 +1,54 @@ |
|||
namespace Volo.Abp.Data |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.Abp.Data |
|||
{ |
|||
public class AbpDbConnectionOptions |
|||
{ |
|||
public ConnectionStrings ConnectionStrings { get; set; } |
|||
|
|||
public AbpDatabaseInfoDictionary Databases { get; set; } |
|||
|
|||
public AbpDbConnectionOptions() |
|||
{ |
|||
ConnectionStrings = new ConnectionStrings(); |
|||
Databases = new AbpDatabaseInfoDictionary(); |
|||
} |
|||
|
|||
public string GetConnectionStringOrNull( |
|||
string connectionStringName, |
|||
bool fallbackToDatabaseMappings = true, |
|||
bool fallbackToDefault = true) |
|||
{ |
|||
var connectionString = ConnectionStrings.GetOrDefault(connectionStringName); |
|||
if (!connectionString.IsNullOrEmpty()) |
|||
{ |
|||
return connectionString; |
|||
} |
|||
|
|||
if (fallbackToDatabaseMappings) |
|||
{ |
|||
var database = Databases.GetMappedDatabaseOrNull(connectionStringName); |
|||
if (database != null) |
|||
{ |
|||
connectionString = ConnectionStrings.GetOrDefault(database.DatabaseName); |
|||
if (!connectionString.IsNullOrEmpty()) |
|||
{ |
|||
return connectionString; |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (fallbackToDefault) |
|||
{ |
|||
connectionString = ConnectionStrings.Default; |
|||
if (!connectionString.IsNullOrWhiteSpace()) |
|||
{ |
|||
return connectionString; |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1,26 +1,33 @@ |
|||
// using JetBrains.Annotations;
|
|||
// using Microsoft.EntityFrameworkCore;
|
|||
// using System;
|
|||
// using Oracle.EntityFrameworkCore.Infrastructure;
|
|||
// using Volo.Abp.EntityFrameworkCore.DependencyInjection;
|
|||
//
|
|||
// namespace Volo.Abp.EntityFrameworkCore
|
|||
// {
|
|||
// public static class AbpDbContextConfigurationContextOracleExtensions
|
|||
// {
|
|||
// public static DbContextOptionsBuilder UseOracle(
|
|||
// [NotNull] this AbpDbContextConfigurationContext context,
|
|||
// [CanBeNull] Action<OracleDbContextOptionsBuilder> oracleOptionsAction = null)
|
|||
// {
|
|||
// TODO: UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
|
|||
// if (context.ExistingConnection != null)
|
|||
// {
|
|||
// return context.DbContextOptions.UseOracle(context.ExistingConnection, oracleOptionsAction);
|
|||
// }
|
|||
// else
|
|||
// {
|
|||
// return context.DbContextOptions.UseOracle(context.ConnectionString, oracleOptionsAction);
|
|||
// }
|
|||
// }
|
|||
// }
|
|||
// }
|
|||
using JetBrains.Annotations; |
|||
using Microsoft.EntityFrameworkCore; |
|||
using System; |
|||
using Oracle.EntityFrameworkCore.Infrastructure; |
|||
using Volo.Abp.EntityFrameworkCore.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore |
|||
{ |
|||
public static class AbpDbContextConfigurationContextOracleExtensions |
|||
{ |
|||
public static DbContextOptionsBuilder UseOracle( |
|||
[NotNull] this AbpDbContextConfigurationContext context, |
|||
[CanBeNull] Action<OracleDbContextOptionsBuilder> oracleOptionsAction = null) |
|||
{ |
|||
if (context.ExistingConnection != null) |
|||
{ |
|||
return context.DbContextOptions.UseOracle(context.ExistingConnection, optionsBuilder => |
|||
{ |
|||
optionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); |
|||
oracleOptionsAction?.Invoke(optionsBuilder); |
|||
}); |
|||
} |
|||
else |
|||
{ |
|||
return context.DbContextOptions.UseOracle(context.ConnectionString, optionsBuilder => |
|||
{ |
|||
optionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); |
|||
oracleOptionsAction?.Invoke(optionsBuilder); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1,30 +1,30 @@ |
|||
// using JetBrains.Annotations;
|
|||
// using System;
|
|||
// using Oracle.EntityFrameworkCore.Infrastructure;
|
|||
//
|
|||
// namespace Volo.Abp.EntityFrameworkCore
|
|||
// {
|
|||
// public static class AbpDbContextOptionsOracleExtensions
|
|||
// {
|
|||
// public static void UseOracle(
|
|||
// [NotNull] this AbpDbContextOptions options,
|
|||
// [CanBeNull] Action<OracleDbContextOptionsBuilder> oracleOptionsAction = null)
|
|||
// {
|
|||
// options.Configure(context =>
|
|||
// {
|
|||
// context.UseOracle(oracleOptionsAction);
|
|||
// });
|
|||
// }
|
|||
//
|
|||
// public static void UseOracle<TDbContext>(
|
|||
// [NotNull] this AbpDbContextOptions options,
|
|||
// [CanBeNull] Action<OracleDbContextOptionsBuilder> oracleOptionsAction = null)
|
|||
// where TDbContext : AbpDbContext<TDbContext>
|
|||
// {
|
|||
// options.Configure<TDbContext>(context =>
|
|||
// {
|
|||
// context.UseOracle(oracleOptionsAction);
|
|||
// });
|
|||
// }
|
|||
// }
|
|||
// }
|
|||
using JetBrains.Annotations; |
|||
using System; |
|||
using Oracle.EntityFrameworkCore.Infrastructure; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore |
|||
{ |
|||
public static class AbpDbContextOptionsOracleExtensions |
|||
{ |
|||
public static void UseOracle( |
|||
[NotNull] this AbpDbContextOptions options, |
|||
[CanBeNull] Action<OracleDbContextOptionsBuilder> oracleOptionsAction = null) |
|||
{ |
|||
options.Configure(context => |
|||
{ |
|||
context.UseOracle(oracleOptionsAction); |
|||
}); |
|||
} |
|||
|
|||
public static void UseOracle<TDbContext>( |
|||
[NotNull] this AbpDbContextOptions options, |
|||
[CanBeNull] Action<OracleDbContextOptionsBuilder> oracleOptionsAction = null) |
|||
where TDbContext : AbpDbContext<TDbContext> |
|||
{ |
|||
options.Configure<TDbContext>(context => |
|||
{ |
|||
context.UseOracle(oracleOptionsAction); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
|
|||