.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 @   李嘉图正在调试  阅读(143)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示