diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000000..e33a07a197 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,30 @@ +name: "Main" +on: + pull_request: + paths: + - "framework/**" + - "modules/**" + - "templates/**" + push: + paths: + - "framework/**" + - "modules/**" + - "templates/**" +jobs: + build-test: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-dotnet@master + with: + dotnet-version: 3.1.100 + + - name: Build All + run: .\build-all.ps1 + working-directory: .\build + shell: powershell + + - name: Test All + run: .\test-all.ps1 + working-directory: .\build + shell: powershell diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/ru.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/ru.json new file mode 100644 index 0000000000..a39240b02e --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/ru.json @@ -0,0 +1,13 @@ +{ + "culture": "ru", + "texts": { + "Account": "Аккаунт", + "Welcome": "Добро пожаловать", + "UseOneOfTheFollowingLinksToContinue": "Для продолжения используйте одну из следующих ссылок", + "FrameworkHomePage": "Главная страница фреймворка", + "FrameworkDocumentation": "Документация фреймворка", + "OfficialBlog": "Официальный блог", + "CommercialHomePage": "Главная страница коммерческой версии", + "CommercialSupportWebSite": "Сайт коммерческой поддержки" + } +} diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 2a27cce735..a5089e06f7 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -14,6 +14,7 @@ "Permission:Delete": "Delete", "Permission:Create": "Create", "Menu:Organizations": "Organizations", + "Menu:Accounting": "Accounting", "Menu:Packages": "Packages", "NpmPackageDeletionWarningMessage": "This NPM Package will be deleted. Do you confirm that?", "NugetPackageDeletionWarningMessage": "This Nuget Package will be deleted. Do you confirm that?", @@ -75,7 +76,8 @@ "AddDeveloper": "Add developer", "Create": "Create", "UserNotFound": "User not found", - "{0}WillBeRemovedFromMembers": "{0} Will be removed from members", + "{0}WillBeRemovedFromDevelopers": "{0} Will be removed from developers, do you confirm?", + "{0}WillBeRemovedFromOwners": "{0} Will be removed from owners, do you confirm?", "Computers": "Computers", "UniqueComputerId": "Unique computer id", "LastSeenDate": "Last seen date", @@ -91,6 +93,14 @@ "OrganizationNamePlaceholder": "Organization name...", "UsernameOrEmail": "Username or email", "UsernameOrEmailPlaceholder": "Username or email...", - "Member": "Member" + "Member": "Member", + "QuotationPurchasedOrderNo": "Quotation Purchased Order No", + "QuotationTime": "Quotation Time", + "CompanyName": "Company Name", + "CompanyAddress": "Company Address", + "Price": "Price", + "ExtraText": "Extra Text", + "ExtraAmount": "Extra Amount", + "DownloadQuotation": "Download Quotation" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/ru.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/ru.json new file mode 100644 index 0000000000..38b4e728f8 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/ru.json @@ -0,0 +1,90 @@ +{ + "culture": "ru", + "texts": { + "Permission:Organizations": "Организации", + "Permission:Manage": "Управление организациями", + "Permission:NpmPackages": "Пакеты NPM", + "Permission:NugetPackages": "Пакеты NuGet", + "Permission:Maintenance": "Обслуживание", + "Permission:Maintain": "Обслуживать", + "Permission:ClearCaches": "Очистить кэш", + "Permission:Modules": "Модули", + "Permission:Packages": "Пакеты", + "Permission:Edit": "Редактировать", + "Permission:Delete": "Удалить", + "Permission:Create": "Создать", + "Menu:Organizations": "Организации", + "Menu:Packages": "Пакеты", + "NpmPackageDeletionWarningMessage": "Этот пакет NPM будет удален. Вы подтверждаете это?", + "NugetPackageDeletionWarningMessage": "Этот пакет NuGet будет удален. Вы подтверждаете это?", + "ModuleDeletionWarningMessage": "Этот модуль будет удален. Вы подтверждаете это?", + "Name": "Имя", + "DisplayName": "Отображаемое имя", + "ShortDescription": "Краткое описание", + "NameFilter": "Имя", + "CreationTime": "Время создания", + "IsPro": "Is pro", + "EfCoreConfigureMethodName": "Настроить имя метода", + "IsProFilter": "Is pro", + "ApplicationType": "Тип приложения", + "Target": "Цель", + "TargetFilter": "Цель", + "ModuleClass": "Класс модуля", + "NugetPackageTarget.DomainShared": "Domain Shared", + "NugetPackageTarget.Domain": "Domain", + "NugetPackageTarget.Application": "Application", + "NugetPackageTarget.ApplicationContracts": "Application Contracts", + "NugetPackageTarget.HttpApi": "Http Api", + "NugetPackageTarget.HttpApiClient": "Http Api Client", + "NugetPackageTarget.Web": "Web", + "NugetPackageTarget.EntityFrameworkCore": "DeleteAllEntityFramework Core", + "NugetPackageTarget.MongoDB": "MongoDB", + "Edit": "Редактировать", + "Delete": "Удалить", + "Refresh": "Обновить", + "NpmPackages": "NPM пакеты", + "NugetPackages": "NuGet пакеты", + "NpmPackageCount": "Количество пакетов NPM", + "NugetPackageCount": "Количество пакетов NuGet", + "Module": "Модули", + "ModuleInfo": "Информация о модуле", + "CreateANpmPackage": "Создать пакет NPM", + "CreateAModule": "Создать модуль", + "CreateANugetPackage": "Создать пакет NuGet", + "AddNew": "Добавить новый", + "PackageAlreadyExist{0}": "\"{0}\" пакет уже существует.", + "ModuleAlreadyExist{0}": "\"{0}\" модуль уже добавлен.", + "ClearCache": "Очистить кэш", + "SuccessfullyCleared": "Успешно очищено", + "Menu:NpmPackages": "Пакеты NPM", + "Menu:Modules": "Модули", + "Menu:Maintenance": "Поддержка", + "Menu:NugetPackages": "Пакеты NuGet", + "CreateAnOrganization": "Создать организацию", + "Organizations": "Организации", + "LongName": "Полное название", + "LicenseType": "Тип лицензии", + "LicenseStartTime": "Время начала действия лицензии", + "LicenseEndTime": "Время окончания действия лицензии", + "AllowedDeveloperCount": "Разрешенное количество разработчиков", + "UserNameOrEmailAddress": "Имя пользователя или адрес электронной почты", + "AddOwner": "Добавить владельца", + "UserName": "Имя пользователя", + "Email": "Электронная почта", + "Developers": "Разработчики", + "AddDeveloper": "Добавить разработчика", + "Create": "Создать", + "UserNotFound": "Пользователь не обнаружен", + "{0}WillBeRemovedFromMembers": "{0} будет удален из членов", + "Computers": "Компьютеры", + "UniqueComputerId": "Уникальный id компьютера", + "LastSeenDate": "Дата последнего визита", + "{0}Computer{1}WillBeRemovedFromRecords": "Компьютер {0} ({1}) будет удален из записей", + "OrganizationDeletionWarningMessage": "Организация будет удалена", + "This{0}AlreadyExistInThisOrganization": "{0} уже существует в данной организации", + "AreYouSureYouWantToDeleteAllComputers": "Вы уверены, что хотите удалить все компьютеры?", + "DeleteAll": "Удалить все", + "DoYouWantToCreateNewUser": "Вы хотите создать нового пользователя?", + "MasterModules": "Мастер модулей" + } +} diff --git a/common.props b/common.props index 8b0b8291c4..266f2fb3cf 100644 --- a/common.props +++ b/common.props @@ -1,7 +1,7 @@ latest - 2.5.0 + 2.6.2 $(NoWarn);CS1591 https://abp.io/assets/abp_nupkg.png https://abp.io diff --git a/docs/en/AutoMapper-Integration.md b/docs/en/AutoMapper-Integration.md deleted file mode 100644 index d197861f25..0000000000 --- a/docs/en/AutoMapper-Integration.md +++ /dev/null @@ -1,3 +0,0 @@ -## AutoMapper Integration - -TODO \ No newline at end of file diff --git a/docs/en/Best-Practices/Application-Services.md b/docs/en/Best-Practices/Application-Services.md index 0979304931..876105c214 100644 --- a/docs/en/Best-Practices/Application-Services.md +++ b/docs/en/Best-Practices/Application-Services.md @@ -17,17 +17,18 @@ ##### Basic DTO -**Do** define a **basic** DTO for an entity. +**Do** define a **basic** DTO for an aggregate root. -- Include all the **primitive properties** directly on the entity. - - Exception: Can **exclude** properties for **security** reasons (like User.Password). +- Include all the **primitive properties** directly on the aggregate root. + - Exception: Can **exclude** properties for **security** reasons (like `User.Password`). - Include all the **sub collections** of the entity where every item in the collection is a simple **relation DTO**. +- Inherit from one of the **extensible entity DTO** classes for aggregate roots (and entities implement the `IHasExtraProperties`). Example: ```c# [Serializable] -public class IssueDto : FullAuditedEntityDto +public class IssueDto : ExtensibleFullAuditedEntityDto { public string Title { get; set; } public string Text { get; set; } @@ -57,7 +58,7 @@ Example: ````C# [Serializable] -public class IssueWithDetailsDto : FullAuditedEntityDto +public class IssueWithDetailsDto : ExtensibleFullAuditedEntityDto { public string Title { get; set; } public string Text { get; set; } @@ -66,14 +67,14 @@ public class IssueWithDetailsDto : FullAuditedEntityDto } [Serializable] -public class MilestoneDto : EntityDto +public class MilestoneDto : ExtensibleEntityDto { public string Name { get; set; } public bool IsClosed { get; set; } } [Serializable] -public class LabelDto : EntityDto +public class LabelDto : ExtensibleEntityDto { public string Name { get; set; } public string Color { get; set; } @@ -120,6 +121,7 @@ Task> GetListAsync(QuestionListQueryDto queryDto); * **Do** use the `CreateAsync` **method name**. * **Do** get a **specialized input** DTO to create the entity. +* **Do** inherit the DTO class from the `ExtensibleObject` (or any other class implements the `IHasExtraProperties`) to allow to pass extra properties if needed. * **Do** use **data annotations** for input validation. * Share constants between domain wherever possible (via constants defined in the **domain shared** package). * **Do** return **the detailed** DTO for new created entity. @@ -135,10 +137,11 @@ The related **DTO**: ````C# [Serializable] -public class CreateQuestionDto +public class CreateQuestionDto : ExtensibleObject { [Required] - [StringLength(QuestionConsts.MaxTitleLength, MinimumLength = QuestionConsts.MinTitleLength)] + [StringLength(QuestionConsts.MaxTitleLength, + MinimumLength = QuestionConsts.MinTitleLength)] public string Title { get; set; } [StringLength(QuestionConsts.MaxTextLength)] @@ -152,6 +155,7 @@ public class CreateQuestionDto - **Do** use the `UpdateAsync` **method name**. - **Do** get a **specialized input** DTO to update the entity. +- **Do** inherit the DTO class from the `ExtensibleObject` (or any other class implements the `IHasExtraProperties`) to allow to pass extra properties if needed. - **Do** get the Id of the entity as a separated primitive parameter. Do not include to the update DTO. - **Do** use **data annotations** for input validation. - Share constants between domain wherever possible (via constants defined in the **domain shared** package). @@ -200,6 +204,10 @@ This method votes a question and returns the current score of the question. * **Do not** use LINQ/SQL for querying data from database inside the application service methods. It's repository's responsibility to perform LINQ/SQL queries from the data source. +#### Extra Properties + +* **Do** use either `MapExtraPropertiesTo` extension method ([see](Object-Extensions.md)) or configure the object mapper (`MapExtraProperties`) to allow application developers to be able to extend the objects and services. + #### Manipulating / Deleting Entities * **Do** always get all the related entities from repositories to perform the operations on them. diff --git a/docs/en/Best-Practices/Data-Transfer-Objects.md b/docs/en/Best-Practices/Data-Transfer-Objects.md index 0c8580abb7..0fca0e86f2 100644 --- a/docs/en/Best-Practices/Data-Transfer-Objects.md +++ b/docs/en/Best-Practices/Data-Transfer-Objects.md @@ -2,6 +2,7 @@ * **Do** define DTOs in the **application contracts** package. * **Do** inherit from the pre-built **base DTO classes** where possible and necessary (like `EntityDto`, `CreationAuditedEntityDto`, `AuditedEntityDto`, `FullAuditedEntityDto` and so on). + * **Do** inherit from the **extensible DTO** classes for the **aggregate roots** (like `ExtensibleAuditedEntityDto`), because aggregate roots are extensible objects and extra properties are mapped to DTOs in this way. * **Do** define DTO members with **public getter and setter**. * **Do** use **data annotations** for **validation** on the properties of DTOs those are inputs of the service. * **Do** not add any **logic** into DTOs except implementing `IValidatableObject` when necessary. diff --git a/docs/en/Customizing-Application-Modules-Extending-Entities.md b/docs/en/Customizing-Application-Modules-Extending-Entities.md index be28465ff7..a7c3f4e7cd 100644 --- a/docs/en/Customizing-Application-Modules-Extending-Entities.md +++ b/docs/en/Customizing-Application-Modules-Extending-Entities.md @@ -50,7 +50,7 @@ ObjectExtensionManager.Instance * You provide the `IdentityUser` as the entity name, `string` as the type of the new property, `SocialSecurityNumber` as the property name (also, the field name in the database table). * You also need to provide an action that defines the database mapping properties using the [EF Core Fluent API](https://docs.microsoft.com/en-us/ef/core/modeling/entity-properties). -> This code part must be executed before the related `DbContext` used. The [application startup template](Startup-Templates/Application.md) defines a static class named `YourProjectNameEntityExtensions`. You can define your extensions in this class to ensure that it is executed in the proper time. Otherwise, you should handle it yourself. +> This code part must be executed before the related `DbContext` used. The [application startup template](Startup-Templates/Application.md) defines a static class named `YourProjectNameEfCoreEntityExtensionMappings`. You can define your extensions in this class to ensure that it is executed in the proper time. Otherwise, you should handle it yourself. Once you define an entity extension, you then need to use the standard [Add-Migration](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#add-migration) and [Update-Database](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#update-database) commands of the EF Core to create a code first migration class and update your database. @@ -58,8 +58,6 @@ You can then use the same extra properties system defined in the previous sectio ## Creating a New Entity Maps to the Same Database Table/Collection -While using the extra properties approach is **easy to use** and suitable for some scenarios, it has some drawbacks described in the [entities document](Entities.md). - Another approach can be **creating your own entity** mapped to **the same database table** (or collection for a MongoDB database). `AppUser` entity in the [application startup template](Startup-Templates/Application.md) already implements this approach. [EF Core Migrations document](Entity-Framework-Core-Migrations.md) describes how to implement it and manage **EF Core database migrations** in such a case. It is also possible for MongoDB, while this time you won't deal with the database migration problems. @@ -176,4 +174,4 @@ public class MyDistributedIdentityUserCreatedEventHandler : ## See Also * [Migration System for the EF Core](Entity-Framework-Core-Migrations.md) -* [Customizing the Existing Modules](Customizing-Application-Modules-Guide.md) \ No newline at end of file +* [Customizing the Existing Modules](Customizing-Application-Modules-Guide.md) diff --git a/docs/en/Customizing-Application-Modules-Overriding-Services.md b/docs/en/Customizing-Application-Modules-Overriding-Services.md index b735d51f6f..7f0c0502ea 100644 --- a/docs/en/Customizing-Application-Modules-Overriding-Services.md +++ b/docs/en/Customizing-Application-Modules-Overriding-Services.md @@ -60,6 +60,7 @@ In most cases, you will want to change one or a few methods of the current imple ````csharp [Dependency(ReplaceServices = true)] +[ExposeServices(typeof(IIdentityUserAppService), typeof(IdentityUserAppService))] public class MyIdentityUserAppService : IdentityUserAppService { //... @@ -161,6 +162,105 @@ Check the [localization system](Localization.md) to learn how to localize the er Overriding controllers, framework services, view component classes and any other type of classes registered to dependency injection can be overridden just like the examples above. +## Extending Data Transfer Objects + +**Extending [entities](Entities.md)** is possible as described in the [Extending Entities document](Customizing-Application-Modules-Extending-Entities.md). In this way, you can add **custom properties** to entities and perform **additional business logic** by overriding the related services as described above. + +It is also possible to extend Data Transfer Objects (**DTOs**) used by the application services. In this way, you can get extra properties from the UI (or client) and return extra properties from the service. + +### Example + +Assuming that you've already added a `SocialSecurityNumber` as described in the [Extending Entities document](Customizing-Application-Modules-Extending-Entities.md) and want to include this information while getting the list of users from the `GetListAsync` method of the `IdentityUserAppService`. + +You can use the [object extension system](Object-Extensions.md) to add the property to the `IdentityUserDto`. Write this code inside the `YourProjectNameDtoExtensions` class comes with the application startup template: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "SocialSecurityNumber" + ); +```` + +This code defines a `SocialSecurityNumber` to the `IdentityUserDto` class as a `string` type. That's all. Now, if you call the `/api/identity/users` HTTP API (which uses the `IdentityUserAppService` internally) from a REST API client, you will see the `SocialSecurityNumber` value in the `extraProperties` section. + +````json +{ + "totalCount": 1, + "items": [{ + "tenantId": null, + "userName": "admin", + "name": "admin", + "surname": null, + "email": "admin@abp.io", + "emailConfirmed": false, + "phoneNumber": null, + "phoneNumberConfirmed": false, + "twoFactorEnabled": false, + "lockoutEnabled": true, + "lockoutEnd": null, + "concurrencyStamp": "b4c371a0ab604de28af472fa79c3b70c", + "isDeleted": false, + "deleterId": null, + "deletionTime": null, + "lastModificationTime": "2020-04-09T21:25:47.0740706", + "lastModifierId": null, + "creationTime": "2020-04-09T21:25:46.8308744", + "creatorId": null, + "id": "8edecb8f-1894-a9b1-833b-39f4725db2a3", + "extraProperties": { + "SocialSecurityNumber": "123456789" + } + }] +} +```` + +Manually added the `123456789` value to the database for now. + +All pre-built modules support extra properties in their DTOs, so you can configure easily. + +### Definition Check + +When you [define](Customizing-Application-Modules-Extending-Entities.md) an extra property for an entity, it doesn't automatically appear in all the related DTOs, because of the security. The extra property may contain a sensitive data and you may not want to expose it to the clients by default. + +So, you need to explicitly define the same property for the corresponding DTO if you want to make it available for the DTO (as just done above). If you want to allow to set it on user creation, you also need to define it for the `IdentityUserCreateDto`. + +If the property is not so secure, this can be tedious. Object extension system allows you to ignore this definition check for a desired property. See the example below: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "SocialSecurityNumber", + options => + { + options.MapEfCore(b => b.HasMaxLength(32)); + options.CheckPairDefinitionOnMapping = false; + } + ); +```` + +This is another approach to define a property for an entity (`ObjectExtensionManager` has more, see [its document](Object-Extensions.md)). This time, we set `CheckPairDefinitionOnMapping` to false to skip definition check while mapping entities to DTOs and vice verse. + +If you don't like this approach but want to add a single property to multiple objects (DTOs) easier, `AddOrUpdateProperty` can get an array of types to add the extra property: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + new[] + { + typeof(IdentityUserDto), + typeof(IdentityUserCreateDto), + typeof(IdentityUserUpdateDto) + }, + "SocialSecurityNumber" + ); +```` + +### About the User Interface + +This system allows you to add extra properties to entities and DTOs and execute custom business code, however it does nothing related to the User Interface. + +See [Overriding the User Interface](Customizing-Application-Modules-Overriding-User-Interface.md) guide for the UI part. + ## How to Find the Services? -[Module documents](Modules/Index.md) includes the list of the major services they define. In addition, you can investigate [their source code](https://github.com/abpframework/abp/tree/dev/modules) to explore all the services. \ No newline at end of file +[Module documents](Modules/Index.md) includes the list of the major services they define. In addition, you can investigate [their source code](https://github.com/abpframework/abp/tree/dev/modules) to explore all the services. diff --git a/docs/en/Entity-Framework-Core-Migrations.md b/docs/en/Entity-Framework-Core-Migrations.md index 5a8e955701..e0772579ec 100644 --- a/docs/en/Entity-Framework-Core-Migrations.md +++ b/docs/en/Entity-Framework-Core-Migrations.md @@ -882,4 +882,4 @@ This document explains how to split your databases and manage your database migr ## Source Code -You can find the source code of the example project referenced by this document [here](https://github.com/abpframework/abp/tree/dev/samples/EfCoreMigrationDemo). However, you need to read and understand this document in order to understand the example project's source code. \ No newline at end of file +You can find the source code of the example project referenced by this document [here](https://github.com/abpframework/abp-samples/tree/master/EfCoreMigrationDemo). However, you need to read and understand this document in order to understand the example project's source code. \ No newline at end of file diff --git a/docs/en/Exception-Handling.md b/docs/en/Exception-Handling.md index 6dc4ccbd9f..3ccade9767 100644 --- a/docs/en/Exception-Handling.md +++ b/docs/en/Exception-Handling.md @@ -315,8 +315,8 @@ The `context` object contains necessary information about the exception occurred Some exception types are automatically thrown by the framework: -- `AbpAuthorizationException` is thrown if the current user has no permission to perform the requested operation. See authorization document (TODO: link) for more. -- `AbpValidationException` is thrown if the input of the current request is not valid. See validation document (TODO: link) for more. +- `AbpAuthorizationException` is thrown if the current user has no permission to perform the requested operation. See [authorization](Authorization.md) for more. +- `AbpValidationException` is thrown if the input of the current request is not valid. See [validation](Validation.md) for more. - `EntityNotFoundException` is thrown if the requested entity is not available. This is mostly thrown by [repositories](Repositories.md). You can also throw these type of exceptions in your code (although it's rarely needed). diff --git a/docs/en/Getting-Started-Angular-Template.md b/docs/en/Getting-Started-Angular-Template.md index 22d672d97a..9beb84bcef 100644 --- a/docs/en/Getting-Started-Angular-Template.md +++ b/docs/en/Getting-Started-Angular-Template.md @@ -1,126 +1,8 @@ -## Getting Started With the Angular Application Template +# Getting Started with the Startup Templates -This tutorial explains how to create a new Angular application using the startup template, configure and run it. +See the following tutorials to learn how to get started with the ABP Framework using the pre-built application startup templates: -### Creating a New Project +* [Getting Started With the ASP.NET Core MVC / Razor Pages UI](Getting-Started?UI=MVC&DB=EF&Tiered=No) +* [Getting Started with the Angular UI](Getting-Started?UI=NG&DB=EF&Tiered=No) -This tutorial uses **ABP CLI** to create a new project. See the [Get Started](https://abp.io/get-started) page for other options. - -Install the ABP CLI using a command line window, if you've not installed before: - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -Use `abp new` command in an empty folder to create your project: - -````bash -abp new Acme.BookStore -u angular -```` - -> You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore. - -`-u angular` option specifies the UI framework to be Angular. Default database provider is EF Core. See the [CLI documentation](CLI.md) for all available options. - -#### Pre Requirements - -The created solution requires; - -* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) -* [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) -* [Node v12+](https://nodejs.org) -* [Yarn v1.19+](https://classic.yarnpkg.com/) - -### The Solution Structure - -Open the solution in **Visual Studio**: - -![bookstore-visual-studio-solution](images/bookstore-visual-studio-solution-for-spa.png) - -The solution has a layered structure (based on [Domain Driven Design](Domain-Driven-Design.md)) and contains unit & integration test projects properly configured to work with **EF Core** & **SQLite in-memory** database. - -> See the [Application Template Document](Startup-Templates/Application.md) to understand the solution structure in details. - -### Database Connection String - -Check the **connection string** in the `appsettings.json` file under the `.HttpApi.Host` project: - -````json -{ - "ConnectionStrings": { - "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" - } -} -```` - -The solution is configured to use **Entity Framework Core** with **MS SQL Server**. EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers, so you can use another DBMS if you want. Change the connection string if you need. - -### Create Database & Apply Database Migrations - -You have two options to create the database. - -#### Using the DbMigrator Application - -The solution contains a console application (named `Acme.BookStore.DbMigrator` in this sample) that can create database, apply migrations and seed initial data. It is useful on development as well as on production environment. - -> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. - -Right click to the `.DbMigrator` project and select **Set as StartUp Project**: - -![set-as-startup-project](images/set-as-startup-project.png) - -Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below: - -![set-as-startup-project](images/db-migrator-app.png) - -#### Using EF Core Update-Database Command - -Ef Core has `Update-Database` command which creates database if necessary and applies pending migrations. Right click to the `.HttpApi.Host` project and select **Set as StartUp Project**: - -![set-as-startup-project](images/set-as-startup-project.png) - -Open the **Package Manager Console**, select `.EntityFrameworkCore.DbMigrations` project as the **Default Project** and run the `Update-Database` command: - -![pcm-update-database](images/pcm-update-database-v2.png) - -This will create a new database based on the configured connection string. - -> Using the `.DbMigrator` tool is the suggested way, because it also seeds the initial data to be able to properly run the web application. - -### Running the Application - -#### Run the API Host (Server Side) - -Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI: - -![bookstore-homepage](images/bookstore-swagger-ui-host.png) - -You can see the application APIs and test them here. Get [more info](https://swagger.io/tools/swagger-ui/) about the Swagger UI. - -##### Authorization for the Swagger UI - -Most of the application APIs require authentication & authorization. If you want to test authorized APIs, manually go to the `/Account/Login` page, enter `admin` as the username and `1q2w3E*` as the password to login to the application. Then you will be able to execute authorized APIs too. - -#### Run the Angular Application (Client Side) - -Go to the `angular` folder, open a command line terminal, type the `yarn` command (we suggest the [yarn](https://yarnpkg.com) package manager while `npm install` will also work in most cases) - -````bash -yarn -```` - -Once all node modules are loaded, execute `yarn start` or `npm start` command: - -````bash -yarn start -```` - -Open your favorite browser and go to `localhost:4200` URL. Initial username is `admin` and password is `1q2w3E*`. - -The startup template includes the **identity management** and **tenant management** modules. Once you login, the Administration menu will be available where you can manage **tenants**, **roles**, **users** and their **permissions**. - -> We recommend [Visual Studio Code](https://code.visualstudio.com/) as the editor for the Angular project, but you are free to use your favorite editor. - -### What's Next? - -* [Application development tutorial](Tutorials/Part-1) + \ No newline at end of file diff --git a/docs/en/Getting-Started-AspNetCore-MVC-Template.md b/docs/en/Getting-Started-AspNetCore-MVC-Template.md index d074e8aaef..9beb84bcef 100644 --- a/docs/en/Getting-Started-AspNetCore-MVC-Template.md +++ b/docs/en/Getting-Started-AspNetCore-MVC-Template.md @@ -1,104 +1,8 @@ -## Getting Started With the ASP.NET Core MVC Template +# Getting Started with the Startup Templates -This tutorial explains how to create a new ASP.NET Core MVC web application using the startup template, configure and run it. +See the following tutorials to learn how to get started with the ABP Framework using the pre-built application startup templates: -### Creating a New Project +* [Getting Started With the ASP.NET Core MVC / Razor Pages UI](Getting-Started?UI=MVC&DB=EF&Tiered=No) +* [Getting Started with the Angular UI](Getting-Started?UI=NG&DB=EF&Tiered=No) -This tutorial uses **ABP CLI** to create a new project. See the [Get Started](https://abp.io/get-started) page for other options. - -Install the ABP CLI using a command line window, if you've not installed before: - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -Use `abp new` command in an empty folder to create your project: - -````bash -abp new Acme.BookStore -```` - -> You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore. - -`new` command creates a **layered MVC application** with **Entity Framework Core** as the database provider. However, it has additional options. See the [CLI documentation](CLI.md) for all available options. - -#### Pre Requirements - -The created solution requires; - -* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) -* [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) -* [Node v12+](https://nodejs.org) -* [Yarn v1.19+](https://classic.yarnpkg.com/) - -### The Solution Structure - -Open the solution in **Visual Studio**: - -![bookstore-visual-studio-solution](images/bookstore-visual-studio-solution-v3.png) - -The solution has a layered structure (based on [Domain Driven Design](Domain-Driven-Design.md)) and contains unit & integration test projects properly configured to work with **EF Core** & **SQLite in-memory** database. - -> See [Application template document](Startup-Templates/Application.md) to understand the solution structure in details. - -### Database Connection String - -Check the **connection string** in the `appsettings.json` file under the `.Web` project: - -````json -{ - "ConnectionStrings": { - "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" - } -} -```` - -The solution is configured to use **Entity Framework Core** with **MS SQL Server**. EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers, so you can use another DBMS if you want. Change the connection string if you need. - -### Create Database & Apply Database Migrations - -You have two options to create the database. - -#### Using the DbMigrator Application - -The solution contains a console application (named `Acme.BookStore.DbMigrator` in this sample) that can create database, apply migrations and seed initial data. It is useful on development as well as on production environment. - -> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. - -Right click to the `.DbMigrator` project and select **Set as StartUp Project**: - -![set-as-startup-project](images/set-as-startup-project.png) - -Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below: - -![set-as-startup-project](images/db-migrator-app.png) - -#### Using EF Core Update-Database Command - -Ef Core has `Update-Database` command which creates database if necessary and applies pending migrations. Right click to the `.Web` project and select **Set as StartUp Project**: - -![set-as-startup-project](images/set-as-startup-project.png) - -Open the **Package Manager Console**, select `.EntityFrameworkCore.DbMigrations` project as the **Default Project** and run the `Update-Database` command: - -![pcm-update-database](images/pcm-update-database-v2.png) - -This will create a new database based on the configured connection string. - -> Using the `.Migrator` tool is the suggested way, because it also seeds the initial data to be able to properly run the web application. - -### Running the Application - -Ensure that the `.Web` project is the startup project. Run the application which will open the **home** page in your browser: - -![bookstore-homepage](images/bookstore-homepage.png) - -Click the **Login** button, enter `admin` as the username and `1q2w3E*` as the password to login to the application. - -The startup template includes the **identity management** and **tenant management** modules. Once you login, the Administration menu will be available where you can manage **tenants**, **roles**, **users** and their **permissions**. User management page is shown below: - -![bookstore-user-management](images/bookstore-user-management-v2.png) - -### What's Next? - -* [Application development tutorial](Tutorials/Part-1.md) + \ No newline at end of file diff --git a/docs/en/Getting-Started-With-Startup-Templates.md b/docs/en/Getting-Started-With-Startup-Templates.md index 19442ec81e..be2bb201b8 100644 --- a/docs/en/Getting-Started-With-Startup-Templates.md +++ b/docs/en/Getting-Started-With-Startup-Templates.md @@ -2,5 +2,7 @@ See the following tutorials to learn how to get started with the ABP Framework using the pre-built application startup templates: -* [Getting Started With the ASP.NET Core MVC / Razor Pages UI](Getting-Started-AspNetCore-MVC-Template.md) -* [Getting Started with the Angular UI](Getting-Started-Angular-Template.md) \ No newline at end of file +* [Getting Started With the ASP.NET Core MVC / Razor Pages UI](Getting-Started?UI=MVC&DB=EF&Tiered=No) +* [Getting Started with the Angular UI](Getting-Started?UI=NG&DB=EF&Tiered=No) + + \ No newline at end of file diff --git a/docs/en/Getting-Started.md b/docs/en/Getting-Started.md new file mode 100644 index 0000000000..07e55f553e --- /dev/null +++ b/docs/en/Getting-Started.md @@ -0,0 +1,407 @@ +# Getting started + +````json +//[doc-params] +{ + "UI": ["MVC","NG"], + "DB": ["EF", "Mongo"], + "Tiered": ["Yes", "No"] +} +```` + +This tutorial explains how to create a new {{if UI == "MVC"}} ASP.NET Core MVC web {{else if UI == "NG"}} Angular {{end}} application using the startup template, configure and run it. + + +## Setup your development environment + +First things first! Let's setup your development environment before creating the first project. + +### Pre-requirements + +The following tools should be installed on your development machine: + +* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) for Windows / [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/). +* [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) + +* [Node v12+](https://nodejs.org) +* [Yarn v1.19+](https://classic.yarnpkg.com/) + +> You can use another editor instead of Visual Studio as long as it supports .NET Core and ASP.NET Core. + +### Install the ABP CLI + +[ABP CLI](./CLI.md) is a command line interface that is used to authenticate and automate some tasks for ABP based applications. + +> ABP CLI is a free & open source tool for [the ABP framework](https://abp.io/). + +First, you need to install the ABP CLI using the following command: + +````shell +dotnet tool install -g Volo.Abp.Cli +```` + +If you've already installed, you can update it using the following command: + +````shell +dotnet tool update -g Volo.Abp.Cli +```` + +## Create a new project + +> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. + +### Using the ABP CLI to create a new project + +Use the `new` command of the ABP CLI to create a new project: + +````shell +abp new Acme.BookStore -t app{{if UI == "NG"}} -u angular {{end}}{{if DB == "Mongo"}} -d mongodb{{end}}{{if Tiered == "Yes" && UI != "NG"}} --tiered {{else if Tiered == "Yes" && UI == "NG"}}--separate-identity-server{{end}} +```` + +* `-t` argument specifies the [startup template](Startup-Templates/Application.md) name. `app` is the startup template that contains the essential [ABP Modules](Modules/Index.md) pre-installed and configured for you. + +{{ if UI == "NG" }} + +* `-u` argument specifies the UI framework, `angular` in this case. + +{{ if Tiered == "Yes" }} + +* `--separate-identity-server` argument is used to separate the identity server application from the API host application. If not specified, you will have a single endpoint. + +{{ end }} + +{{ end }} + +{{ if DB == "Mongo" }} + +* `-d` argument specifies the database provider, `mongodb` in this case. + +{{ end }} + +{{ if Tiered == "Yes" && UI != "NG" }} + +* `--tiered` argument is used to create N-tiered solution where authentication server, UI and API layers are physically separated. + +{{ end }} + +> You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore. + +#### ABP CLI commands & options + +[ABP CLI document](./CLI.md) covers all of the available commands and options for the ABP CLI. See the [ABP Startup Templates](Startup-Templates/Index.md) document for other templates. + +## The solution structure + +{{ if UI == "MVC" }} + +After creating your project, you will have the following solution folders & files: + +![](images/solution-files-mvc.png) + +You will see the following solution structure when you open the `.sln` file in the Visual Studio: + +{{if DB == "Mongo"}} + +![vs-default-app-solution-structure](images/vs-app-solution-structure-mongodb.png) + +{{else}} + +![vs-default-app-solution-structure](images/vs-app-solution-structure{{if Tiered == "Yes"}}-tiered{{end}}.png) + +{{end}} + +{{ else if UI == "NG" }} +There are three folders in the created solution: + +![](images/solution-files-non-mvc.png) + +* `angular` folder contains the Angular UI application. +* `aspnet-core` folder contains the backend solution. +* `react-native` folder contains the React Native UI application. + +Open the `.sln` (Visual Studio solution) file under the `aspnet-core` folder: + +![vs-angular-app-backend-solution-structure](images/vs-spa-app-backend-structure{{if DB == "Mongo"}}-mongodb{{end}}.png) + +{{ end }} + +> ###### About the projects in your solution +> +> Your solution may have slightly different structure based on your **UI**, **database** and other preferences. + +The solution has a layered structure (based on [Domain Driven Design](./Domain-Driven-Design.md)) and also contains unit & integration test projects. + +{{ if DB == "EF" }} + +Integration tests projects are properly configured to work with **EF Core** & **SQLite in-memory** database. + +{{ else if DB == "Mongo" }} + +Integration tests projects are properly configured to work with in-memory **MongoDB** database created per test (used [Mongo2Go](https://github.com/Mongo2Go/Mongo2Go) library). + +{{ end }} + +> See the [application template document](Startup-Templates/Application.md) to understand the solution structure in details. + +## Create the database + +### Database connection string + +Check the **connection string** in the `appsettings.json` file under the {{if UI == "MVC"}}{{if Tiered == "Yes"}}`.IdentityServer` and `.HttpApi.Host` projects{{else}}`.Web` project{{end}}{{else if UI == "NG" }}`.HttpApi.Host` project{{end}}: + +{{ if DB == "EF" }} + +````json +"ConnectionStrings": { + "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" +} +```` + +The solution is configured to use **Entity Framework Core** with **MS SQL Server**. EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers, so you can use any supported DBMS. See [the Entity Framework integration document](https://docs.abp.io/en/abp/latest/Entity-Framework-Core) to learn how to switch to another DBMS. + +### Apply the migrations + +The solution uses the [Entity Framework Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli). So, you need to apply migrations to create the database. There are two ways of applying the database migrations. + +#### Apply migrations using the DbMigrator + +The solution comes with a `.DbMigrator` console application which applies migrations and also seed the initial data. It is useful on development as well as on production environment. + +> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. + +Right click to the `.DbMigrator` project and select **Set as StartUp Project** + +![set-as-startup-project](images/set-as-startup-project.png) + + Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below: + + ![db-migrator-output](images/db-migrator-output.png) + +> Initial seed data creates the `admin` user in the database which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database. + +#### Using EF Core Update-Database command + +Ef Core has `Update-Database` command which creates database if necessary and applies pending migrations. + +{{ if UI == "MVC" }} + +Right click to the {{if Tiered == "Yes"}}`.IdentityServer`{{else}}`.Web`{{end}} project and select **Set as StartUp project**: + +{{ else if UI != "MVC" }} + +Right click to the `.HttpApi.Host` project and select **Set as StartUp Project**: + +{{ end }} + +![set-as-startup-project](images/set-as-startup-project.png) + +Open the **Package Manager Console**, select `.EntityFrameworkCore.DbMigrations` project as the **Default Project** and run the `Update-Database` command: + +![package-manager-console-update-database](images/package-manager-console-update-database.png) + +This will create a new database based on the configured connection string. + +> Using the `.Migrator` tool is the suggested way, because it also seeds the initial data to be able to properly run the web application. + +{{ else if DB == "Mongo" }} + +````json +"ConnectionStrings": { + "Default": "mongodb://localhost:27017/BookStore" +} +```` + +The solution is configured to use **MongoDB** in your local computer, so you need to have a MongoDB server instance up and running or change the connection string to another MongoDB server. + +### Seed initial data + +The solution comes with a `.DbMigrator` console application which seeds the initial data. It is useful on development as well as on production environment. + +> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. + +Right click to the `.DbMigrator` project and select **Set as StartUp Project** + +![set-as-startup-project](images/set-as-startup-project.png) + + Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below: + + ![db-migrator-output](images/db-migrator-output.png) + +> Initial seed data creates the `admin` user in the database which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database. + +{{ end }} + +## Run the application + +{{ if UI == "MVC" }} + +{{ if Tiered == "Yes" }} + +Ensure that the `.IdentityServer` project is the startup project. Run the application which will open a **login** page in your browser. + +> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. + +You can login, but you cannot enter to the main application here. This is just the authentication server. + +Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a **Swagger UI** in your browser. + +![swagger-ui](images/swagger-ui.png) + +This is the API application that is used by the web application. + +Lastly, ensure that the `.Web` project is the startup project and run the application which will open a **welcome** page in your browser + +![mvc-tiered-app-home](images/bookstore-home.png) + +Click to the **login** button which will redirect you to the `Identity Server` to login to the application: + +![bookstore-login](images/bookstore-login.png) + +{{ else }} + +Ensure that the `.Web` project is the startup project. Run the application which will open the **login** page in your browser: + +> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. + +![bookstore-login](images/bookstore-login.png) + +{{ end }} + +{{ else if UI != "MVC" }} + +#### Running the HTTP API Host (server-side) + +{{ if Tiered == "Yes" }} + +Ensure that the `.IdentityServer` project is the startup project. Run the application which will open a **login** page in your browser. + +> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. + +You can login, but you cannot enter to the main application here. This is just the authentication server. + +{{ end }} + +Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI: + +{{ if Tiered == "No" }} + +> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. + +{{ end }} + +![swagger-ui](images/swagger-ui.png) + +You can see the application APIs and test them here. Get [more info](https://swagger.io/tools/swagger-ui/) about the Swagger UI. + +> ##### Authorization for the Swagger UI +> +> Most of the HTTP APIs require authentication & authorization. If you want to test authorized APIs, manually go to the `/Account/Login` page, enter `admin` as the username and `1q2w3E*` as the password to login to the application. Then you will be able to execute authorized APIs too. + +{{ end }} + +{{ if UI == "NG" }} +#### Running the Angular application (client-side) + +Go to the `angular` folder, open a command line terminal, type the `yarn` command (we suggest to the [yarn](https://yarnpkg.com/) package manager while `npm install` will also work in most cases) + +```bash +yarn +``` + +Once all node modules are loaded, execute `yarn start` (or `npm start`) command: + +```bash +yarn start +``` + +Wait `Angular CLI` to launch `Webpack` dev-server with `BrowserSync`. +This will take care of compiling your `TypeScript` code, and automatically reloading your browser. +After it finishes, `Angular Live Development Server` will be listening on localhost:4200, +open your web browser and navigate to [localhost:4200](http://localhost:4200/) + + + +![bookstore-login](images/bookstore-login.png) + +{{ end }} + +Enter **admin** as the username and **1q2w3E*** as the password to login to the application: + +![bookstore-home](images/bookstore-home.png) + +The application is up and running. You can start developing your application based on this startup template. + +#### Mobile Development + +ABP platform provide [React Native](https://reactnative.dev/) template to develop mobile applications. + +>The solution includes the React Native application in the `react-native` folder as default. If you don't plan to develop a mobile application with React Native, you can ignore this step and delete the `react-native` folder. + +The React Native application running on an Android emulator or a physical phone cannot connect to the backend on `localhost`. To fix this problem, it is necessary to run backend on the local IP. + +{{ if Tiered == "No"}} +![React Native host project local IP entry](images/rn-host-local-ip.png) + +* Open the `appsettings.json` in the `.HttpApi.Host` folder. Replace the `localhost` address on the `SelfUrl` and `Authority` properties with your local IP address. +* Open the `launchSettings.json` in the `.HttpApi.Host/Properties` folder. Replace the `localhost` address on the `applicationUrl` properties with your local IP address. + +{{ else if Tiered == "Yes" }} + +![React Native tiered project local IP entry](images/rn-tiered-local-ip.png) + +* Open the `appsettings.json` in the `.IdentityServer` folder. Replace the `localhost` address on the `SelfUrl` property with your local IP address. +* Open the `launchSettings.json` in the `.IdentityServer/Properties` folder. Replace the `localhost` address on the `applicationUrl` properties with your local IP address. +* Open the `appsettings.json` in the `.HttpApi.Host` folder. Replace the `localhost` address on the `Authority` property with your local IP address. +* Open the `launchSettings.json` in the `.HttpApi.Host/Properties` folder. Replace the `localhost` address on the `applicationUrl` properties with your local IP address. + +{{ end }} + +Run the backend as described in the [**Running the HTTP API Host (server-side)**](#running-the-http-api-host-server-side) section. + +> React Native application does not trust the auto-generated .NET HTTPS certificate, you should use the HTTP during development. + +Go to the `react-native` folder, open a command line terminal, type the `yarn` command (we suggest to the [yarn](https://yarnpkg.com/) package manager while `npm install` will also work in most cases): + +```bash +yarn +``` + +* Open the `Environment.js` in the `react-native` folder and replace the `localhost` address on the `apiUrl` and `issuer` properties with your local IP address as shown below: + +![react native environment local IP](images/rn-environment-local-ip.png) + +{{ if Tiered == "Yes" }} + +> Make sure that `issuer` matches the running address of the `.IdentityServer` project, `apiUrl` matches the running address of the `.HttpApi.Host` project. + +{{else}} + +> Make sure that `issuer` and `apiUrl` matches the running address of the `.HttpApi.Host` project. + +{{ end }} + +Once all node modules are loaded, execute `yarn start` (or `npm start`) command: + +```bash +yarn start +``` + +Wait Expo CLI to start. Expo CLI opens the management interface on the `http://localhost:19002/` address. + +![expo-interface](images/rn-expo-interface.png) + +In the above management interface, you can start the application with an Android emulator, an iOS simulator or a physical phone by the scan the QR code with the [Expo Client](https://expo.io/tools#client). + +> See the [Android Studio Emulator](https://docs.expo.io/versions/v36.0.0/workflow/android-studio-emulator/), [iOS Simulator](https://docs.expo.io/versions/v36.0.0/workflow/ios-simulator/) documents on expo.io. + +![React Native login screen on iPhone 11](images/rn-login-iphone.png) + +Enter **admin** as the username and **1q2w3E*** as the password to login to the application. + +The application is up and running. You can continue to develop your application based on this startup template. + +> The [application startup template](startup-templates/application/index.md) includes the TenantManagement and Identity modules. + +## What's next? + +[Application development tutorial](tutorials/book-store/part-1.md) diff --git a/docs/en/How-To/Customize-SignIn-Manager.md b/docs/en/How-To/Customize-SignIn-Manager.md index 61b6c3b7b5..03c1f06dd8 100644 --- a/docs/en/How-To/Customize-SignIn-Manager.md +++ b/docs/en/How-To/Customize-SignIn-Manager.md @@ -42,14 +42,14 @@ public override async Task GetE { var auth = await Context.AuthenticateAsync(Microsoft.AspNetCore.Identity.IdentityConstants.ExternalScheme); var items = auth?.Properties?.Items; - if (auth?.Principal == null || items == null || !items.ContainsKey("LoginProviderKey")) + if (auth?.Principal == null || items == null || !items.ContainsKey(LoginProviderKey)) { return null; } if (expectedXsrf != null) { - if (!items.ContainsKey("XsrfKey")) + if (!items.ContainsKey(XsrfKey)) { return null; } diff --git a/docs/en/Multi-Tenancy.md b/docs/en/Multi-Tenancy.md index 27fe70086c..6adef9b214 100644 --- a/docs/en/Multi-Tenancy.md +++ b/docs/en/Multi-Tenancy.md @@ -172,11 +172,11 @@ namespace MyCompany.MyProject { options.Tenants = new[] { - new TenantInformation( + new TenantConfiguration( Guid.Parse("446a5211-3d72-4339-9adc-845151f8ada0"), //Id "tenant1" //Name ), - new TenantInformation( + new TenantConfiguration( Guid.Parse("25388015-ef1c-4355-9c18-f6b6ddbaf89d"), //Id "tenant2" //Name ) @@ -252,7 +252,7 @@ TODO: This package implements ITenantStore using a real database... #### Tenant Information -ITenantStore works with **TenantInformation** class that has several properties for a tenant: +ITenantStore works with **TenantConfiguration** class that has several properties for a tenant: * **Id**: Unique Id of the tenant. * **Name**: Unique name of the tenant. diff --git a/docs/en/Nightly-Builds.md b/docs/en/Nightly-Builds.md index e7ebe0f33b..1a66af32e1 100644 --- a/docs/en/Nightly-Builds.md +++ b/docs/en/Nightly-Builds.md @@ -24,3 +24,18 @@ Now, you can install preview / nightly packages to your project from Nuget Brows 3. Search a package. You will see prereleases of the package formatted as `(VERSION)-preview(DATE)` (like *v0.16.0-preview20190401* in this sample). 4. You can click to the `Install` button to add package to your project. +## Install & Uninstall Preview NPM Packages + +The latest version of preview NPM packages can be installed by the running below command in the root folder of application: + +```bash +abp switch-to-preview +``` + +If you're using the ABP Framework preview packages, you can switch back to stable version using this command: + +```bash +abp switch-to-stable +``` + +See the [ABP CLI documentation](./CLI.md) for more information. \ No newline at end of file diff --git a/docs/en/Object-Extensions.md b/docs/en/Object-Extensions.md index fad3ff2b0c..6eff328c85 100644 --- a/docs/en/Object-Extensions.md +++ b/docs/en/Object-Extensions.md @@ -1,3 +1,365 @@ # Object Extensions -TODO \ No newline at end of file +ABP Framework provides an **object extension system** to allow you to **add extra properties** to an existing object **without modifying** the related class. This allows to extend functionalities implemented by a depended [application module](Modules/Index.md), especially when you want to [extend entities](Customizing-Application-Modules-Extending-Entities.md) and [DTOs](Customizing-Application-Modules-Overriding-Services.md) defined by the module. + +> Object extension system is not normally not needed for your own objects since you can easily add regular properties to your own classes. + +## IHasExtraProperties Interface + +This is the interface to make a class extensible. It simply defines a `Dictionary` property: + +````csharp +Dictionary ExtraProperties { get; } +```` + +Then you can add or get extra properties using this dictionary. + +### Base Classes + +`IHasExtraProperties` interface is implemented by several base classes by default: + +* Implemented by the `AggregateRoot` class (see [entities](Entities.md)). +* Implemented by `ExtensibleEntityDto`, `ExtensibleAuditedEntityDto`... base [DTO](Data-Transfer-Objects.md) classes. +* Implemented by the `ExtensibleObject`, which is a simple base class can be inherited for any type of object. + +So, if you inherit from these classes, your class will also be extensible. If not, you can always implement it manually. + +### Fundamental Extension Methods + +While you can directly use the `ExtraProperties` property of a class, it is suggested to use the following extension methods while working with the extra properties. + +#### SetProperty + +Used to set the value of an extra property: + +````csharp +user.SetProperty("Title", "My Title"); +user.SetProperty("IsSuperUser", true); +```` + +`SetProperty` returns the same object, so you can chain it: + +````csharp +user.SetProperty("Title", "My Title") + .SetProperty("IsSuperUser", true); +```` + +#### GetProperty + +Used to read the value of an extra property: + +````csharp +var title = user.GetProperty("Title"); + +if (user.GetProperty("IsSuperUser")) +{ + //... +} +```` + +* `GetProperty` is a generic method and takes the object type as the generic parameter. +* Returns the default value if given property was not set before (default value is `0` for `int`, `false` for `bool`... etc). + +##### Non Primitive Property Types + +If your property type is not a primitive (int, bool, enum, string... etc) type, then you need to use non-generic version of the `GetProperty` which returns an `object`. + +#### HasProperty + +Used to check if the object has a property set before. + +#### RemoveProperty + +Used to remove a property from the object. Use this methods instead of setting a `null` value for the property. + +### Some Best Practices + +Using magic strings for the property names is dangerous since you can easily type the property name wrong - it is not type safe. Instead; + +* Define a constant for your extra property names +* Create extension methods to easily set your extra properties. + +Example: + +````csharp +public static class IdentityUserExtensions +{ + private const string TitlePropertyName = "Title"; + + public static void SetTitle(this IdentityUser user, string title) + { + user.SetProperty(TitlePropertyName, title); + } + + public static string GetTitle(this IdentityUser user) + { + return user.GetProperty(TitlePropertyName); + } +} +```` + +Then you can easily set or get the `Title` property: + +````csharp +user.SetTitle("My Title"); +var title = user.GetTitle(); +```` + +## Object Extension Manager + +While you can set arbitrary properties to an extensible object (which implements the `IHasExtraProperties` interface), `ObjectExtensionManager` is used to explicitly define extra properties for extensible classes. + +Explicitly defining an extra property has some use cases: + +* Allows to control how the extra property is handled on object to object mapping (see the section below). +* Allows to define metadata for the property. For example, you can map an extra property to a table field in the database while using the [EF Core](Entity-Framework-Core.md). + +> `ObjectExtensionManager` implements the singleton pattern (`ObjectExtensionManager.Instance`) and you should define object extensions before your application startup. The [application startup template](Startup-Templates/Application.md) has some pre-defined static classes to safely define object extensions inside. + +### AddOrUpdate + +`AddOrUpdate` is the main method to define a extra properties or update extra properties for an object. + +Example: Define extra properties for the `IdentityUser` entity: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdate(options => + { + options.AddOrUpdateProperty("SocialSecurityNumber"); + options.AddOrUpdateProperty("IsSuperUser"); + } + ); +```` + +### AddOrUpdateProperty + +While `AddOrUpdateProperty` can be used on the `options` as shown before, if you want to define a single extra property, you can use the shortcut extension method too: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty("SocialSecurityNumber"); +```` + +Sometimes it would be practical to define a single extra property to multiple types. Instead of defining one by one, you can use the following code: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + new[] + { + typeof(IdentityUserDto), + typeof(IdentityUserCreateDto), + typeof(IdentityUserUpdateDto) + }, + "SocialSecurityNumber" + ); +```` + +### Property Configuration + +`AddOrUpdateProperty` can also get an action that can perform additional configuration on the property definition: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "SocialSecurityNumber", + options => + { + //Configure options... + }); +```` + +> `options` has a dictionary, named `Configuration` which makes the object extension definitions even extensible. It is used by the EF Core to map extra properties to table fields in the database. See the [extending entities](Customizing-Application-Modules-Extending-Entities.md) document. + +The following sections explain the fundamental property configuration options. + +#### CheckPairDefinitionOnMapping + +Controls how to check property definitions while mapping two extensible objects. See the "Object to Object Mapping" section to understand the `CheckPairDefinitionOnMapping` option better. + +## Validation + +You may want to add some **validation rules** for the extra properties you've defined. `AddOrUpdateProperty` method options allows two ways of performing validation: + +1. You can add **data annotation attributes** for a property. +2. You can write an action (code block) to perform a **custom validation**. + +Validation works when you use the object in a method that is **automatically validated** (e.g. controller actions, page handler methods, application service methods...). So, all extra properties are validated whenever the extended object is being validated. + +### Data Annotation Attributes + +All of the standard data annotation attributes are valid for extra properties. Example: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "SocialSecurityNumber", + options => + { + options.ValidationAttributes.Add(new RequiredAttribute()); + options.ValidationAttributes.Add( + new StringLengthAttribute(32) { + MinimumLength = 6 + } + ); + }); +```` + +With this configuration, `IdentityUserCreateDto` objects will be invalid without a valid `SocialSecurityNumber` value provided. + +### Custom Validation + +If you need, you can add a custom action that is executed to validate the extra properties. Example: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "SocialSecurityNumber", + options => + { + options.Validators.Add(context => + { + var socialSecurityNumber = context.Value as string; + + if (socialSecurityNumber == null || + socialSecurityNumber.StartsWith("X")) + { + context.ValidationErrors.Add( + new ValidationResult( + "Invalid social security number: " + socialSecurityNumber, + new[] { "SocialSecurityNumber" } + ) + ); + } + }); + }); +```` + +`context.ServiceProvider` can be used to resolve a service dependency for advanced scenarios. + +In addition to add custom validation logic for a single property, you can add a custom validation logic that is executed in object level. Example: + +````csharp +ObjectExtensionManager.Instance +.AddOrUpdate(objConfig => +{ + //Define two properties with their own validation rules + + objConfig.AddOrUpdateProperty("Password", propertyConfig => + { + propertyConfig.ValidationAttributes.Add(new RequiredAttribute()); + }); + + objConfig.AddOrUpdateProperty("PasswordRepeat", propertyConfig => + { + propertyConfig.ValidationAttributes.Add(new RequiredAttribute()); + }); + + //Write a common validation logic works on multiple properties + + objConfig.Validators.Add(context => + { + if (context.ValidatingObject.GetProperty("Password") != + context.ValidatingObject.GetProperty("PasswordRepeat")) + { + context.ValidationErrors.Add( + new ValidationResult( + "Please repeat the same password!", + new[] { "Password", "PasswordRepeat" } + ) + ); + } + }); +}); +```` + +## Object to Object Mapping + +Assume that you've added an extra property to an extensible entity object and used auto [object to object mapping](Object-To-Object-Mapping.md) to map this entity to an extensible DTO class. You need to be careful in such a case, because the extra property may contain a **sensitive data** that should not be available to clients. + +This section offers some **good practices** to control your extra properties on object mapping. + +### MapExtraPropertiesTo + +`MapExtraPropertiesTo` is an extension method provided by the ABP Framework to copy extra properties from an object to another in a controlled manner. Example usage: + +````csharp +identityUser.MapExtraPropertiesTo(identityUserDto); +```` + +`MapExtraPropertiesTo` **requires to define properties** (as described above) in **both sides** (`IdentityUser` and `IdentityUserDto` in this case) in order to copy the value to the target object. Otherwise, it doesn't copy the value even if it does exists in the source object (`identityUser` in this example). There are some ways to overload this restriction. + +#### MappingPropertyDefinitionChecks + +`MapExtraPropertiesTo` gets an additional parameter to control the definition check for a single mapping operation: + +````csharp +identityUser.MapExtraPropertiesTo( + identityUserDto, + MappingPropertyDefinitionChecks.None +); +```` + +> Be careful since `MappingPropertyDefinitionChecks.None` copies all extra properties without any check. `MappingPropertyDefinitionChecks` enum has other members too. + +If you want to completely disable definition check for a property, you can do it while defining the extra property (or update an existing definition) as shown below: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "SocialSecurityNumber", + options => + { + options.CheckPairDefinitionOnMapping = false; + }); +```` + +#### Ignored Properties + +You may want to ignore some properties on a specific mapping operation: + +````csharp +identityUser.MapExtraPropertiesTo( + identityUserDto, + ignoredProperties: new[] {"MySensitiveProp"} +); +```` + +Ignored properties are not copied to the target object. + +#### AutoMapper Integration + +If you're using the [AutoMapper](https://automapper.org/) library, the ABP Framework also provides an extension method to utilize the `MapExtraPropertiesTo` method defined above. + +You can use the `MapExtraProperties()` method inside your mapping profile. + +````csharp +public class MyProfile : Profile +{ + public MyProfile() + { + CreateMap() + .MapExtraProperties(); + } +} +```` + +It has the same parameters with the `MapExtraPropertiesTo` method. + +## Entity Framework Core Database Mapping + +If you're using the EF Core, you can map an extra property to a table field in the database. Example: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "SocialSecurityNumber", + options => + { + options.MapEfCore(b => b.HasMaxLength(32)); + } + ); +```` + +See the [Entity Framework Core Integration document](Entity-Framework-Core.md) for more. \ No newline at end of file diff --git a/docs/en/Object-To-Object-Mapping.md b/docs/en/Object-To-Object-Mapping.md index 52260402f1..b7463607e9 100644 --- a/docs/en/Object-To-Object-Mapping.md +++ b/docs/en/Object-To-Object-Mapping.md @@ -145,6 +145,23 @@ options.AddProfile(validate: true); > If you have multiple profiles and need to enable validation only for a few of them, first use `AddMaps` without validation, then use `AddProfile` for each profile you want to validate. +### Mapping the Object Extensions + +[Object extension system](Object-Extensions.md) allows to define extra properties for existing classes. ABP Framework provides a mapping definition extension to properly map extra properties of two objects. + +````csharp +public class MyProfile : Profile +{ + public MyProfile() + { + CreateMap() + .MapExtraProperties(); + } +} +```` + +It is suggested to use the `MapExtraProperties()` method if both classes are extensible objects (implement the `IHasExtraProperties` interface). See the [object extension document](Object-Extensions.md) for more. + ## Advanced Topics ### IObjectMapper Interface diff --git a/docs/en/Tutorials/Angular/Part-I.md b/docs/en/Tutorials/Angular/Part-I.md index 65a7dc5714..2867a3159f 100644 --- a/docs/en/Tutorials/Angular/Part-I.md +++ b/docs/en/Tutorials/Angular/Part-I.md @@ -4,3 +4,5 @@ * [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) * [With Angular UI](../Part-1?UI=NG) + + \ No newline at end of file diff --git a/docs/en/Tutorials/Angular/Part-II.md b/docs/en/Tutorials/Angular/Part-II.md index 65a7dc5714..2867a3159f 100644 --- a/docs/en/Tutorials/Angular/Part-II.md +++ b/docs/en/Tutorials/Angular/Part-II.md @@ -4,3 +4,5 @@ * [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) * [With Angular UI](../Part-1?UI=NG) + + \ No newline at end of file diff --git a/docs/en/Tutorials/Angular/Part-III.md b/docs/en/Tutorials/Angular/Part-III.md index 65a7dc5714..2867a3159f 100644 --- a/docs/en/Tutorials/Angular/Part-III.md +++ b/docs/en/Tutorials/Angular/Part-III.md @@ -4,3 +4,5 @@ * [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) * [With Angular UI](../Part-1?UI=NG) + + \ No newline at end of file diff --git a/docs/en/Tutorials/AspNetCore-Mvc/Part-I.md b/docs/en/Tutorials/AspNetCore-Mvc/Part-I.md index 65a7dc5714..2867a3159f 100644 --- a/docs/en/Tutorials/AspNetCore-Mvc/Part-I.md +++ b/docs/en/Tutorials/AspNetCore-Mvc/Part-I.md @@ -4,3 +4,5 @@ * [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) * [With Angular UI](../Part-1?UI=NG) + + \ No newline at end of file diff --git a/docs/en/Tutorials/AspNetCore-Mvc/Part-II.md b/docs/en/Tutorials/AspNetCore-Mvc/Part-II.md index 65a7dc5714..2867a3159f 100644 --- a/docs/en/Tutorials/AspNetCore-Mvc/Part-II.md +++ b/docs/en/Tutorials/AspNetCore-Mvc/Part-II.md @@ -4,3 +4,5 @@ * [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) * [With Angular UI](../Part-1?UI=NG) + + \ No newline at end of file diff --git a/docs/en/Tutorials/AspNetCore-Mvc/Part-III.md b/docs/en/Tutorials/AspNetCore-Mvc/Part-III.md index 65a7dc5714..2867a3159f 100644 --- a/docs/en/Tutorials/AspNetCore-Mvc/Part-III.md +++ b/docs/en/Tutorials/AspNetCore-Mvc/Part-III.md @@ -4,3 +4,5 @@ * [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) * [With Angular UI](../Part-1?UI=NG) + + \ No newline at end of file diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md index a32bf8d8a9..0ff28f2c47 100644 --- a/docs/en/Tutorials/Part-1.md +++ b/docs/en/Tutorials/Part-1.md @@ -22,7 +22,7 @@ end ### About this tutorial: -In this tutorial series, you will build an ABP Commercial application named `Acme.BookStore`. In this sample project, we will manage a list of books and authors. **{{DB_Text}}** will be used as the ORM provider. And on the front-end side {{UI_Value}} and JavaScript will be used. +In this tutorial series, you will build an ABP application named `Acme.BookStore`. In this sample project, we will manage a list of books and authors. **{{DB_Text}}** will be used as the ORM provider. And on the front-end side {{UI_Value}} and JavaScript will be used. The ASP.NET Core {{UI_Value}} tutorial series consists of 3 parts: @@ -34,14 +34,14 @@ The ASP.NET Core {{UI_Value}} tutorial series consists of 3 parts: ### Creating the project -Create a new project named `Acme.BookStore` where `Acme` is the company name and `BookStore` is the project name. You can check out [creating a new project](../Getting-Started-{{if UI == 'NG'}}Angular{{else}}AspNetCore-MVC{{end}}-Template#creating-a-new-project) document to see how you can create a new project. We will create the project with ABP CLI. But first of all, we need to login to the ABP Platform to create a commercial project. +Create a new project named `Acme.BookStore` where `Acme` is the company name and `BookStore` is the project name. You can check out [creating a new project](../Getting-Started-{{if UI == 'NG'}}Angular{{else}}AspNetCore-MVC{{end}}-Template#creating-a-new-project) document to see how you can create a new project. We will create the project with ABP CLI. #### Create the project -By running the below command, it creates a new ABP Commercial project with the database provider `{{DB_Text}}` and UI option `MVC`. To see the other CLI options, check out [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) document. +By running the below command, it creates a new ABP project with the database provider `{{DB_Text}}` and UI option `MVC`. To see the other CLI options, check out [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) document. ```bash -abp new Acme.BookStore --template app --database-provider {{DB}} --ui {{UI_Text}} +abp new Acme.BookStore --template app --database-provider {{DB}} --ui {{UI_Text}} --mobile none ``` ![Creating project](./images/bookstore-create-project-{{UI_Text}}.png) @@ -1001,11 +1001,13 @@ import { GetBooks } from '../actions/books.actions'; import { Books } from '../models/books'; import { BooksService } from '../../books/shared/books.service'; import { tap } from 'rxjs/operators'; +import { Injectable } from '@angular/core'; @State({ name: 'BooksState', defaults: { books: {} } as Books.State, }) +@Injectable() export class BooksState { @Selector() static getBooks(state: Books.State) { diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md index 08e17e0e3f..9565724cb8 100644 --- a/docs/en/Tutorials/Part-2.md +++ b/docs/en/Tutorials/Part-2.md @@ -574,11 +574,13 @@ import { GetBooks, CreateUpdateBook } from '../actions/books.actions'; //<== add import { Books } from '../models/books'; import { BooksService } from '../../books/shared/books.service'; import { tap } from 'rxjs/operators'; +import { Injectable } from '@angular/core'; @State({ name: 'BooksState', defaults: { books: {} } as Books.State, }) +@Injectable() export class BooksState { @Selector() static getBooks(state: Books.State) { @@ -688,7 +690,7 @@ Open `book-list.component.html` file in `books\book-list` folder and replace the * `abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits. * We added `New book` button to the `AbpContentToolbar`. -Open `book-list.component.` file in `books\book-list` folder and replace the content as below: +Open `book-list.component.ts` file in `books\book-list` folder and replace the content as below: ```js import { Component, OnInit } from '@angular/core'; @@ -1330,11 +1332,13 @@ import { GetBooks, CreateUpdateBook, DeleteBook } from '../actions/books.actions import { Books } from '../models/books'; import { BooksService } from '../../books/shared/books.service'; import { tap } from 'rxjs/operators'; +import { Injectable } from '@angular/core'; @State({ name: 'BooksState', defaults: { books: {} } as Books.State, }) +@Injectable() export class BooksState { @Selector() static getBooks(state: Books.State) { diff --git a/docs/en/UI/Angular/Component-Replacement.md b/docs/en/UI/Angular/Component-Replacement.md index d718b46117..fb85aa476e 100644 --- a/docs/en/UI/Angular/Component-Replacement.md +++ b/docs/en/UI/Angular/Component-Replacement.md @@ -11,15 +11,18 @@ Create a new component that you want to use instead of an ABP component. Add tha Then, open the `app.component.ts` and dispatch the `AddReplaceableComponent` action to replace your component with an ABP component as shown below: ```js -import { ..., AddReplaceableComponent } from '@abp/ng.core'; +import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent action +import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum +import { Store } from '@ngxs/store'; // imported Store +//... export class AppComponent { - constructor(..., private store: Store) {} + constructor(..., private store: Store) {} // injected Store ngOnInit() { this.store.dispatch( new AddReplaceableComponent({ component: YourNewRoleComponent, - key: 'Identity.RolesComponent', + key: eIdentityComponents.Roles, }), ); //... @@ -56,6 +59,7 @@ Open the `app.component.ts` and add the below content: ```js import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent +import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents enum for component keys import { MyApplicationLayoutComponent } from './shared/my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent import { Store } from '@ngxs/store'; // imported Store //... @@ -67,7 +71,7 @@ export class AppComponent { this.store.dispatch( new AddReplaceableComponent({ component: MyApplicationLayoutComponent, - key: 'Theme.ApplicationLayoutComponent', + key: eThemeBasicComponents.ApplicationLayout, }), ); @@ -76,24 +80,6 @@ export class AppComponent { } ``` -### Available Replaceable Components - -| Component key | Description | -| -------------------------------------------------- | --------------------------------------------- | -| Account.LoginComponent | Login page | -| Account.RegisterComponent | Register page | -| Account.ManageProfileComponent | Manage Profile page | -| Account.AuthWrapperComponent | This component wraps register and login pages | -| Account.ChangePasswordComponent | Change password form | -| Account.PersonalSettingsComponent | Personal settings form | -| Account.TenantBoxComponentInputs | Tenant changing box | -| FeatureManagement.FeatureManagementComponent | Features modal | -| Identity.UsersComponent | Users page | -| Identity.RolesComponent | Roles page | -| PermissionManagement.PermissionManagementComponent | Permissions modal | -| SettingManagement.SettingManagementComponent | Setting Management page | -| TenantManagement.TenantsComponent | Tenants page | - ## What's Next? - [Custom Setting Page](./Custom-Setting-Page.md) diff --git a/docs/en/UI/Angular/Confirmation-Service.md b/docs/en/UI/Angular/Confirmation-Service.md new file mode 100644 index 0000000000..a8d2c4c2a7 --- /dev/null +++ b/docs/en/UI/Angular/Confirmation-Service.md @@ -0,0 +1,163 @@ +# Confirmation Popup + +You can use the `ConfirmationService` in @abp/ng.theme.shared package to display a confirmation popup by placing at the root level in your project. + + +## Getting Started + +You do not have to provide the `ConfirmationService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. + + +```js +import { ConfirmationService } from '@abp/ng.theme.shared'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + constructor(private confirmation: ConfirmationService) {} +} +``` + +## Usage + +You can use the `success`, `warn`, `error`, and `info` methods of `ConfirmationService` to display a confirmation popup. + +### How to Display a Confirmation Popup + +```js +const confirmationStatus$ = this.confirmation.success('Message', 'Title'); +``` + +- The `ConfirmationService` methods accept three parameters that are `message`, `title`, and `options`. +- `success`, `warn`, `error`, and `info` methods return an [RxJS Subject](https://rxjs-dev.firebaseapp.com/guide/subject) to listen to confirmation popup closing event. The type of event value is [`Confirmation.Status`](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts#L24) that is an enum. + +### How to Listen Closing Event + +You can subscribe to the confirmation closing event like below: + +```js +import { Confirmation, ConfirmationService } from '@abp/ng.theme.shared'; + +constructor(private confirmation: ConfirmationService) {} + +this.confirmation + .warn('::WillBeDeleted', { key: '::AreYouSure', defaultValue: 'Are you sure?' }) + .subscribe((status: Confirmation.Status) => { + // your code here + }); +``` + + +- The `message` and `title` parameters accept a string, localization key or localization object. See the [localization document](./Localization.md) +- `Confirmation.Status` is an enum and has three properties; + - `Confirmation.Status.confirm` is a closing event value that will be emitted when the popup is closed by the confirm button. + - `Confirmation.Status.reject` is a closing event value that will be emitted when the popup is closed by the cancel button. + - `Confirmation.Status.dismiss` is a closing event value that will be emitted when the popup is closed by pressing the escape. + + +If you are not interested in the confirmation status, you do not have to subscribe to the returned observable: + +```js +this.confirmation.error('You are not authorized.', 'Error'); +``` + +### How to Display a Confirmation Popup With Given Options + +Options can be passed as the third parameter to `success`, `warn`, `error`, and `info` methods: + +```js +const options: Partial = { + hideCancelBtn: false, + hideYesBtn: false, + cancelText: 'Close', + yesText: 'Confirm', + messageLocalizationParams: ['Demo'], + titleLocalizationParams: [], +}; + +this.confirmation.warn( + 'AbpIdentity::RoleDeletionConfirmationMessage', + 'Are you sure?', + options, +); +``` + +- `hideCancelBtn` option hides the cancellation button when `true`. Default value is `false` +- `hideYesBtn` option hides the confirmation button when `true`. Default value is `false` +- `cancelText` is the text of the cancellation button. A localization key or localization object can be passed. Default value is `AbpUi::Cancel` +- `yesText` is the text of the confirmation button. A localization key or localization object can be passed. Default value is `AbpUi::Yes` +- `messageLocalizationParams` is the interpolation parameters for the localization of the message. +- `titleLocalizationParams` is the interpolation parameters for the localization of the title. + +With the options above, the confirmation popup looks like this: + +![confirmation](./images/confirmation.png) + +### How to Remove a Confirmation Popup + +The open confirmation popup can be removed manually via the `clear` method: + +```js +this.confirmation.clear(); +``` + +## API + +### success + +```js +success( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): Observable +``` + +> See the [`Config.LocalizationParam` type](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/core/src/lib/models/config.ts#L46) and [`Confirmation` namespace](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts) + + +### warn + +```js +warn( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): Observable +``` + +### error + +```js +error( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): Observable +``` + +### info + +```js +info( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): Observable +``` + +### clear + +```js +clear( + status: Confirmation.Status = Confirmation.Status.dismiss +): void +``` + +- `status` parameter is the value of the confirmation closing event. + + +## What's Next? + +- [Toast Overlay](./Toaster-Service.md) diff --git a/docs/en/UI/Angular/Container-Strategy.md b/docs/en/UI/Angular/Container-Strategy.md new file mode 100644 index 0000000000..3610c5ddd8 --- /dev/null +++ b/docs/en/UI/Angular/Container-Strategy.md @@ -0,0 +1,101 @@ +# ContainerStrategy + +`ContainerStrategy` is an abstract class exposed by @abp/ng.core package. There are two container strategies extending it: `ClearContainerStrategy` and `InsertIntoContainerStrategy`. Implementing the same methods and properties, both of these strategies help you define how your containers will be prepared and where your content will be projected. + + + +## API + +`ClearContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **clear a container before projecting content in it**. + + +### constructor + +```js +constructor( + public containerRef: ViewContainerRef, + private index?: number, // works only in InsertIntoContainerStrategy +) +``` + +- `containerRef` is the `ViewContainerRef` that will be used when projecting the content. + + +### getIndex + +```js +getIndex(): number +``` + +This method return the given index clamped by `0` and `length` of the `containerRef`. For strategies without an index, it returns `0`. + + +### prepare + +```js +prepare(): void +``` + +This method is called before content projection. Based on used container strategy, it either clears the container or does nothing (noop). + + + +## ClearContainerStrategy + +`ClearContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **clear a container before projecting content in it**. + + + +## InsertIntoContainerStrategy + +`InsertIntoContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **project your content at a specific node index in the container**. + + + +## Predefined Container Strategies + +Predefined container strategies are accessible via `CONTAINER_STRATEGY` constant. + + +### Clear + +```js +CONTAINER_STRATEGY.Clear(containerRef: ViewContainerRef) +``` + +Clears given container before content projection. + + +### Append + +```js +CONTAINER_STRATEGY.Append(containerRef: ViewContainerRef) +``` + +Projected content will be appended to the container. + + +### Prepend + +```js +CONTAINER_STRATEGY.Prepend(containerRef: ViewContainerRef) +``` + +Projected content will be prepended to the container. + + +### Insert + +```js +CONTAINER_STRATEGY.Insert( + containerRef: ViewContainerRef, + index: number, +) +``` + +Projected content will be inserted into to the container at given index (clamped by `0` and `length` of the `containerRef`). + + +## See Also + +- [ProjectionStrategy](./Projection-Strategy.md) diff --git a/docs/en/UI/Angular/Content-Projection-Service.md b/docs/en/UI/Angular/Content-Projection-Service.md new file mode 100644 index 0000000000..db7c52be81 --- /dev/null +++ b/docs/en/UI/Angular/Content-Projection-Service.md @@ -0,0 +1,78 @@ +# Content Projection + +You can use the `ContentProjectionService` in @abp/ng.core package in order to project content in an easy and explicit way. + +## Getting Started + +You do not have to provide the `ContentProjectionService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. + +```js +import { ContentProjectionService } from '@abp/ng.core'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + constructor(private contentProjectionService: ContentProjectionService) {} +} +``` + +## Usage + +You can use the `projectContent` method of `ContentProjectionService` to render components and templates dynamically in your project. + +### How to Project Components to Root Level + +If you pass a `RootComponentProjectionStrategy` as the first parameter of `projectContent` method, the `ContentProjectionService` will resolve the projected component and place it at the root level. If provided, it will also pass the component a context. + +```js +const strategy = PROJECTION_STRATEGY.AppendComponentToBody( + SomeOverlayComponent, + { someOverlayProp: "SOME_VALUE" } +); + +const componentRef = this.contentProjectionService.projectContent(strategy); +``` + +In the example above, `SomeOverlayComponent` component will placed at the **end** of `` and a `ComponentRef` will be returned. Additionally, the given context will be applied, so `someOverlayProp` of the component will be set to `SOME_VALUE`. + +> You should keep the returned `ComponentRef` instance, as it is a reference to the projected component and you will need that reference to destroy the projected view and the component instance. + +### How to Project Components and Templates into a Container + +If you pass a `ComponentProjectionStrategy` or `TemplateProjectionStrategy` as the first parameter of `projectContent` method, and a `ViewContainerRef` as the second parameter of that strategy, the `ContentProjectionService` will project the component or template to the given container. If provided, it will also pass the component or the template a context. + +```js +const strategy = PROJECTION_STRATEGY.ProjectComponentToContainer( + SomeComponent, + viewContainerRefOfTarget, + { someProp: "SOME_VALUE" } +); + +const componentRef = this.contentProjectionService.projectContent(strategy); +``` + +In this example, the `viewContainerRefOfTarget`, which is a `ViewContainerRef` instance, will be cleared and `SomeComponent` component will be placed inside it. In addition, the given context will be applied and `someProp` of the component will be set to `SOME_VALUE`. + +> You should keep the returned `ComponentRef` or `EmbeddedViewRef`, as they are a reference to the projected content and you will need them to destroy it when necessary. + +Please refer to [ProjectionStrategy](./Projection-Strategy.md) to see all available projection strategies and how you can build your own projection strategy. + +## API + +### projectContent + +```js +projectContent | TemplateRef>( + projectionStrategy: ProjectionStrategy, + injector = this.injector, +): ComponentRef | EmbeddedViewRef +``` + +- `projectionStrategy` parameter is the primary focus here and is explained above. +- `injector` parameter is the `Injector` instance you can pass to the projected content. It is not used in `TemplateProjectionStrategy`. + + +## What's Next? + +- [TrackByService](./Track-By-Service.md) diff --git a/docs/en/UI/Angular/Context-Strategy.md b/docs/en/UI/Angular/Context-Strategy.md new file mode 100644 index 0000000000..a474c50ad6 --- /dev/null +++ b/docs/en/UI/Angular/Context-Strategy.md @@ -0,0 +1,117 @@ +# ContextStrategy + +`ContextStrategy` is an abstract class exposed by @abp/ng.core package. There are three context strategies extending it: `ComponentContextStrategy`, `TemplateContextStrategy`, and `NoContextStrategy`. Implementing the same methods and properties, all of these strategies help you define how projected content will get their context. + + + +## ComponentContextStrategy + +`ComponentContextStrategy` is a class that extends `ContextStrategy`. It lets you **pass context to a projected component**. + + +### constructor + +```js +constructor(public context: Partial>) {} +``` + +- `T` refers to component type here, i.e. `Type`. +- `InferredInstanceOf` is a utility type exposed by @abp/ng.core package. It infers component shape. +- `context` will be mapped to properties of the projected component. + + +### setContext + +```js +setContext(componentRef: ComponentRef>): Partial> +``` + +This method maps each prop of the context to the component property with the same name and calls change detection. It returns the context after mapping. + + + +## TemplateContextStrategy + +`TemplateContextStrategy` is a class that extends `ContextStrategy`. It lets you **pass context to a projected template**. + + +### constructor + +```js +constructor(public context: Partial>) {} +``` + +- `T` refers to template context type here, i.e. `TemplateRef`. +- `InferredContextOf` is a utility type exposed by @abp/ng.core package. It infers context shape. +- `context` will be mapped to properties of the projected template. + + +### setContext + +```js +setContext(): Partial> +``` + +This method does nothing and only returns the context, because template context is not mapped but passed in as parameter to `createEmbeddedView` method. + + + +## NoContextStrategy + +`NoContextStrategy` is a class that extends `ContextStrategy`. It lets you **skip passing any context to projected content**. + + +### constructor + +```js +constructor() +``` + +Unlike other context strategies, `NoContextStrategy` contructor takes no parameters. + + +### setContext + +```js +setContext(): undefined +``` + +Since there is no context, this method gets no parameters and will return `undefined`. + + + +## Predefined Context Strategies + +Predefined context strategies are accessible via `CONTEXT_STRATEGY` constant. + + +### None + +```js +CONTEXT_STRATEGY.None() +``` + +This strategy will not pass any context to the projected content. + + +### Component + +```js +CONTEXT_STRATEGY.Component(context: Partial>) +``` + +This strategy will help you pass the given context to the projected component. + + +### Template + +```js +CONTEXT_STRATEGY.Template(context: Partial>) +``` + +This strategy will help you pass the given context to the projected template. + + +## See Also + +- [ProjectionStrategy](./Projection-Strategy.md) diff --git a/docs/en/UI/Angular/Dom-Insertion-Service.md b/docs/en/UI/Angular/Dom-Insertion-Service.md index 5d7714d948..88f30136b3 100644 --- a/docs/en/UI/Angular/Dom-Insertion-Service.md +++ b/docs/en/UI/Angular/Dom-Insertion-Service.md @@ -1,8 +1,7 @@ -# How to Insert Scripts and Styles +# Dom Insertion (of Scripts and Styles) You can use the `DomInsertionService` in @abp/ng.core package in order to insert scripts and styles in an easy and explicit way. - ## Getting Started You do not have to provide the `DomInsertionService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. @@ -20,8 +19,7 @@ class DemoComponent { ## Usage -You can use the `insertContent` method of `DomInsertionService` to create a `` element will place at the **end** of ``. +In the example above, `` element will place at the **end** of `` and `scriptElement` will be an `HTMLScriptElement`. Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy. +> Important Note: `DomInsertionService` does not insert the same content twice. In order to add a content again, you first should remove the old content using `removeContent` method. ### How to Insert Styles @@ -63,29 +62,79 @@ class DemoComponent { constructor(private domInsertionService: DomInsertionService) {} ngOnInit() { - this.domInsertionService.insertContent( + const styleElement = this.domInsertionService.insertContent( CONTENT_STRATEGY.AppendStyleToHead('body {margin: 0;}') ); } } ``` -In the example above, `` element will place at the **end** of ``. +In the example above, `` element will place at the **end** of `` and `styleElement` will be an `HTMLStyleElement`. Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy. +> Important Note: `DomInsertionService` does not insert the same content twice. In order to add a content again, you first should remove the old content using `removeContent` method. + +### How to Remove Inserted Scripts & Styles + +If you pass the inserted `HTMLScriptElement` or `HTMLStyleElement` element as the first parameter of `removeContent` method, the `DomInsertionService` will remove the given element. + +```js +import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + private styleElement: HTMLStyleElement; + + constructor(private domInsertionService: DomInsertionService) {} + + ngOnInit() { + this.styleElement = this.domInsertionService.insertContent( + CONTENT_STRATEGY.AppendStyleToHead('body {margin: 0;}') + ); + } + + ngOnDestroy() { + this.domInsertionService.removeContent(this.styleElement); + } +} +``` + +In the example above, `` element **will be removed** from `` when the component is destroyed. ## API ### insertContent ```js -insertContent(strategy: ContentStrategy): void +insertContent( + contentStrategy: ContentStrategy, +): T +``` + +- `contentStrategy` parameter is the primary focus here and is explained above. +- returns `HTMLScriptElement` or `HTMLStyleElement` based on given strategy. + +### removeContent + +```js +removeContent(element: HTMLScriptElement | HTMLStyleElement): void +``` + +- `element` parameter is the inserted `HTMLScriptElement` or `HTMLStyleElement` element, which was returned by `insertContent` method. + +### has + +```js +has(content: string): boolean ``` -`strategy` parameter is the primary focus here and is explained above. +The `has` method returns a boolean value that indicates the given content has already been added to the DOM or not. +- `content` parameter is the content of the inserted `HTMLScriptElement` or `HTMLStyleElement` element. ## What's Next? -- [TrackByService](./Track-By-Service.md) +- [ContentProjectionService](./Content-Projection-Service.md) diff --git a/docs/en/UI/Angular/Dom-Strategy.md b/docs/en/UI/Angular/Dom-Strategy.md index 2318e13205..e7b6c68b0f 100644 --- a/docs/en/UI/Angular/Dom-Strategy.md +++ b/docs/en/UI/Angular/Dom-Strategy.md @@ -87,3 +87,4 @@ DOM_STRATEGY.BeforeElement(target: HTMLElement) - [LazyLoadService](./Lazy-Load-Service.md) - [LoadingStrategy](./Loading-Strategy.md) - [ContentStrategy](./Content-Strategy.md) +- [ProjectionStrategy](./Projection-Strategy.md) diff --git a/docs/en/UI/Angular/Http-Requests.md b/docs/en/UI/Angular/HTTP-Requests.md similarity index 99% rename from docs/en/UI/Angular/Http-Requests.md rename to docs/en/UI/Angular/HTTP-Requests.md index 71878c8045..2ac5aa1b49 100644 --- a/docs/en/UI/Angular/Http-Requests.md +++ b/docs/en/UI/Angular/HTTP-Requests.md @@ -206,4 +206,4 @@ You may find `Rest.Observe` enum [here](https://github.com/abpframework/abp/blob ## What's Next? -* [Localization](./Localization.md) \ No newline at end of file +* [Localization](./Localization.md) diff --git a/docs/en/UI/Angular/Lazy-Load-Service.md b/docs/en/UI/Angular/Lazy-Load-Service.md index a03381869e..b4eddf7b25 100644 --- a/docs/en/UI/Angular/Lazy-Load-Service.md +++ b/docs/en/UI/Angular/Lazy-Load-Service.md @@ -52,7 +52,7 @@ class DemoComponent { The `load` method returns an observable to which you can subscibe in your component or with an `async` pipe. In the example above, the `NgIf` directive will render `` only **if the script gets successfully loaded or is already loaded before**. -> You can subscribe multiple times in your template with `async` pipe. The styles will only be loaded once. +> You can subscribe multiple times in your template with `async` pipe. The Scripts will only be loaded once. Please refer to [LoadingStrategy](./Loading-Strategy.md) to see all available loading strategies and how you can build your own loading strategy. diff --git a/docs/en/UI/Angular/Loading-Strategy.md b/docs/en/UI/Angular/Loading-Strategy.md index 1a7e7b362f..5322d13eda 100644 --- a/docs/en/UI/Angular/Loading-Strategy.md +++ b/docs/en/UI/Angular/Loading-Strategy.md @@ -57,7 +57,7 @@ This method creates and returns an observable stream that emits on success and t ## Predefined Loading Strategies -Predefined content security strategies are accessible via `LOADING_STRATEGY` constant. +Predefined loading strategies are accessible via `LOADING_STRATEGY` constant. ### AppendAnonymousScriptToHead diff --git a/docs/en/UI/Angular/Permission-Management.md b/docs/en/UI/Angular/Permission-Management.md index ababd25e7f..d86e8c96b2 100644 --- a/docs/en/UI/Angular/Permission-Management.md +++ b/docs/en/UI/Angular/Permission-Management.md @@ -76,4 +76,4 @@ Granted Policies are stored in the `auth` property of `ConfigState`. ## What's Next? -* [Config State](./Config-State.md) \ No newline at end of file +- [Confirmation Popup](./Confirmation-Service.md) \ No newline at end of file diff --git a/docs/en/UI/Angular/Projection-Strategy.md b/docs/en/UI/Angular/Projection-Strategy.md new file mode 100644 index 0000000000..4d546566b3 --- /dev/null +++ b/docs/en/UI/Angular/Projection-Strategy.md @@ -0,0 +1,200 @@ +# ProjectionStrategy + +`ProjectionStrategy` is an abstract class exposed by @abp/ng.core package. There are three projection strategies extending it: `ComponentProjectionStrategy`, `RootComponentProjectionStrategy`, and `TemplateProjectionStrategy`. Implementing the same methods and properties, all of these strategies help you define how your content projection will work. + + + +## ComponentProjectionStrategy + +`ComponentProjectionStrategy` is a class that extends `ProjectionStrategy`. It lets you **project a component into a container**. + + +### constructor + +```js +constructor( + component: T, + private containerStrategy: ContainerStrategy, + private contextStrategy?: ContextStrategy, +) +``` + +- `component` is class of the component you would like to project. +- `containerStrategy` is the `ContainerStrategy` that will be used when projecting the component. +- `contextStrategy` is the `ContextStrategy` that will be used on the projected component. (_default: None_) + +Please refer to [ContainerStrategy](./Container-Strategy.md) and [ContextStrategy](./Context-Strategy.md) documentation for their usage. + + +### injectContent + +```js +injectContent(injector: Injector): ComponentRef +``` + +This method prepares the container, resolves the component, sets its context, and projects it to the container. It returns a `ComponentRef` instance, which you should keep in order to clear projected components later on. + + + +## RootComponentProjectionStrategy + +`RootComponentProjectionStrategy` is a class that extends `ProjectionStrategy`. It lets you **project a component into the document**, such as appending it to ``. + + +### constructor + +```js +constructor( + component: T, + private contextStrategy?: ContextStrategy, + private domStrategy?: DomStrategy, +) +``` + +- `component` is class of the component you would like to project. +- `contextStrategy` is the `ContextStrategy` that will be used on the projected component. (_default: None_) +- `domStrategy` is the `DomStrategy` that will be used when inserting component. (_default: AppendToBody_) + +Please refer to [ContextStrategy](./Context-Strategy.md) and [DomStrategy](./Dom-Strategy.md) documentation for their usage. + + +### injectContent + +```js +injectContent(injector: Injector): ComponentRef +``` + +This method resolves the component, sets its context, and projects it to the document. It returns a `ComponentRef` instance, which you should keep in order to clear projected components later on. + + + +## TemplateProjectionStrategy + +`TemplateProjectionStrategy` is a class that extends `ProjectionStrategy`. It lets you **project a template into a container**. + + +### constructor + +```js +constructor( + template: T, + private containerStrategy: ContainerStrategy, + private contextStrategy?: ContextStrategy, +) +``` + +- `template` is `TemplateRef` you would like to project. +- `containerStrategy` is the `ContainerStrategy` that will be used when projecting the component. +- `contextStrategy` is the `ContextStrategy` that will be used on the projected component. (_default: None_) + +Please refer to [ContainerStrategy](./Container-Strategy.md) and [ContextStrategy](./Context-Strategy.md) documentation for their usage. + + +### injectContent + +```js +injectContent(): EmbeddedViewRef +``` + +This method prepares the container, and projects the template together with the defined context to it. It returns an `EmbeddedViewRef`, which you should keep in order to clear projected templates later on. + + + +## Predefined Projection Strategies + +Predefined projection strategies are accessible via `PROJECTION_STRATEGY` constant. + + +### AppendComponentToBody + +```js +PROJECTION_STRATEGY.AppendComponentToBody( + component: T, + contextStrategy?: ComponentContextStrategy, +) +``` + +Sets given context to the component and places it at the **end** of `` tag in the document. + + +### AppendComponentToContainer + +```js +PROJECTION_STRATEGY.AppendComponentToContainer( + component: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, +) +``` + +Sets given context to the component and places it at the **end** of the container. + + +### AppendTemplateToContainer + +```js +PROJECTION_STRATEGY.AppendTemplateToContainer( + templateRef: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, +) +``` + +Sets given context to the template and places it at the **end** of the container. + + +### PrependComponentToContainer + +```js +PROJECTION_STRATEGY.PrependComponentToContainer( + component: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, +) +``` + +Sets given context to the component and places it at the **beginning** of the container. + + +### PrependTemplateToContainer + +```js +PROJECTION_STRATEGY.PrependTemplateToContainer( + templateRef: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, +) +``` + +Sets given context to the template and places it at the **beginning** of the container. + + +### ProjectComponentToContainer + +```js +PROJECTION_STRATEGY.ProjectComponentToContainer( + component: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, +) +``` + +Clears the container, sets given context to the component, and places it **in the cleared** the container. + + +### ProjectTemplateToContainer + +```js +PROJECTION_STRATEGY.ProjectTemplateToContainer( + templateRef: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, +) +``` + +Clears the container, sets given context to the template, and places it **in the cleared** the container. + + +## See Also + +- [DomInsertionService](./Dom-Insertion-Service.md) diff --git a/docs/en/UI/Angular/Toaster-Service.md b/docs/en/UI/Angular/Toaster-Service.md new file mode 100644 index 0000000000..191f0b34fd --- /dev/null +++ b/docs/en/UI/Angular/Toaster-Service.md @@ -0,0 +1,157 @@ +# Toast Overlay + +You can use the `ToasterService` in @abp/ng.theme.shared package to display messages in an overlay by placing at the root level in your project. + + +## Getting Started + +You do not have to provide the `ToasterService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. + + +```js +import { ToasterService } from '@abp/ng.theme.shared'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + constructor(private toaster: ToasterService) {} +} +``` + +## Usage + +You can use the `success`, `warn`, `error`, and `info` methods of `ToasterService` to display an overlay. + +### How to Display a Toast Overlay + +```js +this.toast.success('Message', 'Title'); +``` + +- The `ToasterService` methods accept three parameters that are `message`, `title`, and `options`. +- `success`, `warn`, `error`, and `info` methods return the id of opened toast overlay. The toast can be removed with this id. + +### How to Display a Toast Overlay With Given Options + +Options can be passed as the third parameter to `success`, `warn`, `error`, and `info` methods: + +```js +import { Toaster, ToasterService } from '@abp/ng.theme.shared'; +//... + +constructor(private toaster: ToasterService) {} + +//... +const options: Partial = { + life: 10000, + sticky: false, + closable: true, + tapToDismiss: true, + messageLocalizationParams: ['Demo', '1'], + titleLocalizationParams: [] + }; + + this.toaster.error('AbpUi::EntityNotFoundErrorMessage', 'AbpUi::Error', options); +``` + +- `life` option is the closing time in milliseconds. Default value is `5000`. +- `sticky` option keeps toast overlay on the screen by ignoring the `life` option when `true`. Default value is `false`. +- `closable` option displays the close icon on the toast overlay when it is `true`. Default value is `true`. +- `tapToDismiss` option, when `true`, allows closing the toast overlay by clicking over it. Default value is `false`. +- `yesText` is the text of the confirmation button. A localization key or localization object can be passed. Default value is `AbpUi::Yes`. +- `messageLocalizationParams` is the interpolation parameters for the localization of the message. +- `titleLocalizationParams` is the interpolation parameters for the localization of the title. + +With the options above, the toast overlay looks like this: + +![toast](./images/toast.png) + +### How to Remove a Toast Overlay + +The open toast overlay can be removed manually via the `remove` method by passing the `id` of toast: + +```js +const toastId = this.toast.success('Message', 'Title') + +this.toast.remove(toastId); +``` + +### How to Remove All Toasts + +The all open toasts can be removed manually via the `clear` method: + +```js +this.toast.clear(); +``` + +## API + +### success + +```js +success( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): number +``` + +- `Config` namespace can be imported from `@abp/ng.core`. +- `Toaster` namespace can be imported from `@abp/ng.theme.shared`. + +> See the [`Config.LocalizationParam` type](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/core/src/lib/models/config.ts#L46) and [`Toaster` namespace](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/toaster.ts) + + +### warn + +```js +warn( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): number +``` + +### error + +```js +error( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): number +``` + +### info + +```js +info( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): number +``` + +### remove + +```js +remove(id: number): void +``` + +Removes an open toast by the given id. + +### clear + +```js +clear(): void +``` + +Removes all open toasts. + +## See Also +- [Confirmation Popup](./Confirmation-Service.md) + +## What's Next? + +- [Config State](./Config-State.md) diff --git a/docs/en/UI/Angular/images/confirmation.png b/docs/en/UI/Angular/images/confirmation.png new file mode 100644 index 0000000000..efe4c98ea7 Binary files /dev/null and b/docs/en/UI/Angular/images/confirmation.png differ diff --git a/docs/en/UI/Angular/images/toast.png b/docs/en/UI/Angular/images/toast.png new file mode 100644 index 0000000000..24cdd0fe0c Binary files /dev/null and b/docs/en/UI/Angular/images/toast.png differ diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md b/docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md index 6d700f030d..5b4b766a27 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md @@ -1,3 +1,277 @@ -## Dynamic Forms +# Dynamic Forms + +`Warning:` Before getting into this document, be sure that you have clearly understood [abp form elements](Form-elements.md) document. + +## Introduction + +`abp-dynamic-form` creates a bootstrap form for a given c# model. + +Basic usage: + +````xml + +```` +Model: +````csharp +public class DynamicFormsModel : PageModel + { + [BindProperty] + public DetailedModel MyDetailedModel { get; set; } + + public List CountryList { get; set; } = new List + { + 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() { "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 NeighborCountries { get; set; } + } + + public enum CarType + { + Sedan, + Hatchback, + StationWagon, + Coupe + } + } +```` +## Demo + +See the [dynamic forms demo page](https://bootstrap-taghelpers.abp.io/Components/DynamicForms) to see it in action. + +## Attributes + +### abp-model + +Sets the c# model for dynamic form. Properties of this modal are turned into inputs in the form. + +### submit-button + +Can be `True` or `False`. + +If `True`, a submit button will be generated at the bottom of the form. + +Default value is `False`. + +### required-symbols + +Can be `True` or `False`. + +If `True`, required inputs will have a symbol (*) that indicates they are required. + +Default value is `True`. + +## Form Content Placement + +By default, `abp-dynamicform` clears the inner html and places the inputs into itself. If you want to add additional content to dynamic form or place the inputs to some specific area, you can use ` ` tag. This tag will be replaced by form content and rest of the inner html of `abp-dynamic-form` tag will be unchanged. + +Usage: + +````xml + +
+ Some content.... +
+
+ +
+
+ Some more content.... +
+
+```` + +## Input Order + +`abp-dynamic-form` orders the properties by their `DisplayOrder` attribute and then their property order in model class. + +Default `DisplayOrder` attribute number is 10000 for every property. + +See example below: + +````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; } + } +```` + +In this example, input fields will be displayed with this order: `City` > `EmailAddress` > `PhoneNumber` > `Name` > `Surname`. + +## Ignoring a property + +By default, `abp-dynamic-form` generates input for every property in model class. If you want to ignore a property, use `DynamicFormIgnore` attribute. + +See example below: + +````csharp + public class MyModel + { + public string Name { get; set; } + + [DynamicFormIgnore] + public string Surname { get; set; } + } +```` + +In this example, no input will be generated for `Surname` property. + +## Indicating Text box, Radio Group and Combobox + +If you have read the [Form elements document](Form-elements.md), you noticed that `abp-radio` and `abp-select` tags are very similar on c# model. So we have to use `[AbpRadioButton()]` attribute to tell `abp-dynamic-form` which of your properties will be radio group and which will be combobox. See example below: + +````xml + +```` +Model: +````csharp +public class DynamicFormsModel : PageModel + { + [BindProperty] + public DetailedModel MyDetailedModel { get; set; } + + public List CountryList { get; set; } = new List + { + 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 + } + } +```` + +As you see in example above: + +* If `[AbpRadioButton()]` are used on a **Enum** property, it will be a radio group. Otherwise, combobox. +* If `[SelectItems()]` and `[AbpRadioButton()]` are used on a property, it will be a radio group. +* If just `[SelectItems()]` is used on a property, it will be a combobox. +* If none of these attributes are used on a property, it will be a text box. + +## Localization + +`abp-dynamic-form` handles localization as well. + +By default, it will try to find "DisplayName:{PropertyName}" or "{PropertyName}" localization keys and set the localization value as input label. + +You can set it yourself by using `[Display()]` attribute of Asp.Net Core. You can use a localization key in this attribute. See example below: + +````csharp + [Display(Name = "Name")] + public string Name { get; set; } +```` + + + + + + -This is not documented yet. You can see a [demo](http://bootstrap-taghelpers.abp.io/Components/DynamicForms) for now. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Form-elements.md b/docs/en/UI/AspNetCore/Tag-Helpers/Form-elements.md new file mode 100644 index 0000000000..df23fd649c --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Form-elements.md @@ -0,0 +1,261 @@ +# Form Elements + +## Introduction + +Abp provides form input tag helpers to make building forms easier. + +## Demo + +See the [form elements demo page](https://bootstrap-taghelpers.abp.io/Components/FormElements) to see it in action. + +## abp-input + +`abp-input` tag creates a Bootstrap form input for a given c# property. It uses [Asp.Net Core Input Tag Helper](https://docs.microsoft.com/tr-tr/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-3.1#the-input-tag-helper) in background, so every data annotation attribute of `input` tag helper of Asp.Net Core is also valid for `abp-input`. + +Usage: + +````xml + + + + +```` + +Model: + +````csharp + public class FormElementsModel : PageModel + { + public SampleModel MyModel { get; set; } + + public void OnGet() + { + MyModel = new SampleModel(); + } + + public class SampleModel + { + [Required] + [Placeholder("Enter your name...")] + [InputInfoText("What is your name?")] + public string Name { get; set; } + + [Required] + [FormControlSize(AbpFormControlSize.Large)] + public string SurName { get; set; } + + [TextArea(Rows = 4)] + public string Description { get; set; } + + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } + + public bool IsActive { get; set; } + } + } +```` + +### Attributes + +You can set some of the attributes on your c# property, or directly on html tag. If you are going to use this property in a [abp-dynamic-form](Dynamic-forms.md), then you can only set these properties via property attributes. + +#### Property Attributes + +- `[TextArea()]`: Converts the input into a text area. + +* `[Placeholder()]`: Sets placeholder for input. You can use a localization key directly. +* `[InputInfoText()]`: Sets a small info text for input. You can use a localization key directly. +* `[FormControlSize()]`: Sets size of form-control wrapper element. Available values are + - `AbpFormControlSize.Default` + - `AbpFormControlSize.Small` + - `AbpFormControlSize.Medium` + - `AbpFormControlSize.Large` +* `[DisabledInput]` : Input is disabled. +* `[ReadOnlyInput]`: Input is read-only. + +#### Tag Attributes + +* `info`: Sets a small info text for input. You can use a localization key directly. +* `auto-focus`: If true, browser auto focuses on the element. +* `size`: Sets size of form-control wrapper element. Available values are + - `AbpFormControlSize.Default` + - `AbpFormControlSize.Small` + - `AbpFormControlSize.Medium` + - `AbpFormControlSize.Large` +* `disabled`: Input is disabled. +* `readonly`: Input is read-only. +* `label`: Sets the label for input. +* `display-required-symbol`: Adds the required symbol (*) to label if input is required. Default `True`. + +### Label & Localization + +You can set label of your input in different ways: + +- You can use `Label` attribute and directly set the label. But it doesn't auto localize your localization key. So use it as `label="@L["{LocalizationKey}"].Value"`. +- You can set it using `[Display(name="{LocalizationKey}")]` attribute of Asp.Net Core. +- You can just let **abp** find the localization key for the property. It will try to find "DisplayName:{PropertyName}" or "{PropertyName}" localization keys, if `label` or `[DisplayName]` attributes are not set. + +## abp-select + +`abp-select` tag creates a Bootstrap form select for a given c# property. It uses [Asp.Net Core Select Tag Helper](https://docs.microsoft.com/tr-tr/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-3.1#the-select-tag-helper) in background, so every data annotation attribute of `select` tag helper of Asp.Net Core is also valid for `abp-select`. + +`abp-select` tag needs a list of `Microsoft.AspNetCore.Mvc.Rendering.SelectListItem ` to work. It can be provided by `asp-items` attriube on the tag or `[SelectItems()]` attribute on c# property. (if you are using [abp-dynamic-form](Dynamic-forms.md), c# attribute is the only way.) + +`abp-select` supports multiple selection. + +`abp-select` auto-creates a select list for **Enum** properties. No extra data is needed. If property is nullable, an empty key and value is added to top of the auto-generated list. + +Usage: + +````xml + + + + + + + + + +```` + +Model: + +````csharp + public class FormElementsModel : PageModel + { + public SampleModel MyModel { get; set; } + + public List CityList { get; set; } + + public void OnGet() + { + MyModel = new SampleModel(); + + CityList = new List + { + new SelectListItem { Value = "NY", Text = "New York"}, + new SelectListItem { Value = "LDN", Text = "London"}, + new SelectListItem { Value = "IST", Text = "Istanbul"}, + new SelectListItem { Value = "MOS", Text = "Moscow"} + }; + } + + public class SampleModel + { + public string City { get; set; } + + [SelectItems(nameof(CityList))] + public string AnotherCity { get; set; } + + public List MultipleCities { get; set; } + + public CarType MyCarType { get; set; } + + public CarType? MyNullableCarType { get; set; } + } + + public enum CarType + { + Sedan, + Hatchback, + StationWagon, + Coupe + } + } +```` + +### Attributes + +You can set some of the attributes on your c# property, or directly on html tag. If you are going to use this property in a [abp-dynamic-form](Dynamic-forms.md), then you can only set these properties via property attributes. + +#### Property Attributes + +* `[SelectItems()]`: Sets the select data. Parameter should be the name of the data list. (see example above) + +- `[InputInfoText()]`: Sets a small info text for input. You can use a localization key directly. +- `[FormControlSize()]`: Sets size of form-control wrapper element. Available values are + - `AbpFormControlSize.Default` + - `AbpFormControlSize.Small` + - `AbpFormControlSize.Medium` + - `AbpFormControlSize.Large` + +#### Tag Attributes + +- `asp-items`: Sets the select data. This Should be a list of SelectListItem. +- `info`: Sets a small info text for input. You can use a localization key directly. +- `size`: Sets size of form-control wrapper element. Available values are + - `AbpFormControlSize.Default` + - `AbpFormControlSize.Small` + - `AbpFormControlSize.Medium` + - `AbpFormControlSize.Large` +- `label`: Sets the label for input. +- `display-required-symbol`: Adds the required symbol (*) to label if input is required. Default `True`. + +### Label & Localization + +You can set label of your input in different ways: + +- You can use `Label` attribute and directly set the label. But it doesn't auto localize your localization key. So use it as `label="@L["{LocalizationKey}"].Value".` +- You can set it using `[Display(name="{LocalizationKey}")]` attribute of Asp.Net Core. +- You can just let **abp** find the localization key for the property. It will try to find "DisplayName:{PropertyName}" or "{PropertyName}" localization keys. + +Localizations of combobox values are set by `abp-select` for **Enum** property. It searches for "{EnumTypeName}.{EnumPropertyName}" or "{EnumPropertyName}" localization keys. For instance, in the example above, it will use "CarType.StationWagon" or "StationWagon" keys for localization when it localizes combobox values. + +## abp-radio + +`abp-radio` tag creates a Bootstrap form radio group for a given c# property. Usage is very similar to `abp-select` tag. + +Usage: + +````xml + + + +```` + +Model: + +````csharp + public class FormElementsModel : PageModel + { + public SampleModel MyModel { get; set; } + + public List CityList { get; set; } = new List + { + new SelectListItem { Value = "NY", Text = "New York"}, + new SelectListItem { Value = "LDN", Text = "London"}, + new SelectListItem { Value = "IST", Text = "Istanbul"}, + new SelectListItem { Value = "MOS", Text = "Moscow"} + }; + + public void OnGet() + { + MyModel = new SampleModel(); + MyModel.CityRadio = "IST"; + MyModel.CityRadio2 = "MOS"; + } + + public class SampleModel + { + public string CityRadio { get; set; } + + [SelectItems(nameof(CityList))] + public string CityRadio2 { get; set; } + } + } +```` + +### Attributes + +You can set some of the attributes on your c# property, or directly on html tag. If you are going to use this property in a [abp-dynamic-form](Dynamic-forms.md), then you can only set these properties via property attributes. + +#### Property Attributes + +- `[SelectItems()]`: Sets the select data. Parameter should be the name of the data list. (see example above) + +#### Tag Attributes + +- `asp-items`: Sets the select data. This Should be a list of SelectListItem. +- `Inline`: If true, radio buttons will be in single line, next to each other. If false, they will be under each other. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Index.md b/docs/en/UI/AspNetCore/Tag-Helpers/Index.md index a7aa0b65d2..229460de92 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Index.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Index.md @@ -12,18 +12,18 @@ ABP Framework also adds some **useful features** to the standard bootstrap compo Here, the list of components those are wrapped by the ABP Framework: +* [Alerts](Alerts.md) * [Buttons](Buttons.md) * [Cards](Cards.md) -* [Alerts](Alerts.md) -* [Tabs](Tabs.md) -* [Grids](Grids.md) -* [Modals](Modals.md) * [Collapse](Collapse.md) * [Dropdowns](Dropdowns.md) +* [Grids](Grids.md) * [List Groups](List-Groups.md) +* [Modals](Modals.md) * [Paginator](Paginator.md) * [Popovers](Popovers.md) * [Progress Bars](Progress-Bars.md) +* [Tabs](Tabs.md) * [Tooltips](Tooltips.md) * ... @@ -31,8 +31,8 @@ Here, the list of components those are wrapped by the ABP Framework: ## Form Elements -See [demo](https://bootstrap-taghelpers.abp.io/Components/FormElements). +**Abp Tag Helpers** add new features to standard **Asp.Net Core MVC input & select Tag Helpers** and wrap them with **Bootstrap** form controls. See [Form Elements documentation](Form-elements.md) . -## Dynamic Inputs +## Dynamic Forms -See [demo](https://bootstrap-taghelpers.abp.io/Components/DynamicForms). \ No newline at end of file +**Abp Tag helpers** offer an easy way to build complete **Bootstrap forms**. See [Dynamic Forms documentation](Dynamic-Forms.md). \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/List-Groups.md b/docs/en/UI/AspNetCore/Tag-Helpers/List-Groups.md index b1e6e7f499..08b942bd09 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/List-Groups.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/List-Groups.md @@ -19,7 +19,7 @@ Basic usage: ## Demo -See the [list groups demo page](https://bootstrap-taghelpers.abp.io/Components/ListGroups) to see it in action. +See the [list groups demo page](https://bootstrap-taghelpers.abp.io/Components/ListGroup) to see it in action. ## Attributes diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Progress-Bars.md b/docs/en/UI/AspNetCore/Tag-Helpers/Progress-Bars.md index dc69f7e47e..1f723f80ae 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Progress-Bars.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Progress-Bars.md @@ -26,7 +26,7 @@ Basic usage: ## Demo -See the [progress bars demo page](https://bootstrap-taghelpers.abp.io/Components/Progress-Bars) to see it in action. +See the [progress bars demo page](https://bootstrap-taghelpers.abp.io/Components/Progressbars) to see it in action. ## Attributes @@ -67,4 +67,4 @@ A value indicates if the background style of the progress bar is stripped. Shoul A value indicates if the stripped background style of the progress bar is animated. Should be one of the following values: * `false` (default value) -* `true` \ No newline at end of file +* `true` diff --git a/docs/en/UI/Common/Utils/Linked-List.md b/docs/en/UI/Common/Utils/Linked-List.md index 5da9cc10e9..0d6b259603 100644 --- a/docs/en/UI/Common/Utils/Linked-List.md +++ b/docs/en/UI/Common/Utils/Linked-List.md @@ -2,20 +2,29 @@ -The core module provides a useful data structure known as a [doubly linked list](https://en.wikipedia.org/wiki/Doubly_linked_list). Briefly, a doubly linked list is a series of records (a.k.a. nodes) which has information on the previous node, the next node, and its own value (or data). +The @abp/utils package provides a useful data structure known as a [doubly linked list](https://en.wikipedia.org/wiki/Doubly_linked_list). It is availabe in both Angular (via an import) and MVC (via `abp.utils.common` global object). + +Briefly, a doubly linked list is a series of records (a.k.a. nodes) which has information on the previous node, the next node, and its own value (or data). ## Getting Started -To create a doubly linked list, all you have to do is to import and create a new instance of it: +To create a doubly linked list, all you have to do is to create a new instance of it: + +In Angular: ```js -import { LinkedList } from '@abp/ng.core'; +import { LinkedList } from '@abp/utils'; const list = new LinkedList(); ``` +In MVC: + +```js +var list = new abp.utils.common.LinkedList(); +``` The constructor does not get any parameters. @@ -33,7 +42,7 @@ There are several methods to create new nodes in a linked list and all of them a #### addHead(value) ```js -addHead(value: T): ListNode\ +addHead(value: T): ListNode ``` Adds a node with given value as the first node in list: @@ -57,7 +66,7 @@ list.addHead('c'); #### addManyHead(values) ```js -addManyHead(values: T\[\]): ListNode\\[\] +addManyHead(values: T[]): ListNode[] ``` Adds multiple nodes with given values as the first nodes in list: @@ -77,7 +86,7 @@ list.addManyHead(['x', 'y', 'z']); #### addTail(value) ```js -addTail(value: T): ListNode\ +addTail(value: T): ListNode ``` Adds a node with given value as the last node in list: @@ -101,7 +110,7 @@ list.addTail('c'); #### addManyTail(values) ```js -addManyTail(values: T\[\]): ListNode\\[\] +addManyTail(values: T[]): ListNode[] ``` Adds multiple nodes with given values as the last nodes in list: @@ -118,10 +127,10 @@ list.addManyTail(['x', 'y', 'z']); -#### addAfter(value, previousValue, compareFn) +#### addAfter(value, previousValue [, compareFn]) ```js -addAfter(value: T, previousValue: T, compareFn = compare): ListNode\ +addAfter(value: T, previousValue: T, compareFn?: ListComparisonFn): ListNode ``` Adds a node with given value after the first node that has the previous value: @@ -165,10 +174,10 @@ list.addAfter( -#### addManyAfter(values, previousValue, compareFn) +#### addManyAfter(values, previousValue [, compareFn]) ```js -addManyAfter(values: T\[\], previousValue: T, compareFn = compare): ListNode\\[\] +addManyAfter(values: T[], previousValue: T, compareFn?: ListComparisonFn): ListNode[] ``` Adds multiple nodes with given values after the first node that has the previous value: @@ -207,10 +216,10 @@ list.addManyAfter( -#### addBefore(value, nextValue, compareFn) +#### addBefore(value, nextValue [, compareFn]) ```js -addBefore(value: T, nextValue: T, compareFn = compare): ListNode\ +addBefore(value: T, nextValue: T, compareFn?: ListComparisonFn): ListNode ``` Adds a node with given value before the first node that has the next value: @@ -254,10 +263,10 @@ list.addBefore( -#### addManyBefore(values, nextValue, compareFn) +#### addManyBefore(values, nextValue [, compareFn]) ```js -addManyBefore(values: T\[\], nextValue: T, compareFn = compare): ListNode\\[\] +addManyBefore(values: T[], nextValue: T, compareFn?: ListComparisonFn): ListNode[] ``` Adds multiple nodes with given values before the first node that has the next value: @@ -299,7 +308,7 @@ list.addManyBefore( #### addByIndex(value, position) ```js -addByIndex(value: T, position: number): ListNode\ +addByIndex(value: T, position: number): ListNode ``` Adds a node with given value at the specified position in the list: @@ -337,7 +346,7 @@ list.addByIndex('x', -1); #### addManyByIndex(values, position) ```js -addManyByIndex(values: T\[\], position: number): ListNode\\[\] +addManyByIndex(values: T[], position: number): ListNode[] ``` Adds multiple nodes with given values at the specified position in the list: @@ -371,7 +380,7 @@ list.addManyByIndex(['x', 'y'], -1); #### add(value).head() ```js -add(value: T).head(): ListNode\ +add(value: T).head(): ListNode ``` Adds a node with given value as the first node in list: @@ -399,7 +408,7 @@ list.add('c').head(); #### add(value).tail() ```js -add(value: T).tail(): ListNode\ +add(value: T).tail(): ListNode ``` Adds a node with given value as the last node in list: @@ -424,10 +433,10 @@ list.add('c').tail(); -#### add(value).after(previousValue, compareFn) +#### add(value).after(previousValue [, compareFn]) ```js -add(value: T).after(previousValue: T, compareFn = compare): ListNode\ +add(value: T).after(previousValue: T, compareFn?: ListComparisonFn): ListNode ``` Adds a node with given value after the first node that has the previous value: @@ -471,10 +480,10 @@ list -#### add(value).before(nextValue, compareFn) +#### add(value).before(nextValue [, compareFn]) ```js -add(value: T).before(nextValue: T, compareFn = compare): ListNode\ +add(value: T).before(nextValue: T, compareFn?: ListComparisonFn): ListNode ``` Adds a node with given value before the first node that has the next value: @@ -521,7 +530,7 @@ list #### add(value).byIndex(position) ```js -add(value: T).byIndex(position: number): ListNode\ +add(value: T).byIndex(position: number): ListNode ``` Adds a node with given value at the specified position in the list: @@ -563,7 +572,7 @@ list.add('x').byIndex(-1); #### addMany(values).head() ```js -addMany(values: T\[\]).head(): ListNode\\[\] +addMany(values: T[]).head(): ListNode[] ``` Adds multiple nodes with given values as the first nodes in list: @@ -587,7 +596,7 @@ list.addMany(['x', 'y', 'z']).head(); #### addMany(values).tail() ```js -addMany(values: T\[\]).tail(): ListNode\\[\] +addMany(values: T[]).tail(): ListNode[] ``` Adds multiple nodes with given values as the last nodes in list: @@ -608,10 +617,10 @@ list.addMany(['x', 'y', 'z']).tail(); -#### addMany(values).after(previousValue, compareFn) +#### addMany(values).after(previousValue [, compareFn]) ```js -addMany(values: T\[\]).after(previousValue: T, compareFn = compare): ListNode\\[\] +addMany(values: T[]).after(previousValue: T, compareFn?: ListComparisonFn): ListNode[] ``` Adds multiple nodes with given values after the first node that has the previous value: @@ -650,10 +659,10 @@ list -#### addMany(values).before(nextValue, compareFn) +#### addMany(values).before(nextValue [, compareFn]) ```js -addMany(values: T\[\]).before(nextValue: T, compareFn = compare): ListNode\\[\] +addMany(values: T[]).before(nextValue: T, compareFn?: ListComparisonFn): ListNode[] ``` Adds multiple nodes with given values before the first node that has the next value: @@ -695,7 +704,7 @@ list #### addMany(values).byIndex(position) ```js -addMany(values: T\[\]).byIndex(position: number): ListNode\\[\] +addMany(values: T[]).byIndex(position: number): ListNode[] ``` Adds multiple nodes with given values at the specified position in the list: @@ -739,7 +748,7 @@ There are a few methods to remove nodes from a linked list and all of them are s #### dropHead() ```js -dropHead(): ListNode\ | undefined +dropHead(): ListNode | undefined ``` Removes the first node from the list: @@ -759,7 +768,7 @@ list.dropHead(); #### dropManyHead(count) ```js -dropManyHead(count: number): ListNode\\[\] +dropManyHead(count: number): ListNode[] ``` Removes the first nodes from the list based on given count: @@ -779,7 +788,7 @@ list.dropManyHead(2); #### dropTail() ```js -dropTail(): ListNode\ | undefined +dropTail(): ListNode | undefined ``` Removes the last node from the list: @@ -799,7 +808,7 @@ list.dropTail(); #### dropManyTail(count) ```js -dropManyTail(count: number): ListNode\\[\] +dropManyTail(count: number): ListNode[] ``` Removes the last nodes from the list based on given count: @@ -819,7 +828,7 @@ list.dropManyTail(2); #### dropByIndex(position) ```js -dropByIndex(position: number): ListNode\ | undefined +dropByIndex(position: number): ListNode | undefined ``` Removes the node with the specified position from the list: @@ -853,7 +862,7 @@ list.dropByIndex(-2); #### dropManyByIndex(count, position) ```js -dropManyByIndex(count: number, position: number): ListNode\\[\] +dropManyByIndex(count: number, position: number): ListNode[] ``` Removes the nodes starting from the specified position from the list based on given count: @@ -884,10 +893,10 @@ list.dropManyByIndex(2, -2); -#### dropByValue(value, compareFn) +#### dropByValue(value [, compareFn]) ```js -dropByValue(value: T, compareFn = compare): ListNode\ | undefined +dropByValue(value: T, compareFn?: ListComparisonFn): ListNode | undefined ``` Removes the first node with given value from the list: @@ -922,10 +931,10 @@ list.dropByValue(0, (value, searchedValue) => value.x === searchedValue); -#### dropByValueAll(value, compareFn) +#### dropByValueAll(value [, compareFn]) ```js -dropByValueAll(value: T, compareFn = compare): ListNode\\[\] +dropByValueAll(value: T, compareFn?: ListComparisonFn): ListNode[] ``` Removes all nodes with given value from the list: @@ -963,7 +972,7 @@ list.dropByValue(0, (value, searchedValue) => value.x === searchedValue); #### drop().head() ```js -drop().head(): ListNode\ | undefined +drop().head(): ListNode | undefined ``` Removes the first node in list: @@ -987,7 +996,7 @@ list.drop().head(); #### drop().tail() ```js -drop().tail(): ListNode\ | undefined +drop().tail(): ListNode | undefined ``` Removes the last node in list: @@ -1011,7 +1020,7 @@ list.drop().tail(); #### drop().byIndex(position) ```js -drop().byIndex(position: number): ListNode\ | undefined +drop().byIndex(position: number): ListNode | undefined ``` Removes the node with the specified position from the list: @@ -1046,10 +1055,10 @@ list.drop().byIndex(-2); -#### drop().byValue(value, compareFn) +#### drop().byValue(value [, compareFn]) ```js -drop().byValue(value: T, compareFn = compare): ListNode\ | undefined +drop().byValue(value: T, compareFn?: ListComparisonFn): ListNode | undefined ``` Removes the first node with given value from the list: @@ -1088,10 +1097,10 @@ list -#### drop().byValueAll(value, compareFn) +#### drop().byValueAll(value [, compareFn]) ```js -drop().byValueAll(value: T, compareFn = compare): ListNode\\[\] +drop().byValueAll(value: T, compareFn?: ListComparisonFn): ListNode[] ``` Removes all nodes with given value from the list: @@ -1133,7 +1142,7 @@ list #### dropMany(count).head() ```js -dropMany(count: number).head(): ListNode\\[\] +dropMany(count: number).head(): ListNode[] ``` Removes the first nodes from the list based on given count: @@ -1157,7 +1166,7 @@ list.dropMany(2).head(); #### dropMany(count).tail() ```js -dropMany(count: number).tail(): ListNode\\[\] +dropMany(count: number).tail(): ListNode[] ``` Removes the last nodes from the list based on given count: @@ -1181,7 +1190,7 @@ list.dropMany(2).tail(); #### dropMany(count).byIndex(position) ```js -dropMany(count: number).byIndex(position: number): ListNode\\[\] +dropMany(count: number).byIndex(position: number): ListNode[] ``` Removes the nodes starting from the specified position from the list based on given count: @@ -1222,10 +1231,40 @@ There are a few methods to find specific nodes in a linked list. +#### head + +```js +head: ListNode | undefined; +``` + +Refers to the first node in the list. + + + +#### tail + +```js +tail: ListNode | undefined; +``` + +Refers to the last node in the list. + + + +#### length + +```js +length: number; +``` + +Is the total number of nodes in the list. + + + #### find(predicate) ```js -find(predicate: ListIteratorFunction\): ListNode\ | undefined +find(predicate: ListIteratorFn): ListNode | undefined ``` Finds the first node from the list that matches the given predicate: @@ -1235,7 +1274,7 @@ list.addTailMany(['a', 'b', 'b', 'c']); // "a" <-> "b" <-> "b" <-> "c" -const found = list.find(node => node.value === 'b'); +var found = list.find(node => node.value === 'b'); /* found.value === "b" @@ -1249,7 +1288,7 @@ found.next.value === "b" #### findIndex(predicate) ```js -findIndex(predicate: ListIteratorFunction\): number +findIndex(predicate: ListIteratorFn): number ``` Finds the position of the first node from the list that matches the given predicate: @@ -1259,10 +1298,10 @@ list.addTailMany(['a', 'b', 'b', 'c']); // "a" <-> "b" <-> "b" <-> "c" -const i0 = list.findIndex(node => node.next && node.next.value === 'b'); -const i1 = list.findIndex(node => node.value === 'b'); -const i2 = list.findIndex(node => node.previous && node.previous.value === 'b'); -const i3 = list.findIndex(node => node.value === 'x'); +var i0 = list.findIndex(node => node.next && node.next.value === 'b'); +var i1 = list.findIndex(node => node.value === 'b'); +var i2 = list.findIndex(node => node.previous && node.previous.value === 'b'); +var i3 = list.findIndex(node => node.value === 'x'); /* i0 === 0 @@ -1277,7 +1316,7 @@ i3 === -1 #### get(position) ```js -get(position: number): ListNode\ | undefined +get(position: number): ListNode | undefined ``` Finds and returns the node with specific position in the list: @@ -1287,7 +1326,7 @@ list.addTailMany(['a', 'b', 'c']); // "a" <-> "b" <-> "c" -const found = list.get(1); +var found = list.get(1); /* found.value === "b" @@ -1298,10 +1337,10 @@ found.next.value === "c" -#### indexOf(value, compareFn) +#### indexOf(value [, compareFn]) ```js -indexOf(value: T, compareFn = compare): number +indexOf(value: T, compareFn?: ListComparisonFn): number ``` Finds the position of the first node from the list that has the given value: @@ -1311,10 +1350,10 @@ list.addTailMany(['a', 'b', 'b', 'c']); // "a" <-> "b" <-> "b" <-> "c" -const i0 = list.indexOf('a'); -const i1 = list.indexOf('b'); -const i2 = list.indexOf('c'); -const i3 = list.indexOf('x'); +var i0 = list.indexOf('a'); +var i1 = list.indexOf('b'); +var i2 = list.indexOf('c'); +var i3 = list.indexOf('x'); /* i0 === 0 @@ -1333,11 +1372,11 @@ list.addTailMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]); // {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} -const i0 = indexOf(1, (value, searchedValue) => value.x === searchedValue); -const i1 = indexOf(2, (value, searchedValue) => value.x === searchedValue); -const i2 = indexOf(3, (value, searchedValue) => value.x === searchedValue); -const i3 = indexOf(0, (value, searchedValue) => value.x === searchedValue); -const i4 = indexOf(4, (value, searchedValue) => value.x === searchedValue); +var i0 = indexOf(1, (value, searchedValue) => value.x === searchedValue); +var i1 = indexOf(2, (value, searchedValue) => value.x === searchedValue); +var i2 = indexOf(3, (value, searchedValue) => value.x === searchedValue); +var i3 = indexOf(0, (value, searchedValue) => value.x === searchedValue); +var i4 = indexOf(4, (value, searchedValue) => value.x === searchedValue); /* i0 === 0 @@ -1360,13 +1399,13 @@ There are a few ways to iterate over or display a linked list. -#### forEach(callback) +#### forEach(iteratorFn) ```js -forEach(callback: ListIteratorFunction\): void +forEach(iteratorFn: ListIteratorFn): void ``` -Runs a callback function on all nodes in a linked list from head to tail: +Runs a function on all nodes in a linked list from head to tail: ```js list.addTailMany(['a', 'b', 'c']); @@ -1381,7 +1420,6 @@ list.forEach((node, index) => console.log(node.value + index)); ``` - #### \*\[Symbol.iterator\]\(\) A linked list is iterable. In other words, you may use methods like `for...of` on it. @@ -1391,7 +1429,7 @@ list.addTailMany(['a', 'b', 'c']); // "a" <-> "b" <-> "c" -for(const node of list) { +for(const node of list) { /* ES6 for...of statement */ console.log(node.value); } @@ -1405,7 +1443,7 @@ for(const node of list) { #### toArray() ```js -toArray(): T\[\] +toArray(): T[] ``` Converts a linked list to an array of values: @@ -1415,7 +1453,7 @@ list.addTailMany(['a', 'b', 'c']); // "a" <-> "b" <-> "c" -const arr = list.toArray(); +var arr = list.toArray(); /* arr === ['a', 'b', 'c'] @@ -1427,7 +1465,7 @@ arr === ['a', 'b', 'c'] #### toNodeArray() ```js -toNodeArray(): T\[\] +toNodeArray(): ListNode[] ``` Converts a linked list to an array of nodes: @@ -1437,7 +1475,7 @@ list.addTailMany(['a', 'b', 'c']); // "a" <-> "b" <-> "c" -const arr = list.toNodeArray(); +var arr = list.toNodeArray(); /* arr[0].value === 'a' @@ -1448,10 +1486,10 @@ arr[2].value === 'a' -#### toString() +#### toString([mapperFn]) ```js -toString(): string +toString(mapperFn: ListMapperFn = JSON.stringify): string ``` Converts a linked list to a string representation of nodes and their relations: @@ -1461,7 +1499,7 @@ list.addTailMany(['a', 2, 'c', { k: 4, v: 'd' }]); // "a" <-> 2 <-> "c" <-> {"k":4,"v":"d"} -const str = list.toString(); +var str = list.toString(); /* str === '"a" <-> 2 <-> "c" <-> {"k":4,"v":"d"}' @@ -1477,7 +1515,7 @@ list.addMany([{ x: 1 }, { x: 2 }, { x: 3 }, { x: 4 }, { x: 5 }]).tail(); // {"x":1} <-> {"x":2} <-> {"x":3} <-> {"x":4} <-> {"x":5} -const str = list.toString(value => value.x); +var str = list.toString(value => value.x); /* str === '1 <-> 2 <-> 3 <-> 4 <-> 5' @@ -1486,3 +1524,93 @@ str === '1 <-> 2 <-> 3 <-> 4 <-> 5' + + + +## API + +### Classes + +#### LinkedList + +```js +export class LinkedList { + + // properties and methods are explained above + +} +``` + + + +#### ListNode + +```js +export class ListNode { + next: ListNode | undefined; + + previous: ListNode | undefined; + + constructor(public readonly value: T) {} +} +``` + +`ListNode` is the node that is being stored in the `LinkedList` for every record. + +- `value` is the value stored in the node and is passed through the constructor. +- `next` refers to the next node in the list. +- `previous` refers to the previous node in the list. + +```js +list.addTailMany([ 0, 1, 2 ]); + +console.log( + list.head.value, // 0 + list.head.next.value, // 1 + list.head.next.next.value, // 2 + list.head.next.next.previous.value, // 1 + list.head.next.next.previous.previous.value, // 0 + list.tail.value, // 2 + list.tail.previous.value, // 1 + list.tail.previous.previous.value, // 0 + list.tail.previous.previous.next.value, // 1 + list.tail.previous.previous.next.next.value, // 2 +); +``` + + + + +### Types + +#### ListMapperFn + +```js +type ListMapperFn = (value: T) => any; +``` + +This function is used in `toString` method to map the node values before generating a string representation of the list. + + + +#### ListComparisonFn + +```js +type ListComparisonFn = (nodeValue: T, comparedValue: any) => boolean; +``` + +This function is used while adding, dropping, ang finding nodes based on a comparison value. + + + +#### ListIteratorFn + +```js +type ListIteratorFn = ( + node: ListNode, + index?: number, + list?: LinkedList, +) => R; +``` + +This function is used while iterating over the list either to do something with each node or to find a node. diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 471e215e3a..472f153321 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -5,17 +5,7 @@ "items": [ { "text": "From Startup Templates", - "path": "Getting-Started-With-Startup-Templates.md", - "items": [ - { - "text": "Application with MVC (Razor Pages) UI", - "path": "Getting-Started-AspNetCore-MVC-Template.md" - }, - { - "text": "Application with Angular UI", - "path": "Getting-Started-Angular-Template.md" - } - ] + "path": "Getting-Started.md" }, { "text": "From Empty Projects", @@ -155,6 +145,10 @@ { "text": "Data Filtering", "path": "Data-Filtering.md" + }, + { + "text": "Object Extensions", + "path": "Object-Extensions.md" } ] }, @@ -298,7 +292,17 @@ }, { "text": "Tag Helpers", - "path": "UI/AspNetCore/Tag-Helpers/Index.md" + "path": "UI/AspNetCore/Tag-Helpers/Index.md", + "items": [ + { + "text": "Form Elements", + "path": "UI/AspNetCore/Tag-Helpers/Form-elements.md" + }, + { + "text": "Dynamic Forms", + "path": "UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md" + } + ] }, { "text": "Widgets", @@ -333,6 +337,14 @@ "text": "Permission Management", "path": "UI/Angular/Permission-Management.md" }, + { + "text": "Confirmation Popup", + "path": "UI/Angular/Confirmation-Service.md" + }, + { + "text": "Toast Overlay", + "path": "UI/Angular/Toaster-Service.md" + }, { "text": "Config State", "path": "UI/Angular/Config-State.md" @@ -353,6 +365,10 @@ "text": "DomInsertionService", "path": "UI/Angular/Dom-Insertion-Service.md" }, + { + "text": "ContentProjectionService", + "path": "UI/Angular/Content-Projection-Service.md" + }, { "text": "TrackByService", "path": "UI/Angular/Track-By-Service.md" @@ -363,8 +379,13 @@ "text": "Common", "items": [ { - "text": "Linked List (Doubly)", - "path": "UI/Common/Utils/Linked-List.md" + "text": "Utilities", + "items": [ + { + "text": "Linked List (Doubly)", + "path": "UI/Common/Utils/Linked-List.md" + } + ] } ] } diff --git a/docs/en/images/bookstore-home.png b/docs/en/images/bookstore-home.png new file mode 100644 index 0000000000..5e5b512220 Binary files /dev/null and b/docs/en/images/bookstore-home.png differ diff --git a/docs/en/images/bookstore-login.png b/docs/en/images/bookstore-login.png new file mode 100644 index 0000000000..cd8bfa9bf9 Binary files /dev/null and b/docs/en/images/bookstore-login.png differ diff --git a/docs/en/images/db-migrator-output.png b/docs/en/images/db-migrator-output.png new file mode 100644 index 0000000000..ace6abb226 Binary files /dev/null and b/docs/en/images/db-migrator-output.png differ diff --git a/docs/en/images/package-manager-console-update-database.png b/docs/en/images/package-manager-console-update-database.png new file mode 100644 index 0000000000..d5bb9c2975 Binary files /dev/null and b/docs/en/images/package-manager-console-update-database.png differ diff --git a/docs/en/images/rn-environment-local-ip.png b/docs/en/images/rn-environment-local-ip.png new file mode 100644 index 0000000000..7e60efff2f Binary files /dev/null and b/docs/en/images/rn-environment-local-ip.png differ diff --git a/docs/en/images/rn-expo-interface.png b/docs/en/images/rn-expo-interface.png new file mode 100644 index 0000000000..f1f405ebf4 Binary files /dev/null and b/docs/en/images/rn-expo-interface.png differ diff --git a/docs/en/images/rn-host-local-ip.png b/docs/en/images/rn-host-local-ip.png new file mode 100644 index 0000000000..8691d749e5 Binary files /dev/null and b/docs/en/images/rn-host-local-ip.png differ diff --git a/docs/en/images/rn-login-iphone.png b/docs/en/images/rn-login-iphone.png new file mode 100644 index 0000000000..2da1d24601 Binary files /dev/null and b/docs/en/images/rn-login-iphone.png differ diff --git a/docs/en/images/rn-tiered-local-ip.png b/docs/en/images/rn-tiered-local-ip.png new file mode 100644 index 0000000000..35168455ab Binary files /dev/null and b/docs/en/images/rn-tiered-local-ip.png differ diff --git a/docs/en/images/solution-files-mvc.png b/docs/en/images/solution-files-mvc.png new file mode 100644 index 0000000000..08bbfb9595 Binary files /dev/null and b/docs/en/images/solution-files-mvc.png differ diff --git a/docs/en/images/solution-files-non-mvc.png b/docs/en/images/solution-files-non-mvc.png new file mode 100644 index 0000000000..880cf20d46 Binary files /dev/null and b/docs/en/images/solution-files-non-mvc.png differ diff --git a/docs/en/images/swagger-ui.png b/docs/en/images/swagger-ui.png new file mode 100644 index 0000000000..7f52269474 Binary files /dev/null and b/docs/en/images/swagger-ui.png differ diff --git a/docs/en/images/vs-app-solution-structure-mongodb.png b/docs/en/images/vs-app-solution-structure-mongodb.png new file mode 100644 index 0000000000..8e0e6ba565 Binary files /dev/null and b/docs/en/images/vs-app-solution-structure-mongodb.png differ diff --git a/docs/en/images/vs-app-solution-structure-tiered.png b/docs/en/images/vs-app-solution-structure-tiered.png new file mode 100644 index 0000000000..fd41ef4b0a Binary files /dev/null and b/docs/en/images/vs-app-solution-structure-tiered.png differ diff --git a/docs/en/images/vs-app-solution-structure.png b/docs/en/images/vs-app-solution-structure.png new file mode 100644 index 0000000000..00d92164e7 Binary files /dev/null and b/docs/en/images/vs-app-solution-structure.png differ diff --git a/docs/en/images/vs-spa-app-backend-structure-mongodb.png b/docs/en/images/vs-spa-app-backend-structure-mongodb.png new file mode 100644 index 0000000000..8f0427c14b Binary files /dev/null and b/docs/en/images/vs-spa-app-backend-structure-mongodb.png differ diff --git a/docs/en/images/vs-spa-app-backend-structure.png b/docs/en/images/vs-spa-app-backend-structure.png new file mode 100644 index 0000000000..2cd394c8eb Binary files /dev/null and b/docs/en/images/vs-spa-app-backend-structure.png differ diff --git a/docs/pt-BR/Tutorials/Angular/Part-I.md b/docs/pt-BR/Tutorials/Angular/Part-I.md index 3b79483cd3..3b0b1daf1b 100644 --- a/docs/pt-BR/Tutorials/Angular/Part-I.md +++ b/docs/pt-BR/Tutorials/Angular/Part-I.md @@ -537,11 +537,13 @@ import { GetBooks } from '../actions/books.actions'; import { Books } from '../models/books'; import { BooksService } from '../../books/shared/books.service'; import { tap } from 'rxjs/operators'; +import { Injectable } from '@angular/core'; @State({ name: 'BooksState', defaults: { books: {} } as Books.State, }) +@Injectable() export class BooksState { @Selector() static getBooks(state: Books.State) { diff --git a/docs/zh-Hans/API/Auto-API-Controllers.md b/docs/zh-Hans/API/Auto-API-Controllers.md index 93ac124913..c3f5d44d71 100644 --- a/docs/zh-Hans/API/Auto-API-Controllers.md +++ b/docs/zh-Hans/API/Auto-API-Controllers.md @@ -6,7 +6,7 @@ ABP可以按照惯例 **自动** 将你的应用程序服务配置为API控制 ## 配置 -基本配置很简单. 只需配置`AbpAspNetCoreMvcOptions`并使用`ConventionalControllers.Create`方法,如下所示: +基本配置很简单. 只需配置`AbpAspNetCoreMvcOptions`并使用`ConventionalControllers.Create`方法,如下所示: ````csharp [DependsOn(BookStoreApplicationModule)] @@ -82,7 +82,7 @@ Configure(options => * 删除'**Async**'后缀. 如果方法名称为'GetPhonesAsync',则变为`GetPhones`. * 删除**HTTP method前缀**. 基于的HTTP method删除`GetList`,`GetAll`,`Get`,`Put`,`Update`,`Delete`,`Remove`,`Create`,`Add`,`Insert`,`Post`和`Patch`前缀, 因此`GetPhones`变为`Phones`, 因为`Get`前缀和GET请求重复. * 将结果转换为**camelCase**. - * 如果生成的操作名称为**空**,则它不会添加到路径中.否则它会被添加到路由中(例如'/phones').对于`GetAllAsync`方法名称,它将为空,因为`GetPhonesAsync`方法名称将为`phone`. + * 如果生成的操作名称为**空**,则它不会添加到路径中.否则它会被添加到路由中(例如'/phones').对于`GetAllAsync`方法名称,它将为空,因为`GetPhonesAsync`方法名称将为`phone`. * 可以通过设置`UrlActionNameNormalizer`选项来自定义.It's an action delegate that is called for every method. * 如果有另一个带有'Id'后缀的参数,那么它也会作为最终路线段添加到路线中(例如'/phoneId'). diff --git a/docs/zh-Hans/Application-Services.md b/docs/zh-Hans/Application-Services.md index 72e0319cde..d0e29c2881 100644 --- a/docs/zh-Hans/Application-Services.md +++ b/docs/zh-Hans/Application-Services.md @@ -380,4 +380,4 @@ public class DistrictKey ### 生命周期 -应用服务的生命周期是[transient](Dependency-Injection)的,它们会自动注册到依赖注入系统. \ No newline at end of file +应用服务的生命周期是[transient](Dependency-Injection)的,它们会自动注册到依赖注入系统. \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Tag-Helpers/Buttons.md b/docs/zh-Hans/AspNetCore/Tag-Helpers/Buttons.md index 848d043312..ebfeb6f2aa 100644 --- a/docs/zh-Hans/AspNetCore/Tag-Helpers/Buttons.md +++ b/docs/zh-Hans/AspNetCore/Tag-Helpers/Buttons.md @@ -86,7 +86,7 @@ ABP框架定义了Tag Helper用于简单的创建bootstrap按钮. ### `icon-type` -`icon-type` 是一个可选参数。它的默认值是 `FontAwesome`. 你可以创建自己的图标类型提供程序并更改它. +`icon-type` 是一个可选参数.它的默认值是 `FontAwesome`. 你可以创建自己的图标类型提供程序并更改它. 你可以为按钮选择以下图标类型: diff --git a/docs/zh-Hans/Audit-Logging.md b/docs/zh-Hans/Audit-Logging.md index a028e640b2..a8b6e66449 100644 --- a/docs/zh-Hans/Audit-Logging.md +++ b/docs/zh-Hans/Audit-Logging.md @@ -43,12 +43,12 @@ Configure(options => * `IsEnabledForGetRequests` (默认值: `false`): HTTP GET请求通常不应该在数据库进行任何更改,审计日志系统不会为GET请求保存审计日志对象. 将此值设置为 `true` 可为GET请求启用审计日志系统. * `ApplicationName`: 如果有多个应用程序保存审计日志到单一的数据库,使用此属性设置为你的应用程序名称区分不同的应用程序日志. * `IgnoredTypes`: 审计日志系统忽略的 `Type` 列表. 如果它是实体类型,则不会保存此类型实体的更改. 在序列化操作参数时也使用此列表. -* `EntityHistorySelectors`:选择器列表,用于确定是否选择了用于保存实体更改的实体类型. 有关详细信息请参阅下面的部分. +* `EntityHistorySelectors`:选择器列表,用于确定是否选择了用于保存实体更改的实体类型. 有关详细信息请参阅下面的部分. * `Contributors`: `AuditLogContributor` 实现的列表. 贡献者是扩展审计日志系统的一种方式. 有关详细信息请参阅下面的"审计日志贡献者"部分. ### 实体历史选择器 -保存您的所有实体的所有变化将需要大量的数据库空间. 出于这个原因**审计日志系统不保存为实体的任何改变,除非你明确地对其进行配置**. +保存你的所有实体的所有变化将需要大量的数据库空间. 出于这个原因**审计日志系统不保存为实体的任何改变,除非你明确地对其进行配置**. 要保存的所有实体的所有更改,只需使用 `AddAllEntities()` 扩展方法. @@ -131,7 +131,7 @@ public class HomeController : AbpController 可以为任何类型的类(注册到[依赖注入](Dependency-Injection.md)并从依赖注入解析)启用审计日志,默认情况下仅对控制器和应用程序服务启用. -对于任何需要被审计记录的类或方法都可以使用 `[Audited]` 和`IAuditingEnabled`.此外,您的类可以(直接或固有的)实现 `IAuditingEnabled` 接口以认启用该类的审计日志记录. +对于任何需要被审计记录的类或方法都可以使用 `[Audited]` 和`IAuditingEnabled`.此外,你的类可以(直接或固有的)实现 `IAuditingEnabled` 接口以认启用该类的审计日志记录. ### 启用/禁用 实体 & 属性 @@ -211,8 +211,8 @@ public class MyUser : Entity * **AuditLogInfo**: 具有以下属性: * `ApplicationName`: 当你保存不同的应用审计日志到同一个数据库,这个属性用来区分应用程序. - * `UserId`:当前用户的Id,用户未登录为 `null`. - * `UserName`:当前用户的用户名,如果用户已经登录(这里的值不依赖于标识模块/系统进行查找). + * `UserId`:当前用户的Id,用户未登录为 `null`. + * `UserName`:当前用户的用户名,如果用户已经登录(这里的值不依赖于标识模块/系统进行查找). * `TenantId`: 当前租户的Id,对于多租户应用. * `TenantName`: 当前租户的名称,对于多租户应用. * `ExecutionTime`: 审计日志对象创建的时间. @@ -222,28 +222,28 @@ public class MyUser : Entity * `ClientIpAddress`: 客户端/用户设备的IP地址. * `CorrelationId`: 当前[相关Id](CorrelationId.md). 相关Id用于在单个逻辑操作中关联由不同应用程序(或微服务)写入的审计日志. * `BrowserInfo`: 当前用户的浏览器名称/版本信息,如果有的话. - * `HttpMethod`: 当前HTTP请求的方法(GET,POST,PUT,DELETE ...等). + * `HttpMethod`: 当前HTTP请求的方法(GET,POST,PUT,DELETE ...等). * `HttpStatusCode`: HTTP响应状态码. * `Url`: 请求的URL. * **AuditLogActionInfo**: 一个 审计日志动作通常是web请求期间控制器动作或[应用服务](Application-Services.md)方法调用. 一个审计日志可以包含多个动作. 动作对象具有以下属性: - * `ServiceName`:执行的控制器/服务的名称. - * `MethodName`:控制器/服务执行的方法的名称. - * `Parameters`:传递给方法的参数的JSON格文本. + * `ServiceName`:执行的控制器/服务的名称. + * `MethodName`:控制器/服务执行的方法的名称. + * `Parameters`:传递给方法的参数的JSON格文本. * `ExecutionTime`: 执行的时间. * `ExecutionDuration`: 方法执行时长,以毫秒为单位. 可以用来观察方法的性能. * **EntityChangeInfo**: 表示一个实体在Web请求中的变更. 审计日志可以包含0个或多个实体的变更. 实体变更具有以下属性: * `ChangeTime`: 当实体被改变的时间. - * `ChangeType`:具有以下字段的枚举: `Created`(0), `Updated`(1)和 `Deleted`(2). + * `ChangeType`:具有以下字段的枚举: `Created`(0), `Updated`(1)和 `Deleted`(2). * `EntityId`: 更改实体的Id. - * `EntityTenantId`:实体所属的租户Id. + * `EntityTenantId`:实体所属的租户Id. * `EntityTypeFullName`: 实体的类型(类)的完整命名空间名称(例如Book实体的*Acme.BookStore.Book*. * **EntityPropertyChangeInfo**: 表示一个实体的属性的更改.一个实体的更改信息(上面已说明)可含有具有以下属性的一个或多个属性的更改: * `NewValue`: 属性的新值. 如果实体已被删除为 `null`. - * `OriginalValue`:变更前旧/初始值. 如果实体是新创建为 `null`. + * `OriginalValue`:变更前旧/初始值. 如果实体是新创建为 `null`. * `PropertyName`: 实体类的属性名称. - * `PropertyTypeFullName`:属性类型的完整命名空间名称. + * `PropertyTypeFullName`:属性类型的完整命名空间名称. * **Exception**: 审计日志对象可能包含零个或多个异常. 可以得到失败请求的异常信息. -* **Comment**:用于将自定义消息添加到审计日志条目的任意字符串值. 审计日志对象可能包含零个或多个注释. +* **Comment**:用于将自定义消息添加到审计日志条目的任意字符串值. 审计日志对象可能包含零个或多个注释. 除了上面说明的标准属性之外,`AuditLogInfo`, `AuditLogActionInfo` 和 `EntityChangeInfo` 对象还实现了`IHasExtraProperties` 接口,你可以向这些对象添加自定义属性. @@ -331,7 +331,7 @@ public class MyService : ITransientDependency ### 手动创建审计日志范围 你很少需要手动创建审计日志的范围,但如果你需要,可以使用 `IAuditingManager` 创建审计日志的范围. -例: +例: ````csharp public class MyService : ITransientDependency @@ -366,7 +366,7 @@ public class MyService : ITransientDependency } ```` -您可以调用其他服务,它们可能调用其他服务,它们可能更改实体,等等. 所有这些交互都保存为finally块中的一个审计日志对象. +你可以调用其他服务,它们可能调用其他服务,它们可能更改实体,等等. 所有这些交互都保存为finally块中的一个审计日志对象. ## 审计日志模块 diff --git a/docs/zh-Hans/Authorization.md b/docs/zh-Hans/Authorization.md index a43565279e..99139be3ac 100644 --- a/docs/zh-Hans/Authorization.md +++ b/docs/zh-Hans/Authorization.md @@ -153,7 +153,7 @@ myGroup.AddPermission( myGroup.AddPermission("Author_Management", isEnabled: false); ```` -通常你不需要定义禁用权限(除非您暂时想要禁用应用程序的功能). 无论怎样,你可能想要禁用依赖模块中定义的权限,这样你可以禁用相关的功能. 参阅下面的 "*更改依赖模块的权限定义*" 节,查看示例用法. +通常你不需要定义禁用权限(除非你暂时想要禁用应用程序的功能). 无论怎样,你可能想要禁用依赖模块中定义的权限,这样你可以禁用相关的功能. 参阅下面的 "*更改依赖模块的权限定义*" 节,查看示例用法. > 注意:检查一个未定义的权限会抛出异常,而被禁用的权限的返回禁止(false). diff --git a/docs/zh-Hans/AutoMapper-Integration.md b/docs/zh-Hans/AutoMapper-Integration.md deleted file mode 100644 index d197861f25..0000000000 --- a/docs/zh-Hans/AutoMapper-Integration.md +++ /dev/null @@ -1,3 +0,0 @@ -## AutoMapper Integration - -TODO \ No newline at end of file diff --git a/docs/zh-Hans/Background-Workers.md b/docs/zh-Hans/Background-Workers.md index dfd26c7b82..1daedde384 100644 --- a/docs/zh-Hans/Background-Workers.md +++ b/docs/zh-Hans/Background-Workers.md @@ -2,7 +2,7 @@ ## 介绍 -背景工人在应用简单独立的线程在后台运行。一般来说,他们定期运行,以执行一些任务。例子; +背景工人在应用简单独立的线程在后台运行.一般来说,他们定期运行,以执行一些任务.例子; 后台工作者在应用程序后台运行的简单的独立线程,一般来说它们定期运行执行一些任务.例如; * 后台工作者可以定期**删除过时的日志**. @@ -72,7 +72,7 @@ public class PassiveUserCheckerWorker : AsyncPeriodicBackgroundWorkerBase } ```` -* `AsyncPeriodicBackgroundWorkerBase` 使用 `AbpTimer`(线程安全定时器)对象来确定**时间段**. 我们可以在构造函数中设置了`Period` 属性。 +* `AsyncPeriodicBackgroundWorkerBase` 使用 `AbpTimer`(线程安全定时器)对象来确定**时间段**. 我们可以在构造函数中设置了`Period` 属性. * 它需要实现 `DoWorkAsync` 方法**执行**定期任务. * 最好使用 `PeriodicBackgroundWorkerContext` **解析依赖** 而不是构造函数. 因为 `AsyncPeriodicBackgroundWorkerBase` 使用 `IServiceScope` 在你的任务执行结束时会对其 **disposed**. * `AsyncPeriodicBackgroundWorkerBase` **捕获并记录** 由 `DoWorkAsync` 方法抛出的 **异常**. diff --git a/docs/zh-Hans/Best-Practices/Application-Services.md b/docs/zh-Hans/Best-Practices/Application-Services.md index 6c8d11b32b..6c681edde1 100644 --- a/docs/zh-Hans/Best-Practices/Application-Services.md +++ b/docs/zh-Hans/Best-Practices/Application-Services.md @@ -17,7 +17,7 @@ ##### 基础DTO -**推荐** 为实体定义一个**基础**DTO. +**推荐** 为聚合根定义一个**基础**DTO. - 直接包含实体中所有的**原始属性**. - 例外: 出于**安全**原因,可以**排除**某些属性(像 `User.Password`). @@ -27,7 +27,7 @@ ```c# [Serializable] -public class IssueDto : FullAuditedEntityDto +public class IssueDto : ExtensibleFullAuditedEntityDto { public string Title { get; set; } public string Text { get; set; } @@ -57,7 +57,7 @@ public class IssueLabelDto ````C# [Serializable] -public class IssueWithDetailsDto : FullAuditedEntityDto +public class IssueWithDetailsDto : ExtensibleFullAuditedEntityDto { public string Title { get; set; } public string Text { get; set; } @@ -66,14 +66,14 @@ public class IssueWithDetailsDto : FullAuditedEntityDto } [Serializable] -public class MilestoneDto : EntityDto +public class MilestoneDto : ExtensibleEntityDto { public string Name { get; set; } public bool IsClosed { get; set; } } [Serializable] -public class LabelDto : EntityDto +public class LabelDto : ExtensibleEntityDto { public string Name { get; set; } public string Color { get; set; } @@ -120,6 +120,7 @@ Task> GetListAsync(QuestionListQueryDto queryDto); * **推荐** 使用 `CreateAsync` 做为**方法名**. * **推荐** 使用**专门的输入DTO**来创建实体. +* **推荐** DTO类从 `ExtensibleObject` 类继承(或任何实现 `ExtensibleObject`的类) 以允许在需要时传递额外的属性. * **推荐** 使用 **data annotations** 进行输入验证. * 尽可能在**领域**之间共享常量(通过**domain shared** package定义的常量). * **推荐** 只需要创建实体的**最少**信息, 但是提供了其他可选属性. @@ -134,7 +135,7 @@ Task CreateAsync(CreateQuestionDto questionDto); ````C# [Serializable] -public class CreateQuestionDto +public class CreateQuestionDto : ExtensibleObject { [Required] [StringLength(QuestionConsts.MaxTitleLength, MinimumLength = QuestionConsts.MinTitleLength)] @@ -151,6 +152,7 @@ public class CreateQuestionDto - **推荐** 使用 `UpdateAsync` 做为**方法名**. - **推荐** 使用**专门的输入DTO**来更新实体. +- **推荐** DTO类从 `ExtensibleObject` 类继承(或任何实现 `ExtensibleObject`的类) 以允许在需要时传递额外的属性. - **推荐** 获取实体的id做为分离的原始参数. 不要包含更新DTO. - **推荐** 使用 **data annotations** 进行输入验证. - 尽可能在**领域**之间共享常量(通过**domain shared** package定义的常量). @@ -199,6 +201,10 @@ Task VoteAsync(Guid id, VoteType type); * **不推荐** 在应用程序服务方法中使用linq/sql查询来自数据库的数据. 让仓储负责从数据源执行linq/sql查询. +#### 额外的属性 + +* **推荐** 使用 `MapExtraPropertiesTo` 扩展方法 ([参阅](Object-Extensions.md)) 或配置对象映射 (`MapExtraProperties`) 以允许应用开发人员能够扩展对象和服务. + #### 操作/删除 实体 * **推荐** 总是从数据库中获取所有的相关实体以对他们执行操作. diff --git a/docs/zh-Hans/Best-Practices/Data-Transfer-Objects.md b/docs/zh-Hans/Best-Practices/Data-Transfer-Objects.md index 0c6017e6e7..29e51501ea 100644 --- a/docs/zh-Hans/Best-Practices/Data-Transfer-Objects.md +++ b/docs/zh-Hans/Best-Practices/Data-Transfer-Objects.md @@ -2,6 +2,7 @@ * **推荐** 在 **application.contracts** 层中定义DTO. * **推荐** 在可能和必要的情况下从预构建的 **基础DTO类** 继承 (如 `EntityDto`, `CreationAuditedEntityDto`, `AuditedEntityDto`, `FullAuditedEntityDto` 等). +* **推荐** 从**聚合根**的**扩展DTO**继承(如 `ExtensibleAuditedEntityDto`), 因为聚合根是可扩展的额外的属性使用这种方式映射到DTO. * **推荐** 定义 **public getter 和 setter** 的DTO成员 . * **推荐** 使用 **data annotations** **验证** service输入DTO的属性. * **不推荐** 在DTO中添加任何 **逻辑**, 在必要的时候可以实现 `IValidatableObject` 接口. diff --git a/docs/zh-Hans/Blog-Posts/2019-02-22/Post.md b/docs/zh-Hans/Blog-Posts/2019-02-22/Post.md index 30a5d691e9..998a454abc 100644 --- a/docs/zh-Hans/Blog-Posts/2019-02-22/Post.md +++ b/docs/zh-Hans/Blog-Posts/2019-02-22/Post.md @@ -13,7 +13,7 @@ ABP框架的主要目标之一是提供[创建微服务解决方案的便利基 - 使用[Ocelot](https://github.com/ThreeMammals/Ocelot)库开发了多个**网关** / BFF(后端为前端(Backend for Frontends)). - 使用[IdentityServer](https://identityserver.io/)框架开发**身份验证服务**.它也是一个带有必要UI的SSO(单点登录)应用程序. - 有**多个数据库**.一些微服务有自己的数据库,而一些服务/应用程序共享一个数据库(以演示不同的用例). -- 具有不同类型的数据库:**SQL Server**(使用**Entity Framework Core** ORM)和**MongoDB**. +- 具有不同类型的数据库:**SQL Server**(使用**Entity Framework Core** ORM)和**MongoDB**. - 有一个**控制台应用程序**来显示通过身份验证使用服务的最简单方法. - 使用[Redis](https://redis.io/)进行**分布式缓存**. - 使用[RabbitMQ](https://www.rabbitmq.com/)进行服务到服务(service-to-service)的**消息传递**. @@ -28,27 +28,27 @@ ABP框架的主要目标之一是提供[创建微服务解决方案的便利基 ## 路线图 -在第一个稳定版本(v1.0)之前还有很多工作要做.您可以在GitHub仓库上看到[优先的积压项目](https://github.com/abpframework/abp/issues?q=is%3Aopen+is%3Aissue+milestone%3ABacklog). +在第一个稳定版本(v1.0)之前还有很多工作要做.你可以在GitHub仓库上看到[优先的积压项目](https://github.com/abpframework/abp/issues?q=is%3Aopen+is%3Aissue+milestone%3ABacklog). 根据我们的估计,我们计划在2019年第二季度(可能在五月或六月)发布v1.0.所以,不用等待太长时间了.我们也对第一个稳定版本感到非常兴奋. 我们还将完善[文档](https://abp.io/documents/abp/latest),因为它现在还远未完成. -第一个版本可能不包含SPA模板.但是,如果可能的话,我们想要准备一个简单些的.SPA框架还没有确定下来.备选有:**Angular,React和Blazor**.请将您的想法写为对此帖的评论. +第一个版本可能不包含SPA模板.但是,如果可能的话,我们想要准备一个简单些的.SPA框架还没有确定下来.备选有:**Angular,React和Blazor**.请将你的想法写为对此帖的评论. ## 中文网 -中国有一个大型的ABP社区.他们创建了一个中文版的abp.io网站:https://abp.io/. 他们一直在保持更新.感谢中国的开发人员,特别是[Liming Ma](https://github.com/maliming). +中国有一个大型的ABP社区.他们创建了一个中文版的abp.io网站:https://abp.io/. 他们一直在保持更新.感谢中国的开发人员,特别是[Liming Ma](https://github.com/maliming). ## NDC {London} 2019 很高兴作为合作伙伴参加[NDC {London}](https://ndc-london.com/)2019 .我们已经与许多开发人员讨论过当前的ASP.NET Boilerplate和ABP vNext,我们得到了很好的反馈. -我们还有机会与[Scott Hanselman](https://twitter.com/shanselman)和[Jon Galloway](https://twitter.com/jongalloway)交谈.他们参观了我们的展位,我们谈到了ABP vNext的想法.他们喜欢新的ABP框架的功能,方法和目标.在twitter上查看一些照片和评论: +我们还有机会与[Scott Hanselman](https://twitter.com/shanselman)和[Jon Galloway](https://twitter.com/jongalloway)交谈.他们参观了我们的展位,我们谈到了ABP vNext的想法.他们喜欢新的ABP框架的功能,方法和目标.在twitter上查看一些照片和评论: ![scott-and-jon](scott-and-jon.png) ## 跟上步伐 -* 您可以标星并关注**GitHub**存储库:https://github.com/abpframework/abp -* 您可以关注官方**Twitter**帐户获取新闻:https://twitter.com/abpframework +* 你可以标星并关注**GitHub**存储库:https://github.com/abpframework/abp +* 你可以关注官方**Twitter**帐户获取新闻:https://twitter.com/abpframework diff --git a/docs/zh-Hans/Blog-Posts/2019-06-19 v0_18_Release/Post.md b/docs/zh-Hans/Blog-Posts/2019-06-19 v0_18_Release/Post.md index a25e1c0a97..6b3c76d2e2 100644 --- a/docs/zh-Hans/Blog-Posts/2019-06-19 v0_18_Release/Post.md +++ b/docs/zh-Hans/Blog-Posts/2019-06-19 v0_18_Release/Post.md @@ -4,7 +4,7 @@ ABP v0.18已发布, 包含解决的[80+个issue](https://github.com/abpframework ## 网站更改 -[abp.io](https://abp.io)网站**完全更新**以突出ABP框架的目标和重要功能.文档和博客网址也会更改: +[abp.io](https://abp.io)网站**完全更新**以突出ABP框架的目标和重要功能.文档和博客网址也会更改: - `abp.io/documents`移至[docs.abp.io](https://docs.abp.io). - `abp.io/blog`转移到[blog.abp.io](https://blog.abp.io). @@ -21,25 +21,25 @@ ABP CLI现在是创建新项目的首选方式,你仍然可以从[开始](https: ### 用法 -使用命令行窗口安装ABP CLI: +使用命令行窗口安装ABP CLI: ```` bash dotnet tool install -g Volo.Abp.Cli ```` -创建一个新应用程序: +创建一个新应用程序: ```` bash abp new Acme.BookStore ```` -将模块添加到应用程序: +将模块添加到应用程序: ```` bash abp add-module Volo.Blogging ```` -更新解决方案中所有与ABP相关的包: +更新解决方案中所有与ABP相关的包: ```` bash abp update @@ -59,7 +59,7 @@ abp update ## 更改日志 -以下是此版本附带的一些其他功能和增强功能: +以下是此版本附带的一些其他功能和增强功能: * 新[Volo.Abp.Dapper](https://www.nuget.org/packages/Volo.Abp.Dapper)包. * 新[Volo.Abp.Specifications](https://www.nuget.org/packages/Volo.Abp.Specifications)包. diff --git a/docs/zh-Hans/Blog-Posts/2019-08-16 v0_19_Release/Post.md b/docs/zh-Hans/Blog-Posts/2019-08-16 v0_19_Release/Post.md index 831cc791e9..9f82f6dce3 100644 --- a/docs/zh-Hans/Blog-Posts/2019-08-16 v0_19_Release/Post.md +++ b/docs/zh-Hans/Blog-Posts/2019-08-16 v0_19_Release/Post.md @@ -14,12 +14,12 @@ ABP v0.19已发布,包含解决的[~90个问题](https://github.com/abpframework * 更新了[ABP CLI](https://docs.abp.io/en/abp/latest/CLI)和[下载页面](https://abp.io/get-started),以便能够使用新的UI选项生成项目. * 创建了[教程](https://docs.abp.io/en/abp/latest/Tutorials/Angular/Part-I)以使用新的UI选项快速入门. -我们基于最新的Angular工具和趋势创建了模板,文档和基础架构: +我们基于最新的Angular工具和趋势创建了模板,文档和基础架构: * 使用[NgBootstrap](https://ng-bootstrap.github.io/)和[PrimeNG](https://www.primefaces.org/primeng/)作为UI组件库.你可以使用自己喜欢的库,没问题,但预构建的模块可以使用这些库. * 使用[NGXS](https://ngxs.gitbook.io/ngxs/)作为状态管理库. -Angular是第一个SPA UI选项,但它不是最后一个.在v1.0发布之后,我们将开始第二个UI选项的工作.虽然尚未决定,但候选的有Blazor,React和Vue.js. 等待你的反馈.你可以使用以下issue进行投票(thumb): +Angular是第一个SPA UI选项,但它不是最后一个.在v1.0发布之后,我们将开始第二个UI选项的工作.虽然尚未决定,但候选的有Blazor,React和Vue.js. 等待你的反馈.你可以使用以下issue进行投票(thumb): * [Blazor](https://github.com/abpframework/abp/issues/394) * [Vue.js](https://github.com/abpframework/abp/issues/1168) diff --git a/docs/zh-Hans/Blog-Posts/2019-09-25 v0_21_Release/Post.md b/docs/zh-Hans/Blog-Posts/2019-09-25 v0_21_Release/Post.md index 7633f3ea33..c4d98244fb 100644 --- a/docs/zh-Hans/Blog-Posts/2019-09-25 v0_21_Release/Post.md +++ b/docs/zh-Hans/Blog-Posts/2019-09-25 v0_21_Release/Post.md @@ -14,8 +14,8 @@ ABP框架越来越接近v1.0.我们打算在今年10月中旬发布1.0. 现在, ## Techorama荷兰2019 -[Techorama NL](https://techorama.nl/)是欧洲最大的会议之一.今年,Volosoft是会议的赞助商,并将有一个展位与软件开发人员讨论ABP框架和软件开发.我们的展位墙如下图所示: +[Techorama NL](https://techorama.nl/)是欧洲最大的会议之一.今年,Volosoft是会议的赞助商,并将有一个展位与软件开发人员讨论ABP框架和软件开发.我们的展位墙如下图所示: ![volosoft-booth](volosoft-booth.png) -如果您也参加会议,请到展位讨论ABP框架.我们还为您准备了一些私货:) +如果你也参加会议,请到展位讨论ABP框架.我们还为你准备了一些私货:) diff --git a/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/Post.md b/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/Post.md index a17bef2e1b..73585582f5 100644 --- a/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/Post.md +++ b/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/Post.md @@ -24,7 +24,7 @@ 我们终于完成了**react native移动应用程序**.目前,它可以让你**登录**,管理**用户**和**租户**.它利用ABP框架相同的设置,授权和本地化系统. -应用程序的一些截图: +应用程序的一些截图: ![mobile-ui](react-native-ui.png) @@ -34,7 +34,7 @@ 从我们的Angular应用程序中调用服务器中的REST端点是很常见的.这种情况下,我们一般创建**服务**(在服务器上包含各个服务的方法)和**模型对象**(对应服务器上的[DTO](https://docs.abp.io/en/abp/latest/Data-Transfer-Objects)). -除了手动创建这样的与服务器交互的服务外,我们可以使用像[NSWAG](https://github.com/RicoSuter/NSwag)工具来为我们生成服务代理.但是NSWAG有以下几个我们遇到的问题: +除了手动创建这样的与服务器交互的服务外,我们可以使用像[NSWAG](https://github.com/RicoSuter/NSwag)工具来为我们生成服务代理.但是NSWAG有以下几个我们遇到的问题: * 它产生一个**大,单一**的.ts文件; * 当你的应用程序增长时,它变得**太大**了. @@ -58,12 +58,12 @@ abp generate-proxy ### 添加模块的源代码 -应用程序启动模板带有一些[应用模块](https://docs.abp.io/en/abp/latest/Modules/Index), 以**Nuget和NPM包**的方式**预先安装了** .这样做有几个重要的优点: +应用程序启动模板带有一些[应用模块](https://docs.abp.io/en/abp/latest/Modules/Index), 以**Nuget和NPM包**的方式**预先安装了** .这样做有几个重要的优点: * 当新版本可用时, 你可以 **轻松地[升级](https://docs.abp.io/en/abp/latest/CLI#update)** 这些模块. * 你的解决方案**更干净**,这样你就可以专注于自己的代码. -但是,当你需要对一个依赖的模块**大量定制**时,就不如它的代码在你的应用程序中那么容易.为了解决这个问题,我们引入了一个[ABP CLI](https://docs.abp.io/en/abp/latest/CLI)的新命令, 在你的解决方案中用代码**替换**Nuget包.用法很简单: +但是,当你需要对一个依赖的模块**大量定制**时,就不如它的代码在你的应用程序中那么容易.为了解决这个问题,我们引入了一个[ABP CLI](https://docs.abp.io/en/abp/latest/CLI)的新命令, 在你的解决方案中用代码**替换**Nuget包.用法很简单: ````bash abp add-module --with-source-code @@ -75,19 +75,19 @@ abp add-module --with-source-code 此外,我们也创建了文档来说明如何定制依赖的模块而不改变它们的源代码(见下面的部分).仍然建议以包的方式使用模块,以便在以后可以轻松升级. -> 免费模块的源代码是**MIT**许可,所以你可以自由更改它们并添加到您的解决方案中. +> 免费模块的源代码是**MIT**许可,所以你可以自由更改它们并添加到你的解决方案中. ### 切换到预览版 ABP框架正在迅速发展,我们经常发布新版本.不过,如果你想更紧密地追随它,你可以使用**每日预览包**. -我们创建了一个ABP CLI命令来轻松地为你的解决方案**更新到最新的预览包**.在你的解决方案的根文件夹中运行以下命令: +我们创建了一个ABP CLI命令来轻松地为你的解决方案**更新到最新的预览包**.在你的解决方案的根文件夹中运行以下命令: ````bash abp switch-to-preview ```` -它会修改所有ABP相关的NuGet和NPM包的版本.当你需要时你也可以**切换回最新稳定版**: +它会修改所有ABP相关的NuGet和NPM包的版本.当你需要时你也可以**切换回最新稳定版**: ````bash abp switch-to-stable @@ -131,7 +131,7 @@ abp switch-to-stable ## 下一步? -我们未来几个月的目标如下: +我们未来几个月的目标如下: * 完成**文档和示例**,写更多的教程. * 使框架和现有模块的更加**可定制和可扩展**. diff --git a/docs/zh-Hans/CLI.md b/docs/zh-Hans/CLI.md index 0a802ffdc1..4d037ef2fd 100644 --- a/docs/zh-Hans/CLI.md +++ b/docs/zh-Hans/CLI.md @@ -41,12 +41,12 @@ abp new Acme.BookStore * `--template` 或者 `-t`: 指定模板. 默认的模板是 `app`,会生成web项目.可用的模板有: * `app` (default): [应用程序模板](Startup-Templates/Application.md). 其他选项: - * `--ui` 或者 `-u`: 指定ui框架.默认`mvc`框架.其他选项: - * `mvc`: ASP.NET Core MVC.此模板的其他选项: + * `--ui` 或者 `-u`: 指定ui框架.默认`mvc`框架.其他选项: + * `mvc`: ASP.NET Core MVC.此模板的其他选项: * `--tiered`: 创建分层解决方案,Web和Http Api层在物理上是分开的.如果未指定会创建一个分层的解决方案,此解决方案没有那么复杂,适合大多数场景. - * `angular`: Angular. 这个模板还有一些额外的选项: + * `angular`: Angular. 这个模板还有一些额外的选项: * `--separate-identity-server`: 将Identity Server应用程序与API host应用程序分开. 如果未指定,则服务器端将只有一个端点. - * `none`: 无UI. 这个模板还有一些额外的选项: + * `none`: 无UI. 这个模板还有一些额外的选项: * `--separate-identity-server`: 将Identity Server应用程序与API host应用程序分开. 如果未指定,则服务器端将只有一个端点. * `--mobile` 或者 `-m`: 指定移动应用程序框架. 默认框架是 `react-native`. 其他选项: * `none`: 不包含移动应用程序. @@ -57,7 +57,7 @@ abp new Acme.BookStore * `module`: [Module template](Startup-Templates/Module.md). 其他选项: * `--no-ui`: 不包含UI.仅创建服务模块(也称为微服务 - 没有UI). * `--output-folder` 或者 `-o`: 指定输出文件夹,默认是当前目录. -* `--version` 或者 `-v`: 指定ABP和模板的版本.它可以是 [release tag](https://github.com/abpframework/abp/releases) 或者 [branch name](https://github.com/abpframework/abp/branches). 如果没有指定,则使用最新版本.大多数情况下,您会希望使用最新的版本. +* `--version` 或者 `-v`: 指定ABP和模板的版本.它可以是 [release tag](https://github.com/abpframework/abp/releases) 或者 [branch name](https://github.com/abpframework/abp/branches). 如果没有指定,则使用最新版本.大多数情况下,你会希望使用最新的版本. * `--template-source` 或者 `-ts`: 指定自定义模板源用于生成项目,可以使用本地源和网络源(例如 `D\localTemplate` 或 `https://.zip`). * `--create-solution-folder` 或者 `-csf`: 指定项目是在输出文件夹中的新文件夹中还是直接在输出文件夹中. * `--connection-string` 或者 `-cs`: 重写所有 `appsettings.json` 文件的默认连接字符串. 默认连接字符串是 `Server=localhost;Database=MyProjectName;Trusted_Connection=True;MultipleActiveResultSets=true`. 如果你不想使用默认,你可以设置自己的连接字符串. 默认的数据库提供程序是 `SQL Server`, 所以你只能输入SQL Server连接字符串! @@ -186,9 +186,9 @@ abp generate-proxy [options] #### Options -* `--apiUrl` 或者 `-a`:指定HTTP API的根URL. 如果未指定这个选项,默认使用你Angular应用程序的`environment.ts`文件API URL. 在运行 `generate-proxy` 命令之前,你的host必须启动正在运行. +* `--apiUrl` 或者 `-a`:指定HTTP API的根URL. 如果未指定这个选项,默认使用你Angular应用程序的`environment.ts`文件API URL. 在运行 `generate-proxy` 命令之前,你的host必须启动正在运行. * `--ui` 或者 `-u`: 指定UI框架,默认框架是angular.当前只有angular一个选项, 但我们会通过更改CLI增加新的选项. 尽请关注! -* `--module` 或者 `-m`:指定模块名. 默认模块名称为app. 如果你想所有模块,你可以指定 `--module all` 命令. +* `--module` 或者 `-m`:指定模块名. 默认模块名称为app. 如果你想所有模块,你可以指定 `--module all` 命令. 示例: diff --git a/docs/zh-Hans/Customizing-Application-Modules-Extending-Entities.md b/docs/zh-Hans/Customizing-Application-Modules-Extending-Entities.md index 4cc73989fc..3e5ff3f6cd 100644 --- a/docs/zh-Hans/Customizing-Application-Modules-Extending-Entities.md +++ b/docs/zh-Hans/Customizing-Application-Modules-Extending-Entities.md @@ -50,7 +50,7 @@ ObjectExtensionManager.Instance * 你提供了 `IdentityUser` 作为实体名(泛型参数), `string` 做为新属性的类型, `SocialSecurityNumber` 做为属性名(也是数据库表的字段名). * 你还需要提供一个使用[EF Core Fluent API](https://docs.microsoft.com/en-us/ef/core/modeling/entity-properties)定义数据库映射属性的操作. -> 必须在使用相关的 `DbContext` 之前执行此代码. 应用程序启动模板定义了一个名为 `YourProjectNameEntityExtensions` 的静态类. 你可以在此类中定义扩展确保在正确的时间执行它. 否则你需要自己处理. +> 必须在使用相关的 `DbContext` 之前执行此代码. 应用程序启动模板定义了一个名为 `YourProjectNameEfCoreEntityExtensionMappings` 的静态类. 你可以在此类中定义扩展确保在正确的时间执行它. 否则你需要自己处理. 定义实体扩展后你需要使用EF Core的[Add-Migration](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#add-migration)和[Update-Database](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#update-database)命令来创建code first迁移类并更新数据库. @@ -58,8 +58,6 @@ ObjectExtensionManager.Instance ## 创建新实体映射到同一个数据库表/Collection -尽管额外属性方法**易于使用**并且适用于一些场景,但它具有[实体文档](Entities.md)中描述的一些缺点. - 另一个方法是**创建你自己的实体**映射到**同一个数据库库**(对于MongoDB数据库是collection) [应用程序启动模板](Startup-Templates/Application.md)的 `AppUser` 已经实现了这种方法. [EF Core迁移文档](Entity-Framework-Core-Migrations.md)描述了在这些情况下如何实现和管理**EF Core数据库迁移**. 这种方法同样适用于MongoDB,但你不需要处理数据库迁移问题. diff --git a/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md b/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md index cb40de9bdb..caa448a14a 100644 --- a/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md +++ b/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md @@ -54,12 +54,13 @@ context.Services.Replace( ## 重写一个服务类 -大多数情况下,你会仅想改变服务当前实现的一个或几个方法. 重新实现完整的接口变的繁琐,更好的方法是继承原始类并重写方法。 +大多数情况下,你会仅想改变服务当前实现的一个或几个方法. 重新实现完整的接口变的繁琐,更好的方法是继承原始类并重写方法. ### 示例: 重写服务方法 ````csharp [Dependency(ReplaceServices = true)] +[ExposeServices(typeof(IIdentityUserAppService), typeof(IdentityUserAppService))] public class MyIdentityUserAppService : IdentityUserAppService { //... @@ -161,6 +162,105 @@ public class MyIdentityUserManager : IdentityUserManager 控制器,框架服务,视图组件类以及其他类型注册到依赖注入的类都可以像上面的示例那样被重写. +## 扩展数据传输对象 + +你可以如[扩展实体文档](Customizing-Application-Modules-Extending-Entities.md)所述扩展实体. 并使用上面介绍的重写相关服务**使用自定义属性****执行其他业务逻辑**. + +应用程序使用的数据传输对象(**DTO**)同样可扩展. 这样你可以使服务返回其他属性并在UI(或其他客户端)得到其他属性. + +### 示例 + +假设你已经按照[扩展实体文档](Customizing-Application-Modules-Extending-Entities.md)中的说明添加了 `SocialSecurityNumber` 并希望从 `IdentityUserAppService的GetListAsync` 方法获取用户列表时包括此属性. + +你可以使用[对象扩展系统](Object-Extensions.md)将属性添加到 `IdentityUserDto`. 在应用程序启动模板带有的 `YourProjectNameDtoExtensions` 类中编写以下代码: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "SocialSecurityNumber" + ); +```` + +这段代码为 `IdentityUserDto` 类添加了 `string` 类型的 `SocialSecurityNumber` 属性. 现在你可以在RREST API客户端调用 `/api/identity/users` HTTP API(内部使用 `IdentityUserAppService`),你会在 `extraProperties` 部分看到 `SocialSecurityNumber` 值. + +````json +{ + "totalCount": 1, + "items": [{ + "tenantId": null, + "userName": "admin", + "name": "admin", + "surname": null, + "email": "admin@abp.io", + "emailConfirmed": false, + "phoneNumber": null, + "phoneNumberConfirmed": false, + "twoFactorEnabled": false, + "lockoutEnabled": true, + "lockoutEnd": null, + "concurrencyStamp": "b4c371a0ab604de28af472fa79c3b70c", + "isDeleted": false, + "deleterId": null, + "deletionTime": null, + "lastModificationTime": "2020-04-09T21:25:47.0740706", + "lastModifierId": null, + "creationTime": "2020-04-09T21:25:46.8308744", + "creatorId": null, + "id": "8edecb8f-1894-a9b1-833b-39f4725db2a3", + "extraProperties": { + "SocialSecurityNumber": "123456789" + } + }] +} +```` + +手动添加了 `123456789` 值到数据库中. + +所有预构建的模块都在DTO中支持额外属性,你可以对其轻松的配置. + +### 定义检查 + +当为实体[定义](Customizing-Application-Modules-Extending-Entities.md)额外的属性时,由于安全性它不会自动出现在所有相关的DTO中. 额外属性可能包含敏感数据并且你可能不想默认公开给客户端. + +因此如果要用于DTO,需要为相应的DTO显式定义相同的属性(如上所述). 如果要允许在用户创建时进行设置还需要为 `IdentityUserCreateDto` 定义. + +如果属性并不是安全敏感,这可能会很枯燥. 对象扩展系统允许你忽略检查定义的属性. 参阅示例: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "SocialSecurityNumber", + options => + { + options.MapEfCore(b => b.HasMaxLength(32)); + options.CheckPairDefinitionOnMapping = false; + } + ); +```` + +这是定义实体属性的另一种方法( 有关 `ObjectExtensionManager` 更多信息,请参阅[文档](Object-Extensions.md)). 这次我们设置了 `CheckPairDefinitionOnMapping` 为false,在将实体映射到DTO时会跳过定义检查. + +如果你不喜欢这种方法,但想简单的向多个对象(DTO)添加单个属, `AddOrUpdateProperty` 可以使用类型数组添加额外的属性: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + new[] + { + typeof(IdentityUserDto), + typeof(IdentityUserCreateDto), + typeof(IdentityUserUpdateDto) + }, + "SocialSecurityNumber" + ); +```` + +### 关于用户界面 + +该系统允许你向实体和DTO添加额外的属性并执行自定义业务代码,但它与用户界面无关. + +参阅 [重写用户界面](Customizing-Application-Modules-Overriding-User-Interface.md) 指南了解关于UI部分. + ## 如何找到服务? -[模块文档](Modules/Index.md) 包含了定义的主要服务列表. 另外 你也可以查看[源码](https://github.com/abpframework/abp/tree/dev/modules)找到所有的服务. \ No newline at end of file +[模块文档](Modules/Index.md) 包含了定义的主要服务列表. 另外 你也可以查看[源码](https://github.com/abpframework/abp/tree/dev/modules)找到所有的服务. diff --git a/docs/zh-Hans/Entities.md b/docs/zh-Hans/Entities.md index 96d75895d2..fd8528dc30 100644 --- a/docs/zh-Hans/Entities.md +++ b/docs/zh-Hans/Entities.md @@ -72,7 +72,7 @@ public class BookAppService : ApplicationService, IBookAppService * `BookAppService` 注入图书实体的默认[仓库](Repositories.md),使用`InsertAsync`方法插入 `Book` 到数据库中. * `GuidGenerator`类型是 `IGuidGenerator`,它是在`ApplicationService`基类中定义的属性. ABP将这样常用属性预注入,所以不需要手动[注入](Dependency-Injection.md). -* 如果您想遵循DDD最佳实践,请参阅下面的*聚合示例*部分. +* 如果你想遵循DDD最佳实践,请参阅下面的*聚合示例*部分. ### 具有复合键的实体 @@ -228,7 +228,7 @@ ABP框架不强制你应用任何DDD规则或模式.但是,当你准备应用的 ## 基类和接口的审计属性 -有一些属性,像`CreationTime`,`CreatorId`,`LastModificationTime`...在所有应用中都很常见. ABP框架提供了一些接口和基类来**标准化**这些属性,并**自动设置它们的值**. +有一些属性,像`CreationTime`,`CreatorId`,`LastModificationTime`...在所有应用中都很常见. ABP框架提供了一些接口和基类来**标准化**这些属性,并**自动设置它们的值**. ### 审计接口 @@ -285,7 +285,7 @@ ABP框架不强制你应用任何DDD规则或模式.但是,当你准备应用的 所有这些基类都有非泛型版本,可以使用 `AuditedEntity` 和 `FullAuditedAggregateRoot` 来支持复合主键; -所有这些基类也有 `... WithUser`,像 `FullAuditedAggregateRootWithUser` 和 `FullAuditedAggregateRootWithUser`. 这样就可以将导航属性添加到你的用户实体. 但在聚合根之间添加导航属性不是一个好做法,所以这种用法是不建议的(除非你使用EF Core之类的ORM可以很好地支持这种情况,并且你真的需要它. 请记住这种方法不适用于NoSQL数据库(如MongoDB),你必须真正实现聚合模式). +所有这些基类也有 `... WithUser`,像 `FullAuditedAggregateRootWithUser` 和 `FullAuditedAggregateRootWithUser`. 这样就可以将导航属性添加到你的用户实体. 但在聚合根之间添加导航属性不是一个好做法,所以这种用法是不建议的(除非你使用EF Core之类的ORM可以很好地支持这种情况,并且你真的需要它. 请记住这种方法不适用于NoSQL数据库(如MongoDB),你必须真正实现聚合模式). ## 额外的属性 @@ -379,10 +379,10 @@ public static class IdentityUserExtensions * 这些属性**不容易[自动映射](Object-To-Object-Mapping.md)到其他对象**. * 它**不会**为EF Core在数据库表中**创建字段**,因此在数据库中针对这个字段创建索引或搜索/排序并不容易. -### 额外属性背后的实体 +### 额外属性背后的实体 `IHasExtraProperties` 不限于与实体一起使用. 你可以为任何类型的类实现这个接口,使用 `GetProperty`,`SetProperty` 和其他相关方法. ## 另请参阅 -* [实体设计最佳实践指南](Best-Practices/Entities.md) \ No newline at end of file +* [实体设计最佳实践指南](Best-Practices/Entities.md) diff --git a/docs/zh-Hans/Entity-Framework-Core-Migrations.md b/docs/zh-Hans/Entity-Framework-Core-Migrations.md index caef6e76f2..caf5778753 100644 --- a/docs/zh-Hans/Entity-Framework-Core-Migrations.md +++ b/docs/zh-Hans/Entity-Framework-Core-Migrations.md @@ -1,7 +1,7 @@  # EF Core数据库迁移 -本文首先介绍[应用程序启动模板](Startup-Templates/Application.md)提供的**默认结构**,并讨论您可能希望为自己的应用程序实现的**各种场景**. +本文首先介绍[应用程序启动模板](Startup-Templates/Application.md)提供的**默认结构**,并讨论你可能希望为自己的应用程序实现的**各种场景**. > 本文档适用于希望完全理解和自定义[应用程序启动模板](Startup-Templates/Application.md)附带的数据库结构的人员. 如果你只是想创建实体和管理代码优先(code first)迁移,只需要遵循[启动教程](Tutorials/Index.md). @@ -95,7 +95,7 @@ Volo.Abp.IdentityServer.AbpIdentityServerDbProperties.DbTablePrefix = "Ids"; 这个项目有应用程序的 `DbContext`类(本例中的 `BookStoreDbContex` ). -**每个模块都使用自己的 `DbContext` 类**来访问数据库。同样你的应用程序有它自己的 `DbContext`. 通常在应用程序中使用这个 `DbContet`(如果你遵循最佳实践,应该在[仓储](Repositories.md)中使用). 它几乎是一个空的 `DbContext`,因为你的应用程序在一开始没有任何实体,除了预定义的 `AppUser` 实体: +**每个模块都使用自己的 `DbContext` 类**来访问数据库.同样你的应用程序有它自己的 `DbContext`. 通常在应用程序中使用这个 `DbContet`(如果你遵循最佳实践,应该在[仓储](Repositories.md)中使用). 它几乎是一个空的 `DbContext`,因为你的应用程序在一开始没有任何实体,除了预定义的 `AppUser` 实体: ````csharp [ConnectionStringName("Default")] @@ -268,10 +268,10 @@ public class BackgroundJobsDbContext ##### 重用模块的表 -您可能想在应用程序中**重用依赖模块的表**. 在这种情况下你有两个选择: +你可能想在应用程序中**重用依赖模块的表**. 在这种情况下你有两个选择: 1. 你可以**直接使用模块定义的实体**(你仍然可以在某种程度上[扩展实体](Customizing-Application-Modules-Extending-Entities.md)). -2. 你可以**创建一个新的实体**映射到同一个数据库表。 +2. 你可以**创建一个新的实体**映射到同一个数据库表. ###### 使用由模块定义的实体 @@ -307,7 +307,7 @@ namespace Acme.BookStore 示例注入了 `IRepository`(默认仓储). 它定义了标准的存储库方法并实现了 `IQueryable` 接口. -另外,身份模块定义了 `IIdentityUserRepository`(自定义仓储),你的应用程序也可以注入和使用它. `IIdentityUserRepository` 为 `IdentityUser` 实体提供了额外的定制方法,但它没有实现 `IQueryable`. +另外,身份模块定义了 `IIdentityUserRepository`(自定义仓储),你的应用程序也可以注入和使用它. `IIdentityUserRepository` 为 `IdentityUser` 实体提供了额外的定制方法,但它没有实现 `IQueryable`. ###### 创建一个新的实体 @@ -352,7 +352,7 @@ namespace Acme.BookStore.Roles * 它继承了[`AggregateRoot`类](Entities.md)和实现了[`IMultiTenant`]接口(Multi-Tenancy.md),因为 `IdentityRole` 也做了同样的继承. * 你可以添加 `IdentityRole` 实体定义的任何属性. 本例只加了 `TenantId` 和 `Name` 属性,因为我们这里只需要它们. 你可以把setters设置为私有(如同本例)以防意外更改身份模块的属性. * 你可以添加自定义(附加)属性. 本例添加了 `Title` 属性. -* **构造函数是私有的**,所以它不允许直接创建一个新的 `AppRole` 实体。创建角色身份模块的责任. 你可以查询角色,设置/更新自定义属性,但做为最佳实践你不应该在代码中创建和删除角色(尽管没有强制的限制). +* **构造函数是私有的**,所以它不允许直接创建一个新的 `AppRole` 实体.创建角色身份模块的责任. 你可以查询角色,设置/更新自定义属性,但做为最佳实践你不应该在代码中创建和删除角色(尽管没有强制的限制). 现在是时候定义EF Core映射. 打开应用程序的 `DbContext` (此示例中是 `BookStoreDbContext` )添加以下属性: @@ -360,7 +360,7 @@ namespace Acme.BookStore.Roles public DbSet Roles { get; set; } ```` -然后在 `OnModelCreating` 方法中配置映射(调用 `base.OnModelCreating(builder)` 之后): +然后在 `OnModelCreating` 方法中配置映射(调用 `base.OnModelCreating(builder)` 之后): ````csharp protected override void OnModelCreating(ModelBuilder builder) diff --git a/docs/zh-Hans/Entity-Framework-Core-MySQL.md b/docs/zh-Hans/Entity-Framework-Core-MySQL.md index 5f9d8023ab..15e2205c38 100644 --- a/docs/zh-Hans/Entity-Framework-Core-MySQL.md +++ b/docs/zh-Hans/Entity-Framework-Core-MySQL.md @@ -12,12 +12,12 @@ ## UseMySQL() -查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseMySQL()`. 检查下列文件: +查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseMySQL()`. 检查下列文件: * `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. * `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. -> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. +> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. ## 更改连接字符串 @@ -27,7 +27,7 @@ MySQL连接字符串与SQL Server连接字符串不同. 所以检查你的解决 ## 更改迁移DbContext -MySQL DBMS与SQL Server有一些细微的差异. 某些模块数据库映射配置(尤其是字段长度)会导致MySQL出现问题. 例如某些[IdentityServer模块](Modules/IdentityServer.md)表就存在这样的问题,它提供了一个选项可以根据您的DBMS配置字段. +MySQL DBMS与SQL Server有一些细微的差异. 某些模块数据库映射配置(尤其是字段长度)会导致MySQL出现问题. 例如某些[IdentityServer模块](Modules/IdentityServer.md)表就存在这样的问题,它提供了一个选项可以根据你的DBMS配置字段. 启动模板包含*YourProjectName*MigrationsDbContext,它负责维护和迁移数据库架构. 此DbContext基本上调用依赖模块的扩展方法来配置其数据库表. diff --git a/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md b/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md index ca12d7524f..ca70d8b5c6 100644 --- a/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md +++ b/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md @@ -63,7 +63,7 @@ MySQL连接字符串与SQL Server连接字符串不同. 所以检查你的解决 ## 更改迁移DbContext -MySQL DBMS与SQL Server有一些细微的差异. 某些模块数据库映射配置(尤其是字段长度)会导致MySQL出现问题. 例如某些[IdentityServer模块](Modules/IdentityServer.md)表就存在这样的问题,它提供了一个选项可以根据您的DBMS配置字段. +MySQL DBMS与SQL Server有一些细微的差异. 某些模块数据库映射配置(尤其是字段长度)会导致MySQL出现问题. 例如某些[IdentityServer模块](Modules/IdentityServer.md)表就存在这样的问题,它提供了一个选项可以根据你的DBMS配置字段. 启动模板包含*YourProjectName*MigrationsDbContext,它负责维护和迁移数据库架构. 此DbContext基本上调用依赖模块的扩展方法来配置其数据库表. diff --git a/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md b/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md index bb1de20207..08f8f405d1 100644 --- a/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md +++ b/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md @@ -12,12 +12,12 @@ ## UsePostgreSql() -查找你的解决方案中 `UseSqlServer()`调用,替换为 `UsePostgreSql()`. 检查下列文件: +查找你的解决方案中 `UseSqlServer()`调用,替换为 `UsePostgreSql()`. 检查下列文件: * `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. * `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. -> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. +> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. ## 更改连接字符串 diff --git a/docs/zh-Hans/Entity-Framework-Core-SQLite.md b/docs/zh-Hans/Entity-Framework-Core-SQLite.md index 4b8e009fe4..d75f8ba5b7 100644 --- a/docs/zh-Hans/Entity-Framework-Core-SQLite.md +++ b/docs/zh-Hans/Entity-Framework-Core-SQLite.md @@ -12,12 +12,12 @@ ## UseSqlite() -查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseSqlite()`. 检查下列文件: +查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseSqlite()`. 检查下列文件: * `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. * `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. -> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. +> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. ## 更改连接字符串 diff --git a/docs/zh-Hans/Entity-Framework-Core.md b/docs/zh-Hans/Entity-Framework-Core.md index d2af31f4d2..5ae6ce1070 100644 --- a/docs/zh-Hans/Entity-Framework-Core.md +++ b/docs/zh-Hans/Entity-Framework-Core.md @@ -108,7 +108,7 @@ protected override void OnModelCreating(ModelBuilder builder) ### 配置连接字符串选择 如果你的应用程序有多个数据库,你可以使用 `connectionStringName]` Attribute为你的DbContext配置连接字符串名称. -例: +例: ```csharp [ConnectionStringName("MySecondConnString")] @@ -274,7 +274,7 @@ public override async Task DeleteAsync( ## 访问 EF Core API -大多数情况下应该隐藏仓储后面的EF Core API(这也是仓储的设计目地). 但是如果想要通过仓储访问DbContext实现,则可以使用`GetDbContext()`或`GetDbSet()`扩展方法. 例: +大多数情况下应该隐藏仓储后面的EF Core API(这也是仓储的设计目地). 但是如果想要通过仓储访问DbContext实现,则可以使用`GetDbContext()`或`GetDbSet()`扩展方法. 例: ````csharp public class BookService @@ -304,7 +304,7 @@ public class BookService 默认,实体的所有额外属性存储在数据库的一个 `JSON` 对象中. -实体扩展系统允许你存储额外属性在数据库的单独字段中. 有关额外属性和实体扩展系统的更多信息,请参阅下列文档: +实体扩展系统允许你存储额外属性在数据库的单独字段中. 有关额外属性和实体扩展系统的更多信息,请参阅下列文档: * [自定义应用模块: 扩展实体](Customizing-Application-Modules-Extending-Entities.md) * [实体](Entities.md) @@ -313,7 +313,7 @@ public class BookService ### ObjectExtensionManager.Instance -`ObjectExtensionManager` 实现单例模式,因此你需要使用静态的 `ObjectExtensionManager.Instance` 来执行所有操作。 +`ObjectExtensionManager` 实现单例模式,因此你需要使用静态的 `ObjectExtensionManager.Instance` 来执行所有操作. ### MapEfCoreProperty @@ -417,7 +417,7 @@ context.Services.AddAbpDbContext(options => }); ```` -现在,您的自定义仓储也可以使用`IBookStoreDbContext`接口: +现在,你的自定义仓储也可以使用`IBookStoreDbContext`接口: ````csharp public class BookRepository : EfCoreRepository, IBookRepository diff --git a/docs/zh-Hans/Exception-Handling.md b/docs/zh-Hans/Exception-Handling.md index 6bbbed56d4..3d8408b26d 100644 --- a/docs/zh-Hans/Exception-Handling.md +++ b/docs/zh-Hans/Exception-Handling.md @@ -295,8 +295,8 @@ services.Configure(options => 框架会自动抛出以下异常类型: -- 当用户没有权限执行操作时,会抛出 `AbpAuthorizationException` 异常. 有关更多信息,请参阅授权文档(TODO:link). -- 如果当前请求的输入无效,则抛出`AbpValidationException 异常`. 有关更多信息,请参阅授权文档(TODO:link). +- 当用户没有权限执行操作时,会抛出 `AbpAuthorizationException` 异常. 有关更多信息,请参阅授权文档[authorization](Authorization.md). +- 如果当前请求的输入无效,则抛出`AbpValidationException 异常`. 有关更多信息,请参阅[验证文档](Validation.md). - 如果请求的实体不存在,则抛出`EntityNotFoundException` 异常. 此异常大多数由 [repositories](Repositories.md) 抛出. 你同样可以在代码中抛出这些类型的异常(虽然很少需要这样做) diff --git a/docs/zh-Hans/Getting-Started-AspNetCore-Application.md b/docs/zh-Hans/Getting-Started-AspNetCore-Application.md index d6e0e1f570..46348ecbdf 100644 --- a/docs/zh-Hans/Getting-Started-AspNetCore-Application.md +++ b/docs/zh-Hans/Getting-Started-AspNetCore-Application.md @@ -156,7 +156,7 @@ services.AddApplication(options => }); ```` -4. 更新 `Program.cs`代码, 不再使用`WebHost.CreateDefaultBuilder()`方法(因为它使用默认的DI容器): +4. 更新 `Program.cs`代码, 不再使用`WebHost.CreateDefaultBuilder()`方法(因为它使用默认的DI容器): ````csharp public class Program @@ -186,4 +186,4 @@ public class Program ### 源码 -从[此处](https://github.com/abpframework/abp/tree/dev/samples/BasicAspNetCoreApplication)获取本教程中创建的示例项目的源代码. +从[此处](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication)获取本教程中创建的示例项目的源代码. diff --git a/docs/zh-Hans/Getting-Started-AspNetCore-MVC-Template.md b/docs/zh-Hans/Getting-Started-AspNetCore-MVC-Template.md index 90291e968f..2778f3db42 100644 --- a/docs/zh-Hans/Getting-Started-AspNetCore-MVC-Template.md +++ b/docs/zh-Hans/Getting-Started-AspNetCore-MVC-Template.md @@ -4,7 +4,7 @@ 本教程使用 **ABP CLI** 创建一个新项目. 更多选项, 请参阅[入门](https://abp.io/get-started)页面. -如果你之前未安装,请使用命令行安装ABP CLI: +如果你之前未安装,请使用命令行安装ABP CLI: ````bash dotnet tool install -g Volo.Abp.Cli diff --git a/docs/zh-Hans/Getting-Started-Console-Application.md b/docs/zh-Hans/Getting-Started-Console-Application.md index 474323beea..cac3d2f153 100644 --- a/docs/zh-Hans/Getting-Started-Console-Application.md +++ b/docs/zh-Hans/Getting-Started-Console-Application.md @@ -121,4 +121,4 @@ namespace AbpConsoleDemo ### 源码 -从[这里](https://github.com/abpframework/abp/tree/dev/samples/BasicConsoleApplication)获取本教程中创建的示例项目的源代码. \ No newline at end of file +从[这里](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication)获取本教程中创建的示例项目的源代码. \ No newline at end of file diff --git a/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md b/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md index 92b8596ee3..8b282bbe56 100644 --- a/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md +++ b/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md @@ -1,3 +1,198 @@ # 如何对MVC / Razor页面应用程序使用Azure Active Directory身份验证 -TODO... \ No newline at end of file +本文介绍了如何将AzureAD集成到ABP应用程序中,用 **Azure Active Directory** 凭据使用 OAuth 2.0 登录. + +添加Azure Active Directory到ABP框架非常简单,只需要正确的完成几个配置. + +为了覆盖更多范围,我们演示两种不同的集成AzureAD的**方法**. + +1. **AddAzureAD**: 该方法使用微软[AzureAD UI nuget 包](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/),在网络上搜索如何将AzureAD集成到应用程序时,这个包是最流行的. + +2. **AddOpenIdConnect**: 该方法使用默认的[OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/). 它不仅可用于AzureAD,还可用于所有OpenId连接. + +> 这些方法之间的功能**没有区别**,AddAzureAD是具有预定义Cookie设置的OpenIdConnection([源](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADAuthenticationBuilderExtensions.cs#L122))的抽象方法. +> +> 但是默认配置的登录方案在与ABP应用程序集成方面存在关键差异,下面将对此进行说明. + +## 1. AddAzureAD + +这个方法使用 [Microsoft AzureAD UI nuget 包](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/),它是最常用的集成AzureAD方法. + +如果选择这种方法,需要将 `Microsoft.AspNetCore.Authentication.AzureAD.UI` 软件包安装到 **.Web** 项目中. 由于AddAzureAD扩展使用[配置绑定](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#default-configuration),你需要更改 **.Web** 项目中的appsettings.json文件. + +#### **更改 `appsettings.json`** + +你添加向 `appsettings.json` 添加新的配置节,在配置 `OpenIdConnectOptions` 时绑定配置: + +````json + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "", + "ClientId": "", + "Domain": "domain.onmicrosoft.com", + "CallbackPath": "/signin-azuread-oidc" + } +```` + +> 这里重要的配置是CallbackPath. 值必须与你的 Azure AD-> app registrations-> Authentication -> RedirectUri 之一相同. + +然后你需要配置 `OpenIdConnectOptions` 完成集成. + +#### 配置 OpenIdConnectOptions + +在你的 **.Web** 项目找到 **ApplicationWebModule** 使用以下代码修改 `ConfigureAuthentication` 方法: + +````csharp +private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) + { + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); + context.Services.AddAuthentication() + .AddIdentityServerAuthentication(options => + { + options.Authority = configuration["AuthServer:Authority"]; + options.RequireHttpsMetadata = false; + options.ApiName = "Acme.BookStore"; + }) + .AddAzureAD(options => configuration.Bind("AzureAd", options)); + + context.Services.Configure(AzureADDefaults.OpenIdScheme, options => + { + options.Authority = options.Authority + "/v2.0/"; + options.ClientId = configuration["AzureAd:ClientId"]; + options.CallbackPath = configuration["AzureAd:CallbackPath"]; + options.ResponseType = OpenIdConnectResponseType.CodeIdToken; + options.RequireHttpsMetadata = false; + + options.TokenValidationParameters.ValidateIssuer = false; + options.GetClaimsFromUserInfoEndpoint = true; + options.SaveTokens = true; + options.SignInScheme = IdentityConstants.ExternalScheme; + + options.Scope.Add("email"); + }); + } +```` + +> **不要忘记:** +> +> * 在 `AddAuthentication()` 之后添加 `.AddAzureAD(options => configuration.Bind("AzureAd", options))` . 它绑定了你的 AzureAD 配置并且容易忘记. +> * 添加 `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear()`. 它会禁用默认的 Microsoft claim type 映射. +> * 添加 `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier)`. 映射 [ClaimTypes.NameIdentifier](https://github.com/dotnet/runtime/blob/6d395de48ac718a913e567ae80961050f2a9a4fa/src/libraries/System.Security.Claims/src/System/Security/Claims/ClaimTypes.cs#L59) 很重要,因为默认SignIn Manager和行为使用这个claim type用于外部登录信息. +> * 添加 `options.SignInScheme = IdentityConstants.ExternalScheme` 因为 [默认登录方法为 `AzureADOpenID`](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADOpenIdConnectOptionsConfiguration.cs#L35). +> * 如果你使用的是 **v2.0** 端点,应添加 `options.Scope.Add("email")` 因为 v2.0 端点不会将 `email` 做为默认值返回. [账户模块](../Modules/Account.md) 使用 `email` claim 来 [注册外部账户](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L215). + +你已经完成了集成. + +## 2. 替代方法: AddOpenIdConnect + +如果你不想在应用程序安装一个额外的NuGet包,你可以使用默认的[OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/),它适用于所有的OpenId连接,包括AzureAD外部认证. + +你不必使用 `appsettings.json` 配置, 但将AzureAD信息放在 `appsettings.json` 是一个很好的做法. + +为了从 `appsettings.json` 获取AzureAD信息在 `OpenIdConnectOptions` 配置使用,只需要在你的 **.Web** 项目中的 `appsettings.json` 添加一个新的配置节: + +````json + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "", + "ClientId": "", + "Domain": "domain.onmicrosoft.com", + "CallbackPath": "/signin-azuread-oidc" + } +```` + +然后在你的 **.Web** 项目的 **ApplicationWebModule** 用以下代码修改 `ConfigureAuthentication` 方法: + +````csharp +private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) + { + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); + + context.Services.AddAuthentication() + .AddIdentityServerAuthentication(options => + { + options.Authority = configuration["AuthServer:Authority"]; + options.RequireHttpsMetadata = false; + options.ApiName = "BookStore"; + }) + .AddOpenIdConnect("AzureOpenId", "Azure Active Directory OpenId", options => + { + options.Authority = "https://login.microsoftonline.com/" + configuration["AzureAd:TenantId"] + "/v2.0/"; + options.ClientId = configuration["AzureAd:ClientId"]; + options.ResponseType = OpenIdConnectResponseType.CodeIdToken; + options.CallbackPath = configuration["AzureAd:CallbackPath"]; + options.RequireHttpsMetadata = false; + options.SaveTokens = true; + options.GetClaimsFromUserInfoEndpoint = true; + + options.Scope.Add("email"); + }); + } +```` + +集成结束. 请记住你可以连接任何其他外部认证供应商. + +## 本文的源代码 + +你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization)找到已完成的示例源码. + +# FAQ + +* Help! `GetExternalLoginInfoAsync` 返回 `null`! + + * 有两方面的原因; + + 1. 你在尝试验证错误的方案. 检查是否设置 **SignInScheme** 为 `IdentityConstants.ExternalScheme`: + + ````csharp + options.SignInScheme = IdentityConstants.ExternalScheme; + ```` + + 2. 你的 `ClaimTypes.NameIdentifier` 为 `null`. 检查是否添加 claim 映射: + + ````csharp + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); + ```` + +* Help! 我一直得到 ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** 错误! + + * 如果你在appsettings设置 **CallbackPath** 为: + + ````csharp + "AzureAd": { + ... + "CallbackPath": "/signin-azuread-oidc" + } + ```` + + 你在azure门户的应用程序**重定向URI**必须具有之类 `https://localhost:44320/signin-azuread-oidc` 的域, 而不仅是 `/signin-azuread-oidc`. + +* Help! 我一直得到 ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** 错误! + + * 当你使用 Azure Authority **v2.0 端点** 而不请求 `email` 域, 会发生这些情况. [Abp 创建用户检查了唯一的邮箱](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). 只需添加 + + ````csharp + options.Scope.Add("email"); + ```` + + 到你的 openid 配置. + +* 如何**调试/监视**在映射之前获得的声明? + + * 你可以在 openid 配置下加一个简单的事件在映射之前进行调试,例如: + + ````csharp + options.Events.OnTokenValidated = (async context => + { + var claimsFromOidcProvider = context.Principal.Claims.ToList(); + await Task.CompletedTask; + }); + ```` + +## 另请参阅 + +* [如何为MVC / Razor页面应用程序自定义登录页面](Customize-Login-Page-MVC.md). +* [如何为ABP应用程序定制SignIn Manager](Customize-SignIn-Manager.md). \ No newline at end of file diff --git a/docs/zh-Hans/How-To/Customize-SignIn-Manager.md b/docs/zh-Hans/How-To/Customize-SignIn-Manager.md index 2667509aca..2b6f7a612f 100644 --- a/docs/zh-Hans/How-To/Customize-SignIn-Manager.md +++ b/docs/zh-Hans/How-To/Customize-SignIn-Manager.md @@ -1,3 +1,101 @@ # 如何为ABP应用程序定制SignIn Manager -TODO... \ No newline at end of file +在使用[应用程序启动模板](../Startup-Templates/Application.md)创建新项目后,你可能想要扩展或更改SignIn Manager的默认行为,以满足你需要的身份验证和注册流程. ABP[账户模块](../Modules/Account.md)使用[身份管理模块](../Modules/Identity.md)做为SignIn Manager,而[身份管理模块](../Modules/Identity.md)使用默认的[Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs)([参阅此处]((https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs#L17))). + +编写自定义SignIn Manager,你需要扩展[Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs)类并注入到DI容器. + +本文介绍了如何为你自己的应用程序自定义SignIn Manager. + +## 创建 CustomSignInManager + +创建一个类并继承自Microsoft Identity 包的 [SignInMager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs). + +````csharp +public class CustomSignInManager : Microsoft.AspNetCore.Identity.SignInManager +{ + public CustomSignInManager( + Microsoft.AspNetCore.Identity.UserManager userManager, + Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor, + Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory claimsFactory, + Microsoft.Extensions.Options.IOptions optionsAccessor, + Microsoft.Extensions.Logging.ILogger> logger, + Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes, + Microsoft.AspNetCore.Identity.IUserConfirmation confirmation) + : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) + { + } +} +```` + +> 重点是使用**Volo.Abp.Identity.IdentityUser**做为泛型参数,而不是应用程序的AppUser. + +然后你可以覆盖SignIn Manager的任何方法并且为你的身份验证和注册流程添加需要的方法和属性. + +## 重写 GetExternalLoginInfoAsync 方法 + +在这个用例中我们重写第三方身份验证时使用的 `GetExternalLoginInfoAsync` 方法实现. + +一个好的开始是从复制[源码](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Identity/Core/src/SignInManager.cs#L638-L674)而不是从零开始. 在这个用例中我们对源码进行较少的修改,为了帮助理解概念它显式显示了方法和属性的命名空间. + +````csharp +public override async Task GetExternalLoginInfoAsync(string expectedXsrf = null) +{ + var auth = await Context.AuthenticateAsync(Microsoft.AspNetCore.Identity.IdentityConstants.ExternalScheme); + var items = auth?.Properties?.Items; + if (auth?.Principal == null || items == null || !items.ContainsKey(LoginProviderKey)) + { + return null; + } + + if (expectedXsrf != null) + { + if (!items.ContainsKey(XsrfKey)) + { + return null; + } + var userId = items[XsrfKey] as string; + if (userId != expectedXsrf) + { + return null; + } + } + + var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); + var provider = items[LoginProviderKey] as string; + if (providerKey == null || provider == null) + { + return null; + } + + var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName + ?? provider; + return new Microsoft.AspNetCore.Identity.ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) + { + AuthenticationTokens = auth.Properties.GetTokens() + }; +} +```` + +要使你自定义的SignIn Manager类生效,你需要将其注册[依赖注入系统](../Dependency-Injection.md)中. + +## 注册到依赖注入 + +应该使用 [IdentityBuilder](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/IdentityBuilder.cs) 的 [IdentityBuilderExtensions](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/IdentityBuilderExtensions.cs) 类的 **AddSignInManager** 扩展方法注册 `CustomSignInManager`. + +在你的 `.Web` 项目找到 `YourProjectNameWebModule` 的 `PreConfigureServices` 方法添加以下代码替换老的 `SignInManager`: + +````csharp +PreConfigure(identityBuilder => +{ + identityBuilder.AddSignInManager(); +}); +```` + +## 本文的源代码 + +你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization)找到已完成的示例源码. + +## 另请参阅 + +* [如何为MVC / Razor页面应用程序自定义登录页面](Customize-Login-Page-MVC.md). +* [身份管理模块](../Modules/Identity.md). diff --git a/docs/zh-Hans/Index.md b/docs/zh-Hans/Index.md index d946d8f0bb..71e5a520da 100644 --- a/docs/zh-Hans/Index.md +++ b/docs/zh-Hans/Index.md @@ -12,7 +12,7 @@ ABP是一个**开源应用程序框架**,专注于基于ASP.NET Core的Web应用 * [ASP.NET Core MVC 模板](Getting-Started-AspNetCore-MVC-Template.md) -如果您想从头开始(使用空项目),请手动安装ABP框架并使用以下教程: +如果你想从头开始(使用空项目),请手动安装ABP框架并使用以下教程: * [控制台应用程序](Getting-Started-Console-Application.md) * [ASP.NET Core Web 应用程序](Getting-Started-AspNetCore-Application.md) diff --git a/docs/zh-Hans/Modules/Docs.md b/docs/zh-Hans/Modules/Docs.md index 20382d24f2..96452f2bd5 100644 --- a/docs/zh-Hans/Modules/Docs.md +++ b/docs/zh-Hans/Modules/Docs.md @@ -326,7 +326,7 @@ There are no projects yet! {"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs/zh-Hans/","GitHubAccessToken":"***","GitHubUserAgent":""} ``` - 注意 `GitHubAccessToken` 用 `***` 掩盖. 这是一个私人令牌,你必须从GitHub获取它. 请参阅 https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/ + 注意 `GitHubAccessToken` 用 `***` 掩盖. 这是一个私人令牌,你必须从GitHub获取它. 请参阅 https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/ - MainWebsiteUrl: `/` @@ -338,7 +338,7 @@ There are no projects yet! INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName], [ParametersDocumentName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939658', N'ABP framework (GitHub)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'GitHub', N'{"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs","GitHubAccessToken":"***","GitHubUserAgent":""}', N'/', N'master', N'') ``` -请注意,`GitHubAccessToken` 被屏蔽了.它是一个私人令牌,你必须获得自己的令牌并替换 `***` 字符串. +请注意,`GitHubAccessToken` 被屏蔽了.它是一个私人令牌,你必须获得自己的令牌并替换 `***` 字符串. 现在你可以运行应用程序并导航到 `/Documents`. @@ -378,9 +378,9 @@ INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocume INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName], [ParametersDocumentName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939659', N'ABP framework (FileSystem)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'FileSystem', N'{"Path":"C:\\Github\\abp\\docs"}', N'/', NULL, N'') ``` -添加上面的一个示例项目后运行该应用程序. 在菜单中你会看到`文档` 链接,点击菜单链接打开文档页面. +添加上面的一个示例项目后运行该应用程序. 在菜单中你会看到`文档` 链接,点击菜单链接打开文档页面. -到目前为止, 我们已经从abp.io网站创建了一个新的应用程序,并为Docs模块做好准备. +到目前为止, 我们已经从abp.io网站创建了一个新的应用程序,并为Docs模块做好准备. ### 7- 添加一个新文档 @@ -454,7 +454,7 @@ public class Person } ``` -因为并不是项目中的每个文档都有章节或者不需要所有的参数,你必须声明哪些参数将用于对文档进行分段,在文档的任何地方都可以使用JSON块. +因为并不是项目中的每个文档都有章节或者不需要所有的参数,你必须声明哪些参数将用于对文档进行分段,在文档的任何地方都可以使用JSON块. 例如 [Getting-Started.md](https://github.com/abpio/abp-commercial-docs/blob/master/en/getting-started.md): @@ -570,7 +570,7 @@ This document assumes that you prefer to use **{{ UI_Value }}** as the UI framew ![Navigation menu](../images/docs-module_download-sample-navigation-menu.png) -最后,为您的项目添加了一个新的Docs模块, 该模块由GitHub提供. +最后,为你的项目添加了一个新的Docs模块, 该模块由GitHub提供. ## 全文搜索(Elastic Search) diff --git a/docs/zh-Hans/Modules/Index.md b/docs/zh-Hans/Modules/Index.md index 9b44967c53..fb07b5b001 100644 --- a/docs/zh-Hans/Modules/Index.md +++ b/docs/zh-Hans/Modules/Index.md @@ -16,6 +16,7 @@ ABP是一个 **模块化的应用程序框架** 由十多个 **nuget packages** * **Background Jobs**: 用于在使用默认后台作业管理器时保存后台作业. * **Blogging**: 用于创建精美的博客. ABP的[博客](https://blog.abp.io/) 就使用了此模块. * [**Docs**](Docs.md): 用于创建技术文档页面. ABP的[文档](https://abp.io/documents/) 就使用了此模块. +* **Feature Management**: 用于保存和管理功能. * **Identity**: 基于Microsoft Identity管理角色,用户和他们的权限. * **Identity Server**: 集成了IdentityServer4. * **Permission Management**: 用于保存权限. @@ -27,4 +28,4 @@ ABP是一个 **模块化的应用程序框架** 由十多个 **nuget packages** ## 商业应用模块 -[ABP商业](https://commercial.abp.io/)许可证在ABP框架上提供了额外的预构建应用程序模块. 参见ABP商业版提供的[模块列表](https://commercial.abp.io/module). \ No newline at end of file +[ABP商业](https://commercial.abp.io/)许可证在ABP框架上提供了额外的预构建应用程序模块. 参见ABP商业版提供的[模块列表](https://commercial.abp.io/module). diff --git a/docs/zh-Hans/Multi-Tenancy.md b/docs/zh-Hans/Multi-Tenancy.md index 8913c27ede..49cf358205 100644 --- a/docs/zh-Hans/Multi-Tenancy.md +++ b/docs/zh-Hans/Multi-Tenancy.md @@ -172,11 +172,11 @@ namespace MyCompany.MyProject { options.Tenants = new[] { - new TenantInformation( + new TenantConfiguration( Guid.Parse("446a5211-3d72-4339-9adc-845151f8ada0"), //Id "tenant1" //Name ), - new TenantInformation( + new TenantConfiguration( Guid.Parse("25388015-ef1c-4355-9c18-f6b6ddbaf89d"), //Id "tenant2" //Name ) @@ -252,7 +252,7 @@ TODO: This package implements ITenantStore using a real database... #### 租户信息 -ITenantStore跟 **TenantInformation**类一起工作,并且包含了几个租户属性: +ITenantStore跟 **TenantConfiguration**类一起工作,并且包含了几个租户属性: * **Id**:租户的唯一Id. * **Name**: 租户的唯一名称. diff --git a/docs/zh-Hans/Nightly-Builds.md b/docs/zh-Hans/Nightly-Builds.md index 3d34330ca3..84ea243b05 100644 --- a/docs/zh-Hans/Nightly-Builds.md +++ b/docs/zh-Hans/Nightly-Builds.md @@ -23,3 +23,19 @@ 2. 将包源更改为`全部`. 3. 搜索nuget包. 你将看到包的预发布格式为`(VERSION)-preview(DATE)` (如本示例中的**v0.16.0-preview20190401**). 4. 你可以单击`安装`按钮将包添加到项目中. + +## 安装和卸载预览NPM包 + +预览NPM包的最新版本可以通过在应用程序的根文件夹命令运行命令安装: + +```bash +abp switch-to-preview +``` + +如果你正在使用ABP框架预览包,你可以使用此命令切换回稳定版本: + +```bash +abp switch-to-stable +``` + +参阅 [ABP CLI 文档](./CLI.md) 了解更多信息. \ No newline at end of file diff --git a/docs/zh-Hans/Object-Extensions.md b/docs/zh-Hans/Object-Extensions.md new file mode 100644 index 0000000000..a0f1e5534b --- /dev/null +++ b/docs/zh-Hans/Object-Extensions.md @@ -0,0 +1,267 @@ +# 对象扩展 + +ABP框架提供了 **实体扩展系统** 允许你 **添加额外属性** 到已存在的对象 **无需修改相关类**. 它允许你扩展[应用程序依赖模块](Modules/Index.md)实现的功能,尤其是当你要扩展[模块定义的实体](Customizing-Application-Modules-Extending-Entities.md)和[DTO](Customizing-Application-Modules-Overriding-Services.md)时. + +> 你自己的对象通常不需要对象扩展系统,因为你可以轻松的添加常规属性到你的类中. + +## IHasExtraProperties 接口 + +这是一个使类可扩展的接口. 它定义了 `Dictionary` 属性: + +````csharp +Dictionary ExtraProperties { get; } +```` + +然后你可以使用此字典添加或获取其他属性. + +### 基类 + +默认以下基类实现了 `IHasExtraProperties` 接口: + +* 由 `AggregateRoot` 类实现 (参阅 [entities](Entities.md)). +* 由 `ExtensibleEntityDto`, `ExtensibleAuditedEntityDto`... [DTO](Data-Transfer-Objects.md)基类实现. +* 由 `ExtensibleObject` 实现, 它是一个简单的基类,任何类型的对象都可以继承. + +如果你的类从这些类继承,那么你的类也是可扩展的,如果没有,你也可以随时手动继承. + +### 基本扩展方法 + +虽然可以直接使用类的 `ExtraProperties` 属性,但建议使用以下扩展方法使用额外属性. + +#### SetProperty + +用于设置额外属性值: + +````csharp +user.SetProperty("Title", "My Title"); +user.SetProperty("IsSuperUser", true); +```` + +`SetProperty` 返回相同的对象, 你可以使用链式编程: + +````csharp +user.SetProperty("Title", "My Title") + .SetProperty("IsSuperUser", true); +```` + +#### GetProperty + +用于读取额外属性的值: + +````csharp +var title = user.GetProperty("Title"); + +if (user.GetProperty("IsSuperUser")) +{ + //... +} +```` + +* `GetProperty` 是一个泛型方法,对象类型做为泛型参数. +* 如果未设置给定的属性,则返回默认值 (`int` 的默认值为 `0` , `bool` 的默认值是 `false` ... 等). + +##### 非基本属性类型 + +如果你的属性类型不是原始类型(int,bool,枚举,字符串等),你需要使用 `GetProperty` 的非泛型版本,它会返回 `object`. + +#### HasProperty + +用于检查对象之前是否设置了属性. + +#### RemoveProperty + +用于从对象中删除属性. 使用此方法代替为属性设置 `null` 值. + +### 一些最佳实践 + +为属性名称使用魔术字符串很危险,因为你很容易输入错误的属性名称-这并不安全; + +* 为你的额外属性名称定义一个常量. +* 使用扩展方法轻松设置你的属性. + +示例: + +````csharp +public static class IdentityUserExtensions +{ + private const string TitlePropertyName = "Title"; + + public static void SetTitle(this IdentityUser user, string title) + { + user.SetProperty(TitlePropertyName, title); + } + + public static string GetTitle(this IdentityUser user) + { + return user.GetProperty(TitlePropertyName); + } +} +```` + +然后, 你可以很容易地设置或获取 `Title` 属性: + +````csharp +user.SetTitle("My Title"); +var title = user.GetTitle(); +```` + +## Object Extension Manager + +你可以为可扩展对象(实现 `IHasExtraProperties`接口)设置任意属性, `ObjectExtensionManager` 用于显式定义可扩展类的其他属性. + +显式定义额外的属性有一些用例: + +* 允许控制如何在对象到对象的映射上处理额外的属性 (参阅下面的部分). +* 允许定义属性的元数据. 例如你可以在使用[EF Core](Entity-Framework-Core.md)时将额外的属性映射到数据库中的表字段. + +> `ObjectExtensionManager` 实现单例模式 (`ObjectExtensionManager.Instance`) ,你应该在应用程序启动之前定义对象扩展. [应用程序启动模板](Startup-Templates/Application.md) 有一些预定义的静态类,可以安全在内部定义对象扩展. + +### AddOrUpdate + +`AddOrUpdate` 是定义对象额外属性或更新对象额外属性的主要方法. + +示例: 为 `IdentityUser` 实体定义额外属性: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdate(options => + { + options.AddOrUpdateProperty("SocialSecurityNumber"); + options.AddOrUpdateProperty("IsSuperUser"); + } + ); +```` + +### AddOrUpdateProperty + +虽然可以如上所示使用 `AddOrUpdateProperty`, 但如果要定义单个额外的属性,也可以使用快捷的扩展方法: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty("SocialSecurityNumber"); +```` + +有时将单个额外属性定义为多种类型是可行的. 你可以使用以下代码,而不是一个一个地定义: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + new[] + { + typeof(IdentityUserDto), + typeof(IdentityUserCreateDto), + typeof(IdentityUserUpdateDto) + }, + "SocialSecurityNumber" + ); +```` + +#### 属性配置 + +`AddOrUpdateProperty` 还可以为属性定义执行其他配置的操作. + +Example: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "SocialSecurityNumber", + options => + { + options.CheckPairDefinitionOnMapping = false; + }); +```` + +> 参阅 "对象到对象映射" 部分了解 `CheckPairDefinitionOnMapping` 选项. + +`options` 有一个名为 `Configuration` 的字典,该字典存储对象扩展定义甚至可以扩展. EF Core使用它来将其他属性映射到数据库中的表字段. 请参阅[扩展实体文档](Customizing-Application-Modules-Extending-Entities.md). + +## 对象到对象映射 + +假设你已向可扩展的实体对象添加了额外的属性并使用了自动[对象到对象的映射](Object-To-Object-Mapping.md)将该实体映射到可扩展的DTO类. 在这种情况下你需要格外小心,因为额外属性可能包含**敏感数据**,这些数据对于客户端不可用. + +本节提供了一些**好的做法**,可以控制对象映射的额外属性. + +### MapExtraPropertiesTo + +`MapExtraPropertiesTo` 是ABP框架提供的扩展方法,用于以受控方式将额外的属性从一个对象复制到另一个对象. 示例: + +````csharp +identityUser.MapExtraPropertiesTo(identityUserDto); +```` + +`MapExtraPropertiesTo` 需要在**两侧**(本例中是`IdentityUser` 和 `IdentityUserDto`)**定义属性**. 以将值复制到目标对象. 否则即使源对象(在此示例中为 `identityUser` )中确实存在该值,它也不会复制. 有一些重载此限制的方法. + +#### MappingPropertyDefinitionChecks + +`MapExtraPropertiesTo` 获取一个附加参数来控制单个映射操作的定义检查: + +````csharp +identityUser.MapExtraPropertiesTo( + identityUserDto, + MappingPropertyDefinitionChecks.None +); +```` + +> 要小心,因为 `MappingPropertyDefinitionChecks.None` 会复制所有的额外属性而不进行任何检查. `MappingPropertyDefinitionChecks` 枚举还有其他成员. + +如果要完全禁用属性的定义检查,可以在定义额外的属性(或更新现有定义)时进行,如下所示: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "SocialSecurityNumber", + options => + { + options.CheckPairDefinitionOnMapping = false; + }); +```` + +#### 忽略属性 + +你可能要在映射操作忽略某些属性: + +````csharp +identityUser.MapExtraPropertiesTo( + identityUserDto, + ignoredProperties: new[] {"MySensitiveProp"} +); +```` + +忽略的属性不会复制到目标对象. + +#### AutoMapper集成 + +如果你使用的是[AutoMapper](https://automapper.org/)库,ABP框架还提供了一种扩展方法来利用上面定义的 `MapExtraPropertiesTo` 方法. + +你可以在映射配置文件中使用 `MapExtraProperties()` 方法. + +````csharp +public class MyProfile : Profile +{ + public MyProfile() + { + CreateMap() + .MapExtraProperties(); + } +} +```` + +它与 `MapExtraPropertiesTo()` 方法具有相同的参数. + +## Entity Framework Core 数据库映射 + +如果你使用的是EF Core,可以将额外的属性映射到数据库中的表字段. 例: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "SocialSecurityNumber", + options => + { + options.MapEfCore(b => b.HasMaxLength(32)); + } + ); +```` + +参阅 [Entity Framework Core 集成文档](Entity-Framework-Core.md) 了解更多内容. \ No newline at end of file diff --git a/docs/zh-Hans/Object-To-Object-Mapping.md b/docs/zh-Hans/Object-To-Object-Mapping.md index 67d580ae54..1b60112cb2 100644 --- a/docs/zh-Hans/Object-To-Object-Mapping.md +++ b/docs/zh-Hans/Object-To-Object-Mapping.md @@ -145,13 +145,30 @@ options.AddProfile(validate: true); > 如果你有多个配置文件,并且只需要为其中几个启用验证,那么首先使用`AddMaps`而不进行验证,然后为你想要验证的每个配置文件使用`AddProfile`. +### 映射对象扩展 + +[对象扩展系统](Object-Extensions.md) 允许为已存在的类定义额外属性. ABP 框架提供了一个映射定义扩展可以正确的映射两个对象的额外属性. + +````csharp +public class MyProfile : Profile +{ + public MyProfile() + { + CreateMap() + .MapExtraProperties(); + } +} +```` + +如果两个类都是可扩展对象(实现了 `IHasExtraProperties` 接口),建议使用 `MapExtraProperties` 方法. 更多信息请参阅[对象扩展文档](Object-Extensions.md). + ## 高级主题 ### IObjectMapper 接口 假设你已经创建了一个**可重用的模块**,其中定义了AutoMapper配置文件,并在需要映射对象时使用 `IObjectMapper`. 根据[模块化](Module-Development-Basics.md)的性质,你的模块可以用于不同的应用程序. -`IObjectMapper` 是一个抽象,可以由最终应用程序替换使用另一个映射库. 这里的问题是你的可重用模块设计为使用AutoMapper,因为它为其定义映射配置文件. 这种情况下即使最终应用程序使用另一个默认对象映射库,你也要保证模块始终使用AutoMapper. +`IObjectMapper` 是一个抽象,可以由最终应用程序替换使用另一个映射库. 这里的问题是你的可重用模块设计为使用AutoMapper,因为它为其定义映射配置文件. 这种情况下即使最终应用程序使用另一个默认对象映射库,你也要保证模块始终使用AutoMapper. `IObjectMapper`将对象映射器上下文化,你可以为不同的 模块/上下文 使用不同的库. @@ -184,7 +201,7 @@ public class UserAppService : ApplicationService `UserAppService` 注入 `IObjectMapper`, 它是模块的特定对象映射器,用法与 `IObjectMapper` 完全相同. -上面的示例代码未使用 `ApplicationService` 中定义的 `ObjectMapper` 属性,而是注入了 `IObjectMapper`. 但是 `ApplicationService` 定义了可以在类构造函数中设置的 `ObjectMapperContext` 属性, 因此仍然可以使用基类属性. 示例可以进行以下重写: +上面的示例代码未使用 `ApplicationService` 中定义的 `ObjectMapper` 属性,而是注入了 `IObjectMapper`. 但是 `ApplicationService` 定义了可以在类构造函数中设置的 `ObjectMapperContext` 属性, 因此仍然可以使用基类属性. 示例可以进行以下重写: ````csharp public class UserAppService : ApplicationService diff --git a/docs/zh-Hans/Options.md b/docs/zh-Hans/Options.md index 18ccd90bed..c88ff0adda 100644 --- a/docs/zh-Hans/Options.md +++ b/docs/zh-Hans/Options.md @@ -9,7 +9,7 @@ ABP框架遵循选项模式,并定义了用于配置框架和模块的选项类( ## 配置选项 通常配置选项在 `Startup` 类的 `ConfigureServices` 方法中. 但由于ABP框架提供了模块化基础设施,因此你可以在[模块](Module-Development-Basics.md)的`ConfigureServices` 方法配置选项. -例: +例: ````csharp public override void ConfigureServices(ServiceConfigurationContext context) @@ -34,7 +34,7 @@ public class MyOptions } ```` -然后开发人员可以像上面 `AbpAuditingOptions` 示例一样配置你的选项: +然后开发人员可以像上面 `AbpAuditingOptions` 示例一样配置你的选项: ````csharp public override void ConfigureServices(ServiceConfigurationContext context) @@ -80,9 +80,9 @@ public class MyService : ITransientDependency 如果你正在开发一个模块,可能需要让开发者能够设置一些选项,并在依赖注入注册阶段使用这些选项. 你可能需要根据选项值配置其他服务或更改依赖注入的注册代码. -对于此类情况,ABP为 `IServiceCollection` 引入了 `PreConfigure` 和 `ExecutePreConfiguredActions` 扩展方法. 该模式的工作原理如下所述。 +对于此类情况,ABP为 `IServiceCollection` 引入了 `PreConfigure` 和 `ExecutePreConfiguredActions` 扩展方法. 该模式的工作原理如下所述. -1. 你的模块中定义计划选项类. 例: +1. 你的模块中定义计划选项类. 例: ````csharp public class MyPreOptions @@ -92,7 +92,7 @@ public class MyPreOptions ```` 然后任何依赖于模块的模块类都可以在其 `PreConfigureServices` 方法中使用 `PreConfigure` 方法. -例: +例: ````csharp public override void PreConfigureServices(ServiceConfigurationContext context) diff --git a/docs/zh-Hans/Samples/Microservice-Demo.md b/docs/zh-Hans/Samples/Microservice-Demo.md index 9b559797f7..40cb42cda6 100644 --- a/docs/zh-Hans/Samples/Microservice-Demo.md +++ b/docs/zh-Hans/Samples/Microservice-Demo.md @@ -51,13 +51,13 @@ ABP框架的主要目标之一就是提供[便捷的基础设施来创建微服 ### 创建数据库 -MongoDB 数据库是动态创建的,但是你需要创建 SQL server 数据库的结构。其实你可以很轻松的创建数据库,因为这个解决方案配置了使用 Entity Core Code First 来做迁移。 +MongoDB 数据库是动态创建的,但是你需要创建 SQL server 数据库的结构.其实你可以很轻松的创建数据库,因为这个解决方案配置了使用 Entity Core Code First 来做迁移. -这个解决方案中有两个 SQL server 数据库。 +这个解决方案中有两个 SQL server 数据库. #### MsDemo_Identity 数据库 -* 右键 `AuthServer.Host` 项目,然后点击 `设置为启动项目`. +* 右键 `AuthServer.Host` 项目,然后点击 `设置为启动项目`. * 打开 **程序包管理器控制台** (工具 -> NuGet 包管理器 -> 程序包管理器控制台) * 选择 `AuthServer.Host` 成为 **默认项目**. * 执行 `Update-Database` 命令. @@ -66,7 +66,7 @@ MongoDB 数据库是动态创建的,但是你需要创建 SQL server 数据库 #### MsDemo_ProductManagement -* 右键 `ProductService.Host` 项目,然后点击 `设置为启动项目`. +* 右键 `ProductService.Host` 项目,然后点击 `设置为启动项目`. * 打开 **程序包管理器控制台** (工具 -> NuGet 包管理器 -> 程序包管理器控制台) * 选择 `ProductService.Host` 成为 **默认项目**. * 执行 `Update-Database` 命令. @@ -334,7 +334,7 @@ context.Services.AddAuthentication(options => - 它需要额外的身份范围 *role*, *email* and *phone*. - 它需要API资源范围 *PublicWebSiteGateway*,*BloggingService*和*ProductService*,因为它将这些服务用作API. -IdentityServer客户端设置存储在`appsettings.json`文件中: +IdentityServer客户端设置存储在`appsettings.json`文件中: ```json "AuthServer": { @@ -348,7 +348,7 @@ IdentityServer客户端设置存储在`appsettings.json`文件中: PublicWebSite.Host项目有一个列出产品的页面 (`Pages/Products.cshtml`). 它还使用博客模块中的UI. 为此`PublicWebSiteHostModule`加入了`BloggingWebModule`(*[Volo.Blogging.Web](https://www.nuget.org/packages/Volo.Blogging.Web)* 包)的依赖项. -产品页面的屏幕截图: +产品页面的屏幕截图: ![microservice-sample-public-product-list](../images/microservice-sample-public-product-list.png) @@ -390,7 +390,7 @@ PublicWebSite.Host项目有一个列出产品的页面 (`Pages/Products.cshtml`) #### 远程服务配置 -`appsettings.json`文件中的`RemoteService`配置很简单: +`appsettings.json`文件中的`RemoteService`配置很简单: ````json "RemoteServices": { @@ -418,7 +418,7 @@ PublicWebSite.Host项目有一个列出产品的页面 (`Pages/Products.cshtml`) } ```` -此示例使用`client_credentials` 授予类型,该类型需要`ClientId`和`ClientSecret`进行身份验证过程. 还有[其他授予类型](http://docs.identityserver.io/en/latest/topics/grant_types.html). 例如, 你可以使用以下配置切换到`password`(Resource Owner Password)授予类型: +此示例使用`client_credentials` 授予类型,该类型需要`ClientId`和`ClientSecret`进行身份验证过程. 还有[其他授予类型](http://docs.identityserver.io/en/latest/topics/grant_types.html). 例如, 你可以使用以下配置切换到`password`(Resource Owner Password)授予类型: ````json "IdentityClients": { @@ -571,7 +571,7 @@ app.UseOcelot().Wait(); #### 权限管理 -后端管理应用程序提供权限管理UI(之前见过),并使用此网关获取/设置权限. 权限管理API托管在网关内,而不是单独的服务. 这是一个设计决策,但如果您愿意,它可以作为另一个微服务托管. +后端管理应用程序提供权限管理UI(之前见过),并使用此网关获取/设置权限. 权限管理API托管在网关内,而不是单独的服务. 这是一个设计决策,但如果你愿意,它可以作为另一个微服务托管. #### Dependencies @@ -1229,7 +1229,7 @@ public class ProductCodeAlreadyExistsException : BusinessException } ```` -`PM:000001`是发送给客户端的异常类型的代码,因此他们可以理解错误类型. 在这种情况下没有实现,但也可以本地化业务异常. 请参阅[异常处理文档](../Exception-Handling.md). +`PM:000001`是发送给客户端的异常类型的代码,因此他们可以理解错误类型. 在这种情况下没有实现,但也可以本地化业务异常. 请参阅[异常处理文档](../Exception-Handling.md). #### 应用层 @@ -1278,7 +1278,7 @@ public async Task UpdateAsync(Guid id, UpdateProductDto input) 分布式事件(事件总线)是一种消息传递方式,其中服务引发/触发事件,而其他服务注册/侦听这些事件,以便在发生重要事件时得到通知. ABP通过提供约定,服务和集成使分布式事件更易于使用. -您已经看到`Product`类使用以下代码行发布事件: +你已经看到`Product`类使用以下代码行发布事件: ````csharp AddDistributedEvent(new ProductStockCountChangedEto(Id, StockCount, stockCount)); @@ -1406,7 +1406,7 @@ Kibana URL默认为`http://localhost:5601/`. ABP提供自动审计日志记录,详细保存每个请求(当前用户,浏览器/客户端,执行了哪些操作,哪些实体更改,甚至实体的哪些属性已更新). 有关详细信息,请参阅[审计日志文档](../Audit-Logging.md). -所有服务和应用程序都配置为编写审核日志. 审核日志将保存到MsDemo_Identity SQL数据库中. 因此,您可以从单个点查询所有应用程序的所有审核日志. +所有服务和应用程序都配置为编写审核日志. 审核日志将保存到MsDemo_Identity SQL数据库中. 因此,你可以从单个点查询所有应用程序的所有审核日志. 审核日志记录具有`CorrelationId`属性,可用于跟踪请求. 当服务在单个Web请求中调用另一个服务时,它们都会使用相同的`CorrelationId`保存审核日志. 请参阅数据库中的`AbpAuditLogs`表. diff --git a/docs/zh-Hans/Settings.md b/docs/zh-Hans/Settings.md index 25559082f2..a4d84c91b7 100644 --- a/docs/zh-Hans/Settings.md +++ b/docs/zh-Hans/Settings.md @@ -226,6 +226,6 @@ Configure(options => ## 设置管理模块 -设置系统核心是相当独立的,不做任何关于如何管理(更改)设置值的假设. 默认的`ISettingStore`实现也是`NullSettingStore`,它为所有设置值返回null. +设置系统核心是相当独立的,不做任何关于如何管理(更改)设置值的假设. 默认的`ISettingStore`实现也是`NullSettingStore`,它为所有设置值返回null. 设置管理模块通过管理数据库中的设置值来完成逻辑(实现`ISettingStore`).有关更多信息参阅[设置管理模块](Modules/Setting-Management.md)学习更多. diff --git a/docs/zh-Hans/Startup-Templates/Application.md b/docs/zh-Hans/Startup-Templates/Application.md index 78d43759db..4e2c3f6700 100644 --- a/docs/zh-Hans/Startup-Templates/Application.md +++ b/docs/zh-Hans/Startup-Templates/Application.md @@ -139,7 +139,7 @@ ABP是一个模块化的框架,理想的设计是让每个模块都有自己的 初始化种子数据很重要,ABP具有模块化的种子数据基础设施. 种子数据的更多信息,请参阅[文档](../Data-Seeding.md). -虽然创建数据库和应用迁移似乎只对关系数据库有用,但即使您选择NoSQL数据库提供程序(如MongoDB),也会生成此项目. 这时,它会为应用程序提供必要的初始数据. +虽然创建数据库和应用迁移似乎只对关系数据库有用,但即使你选择NoSQL数据库提供程序(如MongoDB),也会生成此项目. 这时,它会为应用程序提供必要的初始数据. * 它依赖 `.EntityFrameworkCore.DbMigrations` 项目 (针对EF Core),因为它需要访问迁移文件. * 它依赖 `.Application.Contracts` 项目,因为它需要访问权限定义在初始化种子数据时为管理员用户赋予所有权限. @@ -270,6 +270,159 @@ ABP使用开源的[IdentityServer4](https://identityserver.io/)框架做应用 `angular/src/environments` 文件夹下的文件含有应用程序的基础配置. +#### AppModule(应用程序模块) + +`AppModule` 是应用程序的根模块. 一些ABP模块和一些基本模块导入到 `AppModule` 中. + +ABP 配置模块也已经导入到 `AppModule` 中, 以满足可延迟加载 ABP 模块的初始需求. + +#### AppRoutingModule(应用程序路由模块) + +在 `AppRoutingModule` 中有可延迟加载的 ABP 模块作为路由. + +> 不应更改ABP模块的路径. + +你应该在 `data` 对象中添加 `routes` 属性, 以便在菜单中添加一个链接来重定向到自定义页面. + +```js +{ + path: 'dashboard', + loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule), + canActivate: [AuthGuard, PermissionGuard], + data: { + routes: { + name: 'ProjectName::Menu:Dashboard', + order: 2, + iconClass: 'fa fa-dashboard', + requiredPolicy: 'ProjectName.Dashboard.Host' + } as ABP.Route + } +} +``` +在上面的例子中; +* 如果用户没有登录, AuthGuard 会阻塞访问并重定向到登录页面. +* PermissionGuard 使用 `rotues` 对象的 `requiredPolicy` 属性检查用户的权限. 如果用户未被授权访问该页, 则显示403页. +* `routes` 的 `name` 属性是菜单链接标签. 可以定义本地化 key. +* `routes` 对象的 `iconClass` 属性是菜单链接图标类. +* `routes` 对象的 `requiredPolicy` 属性是访问页面所需的策略 key. + +在上述 `routes` 定义之后, 如果用户被授权, 仪表盘链接将出现在菜单上. + +#### Shared Module(共享模块) + +所有模块可能需要的模块已导入到 `SharedModule`. 你应该将 `SharedModule` 导入所有模块. + +参见 [Sharing Modules(共享模块)](https://angular.io/guide/sharing-ngmodules) 文档. + +#### Environments(环境) + +`src/environments` 文件夹下的文件包含应用程序的基本配置. + +#### Home Module + +Home模块是一个可延迟加载的模块, 它加载应用程序的根地址. + +#### Styles(样式) + +在 `angular.json` 中向 `styles` 数组添加所需的样式文件. `AppComponent` 在主包加载后通过 `LazyLoadService` 加载一些样式文件, 以缩短第一次绘制的时间. + +#### Testing(测试) + +你应该在与要测试的文件相同的文件夹中创建测试. + +参见[测试文档](https://angular.io/guide/testing/). + +#### Depended Packages(依赖包) + +* [NG Bootstrap](https://ng-bootstrap.github.io/) 被用作UI组件库. +* [NGXS](https://www.ngxs.io/) 被用作状态管理库. +* [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc) 用于支持OAuth 2和OpenId Connect (OIDC). +* [Chart.js](https://www.chartjs.org/) 用于创建小部件. +* [ngx-validate](https://github.com/ng-turkey/ngx-validate) 用于对交互表单进行动态验证. + +### React Native + +解决方案将[React Native](https://reactnative.dev/)应用程序作为默认值包含在 `react-native` 文件夹中. + +服务器端类似于上面描述的解决方案. `*.HttpApi.Host` 的项目提供 API, 所以 React 本机应用程序使用它. + +React 本机应用程序是用 [Expo](https://expo.io/)生成的. Expo 是一套基于 React Native 构建的工具, 帮助你快速启动一个应用程序, 尽管它有很多功能. + +React Native 应用文件夹结构, 如下图所示: + +![react-native-folder-structure](../images/react-native-folder-structure.png) + +* `App.js` 是应用程序的引导组件. +* `Environment.js` f文件有应用程序的基本配置. 在这个文件中定义了 `prod` and `dev` 配置. +* [Contexts](https://reactjs.org/docs/context.html) 是在 `src/contexts` 文件夹中创建的. +* [Higher order components](https://reactjs.org/docs/higher-order-components.html) 是在 `src/hocs` 文件夹中创建的. +* [Custom hooks](https://reactjs.org/docs/hooks-custom.html#extracting-a-custom-hook) 是在 `src/hooks` 中创建的. +* [Axios interceptors](https://github.com/axios/axios#interceptors) 是在 `src/interceptors` 文件夹中创建. +* 工具函数从 `src/utils` 文件夹导出. + +#### Components(组件) + +可以在所有屏幕上使用的组件是在 `src/components` 文件夹中创建的. 所有组件都是作为一个能够使用 [hooks](https://reactjs.org/docs/hooks-intro.html) 的函数创建的. + +#### Screens(屏幕) + +![react-native-navigation-structure](../images/react-native-navigation-structure.png) + +Screens 是通过在 `src/screens` 文件夹中创建将名称分开的文件夹来创建的. 某些 screens 的某些部分可以拆分为组件. + +每个 screen 都在 `src/navigators` 文件夹中的导航器中使用. + +#### Navigation(导航) + +[React Navigation](https://reactnavigation.org/) 被用作导航库. 导航器是在 `src/navigators` 中创建的. 一个 [drawer](https://reactnavigation.org/docs/drawer-based-navigation/) 导航器和几个 [stack](https://reactnavigation.org/docs/hello-react-navigation/#installing-the-stack-navigator-library) 导航器在此文件夹中创建. 查看 [上图](#screens) 中的导航结构. + +#### State Management(状态管理) + +[Redux](https://redux.js.org/) 被用作状态管理库. [Redux Toolkit](https://redux-toolkit.js.org/) 库被用作高效Redux开发的工具集. + +在 `src/store` 文件夹中创建 Actions, reducers, sagas, selectors. 存储文件夹如下: + +![react-native-store-folder](../images/react-native-store-folder.png) + +* [**Store**](https://redux.js.org/basics/store) 在 `src/store/index.js` 文件中定义. +* [**Actions**](https://redux.js.org/basics/actions/) 是将数据从应用程序发送到存储的有效信息负载. +* [**Reducers**](https://redux.js.org/basics/reducers) 指定应用程序的状态如何更改以响应发送到存储的操作. +* [**Redux-Saga**](https://redux-saga.js.org/) 是一个库, 旨在使应用程序的副作用(即异步的事情, 如数据获取和不纯的事情, 如访问浏览器缓存)更容易管理. Sagas 是在 `src/store/sagas` 文件夹中创建的. +* [**Reselect**](https://github.com/reduxjs/reselect) 库用于创建缓存的选择器. 选择器是在 `src/store/selectors` 文件夹中创建的. + +#### APIs + +[Axios](https://github.com/axios/axios) 用作HTTP客户端库. Axios 实例从 `src/api/API.js` 导出 . 使用相同的配置进行HTTP调用. `src/api` 文件夹中还有为 API 调用创建的 API 文件. + +#### Theming(主题) + +[Native Base](https://nativebase.io/) 被用作UI组件库. 本地基本组件可以很容易地进行自定义.参见[Native Base customize](https://docs.nativebase.io/Customize.html#Customize) 文档.我们沿着同样的路走. + +* Native Base 主题变量在 `src/theme/variables` 文件夹中. +* Native Base 组件样式在 `src/theme/components` 文件夹中.这些文件是用 Native Base's `ejectTheme` 脚本生成的. +* 组件样式用 `src/theme/overrides` 文件夹下的文件覆盖. + +#### Testing(单元测试) + +将创建单元测试. + +参见[测试概述](https://reactjs.org/docs/testing.html)文档. + +#### Depended Libraries(依赖库) + +* [Native Base](https://nativebase.io/) 用作UI组件库. +* [React Navigation](https://reactnavigation.org/) 用作导航库. +* [Axios](https://github.com/axios/axios) 用作HTTP客户端库. +* [Redux](https://redux.js.org/) 用作状态管理库. +* [Redux Toolkit](https://redux-toolkit.js.org/) 库被用作高效Redux开发的工具集. +* [Redux-Saga](https://redux-saga.js.org/) 用于管理异步进程. +* [Redux Persist](https://github.com/rt2zz/redux-persist) 被用作状态持久化. +* [Reselect](https://github.com/reduxjs/reselect) 用于创建缓存的选择器. +* [i18n-js](https://github.com/fnando/i18n-js) 作为国际化库使用. +* [expo-font](https://docs.expo.io/versions/latest/sdk/font/) 库可以轻松加载字体. +* [Formik](https://github.com/jaredpalmer/formik) 用于构建表单. +* [Yup](https://github.com/jquense/yup) 用于表单验证. + ## 下一步是什么? * 参阅[ASP.NET Core MVC 模板入门](../Getting-Started-AspNetCore-MVC-Template.md)创建此模板的新解决方案并运行它. diff --git a/docs/zh-Hans/Startup-Templates/Index.md b/docs/zh-Hans/Startup-Templates/Index.md index 57389e6df0..160a2b3e20 100644 --- a/docs/zh-Hans/Startup-Templates/Index.md +++ b/docs/zh-Hans/Startup-Templates/Index.md @@ -2,7 +2,7 @@ 虽然你可以从一个空项目开始并手动添加所需的包,但启动模板可以非常轻松,舒适地使用ABP框架启动新的解决方案. -单击下面列表中的名称以查看相关启动模板的文档: +单击下面列表中的名称以查看相关启动模板的文档: * [**app**](Application.md): 应用程序模板. * [**module**](Module.md): 模块/服务模板. \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md index e1cbb73cc1..7c2db90997 100644 --- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md +++ b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md @@ -33,7 +33,7 @@ - `Acme.BookStore.Domain`包含你的[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities), [领域服务](https://docs.abp.io/zh-Hans/abp/latest/Domain-Services)和其他核心域对象. - `Acme.BookStore.Domain.Shared`包含可与客户共享的常量,枚举或其他域相关对象. -在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities). 该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个名为`Book`的类,如下所示: +在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities). 该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个名为`Book`的类,如下所示: ````C# using System; @@ -132,7 +132,7 @@ PM> Update-Database #### 添加示例数据 -`Update-Database`命令在数据库中创建了`AppBooks`表. 打开数据库并输入几个示例行,以便在页面上显示它们: +`Update-Database`命令在数据库中创建了`AppBooks`表. 打开数据库并输入几个示例行,以便在页面上显示它们: ![bookstore-books-table](images/bookstore-books-table.png) @@ -290,7 +290,7 @@ namespace Acme.BookStore #### Swagger UI -启动模板配置为使用[Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)运行[swagger UI](https://swagger.io/tools/swagger-ui/). 运行应用程序并在浏览器中输入`https://localhost:XXXX/swagger/`(用您自己的端口替换XXXX)作为URL. +启动模板配置为使用[Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)运行[swagger UI](https://swagger.io/tools/swagger-ui/). 运行应用程序并在浏览器中输入`https://localhost:XXXX/swagger/`(用你自己的端口替换XXXX)作为URL. 你会看到一些内置的接口和`Book`的接口,它们都是REST风格的: @@ -380,7 +380,7 @@ context.Menu.AddItem( ![bookstore-localization-files](images/bookstore-localization-files-v2.png) -打开`en.json`文件,将`Menu:BookStore`和`Menu:Books`键的本地化文本添加到文件末尾: +打开`en.json`文件,将`Menu:BookStore`和`Menu:Books`键的本地化文本添加到文件末尾: ````json { diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md index a5e51134fa..e072e5a1a7 100644 --- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md +++ b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md @@ -14,7 +14,7 @@ ### 解决方案中的测试项目 -解决方案中有多个测试项目: +解决方案中有多个测试项目: ![bookstore-test-projects-v2](images/bookstore-test-projects-v2.png) @@ -68,7 +68,7 @@ namespace Acme.BookStore ```` * 注入`IRepository`并在`SeedAsync`中使用它来创建两个书实体作为测试数据. -* 使用`IGuidGenerator`服务创建GUID. 虽然`Guid.NewGuid()`非常适合测试,但`IGuidGenerator`在使用真实数据库时还有其他特别重要的功能(参见[Guid生成文档](../../../Guid-Generation.md)了解更多信息). +* 使用`IGuidGenerator`服务创建GUID. 虽然`Guid.NewGuid()`非常适合测试,但`IGuidGenerator`在使用真实数据库时还有其他特别重要的功能(参见[Guid生成文档](../../../Guid-Generation.md)了解更多信息). ### 测试 BookAppService diff --git a/docs/zh-Hans/UI/Angular/AddingSettingTab.md b/docs/zh-Hans/UI/Angular/AddingSettingTab.md deleted file mode 100644 index c46a559957..0000000000 --- a/docs/zh-Hans/UI/Angular/AddingSettingTab.md +++ /dev/null @@ -1,3 +0,0 @@ -## Creating a Settings Tab - -TODO... diff --git a/docs/zh-Hans/UI/Angular/Component-Replacement.md b/docs/zh-Hans/UI/Angular/Component-Replacement.md index d2369901dc..dc7c1a33c4 100644 --- a/docs/zh-Hans/UI/Angular/Component-Replacement.md +++ b/docs/zh-Hans/UI/Angular/Component-Replacement.md @@ -1,3 +1,84 @@ -# Component Replacement +## 替换组件 -TODO... \ No newline at end of file +你可以将一些ABP的组件替换为你自己的自定义组件. + +你可以**替换**但**不能自定义**默认ABP组件的原因是禁用或更改该组件的一部分可能会导致问题. 所以我们把这些组件称为可替换组件. + +### 如何替换组件 + +创建一个你想要使用的新组件,添加到 `AppModule` 中的 `declarations` 和`entryComponents` 中. + +然后打开 `app.component.ts` 使用 `AddReplaceableComponent` 将你的组件替换ABP组件. 如下所示: + +```js +import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent action +import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum +import { Store } from '@ngxs/store'; // imported Store +//... +export class AppComponent { + constructor(..., private store: Store) {} // injected Store + + ngOnInit() { + this.store.dispatch( + new AddReplaceableComponent({ + component: YourNewRoleComponent, + key: eIdentityComponents.Roles, + }), + ); + //... + } +} +``` + +![Example Usage](./images/component-replacement.gif) + +### 如何替换布局 + +每个ABP主题模块有3个布局,分别是`ApplicationLayoutComponent`, `AccountLayoutComponent`, `EmptyLayoutComponent`. 这些布局可以用相同的方式替换. + +> 一个布局组件模板应该包含 `` 元素. + +下面的例子解释了如何更换 `ApplicationLayoutComponent`: + +运行以下命令在 `angular` 文件夹中生成布局: + +```bash +yarn ng generate component shared/my-application-layout --export --entryComponent + +# You don't need the --entryComponent option in Angular 9 +``` + +在你的布局模板(`my-layout.component.html`)中添加以下代码: + +```html + +``` + +打开 `app.component.ts` 添加以下内容: + +```js +import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent +import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents enum for component keys +import { MyApplicationLayoutComponent } from './shared/my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent +import { Store } from '@ngxs/store'; // imported Store +//... +export class AppComponent { + constructor(..., private store: Store) {} // injected Store + + ngOnInit() { + // added below content + this.store.dispatch( + new AddReplaceableComponent({ + component: MyApplicationLayoutComponent, + key: eThemeBasicComponents.ApplicationLayout, + }), + ); + + //... + } +} +``` + +## 下一步是什么? + +- [自定义设置页面](./Custom-Setting-Page.md) diff --git a/docs/zh-Hans/UI/Angular/Config-State.md b/docs/zh-Hans/UI/Angular/Config-State.md new file mode 100644 index 0000000000..4967dd78b3 --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Config-State.md @@ -0,0 +1,294 @@ +## 配置状态 + +`ConfigStateService` 是一个单例服务,即在应用程序的根级别提供,用于与 `Store` 中的应用程序配置状态进行交互. + +## 使用前 + +为了使用 `ConfigStateService`,你必须将其注入到你的类中. + +```js +import { ConfigStateService } from '@abp/ng.core'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + constructor(private config: ConfigStateService) {} +} +``` + +你不必在模块或组件/指令级别提供 `ConfigStateService`,因为它已经在**根中**提供. + +## 选择器方法 + +`ConfigStateService` 有许多选择器方法允许你从 `Store` 获取特定或所有的配置. + +### 如何从Store获取所有的配置 + +你可以使用 `ConfigStateService` 的 `getAll` 方法从Store获取所有的配置对象. 用法如下: + +```js +// this.config is instance of ConfigStateService + +const config = this.config.getAll(); +``` + +### 如何从Store获取特定的配置 + +你可以使用 `ConfigStateService` 的 `getOne` 方法从Store获取特定的配置属性. 你需要将属性名做为参数传递给方法: + +```js +// this.config is instance of ConfigStateService + +const currentUser = this.config.getOne("currentUser"); +``` + +有时你想要获取具体信息,而不是当前用户. 例如你只想获取到 `tenantId`: + +```js +const tenantId = this.config.getDeep("currentUser.tenantId"); +``` + +或通过提供键数组作为参数: + +```js +const tenantId = this.config.getDeep(["currentUser", "tenantId"]); +``` + +`getDeep` 可以执行 `getOne` 的所有操作. 但 `getOne` 的执行效率要高一些. + +#### 配置状态属性 + +请参阅 `Config.State` 类型,你可以通过 `getOne` 和 `getDeep` 获取所有属性. 你可以在[config.ts 文件](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L7)中找到. + +### 如何从Store获取应用程序信息 + +`getApplicationInfo` 方法从存储为配置状态存储的环境变量中获取应用程序信息. 你可以这样使用它: + +```js +// this.config is instance of ConfigStateService + +const appInfo = this.config.getApplicationInfo(); +``` + +该方法不会返回 `undefined` 或 `null`,而是会返回一个空对象(`{}`). 换句话说,当你使用上面代码中的 `appInfo` 属性时,永远不会出现错误. + +#### 应用程序信息属性 + +请参阅 `Config.State` 类型,你可以通过 `getApplicationInfo` 获取所有属性. 你可以在[config.ts 文件](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L21)中找到. + +### 如何从Store获取 + +`getApplicationInfo` 方法从存储为配置状态存储的环境变量中获取特定的API URL. 你可以这样使用它: + +```js +// this.config is instance of ConfigStateService + +const apiUrl = this.config.getApiUrl(); +// environment.apis.default.url + +const searchUrl = this.config.getApiUrl("search"); +// environment.apis.search.url +``` + +该方法返回给定键的特定的API `url`. 如果没有Key,则使用 `default`. + +### 如何从Store获取所有的设置 + +你可以使用 `ConfigStateService` 的 `getSettings` 获取配置状态所有的设置对象. 你可以这样使用它: + +```js +// this.config is instance of ConfigStateService + +const settings = this.config.getSettings(); +``` + +实际上该方法可以通过**传递关键字**来搜索设置. + +```js +const localizationSettings = this.config.getSettings("Localization"); +/* +{ + 'Abp.Localization.DefaultLanguage': 'en' +} +*/ +``` + +请注意, **设置搜索区分大小写**. + +### 如何从Store获取特定的设置 + +你可以使用 `ConfigStateService` 的 `getSetting` 获取配置状态特定的设置. 你可以这样使用它: + +```js +// this.config is instance of ConfigStateService + +const defaultLang = this.config.getSetting("Abp.Localization.DefaultLanguage"); +// 'en' +``` + +### 如何从Store获取特定的权限 + +你可以使用 `ConfigStateService` 的 `getGrantedPolicy` 获取配置状态特定的权限. 你应该将策略key做为参数传递给方法: + +```js +// this.config is instance of ConfigStateService + +const hasIdentityPermission = this.config.getGrantedPolicy("Abp.Identity"); +// true +``` + +你还可以使用 **组合策略key** 来微调你的选择: + +```js +// this.config is instance of ConfigStateService + +const hasIdentityAndAccountPermission = this.config.getGrantedPolicy( + "Abp.Identity && Abp.Account" +); +// false + +const hasIdentityOrAccountPermission = this.config.getGrantedPolicy( + "Abp.Identity || Abp.Account" +); +// true +``` + +创建权限选择器时,请考虑以下**规则**: + +- 最多可组合两个键. +- `&&` 操作符查找两个键. +- `||` 操作符查找任意一个键. +- 空字符串 `''` 做为键将返回 `true` +- 使用没有第二个键的操作符将返回 `false` + +### 如何从Store中获取翻译 + +`ConfigStateService` 的 `getLocalization` 方法用于翻译. 这里有一些示例: + +```js +// this.config is instance of ConfigStateService + +const identity = this.config.getLocalization("AbpIdentity::Identity"); +// 'identity' + +const notFound = this.config.getLocalization("AbpIdentity::IDENTITY"); +// 'AbpIdentity::IDENTITY' + +const defaultValue = this.config.getLocalization({ + key: "AbpIdentity::IDENTITY", + defaultValue: "IDENTITY" +}); +// 'IDENTITY' +``` + +请参阅[本地化文档](./Localization.md)了解详情. + +## 分发方法 + +`ConfigStateService` 有几种分发方法,让你方便地将预定义操作分发到 `Store`. + +### 如何从服务器获取应用程序配置 + +`dispatchGetAppConfiguration` 触发对端点的请求,该端点使用应用程序状态进行响应,然后将此响应作为配置状态放置到 `Store`中. + +```js +// this.config is instance of ConfigStateService + +this.config.dispatchGetAppConfiguration(); +// returns a state stream which emits after dispatch action is complete +``` + +请注意,**你不必在应用程序启动时调用此方法**,因为在启动时已经从服务器收到了应用程序配置. + +### 如何修补路由配置 + +`dispatchPatchRouteByName` 根据名称查找路由, 并将其在 `Store` 中的配置替换为作为第二个参数传递的新配置. + +```js +// this.config is instance of ConfigStateService + +const newRouteConfig: Partial = { + name: "Home", + path: "home", + children: [ + { + name: "Dashboard", + path: "dashboard" + } + ] +}; + +this.config.dispatchPatchRouteByName("::Menu:Home", newRouteConfig); +// returns a state stream which emits after dispatch action is complete +``` + +### 如何添加新路由配置 + +`dispatchAddRoute` 向 `Store` 的配置状态添加一个新路由. 应该将路由配置做为方法参数传递. + +```js +// this.config is instance of ConfigStateService + +const newRoute: ABP.Route = { + name: "My New Page", + iconClass: "fa fa-dashboard", + path: "page", + invisible: false, + order: 2, + requiredPolicy: "MyProjectName::MyNewPage" +}; + +this.config.dispatchAddRoute(newRoute); +// returns a state stream which emits after dispatch action is complete +``` + +`newRoute` 将被放置在根级别,没有任何父路由,并且其url将存储为 `'/path'`. + +如果你想要**添加一个子路由,你可以这样做:** + +```js +// this.config is instance of ConfigStateService + +const newRoute: ABP.Route = { + parentName: "AbpAccount::Login", + name: "My New Page", + iconClass: "fa fa-dashboard", + path: "page", + invisible: false, + order: 2, + requiredPolicy: "MyProjectName::MyNewPage" +}; + +this.config.dispatchAddRoute(newRoute); +// returns a state stream which emits after dispatch action is complete +``` + +`newRoute` 做为 `'AbpAccount::Login'` 父路由的子路由被放置,它的url被设置为 `'/account/login/page'`. + +#### 路由配置属性 + +请参阅 `ABP.Route` 类型,获取可在参数中传递给 `dispatchSetEnvironment` 的所有属性. 你可以在[common.ts 文件](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/common.ts#L27)中找到. + +### 如何设置环境 + +`dispatchSetEnvironment` 将传递给它的环境变量放在 `Store` 中的配置状态下. 使用方法如下: + +```js +// this.config is instance of ConfigStateService + +this.config.dispatchSetEnvironment({ + /* environment properties here */ +}); +// returns a state stream which emits after dispatch action is complete +``` + +注意,**你不必在应用程序启动时调用此方法**,因为环境变量已经在启动时存储了. + +#### 环境属性 + +请参阅 `Config.Environment` 类型,获取可在参数中传递给 `dispatchSetEnvironment` 的所有属性. 你可以在[config.ts 文件](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L13)中找到. + +## 下一步是什么? + +* [组件替换](./Component-Replacement.md) \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/Container-Strategy.md b/docs/zh-Hans/UI/Angular/Container-Strategy.md new file mode 100644 index 0000000000..daf3be01db --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Container-Strategy.md @@ -0,0 +1,87 @@ +# ContainerStrategy + +`ContainerStrategy` 是 @abp/ng.core 包暴露出的抽象类. 有两种扩展容器扩展策略: `ClearContainerStrategy` 和 `InsertIntoContainerStrategy`. 它们实现了相同的方法和属性,这两种策略都可以帮助你定义容器的准备方式和内容的投影位置. + +## API + +`ClearContainerStrategy` 是一个扩展了 `ContainerStrategy` 的类. 它允许你**将内容投影之前清除容器**. + +### 构造函数 + +```js +constructor( + public containerRef: ViewContainerRef, + private index?: number, // works only in InsertIntoContainerStrategy +) +``` + +- `containerRef` 是在投影内容时使用的 `ViewContainerRef`. + +### getIndex + +```js +getIndex(): number +``` + +该方法返回被 `0` 和 `containerRef` `length` 限制的给定索引. 对于没有索引的策略,它返回`0`. + +### prepare + +```js +prepare(): void +``` + +此方法在内容投影之前调用. 基于使用的容器策略,它要么清除容器,要么什么都不做(空操作). + +## ClearContainerStrategy + +`ClearContainerStrategy` 是一个扩展了 `ContainerStrategy` 的类. 它允许你**将内容投影之前清除容器**. + +## InsertIntoContainerStrategy + +`InsertIntoContainerStrategy` 是一个扩展了 `ContainerStrategy` 的类. 它允许你**将内容投影到容器中的特定节点索引上**. + +## 预定义的容器策略 + +可以通过 `CONTAINER_STRATEGY` 常量访问预定义的容器策略. + +### Clear + +```js +CONTAINER_STRATEGY.Clear(containerRef: ViewContainerRef) +``` + +在内容投影之前清除给定的容器. + + +### Append + +```js +CONTAINER_STRATEGY.Append(containerRef: ViewContainerRef) +``` + +将投影内容附加到容器中. + + +### Prepend + +```js +CONTAINER_STRATEGY.Prepend(containerRef: ViewContainerRef) +``` + +将投影的内容预先写入容器中. + +### Insert + +```js +CONTAINER_STRATEGY.Insert( + containerRef: ViewContainerRef, + index: number, +) +``` + +将投影内容按照给定的索引(在`0` 到 `containerRef`的长度之间)插入到容器中. + +## 另请参阅 + +- [ProjectionStrategy](./Projection-Strategy.md) diff --git a/docs/zh-Hans/UI/Angular/Content-Projection-Service.md b/docs/zh-Hans/UI/Angular/Content-Projection-Service.md new file mode 100644 index 0000000000..72fa468a79 --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Content-Projection-Service.md @@ -0,0 +1,77 @@ +# 内容投影 + +你可以使用位于@abp/ng.core包中的 `ContentProjectionService` 简单明确的投影内容. + +## 入门 + +你不必在模块或组件级别提供 `ContentProjectionService`,因为它已经在**根中提供了**. 你可以在组件中注入并开始使用它. 为了获得更好的类型支持,你可以将迭代项目的类型传递给它. + +```js +import { ContentProjectionService } from '@abp/ng.core'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + constructor(private contentProjectionService: ContentProjectionService) {} +} +``` + +## 用法 + +你可以使用 `ContentProjectionService` 的 `projectContent` 方法在你的项目中动态的渲染组件和模板. + +### 如何将组件投影到根级别 + +如果将 `RootComponentProjectionStrategy` 做为 `projectContent` 方法的第一个参数,那么 `ContentProjectionService` 会解析投影组件并放在根级别,它还为组件传递上下文. + +```js +const strategy = PROJECTION_STRATEGY.AppendComponentToBody( + SomeOverlayComponent, + { someOverlayProp: "SOME_VALUE" } +); + +const componentRef = this.contentProjectionService.projectContent(strategy); +``` + +在上面的示例中, `SomeOverlayComponent` 组件放置在 `` 的**末尾**并返回 `ComponentRef`. 另外将应用给定的上下文,因此组件的 `someOverlayProp` 被设置为 `SOME_VALUE`. + +> 你应该总是返回 `ComponentRef` 实例,因为它是对投影组件的引用,在你需要时使用该引用销毁投影视图和组件实例. + +### 如何将组件和模板投影到容器中 + +如果将 `ComponentProjectionStrategy` 或 `TemplateProjectionStrategy` 做为 `projectContent` 方法的第一个参数,并且传递 `ViewContainerRef` 做为策略的第二个参数传递. 那么 `ContentProjectionService` 把组件或模板投影到给定的容器中,它还为组件或模板传递上下文. + +```js +const strategy = PROJECTION_STRATEGY.ProjectComponentToContainer( + SomeComponent, + viewContainerRefOfTarget, + { someProp: "SOME_VALUE" } +); + +const componentRef = this.contentProjectionService.projectContent(strategy); +``` + +在上面的示例中,`viewContainerRefOfTarget`(它是一个`ViewContainerRef` 实例)将被清除,并把 `SomeComponent` 组件放在其中. 另外将应用给定的上下文,因此组件的 `someProp` 被设置为 `SOME_VALUE`. + +> 你应该总是返回 `ComponentRef` 或 `EmbeddedViewRef` ,因为它是对投影内容的引用,在你需要时使用该引用销毁它们. + +请参考[ProjectionStrategy](./Projection-Strategy.md)查看所有可用的投影策略以及如何构建自己的投影策略. + +## API + +### projectContent + +```js +projectContent | TemplateRef>( + projectionStrategy: ProjectionStrategy, + injector = this.injector, +): ComponentRef | EmbeddedViewRef +``` + +- `projectionStrategy` 参数是此处的要点,在上面进行了说明. +- `injector` 参数是 `Injector` 实例,你可以传递到投影内容. 在 `TemplateProjectionStrategy` 并没有使用到它. + +## 下一步是什么? + +- [TrackByService](./Track-By-Service.md) \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/Content-Security-Strategy.md b/docs/zh-Hans/UI/Angular/Content-Security-Strategy.md new file mode 100644 index 0000000000..7e419f8ab7 --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Content-Security-Strategy.md @@ -0,0 +1,55 @@ +# ContentSecurityStrategy + +`ContentSecurityStrategy` 是@abp/ng.core包暴露出的抽象类. 它可以根据[内容安全策略](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy)帮助你将内联脚本或样式标记为安全. + +## API + +### 构造函数 + +```js +constructor(public nonce?: string) +``` + +- `nonce` 启用将内联脚本或样式列入白名单,避免在[script-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#Unsafe_inline_script)和[style-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src#Unsafe_inline_styles)指令中使用 `unsafe-inline`. + +### applyCSP + +```js +applyCSP(element: HTMLScriptElement | HTMLStyleElement): void +``` + +该方法将上述属性映射到给定`element`. + +## LooseContentSecurityPolicy + +`LooseContentSecurityPolicy` 是扩展了 `ContentSecurityStrategy` 的类. 它需要 `nonce` 和带有给定 `` 元素放置在 ``的末尾, `scriptElement` 类型是一个 `HTMLScriptElement`. + +请参考[ContentStrategy](./Content-Strategy.md)查看所有可用的内容策略以及如何构建自己的内容策略. + +> 重要说明: `DomInsertionService` 不会两次插入相同的内容. 为了再次添加内容你首先应该使用 `removeContent` 方法删除旧内容. + +### 如何插入Styles + +`insertContent` 方法的第一个参数需要一个 `ContentStrategy`. 如果传递 `StyleContentStrategy` 实例, `DomInsertionService` 将创建具有给定内容的 `` 元素放置在 ``的末尾, `styleElement` 类型是一个 `HTMLStyleElement`. + +请参考[ContentStrategy](./Content-Strategy.md)查看所有可用的内容策略以及如何构建自己的内容策略. +. +> 重要说明: `DomInsertionService` 不会两次插入相同的内容. 为了再次添加内容你首先应该使用 `removeContent` 方法删除旧内容. + +### 如何删除已插入的 Scripts & Styles + +如果你传递 `HTMLScriptElement` 或 `HTMLStyleElement` 做为 `removeContent` 方法的第一个参数, `DomInsertionService` 将删除给定的元素. + +```js +import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + private styleElement: HTMLStyleElement; + + constructor(private domInsertionService: DomInsertionService) {} + + ngOnInit() { + this.styleElement = this.domInsertionService.insertContent( + CONTENT_STRATEGY.AppendStyleToHead('body {margin: 0;}') + ); + } + + ngOnDestroy() { + this.domInsertionService.removeContent(this.styleElement); + } +} +``` + +在上面的示例中,销毁组件时,将从 `` 中删除 `` 元素. + +## API + +### insertContent + +```js +insertContent( + contentStrategy: ContentStrategy, +): T +``` + +- `contentStrategy` 是方法的重要参数,已经在上方进行说明. +- 根据给定的策略返回 `HTMLScriptElement` 或 `HTMLStyleElement`. + +### removeContent + +```js +removeContent(element: HTMLScriptElement | HTMLStyleElement): void +``` + +- `element` 参数是已插入的 `HTMLScriptElement` 或 `HTMLStyleElement` 元素,它们应由 `insertContent` 方法返回. + +### has + +```js +has(content: string): boolean +``` + +`has` 返回一个布尔值,用于表示给定的内容是否插入到DOM. + +- `content` 参数是 `HTMLScriptElement` 或 `HTMLStyleElement` 元素的内容. + +## 下一步是什么? + +- [ContentProjectionService](./Content-Projection-Service.md) \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/Dom-Strategy.md b/docs/zh-Hans/UI/Angular/Dom-Strategy.md new file mode 100644 index 0000000000..c4982853b9 --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Dom-Strategy.md @@ -0,0 +1,80 @@ +# DomStrategy + +`DomStrategy` 是@abp/ng.core包暴露出的抽象类. 它的实例定义了如何将元素附加到DOM以及如何被其它类(如`LoadingStrategy`)使用. + +## API + +### 构造函数 + +```js +constructor( + public target?: HTMLElement, + public position?: InsertPosition +) +``` + +- `target` 是一个 HTMLElement (默认值: document.head_). +- `position` 定义将创建的元素放置在何处. 可以在[此处](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement)找到所有可能的 `position` 值(默认值: 'beforeend'_). + +### insertElement + +```js +insertElement(element: HTMLElement): void +``` + +该方法根据 `postion` 将给定 `元素` 插入到目标中. + +## 预定义DOM策略 + +可以通过 `DOM_STRATEGY` 常量访问预定义的dom策略. + + +### AppendToBody + +```js +DOM_STRATEGY.AppendToBody() +``` + +`insertElement` 将给定 `元素` 放在 `` 的末尾. + + +### AppendToHead + +```js +DOM_STRATEGY.AppendToHead() +``` + +`insertElement` 将给定 `元素` 放在 `` 的末尾. + +### PrependToHead + +```js +DOM_STRATEGY.PrependToHead() +``` + +`insertElement` 将给定 `元素` 放在 `` 的头部. + + +### AfterElement + +```js +DOM_STRATEGY.AfterElement(target: HTMLElement) +``` + +`insertElement` 将给定 `元素` 放在 `target` 之后 (做为同级元素). + +### BeforeElement + +```js +DOM_STRATEGY.BeforeElement(target: HTMLElement) +``` + +`insertElement` 将给定 `元素` 放在 `target` 之前 (做为同级元素). + +## 另请参阅 + +- [DomInsertionService](./Dom-Insertion-Service.md) +- [LazyLoadService](./Lazy-Load-Service.md) +- [LoadingStrategy](./Loading-Strategy.md) +- [ContentStrategy](./Content-Strategy.md) +- [ProjectionStrategy](./Projection-Strategy.md) diff --git a/docs/zh-Hans/UI/Angular/HTTP-Requests.md b/docs/zh-Hans/UI/Angular/HTTP-Requests.md new file mode 100644 index 0000000000..47f871d6e6 --- /dev/null +++ b/docs/zh-Hans/UI/Angular/HTTP-Requests.md @@ -0,0 +1,179 @@ +## HTTP请求 + +## 关于 HttpClient + +Angular具有很棒的 `HttpClient` 与后端服务进行通信. 它位于顶层,是[XMLHttpRequest Web API](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)的封装. 同时也是Angular建议用于任何HTTP请求的代理,在你的ABP项目中使用 `HttpClient` 是最佳做法. + +但是 `HttpClient` 将错误处理留给调用方,换句话说HTTP错误是通过手动处理的,通过挂接到返回的 `Observable` 的观察者中来处理. + +```js +getConfig() { + this.http.get(this.configUrl).subscribe( + config => this.updateConfig(config), + error => { + // Handle error here + }, + ); +} +``` + +上面的代码尽管清晰灵活,但即使将错误处理委派给Store或任何其他注入. 以这种方式处理错误也是重复性的工作. + +`HttpInterceptor` 能够捕获 `HttpErrorResponse` 并可用于集中的错误处理. 然而,在必须放置错误处理程序(也就是拦截器)的情况下,需要额外的工作以及对Angular内部机制的理解. 检查[这个issue](https://github.com/angular/angular/issues/20203)了解详情. + +## RestService + +ABP核心模块有用于HTTP请求的实用程序服务: `RestService`. 除非另有明确配置,否则它将捕获HTTP错误并调度 `RestOccurError` 操作, 然后由 `ThemeSharedModule` 引入的 `ErrorHandler` 捕获此操作. 你应该已经在应用程序中导入了此模块,在使用 `RestService` 时,默认情况下将自动处理所有HTTP错误. + +### RestService 入门 + +为了使用 `RestService`, 你必须将它注入到你的类中. + +```js +import { RestService } from '@abp/ng.core'; + +@Injectable({ + /* class metadata here */ +}) +class DemoService { + constructor(private rest: RestService) {} +} +``` + +你不必在模块或组件/指令级别提供 `estService`,因为它已经在**根中**中提供了. + +### 如何使用RestService发出请求 + +你可以使用 `RestService` 的 `request` 方法来处理HTTP请求. 示例: + +```js +getFoo(id: number) { + const request: Rest.Request = { + method: 'GET', + url: '/api/some/path/to/foo/' + id, + }; + + return this.rest.request(request); +} +``` + +`request` 方法始终返回 `Observable`. 无论何时使用 `getFoo` 方法,都可以执行以下操作: + +```js +doSomethingWithFoo(id: number) { + this.demoService.getFoo(id).subscribe( + foo => { + // Do something with foo. + } + ) +} +``` + +**你不必担心关于取消订阅**. `RestService` 在内部使用 `HttpClient`,因此它返回的每个可观察对象都是有限的可观察对象,成功或出错后将自动关闭订阅. + +如你所见,`request` 方法获取一个具有 `Rest.Reques` 类型的请求选项对象. 此泛型类型需要请求主体的接口. 当没有正文时,例如在 `GET` 或 `DELETE` 请求中,你可以传递 `null`. 示例: + +```js +postFoo(body: Foo) { + const request: Rest.Request = { + method: 'POST', + url: '/api/some/path/to/foo', + body + }; + + return this.rest.request(request); +} +``` + +你可以在[此处检查](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L23)完整的 `Rest.Request` 类型,与Angular中的[HttpRequest](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L23)类相比只有很少的改动. + +### 如何禁用RestService的默认错误处理程序 + +默认 `request` 方法始终处理错误. 让我们看看如何改变这种行为并由自己处理错误: + +```js +deleteFoo(id: number) { + const request: Rest.Request = { + method: 'DELETE', + url: '/api/some/path/to/foo/' + id, + }; + + return this.rest.request(request, { skipHandleError: true }); +} +``` + +`skipHandleError` 配置选项设置为 `true` 时,禁用错误处理程序,并返回 `observable` 引发错误,你可以在订阅中捕获该错误. + +```js +removeFooFromList(id: number) { + this.demoService.deleteFoo(id).subscribe( + foo => { + // Do something with foo. + }, + error => { + // Do something with error. + } + ) +} +``` + +### 如何从应用程序配置获取特定的API端点 + +`request` 方法接收到的另一个配置选项是 `apiName` (在v2.4中可用),它用于从应用程序配置获取特定的模块端点. + +```js +putFoo(body: Foo, id: string) { + const request: Rest.Request = { + method: 'PUT', + url: '/' + id, + body + }; + + return this.rest.request(request, {apiName: 'foo'}); +} +``` + +上面的putFoo将请求 `https://localhost:44305/api/some/path/to/foo/{id}` 当环境变量如下: + +```js +// environment.ts + +export const environment = { + apis: { + default: { + url: 'https://localhost:44305', + }, + foo: { + url: 'https://localhost:44305/api/some/path/to/foo', + }, + }, + + /* rest of the environment variables here */ +} +``` + +### 如何观察响应对象或HTTP事件而不是正文 + +`RestService` 假定你通常对响应的正文感兴趣,默认情况下将 `observe` 属性设置为 `body`. 但是有时你可能对其他内容(例如自定义标头)非常感兴趣. 为此, `request` 方法在 `config` 对象中接收 `watch` 属性. + +```js +getSomeCustomHeaderValue() { + const request: Rest.Request = { + method: 'GET', + url: '/api/some/path/that/sends/some-custom-header', + }; + + return this.rest.request>( + request, + {observe: Rest.Observe.Response}, + ).pipe( + map(response => response.headers.get('Some-Custom-Header')) + ); +} +``` + +你可以在[此处](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L10)找到 `Rest.Observe` 枚举. + +## 下一步是什么? + +* [本地化](./Localization.md) \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/Lazy-Load-Service.md b/docs/zh-Hans/UI/Angular/Lazy-Load-Service.md new file mode 100644 index 0000000000..07753f8541 --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Lazy-Load-Service.md @@ -0,0 +1,189 @@ +# 如何懒加载 Scripts 与 Styles + +你可以使用@abp/ng.core包中的 `LazyLoadService` 以简单明了的方式延迟加载脚本和样式. + +## 入门 + +你不必在模块或组件/指令级别提供 `LazyLoadService`,因为它已经在**根中**中提供了. 你可以在组件,指令或服务中注入并使用它. + +```js +import { LazyLoadService } from '@abp/ng.core'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + constructor(private lazyLoadService: LazyLoadService) {} +} +``` + +## 用法 + +你可以使用 `LazyLoadService` 的 `load` 方法在DOM中的所需位置创建 `