中介者模式实现与MediatR应用

概要

1. Console App Demo
2. Web API + MediatR Demo

Console App Demo

简单的Console App Demo,包括使用自建的中介者对象实现以及MediatR实现

Web API + MediatR Demo

在WebAPI中使用MediatR,最大的改变是将原本注入到controller的依赖分散到了MediatR消息的处理方法中

传统的依赖注入如下图
传统的依赖注入

使用MediatR后,MediatR作为抽象的一层将依赖进一步根据具体逻辑分散
使用MediatR的依赖注入

而接口则变得非常简洁,即
1.发送MediatR消息
2.获得结果
3.返回结果

在MediatR消息的处理方法中,当然也可以通过注入MediatR来再次发送消息,即嵌套的调用。最大程度得分离逻辑来降低逻辑上的耦合

...
using MediatR;
using Application.Exceptions;
using Application.Item.Models;
using Data.RepositoryInterfaces;
using Domain.BaseInterfaces;
using Domain.Entities;

namespace WebApp.Controllers
{

    [ApiController]
    public class ItemController : ControllerBase
    {

        private readonly IMediator _mediator;

        public ItemController(IMediator mediator)
        {
            _mediator = mediator;
        }

        [HttpGet]
        [Route("")]
        public async Task<ActionResult> Get(int itemId)
        {
            var result = await _mediator.Send(new GetItem(id, userid));
            if (result == null) return NotFound();
            return Ok(result);
        }
    }
}

namespace Application.Item.Queries
{
    public class GetItem : IRequest<ItemViewModel>
    {
        public GetItem(int itemId, string userId)
        {
            UserId = userId;
            ItemId = itemId;
        }

        public string UserId { get; }
        public int ItemId { get; }
    }

    public class GetItemHandler : IRequestHandler<GetItem, ItemViewModel>
    {
        private readonly IMediator _mediator;
        private readonly IItemRepository _itemRepository;

        public GetItemHandler(
            IMediator mediator,
            IItemRepository itemRepository
        )
        {
            _mediator = mediator;
            _itemRepository = itemRepository;
        }

        public async Task<ItemViewModel> Handle(GetItem arg, CancellationToken cancellationToken)
        {
            var item = await _itemRepository.Get()
                .Where(i => i.Id == arg.ItemId)
                .Select(i => new
                {
                    Id = i.Id,
                    Name = i.Name,
                }).SingleOrDefaultAsync();

            if (item == null)
                throw new NotFoundException(arg.ItemId.ToString(), nameof(Domain.Entities.Item));

            var companyUser = await _mediator.Send(new GetCompanyUser(arg.UserId, item.CompanyId));

            bool isCompanyUser = companyUser != null;

            return new ItemViewModel(){
                ItemId = item.Id,
                ItemName = item.Name,
                IsCompanyUser = isCompanyUser
            };
        }
    }

    public class GetItemPreProcessor : IRequestPreProcessor<GetItem>
    {
        private readonly IMediator _mediator;
        public GetItemPreProcessor(
            IMediator mediator)
        {
            _mediator = mediator;
        }

        public Task<ItemViewModel>  Process(GetItem request, CancellationToken cancellationToken)
        {
            return await _mediator.Send(new InsertItemViewRecord(request));
        }
    }
}


namespace Application.Item.Commands
{
    //implement InsertItemViewRecord
}

Demo主要流程如下图

中介者设计模式与MediatR

概要

1. 中介者模式
2. MediatR

中介者模式

什么是中介者模式

中介者模式即对象通过中介者交互而非直接进行。

中介者模式让原本类之间的依赖转移到为了中介者与类间,由此在很大程度上降低程序的耦合度,并大大简化类之间的通信。

中介者类似消息的处理中心,对象向中介者发送请求,中介者分发请求至对应目标。获得目标响应后返回至发起者。

为什么要使用中介者模式

1.程序随功能增多产生复杂的依赖关系,某一方法可能分别依赖不同层级的对象

2.类间的通信、调用流程由于多对多的关系变得繁琐

3.由于类间的相互引用而产生高耦合的代码

中介者模式如何解决问题

以WebApi工程为例,最上层的controller只依赖于中介者对象,中介者对象依赖于业务逻辑。

1.根据中介者的位置进行分割,此时中介者作为controller和业务逻辑之间的分隔起到解耦的作用。上层的controller与下层的业务逻辑始终只与中介者交互,两部分即消息的发送和接收者可以独立的修改,controller的替换或者更新的工作量也就大大减少。

2.在我们对所有的逻辑都尽可能地进行分割的前提下,通过于中介者交互获得某一逻辑片段的响应,组合后完成复杂逻辑。

原本的复杂逻辑可能依赖于多个下层方法,分隔后复杂逻辑只依赖于中介者。

原本依赖的多个下层方法现在分布在各个逻辑片段中,由此,下层方法的改动只对这些逻辑片段产生影响。

3.以复杂逻辑为例,中介消息的接收者只表明调用各个逻辑的顺序以及组合方式。逻辑片段及其依赖的下层方法的修改只要保证自身的返回内容正确,符合请求目的,就不会对其他的所依赖的对象产生影响

中介者模式存在的问题

有的朋友可能说了,我只要写一个巨大的类,把所有的业务逻辑的方法都放进去,让所有的controller都依赖于这个类,那我是不是也实现了中介者模式?

从结构上来说,这个类也是起到了中介者的角色。但是随着业务增加,中介者对象变得臃肿庞大。

其次这个巨大的类相当于对所有的下层方法产生了依赖,每次使用这个类时,都需要将所有的依赖注入,与依赖注入的目的相违背。

而MediatR在一定程度上解决了这个问题。MediatR提供了简洁且功能丰富的中介者对象
并将依赖向外部分散,依赖注入到细致的下层逻辑中

MediatR

什么是MediatR

.NET中的中介者模式实现,一种进程内消息传递框架,提供了中介者对象
MediatR支持以同步或异步的形式进行请求/响应,支持消息的单播/多播

WebApp中应用MediatR

MediatR在WebApi中的位置处于Application与Presentation/Controller之间,请求进入后发送通过MediatR消息,消息处理方法中调用Application

由此,MediatR作为Presentation/Controller和Application的中间连接件,前者只对MediatR产生依赖,而MediatR只对后者产生依赖

把MediatR inject 到 presentation和把service inject到presentation有什么区别和各自的利弊?

首先就是两个依赖关系不一样 理想状态下是presentation只对mediator中介者产生依赖 如果是service直接注入的话 通常情况下不可能只注入一个 presentation就会产生多个引用 增加两者之间的耦合

MediatR把presentation和service分隔开来 这样两个部分的修改变更就更简单

优势是MediatR能分散所有的依赖 降低耦合
弊端是中介者成了程序不能分割的一部分 所以他必须要可靠高效

Dynamic Business Central API 接入与调试

概要

1. Business Central
2. 配置以及调试

Business Central

简单介绍

Dynamic 365 Business Central是专为中小企业设计的全功能ERP解决方案,Dynamic NAV的云上Saas版本。包括财务,进销存,CRM,供应链管理等功能。

接口相关

Dynamic 365 Business Central 提供除了仓储相关外所有的REST API,并提供OAuth 2.0,Basic Auth(只建议开发使用)两种验证方式。提供各个模块中每个文档的CURD,文档地址
需要使用仓库相关接口,则需要配置Web Servic以OData的方式请求。也可以通过安装第三方的BC插件,得到第三方提供的Web Service。使用OData V4即可以得到Json格式的返回报文。

配置以及调试

验证方式

Basic Auth即用户名密码验证,实质上也是在Auth服务器请求得到Token后发起。在BC Users card中得到用户名User Name以及密码信息Web Service Access Key。
填入Basic Auth对应字段即可。

OAuth2.0即使用Azure AD的验证服务器请求得到Token后,使用Token请求。

OData

Open Data Protocol是一种描述如何创建和访问Restful服务的OASIS标准。简而言之,OData通过url变化和http method的组合,达到新增、变更、删除所调用资源的目的。这里以Business Central Warehouse Shipment为例。
首先进入Web Service添加相关服务。可以看到BC自带的shipment服务(ID 7335)和安装了第三方插件后增加的相关Web Service。
使用OData V4 URL以获得Json格式的响应。

以下是GET示例

Azure App Service 多容器部署实例

概要

1. 项目结构
2. 构建以及部署
3. 相关链接

项目结构

一个简单的Web应用 – Switches

应用实现了一个可变长度的布尔数组修改存储,并在前端以switch的形式展示。点击switch时发送请求修改数组中某元素值,修改成功后重新获取数组值。
项目使用Angular, .Net Core, Redis, 前后端分离结构。后端连接redis以实现简单存储的功能。

在一个App Service中实现

App Service实例中只需要运行两个容器即.Net Core App以及Redis, 前端打包后的js以静态文件的形式host在Net Core的默认目录下。
核心是Docker Compose的使用,并为.Net Core App,redis两者之间建立连接。

为什么要需要Scaling

AutoScale在应用负载时让适当数量的资源运行。 当负载增加时,添加资源来处理增加的负载;当资源空闲时,删除资源以节省资金。
App Service中存在两种Scaling即Scale Up以及Scale Out。

Scale Up即对整个实例进行扩大,得到更强计算能力的同时也意味着需要付出更多的成本。
Scale Out即对实例的数量进行调整,即时每个实例的大小相同,但处理工作的能力自然随着数量增加而提高

AutoScale只支持Scale out,并能制定复杂的规则自动化调整,对于用户数量不多但并发较高的应用更加友好。

为什么要需要多容器运行

发挥多实例即Scale Out的优势。Scale Out后拥有多个后端实例,多个redis实例。为高并发提供保障。

App Service的Scaling功能以实例为单位,故多容器运行的实例在Scaling时容器运行数量会成倍增长。而且由于自动创建销毁的特性,App Service不适用于存储功能的应用,但我们可以在实例中使用缓存服务来提高应用的响应速度。
例如WordPress的Redis应用。每个实例第一次访问数据库后将数据存储在Redis,下一次获取数据时先检索redis获取,由此减少对数据库的访问。

当然也可以对Redis做持久化的存储,在容器重启时不丢失数据。

构建以及部署

构建.Net Core App

工程简单分为API以及Data两个工程,Data中的RedisService类包含连接Redis,操作Redis的方法。
在StartUp时注册为一个singleton service,注入至Controller实现调用。
API中存在两个接口,获取当前布尔数组值,修改当前数组值。

[HttpGet]
public async Task<string> Get()
{
    return await _redisService.Get(_switchKey);
}

异步调用RedisService中方法并返回结果。

构建Angular App

工程简单分为Service,Components两个部分,Service中使用Anglar HttpClient发送请求,Component中存在一个组件使用多个Switch的状态展示当前数组值。OnChange时获取所有Switch状态作为参数通过注入的Service发起修改请求。
这里修改了build prod时的输出目录为.Net Core App开启静态文件支持后默认的wwwroot目录,即以静态文件的形式serve。

  "scripts": {
    "build:prod": "ng build --prod --output-path=../Switches/wwwroot/",
  }
构建Dock Compose

App service中多容器运行的核心即Dock Compose,
这里首先声明所有需要的容器即.Net Core App以及Redis。
由于.Net Core App在启动时首先尝试连接Redis,但若此时Redis仍未启动,将导致无法连接从而无法正完成startUp。如何解决?使用depends_on字段,使得Redis容器能够先于.Net Core App启动。

version: "3"

services:
  switches:
    container_name: APIServer
    image: "ymhvic/switches"
    depends_on:
      - redis
    restart: always
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "80:80"
      - "443:443"

  redis:
    container_name: redis
    image: "redis:latest"
    restart: always
    ports:
      - "6379:6379"

以此模板启动,宿主机的80以及443端口将映射到.Net Core App容器对应端口,6379映射至Redis。

由于App Service不支持networks字段,这里无法使用内网ip连接
.Net Core App的redis连接字符串如下:

{
    "RedisConnectionString": "redis:6379"
}

可以在下文的git仓库中找到使用networks的Docker compose。
这里附上App Service不支持的Docker compose字段。https://docs.microsoft.com/en-us/azure/app-service/containers/configure-custom-container#configure-multi-container-apps

迭代更新流程

前端修改后build prod输出至wwwroot,后端修改后重新生成镜像push至仓库。
重启app service下载最新镜像即完成新版本的更新。

相关链接

App Service实例
https://switches.azurewebsites.net

ymhvic/Switches镜像仓库
https://hub.docker.com/repository/docker/ymhvic/switches

.Net Core App代码仓库
https://github.com/VictorYMH/Switches

Azure App Service 多容器运行以及代码直接部署

概要

1. 多容器以及代码直接部署
2. 部署过程

多容器以及代码直接部署

Azure App Service中同时运行多个容器

在之前的博客中我们提到过,App Service本质上以容器的方式运行在一个Azure VM中,只是根据选择的Service Plan不同在网络环境以及硬件上有所区别。由此,一个App Service理所当然可以同时运行多个容器。主要的实现方式为使用Docker Compose构建规划多容器的运行。

什么是Docker Compose

Docker Compose是一个用于定义和运行多容器 Docker 的应用程序工具,在目录中添加yaml文件,列出所有需要运行的容器及其相关信息如版本,运行命令等。最后使用docker-compose up即可开始运行多个容器。

Azure App Service中Code直接部署

Code直接部署即直接由本地开环境的代码一步直接部署至Azure。相较于其他部署方式,Code直接部署更快更方便。真正实现了无需任何前置操作的“一键部署”。

部署过程

Code直接部署

Code直接部署这里以C#为例,本文使用了一个.Net Core 3.1的WebAPI工程,Visual Studio中简单构建代码后,直接打开需要发布工程的Publish发布标签,选择App Service为目标。

在VS中登陆需要发布到目标的Azure账号,在创建发布模板时即可通过选择账号下的Subscription订阅、Resource Group资源组,找到对应的App Service(笔者已经先行在Azure中创建了一个App Service实例)。

点击发布,即可以看到Output中的开始输出的发布过程,首先在本地生成DLL等文件,在App Service中创建目录,传输至对应的目录。

笔者由于需要在K8s中测试负载均衡的原因,在WebAPI中添加了一个接口/api/ServerInfo, 线程挂起5s后返回当前主机IP。
使用POSTMAN 请求接口 https://vicdemocode.azurewebsites.net/api/serverinfo

这里返回的是App Service实例在资源组中的内网地址。

到此,就实现了一个WebAPI至App Service的Code方式部署。

单实例运行多容器

与单容器部署类似,多容器的部署只是选择了以Docker Compose的方式完成对实例的设置。在Container Setting中选择Docker Compose,选择上传yaml文件或直接键入文件内容,运行后即可在下方日志看到实例的运行情况。

Docker + Kubernetes 部署.Net Core WebAPI

概要

1. 什么是Docker,Kubernetes?
2. 如何在Docker容器中运行.Net Core WebAPI?
3. 如何使用K8s管理Docker容器并使用负载均衡服务?

1.什么是Docker,Kubernetes?

Docker

这里推荐一篇Docker结构相关文章
1. 为什么要使用Docker,为什么要在容器中运行app?

使用Docker,我们可以在不同的操作系统中对应用完成更快,更方便的部署。
使用容器,可以为应用创建独立,安全的运行环境。

2. 什么是container,image,repository?

container是一个应用的运行实例,包括所有的依赖以及运行库。
image是container的标准副本。
repository是image的托管库。

3. 如何将应用转化为image,从而可以在Docker中作为一个容器运行?

首先在本地构建完成应用并成功运行,在项目中新建文件Dockerfile(一般为根目录),从Docker.io中找到所需的运行环境,sdk,runtime等。在Dockerfile写入用于创建image的流程,引用运行环境,拷贝项目代码至上下文,最后build。

使用Docker cli 创建image,并上传至repo。

Kubernetes

1. 什么是kubernetes,为什么还要使用kubernetes来管理容器?

Kubernetes是一个用于发布,管理容器的系统。

使用Kubernetes来管理容器,可以实现更加高效,安全的应用管理,例如应用的always restart,load balance,auto scaling,反向代理,不间断服务的更换版本等。

2. 什么是cluster,pod,service,ingress,node?

cluster是整个系统的总称,包含kubernetes运行的所有单元
cluster中包含两种node,唯一的master节点和多个子节点
master节点中运行整个kubernetes的管理系统,系统api
Ingress为所有service提供反向代理,分发流量
service为选中的deployment提供访问入口,也可以包含负载均衡的功能
pod是node中的运行单元,pod中可以运行单个或多个容器(一般为单个,可能在容器间互相通信或存在代理服务等情况下运行多个)。通常每个pod都是分布式系统中的一个主机


2.如何在Docker容器中运行.Net Core WebAPI?

.Net Core WebAPI

由于关系到容器创建过程,这里先简单介绍后端工程结构
1. 新建 .net core3 + ef core 工程,简单创建三个layer,最底层Model,中间层Data,以及最上层WebAPI
2. model中使用efcore脚手架生成数据库表的实体类,编写自定义的异常,枚举量等
3. Data中为每个数据库封装一个repo类存放相关逻辑方法
4. 配置跨域,配置数据库连接字符串等

构建Docker镜像

进入项目的根目录下新建dockerfile,填入以下代码

#首先使用FROM AS 的指令从docker官方的镜像仓库中下载得到编译工程的SDK
FROM mcr.microsoft.com/dotnet/core/sdk:3.0 AS build
#暴露容器的80端口
EXPOSE 80
#设置/app为根目录
WORKDIR /app
#COPY方法 第一个参数为当前目录 其次是容器构建的目标目录
#将.Net工程的sln解决方案文件,以及各个依赖工程的csproj项目文件拷如容器对应目录为后面build做准备
COPY \*.sln .
COPY BackEndDemo/\*.csproj ./BackEndDemo/
COPY BackEndDemo.Data/\*.csproj ./BackEndDemo.Data/
COPY BackEndDemo.Model/\*.csproj ./BackEndDemo.Model/
#运行detnet指令还原项目所有的nuget依赖
RUN dotnet restore
#将所有的代码等文件拷入容器中
COPY BackEndDemo/. ./BackEndDemo/
COPY BackEndDemo.Data/. ./BackEndDemo.Data/
COPY BackEndDemo.Model/. ./BackEndDemo.Model/
WORKDIR /app/BackEndDemo
#打包发布整个工程
RUN dotnet publish -c Release -o out
#从官方仓库引入aspnetcore3的运行环境
FROM mcr.microsoft.com/dotnet/core/aspnet:3.0 AS runtime
WORKDIR /app
#将打包发布得到的dll等文件拷入 /out./ 目录
COPY --from=build /app/BackEndDemo/out ./
#设置入口dll为BackEndDemo(即最上层的webapi)
ENTRYPOINT ["dotnet", "BackEndDemo.dll"]

打开cmd,使用docker命令构建镜像,填入名称以及标签,然后上传镜像至仓库

docker build -t <repoName/imageName>:<tagName> .
docker push <repoName/imageName>:<tagName>

发布镜像并运行容器

使用run指令本地运行工程

docker run -it --rm -p 44353:80 --name parainstance1 ymhvic/para

44353:80 表示宿主机的44353端口映射至容器的80端口(即在dockfile中暴露的端口),填入运行实例名称以及要运行的镜像名称

容器在本地运行,访问宿主机ip即localhost:44353即可以访问到接口


3.如何使用K8s管理Docker容器并使用负载均衡服务?

搭建MiniKube

前面已经实现了后端程序至docker容器的打包,现在要将容器放入minkube master节点,并为每一个容器分配一个pod。

首先为了在本地运行kubernetes,选用minkube运行hyperV虚拟机,minkube的作用即配置并为虚拟机注入了K8s的运行环境

开始构建minikube,cmd运行以下命令

minikube start --vm-driver=hyperv --image-repository=registry.cn-hangzhou.aliyuncs.com/google\_containers

由于网络原因,这里手动配置了镜像库到aliyun的节点上,构建完成后看到以下log,说明kubectl指令已经被配置到minikube中运行。

Done! kubectl is now configured to use "minikube"

之后使用kubectl查看log,pod,service即返回minikube中运行信息

发布对象至Kuberbetes

使用kubectl apply命令来上传yaml json等文件以达到部署pod,service,ingress的目的,以下是一个用于发布pod的deployment.yaml实例。

#部署对象类型: Ingress, Service, deployment..
kind: Deployment
#Kubernetes API 版本号
apiVersion: apps/v1
#信息数据
metadata:
  name: para-deployment
  #分配到cluster中的某个命名空间下
  namespace: default
spec:
  selector:
  matchLabels:
    app: para
#重复的实例数
  replicas: 2
  template:
metadata:
  labels:
    app: para
spec:
#docker容器的相关
  containers:
  - name: para
    image: 'ymhvic/para:0.12'
    ports:
    - containerPort: 80
  #重启策略
  restartPolicy: Always
  #部署目标node名称
  nodeName: minikube

deployment代表一个发布计划,包括此次发布的所有详细配置,
也可以使用kubectl create deployment 发布pod。

Service与Ingress

相应的,也需要部署一个service来访问刚刚部署的pod。一般在云服务平台中,平台将提供一个外网ip来访问创建的Kuber service,而在开发环境使用minikube时,则需要运行额外的命令来暴露创建的service。

这里在部署负载均衡类型的服务之后,需要使用以下指令来暴露服务。

minikube service <serviceName>

类型还有NodePort即为某个node提供静态的端口,ClusterIP即只暴露给同个cluster下其他的service使用的控制服务。因为使用了minikube,为了访问K8s暴露的service,需要使用

开发中我们可以直接暴露服务到外部,但进入线上还是建议使用Ingress来访问服务,Ingress即一个Ingress Controller,用于处理流量并分发,映射外部请求到级群内部资源,通常我们可以使用Nginx, Traefik, HAproxy来代替,也可以自己编写

运行中Scaling

一旦将此文件apply至k8s,则k8s将一直”维护”此文件。K8s将执行操作pod直到node中的pod运行情况与该文件描述一致,例如replicas参数,若容器运行报错或人为将pod删除等等情况使得pod数量减少,则K8s将自动增加pod数量至replicas值。

关于replicas,也可通过scale指令来调整(从而无需修改yaml),也可以设置autoscale,由k8s来根据运行情况创建或删除pod。

若需要更改,则修改deployment.yaml 并重新apply即可。

负载均衡

为了测试K8s的负载均衡类型的服务,笔者在后端写了一个接口来返回当前运行环境的IP,并在返回结果前sleep 5s,来模拟服务器长时间处理请求的情况。

由于每个pod的内网ip都不同,可以从返回结果中分辨是由哪个pod处理的请求。为了模拟接口处理请求繁忙的情况,这里使用了Thread.Sleep(5000)来挂起当前线程5s。

public async Task<ActionResult<object>> GetServerInfo()
{
    Thread.Sleep(5000);
    return Ok(System.Net.Dns.GetHostAddresses(System.Net.Dns.GetHostName())
              .Select(
                  ip => ip.ToString()
              ));
}

在1个pod以及5个pod的情况测试了同时发起100次请求的平均返回时间,发现多个pod的情况下响应速度更快,即根据运行情况分配流量到了每个pod。

可以看到单pod的100次请求平均返回时间为35.7s,5个pod带负载均衡服务下的返回时间为8.5s

版本升级

使用K8s,也可以实不中断服务的版本升级。通常情况下,更新应用版本后发布新的镜像至Dock repo。

此时,我们修改deployment.yaml中的image值,将其设为目标的镜像版本(通常通过标签区分)。保存并重新apply,如果本地不存在缓存,此时K8s会从repo pull得到镜像。创建一个使用了新版本的pod并运行,然后删去node中的某一pod,将其替换为新版本pod,此时其他pod当前一直还在处理请求,直到每个pod都被更新。

由此,就完成了一次无down time的版本更新。