65.9K
CodeProject 正在变化。 阅读更多。
Home

Clifton 方法 - 第三部分

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2016 年 8 月 25 日

CPOL

6分钟阅读

viewsIcon

12379

downloadIcon

163

使用模块管理器和服务管理器进行引导

系列文章

引言

在之前的两篇文章中,我描述了用于动态加载模块的模块管理器,以及用于使用接口类型实现对象实例化的服务管理器。在本文中,我们将稍作休息,看看如何组合一个可用于几乎任何应用程序的引导程序,无论是 WinForm 客户端应用程序、Web 服务器还是其他。

模块初始化

引导程序使用核心类 ServiceModuleManager。该类派生自 ModuleManager 并协调模块的初始化。正如模块管理器一文中提到的,IModule 实际上需要实现 InitializeServices

public interface IModule
{
  void InitializeServices(IServiceManager serviceManager);
}

我们可以在 ServiceModuleManager 中看到这一点,它重写了 InitializeRegistrants

using System;
using System.Collections.Generic;

using Clifton.Core.ServiceManagement;

namespace Clifton.Core.ModuleManagement
{
  public class ServiceModuleManager : ModuleManager, IServiceModuleManager
  {
    public IServiceManager ServiceManager { get; set; }

    public virtual void Initialize(IServiceManager svcMgr)
    {
      ServiceManager = svcMgr;
    }

    public virtual void FinishedInitialization()
    {
    }

    /// <summary>
    /// Initialize each registrant by passing in the service manager.
    /// This allows the module to register the services it provides.
    /// </summary>
    protected override void InitializeRegistrants(List<IModule> registrants)
    {
      registrants.ForEach(r =>
      {
        try
        {
          r.InitializeServices(ServiceManager);
        }
        catch (System.Exception ex)
        {
          throw new ApplicationException("Error initializing " + 
                r.GetType().AssemblyQualifiedName + "\r\n:" + ex.Message);
        }
      });
    }
  }
}

类和接口层次结构

这为每个实现 IModule 接口的模块提供了初始化其所提供服务的机会。

引导程序

引导程序实例化一个 ServiceModuleManager 而不是 ModuleManager,并执行两步初始化

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;

using Clifton.Core.Assertions;
using Clifton.Core.ExtensionMethods;
using Clifton.Core.Semantics;
using Clifton.Core.ModuleManagement;
using Clifton.Core.ServiceManagement;

namespace BootstrapDemo
{
  static partial class Program
  {
    public static ServiceManager serviceManager;

    public static void InitializeBootstrap()
    {
      serviceManager = new ServiceManager();
      serviceManager.RegisterSingleton<IServiceModuleManager, ServiceModuleManager>();
    }

    public static void Bootstrap(Action<Exception> onBootstrapException)
    {
      try
      {
        IModuleManager moduleMgr = 
                       (IModuleManager)serviceManager.Get<IServiceModuleManager>();
        List<AssemblyFileName> modules = GetModuleList(XmlFileName.Create("modules.xml"));
        moduleMgr.RegisterModules(modules);
        serviceManager.FinishedInitialization();
      }
      catch (Exception ex)
      {
        onBootstrapException(ex);
      }
    }

    /// <summary>
    /// Return the list of assembly names specified in the XML file so that
    /// we know what assemblies are considered modules as part of the application.
    /// </summary>
    private static List<AssemblyFileName> GetModuleList(XmlFileName filename)
    {
      Assert.That(File.Exists(filename.Value), "Module definition file " + 
                  filename.Value + " does not exist.");
      XDocument xdoc = XDocument.Load(filename.Value);

      return GetModuleList(xdoc);
    }

    /// <summary>
    /// Returns the list of modules specified in the XML document so we know what
    /// modules to instantiate.
    /// </summary>
    private static List<AssemblyFileName> GetModuleList(XDocument xdoc)
    {
      List<AssemblyFileName> assemblies = new List<AssemblyFileName>();
        (from module in xdoc.Element("Modules").Elements("Module")
        select module.Attribute("AssemblyName").Value).ForEach
                               (s => assemblies.Add(AssemblyFileName.Create(s)));

      return assemblies;
    }
  }
}

我通常将引导程序实现为部分 Program 类。

初始化工作流

让我们稍微剖析一下这段代码。在引导程序初始化中...

serviceManager = new ServiceManager();
serviceManager.RegisterSingleton<IServiceModuleManager, ServiceModuleManager>();

...我们实例化一个 ServiceManager 并注册 ServiceModuleManager 的实现者。

因为 ServiceModuleManager 本身就是一项服务,所以您可以将其替换为自己的初始化过程。

然后,在引导程序本身中...

IModuleManager moduleMgr = (IModuleManager)serviceManager.Get<IServiceModuleManager>();
List<AssemblyFileName> modules = GetModuleList(XmlFileName.Create("modules.xml"));
moduleMgr.RegisterModules(modules);
serviceManager.FinishedInitialization();
  1. 我们获取初始化注册者(模块)的服务,这使得每个实现 IModule 的类都可以访问服务管理器。 返回的单例被强制转换为 IModuleManager,因为 ServiceModuleManager 派生自实现 IModuleManagerModuleManager
  2. 获取应用程序要加载的模块。在此示例中,它们在文件“modules.xml”中指定
  3. 模块已注册。这会调用虚方法 InitializeRegistrants,该方法在 ServiceModuleManager 中被重写,并实现初始化过程的第一步。这是实现服务的模块可以注册这些服务,以及本地保存服务管理器以供这些服务使用的地方。
  4. 一旦所有模块服务都已注册,引导程序会告诉服务管理器为模块中注册的所有单例调用 FinishInitialization
public override void FinishedInitialization()
{
  singletons.ForEach(kvp => kvp.Value.FinishedInitialization());
}

只有注册为单例的服务才会收到 FinishedInitialization 调用。正如服务管理器文章中提到的,单例会立即实例化,并且由于它们存在,它们现在可以完成所需的任何初始化(包括调用其他服务)。非单例服务通常在其构造函数中初始化自身。

如果您的服务初始化调用了另一个尚未完成其初始化的服务,这会有点问题。在某个时候,我可能会实现一个初始化顺序过程,但从技术上讲,由于服务是按照模块列表的顺序初始化的,您可以将依赖项放在列表的更高位置。

六行高级代码还不错!

使用引导程序

使用引导程序非常简单

using System;

namespace BootstrapDemo
{
  static partial class Program
  {
    static void Main(string[] args)
    {
      InitializeBootstrap();
      Bootstrap((e) => Console.WriteLine(e.Message));
    }
  }
}

几个例子

让我们编写一些作为模块的服务来说明这一切是如何工作的。

Clifton.AppConfigService

这是一个我经常使用的非常简单的服务,它封装了 .NET 的 ConfigurationManager 以获取连接字符串和应用程序设置

using System.Configuration;

using Clifton.Core.ModuleManagement;
using Clifton.Core.ServiceInterfaces;
using Clifton.Core.ServiceManagement;

namespace Clifton.Cores.Services.AppConfigService
{
  public class AppConfigModule : IModule
  {
    public void InitializeServices(IServiceManager serviceManager)
    {
      serviceManager.RegisterSingleton<IAppConfigService, ConfigService>();
    }
  }

  public class ConfigService : ServiceBase, IAppConfigService
  {
    public virtual string GetConnectionString(string key)
    {
      return ConfigurationManager.ConnectionStrings[key].ConnectionString;
    }

    public virtual string GetValue(string key)
    {
      return ConfigurationManager.AppSettings[key];
    }
  }
}

总是有两个部分

  1. 一个实现 IModule 并注册服务的类
  2. 一个或多个实现服务的类

我们将使用一个示例 app.config 文件

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup> 
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <connectionStrings>
    <add name="myConnectionString" connectionString="some connection string"/>
  </connectionStrings>
  <appSettings>
    <add key ="someKey" value="someKeyValue"/>
  </appSettings>
</configuration>

modules.xml 中,我们将指定 Clifton.AppConfigService

<?xml version="1.0" encoding="utf-8" ?>
<Modules>
  <Module AssemblyName='Clifton.AppConfigService.dll'/>
</Modules>

作弊

问题是,您如何将模块 DLL(在本例中为 Clifton.AppConfigService.dll)放入应用程序的 bin\Debug 文件夹中?我通过将其添加为应用程序引用的引用来作弊

这可能是一种不良做法,因为当然,这些类现在可以直接访问您的应用程序!

使用服务

这也很简单——从服务管理器中获取服务并开始使用它

static void Main(string[] args)
{
  InitializeBootstrap();
  Bootstrap((e) => Console.WriteLine(e.Message));

  IConfigService cfgSvc = serviceManager.Get<IConfigService>();
  Console.WriteLine(cfgSvc.GetConnectionString("myConnectionString"));
  Console.WriteLine(cfgSvc.GetValue("someKey"));
}

请注意,我们正在请求一个 IConfigService 实现者。虽然我们可以指定一个 IAppConfigService 实现者,但我们将在下面看到为什么我们引用 abstract 接口。

Clifton.EncryptedAppConfigService

在此示例中,我们假设您的 app.config 要么完全加密,要么完全不加密——换句话说,为我们获取值的服务是排他性的,我们只使用其中一个。这使我们能够利用 IConfigService 接口,而不管具体实现是简单地返回未加密的值,还是首先为我们解密它们。如果我们需要同时支持两者,那么我们需要向服务管理器指定我们想要哪个:具体接口 IAppConfigServiceIEncryptedAppConfigService

加密的应用程序配置服务如下所示

public class ConfigService : ServiceBase, IEncryptedAppConfigService
{
  public virtual string GetConnectionString(string key)
  {
    string enc = ConfigurationManager.ConnectionStrings[key].ConnectionString;

    return Decrypt(enc);
  }

  public virtual string GetValue(string key)
  {
    string enc = ConfigurationManager.AppSettings[key];

    return Decrypt(enc);
  }

  protected string Decrypt(string enc)
  {
    return ServiceManager.Get<IAppConfigDecryption>().Decrypt(enc);
  }
}

请注意,为了解密 app.config 中的 string,我们使用了一个必须由应用程序提供的服务:IAppConfigDecryption。我们可以在另一个模块中实现它,但在此示例中,我将其直接添加到应用程序中。

接口如下所示

public interface IAppConfigDecryption : IService
{
  string Password { get; set; }
  string Salt { get; set; }
  string Decrypt(string text);
}

以及服务作为新模块的实现

using Clifton.Core.ExtensionMethods;
using Clifton.Core.ModuleManagement;
using Clifton.Core.ServiceInterfaces;
using Clifton.Core.ServiceManagement;

namespace AppConfigDecryptionService
{
  public class AppConfigDecryptionModule : IModule
  {
    public void InitializeServices(IServiceManager serviceManager)
    {
      serviceManager.RegisterSingleton<IAppConfigDecryption, AppConfigDecryptionService>(d =>
      {
        d.Password = "somepassword";
        d.Salt = "somesalt";
      });
   }
  }

  public class AppConfigDecryptionService : ServiceBase, IAppConfigDecryption
  {
    public string Password { get; set; }
    public string Salt { get; set; }

    public string Decrypt(string text)
    {
      return text.Decrypt(Password, Salt);
    }
  }
}

显然(我希望如此),您会希望从某个安全位置获取密码和盐,这样通过反编译 DLL 就无法轻易获取 string,或者您可以在应用程序中初始化它们。

现在,我真的很喜欢扩展方法,所以真正的实现就在那里(顺便说一句,我不是说这是最好的实现,实际上这是我在 StackOverflow 上找到的,哈哈)

public static string Decrypt(this string base64, string password, string salt)
{
  string decryptedBytes = null;
  byte[] saltBytes = Encoding.ASCII.GetBytes(salt);
  byte[] passwordBytes = Encoding.ASCII.GetBytes(password);
  byte[] decryptBytes = base64.FromBase64();

  using (MemoryStream ms = new MemoryStream())
  {
    using (RijndaelManaged AES = new RijndaelManaged())
    {
      AES.KeySize = 256;
      AES.BlockSize = 128;

      var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
      AES.Key = key.GetBytes(AES.KeySize / 8);
      AES.IV = key.GetBytes(AES.BlockSize / 8);

      AES.Mode = CipherMode.CBC; // Cipher Block Chaining.

      using (var cs = new CryptoStream(ms, AES.CreateDecryptor(), CryptoStreamMode.Write))
      {
        cs.Write(decryptBytes, 0, decryptBytes.Length);
        cs.Close();
      }

      decryptedBytes = Encoding.Default.GetString(ms.ToArray());
    }
  }

  return decryptedBytes;
}

我们现在可以在 app.config 文件中提供加密值

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup> 
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <connectionStrings>
    <add name="myConnectionString" 
    connectionString="D+560OKzdaeBle1VHcKc+JyAWgRkVNTQxu/t7K5jSUo="/>
  </connectionStrings>
  <appSettings>
    <add key ="someKey" value="JggSd0i52WcOEERjBTQR+g=="/>
  </appSettings>
</configuration>

我们还修改 modules.xml 文件以使用加密的应用程序配置服务,此外还指定我们想要的解密服务

<?xml version="1.0" encoding="utf-8" ?>
<Modules>
  <Module AssemblyName='Clifton.EncryptedAppConfigService.dll'/>
  <Module AssemblyName='AppConfigDecryptionService.dll'/>
</Modules>

现在我们运行应用程序,得到相同的结果,但现在 string 未加密

请注意,我们根本不需要更改应用程序代码。这之所以有效,是因为应用程序可以使用应用程序配置服务,该服务被视为独占服务(无论是纯文本还是加密的),具有抽象接口 IConfigService。如果我们要同时支持纯文本和加密值,我们将不得不使用 IAppConfigService(用于纯文本值)或 IEncryptedAppConfigService(用于加密值)来获取正确的服务。

接口地狱

如果您认为 DLL 地狱很糟糕,那么基于模块/服务的实现开始出现的一个问题是潜在的接口地狱。每个服务都实现一个接口。您将它们保存在哪里?如何组织它们?实现服务的模块和使用服务的应用程序都需要对接口的引用。我通常将接口组织在两个单独的项目中

  • Clifton.Core.ServiceInterface -- 用于我的核心库中提供的服务。
  • [MyAppServiceInterfaces] -- 用于特定于应用程序的服务。

结论

模块和服务是实现依赖反转的好方法,上面的示例希望能说明这种架构的强大之处。但是,我仍然认为它不够抽象,原因很简单,应用程序仍然需要引用抽象或具体接口才能获取实现服务。

这将在下一篇文章中解决,我将在其中介绍语义发布-订阅者。那不会是轻量级的!

历史

  • 2016 年 8 月 25 日:初始版本
© . All rights reserved.