@ -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 @@ |
|||
# Text-Templating |
|||
|
|||
TODO |
|||
|
Before Width: | Height: | Size: 91 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 |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 23 KiB |
@ -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,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: 91 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 |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 23 KiB |
@ -0,0 +1,16 @@ |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Packages.Core; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR |
|||
{ |
|||
[DependsOn(typeof(CoreScriptContributor))] |
|||
public class SignalRBrowserScriptContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.AddIfNotContains("/libs/signalr/browser/signalr.js"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,27 @@ |
|||
{ |
|||
"iisSettings": { |
|||
"windowsAuthentication": false, |
|||
"anonymousAuthentication": true, |
|||
"iisExpress": { |
|||
"applicationUrl": "http://localhost:53900/", |
|||
"sslPort": 44362 |
|||
} |
|||
}, |
|||
"profiles": { |
|||
"IIS Express": { |
|||
"commandName": "IISExpress", |
|||
"launchBrowser": true, |
|||
"environmentVariables": { |
|||
"ASPNETCORE_ENVIRONMENT": "Development" |
|||
} |
|||
}, |
|||
"Volo.Abp.SignalR": { |
|||
"commandName": "Project", |
|||
"launchBrowser": true, |
|||
"environmentVariables": { |
|||
"ASPNETCORE_ENVIRONMENT": "Development" |
|||
}, |
|||
"applicationUrl": "https://localhost:5001;http://localhost:5000" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk.Web"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netcoreapp3.1</TargetFramework> |
|||
<AssemblyName>Volo.Abp.AspNetCore.SignalR</AssemblyName> |
|||
<PackageId>Volo.Abp.AspNetCore.SignalR</PackageId> |
|||
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<IsPackable>true</IsPackable> |
|||
<OutputType>Library</OutputType> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Volo.Abp.AspNetCore\Volo.Abp.AspNetCore.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,115 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Reflection; |
|||
using Microsoft.AspNetCore.Builder; |
|||
using Microsoft.AspNetCore.Http.Connections; |
|||
using Microsoft.AspNetCore.Routing; |
|||
using Microsoft.AspNetCore.SignalR; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.AspNetCore.SignalR |
|||
{ |
|||
[DependsOn( |
|||
typeof(AbpAspNetCoreModule) |
|||
)] |
|||
public class AbpAspNetCoreSignalRModule : AbpModule |
|||
{ |
|||
private static readonly MethodInfo MapHubGenericMethodInfo = |
|||
typeof(AbpAspNetCoreSignalRModule) |
|||
.GetMethod("MapHub", BindingFlags.Static | BindingFlags.NonPublic); |
|||
|
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.AddConventionalRegistrar(new AbpSignalRConventionalRegistrar()); |
|||
|
|||
AutoAddHubTypes(context.Services); |
|||
} |
|||
|
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.AddSignalR(); |
|||
|
|||
Configure<AbpEndpointRouterOptions>(options => |
|||
{ |
|||
options.EndpointConfigureActions.Add(endpointContext => |
|||
{ |
|||
var signalROptions = endpointContext |
|||
.ScopeServiceProvider |
|||
.GetRequiredService<IOptions<AbpSignalROptions>>() |
|||
.Value; |
|||
|
|||
foreach (var hubConfig in signalROptions.Hubs) |
|||
{ |
|||
MapHubType( |
|||
hubConfig.HubType, |
|||
endpointContext.Endpoints, |
|||
hubConfig.RoutePattern, |
|||
opts => |
|||
{ |
|||
foreach (var configureAction in hubConfig.ConfigureActions) |
|||
{ |
|||
configureAction(opts); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
private void AutoAddHubTypes(IServiceCollection services) |
|||
{ |
|||
var hubTypes = new List<Type>(); |
|||
|
|||
services.OnRegistred(context => |
|||
{ |
|||
if (typeof(Hub).IsAssignableFrom(context.ImplementationType)) |
|||
{ |
|||
hubTypes.Add(context.ImplementationType); |
|||
} |
|||
}); |
|||
|
|||
services.Configure<AbpSignalROptions>(options => |
|||
{ |
|||
foreach (var hubType in hubTypes) |
|||
{ |
|||
options.Hubs.Add(HubConfig.Create(hubType)); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private void MapHubType( |
|||
Type hubType, |
|||
IEndpointRouteBuilder endpoints, |
|||
string pattern, |
|||
Action<HttpConnectionDispatcherOptions> configureOptions) |
|||
{ |
|||
MapHubGenericMethodInfo |
|||
.MakeGenericMethod(hubType) |
|||
.Invoke( |
|||
this, |
|||
new object[] |
|||
{ |
|||
endpoints, |
|||
pattern, |
|||
configureOptions |
|||
} |
|||
); |
|||
} |
|||
|
|||
// ReSharper disable once UnusedMember.Local (used via reflection)
|
|||
private static void MapHub<THub>( |
|||
IEndpointRouteBuilder endpoints, |
|||
string pattern, |
|||
Action<HttpConnectionDispatcherOptions> configureOptions) |
|||
where THub : Hub |
|||
{ |
|||
endpoints.MapHub<THub>( |
|||
pattern, |
|||
configureOptions |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,135 @@ |
|||
using System; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Microsoft.AspNetCore.SignalR; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Localization; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.Timing; |
|||
using Volo.Abp.Users; |
|||
|
|||
namespace Volo.Abp.AspNetCore.SignalR |
|||
{ |
|||
public abstract class AbpHub : Hub |
|||
{ |
|||
public IServiceProvider ServiceProvider { get; set; } |
|||
protected readonly object ServiceProviderLock = new object(); |
|||
|
|||
protected TService LazyGetRequiredService<TService>(ref TService reference) |
|||
=> LazyGetRequiredService(typeof(TService), ref reference); |
|||
|
|||
protected TRef LazyGetRequiredService<TRef>(Type serviceType, ref TRef reference) |
|||
{ |
|||
if (reference == null) |
|||
{ |
|||
lock (ServiceProviderLock) |
|||
{ |
|||
if (reference == null) |
|||
{ |
|||
reference = (TRef)ServiceProvider.GetRequiredService(serviceType); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return reference; |
|||
} |
|||
|
|||
public ILoggerFactory LoggerFactory => LazyGetRequiredService(ref _loggerFactory); |
|||
private ILoggerFactory _loggerFactory; |
|||
|
|||
protected ILogger Logger => _lazyLogger.Value; |
|||
private Lazy<ILogger> _lazyLogger => new Lazy<ILogger>(() => LoggerFactory?.CreateLogger(GetType().FullName) ?? NullLogger.Instance, true); |
|||
|
|||
public ICurrentUser CurrentUser => LazyGetRequiredService(ref _currentUser); |
|||
private ICurrentUser _currentUser; |
|||
|
|||
public ICurrentTenant CurrentTenant => LazyGetRequiredService(ref _currentTenant); |
|||
private ICurrentTenant _currentTenant; |
|||
|
|||
public IAuthorizationService AuthorizationService => LazyGetRequiredService(ref _authorizationService); |
|||
private IAuthorizationService _authorizationService; |
|||
|
|||
public IClock Clock => LazyGetRequiredService(ref _clock); |
|||
private IClock _clock; |
|||
|
|||
public IStringLocalizerFactory StringLocalizerFactory => LazyGetRequiredService(ref _stringLocalizerFactory); |
|||
private IStringLocalizerFactory _stringLocalizerFactory; |
|||
|
|||
public IStringLocalizer L => _localizer ?? (_localizer = StringLocalizerFactory.Create(LocalizationResource)); |
|||
private IStringLocalizer _localizer; |
|||
|
|||
protected Type LocalizationResource |
|||
{ |
|||
get => _localizationResource; |
|||
set |
|||
{ |
|||
_localizationResource = value; |
|||
_localizer = null; |
|||
} |
|||
} |
|||
private Type _localizationResource = typeof(DefaultResource); |
|||
} |
|||
|
|||
public abstract class AbpHub<T> : Hub<T> |
|||
where T : class |
|||
{ |
|||
public IServiceProvider ServiceProvider { get; set; } |
|||
protected readonly object ServiceProviderLock = new object(); |
|||
|
|||
protected TService LazyGetRequiredService<TService>(ref TService reference) |
|||
=> LazyGetRequiredService(typeof(TService), ref reference); |
|||
|
|||
protected TRef LazyGetRequiredService<TRef>(Type serviceType, ref TRef reference) |
|||
{ |
|||
if (reference == null) |
|||
{ |
|||
lock (ServiceProviderLock) |
|||
{ |
|||
if (reference == null) |
|||
{ |
|||
reference = (TRef)ServiceProvider.GetRequiredService(serviceType); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return reference; |
|||
} |
|||
|
|||
public ILoggerFactory LoggerFactory => LazyGetRequiredService(ref _loggerFactory); |
|||
private ILoggerFactory _loggerFactory; |
|||
|
|||
protected ILogger Logger => _lazyLogger.Value; |
|||
private Lazy<ILogger> _lazyLogger => new Lazy<ILogger>(() => LoggerFactory?.CreateLogger(GetType().FullName) ?? NullLogger.Instance, true); |
|||
|
|||
public ICurrentUser CurrentUser => LazyGetRequiredService(ref _currentUser); |
|||
private ICurrentUser _currentUser; |
|||
|
|||
public ICurrentTenant CurrentTenant => LazyGetRequiredService(ref _currentTenant); |
|||
private ICurrentTenant _currentTenant; |
|||
|
|||
public IAuthorizationService AuthorizationService => LazyGetRequiredService(ref _authorizationService); |
|||
private IAuthorizationService _authorizationService; |
|||
|
|||
public IClock Clock => LazyGetRequiredService(ref _clock); |
|||
private IClock _clock; |
|||
|
|||
public IStringLocalizerFactory StringLocalizerFactory => LazyGetRequiredService(ref _stringLocalizerFactory); |
|||
private IStringLocalizerFactory _stringLocalizerFactory; |
|||
|
|||
public IStringLocalizer L => _localizer ?? (_localizer = StringLocalizerFactory.Create(LocalizationResource)); |
|||
private IStringLocalizer _localizer; |
|||
|
|||
protected Type LocalizationResource |
|||
{ |
|||
get => _localizationResource; |
|||
set |
|||
{ |
|||
_localizationResource = value; |
|||
_localizer = null; |
|||
} |
|||
} |
|||
private Type _localizationResource = typeof(DefaultResource); |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
using System; |
|||
using Microsoft.AspNetCore.SignalR; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.SignalR |
|||
{ |
|||
public class AbpSignalRConventionalRegistrar : ConventionalRegistrarBase |
|||
{ |
|||
public override void AddType(IServiceCollection services, Type type) |
|||
{ |
|||
if (IsConventionalRegistrationDisabled(type)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (!IsHub(type)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var serviceTypes = ExposedServiceExplorer.GetExposedServices(type); |
|||
|
|||
TriggerServiceExposing(services, type, serviceTypes); |
|||
|
|||
foreach (var serviceType in serviceTypes) |
|||
{ |
|||
services.Add( |
|||
ServiceDescriptor.Describe( |
|||
serviceType, |
|||
type, |
|||
ServiceLifetime.Transient |
|||
) |
|||
); |
|||
} |
|||
} |
|||
|
|||
private static bool IsHub(Type type) |
|||
{ |
|||
return typeof(Hub).IsAssignableFrom(type); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.Abp.AspNetCore.SignalR |
|||
{ |
|||
public class AbpSignalROptions |
|||
{ |
|||
public List<HubConfig> Hubs { get; } |
|||
|
|||
public AbpSignalROptions() |
|||
{ |
|||
Hubs = new List<HubConfig>(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using Microsoft.AspNetCore.SignalR; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Users; |
|||
|
|||
namespace Volo.Abp.AspNetCore.SignalR |
|||
{ |
|||
public class AbpSignalRUserIdProvider : IUserIdProvider, ITransientDependency |
|||
{ |
|||
public ICurrentUser CurrentUser { get; } |
|||
|
|||
public AbpSignalRUserIdProvider(ICurrentUser currentUser) |
|||
{ |
|||
CurrentUser = currentUser; |
|||
} |
|||
|
|||
public virtual string GetUserId(HubConnectionContext connection) |
|||
{ |
|||
return CurrentUser.Id?.ToString(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using JetBrains.Annotations; |
|||
using Microsoft.AspNetCore.Http.Connections; |
|||
using Microsoft.AspNetCore.SignalR; |
|||
|
|||
namespace Volo.Abp.AspNetCore.SignalR |
|||
{ |
|||
public class HubConfig |
|||
{ |
|||
[NotNull] |
|||
public Type HubType { get; } |
|||
|
|||
[NotNull] |
|||
public string RoutePattern { get; set; } |
|||
|
|||
[NotNull] |
|||
public List<Action<HttpConnectionDispatcherOptions>> ConfigureActions { get; set; } |
|||
|
|||
public HubConfig( |
|||
[NotNull] Type hubType, |
|||
[NotNull] string routePattern) |
|||
{ |
|||
HubType = Check.NotNull(hubType, nameof(hubType)); |
|||
RoutePattern = Check.NotNullOrWhiteSpace(routePattern, nameof(routePattern)); |
|||
ConfigureActions = new List<Action<HttpConnectionDispatcherOptions>>(); |
|||
} |
|||
|
|||
public static HubConfig Create<THub>() |
|||
where THub : Hub |
|||
{ |
|||
return Create(typeof(THub)); |
|||
} |
|||
|
|||
public static HubConfig Create(Type hubType) |
|||
{ |
|||
return new HubConfig( |
|||
hubType, |
|||
HubRouteAttribute.GetRoutePattern(hubType) |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
using System; |
|||
using System.Reflection; |
|||
using Microsoft.AspNetCore.SignalR; |
|||
|
|||
namespace Volo.Abp.AspNetCore.SignalR |
|||
{ |
|||
public class HubRouteAttribute : Attribute |
|||
{ |
|||
public string RoutePattern { get; set; } |
|||
|
|||
public HubRouteAttribute(string routePattern) |
|||
{ |
|||
RoutePattern = routePattern; |
|||
} |
|||
|
|||
public static string GetRoutePattern<THub>() |
|||
where THub : Hub |
|||
{ |
|||
return GetRoutePattern(typeof(THub)); |
|||
} |
|||
|
|||
public static string GetRoutePattern(Type hubType) |
|||
{ |
|||
var routeAttribute = hubType.GetSingleAttributeOrNull<HubRouteAttribute>(); |
|||
if (routeAttribute != null) |
|||
{ |
|||
return routeAttribute.RoutePattern; |
|||
} |
|||
|
|||
return "/signalr-hubs/" + hubType.Name.RemovePostFix("Hub").ToKebabCase(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using Microsoft.AspNetCore.Routing; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
|
|||
namespace Microsoft.AspNetCore.Builder |
|||
{ |
|||
public static class AbpAspNetCoreApplicationBuilderExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Maps endpoints configured with the <see cref="AbpEndpointRouterOptions"/>.
|
|||
/// It internally uses the standard app.UseEndpoints(...) method.
|
|||
/// </summary>
|
|||
/// <param name="app">The application builder</param>
|
|||
/// <param name="additionalConfigurationAction">Additional (and optional) endpoint configuration</param>
|
|||
/// <returns></returns>
|
|||
public static IApplicationBuilder UseConfiguredEndpoints( |
|||
this IApplicationBuilder app, |
|||
Action<IEndpointRouteBuilder> additionalConfigurationAction = null) |
|||
{ |
|||
var options = app.ApplicationServices |
|||
.GetRequiredService<IOptions<AbpEndpointRouterOptions>>() |
|||
.Value; |
|||
|
|||
if (!options.EndpointConfigureActions.Any()) |
|||
{ |
|||
return app; |
|||
} |
|||
|
|||
return app.UseEndpoints(endpoints => |
|||
{ |
|||
using (var scope = app.ApplicationServices.CreateScope()) |
|||
{ |
|||
var context = new EndpointRouteBuilderContext(endpoints, scope.ServiceProvider); |
|||
|
|||
foreach (var configureAction in options.EndpointConfigureActions) |
|||
{ |
|||
configureAction(context); |
|||
} |
|||
|
|||
additionalConfigurationAction?.Invoke(endpoints); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Microsoft.AspNetCore.Routing |
|||
{ |
|||
public class AbpEndpointRouterOptions |
|||
{ |
|||
public List<Action<EndpointRouteBuilderContext>> EndpointConfigureActions { get; } |
|||
|
|||
public AbpEndpointRouterOptions() |
|||
{ |
|||
EndpointConfigureActions = new List<Action<EndpointRouteBuilderContext>>(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System; |
|||
|
|||
namespace Microsoft.AspNetCore.Routing |
|||
{ |
|||
public class EndpointRouteBuilderContext |
|||
{ |
|||
public IEndpointRouteBuilder Endpoints { get; } |
|||
public IServiceProvider ScopeServiceProvider { get; } |
|||
|
|||
public EndpointRouteBuilderContext( |
|||
IEndpointRouteBuilder endpoints, |
|||
IServiceProvider scopeServiceProvider) |
|||
{ |
|||
Endpoints = endpoints; |
|||
ScopeServiceProvider = scopeServiceProvider; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Blogging |
|||
{ |
|||
public class BloggingTwitterOptions |
|||
{ |
|||
public string Site { get; set; } |
|||
} |
|||
} |
|||
@ -1,12 +1,12 @@ |
|||
{ |
|||
"version": "2.6.2", |
|||
"version": "2.7.0", |
|||
"name": "@abp/anchor-js", |
|||
"publishConfig": { |
|||
"access": "public" |
|||
}, |
|||
"dependencies": { |
|||
"@abp/core": "^2.6.2", |
|||
"@abp/core": "^2.7.0", |
|||
"anchor-js": "^4.2.2" |
|||
}, |
|||
"gitHead": "eb4a0e507f492e275865b72caeb02ce28d69ae56" |
|||
"gitHead": "0ea3895f3b0b489e3ea81fc88f8f0896b22b61bd" |
|||
} |
|||
|
|||
@ -1,11 +1,11 @@ |
|||
{ |
|||
"version": "2.6.2", |
|||
"version": "2.7.0", |
|||
"name": "@abp/aspnetcore.mvc.ui.theme.basic", |
|||
"publishConfig": { |
|||
"access": "public" |
|||
}, |
|||
"dependencies": { |
|||
"@abp/aspnetcore.mvc.ui.theme.shared": "^2.6.2" |
|||
"@abp/aspnetcore.mvc.ui.theme.shared": "^2.7.0" |
|||
}, |
|||
"gitHead": "eb4a0e507f492e275865b72caeb02ce28d69ae56" |
|||
"gitHead": "0ea3895f3b0b489e3ea81fc88f8f0896b22b61bd" |
|||
} |
|||
|
|||