场景
该设计多适用于MES,ERP,WMS 等管理类型的项目。
在做管理类型项目的时候,前端经常会使用到下拉框,比如:设备,工厂等等。下拉组件中一般只需要他们的ID,Name
属性,而这些数据都创建于其他模块并存储在数据库中。
如图:
写一个设备的下拉组件的数据需要通过请求后端去获取,如:localhost:5555/api/services/app/Resource/GetNdoCombox
,然后携带参数filterText
。
写一个工厂的下拉组件也是同样的要去请求,如:localhost:5555/api/services/app/Factory/GetNdoCombox
,
如果你的后端代码足够规范,那么就会像我写的这样,每个需要有下拉需求的实体建模都有一个GetNdoCombox
的接口。
问题点
为了代码的复用性,你一定不会每个用到设备下拉的地方都去写select
标签,然后请求数据再渲染,而是会封装一个设备下拉框和一个工厂下拉框。于是便出现了一些问题:
前端下拉组件除了请求的接口不一样,placeholder
不一样,其他的代码都是尽数相同的。如果有下拉需求的地方多了,就会出现很多个xx下拉组件,如何优化?如果你有此类相同需求的问题时值得参考这个方案。
方案
思路
在后端写一个接口做统一查询,前端做一个统一的下拉组件,请求后端的统一查询接口,前端传递标识性的参数给后端,后端通过参数自动匹配并查询前端所需要的值。那么重点就在后端如何实现这样的匹配逻辑呢?
核心实现
我的实践架构:.NET CORE + VUE
前端
前端就是写个组件去请求接口,传递参数,这里就不细说了
后端
先粗浅的介绍需要准备的东西:
- 自定义服务描述类,包含:服务接口,服务实现,泛型实体类型,生命周期
- 定义单例存储器:定义Ndo字典,用于存储对应的服务描述,以实体完全限定名为key,定义存和取的方法
- 通过反射获取指定程序集中实现了IComboxQuery接口并且必须实现了IComboxQuery<>的下拉服务的服务描述,再将其注入到IOC容器中,并同时在存储器的字典中添加映射关系。
- 统一获取服务的Hub:从存储器中根据实体名称获取对应的服务描述,再根据自定义服务描述类中的服务接口类型从IOC容器中获取实现的IComboxQuery
详细说明
Ndo:其实就是普通的实体的意思。
首先通过提供的IComboxQuery接口和IComboxQueryController
或Service
必须实现GetNdoCombox
方法。也就是说所有需要下拉的实体的服务都要实现IComboxQuery
程序启动时利用反射将实现了IComboxQuery并且实现了IComboxQuery
定义统一获取服务的Hub,从存储器中根据实体名称获取对应的服务描述,再根据自定义服务描述类中的服务接口类型从IOC容器中获取实现IComboxQuery的Controller
或Service
,然后调用GetNdoCombox
方法
定义统一的Controller
或Service
,随便定义一个方法定义需要的参数为EntityName
和filterText
,方法中使用统一获取服务的Hub,通过参数EntityName
获取实际实现IComboxQuery的对象,然后调用GetNdoCombox
返回数据。
核心的查询逻辑仍然是由服务自己实现的,因为不同的实体,过滤条件的字段名不一样,Hub只负责调用方法得到结果,不关心具体实现。
代码
返回的数据NdoDto
public class NdoDto { public virtual Guid Id { get; set; } public virtual string Name { get; set; } public virtual DateTime CreationTime { get; set; } }
公共接口查询的参数类
/// <summary> /// 下拉框查询的模糊搜索输入 /// </summary> public class GetQueryFilterInput { /// <summary> /// 类型全名称,不涉及业务,用于区分本次请求是哪个实体的接口 /// </summary> public virtual string Name { get; set; } /// <summary> /// 模糊查询 /// </summary> public virtual string FilterText { get; set; } }
统一规范的公共接口
/// <summary> /// 下拉查询 /// </summary> public interface IComboxQuery { Task<List<NdoDto>> GetCombox(GetQueryFilterInput input); } /// <summary> /// 下拉查询 /// </summary> public interface IComboxQuery<TEntity> : IComboxQuery { }
自定义的服务映射描述类
/// <summary> /// 服务映射描述 /// </summary> public class SampleServiceDescriptor { /// <summary> /// 瞬时依赖注入服务接口 /// </summary> public static Type TransientInterface { get; } = typeof(ITransientDependency); /// <summary> /// 单例依赖注入服务接口 /// </summary> public static Type SingletonInterface { get; } = typeof(ISingletonDependency); /// <summary> /// 服务类型 接口 /// </summary> public virtual Type ServiceType { get; } /// <summary> /// 实现类型 /// </summary> public virtual Type ImplementationType { get; } /// <summary> /// 建模实体类型 /// </summary> public virtual Type EntityType { get; } /// <summary> /// 服务依赖注入生命周期 /// </summary> public virtual ServiceLifetime ServiceLifetime { get; } /// <summary> /// 依赖注入服务 /// </summary> /// <param name="serviceType">服务类型</param> /// <param name="implementationType">实现类型</param> public SampleServiceDescriptor(Type serviceType, Type implementationType) { this.ServiceType = serviceType; this.ImplementationType = implementationType; if (serviceType != null && serviceType.GenericTypeArguments.Length > 0) { // 获取IComboxQuery<>中的泛型参数TEntity this.EntityType = serviceType.GenericTypeArguments[0]; } if (SingletonInterface.IsAssignableFrom(this.ImplementationType)) { this.ServiceLifetime = ServiceLifetime.Singleton; } else { this.ServiceLifetime = ServiceLifetime.Transient; } } /// <summary> /// 转换为 <see cref="ServiceDescriptor"/> /// </summary> /// <returns></returns> public ServiceDescriptor ToServiceDescriptor() { return new ServiceDescriptor(this.ServiceType, this.ImplementationType, this.ServiceLifetime); } }
程序启动时的扫描器(反射获取实现了接口的服务)
/// <summary> /// 依赖注入服务描述器 /// </summary> public static class SampleServiceDescriptorHelper { /// <summary> /// 扫描程序集中的某个接口的实现 /// </summary> /// <param name="interfaceType">接口</param> /// <param name="genericInterfaceTypes">接口泛型实现</param> /// <param name="assemblies">程序集列表</param> /// <returns></returns> public static IEnumerable<SampleServiceDescriptor> ScanAssembliesServices (Type interfaceType, IEnumerable<Type> genericInterfaceTypes, params Assembly[] assemblies) { // 泛型接口转字典 var genericInterfaceTypeDict = new Dictionary<Type, bool>(); foreach (var item in genericInterfaceTypes) { genericInterfaceTypeDict[item] = true; } // 遍历程序集中所有的符合条件的类型 foreach (var assembly in assemblies) { var services = assembly.GetTypes() .Where(o => interfaceType.IsAssignableFrom(o) && o.IsPublic && !o.IsInterface && !o.IsAbstract ) .Select(o => { // 筛选某个接口 var entityInterfaceType = o.GetInterfaces() .Where(x => { if (!x.IsGenericType) { return false; } var genericTypeDefinition = x.GetGenericTypeDefinition(); return genericInterfaceTypeDict.ContainsKey(genericTypeDefinition); }) .FirstOrDefault(); // entityInterfaceType = IComboxQuery<> 目前只有一种 return new SampleServiceDescriptor(entityInterfaceType, o); }) .Where(o => o != null && o.ServiceType != null); foreach (var service in services) { yield return service; } } } // interfaceType用于获取所有实现了IComboxQuery的类型 // genericInterfaceTypes用于筛选,必须要实现了IComboxQuery<>的类型,因为需要获取其TEntity的类型 // 如果只是实现了IComboxQuery的类型,是没有TEntity的,会导致ComboxQueryInfoStorage中无法添加映射关系 }
单例的存储器
public class ComboxQueryInfoStorage : IComboxQueryInfoStorage { /// <summary> /// ModelingComboxQueryInfo 存储器实例 /// </summary> public static IComboxQueryInfoStorage Instace { get; set; } = new ComboxQueryInfoStorage(); /// <summary> /// 数据存储器 /// </summary> protected readonly Dictionary<string, SampleServiceDescriptor> _Dict; protected ComboxQueryInfoStorage() { this._Dict = new Dictionary<string, SampleServiceDescriptor>(); } public void Add(params SampleServiceDescriptor[] comboxQueryInfos) { foreach (var item in comboxQueryInfos) { this._Dict[item.EntityType.FullName] = item; } } public SampleServiceDescriptor Get(string name) { if (this._Dict.TryGetValue(name,out var comboxQueryInfo)) { return comboxQueryInfo; } throw new Exception($"found Ndo type: {name}"); } }
统一获取服务的Hub
public class ComboxQueryHub : IComboxQueryHub { /// <summary> /// 依赖注入容器 /// </summary> protected readonly IServiceProvider _serviceProvider; /// <summary> /// 构造函数 /// </summary> /// <param name="serviceProvider"></param> public ComboxQueryHub(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public async Task<List<NdoDto>> GetCombox(GetQueryFilterInput input) { var comboxQuery = this.GetComboxQuery(input.Name); return await comboxQuery.GetCombox(input); } public IComboxQuery GetComboxQuery(string name) { var comboxQueryInfo = ComboxQueryInfoStorage.Instace.Get(name); var comboxQuery = _serviceProvider.GetService(comboxQueryInfo.ServiceType) as IComboxQuery; return comboxQuery; } }
用于将服务注册到IOC和存储器的扩展类
public static class ComboxQueryExtensions { /// <summary> /// ComboxQuery 接口类型 /// </summary> public static Type InterfaceType { get; } = typeof(IComboxQuery); /// <summary> /// IComboxQuery 接口类型 /// </summary> public static List<Type> GenericInterfaceTypes { get; } = new List<Type>() { typeof(IComboxQuery<>) }; /// <summary> /// 注册程序集中的 ComboxQuery /// </summary> /// <returns></returns> public static void RegistrarComboxQuery(this IServiceCollection services, params Assembly[] assemblies) { // query hub if (!services.Any(x=>x.ServiceType == typeof(IComboxQueryHub))) { services.AddTransient<IComboxQueryHub, ComboxQueryHub>(); } // querys var sampleServiceDescriptors = ScanComboxQuerys(assemblies); foreach (var sampleServiceDescriptor in sampleServiceDescriptors) { if (services.Any(x => x.ServiceType == sampleServiceDescriptor.ServiceType)) { continue; } ComboxQueryInfoStorage.Instace.Add(sampleServiceDescriptor); if (sampleServiceDescriptor.ServiceLifetime == ServiceLifetime.Singleton) { services.AddSingleton(sampleServiceDescriptor.ServiceType,sampleServiceDescriptor.ImplementationType); } else { services.AddTransient(sampleServiceDescriptor.ServiceType, sampleServiceDescriptor.ImplementationType); } } } /// <summary> /// 扫描程序集中的 ComboxQuery 实现 /// </summary> /// <param name="assemblies"></param> /// <returns></returns> public static IEnumerable<SampleServiceDescriptor> ScanComboxQuerys(params Assembly[] assemblies) { return SampleServiceDescriptorHelper.ScanAssembliesServices( InterfaceType, GenericInterfaceTypes, assemblies ); } }
使用
在启动类中注册服务
builder.Services.RegistrarComboxQuery(typeof(Program).Assembly);
人员建模:PersonController
public class PersonController : ApiControllerBase, IComboxQuery<Person> { [HttpPost] public Task<List<NdoDto>> GetCombox(GetQueryFilterInput input) { var persons = Person.GetPeoples(); var ndos = persons.Select(x => new NdoDto { Id = x.Id, Name = x.PersonName, }).ToList(); return Task.FromResult(ndos); } }
设备建模:ResourceController
public class ResourceController : ApiControllerBase, IComboxQuery<Resource> { [HttpPost] public Task<List<NdoDto>> GetCombox(GetQueryFilterInput input) { var resources = Resource.GetResources(); var ndos = resources.Select(x => new NdoDto { Id = x.Id, Name = x.ResourceName }).ToList(); return Task.FromResult(ndos); } }
统一查询接口:CommonBoxController
public class CommonBoxController : ApiControllerBase { /// <summary> /// ioc容器 /// </summary> protected readonly IServiceProvider _serviceProvider; public CommonBoxController(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } [HttpPost] public virtual async Task<List<NdoDto>> GetNdoCombox(GetQueryFilterInput input) { var queryHub = this._serviceProvider.GetService<IComboxQueryHub>(); return await queryHub.GetCombox(input); } }
效果
单独请求PersonController
单独请求ResourceController
请求公共接口CommonBoxController
代码仓库
地址:https://gitee.com/huang-yuxiang/common-combox/tree/main/
版权声明
作者:不想只会CURD的猿某人
更多原著文章请参考:https://www.cnblogs.com/hyx1229/