Skip to content
Daryl LaBar edited this page May 30, 2024 · 8 revisions

Inversion of Control (IOC)

IOC can be a very difficult topic to understand the problem it is solving as well as the how it is being solved. So this page will walk through a hypothetical plugin and how the IOC is used to solve the problems.

HolidayEnforcer Plugin

This hypothetical plugin is designed to keep users from working on holidays by throwing an exception if the day is a holiday. There will be many iterations of it given, each one improving on the previous.

V1 The Untestable

public class HolidayEnforcerV1_Untestable : IPlugin
{
    public const int HolidayMonth = 12;
    public const int HolidayDay = 25;

    public void Execute(IServiceProvider serviceProvider)
    {
        var now = DateTime.Now; /***** <-- BUG *****/
        if (now.Month == HolidayMonth
            && now.Day == HolidayDay)
        {
            throw new InvalidPluginExecutionException("It's a holiday.  Stop working!");
        }
    }
}

This plugin has a bug since it uses server time, not user time, but there is no way to test this unless you wait for the holiday to test. In order to fix this, and allow for testing the plugin, the time needs to be injected into the plugin. There are many ways to do this, usually it's by constructor injection, but since we're dealing with a plugin, we'll expose it via a public property, which will allow it to be testable.

V2 It's Testable!

public class HolidayEnforcerV2_TestableButBrittle : IPlugin
{
    public const int HolidayMonth = 12;
    public const int HolidayDay = 25;
    public ITimeProvider TimeProvider { get; set; }

    public void Execute(IServiceProvider serviceProvider)
    {
        var context = serviceProvider.Get<IPluginExecutionContext>();
        var factory = serviceProvider.Get<IOrganizationServiceFactory>();
        var service = factory.CreateOrganizationService(context.InitiatingUserId);
        var timeProvider = TimeProvider ?? new TimeProvider(service);
        var userTime = timeProvider.GetUserLocalDateTime();
        if (userTime.Month == HolidayMonth
            && userTime.Day == HolidayDay)
        {
            throw new InvalidPluginExecutionException("It's a holiday.  Stop working!");
        }
    }
}

During normal execution on the server, the TimeProvider property will of course never get set, and so a new instance of the TimeProvider will be created for each execution of the plugin, which will then be used to get the local time of the user. During the test, the test can inject its own ITimeProvider that can give specific date/times to test with.

Issues with V2

Getting the user's local time is a rather common task, and it wouldn't be too outlandish to have 100 different files/call sites needing to get the user's local time. Assuming that this is the case, due to the dependency of the TimeProvider by the plugins, there are a couple of problems with this implementation that make it brittle:

  1. New Version Required - If it is determined that a different implementation of the ITimeProivder is needed, then all 100 call sites will have to be found and updated.
  2. Constructor Signature Dependency - If it is determined that the TimeProvider needs another argument (maybe a system admin org service) then once again, all 100 call sites will need to be found and updated. This may not be as simple as updating the 100 call sites if the new argument isn't currently in scope at the call sites, and if each call site is in a location that is referenced in 10 other locations, then you could potentially have 10,000 locations in code that need to be edited.

The solution to this is to move the logic needed to create an ITimeProvider out of the HolidayEnforcer plugin. This is what we can start to do using the IIocContainer:

V3 IOC To The Rescue

public class HolidayEnforcerV3_Ioc: IPlugin
{
    public const int HolidayMonth = 12;
    public const int HolidayDay = 25;
    public IIocContainer Container;

    public void Execute(IServiceProvider serviceProvider)
    {
        if (Container == null)
        {
            // Please Note, this isn't thread safe and is handled correctly in the DlaB.Xrm.DLaBGenericPluginBase
            // For simplicity the threadsaftey is skipped here.
            Container = new IocContainer();
            Container.RegisterDefaults();
        }

        serviceProvider = Container.BuildServiceProvider(serviceProvider);
        var timeProvider = serviceProvider.Get<ITimeProvider>();
        var userTime = timeProvider.GetUserLocalDateTime();
        if (userTime.Month == HolidayMonth
            && userTime.Day == HolidayDay)
        {
            throw new InvalidPluginExecutionException("It's a holiday.  Stop working!");
        }
    }
}

public static class IIocContainerExtensions {

    public static IIocContainer RegisterDefaults(this IIocContainer container)
    {
        return container?.AddScoped<IOrganizationService>(s =>
            {
                var context = s.Get<IPluginExecutionContext>();
                var factory = s.Get<IOrganizationServiceFactory>();
                return factory.CreateOrganizationService(context.InitiatingUserId);
            })
            .AddScoped<ITimeProvider, TimeProvider>();
    }
}

In this version the IIocContainer is used to register how an IOrganizationService, should be created, and what type of ITimeProvider should be used. The IocContainer will use reflection and determine that the TimeProvider needs an IOrganizationService. It already knows how to create an IOrganizationService, so it will create the instance of the IOrganizationService, and then use that to call the constructor of the TimeProvider to create an ITimeProvider.

Also notice that since the definitions of what types should be used is done outside the Plugin, this means that the plugin no longer has any direct reference/dependency to the TimeProvider. Any number of types can be defined outside of the plugin, and then used within to create any number of other types.

Finally, by utilizing a plugin base class (this final step isn't shown since it's difficult to understand on a wiki page), all of the logic required to create an Ioc IServiceProvider can be hidden from the derived class and reused.

Full Example

You can view a full example plugin class here: https://github.com/daryllabar/XrmUnitTest.Example/blob/master/Xyz.Xrm.Plugin/ServicesExamplePlugin.cs

General Implementation Details

  1. IIocContainer Resposibility - The IIocContainer is responsible for declaring how the interfaces/classes should be created, and it maintains the state of all singleton objects. This means when a new IIocContainer is created, it will not know about any singleton instances created in a previous container. Ideally only one instance of an IIocContainer should be created per application domain. This means on the CRM server, since each registration of a plugin is isolated from every other plugin by running in its own application domain, there should only ever be one instance of an IIocContainer per registration. This is enforced by creating the IIocContainer in the constructor of the plugin of the base class. For Dataverse plugins, no other IIocContainer should be created.
  2. BuildServiceProvider Scope When IIocContainer.BuildServiceProvider is called, it will create an IServiceProvider with its own scope. Since this is called once in the Execute method of the PluginBase, each scoped type defined will be unique per plugin execution.
  3. Auto Registrations If there is no registration defined for a particular type/interface, the default IServiceProvider will be called to see if it can provide an instance of the type/interface. If it can't, and the type is any of the following: interface, abstract, primitive, enum, array, of string, it will not automatically get registered and instantiated but just end up returning null. Else, it is a class and an attempt will be made to auto register the type with the default lifetime (this is defaulted to scoped in the definition of the BuildServiceProvider method), and an instance of the type created. If it is unable to, null will be returned.
  4. Overriding Registrations Registrations can be overridden, with the last registration called being the registration used by the generated service provider. ie
container.AddTransient<ISomething, Foo>();

// Sometime later
container.AddTransient<ISomething, Bar>();

// Type Bar will be created:
var bar = container.BuildServiceProvider().Get<ISomething>();

This allows for plugins that override the RegisterServices method to be able to utilize the defaults and override only what is desired by calling base.RegisterServices(container) before defining any custom registrations, in the logic of the overrides method.

  1. Order of Registrations Doesn't Matter Outside of defining the same registration for the same type more than once, the order in which the types are registered does not matter. ie:
public interface IAmA { }
public interface IDependOnA { }
public class A : IAmA { }
public class DependOnA : IDependOnA {
    public DependOnA(IAmA a) { }
}

container.AddScoped<IAmA, A>()
    .AddScoped<IDependOnA, DependOnA>();
// or
container.AddScoped<IDependOnA, DependOnA>()
    .AddScoped<IAmA, A>();
// Order of registrations does not matter.
  1. Default Type Registrations It is encouraged to define type registrations in an extension class to make them easier to find and utilize, unless it is specific to a specific plugin, in which case the plugin should override the RegisterServices method, call base.RegisterServices(), and then register any plugin specific registrations required.

Making a Class Constructor IOC Friendly

In order to allow a class to be correctly created by the IOC Container, every argument type of the constructor must be uniquely defined. This results in the following common steps to make class constructors IOC friendly:

  1. Remove strings
// BAD: The IOC Container can't tell what string parameter should be passed in
public ExamplePluginRunner(IServiceProvider serviceProvider, string unsecureConfig, string secureConfig)

// GOOD: The ConfigWrapper explicitly defines what strings it expects
public ExamplePluginRunner(IServiceProvider serviceProvider, ConfigWrapper config)

// The creation of this should defined in the plugin base since that is where the config settings are populated:
container.AddSingleton(new ConfigWrapper {
        UnsecureConfig = UnsecureConfig,
        SecureConfig = SecureConfig
    });
  1. If a constructor requires multiple arguments at the same time, create a wrapper class. The ConfigWrapper above is one example, but the one that may require the most work to update is the IOrganziationService:
// BAD: The IOC Container can't tell what IOrganizationService is expected to be created and passed in
public ExampleBL(IOrganizationService userService, IOrganizationService adminService)
public ExampleBL(IOrganizationService service)

// GOOD:
public ExampleBL(IOrganizationServicesWrapper services)

// IOrganizationService
container.AddScoped(s =>
{
    var context = s.Get<IPluginExecutionContext>();
    var factory = s.Get<IOrganizationServiceFactory>();
    return factory.CreateOrganizationService(context.InitiatingUserId);
})

// IOrganizationServicesWrapper
.AddScoped<IOrganizationServicesWrapper>(s =>
{
    var admin = new Lazy<IOrganizationService>(() =>
    {
        var factory = s.Get<IOrganizationServiceFactory>();
        return = s.Get<IOrganizationService>();
    });

    return new OrganizationServicesWrapper(
        new Lazy<IOrganizationService>(s.Get<IOrganizationService>),
        admin,
        new Lazy<IReadOnlyCachedService>(() => new ReadOnlyCachedService(admin.Value)));
})