5

内部部署 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 会提供对日志对象的访问。我希望几乎每个班级都记录下来。我不想将对日志记录对象的引用传递给每个类。

4

5 回答 5

7

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

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

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

当然,这种“全球性”必须被认为是可传递的。如果您以任何方式将任何内容存储在插件类帮助器类中,多个调用Execute可以访问(例如,使用插件类上的字段或插件或帮助器类上的静态),您让自己对相同问题。

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


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

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

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以使用新SystemService属性。所有这些类之间都有紧密的耦合。

不仅如此,所有这些全局变量都在插件调用之间徘徊。GlobalPluginParameters.EntityIdToFrub如果您的代码有一个错误以某种方式绕过Execute.

CustomEntityFrubber除非您阅读其代码,否则这些全局变量中究竟需要哪些变量也并不明显。将其乘以您拥有的许多帮助程序类,维护此代码开始变得令人头疼。“现在,这个对象需要我设置Guid1还是Guid2在我调用它之前?” 最重要的是,类本身不能确定其他一些代码不会改变它所依赖的全局变量的值。

如果您使用第一种方法,您只需将不同的值传递给CustomEntityFrubber构造函数,无需进一步更改代码。此外,没有过时的数据。构造函数使类具有哪些依赖项一目了然,一旦有了它们,就可以确定它们不会改变,除非它们的设计方式不同。

于 2013-10-01T15:35:31.297 回答
3

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

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

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

this.ServiceProvider = 服务提供者;

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

于 2013-10-01T21:03:07.947 回答
2

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

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

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

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

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

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

于 2013-10-01T16:54:42.647 回答
0

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

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

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

于 2013-10-01T16:13:55.980 回答
-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 的某个人已经告诉您的那样,存在全局变量限制会导致插件在同一上下文(对象上下文和可能的其他环境条件)中调用时不会再次实例化,因此任何自定义全局变量都将被共享在这些实例之间,但由于上下文是相同的,如果你想在很多类之间共享它,将它分配给一个全局变量是没有问题的。

无论如何,我宁愿将上下文传递给构造函数并共享它,对它有更多的控制权,但这只是我。

于 2013-10-02T08:45:54.747 回答