-
Notifications
You must be signed in to change notification settings - Fork 4
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.
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.
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.
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.
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:
-
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. -
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
:
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.
You can view a full example plugin class here: https://github.com/daryllabar/XrmUnitTest.Example/blob/master/Xyz.Xrm.Plugin/ServicesExamplePlugin.cs
-
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 newIIocContainer
is created, it will not know about any singleton instances created in a previous container. Ideally only one instance of anIIocContainer
should be created per application domain. This means on the Dataverse 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 anIIocContainer
per registration. This is enforced by creating theIIocContainer
in the constructor of the plugin of the base class. For Dataverse plugins, no otherIIocContainer
should be created. -
BuildServiceProvider Scope When
IIocContainer.BuildServiceProvider
is called, it will create anIServiceProvider
with its own scope. Since this is called once in the Execute method of thePluginBase
, each scoped type defined will be unique per plugin execution. -
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. - 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.
- 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.
-
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, callbase.RegisterServices()
, and then register any plugin specific registrations required.
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:
- 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
});
- 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)));
})