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.
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.
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
It is true that the builder already has fluent methods -- such as
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
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
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
webHostBuilder .Configure(fun (builder : IApplicationBuilder) -> let services = builder.ApplicationServices let env = services.GetService<IHostingEnvironment>() let config = services.GetService<IConfiguration>() ... )
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!