Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to not touch my DI container for every new method in Api Controller when using f#

I am trying to wrap my head around how to handle DI in F# when using WebApi with something like Ninject.

For example, in C# when i wire up my container I would simply tell the DI what the type resolves to, for example:

kernel.Bind<ISomeInterface>().To<SomeClass>();

My Api Controllers will automatically wire this when required by the controllers constructor.

Great, now I can add methods to the interface & class all day long w/o touching the container again.

However in F# (unless i am doing this completely wrong) I create partial applications and then pass them to the controller, every time I add a method i have to wire up again at the container. Maybe this is correct, I am not sure but it seems like a lot more wiring.

To clarify what I mean, lets take a typical REST Api. For each entity with CRUD - so for example:

Customer (Create, Read, Update, Delete).

Would I then have to inject each function into the controller?

So in this example, lets say that i have the service -> domain -> repo model:

let createCustomerFunc = createCustomerDomainFunc createCustomerRepoFunc
let getAllCustomersFunc = getAllCustomerDomainFunc getAllCustomerRepoFunc
let updateCustomerFunc cust = [...]
let deleteCustomerFunc id = [...]
let getSingleCustomerFunc id = [...]

Now in my container when i bind it, i would then do something like:

kernel.Bind<CustomerController>().To<CustomerController>()
.WithConstructorArgument(createCustomerFunc, getAllCustomerFunc, etc...) 
|> ignore

Now if i add method: GetActiveCustomers I would then have to modify my code above to pass in the new partial application?

That feels ... wrong - am i simply approaching this incorrectly?

like image 736
schmoopy Avatar asked Dec 10 '15 00:12

schmoopy


1 Answers

Using a DI Container provides some advantages, as well as some disadvantages.

The major advantage is that, if you have a large code base, you can use convention over configuration to wire up all the dependencies. With convention over configuration, you also get the benefit that your code will have to be more consistent, because it has to follow conventions.

There are several disadvantages, though. The most immediate is that you lose the rapid feedback from the compiler. You can much easier make a change to your system, and while everything compiles, the system fails at run-time. Some people hope that you can ask a DI Container to self-diagnose, but you can't.

Another, less apparent, disadvantage of using a DI Container is that it becomes too easy, as you say, to simply add more members to Controllers, etc. This actually increases coupling, or reduces cohesion, but the Reflection-based automation provided by DI Containers hides this problem from you.

Since I believe that the disadvantages outweigh the advantages, I recommend that you use Pure DI instead of DI Containers.

This goes for Object-Oriented Programming in C#, Visual Basic .NET, or Java, but applies equally to Functional Programming in F#.

In an unmixed Functional F# code base, I wouldn't use classes or interfaces at all; instead, I'd only compose functions together.

In a mixed code base with e.g. ASP.NET Web API, I'd cross the bridge between OOP and FP as quickly as possible. As I explain in my Test-Driven Development with F# talk (expanded material available on Pluralsight), I'd inject a function into a Controller like this:

type ReservationsController(imp) =
    inherit ApiController()
    member this.Post(rendition : ReservationRendition) : IHttpActionResult =
        match imp rendition with
        | Failure(ValidationError msg) -> this.BadRequest msg :> _
        | Failure CapacityExceeded -> this.StatusCode HttpStatusCode.Forbidden :> _
        | Success () -> this.Ok () :> _

That's the entire code base of that Controller. All behaviour is implemented by imp.

In the startup code of the application, imp is composed like this:

let imp =
    Validate.reservationValid
    >> Rop.bind (Capacity.check 10 SqlGateway.getReservedSeats)
    >> Rop.map SqlGateway.saveReservation

You may argue that the above ReservationsController only defines a single Post method. What if a Controller has to expose more methods?

In that case, inject an implementation function per method. In a REST API, any Controller only ought to have 2-3 methods anyway, so that means, in effect, 2-3 dependencies. In my opinion, that's a perfectly acceptable number of dependencies.


The reason for the 2-3 method maximum is that in proper RESTful design, resources tend to follow a few interaction patterns:

  • GET
  • POST
  • POST, GET
  • PUT, GET
  • DELETE, GET
  • PUT, DELETE, GET

The full combination (POST, GET, PUT, DELETE) is a REST design smell, but all of that is an entirely different discussion.

like image 185
Mark Seemann Avatar answered Nov 15 '22 07:11

Mark Seemann