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

如何在 Orleans Silo 中实现 DI 支持

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.75/5 (3投票s)

2016年5月12日

CPOL

8分钟阅读

viewsIcon

15116

downloadIcon

6

本文介绍如何在 Orleans Silo 中实现 DI 支持。

引言

从 1.1.0 版本开始,Orleans 就支持 ASP.NET vNext 风格的依赖注入支持。 然而,关于如何在 Orleans silo 中实际利用它的文档非常少。 本文旨在提供一个完整的、分步指南,从头到尾将 DI 支持集成到 silo 中。 本文不是关于 Orleans 本身,也不是关于 Castle Windsor 的教程。 假设读者对两者都有初步了解。 此外,尽管本文侧重于与 Castle 集成,但替换任何其他 DI 容器(例如 AutoFac、StructureMap、Ninject 等)应该相当简单。

免责声明

需要指出的是,我对 Orleans 完全是初学者,还在学习中。 如果有人发现任何明显错误的地方,请指出,以便我进行修正。 这在很大程度上是我研究如何将 DI 系统连接到 Orleans 的过程记录。

使用代码

最初,我们将创建一个标准的、没有 DI 支持的三项目解决方案。 一旦一切都连接好,我们将更新项目以通过 Castle Windsor 支持 DI。 这有点啰嗦,但我希望尽可能详尽,不错过任何细节。

第一步 - 创建项目

我们要做的第一件事是创建项目。 第一个项目 (ClientApp) 将是一个 Orleans Dev/Test Host。 第二个项目 (Grains) 将是一个 Orleans Grain 类集合。 最后一个项目 (Grains.Interfaces) 将是一个 Orleans Grain 接口集合。 项目创建并引用后,应该如下面的屏幕截图所示(注意:我已删除项目中的示例代码)。

第二步 - 向 Grains 项目添加 Orleans.Server

我们希望在单独的进程中托管我们的 silo。 最简单的方法是将 nuget 包 'Microsoft.Orleans.Server' 添加到 Grains 项目。 这包括 OrleansHost.exe,它使我们能够轻松地为我们的 grain 启动一个 silo。 最简单的方法是通过 powershell 添加包。

Install-Package Microsoft.Orleans.Server -ProjectName Grains

第三步 - 配置客户端和主机

我们需要添加两个配置文件。 一个客户端配置文件 (ClientConfiguration.xml) 到 ClientApp 项目,一个服务器配置文件 (OrleansConfiguration.xml) 到 Grains 项目。 两者都应设置为 content/copy if newer。

OrleansConfiguration.xml (Grains) 应该如下所示。

<?xml version="1.0" encoding="utf-8"?>
<OrleansConfiguration xmlns="urn:orleans">
  <Globals>
    <SeedNode Address="localhost" Port="10000" />
  </Globals>
  <Defaults>
    <Networking Address="localhost" Port="10000" />
    <ProxyingGateway Address="localhost" Port="30000" />
  </Defaults>
</OrleansConfiguration>

ClientConfiguration.xml (ClientApp) 应该如下所示。

<?xml version="1.0" encoding="utf-8" ?>
<ClientConfiguration xmlns="urn:orleans">
  <Gateway Address="localhost" Port="30000" />
</ClientConfiguration>

第四步 - 配置 Visual Studio 同时启动两个项目(可选)

这是可选步骤。 如果跳过此步骤,则需要在启动 ClientApp 项目之前手动启动 OrleansHost.exe。

虽然配置 Visual Studio 解决方案以自行启动多个项目非常简单,但由于我们的 Grains 项目不是可执行文件,我们需要告诉 Visual Studio 实际运行什么。 最简单的方法是转到 Project Properties > Debug Tab > Start External Program。 然而,通过这种方式,您必须在文本框中输入可执行文件的完整路径。 如果解决方案被复制到另一个文件夹(或者如果其他人将其下载到他们的计算机上),这可能会导致问题。 更健壮的解决方案是在设置 OrleansHost.exe 文件的路径时使用 $(SolutionDir) 宏。 不幸的是,这无法在 Visual Studio 本身中完成,因为 UI 不支持宏。 要做到这一点,我们需要修改 csproj 文件本身。 打开 Grains.csproj 并修改 <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> 元素以包含此信息(见下文:突出显示)。 Visual Studio 会在项目加载时自动为您展开它。

  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
    <Prefer32Bit>false</Prefer32Bit>
    <StartAction>Program</StartAction>
    <StartProgram>$(SolutionDir)Grains\bin\Debug\OrleansHost.exe</StartProgram>
    <StartWorkingDirectory>
    </StartWorkingDirectory>
  </PropertyGroup>

完成此操作并重新加载项目后,我们只需右键单击 Solution > Properties > Startup Project 并选择“Multiple Startup Projects”(见下文)。

第五步 - 编写样板代码来启动 ClientApp

现在项目(大部分)已就绪,让我们回到 ClientApp 中的 Program.cs 并修改它,以便它可以连接到我们的 Grain silo。 删除所有代码并用以下内容替换。

using System;
using Orleans;

namespace ClientApp
{
    public class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Press any key once the silo has started.");
            Console.ReadKey(intercept: true);
            GrainClient.Initialize("ClientConfiguration.xml");

            // TODO: our grains will go here later.

            Console.WriteLine("Press any key to exit.");
            Console.ReadKey(intercept: true);
        }
    }
}

让我们逐一分析。

Console.WriteLine("Press any key once the silo has started.");
Console.ReadKey(intercept: true);

由于 OrleansHost 可能在我们启动 ClientApp 时尚未完全加载,因此我们希望在应用程序中引入一个暂停,让它有机会启动。 如果有人试图同时启动 OrleansHost 和 ClientApp,那么 ClientApp 可能会崩溃,因为当它尝试连接到主机时,主机可能还没有启动(即,两个进程之间存在竞态条件)。

GrainClient.Initialize("ClientConfiguration.xml");

这应该是不言自明的。 基本上,我们使用之前编写的 ClientConfiguration.xml 来配置我们的客户端。

其余部分我们将在稍后填写。

此时,如果您运行解决方案,OrleansHost 和 ClientApp 都应该成功加载。 它们实际上不会做任何事情。 让我们来做点事情!

第六步 - 为我们的 Silo 创建一些 Grains

现在我们的项目已创建、连接并且实际上正在相互通信,让我们创建一个 grain 以便我们可以做一些有意义的工作。 在这个例子中,我们将使用常用的计算器示例。 请记住,第一轮将完全不使用 DI。 一旦我们的 grains 就绪,我们将重构项目以支持 DI。

让我们从 ICalculatorGrain 开始。 这放在 Grains.Interfaces 中,应该如下所示。

using System.Threading.Tasks;
using Orleans;

namespace Grains.Interfaces
{
    public interface ICalculatorGrain : IGrainWithGuidKey
    {
        Task<int> Add(int a, int b);
        Task<int> Sub(int a, int b);
        Task<int> Mul(int a, int b);
        Task<int> Div(int a, int b);
    }
}

Grains 中同样无趣的 CalculatorGrain 应该如下所示。

using System.Threading.Tasks;
using Grains.Interfaces;
using Orleans;

namespace Grains
{
    public class CalculatorGrain : Grain, ICalculatorGrain
    {
        public Task<int> Add(int a, int b) => Task.FromResult(a + b);
        public Task<int> Sub(int a, int b) => Task.FromResult(a - b);
        public Task<int> Mul(int a, int b) => Task.FromResult(a * b);
        public Task<int> Div(int a, int b) => Task.FromResult(a / b);
    }
}

最后,让我们回到 ClientApp 中的 Main() 方法,用一些示例代码替换我们的 // TODO 部分,以测试此功能。

var grain = GrainClient.GrainFactory.GetGrain<ICalculatorGrain>(Guid.NewGuid());

Console.WriteLine($"1 + 2 = {grain.Add(1, 2).Result}");
Console.WriteLine($"2 - 3 = {grain.Sub(2, 3).Result}");
Console.WriteLine($"3 * 4 = {grain.Mul(3, 4).Result}");
Console.WriteLine($"4 / 2 = {grain.Div(4, 2).Result}");

如果我们现在运行解决方案,我们应该会得到以下输出。

一切正常。 太棒了! 现在 DI 支持怎么样?

第七步 - 将 DI 引入 Grains 项目

在继续之前,我们需要向 Grains 项目添加两个 nuget 引用。 第一个是 Castle Windsor(或您选择的任何 DI 容器)。 第二个是 Microsoft 的 Extension Dependeny Injection 框架。 从 powershell 执行以下命令。

Install-Package Castle.Windsor -ProjectName Grains
Install-Package Microsoft.Extensions.DependencyInjection -ProjectName Grains -Pre

请注意 Microsoft.Extensions.DependencyInjection 的 -Pre 参数。 在撰写本文时,此项目仍被视为预发布版。

第八步 - 简单了解 Orleans DI 如何工作

在继续之前,最好解释一下 Orleans 在创建您的 grains 时在后台做了什么。 当 Orleans 最初实例化一个 grain 时,它使用 an IServiceProvider 接口来实际实例化对象。 这是一个非常简单的接口,只有一个方法。

object GetService(Type serviceType);

默认情况下(至少从 1.2.0 版本开始):Orleans 使用的默认服务提供程序仅使用 Activator 类来实例化对象。 例如。

public class DefaultServiceProvider : IServiceProvider
{
    public object GetService(Type serviceType)
    {
        return Activator.CreateInstance(serviceType);
    }
}

仅凭这些信息,就可以清楚第一步是创建一个 IServiceProvider 实现,该实现通过容器进行解析。 让我们现在就做。

using System;
using Castle.MicroKernel;

namespace Grains
{
    public class CastleServiceProvider : IServiceProvider
    {
        private readonly IKernel kernel;

        public CastleServiceProvider(IKernel kernel)
        {
            this.kernel = kernel;
        }

        public object GetService(Type serviceType) => this.kernel.Resolve(serviceType);
    }
}

这很简单——但我们应该把它放在哪里? 深入研究 Orleans 的源代码,您会发现 Orleans.DependencyInjection 项目中的 ConfigureStartupBuilder 类。 无需过多深入文件,它基本上是带有一个参数:startupTypeName(稍后会详细介绍)。 启动构建器会内省该类型,并在其中查找名为“ConfigureServices”的方法,该方法接受 IServiceCollection 类型的参数并返回 IServiceProvider 类型。 如果您仔细研究代码,会发现默认情况下,如果没有配置 startupTypeName,它将仅返回前面提到的 DefaultServiceProvider 的实例。

利用这些信息,让我们创建一个实现此方法的类。

using System;
using Castle.MicroKernel.Registration;
using Castle.Windsor;
using Grains.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using Orleans;

namespace Grains
{
    public class Startup
    {
        public IServiceProvider ConfigureServices(IServiceCollection services)
        {
            var container = new WindsorContainer();

            // Scan for all of the grains in this assembly.
            container.Register(Classes.FromThisAssembly().BasedOn<Grain>().LifestyleTransient());

            // TODO: Register our custom providers here.

            foreach (var service in services)
            {
                switch (service.Lifetime)
                {
                    case ServiceLifetime.Singleton:
                        container.Register(
                            Component
                                .For(service.ServiceType)
                                .ImplementedBy(service.ImplementationType)
                                .LifestyleSingleton());
                        break;
                    case ServiceLifetime.Transient:
                        container.Register(
                            Component
                                .For(service.ServiceType)
                                .ImplementedBy(service.ImplementationType)
                                .LifestyleTransient());
                        break;
                    case ServiceLifetime.Scoped:
                        var error = $"Scoped lifestyle not supported for '{service.ServiceType.Name}'.";
                        throw new InvalidOperationException(error);
                }
            }

            return new CastleServiceProvider(container.Kernel);
        }
    }
}

让我们逐行分析。

我们需要做的第一件事是创建我们的容器,然后将我们所有的 grain 实现注册到其中。 Classes.FromThisAssembly() 是在 Castle Windsor 中批量注册类型的便捷方法。

var container = new WindsorContainer();

// Scan for all of the grains in this assembly.
container.Register(Classes.FromThisAssembly().BasedOn<Grain>().LifestyleTransient());

当 Orleans 调用我们的 ConfigureServices() 方法时,它会传递一个它希望自动注册的类型映射列表,因此我们需要将它们注册到容器中。 我在“Scoped”块中放了一个异常,因为我不确定如何处理(仍在学习)。 此外,在当前撰写本文时,Orleans 不会以这种方式注册任何内容。 事实上,它注册的所有内容都是瞬态的。

foreach (var service in services)
{
    switch (service.Lifetime)
    {
        case ServiceLifetime.Singleton:
            container.Register(
                Component
                    .For(service.ServiceType)
                    .ImplementedBy(service.ImplementationType)
                    .LifestyleSingleton());
            break;
        case ServiceLifetime.Transient:
            container.Register(
                Component
                    .For(service.ServiceType)
                    .ImplementedBy(service.ImplementationType)
                    .LifestyleTransient());
            break;
        case ServiceLifetime.Scoped:
            throw new InvalidOperationException($"Scoped lifestyle not supported '{service.ServiceType.Name}'.");
    }
}

最后,我们将容器的内核传递给我们的 CastleServiceProvider 类的构造函数并返回它。 现在,每当 Orleans 尝试创建某个东西时,它都会使用我们的提供程序而不是默认提供程序。

return new CastleServiceProvider(container.Kernel);

那么,那个 startupTypeName 参数呢? 它去哪儿了? 在 OrleansConfiguration.xml 中!

让我们打开 OrleansConfiguration.xml 并将其添加到 <Defaults /> 部分。

<?xml version="1.0" encoding="utf-8"?>
<OrleansConfiguration xmlns="urn:orleans">
  <Globals>
    <SeedNode Address="localhost" Port="10000" />
  </Globals>
  <Defaults>
    <Startup Type="Grains.Startup, Grains" />
    <Networking Address="localhost" Port="10000" />
    <ProxyingGateway Address="localhost" Port="30000" />
  </Defaults>
</OrleansConfiguration>

注意:您可能会注意到此处有一个 intellisense 警告,表明“Startup”不是有效的元素类型;请忽略它。 它不是 OrleansConfiguration.xsd 文件的一部分,但 Orleans 本身可以识别它。

此时,您可以再次运行解决方案。 行为应该与之前完全相同。 如果您好奇,可以设置一个断点在 Startup 类中,以确认 Orleans 确实调用了它来创建您的 grains。

第九步 - 向我们的 Grains 添加依赖项(终于)

总结一下:我们的解决方案中有三个项目,一个客户端应用程序,一个接口项目,以及一个运行我们 silo 的 grain 实现。 我们创建了一个简单的计算器 grain,并将 Castle Windsor 连接到了所有地方,以便我们现在可以在实例化 grains 时使用 ctor 注入(或 属性注入)。 本教程的最后一部分对于熟悉 DI 的任何人来说都是标准的。

首先,让我们在 Grains.Interfaces 项目中为我们的“新”计算器提供程序创建一个接口。

namespace Grains.Interfaces
{
    public interface ICalculatorProvider
    {
        int Add(int a, int b);
        int Sub(int a, int b);
        int Mul(int a, int b);
        int Div(int a, int b);
    }
}

其次,我们在 Grains 项目中创建一个实现。

using Grains.Interfaces;

namespace Grains
{
    public class CalculatorProvider : ICalculatorProvider
    {
        public int Add(int a, int b) => a + b;
        public int Div(int a, int b) => a / b;
        public int Mul(int a, int b) => a * b;
        public int Sub(int a, int b) => a - b;
    }
}

第三,让我们重写我们的 CalculatorGrain,以便它将 ICalculatorProvider 作为依赖项并使用它。

using System.Threading.Tasks;
using Grains.Interfaces;
using Orleans;

namespace Grains
{
    public class CalculatorGrain : Grain, ICalculatorGrain
    {
        private readonly ICalculatorProvider calc;

        public CalculatorGrain(ICalculatorProvider calc)
        {
            this.calc = calc;
        }

        public Task<int> Add(int a, int b) => Task.FromResult(calc.Add(a, b));
        public Task<int> Div(int a, int b) => Task.FromResult(calc.Div(a, b));
        public Task<int> Mul(int a, int b) => Task.FromResult(calc.Mul(a, b));
        public Task<int> Sub(int a, int b) => Task.FromResult(calc.Sub(a, b));
    }
}

最后,我们需要将我们的提供程序注册到容器中。 在一个正常的应用程序中,我们可能会使用 IWindsorInstaller 来完成此操作,但为了保持简单,我们将通过手动替换 Startup.ConfigureServices() 方法的 // TODO 部分来完成。

container.Register(Component.For<ICalculatorProvider>().ImplementedBy<CalculatorProvider>().LifestyleTransient());

就是这样! 运行解决方案以确保它仍然有效。

尽情享用!

© . All rights reserved.