Browse Source

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

pull/2616/head
liangshiwei 6 years ago
parent
commit
e5a4e4baaa
  1. 2
      README.md
  2. 26
      docs/en/Connection-Strings.md
  3. 18
      docs/en/Dependency-Injection.md
  4. 90
      docs/en/Entity-Framework-Core-Other-DBMS.md
  5. 10
      docs/en/Entity-Framework-Core-PostgreSQL.md
  6. 2
      docs/en/Entity-Framework-Core.md
  7. 58
      docs/en/FluentValidation.md
  8. 5
      docs/en/Logging.md
  9. 12
      docs/en/Modules/Docs.md
  10. 116
      docs/en/Options.md
  11. 159
      docs/en/Validation.md
  12. 3
      docs/en/Virtual-File-System.md
  13. 17
      docs/en/docs-nav.json
  14. 3
      docs/zh-Hans/Virtual-File-System.md
  15. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Controllers/ErrorController.cs
  16. 21
      framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs
  17. 53
      framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacServiceProvider.cs
  18. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Licensing/DeveloperApiKeyResult.cs
  19. 5
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveFolderStep.cs
  20. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Module/ModuleTemplateBase.cs
  21. 1
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NuGetPackageTarget.cs
  22. 4
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectFinder.cs
  23. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs
  24. 10
      framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithExternalServiceProvider.cs
  25. 144
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs
  26. 12
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs
  27. 18
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueComparers/AbpDictionaryValueComparer.cs
  28. 15
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpJsonValueConverter.cs
  29. 6
      framework/src/Volo.Abp.Http.Client/Microsoft/Extensions/DependencyInjection/ServiceCollectionDynamicHttpClientProxyExtensions.cs
  30. 1
      framework/src/Volo.Abp.Http.Client/Volo.Abp.Http.Client.csproj
  31. 4
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientModule.cs
  32. 5
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/AbpUnitOfWorkDefaultOptions.cs
  33. 12
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/AbpUnitOfWorkOptions.cs
  34. 18
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkManagerExtensions.cs
  35. 2
      framework/src/Volo.Abp.Validation/Volo/Abp/Validation/ValidationHelper.cs
  36. 32
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs
  37. 15
      framework/test/Volo.Abp.TestApp.Tests/Volo/Abp/TestApp/Application/PersonAppService_Tests.cs
  38. 2
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/PersonDto.cs
  39. 16
      framework/test/Volo.Abp.Validation.Tests/Volo/Abp/Validation/ApplicationService_Validation_Tests.cs
  40. 4
      modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Settings/AccountSettingDefinitionProvider.cs
  41. 2
      modules/account/src/Volo.Abp.Account.Web/Areas/Account/Controllers/AccountController.cs
  42. 5
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml
  43. 2
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs
  44. 3
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs
  45. 8
      modules/blogging/src/Volo.Blogging.Web/BloggingWebModule.cs
  46. 319
      modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/Detail.cshtml
  47. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/BloggingPage.cs
  48. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/BloggingPageModel.cs
  49. 2
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Index.cshtml
  50. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Index.cshtml.cs
  51. 319
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Detail.cshtml
  52. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Detail.cshtml.cs
  53. 4
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Edit.cshtml
  54. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Edit.cshtml.cs
  55. 10
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml
  56. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml.cs
  57. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.css
  58. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.min.css
  59. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.scss
  60. 4
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/New.cshtml
  61. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/New.cshtml.cs
  62. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/detail.js
  63. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/edit.js
  64. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/new.css
  65. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/new.css.map
  66. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/new.js
  67. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/new.min.css
  68. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/new.scss
  69. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Scripts/blog.js
  70. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/_bootstrap-overwrite.scss
  71. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/_custom.scss
  72. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/_header.scss
  73. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/_home.css
  74. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/_home.min.css
  75. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/_home.scss
  76. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/_post.scss
  77. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/blog.css
  78. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/blog.css.map
  79. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/blog.min.css
  80. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/blog.scss
  81. 0
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/_ViewImports.cshtml
  82. 4
      modules/docs/src/Volo.Docs.Web/Markdown/MarkdownDocumentToHtmlConverter.cs
  83. 4
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs
  84. 4
      modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/en.json
  85. 2
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator.cs
  86. 15
      modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/EntityFrameworkCore/IdentityServerDbContextModelCreatingExtensions.cs
  87. 2
      npm/ng-packs/apps/dev-app/src/index.html
  88. 4
      npm/ng-packs/apps/dev-app/tsconfig.app.json
  89. 1
      npm/ng-packs/package.json
  90. 12
      npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.html
  91. 27
      npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.ts
  92. 4
      npm/ng-packs/packages/account/src/lib/components/change-password/change-password.component.ts
  93. 2
      npm/ng-packs/packages/account/src/lib/components/login/login.component.html
  94. 53
      npm/ng-packs/packages/account/src/lib/components/login/login.component.ts
  95. 8
      npm/ng-packs/packages/account/src/lib/components/register/register.component.html
  96. 49
      npm/ng-packs/packages/account/src/lib/components/register/register.component.ts
  97. 49
      npm/ng-packs/packages/core/src/lib/abstracts/ng-model.component.ts
  98. 8
      npm/ng-packs/packages/core/src/lib/actions/session.actions.ts
  99. 9
      npm/ng-packs/packages/core/src/lib/core.module.ts
  100. 7
      npm/ng-packs/packages/core/src/lib/models/session.ts

2
README.md

@ -27,6 +27,8 @@ Framework solution is located under the `framework` folder. It has no external d
[Modules](modules/) and [Templates](templates/) have their own solutions and have **local references** to the framework and each other.
Visual Studio can not work properly with the local references out of the solution folder. When you open a module/sample solution in the Visual Studio, you may get some errors related to the dependencies. In this case, run the `dotnet restore` on the command prompt for the related solution's folder. You need to run it after you first open the solution or change a dependency.
### Contribution
ABP is an open source platform. Check [the contribution guide](docs/en/Contribution/Index.md) if you want to contribute to the project.

26
docs/en/Connection-Strings.md

@ -35,6 +35,21 @@ This configuration defines three different connection strings:
[Pre-built application modules](Modules/Index.md) define constants for the connection string names. For example, the IdentityServer module defines a ` ConnectionStringName ` constant in the ` AbpIdentityServerDbProperties ` class (located in the ` Volo.Abp.IdentityServer ` namespace). Other modules similarly define constants, so you can investigate the connection string name.
### AbpDbConnectionOptions
ABP actually uses the `AbpDbConnectionOptions` to get the connection strings. If you set the connection strings as explained above, `AbpDbConnectionOptions` is automatically filled. However, you can set or override the connection strings using [the options pattern](Options.md). You can configure the `AbpDbConnectionOptions` in the `ConfigureServices` method of your [module](Module-Development-Basics.md) as shown below:
````csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpDbConnectionOptions>(options =>
{
options.ConnectionStrings.Default = "...";
options.ConnectionStrings["AbpPermissionManagement"] = "...";
});
}
````
## Set the Connection String Name
A module typically has a unique connection string name associated to its `DbContext` class using the `ConnectionStringName` attribute. Example:
@ -61,4 +76,13 @@ Once you want to separate a module's database, you typically will need to create
## Multi-Tenancy
See [the multi-tenancy document](Multi-Tenancy.md) to learn how to use separate databases for tenants.
See [the multi-tenancy document](Multi-Tenancy.md) to learn how to use separate databases for tenants.
## Replace the Connection String Resolver
ABP defines the `IConnectionStringResolver` and uses it whenever it needs a connection string. It has two pre-built implementations:
* `DefaultConnectionStringResolver` uses the `AbpDbConnectionOptions` to select the connection string based on the rules defined in the "Configure the Connection Strings" section above.
* `MultiTenantConnectionStringResolver` used for multi-tenant applications and tries to get the configured connection string for the current tenant if available. It uses the `ITenantStore` to find the connection strings. It inherits from the `DefaultConnectionStringResolver` and fallbacks to the base logic if no connection string specified for the current tenant.
If you need a custom logic to determine the connection string, implement the `IConnectionStringResolver` interface (optionally derive from the existing implementations) and replace the existing implementation using the [dependency injection](Dependency-Injection.md) system.

18
docs/en/Dependency-Injection.md

@ -166,6 +166,24 @@ public class BlogModule : AbpModule
}
````
### Replace a Service
If you need to replace an existing service (defined by the ABP framework or another module dependency), you have two options;
1. Use the `Dependency` attribute of the ABP framework as explained above.
2. Use the `IServiceCollection.Replace` method of the Microsoft Dependency Injection library. Example:
````csharp
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
//Replacing the IConnectionStringResolver service
context.Services.Replace(ServiceDescriptor.Transient<IConnectionStringResolver, MyConnectionStringResolver>());
}
}
````
## Injecting Dependencies
There are three common ways of using a service that has already been registered.

90
docs/en/Entity-Framework-Core-Other-DBMS.md

@ -0,0 +1,90 @@
# Switch to another DBMS for Entity Framework Core
**[The application startup template](Startup-Templates/Application.md)** comes with SQL Server provider pre-configured for the Entity Framework Core. Entity Framework Core supports [many other DBMSs](https://docs.microsoft.com/en-us/ef/core/providers/) and you can use any of them with your ABP based applications.
ABP framework provides integration packages for some common DBMSs to make the configuration a bit easier (see the [entity framework core document](Entity-Framework-Core.md) for a list of available integration packages). However, you can configure your DBMS provider without these integration packages.
While using the integration package is always recommended (it also makes standard for the depended version across different modules), you can do it manually if there is no integration package for your DBMS provider.
This document explains how to switch to MySQL without using [the MySQL integration package](Entity-Framework-Core-MySQL.md).
## Replace the SQL Server Dependency
* Remove the [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet package dependency from the `.EntityFrameworkCore` project.
* Add the [Pomelo.EntityFrameworkCore.MySql](https://www.nuget.org/packages/Pomelo.EntityFrameworkCore.MySql/) NuGet package dependency to your `.EntityFrameworkCore` project.
## Remove the Module Dependency
Remove the `AbpEntityFrameworkCoreSqlServerModule` from the dependency list of your ***YourProjectName*EntityFrameworkCoreModule** class.
## Change the UseSqlServer() Calls
Find the following code part inside the *YourProjectName*EntityFrameworkCoreModule class:
````csharp
Configure<AbpDbContextOptions>(options =>
{
options.UseSqlServer();
});
````
Replace it with the following code part:
````csharp
Configure<AbpDbContextOptions>(options =>
{
options.Configure(ctx =>
{
if (ctx.ExistingConnection != null)
{
ctx.DbContextOptions.UseMySql(ctx.ExistingConnection);
}
else
{
ctx.DbContextOptions.UseMySql(ctx.ConnectionString);
}
});
});
````
* `UseMySql` calls in this code is defined by the Pomelo.EntityFrameworkCore.MySql package and you can use its additional options if you need.
* This code first checks if there is an existing (active) connection to the same database in the current request and reuses it if possible. This allows to share a single transaction among different DbContext types. ABP handles the rest of the things.
* It uses `ctx.ConnectionString` and passes to the `UseMySql` if there is no active connection (which will cause to create a new database connection). Using the `ctx.ConnectionString` is important here. Don't pass a static connection string (or a connection string from a configuration). Because ABP [dynamically determines the correct connection string](Connection-Strings.md) in a multi-database or [multi-tenant](Multi-Tenancy.md) environment.
## Change the Connection Strings
MySQL connection strings are different than SQL Server connection strings. So, check all `appsettings.json` files in your solution and replace the connection strings inside them. See the [connectionstrings.com]( https://www.connectionstrings.com/mysql/ ) for details of MySQL connection string options.
You typically will change the `appsettings.json` inside the `.DbMigrator` and `.Web` projects, but it depends on your solution structure.
## Change the Migrations DbContext
MySQL DBMS has some slight differences than the SQL Server. Some module database mapping configuration (especially the field lengths) causes problems with MySQL. For example, some of the the [IdentityServer module](Modules/IdentityServer.md) tables has such problems and it provides an option to configure the fields based on your DBMS.
The startup template contains a *YourProjectName*MigrationsDbContext which is responsible to maintain and migrate the database schema. This DbContext basically calls extension methods of the depended modules to configure their database tables.
Open the *YourProjectName*MigrationsDbContext and change the `builder.ConfigureIdentityServer();` line as shown below:
````csharp
builder.ConfigureIdentityServer(options =>
{
options.DatabaseProvider = EfCoreDatabaseProvider.MySql;
});
````
Then `ConfigureIdentityServer()` method will set the field lengths to not exceed the MySQL limits. Refer to related module documentation if you have any problem while creating or executing the database migrations.
## Re-Generate the Migrations
The startup template uses [Entity Framework Core's Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/). EF Core Migrations depend on the selected DBMS provider. So, changing the DBMS provider will cause the migration fails.
* Delete the Migrations folder under the `.EntityFrameworkCore.DbMigrations` project and re-build the solution.
* Run `Add-Migration "Initial"` on the Package Manager Console (select the `.DbMigrator` (or `.Web`) project as the startup project in the Solution Explorer and select the `.EntityFrameworkCore.DbMigrations` project as the default project in the Package Manager Console).
This will create a database migration with all database objects (tables) configured.
Run the `.DbMigrator` project to create the database and seed the initial data.
## Run the Application
It is ready. Just run the application and enjoy coding.

10
docs/en/Entity-Framework-Core-PostgreSQL.md

@ -12,12 +12,12 @@ Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFra
## UsePostgreSql()
Find `UseSqlServer()` calls in your solution, replace with `UsePostgreSql()`. Check the following files:
Find `UseSqlServer()` call in *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project and replace with `UsePostgreSql()`.
* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project.
* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project.
> Depending on your solution structure, you may find more code files need to be changed.
Find `UseSqlServer()` call in *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project and replace with `UseNpgsql()`.
> Depending on your solution structure, you may find more `UseSqlServer()` calls that needs to be changed.
## Change the Connection Strings
@ -38,4 +38,4 @@ Run the `.DbMigrator` project to create the database and seed the initial data.
## Run the Application
It is ready. Just run the application and enjoy coding.
It is ready. Just run the application and enjoy coding.

2
docs/en/Entity-Framework-Core.md

@ -38,6 +38,8 @@ ABP framework provides integration packages for some common DBMSs to make the co
* [MySQL](Entity-Framework-Core-MySQL.md)
* [PostgreSQL](Entity-Framework-Core-PostgreSQL.md)
* [SQLite](Entity-Framework-Core-SQLite.md)
* [Others](Entity-Framework-Core-Other-DBMS.md)
## Creating DbContext

58
docs/en/FluentValidation.md

@ -0,0 +1,58 @@
# FluentValidation Integration
ABP [Validation](Validation.md) infrastructure is extensible. [Volo.Abp.FluentValidation](https://www.nuget.org/packages/Volo.Abp.FluentValidation) NuGet package extends the validation system to work with the [FluentValidation](https://fluentvalidation.net/) library.
## Installation
It is suggested to use the [ABP CLI](CLI.md) to install this package.
### Using the ABP CLI
Open a command line window in the folder of the project (.csproj file) and type the following command:
````bash
abp add-package Volo.Abp.FluentValidation
````
### Manual Installation
If you want to manually install;
1. Add the [Volo.Abp.FluentValidation](https://www.nuget.org/packages/Volo.Abp.FluentValidation) NuGet package to your project:
````
Install-Package Volo.Abp.FluentValidation
````
2. Add the `AbpFluentValidationModule` to the dependency list of your module:
````csharp
[DependsOn(
//...other dependencies
typeof(AbpFluentValidationModule) //Add the FluentValidation module
)]
public class YourModule : AbpModule
{
}
````
## Using the FluentValidation
Follow [the FluentValidation documentation](https://fluentvalidation.net/) to create validator classes. Example:
````csharp
public class CreateUpdateBookDtoValidator : AbstractValidator<CreateUpdateBookDto>
{
public CreateUpdateBookDtoValidator()
{
RuleFor(x => x.Name).Length(3, 10);
RuleFor(x => x.Price).ExclusiveBetween(0.0f, 999.0f);
}
}
````
ABP will automatically find this class and associate with the `CreateUpdateBookDto` on object validation.
## See Also
* [Validation System](Validation.md)

5
docs/en/Logging.md

@ -0,0 +1,5 @@
# Logging
ABP Framework doesn't implement any logging infrastructure. It uses the [ASP.NET Core's logging system](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging).
> .NET Core's logging system is actually independent from the ASP.NET Core. It is usable in any type of application.

12
docs/en/Modules/Docs.md

@ -316,7 +316,7 @@ You can use [ABP Framework](https://github.com/abpframework/abp/) GitHub documen
- ExtraProperties:
```json
{"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs/en/","GitHubAccessToken":"***"}
{"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs","GitHubAccessToken":"***"}
```
Note that `GitHubAccessToken` is masked with `***`. It's a private token that you must get it from GitHub. See https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/
@ -328,7 +328,7 @@ You can use [ABP Framework](https://github.com/abpframework/abp/) GitHub documen
For `SQL` databases, you can use the below `T-SQL` command to insert the specified sample into your `DocsProjects` table:
```mssql
INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939658', N'ABP framework (GitHub)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'GitHub', N'{"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs/en/","GitHubAccessToken":"***"}', N'/', N'master')
INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939658', N'ABP framework (GitHub)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'GitHub', N'{"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs","GitHubAccessToken":"***"}', N'/', N'master')
```
Be aware that `GitHubAccessToken` is masked. It's a private token and you must get your own token and replace the `***` string.
@ -354,10 +354,10 @@ You can use [ABP Framework](https://github.com/abpframework/abp/) GitHub documen
- ExtraProperties:
```json
{"Path":"C:\\Github\\abp\\docs\\en"}
{"Path":"C:\\Github\\abp\\docs"}
```
Note that `Path` must be replaced with your local docs directory. You can fetch the ABP Framework's documents from https://github.com/abpframework/abp/tree/master/docs/en and copy to the directory `C:\\Github\\abp\\docs\\en` to get it work.
Note that `Path` must be replaced with your local docs directory. You can fetch the ABP Framework's documents from https://github.com/abpframework/abp/tree/master/docs and copy to the directory `C:\\Github\\abp\\docs` to get it work.
- MainWebsiteUrl: `/`
@ -366,11 +366,9 @@ You can use [ABP Framework](https://github.com/abpframework/abp/) GitHub documen
For `SQL` databases, you can use the below `T-SQL` command to insert the specified sample into your `DocsProjects` table:
```mssql
INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939659', N'ABP framework (FileSystem)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'FileSystem', N'{"Path":"C:\\Github\\abp\\docs\\en"}', N'/', NULL)
INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939659', N'ABP framework (FileSystem)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'FileSystem', N'{"Path":"C:\\Github\\abp\\docs"}', N'/', NULL)
```
Add one of the sample projects above and run the application. In the menu you will see `Documents` link, click the menu link to open the documents page.
So far, we have created a new application from abp.io website and made it up and ready for Docs module.

116
docs/en/Options.md

@ -1,4 +1,118 @@
# Options
TODO!
Microsoft has introduced [the options pattern](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options) that is used to configure a group of settings used by the framework services. This pattern is implemented by the [Microsoft.Extensions.Options](https://www.nuget.org/packages/Microsoft.Extensions.Options) NuGet package, so it is usable by any type of applications in addition to ASP.NET Core based applications.
ABP framework follows this option pattern and defines options classes to configure the framework and the modules (they are explained in the documents of the related feature).
Since [the Microsoft documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options) explains the pattern in detail, no reason to repeat all. However, ABP adds a few more features and they will be explained here.
## Configure Options
You typically configure options in the `ConfigureServices` of the `Startup` class. However, since ABP framework provides a modular infrastructure, you configure options in the `ConfigureServices` of your [module](Module-Development-Basics.md). Example:
````csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.Configure<AbpAuditingOptions>(options =>
{
options.IsEnabled = false;
});
}
````
* `AbpAuditingOptions` is a simple class defines some properties like `IsEnabled` used here.
* `AbpModule` base class defines `Configure` method to make the code simpler. So, instead of `context.Services.Configure<...>`, you can directly use the `Configure<...>` shortcut method.
If you are developing a reusable module, you may need to define an options class to allow developers to configure your module. In this case, define a plain options class as shown below:
````csharp
public class MyOptions
{
public int Value1 { get; set; }
public bool Value2 { get; set; }
}
````
Then developers can configure your options just like the `AbpAuditingOptions` example above:
````csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<MyOptions>(options =>
{
options.Value1 = 42;
options.Value2 = true;
});
}
````
* In this example, used the shortcut `Configure<...>` method.
### Get the Option Value
Whenever you need to get the value of an option, [inject](Dependency-Injection.md) the `IOptions<TOption>` service into your class and use its `.Value` property. Example:
````csharp
public class MyService : ITransientDependency
{
private readonly MyOptions _options;
public MyService(IOptions<MyOptions> options)
{
_options = options.Value; //Notice the options.Value usage!
}
public void DoIt()
{
var v1 = _options.Value1;
var v2 = _options.Value2;
}
}
````
Read [the Microsoft documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options) for all details of the options pattern.
## Pre Configure
One restriction of the options pattern is that you can only resolve (inject) the `IOptions<MyOptions>` and get the option values when the dependency injection configuration completes (that means the `ConfigureServices` methods of all modules complete).
If you are developing a module, you may need to allow developers to set some options and use these options in the dependency injection registration phase. You may need to configure other services or change the dependency injection registration code based on these option values.
For such cases, ABP introduces the `PreConfigure<TOptions>` and the `ExecutePreConfiguredActions<TOptions>` extension methods for the `IServiceCollection`. The pattern works as explained below.
1. Define a plan option class in your module. Example:
````csharp
public class MyPreOptions
{
public bool MyValue { get; set; }
}
````
Then any [module class](Module-Development-Basics.md) depends on your module can use the `PreConfigure<TOptions>` method in its `PreConfigureServices` method. Example:
````csharp
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<MyPreOptions>(options =>
{
options.MyValue = true;
});
}
````
> Multiple modules can pre-configure the options and override the option values based on their dependency order.
Finally, your module can execute the `ExecutePreConfiguredActions` method in its `ConfigureServices` method to get the configured option values. Example:
````csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
var options = context.Services.ExecutePreConfiguredActions<MyPreOptions>();
if (options.MyValue)
{
//...
}
}
````

159
docs/en/Validation.md

@ -1,3 +1,158 @@
## Validation
# Validation
TODO
Validation system is used to validate the user input or client request for a particular controller action or service method.
ABP is compatible with the ASP.NET Core Model Validation system and everything written in [its documentation](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation) is already valid for ABP based applications. So, this document mostly focuses on the ABP features rather than repeating the Microsoft documentation.
In addition, ABP adds the following benefits:
* Defines `IValidationEnabled` to add automatic validation to an arbitrary class. Since all the [application services](Application-Services.md) inherently implements it, they are also validated automatically.
* Automatically localize the validation errors for the data annotation attributes.
* Provides extensible services to validate a method call or an object state.
* Provides [FluentValidation](https://fluentvalidation.net/) integration.
## Validating DTOs
This section briefly introduces the validation system. For details, see the [ASP.NET Core validation documentation](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation).
### Data Annotation Attributes
Using data annotations is a simple way to implement the formal validation for a [DTO](Data-Transfer-Objects.md) in a declarative way. Example:
````csharp
public class CreateBookDto
{
[Required]
[StringLength(100)]
public string Name { get; set; }
[Required]
[StringLength(1000)]
public string Description { get; set; }
[Range(0, 999.99)]
public decimal Price { get; set; }
}
````
When you use this class as a parameter to an [application service](Application-Services.md) or a controller, it is automatically validated and a localized validation exception is thrown ([and handled](Exception-Handling.md) by the ABP framework).
### IValidatableObject
`IValidatableObject` can be implemented by a DTO to perform custom validation logic. `CreateBookDto` in the following example implements this interface and checks if the `Name` is equals to the `Description` and returns a validation error in this case.
````csharp
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Acme.BookStore
{
public class CreateBookDto : IValidatableObject
{
[Required]
[StringLength(100)]
public string Name { get; set; }
[Required]
[StringLength(1000)]
public string Description { get; set; }
[Range(0, 999.99)]
public decimal Price { get; set; }
public IEnumerable<ValidationResult> Validate(
ValidationContext validationContext)
{
if (Name == Description)
{
yield return new ValidationResult(
"Name and Description can not be the same!",
new[] { "Name", "Description" }
);
}
}
}
}
````
#### Resolving a Service
If you need to resolve a service from the [dependency injection system](Dependency-Injection.md), you can use the `ValidationContext` object. Example:
````csharp
var myService = validationContext.GetRequiredService<IMyService>();
````
> While resolving services in the `Validate` method allows any possibility, it is not a good practice to implement your domain validation logic in DTOs. Keep DTOs simple. Their purpose is to transfer data (DTO: Data Transfer Object).
## Validation Infrastructure
This section explains a few additional services provided by the ABP framework.
### IValidationEnabled Interface
`IValidationEnabled` is an empty marker interface that can be implemented by any class (registered to and resolved from the [DI](Dependency-Injection.md)) to let the ABP framework perform the validation system for the methods of the class. Example:
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Validation;
namespace Acme.BookStore
{
public class MyService : ITransientDependency, IValidationEnabled
{
public virtual async Task DoItAsync(MyInput input)
{
//...
}
}
}
````
> ABP framework uses the [dynamic proxying / interception](Dynamic-Proxying-Interceptors.md) system to perform the validation. In order to make it working, your method should be **virtual** or your service should be injected and used over an **interface** (like `IMyService`).
### AbpValidationException
Once ABP determines a validation error, it throws an exception of type `AbpValidationException`. Your application code can throw `AbpValidationException`, but most of the times it is not needed.
* `ValidationErrors` property of the `AbpValidationException` contains the validation error list.
* Log level of the `AbpValidationException` is set to `Warning`. It logs all the validation errors to the [logging system](Logging.md).
* `AbpValidationException` is automatically caught by the ABP framework and converted to a usable error into with HTTP 400 status code. See the [exception handling](Exception-Handling.md) document for more.
## Advanced Topics
### IObjectValidator
In addition to the automatic validation, you may want to manually validate an object. In this case, [inject](Dependency-Injection.md) and use the `IObjectValidator` service:
* `Validate` method validates the given object based on the validation rules and throws an `AbpValidationException` if it is not in a valid state.
* `GetErrors` doesn't throw an exception, but only returns the validation errors.
`IObjectValidator` is implemented by the `ObjectValidator` by default. `ObjectValidator` is extensible; you can implement `IObjectValidationContributor` interface to contribute a custom logic. Example:
````csharp
public class MyObjectValidationContributor
: IObjectValidationContributor, ITransientDependency
{
public void AddErrors(ObjectValidationContext context)
{
//Get the validating object
var obj = context.ValidatingObject;
//Add the validation errors if available
context.Errors.Add(...);
}
}
````
* Remember to register your class to the [DI](Dependency-Injection.md) (implementing `ITransientDependency` does it just like in this example)
* ABP will automatically discover your class and use on any type of object validation (including automatic method call validation).
### IMethodInvocationValidator
`IMethodInvocationValidator` is used to validate a method call. It internally uses the `IObjectValidator` to validate objects passes to the method call. You normally don't need to this service since it is automatically used by the framework, but you may want to reuse or replace it on your application in rare cases.
## FluentValidation Integration
Volo.Abp.FluentValidation package integrates the FluentValidation library to the validation system (by implementing the `IObjectValidationContributor`). See the [FluentValidation Integration document](FluentValidation.md) for more.

3
docs/en/Virtual-File-System.md

@ -38,7 +38,8 @@ If you want to add multiple files, this can be tedious. Alternatively, you can d
````C#
<ItemGroup>
<None Remove="MyResources\**\*.*" />
<EmbeddedResource Include="MyResources\**\*.*" />
<Content Remove="MyResources\**\*.*" />
</ItemGroup>
````

17
docs/en/docs-nav.json

@ -60,6 +60,10 @@
"text": "Configuration",
"path": "Configuration.md"
},
{
"text": "Options",
"path": "Options.md"
},
{
"text": "Dependency Injection",
"path": "Dependency-Injection.md",
@ -83,7 +87,14 @@
"path": "Exception-Handling.md"
},
{
"text": "Validation"
"text": "Validation",
"path": "Validation.md",
"items": [
{
"text": "FluentValidation Integration",
"path": "FluentValidation.md"
}
]
},
{
"text": "Authorization",
@ -281,6 +292,10 @@
{
"text": "Switch to SQLite",
"path": "Entity-Framework-Core-SQLite.md"
},
{
"text": "Switch to another DBMS",
"path": "Entity-Framework-Core-Other-DBMS.md"
}
]
},

3
docs/zh-Hans/Virtual-File-System.md

@ -39,7 +39,8 @@ namespace MyCompany.MyProject
````C#
<ItemGroup>
<None Remove="MyResources\**\*.*" />
<EmbeddedResource Include="MyResources\**\*.*" />
<Content Remove="MyResources\**\*.*" />
</ItemGroup>
````

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Controllers/ErrorController.cs

@ -61,7 +61,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Controllers
if (string.IsNullOrWhiteSpace(page))
{
return "~/Pages/Error/Default.cshtml";
return "~/Views/Error/Default.cshtml";
}
return page;

21
framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs

@ -2,6 +2,7 @@
using JetBrains.Annotations;
using Microsoft.AspNetCore.RequestLocalization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Volo.Abp;
using Volo.Abp.AspNetCore.Auditing;
using Volo.Abp.AspNetCore.ExceptionHandling;
@ -20,7 +21,20 @@ namespace Microsoft.AspNetCore.Builder
Check.NotNull(app, nameof(app));
app.ApplicationServices.GetRequiredService<ObjectAccessor<IApplicationBuilder>>().Value = app;
app.ApplicationServices.GetRequiredService<IAbpApplicationWithExternalServiceProvider>().Initialize(app.ApplicationServices);
var application = app.ApplicationServices.GetRequiredService<IAbpApplicationWithExternalServiceProvider>();
var applicationLifetime = app.ApplicationServices.GetRequiredService<IHostApplicationLifetime>();
applicationLifetime.ApplicationStopping.Register(() =>
{
application.Shutdown();
});
applicationLifetime.ApplicationStopped.Register(() =>
{
application.Dispose();
});
application.Initialize(app.ApplicationServices);
}
public static IApplicationBuilder UseAuditing(this IApplicationBuilder app)
@ -42,7 +56,8 @@ namespace Microsoft.AspNetCore.Builder
.UseMiddleware<AbpCorrelationIdMiddleware>();
}
public static IApplicationBuilder UseAbpRequestLocalization(this IApplicationBuilder app, Action<RequestLocalizationOptions> optionsAction = null)
public static IApplicationBuilder UseAbpRequestLocalization(this IApplicationBuilder app,
Action<RequestLocalizationOptions> optionsAction = null)
{
app.ApplicationServices
.GetRequiredService<IAbpRequestLocalizationOptionsProvider>()
@ -62,4 +77,4 @@ namespace Microsoft.AspNetCore.Builder
return app.UseMiddleware<AbpExceptionHandlingMiddleware>();
}
}
}
}

53
framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacServiceProvider.cs

@ -1,6 +1,6 @@
// This software is part of the Autofac IoC container
// Copyright © 2015 Autofac Contributors
// http://autofac.org
// https://autofac.org
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
@ -33,19 +33,21 @@ namespace Autofac.Extensions.DependencyInjection
/// </summary>
/// <seealso cref="System.IServiceProvider" />
/// <seealso cref="Microsoft.Extensions.DependencyInjection.ISupportRequiredService" />
public class AutofacServiceProvider : IServiceProvider, ISupportRequiredService
public class AutofacServiceProvider : IServiceProvider, ISupportRequiredService, IDisposable
{
private readonly IComponentContext _componentContext;
private readonly ILifetimeScope _lifetimeScope;
private bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="AutofacServiceProvider"/> class.
/// </summary>
/// <param name="componentContext">
/// The component context from which services should be resolved.
/// <param name="lifetimeScope">
/// The lifetime scope from which services will be resolved.
/// </param>
public AutofacServiceProvider(IComponentContext componentContext)
public AutofacServiceProvider(ILifetimeScope lifetimeScope)
{
this._componentContext = componentContext;
this._lifetimeScope = lifetimeScope;
}
/// <summary>
@ -66,7 +68,7 @@ namespace Autofac.Extensions.DependencyInjection
/// </exception>
public object GetRequiredService(Type serviceType)
{
return this._componentContext.Resolve(serviceType);
return this._lifetimeScope.Resolve(serviceType);
}
/// <summary>
@ -81,7 +83,40 @@ namespace Autofac.Extensions.DependencyInjection
/// </returns>
public object GetService(Type serviceType)
{
return this._componentContext.ResolveOptional(serviceType);
return this._lifetimeScope.ResolveOptional(serviceType);
}
/// <summary>
/// Gets the underlying instance of <see cref="ILifetimeScope" />.
/// </summary>
public ILifetimeScope LifetimeScope => _lifetimeScope;
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (!this._disposed)
{
this._disposed = true;
if (disposing)
{
this._lifetimeScope.Dispose();
}
}
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
}
}

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Licensing/DeveloperApiKeyResult.cs

@ -7,7 +7,7 @@ namespace Volo.Abp.Cli.Licensing
public bool HasActiveLicense { get; set; }
public string OrganizationName { get; set; }
public string ApiKey { get; set; }
public DateTime LicenseEndTime { get; set; }
public DateTime? LicenseEndTime { get; set; }
public string LicenseCode { get; set; }
}
}

5
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveFolderStep.cs

@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps
{
@ -14,7 +15,7 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps
public override void Execute(ProjectBuildContext context)
{
//Remove the folder content
var folderPathWithSlash = _folderPath + "/";
var folderPathWithSlash = _folderPath.EnsureEndsWith('/');
context.Files.RemoveAll(file => file.Name.StartsWith(folderPathWithSlash));
//Remove the folder

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Module/ModuleTemplateBase.cs

@ -43,6 +43,8 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates.Module
"MyCompanyName.MyProjectName.Web.Unified",
projectFolderPath: "/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified"
));
steps.Add(new RemoveFolderStep("/angular"));
}
private void RandomizeSslPorts(ProjectBuildContext context, List<ProjectBuildPipelineStep> steps)

1
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NuGetPackageTarget.cs

@ -2,6 +2,7 @@
{
public enum NuGetPackageTarget : byte
{
Undefined = 0,
DomainShared = 1,
Domain = 2,
ApplicationContracts = 3,

4
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectFinder.cs

@ -2,11 +2,13 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using JetBrains.Annotations;
namespace Volo.Abp.Cli.ProjectModification
{
public static class ProjectFinder
{
[CanBeNull]
public static string FindNuGetTargetProjectFile(string[] projectFiles, NuGetPackageTarget target)
{
if (!projectFiles.Any())
@ -40,7 +42,7 @@ namespace Volo.Abp.Cli.ProjectModification
case NuGetPackageTarget.HttpApiClient:
return FindProjectEndsWith(projectFiles, assemblyNames, ".HttpApi.Client");
default:
throw new ApplicationException($"{nameof(NuGetPackageTarget)}.{target} has not implemented!");
return null;
}
}

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

@ -68,7 +68,7 @@ namespace Volo.Abp.Cli.ProjectModification
var targetProjectFile = ProjectFinder.FindNuGetTargetProjectFile(projectFiles, nugetPackage.Target);
if (targetProjectFile == null)
{
Logger.LogDebug($"Target project is not available for NuGet package '{nugetPackage.Name}'");
Logger.LogDebug($"Target project is not available for this NuGet package '{nugetPackage.Name}'");
continue;
}

10
framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithExternalServiceProvider.cs

@ -26,5 +26,15 @@ namespace Volo.Abp
InitializeModules();
}
public override void Dispose()
{
base.Dispose();
if (ServiceProvider is IDisposable disposableServiceProvider)
{
disposableServiceProvider.Dispose();
}
}
}
}

144
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs

@ -11,13 +11,13 @@ using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json;
using Volo.Abp.Auditing;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.EntityFrameworkCore.EntityHistory;
using Volo.Abp.EntityFrameworkCore.Modeling;
using Volo.Abp.EntityFrameworkCore.ValueConverters;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
@ -323,147 +323,9 @@ namespace Volo.Abp.EntityFrameworkCore
return;
}
ConfigureConcurrencyStampProperty<TEntity>(modelBuilder, mutableEntityType);
ConfigureExtraProperties<TEntity>(modelBuilder, mutableEntityType);
ConfigureAuditProperties<TEntity>(modelBuilder, mutableEntityType);
ConfigureTenantIdProperty<TEntity>(modelBuilder, mutableEntityType);
ConfigureGlobalFilters<TEntity>(modelBuilder, mutableEntityType);
}
protected virtual void ConfigureConcurrencyStampProperty<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)
where TEntity : class
{
if (!typeof(IHasConcurrencyStamp).IsAssignableFrom(typeof(TEntity)))
{
return;
}
modelBuilder.Entity<TEntity>(b =>
{
b.Property(x => ((IHasConcurrencyStamp) x).ConcurrencyStamp)
.IsConcurrencyToken()
.HasColumnName(nameof(IHasConcurrencyStamp.ConcurrencyStamp));
});
}
protected virtual void ConfigureExtraProperties<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)
where TEntity : class
{
if (!typeof(IHasExtraProperties).IsAssignableFrom(typeof(TEntity)))
{
return;
}
modelBuilder.Entity<TEntity>(b =>
{
b.Property(x => ((IHasExtraProperties) x).ExtraProperties)
.HasConversion(
d => JsonConvert.SerializeObject(d, Formatting.None),
s => JsonConvert.DeserializeObject<Dictionary<string, object>>(s)
)
.HasColumnName(nameof(IHasExtraProperties.ExtraProperties));
});
}
protected virtual void ConfigureAuditProperties<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)
where TEntity : class
{
if (typeof(TEntity).IsAssignableTo<IHasCreationTime>())
{
modelBuilder.Entity<TEntity>(b =>
{
b.Property(x => ((IHasCreationTime)x).CreationTime)
.IsRequired()
.HasColumnName(nameof(IHasCreationTime.CreationTime));
});
}
modelBuilder.Entity<TEntity>().ConfigureByConvention();
if (typeof(TEntity).IsAssignableTo<IMayHaveCreator>())
{
modelBuilder.Entity<TEntity>(b =>
{
b.Property(x => ((IMayHaveCreator)x).CreatorId)
.IsRequired(false)
.HasColumnName(nameof(IMayHaveCreator.CreatorId));
});
}
if (typeof(TEntity).IsAssignableTo<IMustHaveCreator>())
{
modelBuilder.Entity<TEntity>(b =>
{
b.Property(x => ((IMustHaveCreator)x).CreatorId)
.IsRequired()
.HasColumnName(nameof(IMustHaveCreator.CreatorId));
});
}
if (typeof(TEntity).IsAssignableTo<IHasModificationTime>())
{
modelBuilder.Entity<TEntity>(b =>
{
b.Property(x => ((IHasModificationTime)x).LastModificationTime)
.IsRequired(false)
.HasColumnName(nameof(IHasModificationTime.LastModificationTime));
});
}
if (typeof(TEntity).IsAssignableTo<IModificationAuditedObject>())
{
modelBuilder.Entity<TEntity>(b =>
{
b.Property(x => ((IModificationAuditedObject)x).LastModifierId)
.IsRequired(false)
.HasColumnName(nameof(IModificationAuditedObject.LastModifierId));
});
}
if (typeof(TEntity).IsAssignableTo<ISoftDelete>())
{
modelBuilder.Entity<TEntity>(b =>
{
b.Property(x => ((ISoftDelete) x).IsDeleted)
.IsRequired()
.HasDefaultValue(false)
.HasColumnName(nameof(ISoftDelete.IsDeleted));
});
}
if (typeof(TEntity).IsAssignableTo<IHasDeletionTime>())
{
modelBuilder.Entity<TEntity>(b =>
{
b.Property(x => ((IHasDeletionTime)x).DeletionTime)
.IsRequired(false)
.HasColumnName(nameof(IHasDeletionTime.DeletionTime));
});
}
if (typeof(TEntity).IsAssignableTo<IDeletionAuditedObject>())
{
modelBuilder.Entity<TEntity>(b =>
{
b.Property(x => ((IDeletionAuditedObject)x).DeleterId)
.IsRequired(false)
.HasColumnName(nameof(IDeletionAuditedObject.DeleterId));
});
}
}
protected virtual void ConfigureTenantIdProperty<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)
where TEntity : class
{
if (!typeof(TEntity).IsAssignableTo<IMultiTenant>())
{
return;
}
modelBuilder.Entity<TEntity>(b =>
{
b.Property(x => ((IMultiTenant)x).TenantId)
.IsRequired(false)
.HasColumnName(nameof(IMultiTenant.TenantId));
});
ConfigureGlobalFilters<TEntity>(modelBuilder, mutableEntityType);
}
protected virtual void ConfigureGlobalFilters<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)

12
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs

@ -2,11 +2,11 @@
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Newtonsoft.Json;
using Volo.Abp.Auditing;
using Volo.Abp.Data;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.EntityFrameworkCore.ValueComparers;
using Volo.Abp.EntityFrameworkCore.ValueConverters;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.EntityFrameworkCore.Modeling
@ -57,11 +57,9 @@ namespace Volo.Abp.EntityFrameworkCore.Modeling
if (b.Metadata.ClrType.IsAssignableTo<IHasExtraProperties>())
{
b.Property<Dictionary<string, object>>(nameof(IHasExtraProperties.ExtraProperties))
.HasConversion(
d => JsonConvert.SerializeObject(d, Formatting.None),
s => JsonConvert.DeserializeObject<Dictionary<string, object>>(s)
)
.HasColumnName(nameof(IHasExtraProperties.ExtraProperties));
.HasColumnName(nameof(IHasExtraProperties.ExtraProperties))
.HasConversion(new AbpJsonValueConverter<Dictionary<string, object>>())
.Metadata.SetValueComparer(new AbpDictionaryValueComparer<string, object>());
}
}

18
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueComparers/AbpDictionaryValueComparer.cs

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace Volo.Abp.EntityFrameworkCore.ValueComparers
{
public class AbpDictionaryValueComparer<TKey, TValue> : ValueComparer<Dictionary<TKey, TValue>>
{
public AbpDictionaryValueComparer()
: base(
(d1, d2) => d1.SequenceEqual(d2),
d => d.Aggregate(0, (k, v) => HashCode.Combine(k, v.GetHashCode())),
d => d.ToDictionary(k => k.Key, v => v.Value))
{
}
}
}

15
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpJsonValueConverter.cs

@ -0,0 +1,15 @@
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Newtonsoft.Json;
namespace Volo.Abp.EntityFrameworkCore.ValueConverters
{
public class AbpJsonValueConverter<TPropertyType> : ValueConverter<TPropertyType, string>
{
public AbpJsonValueConverter()
: base(
d => JsonConvert.SerializeObject(d, Formatting.None),
s => JsonConvert.DeserializeObject<TPropertyType>(s))
{
}
}
}

6
framework/src/Volo.Abp.Http.Client/Microsoft/Extensions/DependencyInjection/ServiceCollectionDynamicHttpClientProxyExtensions.cs

@ -8,6 +8,7 @@ using Volo.Abp;
using Volo.Abp.Castle.DynamicProxy;
using Volo.Abp.Http.Client;
using Volo.Abp.Http.Client.DynamicProxying;
using Volo.Abp.Validation;
namespace Microsoft.Extensions.DependencyInjection
{
@ -154,6 +155,9 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddTransient(interceptorType);
var interceptorAdapterType = typeof(AbpAsyncDeterminationInterceptor<>).MakeGenericType(interceptorType);
var validationInterceptorAdapterType =
typeof(AbpAsyncDeterminationInterceptor<>).MakeGenericType(typeof(ValidationInterceptor));
if (asDefaultService)
{
@ -162,6 +166,7 @@ namespace Microsoft.Extensions.DependencyInjection
serviceProvider => ProxyGeneratorInstance
.CreateInterfaceProxyWithoutTarget(
type,
(IInterceptor)serviceProvider.GetRequiredService(validationInterceptorAdapterType),
(IInterceptor)serviceProvider.GetRequiredService(interceptorAdapterType)
)
);
@ -174,6 +179,7 @@ namespace Microsoft.Extensions.DependencyInjection
var service = ProxyGeneratorInstance
.CreateInterfaceProxyWithoutTarget(
type,
(IInterceptor)serviceProvider.GetRequiredService(validationInterceptorAdapterType),
(IInterceptor)serviceProvider.GetRequiredService(interceptorAdapterType)
);

1
framework/src/Volo.Abp.Http.Client/Volo.Abp.Http.Client.csproj

@ -24,6 +24,7 @@
<ProjectReference Include="..\Volo.Abp.Http\Volo.Abp.Http.csproj" />
<ProjectReference Include="..\Volo.Abp.MultiTenancy\Volo.Abp.MultiTenancy.csproj" />
<ProjectReference Include="..\Volo.Abp.Threading\Volo.Abp.Threading.csproj" />
<ProjectReference Include="..\Volo.Abp.Validation\Volo.Abp.Validation.csproj" />
</ItemGroup>
</Project>

4
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientModule.cs

@ -3,6 +3,7 @@ using Volo.Abp.Castle;
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
using Volo.Abp.Validation;
namespace Volo.Abp.Http.Client
{
@ -10,7 +11,8 @@ namespace Volo.Abp.Http.Client
typeof(AbpHttpModule),
typeof(AbpCastleCoreModule),
typeof(AbpThreadingModule),
typeof(AbpMultiTenancyModule)
typeof(AbpMultiTenancyModule),
typeof(AbpValidationModule)
)]
public class AbpHttpClientModule : AbpModule
{

5
framework/src/Volo.Abp.Uow/Volo/Abp/Uow/AbpUnitOfWorkDefaultOptions.cs

@ -10,7 +10,10 @@ namespace Volo.Abp.Uow
/// </summary>
public class AbpUnitOfWorkDefaultOptions
{
public UnitOfWorkTransactionBehavior TransactionBehavior { get; set; }
/// <summary>
/// Default value: <see cref="UnitOfWorkTransactionBehavior.Auto"/>.
/// </summary>
public UnitOfWorkTransactionBehavior TransactionBehavior { get; set; } = UnitOfWorkTransactionBehavior.Auto;
public IsolationLevel? IsolationLevel { get; set; }

12
framework/src/Volo.Abp.Uow/Volo/Abp/Uow/AbpUnitOfWorkOptions.cs

@ -14,6 +14,18 @@ namespace Volo.Abp.Uow
public TimeSpan? Timeout { get; set; }
public AbpUnitOfWorkOptions()
{
}
public AbpUnitOfWorkOptions(bool isTransactional = false, IsolationLevel? isolationLevel = null, TimeSpan? timeout = null)
{
IsTransactional = isTransactional;
IsolationLevel = isolationLevel;
Timeout = timeout;
}
public AbpUnitOfWorkOptions Clone()
{
return new AbpUnitOfWorkOptions

18
framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkManagerExtensions.cs

@ -1,15 +1,27 @@
using JetBrains.Annotations;
using System;
using System.Data;
using JetBrains.Annotations;
namespace Volo.Abp.Uow
{
public static class UnitOfWorkManagerExtensions
{
[NotNull]
public static IUnitOfWork Begin([NotNull] this IUnitOfWorkManager unitOfWorkManager, bool requiresNew = false)
public static IUnitOfWork Begin(
[NotNull] this IUnitOfWorkManager unitOfWorkManager,
bool requiresNew = false,
bool isTransactional = false,
IsolationLevel? isolationLevel = null,
TimeSpan? timeout = null)
{
Check.NotNull(unitOfWorkManager, nameof(unitOfWorkManager));
return unitOfWorkManager.Begin(new AbpUnitOfWorkOptions(), requiresNew);
return unitOfWorkManager.Begin(new AbpUnitOfWorkOptions
{
IsTransactional = isTransactional,
IsolationLevel = isolationLevel,
Timeout = timeout
}, requiresNew);
}
public static void BeginReserved([NotNull] this IUnitOfWorkManager unitOfWorkManager, [NotNull] string reservationName)

2
framework/src/Volo.Abp.Validation/Volo/Abp/Validation/ValidationHandler.cs → framework/src/Volo.Abp.Validation/Volo/Abp/Validation/ValidationHelper.cs

@ -2,7 +2,7 @@
namespace Volo.Abp.Validation
{
public class ValidationHandler
public class ValidationHelper
{
private const string EmailRegEx = @"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?";

32
framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs

@ -2,6 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute.Extensions;
using Shouldly;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Repositories;
@ -9,6 +10,7 @@ using Volo.Abp.Http.Client;
using Volo.Abp.TestApp.Application;
using Volo.Abp.TestApp.Application.Dto;
using Volo.Abp.TestApp.Domain;
using Volo.Abp.Validation;
using Xunit;
namespace Volo.Abp.Http.DynamicProxying
@ -38,7 +40,8 @@ namespace Volo.Abp.Http.DynamicProxying
[Fact]
public async Task GetList()
{
var people = await _peopleAppService.GetListAsync(new PagedAndSortedResultRequestDto()).ConfigureAwait(false);
var people = await _peopleAppService.GetListAsync(new PagedAndSortedResultRequestDto())
.ConfigureAwait(false);
people.TotalCount.ShouldBeGreaterThan(0);
people.Items.Count.ShouldBe((int) people.TotalCount);
}
@ -59,11 +62,11 @@ namespace Volo.Abp.Http.DynamicProxying
{
var uniquePersonName = Guid.NewGuid().ToString();
var person = await _peopleAppService.CreateAsync(new PersonDto
{
Name = uniquePersonName,
Age = 42
}
var person = await _peopleAppService.CreateAsync(new PersonDto
{
Name = uniquePersonName,
Age = 42
}
).ConfigureAwait(false);
person.ShouldNotBeNull();
@ -74,7 +77,20 @@ namespace Volo.Abp.Http.DynamicProxying
personInDb.ShouldNotBeNull();
personInDb.Id.ShouldBe(person.Id);
}
[Fact]
public async Task Create_Validate_Exception()
{
await Assert.ThrowsAsync<AbpValidationException>(async () =>
{
var person = await _peopleAppService.CreateAsync(new PersonDto
{
Age = 42
}
).ConfigureAwait(false);
}).ConfigureAwait(false);
}
[Fact]
public async Task Update()
{
@ -135,4 +151,4 @@ namespace Volo.Abp.Http.DynamicProxying
result.Inner1.Inner2.Value3.ShouldBe("value three");
}
}
}
}

15
framework/test/Volo.Abp.TestApp.Tests/Volo/Abp/TestApp/Application/PersonAppService_Tests.cs

@ -19,7 +19,7 @@ namespace Volo.Abp.TestApp.Application
public PersonAppService_Tests()
{
_peopleAppService = ServiceProvider.GetRequiredService<IPeopleAppService>();
_peopleAppService = ServiceProvider.GetRequiredService<IPeopleAppService>();
}
protected override void AfterAddApplication(IServiceCollection services)
@ -31,14 +31,17 @@ namespace Volo.Abp.TestApp.Application
[Fact]
public async Task GetList()
{
var people = await _peopleAppService.GetListAsync(new PagedAndSortedResultRequestDto()).ConfigureAwait(false);
var people = await _peopleAppService.GetListAsync(new PagedAndSortedResultRequestDto())
.ConfigureAwait(false);
people.Items.Count.ShouldBeGreaterThan(0);
}
[Fact]
public async Task Create()
{
var personDto = await _peopleAppService.CreateAsync(new PersonDto()).ConfigureAwait(false);
var uniquePersonName = Guid.NewGuid().ToString();
var personDto = await _peopleAppService.CreateAsync(new PersonDto {Name = uniquePersonName})
.ConfigureAwait(false);
var repository = ServiceProvider.GetService<IRepository<Person, Guid>>();
var person = await repository.FindAsync(personDto.Id).ConfigureAwait(false);
@ -52,7 +55,9 @@ namespace Volo.Abp.TestApp.Application
{
_fakeCurrentTenant.Id.Returns(TestDataBuilder.TenantId1);
var personDto = await _peopleAppService.CreateAsync(new PersonDto()).ConfigureAwait(false);
var uniquePersonName = Guid.NewGuid().ToString();
var personDto = await _peopleAppService.CreateAsync(new PersonDto {Name = uniquePersonName})
.ConfigureAwait(false);
var repository = ServiceProvider.GetService<IRepository<Person, Guid>>();
var person = await repository.FindAsync(personDto.Id).ConfigureAwait(false);
@ -63,4 +68,4 @@ namespace Volo.Abp.TestApp.Application
person.TenantId.ShouldBe(TestDataBuilder.TenantId1);
}
}
}
}

2
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/PersonDto.cs

@ -1,4 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
using Volo.Abp.Application.Dtos;
using Volo.Abp.MultiTenancy;
@ -6,6 +7,7 @@ namespace Volo.Abp.TestApp.Application.Dto
{
public class PersonDto : EntityDto<Guid>, IMultiTenant
{
[Required]
public string Name { get; set; }
public int Age { get; set; }

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

@ -178,16 +178,16 @@ namespace Volo.Abp.Validation
public void Should_Validate_Emails()
{
//Valid
ValidationHandler.IsValidEmailAddress("john.doe@domain.com").ShouldBe(true);
ValidationHandler.IsValidEmailAddress("ip@1.2.3.123").ShouldBe(true);
ValidationHandler.IsValidEmailAddress("pharaoh@egyptian.museum").ShouldBe(true);
ValidationHandler.IsValidEmailAddress("john.doe+regexbuddy@gmail.com").ShouldBe(true);
ValidationHandler.IsValidEmailAddress("Mike.O'Dell@ireland.com").ShouldBe(true);
ValidationHelper.IsValidEmailAddress("john.doe@domain.com").ShouldBe(true);
ValidationHelper.IsValidEmailAddress("ip@1.2.3.123").ShouldBe(true);
ValidationHelper.IsValidEmailAddress("pharaoh@egyptian.museum").ShouldBe(true);
ValidationHelper.IsValidEmailAddress("john.doe+regexbuddy@gmail.com").ShouldBe(true);
ValidationHelper.IsValidEmailAddress("Mike.O'Dell@ireland.com").ShouldBe(true);
//Invalid
ValidationHandler.IsValidEmailAddress("1024x768@60Hz").ShouldBe(false);
ValidationHandler.IsValidEmailAddress("not.a.valid.email").ShouldBe(false);
ValidationHandler.IsValidEmailAddress("john@aol...com").ShouldBe(false);
ValidationHelper.IsValidEmailAddress("1024x768@60Hz").ShouldBe(false);
ValidationHelper.IsValidEmailAddress("not.a.valid.email").ShouldBe(false);
ValidationHelper.IsValidEmailAddress("john@aol...com").ShouldBe(false);
}
[DependsOn(typeof(AbpAutofacModule))]

4
modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Settings/AccountSettingDefinitionProvider.cs

@ -13,7 +13,7 @@ namespace Volo.Abp.Account.Settings
AccountSettingNames.IsSelfRegistrationEnabled,
"true",
L("DisplayName:Abp.Account.IsSelfRegistrationEnabled"),
L("Description:Abp.Account.IsSelfRegistrationEnabled"))
L("Description:Abp.Account.IsSelfRegistrationEnabled"), isVisibleToClients : true)
);
context.Add(
@ -21,7 +21,7 @@ namespace Volo.Abp.Account.Settings
AccountSettingNames.EnableLocalLogin,
"true",
L("DisplayName:Abp.Account.EnableLocalLogin"),
L("Description:Abp.Account.EnableLocalLogin"))
L("Description:Abp.Account.EnableLocalLogin"), isVisibleToClients : true)
);
}

2
modules/account/src/Volo.Abp.Account.Web/Areas/Account/Controllers/AccountController.cs

@ -64,7 +64,7 @@ namespace Volo.Abp.Account.Web.Areas.Account.Controllers
protected virtual async Task ReplaceEmailToUsernameOfInputIfNeeds(UserLoginInfo login)
{
if (!ValidationHandler.IsValidEmailAddress(login.UserNameOrEmailAddress))
if (!ValidationHelper.IsValidEmailAddress(login.UserNameOrEmailAddress))
{
return;
}

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

@ -1,5 +1,6 @@
@page
@using Volo.Abp.Account.Settings
@using Volo.Abp.Settings
@model Volo.Abp.Account.Web.Pages.Account.LoginModel
@inherits Volo.Abp.Account.Web.Pages.Account.AccountPage
@inject Volo.Abp.Settings.ISettingProvider SettingProvider
@ -7,8 +8,8 @@
{
<div class="card mt-3 shadow-sm rounded">
<div class="card-body p-5">
<h4>@L["Login"]</h4>
@if (string.Equals(await SettingProvider.GetOrNullAsync(AccountSettingNames.IsSelfRegistrationEnabled), "true", StringComparison.OrdinalIgnoreCase))
<h4>@L["Login"]</h4>
@if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled))
{
<strong>
@L["AreYouANewUser"]

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

@ -217,7 +217,7 @@ namespace Volo.Abp.Account.Web.Pages.Account
protected virtual async Task ReplaceEmailToUsernameOfInputIfNeeds()
{
if (!ValidationHandler.IsValidEmailAddress(LoginInput.UserNameOrEmailAddress))
if (!ValidationHelper.IsValidEmailAddress(LoginInput.UserNameOrEmailAddress))
{
return;
}

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

@ -45,7 +45,8 @@ namespace Volo.Abp.Account.Web.Pages.Account
protected virtual async Task CheckSelfRegistrationAsync()
{
if (!await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled).ConfigureAwait(false))
if (!await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled).ConfigureAwait(false) ||
!await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin).ConfigureAwait(false))
{
throw new UserFriendlyException(L["SelfRegistrationDisabledMessage"]);
}

8
modules/blogging/src/Volo.Blogging.Web/BloggingWebModule.cs

@ -59,10 +59,10 @@ namespace Volo.Blogging
var routePrefix = urlOptions.RoutePrefix;
options.Conventions.AddPageRoute("/Blog/Posts/Index", routePrefix + "{blogShortName}");
options.Conventions.AddPageRoute("/Blog/Posts/Detail", routePrefix + "{blogShortName}/{postUrl}");
options.Conventions.AddPageRoute("/Blog/Posts/Edit", routePrefix + "{blogShortName}/posts/{postId}/edit");
options.Conventions.AddPageRoute("/Blog/Posts/New", routePrefix + "{blogShortName}/posts/new");
options.Conventions.AddPageRoute("/Blogs/Posts/Index", routePrefix + "{blogShortName}");
options.Conventions.AddPageRoute("/Blogs/Posts/Detail", routePrefix + "{blogShortName}/{postUrl}");
options.Conventions.AddPageRoute("/Blogs/Posts/Edit", routePrefix + "{blogShortName}/posts/{postId}/edit");
options.Conventions.AddPageRoute("/Blogs/Posts/New", routePrefix + "{blogShortName}/posts/new");
});
}
}

319
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/Detail.cshtml

@ -1,319 +0,0 @@
@page
@inherits Volo.Blogging.Pages.Blog.BloggingPage
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Http.Extensions
@using Volo.Abp.Users
@using Volo.Blogging
@using Volo.Blogging.Pages.Blog.Posts
@using Volo.Blogging.Areas.Blog.Helpers.TagHelpers
@inject IAuthorizationService Authorization
@model DetailModel
@{
ViewBag.PageTitle = "Blog";
var hasCommentingPermission = CurrentUser.IsAuthenticated; //TODO: Apply real policy!
}
@section scripts {
<abp-script-bundle name="@typeof(DetailModel).FullName">
<abp-script src="/Pages/Blog/Posts/detail.js" />
</abp-script-bundle>
}
@section styles {
<abp-style-bundle name="@typeof(DetailModel).FullName">
<abp-style src="/Pages/Blog/Shared/Styles/blog.css" />
</abp-style-bundle>
}
<div class="vs-blog vs-blog-detail">
<abp-input asp-for="FocusCommentId" class="m-0" />
<div class="container">
<div class="row">
<div class="col-md-8 col-lg-8 mx-auto">
<section class="hero-section">
<div class="hero-articles">
<div class="hero-content">
<h1 class="mb-3">
<a asp-page="./Detail" asp-route-postUrl="@Model.Post.Url" asp-route-blogShortName="@Model.BlogShortName">@Model.Post.Title</a>
</h1>
<div class="article-owner">
<div class="article-infos">
<div class="user-card mt-3 mb-4">
<div class="row">
<div class="col-auto pr-1">
@if (Model.Post.Writer != null)
{
<img gravatar-email="@Model.Post.Writer.Email" default-image="Identicon" class="article-avatar" />
}
</div>
<div class="col pl-1">
@if (Model.Post.Writer != null)
{
<h5 class="mt-2 mb-1">@(Model.Post.Writer.UserName) <span>@ConvertDatetimeToTimeAgo(Model.Post.CreationTime)</span></h5>
}
<i class="fa fa-eye"></i> @L["WiewsWithCount", @Model.Post.ReadCount]
<span class="vs-seperator">|</span>
<i class="fa fa-comment"></i> @L["CommentWithCount", @Model.CommentCount]
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Posts.Update))
{
<span class="seperator">|</span>
<a asp-page="./Edit" asp-route-postId="@Model.Post.Id" asp-route-blogShortName="@Model.BlogShortName">
<i class="fa fa-pencil"></i> @L["Edit"]
</a>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Posts.Delete) || (CurrentUser.Id.HasValue && CurrentUser.Id == Model.Post.CreatorId))
{
<span class="seperator">|</span>
<a href="#" id="DeletePostLink" data-postid="@Model.Post.Id" data-blogShortName="@Model.BlogShortName">
<i class="fa fa-trash"></i> @L["Delete"]
</a>
}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="img-container mb-3">
<img src="@Model.Post.CoverImage" />
</div>
</div>
</section>
</div>
</div>
<div class="row">
<div class="col-md-8 col-lg-8 mx-auto">
<section class="post-content">
<p>
@Html.Raw(RenderMarkdownToHtml(Model.Post.Content))
</p>
</section>
</div>
</div>
<div class="row">
<div class="col-md-8 col-lg-8 mx-auto">
@if (Model.Post.Tags.Count > 0)
{
<div class="tags">
<h5>@L["TagsInThisArticle"]</h5>
@foreach (var tag in Model.Post.Tags)
{
<a asp-page="/Blog/Posts/Index" asp-route-blogShortName="@Model.BlogShortName" asp-route-tagName="@tag.Name" class="tag">@tag.Name</a>
}
</div>
}
@if (Model.CommentsWithReplies.Count > 0)
{
<abp-row v-align="Start">
<abp-column size-sm="_12">
<p class="float-left"><i class="fa fa-comment"></i> @L["CommentWithCount", @Model.CommentCount]</p>
@if (hasCommentingPermission)
{
<a abp-button="Primary" class="btn-rounded float-right active" href="#LeaveComment">@L["LeaveComment"]</a>
}
else
{
<a abp-button="Primary" class="btn-rounded float-right active" href="/Account/Login?returnUrl=@Request.GetEncodedPathAndQuery()">@L["LeaveComment"]</a>
}
</abp-column>
</abp-row>
}
<div class="comment-area">
@foreach (var commentWithRepliesDto in Model.CommentsWithReplies)
{
<div class="media">
<img gravatar-email="@commentWithRepliesDto.Comment.Writer.Email" default-image="Identicon" class="d-flex mr-3 rounded-circle comment-avatar" />
<div class="media-body">
<h5 class="comment-owner">
@(commentWithRepliesDto.Comment.Writer == null ? "" : commentWithRepliesDto.Comment.Writer.UserName)
<span>@ConvertDatetimeToTimeAgo(commentWithRepliesDto.Comment.CreationTime)</span>
</h5>
<p id="@commentWithRepliesDto.Comment.Id">
@commentWithRepliesDto.Comment.Text
</p>
<div class="comment-buttons">
@if (hasCommentingPermission)
{
<a href="#" class="tag replyLink" data-relpyid="@commentWithRepliesDto.Comment.Id">
<i class="fa fa-reply" aria-hidden="true"></i> @L["Reply"]
</a>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Delete))
{
<span class="seperator">|</span>
<a href="#" class="tag deleteLink" data-deleteid="@commentWithRepliesDto.Comment.Id">
<i class="fa fa-trash" aria-hidden="true"></i> @L["Delete"]
</a>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Update) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<span class="seperator">|</span>
<a href="#" class="tag updateLink" data-updateid="@commentWithRepliesDto.Comment.Id">
<i class="fa fa-pencil" aria-hidden="true"></i> @L["Edit"]
</a>
}
</div>
@if (hasCommentingPermission)
{
<div class="comment-form mt-4 replyForm">
<div class="clearfix p-4">
<h3 class="mt-0">
@L["ReplyTo", commentWithRepliesDto.Comment.Writer == null ? "" : commentWithRepliesDto.Comment.Writer.UserName]
</h3>
<div>
<form method="post">
<input name="postId" value="@Model.Post.Id" hidden />
<input name="repliedCommentId" value="@commentWithRepliesDto.Comment.Id" hidden />
<div class="form-group">
<textarea class="form-control no-border" name="text" id="textBoxId" rows="4"></textarea>
</div>
<abp-button button-type="Primary" class="btn-rounded float-right" type="submit" text="@L["Comment"].Value" />
<abp-button button-type="Danger" class="btn-rounded float-right replyCancelButton" text="@L["Cancel"].Value" />
</form>
</div>
</div>
</div>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Update) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<div class="comment-form mt-4 editForm">
<div class="clearfix p-4">
<div>
<form class="editFormClass">
<input name="commentId" value="@commentWithRepliesDto.Comment.Id" hidden />
<div class="form-group">
<textarea class="form-control no-border" name="text" id="textBoxId" rows="4">@commentWithRepliesDto.Comment.Text</textarea>
</div>
<abp-button button-type="Primary" class="btn-rounded float-right" type="submit" text="@L["Submit"].Value" />
<abp-button button-type="Danger" class="btn-rounded float-right editCancelButton" text="@L["Cancel"].Value" />
</form>
</div>
</div>
</div>
}
@foreach (var reply in commentWithRepliesDto.Replies)
{
<div class="media">
<img gravatar-email="@reply.Writer.Email" default-image="Identicon" class="d-flex mr-3 rounded-circle comment-avatar" />
<div class="media-body">
<h5 class="comment-owner">
@(reply.Writer == null ? "" : reply.Writer.UserName)
<span>@ConvertDatetimeToTimeAgo(reply.CreationTime)</span>
</h5>
<p id="@reply.Id">
@reply.Text
</p>
<div class="comment-buttons">
@if (hasCommentingPermission)
{
<a href="#" class="tag replyLink" data-relpyid="@commentWithRepliesDto.Comment.Id">
<i class="fa fa-reply" aria-hidden="true"></i> @L["Reply"]
</a>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Delete) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<span class="seperator">|</span>
<a href="#" class="tag deleteLink" data-deleteid="@reply.Id">
<i class="fa fa-trash" aria-hidden="true"></i> @L["Delete"]
</a>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Update) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<span class="seperator">|</span>
<a href="#" class="tag updateLink" data-updateid="@reply.Id">
<i class="fa fa-pencil" aria-hidden="true"></i> @L["Edit"]
</a>
}
</div>
@if (hasCommentingPermission)
{
<div class="comment-form mt-4 replyForm">
<div class="clearfix bg-light p-4">
<h3 class="mt-0">
@L["ReplyTo", commentWithRepliesDto.Comment.Writer == null ? "" : commentWithRepliesDto.Comment.Writer.UserName]
</h3>
<div>
<form method="post">
<input name="postId" value="@Model.Post.Id" hidden />
<input name="repliedCommentId" value="@commentWithRepliesDto.Comment.Id" hidden />
<div class="form-group">
<textarea class="form-control no-border" name="text" id="textBoxId" rows="4"></textarea>
</div>
<abp-button button-type="Primary" class="btn-rounded float-right" type="submit" text="@L["Submit"].Value" />
<abp-button button-type="Danger" class="btn-rounded float-right replyCancelButton" text="@L["Cancel"].Value" />
</form>
</div>
</div>
</div>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Update) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<div class="comment-form mt-4 editForm">
<div class="clearfix bg-light p-4">
<div>
<form class="editFormClass">
<input name="commentId" value="@reply.Id" hidden />
<div class="form-group">
<textarea class="form-control no-border" name="text" id="textBoxId" rows="4">@reply.Text</textarea>
</div>
<abp-button button-type="Primary" class="btn-rounded float-right" type="submit" text="@L["Submit"].Value" />
<abp-button button-type="Danger" class="btn-rounded float-right editCancelButton" text="@L["Cancel"].Value" />
</form>
</div>
</div>
</div>
}
</div>
</div>
}
</div>
</div>
}
</div>
@if (hasCommentingPermission)
{
<div class="comment-form mt-4" id="LeaveComment">
<div class="vs-blog-title mb-0">
<h3>@L["LeaveComment"]</h3>
</div>
<div class="clearfix bg-light p-4">
<div>
<form method="post">
<input name="postId" value="@Model.Post.Id" hidden />
<input name="repliedCommentId" id="repliedCommentId" hidden />
<div class="form-group">
<textarea class="form-control no-border" name="text" id="textBoxId" rows="4"></textarea>
</div>
<abp-button button-type="Primary" class="btn-rounded float-right" type="submit" text="@L["Submit"].Value" />
</form>
</div>
</div>
</div>
}
else
{
<a abp-button="Primary" class="btn-rounded float-right active mt-3" href="/Account/Login?returnUrl=@Request.GetEncodedPathAndQuery()">@L["LeaveComment"]</a>
}
</div>
</div>
</div>
</div>

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/BloggingPage.cs → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/BloggingPage.cs

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/BloggingPageModel.cs → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/BloggingPageModel.cs

2
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Index.cshtml → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Index.cshtml

@ -3,7 +3,7 @@
@inherits BloggingPage
@model IndexModel
@{
ViewBag.PageTitle = "Blog";
ViewBag.PageTitle = "Blogs";
}
<h2>
@L["Blogs"]

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Index.cshtml.cs → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Index.cshtml.cs

319
modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Detail.cshtml

@ -0,0 +1,319 @@
@page
@inherits Volo.Blogging.Pages.Blog.BloggingPage
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Http.Extensions
@using Volo.Abp.Users
@using Volo.Blogging
@using Volo.Blogging.Pages.Blog.Posts
@using Volo.Blogging.Areas.Blog.Helpers.TagHelpers
@inject IAuthorizationService Authorization
@model DetailModel
@{
ViewBag.PageTitle = Model.Post.Title;
var hasCommentingPermission = CurrentUser.IsAuthenticated; //TODO: Apply real policy!
}
@section scripts {
<abp-script-bundle name="@typeof(DetailModel).FullName">
<abp-script src="/Pages/Blogs/Posts/detail.js" />
</abp-script-bundle>
}
@section styles {
<abp-style-bundle name="@typeof(DetailModel).FullName">
<abp-style src="/Pages/Blogs/Shared/Styles/blog.css" />
</abp-style-bundle>
}
<div class="vs-blog vs-blog-detail">
<abp-input asp-for="FocusCommentId" class="m-0" />
<div class="container">
<div class="row">
<div class="col-md-8 col-lg-8 mx-auto">
<section class="hero-section">
<div class="hero-articles">
<div class="hero-content">
<h1 class="mb-3">
<a asp-page="./Detail" asp-route-postUrl="@Model.Post.Url" asp-route-blogShortName="@Model.BlogShortName">@Model.Post.Title</a>
</h1>
<div class="article-owner">
<div class="article-infos">
<div class="user-card mt-3 mb-4">
<div class="row">
<div class="col-auto pr-1">
@if (Model.Post.Writer != null)
{
<img gravatar-email="@Model.Post.Writer.Email" default-image="Identicon" class="article-avatar" />
}
</div>
<div class="col pl-1">
@if (Model.Post.Writer != null)
{
<h5 class="mt-2 mb-1">@(Model.Post.Writer.UserName) <span>@ConvertDatetimeToTimeAgo(Model.Post.CreationTime)</span></h5>
}
<i class="fa fa-eye"></i> @L["WiewsWithCount", @Model.Post.ReadCount]
<span class="vs-seperator">|</span>
<i class="fa fa-comment"></i> @L["CommentWithCount", @Model.CommentCount]
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Posts.Update))
{
<span class="seperator">|</span>
<a asp-page="./Edit" asp-route-postId="@Model.Post.Id" asp-route-blogShortName="@Model.BlogShortName">
<i class="fa fa-pencil"></i> @L["Edit"]
</a>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Posts.Delete) || (CurrentUser.Id.HasValue && CurrentUser.Id == Model.Post.CreatorId))
{
<span class="seperator">|</span>
<a href="#" id="DeletePostLink" data-postid="@Model.Post.Id" data-blogShortName="@Model.BlogShortName">
<i class="fa fa-trash"></i> @L["Delete"]
</a>
}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="img-container mb-3">
<img src="@Model.Post.CoverImage" />
</div>
</div>
</section>
</div>
</div>
<div class="row">
<div class="col-md-8 col-lg-8 mx-auto">
<section class="post-content">
<p>
@Html.Raw(RenderMarkdownToHtml(Model.Post.Content))
</p>
</section>
</div>
</div>
<div class="row">
<div class="col-md-8 col-lg-8 mx-auto">
@if (Model.Post.Tags.Count > 0)
{
<div class="tags">
<h5>@L["TagsInThisArticle"]</h5>
@foreach (var tag in Model.Post.Tags)
{
<a asp-page="/Blog/Posts/Index" asp-route-blogShortName="@Model.BlogShortName" asp-route-tagName="@tag.Name" class="tag">@tag.Name</a>
}
</div>
}
@if (Model.CommentsWithReplies.Count > 0)
{
<abp-row v-align="Start">
<abp-column size-sm="_12">
<p class="float-left"><i class="fa fa-comment"></i> @L["CommentWithCount", @Model.CommentCount]</p>
@if (hasCommentingPermission)
{
<a abp-button="Primary" class="btn-rounded float-right active" href="#LeaveComment">@L["LeaveComment"]</a>
}
else
{
<a abp-button="Primary" class="btn-rounded float-right active" href="/Account/Login?returnUrl=@Request.GetEncodedPathAndQuery()">@L["LeaveComment"]</a>
}
</abp-column>
</abp-row>
<div class="comment-area">
@foreach (var commentWithRepliesDto in Model.CommentsWithReplies)
{
<div class="media">
<img gravatar-email="@commentWithRepliesDto.Comment.Writer.Email" default-image="Identicon" class="d-flex mr-3 rounded-circle comment-avatar" />
<div class="media-body">
<h5 class="comment-owner">
@(commentWithRepliesDto.Comment.Writer == null ? "" : commentWithRepliesDto.Comment.Writer.UserName)
<span>@ConvertDatetimeToTimeAgo(commentWithRepliesDto.Comment.CreationTime)</span>
</h5>
<p id="@commentWithRepliesDto.Comment.Id">
@commentWithRepliesDto.Comment.Text
</p>
<div class="comment-buttons">
@if (hasCommentingPermission)
{
<a href="#" class="tag replyLink" data-relpyid="@commentWithRepliesDto.Comment.Id">
<i class="fa fa-reply" aria-hidden="true"></i> @L["Reply"]
</a>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Delete))
{
<span class="seperator">|</span>
<a href="#" class="tag deleteLink" data-deleteid="@commentWithRepliesDto.Comment.Id">
<i class="fa fa-trash" aria-hidden="true"></i> @L["Delete"]
</a>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Update) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<span class="seperator">|</span>
<a href="#" class="tag updateLink" data-updateid="@commentWithRepliesDto.Comment.Id">
<i class="fa fa-pencil" aria-hidden="true"></i> @L["Edit"]
</a>
}
</div>
@if (hasCommentingPermission)
{
<div class="comment-form mt-4 replyForm">
<div class="clearfix p-4">
<h3 class="mt-0">
@L["ReplyTo", commentWithRepliesDto.Comment.Writer == null ? "" : commentWithRepliesDto.Comment.Writer.UserName]
</h3>
<div>
<form method="post">
<input name="postId" value="@Model.Post.Id" hidden />
<input name="repliedCommentId" value="@commentWithRepliesDto.Comment.Id" hidden />
<div class="form-group">
<textarea class="form-control no-border" name="text" id="textBoxId" rows="4"></textarea>
</div>
<abp-button button-type="Primary" class="btn-rounded float-right" type="submit" text="@L["Comment"].Value" />
<abp-button button-type="Danger" class="btn-rounded float-right replyCancelButton" text="@L["Cancel"].Value" />
</form>
</div>
</div>
</div>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Update) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<div class="comment-form mt-4 editForm">
<div class="clearfix p-4">
<div>
<form class="editFormClass">
<input name="commentId" value="@commentWithRepliesDto.Comment.Id" hidden />
<div class="form-group">
<textarea class="form-control no-border" name="text" id="textBoxId" rows="4">@commentWithRepliesDto.Comment.Text</textarea>
</div>
<abp-button button-type="Primary" class="btn-rounded float-right" type="submit" text="@L["Submit"].Value" />
<abp-button button-type="Danger" class="btn-rounded float-right editCancelButton" text="@L["Cancel"].Value" />
</form>
</div>
</div>
</div>
}
@foreach (var reply in commentWithRepliesDto.Replies)
{
<div class="media">
<img gravatar-email="@reply.Writer.Email" default-image="Identicon" class="d-flex mr-3 rounded-circle comment-avatar" />
<div class="media-body">
<h5 class="comment-owner">
@(reply.Writer == null ? "" : reply.Writer.UserName)
<span>@ConvertDatetimeToTimeAgo(reply.CreationTime)</span>
</h5>
<p id="@reply.Id">
@reply.Text
</p>
<div class="comment-buttons">
@if (hasCommentingPermission)
{
<a href="#" class="tag replyLink" data-relpyid="@commentWithRepliesDto.Comment.Id">
<i class="fa fa-reply" aria-hidden="true"></i> @L["Reply"]
</a>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Delete) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<span class="seperator">|</span>
<a href="#" class="tag deleteLink" data-deleteid="@reply.Id">
<i class="fa fa-trash" aria-hidden="true"></i> @L["Delete"]
</a>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Update) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<span class="seperator">|</span>
<a href="#" class="tag updateLink" data-updateid="@reply.Id">
<i class="fa fa-pencil" aria-hidden="true"></i> @L["Edit"]
</a>
}
</div>
@if (hasCommentingPermission)
{
<div class="comment-form mt-4 replyForm">
<div class="clearfix bg-light p-4">
<h3 class="mt-0">
@L["ReplyTo", commentWithRepliesDto.Comment.Writer == null ? "" : commentWithRepliesDto.Comment.Writer.UserName]
</h3>
<div>
<form method="post">
<input name="postId" value="@Model.Post.Id" hidden />
<input name="repliedCommentId" value="@commentWithRepliesDto.Comment.Id" hidden />
<div class="form-group">
<textarea class="form-control no-border" name="text" id="textBoxId" rows="4"></textarea>
</div>
<abp-button button-type="Primary" class="btn-rounded float-right" type="submit" text="@L["Submit"].Value" />
<abp-button button-type="Danger" class="btn-rounded float-right replyCancelButton" text="@L["Cancel"].Value" />
</form>
</div>
</div>
</div>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Update) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<div class="comment-form mt-4 editForm">
<div class="clearfix bg-light p-4">
<div>
<form class="editFormClass">
<input name="commentId" value="@reply.Id" hidden />
<div class="form-group">
<textarea class="form-control no-border" name="text" id="textBoxId" rows="4">@reply.Text</textarea>
</div>
<abp-button button-type="Primary" class="btn-rounded float-right" type="submit" text="@L["Submit"].Value" />
<abp-button button-type="Danger" class="btn-rounded float-right editCancelButton" text="@L["Cancel"].Value" />
</form>
</div>
</div>
</div>
}
</div>
</div>
}
</div>
</div>
}
</div>
}
@if (hasCommentingPermission)
{
<div class="comment-form mt-4" id="LeaveComment">
<div class="vs-blog-title mb-0">
<h3>@L["LeaveComment"]</h3>
</div>
<div class="clearfix bg-light p-4">
<div>
<form method="post">
<input name="postId" value="@Model.Post.Id" hidden />
<input name="repliedCommentId" id="repliedCommentId" hidden />
<div class="form-group">
<textarea class="form-control no-border" name="text" id="textBoxId" rows="4"></textarea>
</div>
<abp-button button-type="Primary" class="btn-rounded float-right" type="submit" text="@L["Submit"].Value" />
</form>
</div>
</div>
</div>
}
else
{
<a abp-button="Primary" class="btn-rounded float-right active mt-3" href="/Account/Login?returnUrl=@Request.GetEncodedPathAndQuery()">@L["LeaveComment"]</a>
}
</div>
</div>
</div>
</div>

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/Detail.cshtml.cs → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Detail.cshtml.cs

4
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/Edit.cshtml → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Edit.cshtml

@ -9,13 +9,13 @@
@section styles {
<abp-style-bundle name="@typeof(EditModel).FullName">
<abp-style type="@typeof(TuiEditorStyleContributor)" />
<abp-style src="/Pages/Blog/Posts/new.css" />
<abp-style src="/Pages/Blogs/Posts/new.css" />
</abp-style-bundle>
}
@section scripts {
<abp-script-bundle name="@typeof(EditModel).FullName">
<abp-script type="@typeof(TuiEditorScriptContributor)" />
<abp-script src="/Pages/Blog/Posts/edit.js" />
<abp-script src="/Pages/Blogs/Posts/edit.js" />
</abp-script-bundle>
}

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/Edit.cshtml.cs → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Edit.cshtml.cs

10
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/Index.cshtml → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml

@ -12,13 +12,13 @@
@section scripts {
<abp-script-bundle name="@typeof(IndexModel).FullName">
<abp-script type="@typeof(OwlCarouselScriptContributor)" />
<abp-script src="/Pages/Blog/Shared/Scripts/blog.js" />
<abp-script src="/Pages/Blogs/Shared/Scripts/blog.js" />
</abp-script-bundle>
}
@section styles {
<abp-style-bundle name="@typeof(IndexModel).FullName">
<abp-script type="@typeof(OwlCarouselStyleContributor)" />
<abp-style src="/Pages/Blog/Shared/Styles/blog.css" />
<abp-style src="/Pages/Blogs/Shared/Styles/blog.css" />
</abp-style-bundle>
}
@ -69,7 +69,7 @@
<p class="tags">
@foreach (var tag in post.Tags)
{
<a asp-page="/Blog/Posts/Index" asp-route-blogShortName="@Model.BlogShortName" asp-route-tagName="@tag.Name" class="tag">@tag.Name</a>
<a asp-page="/Blogs/Posts/Index" asp-route-blogShortName="@Model.BlogShortName" asp-route-tagName="@tag.Name" class="tag">@tag.Name</a>
}
</p>
@ -178,7 +178,7 @@
<p class="tags">
@foreach (var tag in post.Tags)
{
<a asp-page="/Blog/Posts/Index" asp-route-blogShortName="@Model.BlogShortName" asp-route-tagName="@tag.Name" class="tag">@tag.Name</a>
<a asp-page="/Blogs/Posts/Index" asp-route-blogShortName="@Model.BlogShortName" asp-route-tagName="@tag.Name" class="tag">@tag.Name</a>
}
</p>
</div>
@ -200,7 +200,7 @@
@foreach (var popularTag in Model.PopularTags)
{
<div class="list-group-item">
<a asp-page="/Blog/Posts/Index" asp-route-blogShortName="@Model.BlogShortName" asp-route-tagName="@popularTag.Name">@popularTag.Name <span>(@popularTag.UsageCount @L["Posts"])</span></a>
<a asp-page="/Blogs/Posts/Index" asp-route-blogShortName="@Model.BlogShortName" asp-route-tagName="@popularTag.Name">@popularTag.Name <span>(@popularTag.UsageCount @L["Posts"])</span></a>
</div>
}

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/Index.cshtml.cs → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml.cs

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/Index.css → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.css

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/Index.min.css → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.min.css

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/Index.scss → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.scss

4
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/New.cshtml → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/New.cshtml

@ -9,13 +9,13 @@
@section styles {
<abp-style-bundle name="@typeof(NewModel).FullName">
<abp-style type="@typeof(TuiEditorStyleContributor)" />
<abp-style src="/Pages/Blog/Posts/new.css" />
<abp-style src="/Pages/Blogs/Posts/new.css" />
</abp-style-bundle>
}
@section scripts {
<abp-script-bundle name="@typeof(NewModel).FullName">
<abp-script type="@typeof(TuiEditorScriptContributor)" />
<abp-script src="/Pages/Blog/Posts/new.js" />
<abp-script src="/Pages/Blogs/Posts/new.js" />
</abp-script-bundle>
}
<main>

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/New.cshtml.cs → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/New.cshtml.cs

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/detail.js → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/detail.js

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/edit.js → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/edit.js

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/new.css → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/new.css

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/new.css.map → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/new.css.map

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/new.js → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/new.js

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/new.min.css → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/new.min.css

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/new.scss → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/new.scss

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Shared/Scripts/blog.js → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Scripts/blog.js

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Shared/Styles/_bootstrap-overwrite.scss → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/_bootstrap-overwrite.scss

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Shared/Styles/_custom.scss → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/_custom.scss

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Shared/Styles/_header.scss → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/_header.scss

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Shared/Styles/_home.css → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/_home.css

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Shared/Styles/_home.min.css → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/_home.min.css

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Shared/Styles/_home.scss → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/_home.scss

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Shared/Styles/_post.scss → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/_post.scss

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Shared/Styles/blog.css → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/blog.css

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Shared/Styles/blog.css.map → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/blog.css.map

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Shared/Styles/blog.min.css → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/blog.min.css

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Shared/Styles/blog.scss → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Styles/blog.scss

0
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/_ViewImports.cshtml → modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/_ViewImports.cshtml

4
modules/docs/src/Volo.Docs.Web/Markdown/MarkdownDocumentToHtmlConverter.cs

@ -24,7 +24,7 @@ namespace Volo.Docs.Markdown
}
private const string MdLinkFormat = "[{0}]({1}{2}/{3}/{4}{5}/{6})";
private const string MarkdownLinkRegExp = @"\[(.*?)\]\((.*?\.md)\)";
private const string MarkdownLinkRegExp = @"\[(.*?)\]\((.*?)\)";
private const string AnchorLinkRegExp = @"<a[^>]+href=\""(.*?)\""[^>]*>(.*)?</a>";
public virtual string Convert(ProjectDto project, DocumentWithDetailsDto document, string version,
@ -56,7 +56,7 @@ namespace Volo.Docs.Markdown
var normalized = Regex.Replace(content, MarkdownLinkRegExp, delegate (Match match)
{
var link = match.Groups[2].Value;
if (UrlHelper.IsExternalLink(link))
if (UrlHelper.IsExternalLink(link) || !link.EndsWith(".md"))
{
return match.Value;
}

4
modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs

@ -439,7 +439,7 @@ namespace Volo.Docs.Pages.Documents.Project
var value = keyValue.Split("=")[1];
UserPreferences.Add(key, value);
UserPreferences.Add(key + "_Value", DocumentPreferences.Parameters?.FirstOrDefault(p => p.Name == key)
UserPreferences.Add(key + "_Value", DocumentPreferences?.Parameters?.FirstOrDefault(p => p.Name == key)
?.Values.FirstOrDefault(v => v.Key == value).Value);
}
}
@ -456,7 +456,7 @@ namespace Volo.Docs.Pages.Documents.Project
UserPreferences.Add(key, value);
UserPreferences.Add(key + "_Value",
DocumentPreferences.Parameters?.FirstOrDefault(p => p.Name == key)?.Values
DocumentPreferences?.Parameters?.FirstOrDefault(p => p.Name == key)?.Values
.FirstOrDefault(v => v.Key == value).Value);
}

4
modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/en.json

@ -7,7 +7,7 @@
"UserName": "User name",
"EmailAddress": "Email address",
"PhoneNumber": "Phone number",
"UserInformations": "User informations",
"UserInformations": "User information",
"DisplayName:IsDefault": "Default",
"DisplayName:IsStatic": "Static",
"DisplayName:IsPublic": "Public",
@ -99,4 +99,4 @@
"Description:Abp.Identity.User.IsUserNameUpdateEnabled": "Whether the username can be updated by the user.",
"Description:Abp.Identity.User.IsEmailUpdateEnabled": "Whether the email can be updated by the user."
}
}
}

2
modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator.cs

@ -95,7 +95,7 @@ namespace Volo.Abp.IdentityServer.AspNetIdentity
protected virtual async Task ReplaceEmailToUsernameOfInputIfNeeds(ResourceOwnerPasswordValidationContext context)
{
if (!ValidationHandler.IsValidEmailAddress(context.UserName))
if (!ValidationHelper.IsValidEmailAddress(context.UserName))
{
return;
}

15
modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/EntityFrameworkCore/IdentityServerDbContextModelCreatingExtensions.cs

@ -1,9 +1,10 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.Modeling;
using Volo.Abp.EntityFrameworkCore.ValueComparers;
using Volo.Abp.EntityFrameworkCore.ValueConverters;
using Volo.Abp.IdentityServer.ApiResources;
using Volo.Abp.IdentityServer.Clients;
using Volo.Abp.IdentityServer.Devices;
@ -203,10 +204,8 @@ namespace Volo.Abp.IdentityServer.EntityFrameworkCore
identityResource.Property(x => x.DisplayName).HasMaxLength(IdentityResourceConsts.DisplayNameMaxLength);
identityResource.Property(x => x.Description).HasMaxLength(IdentityResourceConsts.DescriptionMaxLength);
identityResource.Property(x => x.Properties)
.HasConversion(
d => JsonConvert.SerializeObject(d, Formatting.None),
s => JsonConvert.DeserializeObject<Dictionary<string, string>>(s)
);
.HasConversion(new AbpJsonValueConverter<Dictionary<string, string>>())
.Metadata.SetValueComparer(new AbpDictionaryValueComparer<string, string>());
identityResource.HasMany(x => x.UserClaims).WithOne().HasForeignKey(x => x.IdentityResourceId).IsRequired();
});
@ -230,10 +229,8 @@ namespace Volo.Abp.IdentityServer.EntityFrameworkCore
apiResource.Property(x => x.DisplayName).HasMaxLength(ApiResourceConsts.DisplayNameMaxLength);
apiResource.Property(x => x.Description).HasMaxLength(ApiResourceConsts.DescriptionMaxLength);
apiResource.Property(x => x.Properties)
.HasConversion(
d => JsonConvert.SerializeObject(d, Formatting.None),
s => JsonConvert.DeserializeObject<Dictionary<string, string>>(s)
);
.HasConversion(new AbpJsonValueConverter<Dictionary<string, string>>())
.Metadata.SetValueComparer(new AbpDictionaryValueComparer<string, string>());
apiResource.HasMany(x => x.Secrets).WithOne().HasForeignKey(x => x.ApiResourceId).IsRequired();
apiResource.HasMany(x => x.Scopes).WithOne().HasForeignKey(x => x.ApiResourceId).IsRequired();

2
npm/ng-packs/apps/dev-app/src/index.html

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>DevApp</title>
<title>ABP Dev</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />

4
npm/ng-packs/apps/dev-app/tsconfig.app.json

@ -6,8 +6,8 @@
"paths": {
"@abp/ng.core": ["packages/core/src/public-api.ts"],
"@abp/ng.core/*": ["packages/core/src/lib/*"],
// "@abp/ng.theme.shared": ["packages/theme-shared/src/public-api.ts"],
// "@abp/ng.theme.shared/*": ["packages/theme-shared/src/lib/*"],
"@abp/ng.theme.shared": ["packages/theme-shared/src/public-api.ts"],
"@abp/ng.theme.shared/*": ["packages/theme-shared/src/lib/*"],
"@abp/ng.theme.basic": ["packages/theme-basic/src/public-api.ts"],
"@abp/ng.theme.basic/*": ["packages/theme-basic/src/lib/*"],
"@abp/ng.account": ["packages/account/src/public-api.ts"],

1
npm/ng-packs/package.json

@ -74,7 +74,6 @@
"just-compare": "^1.3.0",
"lerna": "^3.19.0",
"ng-packagr": "^5.7.1",
"ngx-perfect-scrollbar": "^8.0.0",
"ngxs-reset-plugin": "^1.2.0",
"ngxs-schematic": "^1.1.9",
"prettier": "^1.18.2",

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

@ -5,7 +5,10 @@
></abp-tenant-box>
<div class="abp-account-container">
<div class="card mt-3 shadow-sm rounded">
<div
*ngIf="enableLocalLogin; else disableLocalLoginTemplate"
class="card mt-3 shadow-sm rounded"
>
<div class="card-body p-5">
<ng-content *ngTemplateOutlet="mainContentRef"></ng-content>
</div>
@ -14,3 +17,10 @@
</div>
</div>
</div>
<ng-template #disableLocalLoginTemplate>
<div class="alert alert-warning">
<strong>{{ 'AbpAccount::InvalidLoginRequest' | abpLocalization }}</strong>
{{ 'AbpAccount::ThereAreNoLoginSchemesConfiguredForThisClient' | abpLocalization }}
</div>
</ng-template>

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

@ -1,4 +1,6 @@
import { Component, Input, TemplateRef } from '@angular/core';
import { ConfigState, takeUntilDestroy } from '@abp/ng.core';
import { Component, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core';
import { Store } from '@ngxs/store';
import { Account } from '../../models/account';
@Component({
@ -7,10 +9,31 @@ import { Account } from '../../models/account';
exportAs: 'abpAuthWrapper',
})
export class AuthWrapperComponent
implements Account.AuthWrapperComponentInputs, Account.AuthWrapperComponentOutputs {
implements
Account.AuthWrapperComponentInputs,
Account.AuthWrapperComponentOutputs,
OnInit,
OnDestroy {
@Input()
readonly mainContentRef: TemplateRef<any>;
@Input()
readonly cancelContentRef: TemplateRef<any>;
enableLocalLogin = true;
constructor(private store: Store) {}
ngOnInit() {
this.store
.select(ConfigState.getSetting('Abp.Account.EnableLocalLogin'))
.pipe(takeUntilDestroy(this))
.subscribe(value => {
if (value) {
this.enableLocalLogin = value.toLowerCase() !== 'false';
}
});
}
ngOnDestroy() {}
}

4
npm/ng-packs/packages/account/src/lib/components/change-password/change-password.component.ts

@ -72,7 +72,7 @@ export class ChangePasswordComponent
required,
validatePassword(passwordRulesArr),
minLength(requiredLength),
maxLength(32),
maxLength(128),
],
},
],
@ -83,7 +83,7 @@ export class ChangePasswordComponent
required,
validatePassword(passwordRulesArr),
minLength(requiredLength),
maxLength(32),
maxLength(128),
],
},
],

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

@ -12,7 +12,7 @@
</abp-auth-wrapper>
<ng-template #mainContentRef>
<h4>{{ 'AbpAccount::Login' | abpLocalization }}</h4>
<strong>
<strong *ngIf="isSelfRegistrationEnabled">
{{ 'AbpAccount::AreYouANewUser' | abpLocalization }}
<a class="text-decoration-none" routerLink="/account/register">{{
'AbpAccount::Register' | abpLocalization

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

@ -1,15 +1,12 @@
import { GetAppConfiguration, ConfigState, SessionState } from '@abp/ng.core';
import { Component, Inject, Optional } from '@angular/core';
import { AuthService, SetRemember, ConfigState } from '@abp/ng.core';
import { ToasterService } from '@abp/ng.theme.shared';
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Navigate } from '@ngxs/router-plugin';
import { Store } from '@ngxs/store';
import { OAuthService } from 'angular-oauth2-oidc';
import { from, throwError } from 'rxjs';
import { Options } from '../../models/options';
import { ToasterService } from '@abp/ng.theme.shared';
import { catchError, finalize, switchMap, tap } from 'rxjs/operators';
import { throwError } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import snq from 'snq';
import { HttpHeaders } from '@angular/common/http';
const { maxLength, minLength, required } = Validators;
@ -17,47 +14,43 @@ const { maxLength, minLength, required } = Validators;
selector: 'abp-login',
templateUrl: './login.component.html',
})
export class LoginComponent {
export class LoginComponent implements OnInit {
form: FormGroup;
inProgress: boolean;
isSelfRegistrationEnabled = true;
constructor(
private fb: FormBuilder,
private oauthService: OAuthService,
private store: Store,
private toasterService: ToasterService,
@Optional() @Inject('ACCOUNT_OPTIONS') private options: Options,
) {
this.oauthService.configure(this.store.selectSnapshot(ConfigState.getOne('environment')).oAuthConfig);
this.oauthService.loadDiscoveryDocument();
private authService: AuthService,
) {}
ngOnInit() {
this.isSelfRegistrationEnabled =
(
(this.store.selectSnapshot(
ConfigState.getSetting('Abp.Account.IsSelfRegistrationEnabled'),
) as string) || ''
).toLowerCase() !== 'false';
this.form = this.fb.group({
username: ['', [required, maxLength(255)]],
password: ['', [required, maxLength(32)]],
password: ['', [required, maxLength(128)]],
remember: [false],
});
}
onSubmit() {
if (this.form.invalid) return;
// this.oauthService.setStorage(this.form.value.remember ? localStorage : sessionStorage);
this.inProgress = true;
const tenant = this.store.selectSnapshot(SessionState.getTenant);
from(
this.oauthService.fetchTokenUsingPasswordFlow(
this.form.get('username').value,
this.form.get('password').value,
new HttpHeaders({ ...(tenant && tenant.id && { __tenant: tenant.id }) }),
),
)
this.authService
.login(this.form.get('username').value, this.form.get('password').value)
.pipe(
switchMap(() => this.store.dispatch(new GetAppConfiguration())),
tap(() => {
const redirectUrl = snq(() => window.history.state).redirectUrl || (this.options || {}).redirectUrl || '/';
this.store.dispatch(new Navigate([redirectUrl]));
}),
catchError(err => {
this.toasterService.error(
snq(() => err.error.error_description) ||
@ -69,6 +62,8 @@ export class LoginComponent {
}),
finalize(() => (this.inProgress = false)),
)
.subscribe();
.subscribe(() => {
this.store.dispatch(new SetRemember(this.form.get('remember').value));
});
}
}

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

@ -16,7 +16,13 @@
'AbpAccount::Login' | abpLocalization
}}</a>
</strong>
<form [formGroup]="form" (ngSubmit)="onSubmit()" validateOnSubmit class="mt-4">
<form
*ngIf="isSelfRegistrationEnabled"
[formGroup]="form"
(ngSubmit)="onSubmit()"
validateOnSubmit
class="mt-4"
>
<div class="form-group">
<label for="input-user-name">{{ 'AbpAccount::UserName' | abpLocalization }}</label
><span> * </span

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

@ -1,4 +1,4 @@
import { ConfigState, GetAppConfiguration, ABP, SessionState } from '@abp/ng.core';
import { ConfigState, GetAppConfiguration, ABP, SessionState, AuthService } from '@abp/ng.core';
import { ToasterService } from '@abp/ng.theme.shared';
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@ -23,20 +23,36 @@ export class RegisterComponent implements OnInit {
inProgress: boolean;
isSelfRegistrationEnabled = true;
constructor(
private fb: FormBuilder,
private accountService: AccountService,
private oauthService: OAuthService,
private store: Store,
private toasterService: ToasterService,
) {
this.oauthService.configure(
this.store.selectSnapshot(ConfigState.getOne('environment')).oAuthConfig,
);
this.oauthService.loadDiscoveryDocument();
}
private authService: AuthService,
) {}
ngOnInit() {
this.isSelfRegistrationEnabled =
(
this.store.selectSnapshot(
ConfigState.getSetting('Abp.Account.IsSelfRegistrationEnabled'),
) || ''
).toLowerCase() !== 'false';
if (!this.isSelfRegistrationEnabled) {
this.toasterService.warn(
{
key: 'AbpAccount::SelfRegistrationDisabledMessage',
defaultValue: 'Self registration is disabled.',
},
null,
{ life: 10000 },
);
return;
}
const passwordRules: ABP.Dictionary<string> = this.store.selectSnapshot(
ConfigState.getSettings('Identity.Password'),
);
@ -67,7 +83,7 @@ export class RegisterComponent implements OnInit {
username: ['', [required, maxLength(255)]],
password: [
'',
[required, validatePassword(passwordRulesArr), minLength(requiredLength), maxLength(32)],
[required, validatePassword(passwordRulesArr), minLength(requiredLength), maxLength(128)],
],
email: ['', [required, email]],
});
@ -85,25 +101,10 @@ export class RegisterComponent implements OnInit {
appName: 'Angular',
} as RegisterRequest;
const tenant = this.store.selectSnapshot(SessionState.getTenant);
this.accountService
.register(newUser)
.pipe(
switchMap(() =>
from(
this.oauthService.fetchTokenUsingPasswordFlow(
newUser.userName,
newUser.password,
new HttpHeaders({
...(tenant && tenant.id && { __tenant: tenant.id }),
}),
),
),
),
switchMap(() => this.store.dispatch(new GetAppConfiguration())),
tap(() => this.store.dispatch(new Navigate(['/']))),
take(1),
switchMap(() => this.authService.login(newUser.userName, newUser.password)),
catchError(err => {
this.toasterService.error(
snq(() => err.error.error_description) ||

49
npm/ng-packs/packages/core/src/lib/abstracts/ng-model.component.ts

@ -1,27 +1,48 @@
import { ControlValueAccessor } from '@angular/forms';
import { ChangeDetectorRef, Component, Injector, Input, Type } from '@angular/core';
import { ChangeDetectorRef, Component, Injector, Input } from '@angular/core';
@Component({ selector: 'abp-abstract-ng-model', template: '' })
export class AbstractNgModelComponent<T = any> implements ControlValueAccessor {
@Input() disabled: boolean;
// Not an abstract class on purpose. Do not change!
// tslint:disable-next-line: use-component-selector
@Component({ template: '' })
export class AbstractNgModelComponent<T = any, U = T> implements ControlValueAccessor {
protected _value: T;
protected cdRef: ChangeDetectorRef;
onChange: (value: T) => {};
onTouched: () => {};
@Input()
disabled: boolean;
@Input()
readonly: boolean;
@Input()
valueFn: (value: U, previousValue?: T) => T = value => (value as any) as T;
@Input()
valueLimitFn: (value: T, previousValue?: T) => any = value => false;
@Input()
set value(value: T) {
value = this.valueFn((value as any) as U, this._value);
if (this.valueLimitFn(value, this._value) !== false || this.readonly) return;
@Input() set value(value: T) {
this._value = value;
this.notifyValueChange();
}
get value(): T {
return this._value;
return this._value || this.defaultValue;
}
onChange: (value: T) => {};
onTouched: () => {};
protected _value: T;
protected cdRef: ChangeDetectorRef;
get defaultValue(): T {
return this._value;
}
constructor(public injector: Injector) {
this.cdRef = injector.get<ChangeDetectorRef>(ChangeDetectorRef as Type<ChangeDetectorRef>);
// tslint:disable-next-line: deprecation
this.cdRef = injector.get(ChangeDetectorRef);
}
notifyValueChange(): void {
@ -31,8 +52,8 @@ export class AbstractNgModelComponent<T = any> implements ControlValueAccessor {
}
writeValue(value: T): void {
this._value = value;
setTimeout(() => this.cdRef.detectChanges(), 0);
this._value = this.valueLimitFn(value, this._value) || value;
setTimeout(() => this.cdRef.markForCheck(), 0);
}
registerOnChange(fn: any): void {

8
npm/ng-packs/packages/core/src/lib/actions/session.actions.ts

@ -8,3 +8,11 @@ export class SetTenant {
static readonly type = '[Session] Set Tenant';
constructor(public payload: ABP.BasicItem) {}
}
export class ModifyOpenedTabCount {
static readonly type = '[Session] Modify Opened Tab Count';
constructor(public operation: 'increase' | 'decrease') {}
}
export class SetRemember {
static readonly type = '[Session] Set Remember';
constructor(public payload: boolean) {}
}

9
npm/ng-packs/packages/core/src/lib/core.module.ts

@ -6,7 +6,7 @@ import { RouterModule } from '@angular/router';
import { NgxsRouterPluginModule } from '@ngxs/router-plugin';
import { NgxsStoragePluginModule } from '@ngxs/storage-plugin';
import { NgxsModule, NGXS_PLUGINS } from '@ngxs/store';
import { OAuthModule } from 'angular-oauth2-oidc';
import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
import { AbstractNgModelComponent } from './abstracts/ng-model.component';
import { DynamicLayoutComponent } from './components/dynamic-layout.component';
import { RouterOutletComponent } from './components/router-outlet.component';
@ -34,12 +34,15 @@ import { ReplaceableComponentsState } from './states/replaceable-components.stat
import { InitDirective } from './directives/init.directive';
import { ReplaceableTemplateDirective } from './directives/replaceable-template.directive';
export function storageFactory(): OAuthStorage {
return localStorage;
}
@NgModule({
imports: [
NgxsModule.forFeature([ReplaceableComponentsState, ProfileState, SessionState, ConfigState]),
NgxsRouterPluginModule.forRoot(),
NgxsStoragePluginModule.forRoot({ key: ['SessionState'] }),
OAuthModule.forRoot(),
OAuthModule,
CommonModule,
HttpClientModule,
FormsModule,
@ -127,6 +130,8 @@ export class CoreModule {
deps: [Injector],
useFactory: localeInitializer,
},
...OAuthModule.forRoot().providers,
{ provide: OAuthStorage, useFactory: storageFactory },
],
};
}

7
npm/ng-packs/packages/core/src/lib/models/session.ts

@ -4,5 +4,12 @@ export namespace Session {
export interface State {
language: string;
tenant: ABP.BasicItem;
sessionDetail: SessionDetail;
}
export interface SessionDetail {
openedTabCount: number;
lastExitTime: number;
remember: boolean;
}
}

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

Loading…
Cancel
Save