Search code examples
c#unity-containersolid-principles

Inject require object depends on condition in constructor injection


I have an interface

public interface ITrnsitReport
{
    List<UserDefinedType> GetTransitReportData ();
}

And it has only one implementation which is

public class TransitReport : ITrnsitReport
{
    private IValidateInput _inputValidation = null;
    private readonly ITransitRepository _transitRepo = null;
    public TransitReport (IValidateInput inputValidation,
                            ITransitRepository transitRepo)
    {
        _inputValidation = inputValidation;
        _transitRepo = transitRepo;
    }
    public List<UserDefinedType> GetTransitReportData (string input1, string input2)
    {
        List<UserDefinedType> ppdcReportList = null;
        bool isValid = _inputValidation.IsInputValid (input1, input2);
        if (isValid)
        {
            ppdcReportList = _transitRepo.GetTransitData (input1, input2);
            // do something with data
        }
        return ppdcReportList;
    }
}

Now IValidateInput has two implementations such as PPDCValidateInput and PAIValidateInput. Again for ITransitRepository such as PPDCTransitRepository and PAITransitRepository. (where PAI and PPDC are business reports and each has different validation and repository)

I am using Unity Framework and have them registered them to UnityConfig.cs.

New I am trying to do is

public TransitInfo : ITransitInfo
{
    private ITrnsitReport _report = null;
    public TransitInfo (ITrnsitReport report)
    {
        _report = report;
    }
    public List<UserDefinedType> GetReportData (string reportType, string input1, string input2)
    {
        if (reportType.Equals ("PAI"))
        {
            _report.GetTransitReportData (input1, input2); //inject PAIValidateInput and PAITransitRepository objects
        }
        if else (reportType.Equals ("PPDC"))
        {
            _report.GetTransitReportData (input1, input2); //inject PPDCValidateInput and PPDCTransitRepository objects
        }
    }
}

When its "PAI" how can I inject PAIValidateInput and PAITransitRepository objects to TransitReport constructor and for "PPDC" PPDCValidateInput and PPDCTransitRepository objects.


Solution

  • There are several ways to go.

    1. Use named registrations.

    container.RegisterType<IValidateInput, PPDCValidateInput>("PPDC");
    
    container.RegisterType<IValidateInput, PAIValidateInput>("PAI");
    

    And in your constructor:

    public TransitReport ([Dependency("PPDC")]IValidateInput inputValidation,
                            ITransitRepository transitRepo)
    {
        _inputValidation = inputValidation;
        _transitRepo = transitRepo;
    }
    

    Or in your registration:

    container.Register<ITrnsitReport>(new InjectionConstructor(new ResolvedParameter<IValidateInput>("PPDC"));
    

    The downside with this approach is that the consumer of the interface needs to know which implementation of the interface it wants, which kind of breaks the whole idéa of injecting an interface in the first place.


    2. Use an Enum to define what it's validating.

    public interface ITrnsitReport
    {
        List<UserDefinedType> GetTransitReportData ();
        ValidateType ValidateType { get; }
    }
    
    public enum ValidateType
    {
        PPDC = 1,
        PAI = 2
    }
    

    And then select it when you want to use it.

    public TransitReport (IValidateInput[] inputValidation,
                            ITransitRepository transitRepo)
    {
        _inputValidation = inputValidation.FirstOrDefault(x => x.ValidateType == ValidateType.PPC);
        _transitRepo = transitRepo;
    }
    

    The good part is that the logic is inside the interface itself, and not regarding the registration. However, it still requires the consumer of the interface to know what it wants.


    3. Use different interfaces. (Probably the best option in my oppinion)

    public interface IPAIValidateInput : IValidateInput
    {
    
    }
    
    public interface IPPDCValidateInput : IValidateInput
    {
    
    }
    

    And then register them separately in your container and inject the interface you actually want.

    public TransitReport (IPAIValidateInput inputValidation,
                            ITransitRepository transitRepo)
    {
        _inputValidation = inputValidation;
        _transitRepo = transitRepo;
    }
    

    Requires interfaces without no actual declaration. But it keeps the DI-logic more pure, in my oppinion.


    4. Use a base class and combine them all.

    First fix named registrations:

    container.RegisterType<IValidateInput, PPDCValidateInput>("PPDC");
    container.RegisterType<IValidateInput, PAIValidateInput>("PAI");
    container.RegisterType<ITransitRepository, PPDCTransitRepository>("PPDC");
    container.RegisterType<ITransitRepository, PAITransitRepository>("PAI");
    container.RegisterType<ITransitReport, PAITransitReport>("PAI");
    container.RegisterType<ITransitReport, PPDCTransitReport>("PPDC");
    

    Then create a base class

    public abstract class TransitReportBase : ITrnsitReport
    {
        private readonly IValidateInput _inputValidation;
        private readonly ITransitRepository _transitRepo;
    
        protected TransitReportBase(IValidateInput inputValidation,
                                ITransitRepository transitRepo)
        {
            _inputValidation = inputValidation;
            _transitRepo = transitRepo;
        }
    
        public List<UserDefinedType> GetTransitReportData(string input1, string input2)
        {
            List<UserDefinedType> ppdcReportList = null;
            bool isValid = _inputValidation.IsInputValid(input1, input2);
            if (isValid)
            {
                ppdcReportList = _transitRepo.GetTransitData(input1, input2);
                // do something with data
            }
            return ppdcReportList;
        }
    }
    
    public class PAITransitReport : TransitReportBase
    {
    
        public PAITransitReport([Dependency("PAI")] IValidateInput inputValidation,
                                [Dependency("PAI")] ITransitRepository transitRepo) : base(inputValidation, transitRepo)
        {
    
        }
    }
    
    public class PPDCTransitReport : TransitReportBase
    {
        public PPDCTransitReport([Dependency("PPDC")] IValidateInput inputValidation,
                                [Dependency("PPDC")] ITransitRepository transitRepo) : base(inputValidation, transitRepo)
        {
    
        }
    }
    

    And use a Factory to resolve them:

    public class TransitReportFactory : ITransitReportFactory
    {
        private readonly IUnityContainer _container;
    
        public TransitReportFactory(IUnityContainer container) // container is injected automatically.
        {
            _container = container;
        }
    
        ITrnsitReport Create(string reportType)
        {
            return _container.Resolve<ITrnsitReport>(reportType);
        }
    }
    

    Thanks to the concrete classes PPDCTransitReport and PAITransitReport we can make sure that the right dependency is injected to the base class.

    How to use the factory:

    First, register it with Unity.

    container.RegisterType<ITransitReportFactory, TransitReportFactory>();
    

    Then inject it in your TransitInfo.

    public TransitInfo : ITransitInfo
    {
        private ITransitReportFactory _transitReportFactory;
        public TransitInfo (ITransitReportFactory transitReportFactory)
        {
            _transitReportFactory = transitReportFactory;
        }
        public List<UserDefinedType> GetReportData (string reportType, string input1, string input2)
        {
            // Create your transitreport object.
            ITransitReport report = _transitReportFactory.Create(reportType);
            var reportData = report.GetTransitReportData (input1, input2);
            return reportData;
        }
    }
    

    But I have to say that the factory itself doesn't add that much to the solution. If you don't mind the service locator-pattern you can inject IUnityContainer directly to TransitInfo.