Dependency Injection for Plugins Part 2 – Make dependencies Easy

In our first session, we demystified Dependency Injection (DI) by comparing it to a strategic cooking experience. Like picking the right ingredients for your lasagna, DI allows you to select and swap components in your code without fuss. We equated different DI methods to kitchen practices: constructor injection as prepping ingredients, setter injection as taste-adjusting, method injection as adding spices at the right moment, and so forth. We rounded out with the best practices to ensure your DI strategy keeps your codebase organized and efficient, much like a professional chef’s kitchen. This time we take a look at Dependency Injection for Plugins.

Crafting a Simple IoC Container Using Constructor Injection

In this part, we’ll roll up our sleeves and craft a basic Inversion of Control (IoC) container from scratch. It might sound hard, but it’s more like making a homemade pasta sauce rather than buying store-bought—simple, yet satisfyingly clear on what goes in. We’ll focus on constructor injection, a straightforward yet powerful pattern. Why constructor injection? Because it’s like having all your ingredients measured and ready before you start cooking. It ensures that as soon as an object is created, it has everything it needs to perform its duties. No halfway surprises. This part of the series will be long and contains lots of code.

Building our own IoC container will illuminate the behind-the-scenes of DI. It’s a journey into the core of DI frameworks, revealing the essence of what happens when we ask for a new instance of a class and how it magically comes with all the necessary dependencies neatly wired up. This hands-on approach is aimed at demystifying the automatic DI processes that can seem obscure at times, providing clarity on how dependencies are managed and served up.

IMPORTANT
Before we illustrate how to build a basic IoC container using constructor injection, a brief but crucial heads-up: the version we’re crafting here is simplified for understanding the principles—it’s not fortified for the rigors of a production environment. Think of it as a conceptual prototype, helpful for learning but not yet ready for real-world deployment. In another part we will tackle those issues and create a production-ready IoC container.

Understanding IoC Containers

Think of an IoC container as a kitchen organizer, making sure every tool and ingredient is ready for the chef. Just like this organizer helps in the kitchen, an IoC container keeps track of all the components in your software. When you need to make a part of your application work, you don’t have to gather every piece; the IoC container has already figured out what you need and where to find it. It’s like having everything prepped for cooking a dish. This way, you can concentrate on the main task of coding without being bogged down by the setup details.

Implementing a Basic IoC Container

Setting up the Container

To get started with a basic IoC (Inversion of Control) container for your Dataverse plugins, let’s focus on a key component: the DataverseContainer class. This class is crucial for handling your plugins’ dependencies, which helps make your code easier to maintain and test. We’ll begin by creating a new class called DataverseContainer. Typically, you’d also create an interface named IDataverseContainer, but to keep things straightforward, we’ll head straight to the most interesting parts.

Initialization
public void Initialize(Assembly assembly, string namespacePath)
{
    var interfaceTypes = this.GetInterfacesFromAssemblyInNamespace(assembly, namespacePath);

    foreach (var type in interfaceTypes)
    {
        var implementation = this.GetInterfaceImplementations(assembly, type).FirstOrDefault();

        if (implementation != null)
        {
            _registrations.Add(type, implementation);
        }
    }
}

public Type[] GetInterfacesFromAssemblyInNamespace(Assembly assembly, string filter)
{
    return assembly.GetTypes()
        .Where(t => t.Namespace != null && t.Namespace.StartsWith(filter) && t.IsInterface)
        .ToArray();
}

public IEnumerable<Type> GetInterfaceImplementations(Assembly assembly, Type type)
{
    if (!type.IsInterface)
    {
        throw new ArgumentException("Type must be an interface.", nameof(type));
    }

    return assembly.GetTypes()
        .Where(t => type.IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract)
        .ToList();
}

The Initialize method is where you populate the _registrations dictionary. It takes an Assembly and a namespacePath as parameters, then filters all types within the assembly to find interfaces that match the namespace path. For each interface found, it then seeks the first implementation and registers this pair in the dictionary. At this point we simply assume that there is always only one implementation.

With this we already do a lot of the heavy lifting. We collect all interfaces that might be used when executing a plugin with their respected implementations.

Registering Dependencies
public void Add<TKey, T>()
{
    var keyType = typeof(TKey);
    var valueType = typeof(T);

    if (!_registrations.ContainsKey(keyType))
    {
        _registrations.Add(keyType, valueType);
    }
}

public void Add<TKey>(object instance)
{
    var type = typeof(TKey);

    if (!_instances.ContainsKey(type))
    {
        _instances.Add(type, instance);
    }
}

There are two Add methods for registration. The first adds a type mapping, while the second registers an existing instance. This is very important for instances Dataverse provides, like the IOrganizationService. They ensure no duplicate registrations for the same key type. For simplicity, we assume that we never want to change the implementations of types already registered, though this could be a requirement in the real world. For example, you might need to serve different logic based on the user’s location, region, or language, just to name a few scenarios.

Dependency Resolution

We follow up with a easy one!

public T Get<T>()
{
    var type = typeof(T);
    return (T)Create(type);
}

The Get<T> method kicks off the resolution process. It determines what type you’re asking for and delegates to the Create method to actually construct the object. You might wonder why there is a method with just two lines of code. The answer will be clear in the next section, which deals with recursion!

private object Create(Type key)
{
    if (_instances.TryGetValue(key, out var singleton))
    {
        return singleton;
    }

    if (!_registrations.ContainsKey(key))
    {
        throw new KeyNotFoundException($"There is no '{key.Name}' or instance registered as a dependency");
    }

    var type = _registrations[key];
    var constructor = type.GetConstructors().OrderByDescending(x => x.GetParameters().Length).First();
    var parameterInstances = constructor.GetParameters().Select(x => Create(x.ParameterType)).ToArray();
    var instance = Activator.CreateInstance(type, parameterInstances);

    return instance;
}

The Create method is where instances are either retrieved or constructed. It looks for an existing instance in _instances. If not found, it locates the type to be created in _registrations, selects the most suitable constructor, resolves its parameters (which may recursively call Create again for each parameter), and uses Activator.CreateInstance to create an instance of the type with the resolved dependencies.

With this setup, the DataverseContainer acts as a foundational IoC container that manages object creation and dependencies. It lays out a pattern for reducing coupling between components and enhances testability by allowing dependencies to be swapped easily, whether for mocking in tests or changing implementations in different environments.

Let’s visualize an example. Imagine we have a Service class whose constructor expects two parameters: one of type RepositoryA and the other of type RepositoryB. Additionally, RepositoryB requires an IDateService. When we call dataverseContainer.Get<IService>(), the IoC container automatically resolves the dependencies in the following sequence:

This DataverseContainer is a simple yet functional example of how an IoC container operates. It takes care of the object lifecycle and dependencies transparently, so your plugins can seamlessly integrate with each other without tightly coupling their constructions. Remember, while this container illustrates the principles well, it’s not suitable for production use due to its simplistic nature and lack of advanced features like lifecycle management and error handling, which are present in professional IoC containers.

Leveraging IoC Container in Dataverse Plugins

When developing plugins for Dataverse, it’s common to encounter boilerplate code that wires up dependencies manually. By introducing an IoC container into the mix, we can streamline this process and make our plugins more adaptable to change.

Traditionally, a plugin receives an IServiceProvider that provides access to various services needed by the plugin to execute. In the classic PluginBase implementation, we’d directly create an instance of ILocalPluginContext. However, we’ve shifted towards a more modern approach where the plugin’s Execute method doesn’t instantiate dependencies directly. Instead, it delegates this responsibility to a custom IoC container, DataverseContainer.

Here’s a step-by-step rundown of the changes:

1. Modify the PluginBase Class:

We modified the ExecuteDataversePlugin method signature to accept an IDataverseContainer instead of ILocalPluginContext. This alteration is pivotal as it allows us to inject dependencies through the container rather than relying on the context directly.

protected virtual void ExecuteDataversePlugin(IDataverseContainer dataverseContainer)
{
    // Do nothing.
}
2. Initialize the Container:

Inside the IPlugin.Execute method, rather than instantiating localPluginContext, we now create and initialize the dataverseContainer with the necessary types and instances specific to our plugin’s operation.

public void Execute(IServiceProvider serviceProvider)
{
    // Check if the service provider is not null
    if (serviceProvider == null)
    {
        throw new InvalidPluginExecutionException(nameof(serviceProvider));
    }

    // Create and initialize the Dataverse container
    IDataverseContainer dataverseContainer = CreateDataverseContainer(serviceProvider);
    dataverseContainer.Initialize(this.GetType().Assembly, "Plugins.Interfaces");

    var tracingService = dataverseContainer.Get<ITracingService>();
    var pluginExecutionContext = dataverseContainer.Get<IPluginExecutionContext>();

    // Trace the execution information
    tracingService.Trace($"Entered {PluginClassName}.Execute() " +
                         $"Correlation Id: {pluginExecutionContext.CorrelationId}, " +
                         $"Initiating User: {pluginExecutionContext.InitiatingUserId}");

    try
    {
        // Invoke the custom implementation of the plugin
        ExecuteDataversePlugin(dataverseContainer);

        // Early return to guard against multiple executions
        return;
    }
    catch (FaultException<OrganizationServiceFault> orgServiceFault)
    {
        tracingService.Trace($"Exception: {orgServiceFault.ToString()}");

        throw new InvalidPluginExecutionException(
            $"OrganizationServiceFault: {orgServiceFault.Message}", orgServiceFault);
    }
    finally
    {
        // Trace the exit point of the execution
        tracingService.Trace($"Exiting {PluginClassName}.Execute()");
    }
}
3. CreateDataverseContainer Method:

This method is responsible for creating and setting up our DataverseContainer. It takes the IServiceProvider and registers various services that our plugin may require.

private IDataverseContainer CreateDataverseContainer(IServiceProvider serviceProvider)
{
    if (serviceProvider == null)
    {
        throw new InvalidPluginExecutionException(nameof(serviceProvider));
    }
    
    var pluginExecutionContext = serviceProvider.Get<IPluginExecutionContext>();
    var container = new DataverseContainer();
    
    // Add various services to the container. Add more if needed.
    container.Add<ILogger>(serviceProvider.Get<ILogger>());
    container.Add<IPluginExecutionContext>(pluginExecutionContext);
    container.Add<ITracingService>(new LocalTracingService(serviceProvider));
    container.Add<IOrganizationServiceFactory>(serviceProvider.Get<IOrganizationServiceFactory>());

    container.Add<IServiceEndpointNotificationService>(
        serviceProvider.Get<IServiceEndpointNotificationService>());

    container.Add<IOrganizationService>(
        serviceProvider.GetOrganizationService(pluginExecutionContext.InitiatingUserId));

    return container;
}

This method initializes a DataverseContainer with various services required for the plugin’s execution. Each service is added to the container with a specific type, which ensures that the correct instance is retrieved when the plugin is executed. Here you can also add additional services you might need. For example for an external API.

Create a Plugin with our IoC Container

In Dataverse, plugins are powerful custom code assemblies that run in response to events fired by various platform operations, such as creating or updating records. These plugins can be thought of as event handlers, allowing developers to inject custom business logic into the platform’s standard processing. Let’s work on a very basic example on how we can leverage our reated dependency injection.

public class OnPreUpdate : PluginBase
{
    public OnPreUpdate(string unsecureConfiguration, string secureConfiguration)
        : base(typeof(OnPreUpdate))
    {
        // TODO: Implement your custom configuration handling
    }

    protected override void ExecuteDataversePlugin(IDataverseContainer dataverseContainer)
    {
        var context = dataverseContainer.Get<IPluginExecutionContext>();
        var target = context.InputParameters["Target"] as Entity;
        var contact = target.ToEntity<Contact>();
        var controller = dataverseContainer.Get<IContactController>();

        controller.OnPreUpdate(contact);
    }
}

We take the dataverseContainer given to us by the PluginBase to get the services we need. First, we look for the ‘IPluginExecutionContext’ service. We need this service to work with the target, which, in our example, is a contact record. Then, we find a ‘IContactController’ to run our OnPreUpdate process. Just a heads up: this example is keeping things simple. We’re not showing what to do if things go wrong, like handling errors. That would make it more complicated. Next, we’ll explore how the IContactController works.

namespace Plugins.Controllers
{
    public class ContactController : IContactController
    {
        private readonly ICorporateDomainsDataAccess _corporateDomainsDataAccess;
        
        public ContactController(ICorporateDomainsDataAccess corporateDomainsDataAccess)
        {
            _corporateDomainsDataAccess = corporateDomainsDataAccess;
        }
        
        public void OnPreUpdate(Contact contact)
        {
            if (!contact.Attributes.ContainsKey(Contact.Fields.EmailAddress1))
            {
                return;
            }

            var domain = contact.EmailAddress1.Split('@').LastOrDefault();
            
            if (_corporateDomainsDataAccess.CorporateDomainExists(domain))
            {
                contact.CR5Ad_IsCorporate = true;
            }
        }
    }
}

To let our IoC container work its magic, we just have to tell it what we need. This class needs to get data from the CorporateDomain entity, and it uses something called ICorporateDomainsDataAccess to do that. The IoC container will set this up for us automatically. All we have to do is say we need it in the constructor and keep it in a variable. Up next, we’ll look at how ICorporateDomainsDataAccess is actually made.

namespace Plugins.DataAccess
{
    public class CorporateDomainDataAccess : ICorporateDomainsDataAccess
    {
        private readonly IOrganizationService _service;

        public CorporateDomainDataAccess(IOrganizationService service)
        {
            _service = service;
        }

        // Assumes "corporate_domain" entity with "domain_name" attribute.
        public bool CorporateDomainExists(string name)
        {
            var query = new QueryExpression("cr5ad_corporate_domain")
            {
                ColumnSet = new ColumnSet("cr5ad_name"),
                Criteria = new FilterExpression
                {
                    Conditions = { new ConditionExpression("cr5ad_name", ConditionOperator.Equal, name) }
                }
            };

            var result = _service.RetrieveMultiple(query);
            return result.Entities.Any();
        }
    }
}

Let’s not worry about the query we talked about before. What we should focus on is that we can ask for IOrganizationService right in the constructor. This means that the IoC container – a special tool we use – can figure out and set up the IOrganizationService and any other parts we made ourselves, as long as we tell it to. To make sure you have all the information, I’ll show you the interfaces we’re talking about.

namespace Plugins.Interfaces
{
    // Interface defining operations for a contact controller
    public interface IContactController
    {
        // Method to perform actions before a contact is updated
        void OnPreUpdate(Contact contact);
    }

    // Interface defining data access operations for corporate domains
    public interface ICorporateDomainsDataAccess
    {
        // Method to check if a corporate domain exists
        bool CorporateDomainExists(string name);
    }
}

Conclusion

As we come to the close of our exploration into the world of dependency injection within the context of Dataverse plugins, we’ve journeyed through the theory and put it into practice with a fundamental example of constructor injection.

Through the lines of code we’ve dissected, and the working example we’ve presented, we’ve seen how dependency injection can simplify the management of dependencies within our plugins. It allows us to write cleaner, more maintainable code, which is easier to test and scale.

Remember, what we’ve covered here is just the tip of the iceberg. Dependency injection is a powerful pattern with many nuances and advanced scenarios that can further enhance your applications. Our basic example serves as a stepping stone into this expansive topic, providing a solid foundation that you can build upon.

As developers, we strive for elegance and efficiency, and incorporating dependency injection into your Dataverse plugins is a step in that direction. Keep experimenting, keep learning, and let this be the starting point for creating more robust and sophisticated applications within the Dataverse ecosystem.

What’s next

Next up, we’ll show you how to use the constructor injection we created to test single methods by themselves. You won’t need large tools like XrmMockup or XrmFakeEasy for this. Instead, we’ll use simple mocks to check that every part of your code works just right. Stay tuned for easy-to-follow examples that will help you make sure your plugin methods are solid and trustworthy.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Navigate
%d bloggers like this: