Browse Source

Merge branch 'dev' into maliming/AbpRemoteServiceApiDescriptionProvider

pull/3526/head
maliming 7 years ago
committed by GitHub
parent
commit
8688ba26af
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      common.props
  2. 3
      docs/en/AutoMapper-Integration.md
  3. 26
      docs/en/Best-Practices/Application-Services.md
  4. 1
      docs/en/Best-Practices/Data-Transfer-Objects.md
  5. 1
      docs/en/CLI.md
  6. 2
      docs/en/Customizing-Application-Modules-Extending-Entities.md
  7. 99
      docs/en/Customizing-Application-Modules-Overriding-Services.md
  8. 2
      docs/en/Entity-Framework-Core-Migrations.md
  9. 266
      docs/en/Object-Extensions.md
  10. 17
      docs/en/Object-To-Object-Mapping.md
  11. 30
      docs/en/UI/Angular/Component-Replacement.md
  12. 101
      docs/en/UI/Angular/Container-Strategy.md
  13. 78
      docs/en/UI/Angular/Content-Projection-Service.md
  14. 117
      docs/en/UI/Angular/Context-Strategy.md
  15. 14
      docs/en/UI/Angular/Dom-Insertion-Service.md
  16. 1
      docs/en/UI/Angular/Dom-Strategy.md
  17. 2
      docs/en/UI/Angular/Loading-Strategy.md
  18. 200
      docs/en/UI/Angular/Projection-Strategy.md
  19. 8
      docs/en/docs-nav.json
  20. 2
      docs/zh-Hans/CLI.md
  21. 2
      docs/zh-Hans/Getting-Started-AspNetCore-Application.md
  22. 2
      docs/zh-Hans/Getting-Started-Console-Application.md
  23. 197
      docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md
  24. 100
      docs/zh-Hans/How-To/Customize-SignIn-Manager.md
  25. 17
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Timeago/TimeagoScriptContributor.cs
  26. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js
  27. 6
      framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Mvc/Abstractions/ActionDescriptorExtensions.cs
  28. 12
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs
  29. 13
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs
  30. 103
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Auditing/AbpAuditPageFilter.cs
  31. 112
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionPageFilter.cs
  32. 44
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Features/AbpFeaturePageFilter.cs
  33. 105
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowPageFilter.cs
  34. 6
      framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingInterceptorRegistrar.cs
  35. 5
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AuthorizationInterceptorRegistrar.cs
  36. 3
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs
  37. 12
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ProjectReferenceReplaceStep.cs
  38. 30
      framework/src/Volo.Abp.Core/Volo/Abp/DynamicProxy/DynamicProxyIgnoreTypes.cs
  39. 6
      framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureInterceptorRegistrar.cs
  40. 2
      framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensibleObjectMapper.cs
  41. 2
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json
  42. 11
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkInterceptorRegistrar.cs
  43. 11
      framework/src/Volo.Abp.Validation/Volo/Abp/Validation/ValidationInterceptorRegistrar.cs
  44. 23
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj
  45. 10
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs
  46. 1
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Auditing/AuditTestController_Tests.cs
  47. 19
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Auditing/AuditTestPage.cshtml
  48. 33
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Auditing/AuditTestPage.cshtml.cs
  49. 65
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Auditing/AuditTestPage_Tests.cs
  50. 19
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Authorization/AuthTestPage.cshtml
  51. 18
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Authorization/AuthTestPage.cshtml.cs
  52. 40
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Authorization/AuthTestPage_Tests.cs
  53. 4
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs
  54. 19
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestPage.cshtml
  55. 18
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestPage.cshtml.cs
  56. 56
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestPage_Tests.cs
  57. 19
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Features/FeatureTestPage.cshtml
  58. 27
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Features/FeatureTestPage.cshtml.cs
  59. 34
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Features/FeatureTestPage_Tests.cs
  60. 37
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkPageFilter_Exception_Rollback_Tests.cs
  61. 22
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkPageFilter_Tests.cs
  62. 19
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkTestPage.cshtml
  63. 54
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkTestPage.cshtml.cs
  64. 4
      framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/AbpAuthorizationTestModule.cs
  65. 4
      framework/test/Volo.Abp.FluentValidation.Tests/Volo/Abp/FluentValidation/ApplicationService_FluentValidation_Tests.cs
  66. 4
      framework/test/Volo.Abp.Validation.Tests/Volo/Abp/Validation/ApplicationService_Validation_Tests.cs
  67. 3
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs
  68. 1
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs
  69. 2
      npm/lerna.json
  70. 4
      npm/ng-packs/.vscode/settings.json
  71. 2
      npm/ng-packs/lerna.version.json
  72. 32
      npm/ng-packs/package.json
  73. 2
      npm/ng-packs/packages/account-config/package.json
  74. 6
      npm/ng-packs/packages/account/package.json
  75. 7
      npm/ng-packs/packages/account/src/lib/account-routing.module.ts
  76. 4
      npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.html
  77. 3
      npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.ts
  78. 2
      npm/ng-packs/packages/account/src/lib/components/login/login.component.html
  79. 3
      npm/ng-packs/packages/account/src/lib/components/login/login.component.ts
  80. 4
      npm/ng-packs/packages/account/src/lib/components/manage-profile/manage-profile.component.html
  81. 5
      npm/ng-packs/packages/account/src/lib/components/manage-profile/manage-profile.component.ts
  82. 2
      npm/ng-packs/packages/account/src/lib/components/register/register.component.html
  83. 3
      npm/ng-packs/packages/account/src/lib/components/register/register.component.ts
  84. 9
      npm/ng-packs/packages/account/src/lib/enums/components.ts
  85. 1
      npm/ng-packs/packages/account/src/public-api.ts
  86. 2
      npm/ng-packs/packages/core/package.json
  87. 1
      npm/ng-packs/packages/core/src/lib/models/index.ts
  88. 4
      npm/ng-packs/packages/core/src/lib/models/utility.ts
  89. 14
      npm/ng-packs/packages/core/src/lib/services/content-projection.service.ts
  90. 2
      npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts
  91. 1
      npm/ng-packs/packages/core/src/lib/services/index.ts
  92. 44
      npm/ng-packs/packages/core/src/lib/strategies/container.strategy.ts
  93. 47
      npm/ng-packs/packages/core/src/lib/strategies/context.strategy.ts
  94. 3
      npm/ng-packs/packages/core/src/lib/strategies/index.ts
  95. 176
      npm/ng-packs/packages/core/src/lib/strategies/projection.strategy.ts
  96. 80
      npm/ng-packs/packages/core/src/lib/tests/container.strategy.spec.ts
  97. 38
      npm/ng-packs/packages/core/src/lib/tests/content-projection.service.spec.ts
  98. 79
      npm/ng-packs/packages/core/src/lib/tests/context.strategy.spec.ts
  99. 36
      npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts
  100. 276
      npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts

2
common.props

@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Version>2.5.0</Version>
<Version>2.6.0</Version>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<PackageIconUrl>https://abp.io/assets/abp_nupkg.png</PackageIconUrl>
<PackageProjectUrl>https://abp.io</PackageProjectUrl>

3
docs/en/AutoMapper-Integration.md

@ -1,3 +0,0 @@
## AutoMapper Integration
TODO

26
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<Guid>
public class IssueDto : ExtensibleFullAuditedEntityDto<Guid>
{
public string Title { get; set; }
public string Text { get; set; }
@ -57,7 +58,7 @@ Example:
````C#
[Serializable]
public class IssueWithDetailsDto : FullAuditedEntityDto<Guid>
public class IssueWithDetailsDto : ExtensibleFullAuditedEntityDto<Guid>
{
public string Title { get; set; }
public string Text { get; set; }
@ -66,14 +67,14 @@ public class IssueWithDetailsDto : FullAuditedEntityDto<Guid>
}
[Serializable]
public class MilestoneDto : EntityDto<Guid>
public class MilestoneDto : ExtensibleEntityDto<Guid>
{
public string Name { get; set; }
public bool IsClosed { get; set; }
}
[Serializable]
public class LabelDto : EntityDto<Guid>
public class LabelDto : ExtensibleEntityDto<Guid>
{
public string Name { get; set; }
public string Color { get; set; }
@ -120,6 +121,7 @@ Task<List<QuestionWithDetailsDto>> 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.

1
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<TKey>`, `CreationAuditedEntityDto<TKey>`, `AuditedEntityDto<TKey>`, `FullAuditedEntityDto<TKey>` and so on).
* **Do** inherit from the **extensible DTO** classes for the **aggregate roots** (like `ExtensibleAuditedEntityDto<TKey>`), 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.

1
docs/en/CLI.md

@ -61,6 +61,7 @@ abp new Acme.BookStore
* `--template-source` or `-ts`: Specifies a custom template source to use to build the project. Local and network sources can be used(Like `D\localTemplate` or `https://<your url>.zip`).
* `--create-solution-folder` or `-csf`: Specifies if the project will be in a new folder in the output folder or directly the output folder.
* `--connection-string` or `-cs`: Overwrites the default connection strings in all `appsettings.json` files. The default connection string is `Server=localhost;Database=MyProjectName;Trusted_Connection=True;MultipleActiveResultSets=true`. You can set your own connection string if you don't want to use the default. Be aware that the default database provider is `SQL Server`, therefore you can only enter connection string for SQL Server!
* `--local-framework-ref --abp-path`: keeps local references to projects instead of replacing with NuGet package references.
### add-package

2
docs/en/Customizing-Application-Modules-Extending-Entities.md

@ -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.

99
docs/en/Customizing-Application-Modules-Overriding-Services.md

@ -161,6 +161,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<IdentityUserDto, string>(
"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<IdentityUser, string>(
"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<string>(
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.

2
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.
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.

266
docs/en/Object-Extensions.md

@ -1,3 +1,267 @@
# Object Extensions
TODO
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<string, object> 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<string>("Title");
if (user.GetProperty<bool>("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<string>(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<IdentityUser>(options =>
{
options.AddOrUpdateProperty<string>("SocialSecurityNumber");
options.AddOrUpdateProperty<bool>("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<IdentityUser, string>("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<string>(
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.
Example:
````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<IdentityUser, string>(
"SocialSecurityNumber",
options =>
{
options.CheckPairDefinitionOnMapping = false;
});
````
> See the "Object to Object Mapping" section to understand the `CheckPairDefinitionOnMapping` option.
`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.
## 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<IdentityUser, string>(
"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<IdentityUser, IdentityUserDto>()
.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<IdentityUser, string>(
"SocialSecurityNumber",
options =>
{
options.MapEfCore(b => b.HasMaxLength(32));
}
);
````
See the [Entity Framework Core Integration document](Entity-Framework-Core.md) for more.

17
docs/en/Object-To-Object-Mapping.md

@ -145,6 +145,23 @@ options.AddProfile<MyProfile>(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<User, UserDto>()
.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<TContext> Interface

30
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)

101
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)

78
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 `<body>` 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<T extends Type<any> | TemplateRef<any>>(
projectionStrategy: ProjectionStrategy<T>,
injector = this.injector,
): ComponentRef<C> | EmbeddedViewRef<C>
```
- `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)

117
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<InferredInstanceOf<T>>) {}
```
- `T` refers to component type here, i.e. `Type<C>`.
- `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<InferredInstanceOf<T>>): Partial<InferredInstanceOf<T>>
```
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<InferredContextOf<T>>) {}
```
- `T` refers to template context type here, i.e. `TemplateRef<C>`.
- `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<InferredContextOf<T>>
```
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<InferredContextOf<T>>)
```
This strategy will help you pass the given context to the projected component.
### Template
```js
CONTEXT_STRATEGY.Template(context: Partial<InferredContextOf<T>>)
```
This strategy will help you pass the given context to the projected template.
## See Also
- [ProjectionStrategy](./Projection-Strategy.md)

14
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 `<script>` or `<style>` element with given content in the DOM at the desired position.
You can use the `insertContent` method of `DomInsertionService` to create a `<script>` or `<style>` element with given content in the DOM at the desired position. There is also the `projectContent` method for dynamically rendering components and templates.
### How to Insert Scripts
@ -48,7 +46,6 @@ In the example above, `<script>alert()</script>` element will place at the **end
Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy.
### How to Insert Styles
If you pass a `StyleContentStrategy` instance as the first parameter of `insertContent` method, the `DomInsertionService` will create a `<style>` element with given `content` and place it in the designated DOM position.
@ -74,18 +71,17 @@ In the example above, `<style>body {margin: 0;}</style>` element will place at t
Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy.
## API
### insertContent
```js
insertContent(strategy: ContentStrategy): void
insertContent(contentStrategy: ContentStrategy): void
```
`strategy` parameter is the primary focus here and is explained above.
- `contentStrategy` parameter is the primary focus here and is explained above.
## What's Next?
- [TrackByService](./Track-By-Service.md)
- [ContentProjectionService](./Content-Projection-Service.md)

1
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)

2
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

200
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<T>
```
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 `<body>`.
### 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<T>
```
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<T>
```
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<T>,
)
```
Sets given context to the component and places it at the **end** of `<body>` tag in the document.
### AppendComponentToContainer
```js
PROJECTION_STRATEGY.AppendComponentToContainer(
component: T,
containerRef: ViewContainerRef,
contextStrategy?: ComponentContextStrategy<T>,
)
```
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<T>,
)
```
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<T>,
)
```
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<T>,
)
```
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<T>,
)
```
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<T>,
)
```
Clears the container, sets given context to the template, and places it **in the cleared** the container.
## See Also
- [DomInsertionService](./Dom-Insertion-Service.md)

8
docs/en/docs-nav.json

@ -155,6 +155,10 @@
{
"text": "Data Filtering",
"path": "Data-Filtering.md"
},
{
"text": "Object Extensions",
"path": "Object-Extensions.md"
}
]
},
@ -353,6 +357,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"

2
docs/zh-Hans/CLI.md

@ -56,12 +56,12 @@ abp new Acme.BookStore
* `mongodb`: MongoDB.
* `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). 如果没有指定,则使用最新版本.大多数情况下,您会希望使用最新的版本.
* `--template-source` 或者 `-ts`: 指定自定义模板源用于生成项目,可以使用本地源和网络源(例如 `D\localTemplate` 或 `https://<your url>.zip`).
* `--create-solution-folder` 或者 `-csf`: 指定项目是在输出文件夹中的新文件夹中还是直接在输出文件夹中.
* `--connection-string` 或者 `-cs`: 重写所有 `appsettings.json` 文件的默认连接字符串. 默认连接字符串是 `Server=localhost;Database=MyProjectName;Trusted_Connection=True;MultipleActiveResultSets=true`. 如果你不想使用默认,你可以设置自己的连接字符串. 默认的数据库提供程序是 `SQL Server`, 所以你只能输入SQL Server连接字符串!
* `--local-framework-ref --abp-path`: 使用对项目的本地引用,而不是替换为NuGet包引用.
### add-package

2
docs/zh-Hans/Getting-Started-AspNetCore-Application.md

@ -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)获取本教程中创建的示例项目的源代码.

2
docs/zh-Hans/Getting-Started-Console-Application.md

@ -121,4 +121,4 @@ namespace AbpConsoleDemo
### 源码
从[这里](https://github.com/abpframework/abp/tree/dev/samples/BasicConsoleApplication)获取本教程中创建的示例项目的源代码.
从[这里](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication)获取本教程中创建的示例项目的源代码.

197
docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md

@ -1,3 +1,198 @@
# 如何对MVC / Razor页面应用程序使用Azure Active Directory身份验证
TODO...
本文介绍了如何将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": "<your-tenant-id>",
"ClientId": "<your-client-id>",
"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<OpenIdConnectOptions>(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": "<your-tenant-id>",
"ClientId": "<your-client-id>",
"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` 的<u>域</u>, 而不仅是 `/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).

100
docs/zh-Hans/How-To/Customize-SignIn-Manager.md

@ -1,3 +1,101 @@
# 如何为ABP应用程序定制SignIn Manager
TODO...
在使用[应用程序启动模板](../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<Volo.Abp.Identity.IdentityUser>
{
public CustomSignInManager(
Microsoft.AspNetCore.Identity.UserManager<Volo.Abp.Identity.IdentityUser> userManager,
Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor,
Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<Volo.Abp.Identity.IdentityUser> claimsFactory,
Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.IdentityOptions> optionsAccessor,
Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser>> logger,
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes,
Microsoft.AspNetCore.Identity.IUserConfirmation<Volo.Abp.Identity.IdentityUser> 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<Microsoft.AspNetCore.Identity.ExternalLoginInfo> 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 =>
{
identityBuilder.AddSignInManager<CustomSignInManager>();
});
````
## 本文的源代码
你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization)找到已完成的示例源码.
## 另请参阅
* [如何为MVC / Razor页面应用程序自定义登录页面](Customize-Login-Page-MVC.md).
* [身份管理模块](../Modules/Identity.md).

17
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Timeago/TimeagoScriptContributor.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Globalization;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQuery;
using Volo.Abp.Modularity;
@ -12,5 +13,21 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Timeago
{
context.Files.AddIfNotContains("/libs/timeago/jquery.timeago.js");
}
public override void ConfigureDynamicResources(BundleConfigurationContext context)
{
var cultureName = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
if (cultureName.StartsWith("en"))
{
return;
}
var cultureFileName = $"/libs/timeago/locales/jquery.timeago.{cultureName}.js";
if (context.FileProvider.GetFileInfo(cultureFileName).Exists)
{
context.Files.Add(cultureFileName);
}
}
}
}

4
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js

@ -60,6 +60,8 @@
container: 'body'
});
args.$el.findWithSelf('.timeago').timeago();
enableFormFeatures(args.$el.findWithSelf('form'), true);
initializeScript(args.$el);
@ -82,6 +84,8 @@
container: 'body'
});
$('.timeago').timeago();
$('[data-auto-focus="true"]').first().findWithSelf('input,select').focus();
});

6
framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Mvc/Abstractions/ActionDescriptorExtensions.cs

@ -1,6 +1,7 @@
using System;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Volo.Abp;
using Volo.Abp.AspNetCore.Mvc;
@ -37,5 +38,10 @@ namespace Microsoft.AspNetCore.Mvc.Abstractions
{
return actionDescriptor is ControllerActionDescriptor;
}
public static bool IsPageAction(this ActionDescriptor actionDescriptor)
{
return actionDescriptor is PageActionDescriptor;
}
}
}

12
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs

@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
@ -14,6 +14,9 @@ using System.Linq;
using System.Net;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.Extensions.Hosting;
using Volo.Abp.ApiVersioning;
using Volo.Abp.AspNetCore.Mvc.ApiExploring;
@ -24,6 +27,7 @@ using Volo.Abp.AspNetCore.Mvc.Localization;
using Volo.Abp.AspNetCore.VirtualFileSystem;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http;
using Volo.Abp.DynamicProxy;
using Volo.Abp.Http.Modeling;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
@ -42,6 +46,9 @@ namespace Volo.Abp.AspNetCore.Mvc
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
DynamicProxyIgnoreTypes.Add<ControllerBase>();
DynamicProxyIgnoreTypes.Add<PageModel>();
context.Services.AddConventionalRegistrar(new AbpAspNetCoreMvcConventionalRegistrar());
}
@ -131,6 +138,9 @@ namespace Volo.Abp.AspNetCore.Mvc
//Use DI to create view components
context.Services.Replace(ServiceDescriptor.Singleton<IViewComponentActivator, ServiceBasedViewComponentActivator>());
//Use DI to create razor page
context.Services.Replace(ServiceDescriptor.Singleton<IPageModelActivatorProvider, ServiceBasedPageModelActivatorProvider>());
//Add feature providers
var partManager = context.Services.GetSingletonInstance<ApplicationPartManager>();
var application = context.Services.GetSingletonInstance<IAbpApplication>();

13
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs

@ -16,7 +16,8 @@ namespace Volo.Abp.AspNetCore.Mvc
public static void AddAbp(this MvcOptions options, IServiceCollection services)
{
AddConventions(options, services);
AddFilters(options);
AddActionFilters(options);
AddPageFilters(options);
AddModelBinders(options);
AddMetadataProviders(options, services);
}
@ -26,7 +27,7 @@ namespace Volo.Abp.AspNetCore.Mvc
options.Conventions.Add(new AbpServiceConventionWrapper(services));
}
private static void AddFilters(MvcOptions options)
private static void AddActionFilters(MvcOptions options)
{
options.Filters.AddService(typeof(AbpAuditActionFilter));
options.Filters.AddService(typeof(AbpNoContentActionFilter));
@ -36,6 +37,14 @@ namespace Volo.Abp.AspNetCore.Mvc
options.Filters.AddService(typeof(AbpExceptionFilter));
}
private static void AddPageFilters(MvcOptions options)
{
options.Filters.AddService(typeof(AbpExceptionPageFilter));
options.Filters.AddService(typeof(AbpAuditPageFilter));
options.Filters.AddService(typeof(AbpFeaturePageFilter));
options.Filters.AddService(typeof(AbpUowPageFilter));
}
private static void AddModelBinders(MvcOptions options)
{
options.ModelBinderProviders.Insert(0, new AbpDateTimeModelBinderProvider());

103
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Auditing/AbpAuditPageFilter.cs

@ -0,0 +1,103 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Options;
using Volo.Abp.Aspects;
using Volo.Abp.Auditing;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Mvc.Auditing
{
public class AbpAuditPageFilter : IAsyncPageFilter, ITransientDependency
{
protected AbpAuditingOptions Options { get; }
private readonly IAuditingHelper _auditingHelper;
private readonly IAuditingManager _auditingManager;
public AbpAuditPageFilter(IOptions<AbpAuditingOptions> options, IAuditingHelper auditingHelper, IAuditingManager auditingManager)
{
Options = options.Value;
_auditingHelper = auditingHelper;
_auditingManager = auditingManager;
}
public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
{
return Task.CompletedTask;
}
public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
{
if (context.HandlerMethod == null || !ShouldSaveAudit(context, out var auditLog, out var auditLogAction))
{
await next();
return;
}
using (AbpCrossCuttingConcerns.Applying(context.HandlerInstance, AbpCrossCuttingConcerns.Auditing))
{
var stopwatch = Stopwatch.StartNew();
try
{
var result = await next();
if (result.Exception != null && !result.ExceptionHandled)
{
auditLog.Exceptions.Add(result.Exception);
}
}
catch (Exception ex)
{
auditLog.Exceptions.Add(ex);
throw;
}
finally
{
stopwatch.Stop();
auditLogAction.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds);
auditLog.Actions.Add(auditLogAction);
}
}
}
private bool ShouldSaveAudit(PageHandlerExecutingContext context, out AuditLogInfo auditLog, out AuditLogActionInfo auditLogAction)
{
auditLog = null;
auditLogAction = null;
if (!Options.IsEnabled)
{
return false;
}
if (!context.ActionDescriptor.IsPageAction())
{
return false;
}
var auditLogScope = _auditingManager.Current;
if (auditLogScope == null)
{
return false;
}
if (!_auditingHelper.ShouldSaveAudit(context.HandlerMethod.MethodInfo, true))
{
return false;
}
auditLog = auditLogScope.Log;
auditLogAction = _auditingHelper.CreateAuditLogAction(
auditLog,
context.HandlerMethod.GetType(),
context.HandlerMethod.MethodInfo,
context.HandlerArguments
);
return true;
}
}
}

112
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionPageFilter.cs

@ -0,0 +1,112 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.AspNetCore.ExceptionHandling;
using Volo.Abp.DependencyInjection;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.Http;
using Volo.Abp.Json;
namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
{
public class AbpExceptionPageFilter : IAsyncPageFilter, ITransientDependency
{
public ILogger<AbpExceptionPageFilter> Logger { get; set; }
private readonly IExceptionToErrorInfoConverter _errorInfoConverter;
private readonly IHttpExceptionStatusCodeFinder _statusCodeFinder;
private readonly IJsonSerializer _jsonSerializer;
public AbpExceptionPageFilter(
IExceptionToErrorInfoConverter errorInfoConverter,
IHttpExceptionStatusCodeFinder statusCodeFinder,
IJsonSerializer jsonSerializer)
{
_errorInfoConverter = errorInfoConverter;
_statusCodeFinder = statusCodeFinder;
_jsonSerializer = jsonSerializer;
Logger = NullLogger<AbpExceptionPageFilter>.Instance;
}
public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
{
return Task.CompletedTask;
}
public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
{
if (context.HandlerMethod == null || !ShouldHandleException(context))
{
await next();
return;
}
var pageHandlerExecutedContext = await next();
if (pageHandlerExecutedContext.Exception == null)
{
return;;
}
await HandleAndWrapException(pageHandlerExecutedContext);
}
protected virtual bool ShouldHandleException(PageHandlerExecutingContext context)
{
//TODO: Create DontWrap attribute to control wrapping..?
if (context.ActionDescriptor.IsPageAction() &&
ActionResultHelper.IsObjectResult(context.HandlerMethod.MethodInfo.ReturnType))
{
return true;
}
if (context.HttpContext.Request.CanAccept(MimeTypes.Application.Json))
{
return true;
}
if (context.HttpContext.Request.IsAjax())
{
return true;
}
return false;
}
protected virtual async Task HandleAndWrapException(PageHandlerExecutedContext context)
{
//TODO: Trigger an AbpExceptionHandled event or something like that.
context.HttpContext.Response.Headers.Add(AbpHttpConsts.AbpErrorFormat, "true");
context.HttpContext.Response.StatusCode = (int)_statusCodeFinder.GetStatusCode(context.HttpContext, context.Exception);
var remoteServiceErrorInfo = _errorInfoConverter.Convert(context.Exception);
context.Result = new ObjectResult(new RemoteServiceErrorResponse(remoteServiceErrorInfo));
var logLevel = context.Exception.GetLogLevel();
Logger.LogWithLevel(logLevel, $"---------- {nameof(RemoteServiceErrorInfo)} ----------");
Logger.LogWithLevel(logLevel, _jsonSerializer.Serialize(remoteServiceErrorInfo, indented: true));
Logger.LogException(context.Exception, logLevel);
await context.HttpContext
.RequestServices
.GetRequiredService<IExceptionNotifier>()
.NotifyAsync(
new ExceptionNotificationContext(context.Exception)
);
context.Exception = null; //Handled!
}
}
}

44
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Features/AbpFeaturePageFilter.cs

@ -0,0 +1,44 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Volo.Abp.Aspects;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Features;
namespace Volo.Abp.AspNetCore.Mvc.Features
{
public class AbpFeaturePageFilter : IAsyncPageFilter, ITransientDependency
{
private readonly IMethodInvocationFeatureCheckerService _methodInvocationAuthorizationService;
public AbpFeaturePageFilter(IMethodInvocationFeatureCheckerService methodInvocationAuthorizationService)
{
_methodInvocationAuthorizationService = methodInvocationAuthorizationService;
}
public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
{
return Task.CompletedTask;
}
public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
{
if (context.HandlerMethod == null || !context.ActionDescriptor.IsPageAction())
{
await next();
return;
}
var methodInfo = context.HandlerMethod.MethodInfo;
using (AbpCrossCuttingConcerns.Applying(context.HandlerInstance, AbpCrossCuttingConcerns.FeatureChecking))
{
await _methodInvocationAuthorizationService.CheckAsync(
new MethodInvocationFeatureCheckerContext(methodInfo)
);
await next();
}
}
}
}

105
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowPageFilter.cs

@ -0,0 +1,105 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Uow;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;
namespace Volo.Abp.AspNetCore.Mvc.Uow
{
public class AbpUowPageFilter : IAsyncPageFilter, ITransientDependency
{
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly AbpUnitOfWorkDefaultOptions _defaultOptions;
public AbpUowPageFilter(IUnitOfWorkManager unitOfWorkManager, IOptions<AbpUnitOfWorkDefaultOptions> options)
{
_unitOfWorkManager = unitOfWorkManager;
_defaultOptions = options.Value;
}
public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
{
return Task.CompletedTask;
}
public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
{
if (context.HandlerMethod == null || !context.ActionDescriptor.IsPageAction())
{
await next();
return;
}
var methodInfo = context.HandlerMethod.MethodInfo;
var unitOfWorkAttr = UnitOfWorkHelper.GetUnitOfWorkAttributeOrNull(methodInfo);
context.HttpContext.Items["_AbpActionInfo"] = new AbpActionInfoInHttpContext
{
IsObjectResult = ActionResultHelper.IsObjectResult(context.HandlerMethod.MethodInfo.ReturnType)
};
if (unitOfWorkAttr?.IsDisabled == true)
{
await next();
return;
}
var options = CreateOptions(context, unitOfWorkAttr);
//Trying to begin a reserved UOW by AbpUnitOfWorkMiddleware
if (_unitOfWorkManager.TryBeginReserved(AbpUnitOfWorkMiddleware.UnitOfWorkReservationName, options))
{
var result = await next();
if (!Succeed(result))
{
await RollbackAsync(context);
}
return;
}
//Begin a new, independent unit of work
using (var uow = _unitOfWorkManager.Begin(options))
{
var result = await next();
if (Succeed(result))
{
await uow.CompleteAsync(context.HttpContext.RequestAborted);
}
}
}
private AbpUnitOfWorkOptions CreateOptions(PageHandlerExecutingContext context, UnitOfWorkAttribute unitOfWorkAttribute)
{
var options = new AbpUnitOfWorkOptions();
unitOfWorkAttribute?.SetOptions(options);
if (unitOfWorkAttribute?.IsTransactional == null)
{
options.IsTransactional = _defaultOptions.CalculateIsTransactional(
autoValue: !string.Equals(context.HttpContext.Request.Method, HttpMethod.Get.Method, StringComparison.OrdinalIgnoreCase)
);
}
return options;
}
private async Task RollbackAsync(PageHandlerExecutingContext context)
{
var currentUow = _unitOfWorkManager.Current;
if (currentUow != null)
{
await currentUow.RollbackAsync(context.HttpContext.RequestAborted);
}
}
private static bool Succeed(PageHandlerExecutedContext result)
{
return result.Exception == null || result.ExceptionHandled;
}
}
}

6
framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingInterceptorRegistrar.cs

@ -1,6 +1,7 @@
using System;
using System.Linq;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
namespace Volo.Abp.Auditing
{
@ -16,6 +17,11 @@ namespace Volo.Abp.Auditing
private static bool ShouldIntercept(Type type)
{
if (DynamicProxyIgnoreTypes.Contains(type))
{
return false;
}
if (ShouldAuditTypeByDefault(type))
{
return true;

5
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AuthorizationInterceptorRegistrar.cs

@ -3,6 +3,7 @@ using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
namespace Volo.Abp.Authorization
{
@ -18,8 +19,8 @@ namespace Volo.Abp.Authorization
private static bool ShouldIntercept(Type type)
{
return type.IsDefined(typeof(AuthorizeAttribute), true) ||
AnyMethodHasAuthorizeAttribute(type);
return !DynamicProxyIgnoreTypes.Contains(type) &&
(type.IsDefined(typeof(AuthorizeAttribute), true) || AnyMethodHasAuthorizeAttribute(type));
}
private static bool AnyMethodHasAuthorizeAttribute(Type implementationType)

3
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs

@ -199,6 +199,8 @@ namespace Volo.Abp.Cli.Commands
sb.AppendLine("-o|--output-folder <output-folder> (default: current folder)");
sb.AppendLine("-v|--version <version> (default: latest version)");
sb.AppendLine("-ts|--template-source <template-source> (your local or network abp template source)");
sb.AppendLine("-csf|--create-solution-folder (default: true)");
sb.AppendLine("-cs|--connection-string <connection-string> (your database connection string)");
sb.AppendLine("--tiered (if supported by the template)");
sb.AppendLine("--no-ui (if supported by the template)");
sb.AppendLine("--separate-identity-server (if supported by the template)");
@ -217,6 +219,7 @@ namespace Volo.Abp.Cli.Commands
sb.AppendLine(" abp new Acme.BookStore -t module");
sb.AppendLine(" abp new Acme.BookStore -t module --no-ui");
sb.AppendLine(" abp new Acme.BookStore -ts \"D:\\localTemplate\\abp\"");
sb.AppendLine(" abp new Acme.BookStore -csf false");
sb.AppendLine(" abp new Acme.BookStore --local-framework-ref --abp-path \"D:\\github\\abp\"");
sb.AppendLine(" abp new Acme.BookStore --connection-string \"Server=myServerName\\myInstanceName;Database=myDatabase;User Id=myUsername;Password=myPassword\"");
sb.AppendLine("");

12
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ProjectReferenceReplaceStep.cs

@ -110,7 +110,7 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps
var oldNodeIncludeValue = oldNode.Attributes["Include"].Value;
// ReSharper disable once PossibleNullReferenceException : Can not be null because nodes are selected with include attribute filter in previous method
if (oldNodeIncludeValue.Contains(_projectName) && _entries.Any(e=>e.Name.EndsWith($"{oldNodeIncludeValue}.csproj")))
if (oldNodeIncludeValue.Contains(_projectName) && _entries.Any(e=>e.Name.EndsWith(GetProjectNameWithExtensionFromProjectReference(oldNodeIncludeValue))))
{
continue;
}
@ -125,6 +125,16 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps
return doc.OuterXml;
}
private string GetProjectNameWithExtensionFromProjectReference(string oldNodeIncludeValue)
{
if (string.IsNullOrWhiteSpace(oldNodeIncludeValue))
{
return oldNodeIncludeValue;
}
return oldNodeIncludeValue.Split('\\', '/').Last();
}
protected abstract XmlElement GetNewReferenceNode(XmlDocument doc, string oldNodeIncludeValue);

30
framework/src/Volo.Abp.Core/Volo/Abp/DynamicProxy/DynamicProxyIgnoreTypes.cs

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Volo.Abp.Threading;
namespace Volo.Abp.DynamicProxy
{
/// <summary>
/// Castle's dynamic proxy class feature will have performance issues for some components, such as the controller of Asp net core MVC.
/// For related discussions, see: https://github.com/castleproject/Core/issues/486 https://github.com/abpframework/abp/issues/3180
/// The Abp framework may enable interceptors for certain components (UOW, Auditing, Authorization, etc.), which requires dynamic proxy classes, but will cause application performance to decline.
/// We need to use other methods for the controller to implement interception, such as middleware or MVC / Page filters.
/// So we provide some ignored types to avoid enabling dynamic proxy classes.
/// By default it is empty. When you use middleware or filters for these components in your application, you can add these types to the list.
/// </summary>
public static class DynamicProxyIgnoreTypes
{
private static HashSet<Type> IgnoredTypes { get; } = new HashSet<Type>();
public static void Add<T>()
{
IgnoredTypes.Locking(() => IgnoredTypes.AddIfNotContains(typeof(T)));
}
public static bool Contains(Type type, bool includeDerivedTypes = true)
{
return includeDerivedTypes ? IgnoredTypes.Any(t => t.IsAssignableFrom(type)) : IgnoredTypes.Contains(type);
}
}
}

6
framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureInterceptorRegistrar.cs

@ -2,6 +2,7 @@
using System.Linq;
using System.Reflection;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
namespace Volo.Abp.Features
{
@ -17,8 +18,9 @@ namespace Volo.Abp.Features
private static bool ShouldIntercept(Type type)
{
return type.IsDefined(typeof(RequiresFeatureAttribute), true) ||
AnyMethodHasRequiresFeatureAttribute(type);
return !DynamicProxyIgnoreTypes.Contains(type) &&
(type.IsDefined(typeof(RequiresFeatureAttribute), true) ||
AnyMethodHasRequiresFeatureAttribute(type));
}
private static bool AnyMethodHasRequiresFeatureAttribute(Type implementationType)

2
framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtendedObjectMapper.cs → framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensibleObjectMapper.cs

@ -122,8 +122,6 @@ namespace Volo.Abp.ObjectExtending
}
}
//TODO: Move these methods to a class like ObjectExtensionHelper
public static bool CanMapProperty<TSource, TDestination>(
[NotNull] string propertyName,
MappingPropertyDefinitionChecks? definitionChecks = null,

2
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json

@ -45,7 +45,7 @@
"PagerInfo": "Showing _START_ to _END_ of _TOTAL_ entries",
"PagerInfoEmpty": "Showing 0 to 0 of 0 entries",
"PagerInfoFiltered": "(filtered from _MAX_ total entries)",
"NoDataAvailableInDatatable": "No data available in table",
"NoDataAvailableInDatatable": "No data available",
"PagerShowMenuEntries": "Show _MENU_ entries",
"DatatableActionDropdownDefaultText": "Actions",
"ChangePassword": "Change password",

11
framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkInterceptorRegistrar.cs

@ -1,5 +1,7 @@
using System.Reflection;
using System;
using System.Reflection;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
namespace Volo.Abp.Uow
{
@ -7,10 +9,15 @@ namespace Volo.Abp.Uow
{
public static void RegisterIfNeeded(IOnServiceRegistredContext context)
{
if (UnitOfWorkHelper.IsUnitOfWorkType(context.ImplementationType.GetTypeInfo()))
if (ShouldIntercept(context.ImplementationType))
{
context.Interceptors.TryAdd<UnitOfWorkInterceptor>();
}
}
private static bool ShouldIntercept(Type type)
{
return !DynamicProxyIgnoreTypes.Contains(type) && UnitOfWorkHelper.IsUnitOfWorkType(type.GetTypeInfo());
}
}
}

11
framework/src/Volo.Abp.Validation/Volo/Abp/Validation/ValidationInterceptorRegistrar.cs

@ -1,4 +1,6 @@
using Volo.Abp.DependencyInjection;
using System;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
namespace Volo.Abp.Validation
{
@ -6,10 +8,15 @@ namespace Volo.Abp.Validation
{
public static void RegisterIfNeeded(IOnServiceRegistredContext context)
{
if (typeof(IValidationEnabled).IsAssignableFrom(context.ImplementationType))
if (ShouldIntercept(context.ImplementationType))
{
context.Interceptors.TryAdd<ValidationInterceptor>();
}
}
private static bool ShouldIntercept(Type type)
{
return !DynamicProxyIgnoreTypes.Contains(type) && typeof(IValidationEnabled).IsAssignableFrom(type);
}
}
}

23
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj

@ -24,6 +24,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.Mvc.UI\Volo.Abp.AspNetCore.Mvc.UI.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.Mvc\Volo.Abp.AspNetCore.Mvc.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" />
<ProjectReference Include="..\Volo.Abp.AspNetCore.Tests\Volo.Abp.AspNetCore.Tests.csproj" />
@ -39,6 +40,28 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Content Update="Volo\Abp\AspNetCore\Mvc\Uow\UnitOfWorkTestPage.cshtml">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="Volo\Abp\AspNetCore\Mvc\Auditing\AuditTestPage.cshtml">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="Volo\Abp\AspNetCore\Mvc\Features\FeatureTestPage.cshtml">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="Volo\Abp\AspNetCore\Mvc\ExceptionHandling\ExceptionTestPage.cshtml">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="Volo\Abp\AspNetCore\Mvc\Authorization\AuthTestPage.cshtml">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<!-- https://github.com/NuGet/Home/issues/4412. -->
<Target Name="CopyDepsFiles" AfterTargets="Build" Condition="'$(TargetFramework)'!=''">

10
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs

@ -1,6 +1,9 @@
using System;
using System;
using System.Linq;
using Localization.Resources.AbpUi;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.Mvc.Authorization;
using Volo.Abp.AspNetCore.Mvc.Localization;
@ -73,6 +76,11 @@ namespace Volo.Abp.AspNetCore.Mvc
options.Languages.Add(new LanguageInfo("en", "en", "English"));
options.Languages.Add(new LanguageInfo("tr", "tr", "Türkçe"));
});
Configure<RazorPagesOptions>(options =>
{
options.RootDirectory = "/Volo/Abp/AspNetCore/Mvc";
});
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)

1
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Auditing/AuditTestController_Tests.cs

@ -51,6 +51,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Auditing
await _auditingStore.Received().SaveAsync(Arg.Any<AuditLogInfo>());
}
[Fact]
public async Task Should_Trigger_Middleware_And_AuditLog_Exception_When_Returns_Object()
{

19
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Auditing/AuditTestPage.cshtml

@ -0,0 +1,19 @@
@page
@model Volo.Abp.AspNetCore.Mvc.Auditing.AuditTestPage
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<div>
</div>
</body>
</html>

33
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Auditing/AuditTestPage.cshtml.cs

@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages;
using Volo.Abp.Auditing;
namespace Volo.Abp.AspNetCore.Mvc.Auditing
{
public class AuditTestPage : AbpPageModel
{
private readonly AbpAuditingOptions _options;
public AuditTestPage(IOptions<AbpAuditingOptions> options)
{
_options = options.Value;
}
public IActionResult OnGetAuditSuccessForGetRequests()
{
return new OkResult();
}
public IActionResult OnGetAuditFailForGetRequests()
{
throw new UserFriendlyException("Exception occurred!");
}
public ObjectResult OnGetAuditFailForGetRequestsReturningObject()
{
throw new UserFriendlyException("Exception occurred!");
}
}
}

65
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Auditing/AuditTestPage_Tests.cs

@ -0,0 +1,65 @@
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using NSubstitute;
using Volo.Abp.Auditing;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc.Auditing
{
public class AuditTestPage_Tests : AspNetCoreMvcTestBase
{
private readonly AbpAuditingOptions _options;
private IAuditingStore _auditingStore;
public AuditTestPage_Tests()
{
_options = ServiceProvider.GetRequiredService<IOptions<AbpAuditingOptions>>().Value;
_auditingStore = ServiceProvider.GetRequiredService<IAuditingStore>();
}
protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services)
{
_auditingStore = Substitute.For<IAuditingStore>();
services.Replace(ServiceDescriptor.Singleton(_auditingStore));
base.ConfigureServices(context, services);
}
[Fact]
public async Task Should_Trigger_Middleware_And_AuditLog_Success_For_GetRequests()
{
_options.IsEnabledForGetRequests = true;
_options.AlwaysLogOnException = false;
await GetResponseAsync("/Auditing/AuditTestPage?handler=AuditSuccessForGetRequests");
await _auditingStore.Received().SaveAsync(Arg.Any<AuditLogInfo>());
}
[Fact]
public async Task Should_Trigger_Middleware_And_AuditLog_Exception_Always()
{
_options.IsEnabled = true;
_options.AlwaysLogOnException = true;
try
{
await GetResponseAsync("/Auditing/AuditTestPage?handler=AuditFailForGetRequests", System.Net.HttpStatusCode.Forbidden);
}
catch { }
await _auditingStore.Received().SaveAsync(Arg.Any<AuditLogInfo>());
}
[Fact]
public async Task Should_Trigger_Middleware_And_AuditLog_Exception_When_Returns_Object()
{
_options.IsEnabled = true;
_options.AlwaysLogOnException = true;
await GetResponseAsync("/Auditing/AuditTestPage?handler=AuditFailForGetRequestsReturningObject", System.Net.HttpStatusCode.Forbidden);
await _auditingStore.Received().SaveAsync(Arg.Any<AuditLogInfo>());
}
}
}

19
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Authorization/AuthTestPage.cshtml

@ -0,0 +1,19 @@
@page
@model Volo.Abp.AspNetCore.Mvc.Authorization.AuthTestPage
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<div>
</div>
</body>
</html>

18
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Authorization/AuthTestPage.cshtml.cs

@ -0,0 +1,18 @@
using System;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages;
namespace Volo.Abp.AspNetCore.Mvc.Authorization
{
[Authorize]
public class AuthTestPage : AbpPageModel
{
public static Guid FakeUserId { get; } = Guid.NewGuid();
public ActionResult OnGet()
{
return Content("OK");
}
}
}

40
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Authorization/AuthTestPage_Tests.cs

@ -0,0 +1,40 @@
using System.Security.Claims;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.AspNetCore.TestBase;
using Volo.Abp.Autofac;
using Volo.Abp.MemoryDb;
using Volo.Abp.Modularity;
using Volo.Abp.Security.Claims;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc.Authorization
{
[DependsOn(
typeof(AbpAspNetCoreTestBaseModule),
typeof(AbpMemoryDbTestModule),
typeof(AbpAspNetCoreMvcModule),
typeof(AbpAutofacModule)
)]
public class AuthTestPage_Tests: AspNetCoreMvcTestBase
{
private readonly FakeUserClaims _fakeRequiredService;
public AuthTestPage_Tests()
{
_fakeRequiredService = GetRequiredService<FakeUserClaims>();
}
[Fact]
public async Task Should_Call_Simple_Authorized_Method_With_Authenticated_User()
{
_fakeRequiredService.Claims.AddRange(new[]
{
new Claim(AbpClaimTypes.UserId, AuthTestController.FakeUserId.ToString())
});
var result = await GetResponseAsStringAsync("/Authorization/AuthTestPage");
result.ShouldBe("OK");
}
}
}

4
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs

@ -30,9 +30,11 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
result.Error.ShouldNotBeNull();
result.Error.Message.ShouldBe("This is a sample exception!");
#pragma warning disable 4014
_fakeExceptionSubscriber
.Received()
.HandleAsync(Arg.Any<ExceptionNotificationContext>());
#pragma warning restore 4014
}
[Fact]
@ -44,9 +46,11 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
)
);
#pragma warning disable 4014
_fakeExceptionSubscriber
.DidNotReceive()
.HandleAsync(Arg.Any<ExceptionNotificationContext>());
#pragma warning restore 4014
}
}
}

19
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestPage.cshtml

@ -0,0 +1,19 @@
@page
@model Volo.Abp.AspNetCore.Mvc.ExceptionHandling.ExceptionTestPage
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<div>
</div>
</body>
</html>

18
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestPage.cshtml.cs

@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages;
namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
{
public class ExceptionTestPage : AbpPageModel
{
public void OnGetUserFriendlyException1()
{
throw new UserFriendlyException("This is a sample exception!");
}
public IActionResult OnGetUserFriendlyException2()
{
throw new UserFriendlyException("This is a sample exception!");
}
}
}

56
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestPage_Tests.cs

@ -0,0 +1,56 @@
using System.Net;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NSubstitute;
using Shouldly;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.Http;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
{
public class ExceptionTestPage_Tests : AspNetCoreMvcTestBase
{
private IExceptionSubscriber _fakeExceptionSubscriber;
protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services)
{
base.ConfigureServices(context, services);
_fakeExceptionSubscriber = Substitute.For<IExceptionSubscriber>();
services.AddSingleton(_fakeExceptionSubscriber);
}
[Fact]
public async Task Should_Return_RemoteServiceErrorResponse_For_UserFriendlyException_For_Void_Return_Value()
{
var result = await GetResponseAsObjectAsync<RemoteServiceErrorResponse>("/ExceptionHandling/ExceptionTestPage?handler=UserFriendlyException1", HttpStatusCode.Forbidden);
result.Error.ShouldNotBeNull();
result.Error.Message.ShouldBe("This is a sample exception!");
#pragma warning disable 4014
_fakeExceptionSubscriber
.Received()
.HandleAsync(Arg.Any<ExceptionNotificationContext>());
#pragma warning restore 4014
}
[Fact]
public async Task Should_Not_Handle_Exceptions_For_ActionResult_Return_Values()
{
await Assert.ThrowsAsync<UserFriendlyException>(
async () => await GetResponseAsObjectAsync<RemoteServiceErrorResponse>(
"/ExceptionHandling/ExceptionTestPage?handler=UserFriendlyException2"
)
);
#pragma warning disable 4014
_fakeExceptionSubscriber
.DidNotReceive()
.HandleAsync(Arg.Any<ExceptionNotificationContext>());
#pragma warning restore 4014
}
}
}

19
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Features/FeatureTestPage.cshtml

@ -0,0 +1,19 @@
@page
@model Volo.Abp.AspNetCore.Mvc.Features.FeatureTestPage
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<div>
</div>
</body>
</html>

27
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Features/FeatureTestPage.cshtml.cs

@ -0,0 +1,27 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages;
using Volo.Abp.Features;
namespace Volo.Abp.AspNetCore.Mvc.Features
{
public class FeatureTestPage : AbpPageModel
{
[RequiresFeature("AllowedFeature")]
public Task OnGetAllowedFeatureAsync()
{
return Task.CompletedTask;
}
[RequiresFeature("NotAllowedFeature")]
public void OnGetNotAllowedFeature()
{
}
public ObjectResult OnGetNoFeature()
{
return new ObjectResult(42);
}
}
}

34
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Features/FeatureTestPage_Tests.cs

@ -0,0 +1,34 @@
using System.Net;
using System.Threading.Tasks;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc.Features
{
public class FeatureTestPage_Tests : AspNetCoreMvcTestBase
{
[Fact]
public async Task Should_Allow_Enabled_Features()
{
await GetResponseAsStringAsync(
"/Features/FeatureTestPage?handler=AllowedFeature"
);
}
[Fact]
public async Task Should_Not_Allow_Not_Enabled_Features()
{
await GetResponseAsStringAsync(
"/Features/FeatureTestPage?handler=NotAllowedFeature",
HttpStatusCode.Unauthorized
);
}
[Fact]
public async Task Should_Allow_Actions_With_No_Feature()
{
await GetResponseAsStringAsync(
"/Features/FeatureTestPage?handler=NoFeature"
);
}
}
}

37
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkPageFilter_Exception_Rollback_Tests.cs

@ -0,0 +1,37 @@
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.Http;
using Volo.Abp.Json;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc.Uow
{
public class UnitOfWorkPageFilter_Exception_Rollback_Tests : AspNetCoreMvcTestBase
{
[Fact]
public async Task Should_Rollback_Transaction_For_Handled_Exceptions()
{
var result = await GetResponseAsObjectAsync<RemoteServiceErrorResponse>("/Uow/UnitOfWorkTestPage?handler=HandledException", HttpStatusCode.Forbidden);
result.Error.ShouldNotBeNull();
result.Error.Message.ShouldBe("This is a sample exception!");
}
[Fact]
public async Task Should_Gracefully_Handle_Exceptions_On_Complete()
{
var response = await GetResponseAsync("/Uow/UnitOfWorkTestPage?handler=ExceptionOnComplete", HttpStatusCode.Forbidden);
response.Headers.GetValues(AbpHttpConsts.AbpErrorFormat).FirstOrDefault().ShouldBe("true");
var resultAsString = await response.Content.ReadAsStringAsync();
var result = ServiceProvider.GetRequiredService<IJsonSerializer>().Deserialize<RemoteServiceErrorResponse>(resultAsString);
result.Error.ShouldNotBeNull();
result.Error.Message.ShouldBe(TestUnitOfWorkConfig.ExceptionOnCompleteMessage);
}
}
}

22
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkPageFilter_Tests.cs

@ -0,0 +1,22 @@
using System.Threading.Tasks;
using Shouldly;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc.Uow
{
public class UnitOfWorkPageFilter_Tests: AspNetCoreMvcTestBase
{
[Fact]
public async Task Get_Actions_Should_Not_Be_Transactional()
{
await GetResponseAsStringAsync("/Uow/UnitOfWorkTestPage?handler=RequiresUow");
}
[Fact]
public async Task Non_Get_Actions_Should_Be_Transactional()
{
var result = await Client.PostAsync("/Uow/UnitOfWorkTestPage?handler=RequiresUow", null);
result.IsSuccessStatusCode.ShouldBeTrue();
}
}
}

19
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkTestPage.cshtml

@ -0,0 +1,19 @@
@page
@model Volo.Abp.AspNetCore.Mvc.Uow.UnitOfWorkTestPage
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<div>
</div>
</body>
</html>

54
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkTestPage.cshtml.cs

@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Mvc;
using Shouldly;
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages;
using Volo.Abp.Uow;
namespace Volo.Abp.AspNetCore.Mvc.Uow
{
[IgnoreAntiforgeryToken]
public class UnitOfWorkTestPage : AbpPageModel
{
private readonly TestUnitOfWorkConfig _testUnitOfWorkConfig;
public UnitOfWorkTestPage(TestUnitOfWorkConfig testUnitOfWorkConfig)
{
_testUnitOfWorkConfig = testUnitOfWorkConfig;
}
public IActionResult OnGetRequiresUow()
{
CurrentUnitOfWork.ShouldNotBeNull();
CurrentUnitOfWork.Options.IsTransactional.ShouldBeFalse();
return Content("OK");
}
public IActionResult OnPostRequiresUow()
{
CurrentUnitOfWork.ShouldNotBeNull();
CurrentUnitOfWork.Options.IsTransactional.ShouldBeTrue();
return Content("OK");
}
[UnitOfWork(isTransactional: true)]
public void OnGetHandledException()
{
CurrentUnitOfWork.ShouldNotBeNull();
CurrentUnitOfWork.Options.IsTransactional.ShouldBeTrue();
throw new UserFriendlyException("This is a sample exception!");
}
public ObjectResult OnGetExceptionOnComplete()
{
CurrentUnitOfWork.ShouldNotBeNull();
CurrentUnitOfWork.Options.IsTransactional.ShouldBeFalse();
_testUnitOfWorkConfig.ThrowExceptionOnComplete = true;
//Prevent rendering of pages.
return new ObjectResult("");
}
}
}

4
framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/AbpAuthorizationTestModule.cs

@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Authorization.TestServices;
using Volo.Abp.Autofac;
using Volo.Abp.DynamicProxy;
using Volo.Abp.Modularity;
namespace Volo.Abp.Authorization
@ -13,7 +14,8 @@ namespace Volo.Abp.Authorization
{
context.Services.OnRegistred(onServiceRegistredContext =>
{
if (typeof(IMyAuthorizedService1).IsAssignableFrom(onServiceRegistredContext.ImplementationType))
if (typeof(IMyAuthorizedService1).IsAssignableFrom(onServiceRegistredContext.ImplementationType) &&
!DynamicProxyIgnoreTypes.Contains(onServiceRegistredContext.ImplementationType))
{
onServiceRegistredContext.Interceptors.TryAdd<AuthorizationInterceptor>();
}

4
framework/test/Volo.Abp.FluentValidation.Tests/Volo/Abp/FluentValidation/ApplicationService_FluentValidation_Tests.cs

@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.Autofac;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
using Volo.Abp.Modularity;
using Volo.Abp.Testing;
using Volo.Abp.Validation;
@ -108,7 +109,8 @@ namespace Volo.Abp.FluentValidation
{
context.Services.OnRegistred(onServiceRegistredContext =>
{
if (typeof(IMyAppService).IsAssignableFrom(onServiceRegistredContext.ImplementationType))
if (typeof(IMyAppService).IsAssignableFrom(onServiceRegistredContext.ImplementationType) &&
!DynamicProxyIgnoreTypes.Contains(onServiceRegistredContext.ImplementationType))
{
onServiceRegistredContext.Interceptors.TryAdd<ValidationInterceptor>();
}

4
framework/test/Volo.Abp.Validation.Tests/Volo/Abp/Validation/ApplicationService_Validation_Tests.cs

@ -8,6 +8,7 @@ using Shouldly;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Autofac;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
using Volo.Abp.Modularity;
using Volo.Abp.Testing;
using Xunit;
@ -198,7 +199,8 @@ namespace Volo.Abp.Validation
{
context.Services.OnRegistred(onServiceRegistredContext =>
{
if (typeof(IMyAppService).IsAssignableFrom(onServiceRegistredContext.ImplementationType))
if (typeof(IMyAppService).IsAssignableFrom(onServiceRegistredContext.ImplementationType) &&
!DynamicProxyIgnoreTypes.Contains(onServiceRegistredContext.ImplementationType))
{
onServiceRegistredContext.Interceptors.TryAdd<ValidationInterceptor>();
}

3
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs

@ -76,7 +76,6 @@ namespace Volo.Abp.Account.Web.Pages.Account
return Page();
}
[UnitOfWork] //TODO: Will be removed when we implement action filter
public virtual async Task<IActionResult> OnPostAsync(string action)
{
await CheckLocalLoginAsync();
@ -147,7 +146,6 @@ namespace Volo.Abp.Account.Web.Pages.Account
.ToList();
}
[UnitOfWork]
public virtual async Task<IActionResult> OnPostExternalLogin(string provider)
{
var redirectUrl = Url.Page("./Login", pageHandler: "ExternalLoginCallback", values: new { ReturnUrl, ReturnUrlHash });
@ -157,7 +155,6 @@ namespace Volo.Abp.Account.Web.Pages.Account
return await Task.FromResult(Challenge(properties, provider));
}
[UnitOfWork]
public virtual async Task<IActionResult> OnGetExternalLoginCallbackAsync(string returnUrl = "", string returnUrlHash = "", string remoteError = null)
{
//TODO: Did not implemented Identity Server 4 sample for this method (see ExternalLoginCallback in Quickstart of IDS4 sample)

1
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs

@ -36,7 +36,6 @@ namespace Volo.Abp.Account.Web.Pages.Account
await CheckSelfRegistrationAsync();
}
[UnitOfWork] //TODO: Will be removed when we implement action filter
public virtual async Task<IActionResult> OnPostAsync()
{
ValidateModel();

2
npm/lerna.json

@ -1,5 +1,5 @@
{
"version": "2.4.0",
"version": "2.5.0",
"packages": [
"packs/*"
],

4
npm/ng-packs/.vscode/settings.json

@ -18,7 +18,9 @@
"titleBar.inactiveForeground": "#e7e7e799",
"statusBar.background": "#1d70a2",
"statusBarItem.hoverBackground": "#258ecd",
"statusBar.foreground": "#e7e7e7"
"statusBar.foreground": "#e7e7e7",
"statusBar.border": "#1d70a2",
"titleBar.border": "#1d70a2"
},
"peacock.color": "#1D70A2"
}

2
npm/ng-packs/lerna.version.json

@ -1,5 +1,5 @@
{
"version": "2.4.0",
"version": "2.5.0",
"packages": [
"packages/*"
],

32
npm/ng-packs/package.json

@ -22,20 +22,20 @@
"generate:changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
},
"devDependencies": {
"@abp/ng.account": "~2.3.0",
"@abp/ng.account.config": "~2.3.0",
"@abp/ng.core": "^2.3.0",
"@abp/ng.feature-management": "^2.3.0",
"@abp/ng.identity": "~2.3.0",
"@abp/ng.identity.config": "~2.3.0",
"@abp/ng.permission-management": "^2.3.0",
"@abp/ng.setting-management": "~2.3.0",
"@abp/ng.setting-management.config": "~2.3.0",
"@abp/ng.tenant-management": "~2.3.0",
"@abp/ng.tenant-management.config": "~2.3.0",
"@abp/ng.theme.basic": "~2.3.0",
"@abp/ng.theme.shared": "^2.3.0",
"@abp/utils": "~2.3.0",
"@abp/ng.account": "~2.5.0",
"@abp/ng.account.config": "~2.5.0",
"@abp/ng.core": "~2.5.0",
"@abp/ng.feature-management": "~2.5.0",
"@abp/ng.identity": "~2.5.0",
"@abp/ng.identity.config": "~2.5.0",
"@abp/ng.permission-management": "~2.5.0",
"@abp/ng.setting-management": "~2.5.0",
"@abp/ng.setting-management.config": "~2.5.0",
"@abp/ng.tenant-management": "~2.5.0",
"@abp/ng.tenant-management.config": "~2.5.0",
"@abp/ng.theme.basic": "~2.5.0",
"@abp/ng.theme.shared": "~2.5.0",
"@abp/utils": "^2.4.0",
"@angular-builders/jest": "^8.2.0",
"@angular-devkit/build-angular": "~0.803.21",
"@angular-devkit/build-ng-packagr": "~0.803.21",
@ -47,7 +47,7 @@
"@angular/core": "~8.2.14",
"@angular/forms": "~8.2.14",
"@angular/language-service": "~8.2.14",
"@angular/localize": "~9.0.2",
"@angular/localize": "~9.1.0",
"@angular/platform-browser": "~8.2.14",
"@angular/platform-browser-dynamic": "~8.2.14",
"@angular/router": "~8.2.14",
@ -72,7 +72,7 @@
"jest": "^24.9.0",
"jest-canvas-mock": "^2.1.2",
"jest-preset-angular": "^7.1.1",
"just-clone": "3.1.0",
"just-clone": "^3.1.0",
"just-compare": "^1.3.0",
"lerna": "^3.19.0",
"ng-packagr": "^5.7.1",

2
npm/ng-packs/packages/account-config/package.json

@ -1,6 +1,6 @@
{
"name": "@abp/ng.account.config",
"version": "2.4.0",
"version": "2.5.0",
"homepage": "https://abp.io",
"repository": {
"type": "git",

6
npm/ng-packs/packages/account/package.json

@ -1,14 +1,14 @@
{
"name": "@abp/ng.account",
"version": "2.4.0",
"version": "2.5.0",
"homepage": "https://abp.io",
"repository": {
"type": "git",
"url": "https://github.com/abpframework/abp.git"
},
"dependencies": {
"@abp/ng.account.config": "^2.4.0",
"@abp/ng.theme.shared": "^2.4.0"
"@abp/ng.account.config": "~2.5.0",
"@abp/ng.theme.shared": "~2.5.0"
},
"publishConfig": {
"access": "public"

7
npm/ng-packs/packages/account/src/lib/account-routing.module.ts

@ -9,6 +9,7 @@ import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './components/login/login.component';
import { ManageProfileComponent } from './components/manage-profile/manage-profile.component';
import { RegisterComponent } from './components/register/register.component';
import { eAccountComponents } from './enums/components';
const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'login' },
@ -21,7 +22,7 @@ const routes: Routes = [
component: ReplaceableRouteContainerComponent,
data: {
replaceableComponent: {
key: 'Account.LoginComponent',
key: eAccountComponents.Login,
defaultComponent: LoginComponent,
} as ReplaceableComponents.RouteData<LoginComponent>,
},
@ -31,7 +32,7 @@ const routes: Routes = [
component: ReplaceableRouteContainerComponent,
data: {
replaceableComponent: {
key: 'Account.RegisterComponent',
key: eAccountComponents.Register,
defaultComponent: RegisterComponent,
} as ReplaceableComponents.RouteData<RegisterComponent>,
},
@ -42,7 +43,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
data: {
replaceableComponent: {
key: 'Account.ManageProfileComponent',
key: eAccountComponents.ManageProfile,
defaultComponent: ManageProfileComponent,
} as ReplaceableComponents.RouteData<ManageProfileComponent>,
},

4
npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.html

@ -1,9 +1,7 @@
<div class="row">
<div class="mx-auto col col-md-5">
<ng-container *ngIf="isMultiTenancyEnabled$ | async">
<abp-tenant-box
*abpReplaceableTemplate="{ componentKey: 'Account.TenantBoxComponent' }"
></abp-tenant-box>
<abp-tenant-box *abpReplaceableTemplate="{ componentKey: tenantBoxKey }"></abp-tenant-box>
</ng-container>
<div class="abp-account-container">

3
npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.ts

@ -3,6 +3,7 @@ import { Component, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core'
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { Account } from '../../models/account';
import { eAccountComponents } from '../../enums/components';
@Component({
selector: 'abp-auth-wrapper',
@ -26,6 +27,8 @@ export class AuthWrapperComponent
enableLocalLogin = true;
tenantBoxKey = eAccountComponents.TenantBox;
constructor(private store: Store) {}
ngOnInit() {

2
npm/ng-packs/packages/account/src/lib/components/login/login.component.html

@ -1,6 +1,6 @@
<abp-auth-wrapper
*abpReplaceableTemplate="{
componentKey: 'Account.AuthWrapperComponent',
componentKey: authWrapperKey,
inputs: {
mainContentRef: { value: mainContentRef },
cancelContentRef: { value: cancelContentRef }

3
npm/ng-packs/packages/account/src/lib/components/login/login.component.ts

@ -7,6 +7,7 @@ import { OAuthService } from 'angular-oauth2-oidc';
import { throwError } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import snq from 'snq';
import { eAccountComponents } from '../../enums/components';
const { maxLength, minLength, required } = Validators;
@ -21,6 +22,8 @@ export class LoginComponent implements OnInit {
isSelfRegistrationEnabled = true;
authWrapperKey = eAccountComponents.AuthWrapper;
constructor(
private fb: FormBuilder,
private oauthService: OAuthService,

4
npm/ng-packs/packages/account/src/lib/components/manage-profile/manage-profile.component.html

@ -33,7 +33,7 @@
<hr />
</h4>
<abp-change-password-form
*abpReplaceableTemplate="{ componentKey: 'Account.ChangePasswordComponent' }"
*abpReplaceableTemplate="{ componentKey: changePasswordKey }"
></abp-change-password-form>
</div>
</div>
@ -44,7 +44,7 @@
<hr />
</h4>
<abp-personal-settings-form
*abpReplaceableTemplate="{ componentKey: 'Account.PersonalSettingsComponent' }"
*abpReplaceableTemplate="{ componentKey: personalSettingsKey }"
></abp-personal-settings-form>
</div>
</div>

5
npm/ng-packs/packages/account/src/lib/components/manage-profile/manage-profile.component.ts

@ -1,6 +1,7 @@
import { fadeIn } from '@abp/ng.theme.shared';
import { transition, trigger, useAnimation } from '@angular/animations';
import { Component } from '@angular/core';
import { eAccountComponents } from '../../enums/components';
@Component({
selector: 'abp-manage-profile',
@ -9,4 +10,8 @@ import { Component } from '@angular/core';
})
export class ManageProfileComponent {
selectedTab = 0;
changePasswordKey = eAccountComponents.ChangePassword;
personalSettingsKey = eAccountComponents.PersonalSettings;
}

2
npm/ng-packs/packages/account/src/lib/components/register/register.component.html

@ -1,6 +1,6 @@
<abp-auth-wrapper
*abpReplaceableTemplate="{
componentKey: 'Account.AuthWrapperComponent',
componentKey: authWrapperKey,
inputs: {
mainContentRef: { value: mainContentRef }
}

3
npm/ng-packs/packages/account/src/lib/components/register/register.component.ts

@ -9,6 +9,7 @@ import { catchError, finalize, switchMap } from 'rxjs/operators';
import snq from 'snq';
import { RegisterRequest } from '../../models';
import { AccountService } from '../../services/account.service';
import { eAccountComponents } from '../../enums/components';
const { maxLength, required, email } = Validators;
@Component({
@ -22,6 +23,8 @@ export class RegisterComponent implements OnInit {
isSelfRegistrationEnabled = true;
authWrapperKey = eAccountComponents.AuthWrapper;
constructor(
private fb: FormBuilder,
private accountService: AccountService,

9
npm/ng-packs/packages/account/src/lib/enums/components.ts

@ -0,0 +1,9 @@
export const enum eAccountComponents {
Login = 'Account.LoginComponent',
Register = 'Account.RegisterComponent',
ManageProfile = 'Account.ManageProfileComponent',
TenantBox = 'Account.TenantBoxComponent',
AuthWrapper = 'Account.AuthWrapperComponent',
ChangePassword = 'Account.ChangePasswordComponent',
PersonalSettings = 'Account.PersonalSettingsComponent',
}

1
npm/ng-packs/packages/account/src/public-api.ts

@ -1,5 +1,6 @@
export * from './lib/account.module';
export * from './lib/components';
export * from './lib/enums/components';
export * from './lib/tokens';
export * from './lib/models';
export * from './lib/services';

2
npm/ng-packs/packages/core/package.json

@ -1,6 +1,6 @@
{
"name": "@abp/ng.core",
"version": "2.4.0",
"version": "2.5.0",
"homepage": "https://abp.io",
"repository": {
"type": "git",

1
npm/ng-packs/packages/core/src/lib/models/index.ts

@ -6,3 +6,4 @@ export * from './profile';
export * from './replaceable-components';
export * from './rest';
export * from './session';
export * from './utility';

4
npm/ng-packs/packages/core/src/lib/models/utility.ts

@ -0,0 +1,4 @@
import { TemplateRef, Type } from '@angular/core';
export type InferredInstanceOf<T> = T extends Type<infer U> ? U : never;
export type InferredContextOf<T> = T extends TemplateRef<infer U> ? U : never;

14
npm/ng-packs/packages/core/src/lib/services/content-projection.service.ts

@ -0,0 +1,14 @@
import { Injectable, Injector, TemplateRef, Type } from '@angular/core';
import { ProjectionStrategy } from '../strategies/projection.strategy';
@Injectable({ providedIn: 'root' })
export class ContentProjectionService {
constructor(private injector: Injector) {}
projectContent<T extends Type<any> | TemplateRef<any>>(
projectionStrategy: ProjectionStrategy<T>,
injector = this.injector,
) {
return projectionStrategy.injectContent(injector);
}
}

2
npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts

@ -4,7 +4,7 @@ import { generateHash } from '../utils';
@Injectable({ providedIn: 'root' })
export class DomInsertionService {
readonly inserted = new Set();
readonly inserted = new Set<number>();
insertContent(contentStrategy: ContentStrategy) {
const hash = generateHash(contentStrategy.content);

1
npm/ng-packs/packages/core/src/lib/services/index.ts

@ -1,6 +1,7 @@
export * from './application-configuration.service';
export * from './auth.service';
export * from './config-state.service';
export * from './content-projection.service';
export * from './dom-insertion.service';
export * from './lazy-load.service';
export * from './localization.service';

44
npm/ng-packs/packages/core/src/lib/strategies/container.strategy.ts

@ -0,0 +1,44 @@
import { ViewContainerRef } from '@angular/core';
export abstract class ContainerStrategy {
constructor(public containerRef: ViewContainerRef) {}
abstract getIndex(): number;
prepare(): void {}
}
export class ClearContainerStrategy extends ContainerStrategy {
getIndex(): number {
return 0;
}
prepare() {
this.containerRef.clear();
}
}
export class InsertIntoContainerStrategy extends ContainerStrategy {
constructor(containerRef: ViewContainerRef, private index: number) {
super(containerRef);
}
getIndex() {
return Math.min(Math.max(0, this.index), this.containerRef.length);
}
}
export const CONTAINER_STRATEGY = {
Clear(containerRef: ViewContainerRef) {
return new ClearContainerStrategy(containerRef);
},
Append(containerRef: ViewContainerRef) {
return new InsertIntoContainerStrategy(containerRef, containerRef.length);
},
Prepend(containerRef: ViewContainerRef) {
return new InsertIntoContainerStrategy(containerRef, 0);
},
Insert(containerRef: ViewContainerRef, index: number) {
return new InsertIntoContainerStrategy(containerRef, index);
},
};

47
npm/ng-packs/packages/core/src/lib/strategies/context.strategy.ts

@ -0,0 +1,47 @@
import { ComponentRef, TemplateRef, Type } from '@angular/core';
import { InferredContextOf, InferredInstanceOf } from '../models';
export abstract class ContextStrategy<T = any> {
constructor(public context: Partial<ContextType<T>>) {}
/* tslint:disable-next-line:no-unused-variable */
setContext(componentRef?: ComponentRef<InferredInstanceOf<T>>): Partial<ContextType<T>> {
return this.context;
}
}
export class NoContextStrategy<
T extends Type<any> | TemplateRef<any> = any
> extends ContextStrategy<T> {
constructor() {
super(undefined);
}
}
export class ComponentContextStrategy<T extends Type<any> = any> extends ContextStrategy<T> {
setContext(componentRef: ComponentRef<InferredInstanceOf<T>>): Partial<InferredInstanceOf<T>> {
Object.keys(this.context).forEach(key => (componentRef.instance[key] = this.context[key]));
componentRef.changeDetectorRef.detectChanges();
return this.context;
}
}
export class TemplateContextStrategy<T extends TemplateRef<any> = any> extends ContextStrategy<T> {
setContext(): Partial<InferredContextOf<T>> {
return this.context;
}
}
export const CONTEXT_STRATEGY = {
None<T extends Type<any> | TemplateRef<any> = any>() {
return new NoContextStrategy<T>();
},
Component<T extends Type<any> = any>(context: Partial<InferredInstanceOf<T>>) {
return new ComponentContextStrategy<T>(context);
},
Template<T extends TemplateRef<any> = any>(context: Partial<InferredContextOf<T>>) {
return new TemplateContextStrategy<T>(context);
},
};
type ContextType<T> = T extends Type<infer U> | TemplateRef<infer U> ? U : never;

3
npm/ng-packs/packages/core/src/lib/strategies/index.ts

@ -1,5 +1,8 @@
export * from './container.strategy';
export * from './content-security.strategy';
export * from './content.strategy';
export * from './context.strategy';
export * from './cross-origin.strategy';
export * from './dom.strategy';
export * from './loading.strategy';
export * from './projection.strategy';

176
npm/ng-packs/packages/core/src/lib/strategies/projection.strategy.ts

@ -0,0 +1,176 @@
import {
ApplicationRef,
ComponentFactoryResolver,
ComponentRef,
EmbeddedViewRef,
Injector,
TemplateRef,
Type,
ViewContainerRef,
} from '@angular/core';
import { InferredContextOf, InferredInstanceOf } from '../models/utility';
import { ContainerStrategy, CONTAINER_STRATEGY } from './container.strategy';
import { ContextStrategy, CONTEXT_STRATEGY } from './context.strategy';
import { DomStrategy, DOM_STRATEGY } from './dom.strategy';
export abstract class ProjectionStrategy<T = any> {
constructor(public content: T) {}
abstract injectContent(injector: Injector): ComponentRefOrEmbeddedViewRef<T>;
}
export class ComponentProjectionStrategy<T extends Type<any>> extends ProjectionStrategy<T> {
constructor(
component: T,
private containerStrategy: ContainerStrategy,
private contextStrategy: ContextStrategy = CONTEXT_STRATEGY.None(),
) {
super(component);
}
injectContent(injector: Injector) {
this.containerStrategy.prepare();
const resolver = injector.get(ComponentFactoryResolver) as ComponentFactoryResolver;
const factory = resolver.resolveComponentFactory<InferredInstanceOf<T>>(this.content);
const componentRef = this.containerStrategy.containerRef.createComponent(
factory,
this.containerStrategy.getIndex(),
injector,
);
this.contextStrategy.setContext(componentRef);
return componentRef as ComponentRefOrEmbeddedViewRef<T>;
}
}
export class RootComponentProjectionStrategy<T extends Type<any>> extends ProjectionStrategy<T> {
constructor(
component: T,
private contextStrategy: ContextStrategy = CONTEXT_STRATEGY.None(),
private domStrategy: DomStrategy = DOM_STRATEGY.AppendToBody(),
) {
super(component);
}
injectContent(injector: Injector) {
const appRef = injector.get(ApplicationRef);
const resolver = injector.get(ComponentFactoryResolver) as ComponentFactoryResolver;
const componentRef = resolver
.resolveComponentFactory<InferredInstanceOf<T>>(this.content)
.create(injector);
this.contextStrategy.setContext(componentRef);
appRef.attachView(componentRef.hostView);
const element: HTMLElement = (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0];
this.domStrategy.insertElement(element);
return componentRef as ComponentRefOrEmbeddedViewRef<T>;
}
}
export class TemplateProjectionStrategy<T extends TemplateRef<any>> extends ProjectionStrategy<T> {
constructor(
templateRef: T,
private containerStrategy: ContainerStrategy,
private contextStrategy = CONTEXT_STRATEGY.None(),
) {
super(templateRef);
}
injectContent() {
this.containerStrategy.prepare();
const embeddedViewRef = this.containerStrategy.containerRef.createEmbeddedView(
this.content,
this.contextStrategy.context,
this.containerStrategy.getIndex(),
);
embeddedViewRef.detectChanges();
return embeddedViewRef as ComponentRefOrEmbeddedViewRef<T>;
}
}
export const PROJECTION_STRATEGY = {
AppendComponentToBody<T extends Type<unknown>>(component: T, context?: InferredInstanceOf<T>) {
return new RootComponentProjectionStrategy<T>(
component,
context && CONTEXT_STRATEGY.Component(context),
);
},
AppendComponentToContainer<T extends Type<unknown>>(
component: T,
containerRef: ViewContainerRef,
context?: InferredInstanceOf<T>,
) {
return new ComponentProjectionStrategy<T>(
component,
CONTAINER_STRATEGY.Append(containerRef),
context && CONTEXT_STRATEGY.Component(context),
);
},
AppendTemplateToContainer<T extends TemplateRef<unknown>>(
templateRef: T,
containerRef: ViewContainerRef,
context?: InferredContextOf<T>,
) {
return new TemplateProjectionStrategy<T>(
templateRef,
CONTAINER_STRATEGY.Append(containerRef),
context && CONTEXT_STRATEGY.Template(context),
);
},
PrependComponentToContainer<T extends Type<unknown>>(
component: T,
containerRef: ViewContainerRef,
context?: InferredInstanceOf<T>,
) {
return new ComponentProjectionStrategy<T>(
component,
CONTAINER_STRATEGY.Prepend(containerRef),
context && CONTEXT_STRATEGY.Component(context),
);
},
PrependTemplateToContainer<T extends TemplateRef<unknown>>(
templateRef: T,
containerRef: ViewContainerRef,
context?: InferredContextOf<T>,
) {
return new TemplateProjectionStrategy<T>(
templateRef,
CONTAINER_STRATEGY.Prepend(containerRef),
context && CONTEXT_STRATEGY.Template(context),
);
},
ProjectComponentToContainer<T extends Type<unknown>>(
component: T,
containerRef: ViewContainerRef,
context?: InferredInstanceOf<T>,
) {
return new ComponentProjectionStrategy<T>(
component,
CONTAINER_STRATEGY.Clear(containerRef),
context && CONTEXT_STRATEGY.Component(context),
);
},
ProjectTemplateToContainer<T extends TemplateRef<unknown>>(
templateRef: T,
containerRef: ViewContainerRef,
context?: InferredContextOf<T>,
) {
return new TemplateProjectionStrategy<T>(
templateRef,
CONTAINER_STRATEGY.Clear(containerRef),
context && CONTEXT_STRATEGY.Template(context),
);
},
};
type ComponentRefOrEmbeddedViewRef<T> = T extends Type<infer U>
? ComponentRef<U>
: T extends TemplateRef<infer C>
? EmbeddedViewRef<C>
: never;

80
npm/ng-packs/packages/core/src/lib/tests/container.strategy.spec.ts

@ -0,0 +1,80 @@
import { ViewContainerRef } from '@angular/core';
import {
ClearContainerStrategy,
CONTAINER_STRATEGY,
InsertIntoContainerStrategy,
} from '../strategies';
describe('ClearContainerStrategy', () => {
const containerRef = ({
clear: jest.fn(),
length: 7,
} as any) as ViewContainerRef;
describe('#getIndex', () => {
it('should return 0', () => {
const strategy = new ClearContainerStrategy(containerRef);
expect(strategy.getIndex()).toBe(0);
});
});
describe('#prepare', () => {
it('should call clear method of containerRef once', () => {
const strategy = new ClearContainerStrategy(containerRef);
strategy.prepare();
expect(strategy.getIndex()).toBe(0);
expect(containerRef.clear).toHaveBeenCalledTimes(1);
});
});
});
describe('InsertIntoContainerStrategy', () => {
const containerRef = ({
clear: jest.fn(),
length: 7,
} as any) as ViewContainerRef;
describe('#getIndex', () => {
test.each`
index | expected
${0} | ${0}
${4} | ${4}
${9} | ${7}
${-1} | ${0}
${Infinity} | ${7}
`(
'should return $expected when index is given $index',
({ index, expected }: { index: number; expected: number }) => {
const strategy = new InsertIntoContainerStrategy(containerRef, index);
expect(strategy.getIndex()).toBe(expected);
},
);
});
describe('#prepare', () => {
it('should not call clear method of containerRef', () => {
const strategy = new InsertIntoContainerStrategy(containerRef, 0);
strategy.prepare();
expect(containerRef.clear).not.toHaveBeenCalled();
});
});
});
describe('CONTAINER_STRATEGY', () => {
const containerRef = ({
clear: jest.fn(),
length: 7,
} as any) as ViewContainerRef;
test.each`
name | Strategy | index
${'Clear'} | ${ClearContainerStrategy} | ${undefined}
${'Append'} | ${InsertIntoContainerStrategy} | ${containerRef.length}
${'Prepend'} | ${InsertIntoContainerStrategy} | ${0}
${'Insert'} | ${InsertIntoContainerStrategy} | ${4}
`('should successfully map $name to $Strategy.name', ({ name, Strategy, index }) => {
expect(CONTAINER_STRATEGY[name](containerRef, index)).toEqual(
new Strategy(containerRef, index),
);
});
});

38
npm/ng-packs/packages/core/src/lib/tests/content-projection.service.spec.ts

@ -0,0 +1,38 @@
import { Component, ComponentRef, NgModule } from '@angular/core';
import { createServiceFactory, SpectatorService } from '@ngneat/spectator';
import { ContentProjectionService } from '../services';
import { PROJECTION_STRATEGY } from '../strategies';
describe('ContentProjectionService', () => {
@Component({ template: '<div class="foo">bar</div>' })
class TestComponent {}
// createServiceFactory does not accept entryComponents directly
@NgModule({
declarations: [TestComponent],
entryComponents: [TestComponent],
})
class TestModule {}
let componentRef: ComponentRef<TestComponent>;
let spectator: SpectatorService<ContentProjectionService>;
const createService = createServiceFactory({
service: ContentProjectionService,
imports: [TestModule],
});
beforeEach(() => (spectator = createService()));
afterEach(() => componentRef.destroy());
describe('#projectContent', () => {
it('should call injectContent of given projectionStrategy and return what it returns', () => {
const strategy = PROJECTION_STRATEGY.AppendComponentToBody(TestComponent);
componentRef = spectator.service.projectContent(strategy);
const foo = document.querySelector('body > ng-component > div.foo');
expect(componentRef).toBeInstanceOf(ComponentRef);
expect(foo.textContent).toBe('bar');
});
});
});

79
npm/ng-packs/packages/core/src/lib/tests/context.strategy.spec.ts

@ -0,0 +1,79 @@
import { ComponentRef } from '@angular/core';
import {
ComponentContextStrategy,
CONTEXT_STRATEGY,
NoContextStrategy,
TemplateContextStrategy,
} from '../strategies';
import { uuid } from '../utils';
describe('ComponentContextStrategy', () => {
describe('#setContext', () => {
let componentRef: ComponentRef<any>;
beforeEach(
() =>
(componentRef = {
instance: {
x: '',
y: '',
z: '',
},
changeDetectorRef: {
detectChanges: jest.fn(),
},
} as any),
);
test.each`
props | values
${['x']} | ${[uuid()]}
${['x', 'y']} | ${[uuid(), uuid()]}
${['x', 'y', 'z']} | ${[uuid(), uuid(), uuid()]}
`(
'should set $props as $values and call detectChanges once',
({ props, values }: { props: string[]; values: string[] }) => {
const context = {};
props.forEach((prop, i) => {
context[prop] = values[i];
});
const strategy = new ComponentContextStrategy(context);
strategy.setContext(componentRef);
expect(props.every(prop => componentRef.instance[prop] === context[prop])).toBe(true);
expect(componentRef.changeDetectorRef.detectChanges).toHaveBeenCalledTimes(1);
},
);
});
});
describe('NoContextStrategy', () => {
describe('#setContext', () => {
it('should return undefined', () => {
const strategy = new NoContextStrategy();
expect(strategy.setContext(null)).toBeUndefined();
});
});
});
describe('TemplateContextStrategy', () => {
describe('#setContext', () => {
it('should return context', () => {
const context = { x: uuid() };
const strategy = new TemplateContextStrategy(context);
expect(strategy.setContext()).toEqual(context);
});
});
});
describe('CONTEXT_STRATEGY', () => {
test.each`
name | Strategy
${'Component'} | ${ComponentContextStrategy}
${'None'} | ${NoContextStrategy}
${'Template'} | ${TemplateContextStrategy}
`('should successfully map $name to $Strategy.name', ({ name, Strategy }) => {
expect(CONTEXT_STRATEGY[name](undefined)).toEqual(new Strategy(undefined));
});
});

36
npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts

@ -3,13 +3,43 @@ import { DomInsertionService } from '../services';
import { CONTENT_STRATEGY } from '../strategies';
describe('DomInsertionService', () => {
let styleElements: NodeListOf<HTMLStyleElement>;
let spectator: SpectatorService<DomInsertionService>;
const createService = createServiceFactory(DomInsertionService);
beforeEach(() => (spectator = createService()));
it('should be insert an element', () => {
spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead('.test {}'));
expect(spectator.service.inserted.has(1437348290)).toBe(true);
afterEach(() => styleElements.forEach(element => element.remove()));
describe('#insertContent', () => {
it('should be able to insert given content', () => {
spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead('.test {}'));
styleElements = document.head.querySelectorAll('style');
expect(styleElements.length).toBe(1);
expect(styleElements[0].textContent).toBe('.test {}');
});
it('should insert only once', () => {
expect(spectator.service.inserted.has(1437348290)).toBe(false);
spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead('.test {}'));
styleElements = document.head.querySelectorAll('style');
expect(styleElements.length).toBe(1);
expect(styleElements[0].textContent).toBe('.test {}');
expect(spectator.service.inserted.has(1437348290)).toBe(true);
spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead('.test {}'));
styleElements = document.head.querySelectorAll('style');
expect(styleElements.length).toBe(1);
expect(styleElements[0].textContent).toBe('.test {}');
expect(spectator.service.inserted.has(1437348290)).toBe(true);
});
it('should be able to insert given content', () => {
spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead('.test {}'));
expect(spectator.service.inserted.has(1437348290)).toBe(true);
});
});
});

276
npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts

@ -0,0 +1,276 @@
import {
Component,
ComponentRef,
EmbeddedViewRef,
TemplateRef,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import {
ComponentProjectionStrategy,
ContainerStrategy,
CONTAINER_STRATEGY,
CONTEXT_STRATEGY,
DOM_STRATEGY,
PROJECTION_STRATEGY,
RootComponentProjectionStrategy,
TemplateProjectionStrategy,
} from '../strategies';
describe('ComponentProjectionStrategy', () => {
@Component({
template: '<div class="foo">{{ bar || baz }}</div>',
})
class TestComponent {
bar: string;
baz = 'baz';
}
@Component({
template: '<ng-container #container></ng-container>',
})
class HostComponent {
@ViewChild('container', { static: true, read: ViewContainerRef })
containerRef: ViewContainerRef;
}
let containerStrategy: ContainerStrategy;
let spectator: Spectator<HostComponent>;
let componentRef: ComponentRef<TestComponent>;
const createComponent = createComponentFactory({
component: HostComponent,
entryComponents: [TestComponent],
});
beforeEach(() => {
spectator = createComponent({});
containerStrategy = CONTAINER_STRATEGY.Clear(spectator.component.containerRef);
});
afterEach(() => {
componentRef.destroy();
spectator.detectChanges();
});
describe('#injectContent', () => {
it('should should insert content into container and return a ComponentRef', () => {
const strategy = new ComponentProjectionStrategy(TestComponent, containerStrategy);
componentRef = strategy.injectContent(spectator);
spectator.detectChanges();
const div = spectator.query('div.foo');
expect(div.textContent).toBe('baz');
expect(componentRef).toBeInstanceOf(ComponentRef);
});
it('should be able to map context to projected component', () => {
const contextStrategy = CONTEXT_STRATEGY.Component({ bar: 'bar' });
const strategy = new ComponentProjectionStrategy(
TestComponent,
containerStrategy,
contextStrategy,
);
componentRef = strategy.injectContent(spectator);
spectator.detectChanges();
const div = spectator.query('div.foo');
expect(div.textContent).toBe('bar');
expect(componentRef.instance.bar).toBe('bar');
});
});
});
describe('RootComponentProjectionStrategy', () => {
@Component({
template: '<div class="foo">{{ bar || baz }}</div>',
})
class TestComponent {
bar: string;
baz = 'baz';
}
@Component({ template: '' })
class HostComponent {}
let spectator: Spectator<HostComponent>;
let componentRef: ComponentRef<TestComponent>;
const createComponent = createComponentFactory({
component: HostComponent,
entryComponents: [TestComponent],
});
beforeEach(() => {
spectator = createComponent({});
});
afterEach(() => {
componentRef.destroy();
spectator.detectChanges();
});
describe('#injectContent', () => {
it('should should insert content into body and return a ComponentRef', () => {
const strategy = new RootComponentProjectionStrategy(TestComponent);
componentRef = strategy.injectContent(spectator);
spectator.detectChanges();
const div = document.querySelector('body > ng-component > div.foo');
expect(div.textContent).toBe('baz');
expect(componentRef).toBeInstanceOf(ComponentRef);
componentRef.destroy();
spectator.detectChanges();
});
it('should be able to map context to projected component', () => {
const contextStrategy = CONTEXT_STRATEGY.Component({ bar: 'bar' });
const strategy = new RootComponentProjectionStrategy(TestComponent, contextStrategy);
componentRef = strategy.injectContent(spectator);
spectator.detectChanges();
const div = document.querySelector('body > ng-component > div.foo');
expect(div.textContent).toBe('bar');
expect(componentRef.instance.bar).toBe('bar');
});
});
});
describe('TemplateProjectionStrategy', () => {
@Component({
template: `
<ng-template #template let-bar>
<div class="foo">{{ bar || baz }}</div>
</ng-template>
<ng-container #container></ng-container>
`,
})
class HostComponent {
@ViewChild('container', { static: true, read: ViewContainerRef })
containerRef: ViewContainerRef;
@ViewChild('template', { static: true })
templateRef: TemplateRef<{ $implicit?: string }>;
baz = 'baz';
}
let containerStrategy: ContainerStrategy;
let spectator: Spectator<HostComponent>;
let embeddedViewRef: EmbeddedViewRef<{ $implicit?: string }>;
const createComponent = createComponentFactory({
component: HostComponent,
});
beforeEach(() => {
spectator = createComponent({});
containerStrategy = CONTAINER_STRATEGY.Clear(spectator.component.containerRef);
});
afterEach(() => {
embeddedViewRef.destroy();
spectator.detectChanges();
});
describe('#injectContent', () => {
it('should should insert content into container and return an EmbeddedViewRef', () => {
const templateRef = spectator.component.templateRef;
const strategy = new TemplateProjectionStrategy(templateRef, containerStrategy);
embeddedViewRef = strategy.injectContent();
spectator.detectChanges();
const div = spectator.query('div.foo');
expect(div.textContent).toBe('baz');
expect(embeddedViewRef).toHaveProperty('detectChanges');
expect(embeddedViewRef).toHaveProperty('markForCheck');
expect(embeddedViewRef).toHaveProperty('detach');
expect(embeddedViewRef).toHaveProperty('reattach');
expect(embeddedViewRef).toHaveProperty('destroy');
expect(embeddedViewRef).toHaveProperty('rootNodes');
expect(embeddedViewRef).toHaveProperty('context');
});
it('should be able to map context to projected template', () => {
const templateRef = spectator.component.templateRef;
const contextStrategy = CONTEXT_STRATEGY.Template<typeof templateRef>({ $implicit: 'bar' });
const strategy = new TemplateProjectionStrategy(
templateRef,
containerStrategy,
contextStrategy,
);
embeddedViewRef = strategy.injectContent();
spectator.detectChanges();
const div = spectator.query('div.foo');
expect(div.textContent).toBe('bar');
expect(embeddedViewRef.context).toEqual(contextStrategy.context);
});
});
});
describe('PROJECTION_STRATEGY', () => {
const content = undefined;
const containerRef = ({ length: 0 } as any) as ViewContainerRef;
let context: any;
test.each`
name | Strategy | containerStrategy
${'AppendComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Append}
${'AppendTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Append}
${'PrependComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Prepend}
${'PrependTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Prepend}
${'ProjectComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Clear}
${'ProjectTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Clear}
`(
'should successfully map $name to $Strategy.name with $containerStrategy.name container strategy and $contextStrategy.name context strategy',
({ name, Strategy, containerStrategy }) => {
expect(PROJECTION_STRATEGY[name](content, containerRef, context)).toEqual(
new Strategy(content, containerStrategy(containerRef), CONTEXT_STRATEGY.None()),
);
},
);
test.each`
name | Strategy | domStrategy
${'AppendComponentToBody'} | ${RootComponentProjectionStrategy} | ${DOM_STRATEGY.AppendToBody}
`(
'should successfully map $name to $Strategy.name with $domStrategy.name dom strategy',
({ name, Strategy, domStrategy }) => {
expect(PROJECTION_STRATEGY[name](content, context)).toEqual(
new Strategy(content, CONTEXT_STRATEGY.None(), domStrategy()),
);
},
);
test.each`
name | Strategy | containerStrategy | contextStrategy
${'AppendComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Append} | ${CONTEXT_STRATEGY.Component}
${'AppendTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Append} | ${CONTEXT_STRATEGY.Template}
${'PrependComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Prepend} | ${CONTEXT_STRATEGY.Component}
${'PrependTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Prepend} | ${CONTEXT_STRATEGY.Template}
${'ProjectComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Clear} | ${CONTEXT_STRATEGY.Component}
${'ProjectTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Clear} | ${CONTEXT_STRATEGY.Template}
`(
'should successfully map $name to $Strategy.name with $containerStrategy.name container strategy and $contextStrategy.name context strategy',
({ name, Strategy, containerStrategy, contextStrategy }) => {
context = { x: true };
expect(PROJECTION_STRATEGY[name](content, containerRef, context)).toEqual(
new Strategy(content, containerStrategy(containerRef), contextStrategy(context)),
);
},
);
test.each`
name | Strategy | contextStrategy | domStrategy
${'AppendComponentToBody'} | ${RootComponentProjectionStrategy} | ${CONTEXT_STRATEGY.Component} | ${DOM_STRATEGY.AppendToBody}
`(
'should successfully map $name to $Strategy.name with $contextStrategy.name context strategy and $domStrategy.name dom strategy',
({ name, Strategy, domStrategy, contextStrategy }) => {
context = { x: true };
expect(PROJECTION_STRATEGY[name](content, context)).toEqual(
new Strategy(content, contextStrategy(context), domStrategy()),
);
},
);
});

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save