.NET 微服务实践(3)-搭建平台服务

文章声明:本文系油管上的一个系列(.NET Microservices – Full Course)教程的学习记录的第二章。不涉及营销,有兴趣看可以原视频(链接在底部)。
本文分成三部分:脚手架(项目创建和包安装)、数据层(模型定义、读写以及转化)、控制器(服务外部调用)。目的是搭建平台服务,测试通过,为之后整体的微服务方案做铺垫。

脚手架

创建项目

ASP.NET Core Web API

添加安装包

  • AutoMapper.Extensions.Microsoft.DependencyInject
  • Microsoft.EntityFrameworkCore.5.0.8
  • Microsoft.EntityFrameworkCore.Design.5.0.8
  • Microsoft.EntityFrameworkCore.InMemory.5.0.8

数据层

创建Platform模型

using System.ComponentModel.DataAnnotations;

namespace PlatformService.PlatformDomain
{
    public class Platform
    {
        [Key]
        [Required]
        public int PlatformId { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public string Publisher { get; set; }

        [Required]
        public string Cost { get; set; }
    }
}

除了定义模型属性外,这里做了两件事情:

  1. 定义了主键
  2. 要求属性必填

另外,我整体采用的是DDD的思路,具体实施的时候命名空间和视频里面不一样。

配置DbContext

using Microsoft.EntityFrameworkCore;
using PlatformService.PlatformDomain;

namespace PlatformService.Data
{
    public class ApplicationDbContext: DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> opt)
            :base(opt)
        {

        }

        public DbSet<Platform> Platforms { get; set; }
    }
}

这里继承了DbContext,并且定义了DbSet Platform。

注册DbContext服务

在Startup.cs文件中的ConfigureService方法里添加服务

using PlatformService.Data;
using Microsoft.EntityFrameworkCore;
......

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(opt =>
        opt.UseInMemoryDatabase("InMemory"));
    services.AddControllers();
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "PlatformService", Version = "v1" });
    });
}

由于.NET Core自带依赖注入容器,这里就是先往容器中添加DbContext服务。注意,尽管最终平台服务的持久化是交给SQL Server来做,但是这里还是先用内存数据库来实现。

定义仓储层接口

using System.Collections.Generic;
using System.Threading.Tasks;

namespace PlatformService.PlatformDomain
{
    public interface IPlatformRepository
    {
        public Task<int> CreatePlatform(Platform createdPlatform);
        public Task<IEnumerable<Platform>> GetAllPlatformsAsync();
        public Task<Platform> GetPlatformById(int platformId);
        public Task<Platform> UpdatePlatform(Platform updatedPlatform);
    }
}

这里和视频教程不一样。

  • 把接口和模型放在了一起(作为Domain的组成部分)、而不是把接口及其实现放在一起。这样的好处在于,接口和实现可以分离,若是在实际项目中,接口可以根据环境已经需要调整不同的实现;同时(仓储层)实现应该和数据持久层保持紧密关联。
  • 按照自己的习惯定义了CRU的接口,同时考虑并发。

实现仓储接口

using PlatformService.PlatformDomain;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace PlatformService.Data
{
    public class PlatformRepository : IPlatformRepository
    {
        private readonly ApplicationDbContext _context;
        public PlatformRepository(ApplicationDbContext context)
        {
            _context = context; 
        }

        public Task<int> CreatePlatformAsync(Platform createdPlatform)
        {
            throw new NotImplementedException();
        }

        public Task<IEnumerable<Platform>> GetAllPlatformsAsync()
        {
            throw new NotImplementedException();
        }

        public Task<Platform> GetPlatformById(int platformId)
        {
            throw new NotImplementedException();
        }

        public Task<Platform> UpdatePlatformAsync(Platform updatedPlatform)
        {
            throw new NotImplementedException();
        }

        private async Task<bool> SaveChnagesAsync()
        {
            return await _context.SaveChangesAsync() >= 0;
        }
    }
}

一般我会在Data文件夹下再加一个Repository的folder用于专门放仓储层的实现,考虑到只有一个Repo,所以没有这么操作。

这里有两点说明:

  • PlatformRepository的构造中利用了依赖注入的方式调用了之前注册的ApplicationContext服务。
  • 实现了一个私有方法用于EFCore整个事务的保存刷新。该方法后续可以用于Create以及Update。

具体实现对Platform对象的读写

public async Task<int> CreatePlatformAsync(Platform createdPlatform)
{
    _context.Platforms.Add(createdPlatform);
    _ = await this.SaveChnagesAsync();
    return createdPlatform.PlatformId;
}

public async Task<IEnumerable<Platform>> GetAllPlatformsAsync()
{
    return await _context.Platforms.AsNoTracking().ToListAsync();
}

public Task<Platform> GetPlatformById(int platformId)
{
    return _context.Platforms
        .Where(platform => platform.PlatformId == platformId)
        .AsNoTracking().FirstOrDefaultAsync();
}

public async Task<Platform> UpdatePlatformAsync(Platform updatedPlatform)
{
    var currentPlatform = _context.Platforms
        .First(platform => platform.PlatformId == platform.PlatformId);
    if(currentPlatform is null)
    {
        throw new Exception($"Platform with Id {updatedPlatform.PlatformId} does not exist");
    }
    _context.Platforms.Update(updatedPlatform);
    _ = await this.SaveChnagesAsync();
    return updatedPlatform;
}

这里和视频不一样的地方是:

  • 对于数据的验证我会放在Controller层完成
  • 读操作加了NoTracking

注册仓储层服务

类似之前ApplicationDBContext,对于仓储层的接口及其实现,也可以通过注册在依赖注入容器的方式进行调用。只需要在Startup中的ConfigureService中加入如下代码即可:

services.AddScoped<IPlatformRepository, PlatformRepository>();

在可以获取仓储层服务的基础上,可以尝试基于mock数据进行在内存数据库中的读写测试。

准备内存数据库以及Mock 数据

准备内存数据库

目前不使用SQL Server,所以要build一个内存数据库。

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

namespace PlatformService.Data
{
    public class MockInMemoryDatabase
    {
        public static void MockPopulation(IApplicationBuilder app)
        {
            using (var serviceScope = app.ApplicationServices.CreateScope())
            {
                SeedData(serviceScope.ServiceProvider.GetService<ApplicationDbContext>());
            }
        }

        private static void SeedData(ApplicationDbContext context)
        {

        }
    }
}

首先构造一个静态方法,能够调用ApplicationBuilder获取依赖注入容器,然后将容器中的ApplicationDbContext取出【在应用实例化时就有对应的DbContext,Scope型】。后期就可以用这个DbContext实现Mock数据的读写了。

然后要在Startup.cs的Configure中调用该静态方法。

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseSwagger();
        app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "PlatformService v1"));
    }

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });

    MockInMemoryDatabase.MockPopulation(app);
}

Configure方法中各个同步操作,可以理解为HTTP请求的pipeline。相当于在最后一步中调用Mock数据进行操作测试。

准备Mock 数据

private static void SeedData(ApplicationDbContext context)
{
    if(!context.Platforms.Any())
    {
        Console.WriteLine(">>>Seeding data...");

        context.Platforms.AddRange(
            new Platform() { Name = "Dot Net", Publisher = "Microsoft", Cost="Free"},
            new Platform() { Name = "SQL Server Express", Publisher = "Microsoft", Cost = "Free" },
            new Platform() { Name = "Kubernetes", Publisher = "Cloud Native Computing Foundation", Cost = "Free" }
        );

        context.SaveChanges();
    }
    else
    {
        Console.WriteLine(">>>Seed data exist...");
    }
}

编译后运行,可以在输出中看到结果

创建DTO

DTO全称Data Transfer Object。它的目的在于隐藏数据结构——通过从Model与DTO的转化,可以控制属性是否应该对调用者可见。保证了数据的隐私性。

相比于ViewModel,我的感觉是它的定位更加灵活,DTO可以分成读和写、ViewModel一般我会将其当做ReadDTO。

这里就创建读和写的DTO

Read DTO

namespace PlatformService.PlatformDomain
{
    public class PlatformReadDto
    {
        public int PlatformId { get; set; }

        public string Name { get; set; }

        public string Publisher { get; set; }

        public string Cost { get; set; }
    }
}

Write DTO

using System.ComponentModel.DataAnnotations;

namespace PlatformService.PlatformDomain
{
    public class PlatformWriteDto
    {
        [Key]
        [Required]
        public int PlatformId { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public string Publisher { get; set; }

        [Required]
        public string Cost { get; set; }
    }
}

这里要解释一下,我整体采用DDD的思路,因此将DTO和Model放在一起。

对于读操作来说,不需要加注解来做模型的验证;对于写操作而言,主键由于是int类型可以在Mapping之后自动生成,因此也不需要Id。但是由于我还考虑了更新操作(也是写操作),所以需要主键来定位。

实现Model和DTO的双向Mapping

注册AutoMapper

在Startup.cs的ConfigureService中注册AutoMapper

// Register AutoMapper
var domainAssemblies = AppDomain.CurrentDomain.GetAssemblies();
services.AddAutoMapper(domainAssemblies);

创建Mapping Profile

在Profile配置Model和DTO的Mapping

using AutoMapper;
using PlatformService.PlatformDomain;

namespace PlatformService.Utils
{
    public class PlatformMappingProfile: Profile
    {
        public PlatformMappingProfile()
        {
            //Source -> Target
            CreateMap<Platform, PlatformReadDto>();
            CreateMap<PlatformWriteDto, Platform>();
        }
    }
}

控制器

创建Controller

using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using PlatformService.PlatformDomain;

namespace PlatformService.Controllers
{
    [Route("api/v1/[controller]s")]
    [ApiController]
    public class PlatformController : ControllerBase
    {
        private readonly IMapper _mapper;
        private readonly IPlatformRepository _platformRepository;

        public PlatformController(
            IPlatformRepository platformRepository,
            IMapper mapper)
        {
            _mapper = mapper;
            _platformRepository = platformRepository;
        }
    }
}

类似IPlatformRepository的构造注入时从依赖注入容器中引入DbContext实例,PlatformController实例化也通过构造注入的方式调用IPlatformRepository和AutoMapper的实例。

路由的注解中,“[controller]”是默认读取类名中Controller之前的部分、也就是“Platform”。

构造Action

[HttpGet]
public async Task<ActionResult<IEnumerable<PlatformReadDto>>> GetAllPlatformsAsync()
{
    Console.WriteLine(">>>Getting Platforms...");
    var platforms = await _platformRepository.GetAllPlatformsAsync();
    return Ok(_mapper.Map<IEnumerable<PlatformReadDto>>(platforms));
}

这里以获取全部Platform,再通过AutoMapper转化Platform为PlatformReadDto。

测试Action

启动工程后,可以在浏览器中输入“http://localhost:58885/swagger/index.html”或者“https://localhost:44388/swagger/index.html”查看Swagger的API文档,进行接口调用测试。

也可以使用Postman进行调用

可以看到platformId是往内存数据库中添加mock数据时自动添加的。
这里面的Http的端口号以及Https的端口号是在launchSetting中配置的。返回值就是在内存数据库中的Mock数据。

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:58885",
      "sslPort": 44388
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "swagger",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "PlatformService": {
      "commandName": "Project",
      "dotnetRunMessages": "true",
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

调用接口之前,实际上App是在依赖注入容器中添加了所有的Controller、并在Configure中实例化所有的Controller;这样外部应用才可以通过路由进行访问。

完成全部Action

[HttpGet]
[Route("all")]
public async Task<ActionResult<IEnumerable<PlatformReadDto>>> GetAllPlatformsAsync()
{
    Console.WriteLine(">>>Getting Platforms...");
    var platforms = await _platformRepository.GetAllPlatformsAsync();
    return Ok(_mapper.Map<IEnumerable<PlatformReadDto>>(platforms));
}


[HttpGet("{platformId}", Name = "GetPlatformByIdAsync")]
public async Task<ActionResult<PlatformReadDto>> GetPlatformByIdAsync([FromRoute] int platformId)
{
    if(platformId <= 0)
    {
        return this.BadRequest(new
            {
                Message = "Platform Id should be greater than 0."
            });
    }

    Console.WriteLine(">>>Getting target Platform...");
    var platform = await _platformRepository.GetPlatformById(platformId);
    if(!(platform is null))
    {
        return Ok(_mapper.Map<PlatformReadDto>(platform));
    }
    return NotFound();
}

[HttpPost]
public async Task<ActionResult<PlatformReadDto>> CreatePlatformAsync(
    [FromBody] PlatformWriteDto platformWriteDto)
{
    if(platformWriteDto is null)
    {
        return this.BadRequest(new
        {
            Message = "Platform data should not be bull."
        });
    }

    Console.WriteLine(">>>Creating target Platform...");
    var platform = _mapper.Map<Platform>(platformWriteDto);
    _ = await _platformRepository.CreatePlatformAsync(platform);

    var platformReadDto = _mapper.Map<PlatformReadDto>(platform);
    return CreatedAtRoute(
        nameof(GetPlatformByIdAsync),
        new { PlatformId = platform.PlatformId },
        platformReadDto);
}

[HttpPut]
public async Task<ActionResult<PlatformReadDto>> UpdatePlatformAsync(
    [FromBody] PlatformWriteDto platformWriteDto)
{
    if (platformWriteDto is null)
    {
        return this.BadRequest(new
        {
            Message = "Platform data should not be bull."
        });
    }

    if (platformWriteDto.PlatformId <= 0)
    {
        return this.BadRequest(new
        {
            Message = "Platform id should greater than 0."
        });
    }


    Console.WriteLine(">>>Update target Platform...");
    var platform = _mapper.Map<Platform>(platformWriteDto);
    _ = await _platformRepository.UpdatePlatformAsync(platform);
    return Ok(_mapper.Map<PlatformReadDto>(platform));
}

几点想法:

  • 由于有多个HttpGet请求,所以需要通过路由对它们进行区分
  • 应该对可预见的错误进行规避,即调用IPlatformRepository前进行类型检查、Mapping前进行检查等
  • 实际上,还应该考虑错误处理、也可以考虑做一个全局错误处理
  • 和视频里面的做法不同,这里还是倾向于在一个Create或者Update中完成SaveAsync,这让我感觉整个方法比较完整。

结果检查

Get Platform By Id

Create Platform

注意Response中有创建的Platform的GetById的路由地址

Update Platform

Get All Platforms


参考链接

posted @ 2022-04-30 14:10  李嘉图正在调试  阅读(161)  评论(0)    收藏  举报