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

时间:2013-10-01 15:05:59

标签: c# oop design-patterns dynamics-crm-2011 dynamics-crm

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而不在任何地方传递它的最佳方法是什么?

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

5 个答案:

答案 0 :(得分:7)

这不是文档中的警告所得到的。在这种情况下,IServiceProvider不是全局变量;它是一个方法参数,因此Execute的每个调用都有自己的提供者。

  

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

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

当然,这种“全球性”必须被认为是过渡性的。如果您在帮助程序类中的任何插件类中存储任何内容,以及对Execute的多次调用可以访问的任何方式(使用插件类中的字段或例如,插件或辅助类上的静态,你可以对同样的问题保持开放。

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


关于全局变量与注入类

所需的值

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

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;
}

// 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.
    }
}

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

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

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

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

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

答案 1 :(得分:3)

正如您所说,您不应该在插件上放置成员变量,因为实例会在插件管道的请求之间进行缓存和重用。

我采用的方法是创建一个执行所需任务的类,并在构造函数上传递Developer Toolkit(http://msdn.microsoft.com/en-us/library/hh372957.aspx)提供的修改后的LocalPluginContext(使其成为公共类)。然后,您的类可以存储实例,以便以与使用任何其他代码相同的方式执行它的工作。您实际上是脱离了插件框架所施加的限制。这种方法也使单元测试变得更容易,因为您只需要为类提供执行上下文,而不是模拟整个插件管道。

值得注意的是,Developer Toolkit中自动生成的Plugin.cs类中存在一个错误,它没有设置ServiceProvider属性 - 在LocalPluginContext的构造函数的末尾添加了行:

this.ServiceProvider = serviceProvider;

我已经在插件中看到了一些IoC方法的实现 - 但恕我直言,它使插件代码过于复杂。我建议您的插件精简且简单,以避免线程/性能问题。

答案 2 :(得分:2)

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

  1. IOrganizationService不是多线程安全的。我假设IServiceProvider的其他方面不太好。
  2. 由于需要模拟的其他属性,在IServiceProvider级别测试事情要复杂得多
  3. 如果您决定在插件之外调用当前插件中的逻辑(例如命令行服务),则需要一种处理日志记录的方法。
  4. 如果您不想在任何地方传递对象,那么简单的解决方案是在某个类上创建一个静态属性,您可以在插件执行时设置它,然后从任何地方进行访问。

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

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

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

答案 3 :(得分:0)

我自己没有制作任何这些插件,但我会将IServiceProvider视为I / O设备。

从中获取所需数据并将该数据转换为适合您插件的格式。使用转换后的数据设置其他类。获取其他类的输出,然后转换回IServiceProvider可以理解和使用的术语。

您的输入和输出取决于IServiceProvider,但不一定要处理。

答案 4 :(得分:-1)

来自Eduardo Avaria http://social.microsoft.com/Forums/en-US/f433fafa-aff7-493d-8ff7-5868c09a9a9b/how-to-avoid-passing-a-context-reference-among-classes

好吧,正如SO的某位人士告诉你的那样,全局变量限制是因为如果在相同的上下文(对象上下文和可能的其他环境条件)中调用插件,则插件不会再次实例化,因此任何自定义全局变量将在这些实例之间共享,但由于上下文将是相同的,如果你想在很多类之间共享它,将它分配给全局变量没有问题。

无论如何,我宁愿在构造函数上传递上下文并分享它对它的更多控制,但那只是我。