构建工业自动化软件框架:番外(一) 从Autofac到DryIoc,WPF上位机依赖注入的“去繁就简”
前言
前段时间开始了UI层的开发,也进行了一部分了,想着等做了一部分再更新的,但是看了看我又有一个月没更新了。所以正好趁着开发阶段出现的一个小问题,写一篇分享。
问题点
本来我在其他层开发的时候容器一直用的都是Autofac,那时候对WPF的prism不是特别了解,就直接用了Autofac,现在在界面开发阶段,prism支持的比较好的容器是DryIoc,所以UI层就直接用了他,这样就会遇到不同容器之间不互通的问题。
方案1: 桥接
最初遇到的是在ViewModel中会需要依赖注入其他层写的类,比如报警、硬件等的服务类。当时写到这部分的时候,想到的就是让Autofac桥接到DryIoc里面,这样可以暂时解决的我遇到的问题,可以正常注入。
public ParamViewModel(IParamService paramService, IDialogService dialogService){_paramService = paramService;_dialogService = dialogService;}
protectedoverride Rules CreateContainerRules(){//桥接DryIoc和Autofacreturn base.CreateContainerRules().WithUnknownServiceResolvers(request =>{if (_autofacContainer != null &&_autofacContainer.IsRegistered(request.ServiceType)){var serviceType = request.ServiceType;return new DelegateFactory(_ =>_autofacContainer.Resolve(serviceType));}return null;});}
方案2:完全替换Autofac(各个项目单独注册)
但是在后续的开发过程中,我又遇到了需要在AlarmService中注入IEventAggregator的情况,报警时通知界面。这样就又需要反过来桥接或者其他方法,越写越麻烦,所以就索性直接把Autofac全部替换成DryIoc,这样就会清晰很多。
public AlarmService(IConfigManagerFactory configManagerFactory, IEventAggregator eventAggregator){_alarmConfigManager = configManagerFactory.CreateConfigManager(ConfigType.json, "alarm_config", "Alarm");_eventAggregator = eventAggregator;}
首先删除所有项目里面的Autofac引用,然后将之前用到的接口,注册所有类的扩展方法,以及特性标签都全部删掉。
//下面这些代码可以直接全部删除public interface IModuleRegister{voidRegister(ContainerBuilder builder);}[AttributeUsage(AttributeTargets.Class)]public class ModuleRegisterAttribute : Attribute{}public static class RegisterAllModule{publicstatic ContainerBuilder RegisterAllModuleExt(this ContainerBuilder builder){List<Assembly> assemblies = new List<Assembly>();var assemblyNames = ConfigurationManager.AppSettings["ModuleAssemblies"].Split(';');foreach (var name in assemblyNames){try{assemblies.Add(Assembly.Load(name));}catch (Exception e){Trace.WriteLine($"加载{name}失败:" + e.Message);}}foreach (var assembly in assemblies){var types = assembly.GetTypes().Where(t => typeof(IModuleRegister).IsAssignableFrom(t)&& !t.IsInterface && !t.IsAbstract&& t.IsDefined(typeof(ModuleRegisterAttribute), false));var moduletypes = types.Select(t => new{Type = t,Order = t.GetCustomAttribute<ModuleRegisterAttribute>().Order}).OrderBy(t => t.Order).ToList();foreach (var moduletype in moduletypes){try{var instance = (IModuleRegister)Activator.CreateInstance(moduletype.Type);instance.Register(builder);}catch (Exception e){Trace.WriteLine(e.Message);}}}return builder;}}
下一步就是在每个项目引入Prism.core和DryIoc.dll两个类库,然后分别在每个项目中开始编写注册代码。DryIoc和Autofac的语法很类似,只有细微差别。
之前注册大概有这样三种。一种是带委托的,一种是单例,还有一个就是作用域的。
builder.Register(c =>{var factory = c.Resolve<ILoggerFactory>();var path = ConfigurationManager.AppSettings["DatebaseFilePath"];string connstr = "Data Source = " + PathResolver.GetAbsolutePath(path) + ";";return new DbContext(connstr, factory); ;}).InstancePerLifetimeScope();builder.RegisterType<DatabaseInitializer>().As<IDatabaseInitializer>().SingleInstance();builder.RegisterType<LoginHistoryRepository>().As<IRepository<LoginHistory>>().InstancePerLifetimeScope();
使用DryIoc之后,可以归类为四种,前三种一样,多了一种RegisterMany的方法,通过反射获取到所有类,找到以特定结尾的类,然后统一注册。
var container = ((IContainerExtension<IContainer>)containerRegistry).Instance;var assembly = typeof(InfrastructureModuleRegister).Assembly;var serviceTypes = assembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract &&(t.Name.EndsWith("Repository") || t.Name.EndsWith("Protocol")));container.RegisterMany(serviceTypes, Reuse.Singleton);container.RegisterDelegate<DbContext>(c =>{var factory = c.Resolve<ILoggerFactory>();var path = ConfigurationManager.AppSettings["DatebaseFilePath"];string connstr = "Data Source = " + PathResolver.GetAbsolutePath(path) + ";";return new DbContext(connstr, factory);}, Reuse.Singleton);containerRegistry.RegisterSingleton<IDatabaseInitializer, DatabaseInitializer>();containerRegistry.RegisterScoped<ITransaction, Transaction>();
同时,将每个项目的注册类都修改成静态类,并且是作为IContainerRegistry的扩展方法来使用的。最后直接在App.xaml里面调用。
publicstaticvoidRegisterInfrastructure(this IContainerRegistry containerRegistry){}protectedoverridevoidRegisterTypes(IContainerRegistry containerRegistry){containerRegistry.RegisterCommon();containerRegistry.RegisterInfrastructure();containerRegistry.RegisterCore();containerRegistry.RegisterApplication();}
方案3:“白名单”方式注册
完成方案2后,虽然已经完全切换成Autofac了,但是还是会有可能注册到不是必须注册的类(可能会有同样结尾但是不需要注册的类),也会需要重复写一堆内容。所以我就考虑到用特性的方式标记需要注册的类,这样找到这些类之后统一注册。这种方式也有不足的地方,就是需要委托注册的部分还是得要手动去写,但是至少比之前要少不少代码。
[AttributeUsage(AttributeTargets.Class, Inherited = false)]public class InjectableAttribute : Attribute{public DependencyLifetime Lifetime { get; }public InjectableAttribute(DependencyLifetime lifetime){Lifetime = lifetime;}}public enum DependencyLifetime{Singleton,Delegate}
其他加载注册的部分和方案2一样,需要看具体代码的可以前往Github上查看。
遇到的问题
在做这部分内容的时候,也踩了好几个坑。 1、首先就是RegisterMany 这个方法,我本能来看是可以写lambda来完成筛选的,但是实际发现并没有注册进去。排查了半天也没有找到根本原因,索性就换成了直接将集合传给他让他注册。
//这个方式注册不进去container.RegisterMany(new[] { assembly },type => type.IsClass && type.Name.EndsWith("Service"), Reuse.Singleton);//修改为这种方式container.RegisterMany(SingletonTypes, Reuse.Singleton);
2、第二个坑是注册一个类的时候,如果他依赖注入的类的和他不是一个类型,比如 DatabaseInitializer 被注册为了 Singleton,他的构造函数里面引用了DbContext,被注册为了 Scoped,那就会报错。查了一下叫囚禁依赖。所以我最终就直接把全部都改成了单例引用,也没有太大影响。
3、最终因为我把所有类都注册成了单例,就会对协议部分的内容跟有影响。这个我会在后续更改,暂时先不管他。
后记
到这样,容器就顺利从Autofac转换到DryIoc了,再也不用考虑桥接这样麻烦的方式了。
最后顺带说一下框架的最新进度,现在已经在界面开发的过程了,开发过程中也遇到了其他层没有完善的内容, 我是想等界面全部开发完成后再继续更新的本来,现在想想还是会中途更新一部分内容(其他层后来增加的部分内容),但是界面部分还是最后再一起更新。
所有的代码都存放再Github中,欢迎大家前往Star、Fork、提Issue。所有文章都发布在专栏里,有需要看前期内容和后续更新的欢迎点击订阅。
https://github.com/JeffreyXXL/Sophon
夜雨聆风