DEV Community

loading...

ASP.NET Core 2 web service without a Startup class

kspeakman profile image Kasey Speakman Updated on ・3 min read

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>()
        ...
    )
Enter fullscreen mode Exit fullscreen mode

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!

Discussion (1)

pic
Editor guide
Collapse
pim profile image
Pim Brouwers

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.