如何避免在类之间传递上下文引用

cja*_*cja 4 c# oop design-patterns dynamics-crm dynamics-crm-2011

Dynamics CRM 2011内部部署.(但是在远离Dynamics CRM的许多情况下存在这个问题.)

CRM插件有一个切入点:

void IPlugin.Execute(IServiceProvider serviceProvider)

(http://msdn.microsoft.com/en-us/library/microsoft.xrm.sdk.iplugin.execute.aspx)

serviceProvider是对插件执行上下文的引用.插件所做的任何有用的事情都需要访问serviceProvider或其成员.

有些插件很大且很复杂,包含几个类.例如,我正在开发一个插件,它有一个多次实例化的类.这个类需要使用serviceProvider.

从所有需要它的类访问serviceProvider的一种方法是向所有这些类添加属性,然后设置该属性.或者为每个类所需的serviceProvider部分添加属性.这些方法中的任何一种都会导致大量重复的代码.

另一种方法是在线程范围内有一个全局变量.但是,根据http://msdn.microsoft.com/en-us/library/cc151102.aspx,一个"不应该在插件中使用全局类变量".

那么访问serviceProvider而不在任何地方传递它的最佳方法是什么?

PS如果示例有帮助,serviceProvider提供对日志记录对象的访问.我希望几乎每个班级都能登录.我不想将对日志记录对象的引用传递给每个类.

sha*_*tor 7

这不是文档中的警告所得到的.该IServiceProvider不是在这方面的全局变量; 它是一个方法参数,因此每次调用Execute都会获得自己的提供者.

为了提高性能,Microsoft Dynamics CRM缓存插件实例.应该将插件的Execute方法编写为无状态,因为每次调用插件时都不会调用构造函数.此外,多个线程可以同时运行插件.所有每个调用状态信息都存储在上下文中.这意味着你不应该在插件中使用全局类变量[Emphasis mine].

将对象从上下文传递给需要它们的辅助类没有任何问题.警告建议不要在插件类本身的字段("类变量")中存储某些内容,这可能会影响Execute对同一实例的后续调用,或者如果同时Execute由同一实例上的多个线程调用则会导致并发问题.

当然,这种"全球性"必须被认为是过渡性的.如果您以插件类辅助类中的任何方式存储多个调用Execute可以访问的任何内容(例如,使用插件类上的字段或插件或辅助类上的静态),您可以将其保持开放状态问题.

作为一个单独的考虑,我会编写所涉及的帮助程序类,以接受尽可能特定于其功能的类型 - 直到单个实体的级别 - 而不是整个IServiceProvider.测试一个只EntityReference需要一个需要完整IServiceProviderIPluginExecutionContext模拟的类的类要容易得多.


关于全局变量vs注入类所需的值

你是对的,这是面向对象代码中无处不在的东西.看看这两个实现:

public class CustomEntityFrubber
{
    public CustomEntityFrubber(IOrganizationService service, Guid entityIdToFrub)
    {
        this.service = service;
        this.entityId = entityIdToFrub;
    }

    public void FrubTheEntity()
    {
        // Do something with service and entityId.
    }

    private readonly IOrganizationService service;
    private readonly Guid entityId;
}
Run Code Online (Sandbox Code Playgroud)
// Initialised by the plugin's Execute method.
public static class GlobalPluginParameters
{
    public static IOrganizationService Service
    {
        get { return service; }
        set { service = value; }
    }

    public static Guid EntityIdToFrub
    {
        get { return entityId; }
        set { entityId = value; }
    }

    [ThreadStatic]
    private static IOrganizationService service;

    [ThreadStatic]
    private static Guid entityId;
}

public class CustomEntityFrubber
{
    public FrubTheEntity()
    {
        // Do something with the members on GlobalPluginParameters.
    }
}
Run Code Online (Sandbox Code Playgroud)

所以假设你已经实现了类似第二种方法的东西,现在你有一堆类使用GlobalPluginParameters.一切都很顺利,直到你发现其中一个偶尔失败,因为它需要一个IOrganizationService通过调用获得的实例CreateOrganizationService(null),所以它作为系统用户而不是主叫用户(谁并不总是拥有所需的权限)访问CRM.

修复第二种方法需要您在不断增长的全局变量列表中添加另一个字段,记住ThreadStatic要避免并发问题,然后更改代码CustomEntityFrubber以使用新SystemService属性.你在所有这些类之间有紧密耦合.

不仅如此,所有这些全局变量都在插件调用之间徘徊.如果您的代码有一个以某种方式绕过分配的错误GlobalPluginParameters.EntityIdToFrub,那么突然您的插件会对当前调用未传递给它的数据进行莫名其妙的操作Execute.

CustomEntityFrubber除非你阅读它的代码,否则这些全局变量究竟需要哪一个并不明显.相比之下,无论你有多少帮助类,维护这些代码开始变得令人头疼."现在,这个对象需要我设置Guid1还是Guid2在我打电话之前?" 最重要的是,类本身不能确定某些其他代码不会改变它所依赖的全局变量的值.

如果您使用第一种方法,则只需向CustomEntityFrubber构造函数传递不同的值,而无需进一步更改代码.此外,没有陈旧的数据.构造函数明确了类具有哪些依赖关系,一旦它拥有它们,就可以确保它们不会因为它们的设计方式而改变.


Dar*_*ryl 2

在这个设计请求中,我会担心很多事情(并不是说它不好,只是人们应该意识到并预见到)。

  1. IOrganizationService 不是多线程安全的。我假设 IServiceProvider 的其他方面也不是那么好。
  2. 由于必须模拟其他属性,因此在 IServiceProvider 级别进行测试要复杂得多
  3. 如果您决定在插件外部调用当前插件中的逻辑(例如命令行服务),则需要一种处理日志记录的方法。

如果您不想在任何地方传递对象,简单的解决方案是在某个类上创建一个静态属性,您可以在插件执行时设置它,然后从任何地方访问。

当然,现在您必须处理上面的问题 #1,因此它必须是某种单例管理器,它可能会使用当前线程的 id来设置和检索该线程的值。这样,如果插件被触发两次,您可以根据当前执行的线程检索正确的上下文。(编辑@shambulator 的ThreadStatic属性应该有效,而不是一些时髦的线程 id 查找字典)

对于问题#2,我不会按IServiceProvider原样存储,而是将其分成不同的属性(例如IPluginExecutionContextIOrganizationService等)

对于问题 #3,在管理器中存储操作或函数而不是对象值本身可能更有意义。例如,如果不是存储 IPluginExecutionContext,而是存储接受要记录的字符串并使用 IPlurginExeuctionContext 进行记录的函数。这允许其他代码设置它自己的日志记录,而不依赖于从插件内执行。