diff --git a/docs/en/Samples/Microservice-Demo.md b/docs/en/Samples/Microservice-Demo.md index d0a05f31a4..ec890c9013 100644 --- a/docs/en/Samples/Microservice-Demo.md +++ b/docs/en/Samples/Microservice-Demo.md @@ -6,7 +6,7 @@ ## Introduction -One of the major goals of the ABP framework is to provide a [convenient infrastructure to create microservice solutions](Microservice-Architecture.md). +One of the major goals of the ABP framework is to provide a [convenient infrastructure to create microservice solutions](../Microservice-Architecture.md). This sample aims to demonstrate a simple yet complete microservice solution; @@ -19,7 +19,8 @@ This sample aims to demonstrate a simple yet complete microservice solution; * Has a **console application** to show the simplest way of using a service by authenticating. * Uses [Redis](https://redis.io/) for **distributed caching**. * Uses [RabbitMQ](https://www.rabbitmq.com/) for service-to-service **messaging**. -* Uses [Kubernates](https://kubernetes.io/) to **deploy** & run all services and applications. +* Uses [Docker](https://www.docker.com/) & [Kubernates](https://kubernetes.io/) to **deploy** & run all services and applications. +* Uses [Elasticsearch](https://www.elastic.co/products/elasticsearch) & [Kibana](https://www.elastic.co/products/kibana) to store and visualize the logs (written using [Serilog](https://serilog.net/)). The diagram below shows the system: @@ -33,8 +34,404 @@ You can get the source code from [the GitHub repository](https://github.com/abpf This sample is still in development, not completed yet. +## Running the Solution + +You can either run from the **source code** or from the pre-configured **docker-compose** file. + +### Using the Docker Containers + +#### Pre Requirements + +Running as docker containers is easier since all dependencies are pre-configured. You only need to install the [latest docker](https://docs.docker.com/compose/install/). + +#### Running Containers + +- Clone or download the [ABP repository](https://github.com/abpframework/abp). + +- Open a command line in the `samples/MicroserviceDemo` folder of the repository. + +- Restore SQL Server databases: + + ``` + docker-compose -f docker-compose.yml -f docker-compose.migrations.yml run restore-database + ``` + +- Start the containers: + + ``` + docker-compose up -d + ``` + + At the first run, it will take a **long time** because it will build all docker images. + +- Add this line to the end of your `hosts` file: + + ``` + 127.0.0.1 auth-server + ``` + + hosts file is located inside the `C:\Windows\System32\Drivers\etc\hosts` folder on Windows and `/etc/hosts` for Linux/MacOS. + +#### Run the Applications + +There are a few applications running in the containers you may want to explore: + +* Backend Admin Application (BackendAdminApp.Host): `http://localhost:51512` + *(Used to manage users & products in the system)* +* Public Web Site (PublicWebsite.Host): `http://localhost:51513` + *(Used to list products and run/manage the blog module)* +* Authentication Server (AuthServer.Host): `http://auth-server:51511/` + *(Used as a single sign on and authentication server built with IdentityServer4)* +* Kibana UI: `http://localhost:51510` + *(Use to show/trace logs written by all services/applications/gateways)* + +### Running From the Source Code + +#### Pre Requirements + +To be able to run the solution from source code, following tools should be installed and running on your computer: + +* [SQL Server](https://www.microsoft.com/en-us/sql-server/sql-server-downloads) 2015+ (can be [express edition](https://www.microsoft.com/en-us/sql-server/sql-server-editions-express)) +* [Redis](https://redis.io/download) 5.0+ +* [RabbitMQ](https://www.rabbitmq.com/install-windows.html) 3.7.11+ +* [MongoDB](https://www.mongodb.com/download-center) 4.0+ +* [ElasticSearch](https://www.elastic.co/downloads/elasticsearch) 6.6+ +* [Kibana](https://www.elastic.co/downloads/kibana) 6.6+ (optional, recommended to show logs) + +#### Open & Build the Visual Studio Solution + +* Open the `samples\MicroserviceDemo\MicroserviceDemo.sln` in Visual Studio 2017 (15.9.0+). +* Run `dotnet restore` from the command line inside the `samples\MicroserviceDemo` folder. +* Build the solution in Visual Studio. + +#### Restore Databases + +Open `MsDemo_Identity.zip` and `MsDemo_ProductManagement.zip` inside the `samples\MicroserviceDemo\databases` folder and restore to the SQL Server. + +> Notice that: These databases have EF Core migrations in the solution, however they don't have seed data, especially required for IdentityServer4 configuration. So, restoring the databases is much more easier. + +#### Run Projects + +Run the projects with the following order (right click to each project, set as startup project an press Ctrl+F5 to run without debug): + +* AuthServer.Host +* IdentityService.Host +* BloggingService.Host +* ProductService.Host +* InternalGateway.Host +* BackendAdminAppGateway.Host +* PublicWebSiteGateway.Host +* BackendAdminApp.Host +* PublicWebSite.Host + +## A Brief Overview of the Solution + +The Visual Studio solution consists of multiple projects each have different roles in the system: + +![microservice-sample-solution](../images/microservice-sample-solution.png) + +### Applications + +These are the actual applications those have user interfaces to interact to the users and use the system. + +- **AuthServer.Host**: Host the IdentityServer4 to provide an authentication service to other services and applications. It is a single-sign server and contains the login page. +- **BackendAdminApp.Host**: This is a backend admin application that host UI for Identity and Product management modules. +- **PubicWebSite.Host**: As public web site that contains a simple product list page and blog module UI. +- **ConsoleClientDemo**: A simple console application to demonstrate the usage of services from a C# application. + +### Gateways / BFFs (Backend for Frontend) + +Gateways are used to provide a single entry point to the applications. It can also used for rate limiting, load balancing... etc. Used the [Ocelot](https://github.com/ThreeMammals/Ocelot) library. + +* **BackendAdminAppGateway.Host**: Used by the BackendAdminApp.Host application as backend. +* **PublicWebSiteGateway.Host**: Used by the PublicWebSite.Host application as backend. +* **InternalGateway.Host**: Used for inter-service communication (the communication between microservices). + +### Microservices + +Microservices have no UI, but exposes some REST APIs. + +- **IdentityService.Host**: Host the ABP Identity module which is used to manage users & roles. It has no additional service, but only hosts the Identity module's API. +- **BloggingService.Host**: Host the ABP Blogging module which is used to manage blog & posts (a typical blog application). It has no additional service, but only hosts the Blogging module's API. +- **ProductService.Host**: Hosts the Product module (that is inside the solution) which is used to manage products. It also contains the EF Core migrations to create/update the Product Management database schema. + +### Modules + +* **Product**: A layered module that is developed with the [module development best practices](../Best-Practices/Index.md). It can be embedded into a monolithic application or can be hosted as a microservice by separately deploying API and UI (as done in this demo solution). + +### Databases + +This solution is using multiple databases: + +* **MsDemo_Identity**: An SQL database. Used **SQL Server** by default, but can be any DBMS supported by the EF Core. Shared by AuthServer and IdentityService. Also audit logs, permissions and settings are stored in this database (while they could easily have their own databases, shared the same database to keep it simple). +* **MsDemo_ProductManagement**: An SQL database. Again, used **SQL Server** by default, but can be any DBMS supported by the EF Core. Used by the ProductService as a dedicated database. +* **MsDemo_Blogging**: A **MongoDB** database. Used by the BloggingService. +* **Elasticsearch**: Used to write logs over Serilog. + +## Applications + +### Authentication Server (AuthServer.Host) + +This project is used by all other services and applications for authentication & single sign on. Mainly, uses **IdentityServer4** to provide these services. It uses some of the [pre-build ABP modules](../Modules/Index) like *Identity*, *Audit Logging* and *Permission Management*. + +#### Database & EF Core Configuration + +This application uses a SQL database (named it as **MsDemo_Identity**) and maintains its schema via **Entity Framework Core migrations.** + +It has a DbContext named **AuthServerDbContext** and defined as shown below: + +````csharp +public class AuthServerDbContext : AbpDbContext +{ + public AuthServerDbContext(DbContextOptions options) + : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.ConfigureIdentity(); + modelBuilder.ConfigureIdentityServer(); + modelBuilder.ConfigureAuditLogging(); + modelBuilder.ConfigurePermissionManagement(); + modelBuilder.ConfigureSettingManagement(); + } +} +```` + +In the **OnModelCreating**, you see **ConfigureX()** method calls. A module with a database schema generally declares such an extension method to configure EF Core mappings for its own entities. This is a flexible approach where you can arrange your databases and modules inside them; You can use a different database for each module, or combine some of them in a shared database. In the AuthServer project, we decided to combine multiple module schemas in a single EF Core DbContext, in a single physical database. These modules are Identity, IdentityServer, AuditLogging, PermissionManagement and SettingManagement modules. + +#### User Interface + +AuthServer has a simple home page that shows the current user info if the current user has logged in: + +![microservice-sample-authserver-home](../images/microservice-sample-authserver-home.png) + +It also provides Login & Register pages: + +![microservice-sample-authserver-login](../images/microservice-sample-authserver-login.png) + +These pages are not included in the project itself. Instead, AuthServer project uses the prebuilt ABP [account module](https://github.com/abpframework/abp/tree/master/modules/account) with IdentityServer extension. That means it can also act as an OpenId Connect server with necessary UI and logic. + +#### Other Dependencies + +* **RabbitMQ** for messaging to other services. +* **Redis** for distributed/shared caching. +* **Elasticsearch** for storing logs. + +### Backend Admin Application (BackendAdminApp.Host) + +This is a web application that is used to manage users, roles, permissions and products in the system. + +#### Authentication + +BackendAdminApp redirects to the AuthServer for authentication. Once the user enters a correct username & password, the page is redirected to the backend application again. Authentication configuration is setup in the `BackendAdminAppHostModule` class: + +````charp +context.Services.AddAuthentication(options => +{ + options.DefaultScheme = "Cookies"; + options.DefaultChallengeScheme = "oidc"; +}) +.AddCookie("Cookies", options => +{ + options.Cookie.Expiration = TimeSpan.FromDays(365); + options.ExpireTimeSpan = TimeSpan.FromDays(365); +}) +.AddOpenIdConnect("oidc", options => +{ + options.Authority = configuration["AuthServer:Authority"]; + options.ClientId = configuration["AuthServer:ClientId"]; + options.ClientSecret = configuration["AuthServer:ClientSecret"]; + options.RequireHttpsMetadata = false; + options.ResponseType = OpenIdConnectResponseType.CodeIdToken; + options.SaveTokens = true; + options.GetClaimsFromUserInfoEndpoint = true; + options.Scope.Add("role"); + options.Scope.Add("email"); + options.Scope.Add("phone"); + options.Scope.Add("BackendAdminAppGateway"); + options.Scope.Add("IdentityService"); + options.Scope.Add("ProductService"); + options.ClaimActions.MapAbpClaimTypes(); +}); +```` + +* It adds "Cookies" authentication as the primary authentication type. +* "oidc" authentication is configured to use the AuthServer application as the authentication server. +* It requires the additional identity scopes *role*, *email* and *phone*. +* It requires the API resource scopes *BackendAdminAppGateway*, *IdentityService* and *ProductService* because it will use these services as APIs. + +IdentityServer client settings are stored inside the `appsettings.json` file: + +````json +"AuthServer": { + "Authority": "http://localhost:64999", + "ClientId": "backend-admin-app-client", + "ClientSecret": "1q2w3e*" +} +```` + +#### User Interface + +The BackendAdminApp.Host project itself has not a single UI element/page. It is only used to serve UI pages of the Identity and Product Management modules. `BackendAdminAppHostModule` adds dependencies to `AbpIdentityWebModule` (*[Volo.Abp.Identity.Web](https://www.nuget.org/packages/Volo.Abp.Identity.Web)* package) and `ProductManagementWebModule` (*ProductManagement.Web* project) for that purpose. + +A screenshot from the user management page: + +![microservice-sample-backend-ui](../images/microservice-sample-backend-ui.png) + +A screenshot from the permission management modal for a role: + +![microservice-sample-backend-ui-permissions](../images/microservice-sample-backend-ui-permissions.png) + +#### Using Microservices + +Backend admin application uses the Identity and Product microservices for all operations, over the Backend Admin Gateway (BackendAdminAppGateway.Host). + +##### Remote End Point + +`appsettings.json` file contains the `RemoteServices` section to declare the remote service endpoint(s). Each microservice will normally have different endpoints. However, this solution uses the API Gateway pattern to provide a single endpoint for the applications: + +````json +"RemoteServices": { + "Default": { + "BaseUrl": "http://localhost:65115/" + } +} +```` + +`http://localhost:65115/` is the URL of the *BackendAdminAppGateway.Host* project. It knows where are Identity and Product services are located. + +##### HTTP Clients + +ABP application modules generally provides C# client libraries to consume services (APIs) easily (they generally uses the [Dynamic C# API Clients](../AspNetCore/Dynamic-CSharp-API-Clients.md) feature of the ABP framework). That means if you need to consume Identity service API, you can reference to its client package and easily use the APIs by provided interfaces. + +For that purpose, `BackendAdminAppHostModule` class declares dependencies for `AbpIdentityHttpApiClientModule` and `ProductManagementHttpApiClientModule`. + +Once you refer these client packages, you can directly inject an application service interface (e.g. `IIdentityUserAppService`) and use its methods like a local method call. It actually invokes remote service calls over HTTP to the related service endpoint. + +##### Passing the Access Token + +Since microservices requires authentication & authorization, each remote service call should contain an Authentication header. This header is obtained from the `access_token` inside the current `HttpContext` for the current user. This is automatically done when you use the `Volo.Abp.Http.Client.IdentityModel` package. `BackendAdminAppHostModule` declares dependencies to this package and to the related `AbpHttpClientIdentityModelModule` class. It is integrated to the HTTP Clients explained above. + +#### Other Dependencies + +- **Redis** for distributed/shared caching. +- **Elasticsearch** for storing logs. + +### Public Web Site (PublicWebSite.Host) + +This is a public web site project that has a web blog and product list page. + +#### Authentication + +PublicWebSite can show blog posts and product list without login. If you login, you can also manage blogs. It redirects to the AuthServer for authentication. Once the user enters a correct username & password, the page is redirected to the public web site application again. Authentication configuration is setup in the `PublicWebSiteHostModule` class: + +```charp +context.Services.AddAuthentication(options => +{ + options.DefaultScheme = "Cookies"; + options.DefaultChallengeScheme = "oidc"; +}) +.AddCookie("Cookies", options => +{ + options.Cookie.Expiration = TimeSpan.FromDays(365); + options.ExpireTimeSpan = TimeSpan.FromDays(365); +}) +.AddOpenIdConnect("oidc", options => +{ + options.Authority = configuration["AuthServer:Authority"]; + options.ClientId = configuration["AuthServer:ClientId"]; + options.ClientSecret = configuration["AuthServer:ClientSecret"]; + options.RequireHttpsMetadata = false; + options.ResponseType = OpenIdConnectResponseType.CodeIdToken; + options.SaveTokens = true; + options.GetClaimsFromUserInfoEndpoint = true; + options.Scope.Add("role"); + options.Scope.Add("email"); + options.Scope.Add("phone"); + options.Scope.Add("PublicWebSiteGateway"); + options.Scope.Add("ProductService"); + options.Scope.Add("BloggingService"); + options.ClaimActions.MapAbpClaimTypes(); +}); +``` + +- It adds "Cookies" authentication as the primary authentication type. +- "oidc" authentication is configured to use the AuthServer application as the authentication server. +- It requires the additional identity scopes *role*, *email* and *phone*. +- It requires the API resource scopes *PublicWebSiteGateway*, *BloggingService* and *ProductService* because it will use these services as APIs. + +IdentityServer client settings are stored inside the `appsettings.json` file: + +```json +"AuthServer": { + "Authority": "http://localhost:64999", + "ClientId": "public-website-client", + "ClientSecret": "1q2w3e*" +} +``` + +#### User Interface + +The PublicWebSite.Host project has a page to list products (`Pages/Products.cshtml`). It also uses the UI from the blogging module. `PublicWebSiteHostModule` adds dependencies to `BloggingWebModule` (*[Volo.Blogging.Web](https://www.nuget.org/packages/Volo.Blogging.Web)* package) for that purpose. + +A screenshot from the Products page: + +![microservice-sample-public-product-list](../images/microservice-sample-public-product-list.png) + +#### Using Microservices + +Publc web site application uses the Blogging and Product microservices for all operations, over the Public Web Site Gateway (PublicWebSiteGateway.Host). + +##### Remote End Point + +`appsettings.json` file contains the `RemoteServices` section to declare the remote service endpoint(s). Each microservice will normally have different endpoints. However, this solution uses the API Gateway pattern to provide a single endpoint for the applications: + +```json +"RemoteServices": { + "Default": { + "BaseUrl": "http://localhost:64897/" + } +} +``` + +`http://localhost:64897/` is the URL of the *PublicWebSiteGateway.Host* project. It knows where are Blogging and Product services are located. + +##### HTTP Clients + +`PublicWebSiteHostModule` class declares dependencies for `BloggingHttpApiClientModule` and `ProductManagementHttpApiClientModule` to be able to use remote HTTP APIs for these services. + +##### Passing the Access Token + +Just like explained in the Backend Admin Application section, Public Web Site project also uses the `AbpHttpClientIdentityModelModule` to pass `access_token` to the calling services for authentication. + +#### Other Dependencies + +- **Redis** for distributed/shared caching. +- **Elasticsearch** for storing logs. + +### Console Client Demo + +TODO + ## Microservices ### Identity Service -... \ No newline at end of file +TODO + +## Infrastructure + +TODO + +### Messaging + +### Caching + +### Logging + +### Correlation Id \ No newline at end of file diff --git a/docs/en/images/microservice-sample-authserver-home.png b/docs/en/images/microservice-sample-authserver-home.png new file mode 100644 index 0000000000..7ae62ad6d5 Binary files /dev/null and b/docs/en/images/microservice-sample-authserver-home.png differ diff --git a/docs/en/images/microservice-sample-authserver-login.png b/docs/en/images/microservice-sample-authserver-login.png new file mode 100644 index 0000000000..f6e8dfaa9f Binary files /dev/null and b/docs/en/images/microservice-sample-authserver-login.png differ diff --git a/docs/en/images/microservice-sample-backend-ui-permissions.png b/docs/en/images/microservice-sample-backend-ui-permissions.png new file mode 100644 index 0000000000..02769be3e5 Binary files /dev/null and b/docs/en/images/microservice-sample-backend-ui-permissions.png differ diff --git a/docs/en/images/microservice-sample-backend-ui.png b/docs/en/images/microservice-sample-backend-ui.png new file mode 100644 index 0000000000..0935a6047a Binary files /dev/null and b/docs/en/images/microservice-sample-backend-ui.png differ diff --git a/docs/en/images/microservice-sample-public-product-list.png b/docs/en/images/microservice-sample-public-product-list.png new file mode 100644 index 0000000000..cf8649bbe5 Binary files /dev/null and b/docs/en/images/microservice-sample-public-product-list.png differ diff --git a/docs/en/images/microservice-sample-solution.png b/docs/en/images/microservice-sample-solution.png new file mode 100644 index 0000000000..4c8344ed68 Binary files /dev/null and b/docs/en/images/microservice-sample-solution.png differ diff --git a/docs/zh-Hans/Event-Bus.md b/docs/zh-Hans/Event-Bus.md new file mode 100644 index 0000000000..0adb56101d --- /dev/null +++ b/docs/zh-Hans/Event-Bus.md @@ -0,0 +1,3 @@ +# Event Bus + + TODO \ No newline at end of file diff --git a/docs/zh-Hans/Microservice-Architecture.md b/docs/zh-Hans/Microservice-Architecture.md new file mode 100644 index 0000000000..166591e779 --- /dev/null +++ b/docs/zh-Hans/Microservice-Architecture.md @@ -0,0 +1,30 @@ +# 微服务架构 + +*"作为**面向服务架构**(SOA)的一个变体,微服务是一种将应用程序分解成**松散耦合服务**的新型架构风格. 通过**细粒度**的服务和**轻量级**的协议,微服务提供了更多的**模块化**,使应用程序更容易理解,开发,测试,并且更容易抵抗架构侵蚀. 它使小型团队能够**开发,部署和扩展**各自的服务,实现开发的**并行化**.它还允许通过**连续重构**形成单个服务的架构. 基于微服务架构可以实现**持续交付和部署**."* + +— [维基百科](https://zh.wikipedia.org/wiki/Microservices) + +## 介绍 + +ABP框架的主要目标之一就是提供**便捷的基础设施来创建微服务解决方案**. 我们做了以下工作: + +* 提供[模块系统](Module-Development-Basics.md),允许将应用程序拆分为模块,其中每个模块可以拥有自己的数据库,实体,服务,API,UI组件/页面....等. +* 提供[架构模型](Best-Practices/Module-Architecture.md)来开发模块,与微服务开发和部署兼容. +* 提供[最佳实践指南](Best-Practices/Index.md)制定模块开发标准. +* 提供基础设施来实现微服务中的[领域驱动设计](Domain-Driven-Design.md). +* 提供从应用程序服务[自动生成REST风格的API](AspNetCore/Auto-API-Controllers.md)的服务. +* 提供[自动创建C#API客户端](AspNetCore/Dynamic-CSharp-API-Clients.md)服务,以便从其他服务/应用程序使用你服务. +* 提供[分布式事件总线](Event-Bus.md)用于服务通信. +* 提供更多其他服务,使日常开发更加简便. + +## 在新应用程序中使用微服务 + +开始一个新解决方案建议**始终从单体开始**, 保持模块化,在单体成为问题时将其拆分为微服务.这使初期进度会很快,特别是如果你的团队人数不多,并且不想处理微服务架构带来的各种挑战. + +然而开发一个良好的模块化应用程序不是那么简单,因为很难像微服务那样**保持模块之间的隔离** (参阅 [Stefan Tilkov的文章](https://martinfowler.com/articles/dont-start-monolith.html)). 微服务架构会自然的让你开发隔离的服务,但是在模块化的单体应用程序中,模块很容易彼此紧密耦合并设计出**弱模块边界**和API约定. + +ABP可以帮助你,它提供了与**与微服务兼容的严格模块架构** 在这个架构中你的模块被分割成多个层/项目,在自己的VS解决方案中进行开发,该解决方案完成独立于其它模块. 这种方式开发的模块是一种天然的微服务,但是它可以很容易的插入到单体应用程序中. 请参阅**微服务优先的模块设计**的[模块开发最佳实践指南](Best-Practices/Index.md). 所有[标准的ABP模块](https://github.com/abpframework/abp/tree/master/modules)都是基于本指南开发的. 因此你可以将这些模块嵌入到单体解决方案中使用它们,也可以单独部署通过远程API调用. 它们可以共享一个数据库,也可以通过简单配置使用自己的数据库. + +## 微服务解决方案示例 + +[微服务解决方案示例](Samples/Microservice-Demo.md)演示了基于ABP框架的完整的微服务的解决方案. \ No newline at end of file diff --git a/docs/zh-Hans/Samples/Microservice-Demo.md b/docs/zh-Hans/Samples/Microservice-Demo.md new file mode 100644 index 0000000000..eeac6e0749 --- /dev/null +++ b/docs/zh-Hans/Samples/Microservice-Demo.md @@ -0,0 +1,40 @@ +# 微服务解决方案示例 + +*"作为**面向服务架构**(SOA)的一个变体,微服务是一种将应用程序分解成**松散耦合服务**的新型架构风格. 通过**细粒度**的服务和**轻量级**的协议,微服务提供了更多的**模块化**,使应用程序更容易理解,开发,测试,并且更容易抵抗架构侵蚀. 它使小型团队能够**开发,部署和扩展**各自的服务,实现开发的**并行化**.它还允许通过**连续重构**形成单个服务的架构. 基于微服务架构可以实现**持续交付和部署**."* + +— [维基百科](https://zh.wikipedia.org/wiki/Microservices) + +## 介绍 + +ABP框架的主要目标之一就是提供[便捷的基础设施来创建微服务解决方案](../Microservice-Architecture.md). + +此示例演示了一个简单而完整的微服务解决方案; + +* 拥有多个可独立可单独部署的**微服务**. +* 多个**Web应用程序**, 每一个都使用不同的API网关. +* 使用[Ocelot](https://github.com/ThreeMammals/Ocelot)库开发了多个**网关** / BFFs ([用于前端的后端](https://docs.microsoft.com/zh-cn/azure/architecture/patterns/backends-for-frontends)). +* 包含使用[IdentityServer](https://identityserver.io/)框架开发的 **身份认证服务**. 它也是一个带有UI的SSO(单点登陆)应用程序. +* 有**多个数据库**. 一些微服务有自己的数据库,也有一些服务/应用程序共享同一个数据库(以演示不同的用例). +* 有不同类型的数据库: **SQL Server** (与 **Entity Framework Core** ORM) 和 **MongoDB**. +* 有一个**控制台应用程序**使用身份验证展示使用服务最简单的方法. +* 使用[Redis](https://redis.io/)做**分布式缓存**. +* 使用[RabbitMQ](https://www.rabbitmq.com/)做服务间的**消息**传递. +* 使用[Kubernates](https://kubernetes.io/)**部署**和运行所有的服务和应用程序. + +下图显示了该系统: + +![microservice-sample-diagram](../images/microservice-sample-diagram.png) + +### 源码 + +你可以从[GitHub仓库](https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo)获取源码. + +### 状态 + +此示例仍处于开发阶段,尚未完成. + +## 微服务 + +### 身份认证服务 + +... \ No newline at end of file diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json index 633155adc7..d9ac2aea60 100644 --- a/docs/zh-Hans/docs-nav.json +++ b/docs/zh-Hans/docs-nav.json @@ -255,6 +255,23 @@ } ] }, + { + "text": "示例", + "items": [ + { + "text": "微服务示例", + "path": "Samples/Microservice-Demo.md" + } + ] + }, + { + "text": "应用模块", + "path": "Modules/Index.md" + }, + { + "text": "微服务架构", + "path": "Microservice-Architecture.md" + }, { "text": "测试" }, diff --git a/docs/zh-Hans/images/microservice-sample-diagram.png b/docs/zh-Hans/images/microservice-sample-diagram.png new file mode 100644 index 0000000000..b1d9f6c66e Binary files /dev/null and b/docs/zh-Hans/images/microservice-sample-diagram.png differ diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo.Abp.AspNetCore.Mvc.Client.csproj b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo.Abp.AspNetCore.Mvc.Client.csproj index 911782697b..9d05c9e34b 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo.Abp.AspNetCore.Mvc.Client.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo.Abp.AspNetCore.Mvc.Client.csproj @@ -17,6 +17,7 @@ + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/AbpAspNetCoreMvcClientModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/AbpAspNetCoreMvcClientModule.cs index 3f1ed47435..6c306073c5 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/AbpAspNetCoreMvcClientModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/AbpAspNetCoreMvcClientModule.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Caching; using Volo.Abp.Http.Client; +using Volo.Abp.Localization; using Volo.Abp.Modularity; namespace Volo.Abp.AspNetCore.Mvc.Client @@ -8,7 +9,8 @@ namespace Volo.Abp.AspNetCore.Mvc.Client [DependsOn( typeof(AbpHttpClientModule), typeof(AbpAspNetCoreMvcContractsModule), - typeof(AbpCachingModule) + typeof(AbpCachingModule), + typeof(AbpLocalizationModule) )] public class AbpAspNetCoreMvcClientModule : AbpModule { @@ -21,6 +23,11 @@ namespace Volo.Abp.AspNetCore.Mvc.Client RemoteServiceName, asDefaultServices: false ); + + Configure(options => + { + options.GlobalContributors.Add(); + }); } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/CachedApplicationConfigurationClient.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/CachedApplicationConfigurationClient.cs index f7df64b257..4ceb8bdd1c 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/CachedApplicationConfigurationClient.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/CachedApplicationConfigurationClient.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using Microsoft.AspNetCore.Http; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; @@ -45,7 +46,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Client async () => await Proxy.Service.GetAsync(), () => new DistributedCacheEntryOptions { - AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(5) + AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(60) //TODO: Should be configurable. Default value should be higher (5 mins would be good). } ); @@ -59,7 +60,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Client protected virtual string CreateCacheKey() { - return $"ApplicationConfiguration_{CurrentUser.Id?.ToString("N") ?? "Anonymous"}"; + return $"ApplicationConfiguration_{CurrentUser.Id?.ToString("N") ?? "Anonymous"}_{CultureInfo.CurrentUICulture.Name}"; } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/CachedApplicationConfigurationClientExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/CachedApplicationConfigurationClientExtensions.cs new file mode 100644 index 0000000000..601a2d1088 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/CachedApplicationConfigurationClientExtensions.cs @@ -0,0 +1,13 @@ +using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; +using Volo.Abp.Threading; + +namespace Volo.Abp.AspNetCore.Mvc.Client +{ + public static class CachedApplicationConfigurationClientExtensions + { + public static ApplicationConfigurationDto Get(this ICachedApplicationConfigurationClient client) + { + return AsyncHelper.RunSync(client.GetAsync); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/RemoteLocalizationContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/RemoteLocalizationContributor.cs new file mode 100644 index 0000000000..d994a84335 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/RemoteLocalizationContributor.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.Localization; + +namespace Volo.Abp.AspNetCore.Mvc.Client +{ + public class RemoteLocalizationContributor : ILocalizationResourceContributor + { + private LocalizationResource _resource; + private ICachedApplicationConfigurationClient _applicationConfigurationClient; + private ILogger _logger; + + public void Initialize(LocalizationResourceInitializationContext context) + { + _resource = context.Resource; + _applicationConfigurationClient = context.ServiceProvider.GetRequiredService(); + _logger = context.ServiceProvider.GetService>() + ?? NullLogger.Instance; + } + + public LocalizedString GetOrNull(string cultureName, string name) + { + var resource = GetResourceOrNull(); + if (resource == null) + { + return null; + } + + var value = resource.GetOrDefault(name); + if (value == null) + { + return null; + } + + return new LocalizedString(name, value); + } + + public void Fill(string cultureName, Dictionary dictionary) + { + var resource = GetResourceOrNull(); + if (resource == null) + { + return; + } + + foreach (var keyValue in resource) + { + dictionary[keyValue.Key] = new LocalizedString(keyValue.Key, keyValue.Value); + } + } + + private Dictionary GetResourceOrNull() + { + var resource = _applicationConfigurationClient + .Get() + .Localization.Values + .GetOrDefault(_resource.ResourceName); + + if (resource == null) + { + _logger.LogWarning($"Could not find the localization resource {_resource.ResourceName} on the remote server!"); + } + + return resource; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/RemoteSettingProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/RemoteSettingProvider.cs new file mode 100644 index 0000000000..db3ea6e9a6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/RemoteSettingProvider.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Settings; + +namespace Volo.Abp.AspNetCore.Mvc.Client +{ + public class RemoteSettingProvider : ISettingProvider, ITransientDependency + { + protected ICachedApplicationConfigurationClient ConfigurationClient { get; } + + public RemoteSettingProvider(ICachedApplicationConfigurationClient configurationClient) + { + ConfigurationClient = configurationClient; + } + + public async Task GetOrNullAsync(string name) + { + var configuration = await ConfigurationClient.GetAsync(); + return configuration.Setting.Values.GetOrDefault(name); + } + + public async Task> GetAllAsync() + { + var configuration = await ConfigurationClient.GetAsync(); + return configuration + .Setting.Values + .Select(s => new SettingValue(s.Key, s.Value)) + .ToList(); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationDto.cs index ecafcb3274..6f41399c29 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationDto.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationDto.cs @@ -9,6 +9,8 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations public ApplicationAuthConfigurationDto Auth { get; set; } + public ApplicationSettingConfigurationDto Setting { get; set; } + public CurrentUserDto CurrentUser { get; set; } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationSettingConfigurationDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationSettingConfigurationDto.cs new file mode 100644 index 0000000000..40cbd89fd1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationSettingConfigurationDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations +{ + [Serializable] + public class ApplicationSettingConfigurationDto + { + public Dictionary Values { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/AbpTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/AbpTagHelperService.cs index ce47163be0..566cbcffbc 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/AbpTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/AbpTagHelperService.cs @@ -56,93 +56,5 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers Process(context, output); return Task.CompletedTask; } - - protected virtual TagHelperOutput GetInnerTagHelper(TagHelperAttributeList attributeList, TagHelperContext context, TagHelper tagHelper, string tagName = "div", TagMode tagMode = TagMode.SelfClosing, bool runAsync = false) - { - var innerOutput = new TagHelperOutput(tagName, attributeList, (useCachedResult, encoder) => Task.Run(() => new DefaultTagHelperContent())) - { - TagMode = tagMode - }; - - var innerContext = new TagHelperContext(attributeList, context.Items, Guid.NewGuid().ToString()); - - tagHelper.Init(context); - - if (runAsync) - { - AsyncHelper.RunSync(() => tagHelper.ProcessAsync(innerContext, innerOutput)); - } - else - { - tagHelper.Process(innerContext, innerOutput); - } - - return innerOutput; - } - - protected virtual string RenderTagHelper(TagHelperAttributeList attributeList, TagHelperContext context, TagHelper tagHelper, HtmlEncoder htmlEncoder, string tagName = "div", TagMode tagMode = TagMode.SelfClosing, bool runAsync = false) - { - var innerOutput = GetInnerTagHelper(attributeList, context, tagHelper, tagName, tagMode, runAsync); - - return RenderTagHelperOutput(innerOutput, htmlEncoder); - } - - protected virtual string RenderTagHelperOutput(TagHelperOutput output, HtmlEncoder htmlEncoder) - { - using (var writer = new StringWriter()) - { - output.WriteTo(writer, htmlEncoder); - return writer.ToString(); - } - } - - protected virtual T GetAttribute(ModelExplorer property) where T : Attribute - { - return property?.Metadata?.ContainerType?.GetTypeInfo()?.GetProperty(property.Metadata.PropertyName)?.GetCustomAttribute(); - } - - protected virtual List GetFormGroupContentsList(TagHelperContext context, out bool surpress) - { - var items = GetValueFromContext>(context, FormGroupContents); - surpress = items != null; - - return items ?? new List(); - } - - protected virtual T GetValueFromContext(TagHelperContext context, string key) - { - if (!context.Items.ContainsKey(key)) - { - return default(T); - } - - return (T)context.Items[key]; - } - - protected virtual string GetIdAttributeAsString(TagHelperOutput inputTag) - { - var idAttr = inputTag.Attributes.FirstOrDefault(a => a.Name == "id"); - - return idAttr != null ? "for=\"" + idAttr.Value + "\"" : ""; - } - - protected virtual int GetInputOrder(ModelExplorer explorer) - { - return GetAttribute(explorer)?.Number ?? DisplayOrder.Default; - } - - protected virtual void AddGroupToFormGroupContents(TagHelperContext context, string propertyName, string html, int order, out bool surpress) - { - var list = GetFormGroupContentsList(context, out surpress); - - if (list != null && !list.Any(igc => igc.HtmlContent.Contains("id=\"" + propertyName.Replace('.', '_') + "\""))) - { - list.Add(new FormGroupItem - { - HtmlContent = html, - Order = order - }); - } - } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Breadcrumb/AbpBreadcrumbItemTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Breadcrumb/AbpBreadcrumbItemTagHelperService.cs index c19ac3acd8..1d6204373a 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Breadcrumb/AbpBreadcrumbItemTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Breadcrumb/AbpBreadcrumbItemTagHelperService.cs @@ -2,6 +2,7 @@ using System.Text.Encodings.Web; using Microsoft.AspNetCore.Razor.TagHelpers; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Microsoft.AspNetCore.Razor.TagHelpers; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Breadcrumb { @@ -20,13 +21,13 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Breadcrumb output.Attributes.AddClass("breadcrumb-item"); output.Attributes.AddClass(AbpBreadcrumbItemActivePlaceholder); - var list = GetValueFromContext>(context, BreadcrumbItemsContent); + var list = context.GetValue>(BreadcrumbItemsContent); output.Content.SetHtmlContent(GetInnerHtml(context, output)); list.Add(new BreadcrumbItem { - Html = RenderTagHelperOutput(output, _encoder), + Html = output.Render(_encoder), Active = TagHelper.Active }); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Carousel/AbpCarouselItemTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Carousel/AbpCarouselItemTagHelperService.cs index 9b44254f17..5a4bafd47d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Carousel/AbpCarouselItemTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Carousel/AbpCarouselItemTagHelperService.cs @@ -3,6 +3,7 @@ using System.Text; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Razor.TagHelpers; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Microsoft.AspNetCore.Razor.TagHelpers; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Carousel { @@ -32,9 +33,9 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Carousel private void AddToContext(TagHelperContext context, TagHelperOutput output) { - var getOutputAsHtml = RenderTagHelperOutput(output, _encoder); + var getOutputAsHtml = output.Render(_encoder); - var itemList = GetValueFromContext>(context, CarouselItemsContent); + var itemList = context.GetValue>(CarouselItemsContent); itemList.Add(new CarouselItem(getOutputAsHtml, TagHelper.Active ?? false)); } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Collapse/AbpAccordionItemTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Collapse/AbpAccordionItemTagHelperService.cs index 1f9f57c567..9dc279334b 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Collapse/AbpAccordionItemTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Collapse/AbpAccordionItemTagHelperService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.TagHelpers; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Collapse { @@ -15,7 +16,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Collapse var html = GetAccordionHeaderItem(context, output) + GetAccordionContentItem(context, output, innerContent); - var tabHeaderItems = GetValueFromContext>(context, AccordionItems); + var tabHeaderItems = context.GetValue>(AccordionItems); tabHeaderItems.Add(html); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Dropdown/AbpDropdownButtonTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Dropdown/AbpDropdownButtonTagHelperService.cs index 8b01781602..ee17262d96 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Dropdown/AbpDropdownButtonTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Dropdown/AbpDropdownButtonTagHelperService.cs @@ -1,10 +1,12 @@ using System; using System.Text; using System.Text.Encodings.Web; +using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Microsoft.AspNetCore.Razor.TagHelpers; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Button; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Dropdown { @@ -22,22 +24,25 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Dropdown _serviceProvider = serviceProvider; } - public override void Process(TagHelperContext context, TagHelperOutput output) + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { - var buttonsAsHtml = GetButtonsAsHtml(context, output); + var content = await output.GetChildContentAsync(); + + var buttonsAsHtml = GetButtonsAsHtml(context, output, content); output.PreElement.SetHtmlContent(buttonsAsHtml); output.TagName = "div"; output.TagMode = TagMode.StartTagAndEndTag; + output.Content.SetContent(""); output.Attributes.Clear(); } - protected virtual string GetButtonsAsHtml(TagHelperContext context, TagHelperOutput output) + protected virtual string GetButtonsAsHtml(TagHelperContext context, TagHelperOutput output, TagHelperContent content) { var buttonBuilder = new StringBuilder(""); - var mainButton = GetMainButton(context, output); + var mainButton = GetMainButton(context, output, content); buttonBuilder.AppendLine(mainButton); @@ -51,10 +56,10 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Dropdown return buttonBuilder.ToString(); } - protected virtual string GetMainButton(TagHelperContext context, TagHelperOutput output) + protected virtual string GetMainButton(TagHelperContext context, TagHelperOutput output, TagHelperContent content) { var abpButtonTagHelper = _serviceProvider.GetRequiredService(); - + abpButtonTagHelper.Icon = TagHelper.Icon; abpButtonTagHelper.Text = TagHelper.Text; abpButtonTagHelper.IconType = TagHelper.IconType; @@ -62,15 +67,17 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Dropdown abpButtonTagHelper.ButtonType = TagHelper.ButtonType; var attributes = GetAttributesForMainButton(context, output); - var buttonTag = GetInnerTagHelper(attributes, context, abpButtonTagHelper, "button", TagMode.StartTagAndEndTag); + var buttonTag = abpButtonTagHelper.ProcessAndGetOutput(attributes, context, "button", TagMode.StartTagAndEndTag); + + buttonTag.PreContent.SetHtmlContent(content.GetContent()); if ((TagHelper.NavLink ?? false) || (TagHelper.Link ?? false)) { var linkTag = ConvertButtonToLink(buttonTag); - return RenderTagHelperOutput(linkTag, _htmlEncoder); + return linkTag.Render(_htmlEncoder); } - return RenderTagHelperOutput(buttonTag, _htmlEncoder); + return buttonTag.Render(_htmlEncoder); } protected virtual string GetSplitButton(TagHelperContext context, TagHelperOutput output) @@ -81,7 +88,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Dropdown abpButtonTagHelper.ButtonType = TagHelper.ButtonType; var attributes = GetAttributesForSplitButton(context, output); - return RenderTagHelper(attributes, context, abpButtonTagHelper, _htmlEncoder, "button", TagMode.StartTagAndEndTag); + return abpButtonTagHelper.Render(attributes, context, _htmlEncoder, "button", TagMode.StartTagAndEndTag); } protected virtual TagHelperAttributeList GetAttributesForMainButton(TagHelperContext context, TagHelperOutput output) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Dropdown/AbpDropdownTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Dropdown/AbpDropdownTagHelperService.cs index 945221ff2d..8f37fca5ee 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Dropdown/AbpDropdownTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Dropdown/AbpDropdownTagHelperService.cs @@ -8,6 +8,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Dropdown public override void Process(TagHelperContext context, TagHelperOutput output) { output.TagName = "div"; + output.Attributes.AddClass("dropdown"); output.Attributes.AddClass("btn-group"); SetDirection(context, output); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/ModelExplorerExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/ModelExplorerExtensions.cs new file mode 100644 index 0000000000..b6bedac11b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/ModelExplorerExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions +{ + public static class ModelExplorerExtensions + { + public static T GetAttribute(this ModelExplorer property) where T : Attribute + { + return property?.Metadata?.ContainerType?.GetTypeInfo()?.GetProperty(property.Metadata.PropertyName)?.GetCustomAttribute(); + } + + public static int GetDisplayOrder(this ModelExplorer explorer) + { + return GetAttribute(explorer)?.Number ?? DisplayOrder.Default; + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperContextExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperContextExtensions.cs new file mode 100644 index 0000000000..c2b3e41192 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperContextExtensions.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions +{ + public static class TagHelperContextExtensions + { + public static T GetValue(this TagHelperContext context, string key) + { + if (!context.Items.ContainsKey(key)) + { + return default(T); + } + + return (T)context.Items[key]; + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperExtensions.cs new file mode 100644 index 0000000000..ee984474d2 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperExtensions.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.TagHelpers; +using System.Text.Encodings.Web; +using Volo.Abp.Threading; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions +{ + public static class TagHelperExtensions + { + public static TagHelperOutput ProcessAndGetOutput(this TagHelper tagHelper, TagHelperAttributeList attributeList, TagHelperContext context, string tagName = "div", TagMode tagMode = TagMode.SelfClosing, bool runAsync = false) + { + var innerOutput = new TagHelperOutput(tagName, attributeList, (useCachedResult, encoder) => Task.Run(() => new DefaultTagHelperContent())) + { + TagMode = tagMode + }; + + var innerContext = new TagHelperContext(attributeList, context.Items, Guid.NewGuid().ToString()); + + tagHelper.Init(context); + + if (runAsync) + { + AsyncHelper.RunSync(() => tagHelper.ProcessAsync(innerContext, innerOutput)); + } + else + { + tagHelper.Process(innerContext, innerOutput); + } + + return innerOutput; + } + + public static string Render(this TagHelper tagHelper, TagHelperAttributeList attributeList, TagHelperContext context, HtmlEncoder htmlEncoder, string tagName = "div", TagMode tagMode = TagMode.SelfClosing, bool runAsync = false) + { + var innerOutput = tagHelper.ProcessAndGetOutput(attributeList, context, tagName, tagMode, runAsync); + + return innerOutput.Render(htmlEncoder); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperOutputExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperOutputExtensions.cs new file mode 100644 index 0000000000..0cc9bae6e2 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperOutputExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Razor.TagHelpers; +using System.IO; +using System.Text.Encodings.Web; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions +{ + public static class TagHelperOutputExtensions + { + public static string Render(this TagHelperOutput output, HtmlEncoder htmlEncoder) + { + using (var writer = new StringWriter()) + { + output.WriteTo(writer, htmlEncoder); + return writer.ToString(); + } + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicformTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicformTagHelperService.cs index fa3ec9cce6..a9f3bb9c62 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicformTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicformTagHelperService.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Microsoft.AspNetCore.Razor.TagHelpers; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Button; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form { @@ -66,7 +67,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form ViewContext = TagHelper.ViewContext }; - var formTagOutput = GetInnerTagHelper(output.Attributes, context, formTagHelper, "form", TagMode.StartTagAndEndTag); + var formTagOutput = formTagHelper.ProcessAndGetOutput(output.Attributes, context, "form", TagMode.StartTagAndEndTag); await formTagOutput.GetChildContentAsync(); @@ -146,7 +147,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form { var abpSelectTagHelper = GetSelectGroupTagHelper(context, output, model); - RenderTagHelper(new TagHelperAttributeList(), context, abpSelectTagHelper, _htmlEncoder, "div", TagMode.StartTagAndEndTag); + abpSelectTagHelper.Render(new TagHelperAttributeList(), context, _htmlEncoder, "div", TagMode.StartTagAndEndTag); } protected virtual AbpTagHelper GetSelectGroupTagHelper(TagHelperContext context, TagHelperOutput output, ModelExpression model) @@ -156,7 +157,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form GetSelectTagHelper(model); } - private AbpTagHelper GetSelectTagHelper(ModelExpression model) + protected virtual AbpTagHelper GetSelectTagHelper(ModelExpression model) { var abpSelectTagHelper = _serviceProvider.GetRequiredService(); abpSelectTagHelper.AspFor = model; @@ -165,9 +166,9 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form return abpSelectTagHelper; } - private AbpTagHelper GetAbpRadioInputTagHelper(ModelExpression model) + protected virtual AbpTagHelper GetAbpRadioInputTagHelper(ModelExpression model) { - var radioButtonAttribute = GetAttribute(model.ModelExplorer); + var radioButtonAttribute = model.ModelExplorer.GetAttribute(); var abpRadioInputTagHelper = _serviceProvider.GetRequiredService(); abpRadioInputTagHelper.AspFor = model; abpRadioInputTagHelper.AspItems = null; @@ -184,7 +185,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form abpButtonTagHelper.Text = "Submit"; abpButtonTagHelper.ButtonType = AbpButtonType.Primary; - return RenderTagHelper(attributes, context, abpButtonTagHelper, _htmlEncoder, "button", TagMode.StartTagAndEndTag); + return abpButtonTagHelper.Render(attributes, context, _htmlEncoder, "button", TagMode.StartTagAndEndTag); } protected virtual void ProcessInputGroup(TagHelperContext context, TagHelperOutput output, ModelExpression model) @@ -194,7 +195,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form abpInputTagHelper.ViewContext = TagHelper.ViewContext; abpInputTagHelper.DisplayRequiredSymbol = TagHelper.RequiredSymbols ?? true; - RenderTagHelper(new TagHelperAttributeList(), context, abpInputTagHelper, _htmlEncoder, "div", TagMode.StartTagAndEndTag); + abpInputTagHelper.Render(new TagHelperAttributeList(), context, _htmlEncoder, "div", TagMode.StartTagAndEndTag); } protected virtual List GetModels(TagHelperContext context, TagHelperOutput output) @@ -280,12 +281,12 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form protected virtual bool AreSelectItemsProvided(ModelExplorer explorer) { - return GetAttribute(explorer) != null; + return explorer.GetAttribute() != null; } protected virtual bool IsRadioGroup(ModelExplorer explorer) { - return GetAttribute(explorer) != null; + return explorer.GetAttribute() != null; } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs index bc48d7b1d8..cdf1f30788 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text.Encodings.Web; @@ -6,6 +7,7 @@ using Microsoft.AspNetCore.Mvc.TagHelpers; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Microsoft.AspNetCore.Razor.TagHelpers; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form { @@ -26,7 +28,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form { var innerHtml = GetFormInputGroupAsHtml(context, output, out var isCheckbox); - var order = GetInputOrder(TagHelper.AspFor.ModelExplorer); + var order = TagHelper.AspFor.ModelExplorer.GetDisplayOrder(); AddGroupToFormGroupContents( context, @@ -45,7 +47,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form output.TagMode = TagMode.StartTagAndEndTag; output.TagName = "div"; LeaveOnlyGroupAttributes(context, output); - output.Attributes.AddClass(isCheckbox ? "form-check" : "form-group"); + output.Attributes.AddClass(isCheckbox ? "custom-checkbox" : "form-group"); + output.Attributes.AddClass(isCheckbox ? "custom-control" : ""); output.Attributes.AddClass(isCheckbox ? "mb-2" : ""); output.Content.SetHtmlContent(output.Content.GetContent() + innerHtml); } @@ -54,8 +57,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form protected virtual string GetFormInputGroupAsHtml(TagHelperContext context, TagHelperOutput output, out bool isCheckbox) { var inputTag = GetInputTagHelperOutput(context, output, out isCheckbox); - - var inputHtml = RenderTagHelperOutput(inputTag, _encoder); + + var inputHtml = inputTag.Render(_encoder); var label = GetLabelAsHtml(context, output, inputTag, isCheckbox); var info = GetInfoAsHtml(context, output, inputTag, isCheckbox); var validation = isCheckbox ? "" : GetValidationAsHtml(context, output, inputTag); @@ -78,7 +81,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form var attributeList = new TagHelperAttributeList { { "class", "text-danger" } }; - return RenderTagHelper(attributeList, context, validationMessageTagHelper, _encoder, "span", TagMode.StartTagAndEndTag, true); + return validationMessageTagHelper.Render(attributeList, context, _encoder, "span", TagMode.StartTagAndEndTag, true); } protected virtual string GetContent(TagHelperContext context, TagHelperOutput output, string label, string inputHtml, string validation, string infoHtml, bool isCheckbox) @@ -92,14 +95,14 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form protected virtual string SurroundInnerHtmlAndGet(TagHelperContext context, TagHelperOutput output, string innerHtml, bool isCheckbox) { - return "
" + + return "
" + Environment.NewLine + innerHtml + Environment.NewLine + "
"; } protected virtual TagHelper GetInputTagHelper(TagHelperContext context, TagHelperOutput output) { - var textAreaAttribute = GetAttribute