@ -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: |
|||
|
|||
 |
|||
|
|||
ABP.IO Platform is rapidly growing and we are getting more and more contributions from the community. |
|||
|
|||
## What's New in the ABP Framework? |
|||
|
|||
### Object Extending System |
|||
|
|||
In the last few releases, we've mostly focused on providing ways to extend existing modules when you use them as NuGet/NPM Packages. |
|||
|
|||
The Object Extending System allows module developers to create extensible modules and allows application developers to customize and extend a module easily. |
|||
|
|||
For example, you can add two extension properties to the user entity of the identity module: |
|||
|
|||
````csharp |
|||
ObjectExtensionManager.Instance |
|||
.AddOrUpdate<IdentityUser>(options => |
|||
{ |
|||
options.AddOrUpdateProperty<string>("SocialSecurityNumber"); |
|||
options.AddOrUpdateProperty<bool>("IsSuperUser"); |
|||
} |
|||
); |
|||
```` |
|||
|
|||
It is easy to define validation rules for the properties: |
|||
|
|||
````csharp |
|||
ObjectExtensionManager.Instance |
|||
.AddOrUpdateProperty<IdentityUserCreateDto, string>( |
|||
"SocialSecurityNumber", |
|||
options => |
|||
{ |
|||
options.Attributes.Add(new RequiredAttribute()); |
|||
options.Attributes.Add( |
|||
new StringLengthAttribute(32) { |
|||
MinimumLength = 6 |
|||
} |
|||
); |
|||
}); |
|||
```` |
|||
|
|||
You can even write custom code to validate the property. It automatically works for the objects those are parameters of an application service, controller or a page. |
|||
|
|||
While extension properties of an entity are normally stored in a single JSON formatted field in the database table, you can easily configure to store a property as a table field using the EF Core mapping: |
|||
|
|||
````csharp |
|||
ObjectExtensionManager.Instance |
|||
.AddOrUpdateProperty<IdentityUser, string>( |
|||
"SocialSecurityNumber", |
|||
options => |
|||
{ |
|||
options.MapEfCore(b => b.HasMaxLength(32)); |
|||
} |
|||
); |
|||
```` |
|||
|
|||
See the [Object Extensions document](https://docs.abp.io/en/abp/latest/Object-Extensions) for details about this system. |
|||
|
|||
See also the [Customizing the Existing Modules](https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Guide) guide to learn all the possible customization options. |
|||
|
|||
### Text Templating Package |
|||
|
|||
[Volo.Abp.TextTemplating](https://www.nuget.org/packages/Volo.Abp.TextTemplating) is a new package introduced with the v2.7.0. Previously, [Volo.Abp.Emailing](https://www.nuget.org/packages/Volo.Abp.Emailing) package had a similar functionality but it was limited, experimental and tightly coupled to the emailing. |
|||
|
|||
The new text templating package allows you to define text based templates those can be easily localized and reused. You can define layout templates and share the layout from other templates. |
|||
|
|||
We are currently using it for email sending. A module needs to send an email typically defines a template. Example: |
|||
|
|||
````xml |
|||
<h3>{{L "PasswordReset"}}</h3> |
|||
|
|||
<p>{{L "PasswordResetInfoInEmail"}}</p> |
|||
|
|||
<div> |
|||
<a href="{{model.link}}">{{L "ResetMyPassword"}}</a> |
|||
</div> |
|||
```` |
|||
|
|||
This is a typical password reset email template. |
|||
|
|||
* The template system is based on the open source [Scriban library](https://github.com/lunet-io/scriban). So it supports if conditions, loops and much more. |
|||
* `model` is used to pass data to the template (just like the ASP.NET Core MVC). |
|||
* `L` is a special function that localizes the given string. |
|||
|
|||
It is typical to use the same layout for all emails. So, you can define a layout template. This is the standard layout template comes with the framework: |
|||
|
|||
````xml |
|||
<!DOCTYPE html> |
|||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
</head> |
|||
<body> |
|||
{{content}} |
|||
</body> |
|||
</html> |
|||
```` |
|||
|
|||
A layout should have a `{{content}}` area to render the child content (just like the `RenderBody()` in the MVC). |
|||
|
|||
It is very easy to override a template content by the final application to customize it. |
|||
|
|||
Whenever you need to render a template, use the `ITemplateRenderer` service by providing the template name and a model. See the [text templating documentation](https://docs.abp.io/en/abp/latest/Text-Templating) for details. We've even created a UI for the ABP Commercial (see the related section below). |
|||
|
|||
### Subscribing to the Exceptions |
|||
|
|||
ABP Framework's [exception handling system](https://docs.abp.io/en/abp/latest/Exception-Handling) automatically handles exceptions and returns an appropriate result to the client. In some cases, you may want to have a callback that is notified whenever an exception occurs. In this way, for example, you can send an email or take any action based on the exception. |
|||
|
|||
Just create a class derived from the `ExceptionSubscriber` class in your application: |
|||
|
|||
````csharp |
|||
public class MyExceptionSubscriber : ExceptionSubscriber |
|||
{ |
|||
public override async Task HandleAsync(ExceptionNotificationContext context) |
|||
{ |
|||
//TODO... |
|||
} |
|||
} |
|||
```` |
|||
|
|||
See the [exception handling](https://docs.abp.io/en/abp/latest/Exception-Handling) document for more. |
|||
|
|||
### Others |
|||
|
|||
There are many minor features and enhancements made to the framework in the past releases. Here, a few ones: |
|||
|
|||
* Added `AbpLocalizationOptions.DefaultResourceType` to set the default resource type for the application. In this way, the localization system uses the default resource whenever the resource was not specified. The latest application startup template already configures it, but you may want to set it for your existing applications. |
|||
* Added `IsEnabled` to permission definition. In this way, you can completely disable a permission and hide the related functionality from the application. This can be a way of feature switch for some applications. See [#3486](https://github.com/abpframework/abp/issues/3486) for usage. |
|||
* Added Dutch and German localizations to all the localization resources defined by the framework. Thanks to the contributors. |
|||
|
|||
## What's New in the ABP Commercial |
|||
|
|||
The goal of the [ABP Commercial](https://commercial.abp.io/) is to provide pre-build application functionalities, code generation tools, professional themes, advanced samples and premium support for ABP Framework based projects. |
|||
|
|||
We are working on the ABP Commercial in the parallel to align with the ABP Framework features and provide more modules, theme options and tooling. |
|||
|
|||
This section explains what's going on the ABP Commercial side. |
|||
|
|||
### Module Entity Extension System |
|||
|
|||
Module entity extension system is a higher level API that uses the object extension system (introduced above) and provides an easy way to add extension properties to existing entities. A new extension property easily automatically becomes a part of the HTTP API and the User Interface. |
|||
|
|||
Example: Add a `SocialSecurityNumber` to the user entity of the identity module |
|||
|
|||
````csharp |
|||
ObjectExtensionManager.Instance.Modules() |
|||
.ConfigureIdentity(identity => |
|||
{ |
|||
identity.ConfigureUser(user => |
|||
{ |
|||
user.AddOrUpdateProperty<string>( //property type: string |
|||
"SocialSecurityNumber", //property name |
|||
property => |
|||
{ |
|||
//validation rules |
|||
property.Attributes.Add(new RequiredAttribute()); |
|||
property.Attributes.Add( |
|||
new StringLengthAttribute(64) { |
|||
MinimumLength = 4 |
|||
} |
|||
); |
|||
|
|||
//...other configurations for this property |
|||
} |
|||
); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
With just such a configuration, the user interface will have the new property (on the table and on the create/edit forms): |
|||
|
|||
 |
|||
|
|||
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. |
|||
|
|||
 |
|||
|
|||
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: |
|||
|
|||
 |
|||
|
|||
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. |
|||
|
|||
 |
|||
|
|||
You can also check history for an entity when you click to the actions menu for the entity: |
|||
|
|||
 |
|||
|
|||
### 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: |
|||
|
|||
 |
|||
|
|||
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. |
|||
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 385 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 135 KiB |
@ -0,0 +1,3 @@ |
|||
# Distributed Event Bus RabbitMQ Integration |
|||
|
|||
TODO |
|||
@ -0,0 +1,62 @@ |
|||
# Sample Applications |
|||
|
|||
Here, a list of official samples built with the ABP Framework. Most of these samples are located under the [abpframework/abp-samples](https://github.com/abpframework/abp-samples) GitHub repository. |
|||
|
|||
### Microservice Demo |
|||
|
|||
A complete solution to demonstrate how to build systems based on the microservice architecture. |
|||
|
|||
* [The complete documentation for this sample](Microservice-Demo.md) |
|||
* [Source code](https://github.com/abpframework/abp/tree/dev/samples/MicroserviceDemo) |
|||
* [Microservice architecture document](../Microservice-Architecture.md) |
|||
|
|||
### Book Store |
|||
|
|||
A simple CRUD application to show basic principles of developing an application with the ABP Framework. The same sample was implemented with different technologies: |
|||
|
|||
* **Book Store: Razor Pages UI & Entity Framework Core** |
|||
|
|||
* [Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC) |
|||
* [Source code](https://github.com/abpframework/abp-samples/tree/master/BookStore) |
|||
|
|||
* **Book Store: Angular UI & MongoDB** |
|||
|
|||
* [Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=NG) |
|||
* [Source code](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) |
|||
|
|||
* **Book Store: Modular application (Razor Pages UI & EF Core)** |
|||
|
|||
* [Source code](https://github.com/abpframework/abp-samples/tree/master/BookStore-Modular) |
|||
|
|||
While there is no Razor Pages & MongoDB combination, you can check both documents to understand it since DB & UI selection don't effect each other. |
|||
|
|||
### Other Samples |
|||
|
|||
* **Entity Framework Migrations**: A solution to demonstrate how to split your application into multiple databases each database contains different modules. |
|||
* [Source code](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) |
|||
* [EF Core database migrations document](../Entity-Framework-Core-Migrations.md) |
|||
* **SignalR Demo**: A simple chat application that allows to send and receive messages among authenticated users. |
|||
* [Source code](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) |
|||
* [SignalR Integration document](../SignalR-Integration.md) |
|||
* **Dashboard Demo**: A simple application to show how to use the widget system for the ASP.NET Core MVC UI. |
|||
* [Source code](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) |
|||
* [Widget documentation](../UI/AspNetCore/Widgets.md) |
|||
* **RabbitMQ Event Bus Demo**: A solution consists of two applications communicating to each other via distributed events with RabbitMQ integration. |
|||
* [Source code](https://github.com/abpframework/abp-samples/tree/master/RabbitMqEventBus) |
|||
* [Distributed event bus document](../Distributed-Event-Bus.md) |
|||
* [RabbitMQ distributed event bus integration document](../Distributed-Event-Bus-RabbitMQ-Integration.md) |
|||
* **Text Templates Demo**: Shows different use cases of the text templating system. |
|||
* [Source code](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo) |
|||
* [Text templating documentation](../Text-Templating.md) |
|||
* **Authentication Customization**: A solution to show how to customize the authentication for ASP.NET Core MVC / Razor Pages applications. |
|||
* [Source code](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization) |
|||
* Related "[How To](../How-To/Index.md)" documents: |
|||
* [Azure Active Directory Authentication](../How-To/Azure-Active-Directory-Authentication-MVC.md) |
|||
* [Customize the Login Page](../How-To/Customize-Login-Page-MVC.md) |
|||
* [Customize the SignIn Manager](../How-To/Customize-SignIn-Manager.md) |
|||
* **Empty ASP.NET Core Application**: The most basic ASP.NET Core application with the ABP Framework installed. |
|||
* [Source code](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication) |
|||
* [Documentation](../Getting-Started-AspNetCore-Application.md) |
|||
* **Empty Console Application**: The most basic console application with the ABP Framework installed. |
|||
* [Source code](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication) |
|||
* [Documentation](../Getting-Started-Console-Application.md) |
|||
@ -0,0 +1,227 @@ |
|||
# SignalR Integration |
|||
|
|||
> It is already possible to follow [the standard Microsoft tutorial](https://docs.microsoft.com/en-us/aspnet/core/tutorials/signalr) to add [SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) to your application. However, ABP provides a SignalR integration packages that simplify the integration and usage. |
|||
|
|||
## Installation |
|||
|
|||
### Server Side |
|||
|
|||
It is suggested to use the [ABP CLI](CLI.md) to install this package. |
|||
|
|||
#### Using the ABP CLI |
|||
|
|||
Open a command line window in the folder of your project (.csproj file) and type the following command: |
|||
|
|||
```bash |
|||
abp add-package Volo.Abp.AspNetCore.SignalR |
|||
``` |
|||
|
|||
> You typically want to add this package to the web or API layer of your application, depending on your architecture. |
|||
|
|||
#### Manual Installation |
|||
|
|||
If you want to manually install; |
|||
|
|||
1. Add the [Volo.Abp.AspNetCore.SignalR](https://www.nuget.org/packages/Volo.Abp.AspNetCore.SignalR) NuGet package to your project: |
|||
|
|||
``` |
|||
Install-Package Volo.Abp.AspNetCore.SignalR |
|||
``` |
|||
|
|||
Or use the Visual Studio NuGet package management UI to install it. |
|||
|
|||
2. Add the `AbpAspNetCoreSignalRModule` to the dependency list of your module: |
|||
|
|||
```csharp |
|||
[DependsOn( |
|||
//...other dependencies |
|||
typeof(AbpAspNetCoreSignalRModule) //Add the new module dependency |
|||
)] |
|||
public class YourModule : AbpModule |
|||
{ |
|||
} |
|||
``` |
|||
|
|||
> You don't need to use the `services.AddSignalR()` and the `app.UseEndpoints(...)`, it's done by the `AbpAspNetCoreSignalRModule`. |
|||
|
|||
### Client Side |
|||
|
|||
Client side installation depends on your UI framework / client type. |
|||
|
|||
#### ASP.NET Core MVC / Razor Pages UI |
|||
|
|||
Run the following command in the root folder of your web project: |
|||
|
|||
````bash |
|||
yarn add @abp/signalr |
|||
```` |
|||
|
|||
> This requires to [install yarn](https://yarnpkg.com/) if you haven't install before. |
|||
|
|||
This will add the `@abp/signalr` to the dependencies in the `package.json` of your project: |
|||
|
|||
````json |
|||
{ |
|||
... |
|||
"dependencies": { |
|||
... |
|||
"@abp/signalr": "~2.7.0" |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Run the `gulp` in the root folder of your web project: |
|||
|
|||
````bash |
|||
gulp |
|||
```` |
|||
|
|||
This will copy the SignalR JavaScript files into your project: |
|||
|
|||
 |
|||
|
|||
Finally, add the following code to your page/view to include the `signalr.js` file |
|||
|
|||
````xml |
|||
@section scripts { |
|||
<abp-script type="typeof(SignalRBrowserScriptContributor)" /> |
|||
} |
|||
```` |
|||
|
|||
It requires to add `@using Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR` to your page/view. |
|||
|
|||
> You could add the `signalr.js` file in a standard way. But using the `SignalRBrowserScriptContributor` has additional benefits. See the [Client Side Package Management](UI/AspNetCore/Client-Side-Package-Management.md) and [Bundling & Minification](UI/AspNetCore/Bundling-Minification.md) documents for details. |
|||
|
|||
That's all. you can use the [SignalR JavaScript API](https://docs.microsoft.com/en-us/aspnet/core/signalr/javascript-client) in your page. |
|||
|
|||
#### Other UI Frameworks / Clients |
|||
|
|||
Please refer to [Microsoft's documentation](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) for other type of clients. |
|||
|
|||
## The ABP Framework Integration |
|||
|
|||
This section covers the additional benefits when you use the ABP Framework integration packages. |
|||
|
|||
### Hub Route & Mapping |
|||
|
|||
ABP automatically registers all the hubs to the [dependency injection](Dependency-Injection.md) (as transient) and maps the hub endpoint. So, you don't have to use the ` app.UseEndpoints(...)` to map your hubs. Hub route (URL) is determined conventionally based on your hub name. |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
public class MessagingHub : Hub |
|||
{ |
|||
//... |
|||
} |
|||
```` |
|||
|
|||
The hub route will be `/signalr-hubs/messasing` for the `MessasingHub`: |
|||
|
|||
* Adding a standard `/signalr-hubs/` prefix |
|||
* Continue with the **camel case** hub name, without the `Hub` suffix. |
|||
|
|||
If you want to specify the route, you can use the `HubRoute` attribute: |
|||
|
|||
````csharp |
|||
[HubRoute("/my-messasing-hub")] |
|||
public class MessagingHub : Hub |
|||
{ |
|||
//... |
|||
} |
|||
```` |
|||
|
|||
### AbpHub Base Classes |
|||
|
|||
Instead of the standard `Hub` and `Hub<T>` classes, you can inherit from the `AbpHub` or `AbpHub<T>` which hve useful base properties like `CurrentUser`. |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
public class MessagingHub : AbpHub |
|||
{ |
|||
public async Task SendMessage(string targetUserName, string message) |
|||
{ |
|||
var currentUserName = CurrentUser.UserName; //Access to the current user info |
|||
var txt = L["MyText"]; //Localization |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> While you could inject the same properties into your hub constructor, this way simplifies your hub class. |
|||
|
|||
### Manual Registration / Mapping |
|||
|
|||
ABP automatically registers all the hubs to the [dependency injection](Dependency-Injection.md) as a **transient service**. If you want to **disable auto dependency injection** registration for your hub class, just add a `DisableConventionalRegistration` attribute. You can still register your hub class to dependency injection in the `ConfigureServices` method of your module if you like: |
|||
|
|||
````csharp |
|||
context.Services.AddTransient<MessagingHub>(); |
|||
```` |
|||
|
|||
When **you or ABP** register the class to the dependency injection, it is automatically mapped to the endpoint route configuration just as described in the previous sections. You can use `DisableAutoHubMap` attribute if you want to manually map your hub class. |
|||
|
|||
For manual mapping, you have two options: |
|||
|
|||
1. Use the `AbpSignalROptions` to add your map configuration (in the `ConfigureServices` method of your [module](Module-Development-Basics.md)), so ABP still performs the endpoint mapping for your hub: |
|||
|
|||
````csharp |
|||
Configure<AbpSignalROptions>(options => |
|||
{ |
|||
options.Hubs.Add( |
|||
new HubConfig( |
|||
typeof(MessagingHub), //Hub type |
|||
"/my-messaging/route", //Hub route (URL) |
|||
hubOptions => |
|||
{ |
|||
//Additional options |
|||
hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); |
|||
} |
|||
) |
|||
); |
|||
}); |
|||
```` |
|||
|
|||
This is a good way to provide additional SignalR options. |
|||
|
|||
If you don't want to disable auto hub map, but still want to perform additional SignalR configuration, use the `options.Hubs.AddOrUpdate(...)` method: |
|||
|
|||
````csharp |
|||
Configure<AbpSignalROptions>(options => |
|||
{ |
|||
options.Hubs.AddOrUpdate( |
|||
typeof(MessagingHub), //Hub type |
|||
config => //Additional configuration |
|||
{ |
|||
config.RoutePattern = "/my-messaging-hub"; //override the default route |
|||
config.ConfigureActions.Add(hubOptions => |
|||
{ |
|||
//Additional options |
|||
hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); |
|||
}); |
|||
} |
|||
); |
|||
}); |
|||
```` |
|||
|
|||
This is the way you can modify the options of a hub class defined in a depended module (where you don't have the source code access). |
|||
|
|||
2. Change `app.UseConfiguredEndpoints` in the `OnApplicationInitialization` method of your [module](Module-Development-Basics.md) as shown below (added a lambda method as the parameter). |
|||
|
|||
````csharp |
|||
app.UseConfiguredEndpoints(endpoints => |
|||
{ |
|||
endpoints.MapHub<MessagingHub>("/my-messaging-hub", options => |
|||
{ |
|||
options.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
### UserIdProvider |
|||
|
|||
ABP implements SignalR's `IUserIdProvider` interface to provide the current user id from the `ICurrentUser` service of the ABP framework (see [the current user service](CurrentUser.md)), so it will be integrated to the authentication system of your application. The implementing class is the `AbpSignalRUserIdProvider`, if you want to change/override it. |
|||
|
|||
## Example Application |
|||
|
|||
See the [SignalR Integration Demo](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) as a sample application. It has a simple Chat page to send messages between (authenticated) users. |
|||
|
|||
 |
|||
@ -0,0 +1,455 @@ |
|||
# Text Templating |
|||
|
|||
## Introduction |
|||
|
|||
ABP Framework provides a simple, yet efficient text template system. Text templating is used to dynamically render contents based on a template and a model (a data object): |
|||
|
|||
***TEMPLATE + MODEL ==render==> RENDERED CONTENT*** |
|||
|
|||
It is very similar to an ASP.NET Core Razor View (or Page): |
|||
|
|||
*RAZOR VIEW (or PAGE) + MODEL ==render==> HTML CONTENT* |
|||
|
|||
You can use the rendered output for any purpose, like sending emails or preparing some reports. |
|||
|
|||
### Example |
|||
|
|||
Here, a simple template: |
|||
|
|||
```` |
|||
Hello {%{{{model.name}}}%} :) |
|||
```` |
|||
|
|||
You can define a class with a `Name` property to render this template: |
|||
|
|||
````csharp |
|||
public class HelloModel |
|||
{ |
|||
public string Name { get; set; } |
|||
} |
|||
```` |
|||
|
|||
If you render the template with a `HelloModel` with the `Name` is `John`, the rendered output is will be: |
|||
|
|||
```` |
|||
Hello John :) |
|||
```` |
|||
|
|||
Template rendering engine is very powerful; |
|||
|
|||
* It is based on the [Scriban](https://github.com/lunet-io/scriban) library, so it supports **conditional logics**, **loops** and much more. |
|||
* Template content **can be localized**. |
|||
* You can define **layout templates** to be used as the layout while rendering other templates. |
|||
* You can pass arbitrary objects to the template context (beside the model) for advanced scenarios. |
|||
|
|||
### Source Code |
|||
|
|||
Get [the source code of the sample application](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo) developed and referred through this document. |
|||
|
|||
## Installation |
|||
|
|||
It is suggested to use the [ABP CLI](CLI.md) to install this package. |
|||
|
|||
### Using the ABP CLI |
|||
|
|||
Open a command line window in the folder of the project (.csproj file) and type the following command: |
|||
|
|||
````bash |
|||
abp add-package Volo.Abp.TextTemplating |
|||
```` |
|||
|
|||
### Manual Installation |
|||
|
|||
If you want to manually install; |
|||
|
|||
1. Add the [Volo.Abp.TextTemplating](https://www.nuget.org/packages/Volo.Abp.TextTemplating) NuGet package to your project: |
|||
|
|||
```` |
|||
Install-Package Volo.Abp.TextTemplating |
|||
```` |
|||
|
|||
2. Add the `AbpTextTemplatingModule` to the dependency list of your module: |
|||
|
|||
````csharp |
|||
[DependsOn( |
|||
//...other dependencies |
|||
typeof(AbpTextTemplatingModule) //Add the new module dependency |
|||
)] |
|||
public class YourModule : AbpModule |
|||
{ |
|||
} |
|||
```` |
|||
|
|||
## Defining Templates |
|||
|
|||
Before rendering a template, you should define it. Create a class inheriting from the `TemplateDefinitionProvider` base class: |
|||
|
|||
````csharp |
|||
public class DemoTemplateDefinitionProvider : TemplateDefinitionProvider |
|||
{ |
|||
public override void Define(ITemplateDefinitionContext context) |
|||
{ |
|||
context.Add( |
|||
new TemplateDefinition("Hello") //template name: "Hello" |
|||
.WithVirtualFilePath( |
|||
"/Demos/Hello/Hello.tpl", //template content path |
|||
isInlineLocalized: true |
|||
) |
|||
); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `context` object is used to add new templates or get the templates defined by depended modules. Used `context.Add(...)` to define a new template. |
|||
* `TemplateDefinition` is the class represents a template. Each template must have a unique name (that will be used while you are rendering the template). |
|||
* `/Demos/Hello/Hello.tpl` is the path of the template file. |
|||
* `isInlineLocalized` is used to declare if you are using a single template for all languages (`true`) or different templates for each language (`false`). See the Localization section below for more. |
|||
|
|||
### The Template Content |
|||
|
|||
`WithVirtualFilePath` indicates that we are using the [Virtual File System](Virtual-File-System.md) to store the template content. Create a `Hello.tpl` file inside your project and mark it as "**embedded resource**" on the properties window: |
|||
|
|||
 |
|||
|
|||
Example `Hello.tpl` content is shown below: |
|||
|
|||
```` |
|||
Hello {%{{{model.name}}}%} :) |
|||
```` |
|||
|
|||
The [Virtual File System](Virtual-File-System.md) requires to add your files in the `ConfigureServices` method of your [module](Module-Development-Basics.md) class: |
|||
|
|||
````csharp |
|||
Configure<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<TextTemplateDemoModule>("TextTemplateDemo"); |
|||
}); |
|||
```` |
|||
|
|||
* `TextTemplateDemoModule` is the module class that you define your template in. |
|||
* `TextTemplateDemo` is the root namespace of your project. |
|||
|
|||
## Rendering the Template |
|||
|
|||
`ITemplateRenderer` service is used to render a template content. |
|||
|
|||
### Example: Rendering a Simple Template |
|||
|
|||
````csharp |
|||
public class HelloDemo : ITransientDependency |
|||
{ |
|||
private readonly ITemplateRenderer _templateRenderer; |
|||
|
|||
public HelloDemo(ITemplateRenderer templateRenderer) |
|||
{ |
|||
_templateRenderer = templateRenderer; |
|||
} |
|||
|
|||
public async Task RunAsync() |
|||
{ |
|||
var result = await _templateRenderer.RenderAsync( |
|||
"Hello", //the template name |
|||
new HelloModel |
|||
{ |
|||
Name = "John" |
|||
} |
|||
); |
|||
|
|||
Console.WriteLine(result); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `HelloDemo` is a simple class that injects the `ITemplateRenderer` in its constructor and uses it inside the `RunAsync` method. |
|||
* `RenderAsync` gets two fundamental parameters: |
|||
* `templateName`: The name of the template to be rendered (`Hello` in this example). |
|||
* `model`: An object that is used as the `model` inside the template (a `HelloModel` object in this example). |
|||
|
|||
The result shown below for this example: |
|||
|
|||
````csharp |
|||
Hello John :) |
|||
```` |
|||
|
|||
### Anonymous Model |
|||
|
|||
While it is suggested to create model classes for the templates, it would be practical (and possible) to use anonymous objects for simple cases: |
|||
|
|||
````csharp |
|||
var result = await _templateRenderer.RenderAsync( |
|||
"Hello", |
|||
new |
|||
{ |
|||
Name = "John" |
|||
} |
|||
); |
|||
```` |
|||
|
|||
In this case, we haven't created a model class, but created an anonymous object as the model. |
|||
|
|||
### PascalCase vs camelCase |
|||
|
|||
PascalCase property names (like `UserName`) is used as camelCase (like `userName`) in the templates. |
|||
|
|||
## Localization |
|||
|
|||
It is possible to localize a template content based on the current culture. There are two types of localization options described in the following sections. |
|||
|
|||
### Inline localization |
|||
|
|||
Inline localization uses the [localization system](Localization.md) to localize texts inside templates. |
|||
|
|||
#### Example: Reset Password Link |
|||
|
|||
Assuming you need to send an email to a user to reset her/his password. Here, the template content: |
|||
|
|||
```` |
|||
<a href="{%{{{model.link}}}%}">{%{{{L "ResetMyPassword"}}}%}</a> |
|||
```` |
|||
|
|||
`L` function is used to localize the given key based on the current user culture. You need to define the `ResetMyPassword` key inside your localization file: |
|||
|
|||
````json |
|||
"ResetMyPassword": "Click here to reset your password" |
|||
```` |
|||
|
|||
You also need to declare the localization resource to be used with this template, inside your template definition provider class: |
|||
|
|||
````csharp |
|||
context.Add( |
|||
new TemplateDefinition( |
|||
"PasswordReset", //Template name |
|||
typeof(DemoResource) //LOCALIZATION RESOURCE |
|||
).WithVirtualFilePath( |
|||
"/Demos/PasswordReset/PasswordReset.tpl", //template content path |
|||
isInlineLocalized: true |
|||
) |
|||
); |
|||
```` |
|||
|
|||
That's all. When you render this template like that: |
|||
|
|||
````csharp |
|||
var result = await _templateRenderer.RenderAsync( |
|||
"PasswordReset", //the template name |
|||
new PasswordResetModel |
|||
{ |
|||
Link = "https://abp.io/example-link?userId=123&token=ABC" |
|||
} |
|||
); |
|||
```` |
|||
|
|||
You will see the localized result: |
|||
|
|||
````csharp |
|||
<a href="https://abp.io/example-link?userId=123&token=ABC">Click here to reset your password</a> |
|||
```` |
|||
|
|||
> If you define the [default localization resource](Localization.md) for your application, then no need to declare the resource type for the template definition. |
|||
|
|||
### Multiple Contents Localization |
|||
|
|||
Instead of a single template that uses the localization system to localize the template, you may want to create different template files for each language. It can be needed if the template should be completely different for a specific culture rather than simple text localizations. |
|||
|
|||
#### Example: Welcome Email Template |
|||
|
|||
Assuming that you want to send a welcome email to your users, but want to define a completely different template based on the user culture. |
|||
|
|||
First, create a folder and put your templates inside it, like `en.tpl`, `tr.tpl`... one for each culture you support: |
|||
|
|||
 |
|||
|
|||
Then add your template definition in the template definition provider class: |
|||
|
|||
````csharp |
|||
context.Add( |
|||
new TemplateDefinition( |
|||
name: "WelcomeEmail", |
|||
defaultCultureName: "en" |
|||
) |
|||
.WithVirtualFilePath( |
|||
"/Demos/WelcomeEmail/Templates", //template content folder |
|||
isInlineLocalized: false |
|||
) |
|||
); |
|||
```` |
|||
|
|||
* Set **default culture name**, so it fallbacks to the default culture if there is no template for the desired culture. |
|||
* Specify **the template folder** rather than a single template file. |
|||
* Set `isInlineLocalized` to `false` for this case. |
|||
|
|||
That's all, you can render the template for the current culture: |
|||
|
|||
````csharp |
|||
var result = await _templateRenderer.RenderAsync("WelcomeEmail"); |
|||
```` |
|||
|
|||
> Skipped the modal for this example to keep it simple, but you can use models as just explained before. |
|||
|
|||
### Specify the Culture |
|||
|
|||
`ITemplateRenderer` service uses the current culture (`CultureInfo.CurrentUICulture`) if not specified. If you need, you can specify the culture as the `cultureName` parameter: |
|||
|
|||
````csharp |
|||
var result = await _templateRenderer.RenderAsync( |
|||
"WelcomeEmail", |
|||
cultureName: "en" |
|||
); |
|||
```` |
|||
|
|||
## Layout Templates |
|||
|
|||
Layout templates are used to create shared layouts among other templates. It is similar to the layout system in the ASP.NET Core MVC / Razor Pages. |
|||
|
|||
### Example: Email HTML Layout Template |
|||
|
|||
For example, you may want to create a single layout for all of your email templates. |
|||
|
|||
First, create a template file just like before: |
|||
|
|||
````xml |
|||
<!DOCTYPE html> |
|||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
</head> |
|||
<body> |
|||
{%{{{content}}}%} |
|||
</body> |
|||
</html> |
|||
```` |
|||
|
|||
* A layout template must have a **{%{{{content}}}%}** part as a place holder for the rendered child content. |
|||
|
|||
The register your template in the template definition provider: |
|||
|
|||
````csharp |
|||
context.Add( |
|||
new TemplateDefinition( |
|||
"EmailLayout", |
|||
isLayout: true //SET isLayout! |
|||
).WithVirtualFilePath( |
|||
"/Demos/EmailLayout/EmailLayout.tpl", |
|||
isInlineLocalized: true |
|||
) |
|||
); |
|||
```` |
|||
|
|||
Now, you can use this template as the layout of any other template: |
|||
|
|||
````csharp |
|||
context.Add( |
|||
new TemplateDefinition( |
|||
name: "WelcomeEmail", |
|||
defaultCultureName: "en", |
|||
layout: "EmailLayout" //Set the LAYOUT |
|||
).WithVirtualFilePath( |
|||
"/Demos/WelcomeEmail/Templates", |
|||
isInlineLocalized: false |
|||
) |
|||
); |
|||
```` |
|||
|
|||
## Global Context |
|||
|
|||
ABP passes the `model` that can be used to access to the model inside the template. You can pass more global variables if you need. |
|||
|
|||
An example template content: |
|||
|
|||
```` |
|||
A global object value: {%{{{myGlobalObject}}}%} |
|||
```` |
|||
|
|||
This template assumes that that is a `myGlobalObject` object in the template rendering context. You can provide it like shown below: |
|||
|
|||
````csharp |
|||
var result = await _templateRenderer.RenderAsync( |
|||
"GlobalContextUsage", |
|||
globalContext: new Dictionary<string, object> |
|||
{ |
|||
{"myGlobalObject", "TEST VALUE"} |
|||
} |
|||
); |
|||
```` |
|||
|
|||
The rendering result will be: |
|||
|
|||
```` |
|||
A global object value: TEST VALUE |
|||
```` |
|||
|
|||
## Advanced Features |
|||
|
|||
This section covers some internals and more advanced usages of the text templating system. |
|||
|
|||
### Template Content Provider |
|||
|
|||
`ITemplateRenderer` is used to render the template, which is what you want for most of the cases. However, you can use the `ITemplateContentProvider` to get the raw (not rendered) template contents. |
|||
|
|||
> `ITemplateContentProvider` is internally used by the `ITemplateRenderer` to get the raw template contents. |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
public class TemplateContentDemo : ITransientDependency |
|||
{ |
|||
private readonly ITemplateContentProvider _templateContentProvider; |
|||
|
|||
public TemplateContentDemo(ITemplateContentProvider templateContentProvider) |
|||
{ |
|||
_templateContentProvider = templateContentProvider; |
|||
} |
|||
|
|||
public async Task RunAsync() |
|||
{ |
|||
var result = await _templateContentProvider |
|||
.GetContentOrNullAsync("Hello"); |
|||
|
|||
Console.WriteLine(result); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
The result will be the raw template content: |
|||
|
|||
```` |
|||
Hello {%{{{model.name}}}%} :) |
|||
```` |
|||
|
|||
* `GetContentOrNullAsync` returns `null` if no content defined for the requested template. |
|||
* It can get a `cultureName` parameter that is used if template has different files for different cultures (see Multiple Contents Localization section above). |
|||
|
|||
### Template Content Contributor |
|||
|
|||
`ITemplateContentProvider` service uses `ITemplateContentContributor` implementations to find template contents. There is a single pre-implemented content contributor, `VirtualFileTemplateContentContributor`, which gets template contents from the virtual file system as described above. |
|||
|
|||
You can implement the `ITemplateContentContributor` to read raw template contents from another source. |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
public class MyTemplateContentProvider |
|||
: ITemplateContentContributor, ITransientDependency |
|||
{ |
|||
public async Task<string> GetOrNullAsync(TemplateContentContributorContext context) |
|||
{ |
|||
var templateName = context.TemplateDefinition.Name; |
|||
|
|||
//TODO: Try to find content from another source |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
```` |
|||
|
|||
Return `null` if your source can not find the content, so `ITemplateContentProvider` fallbacks to the next contributor. |
|||
|
|||
### Template Definition Manager |
|||
|
|||
`ITemplateDefinitionManager` service can be used to get the template definitions (created by the template definition providers). |
|||
|
|||
## See Also |
|||
|
|||
* [The source code of the sample application](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo) developed and referred through this document. |
|||
* [Localization system](Localization.md). |
|||
* [Virtual File System](Virtual-File-System.md). |
|||
|
Before Width: | Height: | Size: 241 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 23 KiB |
@ -0,0 +1,165 @@ |
|||
# Querying Lists Easily with ListService |
|||
|
|||
`ListService` is a utility service to provide an easy pagination, sorting, and search implementation. |
|||
|
|||
|
|||
|
|||
## Getting Started |
|||
|
|||
`ListService` is **not provided in root**. The reason is, this way, it will clear any subscriptions on component destroy. You may use the optional `LIST_QUERY_DEBOUNCE_TIME` token to adjust the debounce behavior. |
|||
|
|||
```js |
|||
import { ListService } from '@abp/ng.core'; |
|||
import { BookDto } from '../models'; |
|||
import { BookService } from '../services'; |
|||
|
|||
@Component({ |
|||
/* class metadata here */ |
|||
providers: [ |
|||
// [Required] |
|||
ListService, |
|||
|
|||
// [Optional] |
|||
// Provide this token if you want a different debounce time. |
|||
// Default is 300. Cannot be 0. Any value below 100 is not recommended. |
|||
{ provide: LIST_QUERY_DEBOUNCE_TIME, useValue: 500 }, |
|||
], |
|||
template: ` |
|||
|
|||
`, |
|||
}) |
|||
class BookComponent { |
|||
items: BookDto[] = []; |
|||
count = 0; |
|||
|
|||
constructor( |
|||
public readonly list: ListService, |
|||
private bookService: BookService, |
|||
) {} |
|||
|
|||
ngOnInit() { |
|||
// A function that gets query and returns an observable |
|||
const bookStreamCreator = query => this.bookService.getList(query); |
|||
|
|||
this.list.hookToQuery(bookStreamCreator).subscribe( |
|||
response => { |
|||
this.items = response.items; |
|||
this.count = response.count; |
|||
// If you use OnPush change detection strategy, |
|||
// call detectChanges method of ChangeDetectorRef here. |
|||
} |
|||
); // Subscription is auto-cleared on destroy. |
|||
} |
|||
} |
|||
``` |
|||
|
|||
> Noticed `list` is `public` and `readonly`? That is because we will use `ListService` properties directly in the component's template. That may be considered as an anti-pattern, but it is much quicker to implement. You can always use public component properties instead. |
|||
|
|||
Place `ListService` properties into the template like this: |
|||
|
|||
```html |
|||
<abp-table |
|||
[value]="book.items" |
|||
[(page)]="list.page" |
|||
[rows]="list.maxResultCount" |
|||
[totalRecords]="book.totalCount" |
|||
[headerTemplate]="tableHeader" |
|||
[bodyTemplate]="tableBody" |
|||
[abpLoading]="list.isLoading$ | async" |
|||
> |
|||
</abp-table> |
|||
|
|||
<ng-template #tableHeader> |
|||
<tr> |
|||
<th (click)="nameSort.sort('name')"> |
|||
{%{{{ '::Name' | abpLocalization }}}%} |
|||
<abp-sort-order-icon |
|||
#nameSort |
|||
sortKey="name" |
|||
[(selectedSortKey)]="list.sortKey" |
|||
[(order)]="list.sortOrder" |
|||
></abp-sort-order-icon> |
|||
</th> |
|||
</tr> |
|||
</ng-template> |
|||
|
|||
<ng-template #tableBody let-data> |
|||
<tr> |
|||
<td>{%{{{ data.name }}}%}</td> |
|||
</tr> |
|||
</ng-template> |
|||
``` |
|||
|
|||
## Usage with Observables |
|||
|
|||
You may use observables in combination with [AsyncPipe](https://angular.io/guide/observables-in-angular#async-pipe) of Angular instead. Here are some possibilities: |
|||
|
|||
```ts |
|||
book$ = this.list.hookToQuery(query => this.bookService.getListByInput(query)); |
|||
``` |
|||
|
|||
```html |
|||
<!-- simplified representation of the template --> |
|||
|
|||
<abp-table |
|||
[value]="(book$ | async)?.items || []" |
|||
[totalRecords]="(book$ | async)?.totalCount" |
|||
> |
|||
</abp-table> |
|||
|
|||
<!-- DO NOT WORRY, ONLY ONE REQUEST WILL BE MADE --> |
|||
``` |
|||
|
|||
...or... |
|||
|
|||
|
|||
```ts |
|||
@Select(BookState.getBooks) |
|||
books$: Observable<BookDto[]>; |
|||
|
|||
@Select(BookState.getBookCount) |
|||
bookCount$: Observable<number>; |
|||
|
|||
ngOnInit() { |
|||
this.list.hookToQuery((query) => this.store.dispatch(new GetBooks(query))).subscribe(); |
|||
} |
|||
``` |
|||
|
|||
```html |
|||
<!-- simplified representation of the template --> |
|||
|
|||
<abp-table |
|||
[value]="books$ | async" |
|||
[totalRecords]="bookCount$ | async" |
|||
> |
|||
</abp-table> |
|||
``` |
|||
|
|||
## How to Refresh Table on Create/Update/Delete |
|||
|
|||
`ListService` exposes a `get` method to trigger a request with the current query. So, basically, whenever a create, update, or delete action resolves, you can call `this.list.get();` and it will call hooked stream creator again. |
|||
|
|||
```ts |
|||
this.store.dispatch(new DeleteBook(id)).subscribe(this.list.get); |
|||
``` |
|||
|
|||
...or... |
|||
|
|||
```ts |
|||
this.bookService.createByInput(form.value) |
|||
.subscribe(() => { |
|||
this.list.get(); |
|||
|
|||
// Other subscription logic here |
|||
}) |
|||
``` |
|||
|
|||
## How to Implement Server-Side Search in a Table |
|||
|
|||
`ListService` exposes a `filter` property that will trigger a request with the current query and the given search string. All you need to do is to bind it to an input element with two-way binding. |
|||
|
|||
```html |
|||
<!-- simplified representation --> |
|||
|
|||
<input type="text" name="search" [(ngModel)]="list.filter"> |
|||
``` |
|||
@ -0,0 +1,199 @@ |
|||
# Modifying the Menu |
|||
|
|||
The menu is inside the `ApplicationLayoutComponent` in the @abp/ng.theme.basic package. There are several methods for modifying the menu elements. This document covers these methods. If you would like to replace the menu completely, please refer to [Component Replacement documentation](./Component-Replacement.md) and learn how to replace a layout. |
|||
|
|||
|
|||
<!-- TODO: Replace layout replacement document with component replacement. Layout replacement document will be created.--> |
|||
|
|||
|
|||
## How to Add a Logo |
|||
|
|||
The `logoUrl` property in the environment variables is the url of the logo. |
|||
|
|||
You can add your logo to `src/assets` folder and set the `logoUrl` as shown below: |
|||
|
|||
```js |
|||
export const environment = { |
|||
// other configurations |
|||
application: { |
|||
name: 'MyProjectName', |
|||
logoUrl: 'assets/logo.png', |
|||
}, |
|||
// other configurations |
|||
}; |
|||
``` |
|||
|
|||
## How to Add a Navigation Element |
|||
|
|||
### Via `routes` Property in `AppRoutingModule` |
|||
|
|||
You can define your routes by adding `routes` as a child property to `data` property of a route configuration in the `app-routing.module`. The `@abp/ng.core` package organizes your routes and stores them in the `ConfigState`. `ApplicationLayoutComponent` gets routes from store and displays them on the menu. |
|||
|
|||
You can add the `routes` property like below: |
|||
|
|||
```js |
|||
{ |
|||
path: 'your-path', |
|||
data: { |
|||
routes: { |
|||
name: 'Your navigation', |
|||
order: 3, |
|||
iconClass: 'fas fa-question-circle', |
|||
requiredPolicy: 'permission key here', |
|||
children: [ |
|||
{ |
|||
path: 'child', |
|||
name: 'Your child navigation', |
|||
order: 1, |
|||
requiredPolicy: 'permission key here', |
|||
}, |
|||
], |
|||
} as ABP.Route, // can be imported from @abp/ng.core |
|||
} |
|||
} |
|||
``` |
|||
|
|||
- `name` is the label of the navigation element. A localization key or a localization object can be passed. |
|||
- `order` is the order of the navigation element. |
|||
- `iconClass` is the class of the `i` tag, which is placed to the left of the navigation label. |
|||
- `requiredPolicy` is the permission key to access the page. See the [Permission Management document](./Permission-Management.md) |
|||
- `children` is an array and is used for declaring child navigation elements. The child navigation element will be placed as a child route which will be available at `'/your-path/child'` based on the given `path` property. |
|||
|
|||
After adding the `routes` property as described above, the navigation menu looks like this: |
|||
|
|||
 |
|||
|
|||
## Via ConfigState |
|||
|
|||
The `dispatchAddRoute` method of `ConfigStateService` adds a new navigation element to the menu. |
|||
|
|||
```js |
|||
// this.config is instance of ConfigStateService |
|||
|
|||
const newRoute: ABP.Route = { |
|||
name: 'My New Page', |
|||
iconClass: 'fa fa-dashboard', |
|||
path: 'page', |
|||
invisible: false, |
|||
order: 2, |
|||
requiredPolicy: 'MyProjectName.MyNewPage', |
|||
} as Omit<ABP.Route, 'children'>; |
|||
|
|||
this.config.dispatchAddRoute(newRoute); |
|||
// returns a state stream which emits after dispatch action is complete |
|||
``` |
|||
|
|||
The `newRoute` will be placed as at root level, i.e. without any parent routes, and its url will be stored as `'/path'`. |
|||
|
|||
If you want **to add a child route, you can do this:** |
|||
|
|||
```js |
|||
// this.config is instance of ConfigStateService |
|||
// eIdentityRouteNames enum can be imported from @abp/ng.identity |
|||
|
|||
const newRoute: ABP.Route = { |
|||
parentName: eIdentityRouteNames.IdentityManagement, |
|||
name: 'My New Page', |
|||
iconClass: 'fa fa-dashboard', |
|||
path: 'page', |
|||
invisible: false, |
|||
order: 3, |
|||
requiredPolicy: 'MyProjectName.MyNewPage' |
|||
} as Omit<ABP.Route, 'children'>; |
|||
|
|||
this.config.dispatchAddRoute(newRoute); |
|||
// returns a state stream which emits after dispatch action is complete |
|||
``` |
|||
|
|||
The `newRoute` will then be placed as a child of the parent route named `eIdentityRouteNames.IdentityManagement` and its url will be set as `'/identity/page'`. |
|||
|
|||
The new route will be added like below: |
|||
|
|||
 |
|||
|
|||
## How to Patch a Navigation Element |
|||
|
|||
The `dispatchPatchRouteByName` method finds a route by its name and replaces its configuration in the store with the new configuration passed as the second parameter. |
|||
|
|||
```js |
|||
// this.config is instance of ConfigStateService |
|||
// eIdentityRouteNames enum can be imported from @abp/ng.identity |
|||
|
|||
const newRouteConfig: Partial<ABP.Route> = { |
|||
iconClass: 'fas fa-home', |
|||
parentName: eIdentityRouteNames.Administration, |
|||
order: 0, |
|||
children: [ |
|||
{ |
|||
name: 'Dashboard', |
|||
path: 'dashboard', |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
this.config.dispatchPatchRouteByName('::Menu:Home', newRouteConfig); |
|||
// returns a state stream which emits after dispatch action is complete |
|||
``` |
|||
|
|||
* Moved the _Home_ navigation under the _Administration_ dropdown based on given `parentName`. |
|||
* Added an icon. |
|||
* Specified the order. |
|||
* Added a child route named _Dashboard_. |
|||
|
|||
After the patch above, navigation elements looks like below: |
|||
|
|||
 |
|||
|
|||
|
|||
## How to Add an Element to Right Part of the Menu |
|||
|
|||
The right part elements are stored in the `LayoutState` that is in the @abp/ng.theme.basic package. |
|||
|
|||
The `dispatchAddNavigationElement` method of the `LayoutStateService` adds an element to the right part of the menu. |
|||
|
|||
You can insert an element by adding your template to `app.component` and calling the `dispatchAddNavigationElement` method: |
|||
|
|||
```js |
|||
import { Layout, LayoutStateService } from '@abp/ng.theme.basic'; // added this line |
|||
|
|||
@Component({ |
|||
selector: 'app-root', |
|||
template: ` |
|||
|
|||
<!-- Added below content --> |
|||
<ng-template #search |
|||
><input type="search" placeholder="Search" class="bg-transparent border-0" |
|||
/></ng-template> |
|||
`, |
|||
}) |
|||
export class AppComponent { |
|||
// Added ViewChild |
|||
@ViewChild('search', { static: false, read: TemplateRef }) searchElementRef: TemplateRef<any>; |
|||
|
|||
constructor(private layout: LayoutStateService) {} // injected LayoutStateService |
|||
|
|||
// Added ngAfterViewInit |
|||
ngAfterViewInit() { |
|||
const newElement = { |
|||
name: 'Search', |
|||
element: this.searchElementRef, |
|||
order: 1, |
|||
} as Layout.NavigationElement; |
|||
|
|||
this.layout.dispatchAddNavigationElement(newElement); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
This inserts a search input to the menu. The final UI looks like below: |
|||
|
|||
 |
|||
|
|||
## How to Remove an Element From Right Part of the Menu |
|||
|
|||
TODO |
|||
|
|||
|
|||
## What's Next |
|||
|
|||
* [Component Replacement](./Component-Replacement.md) |
|||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 45 KiB |
@ -1,3 +1,8 @@ |
|||
## Getting Started With the Angular Application Template |
|||
# 启动模板入门 |
|||
|
|||
TODO... |
|||
请参阅以下教程,了解如何使用预构建的应用程序启动模板开始使用ABP框架: |
|||
|
|||
* [ASP.NET Core MVC / Razor Pages UI 入门](Getting-Started?UI=MVC&DB=EF&Tiered=No) |
|||
* [Angular UI 入门](Getting-Started?UI=NG&DB=EF&Tiered=No) |
|||
|
|||
<!-- TODO: this document has been moved, it should be deleted in the future. --> |
|||
@ -1,102 +1,8 @@ |
|||
## ASP.NET Core MVC 模板入门 |
|||
# 启动模板入门 |
|||
|
|||
### 创建新项目 |
|||
请参阅以下教程,了解如何使用预构建的应用程序启动模板开始使用ABP框架: |
|||
|
|||
本教程使用 **ABP CLI** 创建一个新项目. 更多选项, 请参阅[入门](https://abp.io/get-started)页面. |
|||
* [ASP.NET Core MVC / Razor Pages UI 入门](Getting-Started?UI=MVC&DB=EF&Tiered=No) |
|||
* [Angular UI 入门](Getting-Started?UI=NG&DB=EF&Tiered=No) |
|||
|
|||
如果你之前未安装,请使用命令行安装ABP CLI: |
|||
|
|||
````bash |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
```` |
|||
|
|||
在空文件夹中使用 `abp new` 命令来创建项目: |
|||
|
|||
````bash |
|||
abp new Acme.BookStore |
|||
```` |
|||
|
|||
> 你可以使用不同级别的命名空间; 例如BookStore, Acme.BookStore或Acme.Retail.BookStore. |
|||
|
|||
`new` 命令创建**分层MVC应用程序**, **Entity Framework Core**作为数据库提供程序. 但是,它还有其他选择. 有关所有可用选项,请参见[CLI文档](CLI.md) |
|||
|
|||
#### 预先要求 |
|||
|
|||
创建项目的要求: |
|||
|
|||
* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) |
|||
* [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) |
|||
* [Node v12+](https://nodejs.org) |
|||
* [Yarn v1.19+](https://classic.yarnpkg.com/) |
|||
|
|||
### 解决方案结构 |
|||
|
|||
在**Visual Studio**中打开解决方案: |
|||
|
|||
 |
|||
|
|||
该解决方案具有分层结构(基于[Domain Driven Design](Domain-Driven-Design.md)), 并包含配置好的的单元&集成测试项目,可与**EF Core**和**SQLite**数据库内存一起使用. |
|||
|
|||
> 请参阅[应用程序模板文档](Startup-Templates/Application.md)以详细了解解决方案结构. |
|||
|
|||
### 数据库连接字符串 |
|||
|
|||
查看`.Web`项目下`appsettings.json`文件中的 **连接字符串**: |
|||
|
|||
````json |
|||
{ |
|||
"ConnectionStrings": { |
|||
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" |
|||
} |
|||
} |
|||
```` |
|||
|
|||
解决方案使用 **Entity Framework Core** 和 **MS SQL Server**. EF Core支持[各种](https://docs.microsoft.com/zh-cn/ef/core/providers/)数据库提供程序,因此你可以根据实际需要使用其他DBMS. 如果需要,请更改连接字符串. |
|||
|
|||
### 创建数据库并应用数据库迁移 |
|||
|
|||
你有两个选项来创建数据库. |
|||
|
|||
#### 使用DbMigrator应用程序 |
|||
|
|||
该解决方案包含一个控制台应用程序(在此示例中名为`Acme.BookStore.DbMigrator`),可以创建数据库,应用迁移和初始化数据. 它对开发和生产环境都很有用. |
|||
|
|||
> `.DbMigrator`项目有自己的`appsettings.json`. 因此,如果你更改了上面的连接字符串,则还应更改此字符串. |
|||
|
|||
右键单击`.DbMigrator`项目并选择 **设置为启动项目**: |
|||
|
|||
 |
|||
|
|||
按F5(或Ctrl + F5)运行应用程序. 它将具有如下所示的输出: |
|||
|
|||
 |
|||
|
|||
#### 使用EF Core Update-Database命令 |
|||
|
|||
Ef Core具有`Update-Database`命令, 可根据需要创建数据库并应用挂起的迁移. 右键单击`.Web`项目并选择**设置为启动项目**: |
|||
|
|||
 |
|||
|
|||
打开**包管理器控制台(Package Manager Console)**, 选择`.EntityFrameworkCore.DbMigrations`项目作为**默认项目**并运行`Update-Database`命令: |
|||
|
|||
 |
|||
|
|||
这将基于配置的连接字符串创建新数据库. |
|||
|
|||
> 使用`.Migrator`工具是建议的方法, 因为它还能初始化初始数据能够正确运行Web应用程序. |
|||
|
|||
### 运行应用程序 |
|||
|
|||
你现在可以运行应用程序,它将会打开**home**页面: |
|||
|
|||
 |
|||
|
|||
单击 **登录** 按钮, 输入用户名`admin`, 密码`1q2w3E*`, 登录应用程序. |
|||
|
|||
启动模板包括**身份管理**和**租户管理**模块. 登录后,将显示"管理"菜单, 你可以在其中管理**租户**,**角色**,**用户**和**权限**. 用户管理页面如下所示: |
|||
|
|||
 |
|||
|
|||
### 下一步是什么? |
|||
|
|||
* [应用程序开发教程](Tutorials/AspNetCore-Mvc/Part-I.md) |
|||
<!-- TODO: this document has been moved, it should be deleted in the future. --> |
|||
@ -1,6 +1,8 @@ |
|||
# 启动模板入门 |
|||
|
|||
参阅下面的教程来学习如何开始使用的ABP框架预构建的应用程序启动模板: |
|||
请参阅以下教程,了解如何使用预构建的应用程序启动模板开始使用ABP框架: |
|||
|
|||
* [ASP.NET Core MVC/Razor页面模板入门](Getting-Started-AspNetCore-MVC-Template.md) |
|||
* [Angular UI模板入门](Getting-Started-Angular-Template.md) |
|||
* [ASP.NET Core MVC / Razor Pages UI 入门](Getting-Started?UI=MVC&DB=EF&Tiered=No) |
|||
* [Angular UI 入门](Getting-Started?UI=NG&DB=EF&Tiered=No) |
|||
|
|||
<!-- TODO: this document has been moved, it should be deleted in the future. --> |
|||
@ -0,0 +1,418 @@ |
|||
## 入门 |
|||
|
|||
````json |
|||
//[doc-params] |
|||
{ |
|||
"UI": ["MVC","NG"], |
|||
"DB": ["EF", "Mongo"], |
|||
"Tiered": ["Yes", "No"] |
|||
} |
|||
```` |
|||
|
|||
本教程介绍了如何创建一个新的{{if UI == "MVC"}} ASP.NET Core MVC web {{else if UI == "NG"}} Angular {{end}}. 配置并运行它. |
|||
|
|||
## 设置你的开发环境 |
|||
|
|||
创建第一个项目之前,需要正确的设置你的开发环境. |
|||
|
|||
### 预先要求 |
|||
|
|||
你需要安装以下工具: |
|||
|
|||
* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) for Windows / [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/). |
|||
* [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) |
|||
|
|||
* [Node v12+](https://nodejs.org) |
|||
* [Yarn v1.19+](https://classic.yarnpkg.com/) |
|||
{{ if Tiered == "Yes" }} |
|||
|
|||
* [Redis](https://redis.io/): 应用程序将Redis用作[分布式缓存](../Caching.md). 因此你需要安装并运行Redis. |
|||
|
|||
{{ end }} |
|||
|
|||
> 你可以也使用其他支持.NET Core 和 ASP.NET Core的编辑器. |
|||
|
|||
### 安装ABP CLI |
|||
|
|||
[ABP CLI](./CLI.md)是一个命令行页面,用于为基于ABP的应用程序验证和自动化一些任务. |
|||
|
|||
> ABP CLI是[ABP框架](https://abp.io/)一个免费开源的工具. |
|||
|
|||
你需要使用以下命令安排ABP CLI: |
|||
|
|||
````shell |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
```` |
|||
|
|||
如果你已经安装,你可以使用以下命令更新到最新版本: |
|||
|
|||
````shell |
|||
dotnet tool update -g Volo.Abp.Cli |
|||
```` |
|||
|
|||
## 创建新项目 |
|||
|
|||
> 本文假设你使用 **{{ UI_Value }}** 做为UI框架 **{{ DB_Value }}** 做为数据库提供程序,对于其它选项,你可以更改文档顶部的首选项. |
|||
|
|||
### 使用ABP CLI创建一个新项目 |
|||
|
|||
使用ABP CLI的 `new` 命令创建新项目: |
|||
|
|||
````shell |
|||
abp new Acme.BookStore -t app{{if UI == "NG"}} -u angular {{end}}{{if DB == "Mongo"}} -d mongodb{{end}}{{if Tiered == "Yes" && UI != "NG"}} --tiered {{else if Tiered == "Yes" && UI == "NG"}}--separate-identity-server{{end}} |
|||
```` |
|||
|
|||
* `-t` 参数指定 [启动模板](Startup-Templates/Application.md) 名称. `app` 是一个启动模板名称,包含了预安装并且配置好的[ABP模块](Modules/Index.md). |
|||
|
|||
{{ if UI == "NG" }} |
|||
|
|||
* `-u` 指定UI框架, 本例中是 `angular`. |
|||
|
|||
{{ if Tiered == "Yes" }} |
|||
|
|||
* `--separate-identity-server` 参数用于将Identity服务器应用程序与API主机应用程序分隔开. 如果未指定,你将只有一个端点. |
|||
|
|||
{{ end }} |
|||
|
|||
{{ end }} |
|||
|
|||
{{ if DB == "Mongo" }} |
|||
|
|||
* `-d` 指定数据库提供程序, 本例中是 `mongodb`. |
|||
|
|||
{{ end }} |
|||
|
|||
{{ if Tiered == "Yes" && UI != "NG" }} |
|||
|
|||
* `--tiered` 参数用于创建n层解决方案,其中身份验证服务器层,UI层和API层在物理上是分离的. |
|||
|
|||
{{ end }} |
|||
|
|||
> 你可以使用不同级别的命令空间; 例如. BookStore, Acme.BookStore or Acme.Retail.BookStore. |
|||
|
|||
## 解决方案结构 |
|||
|
|||
{{ if UI == "MVC" }} |
|||
|
|||
创建项目后你会有以下解决方案目录和文件: |
|||
|
|||
 |
|||
|
|||
在Visual Studio中打开 `.sln` 文件时,将看到以下解决方案结构: |
|||
|
|||
{{if DB == "Mongo"}} |
|||
|
|||
 |
|||
|
|||
{{else}} |
|||
|
|||
 |
|||
|
|||
{{end}} |
|||
|
|||
{{ else if UI == "NG" }} |
|||
|
|||
在创建的解决方案中有三个文件夹: |
|||
|
|||
 |
|||
|
|||
* `angular` 文件夹包含Angular UI应用程序. |
|||
* `aspnet-core` 文件夹包含后端应用程序. |
|||
* `react-native` 文件夹包含React Native UI 应用程序. |
|||
|
|||
打开 `aspnet-core` 文件夹下的 `.sln`(`Visual Studio`解决方案)文件: |
|||
|
|||
 |
|||
|
|||
{{ end }} |
|||
|
|||
> ###### 关于解决方案中的项目 |
|||
> |
|||
> 根据你的**UI**,**数据库**和其他选项,你的解决方案的结构可能略有不同. |
|||
|
|||
该解决方案具有分层结构(基于[Domain Driven Design](Domain-Driven-Design.md)), 并包含配置好的的单元&集成测试项目. |
|||
|
|||
{{ if DB == "EF" }} |
|||
|
|||
集成测试项目已配置为可与 **EF Core** & **SQLite 内存** database同时使用. |
|||
|
|||
{{ else if DB == "Mongo" }} |
|||
|
|||
集成测试项目已配置为每个测试创建的内存中的**MongoDB**数据库(使用的[Mongo2Go](https://github.com/Mongo2Go/Mongo2Go)库). |
|||
|
|||
{{ end }} |
|||
|
|||
> 请参阅[应用程序模板文档](Startup-Templates/Application.md)详细了解解决方案结构. |
|||
|
|||
## 创建数据库 |
|||
|
|||
### 数据库连接字符串 |
|||
|
|||
检查 {{if UI == "MVC"}}{{if Tiered == "Yes"}}`.IdentityServer` 和 `.HttpApi.Host` 项目{{else}}`.Web` 项目{{end}}{{else if UI == "NG" }}`.HttpApi.Host` 项目{{end}}下 `appsettings.json` 文件中的 **链接字符串**: |
|||
|
|||
{{ if DB == "EF" }} |
|||
|
|||
````json |
|||
"ConnectionStrings": { |
|||
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" |
|||
} |
|||
```` |
|||
|
|||
该解决方案配置为**Entity Framework Core**与**MS SQL Server**一起使用. EF Core支持[各种](https://docs.microsoft.com/en-us/ef/core/providers/)数据库提供程序,因此你可以使用任何受支持的DBMS. 请参阅[Entity Framework集成文档](https://docs.abp.io/en/abp/latest/Entity-Framework-Core)了解如何切换到另一个DBMS. |
|||
|
|||
### 数据库连接字符串 |
|||
|
|||
查看`.Web`项目下`appsettings.json`文件中的 **连接字符串**: |
|||
|
|||
````json |
|||
{ |
|||
"ConnectionStrings": { |
|||
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" |
|||
} |
|||
} |
|||
```` |
|||
|
|||
解决方案使用 **Entity Framework Core** 和 **MS SQL Server**. EF Core支持[各种](https://docs.microsoft.com/zh-cn/ef/core/providers/)数据库提供程序,因此你可以根据实际需要使用其他DBMS. 如果需要,请更改连接字符串. |
|||
|
|||
### 应用迁移 |
|||
|
|||
该解决方案使用[Entity Framework Core Code First 迁移](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli). 你需要应用迁移来创建数据库,有两种方法迁移数据库. |
|||
|
|||
#### 使用DbMigrator应用程序应用迁移 |
|||
|
|||
该解决方案包含一个控制台应用程序(在此示例中名为`Acme.BookStore.DbMigrator`),可以创建数据库,应用迁移和初始化数据. 它对开发和生产环境都很有用. |
|||
|
|||
> `.DbMigrator`项目有自己的`appsettings.json`. 因此,如果你更改了上面的连接字符串,则还应更改此字符串. |
|||
|
|||
右键单击`.DbMigrator`项目并选择 **设置为启动项目**: |
|||
|
|||
 |
|||
|
|||
按F5(或Ctrl + F5)运行应用程序. 它将具有如下所示的输出: |
|||
|
|||
 |
|||
|
|||
#### 使用EF Core Update-Database命令 |
|||
|
|||
Ef Core具有`Update-Database`命令, 可根据需要创建数据库并应用挂起的迁移. 右键单击`.Web`项目并选择**设置为启动项目**: |
|||
|
|||
{{ if UI == "MVC" }} |
|||
|
|||
右键单击{{if Tiered == "Yes"}}`.IdentityServer`{{else}}`.Web`{{end}}项目并选择**设置为启动项目**: |
|||
|
|||
{{ else if UI != "MVC" }} |
|||
|
|||
右键单击`.HttpApi.Host`项目并选择**设置为启动项目**: |
|||
|
|||
{{ end }} |
|||
|
|||
 |
|||
|
|||
打开**包管理器控制台(Package Manager Console)**, 选择`.EntityFrameworkCore.DbMigrations`项目作为**默认项目**并运行`Update-Database`命令: |
|||
|
|||
 |
|||
|
|||
这将基于配置的连接字符串创建新数据库. |
|||
|
|||
> 使用`.Migrator`工具是建议的方法, 因为它还能初始化初始数据能够正确运行Web应用程序. |
|||
|
|||
{{ else if DB == "Mongo" }} |
|||
|
|||
````json |
|||
"ConnectionStrings": { |
|||
"Default": "mongodb://localhost:27017/BookStore" |
|||
} |
|||
```` |
|||
|
|||
该解决方案被配置为在你的本地计算机中使用 **MongoDB**,因此你需要启动并运行一个MongoDB服务器实例或者将连接字符串更改为另一个MongoDB服务器. |
|||
|
|||
### 初始化种子数据 |
|||
|
|||
该解决方案附带一个 `.DbMigrator` 控制台应用程序,该应用程序为初始数据提供了种子. 它对于开发以及生产环境都很有用. |
|||
|
|||
> `.DbMigrator` 项目有自己的 `appsettings.json`.如果你更改了其他项目的 `appsettings.json`,也应该更改这个. |
|||
|
|||
右键点击 `.DbMigrator` 并选择 **设置为启动项目**. |
|||
|
|||
 |
|||
|
|||
按F5(或Ctrl+F5)启动应用程序,你会看到以下输出: |
|||
|
|||
 |
|||
|
|||
> 数据库创建后会初始化种子数据, 其中包含用于登录的 `admin` 用户. 所以你至少使用 `.DbMigrator` 一次. |
|||
|
|||
{{ end }} |
|||
|
|||
### 运行应用程序 |
|||
|
|||
{{ if UI == "MVC" }} |
|||
|
|||
{{ if Tiered == "Yes" }} |
|||
|
|||
确保 `.IdentityServer` 是启动项目,运行应用程序后会在你的浏览器打开一个 **login** 页面. |
|||
|
|||
> 在Visual Studio中使用Ctrl+F5(而不是F5)运行应用,如果你不用于调试,这会减少启动时间. |
|||
|
|||
你可以登录,但是不能在这里进入主应用程序,它仅是验证服务器. |
|||
|
|||
确保 `.HttpApi.Host` 是启动项目,运行应用程序后会在你的浏览器打开一个 **Swagger UI** 页面. |
|||
|
|||
 |
|||
|
|||
这里是Web应用程序使用的API应用程序. |
|||
|
|||
最后确保 `.Web` 是启动项目,运行应用程序后会在你的浏览器打开一个 **welcome** 页面. |
|||
|
|||
 |
|||
|
|||
点击 **login** 按钮重定向到 `Identity Server` 来登录应用程序. |
|||
|
|||
 |
|||
|
|||
{{ else }} |
|||
|
|||
最后确保 `.Web` 是启动项目,运行应用程序后会在你的浏览器打开一个 **login** 页面. |
|||
|
|||
> 在Visual Studio中使用Ctrl+F5(而不是F5)运行应用,如果你不用于调试,这会减少启动时间. |
|||
|
|||
 |
|||
|
|||
{{ end }} |
|||
|
|||
{{ else if UI != "MVC" }} |
|||
|
|||
#### 运行HTTP API Host (服务器端) |
|||
|
|||
{{ if Tiered == "Yes" }} |
|||
|
|||
确保 `.IdentityServer` 是启动项目,运行应用程序后会在你的浏览器打开一个 **login** 页面. |
|||
|
|||
> 在Visual Studio中使用Ctrl+F5(而不是F5)运行应用,如果你不用于调试,这会减少启动时间. |
|||
|
|||
你可以登录,但是不能在这里进入主应用程序,它仅是验证服务器. |
|||
|
|||
{{ end }} |
|||
|
|||
确保 `.HttpApi.Host` 是启动项目,运行应用程序后会在你的浏览器打开一个 **Swagger UI** 页面. |
|||
|
|||
{{ if Tiered == "No" }} |
|||
|
|||
> 在Visual Studio中使用Ctrl+F5(而不是F5)运行应用,如果你不用于调试,这会减少启动时间. |
|||
|
|||
{{ end }} |
|||
|
|||
 |
|||
|
|||
你可以看到应用程序的API并进行测试. 更多信息,请参阅[Swagger UI](https://swagger.io/tools/swagger-ui/). |
|||
|
|||
> ##### Swagger UI 授权 |
|||
> |
|||
> 大多数的HTTP API都需要身份验证和授权. 如果你要测试授权API, 请手动进入 `/Account/Login` 页面, 输入用户名: `admin` 和密码: `1q2w3E*` 登录到应用程序. 然后你可以访问授权API. |
|||
|
|||
{{ end }} |
|||
|
|||
{{ if UI == "NG" }} |
|||
#### 运行 Angular 应用程序 (客户端) |
|||
|
|||
在 `angular` 下打开命令行终端, 输入 `yarn` 命令(我们推荐使用[yarn](https://yarnpkg.com/)包管理, `npm install` 在大多数情况下也可以工作). |
|||
|
|||
```bash |
|||
yarn |
|||
``` |
|||
|
|||
等到所有node模块加载成功, 执行 `yarn start` (或 `npm start`) 命令: |
|||
|
|||
```bash |
|||
yarn start |
|||
``` |
|||
|
|||
等待 `Angular CLI` 使用 `BrowserSync` 启动 `Webpack` dev-server. |
|||
它会负责编译你的 `TypeScript`代码, 并自动重新加载浏览器. |
|||
完成后 `Angular Live Development Server` 会监听 localhost:4200. |
|||
打开你的浏览器并导航到[localhost:4200](http://localhost:4200/). |
|||
|
|||
 |
|||
|
|||
{{ end }} |
|||
|
|||
输入用户名 **admin**,密码 **1q2w3E*** 登录到应用程序. |
|||
|
|||
 |
|||
|
|||
应用程序已经启动并执行,你可以基于该启动模板开发应用程序. |
|||
|
|||
#### 移动开发 |
|||
|
|||
ABP平台提供了[React Native](https://reactnative.dev/)模板用于开发移动应用程序. |
|||
|
|||
> 该解决方案默认 `react-native` 包含了React Native应用程序,如果你不计划使用React Native开发移动应用程序,你可以忽略并删除 `react-native` 文件夹. |
|||
|
|||
运行在Android模拟器或真机上的React Native应用程序无法连接到 `localhost` 上的后.要修复此问题,需要在本地IP上运行后端. |
|||
|
|||
{{ if Tiered == "No"}} |
|||
 |
|||
|
|||
* 打开 `.HttpApi.Host` 文件夹下的 `appsettings.json` 文件. 将 `SelfUrl` 和 `Authority` 属性的 `localhost` 替换为你本地的IP地址. |
|||
* 打开 `.HttpApi.Host/Properties` 文件夹下的 `launchSettings.json` 文件. 将 `applicationUrl` 属性的 `localhost` 替换为你本地的IP地址. |
|||
|
|||
{{ else if Tiered == "Yes" }} |
|||
|
|||
 |
|||
|
|||
* 打开 `.IdentityServer` 文件夹下的 `appsettings.json` 文件. 将 `SelfUrl` 属性的 `localhost` 替换为你本地的IP地址. |
|||
* 打开 `.IdentityServer/Properties` 文件夹下的 `launchSettings.json` 文件. 将 `applicationUrl` 属性的 `localhost` 替换为你本地的IP地址. |
|||
* 打开 `.HttpApi.Host` 文件夹下的 `appsettings.json` 文件. 将 `Authority` 属性的 `localhost` 替换为你本地的IP地址. |
|||
* 打开 `.HttpApi.Host/Properties` 文件夹下的 `launchSettings.json` 文件. 将 `applicationUrl` 属性的 `localhost` 替换为你本地的IP地址. |
|||
|
|||
{{ end }} |
|||
|
|||
按照**运行HTTP API Host (服务端口)**那样运行后端. |
|||
|
|||
> React Native应用程序不信任自动生成的.NET HTTPS证书,你可以在开发期间使用HTTP. |
|||
|
|||
在 `react-native` 文件夹打开命令行终端,输入 `yarn` 命令(我们推荐使用[yarn](https://yarnpkg.com/)包管理, `npm install` 在大多数情况下也可以工作). |
|||
|
|||
```bash |
|||
yarn |
|||
``` |
|||
|
|||
* 打开 `react-nativer` 文件夹下的 `Environment.js` 文件. 将 `apiUrl` 和 `issuer` 属性的 `localhost` 替换为你本地的IP地址: |
|||
|
|||
 |
|||
|
|||
{{ if Tiered == "Yes" }} |
|||
|
|||
> 确保 `issuer` 与正在运行的 `.IdentityServer` 项目匹配, `apiUrl` 与正在运行的 `.HttpApi.Host` 项目匹配. |
|||
|
|||
{{else}} |
|||
|
|||
> 确保 `issuer` 和 `apiUrl` 与正在运行的 `.HttpApi.Host` 项目匹配 |
|||
|
|||
{{ end }} |
|||
|
|||
等到所有node模块加载成功, 执行 `yarn start` (或 `npm start`) 命令: |
|||
|
|||
```bash |
|||
yarn start |
|||
``` |
|||
|
|||
等待Expo CLI启动后Expo CLI在 `http://localhost:19002/` 地址要开管理页面. |
|||
|
|||
 |
|||
|
|||
在上面的管理界面中,可以通过使用[Expo Client](https://expo.io/tools#client)扫描二维码,使用Android模拟器,iOS模拟器或真机来启动应用程序. |
|||
|
|||
> 请参阅[expo.io](https://docs.expo.io/versions/v36.0.0/workflow/ios-simulator/)上的[Android Studio模拟器](https://docs.expo.io/versions/v36.0.0/workflow/android-studio-emulator/)和[iOS模拟器文档](https://docs.expo.io/versions/v36.0.0/workflow/android-studio-emulator/). |
|||
|
|||
 |
|||
|
|||
输入用户名 **admin**,密码 **1q2w3E*** 登录到应用程序. |
|||
|
|||
应用程序已经启动并执行,你可以基于该启动模板开发应用程序. |
|||
|
|||
> [应用程序启动模板](Startup-Templates/Application.md) 包含租户管理和Identity模块. |
|||
|
|||
## 下一步是什么? |
|||
|
|||
[应用程序开发教程](Tutorials/Part-1.md) |
|||
@ -0,0 +1,59 @@ |
|||
# 示例应用 |
|||
|
|||
这些是ABP框架创建的官方示例. 这些示例大部分在[abpframework/abp-samples](https://github.com/abpframework/abp-samples) GitHub 仓库. |
|||
|
|||
### 微服务示例 |
|||
|
|||
演示如何基于微服务体系结构构建系统的完整解决方案. |
|||
|
|||
* [示例的文档](Microservice-Demo.md) |
|||
* [源码](https://github.com/abpframework/abp/tree/dev/samples/MicroserviceDemo) |
|||
* [微服务架构文档](../Microservice-Architecture.md) |
|||
|
|||
### Book Store |
|||
|
|||
一个简单的CRUD应用程序,展示了使用ABP框架开发应用程序的基本原理. 使用不同的技术实现了相同的示例: |
|||
|
|||
* **Book Store: Razor Pages UI & Entity Framework Core** |
|||
|
|||
* [教程](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC) |
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/BookStore) |
|||
|
|||
* **Book Store: Angular UI & MongoDB** |
|||
|
|||
* [教程](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=NG) |
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) |
|||
|
|||
* **Book Store: Modular application (Razor Pages UI & EF Core)** |
|||
|
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/BookStore-Modular) |
|||
|
|||
如果没有Razor Pages & MongoDB 结合,但你可以检查两个文档来理解它,因为DB和UI不会互相影响. |
|||
|
|||
### 其他示例 |
|||
|
|||
* **Entity Framework 迁移**: 演示如何将应用程序拆分为多个数据库的解决方案. 每个数据库包含不同的模块. |
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) |
|||
* [EF Core数据库迁移文档](../Entity-Framework-Core-Migrations.md) |
|||
* **SignalR Demo**: A simple chat application that allows to send and receive messages among authenticated users. |
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) |
|||
* [SignalR 集成文档](../SignalR-Integration.md) |
|||
* **Dashboard Demo**: 一个简单的应用程序,展示了如何在ASP.NET Core MVC UI中使用widget系统. |
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) |
|||
* [Widget 文档](../UI/AspNetCore/Widgets.md) |
|||
* **RabbitMQ 事件总线 Demo**: 由两个通过RabbitMQ集成的分布式事件相互通信的应用程序组成的解决方案. |
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/RabbitMqEventBus) |
|||
* [分布式事件总线文档](../Distributed-Event-Bus.md) |
|||
* [RabbitMQ 分布式事件总线集成文档](../Distributed-Event-Bus-RabbitMQ-Integration.md) |
|||
* **自定义认证**: 如何为ASP.NET Core MVC / Razor Pages应用程序自定义身份验证的解决方案. |
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization) |
|||
* 相关 "[How To](../How-To/Index.md)" 文档: |
|||
* [Azure Active Directory 认证](../How-To/Azure-Active-Directory-Authentication-MVC.md) |
|||
* [自定义登录页面](../How-To/Customize-Login-Page-MVC.md) |
|||
* [自定义 SignIn Manager](../How-To/Customize-SignIn-Manager.md) |
|||
* **空的ASP.NET Core应用程序**: 从基本的ASP.NET Core应用程序使用ABP框架. |
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication) |
|||
* [文档](../Getting-Started-AspNetCore-Application.md) |
|||
* **空的控制台应用程序**: 从基本的控制台应用程序安装ABP框架. |
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication) |
|||
* [文档](../Getting-Started-Console-Application.md) |
|||
@ -0,0 +1,227 @@ |
|||
# SignalR 集成 |
|||
|
|||
> 你可以按照[标准的微软教程](https://docs.microsoft.com/en-us/aspnet/core/tutorials/signal)添加[SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction)到你的应用程序,但ABP提供了简化集成的SignalR集成包. |
|||
|
|||
## 安装 |
|||
|
|||
### 服务器端 |
|||
|
|||
建议使用[ABP CLI](CLI.md)安装包. |
|||
|
|||
#### 使用 ABP CLI |
|||
|
|||
在项目的文件夹(.csproj文件)中打开命令行窗口,然后输入以下命令: |
|||
|
|||
```bash |
|||
abp add-package Volo.Abp.AspNetCore.SignalR |
|||
``` |
|||
|
|||
> 你通常需要将此软件包添加到应用程序的Web或API层,具体取决于你的架构. |
|||
|
|||
#### 手动安装 |
|||
|
|||
如果你想手动安装: |
|||
|
|||
1. 添加[Volo.Abp.AspNetCore.SignalR](https://www.nuget.org/packages/Volo.Abp.AspNetCore.SignalR)NuGet包到你的项目: |
|||
|
|||
``` |
|||
Install-Package Volo.Abp.AspNetCore.SignalR |
|||
``` |
|||
|
|||
或者使用VisualStudio提供的UI安装 |
|||
|
|||
2. 添加 `AbpAspNetCoreSignalRModule` 到你的模块的依赖列表. |
|||
|
|||
```csharp |
|||
[DependsOn( |
|||
//...other dependencies |
|||
typeof(AbpAspNetCoreSignalRModule) //Add the new module dependency |
|||
)] |
|||
public class YourModule : AbpModule |
|||
{ |
|||
} |
|||
``` |
|||
|
|||
> 你不需要 `services.AddSignalR()` 和 `app.UseEndpoints(...)`,它们在 `AbpAspNetCoreSignalRModule` 中已经添加了. |
|||
|
|||
### 客户端 |
|||
|
|||
客户端安装取决于你的UI框架/客户端类型. |
|||
|
|||
#### ASP.NET Core MVC / Razor Pages UI |
|||
|
|||
在你的Web项目的根文件夹中运行以下命令: |
|||
|
|||
````bash |
|||
yarn add @abp/signalr |
|||
```` |
|||
|
|||
> 需要 [yarn](https://yarnpkg.com/) 环境. |
|||
|
|||
它会添加 `@abp/signalr` 到你的项目中的 `package.json` 依赖项: |
|||
|
|||
````json |
|||
{ |
|||
... |
|||
"dependencies": { |
|||
... |
|||
"@abp/signalr": "~2.7.0" |
|||
} |
|||
} |
|||
```` |
|||
|
|||
在你的Web项目的根文件夹中运行 `gulp`: |
|||
|
|||
````bash |
|||
gulp |
|||
```` |
|||
|
|||
它会将SignalR JavaScript文件拷贝到你的项目: |
|||
|
|||
 |
|||
|
|||
最后将以下代码添加到页面/视图中, 添加包含 `signalr.js` 文件: |
|||
|
|||
````xml |
|||
@section scripts { |
|||
<abp-script type="typeof(SignalRBrowserScriptContributor)" /> |
|||
} |
|||
```` |
|||
|
|||
它需要将 `@using Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR` 添加到你的页面/视图. |
|||
|
|||
> 你可以用标准方式添加 `signalr.js` 文件. 但是使用 `SignalRBrowserScriptContributor` 具有其他好处. 有关详细信息,请参见[客户端程序包管理](UI/AspNetCore/Client-Side-Package-Management.md)和[捆绑和压缩文档](UI/AspNetCore/Bundling-Minification.md). |
|||
|
|||
这就是全部了,你可以在你的页面使用[SignalR JavaScript API](https://docs.microsoft.com/en-us/aspnet/core/signalr/javascript-client). |
|||
|
|||
#### 其他的UI框架/客户端 |
|||
|
|||
其他类型的客户端请参考[微软文档](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction). |
|||
|
|||
## ABP框架集成 |
|||
|
|||
本节介绍了使用ABP框架集成包的其他好处. |
|||
|
|||
### Hub 路由与Mapping |
|||
|
|||
ABP自动将所有集线器注册到[依赖注入](Dependency-Injection.md)(做为transient)并映射集线器端点. 因此你不需要使用 `app.UseEndpoints(...)` 即可映射你的集线器.集线器路由(URL)通常是根据你的集线器名称确定. |
|||
|
|||
示例: |
|||
|
|||
````csharp |
|||
public class MessagingHub : Hub |
|||
{ |
|||
//... |
|||
} |
|||
```` |
|||
|
|||
`MessasingHub` 集线器的路由为 `/signalr-hubs/messasing`: |
|||
|
|||
* 添加了标准 `/signalr-hubs/` 前缀. |
|||
* 使用**驼峰命名**集线器名称,不包含 `Hub` 后缀. |
|||
|
|||
如果你想指定路由,你可以使用 `HubRoute` attribute: |
|||
|
|||
````csharp |
|||
[HubRoute("/my-messasing-hub")] |
|||
public class MessagingHub : Hub |
|||
{ |
|||
//... |
|||
} |
|||
```` |
|||
|
|||
### AbpHub 基类 |
|||
|
|||
你可以从 `AbpHub` 或 `AbpHub<T>` 继承标准的 `Hub` 和 `Hub<T>` 类,它们具有实用的基本属性,如 `CurrentUser`. |
|||
|
|||
示例: |
|||
|
|||
````csharp |
|||
public class MessagingHub : AbpHub |
|||
{ |
|||
public async Task SendMessage(string targetUserName, string message) |
|||
{ |
|||
var currentUserName = CurrentUser.UserName; //Access to the current user info |
|||
var txt = L["MyText"]; //Localization |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> 虽然可以将相同的属性注入到集线器构造函数中,但是这种方式简化了集线器类. |
|||
|
|||
### 手动注册/Mapping |
|||
|
|||
ABP会自动将所有集线器注册到[依赖注入](Dependency-Injection.md)作为**transient service**. 如果想要禁用集线器类**自动添加依赖注入**,只需要使用 `DisableConventionalRegistration` attribute. 如果愿意,你仍然可以在模块的 `ConfigureServices` 方法中注册集线器类: |
|||
|
|||
````csharp |
|||
context.Services.AddTransient<MessagingHub>(); |
|||
```` |
|||
|
|||
当**你或ABP**将类注册到依赖注入时,如前几节所述,它会自动映射到端点路由配置. 如果要手动映射集线器类,你可以使用 `DisableAutoHubMap` attribute. |
|||
|
|||
对于手动映射,你有两个选择: |
|||
|
|||
1. 使用 `AbpSignalROptions` 添加map配置(在[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法中),ABP会为集线器执行端点映射: |
|||
|
|||
````csharp |
|||
Configure<AbpSignalROptions>(options => |
|||
{ |
|||
options.Hubs.Add( |
|||
new HubConfig( |
|||
typeof(MessagingHub), //Hub type |
|||
"/my-messaging/route", //Hub route (URL) |
|||
hubOptions => |
|||
{ |
|||
//Additional options |
|||
hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); |
|||
} |
|||
) |
|||
); |
|||
}); |
|||
```` |
|||
|
|||
这是提供其他SignalR选项的好方式. |
|||
|
|||
如果你不想禁用自动集线器map,但仍想执行其他SignalR配置,可以使用 `options.Hubs.AddOrUpdate(...)` 方法: |
|||
|
|||
````csharp |
|||
Configure<AbpSignalROptions>(options => |
|||
{ |
|||
options.Hubs.AddOrUpdate( |
|||
typeof(MessagingHub), //Hub type |
|||
config => //Additional configuration |
|||
{ |
|||
config.RoutePattern = "/my-messaging-hub"; //override the default route |
|||
config.ConfigureActions.Add(hubOptions => |
|||
{ |
|||
//Additional options |
|||
hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); |
|||
}); |
|||
} |
|||
); |
|||
}); |
|||
```` |
|||
|
|||
你可以通过这种方式修改在依赖模块(没有源代码访问权限)中定义的集线器类的选项. |
|||
|
|||
2. 在[模块](Module-Development-Basics.md)的 `OnApplicationInitialization` 方法中更改 `app.UseConfiguredEndpoints`(添加了lambda方法作为参数). |
|||
|
|||
````csharp |
|||
app.UseConfiguredEndpoints(endpoints => |
|||
{ |
|||
endpoints.MapHub<MessagingHub>("/my-messaging-hub", options => |
|||
{ |
|||
options.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
### UserIdProvider |
|||
|
|||
ABP实现 `SignalR` 的 `IUserIdProvider` 接口,从ABP框架的 `ICurrentUser` 服务提供当前用户ID(请参阅[当前用户服务](CurrentUser.md)),它将集成到应用程序的身份验证系统中,实现类是 `AbpSignalRUserIdProvider` (如果你想更改/覆盖它). |
|||
|
|||
## 示例应用程序 |
|||
|
|||
参阅 [SignalR集成Demo](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo),它有一个简单的聊天页面,可以在(经过身份验证的)用户之间发送消息. |
|||
|
|||
 |
|||
@ -1,3 +1,8 @@ |
|||
## Angular 教程 - 第一章 |
|||
# 教程 |
|||
|
|||
TODO... |
|||
## 应用程序开发 |
|||
|
|||
* [ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) |
|||
* [Angular UI](../Part-1?UI=NG) |
|||
|
|||
<!-- TODO: this document has been moved, it should be deleted in the future. --> |
|||
@ -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)运行应用程序. |
|||
|
|||
### 解决方案的结构 |
|||
|
|||
下面的图片展示了从启动模板创建的项目是如何分层的. |
|||
|
|||
 |
|||
|
|||
> 你可以查看[应用程序模板文档](../startup-templates/application#solution-structure)以详细了解解决方案结构.但是,你将通过本教程了解基础知识. |
|||
|
|||
### 创建Book实体 |
|||
|
|||
启动模板中的域层分为两个项目: |
|||
|
|||
- `Acme.BookStore.Domain`包含你的[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities), [领域服务](https://docs.abp.io/zh-Hans/abp/latest/Domain-Services)和其他核心域对象. |
|||
- `Acme.BookStore.Domain.Shared`包含可与客户共享的常量,枚举或其他域相关对象. |
|||
|
|||
在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities). 该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个名为`Book`的类,如下所示: |
|||
|
|||
````C# |
|||
using System; |
|||
using Volo.Abp.Domain.Entities.Auditing; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class Book : AuditedAggregateRoot<Guid> |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public BookType Type { get; set; } |
|||
|
|||
public DateTime PublishDate { get; set; } |
|||
|
|||
public float Price { get; set; } |
|||
|
|||
protected Book() |
|||
{ |
|||
} |
|||
public Book(Guid id, string name, BookType type, DateTime publishDate, float price) |
|||
:base(id) |
|||
{ |
|||
Name = name; |
|||
Type = type; |
|||
PublishDate = publishDate; |
|||
Price = price; |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是**域驱动设计(DDD)** 概念之一. 有关详细信息和最佳做法,请参阅[实体文档](https://docs.abp.io/zh-Hans/abp/latest/Entities). |
|||
* `Book`实体继承了`AuditedAggregateRoot`,`AuditedAggregateRoot`类在`AggregateRoot`类的基础上添加了一些审计属性(`CreationTime`, `CreatorId`, `LastModificationTime` 等). |
|||
* `Guid`是`Book`实体的主键类型. |
|||
* 使用 **数据注解** 为EF Core添加映射.或者你也可以使用 EF Core 自带的[fluent mapping API](https://docs.microsoft.com/en-us/ef/core/modeling). |
|||
|
|||
#### BookType枚举 |
|||
|
|||
上面所用到的`BookType`枚举定义如下: |
|||
|
|||
````C# |
|||
namespace Acme.BookStore |
|||
{ |
|||
public enum BookType |
|||
{ |
|||
Undefined, |
|||
Adventure, |
|||
Biography, |
|||
Dystopia, |
|||
Fantastic, |
|||
Horror, |
|||
Science, |
|||
ScienceFiction, |
|||
Poetry |
|||
} |
|||
} |
|||
```` |
|||
|
|||
#### 将Book实体添加到DbContext中 |
|||
|
|||
EF Core需要你将实体和DbContext建立关联.最简单的做法是在`Acme.BookStore.EntityFrameworkCore`项目的`BookStoreDbContext`类中添加`DbSet`属性.如下所示: |
|||
|
|||
````C# |
|||
public class BookStoreDbContext : AbpDbContext<BookStoreDbContext> |
|||
{ |
|||
public DbSet<Book> Books { get; set; } |
|||
... |
|||
} |
|||
```` |
|||
|
|||
#### 配置你的Book实体 |
|||
|
|||
在`Acme.BookStore.EntityFrameworkCore`项目中打开`BookStoreDbContextModelCreatingExtensions.cs`文件,并将以下代码添加到`ConfigureBookStore`方法的末尾以配置Book实体: |
|||
|
|||
````C# |
|||
builder.Entity<Book>(b => |
|||
{ |
|||
b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema); |
|||
b.ConfigureByConvention(); //auto configure for the base class props |
|||
b.Property(x => x.Name).IsRequired().HasMaxLength(128); |
|||
}); |
|||
```` |
|||
|
|||
#### 添加新的Migration并更新数据库 |
|||
|
|||
这个启动模板使用了[EF Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/)来创建并维护数据库结构.打开 **程序包管理器控制台(Package Manager Console) (PMC)** (工具/Nuget包管理器菜单),选择 `Acme.BookStore.EntityFrameworkCore.DbMigrations`作为默认的项目然后执行下面的命令: |
|||
|
|||
 |
|||
|
|||
这样就会在`Migrations`文件夹中创建一个新的migration类.然后执行`Update-Database`命令更新数据库结构. |
|||
|
|||
```` |
|||
PM> Update-Database |
|||
```` |
|||
|
|||
#### 添加示例数据 |
|||
|
|||
`Update-Database`命令在数据库中创建了`AppBooks`表. 打开数据库并输入几个示例行,以便在页面上显示它们: |
|||
|
|||
 |
|||
|
|||
### 创建应用服务 |
|||
|
|||
下一步是创建[应用服务](../../../Application-Services.md)来管理(创建,列出,更新,删除)书籍. 启动模板中的应用程序层分为两个项目: |
|||
|
|||
* `Acme.BookStore.Application.Contracts`主要包含你的DTO和应用程序服务接口. |
|||
* `Acme.BookStore.Application`包含应用程序服务的实现. |
|||
|
|||
#### BookDto |
|||
|
|||
在`Acme.BookStore.Application.Contracts`项目中创建一个名为`BookDto`的DTO类: |
|||
|
|||
````C# |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookDto : AuditedEntityDto<Guid> |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public BookType Type { get; set; } |
|||
|
|||
public DateTime PublishDate { get; set; } |
|||
|
|||
public float Price { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* **DTO**类被用来在 **表示层** 和 **应用层** **传递数据**.查看[DTO文档](https://docs.abp.io/zh-Hans/abp/latest/Data-Transfer-Objects)查看更多信息. |
|||
* 为了在页面上展示书籍信息,`BookDto`被用来将书籍数据传递到表示层. |
|||
* `BookDto`继承自 `AuditedEntityDto<Guid>`.跟上面定义的`Book`类一样具有一些审计属性. |
|||
|
|||
在将书籍返回到表示层时,需要将`Book`实体转换为`BookDto`对象. [AutoMapper](https://automapper.org)库可以在定义了正确的映射时自动执行此转换. 启动模板配置了AutoMapper,因此你只需在`Acme.BookStore.Application`项目的`BookStoreApplicationAutoMapperProfile`类中定义映射: |
|||
|
|||
````csharp |
|||
using AutoMapper; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookStoreApplicationAutoMapperProfile : Profile |
|||
{ |
|||
public BookStoreApplicationAutoMapperProfile() |
|||
{ |
|||
CreateMap<Book, BookDto>(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
#### CreateUpdateBookDto |
|||
|
|||
在`Acme.BookStore.Application.Contracts`项目中创建一个名为`CreateUpdateBookDto`的DTO类: |
|||
|
|||
````c# |
|||
using System; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using Volo.Abp.AutoMapper; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class CreateUpdateBookDto |
|||
{ |
|||
[Required] |
|||
[StringLength(128)] |
|||
public string Name { get; set; } |
|||
|
|||
[Required] |
|||
public BookType Type { get; set; } = BookType.Undefined; |
|||
|
|||
[Required] |
|||
public DateTime PublishDate { get; set; } |
|||
|
|||
[Required] |
|||
public float Price { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* 这个DTO类被用于在创建或更新书籍的时候从用户界面获取图书信息. |
|||
* 它定义了数据注释属性(如`[Required]`)来定义属性的验证. DTO由ABP框架[自动验证](https://docs.abp.io/zh-Hans/abp/latest/Validation). |
|||
|
|||
就像上面的`BookDto`一样,创建一个从`CreateUpdateBookDto`对象到`Book`实体的映射: |
|||
|
|||
````csharp |
|||
CreateMap<CreateUpdateBookDto, Book>(); |
|||
```` |
|||
|
|||
#### IBookAppService |
|||
|
|||
在`Acme.BookStore.Application.Contracts`项目中定义一个名为`IBookAppService`的接口: |
|||
|
|||
````C# |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public interface IBookAppService : |
|||
ICrudAppService< //定义了CRUD方法 |
|||
BookDto, //用来展示书籍 |
|||
Guid, //Book实体的主键 |
|||
PagedAndSortedResultRequestDto, //获取书籍的时候用于分页和排序 |
|||
CreateUpdateBookDto, //用于创建书籍 |
|||
CreateUpdateBookDto> //用于更新书籍 |
|||
{ |
|||
|
|||
} |
|||
} |
|||
```` |
|||
|
|||
* 框架定义应用程序服务的接口<u>不是必需的</u>. 但是,它被建议作为最佳实践. |
|||
* `ICrudAppService`定义了常见的**CRUD**方法:`GetAsync`,`GetListAsync`,`CreateAsync`,`UpdateAsync`和`DeleteAsync`. 你可以从空的`IApplicationService`接口继承并手动定义自己的方法. |
|||
* `ICrudAppService`有一些变体, 你可以在每个方法中使用单独的DTO,也可以分别单独指定. |
|||
|
|||
|
|||
#### BookAppService |
|||
|
|||
在`Acme.BookStore.Application`项目中实现名为`BookAppService`的`IBookAppService`: |
|||
|
|||
````C# |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookAppService : |
|||
CrudAppService<Book, BookDto, Guid, PagedAndSortedResultRequestDto, |
|||
CreateUpdateBookDto, CreateUpdateBookDto>, |
|||
IBookAppService |
|||
{ |
|||
public BookAppService(IRepository<Book, Guid> repository) |
|||
: base(repository) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `BookAppService`继承了`CrudAppService<...>`.它实现了上面定义的CRUD方法. |
|||
* `BookAppService`注入`IRepository <Book,Guid>`,这是`Book`实体的默认仓储. ABP自动为每个聚合根(或实体)创建默认仓储. 请参阅[仓储文档](https://docs.abp.io/zh-Hans/abp/latest/Repositories) |
|||
* `BookAppService`使用`IObjectMapper`将`Book`对象转换为`BookDto`对象, 将`CreateUpdateBookDto`对象转换为`Book`对象. 启动模板使用[AutoMapper](http://automapper.org/)库作为对象映射提供程序. 你之前定义了映射, 因此它将按预期工作. |
|||
|
|||
### 自动生成API Controllers |
|||
|
|||
你通常创建**Controller**以将应用程序服务公开为**HTTP API**端点. 因此允许浏览器或第三方客户端通过AJAX调用它们. ABP可以[**自动**](https://docs.abp.io/zh-Hans/abp/latest/API/Auto-API-Controllers)按照惯例将你的应用程序服务配置为MVC API控制器. |
|||
|
|||
#### Swagger UI |
|||
|
|||
启动模板配置为使用[Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)运行[swagger UI](https://swagger.io/tools/swagger-ui/). 运行应用程序并在浏览器中输入`https://localhost:XXXX/swagger/`(用你自己的端口替换XXXX)作为URL. |
|||
|
|||
你会看到一些内置的接口和`Book`的接口,它们都是REST风格的: |
|||
|
|||
 |
|||
|
|||
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`)方法中来获取服务返回的结果. |
|||
|
|||
运行这段代码会产生下面的输出: |
|||
|
|||
 |
|||
|
|||
你可以看到服务器返回的 **book list**.你还可以切换到开发者工具的 **network** 查看客户端到服务器端的通讯信息: |
|||
|
|||
 |
|||
|
|||
我们使用`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. |
|||
|
|||
 |
|||
|
|||
打开`Index.cshtml`并把内容修改成下面这样: |
|||
|
|||
````html |
|||
@page |
|||
@using Acme.BookStore.Web.Pages.Books |
|||
@inherits Acme.BookStore.Web.Pages.BookStorePage |
|||
@model IndexModel |
|||
|
|||
<h2>Books</h2> |
|||
```` |
|||
|
|||
* 此代码更改了Razor View Page Model的默认继承,因此它从`BookStorePage`类(而不是`PageModel`)继承.启动模板附带的`BookStorePage`类,提供所有页面使用的一些共享属性/方法. |
|||
* 确保`IndexModel`(Index.cshtml.cs)具有`Acme.BookStore.Web.Pages.Books`命名空间,或者在`Index.cshtml`中更新它. |
|||
|
|||
#### 将Books页面添加到主菜单 |
|||
|
|||
打开`Menus`文件夹中的 `BookStoreMenuContributor` 类,在`ConfigureMainMenuAsync`方法的底部添加如下代码: |
|||
|
|||
````c# |
|||
context.Menu.AddItem( |
|||
new ApplicationMenuItem("BooksStore", l["Menu:BookStore"]) |
|||
.AddItem(new ApplicationMenuItem("BooksStore.Books", l["Menu:Books"], url: "/Books")) |
|||
); |
|||
```` |
|||
|
|||
#### 本地化菜单 |
|||
|
|||
本地化文本位于`Acme.BookStore.Domain.Shared`项目的`Localization/BookStore`文件夹下: |
|||
|
|||
 |
|||
|
|||
打开`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的标准行为). |
|||
|
|||
运行该应用程序,看到新菜单项已添加到顶部栏: |
|||
|
|||
 |
|||
|
|||
点击Books菜单项就会跳转到新增的书籍页面. |
|||
|
|||
#### 书籍列表 |
|||
|
|||
我们将使用[Datatables.net](https://datatables.net/)JQuery插件来显示页面上的表格列表. 数据表可以完全通过AJAX工作,速度快,并提供良好的用户体验. Datatables插件在启动模板中配置,因此你可以直接在任何页面中使用它,而需要在页面中引用样式和脚本文件. |
|||
|
|||
##### Index.cshtml |
|||
|
|||
将`Pages/Books/Index.cshtml`改成下面的样子: |
|||
|
|||
````html |
|||
@page |
|||
@inherits Acme.BookStore.Web.Pages.BookStorePage |
|||
@model Acme.BookStore.Web.Pages.Books.IndexModel |
|||
@section scripts |
|||
{ |
|||
<abp-script src="/Pages/Books/index.js" /> |
|||
} |
|||
<abp-card> |
|||
<abp-card-header> |
|||
<h2>@L["Books"]</h2> |
|||
</abp-card-header> |
|||
<abp-card-body> |
|||
<abp-table striped-rows="true" id="BooksTable"> |
|||
<thead> |
|||
<tr> |
|||
<th>@L["Name"]</th> |
|||
<th>@L["Type"]</th> |
|||
<th>@L["PublishDate"]</th> |
|||
<th>@L["Price"]</th> |
|||
<th>@L["CreationTime"]</th> |
|||
</tr> |
|||
</thead> |
|||
</abp-table> |
|||
</abp-card-body> |
|||
</abp-card> |
|||
```` |
|||
|
|||
* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro)用于将外部的 **脚本** 添加到页面中.它比标准的`script`标签多了很多额外的功能.它可以处理 **最小化**和 **版本**.查看[捆绑 & 压缩文档](https://docs.abp.io/zh-Hans/abp/latest/UI/AspNetCore/Bundling-Minification)获取更多信息. |
|||
* `abp-card` 和 `abp-table` 是为Twitter Bootstrap的[card component](http://getbootstrap.com/docs/4.1/components/card/)封装的 **tag helpers**.ABP中有很多tag helpers,可以很方便的使用大多数[bootstrap](https://getbootstrap.com/)组件.你也可以使用原生的HTML标签代替tag helpers.使用tag helper可以通过智能提示和编译时类型检查减少HTML代码并防止错误.查看[tag helpers 文档](https://docs.abp.io/zh-Hans/abp/latest/UI/AspNetCore/Tag-Helpers/Index). |
|||
* 你可以像上面本地化菜单一样 **本地化** 列名. |
|||
|
|||
#### 添加脚本文件 |
|||
|
|||
在`Pages/Books/`文件夹中创建 `index.js`文件 |
|||
|
|||
 |
|||
|
|||
`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/) 了解更多配置项. |
|||
|
|||
最终的页面如下: |
|||
|
|||
 |
|||
|
|||
### 下一章 |
|||
|
|||
点击查看 [下一章](Part-II.md) 的介绍. |
|||
<!-- TODO: this document has been moved, it should be deleted in the future. --> |
|||
@ -1,432 +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 III: 集成测试](Part-III.md) |
|||
|
|||
你可以从[GitHub存储库](https://github.com/volosoft/abp/tree/master/samples/BookStore)访问应用程序的**源代码**. |
|||
|
|||
> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). |
|||
|
|||
### 新增 Book 实体 |
|||
|
|||
通过本节, 你将会了解如何创建一个 modal form 来实现新增书籍的功能. 最终成果如下图所示: |
|||
|
|||
 |
|||
|
|||
#### 新建 modal form |
|||
|
|||
在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个 `CreateModal.cshtml` Razor页面: |
|||
|
|||
 |
|||
|
|||
##### CreateModal.cshtml.cs |
|||
|
|||
展开 `CreateModal.cshtml`,打开 `CreateModal.cshtml.cs` 代码文件,用如下代码替换 `CreateModalModel` 类的实现: |
|||
|
|||
````C# |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
|
|||
namespace Acme.BookStore.Web.Pages.Books |
|||
{ |
|||
public class CreateModalModel : BookStorePageModel |
|||
{ |
|||
[BindProperty] |
|||
public CreateUpdateBookDto Book { get; set; } |
|||
|
|||
private readonly IBookAppService _bookAppService; |
|||
|
|||
public CreateModalModel(IBookAppService bookAppService) |
|||
{ |
|||
_bookAppService = bookAppService; |
|||
} |
|||
|
|||
public async Task<IActionResult> OnPostAsync() |
|||
{ |
|||
await _bookAppService.CreateAsync(Book); |
|||
return NoContent(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* 该类派生于 `BookStorePageModel` 而非默认的 `PageModel`. `BookStorePageModel` 继承了 `PageModel` 并且添加了一些可以被你的page model类使用的通用属性和方法. |
|||
* `Book` 属性上的 `[BindProperty]` 特性将post请求提交上来的数据绑定到该属性上. |
|||
* 该类通过构造函数注入了 `IBookAppService` 应用服务,并且在 `OnPostAsync` 处理程序中调用了服务的 `CreateAsync` 方法. |
|||
|
|||
##### CreateModal.cshtml |
|||
|
|||
打开 `CreateModal.cshtml` 文件并粘贴如下代码: |
|||
|
|||
````html |
|||
@page |
|||
@inherits Acme.BookStore.Web.Pages.BookStorePage |
|||
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal |
|||
@model Acme.BookStore.Web.Pages.Books.CreateModalModel |
|||
@{ |
|||
Layout = null; |
|||
} |
|||
<abp-dynamic-form abp-model="Book" data-ajaxForm="true" asp-page="/Books/CreateModal"> |
|||
<abp-modal> |
|||
<abp-modal-header title="@L["NewBook"].Value"></abp-modal-header> |
|||
<abp-modal-body> |
|||
<abp-form-content /> |
|||
</abp-modal-body> |
|||
<abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer> |
|||
</abp-modal> |
|||
</abp-dynamic-form> |
|||
```` |
|||
|
|||
* 这个 modal 使用 `abp-dynamic-form` Tag Helper 根据 `CreateBookViewModel` 类自动构建了表单. |
|||
* `abp-model` 指定了 `Book` 属性为模型对象. |
|||
* `data-ajaxForm` 设置了表单通过AJAX提交,而不是经典的页面回发. |
|||
* `abp-form-content` tag helper 作为表单控件渲染位置的占位符 (这是可选的,只有你在 `abp-dynamic-form` 中像本示例这样添加了其他内容才需要). |
|||
|
|||
#### 添加 "New book" 按钮 |
|||
|
|||
打开 `Pages/Books/Index.cshtml` 并按如下代码修改 `abp-card-header` : |
|||
|
|||
````html |
|||
<abp-card-header> |
|||
<abp-row> |
|||
<abp-column size-md="_6"> |
|||
<h2>@L["Books"]</h2> |
|||
</abp-column> |
|||
<abp-column size-md="_6" class="text-right"> |
|||
<abp-button id="NewBookButton" |
|||
text="@L["NewBook"].Value" |
|||
icon="plus" |
|||
button-type="Primary" /> |
|||
</abp-column> |
|||
</abp-row> |
|||
</abp-card-header> |
|||
```` |
|||
|
|||
如下图所示,只是在表格 **右上方** 添加了 **New book** 按钮: |
|||
|
|||
 |
|||
|
|||
打开 `Pages/books/index.js` 在datatable配置代码后面添加如下代码: |
|||
|
|||
````js |
|||
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); |
|||
|
|||
createModal.onResult(function () { |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
|
|||
$('#NewBookButton').click(function (e) { |
|||
e.preventDefault(); |
|||
createModal.open(); |
|||
}); |
|||
```` |
|||
|
|||
* `abp.ModalManager` 是一个在客户端打开和管理modal的辅助类.它基于Twitter Bootstrap的标准modal组件通过简化的API抽象隐藏了许多细节. |
|||
|
|||
现在,你可以 **运行程序** 通过新的 modal form 来创建书籍了. |
|||
|
|||
### 编辑更新已存在的 Book 实体 |
|||
|
|||
在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个名叫 `EditModal.cshtml` 的Razor页面: |
|||
|
|||
 |
|||
|
|||
#### EditModal.cshtml.cs |
|||
|
|||
展开 `EditModal.cshtml`,打开 `EditModal.cshtml.cs` 文件( `EditModalModel` 类) 并替换成以下代码: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
|
|||
namespace Acme.BookStore.Web.Pages.Books |
|||
{ |
|||
public class EditModalModel : BookStorePageModel |
|||
{ |
|||
[HiddenInput] |
|||
[BindProperty(SupportsGet = true)] |
|||
public Guid Id { get; set; } |
|||
|
|||
[BindProperty] |
|||
public CreateUpdateBookDto Book { get; set; } |
|||
|
|||
private readonly IBookAppService _bookAppService; |
|||
|
|||
public EditModalModel(IBookAppService bookAppService) |
|||
{ |
|||
_bookAppService = bookAppService; |
|||
} |
|||
|
|||
public async Task OnGetAsync() |
|||
{ |
|||
var bookDto = await _bookAppService.GetAsync(Id); |
|||
Book = ObjectMapper.Map<BookDto, CreateUpdateBookDto>(bookDto); |
|||
} |
|||
|
|||
public async Task<IActionResult> OnPostAsync() |
|||
{ |
|||
await _bookAppService.UpdateAsync(Id, Book); |
|||
return NoContent(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `[HiddenInput]` 和 `[BindProperty]` 是标准的 ASP.NET Core MVC 特性.这里启用 `SupportsGet` 从Http请求的查询字符串中获取Id的值. |
|||
* 在 `OnGetAsync` 方法中,将 `BookAppService.GetAsync` 方法返回的 `BookDto` 映射成 `CreateUpdateBookDto` 并赋值给Book属性. |
|||
* `OnPostAsync` 方法直接使用 `BookAppService.UpdateAsync` 来更新实体. |
|||
|
|||
#### BookDto到CreateUpdateBookDto对象映射 |
|||
|
|||
为了执行`BookDto`到`CreateUpdateBookDto`对象映射,请打开`Acme.BookStore.Web`项目中的`BookStoreWebAutoMapperProfile.cs`并更改它,如下所示: |
|||
|
|||
````csharp |
|||
using AutoMapper; |
|||
|
|||
namespace Acme.BookStore.Web |
|||
{ |
|||
public class BookStoreWebAutoMapperProfile : Profile |
|||
{ |
|||
public BookStoreWebAutoMapperProfile() |
|||
{ |
|||
CreateMap<BookDto, CreateUpdateBookDto>(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* 刚刚添加了`CreateMap<BookDto, CreateUpdateBookDto>();`作为映射定义. |
|||
|
|||
#### EditModal.cshtml |
|||
|
|||
将 `EditModal.cshtml` 页面内容替换成如下代码: |
|||
|
|||
````html |
|||
@page |
|||
@inherits Acme.BookStore.Web.Pages.BookStorePage |
|||
@using Acme.BookStore.Web.Pages.Books |
|||
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal |
|||
@model EditModalModel |
|||
@{ |
|||
Layout = null; |
|||
} |
|||
<abp-dynamic-form abp-model="Book" data-ajaxForm="true" asp-page="/Books/EditModal"> |
|||
<abp-modal> |
|||
<abp-modal-header title="@L["Update"].Value"></abp-modal-header> |
|||
<abp-modal-body> |
|||
<abp-input asp-for="Id" /> |
|||
<abp-form-content /> |
|||
</abp-modal-body> |
|||
<abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer> |
|||
</abp-modal> |
|||
</abp-dynamic-form> |
|||
```` |
|||
|
|||
这个页面内容和 `CreateModal.cshtml` 非常相似,除了以下几点: |
|||
|
|||
* 它包含`id`属性的`abp-input`, 用于存储编辑书的id(它是隐藏的Input) |
|||
* 此页面指定的post地址是`Books/EditModal`, 并用文本 *Update* 作为 modal 标题. |
|||
|
|||
#### 为表格添加 "操作(Actions)" 下拉菜单 |
|||
|
|||
我们将为表格每行添加下拉按钮 ("Actions") . 最终效果如下: |
|||
|
|||
 |
|||
|
|||
打开 `Pages/Books/Index.cshtml` 页面,并按下方所示修改表格部分的代码: |
|||
|
|||
````html |
|||
<abp-table striped-rows="true" id="BooksTable"> |
|||
<thead> |
|||
<tr> |
|||
<th>@L["Actions"]</th> |
|||
<th>@L["Name"]</th> |
|||
<th>@L["Type"]</th> |
|||
<th>@L["PublishDate"]</th> |
|||
<th>@L["Price"]</th> |
|||
<th>@L["CreationTime"]</th> |
|||
</tr> |
|||
</thead> |
|||
</abp-table> |
|||
```` |
|||
|
|||
* 只是为"Actions"增加了一个 `th` 标签. |
|||
|
|||
打开 `Pages/books/index.js` 并用以下内容进行替换: |
|||
|
|||
````js |
|||
$(function () { |
|||
|
|||
var l = abp.localization.getResource('BookStore'); |
|||
|
|||
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); |
|||
var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); |
|||
|
|||
var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ |
|||
processing: true, |
|||
serverSide: true, |
|||
paging: true, |
|||
searching: false, |
|||
autoWidth: false, |
|||
scrollCollapse: true, |
|||
order: [[1, "asc"]], |
|||
ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), |
|||
columnDefs: [ |
|||
{ |
|||
rowAction: { |
|||
items: |
|||
[ |
|||
{ |
|||
text: l('Edit'), |
|||
action: function (data) { |
|||
editModal.open({ id: data.record.id }); |
|||
} |
|||
} |
|||
] |
|||
} |
|||
}, |
|||
{ data: "name" }, |
|||
{ data: "type" }, |
|||
{ data: "publishDate" }, |
|||
{ data: "price" }, |
|||
{ data: "creationTime" } |
|||
] |
|||
})); |
|||
|
|||
createModal.onResult(function () { |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
|
|||
editModal.onResult(function () { |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
|
|||
$('#NewBookButton').click(function (e) { |
|||
e.preventDefault(); |
|||
createModal.open(); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
* 通过 `abp.localization.getResource('BookStore')` 可以在客户端使用服务器端定义的相同的本地化语言文本. |
|||
* 添加了一个名为 `createModal` 的新的 `ModalManager` 来打开创建用的 modal 对话框. |
|||
* 添加了一个名为 `editModal` 的新的 `ModalManager` 来打开编辑用的 modal 对话框. |
|||
* 在 `columnDefs` 起始处新增一列用于显示 "Actions" 下拉按钮. |
|||
* "New Book"动作只需调用`createModal.open`来打开创建对话框. |
|||
* "Edit" 操作只是简单调用 `editModal.open` 来打开编辑对话框. |
|||
|
|||
现在,你可以运行程序,通过编辑操作来更新任一个book实体. |
|||
|
|||
### 删除一个已有的Book实体 |
|||
|
|||
打开 `Pages/books/index.js` 文件,在 `rowAction` `items` 下新增一项: |
|||
|
|||
````js |
|||
{ |
|||
text: l('Delete'), |
|||
confirmMessage: function (data) { |
|||
return l('BookDeletionConfirmationMessage', data.record.name); |
|||
}, |
|||
action: function (data) { |
|||
acme.bookStore.book |
|||
.delete(data.record.id) |
|||
.then(function() { |
|||
abp.notify.info(l('SuccessfullyDeleted')); |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `confirmMessage` 用来在实际执行 `action` 之前向用户进行确认. |
|||
* 通过javascript代理方法 `acme.bookStore.book.delete` 执行一个AJAX请求来删除一个book实体. |
|||
* `abp.notify.info` 用来在执行删除操作后显示一个toastr通知信息. |
|||
|
|||
最终的 `index.js` 文件内容如下所示: |
|||
|
|||
````js |
|||
$(function () { |
|||
|
|||
var l = abp.localization.getResource('BookStore'); |
|||
|
|||
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); |
|||
var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); |
|||
|
|||
var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ |
|||
processing: true, |
|||
serverSide: true, |
|||
paging: true, |
|||
searching: false, |
|||
autoWidth: false, |
|||
scrollCollapse: true, |
|||
order: [[1, "asc"]], |
|||
ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), |
|||
columnDefs: [ |
|||
{ |
|||
rowAction: { |
|||
items: |
|||
[ |
|||
{ |
|||
text: l('Edit'), |
|||
action: function (data) { |
|||
editModal.open({ id: data.record.id }); |
|||
} |
|||
}, |
|||
{ |
|||
text: l('Delete'), |
|||
confirmMessage: function (data) { |
|||
return l('BookDeletionConfirmationMessage', data.record.name); |
|||
}, |
|||
action: function (data) { |
|||
acme.bookStore.book |
|||
.delete(data.record.id) |
|||
.then(function() { |
|||
abp.notify.info(l('SuccessfullyDeleted')); |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
} |
|||
} |
|||
] |
|||
} |
|||
}, |
|||
{ data: "name" }, |
|||
{ data: "type" }, |
|||
{ data: "publishDate" }, |
|||
{ data: "price" }, |
|||
{ data: "creationTime" } |
|||
] |
|||
})); |
|||
|
|||
createModal.onResult(function () { |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
|
|||
editModal.onResult(function () { |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
|
|||
$('#NewBookButton').click(function (e) { |
|||
e.preventDefault(); |
|||
createModal.open(); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
打开`Acme.BookStore.Domain.Shared`项目中的`en.json`并添加以下行: |
|||
|
|||
````json |
|||
"BookDeletionConfirmationMessage": "Are you sure to delete the book {0}?", |
|||
"SuccessfullyDeleted": "Successfully deleted" |
|||
```` |
|||
|
|||
运行程序并尝试删除一个book实体. |
|||
|
|||
### 下一章 |
|||
|
|||
查看本教程的 [下一章](Part-III.md) . |
|||
<!-- TODO: this document has been moved, it should be deleted in the future. --> |
|||
@ -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). |
|||
|
|||
### 解决方案中的测试项目 |
|||
|
|||
解决方案中有多个测试项目: |
|||
|
|||
 |
|||
|
|||
每个项目用于测试相关的应用程序项目.测试项目使用以下库进行测试: |
|||
|
|||
* [xunit](https://xunit.github.io/) 作为主测试框架. |
|||
* [Shoudly](http://shouldly.readthedocs.io/en/latest/) 作为断言库. |
|||
* [NSubstitute](http://nsubstitute.github.io/) 作为模拟库. |
|||
|
|||
### 添加测试用数据 |
|||
|
|||
启动模板包含`Acme.BookStore.TestBase`项目中的`BookStoreTestDataSeedContributor`类,它创建一些数据来运行测试. |
|||
更改`BookStoreTestDataSeedContributor`类如下所示: |
|||
|
|||
````C# |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Data; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.Guids; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookStoreTestDataSeedContributor |
|||
: IDataSeedContributor, ITransientDependency |
|||
{ |
|||
private readonly IRepository<Book, Guid> _bookRepository; |
|||
private readonly IGuidGenerator _guidGenerator; |
|||
|
|||
public BookStoreTestDataSeedContributor( |
|||
IRepository<Book, Guid> bookRepository, |
|||
IGuidGenerator guidGenerator) |
|||
{ |
|||
_bookRepository = bookRepository; |
|||
_guidGenerator = guidGenerator; |
|||
} |
|||
|
|||
public async Task SeedAsync(DataSeedContext context) |
|||
{ |
|||
await _bookRepository.InsertAsync( |
|||
new Book(_guidGenerator.Create(), "Test book 1", |
|||
BookType.Fantastic, new DateTime(2015, 05, 24), 21)); |
|||
|
|||
await _bookRepository.InsertAsync( |
|||
new Book(_guidGenerator.Create(), "Test book 2", |
|||
BookType.Science, new DateTime(2014, 02, 11), 15)); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* 注入`IRepository<Book,Guid>`并在`SeedAsync`中使用它来创建两个书实体作为测试数据. |
|||
* 使用`IGuidGenerator`服务创建GUID. 虽然`Guid.NewGuid()`非常适合测试,但`IGuidGenerator`在使用真实数据库时还有其他特别重要的功能(参见[Guid生成文档](../../../Guid-Generation.md)了解更多信息). |
|||
|
|||
### 测试 BookAppService |
|||
|
|||
在 `Acme.BookStore.Application.Tests` 项目中创建一个名叫 `BookAppService_Tests` 的测试类: |
|||
|
|||
````C# |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Xunit; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookAppService_Tests : BookStoreApplicationTestBase |
|||
{ |
|||
private readonly IBookAppService _bookAppService; |
|||
|
|||
public BookAppService_Tests() |
|||
{ |
|||
_bookAppService = GetRequiredService<IBookAppService>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Get_List_Of_Books() |
|||
{ |
|||
//Act |
|||
var result = await _bookAppService.GetListAsync( |
|||
new PagedAndSortedResultRequestDto() |
|||
); |
|||
|
|||
//Assert |
|||
result.TotalCount.ShouldBeGreaterThan(0); |
|||
result.Items.ShouldContain(b => b.Name == "Test book 1"); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* 测试方法 `Should_Get_List_Of_Books` 直接使用 `BookAppService.GetListAsync` 方法来获取用户列表,并执行检查. |
|||
|
|||
新增测试方法,用以测试创建一个合法book实体的场景: |
|||
|
|||
````C# |
|||
[Fact] |
|||
public async Task Should_Create_A_Valid_Book() |
|||
{ |
|||
//Act |
|||
var result = await _bookAppService.CreateAsync( |
|||
new CreateUpdateBookDto |
|||
{ |
|||
Name = "New test book 42", |
|||
Price = 10, |
|||
PublishDate = DateTime.Now, |
|||
Type = BookType.ScienceFiction |
|||
} |
|||
); |
|||
|
|||
//Assert |
|||
result.Id.ShouldNotBe(Guid.Empty); |
|||
result.Name.ShouldBe("New test book 42"); |
|||
} |
|||
```` |
|||
|
|||
新增测试方法,用以测试创建一个非法book实体失败的场景: |
|||
|
|||
````C# |
|||
[Fact] |
|||
public async Task Should_Not_Create_A_Book_Without_Name() |
|||
{ |
|||
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () => |
|||
{ |
|||
await _bookAppService.CreateAsync( |
|||
new CreateUpdateBookDto |
|||
{ |
|||
Name = "", |
|||
Price = 10, |
|||
PublishDate = DateTime.Now, |
|||
Type = BookType.ScienceFiction |
|||
} |
|||
); |
|||
}); |
|||
|
|||
exception.ValidationErrors |
|||
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); |
|||
} |
|||
```` |
|||
|
|||
* 由于 `Name` 是空值, ABP 抛出一个 `AbpValidationException` 异常. |
|||
|
|||
打开**测试资源管理器**(测试 -> Windows -> 测试资源管理器)并**执行**所有测试: |
|||
|
|||
 |
|||
|
|||
恭喜, 绿色图标表示测试已成功通过! |
|||
<!-- TODO: this document has been moved, it should be deleted in the future. --> |
|||
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 34 KiB |
@ -0,0 +1,198 @@ |
|||
## ASP.NET Core {{UI_Value}} 教程 - 第三章 |
|||
````json |
|||
//[doc-params] |
|||
{ |
|||
"UI": ["MVC","NG"] |
|||
} |
|||
```` |
|||
|
|||
{{ |
|||
if UI == "MVC" |
|||
DB="ef" |
|||
DB_Text="Entity Framework Core" |
|||
UI_Text="mvc" |
|||
else if UI == "NG" |
|||
DB="mongodb" |
|||
DB_Text="MongoDB" |
|||
UI_Text="angular" |
|||
else |
|||
DB ="?" |
|||
UI_Text="?" |
|||
end |
|||
}} |
|||
|
|||
### 关于本教程 |
|||
|
|||
这是ASP.NET Core{{UI_Value}}系列教程的第二章. 共有三章: |
|||
|
|||
- [Part-1: 创建项目和书籍列表页面](Part-1.md) |
|||
- [Part 2: 创建,编辑,删除书籍](Part-2.md) |
|||
- **Part-3: 集成测试(本章)** |
|||
|
|||
> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). |
|||
|
|||
### 解决方案中的测试项目 |
|||
|
|||
解决方案中有多个测试项目: |
|||
|
|||
 |
|||
|
|||
每个项目用于测试相关的应用程序项目.测试项目使用以下库进行测试: |
|||
|
|||
* [xunit](https://xunit.github.io/) 作为主测试框架. |
|||
* [Shoudly](http://shouldly.readthedocs.io/en/latest/) 作为断言库. |
|||
* [NSubstitute](http://nsubstitute.github.io/) 作为模拟库. |
|||
|
|||
### 添加测试数据 |
|||
|
|||
启动模板包含`Acme.BookStore.TestBase`项目中的`BookStoreTestDataSeedContributor`类,它创建一些数据来运行测试. |
|||
更改`BookStoreTestDataSeedContributor`类如下所示: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Data; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.Guids; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookStoreTestDataSeedContributor |
|||
: IDataSeedContributor, ITransientDependency |
|||
{ |
|||
private readonly IRepository<Book, Guid> _bookRepository; |
|||
private readonly IGuidGenerator _guidGenerator; |
|||
|
|||
public BookStoreTestDataSeedContributor( |
|||
IRepository<Book, Guid> bookRepository, |
|||
IGuidGenerator guidGenerator) |
|||
{ |
|||
_bookRepository = bookRepository; |
|||
_guidGenerator = guidGenerator; |
|||
} |
|||
|
|||
public async Task SeedAsync(DataSeedContext context) |
|||
{ |
|||
await _bookRepository.InsertAsync( |
|||
new Book(id: _guidGenerator.Create(), |
|||
name: "Test book 1", |
|||
type: BookType.Fantastic, |
|||
publishDate: new DateTime(2015, 05, 24), |
|||
price: 21 |
|||
) |
|||
); |
|||
|
|||
await _bookRepository.InsertAsync( |
|||
new Book(id: _guidGenerator.Create(), |
|||
name: "Test book 2", |
|||
type: BookType.Science, |
|||
publishDate: new DateTime(2014, 02, 11), |
|||
price: 15 |
|||
) |
|||
); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* 注入`IRepository<Book,Guid>`并在`SeedAsync`中使用它来创建两个书实体作为测试数据. |
|||
* 使用`IGuidGenerator`服务创建GUID. 虽然`Guid.NewGuid()`非常适合测试,但`IGuidGenerator`在使用真实数据库时还有其他特别重要的功能(参见[Guid生成文档](../Guid-Generation.md)了解更多信息). |
|||
|
|||
### 测试 BookAppService |
|||
|
|||
在 `Acme.BookStore.Application.Tests` 项目中创建一个名叫 `BookAppService_Tests` 的测试类: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Xunit; |
|||
using Shouldly; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Validation; |
|||
using Microsoft.EntityFrameworkCore.Internal; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookAppService_Tests : BookStoreApplicationTestBase |
|||
{ |
|||
private readonly IBookAppService _bookAppService; |
|||
|
|||
public BookAppService_Tests() |
|||
{ |
|||
_bookAppService = GetRequiredService<IBookAppService>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Get_List_Of_Books() |
|||
{ |
|||
//Act |
|||
var result = await _bookAppService.GetListAsync( |
|||
new PagedAndSortedResultRequestDto() |
|||
); |
|||
|
|||
//Assert |
|||
result.TotalCount.ShouldBeGreaterThan(0); |
|||
result.Items.ShouldContain(b => b.Name == "Test book 1"); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* 测试方法 `Should_Get_List_Of_Books` 直接使用 `BookAppService.GetListAsync` 方法来获取用户列表,并执行检查. |
|||
|
|||
新增测试方法,用以测试创建一个合法book实体的场景: |
|||
|
|||
````C# |
|||
[Fact] |
|||
public async Task Should_Create_A_Valid_Book() |
|||
{ |
|||
//Act |
|||
var result = await _bookAppService.CreateAsync( |
|||
new CreateUpdateBookDto |
|||
{ |
|||
Name = "New test book 42", |
|||
Price = 10, |
|||
PublishDate = DateTime.Now, |
|||
Type = BookType.ScienceFiction |
|||
} |
|||
); |
|||
|
|||
//Assert |
|||
result.Id.ShouldNotBe(Guid.Empty); |
|||
result.Name.ShouldBe("New test book 42"); |
|||
} |
|||
```` |
|||
|
|||
新增测试方法,用以测试创建一个非法book实体失败的场景: |
|||
|
|||
````csharp |
|||
[Fact] |
|||
public async Task Should_Not_Create_A_Book_Without_Name() |
|||
{ |
|||
var exception = await Assert.ThrowsAsync<Volo.Abp.Validation.AbpValidationException>(async () => |
|||
{ |
|||
await _bookAppService.CreateAsync( |
|||
new CreateUpdateBookDto |
|||
{ |
|||
Name = "", |
|||
Price = 10, |
|||
PublishDate = DateTime.Now, |
|||
Type = BookType.ScienceFiction |
|||
} |
|||
); |
|||
}); |
|||
|
|||
exception.ValidationErrors |
|||
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); |
|||
} |
|||
```` |
|||
|
|||
* 由于 `Name` 是空值, ABP 抛出一个 `AbpValidationException` 异常. |
|||
|
|||
打开**测试资源管理器**(测试 -> Windows -> 测试资源管理器)并**执行**所有测试: |
|||
|
|||
 |
|||
|
|||
恭喜, 绿色图标表示测试已成功通过! |
|||
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |