如何在Orleans筒仓中实现DI(1.x版本,依赖注入)
介绍
从1.1.0版开始,奥尔良已支持ASP.NET vNext样式依赖项注入支持。然而,关于如何 在奥尔良筒仓中充分利用它的实际记载很少。本文旨在提供一个完整的分步指南,从头到尾将DI支持集成到筒仓中。本文不是关于奥尔良本身的教程,也不是关于温莎城堡的教程。假定两者都有粗略的知识。另外,尽管本文着重于与Castle的集成,但是替换任何其他DI容器(例如AutoFac,StructureMap,Ninject等)应该是相当琐碎的。
免责声明
应该提到的是,我是一个完全的初学者,涉及奥尔良并仍在学习中。如果有人在这里发现任何明显的错误,请指出来,以便我修复。这很大程度上是我将DI系统连接到奥尔良的研究纪事。
使用代码
最初,我们将创建一个不带DI支持的标准三项目解决方案。一切就绪后,我们将更新项目以通过Castle Windsor支持DI。这有点漫长,但是我想尽可能地透彻,不要错过任何东西。
第1步-创建项目
我们要做的第一件事是创建我们的项目。第一个项目(ClientApp)将是Orleans开发/测试主机。第二个(谷物)将是奥尔良谷物类集合。最终的(Grains.Interfaces)将是Orleans Grain接口集合。一旦创建并引用了项目,它应该看起来像以下屏幕截图(注意:我从项目中删除了示例代码)。



第2步-将Orleans.Server添加到Grains项目
我们想在一个单独的过程中托管我们的筒仓。最简单的方法是将nuget包“ Microsoft.Orleans.Server”添加到Grains项目中。这包括OrleansHost.exe,可以轻松为我们的谷物旋转筒仓。最简单的方法是通过powershell添加软件包:
Install-Package Microsoft.Orleans.Server -ProjectName Grains
步骤3-配置客户端和主机
我们需要添加两个配置文件。客户端配置(ClientConfiguration.xml)到ClientApp项目和服务器配置(OrleansConfiguration.xml)与纹理项目。两者都应设置为内容/副本(如果较新)。
该OrleansConfiguration.xml(谷物)应该是这样的:
<?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>
步骤4-配置Visual Studio以同时启动两个项目(可选)
这是一个可选步骤。如果跳过此步骤,则需要在启动ClientApp项目之前手动启动OrleansHost.exe。
在配置Visual Studio解决方案,推出多个项目本身是平凡的,因为我们的谷物的项目是不是一个可执行我们需要告诉Visual Studio的实际运行什么。最简单的方法是转到项目属性>调试选项卡>启动外部程序。但是,执行此路由时,必须在文本框中输入可执行文件的完整路径。如果将解决方案复制到另一个文件夹(或者如果有人将其下载到他们的计算机上),则可能导致问题。一种更可靠的解决方案是在设置OrleansHost.exe文件的路径时使用$(SolutionDir)宏。不幸的是,由于UI不支持宏,因此无法在Visual Studio本身中完成此操作。为此,我们需要修改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>
完成此操作并重新加载项目后,我们只需右键单击“解决方案”>“属性”>“启动项目”,然后选择“多个启动项目”(请参见下文)

步骤5-编写样板代码以启动ClientApp
现在(大部分)项目就位了,让我们返回ClientApp中的Program.cs并对其进行修改,以便它可以连接到我们的Grain筒仓。删除所有代码,并将其替换为以下代码:
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);
由于启动ClientApp时OrleansHost可能未完全加载,因此我们希望在应用程序中引入一个暂停,以使其有机会启动。如果要尝试同时启动OrleansHost和ClientApp,则ClientApp可能会崩溃,因为在尝试连接主机时主机将无法存活(即两个进程之间的竞争状态)。
GrainClient.Initialize("ClientConfiguration.xml");
这应该是不言自明的。本质上,我们使用前面编写的ClientConfiguration.xml配置客户端。
其余的我们稍后再填写。
此时,如果您运行解决方案,则OrleansHost和ClientApp均应成功加载。他们实际上不会做任何事情。让我们为此做些事情!
第6步-为我们的筒仓创建一些谷物
现在,我们已经创建了项目,将其连接起来并进行了实际的交谈,现在让我们创建一个颗粒,以便我们可以做一些有意义的工作。在此示例中,我们将转到经常使用的计算器示例。请记住,第一轮将完全不使用DI。一旦到位,我们将重构项目以支持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); } }
谷物中同样无聊的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?
步骤7-将DI引入Grains项目
在继续之前,我们需要为Grains项目添加两个nuget引用。第一个是温莎城堡(或您喜欢的任何一个DI容器)。第二个是Microsoft的Extension Dependeny Injection框架。在powershell中执行以下命令:
Install-Package Castle.Windsor -ProjectName Grains Install-Package Microsoft.Extensions.DependencyInjection -ProjectName Grains -Pre
注意Microsoft.Extensions.DependencyInjection的-Pre参数。在撰写本文时,该项目仍被视为预发布。
第8步-快速绕开Orleans DI的工作方式
在继续之前,最好先解释奥尔良在创建谷物时在幕后所做的事情。当奥尔良最初使粮食成为现实时,它使用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.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); } } }
让我们逐行进行以下操作:
我们需要做的第一件事是创建我们的容器,然后针对此注册我们所有的谷物实现。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'块中放置了一个异常,因为我不确定如何处理(仍在学习中)。同样,在撰写本文时,奥尔良还没有以这种方式注册任何东西。实际上,它注册的所有内容都是瞬态的。
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类的构造函数,然后将其返回。现在,每当奥尔良尝试创建某项内容时,它将使用我们的提供程序。
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>
注意:您可能会在这里注意到“智能提示”警告,即“启动”不是有效的元素类型。忽略它。它不是OrleansConfiguration.xsd文件的一部分,但是Orleans本身可以识别它。
此时,您可以再次运行该解决方案。该行为应与以前相同。如果您好奇,请在Startup类中设置一个断点,以确认实际上是由奥尔良调用来创建您的谷物。
步骤9-向我们的谷物添加依赖关系(最后)
回顾一下:我们的解决方案中有三个项目,一个客户端应用程序,一个界面项目以及一个运行我们筒仓的谷歌实现。我们已经创建了一个简单的计算器纹理,并且已经将Castle Windsor连接到所有内容中,因此现在我们可以在实例化纹理(或属性注入)时利用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());
而且...就是这样!继续并运行解决方案,以确保其仍然有效。
请享用!
https://www.codeproject.com/Articles/1099750/How-to-implement-DI-support-in-your-Orleans-Silo
浙公网安备 33010602011771号