Browse Source

Merge remote-tracking branch 'abpframework/dev' into docs

pull/3498/head
liangshiwei 7 years ago
parent
commit
047e645199
  1. 1
      build/common.ps1
  2. 1
      common.DotSettings
  3. 2
      common.props
  4. 4
      docs/en/API/Dynamic-CSharp-API-Clients.md
  5. 51
      docs/en/Application-Services.md
  6. 30
      docs/en/Authorization.md
  7. 43
      docs/en/Background-Jobs-Hangfire.md
  8. 4
      docs/en/Best-Practices/Entity-Framework-Core-Integration.md
  9. 4
      docs/en/Best-Practices/MongoDB-Integration.md
  10. 1
      docs/en/CLI.md
  11. 201
      docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md
  12. 113
      docs/en/How-To/Customize-Login-Page-MVC.md
  13. 101
      docs/en/How-To/Customize-SignIn-Manager.md
  14. 9
      docs/en/How-To/Index.md
  15. 2
      docs/en/Modules/Docs.md
  16. 177
      docs/en/Startup-Templates/Application.md
  17. 74
      docs/en/UI/Angular/Content-Security-Strategy.md
  18. 95
      docs/en/UI/Angular/Content-Strategy.md
  19. 60
      docs/en/UI/Angular/Cross-Origin-Strategy.md
  20. 2
      docs/en/UI/Angular/Custom-Setting-Page.md
  21. 91
      docs/en/UI/Angular/Dom-Insertion-Service.md
  22. 89
      docs/en/UI/Angular/Dom-Strategy.md
  23. 213
      docs/en/UI/Angular/Lazy-Load-Service.md
  24. 110
      docs/en/UI/Angular/Loading-Strategy.md
  25. 4
      docs/en/UI/Angular/Track-By-Service.md
  26. 4
      docs/en/UI/AspNetCore/Bundling-Minification.md
  27. 42
      docs/en/UI/AspNetCore/Tag-Helpers/Popovers.md
  28. 326
      docs/en/UI/Common/Utils/Linked-List.md
  29. 21
      docs/en/docs-nav.json
  30. BIN
      docs/en/images/angular-folder-structure.png
  31. BIN
      docs/en/images/angular-template-structure-diagram.png
  32. BIN
      docs/en/images/react-native-folder-structure.png
  33. BIN
      docs/en/images/react-native-navigation-structure.png
  34. BIN
      docs/en/images/react-native-store-folder.png
  35. 4
      docs/zh-Hans/API/Dynamic-CSharp-API-Clients.md
  36. 47
      docs/zh-Hans/Application-Services.md
  37. 4
      docs/zh-Hans/Authorization.md
  38. 4
      docs/zh-Hans/Best-Practices/Entity-Framework-Core-Integration.md
  39. 6
      docs/zh-Hans/Best-Practices/MongoDB-Integration.md
  40. 12
      docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md
  41. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Popover/AbpPopoverTagHelper.cs
  42. 60
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Popover/AbpPopoverTagHelperService.cs
  43. 5
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperResourceService.cs
  44. 11
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/TuiEditor/TuiEditorScriptContributor.cs
  45. 5
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Empty.cshtml
  46. 38
      framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingHelper.cs
  47. 4
      framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditingHelper.cs
  48. 25
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IPermissionDefinitionContext.cs
  49. 4
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IPermissionDefinitionProvider.cs
  50. 9
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionChecker.cs
  51. 23
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs
  52. 41
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs
  53. 34
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionContextExtensions.cs
  54. 26
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionManager.cs
  55. 10
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionProvider.cs
  56. 39
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionGroupDefinition.cs
  57. 9
      framework/src/Volo.Abp.AutoMapper/AutoMapper/AbpAutoMapperExtensibleDtoExtensions.cs
  58. 22
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs
  59. 76
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ConnectionStringChangeStep.cs
  60. 3
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ProjectReferenceReplaceStep.cs
  61. 9
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/TemplateProjectBuildPipelineBuilder.cs
  62. 7
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/ProjectBuildArgs.cs
  63. 10
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/AngularModuleSourceCodeAdder.cs
  64. 23
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs
  65. 5
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NugetPackageToLocalReferenceConverter.cs
  66. 29
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs
  67. 5
      framework/src/Volo.Abp.Core/Microsoft/Extensions/Configuration/ConfigurationHelper.cs
  68. 30
      framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifier.cs
  69. 19
      framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/NullExceptionNotifier.cs
  70. 61
      framework/src/Volo.Abp.Core/Volo/Abp/IO/FileHelper.cs
  71. 12
      framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs
  72. 37
      framework/src/Volo.Abp.Core/Volo/Abp/Text/StringHelper.cs
  73. 36
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs
  74. 15
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs
  75. 22
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/UrlBuilder.cs
  76. 10
      framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelAuthenticationService.cs
  77. 5
      framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs
  78. 237
      framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtendedObjectMapper.cs
  79. 160
      framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/HasExtraPropertiesObjectExtendingExtensions.cs
  80. 15
      framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionManager.cs
  81. 41
      framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionManagerExtensions.cs
  82. 14
      framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs
  83. 4
      framework/src/Volo.Abp.Quartz/Volo/Abp/Quartz/AbpQuartzModule.cs
  84. 6
      framework/src/Volo.Abp.Threading/Volo/Abp/Threading/AbpTimer.cs
  85. 31
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Popovers.cshtml
  86. 11
      framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/TestServices/AuthorizationTestPermissionDefinitionProvider.cs
  87. 6
      framework/test/Volo.Abp.AutoMapper.Tests/AutoMapper/AbpAutoMapperExtensibleDtoExtensions_Tests.cs
  88. 8
      framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/ObjectMapperExtensions_Tests.cs
  89. 2
      framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyMapProfile.cs
  90. 5
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/IRegularTestController.cs
  91. 22
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestController.cs
  92. 46
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestControllerClientProxy_Tests.cs
  93. 5
      framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/AbpObjectExtendingTestModule.cs
  94. 29
      framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/HasExtraPropertiesObjectExtendingExtensions_Tests.cs
  95. 40
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/SoftDelete_Tests.cs
  96. 19
      modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs
  97. 31
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs
  98. 25
      modules/audit-logging/src/Volo.Abp.AuditLogging.Domain/Volo/Abp/AuditLogging/IAuditLogRepository.cs
  99. 12
      modules/audit-logging/src/Volo.Abp.AuditLogging.EntityFrameworkCore/Volo/Abp/AuditLogging/AbpAuditLoggingEfCoreQueryableExtensions.cs
  100. 63
      modules/audit-logging/src/Volo.Abp.AuditLogging.EntityFrameworkCore/Volo/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogRepository.cs

1
build/common.ps1

@ -28,6 +28,7 @@ $solutionPaths = (
"../samples/BookStore-Modular/modules/book-management",
"../samples/BookStore-Modular/application",
"../samples/DashboardDemo",
"../samples/EfCoreMigrationDemo",
"../samples/MicroserviceDemo",
"../samples/RabbitMqEventBus",
"../abp_io/AbpIoLocalization"

1
common.DotSettings

@ -20,4 +20,5 @@
<s:String x:Key="/Default/CodeStyle/Generate/=Overrides/Options/=Async/@EntryIndexedValue">False</s:String>
<s:String x:Key="/Default/CodeStyle/Generate/=Overrides/Options/=Mutable/@EntryIndexedValue">False</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SQL/@EntryIndexedValue">SQL</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Volo/@EntryIndexedValue">True</s:Boolean>
</wpf:ResourceDictionary>

2
common.props

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

4
docs/en/API/Dynamic-CSharp-API-Clients.md

@ -69,7 +69,7 @@ public class MyClientAppModule : AbpModule
}
````
See the "RemoteServiceOptions" section below for more detailed configuration.
See the "AbpRemoteServiceOptions" section below for more detailed configuration.
## Usage
@ -104,7 +104,7 @@ While you can inject `IBookAppService` like above to use the client proxy, you c
## Configuration
### RemoteServiceOptions
### AbpRemoteServiceOptions
`AbpRemoteServiceOptions` is automatically set from the `appsettings.json` by default. Alternatively, you can use `Configure` method to set or override it. Example:

51
docs/en/Application-Services.md

@ -132,6 +132,8 @@ The `CreateAsync` method above manually creates a `Book` entity from given `Crea
However, in many cases, it's very practical to use **auto object mapping** to set properties of an object from a similar object. ABP provides an [object to object mapping](Object-To-Object-Mapping.md) infrastructure to make this even easier.
Object to object mapping provides abstractions and it is implemented by the [AutoMapper](https://automapper.org/) library by default.
Let's create another method to get a book. First, define the method in the `IBookAppService` interface:
````csharp
@ -146,7 +148,6 @@ public interface IBookAppService : IApplicationService
`BookDto` is a simple [DTO](Data-Transfer-Objects.md) class defined as below:
````csharp
[AbpAutoMapFrom(typeof(Book))] //Defines the mapping
public class BookDto
{
public Guid Id { get; set; }
@ -159,21 +160,38 @@ public class BookDto
}
````
* `BookDto` defines `[AbpAutoMapFrom(typeof(Book))]` attribute to create the object mapping from `Book` to `BookDto`.
AutoMapper requires to create a mapping [profile class](https://docs.automapper.org/en/stable/Configuration.html#profile-instances). Example:
Then you can implement the `GetAsync` method as shown below:
````csharp
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<Book, BookDto>();
}
}
````
You should then register profiles using the `AbpAutoMapperOptions`:
````csharp
public async Task<BookDto> GetAsync(Guid id)
[DependsOn(typeof(AbpAutoMapperModule))]
public class MyModule : AbpModule
{
var book = await _bookRepository.GetAsync(id);
return book.MapTo<BookDto>();
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpAutoMapperOptions>(options =>
{
//Add all mappings defined in the assembly of the MyModule class
options.AddMaps<MyModule>();
});
}
}
````
`MapTo` extension method converts `Book` object to `BookDto` object by copying all properties with the same naming.
`AddMaps` registers all profile classes defined in the assembly of the given class, typically your module class. It also registers for the [attribute mapping](https://docs.automapper.org/en/stable/Attribute-mapping.html).
An alternative to the `MapTo` is using the `IObjectMapper` service:
Then you can implement the `GetAsync` method as shown below:
````csharp
public async Task<BookDto> GetAsync(Guid id)
@ -183,8 +201,6 @@ public async Task<BookDto> GetAsync(Guid id)
}
````
While the second syntax is a bit harder to write, it better works if you write unit tests.
See the [object to object mapping document](Object-To-Object-Mapping.md) for more.
## Validation
@ -250,7 +266,6 @@ public interface ICrudAppService<
DTO classes used in this example are `BookDto` and `CreateUpdateBookDto`:
````csharp
[AbpAutoMapFrom(typeof(Book))]
public class BookDto : AuditedEntityDto<Guid>
{
public string Name { get; set; }
@ -260,7 +275,6 @@ public class BookDto : AuditedEntityDto<Guid>
public float Price { get; set; }
}
[AbpAutoMapTo(typeof(Book))]
public class CreateUpdateBookDto
{
[Required]
@ -275,6 +289,19 @@ public class CreateUpdateBookDto
}
````
[Profile](https://docs.automapper.org/en/stable/Configuration.html#profile-instances) class of DTO class.
```csharp
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<Book, BookDto>();
CreateMap<CreateUpdateBookDto, Book>();
}
}
```
* `CreateUpdateBookDto` is shared by create and update operations, but you could use separated DTO classes as well.
And finally, the `BookAppService` implementation is very simple:

30
docs/en/Authorization.md

@ -142,6 +142,20 @@ myGroup.AddPermission(
);
```
#### Enable/Disable Permissions
A permission is enabled by default. It is possible to disable a permission. A disabled permission will be prohibited for everyone. You can still check for the permission, but it will always return prohibited.
Example definition:
````csharp
myGroup.AddPermission("Author_Management", isEnabled: false);
````
You normally don't need to define a disabled permission (unless you temporary want disable a feature of your application). However, you may want to disable a permission defined in a depended module. In this way you can disable the related application functionality. See the "*Changing Permission Definitions of a Depended Module*" section below for an example usage.
> Note: Checking an undefined permission will throw an exception while a disabled permission check simply returns prohibited (false).
#### Child Permissions
A permission may have child permissions. It is especially useful when you want to create a hierarchical permission tree where a permission may have additional sub permissions which are available only if the parent permission has been granted.
@ -208,6 +222,18 @@ See [policy based authorization](https://docs.microsoft.com/en-us/aspnet/core/se
A class deriving from the `PermissionDefinitionProvider` (just like the example above) can also get existing permission definitions (defined by the depended [modules](Module-Development-Basics.md)) and change their definitions.
Example:
````csharp
context
.GetPermissionOrNull(IdentityPermissions.Roles.Delete)
.IsEnabled = false;
````
When you write this code inside your permission definition provider, it finds the "role deletion" permission of the [Identity Module](Modules/Identity.md) and disabled the permission, so no one can delete a role on the application.
> Tip: It is better to check the value returned by the `GetPermissionOrNull` method since it may return null if the given permission was not defined.
## IAuthorizationService
ASP.NET Core provides the `IAuthorizationService` that can be used to check for authorization. Once you inject, you can use it in your code to conditionally control the authorization.
@ -339,10 +365,10 @@ A permission value provider should return one of the following values from the `
- `PermissionGrantResult.Prohibited` is returned to prohibit the user for the permission. If any of the providers return `Prohibited`, the result will always be `Prohibited`. Doesn't matter what other providers return.
- `PermissionGrantResult.Undefined` is returned if this value provider could not decide about the permission value. Return this to let other providers check the permission.
Once a provider is defined, it should be added to the `PermissionOptions` as shown below:
Once a provider is defined, it should be added to the `AbpPermissionOptions` as shown below:
```csharp
Configure<PermissionOptions>(options =>
Configure<AbpPermissionOptions>(options =>
{
options.ValueProviders.Add<SystemAdminPermissionValueProvider>();
});

43
docs/en/Background-Jobs-Hangfire.md

@ -39,5 +39,46 @@ public class YourModule : AbpModule
````
## Configuration
You can install any storage for Hangfire. The most common one is SQL Server (see the [Hangfire.SqlServer](https://www.nuget.org/packages/Hangfire.SqlServer) NuGet package).
TODO...
After you have installed these NuGet packages, you need to configure your project to use Hangfire.
1.First, we change the `Module` class (example: `<YourProjectName>HttpApiHostModule`) to add Hangfire configuration of the storage and connection string in the `ConfigureServices` method:
````csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
var hostingEnvironment = context.Services.GetHostingEnvironment();
//... other configarations
ConfigureHangfire(context, configuration);
}
private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.AddHangfire(config =>
{
config.UseSqlServerStorage(configuration.GetConnectionString("Default"));
});
}
````
2. We need to add `UseHangfireServer` call in the `OnApplicationInitialization` method in `Module` class
If you want to use hangfire's dashboard, you can add it, too: by `UseHangfireDashboard`
````csharp
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
// ... others
app.UseHangfireServer();
app.UseHangfireDashboard();
}
````

4
docs/en/Best-Practices/Entity-Framework-Core-Integration.md

@ -107,10 +107,10 @@ public static class IdentityDbContextModelBuilderExtensions
````
* **Do** call `b.ConfigureByConvention();` for each entity mapping (as shown above).
* **Do** create a **configuration options** class by inheriting from the `ModelBuilderConfigurationOptions`. Example:
* **Do** create a **configuration options** class by inheriting from the `AbpModelBuilderConfigurationOptions`. Example:
````C#
public class IdentityModelBuilderConfigurationOptions : ModelBuilderConfigurationOptions
public class IdentityModelBuilderConfigurationOptions : AbpModelBuilderConfigurationOptions
{
public IdentityModelBuilderConfigurationOptions()
: base(AbpIdentityConsts.DefaultDbTablePrefix, AbpIdentityConsts.DefaultDbSchema)

4
docs/en/Best-Practices/MongoDB-Integration.md

@ -90,11 +90,11 @@ public static class AbpIdentityMongoDbContextExtensions
}
```
- **Do** create a **configuration options** class by inheriting from the `MongoModelBuilderConfigurationOptions`. Example:
- **Do** create a **configuration options** class by inheriting from the `AbpMongoModelBuilderConfigurationOptions`. Example:
```c#
public class IdentityMongoModelBuilderConfigurationOptions
: MongoModelBuilderConfigurationOptions
: AbpMongoModelBuilderConfigurationOptions
{
public IdentityMongoModelBuilderConfigurationOptions()
: base(AbpIdentityConsts.DefaultDbTablePrefix)

1
docs/en/CLI.md

@ -60,6 +60,7 @@ abp new Acme.BookStore
* `--version` or `-v`: Specifies the ABP & template version. It can be a [release tag](https://github.com/abpframework/abp/releases) or a [branch name](https://github.com/abpframework/abp/branches). Uses the latest release if not specified. Most of the times, you will want to use the latest version.
* `--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!
### add-package

201
docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md

@ -0,0 +1,201 @@
# How to Use the Azure Active Directory Authentication for MVC / Razor Page Applications
This guide demonstrates how to integrate AzureAD to an ABP application that enables users to sign in using OAuth 2.0 with credentials from **Azure Active Directory**.
Adding Azure Active Directory is pretty straightforward in ABP framework. Couple of configurations needs to be done correctly.
Two different **alternative approaches** for AzureAD integration will be demonstrated for better coverage.
1. **AddAzureAD**: This approach uses Microsoft [AzureAD UI nuget package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/) which is very popular when users search the web about how to integrate AzureAD to their web application.
2. **AddOpenIdConnect**: This approach uses default [OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/) which can be used for not only AzureAD but for all OpenId connections.
> There is **no difference** in functionality between these approaches. AddAzureAD is an abstracted way of OpenIdConnection ([source](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADAuthenticationBuilderExtensions.cs#L122)) with predefined cookie settings.
>
> However there are key differences in integration to ABP applications because of default configurated signin schemes which will be explained below.
## 1. AddAzureAD
This approach uses the most common way to integrate AzureAD by using the [Microsoft AzureAD UI nuget package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/).
If you choose this approach, you will need to install `Microsoft.AspNetCore.Authentication.AzureAD.UI` package to your **.Web** project. Also, since AddAzureAD extension uses [configuration binding](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#default-configuration), you need to update your appsettings.json file located in your **.Web** project.
#### **Updating `appsettings.json`**
You need to add a new section to your `appsettings.json` which will be binded to configuration when configuring the `OpenIdConnectOptions`:
````json
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "<your-tenant-id>",
"ClientId": "<your-client-id>",
"Domain": "domain.onmicrosoft.com",
"CallbackPath": "/signin-azuread-oidc"
}
````
> Important configuration here is the CallbackPath. This value must be the same with one of your Azure AD-> app registrations-> Authentication -> RedirectUri.
Then, you need to configure the `OpenIdConnectOptions` to complete the integration.
#### Configuring OpenIdConnectOptions
In your **.Web** project, locate your **ApplicationWebModule** and modify `ConfigureAuthentication` method with the following:
````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");
});
}
````
> **Don't forget to:**
>
> * Add `.AddAzureAD(options => configuration.Bind("AzureAd", options))` after `.AddAuthentication()`. This binds your AzureAD appsettings and easy to miss out.
> * Add `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear()`. This will disable the default Microsoft claim type mapping.
> * Add `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier)`. Mapping this to [ClaimTypes.NameIdentifier](https://github.com/dotnet/runtime/blob/6d395de48ac718a913e567ae80961050f2a9a4fa/src/libraries/System.Security.Claims/src/System/Security/Claims/ClaimTypes.cs#L59) is important since default SignIn Manager behavior uses this claim type for external login information.
> * Add `options.SignInScheme = IdentityConstants.ExternalScheme` since [default signin scheme is `AzureADOpenID`](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADOpenIdConnectOptionsConfiguration.cs#L35).
> * Add `options.Scope.Add("email")` if you are using **v2.0** endpoint of AzureAD since v2.0 endpoint doesn't return the `email` claim as default. The [Account Module](../Modules/Account.md) uses `email` claim to [register external users](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L215).
You are done and integration is completed.
## 2. Alternative Approach: AddOpenIdConnect
If you don't want to use an extra nuget package in your application, you can use the straight default [OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/) which can be used for all OpenId connections including AzureAD external authentication.
You don't have to use `appsettings.json` configuration but it is a good practice to set AzureAD information in the `appsettings.json`.
To get the AzureAD information from `appsettings.json`, which will be used in `OpenIdConnectOptions` configuration, simply add a new section to `appsettings.json` located in your **.Web** project:
````json
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "<your-tenant-id>",
"ClientId": "<your-client-id>",
"Domain": "domain.onmicrosoft.com",
"CallbackPath": "/signin-azuread-oidc"
}
````
Then, In your **.Web** project; you can modify the `ConfigureAuthentication` method located in your **ApplicationWebModule** with the following:
````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");
});
}
````
And that's it, integration is completed. Keep on mind that you can connect any other external authentication providers.
## The Source Code
You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization).
# FAQ
* Help! `GetExternalLoginInfoAsync` returns `null`!
* There can be 2 reasons for this;
1. You are trying to authenticate against wrong scheme. Check if you set **SignInScheme** to `IdentityConstants.ExternalScheme`:
````csharp
options.SignInScheme = IdentityConstants.ExternalScheme;
````
2. Your `ClaimTypes.NameIdentifier` is `null`. Check if you added claim mapping:
````csharp
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier);
````
* Help! I keep getting ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** error!
* If you set your **CallbackPath** in appsettings as:
````csharp
"AzureAd": {
...
"CallbackPath": "/signin-azuread-oidc"
}
````
your **Redirect URI** of your application in azure portal must be with <u>domain</u> like `https://localhost:44320/signin-azuread-oidc`, not only `/signin-azuread-oidc`.
* Help! I am getting ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** error!
* This occurs when you use Azure Authority **v2.0 endpoint** without requesting `email` scope. [Abp checks unique email to create user](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). Simply add
````csharp
options.Scope.Add("email");
````
to your openid configuration.
* How can I **debug/watch** which claims I get before they get mapped?
* You can add a simple event under openid configuration to debug before mapping like:
````csharp
options.Events.OnTokenValidated = (async context =>
{
var claimsFromOidcProvider = context.Principal.Claims.ToList();
await Task.CompletedTask;
});
````
## See Also
* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md).
* [How to Customize the SignIn Manager for ABP Applications](Customize-SignIn-Manager.md).

113
docs/en/How-To/Customize-Login-Page-MVC.md

@ -0,0 +1,113 @@
# How to Customize the Login Page for MVC / Razor Page Applications
When you create a new application using the [application startup template](../Startup-Templates/Application.md), source code of the login page will not be inside your solution, so you can not directly change it. The login page comes from the [Account Module](../Modules/Account.md) that is used a [NuGet package](https://www.nuget.org/packages/Volo.Abp.Account.Web) reference.
This document explains how to customize the login page for your own application.
## Create a Login PageModel
Create a new class inheriting from the [LoginModel](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs) of the Account module.
````csharp
public class CustomLoginModel : LoginModel
{
public CustomLoginModel(
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemeProvider,
Microsoft.Extensions.Options.IOptions<Volo.Abp.Account.Web.AbpAccountOptions> accountOptions)
: base(schemeProvider, accountOptions)
{
}
}
````
> Naming convention is important here. If your class name doesn't end with `LoginModel`, you need to manually replace the `LoginModel` using the [dependency injection](../Dependency-Injection.md) system.
Then you can override any method you need and add new methods and properties needed by the UI.
## Overriding the Login Page UI
Create folder named **Account** under **Pages** directory and create a **Login.cshtml** under this folder. It will automatically override the `Login.cshtml` file defined in the Account Module thanks to the [Virtual File System](../Virtual-File-System.md).
A good way to customize a page is to copy its source code. [Click here](https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml) for the source code of the login page. At the time this document has been written, the source code was like below:
````xml
@page
@using Volo.Abp.Account.Settings
@using Volo.Abp.Settings
@model Acme.BookStore.Web.Pages.Account.CustomLoginModel
@inherits Volo.Abp.Account.Web.Pages.Account.AccountPage
@inject Volo.Abp.Settings.ISettingProvider SettingProvider
@if (Model.EnableLocalLogin)
{
<div class="card mt-3 shadow-sm rounded">
<div class="card-body p-5">
<h4>@L["Login"]</h4>
@if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled))
{
<strong>
@L["AreYouANewUser"]
<a href="@Url.Page("./Register", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})" class="text-decoration-none">@L["Register"]</a>
</strong>
}
<form method="post" class="mt-4">
<input asp-for="ReturnUrl" />
<input asp-for="ReturnUrlHash" />
<div class="form-group">
<label asp-for="LoginInput.UserNameOrEmailAddress"></label>
<input asp-for="LoginInput.UserNameOrEmailAddress" class="form-control" />
<span asp-validation-for="LoginInput.UserNameOrEmailAddress" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="LoginInput.Password"></label>
<input asp-for="LoginInput.Password" class="form-control" />
<span asp-validation-for="LoginInput.Password" class="text-danger"></span>
</div>
<div class="form-check">
<label asp-for="LoginInput.RememberMe" class="form-check-label">
<input asp-for="LoginInput.RememberMe" class="form-check-input" />
@Html.DisplayNameFor(m => m.LoginInput.RememberMe)
</label>
</div>
<abp-button type="submit" button-type="Primary" name="Action" value="Login" class="btn-block btn-lg mt-3">@L["Login"]</abp-button>
</form>
</div>
<div class="card-footer text-center border-0">
<abp-button type="button" button-type="Link" name="Action" value="Cancel" class="px-2 py-0">@L["Cancel"]</abp-button> @* TODO: Only show if identity server is used *@
</div>
</div>
}
@if (Model.VisibleExternalProviders.Any())
{
<div class="col-md-6">
<h4>@L["UseAnotherServiceToLogIn"]</h4>
<form asp-page="./Login" asp-page-handler="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" asp-route-returnUrlHash="@Model.ReturnUrlHash" method="post">
<input asp-for="ReturnUrl" />
<input asp-for="ReturnUrlHash" />
@foreach (var provider in Model.VisibleExternalProviders)
{
<button type="submit" class="btn btn-primary" name="provider" value="@provider.AuthenticationScheme" title="@L["GivenTenantIsNotAvailable", provider.DisplayName]">@provider.DisplayName</button>
}
</form>
</div>
}
@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any())
{
<div class="alert alert-warning">
<strong>@L["InvalidLoginRequest"]</strong>
@L["ThereAreNoLoginSchemesConfiguredForThisClient"]
</div>
}
````
Just changed the `@model` to `Acme.BookStore.Web.Pages.Account.CustomLoginModel` to use the customized `PageModel` class. You can change it however your application needs.
## The Source Code
You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization).
## See Also
* [ASP.NET Core (MVC / Razor Pages) User Interface Customization Guide](../UI/AspNetCore/Customization-User-Interface.md).

101
docs/en/How-To/Customize-SignIn-Manager.md

@ -0,0 +1,101 @@
# How to Customize the SignIn Manager for ABP Applications
After creating a new application using the [application startup template](../Startup-Templates/Application.md), you may want extend or change the default behavior of the SignIn Manager for your authentication and registration flow needs. ABP [Account Module](../Modules/Account.md) uses the [Identity Management Module](../Modules/Identity.md) for SignIn Manager and the [Identity Management Module](../Modules/Identity.md) uses default [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) ([see here](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs#L17)).
To write your Custom SignIn Manager, you need to extend [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) class and register it to the DI container.
This document explains how to customize the SignIn Manager for your own application.
## Create a CustomSignInManager
Create a new class inheriting the [SignInMager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) of Microsoft Identity package.
````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)
{
}
}
````
> It is important to use **Volo.Abp.Identity.IdentityUser** type for SignInManager to inherit, not the AppUser of your application.
Afterwards you can override any of the SignIn Manager methods you need and add new methods and properties needed for your authentication or registration flow.
## Overriding the GetExternalLoginInfoAsync Method
In this case we'll be overriding the `GetExternalLoginInfoAsync` method which is invoked when a third party authentication is implemented.
A good way to override a method is copying its [source code](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Identity/Core/src/SignInManager.cs#L638-L674). In this case, we will be using a minorly modified version of the source code which explicitly shows the namespaces of the methods and properties to help better understanding of the concept.
````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()
};
}
````
To get your overridden method invoked and your customized SignIn Manager class to work, you need to register your class to the [Dependency Injection System](../Dependency-Injection.md).
## Register to Dependency Injection
Registering `CustomSignInManager` should be done with adding **AddSignInManager** extension method of the [IdentityBuilderExtensions](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/IdentityBuilderExtensions.cs) of the [IdentityBuilder](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/IdentityBuilder.cs).
Inside your `.Web` project, locate the `YourProjectNameWebModule` and add the following code under the `PreConfigureServices` method to replace the old `SignInManager` with your customized one:
````csharp
PreConfigure<IdentityBuilder>(identityBuilder =>
{
identityBuilder.AddSignInManager<CustomSignInManager>();
});
````
## The Source Code
You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization).
## See Also
* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md).
* [Identity Management Module](../Modules/Identity.md).

9
docs/en/How-To/Index.md

@ -0,0 +1,9 @@
# "How To" Guides
This section contains "how to" guides for some specific questions frequently asked. While some of them are common development tasks and not directly related to the ABP Framework, we think it is useful to have some concrete examples those directly work with your ABP based applications.
## Authentication
* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md)
* [How to Use the Azure Active Directory Authentication for MVC / Razor Page Applications](Azure-Active-Directory-Authentication-MVC.md)
* [How to Customize the SignIn Manager for ABP Applications](Customize-SignIn-Manager.md)

2
docs/en/Modules/Docs.md

@ -134,7 +134,7 @@ An ABP module must declare `[DependsOn]` attribute if it has a dependency upon a
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<PermissionOptions>(options =>
Configure<AbpPermissionOptions>(options =>
{
options.DefinitionProviders.Add<MyProjectPermissionDefinitionProvider>();
});

177
docs/en/Startup-Templates/Application.md

@ -261,14 +261,181 @@ You should run the application with the given order:
### Angular UI
If you choose Angular as the UI framework (using the `-u angular` option), the solution is separated into two folders:
If you choose `Angular` as the UI framework (using the `-u angular` option), the solution is being separated into three folders:
* `angular` folder contains the Angular UI solution, the client side.
* `aspnet-core` folder contains the ASP.NET Core solution, the server side.
* `angular` folder contains the Angular UI application, the client-side code.
* `aspnet-core` folder contains the ASP.NET Core solution, the server-side code.
* `react-native` folder contains the React Native UI application, the client-side code for mobile.
Server side is very similar to the solution described above. `.HttpApi.Host` project serves the API, so the Angular application can consume it.
The server-side is similar to the solution described above. `*.HttpApi.Host` project serves the API, so the `Angular` application consumes it.
The files under the `angular/src/environments` folder has the essential configuration of the application.
Angular application folder structure looks like below:
![angular-folder-structure](../images/angular-folder-structure.png)
Each of ABP Commercial modules is an NPM package. Some ABP modules are added as a dependency in `package.json`. These modules install with their dependencies. To see all ABP packages, you can run the following command in the `angular` folder:
```bash
yarn list --pattern abp
```
Angular application module structure:
![Angular template structure diagram](../images/angular-template-structure-diagram.png)
#### AppModule
`AppModule` is the root module of the application. Some of ABP modules and some essential modules imported to the `AppModule`.
ABP Config modules also have imported to `AppModule`  for initially requirements of lazy-loadable ABP modules.
#### AppRoutingModule
There are lazy-loadable ABP modules in the `AppRoutingModule` as routes.
> Paths of ABP Modules should not be changed.
You should add `routes` property in the `data` object to add a link on the menu to redirect to your custom pages.
```js
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule),
canActivate: [AuthGuard, PermissionGuard],
data: {
routes: {
name: 'ProjectName::Menu:Dashboard',
order: 2,
iconClass: 'fa fa-dashboard',
requiredPolicy: 'ProjectName.Dashboard.Host'
} as ABP.Route
}
}
```
In the above example;
* If the user is not logged in, AuthGuard blocks access and redirects to the login page.
* PermissionGuard checks the user's permission with `requiredPolicy` property of the `rotues` object. If the user is not authorized to access the page, the 403 page appears.
* `name` property of `routes` is the menu link label. A localization key can be defined .
* `iconClass` property of `routes` object is the menu link icon class.
* `requiredPolicy` property of `routes` object is the required policy key to access the page.
After the above `routes` definition, if the user is authorized, the dashboard link will appear on the menu.
#### Shared Module
The modules that may be required for all modules have imported to the `SharedModule`. You should import the `SharedModule` to all modules.
See the [Sharing Modules](https://angular.io/guide/sharing-ngmodules) document.
#### Environments
The files under the `src/environments` folder has the essential configuration of the application.
#### Home Module
Home module is an example lazy-loadable module that loads on the root address of the application.
#### Styles
The required style files added to `styles` array in the `angular.json`. `AppComponent` loads some style files lazily via `LazyLoadService` after the main bundle is loaded to shorten the first rendering time.
#### Testing
You should create your tests in the same folder as the file file you want to test.
See the [testing document](https://angular.io/guide/testing).
#### Depended Packages
* [NG Bootstrap](https://ng-bootstrap.github.io/) is used as UI component library.
* [NGXS](https://www.ngxs.io/) is used as state management library.
* [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc) is used to support for OAuth 2 and OpenId Connect (OIDC).
* [Chart.js](https://www.chartjs.org/) is used to create widgets.
* [ngx-validate](https://github.com/ng-turkey/ngx-validate) is used for dynamic validation of reactive forms.
### React Native
The solution includes the [React Native](https://reactnative.dev/) application in the `react-native` folder as default.
The server-side is similar to the solution described above. `*.HttpApi.Host` project serves the API, so the React Native application consumes it.
The React Native application was generated with [Expo](https://expo.io/). Expo is a set of tools built around React Native to help you quickly start an app and, while it has many features.
React Native application folder structure as like below:
![react-native-folder-structure](../images/react-native-folder-structure.png)
* `App.js` is bootstrap component of the application.
* `Environment.js` file has the essential configuration of the application. `prod` and `dev` configurations defined in this file.
* [Contexts](https://reactjs.org/docs/context.html) are created in the `src/contexts` folder.
* [Higher order components](https://reactjs.org/docs/higher-order-components.html) are created in the`src/hocs` folder.
* [Custom hooks](https://reactjs.org/docs/hooks-custom.html#extracting-a-custom-hook) are created in the`src/hooks`.
* [Axios interceptors](https://github.com/axios/axios#interceptors) are created in the `src/interceptors` folder.
* Utility functions are exported from `src/utils` folder.
#### Components
Components that can be used on all screens are created in the `src/components` folder. All components have created as a function that able to use [hooks](https://reactjs.org/docs/hooks-intro.html).
#### Screens
![react-native-navigation-structure](../images/react-native-navigation-structure.png)
Screens are created by creating folders that separate their names in the `src/screens` folder. Certain parts of some screens can be split into components.
Each screen is used in a navigator in the `src/navigators` folder.
#### Navigation
[React Navigation](https://reactnavigation.org/) is used as a navigation library. Navigators are created in the `src/navigators`. A [drawer](https://reactnavigation.org/docs/drawer-based-navigation/) navigator and several [stack](https://reactnavigation.org/docs/hello-react-navigation/#installing-the-stack-navigator-library) navigators have created in this folder. See the [above diagram](#screens) for navigation structure.
#### State Management
[Redux](https://redux.js.org/) is used as state management library. [Redux Toolkit](https://redux-toolkit.js.org/) library is used as a toolset for efficient Redux development.
Actions, reducers, sagas, selectors are created in the `src/store` folder. Store folder as like below:
![react-native-store-folder](../images/react-native-store-folder.png)
* [**Store**](https://redux.js.org/basics/store) is defined in the `src/store/index.js` file.
* [**Actions**](https://redux.js.org/basics/actions/) are payloads of information that send data from your application to your store.
* [**Reducers**](https://redux.js.org/basics/reducers) specify how the application's state changes in response to actions sent to the store.
* [**Redux-Saga**](https://redux-saga.js.org/) is a library that aims to make application side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage. Sagas are created in the `src/store/sagas` folder.
* [**Reselect**](https://github.com/reduxjs/reselect) library is used to create memoized selectors. Selectors are created in the `src/store/selectors` folder.
#### APIs
[Axios](https://github.com/axios/axios) is used as an HTTP client library. An Axios instance has exported from `src/api/API.js` file to make HTTP calls with the same config. `src/api` folder also has the API files that have been created for API calls.
#### Theming
[Native Base](https://nativebase.io/) is used as UI components library. Native Base components can customize easily. See the [Native Base customize](https://docs.nativebase.io/Customize.html#Customize) documentation. We followed the same way.
* Native Base theme variables are in the `src/theme/variables` folder.
* Native Base component styles are in the `src/theme/components` folder. These files have been generated with Native Base's `ejectTheme` script.
* Styles of components override with the files under the `src/theme/overrides` folder.
#### Testing
Unit tests will be created.
See the [Testing Overview](https://reactjs.org/docs/testing.html) document.
#### Depended Libraries
* [Native Base](https://nativebase.io/) is used as UI components library.
* [React Navigation](https://reactnavigation.org/) is used as navigation library.
* [Axios](https://github.com/axios/axios) is used as HTTP client library.
* [Redux](https://redux.js.org/) is used as state management library.
* [Redux Toolkit](https://redux-toolkit.js.org/) library is used as a toolset for efficient Redux development.
* [Redux-Saga](https://redux-saga.js.org/) is used to manage asynchronous processes.
* [Redux Persist](https://github.com/rt2zz/redux-persist) is used as state persistance.
* [Reselect](https://github.com/reduxjs/reselect) is used to create memoized selectors.
* [i18n-js](https://github.com/fnando/i18n-js) is used as i18n library.
* [expo-font](https://docs.expo.io/versions/latest/sdk/font/) library allows loading fonts easily.
* [Formik](https://github.com/jaredpalmer/formik) is used to build forms.
* [Yup](https://github.com/jquense/yup) is used for form validations.
## What's Next?

74
docs/en/UI/Angular/Content-Security-Strategy.md

@ -0,0 +1,74 @@
# ContentSecurityStrategy
`ContentSecurityStrategy` is an abstract class exposed by @abp/ng.core package. It helps you mark inline scripts or styles as safe in terms of [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy).
## API
### constructor
```js
constructor(public nonce?: string)
```
- `nonce` enables whitelisting inline script or styles in order to avoid using `unsafe-inline` in [script-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#Unsafe_inline_script) and [style-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src#Unsafe_inline_styles) directives.
### applyCSP
```js
applyCSP(element: HTMLScriptElement | HTMLStyleElement): void
```
This method maps the aforementioned properties to the given `element`.
## LooseContentSecurityPolicy
`LooseContentSecurityPolicy` is a class that extends `ContentSecurityStrategy`. It requires `nonce` and marks given `<script>` or `<style>` tag with it.
## NoContentSecurityPolicy
`NoContentSecurityPolicy` is a class that extends `ContentSecurityStrategy`. It does not mark inline scripts and styles as safe. You can consider it as a noop alternative.
## Predefined Content Security Strategies
Predefined content security strategies are accessible via `CONTENT_SECURITY_STRATEGY` constant.
### Loose
```js
CONTENT_SECURITY_STRATEGY.Loose(nonce: string)
```
`nonce` will be set.
### None
```js
CONTENT_SECURITY_STRATEGY.None()
```
Nothing will be done.
## See Also
- [DomInsertionService](./Dom-Insertion-Service.md)
- [ContentStrategy](./Content-Strategy.md)

95
docs/en/UI/Angular/Content-Strategy.md

@ -0,0 +1,95 @@
# ContentStrategy
`ContentStrategy` is an abstract class exposed by @abp/ng.core package. It helps you create inline scripts or styles.
## API
### constructor
```js
constructor(
public content: string,
protected domStrategy?: DomStrategy,
protected contentSecurityStrategy?: ContentSecurityStrategy
)
```
- `content` is set to `<script>` and `<style>` elements as `textContent` property.
- `domStrategy` is the `DomStrategy` that will be used when inserting the created element. (_default: AppendToHead_)
- `contentSecurityStrategy` is the `ContentSecurityStrategy` that will be used on the created element before inserting it. (_default: None_)
Please refer to [DomStrategy](./Dom-Strategy.md) and [ContentSecurityStrategy](./Content-Security-Strategy.md) documentation for their usage.
### createElement
```js
createElement(): HTMLScriptElement | HTMLStyleElement
```
This method creates and returns a `<script>` or `<style>` element with `content` set as `textContent`.
### insertElement
```js
insertElement(): void
```
This method creates and inserts a `<script>` or `<style>` element.
## ScriptContentStrategy
`ScriptContentStrategy` is a class that extends `ContentStrategy`. It lets you **insert a `<script>` element to the DOM**.
## StyleContentStrategy
`StyleContentStrategy` is a class that extends `ContentStrategy`. It lets you **insert a `<style>` element to the DOM**.
## Predefined Content Strategies
Predefined content strategies are accessible via `CONTENT_STRATEGY` constant.
### AppendScriptToBody
```js
CONTENT_STRATEGY.AppendScriptToBody(content: string)
```
Creates a `<script>` element with the given content and places it at the **end** of `<body>` tag in the document.
### AppendScriptToHead
```js
CONTENT_STRATEGY.AppendScriptToHead(content: string)
```
Creates a `<script>` element with the given content and places it at the **end** of `<head>` tag in the document.
### AppendStyleToHead
```js
CONTENT_STRATEGY.AppendStyleToHead(content: string)
```
Creates a `<style>` element with the given content and places it at the **end** of `<head>` tag in the document.
### PrependStyleToHead
```js
CONTENT_STRATEGY.PrependStyleToHead(content: string)
```
Creates a `<style>` element with the given content and places it at the **beginning** of `<head>` tag in the document.
## See Also
- [DomInsertionService](./Dom-Insertion-Service.md)

60
docs/en/UI/Angular/Cross-Origin-Strategy.md

@ -0,0 +1,60 @@
# CrossOriginStrategy
`CrossOriginStrategy` is a class exposed by @abp/ng.core package. Its instances define how a source referenced by an element will be retrieved by the browser and are consumed by other classes such as `LoadingStrategy`.
## API
### constructor
```js
constructor(
public crossorigin: 'anonymous' | 'use-credentials',
public integrity?: string
)
```
- `crossorigin` is mapped to [the HTML attribute with the same name](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin).
- `integrity` is a hash for validating a remote resource. Its use is explained [here](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity).
### setCrossOrigin
```js
setCrossOrigin(element: HTMLElement): void
```
This method maps the aforementioned properties to the given `element`.
## Predefined Cross-Origin Strategies
Predefined cross-origin strategies are accessible via `CROSS_ORIGIN_STRATEGY` constant.
### Anonymous
```js
CROSS_ORIGIN_STRATEGY.Anonymous(integrity?: string)
```
`crossorigin` will be set as `"anonymous"` and `integrity` is optional.
### UseCredentials
```js
CROSS_ORIGIN_STRATEGY.UseCredentials(integrity?: string)
```
`crossorigin` will be set as `"use-credentials"` and `integrity` is optional.
## What's Next?
- [LoadingStrategy](./Loading-Strategy.md)

2
docs/en/UI/Angular/Custom-Setting-Page.md

@ -43,4 +43,4 @@ Navigate to `/setting-management` route to see the changes:
## What's Next?
- [TrackByService](./Track-By-Service.md)
- [Lazy Loading Scripts & Styles](./Lazy-Load-Service.md)

91
docs/en/UI/Angular/Dom-Insertion-Service.md

@ -0,0 +1,91 @@
# How to Insert 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.
```js
import { DomInsertionService } from '@abp/ng.core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private domInsertionService: DomInsertionService) {}
}
```
## 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.
### How to Insert Scripts
The first parameter of `insertContent` method expects a `ContentStrategy`. If you pass a `ScriptContentStrategy` instance, the `DomInsertionService` will create a `<script>` element with given `content` and place it in the designated DOM position.
```js
import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private domInsertionService: DomInsertionService) {}
ngOnInit() {
this.domInsertionService.insertContent(
CONTENT_STRATEGY.AppendScriptToBody('alert()')
);
}
}
```
In the example above, `<script>alert()</script>` element will place at the **end** of `<body>`.
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.
```js
import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private domInsertionService: DomInsertionService) {}
ngOnInit() {
this.domInsertionService.insertContent(
CONTENT_STRATEGY.AppendStyleToHead('body {margin: 0;}')
);
}
}
```
In the example above, `<style>body {margin: 0;}</style>` element will place at the **end** of `<head>`.
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
```
`strategy` parameter is the primary focus here and is explained above.
## What's Next?
- [TrackByService](./Track-By-Service.md)

89
docs/en/UI/Angular/Dom-Strategy.md

@ -0,0 +1,89 @@
# DomStrategy
`DomStrategy` is a class exposed by @abp/ng.core package. Its instances define how an element will be attached to the DOM and are consumed by other classes such as `LoadingStrategy`.
## API
### constructor
```js
constructor(
public target?: HTMLElement,
public position?: InsertPosition
)
```
- `target` is an HTMLElement (_default: document.head_).
- `position` defines where the created element will be placed. All possible values of `position` can be found [here](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement) (_default: 'beforeend'_).
### insertElement
```js
insertElement(element: HTMLElement): void
```
This method inserts given `element` to `target` based on the `position`.
## Predefined Dom Strategies
Predefined dom strategies are accessible via `DOM_STRATEGY` constant.
### AppendToBody
```js
DOM_STRATEGY.AppendToBody()
```
`insertElement` will place the given `element` at the end of `<body>`.
### AppendToHead
```js
DOM_STRATEGY.AppendToHead()
```
`insertElement` will place the given `element` at the end of `<head>`.
### PrependToHead
```js
DOM_STRATEGY.PrependToHead()
```
`insertElement` will place the given `element` at the beginning of `<head>`.
### AfterElement
```js
DOM_STRATEGY.AfterElement(target: HTMLElement)
```
`insertElement` will place the given `element` after (as a sibling to) the `target`.
### BeforeElement
```js
DOM_STRATEGY.BeforeElement(target: HTMLElement)
```
`insertElement` will place the given `element` before (as a sibling to) the `target`.
## See Also
- [DomInsertionService](./Dom-Insertion-Service.md)
- [LazyLoadService](./Lazy-Load-Service.md)
- [LoadingStrategy](./Loading-Strategy.md)
- [ContentStrategy](./Content-Strategy.md)

213
docs/en/UI/Angular/Lazy-Load-Service.md

@ -0,0 +1,213 @@
# How to Lazy Load Scripts and Styles
You can use the `LazyLoadService` in @abp/ng.core package in order to lazy load scripts and styles in an easy and explicit way.
## Getting Started
You do not have to provide the `LazyLoadService` 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 { LazyLoadService } from '@abp/ng.core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private lazyLoadService: LazyLoadService) {}
}
```
## Usage
You can use the `load` method of `LazyLoadService` to create a `<script>` or `<link>` element in the DOM at the desired position and force the browser to download the target resource.
### How to Load Scripts
The first parameter of `load` method expects a `LoadingStrategy`. If you pass a `ScriptLoadingStrategy` instance, the `LazyLoadService` will create a `<script>` element with given `src` and place it in the designated DOM position.
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
@Component({
template: `
<some-component *ngIf="libraryLoaded$ | async"></some-component>
`
})
class DemoComponent {
libraryLoaded$ = this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/some-library.js'),
);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
The `load` method returns an observable to which you can subscibe in your component or with an `async` pipe. In the example above, the `NgIf` directive will render `<some-component>` only **if the script gets successfully loaded or is already loaded before**.
> You can subscribe multiple times in your template with `async` pipe. The styles will only be loaded once.
Please refer to [LoadingStrategy](./Loading-Strategy.md) to see all available loading strategies and how you can build your own loading strategy.
### How to Load Styles
If you pass a `StyleLoadingStrategy` instance as the first parameter of `load` method, the `LazyLoadService` will create a `<link>` element with given `href` and place it in the designated DOM position.
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
@Component({
template: `
<some-component *ngIf="stylesLoaded$ | async"></some-component>
`
})
class DemoComponent {
stylesLoaded$ = this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousStyleToHead('/assets/some-styles.css'),
);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
The `load` method returns an observable to which you can subscibe in your component or with an `AsyncPipe`. In the example above, the `NgIf` directive will render `<some-component>` only **if the style gets successfully loaded or is already loaded before**.
> You can subscribe multiple times in your template with `async` pipe. The styles will only be loaded once.
Please refer to [LoadingStrategy](./Loading-Strategy.md) to see all available loading strategies and how you can build your own loading strategy.
### Advanced Usage
You have quite a bit of **freedom to define how your lazy load will work**. Here is an example:
```js
const domStrategy = DOM_STRATEGY.PrependToHead();
const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous(
'sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh',
);
const loadingStrategy = new StyleLoadingStrategy(
'https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css',
domStrategy,
crossOriginStrategy,
);
this.lazyLoad.load(loadingStrategy, 1, 2000);
```
This code will create a `<link>` element with given url and integrity hash, insert it to to top of the `<head>` element, and retry once after 2 seconds if first try fails.
A common usecase is **loading multiple scripts and/or styles before using a feature**:
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
import { frokJoin } from 'rxjs';
@Component({
template: `
<some-component *ngIf="scriptsAndStylesLoaded$ | async"></some-component>
`
})
class DemoComponent {
private stylesLoaded$ = forkJoin(
this.lazyLoad.load(
LOADING_STRATEGY.PrependAnonymousStyleToHead('/assets/library-dark-theme.css'),
),
this.lazyLoad.load(
LOADING_STRATEGY.PrependAnonymousStyleToHead('/assets/library.css'),
),
);
private scriptsLoaded$ = forkJoin(
this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/library.js'),
),
this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/other-library.css'),
),
);
scriptsAndStylesLoaded$ = forkJoin(this.scriptsLoaded$, this.stylesLoaded$);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
RxJS `forkJoin` will load all scripts and styles in parallel and emit only when all of them are loaded. So, when `<some-component>` is placed, all required dependencies will be available.
> Noticed we have prepended styles to the document head? This is sometimes necessary, because your application styles may be overriding some of the library styles. In such a case, you must be careful about the order of prepended styles. They will be placed one-by-one and, **when prepending, the last one placed will be on top**.
Another frequent usecase is **loading dependent scripts in order**:
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
import { concat } from 'rxjs';
@Component({
template: `
<some-component *ngIf="scriptsLoaded$ | async"></some-component>
`
})
class DemoComponent {
scriptsLoaded$ = concat(
this.lazyLoad.load(
LOADING_STRATEGY.PrependAnonymousScriptToHead('/assets/library.js'),
),
this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/script-that-requires-library.js'),
),
);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
In this example, the second file needs the first one to be loaded beforehand. RxJS `concat` function will let you load all scripts one-by-one in the given order and emit only when all of them are loaded.
## API
### loaded
```js
loaded: Set<string>
```
All previously loaded paths are available via this property. It is a simple [JavaScript Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set).
### load
```js
load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Observable<Event>
```
- `strategy` parameter is the primary focus here and is explained above.
- `retryTimes` defines how many times the loading will be tried again before fail (_default: 2_).
- `retryDelay` defines how much delay there will be between retries (_default: 1000_).
## What's Next?
- [DomInsertionService](./Dom-Insertion-Service.md)

110
docs/en/UI/Angular/Loading-Strategy.md

@ -0,0 +1,110 @@
# LoadingStrategy
`LoadingStrategy` is an abstract class exposed by @abp/ng.core package. There are two loading strategies extending it: `ScriptLoadingStrategy` and `StyleLoadingStrategy`. Implementing the same methods and properties, both of these strategies help you define how your lazy loading will work.
## API
### constructor
```js
constructor(
public path: string,
protected domStrategy?: DomStrategy,
protected crossOriginStrategy?: CrossOriginStrategy
)
```
- `path` is set to `<script>` elements as `src` and `<link>` elements as `href` attribute.
- `domStrategy` is the `DomStrategy` that will be used when inserting the created element. (_default: AppendToHead_)
- `crossOriginStrategy` is the `CrossOriginStrategy` that will be used on the created element before inserting it. (_default: Anonymous_)
Please refer to [DomStrategy](./Dom-Strategy.md) and [CrossOriginStrategy](./Cross-Origin-Strategy.md) documentation for their usage.
### createElement
```js
createElement(): HTMLScriptElement | HTMLLinkElement
```
This method creates and returns a `<script>` or `<link>` element with `path` set as `src` or `href`.
### createStream
```js
createStream(): Observable<Event>
```
This method creates and returns an observable stream that emits on success and throws on error.
## ScriptLoadingStrategy
`ScriptLoadingStrategy` is a class that extends `LoadingStrategy`. It lets you **lazy load a script**.
## StyleLoadingStrategy
`StyleLoadingStrategy` is a class that extends `LoadingStrategy`. It lets you **lazy load a style**.
## Predefined Loading Strategies
Predefined content security strategies are accessible via `LOADING_STRATEGY` constant.
### AppendAnonymousScriptToHead
```js
LOADING_STRATEGY.AppendAnonymousScriptToHead(src: string, integrity?: string)
```
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **end** of `<head>` tag in the document.
### PrependAnonymousScriptToHead
```js
LOADING_STRATEGY.PrependAnonymousScriptToHead(src: string, integrity?: string)
```
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **beginning** of `<head>` tag in the document.
### AppendAnonymousScriptToBody
```js
LOADING_STRATEGY.AppendAnonymousScriptToBody(src: string, integrity?: string)
```
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **end** of `<body>` tag in the document.
### AppendAnonymousStyleToHead
```js
LOADING_STRATEGY.AppendAnonymousStyleToHead(href: string, integrity?: string)
```
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<style>` element and places it at the **end** of `<head>` tag in the document.
### PrependAnonymousStyleToHead
```js
LOADING_STRATEGY.PrependAnonymousStyleToHead(href: string, integrity?: string)
```
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<style>` element and places it at the **beginning** of `<head>` tag in the document.
## See Also
- [LazyLoadService](./Lazy-Load-Service.md)

4
docs/en/UI/Angular/Track-By-Service.md

@ -111,7 +111,3 @@ class DemoComponent {
trackByTenantAccountId = trackByDeep<Item>('tenant', 'account', 'id');
}
```
## What's Next?
* [Linked List (Doubly)](./Linked-List.md)

4
docs/en/UI/AspNetCore/Bundling-Minification.md

@ -246,10 +246,10 @@ public class MyPrismjsStyleExtension : BundleContributor
}
````
Then you can configure `BundleContributorOptions` to extend existing `PrismjsStyleBundleContributor`.
Then you can configure `AbpBundleContributorOptions` to extend existing `PrismjsStyleBundleContributor`.
````csharp
Configure<BundleContributorOptions>(options =>
Configure<AbpBundleContributorOptions>(options =>
{
options
.Extensions<PrismjsStyleBundleContributor>()

42
docs/en/UI/AspNetCore/Tag-Helpers/Popovers.md

@ -0,0 +1,42 @@
# Popovers
## Introduction
`abp-popover` is the abp tag for popover messages.
Basic usage:
````xml
<abp-button abp-popover="Hi, i'm popover content!">
Popover Default
</abp-button>
````
## Demo
See the [popovers demo page](https://bootstrap-taghelpers.abp.io/Components/Popovers) to see it in action.
## Attributes
### disabled
A value indicates if the element should be disabled for interaction. If this value is set to `true`, `dismissable` attribute will be ignored. Should be one of the following values:
* `false` (default value)
* `true`
### dismissable
A value indicates to dismiss the popovers on the user's next click of a different element than the toggle element. Should be one of the following values:
* `false` (default value)
* `true`
### hoverable
A value indicates if the popover content will be displayed on mouse hover. Should be one of the following values:
* `false` (default value)
* `true`

326
docs/en/UI/Angular/Linked-List.md → docs/en/UI/Common/Utils/Linked-List.md

@ -30,7 +30,11 @@ There are several methods to create new nodes in a linked list and all of them a
#### addHead(value: T): ListNode\<T\>
#### addHead(value)
```js
addHead(value: T): ListNode\<T\>
```
Adds a node with given value as the first node in list:
@ -50,7 +54,11 @@ list.addHead('c');
#### addManyHead(values: T\[\]): ListNode\<T\>\[\]
#### addManyHead(values)
```js
addManyHead(values: T\[\]): ListNode\<T\>\[\]
```
Adds multiple nodes with given values as the first nodes in list:
@ -66,7 +74,11 @@ list.addManyHead(['x', 'y', 'z']);
#### addTail(value: T): ListNode\<T\>
#### addTail(value)
```js
addTail(value: T): ListNode\<T\>
```
Adds a node with given value as the last node in list:
@ -86,7 +98,11 @@ list.addTail('c');
#### addManyTail(values: T\[\]): ListNode\<T\>\[\]
#### addManyTail(values)
```js
addManyTail(values: T\[\]): ListNode\<T\>\[\]
```
Adds multiple nodes with given values as the last nodes in list:
@ -102,7 +118,11 @@ list.addManyTail(['x', 'y', 'z']);
#### addAfter(value: T, previousValue: T, compareFn = compare): ListNode\<T\>
#### addAfter(value, previousValue, compareFn)
```js
addAfter(value: T, previousValue: T, compareFn = compare): ListNode\<T\>
```
Adds a node with given value after the first node that has the previous value:
@ -130,7 +150,11 @@ list.addTail({ x: 3 });
// {"x":1} <-> {"x":2} <-> {"x":3}
list.addAfter({ x: 0 }, { x: 2 }, (v1, v2) => v1.x === v2.x);
list.addAfter(
{ x: 0 },
2,
(value, searchedValue) => value.x === searchedValue
);
// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3}
```
@ -141,7 +165,11 @@ list.addAfter({ x: 0 }, { x: 2 }, (v1, v2) => v1.x === v2.x);
#### addManyAfter(values: T\[\], previousValue: T, compareFn = compare): ListNode\<T\>\[\]
#### addManyAfter(values, previousValue, compareFn)
```js
addManyAfter(values: T\[\], previousValue: T, compareFn = compare): ListNode\<T\>\[\]
```
Adds multiple nodes with given values after the first node that has the previous value:
@ -164,7 +192,11 @@ list.addManyTail([{ x: 1 },{ x: 2 },{ x: 3 }]);
// {"x":1} <-> {"x":2} <-> {"x":3}
list.addManyAfter([{ x: 4 }, { x: 5 }], { x: 2 }, (v1, v2) => v1.x === v2.x);
list.addManyAfter(
[{ x: 4 }, { x: 5 }],
2,
(value, searchedValue) => value.x === searchedValue
);
// {"x":1} <-> {"x":2} <-> {"x":4} <-> {"x":5} <-> {"x":3}
```
@ -175,7 +207,11 @@ list.addManyAfter([{ x: 4 }, { x: 5 }], { x: 2 }, (v1, v2) => v1.x === v2.x);
#### addBefore(value: T, nextValue: T, compareFn = compare): ListNode\<T\>
#### addBefore(value, nextValue, compareFn)
```js
addBefore(value: T, nextValue: T, compareFn = compare): ListNode\<T\>
```
Adds a node with given value before the first node that has the next value:
@ -203,7 +239,11 @@ list.addTail({ x: 3 });
// {"x":1} <-> {"x":2} <-> {"x":3}
list.addBefore({ x: 0 }, { x: 2 }, (v1, v2) => v1.x === v2.x);
list.addBefore(
{ x: 0 },
2,
(value, searchedValue) => value.x === searchedValue
);
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":3}
```
@ -214,7 +254,11 @@ list.addBefore({ x: 0 }, { x: 2 }, (v1, v2) => v1.x === v2.x);
#### addManyBefore(values: T\[\], nextValue: T, compareFn = compare): ListNode\<T\>\[\]
#### addManyBefore(values, nextValue, compareFn)
```js
addManyBefore(values: T\[\], nextValue: T, compareFn = compare): ListNode\<T\>\[\]
```
Adds multiple nodes with given values before the first node that has the next value:
@ -237,7 +281,11 @@ list.addManyTail([{ x: 1 },{ x: 2 },{ x: 3 }]);
// {"x":1} <-> {"x":2} <-> {"x":3}
list.addManyBefore([{ x: 4 }, { x: 5 }], { x: 2 }, (v1, v2) => v1.x === v2.x);
list.addManyBefore(
[{ x: 4 }, { x: 5 }],
2,
(value, searchedValue) => value.x === searchedValue
);
// {"x":1} <-> {"x":4} <-> {"x":5} <-> {"x":2} <-> {"x":3}
```
@ -248,7 +296,11 @@ list.addManyBefore([{ x: 4 }, { x: 5 }], { x: 2 }, (v1, v2) => v1.x === v2.x);
#### addByIndex(value: T, position: number): ListNode\<T\>
#### addByIndex(value, position)
```js
addByIndex(value: T, position: number): ListNode\<T\>
```
Adds a node with given value at the specified position in the list:
@ -282,7 +334,11 @@ list.addByIndex('x', -1);
#### addManyByIndex(values: T\[\], position: number): ListNode\<T\>\[\]
#### addManyByIndex(values, position)
```js
addManyByIndex(values: T\[\], position: number): ListNode\<T\>\[\]
```
Adds multiple nodes with given values at the specified position in the list:
@ -312,7 +368,11 @@ list.addManyByIndex(['x', 'y'], -1);
#### add(value: T).head(): ListNode\<T\>
#### add(value).head()
```js
add(value: T).head(): ListNode\<T\>
```
Adds a node with given value as the first node in list:
@ -336,7 +396,11 @@ list.add('c').head();
#### add(value: T).tail(): ListNode\<T\>
#### add(value).tail()
```js
add(value: T).tail(): ListNode\<T\>
```
Adds a node with given value as the last node in list:
@ -360,7 +424,11 @@ list.add('c').tail();
#### add(value: T).after(previousValue: T, compareFn = compare): ListNode\<T\>
#### add(value).after(previousValue, compareFn)
```js
add(value: T).after(previousValue: T, compareFn = compare): ListNode\<T\>
```
Adds a node with given value after the first node that has the previous value:
@ -388,7 +456,9 @@ list.add({ x: 3 }).tail();
// {"x":1} <-> {"x":2} <-> {"x":3}
list.add({ x: 0 }).after({ x: 2 }, (v1, v2) => v1.x === v2.x);
list
.add({ x: 0 })
.after(2, (value, searchedValue) => value.x === searchedValue);
// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3}
```
@ -401,7 +471,11 @@ list.add({ x: 0 }).after({ x: 2 }, (v1, v2) => v1.x === v2.x);
#### add(value: T).before(nextValue: T, compareFn = compare): ListNode\<T\>
#### add(value).before(nextValue, compareFn)
```js
add(value: T).before(nextValue: T, compareFn = compare): ListNode\<T\>
```
Adds a node with given value before the first node that has the next value:
@ -429,7 +503,9 @@ list.add({ x: 3 }).tail();
// {"x":1} <-> {"x":2} <-> {"x":3}
list.add({ x: 0 }).before({ x: 2 }, (v1, v2) => v1.x === v2.x);
list
.add({ x: 0 })
.before(2, (value, searchedValue) => value.x === searchedValue);
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":3}
```
@ -442,7 +518,11 @@ list.add({ x: 0 }).before({ x: 2 }, (v1, v2) => v1.x === v2.x);
#### add(value: T).byIndex(position: number): ListNode\<T\>
#### add(value).byIndex(position)
```js
add(value: T).byIndex(position: number): ListNode\<T\>
```
Adds a node with given value at the specified position in the list:
@ -480,7 +560,11 @@ list.add('x').byIndex(-1);
#### addMany(values: T\[\]).head(): ListNode\<T\>\[\]
#### addMany(values).head()
```js
addMany(values: T\[\]).head(): ListNode\<T\>\[\]
```
Adds multiple nodes with given values as the first nodes in list:
@ -500,7 +584,11 @@ list.addMany(['x', 'y', 'z']).head();
#### addMany(values: T\[\]).tail(): ListNode\<T\>\[\]
#### addMany(values).tail()
```js
addMany(values: T\[\]).tail(): ListNode\<T\>\[\]
```
Adds multiple nodes with given values as the last nodes in list:
@ -520,7 +608,11 @@ list.addMany(['x', 'y', 'z']).tail();
#### addMany(values: T\[\]).after(previousValue: T, compareFn = compare): ListNode\<T\>\[\]
#### addMany(values).after(previousValue, compareFn)
```js
addMany(values: T\[\]).after(previousValue: T, compareFn = compare): ListNode\<T\>\[\]
```
Adds multiple nodes with given values after the first node that has the previous value:
@ -543,7 +635,9 @@ list.addMany([{ x: 1 }, { x: 2 }, { x: 3 }]).tail();
// {"x":1} <-> {"x":2} <-> {"x":3}
list.addMany([{ x: 4 }, { x: 5 }]).after({ x: 2 }, (v1, v2) => v1.x === v2.x);
list
.addMany([{ x: 4 }, { x: 5 }])
.after(2, (value, searchedValue) => value.x === searchedValue);
// {"x":1} <-> {"x":2} <-> {"x":4} <-> {"x":5} <-> {"x":3}
```
@ -556,7 +650,11 @@ list.addMany([{ x: 4 }, { x: 5 }]).after({ x: 2 }, (v1, v2) => v1.x === v2.x);
#### addMany(values: T\[\]).before(nextValue: T, compareFn = compare): ListNode\<T\>\[\]
#### addMany(values).before(nextValue, compareFn)
```js
addMany(values: T\[\]).before(nextValue: T, compareFn = compare): ListNode\<T\>\[\]
```
Adds multiple nodes with given values before the first node that has the next value:
@ -579,7 +677,9 @@ list.addMany([{ x: 1 }, { x: 2 }, { x: 3 }]).tail();
// {"x":1} <-> {"x":2} <-> {"x":3}
list.addMany([{ x: 4 }, { x: 5 }]).before({ x: 2 }, (v1, v2) => v1.x === v2.x);
list
.addMany([{ x: 4 }, { x: 5 }])
.before(2, (value, searchedValue) => value.x === searchedValue);
// {"x":1} <-> {"x":4} <-> {"x":5} <-> {"x":2} <-> {"x":3}
```
@ -592,7 +692,11 @@ list.addMany([{ x: 4 }, { x: 5 }]).before({ x: 2 }, (v1, v2) => v1.x === v2.x);
#### addMany(values: T\[\]).byIndex(position: number): ListNode\<T\>\[\]
#### addMany(values).byIndex(position)
```js
addMany(values: T\[\]).byIndex(position: number): ListNode\<T\>\[\]
```
Adds multiple nodes with given values at the specified position in the list:
@ -632,7 +736,11 @@ There are a few methods to remove nodes from a linked list and all of them are s
#### dropHead(): ListNode\<T\> | undefined
#### dropHead()
```js
dropHead(): ListNode\<T\> | undefined
```
Removes the first node from the list:
@ -648,7 +756,11 @@ list.dropHead();
#### dropManyHead(count: number): ListNode\<T\>\[\]
#### dropManyHead(count)
```js
dropManyHead(count: number): ListNode\<T\>\[\]
```
Removes the first nodes from the list based on given count:
@ -664,7 +776,11 @@ list.dropManyHead(2);
#### dropTail(): ListNode\<T\> | undefined
#### dropTail()
```js
dropTail(): ListNode\<T\> | undefined
```
Removes the last node from the list:
@ -680,7 +796,11 @@ list.dropTail();
#### dropManyTail(count: number): ListNode\<T\>\[\]
#### dropManyTail(count)
```js
dropManyTail(count: number): ListNode\<T\>\[\]
```
Removes the last nodes from the list based on given count:
@ -696,7 +816,11 @@ list.dropManyTail(2);
#### dropByIndex(position: number): ListNode\<T\> | undefined
#### dropByIndex(position)
```js
dropByIndex(position: number): ListNode\<T\> | undefined
```
Removes the node with the specified position from the list:
@ -726,7 +850,11 @@ list.dropByIndex(-2);
#### dropManyByIndex(count: number, position: number): ListNode\<T\>\[\]
#### dropManyByIndex(count, position)
```js
dropManyByIndex(count: number, position: number): ListNode\<T\>\[\]
```
Removes the nodes starting from the specified position from the list based on given count:
@ -756,7 +884,11 @@ list.dropManyByIndex(2, -2);
#### dropByValue(value: T, compareFn = compare): ListNode\<T\> | undefined
#### dropByValue(value, compareFn)
```js
dropByValue(value: T, compareFn = compare): ListNode\<T\> | undefined
```
Removes the first node with given value from the list:
@ -779,7 +911,7 @@ list.addMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]).tail();
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3}
list.dropByValue({ x: 0 }, (v1, v2) => v1.x === v2.x);
list.dropByValue(0, (value, searchedValue) => value.x === searchedValue);
// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3}
```
@ -790,7 +922,11 @@ list.dropByValue({ x: 0 }, (v1, v2) => v1.x === v2.x);
#### dropByValueAll(value: T, compareFn = compare): ListNode\<T\>\[\]
#### dropByValueAll(value, compareFn)
```js
dropByValueAll(value: T, compareFn = compare): ListNode\<T\>\[\]
```
Removes all nodes with given value from the list:
@ -813,7 +949,7 @@ list.addMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]).tail();
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3}
list.dropByValue({ x: 0 }, (v1, v2) => v1.x === v2.x);
list.dropByValue(0, (value, searchedValue) => value.x === searchedValue);
// {"x":1} <-> {"x":2} <-> {"x":3}
```
@ -824,7 +960,11 @@ list.dropByValue({ x: 0 }, (v1, v2) => v1.x === v2.x);
#### drop().head(): ListNode\<T\> | undefined
#### drop().head()
```js
drop().head(): ListNode\<T\> | undefined
```
Removes the first node in list:
@ -844,7 +984,11 @@ list.drop().head();
#### drop().tail(): ListNode\<T\> | undefined
#### drop().tail()
```js
drop().tail(): ListNode\<T\> | undefined
```
Removes the last node in list:
@ -864,7 +1008,11 @@ list.drop().tail();
#### drop().byIndex(position: number): ListNode\<T\> | undefined
#### drop().byIndex(position)
```js
drop().byIndex(position: number): ListNode\<T\> | undefined
```
Removes the node with the specified position from the list:
@ -898,7 +1046,11 @@ list.drop().byIndex(-2);
#### drop().byValue(value: T, compareFn = compare): ListNode\<T\> | undefined
#### drop().byValue(value, compareFn)
```js
drop().byValue(value: T, compareFn = compare): ListNode\<T\> | undefined
```
Removes the first node with given value from the list:
@ -921,7 +1073,9 @@ list.addMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]).tail();
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3}
list.drop().byValue({ x: 0 }, (v1, v2) => v1.x === v2.x);
list
.drop()
.byValue(0, (value, searchedValue) => value.x === searchedValue);
// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3}
```
@ -934,7 +1088,11 @@ list.drop().byValue({ x: 0 }, (v1, v2) => v1.x === v2.x);
#### drop().byValueAll(value: T, compareFn = compare): ListNode\<T\>\[\]
#### drop().byValueAll(value, compareFn)
```js
drop().byValueAll(value: T, compareFn = compare): ListNode\<T\>\[\]
```
Removes all nodes with given value from the list:
@ -957,7 +1115,9 @@ list.addMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]).tail();
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3}
list.drop().byValueAll({ x: 0 }, (v1, v2) => v1.x === v2.x);
list
.drop()
.byValueAll(0, (value, searchedValue) => value.x === searchedValue);
// {"x":1} <-> {"x":2} <-> {"x":3}
```
@ -970,7 +1130,11 @@ list.drop().byValueAll({ x: 0 }, (v1, v2) => v1.x === v2.x);
#### dropMany(count: number).head(): ListNode\<T\>\[\]
#### dropMany(count).head()
```js
dropMany(count: number).head(): ListNode\<T\>\[\]
```
Removes the first nodes from the list based on given count:
@ -990,7 +1154,11 @@ list.dropMany(2).head();
#### dropMany(count: number).tail(): ListNode\<T\>\[\]
#### dropMany(count).tail()
```js
dropMany(count: number).tail(): ListNode\<T\>\[\]
```
Removes the last nodes from the list based on given count:
@ -1010,7 +1178,11 @@ list.dropMany(2).tail();
#### dropMany(count: number).byIndex(position: number): ListNode\<T\>\[\]
#### dropMany(count).byIndex(position)
```js
dropMany(count: number).byIndex(position: number): ListNode\<T\>\[\]
```
Removes the nodes starting from the specified position from the list based on given count:
@ -1050,7 +1222,11 @@ There are a few methods to find specific nodes in a linked list.
#### find(predicate: ListIteratorFunction\<T\>): ListNode\<T\> | undefined
#### find(predicate)
```js
find(predicate: ListIteratorFunction\<T\>): ListNode\<T\> | undefined
```
Finds the first node from the list that matches the given predicate:
@ -1070,7 +1246,11 @@ found.next.value === "b"
#### findIndex(predicate: ListIteratorFunction\<T\>): number
#### findIndex(predicate)
```js
findIndex(predicate: ListIteratorFunction\<T\>): number
```
Finds the position of the first node from the list that matches the given predicate:
@ -1094,7 +1274,11 @@ i3 === -1
#### get(position: number): ListNode\<T\> | undefined
#### get(position)
```js
get(position: number): ListNode\<T\> | undefined
```
Finds and returns the node with specific position in the list:
@ -1114,7 +1298,11 @@ found.next.value === "c"
#### indexOf(value: T, compareFn = compare): number
#### indexOf(value, compareFn)
```js
indexOf(value: T, compareFn = compare): number
```
Finds the position of the first node from the list that has the given value:
@ -1145,11 +1333,11 @@ list.addTailMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]);
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3}
const i0 = indexOf({ x: 1 }, (v1, v2) => v1.x === v2.x);
const i1 = indexOf({ x: 2 }, (v1, v2) => v1.x === v2.x);
const i2 = indexOf({ x: 3 }, (v1, v2) => v1.x === v2.x);
const i3 = indexOf({ x: 0 }, (v1, v2) => v1.x === v2.x);
const i4 = indexOf({ x: 4 }, (v1, v2) => v1.x === v2.x);
const i0 = indexOf(1, (value, searchedValue) => value.x === searchedValue);
const i1 = indexOf(2, (value, searchedValue) => value.x === searchedValue);
const i2 = indexOf(3, (value, searchedValue) => value.x === searchedValue);
const i3 = indexOf(0, (value, searchedValue) => value.x === searchedValue);
const i4 = indexOf(4, (value, searchedValue) => value.x === searchedValue);
/*
i0 === 0
@ -1172,7 +1360,11 @@ There are a few ways to iterate over or display a linked list.
#### forEach(callback: ListIteratorFunction\<T\>): void
#### forEach(callback)
```js
forEach(callback: ListIteratorFunction\<T\>): void
```
Runs a callback function on all nodes in a linked list from head to tail:
@ -1210,7 +1402,11 @@ for(const node of list) {
#### toArray(): T\[\]
#### toArray()
```js
toArray(): T\[\]
```
Converts a linked list to an array of values:
@ -1228,7 +1424,11 @@ arr === ['a', 'b', 'c']
#### toNodeArray(): T\[\]
#### toNodeArray()
```js
toNodeArray(): T\[\]
```
Converts a linked list to an array of nodes:
@ -1248,7 +1448,11 @@ arr[2].value === 'a'
#### toString(): string
#### toString()
```js
toString(): string
```
Converts a linked list to a string representation of nodes and their relations:

21
docs/en/docs-nav.json

@ -75,6 +75,10 @@
}
]
},
{
"text": "\"How to\" Guides",
"path": "How-To/Index.md"
},
{
"text": "Migrating from the ASP.NET Boilerplate",
"path": "AspNet-Boilerplate-Migration-Guide.md"
@ -341,13 +345,26 @@
"text": "Custom Setting Page",
"path": "UI/Angular/Custom-Setting-Page.md"
},
{
"text": "Lazy Loading Scripts & Styles",
"path": "UI/Angular/Lazy-Load-Service.md"
},
{
"text": "DomInsertionService",
"path": "UI/Angular/Dom-Insertion-Service.md"
},
{
"text": "TrackByService",
"path": "UI/Angular/Track-By-Service.md"
},
}
]
},
{
"text": "Common",
"items": [
{
"text": "Linked List (Doubly)",
"path": "UI/Angular/Linked-List.md"
"path": "UI/Common/Utils/Linked-List.md"
}
]
}

BIN
docs/en/images/angular-folder-structure.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
docs/en/images/angular-template-structure-diagram.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
docs/en/images/react-native-folder-structure.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
docs/en/images/react-native-navigation-structure.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
docs/en/images/react-native-store-folder.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

4
docs/zh-Hans/API/Dynamic-CSharp-API-Clients.md

@ -69,7 +69,7 @@ public class MyClientAppModule : AbpModule
}
````
查看下面的"RemoteServiceOptions"章节获取更多详细配置.
查看下面的"AbpRemoteServiceOptions"章节获取更多详细配置.
## 使用
@ -104,7 +104,7 @@ public class MyService : ITransientDependency
## 配置
### RemoteServiceOptions
### AbpRemoteServiceOptions
默认情况下`AbpRemoteServiceOptions`从`appsettings.json`获取.或者,你可以使用`Configure`方法来设置或重写它.如:

47
docs/zh-Hans/Application-Services.md

@ -146,7 +146,6 @@ public interface IBookAppService : IApplicationService
`BookDto`是一个简单的[DTO](Data-Transfer-Objects.md)类, 定义如下:
````csharp
[AbpAutoMapFrom(typeof(Book))] //Defines the mapping
public class BookDto
{
public Guid Id { get; set; }
@ -159,7 +158,36 @@ public class BookDto
}
````
* `BookDto`定义了`[AbpAutoMapFrom(typeof(Book))]`属性来从创建对象映射Book到BookDto.
我们创建一个Automapper的[Profile](https://docs.automapper.org/en/stable/Configuration.html#profile-instances)类. 例如:
```csharp
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<Book, BookDto>();
}
}
```
然后使用`AbpAutoMapperOptions`注册配置文件:
````csharp
[DependsOn(typeof(AbpAutoMapperModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpAutoMapperOptions>(options =>
{
//Add all mappings defined in the assembly of the MyModule class
options.AddMaps<MyModule>();
});
}
}
````
`AddMaps` 注册给定类的程序集中所有的配置类,通常使用模块类. 它还会注册 [attribute 映射](https://docs.automapper.org/en/stable/Attribute-mapping.html). 更多信息请参考[对象到对象映射](Object-To-Object-Mapping.md)文档
然后你可以实现`GetAsync`方法. 如下所示:
@ -249,7 +277,6 @@ public interface ICrudAppService<
示例中使用的DTO类是`BookDto`和`CreateUpdateBookDto`:
````csharp
[AbpAutoMapFrom(typeof(Book))]
public class BookDto : AuditedEntityDto<Guid>
{
public string Name { get; set; }
@ -259,7 +286,6 @@ public class BookDto : AuditedEntityDto<Guid>
public float Price { get; set; }
}
[AbpAutoMapTo(typeof(Book))]
public class CreateUpdateBookDto
{
[Required]
@ -274,6 +300,19 @@ public class CreateUpdateBookDto
}
````
DTO类的[Profile](https://docs.automapper.org/en/stable/Configuration.html#profile-instances)类.
```csharp
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<Book, BookDto>();
CreateMap<CreateUpdateBookDto, Book>();
}
}
```
* `CreateUpdateBookDto`由创建和更新操作共享,但你也可以使用单独的DTO类.
最后`BookAppService`实现非常简单:

4
docs/zh-Hans/Authorization.md

@ -340,10 +340,10 @@ public class SystemAdminPermissionValueProvider : PermissionValueProvider
* `PermissionGrantResult.Prohibited` 禁止授权用户,任何一个授权值提供程序返回了 `Prohibited`, 那么其他的提供程序返回的值都不再重要.
* `PermissionGrantResult.Undefined` 代表当前无法确定是否授予或禁止权限, 返回`UnDefined`由其他权限值提供程序检查权限.
定义`Provider`后将其添加到 `PermissionOptions`,如下所示:
定义`Provider`后将其添加到 `AbpPermissionOptions`,如下所示:
````csharp
Configure<PermissionOptions>(options =>
Configure<AbpPermissionOptions>(options =>
{
options.ValueProviders.Add<SystemAdminPermissionValueProvider>();
});

4
docs/zh-Hans/Best-Practices/Entity-Framework-Core-Integration.md

@ -106,10 +106,10 @@ public static class IdentityDbContextModelBuilderExtensions
````
* **推荐** 为每个Enttiy映射调用 `b.ConfigureByConvention();`(如上所示).
* **推荐** 通过继承 `ModelBuilderConfigurationOptions` 来创建 **configuration Options** 类. 例如:
* **推荐** 通过继承 `AbpModelBuilderConfigurationOptions` 来创建 **configuration Options** 类. 例如:
````C#
public class IdentityModelBuilderConfigurationOptions : ModelBuilderConfigurationOptions
public class IdentityModelBuilderConfigurationOptions : AbpModelBuilderConfigurationOptions
{
public IdentityModelBuilderConfigurationOptions()
: base(AbpIdentityConsts.DefaultDbTablePrefix, AbpIdentityConsts.DefaultDbSchema)

6
docs/zh-Hans/Best-Practices/MongoDB-Integration.md

@ -1,4 +1,4 @@
## MongoDB 集成
## MongoDB 集成
* **推荐** 为每个模块定义一个独立的 `MongoDbContext` 接口与实现类.
@ -90,11 +90,11 @@ public static class AbpIdentityMongoDbContextExtensions
}
```
- **推荐** 通过继承 `MongoModelBuilderConfigurationOptions` 来创建 **configuration Options** 类. 示例:
- **推荐** 通过继承 `AbpMongoModelBuilderConfigurationOptions` 来创建 **configuration Options** 类. 示例:
```c#
public class IdentityMongoModelBuilderConfigurationOptions
: MongoModelBuilderConfigurationOptions
: AbpMongoModelBuilderConfigurationOptions
{
public IdentityMongoModelBuilderConfigurationOptions()
: base(AbpIdentityConsts.DefaultDbTablePrefix)

12
docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md

@ -115,7 +115,7 @@ public class MyWebModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<BundlingOptions>(options =>
Configure<AbpBundlingOptions>(options =>
{
options
.ScriptBundles
@ -153,7 +153,7 @@ public class MyWebExtensionModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<BundlingOptions>(options =>
Configure<AbpBundlingOptions>(options =>
{
options
.ScriptBundles
@ -191,7 +191,7 @@ public class MyExtensionGlobalStyleContributor : BundleContributor
然后你可以按照下面的代码使用这个贡献者:
````C#
services.Configure<BundlingOptions>(options =>
services.Configure<AbpBundlingOptions>(options =>
{
options
.ScriptBundles
@ -248,10 +248,10 @@ public class MyPrismjsStyleExtension : BundleContributor
}
````
然后你可以配置 `BundleContributorOptions` 去扩展已存在的 `PrismjsStyleBundleContributor`.
然后你可以配置 `AbpBundleContributorOptions` 去扩展已存在的 `PrismjsStyleBundleContributor`.
````csharp
Configure<BundleContributorOptions>(options =>
Configure<AbpBundleContributorOptions>(options =>
{
options
.Extensions<PrismjsStyleBundleContributor>()
@ -319,7 +319,7 @@ namespace MyCompany.MyProject
例如:
````c#
services.Configure<BundlingOptions>(options =>
services.Configure<AbpBundlingOptions>(options =>
{
options
.StyleBundles

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Popover/AbpPopoverTagHelper.cs

@ -18,6 +18,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Popover
public bool? Dismissible { get; set; }
public bool? Hoverable { get; set; }
public string AbpPopover { get; set; }
public string AbpPopoverRight { get; set; }

60
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Popover/AbpPopoverTagHelperService.cs

@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Popover
@ -13,34 +14,67 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Popover
SetDataPlacement(context, output);
SetPopoverData(context, output);
SetDataTriggerIfDismissible(context, output);
SetDataTriggerIfHoverable(context, output);
}
else
{
SetDisabled(context, output);
}
}
}
protected virtual void SetDisabled(TagHelperContext context, TagHelperOutput output)
{
var triggerAsHtml = TagHelper.Dismissible ?? false ? "datatrigger=\"focus\" " : "";
var triggerAsHtml = TagHelper.Dismissible ?? false ? "data-trigger=\"focus\" " : "";
if (TagHelper.Hoverable ?? false)
{
if (triggerAsHtml.Contains("focus"))
{
triggerAsHtml = triggerAsHtml.Replace("focus", "focus hover");
}
else
{
triggerAsHtml = "data-trigger=\"hover\" ";
}
}
var dataPlacementAsHtml = "data-placement=\"" + GetDirectory().ToString().ToLowerInvariant() + "\" ";
// data-placement="default" with data-trigger="focus" causes Cannot read property 'indexOf' of undefined at computeAutoPlacement(bootstrap.bundle.js?_v=637146714627330435:2185) error
if (IsDismissibleOrHoverable() && GetDirectory() == PopoverDirectory.Default)
{
//dataPlacementAsHtml = string.Empty; //bootstrap default placement is right, abp's is top.
dataPlacementAsHtml = dataPlacementAsHtml.Replace("default", "top");
}
var titleAttribute = output.Attributes.FirstOrDefault(at => at.Name == "title");
var titleAsHtml = titleAttribute == null ? "" : "title=\"" + titleAttribute.Value + "\" ";
var preElementHtml = "<span class=\"d-inline-block\" " + titleAsHtml + triggerAsHtml + dataPlacementAsHtml + "data-toggle=\"popover\" data-content=\"" + GetDataContent() + "\">";
var preElementHtml = "<span tabindex=\"0\" class=\"d-inline-block\" " + titleAsHtml + triggerAsHtml + dataPlacementAsHtml + "data-toggle=\"popover\" data-content=\"" + GetDataContent() + "\">";
var postElementHtml = "</span>";
output.PreElement.SetHtmlContent(preElementHtml);
output.PostElement.SetHtmlContent(postElementHtml);
output.Attributes.Add("style", "pointer-events: none;");
}
}
protected virtual void SetDataTriggerIfDismissible(TagHelperContext context, TagHelperOutput output)
{
if (TagHelper.Dismissible ?? false)
{
output.Attributes.Add("data-trigger", "focus");
}
}
protected virtual void SetDataTriggerIfHoverable(TagHelperContext context, TagHelperOutput output)
{
if (TagHelper.Hoverable ?? false)
{
//If already has focus data trigger
if (output.Attributes.TryGetAttribute("data-trigger", out _))
{
output.Attributes.SetAttribute(new TagHelperAttribute("data-trigger", "focus hover"));
}
else
{
output.Attributes.Add("data-trigger", "hover");
}
}
}
protected virtual void SetDataToggle(TagHelperContext context, TagHelperOutput output)
@ -101,5 +135,17 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Popover
return PopoverDirectory.Default;
}
protected virtual bool IsDismissibleOrHoverable()
{
if (TagHelper.Dismissible ?? false)
{
return true;
}
if (TagHelper.Hoverable ?? false)
{
return true;
}
return false;
}
}
}

5
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperResourceService.cs

@ -63,9 +63,10 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling.TagHelpers
foreach (var bundleFile in bundleFiles)
{
var file = WebContentFileProvider.GetFileInfo(bundleFile);
if (file == null)
if (file == null || !file.Exists)
{
throw new AbpException($"Could not find the bundle file from {nameof(IWebContentFileProvider)}");
throw new AbpException($"Could not find the bundle file '{bundleFile}' from {nameof(IWebContentFileProvider)}");
}
AddHtmlTag(context, output, bundleFile + "?_v=" + file.LastModified.UtcTicks);

11
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/TuiEditor/TuiEditorScriptContributor.cs

@ -17,7 +17,16 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.TuiEditor
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/libs/to-mark/to-mark.min.js");
context.Files.AddIfNotContains("/libs/tui-code-snippet/tui-code-snippet.min.js");
if (context.FileProvider.GetFileInfo("/libs/tui-code-snippet/tui-code-snippet.min.js").Exists)
{
context.Files.AddIfNotContains("/libs/tui-code-snippet/tui-code-snippet.min.js");
}
else
{
context.Files.AddIfNotContains("/libs/tui-code-snippet/tui-code-snippet.js");
}
context.Files.AddIfNotContains("/libs/squire-rte/squire.js");
context.Files.AddIfNotContains("/libs/tui-editor/tui-editor-Editor.min.js");
}

5
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Empty.cshtml

@ -39,7 +39,10 @@
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>@pageTitle</title>
@if (ViewBag.Description!=null)
{
<meta name="description" content="@ViewBag.Description" />
}
<abp-style-bundle name="@BasicThemeBundles.Styles.Global" />
@await Component.InvokeAsync(typeof(WidgetStylesViewComponent))

38
framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingHelper.cs

@ -86,6 +86,44 @@ namespace Volo.Abp.Auditing
return defaultValue;
}
public virtual bool IsEntityHistoryEnabled(Type entityType, bool defaultValue = false)
{
if (!entityType.IsPublic)
{
return false;
}
if (Options.IgnoredTypes.Any(t => t.IsAssignableFrom(entityType)))
{
return false;
}
if (entityType.IsDefined(typeof(AuditedAttribute), true))
{
return true;
}
foreach (var propertyInfo in entityType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if(propertyInfo.IsDefined(typeof(AuditedAttribute)))
{
return true;
}
}
if (entityType.IsDefined(typeof(DisableAuditingAttribute), true))
{
return false;
}
if (Options.EntityHistorySelectors.Any(selector => selector.Predicate(entityType)))
{
return true;
}
return defaultValue;
}
public virtual AuditLogInfo CreateAuditLogInfo()
{
var auditInfo = new AuditLogInfo

4
framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditingHelper.cs

@ -4,11 +4,13 @@ using System.Reflection;
namespace Volo.Abp.Auditing
{
//TODO: Move ShouldSaveAudit and rename to IAuditingFactory
//TODO: Move ShouldSaveAudit & IsEntityHistoryEnabled and rename to IAuditingFactory
public interface IAuditingHelper
{
bool ShouldSaveAudit(MethodInfo methodInfo, bool defaultValue = false);
bool IsEntityHistoryEnabled(Type entityType, bool defaultValue = false);
AuditLogInfo CreateAuditLogInfo();
AuditLogActionInfo CreateAuditLogAction(

25
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IPermissionDefinitionContext.cs

@ -1,4 +1,5 @@
using JetBrains.Annotations;
using System;
using JetBrains.Annotations;
using Volo.Abp.Localization;
using Volo.Abp.MultiTenancy;
@ -7,13 +8,35 @@ namespace Volo.Abp.Authorization.Permissions
public interface IPermissionDefinitionContext
{
//TODO: Add Get methods to find and modify a permission or group.
IServiceProvider ServiceProvider { get; }
/// <summary>
/// Gets a pre-defined permission group.
/// Throws <see cref="AbpException"/> if can not find the given group.
/// </summary>
/// <param name="name">Name of the group</param>
/// <returns></returns>
PermissionGroupDefinition GetGroup([NotNull] string name);
/// <summary>
/// Tries to get a pre-defined permission group.
/// Returns null if can not find the given group.
/// </summary>
/// <param name="name">Name of the group</param>
/// <returns></returns>
[NotNull]
PermissionGroupDefinition GetGroupOrNull(string name);
[CanBeNull]
PermissionGroupDefinition AddGroup(
[NotNull] string name,
ILocalizableString displayName = null,
MultiTenancySides multiTenancySide = MultiTenancySides.Both);
void RemoveGroup(string name);
[CanBeNull]
PermissionDefinition GetPermissionOrNull([NotNull] string name);
}
}

4
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IPermissionDefinitionProvider.cs

@ -2,6 +2,10 @@
{
public interface IPermissionDefinitionProvider
{
void PreDefine(IPermissionDefinitionContext context);
void Define(IPermissionDefinitionContext context);
void PostDefine(IPermissionDefinitionContext context);
}
}

9
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionChecker.cs

@ -32,12 +32,19 @@ namespace Volo.Abp.Authorization.Permissions
return await IsGrantedAsync(PrincipalAccessor.Principal, name);
}
public virtual async Task<bool> IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string name)
public virtual async Task<bool> IsGrantedAsync(
ClaimsPrincipal claimsPrincipal,
string name)
{
Check.NotNull(name, nameof(name));
var permission = PermissionDefinitionManager.Get(name);
if (!permission.IsEnabled)
{
return false;
}
var multiTenancySide = claimsPrincipal?.GetMultiTenancySide()
?? CurrentTenant.GetMultiTenancySide();

23
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs

@ -46,6 +46,19 @@ namespace Volo.Abp.Authorization.Permissions
/// </summary>
public Dictionary<string, object> Properties { get; }
/// <summary>
/// Indicates whether this permission is enabled or disabled.
/// A permission is normally enabled.
/// A disabled permission can not be granted to anyone, but it is still
/// will be available to check its value (while it will always be false).
///
/// Disabling a permission would be helpful to hide a related application
/// functionality from users/clients.
///
/// Default: true.
/// </summary>
public bool IsEnabled { get; set; }
/// <summary>
/// Gets/sets a key-value on the <see cref="Properties"/>.
/// </summary>
@ -63,11 +76,13 @@ namespace Volo.Abp.Authorization.Permissions
protected internal PermissionDefinition(
[NotNull] string name,
ILocalizableString displayName = null,
MultiTenancySides multiTenancySide = MultiTenancySides.Both)
MultiTenancySides multiTenancySide = MultiTenancySides.Both,
bool isEnabled = true)
{
Name = Check.NotNull(name, nameof(name));
DisplayName = displayName ?? new FixedLocalizableString(name);
MultiTenancySide = multiTenancySide;
IsEnabled = isEnabled;
Properties = new Dictionary<string, object>();
Providers = new List<string>();
@ -77,12 +92,14 @@ namespace Volo.Abp.Authorization.Permissions
public virtual PermissionDefinition AddChild(
[NotNull] string name,
ILocalizableString displayName = null,
MultiTenancySides multiTenancySide = MultiTenancySides.Both)
MultiTenancySides multiTenancySide = MultiTenancySides.Both,
bool isEnabled = true)
{
var child = new PermissionDefinition(
name,
displayName,
multiTenancySide)
multiTenancySide,
isEnabled)
{
Parent = this
};

41
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs

@ -1,4 +1,6 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Volo.Abp.Localization;
using Volo.Abp.MultiTenancy;
@ -6,10 +8,13 @@ namespace Volo.Abp.Authorization.Permissions
{
public class PermissionDefinitionContext : IPermissionDefinitionContext
{
public IServiceProvider ServiceProvider { get; }
internal Dictionary<string, PermissionGroupDefinition> Groups { get; }
internal PermissionDefinitionContext()
internal PermissionDefinitionContext(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
Groups = new Dictionary<string, PermissionGroupDefinition>();
}
@ -28,7 +33,20 @@ namespace Volo.Abp.Authorization.Permissions
return Groups[name] = new PermissionGroupDefinition(name, displayName, multiTenancySide);
}
public virtual PermissionGroupDefinition GetGroupOrNull(string name)
[NotNull]
public virtual PermissionGroupDefinition GetGroup([NotNull] string name)
{
var group = GetGroupOrNull(name);
if (group == null)
{
throw new AbpException($"Could not find a permission definition group with the given name: {name}");
}
return group;
}
public virtual PermissionGroupDefinition GetGroupOrNull([NotNull] string name)
{
Check.NotNull(name, nameof(name));
@ -51,5 +69,22 @@ namespace Volo.Abp.Authorization.Permissions
Groups.Remove(name);
}
public virtual PermissionDefinition GetPermissionOrNull([NotNull] string name)
{
Check.NotNull(name, nameof(name));
foreach (var groupDefinition in Groups.Values)
{
var permissionDefinition = groupDefinition.GetPermissionOrNull(name);
if (permissionDefinition != null)
{
return permissionDefinition;
}
}
return null;
}
}
}

34
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionContextExtensions.cs

@ -0,0 +1,34 @@
using JetBrains.Annotations;
namespace Volo.Abp.Authorization.Permissions
{
public static class PermissionDefinitionContextExtensions
{
/// <summary>
/// Finds and disables a permission with the given <paramref name="name"/>.
/// Returns false if given permission was not found.
/// </summary>
/// <param name="context">Permission definition context</param>
/// <param name="name">Name of the permission</param>
/// <returns>
/// Returns true if given permission was found.
/// Returns false if given permission was not found.
/// </returns>
public static bool TryDisablePermission(
[NotNull] this IPermissionDefinitionContext context,
[NotNull] string name)
{
Check.NotNull(context, nameof(context));
Check.NotNull(name, nameof(name));
var permission = context.GetPermissionOrNull(name);
if (permission == null)
{
return false;
}
permission.IsEnabled = false;
return true;
}
}
}

26
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionManager.cs

@ -83,7 +83,7 @@ namespace Volo.Abp.Authorization.Permissions
}
protected virtual void AddPermissionToDictionaryRecursively(
Dictionary<string, PermissionDefinition> permissions,
Dictionary<string, PermissionDefinition> permissions,
PermissionDefinition permission)
{
if (permissions.ContainsKey(permission.Name))
@ -101,22 +101,32 @@ namespace Volo.Abp.Authorization.Permissions
protected virtual Dictionary<string, PermissionGroupDefinition> CreatePermissionGroupDefinitions()
{
var context = new PermissionDefinitionContext();
using (var scope = _serviceProvider.CreateScope())
{
var context = new PermissionDefinitionContext(scope.ServiceProvider);
var providers = Options
.DefinitionProviders
.Select(p => scope.ServiceProvider.GetRequiredService(p) as IPermissionDefinitionProvider)
.ToList();
.DefinitionProviders
.Select(p => scope.ServiceProvider.GetRequiredService(p) as IPermissionDefinitionProvider)
.ToList();
foreach (var provider in providers)
{
provider.PreDefine(context);
}
foreach (var provider in providers)
{
provider.Define(context);
}
}
return context.Groups;
foreach (var provider in providers)
{
provider.PostDefine(context);
}
return context.Groups;
}
}
}
}

10
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionProvider.cs

@ -4,6 +4,16 @@ namespace Volo.Abp.Authorization.Permissions
{
public abstract class PermissionDefinitionProvider : IPermissionDefinitionProvider, ITransientDependency
{
public virtual void PreDefine(IPermissionDefinitionContext context)
{
}
public abstract void Define(IPermissionDefinitionContext context);
public virtual void PostDefine(IPermissionDefinitionContext context)
{
}
}
}

39
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionGroupDefinition.cs

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using JetBrains.Annotations;
using Volo.Abp.Localization;
using Volo.Abp.MultiTenancy;
@ -60,9 +61,15 @@ namespace Volo.Abp.Authorization.Permissions
public virtual PermissionDefinition AddPermission(
string name,
ILocalizableString displayName = null,
MultiTenancySides multiTenancySide = MultiTenancySides.Both)
MultiTenancySides multiTenancySide = MultiTenancySides.Both,
bool isEnabled = true)
{
var permission = new PermissionDefinition(name, displayName, multiTenancySide);
var permission = new PermissionDefinition(
name,
displayName,
multiTenancySide,
isEnabled
);
_permissions.Add(permission);
@ -95,5 +102,33 @@ namespace Volo.Abp.Authorization.Permissions
{
return $"[{nameof(PermissionGroupDefinition)} {Name}]";
}
[CanBeNull]
public PermissionDefinition GetPermissionOrNull([NotNull] string name)
{
Check.NotNull(name, nameof(name));
return GetPermissionOrNullRecursively(Permissions, name);
}
private PermissionDefinition GetPermissionOrNullRecursively(
IReadOnlyList<PermissionDefinition> permissions, string name)
{
foreach (var permission in permissions)
{
if (permission.Name == name)
{
return permission;
}
var childPermission = GetPermissionOrNullRecursively(permission.Children, name);
if (childPermission != null)
{
return childPermission;
}
}
return null;
}
}
}

9
framework/src/Volo.Abp.AutoMapper/AutoMapper/AbpAutoMapperExtensibleDtoExtensions.cs

@ -8,7 +8,8 @@ namespace AutoMapper
{
public static IMappingExpression<TSource, TDestination> MapExtraProperties<TSource, TDestination>(
this IMappingExpression<TSource, TDestination> mappingExpression,
MappingPropertyDefinitionChecks definitionChecks = MappingPropertyDefinitionChecks.Both)
MappingPropertyDefinitionChecks? definitionChecks = null,
string[] ignoredProperties = null)
where TDestination : IHasExtraProperties
where TSource : IHasExtraProperties
{
@ -22,10 +23,12 @@ namespace AutoMapper
? new Dictionary<string, object>()
: new Dictionary<string, object>(extraProps);
HasExtraPropertiesObjectExtendingExtensions
ExtensibleObjectMapper
.MapExtraPropertiesTo<TSource, TDestination>(
source.ExtraProperties,
result
result,
definitionChecks,
ignoredProperties
);
return result;

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

@ -73,6 +73,12 @@ namespace Volo.Abp.Cli.Commands
Logger.LogInformation("UI Framework: " + uiFramework);
}
var connectionString = GetConnectionString(commandLineArgs);
if (connectionString != null)
{
Logger.LogInformation("Connection string: " + connectionString);
}
var mobileApp = GetMobilePreference(commandLineArgs);
if (mobileApp != MobileApp.None)
{
@ -122,7 +128,8 @@ namespace Volo.Abp.Cli.Commands
mobileApp,
gitHubLocalRepositoryPath,
templateSource,
commandLineArgs.Options
commandLineArgs.Options,
connectionString
)
);
@ -168,6 +175,12 @@ namespace Volo.Abp.Cli.Commands
Logger.LogInformation($"'{projectName}' has been successfully created to '{outputFolder}'");
}
private static string GetConnectionString(CommandLineArgs commandLineArgs)
{
var connectionString = commandLineArgs.Options.GetOrNull(Options.ConnectionString.Short, Options.ConnectionString.Long);
return string.IsNullOrWhiteSpace(connectionString) ? null : connectionString;
}
public string GetUsageInfo()
{
var sb = new StringBuilder();
@ -205,6 +218,7 @@ namespace Volo.Abp.Cli.Commands
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 --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("");
sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI");
@ -309,6 +323,12 @@ namespace Volo.Abp.Cli.Commands
public const string Long = "template-source";
}
public static class ConnectionString
{
public const string Short = "cs";
public const string Long = "connection-string";
}
public static class CreateSolutionFolder
{
public const string Short = "csf";

76
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ConnectionStringChangeStep.cs

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Volo.Abp.Cli.ProjectBuilding.Files;
using Volo.Abp.Text;
namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps
{
public class ConnectionStringChangeStep : ProjectBuildPipelineStep
{
private const string DefaultConnectionStringKey = "Default";
public override void Execute(ProjectBuildContext context)
{
var appSettingsJsonFiles = context.Files.Where(f =>
f.Name.EndsWith("appsettings.json", StringComparison.OrdinalIgnoreCase))
.ToArray();
if (!appSettingsJsonFiles.Any())
{
return;
}
var newConnectionString = $"\"{DefaultConnectionStringKey}\": \"{context.BuildArgs.ConnectionString}\"";
foreach (var appSettingsJson in appSettingsJsonFiles)
{
try
{
var appSettingJsonContentWithoutBom = StringHelper.ConvertFromBytesWithoutBom(appSettingsJson.Bytes);
var jsonObject = JObject.Parse(appSettingJsonContentWithoutBom);
var connectionStringContainer = (JContainer)jsonObject?["ConnectionStrings"];
if (connectionStringContainer == null)
{
continue;
}
if (!connectionStringContainer.Any())
{
continue;
}
var connectionStrings = connectionStringContainer.ToList();
foreach (var connectionString in connectionStrings)
{
var property = ((JProperty)connectionString);
var connectionStringName = property.Name;
if (connectionStringName == DefaultConnectionStringKey)
{
var defaultConnectionString = property.ToString();
if (defaultConnectionString == null)
{
continue;
}
appSettingsJson.ReplaceText(defaultConnectionString, newConnectionString);
break;
}
}
}
catch (Exception ex)
{
Console.WriteLine("Cannot change the connection string in " + appSettingsJson.Name + ". Error: " + ex.Message);
}
}
}
}
}

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

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;
using Volo.Abp.Cli.ProjectBuilding.Files;
@ -109,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))
if (oldNodeIncludeValue.Contains(_projectName) && _entries.Any(e=>e.Name.EndsWith($"{oldNodeIncludeValue}.csproj")))
{
continue;
}

9
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/TemplateProjectBuildPipelineBuilder.cs

@ -1,6 +1,7 @@
using System;
using Volo.Abp.Cli.ProjectBuilding.Building.Steps;
using Volo.Abp.Cli.ProjectBuilding.Templates.App;
using Volo.Abp.Cli.ProjectBuilding.Templates.MvcModule;
namespace Volo.Abp.Cli.ProjectBuilding.Building
{
@ -18,7 +19,8 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building
pipeline.Steps.Add(new TemplateCodeDeleteStep());
pipeline.Steps.Add(new SolutionRenameStep());
if (context.Template.Name == AppProTemplate.TemplateName)
if (context.Template.Name == AppProTemplate.TemplateName ||
context.Template.Name == ModuleProTemplate.TemplateName)
{
pipeline.Steps.Add(new LicenseCodeReplaceStep());
}
@ -28,6 +30,11 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building
pipeline.Steps.Add(new RemoveRootFolderStep());
}
if (context.BuildArgs.ConnectionString != null)
{
pipeline.Steps.Add(new ConnectionStringChangeStep());
}
pipeline.Steps.Add(new CreateProjectResultZipStep());
return pipeline;

7
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/ProjectBuildArgs.cs

@ -27,6 +27,9 @@ namespace Volo.Abp.Cli.ProjectBuilding
[CanBeNull]
public string TemplateSource { get; set; }
[CanBeNull]
public string ConnectionString { get; set; }
[NotNull]
public Dictionary<string, string> ExtraProperties { get; set; }
@ -39,7 +42,8 @@ namespace Volo.Abp.Cli.ProjectBuilding
MobileApp? mobileApp = null,
[CanBeNull] string abpGitHubLocalRepositoryPath = null,
[CanBeNull] string templateSource = null,
Dictionary<string, string> extraProperties = null)
Dictionary<string, string> extraProperties = null,
[CanBeNull] string connectionString = null)
{
SolutionName = Check.NotNull(solutionName, nameof(solutionName));
TemplateName = templateName;
@ -50,6 +54,7 @@ namespace Volo.Abp.Cli.ProjectBuilding
AbpGitHubLocalRepositoryPath = abpGitHubLocalRepositoryPath;
TemplateSource = templateSource;
ExtraProperties = extraProperties ?? new Dictionary<string, string>();
ConnectionString = connectionString;
}
}
}

10
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/AngularModuleSourceCodeAdder.cs

@ -127,11 +127,17 @@ namespace Volo.Abp.Cli.ProjectModification
{
var jestConfigPath = Path.Combine(angularProjectsPath, project, "jest.config.js");
File.Delete(jestConfigPath);
if (File.Exists(jestConfigPath))
{
File.Delete(jestConfigPath);
}
var testPath = Path.Combine(angularProjectsPath, project, "src", "test.ts");
File.Delete(testPath);
if (File.Exists(testPath))
{
File.Delete(testPath);
}
}
}

23
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@ -51,9 +52,14 @@ namespace Volo.Abp.Cli.ProjectModification
{
var fileDirectory = Path.GetDirectoryName(file).EnsureEndsWith(Path.DirectorySeparatorChar);
if (IsAngularProject(fileDirectory))
{
CreateNpmrcFile(Path.GetDirectoryName(file));
}
RunYarn(fileDirectory);
if (IsAngularProject(fileDirectory) == false)
if (!IsAngularProject(fileDirectory))
{
Thread.Sleep(500);
RunGulp(fileDirectory);
@ -62,6 +68,21 @@ namespace Volo.Abp.Cli.ProjectModification
}
}
private void CreateNpmrcFile(string directoryName)
{
var fileName = Path.Combine(directoryName, ".npmrc");
if (File.Exists(fileName))
{
return;
}
using var fs = File.Create(fileName);
var content = new UTF8Encoding(true).GetBytes("@abp:registry:https://www.myget.org/F/abp-nightly/npm");
fs.Write(content, 0, content.Length);
}
private bool IsAngularProject(string fileDirectory)
{
return File.Exists(Path.Combine(fileDirectory, "angular.json"));

5
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NugetPackageToLocalReferenceConverter.cs

@ -43,7 +43,7 @@ namespace Volo.Abp.Cli.ProjectModification
private string ProcessReferenceNodes(string folder, XmlDocument doc, List<NugetPackageInfoWithModuleName> nugetPackageList, string localPathPrefix, string sourceFile = "src")
{
var nodes = doc.SelectNodes("/Project/ItemGroup/PackageReference[starts-with(@Include, 'Volo.Abp')]");
var nodes = doc.SelectNodes("/Project/ItemGroup/PackageReference[starts-with(@Include, 'Volo.')]");
if (nodes == null)
{
@ -66,7 +66,8 @@ namespace Volo.Abp.Cli.ProjectModification
moduleName = Directory.GetParent(Directory.GetParent(Path.GetDirectoryName(localProject)).FullName).Name;
if (oldNodeIncludeValue.EndsWith(".test", StringComparison.InvariantCultureIgnoreCase) ||
oldNodeIncludeValue.EndsWith(".tests", StringComparison.InvariantCultureIgnoreCase))
oldNodeIncludeValue.EndsWith(".tests", StringComparison.InvariantCultureIgnoreCase) ||
oldNodeIncludeValue.EndsWith(".testbase", StringComparison.InvariantCultureIgnoreCase))
{
tempSourceFile = "test";
}

29
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs

@ -87,24 +87,39 @@ namespace Volo.Abp.Cli.ProjectModification
await SolutionFileModifier.AddModuleToSolutionFileAsync(module, solutionFile);
await NugetPackageToLocalReferenceConverter.Convert(module, solutionFile);
await HandleAngularProject(module, solutionFile);
await HandleAngularProject(modulesFolderInSolution, solutionFile);
}
ModifyDbContext(projectFiles, module, startupProject, skipDbMigrations);
}
private async Task HandleAngularProject(ModuleWithMastersInfo module, string solutionFilePath)
private async Task HandleAngularProject(string modulesFolderInSolution, string solutionFilePath)
{
var angularPath = Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(solutionFilePath)), "angular");
if (!Directory.Exists(angularPath))
{
DeleteAngularDirectoriesInModulesFolder(modulesFolderInSolution);
return;
}
await AngularModuleSourceCodeAdder.AddAsync(solutionFilePath, angularPath);
}
private static void DeleteAngularDirectoriesInModulesFolder(string modulesFolderInSolution)
{
var moduleFolders = Directory.GetDirectories(modulesFolderInSolution);
foreach (var moduleFolder in moduleFolders)
{
var angDir = Path.Combine(moduleFolder, "angular");
if (Directory.Exists(angDir))
{
Directory.Delete(angDir, true);
}
}
}
private async Task DownloadSourceCodesToSolutionFolder(ModuleWithMastersInfo module, string modulesFolderInSolution, string version = null)
{
var targetModuleFolder = Path.Combine(modulesFolderInSolution, module.Name);
@ -117,7 +132,7 @@ namespace Volo.Abp.Cli.ProjectModification
null
);
await DeleteAppFolderAsync(targetModuleFolder);
await DeleteAppAndDemoFolderAsync(targetModuleFolder);
if (module.MasterModuleInfos == null)
{
@ -130,13 +145,19 @@ namespace Volo.Abp.Cli.ProjectModification
}
}
private async Task DeleteAppFolderAsync(string targetModuleFolder)
private async Task DeleteAppAndDemoFolderAsync(string targetModuleFolder)
{
var appFolder = Path.Combine(targetModuleFolder, "app");
if (Directory.Exists(appFolder))
{
Directory.Delete(appFolder, true);
}
var demoFolder = Path.Combine(targetModuleFolder, "demo");
if (Directory.Exists(demoFolder))
{
Directory.Delete(demoFolder, true);
}
}
private async Task AddNugetAndNpmReferences(ModuleWithMastersInfo module, string[] projectFiles)

5
framework/src/Volo.Abp.Core/Microsoft/Extensions/Configuration/ConfigurationHelper.cs

@ -6,7 +6,8 @@ namespace Microsoft.Extensions.Configuration
public static class ConfigurationHelper
{
public static IConfigurationRoot BuildConfiguration(
AbpConfigurationBuilderOptions options = null)
AbpConfigurationBuilderOptions options = null,
Action<IConfigurationBuilder> builderAction = null)
{
options = options ?? new AbpConfigurationBuilderOptions();
@ -43,6 +44,8 @@ namespace Microsoft.Extensions.Configuration
builder = builder.AddCommandLine(options.CommandLineArgs);
}
builderAction?.Invoke(builder);
return builder.Build();
}
}

30
framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifier.cs

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.DependencyInjection;
@ -12,11 +12,11 @@ namespace Volo.Abp.ExceptionHandling
{
public ILogger<ExceptionNotifier> Logger { get; set; }
protected IEnumerable<IExceptionSubscriber> ExceptionSubscribers { get; }
protected IHybridServiceScopeFactory ServiceScopeFactory { get; }
public ExceptionNotifier(IEnumerable<IExceptionSubscriber> exceptionSubscribers)
public ExceptionNotifier(IHybridServiceScopeFactory serviceScopeFactory)
{
ExceptionSubscribers = exceptionSubscribers;
ServiceScopeFactory = serviceScopeFactory;
Logger = NullLogger<ExceptionNotifier>.Instance;
}
@ -24,16 +24,22 @@ namespace Volo.Abp.ExceptionHandling
{
Check.NotNull(context, nameof(context));
foreach (var exceptionSubscriber in ExceptionSubscribers)
using (var scope = ServiceScopeFactory.CreateScope())
{
try
{
await exceptionSubscriber.HandleAsync(context);
}
catch (Exception e)
var exceptionSubscribers = scope.ServiceProvider
.GetServices<IExceptionSubscriber>();
foreach (var exceptionSubscriber in exceptionSubscribers)
{
Logger.LogWarning($"Exception subscriber of type {exceptionSubscriber.GetType().AssemblyQualifiedName} has thrown an exception!");
Logger.LogException(e, LogLevel.Warning);
try
{
await exceptionSubscriber.HandleAsync(context);
}
catch (Exception e)
{
Logger.LogWarning($"Exception subscriber of type {exceptionSubscriber.GetType().AssemblyQualifiedName} has thrown an exception!");
Logger.LogException(e, LogLevel.Warning);
}
}
}
}

19
framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/NullExceptionNotifier.cs

@ -0,0 +1,19 @@
using System.Threading.Tasks;
namespace Volo.Abp.ExceptionHandling
{
public class NullExceptionNotifier : IExceptionNotifier
{
public static NullExceptionNotifier Instance { get; } = new NullExceptionNotifier();
private NullExceptionNotifier()
{
}
public Task NotifyAsync(ExceptionNotificationContext context)
{
return Task.CompletedTask;
}
}
}

61
framework/src/Volo.Abp.Core/Volo/Abp/IO/FileHelper.cs

@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Volo.Abp.Text;
namespace Volo.Abp.IO
{
@ -73,6 +75,63 @@ namespace Volo.Abp.IO
}
}
//TODO: ReadAllLinesAsync
/// <summary>
/// Opens a text file, reads all lines of the file, and then closes the file.
/// </summary>
/// <param name="path">The file to open for reading.</param>
/// <param name="encoding">Encoding of the file. Default is UTF8</param>
/// <param name="fileMode">Specifies how the operating system should open a file. Default is Open</param>
/// <param name="fileAccess">Defines constants for read, write, or read/write access to a file. Default is Read</param>
/// <param name="fileShare">Contains constants for controlling the kind of access other FileStream objects can have to the same file. Default is Read</param>
/// <param name="bufferSize">Length of StreamReader buffer. Default is 4096.</param>
/// <param name="fileOptions">Indicates FileStream options. Default is Asynchronous (The file is to be used for asynchronous reading.) and SequentialScan (The file is to be accessed sequentially from beginning to end.) </param>
/// <returns>A string containing all lines of the file.</returns>
public static async Task<string[]> ReadAllLinesAsync(string path,
Encoding encoding = null,
FileMode fileMode = FileMode.Open,
FileAccess fileAccess = FileAccess.Read,
FileShare fileShare = FileShare.Read,
int bufferSize = 4096,
FileOptions fileOptions = FileOptions.Asynchronous | FileOptions.SequentialScan)
{
if (encoding == null)
{
encoding = Encoding.UTF8;
}
var lines = new List<string>();
using (var stream = new FileStream(
path,
fileMode,
fileAccess,
fileShare,
bufferSize,
fileOptions))
{
using (var reader = new StreamReader(stream, encoding))
{
string line;
while ((line = await reader.ReadLineAsync()) != null)
{
lines.Add(line);
}
}
}
return lines.ToArray();
}
/// <summary>
/// Opens a text file, reads content without BOM
/// </summary>
/// <param name="path">The file to open for reading.</param>
/// <returns>A string containing all lines of the file.</returns>
public static async Task<string> ReadFileWithoutBomAsync(string path)
{
var content = await ReadAllBytesAsync(path);
return StringHelper.ConvertFromBytesWithoutBom(content);
}
}
}

12
framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs

@ -1,7 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
@ -15,7 +14,7 @@ namespace Volo.Abp.Reflection
{
return false;
}
var type = obj.GetType();
if (!type.GetTypeInfo().IsGenericType)
{
@ -37,9 +36,7 @@ namespace Volo.Abp.Reflection
return true;
}
if (includeNullables &&
type.IsGenericType &&
type.GetGenericTypeDefinition() == typeof(Nullable<>))
if (includeNullables && IsNullable(type))
{
return IsPrimitiveExtendedInternal(type.GenericTypeArguments[0], includeEnums);
}
@ -47,6 +44,11 @@ namespace Volo.Abp.Reflection
return false;
}
public static bool IsNullable(Type type)
{
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
}
public static Type GetFirstGenericArgumentIfNullable(this Type t)
{
if (t.GetGenericArguments().Length > 0 && t.GetGenericTypeDefinition() == typeof(Nullable<>))

37
framework/src/Volo.Abp.Core/Volo/Abp/Text/StringHelper.cs

@ -0,0 +1,37 @@
using System.Text;
namespace Volo.Abp.Text
{
public class StringHelper
{
/// <summary>
/// Converts a byte[] to string without BOM (byte order mark).
/// </summary>
/// <param name="bytes">The byte[] to be converted to string</param>
/// <param name="encoding">The encoding to get string. Default is UTF8</param>
/// <returns></returns>
public static string ConvertFromBytesWithoutBom(byte[] bytes, Encoding encoding = null)
{
if (bytes == null)
{
return null;
}
if (encoding == null)
{
encoding = Encoding.UTF8;
}
var hasBom = bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF;
if (hasBom)
{
return encoding.GetString(bytes, 3, bytes.Length - 3);
}
else
{
return encoding.GetString(bytes);
}
}
}
}

36
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
@ -15,7 +14,6 @@ using Volo.Abp.Json;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Reflection;
using Volo.Abp.Timing;
using Volo.Abp.Uow;
namespace Volo.Abp.EntityFrameworkCore.EntityHistory
{
@ -26,6 +24,7 @@ namespace Volo.Abp.EntityFrameworkCore.EntityHistory
protected IAuditingStore AuditingStore { get; }
protected IJsonSerializer JsonSerializer { get; }
protected AbpAuditingOptions Options { get; }
protected IAuditingHelper AuditingHelper { get; }
private readonly IClock _clock;
@ -33,11 +32,13 @@ namespace Volo.Abp.EntityFrameworkCore.EntityHistory
IAuditingStore auditingStore,
IOptions<AbpAuditingOptions> options,
IClock clock,
IJsonSerializer jsonSerializer)
IJsonSerializer jsonSerializer,
IAuditingHelper auditingHelper)
{
_clock = clock;
AuditingStore = auditingStore;
JsonSerializer = jsonSerializer;
AuditingHelper = auditingHelper;
Options = options.Value;
Logger = NullLogger<EntityHistoryHelper>.Instance;
@ -203,39 +204,14 @@ namespace Volo.Abp.EntityFrameworkCore.EntityHistory
return false;
}
if (Options.IgnoredTypes.Any(t => t.IsInstanceOfType(entityEntry.Entity)))
{
return false;
}
var entityType = entityEntry.Metadata.ClrType;
var entityType = entityEntry.Entity.GetType();
if (!EntityHelper.IsEntity(entityType))
{
return false;
}
if (!entityType.IsPublic)
{
return false;
}
if (entityType.IsDefined(typeof(AuditedAttribute), true))
{
return true;
}
if (entityEntry.Metadata.GetProperties()
.Any(p => p.PropertyInfo?.IsDefined(typeof(AuditedAttribute)) ?? false))
{
return true;
}
if (entityType.IsDefined(typeof(DisableAuditingAttribute), true))
{
return false;
}
if (Options.EntityHistorySelectors.Any(selector => selector.Predicate(entityType)))
if (AuditingHelper.IsEntityHistoryEnabled(entityType))
{
return true;
}

15
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs

@ -56,7 +56,7 @@ namespace Volo.Abp.Http.Client.DynamicProxying
IJsonSerializer jsonSerializer,
IRemoteServiceHttpClientAuthenticator clientAuthenticator,
ICancellationTokenProvider cancellationTokenProvider,
ICorrelationIdProvider correlationIdProvider,
ICorrelationIdProvider correlationIdProvider,
IOptions<AbpCorrelationIdOptions> correlationIdOptions,
ICurrentTenant currentTenant)
{
@ -110,7 +110,14 @@ namespace Volo.Abp.Http.Client.DynamicProxying
//TODO: Think on that
if (TypeHelper.IsPrimitiveExtended(typeof(T), true))
{
return (T)Convert.ChangeType(responseAsString, typeof(T));
if (typeof(DateTime).IsAssignableFrom(typeof(T)))
{
return (T)(object)DateTime.Parse(responseAsString.Trim('\"'), CultureInfo.InvariantCulture);
}
else
{
return (T)Convert.ChangeType(responseAsString, typeof(T));
}
}
return JsonSerializer.Deserialize<T>(responseAsString);
@ -151,8 +158,8 @@ namespace Volo.Abp.Http.Client.DynamicProxying
}
return await response.Content.ReadAsStringAsync();
}
}
private ApiVersionInfo GetApiVersionInfo(ActionApiDescriptionModel action)
{
var apiVersion = FindBestApiVersion(action);

22
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/UrlBuilder.cs

@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using JetBrains.Annotations;
using Volo.Abp.Http.Modeling;
using Volo.Abp.Http.ProxyScripting.Generators;
using Volo.Abp.Reflection;
namespace Volo.Abp.Http.Client.DynamicProxying
{
@ -88,10 +91,25 @@ namespace Volo.Abp.Http.Client.DynamicProxying
}
}
private static void AddQueryStringParameter(StringBuilder urlBuilder, bool isFirstParam, string name, object value)
private static void AddQueryStringParameter(
StringBuilder urlBuilder,
bool isFirstParam,
string name,
[NotNull] object value)
{
urlBuilder.Append(isFirstParam ? "?" : "&");
urlBuilder.Append(name + "=" + System.Net.WebUtility.UrlEncode(value.ToString()));
urlBuilder.Append(name + "=" + System.Net.WebUtility.UrlEncode(ConvertValueToString(value)));
}
private static string ConvertValueToString([NotNull] object value)
{
if (value is DateTime dateTimeValue)
{
return dateTimeValue.ToUniversalTime().ToString("u");
}
return value.ToString();
}
}
}

10
framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelAuthenticationService.cs

@ -70,9 +70,17 @@ namespace Volo.Abp.IdentityModel
}
var tokenResponse = await GetTokenResponse(discoveryResponse, configuration);
if (tokenResponse.IsError)
{
throw new AbpException($"Could not get token from the OpenId Connect server! ErrorType: {tokenResponse.ErrorType}. Error: {tokenResponse.Error}. ErrorDescription: {tokenResponse.ErrorDescription}. HttpStatusCode: {tokenResponse.HttpStatusCode}");
if (tokenResponse.ErrorDescription != null)
{
throw new AbpException($"Could not get token from the OpenId Connect server! ErrorType: {tokenResponse.ErrorType}. Error: {tokenResponse.Error}. ErrorDescription: {tokenResponse.ErrorDescription}. HttpStatusCode: {tokenResponse.HttpStatusCode}");
}
var rawError = tokenResponse.Raw;
var withoutInnerException = rawError.Split(new string[] { "<eof/>" }, StringSplitOptions.RemoveEmptyEntries);
throw new AbpException(withoutInnerException[0]);
}
return tokenResponse.AccessToken;

5
framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs

@ -369,10 +369,7 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
bool autoSave = false,
CancellationToken cancellationToken = default)
{
return Collection.DeleteOneAsync(
CreateEntityFilter(id),
GetCancellationToken(cancellationToken)
);
return DeleteAsync(x => x.Id.Equals(id), autoSave, cancellationToken);
}
protected override FilterDefinition<TEntity> CreateEntityFilter(TEntity entity, bool withConcurrencyStamp = false, string concurrencyStamp = null)

237
framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtendedObjectMapper.cs

@ -0,0 +1,237 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Volo.Abp.Data;
namespace Volo.Abp.ObjectExtending
{
public static class ExtensibleObjectMapper
{
/// <summary>
/// Copies extra properties from the <paramref name="source"/> object
/// to the <paramref name="destination"/> object.
///
/// Checks property definitions (over the <see cref="ObjectExtensionManager"/>)
/// based on the <paramref name="definitionChecks"/> preference.
/// </summary>
/// <typeparam name="TSource">Source class type</typeparam>
/// <typeparam name="TDestination">Destination class type</typeparam>
/// <param name="source">The source object</param>
/// <param name="destination">The destination object</param>
/// <param name="definitionChecks">
/// Controls which properties to map.
/// </param>
/// <param name="ignoredProperties">Used to ignore some properties</param>
public static void MapExtraPropertiesTo<TSource, TDestination>(
[NotNull] TSource source,
[NotNull] TDestination destination,
MappingPropertyDefinitionChecks? definitionChecks = null,
string[] ignoredProperties = null)
where TSource : IHasExtraProperties
where TDestination : IHasExtraProperties
{
Check.NotNull(source, nameof(source));
Check.NotNull(destination, nameof(destination));
ExtensibleObjectMapper.MapExtraPropertiesTo(
typeof(TSource),
typeof(TDestination),
source.ExtraProperties,
destination.ExtraProperties,
definitionChecks,
ignoredProperties
);
}
/// <summary>
/// Copies extra properties from the <paramref name="sourceDictionary"/> object
/// to the <paramref name="destinationDictionary"/> object.
///
/// Checks property definitions (over the <see cref="ObjectExtensionManager"/>)
/// based on the <paramref name="definitionChecks"/> preference.
/// </summary>
/// <typeparam name="TSource">Source class type (for definition check)</typeparam>
/// <typeparam name="TDestination">Destination class type (for definition check)</typeparam>
/// <param name="sourceDictionary">The source dictionary object</param>
/// <param name="destinationDictionary">The destination dictionary object</param>
/// <param name="definitionChecks">
/// Controls which properties to map.
/// </param>
/// <param name="ignoredProperties">Used to ignore some properties</param>
public static void MapExtraPropertiesTo<TSource, TDestination>(
[NotNull] Dictionary<string, object> sourceDictionary,
[NotNull] Dictionary<string, object> destinationDictionary,
MappingPropertyDefinitionChecks? definitionChecks = null,
string[] ignoredProperties = null)
where TSource : IHasExtraProperties
where TDestination : IHasExtraProperties
{
MapExtraPropertiesTo(
typeof(TSource),
typeof(TDestination),
sourceDictionary,
destinationDictionary,
definitionChecks,
ignoredProperties
);
}
/// <summary>
/// Copies extra properties from the <paramref name="sourceDictionary"/> object
/// to the <paramref name="destinationDictionary"/> object.
///
/// Checks property definitions (over the <see cref="ObjectExtensionManager"/>)
/// based on the <paramref name="definitionChecks"/> preference.
/// </summary>
/// <param name="sourceType">Source type (for definition check)</param>
/// <param name="destinationType">Destination class type (for definition check)</param>
/// <param name="sourceDictionary">The source dictionary object</param>
/// <param name="destinationDictionary">The destination dictionary object</param>
/// <param name="definitionChecks">
/// Controls which properties to map.
/// </param>
/// <param name="ignoredProperties">Used to ignore some properties</param>
public static void MapExtraPropertiesTo(
[NotNull] Type sourceType,
[NotNull] Type destinationType,
[NotNull] Dictionary<string, object> sourceDictionary,
[NotNull] Dictionary<string, object> destinationDictionary,
MappingPropertyDefinitionChecks? definitionChecks = null,
string[] ignoredProperties = null)
{
Check.AssignableTo<IHasExtraProperties>(sourceType, nameof(sourceType));
Check.AssignableTo<IHasExtraProperties>(destinationType, nameof(destinationType));
Check.NotNull(sourceDictionary, nameof(sourceDictionary));
Check.NotNull(destinationDictionary, nameof(destinationDictionary));
var sourceObjectExtension = ObjectExtensionManager.Instance.GetOrNull(sourceType);
var destinationObjectExtension = ObjectExtensionManager.Instance.GetOrNull(destinationType);
foreach (var keyValue in sourceDictionary)
{
if (CanMapProperty(
keyValue.Key,
sourceObjectExtension,
destinationObjectExtension,
definitionChecks,
ignoredProperties))
{
destinationDictionary[keyValue.Key] = keyValue.Value;
}
}
}
//TODO: Move these methods to a class like ObjectExtensionHelper
public static bool CanMapProperty<TSource, TDestination>(
[NotNull] string propertyName,
MappingPropertyDefinitionChecks? definitionChecks = null,
string[] ignoredProperties = null)
{
return CanMapProperty(
typeof(TSource),
typeof(TDestination),
propertyName,
definitionChecks,
ignoredProperties
);
}
public static bool CanMapProperty(
[NotNull] Type sourceType,
[NotNull] Type destinationType,
[NotNull] string propertyName,
MappingPropertyDefinitionChecks? definitionChecks = null,
string[] ignoredProperties = null)
{
Check.AssignableTo<IHasExtraProperties>(sourceType, nameof(sourceType));
Check.AssignableTo<IHasExtraProperties>(destinationType, nameof(destinationType));
Check.NotNull(propertyName, nameof(propertyName));
var sourceObjectExtension = ObjectExtensionManager.Instance.GetOrNull(sourceType);
var destinationObjectExtension = ObjectExtensionManager.Instance.GetOrNull(destinationType);
return CanMapProperty(
propertyName,
sourceObjectExtension,
destinationObjectExtension,
definitionChecks,
ignoredProperties);
}
private static bool CanMapProperty(
[NotNull] string propertyName,
[CanBeNull] ObjectExtensionInfo sourceObjectExtension,
[CanBeNull] ObjectExtensionInfo destinationObjectExtension,
MappingPropertyDefinitionChecks? definitionChecks = null,
string[] ignoredProperties = null)
{
Check.NotNull(propertyName, nameof(propertyName));
if (ignoredProperties != null &&
ignoredProperties.Contains(propertyName))
{
return false;
}
if (definitionChecks != null)
{
if (definitionChecks.Value.HasFlag(MappingPropertyDefinitionChecks.Source))
{
if (sourceObjectExtension == null)
{
return false;
}
if (!sourceObjectExtension.HasProperty(propertyName))
{
return false;
}
}
if (definitionChecks.Value.HasFlag(MappingPropertyDefinitionChecks.Destination))
{
if (destinationObjectExtension == null)
{
return false;
}
if (!destinationObjectExtension.HasProperty(propertyName))
{
return false;
}
}
return true;
}
else
{
var sourcePropertyDefinition = sourceObjectExtension?.GetPropertyOrNull(propertyName);
var destinationPropertyDefinition = destinationObjectExtension?.GetPropertyOrNull(propertyName);
if (sourcePropertyDefinition != null)
{
if (destinationPropertyDefinition != null)
{
return true;
}
if (sourcePropertyDefinition.CheckPairDefinitionOnMapping == false)
{
return true;
}
}
else if (destinationPropertyDefinition != null)
{
if (destinationPropertyDefinition.CheckPairDefinitionOnMapping == false)
{
return true;
}
}
return false;
}
}
}
}

160
framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/HasExtraPropertiesObjectExtendingExtensions.cs

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using JetBrains.Annotations;
using JetBrains.Annotations;
using Volo.Abp.Data;
namespace Volo.Abp.ObjectExtending
@ -11,7 +8,7 @@ namespace Volo.Abp.ObjectExtending
/// <summary>
/// Copies extra properties from the <paramref name="source"/> object
/// to the <paramref name="destination"/> object.
///
///
/// Checks property definitions (over the <see cref="ObjectExtensionManager"/>)
/// based on the <paramref name="definitionChecks"/> preference.
/// </summary>
@ -20,156 +17,23 @@ namespace Volo.Abp.ObjectExtending
/// <param name="source">The source object</param>
/// <param name="destination">The destination object</param>
/// <param name="definitionChecks">
/// Controls which properties to map.
/// Controls which properties to map.
/// </param>
/// <param name="ignoredProperties">Used to ignore some properties</param>
public static void MapExtraPropertiesTo<TSource, TDestination>(
[NotNull] this TSource source,
[NotNull] TDestination destination,
MappingPropertyDefinitionChecks definitionChecks = MappingPropertyDefinitionChecks.Both)
where TSource : IHasExtraProperties
where TDestination : IHasExtraProperties
{
Check.NotNull(source, nameof(source));
Check.NotNull(destination, nameof(destination));
MapExtraPropertiesTo(
typeof(TSource),
typeof(TDestination),
source.ExtraProperties,
destination.ExtraProperties,
definitionChecks
);
}
/// <summary>
/// Copies extra properties from the <paramref name="sourceDictionary"/> object
/// to the <paramref name="destinationDictionary"/> object.
///
/// Checks property definitions (over the <see cref="ObjectExtensionManager"/>)
/// based on the <paramref name="definitionChecks"/> preference.
/// </summary>
/// <typeparam name="TSource">Source class type (for definition check)</typeparam>
/// <typeparam name="TDestination">Destination class type (for definition check)</typeparam>
/// <param name="sourceDictionary">The source dictionary object</param>
/// <param name="destinationDictionary">The destination dictionary object</param>
/// <param name="definitionChecks">
/// Controls which properties to map.
/// </param>
public static void MapExtraPropertiesTo<TSource, TDestination>(
[NotNull] Dictionary<string, object> sourceDictionary,
[NotNull] Dictionary<string, object> destinationDictionary,
MappingPropertyDefinitionChecks definitionChecks = MappingPropertyDefinitionChecks.Both)
MappingPropertyDefinitionChecks? definitionChecks = null,
string[] ignoredProperties = null)
where TSource : IHasExtraProperties
where TDestination : IHasExtraProperties
{
MapExtraPropertiesTo(
typeof(TSource),
typeof(TDestination),
sourceDictionary,
destinationDictionary,
definitionChecks
ExtensibleObjectMapper.MapExtraPropertiesTo(
source,
destination,
definitionChecks,
ignoredProperties
);
}
/// <summary>
/// Copies extra properties from the <paramref name="sourceDictionary"/> object
/// to the <paramref name="destinationDictionary"/> object.
///
/// Checks property definitions (over the <see cref="ObjectExtensionManager"/>)
/// based on the <paramref name="definitionChecks"/> preference.
/// </summary>
/// <param name="sourceType">Source type (for definition check)</param>
/// <param name="destinationType">Destination class type (for definition check)</param>
/// <param name="sourceDictionary">The source dictionary object</param>
/// <param name="destinationDictionary">The destination dictionary object</param>
/// <param name="definitionChecks">
/// Controls which properties to map.
/// </param>
public static void MapExtraPropertiesTo(
[NotNull] Type sourceType,
[NotNull] Type destinationType,
[NotNull] Dictionary<string, object> sourceDictionary,
[NotNull] Dictionary<string, object> destinationDictionary,
MappingPropertyDefinitionChecks definitionChecks = MappingPropertyDefinitionChecks.Both)
{
Check.AssignableTo<IHasExtraProperties>(sourceType, nameof(sourceType));
Check.AssignableTo<IHasExtraProperties>(destinationType, nameof(destinationType));
Check.NotNull(sourceDictionary, nameof(sourceDictionary));
Check.NotNull(destinationDictionary, nameof(destinationDictionary));
var sourceObjectExtension = ObjectExtensionManager.Instance.GetOrNull(sourceType);
if (definitionChecks.HasFlag(MappingPropertyDefinitionChecks.Source) &&
sourceObjectExtension == null)
{
return;
}
var destinationObjectExtension = ObjectExtensionManager.Instance.GetOrNull(destinationType);
if (definitionChecks.HasFlag(MappingPropertyDefinitionChecks.Destination) &&
destinationObjectExtension == null)
{
return;
}
if (definitionChecks == MappingPropertyDefinitionChecks.None)
{
foreach (var keyValue in sourceDictionary)
{
destinationDictionary[keyValue.Key] = keyValue.Value;
}
}
else if (definitionChecks == MappingPropertyDefinitionChecks.Source)
{
Debug.Assert(sourceObjectExtension != null, nameof(sourceObjectExtension) + " != null");
foreach (var property in sourceObjectExtension.GetProperties())
{
if (!sourceDictionary.ContainsKey(property.Name))
{
continue;
}
destinationDictionary[property.Name] = sourceDictionary[property.Name];
}
}
else if (definitionChecks == MappingPropertyDefinitionChecks.Destination)
{
Debug.Assert(destinationObjectExtension != null, nameof(destinationObjectExtension) + " != null");
foreach (var keyValue in sourceDictionary)
{
if (!destinationObjectExtension.HasProperty(keyValue.Key))
{
continue;
}
destinationDictionary[keyValue.Key] = keyValue.Value;
}
}
else if (definitionChecks == MappingPropertyDefinitionChecks.Both)
{
Debug.Assert(sourceObjectExtension != null, nameof(sourceObjectExtension) + " != null");
Debug.Assert(destinationObjectExtension != null, nameof(destinationObjectExtension) + " != null");
foreach (var property in sourceObjectExtension.GetProperties())
{
if (!sourceDictionary.ContainsKey(property.Name))
{
continue;
}
if (!destinationObjectExtension.HasProperty(property.Name))
{
continue;
}
destinationDictionary[property.Name] = sourceDictionary[property.Name];
}
}
else
{
throw new NotImplementedException(definitionChecks + " was not implemented!");
}
}
}
}
}

15
framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionManager.cs

@ -25,6 +25,21 @@ namespace Volo.Abp.ObjectExtending
return AddOrUpdate(typeof(TObject), configureAction);
}
[NotNull]
public virtual ObjectExtensionManager AddOrUpdate(
[NotNull] Type[] types,
[CanBeNull] Action<ObjectExtensionInfo> configureAction = null)
{
Check.NotNull(types, nameof(types));
foreach (var type in types)
{
AddOrUpdate(type, configureAction);
}
return this;
}
[NotNull]
public virtual ObjectExtensionManager AddOrUpdate(
[NotNull] Type type,

41
framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionManagerExtensions.cs

@ -6,10 +6,25 @@ namespace Volo.Abp.ObjectExtending
{
public static class ObjectExtensionManagerExtensions
{
[NotNull]
public static ObjectExtensionManager AddOrUpdateProperty<TProperty>(
[NotNull] this ObjectExtensionManager objectExtensionManager,
[NotNull] Type[] objectTypes,
[NotNull] string propertyName,
[CanBeNull] Action<ObjectExtensionPropertyInfo> configureAction = null)
{
return objectExtensionManager.AddOrUpdateProperty(
objectTypes,
typeof(TProperty),
propertyName, configureAction
);
}
[NotNull]
public static ObjectExtensionManager AddOrUpdateProperty<TObject, TProperty>(
[NotNull] this ObjectExtensionManager objectExtensionManager,
[NotNull] string propertyName,
[CanBeNull] Action<ObjectExtensionPropertyInfo> configureAction = null)
[CanBeNull] Action<ObjectExtensionPropertyInfo> configureAction = null)
where TObject : IHasExtraProperties
{
return objectExtensionManager.AddOrUpdateProperty(
@ -20,6 +35,30 @@ namespace Volo.Abp.ObjectExtending
);
}
[NotNull]
public static ObjectExtensionManager AddOrUpdateProperty(
[NotNull] this ObjectExtensionManager objectExtensionManager,
[NotNull] Type[] objectTypes,
[NotNull] Type propertyType,
[NotNull] string propertyName,
[CanBeNull] Action<ObjectExtensionPropertyInfo> configureAction = null)
{
Check.NotNull(objectTypes, nameof(objectTypes));
foreach (var objectType in objectTypes)
{
objectExtensionManager.AddOrUpdateProperty(
objectType,
propertyType,
propertyName,
configureAction
);
}
return objectExtensionManager;
}
[NotNull]
public static ObjectExtensionManager AddOrUpdateProperty(
[NotNull] this ObjectExtensionManager objectExtensionManager,
[NotNull] Type objectType,

14
framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs

@ -15,6 +15,20 @@ namespace Volo.Abp.ObjectExtending
[NotNull]
public Type Type { get; }
/// <summary>
/// Indicates whether to check the other side of the object mapping
/// if it explicitly defines the property. This property is used in;
///
/// * .MapExtraPropertiesTo() extension method.
/// * .MapExtraProperties() configuration for the AutoMapper.
///
/// It this is true, these methods check if the mapping object
/// has defined the property using the <see cref="ObjectExtensionManager"/>.
///
/// Default: null (unspecified, uses the default logic).
/// </summary>
public bool? CheckPairDefinitionOnMapping { get; set; }
[NotNull]
public Dictionary<object, object> Configuration { get; }

4
framework/src/Volo.Abp.Quartz/Volo/Abp/Quartz/AbpQuartzModule.cs

@ -22,13 +22,13 @@ namespace Volo.Abp.Quartz
{
_scheduler = context.ServiceProvider.GetService<IScheduler>();
_scheduler.JobFactory = context.ServiceProvider.GetService<IJobFactory>();
_scheduler.Start();
AsyncHelper.RunSync(() => _scheduler.Start());
}
public override void OnApplicationShutdown(ApplicationShutdownContext context)
{
//TODO: ABP may provide two methods for application shutdown: OnPreApplicationShutdown & OnApplicationShutdown
_scheduler.Shutdown();
AsyncHelper.RunSync(() => _scheduler.Shutdown());
}
}
}

6
framework/src/Volo.Abp.Threading/Volo/Abp/Threading/AbpTimer.cs

@ -30,15 +30,15 @@ namespace Volo.Abp.Threading
public ILogger<AbpTimer> Logger { get; set; }
protected IExceptionNotifier ExceptionNotifier { get; }
public IExceptionNotifier ExceptionNotifier { get; set; }
private readonly Timer _taskTimer;
private volatile bool _performingTasks;
private volatile bool _isRunning;
public AbpTimer(IExceptionNotifier exceptionNotifier)
public AbpTimer()
{
ExceptionNotifier = exceptionNotifier;
ExceptionNotifier = NullExceptionNotifier.Instance;
Logger = NullLogger<AbpTimer>.Instance;
_taskTimer = new Timer(

31
framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Popovers.cshtml

@ -31,20 +31,23 @@
<h4>Example</h4>
<div class="demo-with-code">
<div class="demo-area">
<abp-button abp-popover="Hi, i'm popover content!">
Popover Default
</abp-button>
<abp-button abp-popover-top="Hi, i'm popover content!" title="Popover Title">
Popover With Title
</abp-button>
<abp-button abp-popover-right="Hi, i'm popover content!" title="Popover Title" dismissible="true">
Dismissible Popover
</abp-button>
<abp-button abp-popover-left="Hi, i'm popover content!" title="Popover Title" disabled="true">
Disabled Popover
</abp-button>
<div class="demo-area">
<abp-button abp-popover="Hi, i'm popover content!">
Popover Default
</abp-button>
<abp-button abp-popover-top="Hi, i'm hoverable popover content!" title="Popover Title" hoverable="true">
Popover With Title
</abp-button>
<abp-button abp-popover-right="Hi, i'm dismissible popover content!" title="Popover Title" dismissible="true">
Dismissible Popover
</abp-button>
<abp-button abp-popover-left="Hi, i'm disabled popover content!" title="Popover Title" disabled="true">
Disabled Popover
</abp-button>
<abp-button abp-popover="Hi, i'm disabled dismissible hoverable popover content!" title="Popover Title" disabled="true" dismissible="true" hoverable="true">
Disabled Popover
</abp-button>
</div>
<div class="code-area">
<abp-tabs>

11
framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/TestServices/AuthorizationTestPermissionDefinitionProvider.cs

@ -1,4 +1,5 @@
using Volo.Abp.Authorization.Permissions;
using Shouldly;
using Volo.Abp.Authorization.Permissions;
namespace Volo.Abp.Authorization.TestServices
{
@ -6,14 +7,18 @@ namespace Volo.Abp.Authorization.TestServices
{
public override void Define(IPermissionDefinitionContext context)
{
PermissionGroupDefinition getGroup = context.GetGroupOrNull("TestGetGroup");
var getGroup = context.GetGroupOrNull("TestGetGroup");
if (getGroup == null)
{
getGroup = context.AddGroup("TestGetGroup");
}
PermissionGroupDefinition group = context.AddGroup("TestGroup");
var group = context.AddGroup("TestGroup");
group.AddPermission("MyAuthorizedService1");
group.GetPermissionOrNull("MyAuthorizedService1").ShouldNotBeNull();
context.RemoveGroup("TestGetGroup");
}
}

6
framework/test/Volo.Abp.AutoMapper.Tests/AutoMapper/AbpAutoMapperExtensibleDtoExtensions_Tests.cs

@ -24,7 +24,8 @@ namespace AutoMapper
.SetProperty("Name", "John")
.SetProperty("Age", 42)
.SetProperty("ChildCount", 2)
.SetProperty("Sex", "male");
.SetProperty("Sex", "male")
.SetProperty("CityName", "Adana");
var personDto = new ExtensibleTestPersonDto()
.SetProperty("ExistingDtoProperty", "existing-value");
@ -32,10 +33,11 @@ namespace AutoMapper
_objectMapper.Map(person, personDto);
personDto.GetProperty<string>("Name").ShouldBe("John"); //Defined in both classes
personDto.GetProperty<string>("ExistingDtoProperty").ShouldBe("existing-value"); //Should not clear existing values
personDto.HasProperty("Age").ShouldBeFalse(); //Not defined on the destination
personDto.HasProperty("ChildCount").ShouldBeFalse(); //Not defined in the source
personDto.HasProperty("Sex").ShouldBeFalse(); //Not defined in both classes
personDto.GetProperty<string>("ExistingDtoProperty").ShouldBe("existing-value"); //Should not clear existing values
personDto.HasProperty("CityName").ShouldBeFalse(); //Ignored
}
}
}

8
framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/ObjectMapperExtensions_Tests.cs

@ -20,7 +20,13 @@ namespace Volo.Abp.AutoMapper
[Fact]
public void Should_Map_Objects_With_AutoMap_Attributes()
{
var dto = _objectMapper.Map(typeof(MyEntity), typeof(MyEntityDto), new MyEntity { Number = 42 });
var dto = _objectMapper.Map<MyEntity, MyEntityDto>(
new MyEntity
{
Number = 42
}
);
dto.As<MyEntityDto>().Number.ShouldBe(42);
}
}

2
framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyMapProfile.cs

@ -10,7 +10,7 @@ namespace Volo.Abp.AutoMapper.SampleClasses
CreateMap<MyEntity, MyEntityDto>().ReverseMap();
CreateMap<ExtensibleTestPerson, ExtensibleTestPersonDto>()
.MapExtraProperties();
.MapExtraProperties(ignoredProperties: new[] { "CityName" });
}
}
}

5
framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/IRegularTestController.cs

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
namespace Volo.Abp.Http.DynamicProxying
{
@ -8,6 +9,8 @@ namespace Volo.Abp.Http.DynamicProxying
Task GetException1Async();
Task<DateTime> GetWithDateTimeParameterAsync(DateTime dateTime1);
Task<string> PostValueWithHeaderAndQueryStringAsync(string headerValue, string qsValue);
Task<string> PostValueWithBodyAsync(string bodyValue);

22
framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestController.cs

@ -1,4 +1,6 @@
using System.Threading.Tasks;
using System;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Application.Services;
using Volo.Abp.AspNetCore.Mvc;
@ -25,6 +27,14 @@ namespace Volo.Abp.Http.DynamicProxying
throw new UserFriendlyException("This is an error message!");
}
[HttpGet]
[Route("get-with-datetime-parameter")]
public Task<DateTime> GetWithDateTimeParameterAsync(DateTime dateTime1)
{
var culture = CultureInfo.CurrentCulture;
return Task.FromResult(dateTime1);
}
[HttpPost]
[Route("post-with-header-and-qs")]
public Task<string> PostValueWithHeaderAndQueryStringAsync([FromHeader] string headerValue, [FromQuery] string qsValue)
@ -48,7 +58,7 @@ namespace Volo.Abp.Http.DynamicProxying
[HttpPost]
[Route("post-object-with-query")]
public Task<Car> PostObjectWithQueryAsync( Car bodyValue)
public Task<Car> PostObjectWithQueryAsync(Car bodyValue)
{
return Task.FromResult(bodyValue);
}
@ -59,7 +69,7 @@ namespace Volo.Abp.Http.DynamicProxying
{
return Task.FromResult(bodyValue);
}
[HttpGet]
[Route("post-object-and-id-with-url/{id}")]
public Task<Car> GetObjectandIdAsync(int id, [FromBody] Car bodyValue)
@ -67,7 +77,7 @@ namespace Volo.Abp.Http.DynamicProxying
bodyValue.Year = id;
return Task.FromResult(bodyValue);
}
[HttpGet]
[Route("post-object-and-id-with-url-and-query/{id}")]
public Task<Car> GetObjectAndIdWithQueryAsync(int id, Car bodyValue)
@ -116,7 +126,11 @@ namespace Volo.Abp.Http.DynamicProxying
{
[FromQuery]
public int Year { get; set; }
[FromQuery]
public string Model { get; set; }
[FromQuery]
public DateTime FirstReleaseDate { get; set; }
}
}

46
framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestControllerClientProxy_Tests.cs

@ -1,7 +1,9 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.Http.Client;
using Volo.Abp.Localization;
using Xunit;
namespace Volo.Abp.Http.DynamicProxying
@ -30,6 +32,25 @@ namespace Volo.Abp.Http.DynamicProxying
exception.Error.Message.ShouldBe("This is an error message!");
}
[Fact]
public async Task GetWithDateTimeParameterAsync()
{
var dateTime1 = new DateTime(2020, 04, 19, 19, 05, 01);
var result = await _controller.GetWithDateTimeParameterAsync(dateTime1);
result.ShouldBe(dateTime1);
}
[Fact]
public async Task GetWithDateTimeParameterAsync_With_Different_Culture()
{
using (CultureHelper.Use("es"))
{
var dateTime1 = new DateTime(2020, 04, 19, 19, 05, 01);
var result = await _controller.GetWithDateTimeParameterAsync(dateTime1);
result.ShouldBe(dateTime1);
}
}
[Fact]
public async Task PostValueWithHeaderAndQueryStringAsync()
{
@ -54,7 +75,7 @@ namespace Volo.Abp.Http.DynamicProxying
[Fact]
public async Task PostObjectWithBodyAsync()
{
var result = await _controller.PostObjectWithBodyAsync(new Car { Year = 1976, Model = "Ford" });
var result = await _controller.PostObjectWithBodyAsync(new Car { Year = 1976, Model = "Ford", FirstReleaseDate = new DateTime(1976, 02, 22, 15, 0, 6, 22) });
result.Year.ShouldBe(1976);
result.Model.ShouldBe("Ford");
}
@ -62,15 +83,26 @@ namespace Volo.Abp.Http.DynamicProxying
[Fact]
public async Task PostObjectWithQueryAsync()
{
var result = await _controller.PostObjectWithQueryAsync(new Car { Year = 1976, Model = "Ford" });
var result = await _controller.PostObjectWithQueryAsync(new Car { Year = 1976, Model = "Ford", FirstReleaseDate = new DateTime(1976, 02, 22, 15, 0, 6, 22) });
result.Year.ShouldBe(1976);
result.Model.ShouldBe("Ford");
}
[Fact]
public async Task PostObjectWithQueryAsync_With_Different_Culture()
{
using (CultureHelper.Use("tr"))
{
var result = await _controller.PostObjectWithQueryAsync(new Car { Year = 1976, Model = "Ford", FirstReleaseDate = new DateTime(1976, 02, 22, 15, 0, 6, 22) });
result.Year.ShouldBe(1976);
result.Model.ShouldBe("Ford");
}
}
[Fact]
public async Task GetObjectWithUrlAsync()
{
var result = await _controller.GetObjectWithUrlAsync(new Car { Year = 1976, Model = "Ford" });
var result = await _controller.GetObjectWithUrlAsync(new Car { Year = 1976, Model = "Ford", FirstReleaseDate = new DateTime(1976, 02, 22, 15, 0, 6, 22) });
result.Year.ShouldBe(1976);
result.Model.ShouldBe("Ford");
}
@ -78,7 +110,7 @@ namespace Volo.Abp.Http.DynamicProxying
[Fact]
public async Task GetObjectandIdAsync()
{
var result = await _controller.GetObjectandIdAsync(42, new Car { Year = 1976, Model = "Ford" });
var result = await _controller.GetObjectandIdAsync(42, new Car { Year = 1976, Model = "Ford", FirstReleaseDate = new DateTime(1976, 02, 22, 15, 0, 6, 22) });
result.Year.ShouldBe(42);
result.Model.ShouldBe("Ford");
}
@ -86,7 +118,7 @@ namespace Volo.Abp.Http.DynamicProxying
[Fact]
public async Task GetObjectAndIdWithQueryAsync()
{
var result = await _controller.GetObjectAndIdWithQueryAsync(42, new Car { Year = 1976, Model = "Ford" });
var result = await _controller.GetObjectAndIdWithQueryAsync(42, new Car { Year = 1976, Model = "Ford", FirstReleaseDate = new DateTime(1976, 02, 22, 15, 0, 6, 22) });
result.Year.ShouldBe(42);
result.Model.ShouldBe("Ford");
}
@ -97,7 +129,7 @@ namespace Volo.Abp.Http.DynamicProxying
var result = await _controller.PatchValueWithBodyAsync("mybody");
result.ShouldBe("mybody");
}
[Fact]
public async Task PutValueWithHeaderAndQueryStringAsync()
{

5
framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/AbpObjectExtendingTestModule.cs

@ -19,8 +19,11 @@ namespace Volo.Abp.ObjectExtending
ObjectExtensionManager.Instance
.AddOrUpdateProperty<ExtensibleTestPerson, string>("Name")
.AddOrUpdateProperty<ExtensibleTestPerson, int>("Age")
.AddOrUpdateProperty<ExtensibleTestPerson, string>("NoPairCheck", options => options.CheckPairDefinitionOnMapping = false)
.AddOrUpdateProperty<ExtensibleTestPerson, string>("CityName")
.AddOrUpdateProperty<ExtensibleTestPersonDto, string>("Name")
.AddOrUpdateProperty<ExtensibleTestPersonDto, int>("ChildCount");
.AddOrUpdateProperty<ExtensibleTestPersonDto, int>("ChildCount")
.AddOrUpdateProperty<ExtensibleTestPersonDto, int>("CityName");
});
}
}

29
framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/HasExtraPropertiesObjectExtendingExtensions_Tests.cs

@ -16,22 +16,40 @@ namespace Volo.Abp.ObjectExtending
.SetProperty("Name", "John")
.SetProperty("Age", 42)
.SetProperty("ChildCount", 2)
.SetProperty("Sex", "male");
.SetProperty("Sex", "male")
.SetProperty("NoPairCheck", "test-value")
.SetProperty("CityName", "Adana");
_personDto = new ExtensibleTestPersonDto()
.SetProperty("ExistingDtoProperty", "existing-value");
}
[Fact]
public void MapExtraPropertiesTo_Should_Only_Map_Defined_Properties_By_Default()
{
_person.MapExtraPropertiesTo(_personDto);
_personDto.GetProperty<string>("Name").ShouldBe("John"); //Defined in both classes
_personDto.GetProperty<string>("CityName").ShouldBe("Adana"); //Defined in both classes
_personDto.GetProperty<string>("NoPairCheck").ShouldBe("test-value"); //CheckPairDefinitionOnMapping = false
_personDto.GetProperty<string>("ExistingDtoProperty").ShouldBe("existing-value"); //Should not clear existing values
_personDto.HasProperty("Age").ShouldBeFalse(); //Not defined on the destination
_personDto.HasProperty("ChildCount").ShouldBeFalse(); //Not defined in the source
_personDto.HasProperty("Sex").ShouldBeFalse(); //Not defined in both classes
}
[Fact]
public void MapExtraPropertiesTo_Should_Ignore_Desired_Properties()
{
_person.MapExtraPropertiesTo(_personDto, ignoredProperties: new[] { "CityName" });
_personDto.GetProperty<string>("Name").ShouldBe("John"); //Defined in both classes
_personDto.GetProperty<string>("NoPairCheck").ShouldBe("test-value"); //CheckPairDefinitionOnMapping = false
_personDto.GetProperty<string>("ExistingDtoProperty").ShouldBe("existing-value"); //Should not clear existing values
_personDto.HasProperty("CityName").ShouldBeFalse(); //Ignored!
_personDto.HasProperty("Age").ShouldBeFalse(); //Not defined on the destination
_personDto.HasProperty("ChildCount").ShouldBeFalse(); //Not defined in the source
_personDto.HasProperty("Sex").ShouldBeFalse(); //Not defined in both classes
}
[Fact]
@ -40,10 +58,11 @@ namespace Volo.Abp.ObjectExtending
_person.MapExtraPropertiesTo(_personDto, MappingPropertyDefinitionChecks.Source);
_personDto.GetProperty<string>("Name").ShouldBe("John"); //Defined in both classes
_personDto.GetProperty<string>("CityName").ShouldBe("Adana"); //Defined in both classes
_personDto.GetProperty<int>("Age").ShouldBe(42); //Defined in source
_personDto.GetProperty<string>("ExistingDtoProperty").ShouldBe("existing-value"); //Should not clear existing values
_personDto.HasProperty("ChildCount").ShouldBeFalse(); //Not defined in the source
_personDto.HasProperty("Sex").ShouldBeFalse(); //Not defined in both classes
_personDto.GetProperty<string>("ExistingDtoProperty").ShouldBe("existing-value"); //Should not clear existing values
}
[Fact]
@ -52,10 +71,11 @@ namespace Volo.Abp.ObjectExtending
_person.MapExtraPropertiesTo(_personDto, MappingPropertyDefinitionChecks.Destination);
_personDto.GetProperty<string>("Name").ShouldBe("John"); //Defined in both classes
_personDto.GetProperty<string>("CityName").ShouldBe("Adana"); //Defined in both classes
_personDto.GetProperty<int>("ChildCount").ShouldBe(2); //Defined in destination
_personDto.GetProperty<string>("ExistingDtoProperty").ShouldBe("existing-value"); //Should not clear existing values
_personDto.HasProperty("Age").ShouldBeFalse(); //Not defined in destination
_personDto.HasProperty("Sex").ShouldBeFalse(); //Not defined in both classes
_personDto.GetProperty<string>("ExistingDtoProperty").ShouldBe("existing-value"); //Should not clear existing values
}
[Fact]
@ -64,6 +84,7 @@ namespace Volo.Abp.ObjectExtending
_person.MapExtraPropertiesTo(_personDto, MappingPropertyDefinitionChecks.None);
_personDto.GetProperty<string>("Name").ShouldBe("John");
_personDto.GetProperty<string>("CityName").ShouldBe("Adana");
_personDto.GetProperty<int>("Age").ShouldBe(42);
_personDto.GetProperty<int>("ChildCount").ShouldBe(2);
_personDto.GetProperty<string>("Sex").ShouldBe("male");

40
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/SoftDelete_Tests.cs

@ -39,6 +39,24 @@ namespace Volo.Abp.TestApp.Testing
}
}
[Fact]
public async Task Should_Cancel_Deletion_For_Soft_Delete_Entities_ById()
{
var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId);
await PersonRepository.DeleteAsync(douglas.Id);
douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId);
douglas.ShouldBeNull();
using (DataFilter.Disable<ISoftDelete>())
{
douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId);
douglas.ShouldNotBeNull();
douglas.IsDeleted.ShouldBeTrue();
douglas.DeletionTime.ShouldNotBeNull();
}
}
[Fact]
public async Task Should_Handle_Deletion_On_Update_For_Soft_Delete_Entities()
{
@ -82,5 +100,27 @@ namespace Volo.Abp.TestApp.Testing
douglas.Phones.ShouldNotBeEmpty();
}
}
[Fact]
public async Task Cascading_Entities_Should_Not_Be_Deleted_When_Soft_Deleting_Entities_ById()
{
var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId);
douglas.Phones.ShouldNotBeEmpty();
await PersonRepository.DeleteAsync(douglas.Id);
douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId);
douglas.ShouldBeNull();
using (DataFilter.Disable<ISoftDelete>())
{
douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId);
douglas.ShouldNotBeNull();
douglas.IsDeleted.ShouldBeTrue();
douglas.DeletionTime.ShouldNotBeNull();
douglas.Phones.ShouldNotBeEmpty();
}
}
}
}

19
modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs

@ -68,18 +68,11 @@ namespace Volo.Abp.Account.Web.Pages.Account
return Page();
}
var schemes = await SchemeProvider.GetAllSchemesAsync();
var providers = schemes
.Where(x => x.DisplayName != null || x.Name.Equals(AccountOptions.WindowsAuthenticationSchemeName, StringComparison.OrdinalIgnoreCase))
.Select(x => new ExternalProviderModel
{
DisplayName = x.DisplayName,
AuthenticationScheme = x.Name
})
.ToList();
var providers = await GetExternalProviders();
ExternalProviders = providers.ToList();
EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin);
if (context?.ClientId != null)
{
var client = await ClientStore.FindEnabledClientByIdAsync(context.ClientId);
@ -94,8 +87,6 @@ namespace Volo.Abp.Account.Web.Pages.Account
}
}
ExternalProviders = providers.ToArray();
if (IsExternalLoginOnly)
{
return await base.OnPostExternalLogin(providers.First().AuthenticationScheme);
@ -124,6 +115,10 @@ namespace Volo.Abp.Account.Web.Pages.Account
ValidateModel();
ExternalProviders = await GetExternalProviders();
EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin);
await ReplaceEmailToUsernameOfInputIfNeeds();
var result = await SignInManager.PasswordSignInAsync(

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

@ -63,21 +63,10 @@ namespace Volo.Abp.Account.Web.Pages.Account
{
LoginInput = new LoginInputModel();
var schemes = await SchemeProvider.GetAllSchemesAsync();
var providers = schemes
.Where(x => x.DisplayName != null || x.Name.Equals(AccountOptions.WindowsAuthenticationSchemeName, StringComparison.OrdinalIgnoreCase))
.Select(x => new ExternalProviderModel
{
DisplayName = x.DisplayName,
AuthenticationScheme = x.Name
})
.ToList();
ExternalProviders = await GetExternalProviders();
EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin);
ExternalProviders = providers.ToArray();
if (IsExternalLoginOnly)
{
//return await ExternalLogin(vm.ExternalLoginScheme, returnUrl);
@ -94,6 +83,10 @@ namespace Volo.Abp.Account.Web.Pages.Account
ValidateModel();
ExternalProviders = await GetExternalProviders();
EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin);
await ReplaceEmailToUsernameOfInputIfNeeds();
var result = await SignInManager.PasswordSignInAsync(
@ -140,6 +133,20 @@ namespace Volo.Abp.Account.Web.Pages.Account
return RedirectSafely(ReturnUrl, ReturnUrlHash);
}
protected virtual async Task<List<ExternalProviderModel>> GetExternalProviders()
{
var schemes = await SchemeProvider.GetAllSchemesAsync();
return schemes
.Where(x => x.DisplayName != null || x.Name.Equals(AccountOptions.WindowsAuthenticationSchemeName, StringComparison.OrdinalIgnoreCase))
.Select(x => new ExternalProviderModel
{
DisplayName = x.DisplayName,
AuthenticationScheme = x.Name
})
.ToList();
}
[UnitOfWork]
public virtual async Task<IActionResult> OnPostExternalLogin(string provider)
{

25
modules/audit-logging/src/Volo.Abp.AuditLogging.Domain/Volo/Abp/AuditLogging/IAuditLogRepository.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.Auditing;
using Volo.Abp.Domain.Repositories;
namespace Volo.Abp.AuditLogging
@ -44,5 +45,29 @@ namespace Volo.Abp.AuditLogging
Task<Dictionary<DateTime, double>> GetAverageExecutionDurationPerDayAsync(
DateTime startDate,
DateTime endDate);
Task<EntityChange> GetEntityChange(Guid entityChangeId);
Task<List<EntityChange>> GetEntityChangeListAsync(
string sorting = null,
int maxResultCount = 50,
int skipCount = 0,
Guid? auditLogId = null,
DateTime? startTime = null,
DateTime? endTime = null,
EntityChangeType? changeType = null,
string entityId = null,
string entityTypeFullName = null,
bool includeDetails = false,
CancellationToken cancellationToken = default);
Task<long> GetEntityChangeCountAsync(
Guid? auditLogId = null,
DateTime? startTime = null,
DateTime? endTime = null,
EntityChangeType? changeType = null,
string entityId = null,
string entityTypeFullName = null,
CancellationToken cancellationToken = default);
}
}

12
modules/audit-logging/src/Volo.Abp.AuditLogging.EntityFrameworkCore/Volo/Abp/AuditLogging/AbpAuditLoggingEfCoreQueryableExtensions.cs

@ -18,5 +18,17 @@ namespace Volo.Abp.AuditLogging
.Include(x => x.Actions)
.Include(x => x.EntityChanges).ThenInclude(ec=>ec.PropertyChanges);
}
public static IQueryable<EntityChange> IncludeDetails(
this IQueryable<EntityChange> queryable,
bool include = true)
{
if (!include)
{
return queryable;
}
return queryable.Include(x => x.PropertyChanges);
}
}
}

63
modules/audit-logging/src/Volo.Abp.AuditLogging.EntityFrameworkCore/Volo/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogRepository.cs

@ -6,6 +6,7 @@ using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Auditing;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
@ -134,10 +135,70 @@ namespace Volo.Abp.AuditLogging.EntityFrameworkCore
return result.ToDictionary(element => element.Day.ClearTime(), element => element.avgExecutionTime);
}
public override IQueryable<AuditLog> WithDetails()
{
return GetQueryable().IncludeDetails();
}
public Task<EntityChange> GetEntityChange(Guid entityChangeId)
{
return DbContext.Set<EntityChange>().AsNoTracking().IncludeDetails().Where(x => x.Id == entityChangeId).FirstAsync();
}
public virtual async Task<List<EntityChange>> GetEntityChangeListAsync(
string sorting = null,
int maxResultCount = 50,
int skipCount = 0,
Guid? auditLogId = null,
DateTime? startTime = null,
DateTime? endTime = null,
EntityChangeType? changeType = null,
string entityId = null,
string entityTypeFullName = null,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
var query = GetEntityChangeListQuery(auditLogId, startTime, endTime, changeType, entityId, entityTypeFullName, includeDetails);
return await query.OrderBy(sorting ?? "changeTime desc")
.PageBy(skipCount, maxResultCount)
.ToListAsync(GetCancellationToken(cancellationToken));
}
public virtual async Task<long> GetEntityChangeCountAsync(
Guid? auditLogId = null,
DateTime? startTime = null,
DateTime? endTime = null,
EntityChangeType? changeType = null,
string entityId = null,
string entityTypeFullName = null,
CancellationToken cancellationToken = default)
{
var query = GetEntityChangeListQuery(auditLogId, startTime, endTime, changeType, entityId, entityTypeFullName);
var totalCount = await query.LongCountAsync(GetCancellationToken(cancellationToken));
return totalCount;
}
protected virtual IQueryable<EntityChange> GetEntityChangeListQuery(
Guid? auditLogId = null,
DateTime? startTime = null,
DateTime? endTime = null,
EntityChangeType? changeType = null,
string entityId = null,
string entityTypeFullName = null,
bool includeDetails = false)
{
return DbContext.Set<EntityChange>().AsNoTracking().IncludeDetails(includeDetails)
.WhereIf(auditLogId.HasValue, e => e.AuditLogId == auditLogId)
.WhereIf(startTime.HasValue, e => e.ChangeTime >= startTime)
.WhereIf(endTime.HasValue, e => e.ChangeTime <= endTime)
.WhereIf(changeType.HasValue, e => e.ChangeType == changeType)
.WhereIf(!string.IsNullOrWhiteSpace(entityId), e => e.EntityId == entityId)
.WhereIf(!string.IsNullOrWhiteSpace(entityTypeFullName),
e => e.EntityTypeFullName.Contains(entityTypeFullName));
}
}
}

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

Loading…
Cancel
Save