-1

I have implemented a sort of Strategy Pattern for implementing a business rule but having trouble with figuring out how to implement it following dependency Injection:

What I have done is I have calculated priority based on

  • Business types (Petroleum, Software Company, Insurance etc..)
  • Revenues (1 million, 2 million, 3 million etc..)
  • and Company Size (Large, Medium, Small)

I need to store this Priority in a database.

Code:

public class Request
    {
        public int Revenue;
        public int CompanySizeId;
        public int BusinessTypeId;
    }

    public interface IRankProvider
    {
        int RankCount { get; }
        int RankIndexOf(Request request);
    }

    public interface IPriorityProvider
    {
        int PriorityOf(Request request);
    }

    public class PriorityProvider : IPriorityProvider
    {
        IRankProvider[] rankProviders;

        public PriorityProvider(params IRankProvider[] rankProviders)
        {
            this.rankProviders = rankProviders;
        }

        public int PriorityOf(Request request)
        {
            int priority = 0;
            foreach (var rp in rankProviders)
                priority = priority * rp.RankCount + rp.RankIndexOf(request);
            return priority + 1;
        }
    }

 public class CompanySize
    {
        public int Id;
        public string Size;
        public int Rank;
    }

    public class CompanySizeRankProvider : IRankProvider
    {
        Dictionary<int, int> indexById;
        public CompanySizeRankProvider(IEnumerable<CompanySize> source)
        {
            indexById = source
            .OrderBy(e => e.Rank)
            .Select((item, index) => new { item, index })
            .ToDictionary(e => e.item.Id, e => e.index);
        }
        public int RankCount { get { return indexById.Count; } }
        public int RankIndexOf(Request request)
        {
            int index;
            if (indexById.TryGetValue(request.CompanySizeId, out index)) return index;
            throw new KeyNotFoundException();
        }
    }

    public class BusinessType
    {
        public int Id;
        public string Title;
        public int Rank;
    }

    public class BusinessTypeRankProvider : IRankProvider
    {
        Dictionary<int, int> indexById;
        public BusinessTypeRankProvider(IEnumerable<BusinessType> source)
        {
            indexById = source
            .OrderBy(e => e.Rank)
            .Select((item, index) => new { item, index })
            .ToDictionary(e => e.item.Id, e => e.index);
        }
        public int RankCount { get { return indexById.Count; } }
        public int RankIndexOf(Request request)
        {
            int index;
            if (indexById.TryGetValue(request.BusinessTypeId, out index)) return index;
            throw new KeyNotFoundException();
        }
    }

    public class Revenue
    {
        public int Id;
        public int LowerLimit;
    }

    public class RevenueRankProvider : IRankProvider
    {
        int[] lowerLimits;
        public RevenueRankProvider(IEnumerable<Revenue> source)
        {
            lowerLimits = source
            .OrderByDescending(e => e.LowerLimit)
            .Select(e => e.LowerLimit)
            .ToArray();
        }
        public int RankCount { get { return lowerLimits.Length + 1; } }
        public int RankIndexOf(Request request)
        {
            int index = 0;
            while (index < lowerLimits.Length && request.Revenue < lowerLimits[index])
                index++;
            return index;
        }
    }

My Controller:

[Route("api/[controller]")]
    [ApiController]
    public class PeopleController : ControllerBase
    {
        IPeopleProvider provider;
        IPriorityProvider _priorityProvider;

        public PeopleController(IPeopleProvider provider, IPriorityProvider priorityProvider)
        {
            this.provider = provider;
            this._priorityProvider = priorityProvider;
        }

 public Person Post()
        {
            // coming from database
            var businessTypes = new BusinessType[]
            {
            new BusinessType { Id = 1, Title = "A", Rank = 1 },
            new BusinessType { Id = 2, Title = "B", Rank = 2 },
            new BusinessType { Id = 3, Title = "C", Rank = 3 },
            new BusinessType { Id = 4, Title = "D", Rank = 4 },
            new BusinessType { Id = 5, Title = "E", Rank = 5 },
            };
            // coming from database
            var companySizes = new CompanySize[]
            {
            new CompanySize { Id = 1, Size = "Large", Rank = 1 },
            new CompanySize { Id = 2, Size = "Medium", Rank = 2 },
            new CompanySize { Id = 3, Size = "Small", Rank = 3 },
            new CompanySize { Id = 4, Size = "Micro", Rank = 4 },
            };
            // coming from database
            var revenues = new Revenue[]
            {
            new Revenue { Id = 1, LowerLimit = 500000001 },
            new Revenue { Id = 2, LowerLimit = 250000001 },
            new Revenue { Id = 3, LowerLimit = 100000001 },
            new Revenue { Id = 4, LowerLimit = 10000001 },
            };
             // I want to avoid new here
            _priorityProvider = new PriorityProvider(
                new RevenueRankProvider(revenues),
                new CompanySizeRankProvider(companySizes),
                new BusinessTypeRankProvider(businessTypes)
                );
            var req = new Request
            {
                Revenue = 12345,
                CompanySizeId = 1,
                BusinessTypeId = 1,
            };

            var priority = _priorityProvider.PriorityOf(req);
            //save priority to Database
}

What I am trying to avoid is this inside my controller method:

 _priorityProvider = new PriorityProvider(
                new RevenueRankProvider(revenues),
                new CompanySizeRankProvider(companySizes),
                new BusinessTypeRankProvider(businessTypes)
                );

So my question is, how do I make this work to follow the whole Dependency Injection process and avoid creating a new instance in the controller?

halfer
  • 19,824
  • 17
  • 99
  • 186
I Love Stackoverflow
  • 6,738
  • 20
  • 97
  • 216
  • @Nkosi I am using Autofac as IOC container. Could you provide an example with improved code with any IOC container please? Goal is to understand the whole dependency injection process. I am creating different different code just to understand what do we consider as injecting as dependency nd what do we consider as directly instantiating inside the class. – I Love Stackoverflow Feb 13 '21 at 01:05

1 Answers1

3

Refactor PriorityProvider

public class PriorityProvider : IPriorityProvider {
    IRankProvider[] rankProviders;

    public PriorityProvider(IEnumerable<IRankProvider> rankProviders) {
        this.rankProviders = rankProviders.ToArray();
    }
    
    //... omitted for brevity
    
}

register all the rank providers and their dependencies with the DI container so they can be injected.

Just doing one for demonstration purposes using .Net Core DI

//Startup

public void ConfigureServices(IServiceCollection services) {

    //...
    
    services.AddScoped<IEnumerable<Revenue>>(sp => {
        // coming from database using service provider above
        // to resolve context and get data
        var revenues = new Revenue[] {
            new Revenue { Id = 1, LowerLimit = 500000001 },
            new Revenue { Id = 2, LowerLimit = 250000001 },
            new Revenue { Id = 3, LowerLimit = 100000001 },
            new Revenue { Id = 4, LowerLimit = 10000001 },
        };
        
        return revenues;
    });
    
    services.AddScoped<IRankProvider, RevenueRankProvider>();
    
    //... Do the same for the other rank providers.
    
    services.AddScoped<IPriorityProvider, PriorityProvider>();
    
    //...
}

Controller thus simplifies to depending on what it explicitly needs to avoid tight coupling to implementation concerns.

[Route("api/[controller]")]
[ApiController]
public class PeopleController : ControllerBase {
    IPeopleProvider provider;
    IPriorityProvider _priorityProvider;

    public PeopleController(IPeopleProvider provider, IPriorityProvider priorityProvider) {
        this.provider = provider;
        this._priorityProvider = priorityProvider;
    }

    public Person Post() {
        
        var req = new Request {
            Revenue = 12345,
            CompanySizeId = 1,
            BusinessTypeId = 1,
        };

        var priority = _priorityProvider.PriorityOf(req);
        
        //save priority to Database
    }
}

By registering multiple providers behind the same abstraction in the composition root, they will all be injected into the controller via the IPriorityProvider (Strategy) because of its dependency on IEnumerbale<IRankProvider>

The IRankProvider implementations however appear to be dependent on run-time data. That topic is too broad for this post so I will defer to this article

Reference Dependency Injection Code Smell: Injecting runtime data into components

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • Thank you so much and upvoted for your kind efforts towards helping me. I am trying to wrap my head around dependency injection where we inject all the dependencies used in the class but I am confused with like whenever we are working on a new functionality so is it like we create the interface for every concrete class just for the sake of unit testing!? This is bit confusing for me. I ll really appreciate if you can shed some lights on this. Do we create interface for every class for DI? – I Love Stackoverflow Feb 13 '21 at 01:12
  • @Learning-Overthinker-Confused that's generally a good practice for the reason you mentioned: testing. That said, there are cases where you don't need interfaces. For example factories are often useful concrete classes to inject. – Kit Feb 13 '21 at 01:37
  • @Kit But the factories vs IOC. When to use what and in what scenarios? Sorry If I am asking stupid questions – I Love Stackoverflow Feb 13 '21 at 01:43
  • 1
    Not stupid at all, but you might try asking separate questions on SO as this would get into extended discussion. DI can coexist with factories and often do. For example, I could inject a DbContextFactory into a controller and then use that factory to create a DbContext on every request. – Kit Feb 13 '21 at 01:48