diff --git a/common.props b/common.props index 19b6f81fe6..ab7b86e3b2 100644 --- a/common.props +++ b/common.props @@ -1,7 +1,7 @@ latest - 2.7.0 + 2.8.0 $(NoWarn);CS1591 https://abp.io/assets/abp_nupkg.png https://abp.io diff --git a/docs/cs/Getting-Started-AspNetCore-Application.md b/docs/cs/Getting-Started-AspNetCore-Application.md index 2e8ea1a343..877976d037 100644 --- a/docs/cs/Getting-Started-AspNetCore-Application.md +++ b/docs/cs/Getting-Started-AspNetCore-Application.md @@ -57,7 +57,7 @@ namespace BasicAspNetCoreApplication app.UseStaticFiles(); app.UseRouting(); - app.UseMvcWithDefaultRouteAndArea(); + app.UseConfiguredEndpoints(); } } } diff --git a/docs/en/API/Dynamic-CSharp-API-Clients.md b/docs/en/API/Dynamic-CSharp-API-Clients.md index 460623440f..94a0222243 100644 --- a/docs/en/API/Dynamic-CSharp-API-Clients.md +++ b/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 ); ```` diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/Post.md b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/Post.md new file mode 100644 index 0000000000..b6c1d3f952 --- /dev/null +++ b/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(options => + { + options.AddOrUpdateProperty("SocialSecurityNumber"); + options.AddOrUpdateProperty("IsSuperUser"); + } + ); +```` + +It is easy to define validation rules for the properties: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "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( + "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 +

{{L "PasswordReset"}}

+ +

{{L "PasswordResetInfoInEmail"}}

+ + +```` + +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 + + + + + + + {{content}} + + +```` + +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( //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. \ No newline at end of file diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/abp-commercial-ui-extensions.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/abp-commercial-ui-extensions.png new file mode 100644 index 0000000000..52fe37a712 Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/abp-commercial-ui-extensions.png differ diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/audit-log-entity-changes.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/audit-log-entity-changes.png new file mode 100644 index 0000000000..e06b3300ee Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/audit-log-entity-changes.png differ diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/easy-crm.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/easy-crm.png new file mode 100644 index 0000000000..e40f399525 Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/easy-crm.png differ diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/github-contribution-graph.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/github-contribution-graph.png new file mode 100644 index 0000000000..ff3db2ad22 Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/github-contribution-graph.png differ diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/module-entity-extended-ui.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/module-entity-extended-ui.png new file mode 100644 index 0000000000..25621aabb7 Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/module-entity-extended-ui.png differ diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/tenant-entity-changes.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/tenant-entity-changes.png new file mode 100644 index 0000000000..4cd573c588 Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/tenant-entity-changes.png differ diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/text-template-content-ui.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/text-template-content-ui.png new file mode 100644 index 0000000000..037e3e4d8b Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/text-template-content-ui.png differ diff --git a/docs/en/Customizing-Application-Modules-Overriding-Services.md b/docs/en/Customizing-Application-Modules-Overriding-Services.md index 7f0c0502ea..b9890b3aca 100644 --- a/docs/en/Customizing-Application-Modules-Overriding-Services.md +++ b/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 optionsAccessor, - IPasswordHasher passwordHasher, - IEnumerable> userValidators, - IEnumerable> passwordValidators, - ILookupNormalizer keyNormalizer, - IdentityErrorDescriber errors, - IServiceProvider services, - ILogger logger, - ICancellationTokenProvider cancellationTokenProvider - ) : base( - store, - optionsAccessor, - passwordHasher, - userValidators, - passwordValidators, - keyNormalizer, - errors, - services, - logger, - cancellationTokenProvider) - { - } + public MyIdentityUserManager( + IdentityUserStore store, + IIdentityRoleRepository roleRepository, + IIdentityUserRepository userRepository, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IServiceProvider services, + ILogger logger, + ICancellationTokenProvider cancellationTokenProvider) : + base(store, + roleRepository, + userRepository, + optionsAccessor, + passwordHasher, + userValidators, + passwordValidators, + keyNormalizer, + errors, + services, + logger, + cancellationTokenProvider) + { + } public override async Task CreateAsync(IdentityUser user) { diff --git a/docs/en/Getting-Started-AspNetCore-Application.md b/docs/en/Getting-Started-AspNetCore-Application.md index b78ce1b9cf..9698b1dfb0 100644 --- a/docs/en/Getting-Started-AspNetCore-Application.md +++ b/docs/en/Getting-Started-AspNetCore-Application.md @@ -57,7 +57,7 @@ namespace BasicAspNetCoreApplication app.UseStaticFiles(); app.UseRouting(); - app.UseMvcWithDefaultRouteAndArea(); + app.UseConfiguredEndpoints(); } } } diff --git a/docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md b/docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md index 10b1e05a4a..434d5f8bac 100644 --- a/docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md +++ b/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 domain like `https://localhost:44320/signin-azuread-oidc`, not only `/signin-azuread-oidc`. + your **Redirect URI** of your application in azure portal must be with domain 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). diff --git a/docs/en/Samples/Microservice-Demo.md b/docs/en/Samples/Microservice-Demo.md index 567d043ef9..25eaf68055 100644 --- a/docs/en/Samples/Microservice-Demo.md +++ b/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(); diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md new file mode 100644 index 0000000000..efde511323 --- /dev/null +++ b/docs/en/Text-Templating.md @@ -0,0 +1,3 @@ +# Text-Templating + +TODO \ No newline at end of file diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md index c684d1fc1c..be812cc162 100644 --- a/docs/en/Tutorials/Part-1.md +++ b/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 - -``` - -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; } -@State({ - name: 'BooksState', - defaults: { book: {} } as BooksStateModel, +@State({ + 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) { + get(ctx: StateContext) { 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; 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
diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md index cfe9936812..69f3e5436b 100644 --- a/docs/en/Tutorials/Part-2.md +++ b/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; } -@State({ - name: 'BooksState', - defaults: { book: {} } as BooksStateModel, +@State({ + 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) { + get(ctx: StateContext) { 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, action: CreateUpdateBook) { + save(ctx: StateContext, 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; 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; booksType = BookType; @@ -774,42 +774,40 @@ Open `book-list.component.html` in `app\books\book-list` folder and replace ` @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; 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; 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 @@ -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, action: CreateUpdateBook) { +save(ctx: StateContext, action: CreateUpdateBook) { if (action.id) { return this.bookService.updateByIdAndInput(action.payload, action.id); } else { @@ -1033,19 +1030,19 @@ save(ctx: StateContext, 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; 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 `
` tag as below: +Open the `book-list.component.html` in `app\book\book-list` folder and replace the `
` tag as below: ```html
@@ -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 `` tag and replace the content as below. +Open `book-list.component.html` in `app\book\book-list` folder and find the `` tag and replace the content as below. ```html @@ -1213,45 +1210,45 @@ Open `book-list.component.html` in `app\books\book-list` folder and find the ` -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; } -@State({ - name: 'BooksState', - defaults: { book: {} } as BooksStateModel, +@State({ + 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) { + get(ctx: StateContext) { return this.bookService.getListByInput().pipe( tap((booksResponse) => { ctx.patchState({ @@ -1262,7 +1259,7 @@ export class BooksState { } @Action(CreateUpdateBook) - save(ctx: StateContext, action: CreateUpdateBook) { + save(ctx: StateContext, 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, action: DeleteBook) { + delete(ctx: StateContext, 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
diff --git a/docs/en/Tutorials/images/bookstore-angular-file-tree.png b/docs/en/Tutorials/images/bookstore-angular-file-tree.png index a3197b6457..ffa8dcd7e2 100644 Binary files a/docs/en/Tutorials/images/bookstore-angular-file-tree.png and b/docs/en/Tutorials/images/bookstore-angular-file-tree.png differ diff --git a/docs/en/Tutorials/images/bookstore-book-list.png b/docs/en/Tutorials/images/bookstore-book-list.png index 9e6cc9e010..d402895c9b 100644 Binary files a/docs/en/Tutorials/images/bookstore-book-list.png and b/docs/en/Tutorials/images/bookstore-book-list.png differ diff --git a/docs/en/Tutorials/images/bookstore-creating-book-list-terminal.png b/docs/en/Tutorials/images/bookstore-creating-book-list-terminal.png index 6f19dcc7bf..13829db60f 100644 Binary files a/docs/en/Tutorials/images/bookstore-creating-book-list-terminal.png and b/docs/en/Tutorials/images/bookstore-creating-book-list-terminal.png differ diff --git a/docs/en/Tutorials/images/bookstore-creating-book-module-terminal.png b/docs/en/Tutorials/images/bookstore-creating-book-module-terminal.png new file mode 100644 index 0000000000..c935a6f130 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-creating-book-module-terminal.png differ diff --git a/docs/en/Tutorials/images/bookstore-creating-books-module-terminal.png b/docs/en/Tutorials/images/bookstore-creating-books-module-terminal.png deleted file mode 100644 index ec9ef4c42f..0000000000 Binary files a/docs/en/Tutorials/images/bookstore-creating-books-module-terminal.png and /dev/null differ diff --git a/docs/en/Tutorials/images/generated-proxies.png b/docs/en/Tutorials/images/generated-proxies.png index 9e466e7d55..1d322c0765 100644 Binary files a/docs/en/Tutorials/images/generated-proxies.png and b/docs/en/Tutorials/images/generated-proxies.png differ diff --git a/docs/zh-Hans/API/Dynamic-CSharp-API-Clients.md b/docs/zh-Hans/API/Dynamic-CSharp-API-Clients.md index eae86d6117..82473f9bbd 100644 --- a/docs/zh-Hans/API/Dynamic-CSharp-API-Clients.md +++ b/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 ); ```` diff --git a/docs/zh-Hans/Customizing-Application-Modules-Guide.md b/docs/zh-Hans/Customizing-Application-Modules-Guide.md index 7e799c885d..bf3404ddb7 100644 --- a/docs/zh-Hans/Customizing-Application-Modules-Guide.md +++ b/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修复**. 然而有一个缺点: diff --git a/docs/zh-Hans/Entity-Framework-Core-Migrations.md b/docs/zh-Hans/Entity-Framework-Core-Migrations.md index caf5778753..77f5ac5cd7 100644 --- a/docs/zh-Hans/Entity-Framework-Core-Migrations.md +++ b/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` 字段的副本. diff --git a/docs/zh-Hans/Getting-Started.md b/docs/zh-Hans/Getting-Started.md index b60fccfaa2..1b156b070d 100644 --- a/docs/zh-Hans/Getting-Started.md +++ b/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 }} diff --git a/docs/zh-Hans/Samples/Microservice-Demo.md b/docs/zh-Hans/Samples/Microservice-Demo.md index 40cb42cda6..b0134eddde 100644 --- a/docs/zh-Hans/Samples/Microservice-Demo.md +++ b/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(); diff --git a/docs/zh-Hans/Tutorials/Angular/Part-I.md b/docs/zh-Hans/Tutorials/Angular/Part-I.md index 8e8c74ec59..e981259db8 100644 --- a/docs/zh-Hans/Tutorials/Angular/Part-I.md +++ b/docs/zh-Hans/Tutorials/Angular/Part-I.md @@ -1,3 +1,8 @@ -## Angular 教程 - 第一章 +# 教程 -TODO... \ No newline at end of file +## 应用程序开发 + +* [ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) +* [Angular UI](../Part-1?UI=NG) + + \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md index 7c2db90997..e981259db8 100644 --- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md +++ b/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 - { - 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 -{ - public DbSet Books { get; set; } - ... -} -```` - -#### 配置你的Book实体 - -在`Acme.BookStore.EntityFrameworkCore`项目中打开`BookStoreDbContextModelCreatingExtensions.cs`文件,并将以下代码添加到`ConfigureBookStore`方法的末尾以配置Book实体: - -````C# -builder.Entity(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 - { - 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`.跟上面定义的`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(); - } - } -} -```` - -#### 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(); -```` - -#### 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> //用于更新书籍 - { - - } -} -```` - -* 框架定义应用程序服务的接口不是必需的. 但是,它被建议作为最佳实践. -* `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, - IBookAppService - { - public BookAppService(IRepository repository) - : base(repository) - { - - } - } -} -```` - -* `BookAppService`继承了`CrudAppService<...>`.它实现了上面定义的CRUD方法. -* `BookAppService`注入`IRepository `,这是`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 - -

Books

-```` - -* 此代码更改了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 -{ - -} - - -

@L["Books"]

-
- - - - - @L["Name"] - @L["Type"] - @L["PublishDate"] - @L["Price"] - @L["CreationTime"] - - - - -
-```` - -* `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) 的介绍. + \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md index e072e5a1a7..e981259db8 100644 --- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md +++ b/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 _bookRepository; - private readonly IGuidGenerator _guidGenerator; - - public BookStoreTestDataSeedContributor( - IRepository 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`并在`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(); - } - - [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(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) - -恭喜, 绿色图标表示测试已成功通过! + \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/Part-1.md b/docs/zh-Hans/Tutorials/Part-1.md index 43590faf2b..9589c430b6 100644 --- a/docs/zh-Hans/Tutorials/Part-1.md +++ b/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 { - public DbSet Books { get; set; } + public DbSet Book { get; set; } ... } ```` @@ -194,13 +194,13 @@ public class BookStoreDbContext : AbpDbContext {{if DB == "mongodb"}} -添加 `IMongoCollection Books` 属性到 `Acme.BookStore.MongoDB` 项目的 `BookStoreMongoDbContext` 中. +添加 `IMongoCollection Book` 属性到 `Acme.BookStore.MongoDB` 项目的 `BookStoreMongoDbContext` 中. ```csharp public class BookStoreMongoDbContext : AbpMongoDbContext { public IMongoCollection Users => Collection(); - public IMongoCollection Books => Collection();//<--added this line--> + public IMongoCollection Book => Collection();//<--added this line--> //... } ``` @@ -216,7 +216,7 @@ public class BookStoreMongoDbContext : AbpMongoDbContext ````csharp builder.Entity(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 -

Books

+

Book

```` * 此代码更改了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 { - + } -

@L["Books"]

+

@L["Book"]

- + @L["Name"] @@ -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 - -``` - 在命令行运行以下命令,生成名为 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; } -@State({ - name: 'BooksState', - defaults: { book: {} } as BooksStateModel, +@State({ + 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) { + get(ctx: StateContext) { 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; 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
@@ -1023,7 +1008,7 @@ export class BookListComponent implements OnInit {
- {%{{{ "::Menu:Books" | abpLocalization }}}%} + {%{{{ "::Menu:Book" | abpLocalization }}}%}
@@ -1031,7 +1016,7 @@ export class BookListComponent implements OnInit {
{%{{{ data.name }}}%} - {%{{{ booksType[data.type] }}}%} + {%{{{ bookType[data.type] }}}%} {%{{{ data.publishDate | date }}}%} {%{{{ data.price }}}%} @@ -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). diff --git a/docs/zh-Hans/Tutorials/Part-2.md b/docs/zh-Hans/Tutorials/Part-2.md index 56dadcbbcd..5e73c753e6 100644 --- a/docs/zh-Hans/Tutorials/Part-2.md +++ b/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; } -@State({ - name: 'BooksState', - defaults: { book: {} } as BooksStateModel, +@State({ + 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) { + get(ctx: StateContext) { 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, action: CreateUpdateBook) { + save(ctx: StateContext, 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
@@ -556,7 +556,7 @@ export class BooksState {
{%{{{ data.name }}}%} - {%{{{ booksType[data.type] }}}%} + {%{{{ bookType[data.type] }}}%} {%{{{ data.publishDate | date }}}%} {%{{{ data.price }}}%} @@ -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; 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; booksType = BookType; @@ -729,7 +729,7 @@ export class BookListComponent implements OnInit { #### 创建表单的DOM元素 -打开 `app\books\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 ` `: +打开 `app\app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 ` `: ```html @@ -748,7 +748,7 @@ export class BookListComponent implements OnInit { *
@@ -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; 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; 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 @@ -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, action: CreateUpdateBook) { +save(ctx: StateContext, action: CreateUpdateBook) { if (action.id) { return this.bookService.updateByIdAndInput(action.payload, action.id); } else { @@ -1029,19 +1028,19 @@ save(ctx: StateContext, 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; + @Select(BookState.getBooks) + book$: Observable; - 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` 文件,使用以下内容替换 `
` 标签: +打开 `app\app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `
` 标签: ```html
{%{{{ data.name }}}%} - {%{{{ booksType[data.type] }}}%} + {%{{{ bookType[data.type] }}}%} {%{{{ data.publishDate | date }}}%} {%{{{ data.price }}}%} @@ -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` 文件,使用以下内容替换 `` 标签: +打开 `app\app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `` 标签: ```html @@ -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; } -@State({ - name: 'BooksState', - defaults: { book: {} } as BooksStateModel, +@State({ + 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) { + get(ctx: StateContext) { return this.bookService.getListByInput().pipe( tap((booksResponse) => { ctx.patchState({ @@ -1258,7 +1257,7 @@ export class BooksState { } @Action(CreateUpdateBook) - save(ctx: StateContext, action: CreateUpdateBook) { + save(ctx: StateContext, 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, action: DeleteBook) { + delete(ctx: StateContext, 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
@@ -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}} diff --git a/docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png b/docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png index a3197b6457..ffa8dcd7e2 100644 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png and b/docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-book-list.png b/docs/zh-Hans/Tutorials/images/bookstore-book-list.png index 9e6cc9e010..d402895c9b 100644 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-book-list.png and b/docs/zh-Hans/Tutorials/images/bookstore-book-list.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-creating-book-list-terminal.png b/docs/zh-Hans/Tutorials/images/bookstore-creating-book-list-terminal.png index 6f19dcc7bf..13829db60f 100644 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-creating-book-list-terminal.png and b/docs/zh-Hans/Tutorials/images/bookstore-creating-book-list-terminal.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-creating-book-module-terminal.png b/docs/zh-Hans/Tutorials/images/bookstore-creating-book-module-terminal.png new file mode 100644 index 0000000000..c935a6f130 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-creating-book-module-terminal.png differ diff --git a/docs/zh-Hans/Tutorials/images/generated-proxies.png b/docs/zh-Hans/Tutorials/images/generated-proxies.png index 9e466e7d55..1d322c0765 100644 Binary files a/docs/zh-Hans/Tutorials/images/generated-proxies.png and b/docs/zh-Hans/Tutorials/images/generated-proxies.png differ diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json index c70284637d..9f7d69a784 100644 --- a/docs/zh-Hans/docs-nav.json +++ b/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" } ] } diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index 5d6f3e99a2..3ff151439f 100644 --- a/framework/Volo.Abp.sln +++ b/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} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/SignalR/SignalRBrowserScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/SignalR/SignalRBrowserScriptContributor.cs new file mode 100644 index 0000000000..736ffdb986 --- /dev/null +++ b/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"); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js index 3998f2df7f..21a8e0ce21 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js +++ b/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); \ No newline at end of file +})(jQuery); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Builder/AbpAspNetCoreMvcApplicationBuilderExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Builder/AbpAspNetCoreMvcApplicationBuilderExtensions.cs index 9a19337c5f..daa5183709 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Builder/AbpAspNetCoreMvcApplicationBuilderExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Builder/AbpAspNetCoreMvcApplicationBuilderExtensions.cs @@ -15,18 +15,12 @@ namespace Microsoft.AspNetCore.Builder /// The . /// Additional action to configure routes /// A reference to this instance after the operation has completed. + [Obsolete("Use app.UseConfiguredEndpoints(...) extension method instead!")] public static IApplicationBuilder UseMvcWithDefaultRouteAndArea( this IApplicationBuilder app, Action 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); } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs index 00ba8423b3..ee81e0a22e 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs +++ b/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(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) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpController.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpController.cs index c3e5c61f36..2a3a25373e 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpController.cs +++ b/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 _lazyLogger => new Lazy(() => 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 _lazyLogger => new Lazy(() => LoggerFactory?.CreateLogger(GetType().FullName) ?? NullLogger.Instance, true); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DependencyInjection/AbpAspNetCoreMvcConventionalDependencyRegistrar.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DependencyInjection/AbpAspNetCoreMvcConventionalDependencyRegistrar.cs index 526470b3ad..2ef536db4d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DependencyInjection/AbpAspNetCoreMvcConventionalDependencyRegistrar.cs +++ b/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 + ) + ); } } diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/FodyWeavers.xml b/framework/src/Volo.Abp.AspNetCore.SignalR/FodyWeavers.xml new file mode 100644 index 0000000000..be0de3a908 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.SignalR/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/FodyWeavers.xsd b/framework/src/Volo.Abp.AspNetCore.SignalR/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.SignalR/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Properties/launchSettings.json b/framework/src/Volo.Abp.AspNetCore.SignalR/Properties/launchSettings.json new file mode 100644 index 0000000000..418c7f2621 --- /dev/null +++ b/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" + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo.Abp.AspNetCore.SignalR.csproj b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo.Abp.AspNetCore.SignalR.csproj new file mode 100644 index 0000000000..f4c833cb90 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo.Abp.AspNetCore.SignalR.csproj @@ -0,0 +1,23 @@ + + + + + + + netcoreapp3.1 + Volo.Abp.AspNetCore.SignalR + Volo.Abp.AspNetCore.SignalR + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + true + Library + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRModule.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRModule.cs new file mode 100644 index 0000000000..e26de184af --- /dev/null +++ b/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(options => + { + options.EndpointConfigureActions.Add(endpointContext => + { + var signalROptions = endpointContext + .ScopeServiceProvider + .GetRequiredService>() + .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(); + + services.OnRegistred(context => + { + if (typeof(Hub).IsAssignableFrom(context.ImplementationType)) + { + hubTypes.Add(context.ImplementationType); + } + }); + + services.Configure(options => + { + foreach (var hubType in hubTypes) + { + options.Hubs.Add(HubConfig.Create(hubType)); + } + }); + } + + private void MapHubType( + Type hubType, + IEndpointRouteBuilder endpoints, + string pattern, + Action configureOptions) + { + MapHubGenericMethodInfo + .MakeGenericMethod(hubType) + .Invoke( + this, + new object[] + { + endpoints, + pattern, + configureOptions + } + ); + } + + // ReSharper disable once UnusedMember.Local (used via reflection) + private static void MapHub( + IEndpointRouteBuilder endpoints, + string pattern, + Action configureOptions) + where THub : Hub + { + endpoints.MapHub( + pattern, + configureOptions + ); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpHub.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpHub.cs new file mode 100644 index 0000000000..a1dc1437ea --- /dev/null +++ b/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(ref TService reference) + => LazyGetRequiredService(typeof(TService), ref reference); + + protected TRef LazyGetRequiredService(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 _lazyLogger => new Lazy(() => 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 : Hub + where T : class + { + public IServiceProvider ServiceProvider { get; set; } + protected readonly object ServiceProviderLock = new object(); + + protected TService LazyGetRequiredService(ref TService reference) + => LazyGetRequiredService(typeof(TService), ref reference); + + protected TRef LazyGetRequiredService(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 _lazyLogger => new Lazy(() => 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); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalRConventionalRegistrar.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalRConventionalRegistrar.cs new file mode 100644 index 0000000000..9dcb46d6bc --- /dev/null +++ b/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); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions.cs new file mode 100644 index 0000000000..c0a07a2ea3 --- /dev/null +++ b/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 Hubs { get; } + + public AbpSignalROptions() + { + Hubs = new List(); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalRUserIdProvider.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalRUserIdProvider.cs new file mode 100644 index 0000000000..10d986933d --- /dev/null +++ b/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(); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfig.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfig.cs new file mode 100644 index 0000000000..59c9342fce --- /dev/null +++ b/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> 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>(); + } + + public static HubConfig Create() + where THub : Hub + { + return Create(typeof(THub)); + } + + public static HubConfig Create(Type hubType) + { + return new HubConfig( + hubType, + HubRouteAttribute.GetRoutePattern(hubType) + ); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubRouteAttribute.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubRouteAttribute.cs new file mode 100644 index 0000000000..5de916a89d --- /dev/null +++ b/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() + where THub : Hub + { + return GetRoutePattern(typeof(THub)); + } + + public static string GetRoutePattern(Type hubType) + { + var routeAttribute = hubType.GetSingleAttributeOrNull(); + if (routeAttribute != null) + { + return routeAttribute.RoutePattern; + } + + return "/signalr-hubs/" + hubType.Name.RemovePostFix("Hub").ToKebabCase(); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpAspNetCoreApplicationBuilderExtensions.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpAspNetCoreApplicationBuilderExtensions.cs new file mode 100644 index 0000000000..1ce443c37f --- /dev/null +++ b/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 + { + /// + /// Maps endpoints configured with the . + /// It internally uses the standard app.UseEndpoints(...) method. + /// + /// The application builder + /// Additional (and optional) endpoint configuration + /// + public static IApplicationBuilder UseConfiguredEndpoints( + this IApplicationBuilder app, + Action additionalConfigurationAction = null) + { + var options = app.ApplicationServices + .GetRequiredService>() + .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); + } + }); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Routing/AbpEndpointRouterOptions.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Routing/AbpEndpointRouterOptions.cs new file mode 100644 index 0000000000..85c81a7949 --- /dev/null +++ b/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> EndpointConfigureActions { get; } + + public AbpEndpointRouterOptions() + { + EndpointConfigureActions = new List>(); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Routing/EndpointRouteBuilderContext.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Routing/EndpointRouteBuilderContext.cs new file mode 100644 index 0000000000..80f25d2d38 --- /dev/null +++ b/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; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/VirtualFileSystem/WebContentFileProvider.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/VirtualFileSystem/WebContentFileProvider.cs index 4fab6d0001..898eae546f 100644 --- a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/VirtualFileSystem/WebContentFileProvider.cs +++ b/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)); } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateProxyCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateProxyCommand.cs index 5c732f6873..667aa77757 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateProxyCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateProxyCommand.cs @@ -21,6 +21,7 @@ namespace Volo.Abp.Cli.Commands public static Dictionary> propertyList = new Dictionary>(); public ILogger 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"; + } } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs index f7f76cda59..66843bea4f 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs +++ b/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; } } diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/zh-Hant.json b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/zh-Hant.json index a66a6d2ff8..2bf2fa5240 100644 --- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/zh-Hant.json +++ b/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": "電子郵件內的文字訊息" } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionManager.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionManager.cs index 2b0ce8977d..fcc6fddbd0 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionManager.cs +++ b/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 Configuration { get; } diff --git a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkManager.cs b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkManager.cs index 479cf45117..0a1fcc7a9f 100644 --- a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkManager.cs +++ b/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 diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs index 39b7d9af5f..90930abc6d 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs +++ b/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(); } } } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/AbpAspNetCoreMvcUiBootstrapDemoModule.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/AbpAspNetCoreMvcUiBootstrapDemoModule.cs index e14e2336d9..90780d3687 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/AbpAspNetCoreMvcUiBootstrapDemoModule.cs +++ b/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(); } } } \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/AbpAspNetCoreMvcUiThemeBasicDemoModule.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/AbpAspNetCoreMvcUiThemeBasicDemoModule.cs index 758985bb27..9af261e3c8 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/AbpAspNetCoreMvcUiThemeBasicDemoModule.cs +++ b/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(); } } } \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Versioning.Tests/Volo/Abp/AspNetCore/Mvc/Versioning/AbpAspNetCoreMvcVersioningTestModule.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Versioning.Tests/Volo/Abp/AspNetCore/Mvc/Versioning/AbpAspNetCoreMvcVersioningTestModule.cs index b06a90bcfa..4e28d77a0a 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Versioning.Tests/Volo/Abp/AspNetCore/Mvc/Versioning/AbpAspNetCoreMvcVersioningTestModule.cs +++ b/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(); } } } diff --git a/framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo/Abp/AspNetCore/App/AbpSerilogTestModule.cs b/framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo/Abp/AspNetCore/App/AbpSerilogTestModule.cs index 557288cd7d..8c7ed40dc7 100644 --- a/framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo/Abp/AspNetCore/App/AbpSerilogTestModule.cs +++ b/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(); } } } \ No newline at end of file diff --git a/modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs b/modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs index dbaefdc1a5..8d57cb8aee 100644 --- a/modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs +++ b/modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs @@ -158,7 +158,7 @@ namespace Volo.BloggingTestApp app.UseRequestLocalization(app.ApplicationServices.GetRequiredService>().Value); - app.UseMvcWithDefaultRouteAndArea(); + app.UseConfiguredEndpoints(); using (var scope = context.ServiceProvider.CreateScope()) diff --git a/modules/blogging/src/Volo.Blogging.Web/BloggingTwitterOptions.cs b/modules/blogging/src/Volo.Blogging.Web/BloggingTwitterOptions.cs new file mode 100644 index 0000000000..8c1b1fbf7d --- /dev/null +++ b/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; } + } +} diff --git a/modules/blogging/src/Volo.Blogging.Web/BloggingUrlOptions.cs b/modules/blogging/src/Volo.Blogging.Web/BloggingUrlOptions.cs index 0c56d1c566..619585d359 100644 --- a/modules/blogging/src/Volo.Blogging.Web/BloggingUrlOptions.cs +++ b/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 { diff --git a/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Detail.cshtml b/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Detail.cshtml index e674ae1c3d..63cfbf8a8e 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Detail.cshtml +++ b/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 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 { diff --git a/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml b/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml index fe5207cb52..61d9827c47 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml +++ b/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml @@ -59,8 +59,8 @@
@post.Writer.UserName @ConvertDatetimeToTimeAgo(post.CreationTime)
@L["WiewsWithCount", post.ReadCount] - | - @L["CommentWithCount", post.CommentCount] + @*| + @L["CommentWithCount", post.CommentCount]*@
@@ -105,19 +105,23 @@

@post.Title

-
-
@@ -161,24 +165,26 @@

Continue Reading → -
-