如何用泛型优化ASP.NET Core API中重复的Seed逻辑?
兄弟,我太懂你这种被重复代码缠得难受的感觉了——你想通过泛型干掉GetSeed里的switch和SeedService里一堆重复的GetAll,但AutoMapper要实体和DTO俩类型确实卡了一下,对吧?我给你几个实用的方案,从简单直接到灵活简化的都有:
方案1:双泛型参数的基类+服务方法(最推荐)
这是最直观的解决方案,直接把实体和DTO作为两个泛型参数传递给基类和服务方法,完美适配AutoMapper的投影需求,完全消除重复代码。
步骤1:定义双泛型基控制器
把通用的GetSeed逻辑放到基类里,所有业务控制器直接继承即可:
using AutoMapper; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Collections.Generic; using System.Threading.Tasks; public abstract class EntityController<TEntity, TDto> : ControllerBase where TEntity : class where TDto : class { private readonly ISeedService _seedService; protected EntityController(ISeedService seedService) { _seedService = seedService; } [HttpGet("seed")] public async Task<IActionResult> GetSeed() { var seedData = await _seedService.GetAll<TEntity, TDto>(); return Ok(seedData); } // 其他通用Action(比如GetById、Post)也可以统一放到这里 } // 业务控制器只需要指定实体和DTO类型,不用写重复逻辑 public class ProductsController : EntityController<Product, ProductDto> { public ProductsController(ISeedService seedService) : base(seedService) { } } public class OrdersController : EntityController<Order, OrderDto> { public OrdersController(ISeedService seedService) : base(seedService) { } }
步骤2:实现双泛型的SeedService
把重复的GetAll逻辑统一成泛型方法,利用AutoMapper的ProjectTo完成投影:
using AutoMapper; using Microsoft.EntityFrameworkCore; using System.Collections.Generic; using System.Threading.Tasks; public interface ISeedService { Task<List<TDto>> GetAll<TEntity, TDto>() where TEntity : class where TDto : class; } public class SeedService : ISeedService { private readonly AppDbContext _dbContext; private readonly IMapper _mapper; public SeedService(AppDbContext dbContext, IMapper mapper) { _dbContext = dbContext; _mapper = mapper; } public async Task<List<TDto>> GetAll<TEntity, TDto>() where TEntity : class where TDto : class { // 直接用泛型Set获取实体集合,再投影到DTO return await _dbContext.Set<TEntity>() .ProjectTo<TDto>(_mapper.ConfigurationProvider) .ToListAsync(); } }
这个方案的优点是可读性极强、性能无损耗,完全符合SOLID原则,是最推荐的做法。
方案2:单泛型控制器+接口约束(简化控制器写法)
如果觉得每个控制器都写两个泛型参数有点繁琐,可以通过让实体实现一个标记接口,来自动关联对应的DTO类型,从而让基类只需要一个泛型参数。
步骤1:定义关联接口
让实体实现IMapTo<TDto>接口,明确它对应的DTO类型:
public interface IMapTo<TDto> where TDto : class { } // 实体实现接口,指定对应的DTO public class Product : IMapTo<ProductDto> { } public class Order : IMapTo<OrderDto> { }
步骤2:单泛型基控制器+反射调用
在基类里通过反射获取实体对应的DTO类型,再调用SeedService的泛型方法:
public abstract class EntityController<TEntity> : ControllerBase where TEntity : class, IMapTo<TDto>, new() where TDto : class { private readonly ISeedService _seedService; private readonly System.Reflection.MethodInfo _getAllMethod; private readonly System.Type _dtoType; protected EntityController(ISeedService seedService) { _seedService = seedService; // 获取实体对应的DTO类型 _dtoType = typeof(TEntity).GetInterfaces() .First(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IMapTo<>)) .GetGenericArguments()[0]; // 获取SeedService的GetAll泛型方法 _getAllMethod = typeof(ISeedService).GetMethod(nameof(ISeedService.GetAll))! .MakeGenericMethod(typeof(TEntity), _dtoType); } [HttpGet("seed")] public async Task<IActionResult> GetSeed() { // 反射调用泛型方法 var seedData = await (Task<object>)_getAllMethod.Invoke(_seedService, null)!; return Ok(seedData); } } // 业务控制器只需要指定实体类型 public class ProductsController : EntityController<Product> { public ProductsController(ISeedService seedService) : base(seedService) { } }
这个方案的优点是控制器写法更简洁,但缺点是引入了反射,性能略有损耗,可读性也稍差,适合对控制器简洁性要求很高的场景。
方案3:字典映射+泛型工厂(兼容现有代码)
如果你已经有了控制器名和类型的字典映射,可以结合这个字典来实现泛化的GetAll方法:
public class SeedService : ISeedService { private readonly AppDbContext _dbContext; private readonly IMapper _mapper; // 假设你已经有了这个字典:控制器名 -> 实体类型 private readonly Dictionary<string, Type> _controllerEntityMap; public SeedService(AppDbContext dbContext, IMapper mapper, Dictionary<string, Type> controllerEntityMap) { _dbContext = dbContext; _mapper = mapper; _controllerEntityMap = controllerEntityMap; } // 非泛型方法,根据控制器名获取数据 public async Task<object> GetSeedByControllerName(string controllerName) { if (!_controllerEntityMap.TryGetValue(controllerName, out var entityType)) throw new KeyNotFoundException($"找不到控制器{controllerName}对应的实体类型"); // 获取DTO类型(这里需要你有实体到DTO的映射关系,比如另一个字典或者AutoMapper的配置) var dtoType = GetDtoTypeForEntity(entityType); // 反射调用泛版GetAll var method = typeof(SeedService).GetMethod(nameof(GetAll))! .MakeGenericMethod(entityType, dtoType); return await (Task<object>)method.Invoke(this, null)!; } // 内部泛型方法 private async Task<List<TDto>> GetAll<TEntity, TDto>() where TEntity : class where TDto : class { return await _dbContext.Set<TEntity>() .ProjectTo<TDto>(_mapper.ConfigurationProvider) .ToListAsync(); } // 这里需要实现实体到DTO类型的映射逻辑,比如从AutoMapper配置中读取 private Type GetDtoTypeForEntity(Type entityType) { var map = _mapper.ConfigurationProvider.GetAllTypeMaps() .First(m => m.SourceType == entityType); return map.DestinationType; } }
这个方案适合已经有字典映射的场景,但同样依赖反射,可读性和性能不如方案1。
总结一下,方案1是最优解,它用最直接的方式解决了重复代码问题,同时完全适配AutoMapper的需求,没有额外的性能损耗和复杂度。
内容的提问来源于stack exchange,提问作者Collin Barrett

