【问题标题】:How to avoid passing a context reference among classes如何避免在类之间传递上下文引用
【发布时间】:2013-10-01 15:05:59
【问题描述】:

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 而不到处传递它的最佳方式呢?

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

【问题讨论】:

  • 我也有同样的问题,但我没有找到任何解决方案。
  • 我现在做了很多Android。在这方面也有必要在所有地方传递上下文参数。

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


【解决方案1】:

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

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

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

当然,这种“全局性”必须被认为是可传递的。如果您将任何内容存储在插件类中的帮助类以任何方式对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 的类。一切正常,直到您发现其中 一个 偶尔失败,因为它需要通过调用 CreateOrganizationService(null) 获得的 IOrganizationService 实例,因此它以系统用户而不是调用用户身份访问 CRM (他们并不总是拥有所需的权限)。

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

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

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

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

【讨论】:

  • 但是将相同事物的引用传递给每个对象是很愚蠢的。在大多数应用程序/插件中,有一些东西让所有东西都可用是务实的。否则,您最终会在大多数类上得到相同的属性负载,并且一遍又一遍地进行相同的分配。
  • 我知道 serviceProvider 不是全局变量。我重新表述了我的问题,以更好地表达我的理解。
  • 传递这些不同对象的最大好处是,正如 shambulator 所提到的,测试/模拟。此外,您不必将 Service Provider 传递给所有内容,只需传递您需要的对象即可。例如,90% 的时间您只需要组织服务和跟踪服务。一旦你抽象到一层类,并且如果你有一个好的对象模型/模式,你可能也会停止传递组织服务。您甚至可以停止传递跟踪服务,具体取决于您需要在堆栈中更深的跟踪级别。
  • “没有错” - 是的 - 很乏味。我发现它在 Android 中也很有必要。
  • @cja 见上文,“这是面向对象代码中随处可见的东西”。
【解决方案2】:

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

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

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

this.ServiceProvider = serviceProvider;

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

【讨论】:

    【解决方案3】:

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

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

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

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

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

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

    【讨论】:

      【解决方案4】:

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

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

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

      【讨论】:

      • 这是查看与 serviceProvider 交互的好方法,但您没有解决如何避免在我的所有类周围传递相同的属性负载。
      【解决方案5】:

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

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

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

      【讨论】:

        猜你喜欢
        • 2018-12-23
        • 2021-01-23
        • 2011-08-23
        • 2011-11-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-02-09
        • 1970-01-01
        相关资源
        最近更新 更多