Browse Source

Merge branch 'dev' of https://github.com/abpframework/abp into dev

pull/3997/head
Akın Sabri Çam 6 years ago
parent
commit
4e4d4450b5
  1. 2
      common.props
  2. 2
      docs/cs/Getting-Started-AspNetCore-Application.md
  3. 12
      docs/en/API/Dynamic-CSharp-API-Clients.md
  4. 247
      docs/en/Blog-Posts/2020-05-08 v2_7_Release/Post.md
  5. BIN
      docs/en/Blog-Posts/2020-05-08 v2_7_Release/abp-commercial-ui-extensions.png
  6. BIN
      docs/en/Blog-Posts/2020-05-08 v2_7_Release/audit-log-entity-changes.png
  7. BIN
      docs/en/Blog-Posts/2020-05-08 v2_7_Release/easy-crm.png
  8. BIN
      docs/en/Blog-Posts/2020-05-08 v2_7_Release/github-contribution-graph.png
  9. BIN
      docs/en/Blog-Posts/2020-05-08 v2_7_Release/module-entity-extended-ui.png
  10. BIN
      docs/en/Blog-Posts/2020-05-08 v2_7_Release/tenant-entity-changes.png
  11. BIN
      docs/en/Blog-Posts/2020-05-08 v2_7_Release/text-template-content-ui.png
  12. 51
      docs/en/Customizing-Application-Modules-Overriding-Services.md
  13. 2
      docs/en/Getting-Started-AspNetCore-Application.md
  14. 39
      docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md
  15. 2
      docs/en/Samples/Microservice-Demo.md
  16. 3
      docs/en/Text-Templating.md
  17. 126
      docs/en/Tutorials/Part-1.md
  18. 159
      docs/en/Tutorials/Part-2.md
  19. BIN
      docs/en/Tutorials/images/bookstore-angular-file-tree.png
  20. BIN
      docs/en/Tutorials/images/bookstore-book-list.png
  21. BIN
      docs/en/Tutorials/images/bookstore-creating-book-list-terminal.png
  22. BIN
      docs/en/Tutorials/images/bookstore-creating-book-module-terminal.png
  23. BIN
      docs/en/Tutorials/images/bookstore-creating-books-module-terminal.png
  24. BIN
      docs/en/Tutorials/images/generated-proxies.png
  25. 12
      docs/zh-Hans/API/Dynamic-CSharp-API-Clients.md
  26. 2
      docs/zh-Hans/Customizing-Application-Modules-Guide.md
  27. 2
      docs/zh-Hans/Entity-Framework-Core-Migrations.md
  28. 2
      docs/zh-Hans/Getting-Started.md
  29. 2
      docs/zh-Hans/Samples/Microservice-Demo.md
  30. 9
      docs/zh-Hans/Tutorials/Angular/Part-I.md
  31. 480
      docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md
  32. 167
      docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md
  33. 241
      docs/zh-Hans/Tutorials/Part-1.md
  34. 199
      docs/zh-Hans/Tutorials/Part-2.md
  35. BIN
      docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png
  36. BIN
      docs/zh-Hans/Tutorials/images/bookstore-book-list.png
  37. BIN
      docs/zh-Hans/Tutorials/images/bookstore-creating-book-list-terminal.png
  38. BIN
      docs/zh-Hans/Tutorials/images/bookstore-creating-book-module-terminal.png
  39. BIN
      docs/zh-Hans/Tutorials/images/generated-proxies.png
  40. 12
      docs/zh-Hans/docs-nav.json
  41. 9
      framework/Volo.Abp.sln
  42. 16
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/SignalR/SignalRBrowserScriptContributor.cs
  43. 12
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js
  44. 10
      framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Builder/AbpAspNetCoreMvcApplicationBuilderExtensions.cs
  45. 12
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs
  46. 6
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpController.cs
  47. 9
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DependencyInjection/AbpAspNetCoreMvcConventionalDependencyRegistrar.cs
  48. 3
      framework/src/Volo.Abp.AspNetCore.SignalR/FodyWeavers.xml
  49. 30
      framework/src/Volo.Abp.AspNetCore.SignalR/FodyWeavers.xsd
  50. 27
      framework/src/Volo.Abp.AspNetCore.SignalR/Properties/launchSettings.json
  51. 23
      framework/src/Volo.Abp.AspNetCore.SignalR/Volo.Abp.AspNetCore.SignalR.csproj
  52. 115
      framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRModule.cs
  53. 135
      framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpHub.cs
  54. 43
      framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalRConventionalRegistrar.cs
  55. 14
      framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions.cs
  56. 21
      framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalRUserIdProvider.cs
  57. 43
      framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfig.cs
  58. 33
      framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubRouteAttribute.cs
  59. 47
      framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpAspNetCoreApplicationBuilderExtensions.cs
  60. 15
      framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Routing/AbpEndpointRouterOptions.cs
  61. 18
      framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Routing/EndpointRouteBuilderContext.cs
  62. 13
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/VirtualFileSystem/WebContentFileProvider.cs
  63. 49
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateProxyCommand.cs
  64. 17
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs
  65. 16
      framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/zh-Hant.json
  66. 2
      framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionManager.cs
  67. 3
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkManager.cs
  68. 2
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs
  69. 2
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/AbpAspNetCoreMvcUiBootstrapDemoModule.cs
  70. 2
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/AbpAspNetCoreMvcUiThemeBasicDemoModule.cs
  71. 2
      framework/test/Volo.Abp.AspNetCore.Mvc.Versioning.Tests/Volo/Abp/AspNetCore/Mvc/Versioning/AbpAspNetCoreMvcVersioningTestModule.cs
  72. 2
      framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo/Abp/AspNetCore/App/AbpSerilogTestModule.cs
  73. 2
      modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs
  74. 12
      modules/blogging/src/Volo.Blogging.Web/BloggingTwitterOptions.cs
  75. 3
      modules/blogging/src/Volo.Blogging.Web/BloggingUrlOptions.cs
  76. 9
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Detail.cshtml
  77. 66
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml
  78. 2
      modules/client-simulation/demo/Volo.ClientSimulation.Demo/ClientSimulationDemoModule.cs
  79. 2
      modules/docs/app/VoloDocs.Web/VoloDocsWebModule.cs
  80. 4
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo.Abp.IdentityServer.Domain.csproj
  81. 2
      npm/lerna.json
  82. 2
      npm/ng-packs/lerna.version.json
  83. 12
      npm/ng-packs/package.json
  84. 2
      npm/ng-packs/packages/account-config/package.json
  85. 6
      npm/ng-packs/packages/account/package.json
  86. 2
      npm/ng-packs/packages/core/package.json
  87. 2
      npm/ng-packs/packages/core/src/lib/states/config.state.ts
  88. 4
      npm/ng-packs/packages/feature-management/package.json
  89. 2
      npm/ng-packs/packages/identity-config/package.json
  90. 8
      npm/ng-packs/packages/identity/package.json
  91. 4
      npm/ng-packs/packages/permission-management/package.json
  92. 2
      npm/ng-packs/packages/setting-management-config/package.json
  93. 6
      npm/ng-packs/packages/setting-management/package.json
  94. 2
      npm/ng-packs/packages/tenant-management-config/package.json
  95. 8
      npm/ng-packs/packages/tenant-management/package.json
  96. 4
      npm/ng-packs/packages/theme-basic/package.json
  97. 4
      npm/ng-packs/packages/theme-shared/package.json
  98. 19
      npm/ng-packs/yarn.lock
  99. 6
      npm/packs/anchor-js/package.json
  100. 6
      npm/packs/aspnetcore.mvc.ui.theme.basic/package.json

2
common.props

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

2
docs/cs/Getting-Started-AspNetCore-Application.md

@ -57,7 +57,7 @@ namespace BasicAspNetCoreApplication
app.UseStaticFiles();
app.UseRouting();
app.UseMvcWithDefaultRouteAndArea();
app.UseConfiguredEndpoints();
}
}
}

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

@ -39,7 +39,7 @@ Now, it's ready to create the client proxies. Example:
````csharp
[DependsOn(
typeof(AbpHttpClientModule), //used to create client proxies
typeof(BookStoreApplicationModule) //contains the application service interfaces
typeof(BookStoreApplicationContractsModule) //contains the application service interfaces
)]
public class MyClientAppModule : AbpModule
{
@ -47,7 +47,7 @@ public class MyClientAppModule : AbpModule
{
//Create dynamic client proxies
context.Services.AddHttpClientProxies(
typeof(BookStoreApplicationModule).Assembly
typeof(BookStoreApplicationContractsModule).Assembly
);
}
}
@ -142,12 +142,12 @@ The examples above have configured the "Default" remote service endpoint. You ma
````csharp
context.Services.AddHttpClientProxies(
typeof(BookStoreApplicationModule).Assembly,
remoteServiceName: "BookStore"
typeof(BookStoreApplicationContractsModule).Assembly,
remoteServiceConfigurationName: "BookStore"
);
````
`remoteServiceName` parameter matches the service endpoint configured via `AbpRemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint.
`remoteServiceConfigurationName` parameter matches the service endpoint configured via `AbpRemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint.
### As Default Services
@ -155,7 +155,7 @@ When you create a service proxy for `IBookAppService`, you can directly inject t
````csharp
context.Services.AddHttpClientProxies(
typeof(BookStoreApplicationModule).Assembly,
typeof(BookStoreApplicationContractsModule).Assembly,
asDefaultServices: false
);
````

247
docs/en/Blog-Posts/2020-05-08 v2_7_Release/Post.md

@ -0,0 +1,247 @@
# ABP Framework v2.7.0 Has Been Released!
The **ABP Framework** & and the **ABP Commercial** v2.7 have been released. We hadn't created blog post for the 2.4, 2.4 and 2.6 releases, so this post will also cover **what's new** with these releases and **what we've done** in the last 2 months.
## About the Release Cycle & Development
Reminding that we had started to release a new minor feature version **in every two weeks**, generally on Thursdays. Our goal is to deliver new features as soon as possible.
We've completed & merged hundreds of issues and pull requests with **1,300+ commits** in the last 7-8 weeks, only for the ABP Framework repository. Daily commit counts are constantly increasing:
![github-contribution-graph](github-contribution-graph.png)
ABP.IO Platform is rapidly growing and we are getting more and more contributions from the community.
## What's New in the ABP Framework?
### Object Extending System
In the last few releases, we've mostly focused on providing ways to extend existing modules when you use them as NuGet/NPM Packages.
The Object Extending System allows module developers to create extensible modules and allows application developers to customize and extend a module easily.
For example, you can add two extension properties to the user entity of the identity module:
````csharp
ObjectExtensionManager.Instance
.AddOrUpdate<IdentityUser>(options =>
{
options.AddOrUpdateProperty<string>("SocialSecurityNumber");
options.AddOrUpdateProperty<bool>("IsSuperUser");
}
);
````
It is easy to define validation rules for the properties:
````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<IdentityUserCreateDto, string>(
"SocialSecurityNumber",
options =>
{
options.Attributes.Add(new RequiredAttribute());
options.Attributes.Add(
new StringLengthAttribute(32) {
MinimumLength = 6
}
);
});
````
You can even write custom code to validate the property. It automatically works for the objects those are parameters of an application service, controller or a page.
While extension properties of an entity are normally stored in a single JSON formatted field in the database table, you can easily configure to store a property as a table field using the EF Core mapping:
````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<IdentityUser, string>(
"SocialSecurityNumber",
options =>
{
options.MapEfCore(b => b.HasMaxLength(32));
}
);
````
See the [Object Extensions document](https://docs.abp.io/en/abp/latest/Object-Extensions) for details about this system.
See also the [Customizing the Existing Modules](https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Guide) guide to learn all the possible customization options.
### Text Templating Package
[Volo.Abp.TextTemplating](https://www.nuget.org/packages/Volo.Abp.TextTemplating) is a new package introduced with the v2.7.0. Previously, [Volo.Abp.Emailing](https://www.nuget.org/packages/Volo.Abp.Emailing) package had a similar functionality but it was limited, experimental and tightly coupled to the emailing.
The new text templating package allows you to define text based templates those can be easily localized and reused. You can define layout templates and share the layout from other templates.
We are currently using it for email sending. A module needs to send an email typically defines a template. Example:
````xml
<h3>{{L "PasswordReset"}}</h3>
<p>{{L "PasswordResetInfoInEmail"}}</p>
<div>
<a href="{{model.link}}">{{L "ResetMyPassword"}}</a>
</div>
````
This is a typical password reset email template.
* The template system is based on the open source [Scriban library](https://github.com/lunet-io/scriban). So it supports if conditions, loops and much more.
* `model` is used to pass data to the template (just like the ASP.NET Core MVC).
* `L` is a special function that localizes the given string.
It is typical to use the same layout for all emails. So, you can define a layout template. This is the standard layout template comes with the framework:
````xml
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
</head>
<body>
{{content}}
</body>
</html>
````
A layout should have a `{{content}}` area to render the child content (just like the `RenderBody()` in the MVC).
It is very easy to override a template content by the final application to customize it.
Whenever you need to render a template, use the `ITemplateRenderer` service by providing the template name and a model. See the [text templating documentation](https://docs.abp.io/en/abp/latest/Text-Templating) for details. We've even created a UI for the ABP Commercial (see the related section below).
### Subscribing to the Exceptions
ABP Framework's [exception handling system](https://docs.abp.io/en/abp/latest/Exception-Handling) automatically handles exceptions and returns an appropriate result to the client. In some cases, you may want to have a callback that is notified whenever an exception occurs. In this way, for example, you can send an email or take any action based on the exception.
Just create a class derived from the `ExceptionSubscriber` class in your application:
````csharp
public class MyExceptionSubscriber : ExceptionSubscriber
{
public override async Task HandleAsync(ExceptionNotificationContext context)
{
//TODO...
}
}
````
See the [exception handling](https://docs.abp.io/en/abp/latest/Exception-Handling) document for more.
### Others
There are many minor features and enhancements made to the framework in the past releases. Here, a few ones:
* Added `AbpLocalizationOptions.DefaultResourceType` to set the default resource type for the application. In this way, the localization system uses the default resource whenever the resource was not specified. The latest application startup template already configures it, but you may want to set it for your existing applications.
* Added `IsEnabled` to permission definition. In this way, you can completely disable a permission and hide the related functionality from the application. This can be a way of feature switch for some applications. See [#3486](https://github.com/abpframework/abp/issues/3486) for usage.
* Added Dutch and German localizations to all the localization resources defined by the framework. Thanks to the contributors.
## What's New in the ABP Commercial
The goal of the [ABP Commercial](https://commercial.abp.io/) is to provide pre-build application functionalities, code generation tools, professional themes, advanced samples and premium support for ABP Framework based projects.
We are working on the ABP Commercial in the parallel to align with the ABP Framework features and provide more modules, theme options and tooling.
This section explains what's going on the ABP Commercial side.
### Module Entity Extension System
Module entity extension system is a higher level API that uses the object extension system (introduced above) and provides an easy way to add extension properties to existing entities. A new extension property easily automatically becomes a part of the HTTP API and the User Interface.
Example: Add a `SocialSecurityNumber` to the user entity of the identity module
````csharp
ObjectExtensionManager.Instance.Modules()
.ConfigureIdentity(identity =>
{
identity.ConfigureUser(user =>
{
user.AddOrUpdateProperty<string>( //property type: string
"SocialSecurityNumber", //property name
property =>
{
//validation rules
property.Attributes.Add(new RequiredAttribute());
property.Attributes.Add(
new StringLengthAttribute(64) {
MinimumLength = 4
}
);
//...other configurations for this property
}
);
});
});
````
With just such a configuration, the user interface will have the new property (on the table and on the create/edit forms):
![module-entity-extended-ui](module-entity-extended-ui.png)
The new property can be easily localized and validated. Currently, it supports primitive types like string, number and boolean, but we planned to add more advanced scenarios by the time (like navigation/lookup properties).
See the [Module Entity Extensions](https://docs.abp.io/en/commercial/latest/guides/module-entity-extensions) guide to learn how to use it and configure details.
#### Other Extension Points
There are also some other pre-defined points to customize and extend the user interface of a depended module:
* You can add a new action for an entity on the data table (left side on the picture below).
* You can add new buttons (or other controls) to the page toolbar (right side on the picture below).
* You can add custom columns to a data table.
![abp-commercial-ui-extensions](abp-commercial-ui-extensions.png)
See the [Customizing the Modules](https://docs.abp.io/en/commercial/latest/guides/customizing-modules) guide to learn all the possible ways to customize a depended module.
### Text Template Management Module
We are introducing a new module with the v2.7 release: [Text Template Management](https://docs.abp.io/en/commercial/latest/modules/text-template-management). It is basically used to edit text/email templates (introduced with the ABP Framework 2.7) on the user interface and save changed in the database.
A screenshot from the content editing for the password reset email template:
![text-template-content-ui](text-template-content-ui.png)
This module comes pre-installed when you create a new project.
### Entity History Views
Audit logging UI module now shows all the entity changes in the application with property change details.
![audit-log-entity-changes](audit-log-entity-changes.png)
You can also check history for an entity when you click to the actions menu for the entity:
![tenant-entity-changes](tenant-entity-changes.png)
### More Samples
We are creating more advanced sample applications built with the ABP Commercial. Easy CRM is one of them which will be available in a few days to the commercial customers.
Here, a screenshot from the Easy CRM dashboard:
![easy-crm](easy-crm.png)
It has accounts, contacts, product groups, products, orders and so on.
### New Modules
We continue to improve existing modules and creating new modules. In addition to the new [text template management](https://docs.abp.io/en/commercial/latest/modules/text-template-management) module introduced above;
* We've recently released a [payment module](https://commercial.abp.io/modules/Volo.Payment) that currently works with PayU and 2Checkout payment gateways. More gateways will be added by the time.
* We've created a simple [Twilio SMS integration](https://docs.abp.io/en/commercial/latest/modules/twilio-sms) module to send SMS over the Twilio.
* We are working on a **chat module** that is currently being developed and will be available in the next weeks.
* We are working on the **organization unit management** system for the identity module to create hierarchical organization units (domain layer will be open source & free).
More modules, theme and tooling options are being developed for the ABP Commercial and the ABP Framework.
## ABP Framework vs ABP Commercial
We ([Volosoft](https://volosoft.com/) - the core team behind the ABP.IO platform), are spending almost equal time on the ABP Framework and the ABP Commercial and we consider the ABP.IO platform as a whole.
[ABP Framework](https://abp.io/) provides all the infrastructure and application independent framework features to make you more productive, focus on your own business code and implement software development best practices. It provides you a well defined and comfortable development experience without repeating yourself.
[ABP Commercial](https://commercial.abp.io/) provides pre-built functionalities, themes and tooling to save your time if your requirements involve these functionalities in addition to the premium support for the framework and the pre-built modules.

BIN
docs/en/Blog-Posts/2020-05-08 v2_7_Release/abp-commercial-ui-extensions.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
docs/en/Blog-Posts/2020-05-08 v2_7_Release/audit-log-entity-changes.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

BIN
docs/en/Blog-Posts/2020-05-08 v2_7_Release/easy-crm.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

BIN
docs/en/Blog-Posts/2020-05-08 v2_7_Release/github-contribution-graph.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
docs/en/Blog-Posts/2020-05-08 v2_7_Release/module-entity-extended-ui.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

BIN
docs/en/Blog-Posts/2020-05-08 v2_7_Release/tenant-entity-changes.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
docs/en/Blog-Posts/2020-05-08 v2_7_Release/text-template-content-ui.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

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

@ -106,30 +106,33 @@ You could completely **re-write** the entire business logic for a user creation
[ExposeServices(typeof(IdentityUserManager))]
public class MyIdentityUserManager : IdentityUserManager
{
public MyIdentityUserManager(
IdentityUserStore store,
IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<IdentityUser> passwordHasher,
IEnumerable<IUserValidator<IdentityUser>> userValidators,
IEnumerable<IPasswordValidator<IdentityUser>> passwordValidators,
ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger<IdentityUserManager> logger,
ICancellationTokenProvider cancellationTokenProvider
) : base(
store,
optionsAccessor,
passwordHasher,
userValidators,
passwordValidators,
keyNormalizer,
errors,
services,
logger,
cancellationTokenProvider)
{
}
public MyIdentityUserManager(
IdentityUserStore store,
IIdentityRoleRepository roleRepository,
IIdentityUserRepository userRepository,
IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<IdentityUser> passwordHasher,
IEnumerable<IUserValidator<IdentityUser>> userValidators,
IEnumerable<IPasswordValidator<IdentityUser>> passwordValidators,
ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger<IdentityUserManager> logger,
ICancellationTokenProvider cancellationTokenProvider) :
base(store,
roleRepository,
userRepository,
optionsAccessor,
passwordHasher,
userValidators,
passwordValidators,
keyNormalizer,
errors,
services,
logger,
cancellationTokenProvider)
{
}
public override async Task<IdentityResult> CreateAsync(IdentityUser user)
{

2
docs/en/Getting-Started-AspNetCore-Application.md

@ -57,7 +57,7 @@ namespace BasicAspNetCoreApplication
app.UseStaticFiles();
app.UseRouting();
app.UseMvcWithDefaultRouteAndArea();
app.UseConfiguredEndpoints();
}
}
}

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

@ -158,6 +158,17 @@ You can find the source code of the completed example [here](https://github.com/
````
* Help! I am getting ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** error!
* This occurs when you use Azure Authority **v2.0 endpoint** without requesting `email` scope. [Abp checks unique email to create user](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). Simply add
````csharp
options.Scope.Add("email");
````
to your openid configuration.
* Help! I keep getting ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** error!
* If you set your **CallbackPath** in appsettings as:
@ -169,18 +180,19 @@ You can find the source code of the completed example [here](https://github.com/
}
````
your **Redirect URI** of your application in azure portal must be with <u>domain</u> like `https://localhost:44320/signin-azuread-oidc`, not only `/signin-azuread-oidc`.
your **Redirect URI** of your application in azure portal must be <u>with domain</u> like `https://localhost:44320/signin-azuread-oidc`, not only `/signin-azuread-oidc`.
* Help! I keep getting ***AADSTS700051: The response_type 'token' is not enabled for the application.*** error!
* Help! I am getting ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** error!
* This error occurs when you request **token** (access token) along with **id_token** without enabling Access tokens on Azure portal app registrations. Simply tick **Access tokens** checkbox located on top of ID tokens to be able to request token aswell.
* Help! I keep getting ***AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret*** error!
* This occurs when you use Azure Authority **v2.0 endpoint** without requesting `email` scope. [Abp checks unique email to create user](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). Simply add
````csharp
options.Scope.Add("email");
````
to your openid configuration.
* This error occurs when you request **code** along with **id_token**. You need to add **client secret** on azure portal app registrations, under **Certificates & secrets** menu. Afterwards, you need to add openid configuration option like:
````csharp
options.ClientSecret = "Value of your secret on azure portal";
````
* How can I **debug/watch** which claims I get before they get mapped?
@ -188,13 +200,12 @@ You can find the source code of the completed example [here](https://github.com/
````csharp
options.Events.OnTokenValidated = (async context =>
{
var claimsFromOidcProvider = context.Principal.Claims.ToList();
await Task.CompletedTask;
});
{
var claimsFromOidcProvider = context.Principal.Claims.ToList();
await Task.CompletedTask;
});
````
## See Also
* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md).

2
docs/en/Samples/Microservice-Demo.md

@ -552,7 +552,7 @@ app.MapWhen(ctx => ctx.Request.Path.ToString().StartsWith("/api/abp/") ||
ctx.Request.Path.ToString().StartsWith("/Abp/"),
app2 =>
{
app2.UseMvcWithDefaultRouteAndArea();
app2.UseConfiguredEndpoints();
});
app.UseOcelot().Wait();

3
docs/en/Text-Templating.md

@ -0,0 +1,3 @@
# Text-Templating
TODO

126
docs/en/Tutorials/Part-1.md

@ -756,28 +756,28 @@ Open a new command line interface (terminal window) and go to your `angular` fol
yarn
```
#### BooksModule
#### BookModule
Run the following command line to create a new module, named `BooksModule`:
Run the following command line to create a new module, named `BookModule`:
```bash
yarn ng generate module books --route books --module app.module
yarn ng generate module book --routing true
```
![Generating books module](./images/bookstore-creating-books-module-terminal.png)
![Generating books module](./images/bookstore-creating-book-module-terminal.png)
#### Routing
Open the `app-routing.module.ts` file in `src\app` folder. Add the new `import` and replace `books` path as shown below
Open the `app-routing.module.ts` file in `src\app` folder. Add the new `import` and add a route as shown below
```js
import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added this line to imports <==
//...replaced original books path with the below
//...added books path with the below to the routes array
{
path: 'books',
component: ApplicationLayoutComponent,
loadChildren: () => import('./books/books.module').then(m => m.BooksModule),
loadChildren: () => import('./book/book.module').then(m => m.BookModule),
data: {
routes: {
name: '::Menu:Books',
@ -789,71 +789,50 @@ import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added th
* The `ApplicationLayoutComponent` configuration sets the application layout to the new page. We added the `data` object. The `name` is the menu item name and the `iconClass` is the icon of the menu item.
Run `yarn start` and wait for Angular to serve the application:
```bash
yarn start
```
Open the browser and navigate to http://localhost:4200/books. You'll see a blank page saying "*books works!*".
![initial-books-page](./images/bookstore-initial-books-page-with-layout.png)
#### Book list component
Replace the `books.component.html` in the `app\books` folder with the following content:
```html
<router-outlet></router-outlet>
```
Then run the command below on the terminal in the root folder to generate a new component, named book-list:
Run the command below on the terminal in the root folder to generate a new component, named book-list:
```bash
yarn ng generate component books/book-list
yarn ng generate component book/book-list
```
![Creating books list](./images/bookstore-creating-book-list-terminal.png)
Open `books.module.ts` file in the `app\books` folder and replace the content as below:
Open `book.module.ts` file in the `app\book` folder and replace the content as below:
```js
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BooksRoutingModule } from './books-routing.module';
import { BooksComponent } from './books.component';
import { BookRoutingModule } from './book-routing.module';
import { BookListComponent } from './book-list/book-list.component';
import { SharedModule } from '../shared/shared.module'; //<== added this line ==>
@NgModule({
declarations: [BooksComponent, BookListComponent],
declarations: [BookListComponent],
imports: [
CommonModule,
BooksRoutingModule,
BookRoutingModule,
SharedModule, //<== added this line ==>
]
],
})
export class BooksModule { }
export class BookModule {}
```
* We imported `SharedModule` and added to `imports` array.
Open `books-routing.module.ts` file in the `app\books` folder and replace the content as below:
Open `book-routing.module.ts` file in the `app\book` folder and replace the content as below:
```js
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { BookListComponent } from './book-list/book-list.component'; // <== added this line ==>
import { BooksComponent } from './books.component';
import { BookListComponent } from './book-list/book-list.component'; //<== added this line ==>
//<== replaced routes ==>
// <== replaced routes ==>
const routes: Routes = [
{
path: '',
component: BooksComponent,
children: [{ path: '', component: BookListComponent }],
component: BookListComponent,
},
];
@ -861,36 +840,42 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class BooksRoutingModule { }
export class BookRoutingModule { }
```
* We imported `BookListComponent` and replaced `routes` const.
We'll see **book-list works!** text on the books page:
Run `yarn start` and wait for Angular to serve the application:
```bash
yarn start
```
Open the browser and navigate to http://localhost:4200/books. We'll see **book-list works!** text on the books page:
![Initial book list page](./images/bookstore-initial-book-list-page.png)
#### Create BooksState
#### Create BookState
Run the following command in the terminal to create a new state, named `BooksState`:
```bash
npx @ngxs/cli --name books --directory src/app/books
npx @ngxs/cli --name book --directory src/app/book
```
* This command creates `books.state.ts` and `books.actions.ts` files in the `src/app/books/state` folder. See the [NGXS CLI documentation](https://www.ngxs.io/plugins/cli).
* This command creates `book.state.ts` and `book.actions.ts` files in the `src/app/book/state` folder. See the [NGXS CLI documentation](https://www.ngxs.io/plugins/cli).
Import the `BooksState` to the `app.module.ts` in the `src/app` folder and then add the `BooksState` to `forRoot` static method of `NgxsModule` as an array element of the first parameter of the method.
Import the `BookState` to the `app.module.ts` in the `src/app` folder and then add the `BookState` to `forRoot` static method of `NgxsModule` as an array element of the first parameter of the method.
```js
// ...
import { BooksState } from './books/state/books.state'; //<== imported BooksState ==>
import { BookState } from './books/state/book.state'; //<== imported BookState ==>
@NgModule({
imports: [
// other imports
NgxsModule.forRoot([BooksState]), //<== added BooksState ==>
NgxsModule.forRoot([BookState]), //<== added BookState ==>
//other imports
],
@ -919,46 +904,46 @@ The generated files looks like below:
Actions can either be thought of as a command which should trigger something to happen, or as the resulting event of something that has already happened. [See NGXS Actions documentation](https://www.ngxs.io/concepts/actions).
Open the `books.actions.ts` file in `app/books/state` folder and replace the content below:
Open the `book.actions.ts` file in `app/book/state` folder and replace the content below:
```js
export class GetBooks {
static readonly type = '[Books] Get';
static readonly type = '[Book] Get';
}
```
#### Implement BooksState
#### Implement BookState
Open the `books.state.ts` file in `app/books/state` folder and replace the content below:
Open the `book.state.ts` file in `app/book/state` folder and replace the content below:
```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks } from './books.actions';
import { BookService } from '../../app/shared/services';
import { GetBooks } from './book.actions';
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../../app/shared/models';
import { BookDto } from '../models';
export class BooksStateModel {
export class BookStateModel {
public book: PagedResultDto<BookDto>;
}
@State<BooksStateModel>({
name: 'BooksState',
defaults: { book: {} } as BooksStateModel,
@State<BookStateModel>({
name: 'BookState',
defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BooksState {
export class BookState {
@Selector()
static getBooks(state: BooksStateModel) {
static getBooks(state: BookStateModel) {
return state.book.items || [];
}
constructor(private bookService: BookService) {}
@Action(GetBooks)
get(ctx: StateContext<BooksStateModel>) {
get(ctx: StateContext<BookStateModel>) {
return this.bookService.getListByInput().pipe(
tap((booksResponse) => {
ctx.patchState({
@ -969,22 +954,23 @@ export class BooksState {
}
}
```
* We added the book property to BooksStateModel model.
* We added the `GetBooks` action that retrieves the books data via `BooksService` that generated via ABP CLI and patches the state.
* We added the book property to BookStateModel model.
* We added the `GetBooks` action that retrieves the book data via `BookService` that generated via ABP CLI and patches the state.
* `NGXS` requires to return the observable without subscribing it in the get function.
#### BookListComponent
Open the `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below:
Open the `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below:
```js
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../../app/shared/models';
import { GetBooks } from '../state/books.actions';
import { BooksState } from '../state/books.state';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
@Component({
selector: 'app-book-list',
@ -992,7 +978,7 @@ import { BooksState } from '../state/books.state';
styleUrls: ['./book-list.component.scss'],
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
booksType = BookType;
@ -1018,7 +1004,7 @@ export class BookListComponent implements OnInit {
* We added the `get` function that updates store to get the books.
* See the [Dispatching actions](https://ngxs.gitbook.io/ngxs/concepts/store#dispatching-actions) and [Select](https://ngxs.gitbook.io/ngxs/concepts/select) on the `NGXS` documentation for more information on these `NGXS` features.
Open the `book-list.component.html` file in `app\books\book-list` folder and replace the content as below:
Open the `book-list.component.html` file in `app\book\book-list` folder and replace the content as below:
```html
<div class="card">

159
docs/en/Tutorials/Part-2.md

@ -458,54 +458,54 @@ In this section, you will learn how to create a new modal dialog form to create
#### State definitions
Open `books.action.ts` in `books\state` folder and replace the content as below:
Open `book.action.ts` in `app\book\state` folder and replace the content as below:
```js
import { CreateUpdateBookDto } from '../../app/shared/models'; //<== added this line ==>
import { CreateUpdateBookDto } from '../models'; //<== added this line ==>
export class GetBooks {
static readonly type = '[Books] Get';
static readonly type = '[Book] Get';
}
// added CreateUpdateBook class
export class CreateUpdateBook {
static readonly type = '[Books] Create Update Book';
static readonly type = '[Book] Create Update Book';
constructor(public payload: CreateUpdateBookDto) { }
}
```
* We imported the `CreateUpdateBookDto` model and created the `CreateUpdateBook` action.
Open `books.state.ts` file in `books\state` folder and replace the content as below:
Open `book.state.ts` file in `app\book\state` folder and replace the content as below:
```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook } from './books.actions'; // <== added CreateUpdateBook==>
import { BookService } from '../../app/shared/services';
import { GetBooks, CreateUpdateBook } from './book.actions'; // <== added CreateUpdateBook==>
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../../app/shared/models';
import { BookDto } from '../models';
export class BooksStateModel {
export class BookStateModel {
public book: PagedResultDto<BookDto>;
}
@State<BooksStateModel>({
name: 'BooksState',
defaults: { book: {} } as BooksStateModel,
@State<BookStateModel>({
name: 'BookState',
defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BooksState {
export class BookState {
@Selector()
static getBooks(state: BooksStateModel) {
static getBooks(state: BookStateModel) {
return state.book.items || [];
}
constructor(private bookService: BookService) {}
@Action(GetBooks)
get(ctx: StateContext<BooksStateModel>) {
get(ctx: StateContext<BookStateModel>) {
return this.bookService.getListByInput().pipe(
tap((bookResponse) => {
ctx.patchState({
@ -517,7 +517,7 @@ export class BooksState {
// added CreateUpdateBook action listener
@Action(CreateUpdateBook)
save(ctx: StateContext<BooksStateModel>, action: CreateUpdateBook) {
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
return this.bookService.createByInput(action.payload);
}
}
@ -605,16 +605,16 @@ Open `book-list.component.html` file in `books\book-list` folder and replace the
* `abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits.
* We added `New book` button to the `AbpContentToolbar`.
Open `book-list.component.ts` file in `books\book-list` folder and replace the content as below:
Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below:
```js
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../../app/shared/models';
import { GetBooks } from '../state/books.actions';
import { BooksState } from '../state/books.state';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
@Component({
selector: 'app-book-list',
@ -622,7 +622,7 @@ import { BooksState } from '../state/books.state';
styleUrls: ['./book-list.component.scss'],
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
booksType = BookType;
@ -662,16 +662,16 @@ You can open your browser and click **New book** button to see the new modal.
[Reactive forms](https://angular.io/guide/reactive-forms) provide a model-driven approach to handling form inputs whose values change over time.
Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below:
Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below:
```js
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../../app/shared/models';
import { GetBooks } from '../state/books.actions';
import { BooksState } from '../state/books.state';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==>
@Component({
@ -680,7 +680,7 @@ import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== adde
styleUrls: ['./book-list.component.scss'],
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
booksType = BookType;
@ -774,42 +774,40 @@ Open `book-list.component.html` in `app\books\book-list` folder and replace `<ng
#### Datepicker requirements
Open `books.module.ts` file in `app\books` folder and replace the content as below:
Open `book.module.ts` file in `app\book` folder and replace the content as below:
```js
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BooksRoutingModule } from './books-routing.module';
import { BooksComponent } from './books.component';
import { BookRoutingModule } from './book-routing.module';
import { BookListComponent } from './book-list/book-list.component';
import { SharedModule } from '../shared/shared.module';
import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; //<== added this line ==>
@NgModule({
declarations: [BooksComponent, BookListComponent],
declarations: [BookListComponent],
imports: [
CommonModule,
BooksRoutingModule,
BookRoutingModule,
SharedModule,
NgbDatepickerModule //<== added this line ==>
]
NgbDatepickerModule, //<== added this line ==>
],
})
export class BooksModule { }
export class BookModule {}
```
* We imported `NgbDatepickerModule` to be able to use the date picker.
Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below:
Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below:
```js
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../../app/shared/models';
import { GetBooks } from '../state/books.actions';
import { BooksState } from '../state/books.state';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; // <== added this line ==>
@ -820,7 +818,7 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added this line ==>
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
booksType = BookType;
@ -885,16 +883,16 @@ Now, you can open your browser to see the changes:
#### Saving the book
Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below:
Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below:
```js
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../../app/shared/models';
import { GetBooks, CreateUpdateBook } from '../state/books.actions'; // <== added CreateUpdateBook ==>
import { BooksState } from '../state/books.state';
import { BookDto, BookType } from '../models';
import { GetBooks, CreateUpdateBook } from '../state/book.actions'; // <== added CreateUpdateBook ==>
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
@ -905,12 +903,11 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
booksType = BookType;
//added bookTypeArr array
bookTypeArr = Object.keys(BookType).filter(
(bookType) => typeof this.booksType[bookType] === 'number'
);
@ -949,7 +946,7 @@ export class BookListComponent implements OnInit {
});
}
//<== added save ==>
// <== added save ==>
save() {
if (this.form.invalid) {
return;
@ -967,7 +964,7 @@ export class BookListComponent implements OnInit {
* We imported `CreateUpdateBook`.
* We added `save` method
Open `book-list.component.html` in `app\books\book-list` folder and add the following `abp-button` to save the new book.
Open `book-list.component.html` in `app\book\book-list` folder and add the following `abp-button` to save the new book.
```html
<ng-template #abpFooter>
@ -1001,28 +998,28 @@ The final modal UI looks like below:
#### CreateUpdateBook action
Open the `books.actions.ts` in `books\state` folder and replace the content as below:
Open the `book.actions.ts` in `app\book\state` folder and replace the content as below:
```js
import { CreateUpdateBookDto } from '../../app/shared/models';
import { CreateUpdateBookDto } from '../models';
export class GetBooks {
static readonly type = '[Books] Get';
static readonly type = '[Book] Get';
}
export class CreateUpdateBook {
static readonly type = '[Books] Create Update Book';
constructor(public payload: CreateUpdateBookDto, public id?: string) { } // <== added id parameter ==>
static readonly type = '[Book] Create Update Book';
constructor(public payload: CreateUpdateBookDto, public id?: string) {} // <== added id parameter ==>
}
```
* We added `id` parameter to the `CreateUpdateBook` action's constructor.
Open the `books.state.ts` in `books\state` folder and replace the `save` method as below:
Open the `book.state.ts` in `app\book\state` folder and replace the `save` method as below:
```js
@Action(CreateUpdateBook)
save(ctx: StateContext<BooksStateModel>, action: CreateUpdateBook) {
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
if (action.id) {
return this.bookService.updateByIdAndInput(action.payload, action.id);
} else {
@ -1033,19 +1030,19 @@ save(ctx: StateContext<BooksStateModel>, action: CreateUpdateBook) {
#### BookListComponent
Open `book-list.component.ts` in `app\books\book-list` folder and inject `BookService` dependency by adding it to the constructor and add a variable named `selectedBook`.
Open `book-list.component.ts` in `app\book\book-list` folder and inject `BookService` dependency by adding it to the constructor and add a variable named `selectedBook`.
```js
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../../app/shared/models';
import { GetBooks, CreateUpdateBook } from '../state/books.actions';
import { BooksState } from '../state/books.state';
import { BookDto, BookType } from '../models';
import { GetBooks, CreateUpdateBook } from '../state/book.actions';
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
import { BookService } from '../../app/shared/services'; // <== imported BookService ==>
import { BookService } from '../services'; // <== imported BookService ==>
@Component({
selector: 'app-book-list',
@ -1054,7 +1051,7 @@ import { BookService } from '../../app/shared/services'; // <== imported BookSer
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
booksType = BookType;
@ -1141,7 +1138,7 @@ export class BookListComponent implements OnInit {
#### Add "Actions" dropdown to the table
Open the `book-list.component.html` in `app\books\book-list` folder and replace the `<div class="card-body">` tag as below:
Open the `book-list.component.html` in `app\book\book-list` folder and replace the `<div class="card-body">` tag as below:
```html
<div class="card-body">
@ -1199,7 +1196,7 @@ The final UI looks like as below:
![Action buttons](./images/bookstore-actions-buttons.png)
Open `book-list.component.html` in `app\books\book-list` folder and find the `<ng-template #abpHeader>` tag and replace the content as below.
Open `book-list.component.html` in `app\book\book-list` folder and find the `<ng-template #abpHeader>` tag and replace the content as below.
```html
<ng-template #abpHeader>
@ -1213,45 +1210,45 @@ Open `book-list.component.html` in `app\books\book-list` folder and find the `<n
#### DeleteBook action
Open `books.actions.ts` in `books\state `folder and add an action named `DeleteBook`.
Open `book.actions.ts` in `app\book\state` folder and add an action named `DeleteBook`.
```js
export class DeleteBook {
static readonly type = '[Books] Delete';
static readonly type = '[Book] Delete';
constructor(public id: string) {}
}
```
Open the `books.state.ts` in `books\state` folder and replace the content as below:
Open the `book.state.ts` in `app\book\state` folder and replace the content as below:
```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook, DeleteBook } from './books.actions'; // <== added DeleteBook==>
import { BookService } from '../../app/shared/services';
import { GetBooks, CreateUpdateBook, DeleteBook } from './book.actions'; // <== added DeleteBook==>
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../../app/shared/models';
import { BookDto } from '../models';
export class BooksStateModel {
export class BookStateModel {
public book: PagedResultDto<BookDto>;
}
@State<BooksStateModel>({
name: 'BooksState',
defaults: { book: {} } as BooksStateModel,
@State<BookStateModel>({
name: 'BookState',
defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BooksState {
export class BookState {
@Selector()
static getBooks(state: BooksStateModel) {
static getBooks(state: BookStateModel) {
return state.book.items || [];
}
constructor(private bookService: BookService) {}
@Action(GetBooks)
get(ctx: StateContext<BooksStateModel>) {
get(ctx: StateContext<BookStateModel>) {
return this.bookService.getListByInput().pipe(
tap((booksResponse) => {
ctx.patchState({
@ -1262,7 +1259,7 @@ export class BooksState {
}
@Action(CreateUpdateBook)
save(ctx: StateContext<BooksStateModel>, action: CreateUpdateBook) {
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
if (action.id) {
return this.bookService.updateByIdAndInput(action.payload, action.id);
} else {
@ -1272,7 +1269,7 @@ export class BooksState {
// <== added DeleteBook action listener ==>
@Action(DeleteBook)
delete(ctx: StateContext<BooksStateModel>, action: DeleteBook) {
delete(ctx: StateContext<BookStateModel>, action: DeleteBook) {
return this.bookService.deleteById(action.id);
}
}
@ -1285,7 +1282,7 @@ export class BooksState {
#### Delete confirmation popup
Open `book-list.component.ts` in`app\books\book-list` folder and inject the `ConfirmationService`.
Open `book-list.component.ts` in`app\book\book-list` folder and inject the `ConfirmationService`.
Replace the constructor as below:
@ -1309,7 +1306,7 @@ See the [Confirmation Popup documentation](https://docs.abp.io/en/abp/latest/UI/
In the `book-list.component.ts` add a delete method :
```js
import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/books.actions' ;// <== imported DeleteBook ==>
import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/book.actions' ;// <== imported DeleteBook ==>
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation ==>
@ -1335,7 +1332,7 @@ The `delete` method shows a confirmation popup and subscribes for the user respo
#### Add a delete button
Open `book-list.component.html` in `app\books\book-list` folder and modify the `ngbDropdownMenu` to add the delete button as shown below:
Open `book-list.component.html` in `app\book\book-list` folder and modify the `ngbDropdownMenu` to add the delete button as shown below:
```html
<div ngbDropdownMenu>

BIN
docs/en/Tutorials/images/bookstore-angular-file-tree.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 57 KiB

BIN
docs/en/Tutorials/images/bookstore-book-list.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 49 KiB

BIN
docs/en/Tutorials/images/bookstore-creating-book-list-terminal.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 158 KiB

BIN
docs/en/Tutorials/images/bookstore-creating-book-module-terminal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
docs/en/Tutorials/images/bookstore-creating-books-module-terminal.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

BIN
docs/en/Tutorials/images/generated-proxies.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 23 KiB

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

@ -39,7 +39,7 @@ public class MyClientAppModule : AbpModule
````csharp
[DependsOn(
typeof(AbpHttpClientModule), //用来创建客户端代理
typeof(BookStoreApplicationModule) //包含应用服务接口
typeof(BookStoreApplicationContractsModule) //包含应用服务接口
)]
public class MyClientAppModule : AbpModule
{
@ -47,7 +47,7 @@ public class MyClientAppModule : AbpModule
{
//创建动态客户端代理
context.Services.AddHttpClientProxies(
typeof(BookStoreApplicationModule).Assembly
typeof(BookStoreApplicationContractsModule).Assembly
);
}
}
@ -142,12 +142,12 @@ public override void ConfigureServices(ServiceConfigurationContext context)
````csharp
context.Services.AddHttpClientProxies(
typeof(BookStoreApplicationModule).Assembly,
remoteServiceName: "BookStore"
typeof(BookStoreApplicationContractsModule).Assembly,
remoteServiceConfigurationName: "BookStore"
);
````
`remoteServiceName`参数会匹配通过`AbpRemoteServiceOptions`配置的服务端点.如果`BookStore`端点没有定义就会使用默认的`Default`端点.
`remoteServiceConfigurationName`参数会匹配通过`AbpRemoteServiceOptions`配置的服务端点.如果`BookStore`端点没有定义就会使用默认的`Default`端点.
### 作为默认服务
@ -155,7 +155,7 @@ context.Services.AddHttpClientProxies(
````csharp
context.Services.AddHttpClientProxies(
typeof(BookStoreApplicationModule).Assembly,
typeof(BookStoreApplicationContractsModule).Assembly,
asDefaultServices: false
);
````

2
docs/zh-Hans/Customizing-Application-Modules-Guide.md

@ -17,7 +17,7 @@ ABP框架提供的设计旨在支持构建完全[模块化的应用程序](Modul
这种方法具有以下优点:
* 你的解决方案会非常**干净**,只包含你**自己的应用程序代码**.
* 你可以**很简单的**升级模块到最新的可用模板. `abp update` [CLI](CLI.md) 命令会使更新变的更加简单. 通过这种方式, 你可以获得**最新功能和Bus修复**.
* 你可以**很简单的**升级模块到最新的可用模板. `abp update` [CLI](CLI.md) 命令会使更新变的更加简单. 通过这种方式, 你可以获得**最新功能和Bug修复**.
然而有一个缺点:

2
docs/zh-Hans/Entity-Framework-Core-Migrations.md

@ -550,6 +550,8 @@ public class IdentityRoleExtendingService : ITransientDependency
你需要做的就是如上所诉使用 `ObjectExtensionManager` 定义额外属性, 然后你就可以使得 `GetProperty``SetProperty` 方法对实体的属性进行get/set,但是这时它存储在数据库表的单独字段中.
参阅[实体扩展系统](Customizing-Application-Modules-Extending-Entities.md)了解更多.
###### 创建新表
你可以创建**自己的表**来存储属性,而不是创建新实体并映射到同一表. 你通常复制原始实体的一些值. 例如可以将 `Name` 字段添加到你自己的表中,它是原表中 `Name` 字段的副本.

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

@ -111,6 +111,7 @@ abp new Acme.BookStore -t app{{if UI == "NG"}} -u angular {{end}}{{if DB == "Mon
{{end}}
{{ else if UI == "NG" }}
在创建的解决方案中有三个文件夹:
![](images/solution-files-non-mvc.png)
@ -120,6 +121,7 @@ abp new Acme.BookStore -t app{{if UI == "NG"}} -u angular {{end}}{{if DB == "Mon
* `react-native` 文件夹包含React Native UI 应用程序.
打开 `aspnet-core` 文件夹下的 `.sln`(`Visual Studio`解决方案)文件:
![vs-angular-app-backend-solution-structure](images/vs-spa-app-backend-structure{{if DB == "Mongo"}}-mongodb{{end}}.png)
{{ end }}

2
docs/zh-Hans/Samples/Microservice-Demo.md

@ -553,7 +553,7 @@ app.MapWhen(ctx => ctx.Request.Path.ToString().StartsWith("/api/abp/") ||
ctx.Request.Path.ToString().StartsWith("/Abp/"),
app2 =>
{
app2.UseMvcWithDefaultRouteAndArea();
app2.UseConfiguredEndpoints();
});
app.UseOcelot().Wait();

9
docs/zh-Hans/Tutorials/Angular/Part-I.md

@ -1,3 +1,8 @@
## Angular 教程 - 第一章
# 教程
TODO...
## 应用程序开发
* [ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC)
* [Angular UI](../Part-1?UI=NG)
<!-- TODO: this document has been moved, it should be deleted in the future. -->

480
docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md

@ -1,478 +1,8 @@
## ASP.NET Core MVC 介绍 - 第一章
# 教程
### 关于本教程
## 应用程序开发
在本系列教程中, 你将构建一个用于管理书籍及其作者列表的应用程序. **Entity Framework Core**(EF Core)将用作ORM提供者,因为它是默认数据库提供者.
* [ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC)
* [Angular UI](../Part-1?UI=NG)
这是本教程所有章节中的第一章,下面是所有的章节:
- **Part I: 创建项目和书籍列表页面(本章)**
- [Part II: 创建,编辑,删除书籍](Part-II.md)
- [Part III: 集成测试](Part-III.md)
你可以从[GitHub存储库](https://github.com/abpframework/abp-samples/tree/master/BookStore)访问应用程序的**源代码**.
> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application).
### 创建项目
创建一个名为`Acme.BookStore`的新项目, 创建数据库并按照[入门文档](../../../Getting-Started-AspNetCore-MVC-Template.md)运行应用程序.
### 解决方案的结构
下面的图片展示了从启动模板创建的项目是如何分层的.
![bookstore-visual-studio-solution](images/bookstore-visual-studio-solution-v3.png)
> 你可以查看[应用程序模板文档](../startup-templates/application#solution-structure)以详细了解解决方案结构.但是,你将通过本教程了解基础知识.
### 创建Book实体
启动模板中的域层分为两个项目:
- `Acme.BookStore.Domain`包含你的[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities), [领域服务](https://docs.abp.io/zh-Hans/abp/latest/Domain-Services)和其他核心域对象.
- `Acme.BookStore.Domain.Shared`包含可与客户共享的常量,枚举或其他域相关对象.
在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities). 该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个名为`Book`的类,如下所示:
````C#
using System;
using Volo.Abp.Domain.Entities.Auditing;
namespace Acme.BookStore
{
public class Book : AuditedAggregateRoot<Guid>
{
public string Name { get; set; }
public BookType Type { get; set; }
public DateTime PublishDate { get; set; }
public float Price { get; set; }
protected Book()
{
}
public Book(Guid id, string name, BookType type, DateTime publishDate, float price)
:base(id)
{
Name = name;
Type = type;
PublishDate = publishDate;
Price = price;
}
}
}
````
* ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是**域驱动设计(DDD)** 概念之一. 有关详细信息和最佳做法,请参阅[实体文档](https://docs.abp.io/zh-Hans/abp/latest/Entities).
* `Book`实体继承了`AuditedAggregateRoot`,`AuditedAggregateRoot`类在`AggregateRoot`类的基础上添加了一些审计属性(`CreationTime`, `CreatorId`, `LastModificationTime` 等).
* `Guid`是`Book`实体的主键类型.
* 使用 **数据注解** 为EF Core添加映射.或者你也可以使用 EF Core 自带的[fluent mapping API](https://docs.microsoft.com/en-us/ef/core/modeling).
#### BookType枚举
上面所用到的`BookType`枚举定义如下:
````C#
namespace Acme.BookStore
{
public enum BookType
{
Undefined,
Adventure,
Biography,
Dystopia,
Fantastic,
Horror,
Science,
ScienceFiction,
Poetry
}
}
````
#### 将Book实体添加到DbContext中
EF Core需要你将实体和DbContext建立关联.最简单的做法是在`Acme.BookStore.EntityFrameworkCore`项目的`BookStoreDbContext`类中添加`DbSet`属性.如下所示:
````C#
public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
{
public DbSet<Book> Books { get; set; }
...
}
````
#### 配置你的Book实体
在`Acme.BookStore.EntityFrameworkCore`项目中打开`BookStoreDbContextModelCreatingExtensions.cs`文件,并将以下代码添加到`ConfigureBookStore`方法的末尾以配置Book实体:
````C#
builder.Entity<Book>(b =>
{
b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema);
b.ConfigureByConvention(); //auto configure for the base class props
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
});
````
#### 添加新的Migration并更新数据库
这个启动模板使用了[EF Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/)来创建并维护数据库结构.打开 **程序包管理器控制台(Package Manager Console) (PMC)** (工具/Nuget包管理器菜单),选择 `Acme.BookStore.EntityFrameworkCore.DbMigrations`作为默认的项目然后执行下面的命令:
![bookstore-pmc-add-book-migration](images/bookstore-pmc-add-book-migration-v2.png)
这样就会在`Migrations`文件夹中创建一个新的migration类.然后执行`Update-Database`命令更新数据库结构.
````
PM> Update-Database
````
#### 添加示例数据
`Update-Database`命令在数据库中创建了`AppBooks`表. 打开数据库并输入几个示例行,以便在页面上显示它们:
![bookstore-books-table](images/bookstore-books-table.png)
### 创建应用服务
下一步是创建[应用服务](../../../Application-Services.md)来管理(创建,列出,更新,删除)书籍. 启动模板中的应用程序层分为两个项目:
* `Acme.BookStore.Application.Contracts`主要包含你的DTO和应用程序服务接口.
* `Acme.BookStore.Application`包含应用程序服务的实现.
#### BookDto
在`Acme.BookStore.Application.Contracts`项目中创建一个名为`BookDto`的DTO类:
````C#
using System;
using Volo.Abp.Application.Dtos;
namespace Acme.BookStore
{
public class BookDto : AuditedEntityDto<Guid>
{
public string Name { get; set; }
public BookType Type { get; set; }
public DateTime PublishDate { get; set; }
public float Price { get; set; }
}
}
````
* **DTO**类被用来在 **表示层****应用层** **传递数据**.查看[DTO文档](https://docs.abp.io/zh-Hans/abp/latest/Data-Transfer-Objects)查看更多信息.
* 为了在页面上展示书籍信息,`BookDto`被用来将书籍数据传递到表示层.
* `BookDto`继承自 `AuditedEntityDto<Guid>`.跟上面定义的`Book`类一样具有一些审计属性.
在将书籍返回到表示层时,需要将`Book`实体转换为`BookDto`对象. [AutoMapper](https://automapper.org)库可以在定义了正确的映射时自动执行此转换. 启动模板配置了AutoMapper,因此你只需在`Acme.BookStore.Application`项目的`BookStoreApplicationAutoMapperProfile`类中定义映射:
````csharp
using AutoMapper;
namespace Acme.BookStore
{
public class BookStoreApplicationAutoMapperProfile : Profile
{
public BookStoreApplicationAutoMapperProfile()
{
CreateMap<Book, BookDto>();
}
}
}
````
#### CreateUpdateBookDto
在`Acme.BookStore.Application.Contracts`项目中创建一个名为`CreateUpdateBookDto`的DTO类:
````c#
using System;
using System.ComponentModel.DataAnnotations;
using Volo.Abp.AutoMapper;
namespace Acme.BookStore
{
public class CreateUpdateBookDto
{
[Required]
[StringLength(128)]
public string Name { get; set; }
[Required]
public BookType Type { get; set; } = BookType.Undefined;
[Required]
public DateTime PublishDate { get; set; }
[Required]
public float Price { get; set; }
}
}
````
* 这个DTO类被用于在创建或更新书籍的时候从用户界面获取图书信息.
* 它定义了数据注释属性(如`[Required]`)来定义属性的验证. DTO由ABP框架[自动验证](https://docs.abp.io/zh-Hans/abp/latest/Validation).
就像上面的`BookDto`一样,创建一个从`CreateUpdateBookDto`对象到`Book`实体的映射:
````csharp
CreateMap<CreateUpdateBookDto, Book>();
````
#### IBookAppService
在`Acme.BookStore.Application.Contracts`项目中定义一个名为`IBookAppService`的接口:
````C#
using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Acme.BookStore
{
public interface IBookAppService :
ICrudAppService< //定义了CRUD方法
BookDto, //用来展示书籍
Guid, //Book实体的主键
PagedAndSortedResultRequestDto, //获取书籍的时候用于分页和排序
CreateUpdateBookDto, //用于创建书籍
CreateUpdateBookDto> //用于更新书籍
{
}
}
````
* 框架定义应用程序服务的接口<u>不是必需的</u>. 但是,它被建议作为最佳实践.
* `ICrudAppService`定义了常见的**CRUD**方法:`GetAsync`,`GetListAsync`,`CreateAsync`,`UpdateAsync`和`DeleteAsync`. 你可以从空的`IApplicationService`接口继承并手动定义自己的方法.
* `ICrudAppService`有一些变体, 你可以在每个方法中使用单独的DTO,也可以分别单独指定.
#### BookAppService
在`Acme.BookStore.Application`项目中实现名为`BookAppService`的`IBookAppService`:
````C#
using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore
{
public class BookAppService :
CrudAppService<Book, BookDto, Guid, PagedAndSortedResultRequestDto,
CreateUpdateBookDto, CreateUpdateBookDto>,
IBookAppService
{
public BookAppService(IRepository<Book, Guid> repository)
: base(repository)
{
}
}
}
````
* `BookAppService`继承了`CrudAppService<...>`.它实现了上面定义的CRUD方法.
* `BookAppService`注入`IRepository <Book,Guid>`,这是`Book`实体的默认仓储. ABP自动为每个聚合根(或实体)创建默认仓储. 请参阅[仓储文档](https://docs.abp.io/zh-Hans/abp/latest/Repositories)
* `BookAppService`使用`IObjectMapper`将`Book`对象转换为`BookDto`对象, 将`CreateUpdateBookDto`对象转换为`Book`对象. 启动模板使用[AutoMapper](http://automapper.org/)库作为对象映射提供程序. 你之前定义了映射, 因此它将按预期工作.
### 自动生成API Controllers
你通常创建**Controller**以将应用程序服务公开为**HTTP API**端点. 因此允许浏览器或第三方客户端通过AJAX调用它们. ABP可以[**自动**](https://docs.abp.io/zh-Hans/abp/latest/API/Auto-API-Controllers)按照惯例将你的应用程序服务配置为MVC API控制器.
#### Swagger UI
启动模板配置为使用[Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)运行[swagger UI](https://swagger.io/tools/swagger-ui/). 运行应用程序并在浏览器中输入`https://localhost:XXXX/swagger/`(用你自己的端口替换XXXX)作为URL.
你会看到一些内置的接口和`Book`的接口,它们都是REST风格的:
![bookstore-swagger](images/bookstore-swagger.png)
Swagger有一个很好的UI来测试API. 你可以尝试执行`[GET] /api/app/book` API来获取书籍列表.
### 动态JavaScript代理
在Javascript端通过AJAX的方式调用HTTP API接口是很常见的,你可以使用`$.ajax`或者其他的工具来调用接口.当然,ABP中提供了更好的方式.
ABP **自动** 为所有的API接口创建了JavaScript **代理**.因此,你可以像调用 **JavaScript function**一样调用任何接口.
#### 在浏览器的开发者控制台中测试接口
你可以使用你钟爱的浏览器的 **开发者控制台** 中轻松测试JavaScript代理.运行程序,并打开浏览器的 **开发者工具**(快捷键:F12),切换到 **Console** 标签,输入下面的代码并回车:
````js
acme.bookStore.book.getList({}).done(function (result) { console.log(result); });
````
* `acme.bookStore`是`BookAppService`的命名空间,转换成了[驼峰命名](https://en.wikipedia.org/wiki/Camel_case).
* `book`是`BookAppService`转换后的名字(去除了AppService后缀并转成了驼峰命名).
* `getList`是定义在`AsyncCrudAppService`基类中的`GetListAsync`方法转换后的名字(去除了Async后缀并转成了驼峰命名).
* `{}`参数用于将空对象发送到`GetListAsync`方法,该方法通常需要一个类型为`PagedAndSortedResultRequestDto`的对象,用于向服务器发送分页和排序选项(所有属性都是可选的,所以你可以发送一个空对象).
* `getList`方法返回了一个`promise`.因此,你可以传递一个回调函数到`done`(或者`then`)方法中来获取服务返回的结果.
运行这段代码会产生下面的输出:
![bookstore-test-js-proxy-getlist](images/bookstore-test-js-proxy-getlist.png)
你可以看到服务器返回的 **book list**.你还可以切换到开发者工具的 **network** 查看客户端到服务器端的通讯信息:
![bookstore-test-js-proxy-getlist-network](images/bookstore-test-js-proxy-getlist-network.png)
我们使用`create`方法 **创建一本新书**:
````js
acme.bookStore.book.create({ name: 'Foundation', type: 7, publishDate: '1951-05-24', price: 21.5 }).done(function (result) { console.log('successfully created the book with id: ' + result.id); });
````
你会看到控制台会显示类似这样的输出:
````
successfully created the book with id: f3f03580-c1aa-d6a9-072d-39e75c69f5c7
````
检查数据库中的`Books`表以查看新书. 你可以自己尝试`get`,`update`和`delete`功能.
### 创建书籍页面
现在我们来创建一些可见和可用的东西,取代经典的MVC,我们使用微软推荐的[Razor Pages UI](https://docs.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/razor-pages-start).
`Acme.BookStore.Web`项目的`Pages`文件夹下创建一个新的文件夹叫`Books`并添加一个名为`Index.cshtml`的Razor Page.
![bookstore-add-index-page](images/bookstore-add-index-page-v2.png)
打开`Index.cshtml`并把内容修改成下面这样:
````html
@page
@using Acme.BookStore.Web.Pages.Books
@inherits Acme.BookStore.Web.Pages.BookStorePage
@model IndexModel
<h2>Books</h2>
````
* 此代码更改了Razor View Page Model的默认继承,因此它从`BookStorePage`类(而不是`PageModel`)继承.启动模板附带的`BookStorePage`类,提供所有页面使用的一些共享属性/方法.
* 确保`IndexModel`(Index.cshtml.cs)具有`Acme.BookStore.Web.Pages.Books`命名空间,或者在`Index.cshtml`中更新它.
#### 将Books页面添加到主菜单
打开`Menus`文件夹中的 `BookStoreMenuContributor` 类,在`ConfigureMainMenuAsync`方法的底部添加如下代码:
````c#
context.Menu.AddItem(
new ApplicationMenuItem("BooksStore", l["Menu:BookStore"])
.AddItem(new ApplicationMenuItem("BooksStore.Books", l["Menu:Books"], url: "/Books"))
);
````
#### 本地化菜单
本地化文本位于`Acme.BookStore.Domain.Shared`项目的`Localization/BookStore`文件夹下:
![bookstore-localization-files](images/bookstore-localization-files-v2.png)
打开`en.json`文件,将`Menu:BookStore`和`Menu:Books`键的本地化文本添加到文件末尾:
````json
{
"culture": "en",
"texts": {
"Menu:BookStore": "Book Store",
"Menu:Books": "Books"
}
}
````
* ABP的本地化功能建立在[ASP.NET Core's standard localization]((https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization))之上并增加了一些扩展.查看[本地化文档](https://docs.abp.io/zh-Hans/abp/latest/Localization).
* 本地化key是任意的. 你可以设置任何名称. 我们更喜欢为菜单项添加`Menu:`前缀以区别于其他文本. 如果未在本地化文件中定义文本,则它将**返回**到本地化的key(ASP.NET Core的标准行为).
运行该应用程序,看到新菜单项已添加到顶部栏:
![bookstore-menu-items](images/bookstore-menu-items.png)
点击Books菜单项就会跳转到新增的书籍页面.
#### 书籍列表
我们将使用[Datatables.net](https://datatables.net/)JQuery插件来显示页面上的表格列表. 数据表可以完全通过AJAX工作,速度快,并提供良好的用户体验. Datatables插件在启动模板中配置,因此你可以直接在任何页面中使用它,而需要在页面中引用样式和脚本文件.
##### Index.cshtml
将`Pages/Books/Index.cshtml`改成下面的样子:
````html
@page
@inherits Acme.BookStore.Web.Pages.BookStorePage
@model Acme.BookStore.Web.Pages.Books.IndexModel
@section scripts
{
<abp-script src="/Pages/Books/index.js" />
}
<abp-card>
<abp-card-header>
<h2>@L["Books"]</h2>
</abp-card-header>
<abp-card-body>
<abp-table striped-rows="true" id="BooksTable">
<thead>
<tr>
<th>@L["Name"]</th>
<th>@L["Type"]</th>
<th>@L["PublishDate"]</th>
<th>@L["Price"]</th>
<th>@L["CreationTime"]</th>
</tr>
</thead>
</abp-table>
</abp-card-body>
</abp-card>
````
* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro)用于将外部的 **脚本** 添加到页面中.它比标准的`script`标签多了很多额外的功能.它可以处理 **最小化**和 **版本**.查看[捆绑 & 压缩文档](https://docs.abp.io/zh-Hans/abp/latest/UI/AspNetCore/Bundling-Minification)获取更多信息.
* `abp-card``abp-table` 是为Twitter Bootstrap的[card component](http://getbootstrap.com/docs/4.1/components/card/)封装的 **tag helpers**.ABP中有很多tag helpers,可以很方便的使用大多数[bootstrap](https://getbootstrap.com/)组件.你也可以使用原生的HTML标签代替tag helpers.使用tag helper可以通过智能提示和编译时类型检查减少HTML代码并防止错误.查看[tag helpers 文档](https://docs.abp.io/zh-Hans/abp/latest/UI/AspNetCore/Tag-Helpers/Index).
* 你可以像上面本地化菜单一样 **本地化** 列名.
#### 添加脚本文件
在`Pages/Books/`文件夹中创建 `index.js`文件
![bookstore-index-js-file](images/bookstore-index-js-file-v2.png)
`index.js`的内容如下:
````js
$(function () {
var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({
ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList),
columnDefs: [
{ data: "name" },
{ data: "type" },
{ data: "publishDate" },
{ data: "price" },
{ data: "creationTime" }
]
}));
});
````
* `abp.libs.datatables.createAjax`是帮助ABP的动态JavaScript API代理跟Datatable的格式相适应的辅助方法.
* `abp.libs.datatables.normalizeConfiguration`是另一个辅助方法.不是必须的, 但是它通过为缺少的选项提供常规值来简化数据表配置.
* `acme.bookStore.book.getList`是获取书籍列表的方法(上面已经介绍过了)
* 查看 [Datatable文档](https://datatables.net/manual/) 了解更多配置项.
最终的页面如下:
![bookstore-book-list](images/bookstore-book-list-2.png)
### 下一章
点击查看 [下一章](Part-II.md) 的介绍.
<!-- TODO: this document has been moved, it should be deleted in the future. -->

167
docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md

@ -1,165 +1,8 @@
## ASP.NET Core MVC 教程 - 第三章
# 教程
### 关于本教程
## 应用程序开发
这是ASP.NET Core MVC教程系列的第三章. 查看其它章节
* [ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC)
* [Angular UI](../Part-1?UI=NG)
- [Part I: 创建项目和书籍列表页面](Part-I.md)
- [Part II: 创建,编辑,删除书籍](Part-II.md)
- **Part III: 集成测试(本章)**
你可以从[GitHub存储库](https://github.com/volosoft/abp/tree/master/samples/BookStore)访问应用程序的**源代码**.
> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application).
### 解决方案中的测试项目
解决方案中有多个测试项目:
![bookstore-test-projects-v2](images/bookstore-test-projects-v2.png)
每个项目用于测试相关的应用程序项目.测试项目使用以下库进行测试:
* [xunit](https://xunit.github.io/) 作为主测试框架.
* [Shoudly](http://shouldly.readthedocs.io/en/latest/) 作为断言库.
* [NSubstitute](http://nsubstitute.github.io/) 作为模拟库.
### 添加测试用数据
启动模板包含`Acme.BookStore.TestBase`项目中的`BookStoreTestDataSeedContributor`类,它创建一些数据来运行测试.
更改`BookStoreTestDataSeedContributor`类如下所示:
````C#
using System;
using System.Threading.Tasks;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Guids;
namespace Acme.BookStore
{
public class BookStoreTestDataSeedContributor
: IDataSeedContributor, ITransientDependency
{
private readonly IRepository<Book, Guid> _bookRepository;
private readonly IGuidGenerator _guidGenerator;
public BookStoreTestDataSeedContributor(
IRepository<Book, Guid> bookRepository,
IGuidGenerator guidGenerator)
{
_bookRepository = bookRepository;
_guidGenerator = guidGenerator;
}
public async Task SeedAsync(DataSeedContext context)
{
await _bookRepository.InsertAsync(
new Book(_guidGenerator.Create(), "Test book 1",
BookType.Fantastic, new DateTime(2015, 05, 24), 21));
await _bookRepository.InsertAsync(
new Book(_guidGenerator.Create(), "Test book 2",
BookType.Science, new DateTime(2014, 02, 11), 15));
}
}
}
````
* 注入`IRepository<Book,Guid>`并在`SeedAsync`中使用它来创建两个书实体作为测试数据.
* 使用`IGuidGenerator`服务创建GUID. 虽然`Guid.NewGuid()`非常适合测试,但`IGuidGenerator`在使用真实数据库时还有其他特别重要的功能(参见[Guid生成文档](../../../Guid-Generation.md)了解更多信息).
### 测试 BookAppService
`Acme.BookStore.Application.Tests` 项目中创建一个名叫 `BookAppService_Tests` 的测试类:
````C#
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Application.Dtos;
using Xunit;
namespace Acme.BookStore
{
public class BookAppService_Tests : BookStoreApplicationTestBase
{
private readonly IBookAppService _bookAppService;
public BookAppService_Tests()
{
_bookAppService = GetRequiredService<IBookAppService>();
}
[Fact]
public async Task Should_Get_List_Of_Books()
{
//Act
var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
//Assert
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(b => b.Name == "Test book 1");
}
}
}
````
* 测试方法 `Should_Get_List_Of_Books` 直接使用 `BookAppService.GetListAsync` 方法来获取用户列表,并执行检查.
新增测试方法,用以测试创建一个合法book实体的场景:
````C#
[Fact]
public async Task Should_Create_A_Valid_Book()
{
//Act
var result = await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
Name = "New test book 42",
Price = 10,
PublishDate = DateTime.Now,
Type = BookType.ScienceFiction
}
);
//Assert
result.Id.ShouldNotBe(Guid.Empty);
result.Name.ShouldBe("New test book 42");
}
````
新增测试方法,用以测试创建一个非法book实体失败的场景:
````C#
[Fact]
public async Task Should_Not_Create_A_Book_Without_Name()
{
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () =>
{
await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
Name = "",
Price = 10,
PublishDate = DateTime.Now,
Type = BookType.ScienceFiction
}
);
});
exception.ValidationErrors
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name"));
}
````
* 由于 `Name` 是空值, ABP 抛出一个 `AbpValidationException` 异常.
打开**测试资源管理器**(测试 -> Windows -> 测试资源管理器)并**执行**所有测试:
![bookstore-appservice-tests](images/bookstore-appservice-tests.png)
恭喜, 绿色图标表示测试已成功通过!
<!-- TODO: this document has been moved, it should be deleted in the future. -->

241
docs/zh-Hans/Tutorials/Part-1.md

@ -30,7 +30,7 @@ ASP.NET Core {{UI_Value}} 系列教程包括三个3个部分:
- [Part-2: 创建,编辑,删除书籍](Part-2.md)
- [Part-3: 集成测试](Part-3.md)
> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application).
> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-booktore-application).
### 创建新项目
@ -44,19 +44,19 @@ ASP.NET Core {{UI_Value}} 系列教程包括三个3个部分:
abp new Acme.BookStore --template app --database-provider {{DB}} --ui {{UI_Text}} --mobile none
```
![Creating project](./images/bookstore-create-project-{{UI_Text}}.png)
![Creating project](./images/booktore-create-project-{{UI_Text}}.png)
### 应用迁移
项目创建后,需要应用初始化迁移创建数据库. 运行 `Acme.BookStore.DbMigrator` 应用程序. 它会应用所有迁移,完成流程后你会看到以下结果,数据库已经准备好了!
![Migrations applied](./images/bookstore-migrations-applied-{{UI_Text}}.png)
![Migrations applied](./images/booktore-migrations-applied-{{UI_Text}}.png)
> 另外你也可以在 Visual Studio 包管理控制台运行 `Update-Database` 命令应用迁移.
#### 初始化数据库表
![Initial database tables](./images/bookstore-database-tables-{{DB}}.png)
![Initial database tables](./images/booktore-database-tables-{{DB}}.png)
### 运行应用程序
@ -64,7 +64,7 @@ abp new Acme.BookStore --template app --database-provider {{DB}} --ui {{UI_Text}
更多信息,参阅[入门教程](../../Getting-Started?UI={{UI}})的运行应用程序部分.
![Set as startup project](./images/bookstore-start-project-{{UI_Text}}.png)
![Set as startup project](./images/booktore-start-project-{{UI_Text}}.png)
{{if UI == "NG"}}
@ -105,7 +105,7 @@ http://localhost:4200/
下面的图片展示了从启动模板创建的项目是如何分层的.
![bookstore-visual-studio-solution](./images/bookstore-solution-structure-{{UI_Text}}.png)
![booktore-visual-studio-solution](./images/booktore-solution-structure-{{UI_Text}}.png)
> 你可以查看[应用程序模板文档](../startup-templates/application#solution-structure)以详细了解解决方案结构.
@ -185,7 +185,7 @@ EF Core需要你将实体和 `DbContext` 建立关联.最简单的做法是在`A
````C#
public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
{
public DbSet<Book> Books { get; set; }
public DbSet<Book> Book { get; set; }
...
}
````
@ -194,13 +194,13 @@ public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
{{if DB == "mongodb"}}
添加 `IMongoCollection<Book> Books` 属性到 `Acme.BookStore.MongoDB` 项目的 `BookStoreMongoDbContext` 中.
添加 `IMongoCollection<Book> Book` 属性到 `Acme.BookStore.MongoDB` 项目的 `BookStoreMongoDbContext` 中.
```csharp
public class BookStoreMongoDbContext : AbpMongoDbContext
{
public IMongoCollection<AppUser> Users => Collection<AppUser>();
public IMongoCollection<Book> Books => Collection<Book>();//<--added this line-->
public IMongoCollection<Book> Book => Collection<Book>();//<--added this line-->
//...
}
```
@ -216,7 +216,7 @@ public class BookStoreMongoDbContext : AbpMongoDbContext
````csharp
builder.Entity<Book>(b =>
{
b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema);
b.ToTable(BookStoreConsts.DbTablePrefix + "Book", BookStoreConsts.DbSchema);
b.ConfigureByConvention(); //auto configure for the base class props
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
});
@ -295,7 +295,7 @@ namespace Acme.BookStore
这个启动模板使用了[EF Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/)来创建并维护数据库结构.打开 **程序包管理器控制台(Package Manager Console) (PMC)** (工具/Nuget包管理器菜单)
![Open Package Manager Console](./images/bookstore-open-package-manager-console.png)
![Open Package Manager Console](./images/booktore-open-package-manager-console.png)
选择 `Acme.BookStore.EntityFrameworkCore.DbMigrations`作为默认的项目然后执行下面的命令:
@ -303,7 +303,7 @@ namespace Acme.BookStore
Add-Migration "Created_Book_Entity"
```
![bookstore-pmc-add-book-migration](./images/bookstore-pmc-add-book-migration-v2.png)
![booktore-pmc-add-book-migration](./images/booktore-pmc-add-book-migration-v2.png)
这样就会在 `Migrations` 文件夹中创建一个新的migration类.然后执行 `Update-Database` 命令更新数据库结构:
@ -311,24 +311,24 @@ Add-Migration "Created_Book_Entity"
Update-Database
````
![bookstore-update-database-after-book-entity](./images/bookstore-update-database-after-book-entity.png)
![booktore-update-database-after-book-entity](./images/booktore-update-database-after-book-entity.png)
#### 添加示例数据
`Update-Database`命令在数据库中创建了`AppBooks`表. 打开数据库并输入几个示例行,以便在页面上显示它们:
`Update-Database`命令在数据库中创建了`AppBook`表. 打开数据库并输入几个示例行,以便在页面上显示它们:
```mssql
INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES
INSERT INTO AppBook (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES
('f3c04764-6bfd-49e2-859e-3f9bfda6183e', '2018-07-01', '1984',3,'1949-06-08','19.84')
INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES
INSERT INTO AppBook (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES
('13024066-35c9-473c-997b-83cd8d3e29dc', '2018-07-01', 'The Hitchhiker`s Guide to the Galaxy',7,'1995-09-27','42')
INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES
INSERT INTO AppBook (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES
('4fa024a1-95ac-49c6-a709-6af9e4d54b54', '2018-07-02', 'Pet Sematary',5,'1983-11-14','23.7')
```
![bookstore-books-table](./images/bookstore-books-table.png)
![booktore-book-table](./images/booktore-book-table.png)
{{end}}
@ -501,7 +501,7 @@ namespace Acme.BookStore
你会看到一些内置的接口和`Book`的接口,它们都是REST风格的:
![bookstore-swagger](images/bookstore-swagger.png)
![booktore-swagger](images/booktore-swagger.png)
Swagger有一个很好的UI来测试API. 你可以尝试执行`[GET] /api/app/book` API来获取书籍列表.
@ -529,11 +529,11 @@ acme.bookStore.book.getList({}).done(function (result) { console.log(result); })
运行这段代码会产生下面的输出:
![bookstore-test-js-proxy-getlist](./images/bookstore-test-js-proxy-getlist.png)
![booktore-test-js-proxy-getlist](./images/booktore-test-js-proxy-getlist.png)
你可以看到服务器返回的 **book list**.你还可以切换到开发者工具的 **network** 查看客户端到服务器端的通讯信息:
![bookstore-test-js-proxy-getlist-network](./images/bookstore-test-js-proxy-getlist-network.png)
![booktore-test-js-proxy-getlist-network](./images/booktore-test-js-proxy-getlist-network.png)
我们使用`create`方法 **创建一本新书**:
@ -547,15 +547,15 @@ acme.bookStore.book.create({ name: 'Foundation', type: 7, publishDate: '1951-05-
successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246
````
检查数据库中的`Books`表以查看新书. 你可以自己尝试`get`,`update`和`delete`功能.
检查数据库中的`Book`表以查看新书. 你可以自己尝试`get`,`update`和`delete`功能.
### 创建书籍页面
现在我们来创建一些可见和可用的东西,取代经典的MVC,我们使用微软推荐的[Razor Pages UI](https://docs.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/razor-pages-start).
`Acme.BookStore.Web`项目的`Pages`文件夹下创建一个新的文件夹叫`Books`并添加一个名为`Index.cshtml`的Razor Page.
`Acme.BookStore.Web`项目的`Pages`文件夹下创建一个新的文件夹叫`Book`并添加一个名为`Index.cshtml`的Razor Page.
![bookstore-add-index-page](./images/bookstore-add-index-page-v2.png)
![booktore-add-index-page](./images/booktore-add-index-page-v2.png)
打开`Index.cshtml`并把内容修改成下面这样:
@ -563,22 +563,22 @@ successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246
````html
@page
@using Acme.BookStore.Web.Pages.Books
@using Acme.BookStore.Web.Pages.Book
@inherits Acme.BookStore.Web.Pages.BookStorePage
@model IndexModel
<h2>Books</h2>
<h2>Book</h2>
````
* 此代码更改了Razor View Page Model的默认继承,因此它从`BookStorePage`类(而不是`PageModel`)继承.启动模板附带的`BookStorePage`类,提供所有页面使用的一些共享属性/方法.
* 确保`IndexModel`(Index.cshtml.cs)具有`Acme.BookStore.Web.Pages.Books`命名空间,或者在`Index.cshtml`中更新它.
* 确保`IndexModel`(Index.cshtml.cs)具有`Acme.BookStore.Web.Pages.Book`命名空间,或者在`Index.cshtml`中更新它.
**Index.cshtml.cs:**
```csharp
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Acme.BookStore.Web.Pages.Books
namespace Acme.BookStore.Web.Pages.Book
{
public class IndexModel : PageModel
{
@ -590,7 +590,7 @@ namespace Acme.BookStore.Web.Pages.Books
}
```
#### 将Books页面添加到主菜单
#### 将Book页面添加到主菜单
打开`Menus`文件夹中的 `BookStoreMenuContributor` 类,在`ConfigureMainMenuAsync`方法的底部添加如下代码:
@ -604,9 +604,9 @@ namespace Acme.BookStore.Web.Menus
{
//<-- added the below code
context.Menu.AddItem(
new ApplicationMenuItem("BooksStore", l["Menu:BookStore"])
new ApplicationMenuItem("BookStore", l["Menu:BookStore"])
.AddItem(
new ApplicationMenuItem("BooksStore.Books", l["Menu:Books"], url: "/Books")
new ApplicationMenuItem("BookStore.Book", l["Menu:Book"], url: "/Book")
)
);
//-->
@ -621,9 +621,9 @@ namespace Acme.BookStore.Web.Menus
本地化文本位于`Acme.BookStore.Domain.Shared`项目的`Localization/BookStore`文件夹下:
![bookstore-localization-files](./images/bookstore-localization-files-v2.png)
![booktore-localization-files](./images/booktore-localization-files-v2.png)
打开`en.json`文件,将`Menu:BookStore`和`Menu:Books`键的本地化文本添加到文件末尾:
打开`en.json`文件,将`Menu:BookStore`和`Menu:Book`键的本地化文本添加到文件末尾:
````json
{
@ -634,7 +634,7 @@ namespace Acme.BookStore.Web.Menus
"LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information, visit abp.io.",
"Menu:BookStore": "Book Store",
"Menu:Books": "Books",
"Menu:Book": "Book",
"Actions": "Actions",
"Edit": "Edit",
"PublishDate": "Publish date",
@ -653,9 +653,9 @@ namespace Acme.BookStore.Web.Menus
运行该应用程序,看到新菜单项已添加到顶部栏:
![bookstore-menu-items](./images/bookstore-new-menu-item.png)
![booktore-menu-items](./images/booktore-new-menu-item.png)
点击BookStore下Books子菜单项就会跳转到新增的书籍页面.
点击BookStore下Book子菜单项就会跳转到新增的书籍页面.
#### 书籍列表
@ -663,22 +663,22 @@ namespace Acme.BookStore.Web.Menus
##### Index.cshtml
将`Pages/Books/Index.cshtml`改成下面的样子:
将`Pages/Book/Index.cshtml`改成下面的样子:
````html
@page
@inherits Acme.BookStore.Web.Pages.BookStorePage
@model Acme.BookStore.Web.Pages.Books.IndexModel
@model Acme.BookStore.Web.Pages.Book.IndexModel
@section scripts
{
<abp-script src="/Pages/Books/index.js" />
<abp-script src="/Pages/Book/index.js" />
}
<abp-card>
<abp-card-header>
<h2>@L["Books"]</h2>
<h2>@L["Book"]</h2>
</abp-card-header>
<abp-card-body>
<abp-table striped-rows="true" id="BooksTable">
<abp-table striped-rows="true" id="BookTable">
<thead>
<tr>
<th>@L["Name"]</th>
@ -699,15 +699,15 @@ namespace Acme.BookStore.Web.Menus
#### 添加脚本文件
在`Pages/Books/`文件夹中创建 `index.js`文件
在`Pages/Book/`文件夹中创建 `index.js`文件
![bookstore-index-js-file](./images/bookstore-index-js-file-v2.png)
![booktore-index-js-file](./images/booktore-index-js-file-v2.png)
`index.js`的内容如下:
````js
$(function () {
var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({
var dataTable = $('#BookTable').DataTable(abp.libs.datatables.normalizeConfiguration({
ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList),
columnDefs: [
{ data: "name" },
@ -727,7 +727,7 @@ $(function () {
最终的页面如下:
![Book list](./images/bookstore-book-list-2.png)
![Book list](./images/booktore-book-list-2.png)
{{end}}
@ -735,7 +735,7 @@ $(function () {
### Angular 开发
#### 创建books页面
#### 创建book页面
是时候创建可见和可用的东西了!开发ABP Angular前端应用程序时,需要使用一些工具:
@ -752,31 +752,31 @@ $(function () {
yarn
```
#### BooksModule
#### BookModule
运行以下命令创建一个名为 `BooksModule` 的新模块:
运行以下命令创建一个名为 `BookModule` 的新模块:
```bash
yarn ng generate module books --route books --module app.module
yarn ng generate module book --routing true
```
![Generating books module](./images/bookstore-creating-books-module-terminal.png)
![Generating book module](./images/booktore-creating-book-module-terminal.png)
#### 路由
打开位于 `src\app` 目录下的 `app-routing.module.ts` 文件. 添加新的 `import`替换 `books` 路径:
打开位于 `src\app` 目录下的 `app-routing.module.ts` 文件. 添加新的 `import`路由:
```js
import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added this line to imports <==
//...replaced original books path with the below
//...added book path with the below to the routes array
{
path: 'books',
path: 'book',
component: ApplicationLayoutComponent,
loadChildren: () => import('./books/books.module').then(m => m.BooksModule),
loadChildren: () => import('./book/book.module').then(m => m.BookModule),
data: {
routes: {
name: '::Menu:Books',
name: '::Menu:Book',
iconClass: 'fas fa-book'
} as ABP.Route
},
@ -785,71 +785,50 @@ import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added th
* `ApplicationLayoutComponent` 配置将应用程序布局设置为新页面, 我们添加了 `data` 对象. `name` 是菜单项的名称,`iconClass` 是菜单项的图标.
运行 `yarn start` 等待Angular为应用程序启动服务:
```bash
yarn start
```
打开浏览器导航到 http://localhost:4200/books. 你会看到一个带有 "*books works!*" 的空白页.
![initial-books-page](./images/bookstore-initial-books-page-with-layout.png)
#### Book 列表组件
用以下内容替换 `books.component.html`:
```html
<router-outlet></router-outlet>
```
在命令行运行以下命令,生成名为 book-list 的新组件:
```bash
yarn ng generate component books/book-list
yarn ng generate component book/book-list
```
![Creating books list](./images/bookstore-creating-book-list-terminal.png)
![Creating book list](./images/booktore-creating-book-list-terminal.png)
打开 `app\books` 目录下的 `books.module.ts` 文件,使用以下内容替换它:
打开 `app\book` 目录下的 `book.module.ts` 文件,使用以下内容替换它:
```js
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BooksRoutingModule } from './books-routing.module';
import { BooksComponent } from './books.component';
import { BookRoutingModule } from './book-routing.module';
import { BookListComponent } from './book-list/book-list.component';
import { SharedModule } from '../shared/shared.module'; //<== added this line ==>
@NgModule({
declarations: [BooksComponent, BookListComponent],
declarations: [BookListComponent],
imports: [
CommonModule,
BooksRoutingModule,
BookRoutingModule,
SharedModule, //<== added this line ==>
]
],
})
export class BooksModule { }
export class BookModule {}
```
* 我们导入了 `SharedModule` 并添加到 `imports` 数组.
打开 `app\books` 目录下的 `books-routing.module.ts` 文件用以下内容替换它:
打开 `app\book` 目录下的 `book-routing.module.ts` 文件用以下内容替换它:
```js
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { BookListComponent } from './book-list/book-list.component'; // <== added this line ==>
import { BooksComponent } from './books.component';
import { BookListComponent } from './book-list/book-list.component'; //<== added this line ==>
//<== replaced routes ==>
// <== replaced routes ==>
const routes: Routes = [
{
path: '',
component: BooksComponent,
children: [{ path: '', component: BookListComponent }],
component: BookListComponent,
},
];
@ -857,36 +836,42 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class BooksRoutingModule { }
export class BookRoutingModule { }
```
* 我们导入了 `BookListComponent` 并替换 `routes` 常量.
我们将看到books页面的 **book-list works!**:
运行 `yarn start`,等待Angular启动服务:
```bash
yarn start
```
我们将看到book页面的 **book-list works!**:
![Initial book list page](./images/bookstore-initial-book-list-page.png)
![Initial book list page](./images/booktore-initial-book-list-page.png)
#### 创建 BooksState
#### 创建 BookState
运行以下命令创建名为 `BooksState` 的新state:
运行以下命令创建名为 `BookState` 的新state:
```bash
npx @ngxs/cli --name books --directory src/app/books
npx @ngxs/cli --name book --directory src/app/book
```
* 此命令在 `src/app/books/state` 文件夹下创建了 `books.state.ts``books.actions.ts` 文件. 参阅 [NGXS CLI文档](https://www.ngxs.io/plugins/cli)了解更多.
* 此命令在 `src/app/book/state` 文件夹下创建了 `book.state.ts``book.actions.ts` 文件. 参阅 [NGXS CLI文档](https://www.ngxs.io/plugins/cli)了解更多.
`BooksState` 导入到 `src/app` 文件夹中的 `app.module.ts` 中. 然后添加 `BooksState``NgxsModule``forRoot` 静态方法,作为该方法的第一个参数的数组元素.
`BookState` 导入到 `src/app` 文件夹中的 `app.module.ts` 中. 然后添加 `BookState``NgxsModule``forRoot` 静态方法,作为该方法的第一个参数的数组元素.
```js
// ...
import { BooksState } from './books/state/books.state'; //<== imported BooksState ==>
import { BookState } from './book/state/book.state'; //<== imported BookState ==>
@NgModule({
imports: [
// other imports
NgxsModule.forRoot([BooksState]), //<== added BooksState ==>
NgxsModule.forRoot([BookState]), //<== added BookState ==>
//other imports
],
@ -911,50 +896,50 @@ abp generate-proxy --module app
![Generated files](./images/generated-proxies.png)
#### GetBooks 动作
#### GetBook 动作
动作可以被认为是一个命令,它应该触发某些事情发生,或者是已经发生的事情的结果事件.[See NGXS Actions文档](https://www.ngxs.io/concepts/actions).
打开 `app/books/state` 目录下的 `books.actions.ts` 文件用以下内容替换它:
打开 `app/book/state` 目录下的 `book.actions.ts` 文件用以下内容替换它:
```js
export class GetBooks {
static readonly type = '[Books] Get';
export class GetBook {
static readonly type = '[Book] Get';
}
```
#### 实现 BooksState
#### 实现 BookState
打开 `app/books/state` 目录下的 `books.state.ts` 文件用以下内容替换它:
打开 `app/book/state` 目录下的 `book.state.ts` 文件用以下内容替换它:
```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks } from './books.actions';
import { BookService } from '../../app/shared/services';
import { GetBooks } from './book.actions';
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../../app/shared/models';
import { BookDto } from '../models';
export class BooksStateModel {
export class BookStateModel {
public book: PagedResultDto<BookDto>;
}
@State<BooksStateModel>({
name: 'BooksState',
defaults: { book: {} } as BooksStateModel,
@State<BookStateModel>({
name: 'BookState',
defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BooksState {
export class BookState {
@Selector()
static getBooks(state: BooksStateModel) {
static getBooks(state: BookStateModel) {
return state.book.items || [];
}
constructor(private bookService: BookService) {}
@Action(GetBooks)
get(ctx: StateContext<BooksStateModel>) {
get(ctx: StateContext<BookStateModel>) {
return this.bookService.getListByInput().pipe(
tap((booksResponse) => {
ctx.patchState({
@ -966,22 +951,22 @@ export class BooksState {
}
```
* 我们添加了book属性到BooksStateModel模态框.
* 我们添加了 `GetBooks` 动作. 它通过 ABP CLI生成的 `BooksService` 检索图书数据.
* 我们添加了book属性到BookStateModel模态框.
* 我们添加了 `GetBook` 动作. 它通过 ABP CLI生成的 `BookService` 检索图书数据.
* `NGXS` 需要在不订阅get函数的情况下返回被观察对象.
#### BookListComponent
打开 `app\books\book-list` 目录下的 `book-list.component.ts` 用以下内容替换它:
打开 `app\book\book-list` 目录下的 `book-list.component.ts` 用以下内容替换它:
```js
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../../app/shared/models';
import { GetBooks } from '../state/books.actions';
import { BooksState } from '../state/books.state';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
@Component({
selector: 'app-book-list',
@ -989,7 +974,7 @@ import { BooksState } from '../state/books.state';
styleUrls: ['./book-list.component.scss'],
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
booksType = BookType;
@ -1012,10 +997,10 @@ export class BookListComponent implements OnInit {
}
```
* 我们添加了 `get` 函数获取books更新store.
* 我们添加了 `get` 函数获取book更新store.
* 有关 `NGXS` 特性的更多信息请参见NGXS文档中的[Dispatching actions](https://ngxs.gitbook.io/ngxs/concepts/store#dispatching-actions)和[Select](https://ngxs.gitbook.io/ngxs/concepts/select).
打开 `app\books\book-list` 目录下的 `book-list.component.html` 用以下内容替换它:
打开 `app\book\book-list` 目录下的 `book-list.component.html` 用以下内容替换它:
```html
<div class="card">
@ -1023,7 +1008,7 @@ export class BookListComponent implements OnInit {
<div class="row">
<div class="col col-md-6">
<h5 class="card-title">
{%{{{ "::Menu:Books" | abpLocalization }}}%}
{%{{{ "::Menu:Book" | abpLocalization }}}%}
</h5>
</div>
<div class="text-right col col-md-6"></div>
@ -1031,7 +1016,7 @@ export class BookListComponent implements OnInit {
</div>
<div class="card-body">
<abp-table
[value]="books$ | async"
[value]="book$ | async"
[abpLoading]="loading"
[headerTemplate]="tableHeader"
[bodyTemplate]="tableBody"
@ -1050,7 +1035,7 @@ export class BookListComponent implements OnInit {
<ng-template #tableBody let-data>
<tr>
<td>{%{{{ data.name }}}%}</td>
<td>{%{{{ booksType[data.type] }}}%}</td>
<td>{%{{{ bookType[data.type] }}}%}</td>
<td>{%{{{ data.publishDate | date }}}%}</td>
<td>{%{{{ data.price }}}%}</td>
</tr>
@ -1063,11 +1048,11 @@ export class BookListComponent implements OnInit {
现在你可以在浏览器看到最终结果:
![Book list final result](./images/bookstore-book-list.png)
![Book list final result](./images/booktore-book-list.png)
项目的文件系统结构:
![Book list final result](./images/bookstore-angular-file-tree.png)
![Book list final result](./images/booktore-angular-file-tree.png)
在本教程中我们遵循了官方的[Angular风格指南](https://angular.io/guide/styleguide#file-tree).

199
docs/zh-Hans/Tutorials/Part-2.md

@ -29,7 +29,7 @@ end
- **Part 2: 创建,编辑,删除书籍(本章)**
- [Part-3: 集成测试](Part-3.md)
> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application).
> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-booktore-application).
{{if UI == "MVC"}}
@ -37,13 +37,13 @@ end
通过本节, 你将会了解如何创建一个 modal form 来实现新增书籍的功能. 最终成果如下图所示:
![bookstore-create-dialog](./images/bookstore-create-dialog-2.png)
![booktore-create-dialog](./images/booktore-create-dialog-2.png)
#### 新建 modal form
`Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个 `CreateModal.cshtml` Razor页面:
![bookstore-add-create-dialog](./images/bookstore-add-create-dialog-v2.png)
![booktore-add-create-dialog](./images/booktore-add-create-dialog-v2.png)
##### CreateModal.cshtml.cs
@ -130,9 +130,9 @@ namespace Acme.BookStore.Web.Pages.Books
如下图所示,只是在表格 **右上方** 添加了 **New book** 按钮:
![bookstore-new-book-button](./images/bookstore-new-book-button.png)
![booktore-new-book-button](./images/booktore-new-book-button.png)
打开 `Pages/books/index.js``datatable` 配置代码后面添加如下代码:
打开 `Pages/book/index.js``datatable` 配置代码后面添加如下代码:
````js
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
@ -155,7 +155,7 @@ $('#NewBookButton').click(function (e) {
`Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个名叫 `EditModal.cshtml` 的Razor页面:
![bookstore-add-edit-dialog](./images/bookstore-add-edit-dialog.png)
![booktore-add-edit-dialog](./images/booktore-add-edit-dialog.png)
#### EditModal.cshtml.cs
@ -258,7 +258,7 @@ namespace Acme.BookStore.Web
我们将为表格每行添加下拉按钮 ("Actions") . 最终效果如下:
![bookstore-books-table-actions](images/bookstore-books-table-actions.png)
![booktore-book-table-actions](images/booktore-book-table-actions.png)
打开 `Pages/Books/Index.cshtml` 页面,并按下方所示修改表格部分的代码:
@ -279,7 +279,7 @@ namespace Acme.BookStore.Web
* 只是为"Actions"增加了一个 `th` 标签.
打开 `Pages/books/index.js` 并用以下内容进行替换:
打开 `Pages/book/index.js` 并用以下内容进行替换:
````js
$(function () {
@ -346,7 +346,7 @@ $(function () {
### 删除一个已有的Book实体
打开 `Pages/books/index.js` 文件,在 `rowAction` `items` 下新增一项:
打开 `Pages/book/index.js` 文件,在 `rowAction` `items` 下新增一项:
````js
{
@ -456,54 +456,54 @@ $(function () {
#### 状态定义
`books\state` 文件夹下打开 `books.action.ts` 文件,使用以下内容替换它:
`app\book\state` 文件夹下打开 `book.action.ts` 文件,使用以下内容替换它:
```js
import { CreateUpdateBookDto } from '../../app/shared/models'; //<== added this line ==>
import { CreateUpdateBookDto } from '../models'; //<== added this line ==>
export class GetBooks {
static readonly type = '[Books] Get';
static readonly type = '[Book] Get';
}
// added CreateUpdateBook class
export class CreateUpdateBook {
static readonly type = '[Books] Create Update Book';
static readonly type = '[Book] Create Update Book';
constructor(public payload: CreateUpdateBookDto) { }
}
```
* 我们导入了 `CreateUpdateBookDto` 模型并且创建了 `CreateUpdateBook` 动作.
打开 `books\state` 文件夹下的 `books.state.ts` 文件,使用以下内容替换它:
打开 `app\book\state` 文件夹下的 `book.state.ts` 文件,使用以下内容替换它:
```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook } from './books.actions'; // <== added CreateUpdateBook==>
import { BookService } from '../../app/shared/services';
import { GetBooks, CreateUpdateBook } from './book.actions'; // <== added CreateUpdateBook==>
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../../app/shared/models';
import { BookDto } from '../models';
export class BooksStateModel {
export class BookStateModel {
public book: PagedResultDto<BookDto>;
}
@State<BooksStateModel>({
name: 'BooksState',
defaults: { book: {} } as BooksStateModel,
@State<BookStateModel>({
name: 'BookState',
defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BooksState {
export class BookState {
@Selector()
static getBooks(state: BooksStateModel) {
static getBooks(state: BookStateModel) {
return state.book.items || [];
}
constructor(private bookService: BookService) {}
@Action(GetBooks)
get(ctx: StateContext<BooksStateModel>) {
get(ctx: StateContext<BookStateModel>) {
return this.bookService.getListByInput().pipe(
tap((bookResponse) => {
ctx.patchState({
@ -515,7 +515,7 @@ export class BooksState {
// added CreateUpdateBook action listener
@Action(CreateUpdateBook)
save(ctx: StateContext<BooksStateModel>, action: CreateUpdateBook) {
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
return this.bookService.createByInput(action.payload);
}
}
@ -527,7 +527,7 @@ export class BooksState {
#### 添加模态到 BookListComponent
打开 `books\book-list` 文件夹内的 `book-list.component.html` 文件,使用以下内容替换它:
打开 `app\book\book-list` 文件夹内的 `book-list.component.html` 文件,使用以下内容替换它:
```html
<div class="card">
@ -556,7 +556,7 @@ export class BooksState {
</div>
<div class="card-body">
<abp-table
[value]="books$ | async"
[value]="book$ | async"
[abpLoading]="loading"
[headerTemplate]="tableHeader"
[bodyTemplate]="tableBody"
@ -575,7 +575,7 @@ export class BooksState {
<ng-template #tableBody let-data>
<tr>
<td>{%{{{ data.name }}}%}</td>
<td>{%{{{ booksType[data.type] }}}%}</td>
<td>{%{{{ bookType[data.type] }}}%}</td>
<td>{%{{{ data.publishDate | date }}}%}</td>
<td>{%{{{ data.price }}}%}</td>
</tr>
@ -603,16 +603,16 @@ export class BooksState {
* `abp-modal` 是显示模态框的预构建组件. 你也可以使用其它方法显示模态框,但 `abp-modal` 提供了一些附加的好处.
* 我们添加了 `New book` 按钮到 `AbpContentToolbar`.
打开 `books\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它:
打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它:
```js
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../../app/shared/models';
import { GetBooks } from '../state/books.actions';
import { BooksState } from '../state/books.state';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
@Component({
selector: 'app-book-list',
@ -620,7 +620,7 @@ import { BooksState } from '../state/books.state';
styleUrls: ['./book-list.component.scss'],
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
booksType = BookType;
@ -654,22 +654,22 @@ export class BookListComponent implements OnInit {
你可以打开浏览器,点击**New book**按钮看到模态框.
![Empty modal for new book](./images/bookstore-empty-new-book-modal.png)
![Empty modal for new book](./images/booktore-empty-new-book-modal.png)
#### 添加响应式表单
[响应式表单](https://angular.io/guide/reactive-forms) 提供一种模型驱动的方法来处理其值随时间变化的表单输入.
打开 `app\books\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它:
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它:
```js
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../../app/shared/models';
import { GetBooks } from '../state/books.actions';
import { BooksState } from '../state/books.state';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==>
@Component({
@ -678,7 +678,7 @@ import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== adde
styleUrls: ['./book-list.component.scss'],
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
booksType = BookType;
@ -729,7 +729,7 @@ export class BookListComponent implements OnInit {
#### 创建表单的DOM元素
打开 `app\books\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `<ng-template #abpBody> </ng-template>`:
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `<ng-template #abpBody> </ng-template>`:
```html
<ng-template #abpBody>
@ -748,7 +748,7 @@ export class BookListComponent implements OnInit {
<label for="book-type">Type</label><span> * </span>
<select class="form-control" id="book-type" formControlName="type">
<option [ngValue]="null">Select a book type</option>
<option [ngValue]="booksType[type]" *ngFor="let type of bookTypeArr"> {%{{{ type }}}%}</option>
<option [ngValue]="bookType[type]" *ngFor="let type of bookTypeArr"> {%{{{ type }}}%}</option>
</select>
</div>
@ -772,14 +772,14 @@ export class BookListComponent implements OnInit {
#### Datepicker 要求
打开 `app\books` 文件夹下的 `books.module.ts` 文件,使用以下内容替换它:
打开 `app\book` 文件夹下的 `book.module.ts` 文件,使用以下内容替换它:
```js
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BooksRoutingModule } from './books-routing.module';
import { BooksComponent } from './books.component';
import { BooksRoutingModule } from './book-routing.module';
import { BooksComponent } from './book.component';
import { BookListComponent } from './book-list/book-list.component';
import { SharedModule } from '../shared/shared.module';
import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; //<== added this line ==>
@ -798,16 +798,16 @@ export class BooksModule { }
* 我们导入了 `NgbDatepickerModule` 来使用日期选择器.
打开 `app\books\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它:
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它:
```js
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../../app/shared/models';
import { GetBooks } from '../state/books.actions';
import { BooksState } from '../state/books.state';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; // <== added this line ==>
@ -818,7 +818,7 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added this line ==>
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
booksType = BookType;
@ -878,20 +878,20 @@ export class BookListComponent implements OnInit {
现在你可以打开浏览器看到以下变化:
![New book modal](./images/bookstore-new-book-form.png)
![New book modal](./images/booktore-new-book-form.png)
#### 保存图书
打开 `app\books\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它:
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它:
```js
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../../app/shared/models';
import { GetBooks, CreateUpdateBook } from '../state/books.actions'; // <== added CreateUpdateBook ==>
import { BooksState } from '../state/books.state';
import { BookDto, BookType } from '../models';
import { GetBooks, CreateUpdateBook } from '../state/book.actions'; // <== added CreateUpdateBook ==>
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
@ -902,12 +902,11 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
booksType = BookType;
//added bookTypeArr array
bookTypeArr = Object.keys(BookType).filter(
(bookType) => typeof this.booksType[bookType] === 'number'
);
@ -946,7 +945,7 @@ export class BookListComponent implements OnInit {
});
}
//<== added save ==>
// <== added save ==>
save() {
if (this.form.invalid) {
return;
@ -964,7 +963,7 @@ export class BookListComponent implements OnInit {
* 我们导入了 `CreateUpdateBook`.
* 我们添加了 `save` 方法.
打开 `app\books\book-list` 文件夹下的 `app\books\book-list`文件, 添加 `abp-button` 保存图书.
打开 `app\app\book\book-list` 文件夹下的 `app\app\book\book-list`文件, 添加 `abp-button` 保存图书.
```html
<ng-template #abpFooter>
@ -991,34 +990,34 @@ export class BookListComponent implements OnInit {
模态框最终看起来像这样:
![Save button to the modal](./images/bookstore-new-book-form-v2.png)
![Save button to the modal](./images/booktore-new-book-form-v2.png)
### 更新图书
#### CreateUpdateBook 动作
打开 `books\state` 文件夹下的 `books.actions.ts` 文件,使用以下内容替换它:
打开 `app\book\state` 文件夹下的 `book.actions.ts` 文件,使用以下内容替换它:
```js
import { CreateUpdateBookDto } from '../../app/shared/models';
import { CreateUpdateBookDto } from '../models';
export class GetBooks {
static readonly type = '[Books] Get';
static readonly type = '[Book] Get';
}
export class CreateUpdateBook {
static readonly type = '[Books] Create Update Book';
constructor(public payload: CreateUpdateBookDto, public id?: string) { } // <== added id parameter ==>
static readonly type = '[Book] Create Update Book';
constructor(public payload: CreateUpdateBookDto, public id?: string) {} // <== added id parameter ==>
}
```
* 我们在 `CreateUpdateBook` 动作的构造函数添加了 `id` 参数.
打开 `books\state` 文件夹下的 `books.state.ts` 文件,使用以下内容替换 `save` 方法:
打开 `app\book\state` 文件夹下的 `book.state.ts` 文件,使用以下内容替换 `save` 方法:
```js
@Action(CreateUpdateBook)
save(ctx: StateContext<BooksStateModel>, action: CreateUpdateBook) {
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
if (action.id) {
return this.bookService.updateByIdAndInput(action.payload, action.id);
} else {
@ -1029,19 +1028,19 @@ save(ctx: StateContext<BooksStateModel>, action: CreateUpdateBook) {
#### BookListComponent
打开 `app\books\book-list` 文件夹下的 `book-list.component.ts` 文件,在构造函数注入 `BookService` 服务,并添加 名为 `selectedBook` 的变量.
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,在构造函数注入 `BookService` 服务,并添加 名为 `selectedBook` 的变量.
```js
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../../app/shared/models';
import { GetBooks, CreateUpdateBook } from '../state/books.actions';
import { BooksState } from '../state/books.state';
import { BookDto, BookType } from '../models';
import { GetBooks, CreateUpdateBook } from '../state/book.actions';
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
import { BookService } from '../../app/shared/services'; // <== imported BookService ==>
import { BookService } from '../services'; // <== imported BookService ==>
@Component({
selector: 'app-book-list',
@ -1050,13 +1049,13 @@ import { BookService } from '../../app/shared/services'; // <== imported BookSer
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
books$: Observable<BookDto[]>;
@Select(BookState.getBooks)
book$: Observable<BookDto[]>;
booksType = BookType;
bookType = BookType;
bookTypeArr = Object.keys(BookType).filter(
(bookType) => typeof this.booksType[bookType] === 'number'
(bookType) => typeof this.bookType[bookType] === 'number'
);
loading = false;
@ -1137,12 +1136,12 @@ export class BookListComponent implements OnInit {
#### 添加 "Actions" 下拉框到表格
打开 `app\books\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `<div class="card-body">` 标签:
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `<div class="card-body">` 标签:
```html
<div class="card-body">
<abp-table
[value]="books$ | async"
[value]="book$ | async"
[abpLoading]="loading"
[headerTemplate]="tableHeader"
[bodyTemplate]="tableBody"
@ -1179,7 +1178,7 @@ export class BookListComponent implements OnInit {
</div>
</td>
<td>{%{{{ data.name }}}%}</td>
<td>{%{{{ booksType[data.type] }}}%}</td>
<td>{%{{{ bookType[data.type] }}}%}</td>
<td>{%{{{ data.publishDate | date }}}%}</td>
<td>{%{{{ data.price }}}%}</td>
</tr>
@ -1193,9 +1192,9 @@ export class BookListComponent implements OnInit {
UI最终看起来像这样:
![Action buttons](./images/bookstore-actions-buttons.png)
![Action buttons](./images/booktore-actions-buttons.png)
打开 `app\books\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `<ng-template #abpHeader>` 标签:
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `<ng-template #abpHeader>` 标签:
```html
<ng-template #abpHeader>
@ -1209,45 +1208,45 @@ UI最终看起来像这样:
#### DeleteBook 动作
打开 `books\state` 文件夹下的 `books.actions.ts` 文件添加名为 `DeleteBook` 的动作.
打开 `app\book\state` 文件夹下的 `book.actions.ts` 文件添加名为 `DeleteBook` 的动作.
```js
export class DeleteBook {
static readonly type = '[Books] Delete';
static readonly type = '[Book] Delete';
constructor(public id: string) {}
}
```
打开 `books\state` 文件夹下的 `books.state.ts` 文件,使用以下内容替换它:
打开 `app\book\state` 文件夹下的 `book.state.ts` 文件,使用以下内容替换它:
```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook, DeleteBook } from './books.actions'; // <== added DeleteBook==>
import { BookService } from '../../app/shared/services';
import { GetBooks, CreateUpdateBook, DeleteBook } from './book.actions'; // <== added DeleteBook==>
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../../app/shared/models';
import { BookDto } from '../models';
export class BooksStateModel {
export class BookStateModel {
public book: PagedResultDto<BookDto>;
}
@State<BooksStateModel>({
name: 'BooksState',
defaults: { book: {} } as BooksStateModel,
@State<BookStateModel>({
name: 'BookState',
defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BooksState {
export class BookState {
@Selector()
static getBooks(state: BooksStateModel) {
static getBooks(state: BookStateModel) {
return state.book.items || [];
}
constructor(private bookService: BookService) {}
@Action(GetBooks)
get(ctx: StateContext<BooksStateModel>) {
get(ctx: StateContext<BookStateModel>) {
return this.bookService.getListByInput().pipe(
tap((booksResponse) => {
ctx.patchState({
@ -1258,7 +1257,7 @@ export class BooksState {
}
@Action(CreateUpdateBook)
save(ctx: StateContext<BooksStateModel>, action: CreateUpdateBook) {
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
if (action.id) {
return this.bookService.updateByIdAndInput(action.payload, action.id);
} else {
@ -1268,7 +1267,7 @@ export class BooksState {
// <== added DeleteBook action listener ==>
@Action(DeleteBook)
delete(ctx: StateContext<BooksStateModel>, action: DeleteBook) {
delete(ctx: StateContext<BookStateModel>, action: DeleteBook) {
return this.bookService.deleteById(action.id);
}
}
@ -1280,7 +1279,7 @@ export class BooksState {
#### 删除确认弹层
打开 `app\books\book-list` 文件夹下的 `book-list.component.ts` 文件,注入 `ConfirmationService`.
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,注入 `ConfirmationService`.
替换构造函数:
@ -1304,7 +1303,7 @@ constructor(
`book-list.component.ts` 中添加删除方法:
```js
import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/books.actions' ;// <== imported DeleteBook ==>
import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/book.actions' ;// <== imported DeleteBook ==>
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation ==>
@ -1323,11 +1322,11 @@ delete(id: string) {
`delete` 方法会显示一个确认弹层并订阅用户响应. 只在用户点击 `Yes` 按钮时分派动作. 确认弹层看起来如下:
![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png)
![booktore-confirmation-popup](./images/booktore-confirmation-popup.png)
#### 添加删除按钮
打开 `app\books\book-list` 文件夹下的 `app\books\book-list` 文件,修改 `ngbDropdownMenu` 添加删除按钮:
打开 `app\app\book\book-list` 文件夹下的 `app\app\book\book-list` 文件,修改 `ngbDropdownMenu` 添加删除按钮:
```html
<div ngbDropdownMenu>
@ -1340,7 +1339,7 @@ delete(id: string) {
最终操作下拉框UI看起来如下:
![bookstore-final-actions-dropdown](./images/bookstore-final-actions-dropdown.png)
![booktore-final-actions-dropdown](./images/booktore-final-actions-dropdown.png)
{{end}}

BIN
docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 57 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-book-list.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 49 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-creating-book-list-terminal.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 158 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-creating-book-module-terminal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
docs/zh-Hans/Tutorials/images/generated-proxies.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 23 KiB

12
docs/zh-Hans/docs-nav.json

@ -30,8 +30,16 @@
"text": "应用开发",
"items": [
{
"text": "使用 ASP.NET Core MVC",
"path": "Tutorials/AspNetCore-Mvc/Part-I.md"
"text": "第一章: 创建新解决方案和列表页",
"path": "Tutorials/Part-1.md"
},
{
"text": "第一章: 增删改查操作",
"path": "Tutorials/Part-2.md"
},
{
"text": "第三章: 集成测试",
"path": "Tutorials/Part-3.md"
}
]
}

9
framework/Volo.Abp.sln

@ -283,7 +283,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.TextTemplating", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.TextTemplating.Tests", "test\Volo.Abp.TextTemplating.Tests\Volo.Abp.TextTemplating.Tests.csproj", "{251C7FD3-D313-4BCE-8068-352EC7EEA275}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Validation.Abstractions", "src\Volo.Abp.Validation.Abstractions\Volo.Abp.Validation.Abstractions.csproj", "{FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Validation.Abstractions", "src\Volo.Abp.Validation.Abstractions\Volo.Abp.Validation.Abstractions.csproj", "{FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.SignalR", "src\Volo.Abp.AspNetCore.SignalR\Volo.Abp.AspNetCore.SignalR.csproj", "{B64FCE08-E9D2-4984-BF12-FE199F257416}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -847,6 +849,10 @@ Global
{FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}.Release|Any CPU.Build.0 = Release|Any CPU
{B64FCE08-E9D2-4984-BF12-FE199F257416}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B64FCE08-E9D2-4984-BF12-FE199F257416}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B64FCE08-E9D2-4984-BF12-FE199F257416}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B64FCE08-E9D2-4984-BF12-FE199F257416}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -991,6 +997,7 @@ Global
{9E53F91F-EACD-4191-A487-E727741F1311} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{251C7FD3-D313-4BCE-8068-352EC7EEA275} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{B64FCE08-E9D2-4984-BF12-FE199F257416} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5}

16
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/SignalR/SignalRBrowserScriptContributor.cs

@ -0,0 +1,16 @@
using System.Collections.Generic;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Packages.Core;
using Volo.Abp.Modularity;
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR
{
[DependsOn(typeof(CoreScriptContributor))]
public class SignalRBrowserScriptContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/libs/signalr/browser/signalr.js");
}
}
}

12
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js

@ -1,4 +1,4 @@
/**
/**
* TODO: Document & prepare typescript definitions
* TODO: Refactor & test more
*/
@ -49,6 +49,7 @@ $.validator.defaults.ignore = ''; //TODO: Would be better if we can apply only f
var _publicApi = null;
var _args = null;
var _onOpenCallbacks = new CallbackList();
var _onCloseCallbacks = new CallbackList();
var _onResultCallbacks = new CallbackList();
@ -106,6 +107,7 @@ $.validator.defaults.ignore = ''; //TODO: Would be better if we can apply only f
}
$firstVisibleInput.focus();
_onOpenCallbacks.triggerAll(_publicApi);
});
var modalClass = abp.modals[options.modalClass];
@ -147,6 +149,10 @@ $.validator.defaults.ignore = ''; //TODO: Would be better if we can apply only f
_$modal.modal('hide');
};
var _onOpen = function (onOpenCallback) {
_onOpenCallbacks.add(onOpenCallback);
};
var _onClose = function (onCloseCallback) {
_onCloseCallbacks.add(onCloseCallback);
};
@ -188,6 +194,8 @@ $.validator.defaults.ignore = ''; //TODO: Would be better if we can apply only f
_onResultCallbacks.triggerAll(_publicApi, arguments);
},
onOpen: _onOpen,
onClose: _onClose,
onResult: _onResult
@ -198,4 +206,4 @@ $.validator.defaults.ignore = ''; //TODO: Would be better if we can apply only f
};
})();
})(jQuery);
})(jQuery);

10
framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Builder/AbpAspNetCoreMvcApplicationBuilderExtensions.cs

@ -15,18 +15,12 @@ namespace Microsoft.AspNetCore.Builder
/// <param name="app">The <see cref="T:Microsoft.AspNetCore.Builder.IApplicationBuilder" />.</param>
/// <param name="additionalConfigurationAction">Additional action to configure routes</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
[Obsolete("Use app.UseConfiguredEndpoints(...) extension method instead!")]
public static IApplicationBuilder UseMvcWithDefaultRouteAndArea(
this IApplicationBuilder app,
Action<IEndpointRouteBuilder> additionalConfigurationAction = null)
{
return app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute("defaultWithArea", "{area}/{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
additionalConfigurationAction?.Invoke(endpoints);
});
return app.UseConfiguredEndpoints(additionalConfigurationAction);
}
}
}

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

@ -13,10 +13,12 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Localization;
using Volo.Abp.ApiVersioning;
@ -165,6 +167,16 @@ namespace Volo.Abp.AspNetCore.Mvc
{
mvcOptions.AddAbp(context.Services);
});
Configure<AbpEndpointRouterOptions>(options =>
{
options.EndpointConfigureActions.Add(context =>
{
context.Endpoints.MapControllerRoute("defaultWithArea", "{area}/{controller=Home}/{action=Index}/{id?}");
context.Endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
context.Endpoints.MapRazorPages();
});
});
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)

6
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpController.cs

@ -75,6 +75,9 @@ namespace Volo.Abp.AspNetCore.Mvc
public ILoggerFactory LoggerFactory => LazyGetRequiredService(ref _loggerFactory);
private ILoggerFactory _loggerFactory;
protected ILogger Logger => _lazyLogger.Value;
private Lazy<ILogger> _lazyLogger => new Lazy<ILogger>(() => LoggerFactory?.CreateLogger(GetType().FullName) ?? NullLogger.Instance, true);
public ICurrentUser CurrentUser => LazyGetRequiredService(ref _currentUser);
private ICurrentUser _currentUser;
@ -118,8 +121,5 @@ namespace Volo.Abp.AspNetCore.Mvc
{
ModelValidator?.Validate(ModelState);
}
protected ILogger Logger => _lazyLogger.Value;
private Lazy<ILogger> _lazyLogger => new Lazy<ILogger>(() => LoggerFactory?.CreateLogger(GetType().FullName) ?? NullLogger.Instance, true);
}
}

9
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DependencyInjection/AbpAspNetCoreMvcConventionalDependencyRegistrar.cs

@ -29,8 +29,13 @@ namespace Volo.Abp.AspNetCore.Mvc.DependencyInjection
foreach (var serviceType in serviceTypes)
{
var serviceDescriptor = ServiceDescriptor.Describe(serviceType, type, lifeTime);
services.Add(serviceDescriptor);
services.Add(
ServiceDescriptor.Describe(
serviceType,
type,
lifeTime
)
);
}
}

3
framework/src/Volo.Abp.AspNetCore.SignalR/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

30
framework/src/Volo.Abp.AspNetCore.SignalR/FodyWeavers.xsd

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

27
framework/src/Volo.Abp.AspNetCore.SignalR/Properties/launchSettings.json

@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53900/",
"sslPort": 44362
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.SignalR": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

23
framework/src/Volo.Abp.AspNetCore.SignalR/Volo.Abp.AspNetCore.SignalR.csproj

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="..\..\..\configureawait.props" />
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyName>Volo.Abp.AspNetCore.SignalR</AssemblyName>
<PackageId>Volo.Abp.AspNetCore.SignalR</PackageId>
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<IsPackable>true</IsPackable>
<OutputType>Library</OutputType>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.AspNetCore\Volo.Abp.AspNetCore.csproj" />
</ItemGroup>
</Project>

115
framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRModule.cs

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.Modularity;
namespace Volo.Abp.AspNetCore.SignalR
{
[DependsOn(
typeof(AbpAspNetCoreModule)
)]
public class AbpAspNetCoreSignalRModule : AbpModule
{
private static readonly MethodInfo MapHubGenericMethodInfo =
typeof(AbpAspNetCoreSignalRModule)
.GetMethod("MapHub", BindingFlags.Static | BindingFlags.NonPublic);
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddConventionalRegistrar(new AbpSignalRConventionalRegistrar());
AutoAddHubTypes(context.Services);
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddSignalR();
Configure<AbpEndpointRouterOptions>(options =>
{
options.EndpointConfigureActions.Add(endpointContext =>
{
var signalROptions = endpointContext
.ScopeServiceProvider
.GetRequiredService<IOptions<AbpSignalROptions>>()
.Value;
foreach (var hubConfig in signalROptions.Hubs)
{
MapHubType(
hubConfig.HubType,
endpointContext.Endpoints,
hubConfig.RoutePattern,
opts =>
{
foreach (var configureAction in hubConfig.ConfigureActions)
{
configureAction(opts);
}
}
);
}
});
});
}
private void AutoAddHubTypes(IServiceCollection services)
{
var hubTypes = new List<Type>();
services.OnRegistred(context =>
{
if (typeof(Hub).IsAssignableFrom(context.ImplementationType))
{
hubTypes.Add(context.ImplementationType);
}
});
services.Configure<AbpSignalROptions>(options =>
{
foreach (var hubType in hubTypes)
{
options.Hubs.Add(HubConfig.Create(hubType));
}
});
}
private void MapHubType(
Type hubType,
IEndpointRouteBuilder endpoints,
string pattern,
Action<HttpConnectionDispatcherOptions> configureOptions)
{
MapHubGenericMethodInfo
.MakeGenericMethod(hubType)
.Invoke(
this,
new object[]
{
endpoints,
pattern,
configureOptions
}
);
}
// ReSharper disable once UnusedMember.Local (used via reflection)
private static void MapHub<THub>(
IEndpointRouteBuilder endpoints,
string pattern,
Action<HttpConnectionDispatcherOptions> configureOptions)
where THub : Hub
{
endpoints.MapHub<THub>(
pattern,
configureOptions
);
}
}
}

135
framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpHub.cs

@ -0,0 +1,135 @@
using System;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.Localization;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Timing;
using Volo.Abp.Users;
namespace Volo.Abp.AspNetCore.SignalR
{
public abstract class AbpHub : Hub
{
public IServiceProvider ServiceProvider { get; set; }
protected readonly object ServiceProviderLock = new object();
protected TService LazyGetRequiredService<TService>(ref TService reference)
=> LazyGetRequiredService(typeof(TService), ref reference);
protected TRef LazyGetRequiredService<TRef>(Type serviceType, ref TRef reference)
{
if (reference == null)
{
lock (ServiceProviderLock)
{
if (reference == null)
{
reference = (TRef)ServiceProvider.GetRequiredService(serviceType);
}
}
}
return reference;
}
public ILoggerFactory LoggerFactory => LazyGetRequiredService(ref _loggerFactory);
private ILoggerFactory _loggerFactory;
protected ILogger Logger => _lazyLogger.Value;
private Lazy<ILogger> _lazyLogger => new Lazy<ILogger>(() => LoggerFactory?.CreateLogger(GetType().FullName) ?? NullLogger.Instance, true);
public ICurrentUser CurrentUser => LazyGetRequiredService(ref _currentUser);
private ICurrentUser _currentUser;
public ICurrentTenant CurrentTenant => LazyGetRequiredService(ref _currentTenant);
private ICurrentTenant _currentTenant;
public IAuthorizationService AuthorizationService => LazyGetRequiredService(ref _authorizationService);
private IAuthorizationService _authorizationService;
public IClock Clock => LazyGetRequiredService(ref _clock);
private IClock _clock;
public IStringLocalizerFactory StringLocalizerFactory => LazyGetRequiredService(ref _stringLocalizerFactory);
private IStringLocalizerFactory _stringLocalizerFactory;
public IStringLocalizer L => _localizer ?? (_localizer = StringLocalizerFactory.Create(LocalizationResource));
private IStringLocalizer _localizer;
protected Type LocalizationResource
{
get => _localizationResource;
set
{
_localizationResource = value;
_localizer = null;
}
}
private Type _localizationResource = typeof(DefaultResource);
}
public abstract class AbpHub<T> : Hub<T>
where T : class
{
public IServiceProvider ServiceProvider { get; set; }
protected readonly object ServiceProviderLock = new object();
protected TService LazyGetRequiredService<TService>(ref TService reference)
=> LazyGetRequiredService(typeof(TService), ref reference);
protected TRef LazyGetRequiredService<TRef>(Type serviceType, ref TRef reference)
{
if (reference == null)
{
lock (ServiceProviderLock)
{
if (reference == null)
{
reference = (TRef)ServiceProvider.GetRequiredService(serviceType);
}
}
}
return reference;
}
public ILoggerFactory LoggerFactory => LazyGetRequiredService(ref _loggerFactory);
private ILoggerFactory _loggerFactory;
protected ILogger Logger => _lazyLogger.Value;
private Lazy<ILogger> _lazyLogger => new Lazy<ILogger>(() => LoggerFactory?.CreateLogger(GetType().FullName) ?? NullLogger.Instance, true);
public ICurrentUser CurrentUser => LazyGetRequiredService(ref _currentUser);
private ICurrentUser _currentUser;
public ICurrentTenant CurrentTenant => LazyGetRequiredService(ref _currentTenant);
private ICurrentTenant _currentTenant;
public IAuthorizationService AuthorizationService => LazyGetRequiredService(ref _authorizationService);
private IAuthorizationService _authorizationService;
public IClock Clock => LazyGetRequiredService(ref _clock);
private IClock _clock;
public IStringLocalizerFactory StringLocalizerFactory => LazyGetRequiredService(ref _stringLocalizerFactory);
private IStringLocalizerFactory _stringLocalizerFactory;
public IStringLocalizer L => _localizer ?? (_localizer = StringLocalizerFactory.Create(LocalizationResource));
private IStringLocalizer _localizer;
protected Type LocalizationResource
{
get => _localizationResource;
set
{
_localizationResource = value;
_localizer = null;
}
}
private Type _localizationResource = typeof(DefaultResource);
}
}

43
framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalRConventionalRegistrar.cs

@ -0,0 +1,43 @@
using System;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.SignalR
{
public class AbpSignalRConventionalRegistrar : ConventionalRegistrarBase
{
public override void AddType(IServiceCollection services, Type type)
{
if (IsConventionalRegistrationDisabled(type))
{
return;
}
if (!IsHub(type))
{
return;
}
var serviceTypes = ExposedServiceExplorer.GetExposedServices(type);
TriggerServiceExposing(services, type, serviceTypes);
foreach (var serviceType in serviceTypes)
{
services.Add(
ServiceDescriptor.Describe(
serviceType,
type,
ServiceLifetime.Transient
)
);
}
}
private static bool IsHub(Type type)
{
return typeof(Hub).IsAssignableFrom(type);
}
}
}

14
framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions.cs

@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace Volo.Abp.AspNetCore.SignalR
{
public class AbpSignalROptions
{
public List<HubConfig> Hubs { get; }
public AbpSignalROptions()
{
Hubs = new List<HubConfig>();
}
}
}

21
framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalRUserIdProvider.cs

@ -0,0 +1,21 @@
using Microsoft.AspNetCore.SignalR;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Users;
namespace Volo.Abp.AspNetCore.SignalR
{
public class AbpSignalRUserIdProvider : IUserIdProvider, ITransientDependency
{
public ICurrentUser CurrentUser { get; }
public AbpSignalRUserIdProvider(ICurrentUser currentUser)
{
CurrentUser = currentUser;
}
public virtual string GetUserId(HubConnectionContext connection)
{
return CurrentUser.Id?.ToString();
}
}
}

43
framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfig.cs

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.SignalR;
namespace Volo.Abp.AspNetCore.SignalR
{
public class HubConfig
{
[NotNull]
public Type HubType { get; }
[NotNull]
public string RoutePattern { get; set; }
[NotNull]
public List<Action<HttpConnectionDispatcherOptions>> ConfigureActions { get; set; }
public HubConfig(
[NotNull] Type hubType,
[NotNull] string routePattern)
{
HubType = Check.NotNull(hubType, nameof(hubType));
RoutePattern = Check.NotNullOrWhiteSpace(routePattern, nameof(routePattern));
ConfigureActions = new List<Action<HttpConnectionDispatcherOptions>>();
}
public static HubConfig Create<THub>()
where THub : Hub
{
return Create(typeof(THub));
}
public static HubConfig Create(Type hubType)
{
return new HubConfig(
hubType,
HubRouteAttribute.GetRoutePattern(hubType)
);
}
}
}

33
framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubRouteAttribute.cs

@ -0,0 +1,33 @@
using System;
using System.Reflection;
using Microsoft.AspNetCore.SignalR;
namespace Volo.Abp.AspNetCore.SignalR
{
public class HubRouteAttribute : Attribute
{
public string RoutePattern { get; set; }
public HubRouteAttribute(string routePattern)
{
RoutePattern = routePattern;
}
public static string GetRoutePattern<THub>()
where THub : Hub
{
return GetRoutePattern(typeof(THub));
}
public static string GetRoutePattern(Type hubType)
{
var routeAttribute = hubType.GetSingleAttributeOrNull<HubRouteAttribute>();
if (routeAttribute != null)
{
return routeAttribute.RoutePattern;
}
return "/signalr-hubs/" + hubType.Name.RemovePostFix("Hub").ToKebabCase();
}
}
}

47
framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpAspNetCoreApplicationBuilderExtensions.cs

@ -0,0 +1,47 @@
using System;
using System.Linq;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Builder
{
public static class AbpAspNetCoreApplicationBuilderExtensions
{
/// <summary>
/// Maps endpoints configured with the <see cref="AbpEndpointRouterOptions"/>.
/// It internally uses the standard app.UseEndpoints(...) method.
/// </summary>
/// <param name="app">The application builder</param>
/// <param name="additionalConfigurationAction">Additional (and optional) endpoint configuration</param>
/// <returns></returns>
public static IApplicationBuilder UseConfiguredEndpoints(
this IApplicationBuilder app,
Action<IEndpointRouteBuilder> additionalConfigurationAction = null)
{
var options = app.ApplicationServices
.GetRequiredService<IOptions<AbpEndpointRouterOptions>>()
.Value;
if (!options.EndpointConfigureActions.Any())
{
return app;
}
return app.UseEndpoints(endpoints =>
{
using (var scope = app.ApplicationServices.CreateScope())
{
var context = new EndpointRouteBuilderContext(endpoints, scope.ServiceProvider);
foreach (var configureAction in options.EndpointConfigureActions)
{
configureAction(context);
}
additionalConfigurationAction?.Invoke(endpoints);
}
});
}
}
}

15
framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Routing/AbpEndpointRouterOptions.cs

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Routing
{
public class AbpEndpointRouterOptions
{
public List<Action<EndpointRouteBuilderContext>> EndpointConfigureActions { get; }
public AbpEndpointRouterOptions()
{
EndpointConfigureActions = new List<Action<EndpointRouteBuilderContext>>();
}
}
}

18
framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Routing/EndpointRouteBuilderContext.cs

@ -0,0 +1,18 @@
using System;
namespace Microsoft.AspNetCore.Routing
{
public class EndpointRouteBuilderContext
{
public IEndpointRouteBuilder Endpoints { get; }
public IServiceProvider ScopeServiceProvider { get; }
public EndpointRouteBuilderContext(
IEndpointRouteBuilder endpoints,
IServiceProvider scopeServiceProvider)
{
Endpoints = endpoints;
ScopeServiceProvider = scopeServiceProvider;
}
}
}

13
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/VirtualFileSystem/WebContentFileProvider.cs

@ -99,17 +99,14 @@ namespace Volo.Abp.AspNetCore.VirtualFileSystem
_virtualFileProvider
};
if (_hostingEnvironment.IsDevelopment() &&
if (_hostingEnvironment.IsDevelopment() &&
_hostingEnvironment.WebRootFileProvider is CompositeFileProvider compositeFileProvider)
{
var staticWebAssetsFileProvider = compositeFileProvider
var staticWebAssetsFileProviders = compositeFileProvider
.FileProviders
.FirstOrDefault(f => f.GetType().Name.Equals("StaticWebAssetsFileProvider"));
.Where(f => f.GetType().Name.Equals("StaticWebAssetsFileProvider")).ToList();
if (staticWebAssetsFileProvider != null)
{
fileProviders.Add(staticWebAssetsFileProvider);
}
fileProviders.AddRange(staticWebAssetsFileProviders);
}
return new CompositeFileProvider(
@ -127,4 +124,4 @@ namespace Volo.Abp.AspNetCore.VirtualFileSystem
return Options.AllowedExtraWebContentFileExtensions.Any(e => path.EndsWith(e, StringComparison.OrdinalIgnoreCase));
}
}
}
}

49
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateProxyCommand.cs

@ -21,6 +21,7 @@ namespace Volo.Abp.Cli.Commands
public static Dictionary<string, Dictionary<string, string>> propertyList = new Dictionary<string, Dictionary<string, string>>();
public ILogger<GenerateProxyCommand> Logger { get; set; }
public static string outputPrefix = "src/app";
public static string output = "";
protected TemplateProjectBuilder TemplateProjectBuilder { get; }
@ -57,6 +58,12 @@ namespace Volo.Abp.Cli.Commands
var uiFramework = GetUiFramework(commandLineArgs);
output = commandLineArgs.Options.GetOrNull(Options.Output.Short, Options.Output.Long);
if (!string.IsNullOrWhiteSpace(output) && !(output.EndsWith("/") || output.EndsWith("\\")))
{
output += "/";
}
WebClient client = new WebClient();
string json = "";
try
@ -99,7 +106,10 @@ namespace Volo.Abp.Cli.Commands
var rootPath = moduleItem.Key;
var apiName = apiNameList.Where(p => p.Key == rootPath).Select(p => p.Value).FirstOrDefault();
Logger.LogInformation($"{rootPath} directory is creating");
if (rootPath != "app")
{
Logger.LogInformation($"{rootPath} directory is creating");
}
if (rootPath == "app")
{
@ -110,7 +120,7 @@ namespace Volo.Abp.Cli.Commands
outputPrefix = "src/app";
}
Directory.CreateDirectory($"{outputPrefix}/{rootPath}");
Directory.CreateDirectory(output + $"{outputPrefix}/{rootPath}");
foreach (var controller in moduleValue.Root.ToList().Select(item => item.First))
{
@ -140,8 +150,8 @@ namespace Volo.Abp.Cli.Commands
var controllerPathName = controllerName.ToLower().Replace("controller", "");
controllerPathName = (controllerPathName.StartsWith(rootPath)) ? controllerPathName.Substring(rootPath.Length) : controllerPathName;
Directory.CreateDirectory($"{outputPrefix}/{rootPath}/{controllerPathName}/models");
Directory.CreateDirectory($"{outputPrefix}/{rootPath}/{controllerPathName}/services");
Directory.CreateDirectory(output + $"{outputPrefix}/{rootPath}/{controllerPathName}/models");
Directory.CreateDirectory(output + $"{outputPrefix}/{rootPath}/{controllerPathName}/services");
foreach (var actionItem in controller["actions"])
{
@ -240,7 +250,7 @@ namespace Volo.Abp.Cli.Commands
foreach (var parameterItem in parameterModel.OrderBy(p => p.DisplayOrder))
{
var parameterItemModelName = parameterItem.Type.PascalToKebabCase() + ".ts";
var parameterItemModelPath = $"{outputPrefix}/{rootPath}/{controllerPathName}/models/{parameterItemModelName}";
var parameterItemModelPath = output + $"{outputPrefix}/{rootPath}/{controllerPathName}/models/{parameterItemModelName}";
if (parameterItem.BindingSourceId == "body" && !File.Exists(parameterItemModelPath))
{
parameterItem.Type = "any";
@ -277,7 +287,7 @@ namespace Volo.Abp.Cli.Commands
var secondType = secondTypeArray[secondTypeArray.Length - 1].TrimEnd('>');
var secondTypeModelName = secondType.PascalToKebabCase() + ".ts";
var secondTypeModelPath = $"{outputPrefix}/{rootPath}/{controllerPathName}/models/{secondTypeModelName}";
var secondTypeModelPath = output + $"{outputPrefix}/{rootPath}/{controllerPathName}/models/{secondTypeModelName}";
if (firstType == "List" && !File.Exists(secondTypeModelPath))
{
secondType = "any";
@ -374,7 +384,7 @@ namespace Volo.Abp.Cli.Commands
serviceFileText.AppendLine("}");
serviceFileText.Replace("[controllerName]", controllerName);
File.WriteAllText($"{outputPrefix}/{rootPath}/{controllerPathName}/services/{controllerServiceName}", serviceFileText.ToString());
File.WriteAllText(output + $"{outputPrefix}/{rootPath}/{controllerPathName}/services/{controllerServiceName}", serviceFileText.ToString());
@ -385,7 +395,7 @@ namespace Volo.Abp.Cli.Commands
serviceIndexFileText.AppendLine($"export * from './{serviceIndexItem}';");
}
File.WriteAllText($"{outputPrefix}/{rootPath}/{controllerPathName}/services/index.ts", serviceIndexFileText.ToString());
File.WriteAllText(output + $"{outputPrefix}/{rootPath}/{controllerPathName}/services/index.ts", serviceIndexFileText.ToString());
if (modelIndexList.Count > 0)
{
@ -396,7 +406,7 @@ namespace Volo.Abp.Cli.Commands
modelIndexFileText.AppendLine($"export * from './{modelIndexItem}';");
}
File.WriteAllText($"{outputPrefix}/{rootPath}/{controllerPathName}/models/index.ts", modelIndexFileText.ToString());
File.WriteAllText(output + $"{outputPrefix}/{rootPath}/{controllerPathName}/models/index.ts", modelIndexFileText.ToString());
}
}
}
@ -467,6 +477,7 @@ namespace Volo.Abp.Cli.Commands
|| returnValueType.Contains("ActionResult")
|| returnValueType.Contains("IStringValueType")
|| returnValueType.Contains("IValueValidator")
|| returnValueType.Contains("Guid")
)
{
return null;
@ -483,7 +494,7 @@ namespace Volo.Abp.Cli.Commands
var typeModelName = typeName.Replace("<", "").Replace(">", "").Replace("?", "").PascalToKebabCase() + ".ts";
var path = $"{outputPrefix}/{rootPath}/{controllerPathName}/models/{typeModelName}";
var path = output + $"{outputPrefix}/{rootPath}/{controllerPathName}/models/{typeModelName}";
var modelFileText = new StringBuilder();
@ -513,7 +524,9 @@ namespace Volo.Abp.Cli.Commands
baseTypeName = "string";
}
modelFileText.AppendLine(Environment.NewLine);
modelFileText.AppendLine($"import {{ {baseTypeName} }} from '{baseTypeKebabCase}';");
extends = "extends " + (!string.IsNullOrWhiteSpace(customBaseTypeName) ? customBaseTypeName : baseTypeName);
var modelIndex = CreateType(data, baseType, rootPath, modelIndexList, controllerPathName);
@ -582,7 +595,7 @@ namespace Volo.Abp.Cli.Commands
)
{
var typeSimpleModelName = typeSimple.PascalToKebabCase() + ".ts";
var modelPath = $"{outputPrefix}/{rootPath}/{controllerPathName}/models/{typeSimpleModelName}";
var modelPath = output + $"{outputPrefix}/{rootPath}/{controllerPathName}/models/{typeSimpleModelName}";
if (!File.Exists(modelPath))
{
typeSimple = "any" + (typeSimple.Contains("[]") ? "[]" : "");
@ -601,14 +614,13 @@ namespace Volo.Abp.Cli.Commands
var propertyType = propertyTypeSplit[propertyTypeSplit.Length - 1];
var propertyTypeKebabCase = propertyType.PascalToKebabCase();
if (File.Exists($"{outputPrefix}/{rootPath}/{controllerPathName}/models/{propertyTypeKebabCase}.ts"))
if (File.Exists(output + $"{outputPrefix}/{rootPath}/{controllerPathName}/models/{propertyTypeKebabCase}.ts"))
{
from = "./" + propertyTypeKebabCase;
}
modelFileText.Insert(0, "");
modelFileText.Insert(0, $"import {{ {propertyType} }} from '{from}';");
modelFileText.Insert(0, "");
modelFileText.Insert(0, Environment.NewLine);
modelIndexList.Add(modelIndex);
}
@ -651,7 +663,7 @@ namespace Volo.Abp.Cli.Commands
modelFileText.AppendLine("}");
}
File.WriteAllText($"{outputPrefix}/{rootPath}/{controllerPathName}/models/{typeModelName}", modelFileText.ToString());
File.WriteAllText(output + $"{outputPrefix}/{rootPath}/{controllerPathName}/models/{typeModelName}", modelFileText.ToString());
return typeModelName.Replace(".ts", "");
}
@ -758,6 +770,11 @@ namespace Volo.Abp.Cli.Commands
public const string Short = "u";
public const string Long = "ui";
}
public static class Output
{
public const string Short = "o";
public const string Long = "out";
}
}
}

17
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs

@ -54,14 +54,8 @@ namespace Volo.Abp.Cli.ProjectBuilding
string templateSource = null)
{
DirectoryHelper.CreateIfNotExists(CliPaths.TemplateCache);
var latestVersion = await GetLatestSourceCodeVersionAsync(name, type);
string latestVersion;
#if DEBUG
latestVersion = await GetLatestSourceCodeVersionAsync(name, type, $"{CliUrls.WwwAbpIoProduction}api/download/{type}/get-version/");
#else
latestVersion = await GetLatestSourceCodeVersionAsync(name, type);
#endif
if (version == null)
{
if (latestVersion == null)
@ -84,13 +78,7 @@ namespace Volo.Abp.Cli.ProjectBuilding
version = latestVersion;
}
string nugetVersion;
#if DEBUG
nugetVersion = version;
#else
nugetVersion = (await GetTemplateNugetVersionAsync(name, type, version)) ?? version;
#endif
var nugetVersion = (await GetTemplateNugetVersionAsync(name, type, version)) ?? version;
if (!string.IsNullOrWhiteSpace(templateSource) && !IsNetworkSource(templateSource))
{
@ -199,7 +187,6 @@ namespace Volo.Abp.Cli.ProjectBuilding
}
catch (Exception ex)
{
Console.WriteLine("Error occured while getting the NuGet version from {0} : {1}", url, ex.Message);
return null;
}
}

16
framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/zh-Hant.json

@ -10,14 +10,16 @@
"DisplayName:Abp.Mailing.Smtp.Domain": "網域",
"DisplayName:Abp.Mailing.Smtp.EnableSsl": "啟用SSL",
"DisplayName:Abp.Mailing.Smtp.UseDefaultCredentials": "使用預設憑證",
"Description:Abp.Mailing.DefaultFromAddress": "預設發信者地址.",
"Description:Abp.Mailing.DefaultFromDisplayName": "預設發信者名稱.",
"Description:Abp.Mailing.Smtp.Host": "SMTP 服務的主機名稱或主機IP位址.",
"Description:Abp.Mailing.DefaultFromAddress": "預設發信者地址.",
"Description:Abp.Mailing.DefaultFromDisplayName": "預設發信者名稱.",
"Description:Abp.Mailing.Smtp.Host": "SMTP 服務的主機名稱或IP位址.",
"Description:Abp.Mailing.Smtp.Port": "SMTP 服務的埠號.",
"Description:Abp.Mailing.Smtp.UserName": "憑證使用者名稱.",
"Description:Abp.Mailing.Smtp.Password": "憑證使用者密碼.",
"Description:Abp.Mailing.Smtp.Domain": "驗證憑證的網域名稱或電腦名稱.",
"Description:Abp.Mailing.Smtp.EnableSsl": "設定 SmtpClient 是否使用安全通訊協定.",
"Description:Abp.Mailing.Smtp.UseDefaultCredentials": "設定是否使用預設憑證."
"Description:Abp.Mailing.Smtp.Domain": "驗證憑證的網域或電腦名稱.",
"Description:Abp.Mailing.Smtp.EnableSsl": "指定 SmtpClient 是否使用安全通訊(SSL)協定.",
"Description:Abp.Mailing.Smtp.UseDefaultCredentials": "設定是否使用預設憑證.",
"TextTemplate:StandardEmailTemplates.Layout": "預設的電子郵件樣式",
"TextTemplate:StandardEmailTemplates.Message": "電子郵件內的文字訊息"
}
}
}

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

@ -9,7 +9,7 @@ namespace Volo.Abp.ObjectExtending
{
public class ObjectExtensionManager
{
public static ObjectExtensionManager Instance { get; set; } = new ObjectExtensionManager();
public static ObjectExtensionManager Instance { get; protected set; } = new ObjectExtensionManager();
[NotNull]
public ConcurrentDictionary<object, object> Configuration { get; }

3
framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkManager.cs

@ -1,5 +1,4 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Uow

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

@ -95,7 +95,7 @@ namespace Volo.Abp.AspNetCore.Mvc
app.UseAuthorization();
app.UseAuditing();
app.UseUnitOfWork();
app.UseMvcWithDefaultRouteAndArea();
app.UseConfiguredEndpoints();
}
}
}

2
framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/AbpAspNetCoreMvcUiBootstrapDemoModule.cs

@ -24,7 +24,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo
app.UseRouting();
app.UseVirtualFiles();
app.UseMvcWithDefaultRouteAndArea();
app.UseConfiguredEndpoints();
}
}
}

2
framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/AbpAspNetCoreMvcUiThemeBasicDemoModule.cs

@ -57,7 +57,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo
app.UseVirtualFiles();
app.UseRouting();
app.UseMvcWithDefaultRouteAndArea();
app.UseConfiguredEndpoints();
}
}
}

2
framework/test/Volo.Abp.AspNetCore.Mvc.Versioning.Tests/Volo/Abp/AspNetCore/Mvc/Versioning/AbpAspNetCoreMvcVersioningTestModule.cs

@ -67,7 +67,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Versioning
{
var app = context.GetApplicationBuilder();
app.UseRouting();
app.UseMvcWithDefaultRouteAndArea();
app.UseConfiguredEndpoints();
}
}
}

2
framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo/Abp/AspNetCore/App/AbpSerilogTestModule.cs

@ -33,7 +33,7 @@ namespace Volo.Abp.AspNetCore.App
app.UseMultiTenancy();
app.UseAuditing();
app.UseAbpSerilogEnrichers();
app.UseMvcWithDefaultRouteAndArea();
app.UseConfiguredEndpoints();
}
}
}

2
modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs

@ -158,7 +158,7 @@ namespace Volo.BloggingTestApp
app.UseRequestLocalization(app.ApplicationServices.GetRequiredService<IOptions<RequestLocalizationOptions>>().Value);
app.UseMvcWithDefaultRouteAndArea();
app.UseConfiguredEndpoints();
using (var scope = context.ServiceProvider.CreateScope())

12
modules/blogging/src/Volo.Blogging.Web/BloggingTwitterOptions.cs

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Volo.Blogging
{
public class BloggingTwitterOptions
{
public string Site { get; set; }
}
}

3
modules/blogging/src/Volo.Blogging.Web/BloggingUrlOptions.cs

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Volo.Blogging
{

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

@ -2,16 +2,25 @@
@inherits Volo.Blogging.Pages.Blog.BloggingPage
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Http.Extensions
@using Microsoft.Extensions.Options
@using Volo.Abp.Users
@using Volo.Blogging
@using Volo.Blogging.Pages.Blog.Posts
@using Volo.Blogging.Areas.Blog.Helpers.TagHelpers
@using Volo.Abp.AspNetCore.Mvc.UI.Packages.Prismjs
@inject IAuthorizationService Authorization
@inject IOptionsSnapshot<BloggingTwitterOptions> twitterOptions
@model DetailModel
@{
ViewBag.Title = Model.Post.Title;
ViewBag.Description = Model.Post.Description;
ViewBag.TwitterCard = "summary_large_image";
ViewBag.TwitterSite = string.IsNullOrWhiteSpace(twitterOptions.Value.Site) ? "" : twitterOptions.Value.Site;
ViewBag.TwitterTitle = Model.Post.Title;
ViewBag.TwitterDescription = Model.Post.Description;
ViewBag.TwitterImage = $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Model.Post.CoverImage}";
var hasCommentingPermission = CurrentUser.IsAuthenticated; //TODO: Apply real policy!
}
@section scripts {

66
modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml

@ -59,8 +59,8 @@
<div class="col pl-1">
<h5 class="mt-2 mb-1">@post.Writer.UserName <span>@ConvertDatetimeToTimeAgo(post.CreationTime)</span></h5>
<i class="fa fa-eye"></i> @L["WiewsWithCount", post.ReadCount]
<span class="vs-seperator">|</span>
<i class="fa fa-comment"></i> @L["CommentWithCount", post.CommentCount]
@*<span class="vs-seperator">|</span>
<i class="fa fa-comment"></i> @L["CommentWithCount", post.CommentCount]*@
</div>
</div>
</div>
@ -105,19 +105,23 @@
<h3 class="font-125">
<a asp-page="./Detail" asp-route-postUrl="@post.Url" asp-route-blogShortName="@Model.BlogShortName">@post.Title</a>
</h3>
<div class="article-owner">
<div class="article-infos">
<div class="user-card">
@if (post.Writer != null)
{
<h5 class="mt-2 mb-1">@post.Writer.UserName <span>@ConvertDatetimeToTimeAgo(post.CreationTime)</span></h5>
}
<i class="fa fa-eye"></i> @L["WiewsWithCount", post.ReadCount]
<span class="vs-seperator">|</span>
<i class="fa fa-comment"></i> @L["CommentWithCount", post.CommentCount]
@if (post.Writer != null)
{
<div class="article-owner">
<div class="article-infos">
<div class="user-card">
@if (post.Writer != null)
{
<h5 class="mt-2 mb-1">@post.Writer.UserName <span>@ConvertDatetimeToTimeAgo(post.CreationTime)</span></h5>
}
<i class="fa fa-eye"></i> @L["WiewsWithCount", post.ReadCount]
@*<span class="vs-seperator">|</span>
<i class="fa fa-comment"></i> @L["CommentWithCount", post.CommentCount]*@
</div>
</div>
</div>
</div>
}
</div>
</div>
</section>
@ -161,24 +165,26 @@
</p>
<a asp-page="./Detail" asp-route-postUrl="@post.Url" asp-route-blogShortName="@Model.BlogShortName" class="read-more-btn">Continue Reading &#8594;</a>
<div class="article-owner">
<div class="article-infos">
<div class="user-card">
<div class="row">
<div class="col-auto pr-1">
<img gravatar-email="@post.Writer.Email" default-image="Identicon" class="article-avatar" />
</div>
<div class="col pl-1">
<h5 class="mt-2 mb-1">@post.Writer.UserName <span>@ConvertDatetimeToTimeAgo(post.CreationTime)</span></h5>
<i class="fa fa-eye"></i> @L["WiewsWithCount", post.ReadCount]
<span class="vs-seperator">|</span>
<i class="fa fa-comment"></i> @L["CommentWithCount", post.CommentCount]
@if (post.Writer != null)
{
<div class="article-owner">
<div class="article-infos">
<div class="user-card">
<div class="row">
<div class="col-auto pr-1">
<img gravatar-email="@post.Writer.Email" default-image="Identicon" class="article-avatar" />
</div>
<div class="col pl-1">
<h5 class="mt-2 mb-1">@post.Writer.UserName <span>@ConvertDatetimeToTimeAgo(post.CreationTime)</span></h5>
<i class="fa fa-eye"></i> @L["WiewsWithCount", post.ReadCount]
@*<span class="vs-seperator">|</span>
<i class="fa fa-comment"></i> @L["CommentWithCount", post.CommentCount]*@
</div>
</div>
</div>
</div>
</div>
</div>
}
<p class="tags">
@foreach (var tag in post.Tags)
{
@ -202,9 +208,9 @@
</div>
<div class="tags">
@foreach (var popularTag in Model.PopularTags)
{
<a class="tag" asp-page="/Blogs/Posts/Index" asp-route-blogShortName="@Model.BlogShortName" asp-route-tagName="@popularTag.Name">@popularTag.Name <small>(@popularTag.UsageCount)</small></a>
}
{
<a class="tag" asp-page="/Blogs/Posts/Index" asp-route-blogShortName="@Model.BlogShortName" asp-route-tagName="@popularTag.Name">@popularTag.Name <small>(@popularTag.UsageCount)</small></a>
}
</div>
</div>
}

2
modules/client-simulation/demo/Volo.ClientSimulation.Demo/ClientSimulationDemoModule.cs

@ -42,7 +42,7 @@ namespace Volo.ClientSimulation.Demo
app.UseVirtualFiles();
app.UseRouting();
app.UseMvcWithDefaultRouteAndArea();
app.UseConfiguredEndpoints();
}
}
}

2
modules/docs/app/VoloDocs.Web/VoloDocsWebModule.cs

@ -167,7 +167,7 @@ namespace VoloDocs.Web
//app.UseMiddleware<GlobalExceptionHandlerMiddleware>();
app.UseMvcWithDefaultRouteAndArea();
app.UseConfiguredEndpoints();
using (var scope = context.ServiceProvider.CreateScope())
{

4
modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo.Abp.IdentityServer.Domain.csproj

@ -25,8 +25,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="IdentityServer4" Version="3.0.2" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="3.0.2" />
<PackageReference Include="IdentityServer4" Version="3.1.3" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="3.1.3" />
</ItemGroup>
</Project>

2
npm/lerna.json

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

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

@ -1,5 +1,5 @@
{
"version": "2.6.2",
"version": "2.7.0",
"packages": [
"packages/*"
],

12
npm/ng-packs/package.json

@ -24,18 +24,18 @@
"devDependencies": {
"@abp/ng.account": "~2.6.2",
"@abp/ng.account.config": "~2.6.2",
"@abp/ng.core": "~2.6.2",
"@abp/ng.feature-management": "~2.6.2",
"@abp/ng.core": "^2.6.2",
"@abp/ng.feature-management": "^2.6.2",
"@abp/ng.identity": "~2.6.2",
"@abp/ng.identity.config": "~2.6.2",
"@abp/ng.permission-management": "~2.6.2",
"@abp/ng.permission-management": "^2.6.2",
"@abp/ng.setting-management": "~2.6.2",
"@abp/ng.setting-management.config": "~2.6.2",
"@abp/ng.tenant-management": "~2.6.2",
"@abp/ng.tenant-management.config": "~2.6.2",
"@abp/ng.theme.basic": "~2.6.2",
"@abp/ng.theme.shared": "~2.6.2",
"@abp/utils": "~2.6.2",
"@abp/ng.theme.shared": "^2.6.2",
"@abp/utils": "^2.6.0",
"@angular-builders/jest": "^8.2.0",
"@angular-devkit/build-angular": "~0.803.21",
"@angular-devkit/build-ng-packagr": "~0.803.21",
@ -54,7 +54,7 @@
"@fortawesome/fontawesome-free": "^5.12.1",
"@ng-bootstrap/ng-bootstrap": "^5.3.0",
"@ngneat/spectator": "^4.5.0",
"@ngx-validate/core": "^0.0.8",
"@ngx-validate/core": "^0.0.7",
"@ngxs/devtools-plugin": "^3.5.1",
"@ngxs/logger-plugin": "^3.5.1",
"@ngxs/router-plugin": "^3.6.2",

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

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

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

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

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

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

2
npm/ng-packs/packages/core/src/lib/states/config.state.ts

@ -324,7 +324,7 @@ function patchRouteDeep(
): ABP.FullRoute[] {
routes = routes.map(route => {
if (route.name === name) {
newValue.url = `${parentUrl}/${(!newValue.path && newValue.path === ''
newValue.url = `${parentUrl}/${(!newValue.path || newValue.path === ''
? route.path
: newValue.path) || ''}`;

4
npm/ng-packs/packages/feature-management/package.json

@ -1,13 +1,13 @@
{
"name": "@abp/ng.feature-management",
"version": "2.6.2",
"version": "2.7.0",
"homepage": "https://abp.io",
"repository": {
"type": "git",
"url": "https://github.com/abpframework/abp.git"
},
"dependencies": {
"@abp/ng.theme.shared": "^2.6.2"
"@abp/ng.theme.shared": "^2.7.0"
},
"publishConfig": {
"access": "public"

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

@ -1,6 +1,6 @@
{
"name": "@abp/ng.identity.config",
"version": "2.6.2",
"version": "2.7.0",
"homepage": "https://abp.io",
"repository": {
"type": "git",

8
npm/ng-packs/packages/identity/package.json

@ -1,15 +1,15 @@
{
"name": "@abp/ng.identity",
"version": "2.6.2",
"version": "2.7.0",
"homepage": "https://abp.io",
"repository": {
"type": "git",
"url": "https://github.com/abpframework/abp.git"
},
"dependencies": {
"@abp/ng.identity.config": "^2.6.2",
"@abp/ng.permission-management": "^2.6.2",
"@abp/ng.theme.shared": "^2.6.2"
"@abp/ng.identity.config": "^2.7.0",
"@abp/ng.permission-management": "^2.7.0",
"@abp/ng.theme.shared": "^2.7.0"
},
"publishConfig": {
"access": "public"

4
npm/ng-packs/packages/permission-management/package.json

@ -1,13 +1,13 @@
{
"name": "@abp/ng.permission-management",
"version": "2.6.2",
"version": "2.7.0",
"homepage": "https://abp.io",
"repository": {
"type": "git",
"url": "https://github.com/abpframework/abp.git"
},
"dependencies": {
"@abp/ng.theme.shared": "^2.6.2"
"@abp/ng.theme.shared": "^2.7.0"
},
"publishConfig": {
"access": "public"

2
npm/ng-packs/packages/setting-management-config/package.json

@ -1,6 +1,6 @@
{
"name": "@abp/ng.setting-management.config",
"version": "2.6.2",
"version": "2.7.0",
"homepage": "https://abp.io",
"repository": {
"type": "git",

6
npm/ng-packs/packages/setting-management/package.json

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

2
npm/ng-packs/packages/tenant-management-config/package.json

@ -1,6 +1,6 @@
{
"name": "@abp/ng.tenant-management.config",
"version": "2.6.2",
"version": "2.7.0",
"homepage": "https://abp.io",
"repository": {
"type": "git",

8
npm/ng-packs/packages/tenant-management/package.json

@ -1,15 +1,15 @@
{
"name": "@abp/ng.tenant-management",
"version": "2.6.2",
"version": "2.7.0",
"homepage": "https://abp.io",
"repository": {
"type": "git",
"url": "https://github.com/abpframework/abp.git"
},
"dependencies": {
"@abp/ng.feature-management": "^2.6.2",
"@abp/ng.tenant-management.config": "^2.6.2",
"@abp/ng.theme.shared": "^2.6.2"
"@abp/ng.feature-management": "^2.7.0",
"@abp/ng.tenant-management.config": "^2.7.0",
"@abp/ng.theme.shared": "^2.7.0"
},
"publishConfig": {
"access": "public"

4
npm/ng-packs/packages/theme-basic/package.json

@ -1,13 +1,13 @@
{
"name": "@abp/ng.theme.basic",
"version": "2.6.2",
"version": "2.7.0",
"homepage": "https://abp.io",
"repository": {
"type": "git",
"url": "https://github.com/abpframework/abp.git"
},
"dependencies": {
"@abp/ng.theme.shared": "^2.6.2"
"@abp/ng.theme.shared": "^2.7.0"
},
"publishConfig": {
"access": "public"

4
npm/ng-packs/packages/theme-shared/package.json

@ -1,13 +1,13 @@
{
"name": "@abp/ng.theme.shared",
"version": "2.6.2",
"version": "2.7.0",
"homepage": "https://abp.io",
"repository": {
"type": "git",
"url": "https://github.com/abpframework/abp.git"
},
"dependencies": {
"@abp/ng.core": "^2.6.2",
"@abp/ng.core": "^2.7.0",
"@fortawesome/fontawesome-free": "^5.12.1",
"@ng-bootstrap/ng-bootstrap": "^5.3.0",
"@ngx-validate/core": "^0.0.7",

19
npm/ng-packs/yarn.lock

@ -18,7 +18,7 @@
"@abp/ng.theme.shared" "^2.6.2"
tslib "^1.9.0"
"@abp/ng.core@^2.6.2", "@abp/ng.core@~2.6.2":
"@abp/ng.core@^2.6.2":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@abp/ng.core/-/ng.core-2.6.2.tgz#d0e4f6666a9a207fc7425001449bea2cdfe93522"
integrity sha512-e7c82d9VF20d4JFUkyzT5iTqGKnkq5FBHdv6IW6lsVyuz2Zf/p4wNtKATRr2ntM7qNIjIQebtGNFyxxX4nmL+w==
@ -35,7 +35,7 @@
ts-toolbelt "^6.3.6"
tslib "^1.9.0"
"@abp/ng.feature-management@^2.6.2", "@abp/ng.feature-management@~2.6.2":
"@abp/ng.feature-management@^2.6.2":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@abp/ng.feature-management/-/ng.feature-management-2.6.2.tgz#da09b07ebe81f09ccf338a7c10c384920375567a"
integrity sha512-JdHuAZk9j9AR1iKU02Xoux40ULOccm+68q7/4L2G1cBET6yTAE8Ua9RPPX9HadmCG0cR8awl3d+5JPwyL3OjFA==
@ -60,7 +60,7 @@
"@abp/ng.theme.shared" "^2.6.2"
tslib "^1.9.0"
"@abp/ng.permission-management@^2.6.2", "@abp/ng.permission-management@~2.6.2":
"@abp/ng.permission-management@^2.6.2":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@abp/ng.permission-management/-/ng.permission-management-2.6.2.tgz#ba7902256acf3e1ff49552983e4d292f670f9ad8"
integrity sha512-6NW5BF+5ZFQI8oLEQWCiJS5iF2v3gHLvohrbN84RsPNTc+ZRDte6MbIOGq2CWxM8qXwisLBiR/RWC4ssgfPqEA==
@ -109,7 +109,7 @@
"@abp/ng.theme.shared" "^2.6.2"
tslib "^1.9.0"
"@abp/ng.theme.shared@^2.6.2", "@abp/ng.theme.shared@~2.6.2":
"@abp/ng.theme.shared@^2.6.2":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@abp/ng.theme.shared/-/ng.theme.shared-2.6.2.tgz#9d70b7aab010af4cd5a451c9c2d162d73ec5f9cc"
integrity sha512-e3QVsPpmbPGmTpQY9J1RgmWer7Tx7mrsOtxjFvbJGqKVF1UUtU5Kc3VyGPb3JsjQtT+890/9qmAf/YSj3cNPIA==
@ -122,7 +122,7 @@
chart.js "^2.9.3"
tslib "^1.9.0"
"@abp/utils@^2.6.0", "@abp/utils@~2.6.2":
"@abp/utils@^2.6.0":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-2.6.2.tgz#dba9e7c0aa1531fae6c5ff22fcbdfb4d98c576e3"
integrity sha512-MqExfPA4FRYORsskVG7kbkAwzs9z4XaMaZ75XkoahD+FyZtm1e0miEEoDvwrdye9jgL/FrWeShWNE49GY7PvpA==
@ -2129,7 +2129,14 @@
tree-kill "1.2.2"
webpack-sources "1.4.3"
"@ngx-validate/core@^0.0.7", "@ngx-validate/core@^0.0.8":
"@ngx-validate/core@^0.0.7":
version "0.0.7"
resolved "https://registry.yarnpkg.com/@ngx-validate/core/-/core-0.0.7.tgz#35a4364e8c8bb43ce1731e8d681ce1ab307c5955"
integrity sha512-APT7kstDaJ0JkC6cDkypbVSXAOAEyPTQ9P8DjEI1szi+Xzuz69XLASK3NdTYjFwogWevhYXoYRaR2Bq9iWzl+g==
dependencies:
tslib "^1.9.0"
"@ngx-validate/core@^0.0.8":
version "0.0.8"
resolved "https://registry.yarnpkg.com/@ngx-validate/core/-/core-0.0.8.tgz#8577405eb1af0f5002cdda7a86fbcda56280f116"
integrity sha512-caIG5ao76Xhf7T+pNA8crnpAwK0yqj3i0OAUGZRUq1W+kNgz+ZnrSd4F9an/W4g+38u/8gfUVvsfsm07ju6qYA==

6
npm/packs/anchor-js/package.json

@ -1,12 +1,12 @@
{
"version": "2.6.2",
"version": "2.7.0",
"name": "@abp/anchor-js",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@abp/core": "^2.6.2",
"@abp/core": "^2.7.0",
"anchor-js": "^4.2.2"
},
"gitHead": "eb4a0e507f492e275865b72caeb02ce28d69ae56"
"gitHead": "0ea3895f3b0b489e3ea81fc88f8f0896b22b61bd"
}

6
npm/packs/aspnetcore.mvc.ui.theme.basic/package.json

@ -1,11 +1,11 @@
{
"version": "2.6.2",
"version": "2.7.0",
"name": "@abp/aspnetcore.mvc.ui.theme.basic",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@abp/aspnetcore.mvc.ui.theme.shared": "^2.6.2"
"@abp/aspnetcore.mvc.ui.theme.shared": "^2.7.0"
},
"gitHead": "eb4a0e507f492e275865b72caeb02ce28d69ae56"
"gitHead": "0ea3895f3b0b489e3ea81fc88f8f0896b22b61bd"
}

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

Loading…
Cancel
Save