3

I use IoC (DI) approach and usually have parameters, which are being read from configuration settings (i.e. connection strings, static values etc) by the lowest layer (DB layer etc). What is the best way to do it?

  1. Read directly in this the lowest layer, i.e.:

    string sendGridApiKey = ConfigurationManager.AppSettings["SendGridApiKey"];
    

It works, but need to add also this key to config file of unit test project. Also, assembly depends on configuration file

  1. Read it in the highest layer (i.e. web application) and throw as parameter from the all layers? It will work, but all middle layers will get parameters, which are not used (so, they will be depended on things, which are not used).

Also there is a problem when different implementations of the the lowest layers can require different parameters. I.e. SendMail1 can require SMTP/login/password, but SendMail2 can require only ApiKey, but SendMail1 and SendMail2 should implement the same interface. So, it creates difficulties to use approach #2

Oleg Sh
  • 8,496
  • 17
  • 89
  • 159

2 Answers2

4

Neither approach you've outlined works well - first (read configuration in services) prevents unit testing as you've mentioned, second (pass config from top level) requires knowledge of all possible implementations of each service by top layer.

I like approaches that rely on DI container's knowledge of both configuration storage and types of objects registered for each interface:

  • pass configuration during registration time - i.e. if container supports registering factory methods such factory method can read configuration and than invoke particular constructor of concrete service

    // constructor: publc ConcreteServiceX(int setting1, string setting2)...
    container.RegisterFactory<IServiceX>(
        container => return new ConcreteServiceX(42, ReadSetting("X"));
    
  • register configuration for each of the service in container as class/interface

    // constructor: publc ConcreteServiceX(IConcreteServiceXSettings settings)...
    container.RegisterType<IService,ConcreteServiceX>();
    container.RegisterInstance<IConcreteServiceXSettings>(
         new ConcreteServiceXSettings(42, ReadSetting("X"));
    

Both approaches localize knowledge of configuration system to one place (container configuration) and allow easier unit testing of each service (no dependency on type of configuration storage) as well as higher level objects (no need to know any settings for services).


Note: sample use Unity-like syntax, adopt to container of your choice

Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
2

option 1 starts out as a simpler solution, but pretty soon ends up being difficult for testing, needing references, breaking a pattern around flowing values from highest to lowest layers etc.

The recommended pattern is #2, where the highest layer sends down all dependencies and their values to the lower layers.

Even though you have to pass it down across all layers, your DI engine should help you here in terms of automatic chained resolution.

e.g.

If your controller needs to instantiate a Business Layer class, which needs to instantiate a Repository class, which needs a Connection class, which needs the setting value, you don't need to manually do it at 3 places.

You can define registrations of BL class, Repository class and Connection class separately in the DI engine, and it'll take care of instantiating a controller for you.

It may look tedious, but normally has great benefits in the long run. (in terms of clear contract definition, unit testing, no anti-pattern, isolated concerns etc.)

And if you are really worried about passing it 3 places, there are various options in terms of Factories and Aggregate services. Each have their pros/cons and depend on the DI engine you are using. Let us know if option 2, is absolutely unacceptable.

e.g. Autofac allows you to wrap a lot of constructor parameters into a Single Aggregate service interface, so that Autofac can inject that for you.

Raja Nadar
  • 9,409
  • 2
  • 32
  • 41
  • Thank you for your response. I have added one more paragraph to my question. Read please – Oleg Sh Feb 21 '16 at 00:46
  • 1
    @OlegSh please add the definition of SendMail1 and 2, and the interface. It is normally easy to add specific parameters to the concrete classes.. the constructors can be injected with specific paramaters – Raja Nadar Feb 21 '16 at 00:50
  • 1
    This is generally a pretty good answer for DI/IoC strategies so that you aren't tightly coupling your various components. But those components themselves should be responsible for getting configurations from settings (or however you handle config settings). They could have overloads to take in various parameters, but that assumes that SendMail1 and SendMail2 have the same number of settings. Perhaps sendmail one is active directory and needs a domain and sendmail 2 requires some other method of auth (username // password // token) – Prescott Feb 21 '16 at 00:51
  • @RajaNadar you propose to add parameters to constructor of each implementation of the lowest layers? Hm, then we don't need to throw these parameters thru all levels... – Oleg Sh Feb 21 '16 at 00:57
  • @OlegSh if you setup your dependencies via interfaces, then the DI can take of the concrete instantiation with all the paremeters it needs, without you ever worrying about passing all values across all layer. and as i said, if you still have concerns, please post your SendMail code, and we can help further – Raja Nadar Feb 21 '16 at 01:08
  • @RajaNadar no matter about concrete SendMail implementation, it's abstraction. Just think, that SendMail1 need to have login/password to send mail, SendMail2 need to have api key... – Oleg Sh Feb 21 '16 at 01:15