乐于分享
好东西不私藏

构建工业自动化软件框架:番外(一) 从Autofac到DryIoc,WPF上位机依赖注入的“去繁就简”

构建工业自动化软件框架:番外(一) 从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和Autofac    return 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<IDatabaseInitializerDatabaseInitializer>();containerRegistry.RegisterScoped<ITransactionTransaction>();

同时,将每个项目的注册类都修改成静态类,并且是作为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(SingletonTypesReuse.Singleton);

2、第二个坑是注册一个类的时候,如果他依赖注入的类的和他不是一个类型,比如 DatabaseInitializer 被注册为了 Singleton,他的构造函数里面引用了DbContext,被注册为了 Scoped,那就会报错。查了一下叫囚禁依赖。所以我最终就直接把全部都改成了单例引用,也没有太大影响。

3、最终因为我把所有类都注册成了单例,就会对协议部分的内容跟有影响。这个我会在后续更改,暂时先不管他。

后记

到这样,容器就顺利从Autofac转换到DryIoc了,再也不用考虑桥接这样麻烦的方式了。

最后顺带说一下框架的最新进度,现在已经在界面开发的过程了,开发过程中也遇到了其他层没有完善的内容, 我是想等界面全部开发完成后再继续更新的本来,现在想想还是会中途更新一部分内容(其他层后来增加的部分内容),但是界面部分还是最后再一起更新。

所有的代码都存放再Github中,欢迎大家前往Star、Fork、提Issue。所有文章都发布在专栏里,有需要看前期内容和后续更新的欢迎点击订阅。

https://github.com/JeffreyXXL/Sophon

构建工业自动化软件框架
设计模式在C#开发工控软件中的应用