SaturnFramework / Saturn

Opinionated, web development framework for F# which implements the server-side, functional MVC pattern
https://saturnframework.org
MIT License
714 stars 108 forks source link

[WIP] Automatic dependency injection for controller #132

Closed Krzysztof-Cieslak closed 5 years ago

Krzysztof-Cieslak commented 6 years ago

Dependency injection is part of .Net Core, and it’s the part that’s hard to ignore. While not idiomatic F# code, you just can’t get access to the framework services (such as ILogger, or IHostingEnv) without DI. Giraffe “solves” this problem by adding GetServixe<‘T> function to HttpContext. However this is rather imperative way of doing things- you need to manually call function for each service you need. And fairly often this is the main activity that’s happening in the controller actions - user just gets those services and passes to the business logic code.

This PRs changes the signature of all controller actions adding additional parameter to it, that represent actions dependencies. Dependencies are modeled as F# type - ripple or record. Framework automatically creates instances of this type calling GetService for ever record field or tuple element, and then pass this instance to the action handler. This enables user to declaratively describe what dependencies action has.

What’s important is that ever action can have its own type for dependencies, meaning it can have its own dependency set.

Opinions and review welcomed.

lmortimer commented 6 years ago

How will Saturn treat things such as database connections or other services which could be made available through D.I, but also work without it? Or phrased another way, will Saturn recommend D.I be used for everything, or only those ASP.NET Core components which require it?

Krzysztof-Cieslak commented 6 years ago

@lmortimer

Or phrased another way, will Saturn recommend D.I be used for everything, or only those ASP.NET Core components which require it

I think (and it may change with time) that we will suggest using it just for framework services. And for composing your normal code users should use normal F# idiomatic ways of handling dependencies. But this is just suggestion - if someone wants to inject IRepository into the action with this mechanism nothing would stop they from doing it. I guess correct usage patterns will emerge in real world usage ;)

OnurGumus commented 6 years ago

I am not sure if dependency injection has a part in functional programming. IMHO dependency injection solves OOPs composition problem. FP doesn't have this problem. We use partial application instead.

andrzejsliwa commented 6 years ago

i wonder if this will be needed if we follow advices from https://www.infoq.com/presentations/mock-fsharp-tdd

Krzysztof-Cieslak commented 6 years ago

Unfortunately the only work by Mark Seemann that ASP.NET team heard about is his DI book. Saturn and Giraffe are both living in the world created for us by ASP.NET Core - we can't change fundamental designs of the framework.For example - you can't get an instance of the ILogger without some kind of service locator / DI - that's exactly what ctx.GetILogger function, that Giraffe provides, do.

As mentioned above - this is not really designed, or suggested to be a general DI mechanism (but it can be if someone really tries) but rather a way to get framework services in more declarative, and up-front manner.

In practice, Saturn controller actions (just like in any other MVC framework used correctly) become the place for some kind of glue code - it's a place where you get dependencies, get model and then pass those services, model and all other required stuff into your pure functional, idiomatic F# code. This solution won't change that, but makes it easier and more declarative which is totally in a spirit of all other F# abstractions. Instead of having set of ctx.getService<'T> calls, you declaratively define set of services that you need from the framework.

NinoFloris commented 6 years ago

Honestly as someone going through the FP struggles of this aspect now, DI isn't all too bad for modularizing codebases.

The only other real 'viable' option for F# is using Reader monad style dependency handling (which is still used a lot in Haskell production codebases due to its 'simplicity' compared to Eff or other more esoteric things). It's not as ergonomic in F# as DI and it's also pretty bad for perf in CLR land due to the endless allocs for closures, Reader binds and the many types of Reader<'T\'T2\'T3> generic instantiations you're going to have to create for the different pieces of your codebase, but it could work if you really want to.

Partial application isn't a true contestant for bigger projects at all as it wrecks your ability to refactor. When, not if, a function needs extra services you are now going to have to find all the places you called or partially applied it and add those extra service arguments, possible having to carry that service all the way up to the place this caller got its services from etc etc. Comparing that to the ease of constructor based DI or even a Reader monad with a record you need to extend and the difference is pretty stark.

For anything better like say PureScript can give with polymorphic records it's all dependent on the language features being there, and they're not there in F#. Has nothing to do with OO or FP, modularizing codebases follows a simple truth and there are only so many ways you can go about it.

EDIT: So I'm all for merging this!

Krzysztof-Cieslak commented 6 years ago

I’m fairly convinced that I like this design. But there is one small thing that worries me a bit - forcing people to add this additional parameter even if they don’t use any dependencies. I wish we already had the overloads for the custom operations :-(

Krzysztof-Cieslak commented 5 years ago

Closed in favour of #164