diff --git a/docs/en/Best-Practices/Entity-Framework-Core-Integration.md b/docs/en/Best-Practices/Entity-Framework-Core-Integration.md index 04ddeaeba4..4db6518201 100644 --- a/docs/en/Best-Practices/Entity-Framework-Core-Integration.md +++ b/docs/en/Best-Practices/Entity-Framework-Core-Integration.md @@ -16,11 +16,13 @@ [ConnectionStringName("AbpIdentity")] public interface IIdentityDbContext : IEfCoreDbContext { - DbSet Users { get; set; } - DbSet Roles { get; set; } + DbSet Users { get; } + DbSet Roles { get; } } ```` +* **Do not** define `set;` for the properties in this interface. + ### DbContext class * **Do** inherit the `DbContext` from the `AbpDbContext` class. diff --git a/docs/en/Blob-Storing-Aliyun.md b/docs/en/Blob-Storing-Aliyun.md index 6cdebb3acc..4060e5ba26 100644 --- a/docs/en/Blob-Storing-Aliyun.md +++ b/docs/en/Blob-Storing-Aliyun.md @@ -36,7 +36,7 @@ Configure(options => aliyun.Policy = "policy"; aliyun.DurationSeconds = "expiration date"; aliyun.ContainerName = "your aliyun container name"; - aliyun.CreateContainerIfNotExists = false; + aliyun.CreateContainerIfNotExists = true; }); }); }); diff --git a/docs/en/Blob-Storing-Aws.md b/docs/en/Blob-Storing-Aws.md index 05e3caa183..aa7707e5d3 100644 --- a/docs/en/Blob-Storing-Aws.md +++ b/docs/en/Blob-Storing-Aws.md @@ -39,7 +39,7 @@ Configure(options => Aws.Policy = "policy"; Aws.DurationSeconds = "expiration date"; Aws.ContainerName = "your Aws container name"; - Aws.CreateContainerIfNotExists = false; + Aws.CreateContainerIfNotExists = true; }); }); }); diff --git a/docs/en/Blob-Storing-Azure.md b/docs/en/Blob-Storing-Azure.md index de6ed63152..99833e71c0 100644 --- a/docs/en/Blob-Storing-Azure.md +++ b/docs/en/Blob-Storing-Azure.md @@ -29,7 +29,7 @@ Configure(options => { azure.ConnectionString = "your azure connection string"; azure.ContainerName = "your azure container name"; - azure.CreateContainerIfNotExists = false; + azure.CreateContainerIfNotExists = true; }); }); }); diff --git a/docs/en/CLI.md b/docs/en/CLI.md index fc76aa997b..d4a33a2a12 100644 --- a/docs/en/CLI.md +++ b/docs/en/CLI.md @@ -31,6 +31,7 @@ Here, the list of all available commands before explaining their details: * **`update`**: Automatically updates all ABP related NuGet and NPM packages in a solution. * **`add-package`**: Adds an ABP package to a project. * **`add-module`**: Adds a [multi-package application module](https://docs.abp.io/en/abp/latest/Modules/Index) to a solution. +* **`list-modules`**: Lists names of open-source application modules. * **`get-source`**: Downloads the source code of a module. * **`generate-proxy`**: Generates client side proxies to use HTTP API endpoints. * **`remove-proxy`**: Removes previously generated client side proxies. @@ -202,6 +203,26 @@ abp add-module ProductManagement --new --add-to-solution-file * `--with-source-code`: Downloads the source code of the module to your solution folder and uses local project references instead of NuGet/NPM packages. This options is always `True` if `--new` is used. * `--add-to-solution-file`: Adds the downloaded/created module to your solution file, so you will also see the projects of the module when you open the solution on a IDE. (only available when `--with-source-code` is `True`.) +### list-modules + +Lists names of open-source application modules. + +Usage + +````bash +abp list-modules [options] +```` + +Example: + +```bash +abp list-modules +``` + +#### Options + +* `--include-pro-modules`: Includes commercial (pro) modules in the output. + ### get-source Downloads the source code of a module to your computer. diff --git a/docs/en/Customizing-Application-Modules-Overriding-Services.md b/docs/en/Customizing-Application-Modules-Overriding-Services.md index ab6f29816b..081b142ad7 100644 --- a/docs/en/Customizing-Application-Modules-Overriding-Services.md +++ b/docs/en/Customizing-Application-Modules-Overriding-Services.md @@ -196,12 +196,16 @@ This example replaces the `AccountController` (An API Controller defined in the **`[ExposeServices(typeof(AccountController))]` is essential** here since it registers this controller for the `AccountController` in the dependency injection system. `[Dependency(ReplaceServices = true)]` is also recommended to clear the old registration (even the ASP.NET Core DI system selects the last registered one). -In addition, The `AccountController` will be removed from [`ApplicationModel`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.applicationmodel.controllers) because it defines `ExposeServicesAttribute`. If you don't want to remove it, you can configure `AbpAspNetCoreMvcOptions`: +In addition, the `MyAccountController` will be removed from [`ApplicationModel`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.applicationmodel.controllers) because it defines `ExposeServicesAttribute`. + +If `IncludeSelf = true` is specified, i.e. `[ExposeServices(typeof(AccountController), IncludeSelf = true)]`, then `AccountController` will be removed instead. This is useful for **extending** a controller. + +If you don't want to remove either controller, you can configure `AbpAspNetCoreMvcOptions`: ```csharp Configure(options => { - options.IgnoredControllersOnModelExclusion.AddIfNotContains(typeof(AccountController)); + options.IgnoredControllersOnModelExclusion.AddIfNotContains(typeof(MyAccountController)); }); ``` diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md index 7bc42b1fc9..627b55a476 100644 --- a/docs/en/Tutorials/Part-2.md +++ b/docs/en/Tutorials/Part-2.md @@ -103,7 +103,7 @@ Before starting to the UI development, we first want to prepare the localization Localization texts are located under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project: -![bookstore-localization-files](./images/bookstore-localization-files-v2.png) +![bookstore-localization-files](images/bookstore-localization-files-v2.png) Open the `en.json` (*the English translations*) file and change the content as below: @@ -157,7 +157,7 @@ It's time to create something visible and usable! Instead of classic MVC, we wil Create `Books` folder under the `Pages` folder of the `Acme.BookStore.Web` project. Add a new Razor Page by right clicking the Books folder then selecting **Add > Razor Page** menu item. Name it as `Index`: -![bookstore-add-index-page](./images/bookstore-add-index-page-v2.png) +![bookstore-add-index-page](images/bookstore-add-index-page-v2.png) Open the `Index.cshtml` and change the whole content as shown below: @@ -208,7 +208,7 @@ context.Menu.AddItem( Run the project, login to the application with the username `admin` and the password `1q2w3E*` and see the new menu item has been added to the main menu: -![bookstore-menu-items](./images/bookstore-new-menu-item.png) +![bookstore-menu-items](images/bookstore-new-menu-item.png) When you click to the Books menu item under the Book Store parent, you are being redirected to the new empty Books Page. @@ -250,7 +250,7 @@ Change the `Pages/Books/Index.cshtml` as following: Create an `Index.js` file under the `Pages/Books` folder: -![bookstore-index-js-file](./images/bookstore-index-js-file-v3.png) +![bookstore-index-js-file](images/bookstore-index-js-file-v3.png) The content of the file is shown below: @@ -344,8 +344,6 @@ It's time to create something visible and usable! There are some tools that we w - [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) will be used as the UI component library. - [Ngx-Datatable](https://swimlane.gitbook.io/ngx-datatable/) will be used as the datatable library. -### BookModule - Run the following command line to create a new module, named `BookModule` in the root folder of the angular application: ```bash @@ -457,7 +455,7 @@ abp generate-proxy This command will create the following files under the `/src/app/proxy/books` folder: -![Generated files](./images/generated-proxies-3.png) +![Generated files](images/generated-proxies-3.png) ### BookComponent @@ -490,7 +488,7 @@ export class BookComponent implements OnInit { ``` * We imported and injected the generated `BookService`. -* We are using the [ListService](https://docs.abp.io/en/abp/latest/UI/Angular/List-Service), a utility service of the ABP Framework which provides easy pagination, sorting and searching. +* We are using the [ListService](../UI/Angular/List-Service.md), a utility service of the ABP Framework which provides easy pagination, sorting and searching. Open the `/src/app/book/book.component.html` and replace the content as below: @@ -531,7 +529,7 @@ Open the `/src/app/book/book.component.html` and replace the content as below: Now you can see the final result on your browser: -![Book list final result](./images/bookstore-book-list.png) +![Book list final result](images/bookstore-book-list.png) {{else if UI == "Blazor"}} diff --git a/docs/en/Tutorials/Part-3.md b/docs/en/Tutorials/Part-3.md index e58857ec89..ec326ddb65 100644 --- a/docs/en/Tutorials/Part-3.md +++ b/docs/en/Tutorials/Part-3.md @@ -48,13 +48,13 @@ This part is also recorded as a video tutorial and ** Localization key names are arbitrary and no forcing rule. But we prefer the convention used above. +> Localization key names are arbitrary and there is no forcing rule. But we prefer the convention used above. ### Permission Management UI diff --git a/docs/en/Tutorials/Part-6.md b/docs/en/Tutorials/Part-6.md index 69c6574520..d5df8133b7 100644 --- a/docs/en/Tutorials/Part-6.md +++ b/docs/en/Tutorials/Part-6.md @@ -124,7 +124,7 @@ Created this class inside the `Acme.BookStore.Domain.Shared` project since we wi ## AuthorManager: The Domain Service -`Author` constructor and `ChangeName` method is `internal`, so they can be usable only in the domain layer. Create an `AuthorManager` class in the `Authors` folder (namespace) of the `Acme.BookStore.Domain` project: +`Author` constructor and `ChangeName` methods are `internal`, so they can be used only in the domain layer. Create an `AuthorManager` class in the `Authors` folder (namespace) of the `Acme.BookStore.Domain` project: ````csharp using System; @@ -186,7 +186,7 @@ namespace Acme.BookStore.Authors * `AuthorManager` forces to create an author and change name of an author in a controlled way. The application layer (will be introduced later) will use these methods. -> **DDD tip**: Do not introduce domain service methods unless they are really needed and perform some core business rules. For this case, we needed to this service to be able to force the unique name constraint. +> **DDD tip**: Do not introduce domain service methods unless they are really needed and perform some core business rules. For this case, we needed this service to be able to force the unique name constraint. Both methods checks if there is already an author with the given name and throws a special business exception, `AuthorAlreadyExistsException`, defined in the `Acme.BookStore.Domain` project (in the `Authors` folder) as shown below: @@ -226,7 +226,7 @@ This is a unique string represents the error code thrown by your application and "BookStore:00001": "There is already an author with the same name: {name}" ```` -Whenever you throw an `AuthorAlreadyExistsException`, the end use will see a this message on the UI. +Whenever you throw an `AuthorAlreadyExistsException`, the end user will see a nice error message on the UI. ## IAuthorRepository diff --git a/docs/en/UI/Angular/Component-Replacement.md b/docs/en/UI/Angular/Component-Replacement.md index 54a1869c0e..0010a30a2f 100644 --- a/docs/en/UI/Angular/Component-Replacement.md +++ b/docs/en/UI/Angular/Component-Replacement.md @@ -546,4 +546,5 @@ The final UI looks like below: ## See Also +- [How Replaceable Components Work with Extensions](./How-Replaceable-Components-Work-with-Extensions.md) - [How to Replace PermissionManagementComponent](./Permission-Management-Component-Replacement.md) diff --git a/docs/en/UI/Angular/How-Replaceable-Components-Work-with-Extensions.md b/docs/en/UI/Angular/How-Replaceable-Components-Work-with-Extensions.md new file mode 100644 index 0000000000..d942975646 --- /dev/null +++ b/docs/en/UI/Angular/How-Replaceable-Components-Work-with-Extensions.md @@ -0,0 +1,288 @@ +# How Replaceable Components Work with Extensions + +Additional UI extensibility points ([Entity action extensions](https://docs.abp.io/en/abp/latest/UI/Angular/Entity-Action-Extensions), [data table column extensions](https://docs.abp.io/en/abp/latest/UI/Angular/Data-Table-Column-Extensions), [page toolbar extensions](https://docs.abp.io/en/abp/latest/UI/Angular/Page-Toolbar-Extensions) and others) are used in ABP pages to allow to control entity actions, table columns and page toolbar of a page. If you replace a page, you need to apply some configurations to be able to work extension components in your component. Let's see how to do this by replacing the roles page. + +Create a new module called `MyRolesModule`: + +```bash +yarn ng generate module my-roles --module app +``` + +Create a new component called `MyRolesComponent`: + +```bash +yarn ng generate component my-roles/my-roles --flat --export +``` + +Open the generated `src/app/my-roles/my-roles.component.ts` file and replace its content with the following: + +```js +import { ListService, PagedAndSortedResultRequestDto } from '@abp/ng.core'; +import { + CreateRole, + DeleteRole, + eIdentityComponents, + GetRoleById, + GetRoles, + IdentityRoleDto, + IdentityState, + RolesComponent, + UpdateRole, +} from '@abp/ng.identity'; +import { ePermissionManagementComponents } from '@abp/ng.permission-management'; +import { Confirmation, ConfirmationService } from '@abp/ng.theme.shared'; +import { + EXTENSIONS_IDENTIFIER, + FormPropData, + generateFormFromProps, +} from '@abp/ng.theme.shared/extensions'; +import { Component, Injector, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { Select, Store } from '@ngxs/store'; +import { Observable } from 'rxjs'; +import { finalize, pluck } from 'rxjs/operators'; + +@Component({ + selector: 'app-my-roles', + templateUrl: './my-roles.component.html', + providers: [ + ListService, + { + provide: EXTENSIONS_IDENTIFIER, + useValue: eIdentityComponents.Roles, + }, + { provide: RolesComponent, useExisting: MyRolesComponent }, + ], +}) +export class MyRolesComponent implements OnInit { + @Select(IdentityState.getRoles) + data$: Observable; + + @Select(IdentityState.getRolesTotalCount) + totalCount$: Observable; + + form: FormGroup; + + selected: IdentityRoleDto; + + isModalVisible: boolean; + + visiblePermissions = false; + + providerKey: string; + + modalBusy = false; + + permissionManagementKey = ePermissionManagementComponents.PermissionManagement; + + onVisiblePermissionChange = event => { + this.visiblePermissions = event; + }; + + constructor( + public readonly list: ListService, + protected confirmationService: ConfirmationService, + protected store: Store, + protected injector: Injector + ) {} + + ngOnInit() { + this.hookToQuery(); + } + + buildForm() { + const data = new FormPropData(this.injector, this.selected); + this.form = generateFormFromProps(data); + } + + openModal() { + this.buildForm(); + this.isModalVisible = true; + } + + add() { + this.selected = {} as IdentityRoleDto; + this.openModal(); + } + + edit(id: string) { + this.store + .dispatch(new GetRoleById(id)) + .pipe(pluck('IdentityState', 'selectedRole')) + .subscribe(selectedRole => { + this.selected = selectedRole; + this.openModal(); + }); + } + + save() { + if (!this.form.valid) return; + this.modalBusy = true; + + this.store + .dispatch( + this.selected.id + ? new UpdateRole({ ...this.selected, ...this.form.value, id: this.selected.id }) + : new CreateRole(this.form.value) + ) + .pipe(finalize(() => (this.modalBusy = false))) + .subscribe(() => { + this.isModalVisible = false; + this.list.get(); + }); + } + + delete(id: string, name: string) { + this.confirmationService + .warn('AbpIdentity::RoleDeletionConfirmationMessage', 'AbpIdentity::AreYouSure', { + messageLocalizationParams: [name], + }) + .subscribe((status: Confirmation.Status) => { + if (status === Confirmation.Status.confirm) { + this.store.dispatch(new DeleteRole(id)).subscribe(() => this.list.get()); + } + }); + } + + private hookToQuery() { + this.list.hookToQuery(query => this.store.dispatch(new GetRoles(query))).subscribe(); + } + + openPermissionsModal(providerKey: string) { + this.providerKey = providerKey; + setTimeout(() => { + this.visiblePermissions = true; + }, 0); + } + + sort(data) { + const { prop, dir } = data.sorts[0]; + this.list.sortKey = prop; + this.list.sortOrder = dir; + } +} +``` + +```js + { + provide: EXTENSIONS_IDENTIFIER, + useValue: eIdentityComponents.Roles, + }, + { + provide: RolesComponent, + useExisting: MyRolesComponent + } +``` + +The two providers we have defined in `MyRolesComponent` are required for the extension components to work correctly. + +* With the first provider, we defined the extension identifier for using `RolesComponent`'s extension actions in the `MyRolesComponent`. +* With the second provider, we have replaced the `RolesComponent` injection with the `MyRolesComponent`. Default extension actions of the `RolesComponent` try to get `RolesComponent` instance. However, the actions can get the `MyRolesComponent` instance after defining the second provider. + +Open the generated `src/app/my-role/my-role.component.html` file and replace its content with the following: + +```html +
+
+
+
+
My Roles
+
+
+ +
+
+
+ +
+ +
+
+ + + +

{%{{{ (selected?.id ? 'AbpIdentity::Edit' : 'AbpIdentity::NewRole') | abpLocalization }}}%}

+
+ + +
+ +
+
+ + + + {%{{{ + 'AbpIdentity::Save' | abpLocalization + }}}%} + +
+ + + +``` + +We have added the `abp-page-toolbar`, `abp-extensible-table`, and `abp-extensible-form` extension components to template of the `MyRolesComponent`. + +You should import the required modules for the `MyRolesComponent` to `MyRolesModule`. Open the `src/my-roles/my-roles.module.ts` file and replace the content with the following: + +```js +import { UiExtensionsModule } from '@abp/ng.theme.shared/extensions'; +import { NgModule } from '@angular/core'; +import { SharedModule } from '../shared/shared.module'; +import { MyRolesComponent } from './my-roles.component'; +import { PermissionManagementModule } from '@abp/ng.permission-management'; + +@NgModule({ + declarations: [MyRolesComponent], + imports: [SharedModule, UiExtensionsModule, PermissionManagementModule], + exports: [MyRolesComponent], +}) +export class MyRolesModule {} +``` + +- `UiExtensionsModule` imported to be able to use the extension components in your component. +- `PermissionManagementModule` imported to be able to use the `abp-permission-*management` in your component. + +As the last step, it is needs to be replaced the `RolesComponent` with the `MyRolesComponent`. Open the `app.component.ts` and modify its content as shown below: + +```js +import { ReplaceableComponentsService } from '@abp/ng.core'; +import { eIdentityComponents } from '@abp/ng.identity'; +import { MyRolesComponent } from './my-roles/my-roles.component'; + +@Component(/* component metadata */) +export class AppComponent { + constructor(private replaceableComponents: ReplaceableComponentsService) { + this.replaceableComponents.add({ component: MyRolesComponent, key: eIdentityComponents.Roles }); + } +} +``` + +After the steps above, the `RolesComponent` has been successfully replaced with the `MyRolesComponent`. When you navigate to the `/identity/roles` URL, you will see the `MyRolesComponent`'s template and see the extension components working correctly. + +![my-roles-component-with-extensions](./images/my-roles-component-with-extensions.jpg) + +![my-roles-component-form-extensions](./images/my-roles-component-form-extensions.jpg) diff --git a/docs/en/UI/Angular/images/my-roles-component-form-extensions.jpg b/docs/en/UI/Angular/images/my-roles-component-form-extensions.jpg new file mode 100644 index 0000000000..a686d26edc Binary files /dev/null and b/docs/en/UI/Angular/images/my-roles-component-form-extensions.jpg differ diff --git a/docs/en/UI/Angular/images/my-roles-component-with-extensions.jpg b/docs/en/UI/Angular/images/my-roles-component-with-extensions.jpg new file mode 100644 index 0000000000..1ac82c40b7 Binary files /dev/null and b/docs/en/UI/Angular/images/my-roles-component-with-extensions.jpg differ diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 7fd7762ad4..efca73a484 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -859,6 +859,27 @@ { "text": "Custom Setting Page", "path": "UI/Angular/Custom-Setting-Page.md" + }, + { + "text": "Extensions", + "items": [ + { + "text": "Entity Action Extensions", + "path": "UI/Angular/Entity-Action-Extensions.md" + }, + { + "text": "Data Table Column Extensions", + "path": "UI/Angular/Data-Table-Column-Extensions.md" + }, + { + "text": "Page Toolbar Extensions", + "path": "UI/Angular/Page-Toolbar-Extensions.md" + }, + { + "text": "Dynamic Form Extensions", + "path": "UI/Angular/Dynamic-Form-Extensions.md" + } + ] } ] } diff --git a/docs/zh-Hans/Blob-Storing-Aliyun.md b/docs/zh-Hans/Blob-Storing-Aliyun.md index c8a1e68bc7..eb67fdbab5 100644 --- a/docs/zh-Hans/Blob-Storing-Aliyun.md +++ b/docs/zh-Hans/Blob-Storing-Aliyun.md @@ -36,7 +36,7 @@ Configure(options => aliyun.Policy = "policy"; aliyun.DurationSeconds = "expiration date"; aliyun.ContainerName = "your aliyun container name"; - aliyun.CreateContainerIfNotExists = false; + aliyun.CreateContainerIfNotExists = true; }); }); }); diff --git a/docs/zh-Hans/Blob-Storing-Aws.md b/docs/zh-Hans/Blob-Storing-Aws.md index 7bfb998297..fc98c5a948 100644 --- a/docs/zh-Hans/Blob-Storing-Aws.md +++ b/docs/zh-Hans/Blob-Storing-Aws.md @@ -39,7 +39,7 @@ Configure(options => Aws.Policy = "policy"; Aws.DurationSeconds = "expiration date"; Aws.ContainerName = "your Aws container name"; - Aws.CreateContainerIfNotExists = false; + Aws.CreateContainerIfNotExists = true; }); }); }); diff --git a/docs/zh-Hans/Blob-Storing-Azure.md b/docs/zh-Hans/Blob-Storing-Azure.md index 8eecc1fde3..e1ed3a6d4e 100644 --- a/docs/zh-Hans/Blob-Storing-Azure.md +++ b/docs/zh-Hans/Blob-Storing-Azure.md @@ -29,7 +29,7 @@ Configure(options => { azure.ConnectionString = "your azure connection string"; azure.ContainerName = "your azure container name"; - azure.CreateContainerIfNotExists = false; + azure.CreateContainerIfNotExists = true; }); }); }); diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index 8c2670fa27..d1288de62d 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -361,6 +361,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Http.Tests", "test EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.EventBus.Abstractions", "src\Volo.Abp.EventBus.Abstractions\Volo.Abp.EventBus.Abstractions.csproj", "{8FDB3BF7-AD89-43F6-8DEB-C3E29B8801FE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Sms.Aliyun", "src\Volo.Abp.Sms.Aliyun\Volo.Abp.Sms.Aliyun.csproj", "{ACFBA3FB-18CE-4655-9D14-1F1F5C3DFC30}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Sms.Aliyun.Tests", "test\Volo.Abp.Sms.Aliyun.Tests\Volo.Abp.Sms.Aliyun.Tests.csproj", "{DADEA538-3CA1-4ADE-A7E6-EF77A0CE4401}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1075,6 +1079,14 @@ Global {8FDB3BF7-AD89-43F6-8DEB-C3E29B8801FE}.Debug|Any CPU.Build.0 = Debug|Any CPU {8FDB3BF7-AD89-43F6-8DEB-C3E29B8801FE}.Release|Any CPU.ActiveCfg = Release|Any CPU {8FDB3BF7-AD89-43F6-8DEB-C3E29B8801FE}.Release|Any CPU.Build.0 = Release|Any CPU + {ACFBA3FB-18CE-4655-9D14-1F1F5C3DFC30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ACFBA3FB-18CE-4655-9D14-1F1F5C3DFC30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ACFBA3FB-18CE-4655-9D14-1F1F5C3DFC30}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ACFBA3FB-18CE-4655-9D14-1F1F5C3DFC30}.Release|Any CPU.Build.0 = Release|Any CPU + {DADEA538-3CA1-4ADE-A7E6-EF77A0CE4401}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DADEA538-3CA1-4ADE-A7E6-EF77A0CE4401}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DADEA538-3CA1-4ADE-A7E6-EF77A0CE4401}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DADEA538-3CA1-4ADE-A7E6-EF77A0CE4401}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1257,6 +1269,8 @@ Global {00D07595-993C-40FC-BD90-0DD6331414D3} = {447C8A77-E5F0-4538-8687-7383196D04EA} {A37BFEB5-7C57-4CDC-93B8-B5CE4BB9ACE1} = {447C8A77-E5F0-4538-8687-7383196D04EA} {8FDB3BF7-AD89-43F6-8DEB-C3E29B8801FE} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {ACFBA3FB-18CE-4655-9D14-1F1F5C3DFC30} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {DADEA538-3CA1-4ADE-A7E6-EF77A0CE4401} = {447C8A77-E5F0-4538-8687-7383196D04EA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor index 18ebfbd4ba..eebfb501a5 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor @@ -1,11 +1,14 @@ @using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using Microsoft.Extensions.Localization @using Volo.Abp.Users @using Volo.Abp.MultiTenancy +@using global::Localization.Resources.AbpUi @inject ICurrentUser CurrentUser @inject ICurrentTenant CurrentTenant @inject IJSRuntime JsRuntime @inject NavigationManager Navigation @inject SignOutSessionStateManager SignOutManager +@inject IStringLocalizer UiLocalizer @@ -28,12 +31,12 @@ } } - Logout + @UiLocalizer["Logout"] -
Log in + @UiLocalizer["Login"] @code{ diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs index 135b506e69..ff7f4913a8 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs @@ -84,7 +84,6 @@ namespace Volo.Abp.AspNetCore.Mvc ); var controllerModel = moduleModel.GetOrAddController( - controllerType.FullName, _options.ControllerNameGenerator(controllerType, setting), controllerType, _modelOptions.IgnoredInterfaces @@ -272,6 +271,7 @@ namespace Volo.Abp.AspNetCore.Mvc actionModel.AddParameter(ParameterApiDescriptionModel.Create( parameterDescription.Name, + _options.ApiParameterNameGenerator?.Invoke(parameterDescription), matchedMethodParamName, parameterDescription.Type, parameterDescription.RouteInfo?.IsOptional ?? false, diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProviderOptions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProviderOptions.cs index 4cd607e07a..f807efcfdf 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProviderOptions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProviderOptions.cs @@ -2,6 +2,9 @@ using System.Linq; using System.Reflection; using System.Text; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Volo.Abp.Application.Services; using Volo.Abp.AspNetCore.Mvc.Conventions; @@ -13,6 +16,8 @@ namespace Volo.Abp.AspNetCore.Mvc public Func ActionNameGenerator { get; set; } + public Func ApiParameterNameGenerator { get; set; } + public AspNetCoreApiDescriptionModelProviderOptions() { ControllerNameGenerator = (controllerType, setting) => @@ -52,6 +57,28 @@ namespace Volo.Abp.AspNetCore.Mvc return methodNameBuilder.ToString(); }; + + ApiParameterNameGenerator = (apiParameterDescription) => + { + if (apiParameterDescription.ModelMetadata is DefaultModelMetadata defaultModelMetadata) + { + var jsonPropertyNameAttribute = (System.Text.Json.Serialization.JsonPropertyNameAttribute) + defaultModelMetadata?.Attributes?.PropertyAttributes?.FirstOrDefault(x => x is System.Text.Json.Serialization.JsonPropertyNameAttribute); + if (jsonPropertyNameAttribute != null) + { + return jsonPropertyNameAttribute.Name; + } + + var jsonPropertyAttribute = (Newtonsoft.Json.JsonPropertyAttribute) + defaultModelMetadata?.Attributes?.PropertyAttributes?.FirstOrDefault(x => x is Newtonsoft.Json.JsonPropertyAttribute); + if (jsonPropertyAttribute != null) + { + return jsonPropertyAttribute.PropertyName; + } + } + + return null; + }; } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs index 4d1490ec40..996e495588 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs @@ -73,7 +73,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions protected virtual void RemoveDuplicateControllers(ApplicationModel application) { - var derivedControllerModels = new List(); + var controllerModelsToRemove = new List(); foreach (var controllerModel in application.Controllers) { @@ -87,19 +87,42 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions continue; } + var exposeServicesAttr = ReflectionHelper.GetSingleAttributeOrDefault(controllerModel.ControllerType); + if (exposeServicesAttr.IncludeSelf) + { + var exposedControllerModels = application.Controllers + .Where(cm => exposeServicesAttr.ServiceTypes.Contains(cm.ControllerType)) + .ToArray(); + + controllerModelsToRemove.AddRange(exposedControllerModels); + Logger.LogInformation($"Removing the controller{(exposedControllerModels.Length > 1 ? "s" : "")} {exposeServicesAttr.ServiceTypes.Select(c => c.AssemblyQualifiedName).JoinAsString(", ")} from the application model since {(exposedControllerModels.Length > 1 ? "they are" : "it is")} replaced by the controller: {controllerModel.ControllerType.AssemblyQualifiedName}"); + continue; + } + var baseControllerTypes = controllerModel.ControllerType .GetBaseClasses(typeof(Controller), includeObject: false) .Where(t => !t.IsAbstract) .ToArray(); - if (baseControllerTypes.Length > 0) + if (baseControllerTypes.Length == 0) { - derivedControllerModels.Add(controllerModel); - Logger.LogInformation($"Removing the controller {controllerModel.ControllerType.AssemblyQualifiedName} from the application model since it replaces the controller(s): {baseControllerTypes.Select(c => c.AssemblyQualifiedName).JoinAsString(", ")}"); + continue; } + + var baseControllerModels = application.Controllers + .Where(cm => baseControllerTypes.Contains(cm.ControllerType)) + .ToArray(); + + if (baseControllerModels.Length == 0) + { + continue; + } + + controllerModelsToRemove.Add(controllerModel); + Logger.LogInformation($"Removing the controller {controllerModel.ControllerType.AssemblyQualifiedName} from the application model since it replaces the controller(s): {baseControllerTypes.Select(c => c.AssemblyQualifiedName).JoinAsString(", ")}"); } - application.Controllers.RemoveAll(derivedControllerModels); + application.Controllers.RemoveAll(controllerModelsToRemove); } protected virtual void ConfigureRemoteService(ControllerModel controller, [CanBeNull] ConventionalControllerSetting configuration) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowActionFilter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowActionFilter.cs index 95a961b814..0512e28f7a 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowActionFilter.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowActionFilter.cs @@ -50,7 +50,6 @@ namespace Volo.Abp.AspNetCore.Mvc.Uow return; } - //Begin a new, independent unit of work using (var uow = unitOfWorkManager.Begin(options)) { var result = await next(); @@ -58,6 +57,10 @@ namespace Volo.Abp.AspNetCore.Mvc.Uow { await uow.CompleteAsync(context.HttpContext.RequestAborted); } + else + { + await uow.RollbackAsync(context.HttpContext.RequestAborted); + } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowPageFilter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowPageFilter.cs index 30c713940f..d808053592 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowPageFilter.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowPageFilter.cs @@ -55,7 +55,6 @@ namespace Volo.Abp.AspNetCore.Mvc.Uow return; } - //Begin a new, independent unit of work using (var uow = unitOfWorkManager.Begin(options)) { var result = await next(); @@ -63,6 +62,10 @@ namespace Volo.Abp.AspNetCore.Mvc.Uow { await uow.CompleteAsync(context.HttpContext.RequestAborted); } + else + { + await uow.RollbackAsync(context.HttpContext.RequestAborted); + } } } diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs index 568ecfef7b..a409355ef0 100644 --- a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Hosting; using Volo.Abp; using Volo.Abp.AspNetCore.Auditing; using Volo.Abp.AspNetCore.ExceptionHandling; +using Volo.Abp.AspNetCore.Security; using Volo.Abp.AspNetCore.Security.Claims; using Volo.Abp.AspNetCore.Tracing; using Volo.Abp.AspNetCore.Uow; @@ -82,5 +83,10 @@ namespace Microsoft.AspNetCore.Builder { return app.UseMiddleware(); } + + public static IApplicationBuilder UseAbpSecurityHeaders(this IApplicationBuilder app) + { + return app.UseMiddleware(); + } } } diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Security/AbpSecurityHeadersMiddleware.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Security/AbpSecurityHeadersMiddleware.cs new file mode 100644 index 0000000000..28b0f3a48e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Security/AbpSecurityHeadersMiddleware.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Security +{ + public class AbpSecurityHeadersMiddleware : IMiddleware, ITransientDependency + { + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + /*X-Content-Type-Options header tells the browser to not try and “guess” what a mimetype of a resource might be, and to just take what mimetype the server has returned as fact.*/ + AddHeaderIfNotExists(context, "X-Content-Type-Options", "nosniff"); + + /*X-XSS-Protection is a feature of Internet Explorer, Chrome and Safari that stops pages from loading when they detect reflected cross-site scripting (XSS) attacks*/ + AddHeaderIfNotExists(context, "X-XSS-Protection", "1; mode=block"); + + /*The X-Frame-Options HTTP response header can be used to indicate whether or not a browser should be allowed to render a page in a ,