Update 2018-07-06
The content of this post is somewhat diminished by the fact that ASP.NET Core does not support running Configure(...)
multiple times. Instead only the last Configure function will be run.
However, I tweaked my helpers a little bit to keep track of the needed Configure functions separately and only apply them on build. Here is an update example for OAuth with JWT.
type AppBuilder = IApplicationBuilder -> IApplicationBuilder
let setAuth0Auth audKey authKey (host : IWebHostBuilder, configs : AppBuilder list) =
( host
.ConfigureServices(fun (context : WebHostBuilderContext) (services : IServiceCollection) ->
let config = context.Configuration
services
.AddAuthentication(fun options ->
options.DefaultAuthenticateScheme <- JwtBearerDefaults.AuthenticationScheme
options.DefaultChallengeScheme <- JwtBearerDefaults.AuthenticationScheme
)
.AddJwtBearer(fun options ->
options.Audience <- config.[audKey]
options.Authority <- config.[authKey]
)
|> ignore
)
, configs @ [ fun app -> app.UseAuthentication() ]
)
let run (host : IWebHostBuilder, configs : AppBuilder list) =
host
.Configure(fun (app : IApplicationBuilder) ->
configs
|> List.fold (fun x f -> f x) app
|> ignore
)
.Build()
.Run()
Otherwise, the content below is still serviceable.
I have been bashing my head on the keyboard for a few days trying to get an ASP.NET Core 2 web service properly configured to run without using a Startup class. And I finally got it working.
Why?
A Startup class is a pretty egregious mix of concerns and an unnecessary abstraction to take on and unnecessary conventions to commit to memory. Not only that, but using a different approach can actually make it easier to create and maintain service configurations.
How?
I'll start from the end result and work backward. Here is what main
looks like on my service.
[<EntryPoint>]
let main args =
let basePath = Directory.GetCurrentDirectory()
new WebHostBuilder()
|> setConfig basePath args
|> setSerilogLogger
|> removeServerHeader // bye X-Powered-By
|> setAuth0Auth Keys.Audience Keys.Authority
|> setCorsPolicy
|> setRouteHandler MyApi.routes
|> run
0 // exit code
Looking at this code, it is (hopefully) obvious what I am configuring. These are, of course, helper functions which I have to create myself initially. But since these functions are self-contained, they are reusable in other projects. They also allow me to opt-in or out of various features by adding or removing a line. For example, I could replace setSerilogLogger
with setLogaryLogger
.
It is true that the builder already has fluent methods -- such as Configure
and ConfigureServices
. And you could make some of the same claims for them. However, some features are not configured with only a single fluent method. For example, the setAuth0Auth
function has to use a couple of methods internally to properly configure authentication:
let setAuth0Auth audienceKey authorityKey (host : IWebHostBuilder) =
host
.ConfigureServices(fun (context : WebHostBuilderContext) (services : IServiceCollection) ->
let config = context.Configuration
services
.AddAuthentication(fun options ->
options.DefaultAuthenticateScheme <- JwtBearerDefaults.AuthenticationScheme
options.DefaultChallengeScheme <- JwtBearerDefaults.AuthenticationScheme
)
.AddJwtBearer(fun options ->
options.Authority <- config.[authorityKey]
options.Audience <- config.[audienceKey]
)
|> ignore
)
.Configure(fun builder -> builder.UseAuthentication() |> ignore)
Organizing this into its own function encapsulates exactly what needs to be done to setup auth, and only auth. Since it only uses what is passed into it, this function is very reusable for other services. (It is a bit unfortunate that the builder uses mutation, but this is not likely to be an issue since this code only runs on startup.)
The tricky part to figuring out how to create a Startup-less service is accessing things you might need, such as the IConfiguration
and IHostingEnvironment
states. Every bit of advice I found was: "Inject them into a Startup class" 🙃. But it turns out they are pretty easy to find with one of the ConfigureService
overloads. The one with the WebHostBuilderContext
is the key. It has both the configuration and hosting objects as properties. You can see in the example above that I used it to get the IConfiguration
object.
Getting these items from Configure
is a little more challenging because it doesn't have an overload with these as immediate properties. However, you can still find them (and other things) buried in the provided IApplicationBuilder
.
webHostBuilder
.Configure(fun (builder : IApplicationBuilder) ->
let services = builder.ApplicationServices
let env = services.GetService<IHostingEnvironment>()
let config = services.GetService<IConfiguration>()
...
)
Summary
A few helper functions go a long way to taming the complexity of configuring services. And when they are reusable for creating other services, all the better!
Top comments (1)
This is an amazing idea, and something I NEVER even considered looking at. But now that you've pointed it out. I can never un-see it. On Monday, I'm to implement this in c#, and share gist.