我们有一个多租户Web应用程序,其中每个租户都有许多页面。因此,我们的许多接口看起来像这样
interface ISprocketDeployer
{
void DeploySprocket(int tenantId);
}
我想到,简化这些界面可能会更好,不知道tenantId
。这些页面也会不知道tenantId
,就像这样
[Inject] // Ninject
public ISprocketDeployer SprocketDeployer { get; set; }
private void _button_OnClick(object sender, EventArgs e)
{
SprocketDeployer.DeploySprocket();
}
依赖注入框架然后通过查看当前经过身份验证的用户将租户ID注入依赖项。 这是一个好主意还是滥用依赖注入?
我进一步想到,许多实现也只是为了查找有关租户的详细信息而采取额外的依赖关系,并且我可以通过直接注入该详细信息来进一步减少依赖项的数量,例如
class SprocketDeployer
{
public SprocketDeployer(ITenantRepository tenantRepository)
{
_tenantRepository = tenantRepository;
}
void DeploySprocket(int tenantId)
{
var tenantName = _tenantRepository.GetTenant(tenantId).Name;
// Do stuff with tenantName
}
}
会变成
class SprocketDeployer
{
public SprocketDeployer(Tenant tenant)
{
_tenant = tenant;
}
void DeploySprocket()
{
var tenantName = _tenant.Name;
// Do stuff with tenantName
}
}
然后我意识到我还可以注入其他"依赖关系",例如以相同方式关于当前登录用户的详细信息。
那时我变得不确定了。虽然起初这似乎是一个奇妙的想法我意识到我不确定何时停止添加额外的"依赖"。 如何确定依赖项应该是什么以及参数应该是什么?
答案 0 :(得分:1)
我不会把它称为滥用,但那说:
依赖注入的一般用例(通过容器)是注入不直接表示状态的纯服务。其中一个直接问题是通知容器应该在运行时注入哪个对象实例。如果您的SprocketDeployer需要租户,并且您的系统包含许多租户,那么容器如何确定哪些租户在运行时提供?
如果您想避免传递租户,请考虑使用线程本地存储(TLS)。但是,在管道中仍然需要将租户添加到TLS中。
修改
来自你的评论:
我解决了确定在运行时提供哪个租户的问题 在Ninject中通过将类型绑定到检查的方法 HttpContext.Current并使用InRequestScope。它工作正常,但我已经 没有看到任何表明这是(或不是)推荐的东西 实践。
如果我理解正确的话,那听起来像是各种各样的工厂?如果是这样的话,我认为没有错。
一个小小的挑剔可能是:能够不必担心你的服务范围如何是很好的。当它们是真正无状态的服务时,您可以将它们视为纯粹的可交换组件,这些组件没有基于容器配置的副作用。
答案 1 :(得分:1)
和菲尔一样,我不会把这种依赖注入滥用称为滥用,尽管它确实感觉有些奇怪。
您至少有几个选择。我将从你提供的细节中详细介绍一对看起来最好的一对,但是当你说'我后来意识到我还可以注入其他“依赖关系”时,这可能是你所指的,例如关于当前登录用户的方式相同。'
拥有代表当前租户的抽象可能是完全合理的。这个抽象是一个工厂,但我更喜欢术语“提供者”,因为工厂意味着创建,而提供者可能只是检索一个现有的对象(注意:我意识到微软引入了一个提供者模式,但这不是我所指的)。在这种情况下,您不是注入数据,而是注入服务。我可能会称之为ICurrentTenantProvider
。实施通常是特定于上下文的。例如,现在,它将来自您的HttpContext
对象。但是,您可以决定某个特定客户需要他们自己的服务器,然后注入一个ICurrentTenantProvider
,它将从您的web.config文件中检索它。
除非您必须根据租户[1]做不同的事情,否则完全隐藏多租户可能会更好。在这种情况下,您将注入我将要调用提供程序的类,这些类是上下文感知的,其函数调用的结果将基于当前租户。例如,您可能有ICssProvider
和IImageProvider
。仅这些提供商就会意识到该应用程序支持多租户。他们可能使用另一个抽象,例如上面引用的ICurrentTenantProvider
,或者可以直接使用HttpContxt
。无论实施如何,他们都会返回特定于租户的背景信息。
在这两种情况下,我都建议注入服务而不是数据。该服务提供了一个抽象层,允许您注入一个适当的上下文感知的实现。
我如何确定应该是什么依赖项以及什么应该是参数?
我通常只会注入服务并避免注入像值对象这样的东西。决定你可能会问自己一些问题:
对于(1),注入值对象没有意义。对于(2),如果它是一致的,就像租户一样,注入一个知道租户的服务可能会更好。如果是(3),则可能表示缺少抽象。如果是(4),你可能会再次错过抽象。
在(3)和(4)的静脉中,根据应用的细节,我可以看到ICurrentTenantProvider
被注入许多地方,这可能表明它有点低水平。此时ICssProvider
或类似的抽象可能有用。
[1] - 如果您注入数据(如int),您将被迫查询,最终可能会出现replace conditional with polymorphism的情况。
答案 2 :(得分:0)
10/14/15 UPDATE BEGIN
三个多月后,我对这种方法遇到的具体情况有了一点改变。
我已经提到很长一段时间了,我现在也经常注入当前的#34;身份" (tenantAccount,用户等),无论何时需要。但是,我遇到了一种情况,我需要能够临时更改该标识仅用于要执行的部分代码(在同一个执行线程内)。
最初,对这种情况的清洁解决方案对我来说并不明显。
我很高兴地说,最终我确实找到了一个可行的解决方案 - 现在已经很高兴地离开了一段时间。
将实际的代码示例(它目前在专有系统中实现)放在一起需要一些时间,但同时这里至少是一个高级概念概述。
注意:根据您的喜好命名接口,类,方法等 - 如果对您有意义,甚至可以组合使用。它只是重要的整体概念。
首先,我们定义一个IIdentityService,公开一个GetIdenity()。这成为了在我们需要的任何地方获取当前身份的事实上的依赖(repos,services等所有东西都使用它)。
IIdentityService实现依赖于IIdentityServiceOrchestrator。
在我的系统中,IIdentityServiceOrchestrator implmentation使用mutliple IIdentityResolvers(其中只有两个实际适用于此讨论:authenticatedIdentityResolver和manualIdentityResolver)。 IIdentityServiceOrchestrator公开.Mode属性以设置活动的IIdentityResolver(默认情况下,它在我的系统中设置为' authenticated'。)
现在,您可以停在那里并将IIdentityServiceOrchestrator注入设置身份所需的任何位置。但是,您负责管理设置和回滚临时标识的整个过程(设置模式,如果已经处于手动模式,还要备份和恢复标识详细信息等)。< / p>
因此,下一步是介绍一个IIdentityServiceOchestratorTemporaryModeSwitcher。是的,我知道名字很长 - 把它命名为你想要的。 ;)这暴露了两个方法:SetTemporaryIdentity()和Rollback()。 SetTemporaryIdentiy()已重载,因此您可以通过模式或手动标识进行设置。该实现依赖于IIdentityServiceOrchestrator,并管理备份当前现有标识详细信息,设置新模式/详细信息以及回滚详细信息的所有详细信息。
现在,您可以再次停在那里,并在您需要设置临时身份的任何地方注入IIdentityServiceOchestratorTemporaryModeSwitcher。但是,那么你将被迫在一个地方使用.SetTemporaryIdentity()而在另一个地方被强制使用.Rollback(),如果没有必要,这可能会变得混乱。
所以,现在我们终于介绍了最后的谜题:TemporaryIdentityContext和ITemporaryIdentityContextFactory。
TemporaryIdentityContext实现IDisposable并通过重载的构造函数依赖于IIdentityServiceOchestratorTemporaryModeSwitcher和Identity / Mode集。在ctor中,我们使用IIdentityServiceOchestratorTemporaryModeSwitcher.SetTemporaryIdentity()来设置临时标识,并在我们调用IIdentityServiceOchestratorTemporaryModeSwitcher.Rollback进行清理时。
现在,在我们需要设置身份的地方,我们注入ITemporaryIdentityContextFactory,它暴露了.Create()(再次为身份/模式重载),这就是我们如何获取临时身份上下文。返回的temporaryIdentityContext对象本身并没有真正触及它只是为了控制临时标识的生命周期。
示例流程:
//原始身份
使用(_TemporaryIdentityContextFactory.Create(manualIdentity)){
// Temp Identity Now in place
DoSomeStuff();
}
//再次回到原始身份..
这在概念上非常重要;显然已经遗漏了很多细节。
还应该讨论IOC寿命问题。在这里讨论的最纯粹的形式中,通常可以将每个组件(IIdentityService,IIdentityServiceOrchestrator,ITemporaryIdentityContextFactory)设置为“PerRequest”。一生。但是,如果您恰好通过单个请求来产生多个线程,那么它可能变得很时髦......在这种情况下,您可能希望使用“终身”等生命周期来确保注射没有线程串扰。
好的,希望实际上可以帮助某人(而且并没有完全错综复杂,哈哈)。我发布了一个代码示例,应该在我有时间的时候进一步清理。
10/14/15更新结束
只是想插话并说你并不孤单。我在野外有几个多租户应用程序,以同样的方式将租户信息注入其中。
然而,最近我遇到了一个问题,这样做会让我感到非常悲伤。
仅仅为了举例说明你有以下(非常线性)依赖图: ISomeService - &gt; IDep2 - &gt; IDep3 - &gt; ISomeRepository - &gt; ITenentInfoProvider
因此,ISomeService依赖于IDep2,它依赖于IDep3 ......依此类推,直到注入ITenentInfoProvider为止。
那么,问题是什么?那么,如果在ISomeService中你需要对另一个租户采取行动而不是你当前登录的租户呢?如何将一组不同的TenantInfo注入ISomeRepository?
好吧,一些IOC容器有基于上下文的条件支持(Ninject&#39; WhenInjectedInto&#34;,&#34; WhenAnyAnchestorNamed&#34;绑定例如)。所以,在更简单的情况下,你可以用这些来管理一些hacky。
但是如果在ISomeService中你需要启动两个操作,每个操作针对不同的租户呢?如果不引入多个标记接口等,上述解决方案将会失败。为了依赖注入而将代码更改为此范围只是在多个级别上闻起来很糟糕。
现在,我确实想出了一个基于容器的解决方案,但我不喜欢它。
您可以介绍一个ITenantInfoResolverStratagy,并为每个&#34;方式&#34;解析TenantInfo(AuthenticationBasedTenantInfoResolverStratagy,UserProvidedTenantInfoResolverStratagy等)。
接下来,您将介绍一个CurrentTenantInfoResolverStratagy(在容器中注册为PerRequestLifeTime,因此它可以作为您通话时间的单身,等等)。这可以在您需要的任何地方注入,以设置下游客户端将使用的策略。因此,在我们的示例中,我们将其注入ISomeService,我们将策略设置为&#34; UserProvided&#34; (给它一个TenantId等),现在,在链中,当ISomeRepository向ITenentInfoProvider请求TenantInfo时,ITenentInfoProvider转向从注入的CurrentTenantInfoResolverStratagy中获取它。
回到ISomeService,可以根据需要多次更改CurrentTenantInfoResolverStratagy。
那么,为什么我不喜欢这个?
对我而言,这实际上只是一个过于复杂的全局变量。在我的脑海中,关于全局变量相关的所有相关问题(由于任何时候任何人都可以发现意外行为,并发问题等等)。
整个事情要解决的问题(主要是不必将tenantId / tenantInfo作为参数传递)可能不值得随之而来的固有问题。
那么什么是更好的解决方案?好吧,我可能只是想到一些优雅的东西(可能是一些指挥链实现?)。
但是,我真的不知道。
它可能不是很优雅,但在任何与租户相关的方法调用中传递TenantId / TenantInfo作为参数肯定会避免这种彻底的崩溃。
如果其他人有更好的想法,请务必参与其中。