一种.NetCore的ServiceCollection的组件版本控制
在服务端开发中,常常遇到一个问题: 某个新版本调整的功能,仅在部分Core的部分版本上生效。传统的做法上,我们用条件分支去控制这些代码,当业务变复杂时,可能导致过多的条件分支。另外,当版本变迁部分代码将会变为冗余代码,如果不及时清理,会导致业务代码臃肿,明明很简单的逻辑,却包含了很多冗余的条件分支,极大的影响代码的可阅读性和可维护性。
这里,我们借助Ioc容器和类的继承,来实现一种版本控制的方法。
类属性标记
首先我们定义一个属性AppVersionAttribute,用于标记指定的Implement类型的输出条件,他的优先级Priority 取决于是否指定了接口类型,以及定义的版本号
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class AppVersionAttribute : Attribute
{
/// <summary>
/// 定义代理类型,如果未设置,则对所有代理类型生效,指定类型判定的优先级高于null判定
/// </summary>
public Type ServiceType { get; set; } = null;
/// <summary>
/// 版本号,版本号高的优先
/// </summary>
public int Version { get; set; } = 0;
/// <summary>
/// 客户端版本
/// </summary>
public int ClientVer { get; set; } = int.MaxValue;
/// <summary>
/// 描述
/// </summary>
public string Description { get; set; }
/// <summary>
/// 优先级,指定接口类型的优先级最高
/// </summary>
public int Priority => (ServiceType == null ? 0 : Int16.MaxValue) + Version;
}
类版本控制器
对于标记的类属性,我们在自动注册到容器的过程中,需要收集信息。这里创建一个版本控制器来负责信息注册和输出。这里如果使用AotuFac或者Castle等其他容器,有更好的实现方式
/// <summary>
/// 版本控制器
/// </summary>
public class VersionManager
{
private static VersionManager _instance;
private static List<VersionInfo> _versions;
public static VersionManager Instance => _instance;
static VersionManager()
{
_instance = new VersionManager();
_versions = new List<VersionInfo>();
}
/// <summary>
/// 获取指定代理类型的所有注册信息
/// </summary>
/// <param name="serviceType"></param>
/// <returns></returns>
public List<VersionInfo> GetImplements(Type serviceType)
{
return _versions.Where(t => t.ServiceType.Contains(serviceType)).ToList();
}
/// <summary>
/// 记录注册信息
/// </summary>
/// <param name="type"></param>
/// <param name="services"></param>
public void RegisterVersionInfo(Type type, IEnumerable<Type> services)
{
var lst = type.GetCustomAttributes<AppVersionAttribute>();
if (lst.Any())
{
lst.ToList().ForEach(t =>
{
_versions.Add(new VersionInfo()
{
AppVersions = t,
ImplementType = type,
ServiceType = t.ServiceType == null ? services.ToArray() : new Type[] { t.ServiceType },
});
});
}
else
{
_versions.Add(new VersionInfo()
{
AppVersions = null,
ImplementType = type,
ServiceType = services.ToArray()
});
}
}
}
版本选择器
为了方便后续业务扩展,我们定义了一个版本选择器接口IVersionSelector,并创建了一个默认实现NullVersionSelector ;后续控制目标发生改变时,注入一个新的选择器来替代这个选择器就好
/// <summary>
/// 版本选择器
/// </summary>
public interface IVersionSelector
{
Type Select<T>();
Type Select(Type service);
}
/// <summary>
/// 默认版本选择器
/// </summary>
internal class NullVersionSelector : IVersionSelector
{
private static NullVersionSelector instance;
public static NullVersionSelector Instance => instance;
static NullVersionSelector()
{
instance = new NullVersionSelector();
}
/// <summary>
/// 选择最高优先级的实现类
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="implementTypes"></param>
/// <returns></returns>
public Type Select<T>()
{
return Select(typeof(T));
}
/// <summary>
/// 选择最高优先级的实现类
/// </summary>
/// <param name="service"></param>
/// <returns></returns>
public Type Select(Type service)
{
var implementTypes = VersionManager.Instance.GetImplements(service);
if (!implementTypes.Any())
{
return null;
}
var all = new List<TypeInfo>();
var dict = implementTypes.Where(t => service.IsAssignableFrom(t.ImplementType))
.GroupBy(t => t.ImplementType)
.ToDictionary(t => t.Key, t => t.ToList());
foreach (var key in dict.Keys)
{
var lst = dict[key];
//先判断是否有指定该接口的设定,取专用接口设定中最高优先级的
var main = lst.Where(t => t.ServiceType?.Count() == 1 && t.ServiceType[0] == service).OrderByDescending(t => GetPriority(t.AppVersions)).FirstOrDefault();
int priority = -1;
if (main == null)
{
//没有指定接口设定的,取通用设定中的最高优先级
priority = lst.Where(t => !t.ServiceType.Any() || t.ServiceType.Contains(service)).Max(x => GetPriority(x.AppVersions));
}
else
{
//存在专用设定的
priority = GetPriority(main.AppVersions);
}
if (priority > -1)
{
all.Add(new TypeInfo() { Implement = key, Priority = priority });
}
}
//取所有注册的实现类型中,优先级最高的
return all.OrderByDescending(t => t.Priority).FirstOrDefault()?.Implement;
}
/// <summary>
/// 根据请求条件判定是否满足
/// </summary>
/// <param name="appVersion"></param>
/// <returns></returns>
private static bool FilterByCoreVer(AppVersionAttribute appVersion)
{
return RequestParam.Current == null || RequestParam.Current.ClientVer >= appVersion.ClientVer;
}
/// <summary>
/// 根据请求条件判定优先级,小于0则是不满足条件
/// </summary>
/// <param name="appVersion"></param>
/// <returns></returns>
private static int GetPriority(AppVersionAttribute appVersion)
{
return appVersion != null ? (FilterByCoreVer(appVersion) ? appVersion.Priority : -1) : 0;
}
class TypeInfo
{
public Type Implement { get; set; }
public int Priority { get; set; }
}
}
使用接口
在需要注入组件的地方,采用VersionItem来替代T,这个类型包含一个T Value的属性,可以获取到受版本选择器选择后的组件。对于延迟加载的情况,我们也提供一个LazyVersionItem来替代。相关代码如下:
/// <summary>
/// 版本控制器
/// </summary>
/// <typeparam name="T"></typeparam>
public class VersionItem<T>
{
/// <summary>
/// 对象
/// </summary>
public T Value { get; }
/// <summary>
/// 构造函数
/// </summary>
/// <param name="serviceProvider"></param>
/// <param name="versionSelector"></param>
public VersionItem(IServiceProvider serviceProvider, IVersionSelector versionSelector)
{
var type = versionSelector.Select<T>();
if(type != null && serviceProvider.GetService(type) is T implement)
{
Value = implement;
}
else
{
Value = serviceProvider.GetService<T>();
}
}
}
public class LazyVersionItem<T> : Lazy<T>
{
public LazyVersionItem(IServiceProvider sp) : base(sp.GetRequiredService<VersionItem<T>>().Value)
{
}
}
如果需要延迟加载的自动实现,也可以在注册延迟加载的地方使用以下方式来替代
public static IServiceCollection AddLazy(this IServiceCollection services)
{
//默认延迟加载的实现
//services.AddTransient(typeof(Lazy<>), typeof(LazyLoader<>));
//替换延迟加载的实现
services.AddTransient(typeof(Lazy<>), typeof(LazyVersionItem<>));
return services;
}
说明
在需要限制版本下发的地方,采用如下方式标记类型:
[AppVersion(ClientVer = 100, Version = 1)]
[AppVersion(ClientVer = 150, Version = 2, ServiceType = typeof(ISampleService))]
public class SampleService : SampleBaseService,ISampleService
在依赖注入的时候,使用LazyVersionItem来获取受版本控制的对象。当请求发生时,就会根据请求条件,指定下发的版本了
扩展
在实际使用中,可以实现一个基于配置的IVersionSelector,根据Configuration和请求条件,去控制下发版本。这样可以实现不修改代码情况下,动态的更改下发版本的配置的效果