Search code examples
c#dependency-injectiondependenciesunity-container

How do you avoid passing a DI container in a unidirectional api design?


I have a business layer with business entities designed using Active Record, and a unidirectional api surface. I have two distinct problems:

  • Without complicating the code, how should runtime values be handled, such as passing in an id value from the DAL to a constructed object? Would this be done with parameter overrides?
  • How do create other business entities and pass dependencies if I am not passing the container down as well (making it more of an anti-pattern / service locator)

Product is the root that wraps the container and acts as our application facade, and entry point to the rest of the BAL. The piece I am trying to solve is in Product.FindCustomer and Customer.FindDocument

public class Product
{
    private IUnityContainer container;

    public void RegisterType<T>() ...
    public void RegisterType<TFrom, TTo>() ...

    public Customer FindCustomer(string customerNumber)
    {
        var id = context.Customers
                        .Where(p => p.CustomerNumber == customerNumber)
                        .Select(p => p.Id)
                        .Single();

        var customer = container.Resolve<Customer>(...); // param override?

        customer.Load();

        return customer;
    }
}

public class Customer : BusinessEntity<Data.Customer, Guid>
{
    private readonly IDocumentFileProvider provider;

    public Customer(IDataContext context, IDocumentFileProvider provider) : base(context)
    {
        this.provider = provider;
    }

    public Customer(IDataContext context, IDocumentFileProvider provider, Guid id) : base(context, id)
    {
        this.provider = provider;
    }

    public Document FindDocument(string code)
    {
        var id = context.Documents
                        .Where(p => p.CustomerNumber == customerNumber)
                        .Select(p => p.Id)
                        .Single()

        var document = new Document(context, provider, id); // Here is the issue

        document.Load();

        return document;
    }
}

public class Document : BusinessEntity<Data.Document, Guid>
{
    public Document(IDataContext context, IDocumentFileProvider provider) : base(context)
    {
        this.provider = provider;
    }

    public Document(IDataContext context, IDocumentFileProvider provider, Guid id) : base(context, id)
    {
        this.provider = provider;
    }

    public IDocumentFile GetFile()
    {
        return provider.GetFile();
    }
}

Here is briefly the other classes.

public abstract class ActiveRecord<TEntity, TKey>
{
    protected ActiveRecord(IDataContext context)
    {
    }

    public virtual void Load() ...
    public virtual void Save() ...
    public virtual void Delete() ...
}

public abstract class BusinessEntity<TEntity, TKey> : ActiveRecord<TEntity, TKey>
{
    protected BusinessEntity(IDataContext context) : base(context)
    {
    }

    protected BusinessEntity(IDataContext context, TKey id) : this(context)
    {
    }

    ...
}

The hierarchies can be quite deep, but a shorter example:

var customer = product.FindCustomer("123");
var account  = customer.FindAccount("321");
var document = account.FindDocument("some_code");
var file     = document.GetFile();

One of my goals is to A) model the domain, and B) provide a very easy to understand API. Currently our BAL uses Service Locator, but I am experimenting on replacing that with proper IoC/DI and a container.

The deeper the API, and the more dependencies are needed, all the higher up class constructors can be quite long, and may no longer seem cohesive.


Solution

  • While DI can be squeezed into most application designs using half-measures, the unfortunate truth is that not all application designs are particularly DI friendly. Creating "smart entities" seems like magic when it comes to API design, but the fact of the matter is that at their core they violate the SRP (load and save are separate responsibilities regardless of how you slice it).

    You basically have 4 options:

    1. Find a design that is more conducive of DI and use its object model for your API
    2. Find a design that is more conducive of DI and create a facade object model for your API
    3. Use property injection to load your dependencies and give the end user control over the constructor
    4. Use a service locator

    I ran into a similar wall when trying to use CSLA in conjunction with DI and after many attempts, finally decided that it was CSLA that needed to go and to find a better design approach.

    For a time, I tried using option 3. In this case you can create a facade wrapper around the DI container, and only expose its BuildUp() method through a static accessor. This prevents the use of the container as a service locator.

    [Dependency]
    public ISomeDependency SomeDepenency { get; set; }
    
    public Customer()
    {
        Ioc.BuildUp(this);
    }
    

    Some DI containers can inject properties using fluent configuration instead of attributes (so your business model doesn't need to reference the container), but this can make the DI configuration very complex. Another option is to make build your own attributes.

    Options 1 and 2 would be similar. You basically make every responsibility into its own class and separate your "entities" out into dumb data containers. An approach that works well for this is to use Command Query Segregation.

    public class FindCustomer : IDataQuery<Customer>
    {
        public string CustomerNumber { get; set; }
    }
    
    public class FindCustomerHandler : IQueryHandler<FindCustomer, Customer>
    {
        private readonly DbContext context;
    
        public FindCustomerHandler(DbContext context)
        {
            if (context == null)
                throw new ArgumentNullException("context");
            this.context = context;
        }
    
        public Customer Handle(GetCustomer query)
        {
            return (from customer in context.Customers
                    where customer.CustomerNumber == query.CustomerNumber
                    select new Customer
                    {
                        Id = customer.Id,
                        Name = customer.Name,
                        Addresses = customer.Addresses.Select(a =>
                            new Address
                            {
                                Id = a.Id,
                                Line1 = a.Line1,
                                Line2 = a.Line2,
                                Line3 = a.Line3
                            })
                            .OrderBy(x => x.Id)
                    }).FirstOrDefault(); 
        }
    }
    

    Using option 1, the end user would create an instance of FindCustomer and call queryProcessor.Handle(findCustomer) (the queryProcessor is injected).

    Using option 2, you would then need to create a wrapper API. You could use a fluent builder approach (more info here) to provide logical default dependencies, but allow the end user to call methods to supply their own.

    var customer = new CustomerBuilder().Build(); // defaults
    
    var customer = new CustomerBuilder(c => 
        c.WithSomeDependency(new SomeDependency()).Build(); // overridden dependency
    

    Unfortunately, the main issue with this is that control of the lifetime of objects is no longer up to the DI container, so dependencies like DbContext need special handling.

    Another variant of this would be to make each entity into a humble object that internally builds up its own DI container using the other (loosely coupled) API objects. This is the recommended approach for legacy frameworks (such as web forms) that are difficult to use with DI.

    Finally, there is making a static service locator that all of your API objects use to resolve their dependencies. While this best accomplishes the goal, it is something that should be considered a last resort. The biggest issue is that you lose the ability to quickly and easily understand what dependencies a class requires. So, you are either forced to create (and update) documentation indicating what the dependencies are to the end user, or end users will have to go digging through the source code to find out. Whether using a service locator is acceptable depends on your target audience and how frequent you expect them to need to be able to customize dependencies beyond the defaults. If custom dependencies are a once in a blue moon thing, it may work, but if 25% of your user base needs to add custom dependencies, service locator is probably not the right approach.

    The bottom line is that if maintainability is your main goal, then option 1 is the clear winner. But if you are married to this particular API design, you will need to choose one of the other options and live with the extra maintenance involved in supporting such an API.

    References: