Like many other .NET developers, we of the UNIFY Solutions product group have been enticed by the promise of platform independent .NET offered by .NET Standard and/or Core. More than simply allowing our product Identity Broker to run on Linux, freeing our products from Windows-based operating environments will allow them to become more flexible and ready for next-generation, cloud-based compute technologies.

To this end we have recently begun the process of re-targeting our core product libraries from .NET 4.5 to .NET Standard 2.0, the basis of modern .NET which all other frameworks are a superset of. While most of this process was simple enough, one component required a more involved migration effort and this will be the topic of this blog post: the API framework.

The API Framework

For UNIFY products, the API framework has the following requirements:

  • is self-hosted
  • support multiple endpoints which can be configured, enabled and disabled independent from the product and each other
  • be run-time configurable to allow users to add/update/remove API endpoints without a service restart
  • accommodate the pluggable and extensible nature of UNIFY products
  • self-document via Swashbuckle, Swagger and Swagger UI

The above requirements were met using .NET 4.5 and ASP.NET as part of the Identity Broker v5.1 release with the API framework which encapsulates the configuration, instantiation and handling of web apps. The trouble with migrating this to .NET Standard, however, comes from the fact that ASP.NET, MVC and OWIN have been merged, rewritten from the ground up and made available as ASP.NET Core, which is a .NET Standard library despite its name.

Self-Hosting a Web Application

The old way of creating a self-hosted web app looked like this:

IDisposable webApp = WebApp.Start(address, (IAppBuilder builder) => { 
    /* Configure builder */ 
})

The new way involves creating an IWebHostBuilder. How this is done depends on what framework you’re actually targeting. If you’re moving to .NET Core proper then you now have the WebHost static class:

IWebHostBuilder builder = WebHost.CreateDefaultBuilder();

If you’re moving to .NET Standard like us, or if you just want more control, the builder must be instantiated manually. The following will achieve the API-relevant equivalent of CreateDefaultBuilder:

IWebHostBuilder builder = new WebHostBuilder()
    .UseKestrel()               // Self-host with Kestrel web server
    .UseIISIntegration();       // Allow for use with IIS

Once you have an IWebHostBuilder, a base URL can be set with the following. Note that this can only be a base URL, scheme, host and port.

builder.UseUrls(baseAddress);

The real meat-and-potatoes of the web hosts configuration comes from the ConfigureServices and Configure methods but there are two options for how to provide these methods. The first, and most widely documented way is with a StartUp class. For example:

public class ExampleStartUp
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Configure services here
    }

    public void Configure(IApplicationBuilder appBuilder)
    {
        // Configure application here
    }
}

which is registered to the IWebHostBuilder with:

builder.StartUp<ExampleStartUp>();

A startUp class has the benefit of dependency injection (see below), but to more easily support the existing configuration pattern in use, we opted for the second, very much less documented option of using the ConfigureServices and Configure extension methods:

builder.ConfigureServices((WebHostBuilderContext context, IServiceCollection services) => { 
    /* Configure services here */ 
})

builder.Configure((IApplicationBuilder) appBuilder => { 
    /* Configure application here */ 
})

Note that multiple ConfigureServices methods can be registered and will execute in order, but only the last Configure method registered will be used.

Once configured, the IWebHost should be built and started:

IWebHost webHost = builder.Build();
webHost.Run()       // Blocking, has Async counterpart
webHost.Start()     // Non-blocking, has Async counterpart

and stopped:

webHost.StopAsync()
webHost.WaitForShutdown()       // Blocks until shutdown event, has Async counterpart

ConfigureServices and Configure

Regardless of how these methods are registered their purpose is the same. What was once done in the function passed to WebApp.Start is now done here. Most middleware configured here require components to be added to the service collection in the ConfigureServices method with an Add___ extension method.

serviceCollection.AddCors()
serviceCollection.AddAuthorization();

Middleware is registered to the applications request pipeline with Use___ extension methods in the Configure method.

appBuilder.UseCors()
appBuilder.UseAuthorization();

The AddMvc Trap

One of the additions to ConfigureServices that most online examples include is this:

serviceCollection.AddMvc();

however this method adds many components to the service collection required for a full MVC application, but unused by an API. Apart from adding unnecessary bloat in a pure API application, the unneeded components may cause unintended behaviour. For us, as we decorate all out API controllers (more below) with AuthorizeAttribute with Roles property set, AddAuthorization being called as part of AddMvc caused these role requirements to be enforced even on otherwise open endpoints.

Lucky, a bare-bones alternative exists in AddMvcCore. For an API application the following is equivalent to AddMvc without any unneeded additions:

IMvcCoreBuilder mvcBuilder = serviceCollection.AddMvcCore();
mvcBuilder.AddJsonFormatters();     // required for serialization
mvcBuilder.AddApiExplorer();        // optional, required for Swashbuckle
mvcBuilder.AddAuthorization();      // optional
mvcBuilder.AddCors()                // optional

Dependency Injection

ASP.NET Core manages pipeline resources with a Dependency Injection container and provides resource dependencies through constructor injection. Resources are registered to the container in the ConfigureServices setup method either directly or with middleware-provided Add___ functions. For a more comprehensive description of ASP.NET Core Dependency Injection see the official documentation.

External objects can also be registered to the DI container, making them available for pipeline resources. We’ve used this technique as a cleaner way of making business logic resources available to controllers. This was previously managed by providing a custom controller construction function to an IDependencyResolver.

Controllers

Our controllers didn’t really require much change during the migration. The most notable alteration was that our controllers all sub-classed System.Web.Http.ApiController which we simply replace with Microsoft.AspNetCore.Mvc.Controller. Microsoft has provided an ApiController compatibility shim in Microsoft.AspNetCore.Mvc.WebApiCompatShim, however as of the time of writing, this package still has .NET 4.5 dependencies. As it turned out, there wasn’t really any functionality the shim provided that we needed and, looking at the source, offered nothing that couldn’t be implemented ourselves if needed.

The only other major change for controllers was the change in return type of controller actions, replacing HttpResponseMessage with an IActionResult implementation. A minimal change, for us, due to the fact that most of our controller actions return DTO types for Swagger compatibility.

Swashbuckle, Swagger and Swagger UI

The implementation details of Swashbuckle has changed to fit ASP.NET Core, however the underlying functionality remains mostly equivalent.

httpConfiguration
    .EnableSwagger((SwaggerDocsConfig config) => {
        // Configure Swagger
    })
    .EnableSwaggerUI((SwaggerUiConfig config) => {
        // Configure Swagger UI
    });

becomes

serviceCollection.AddSwaggerGen((SwaggerGenOptions options) => {
    // Configure Swagger
});

in the ConfigureServices method and

appBuilder.UseSwagger();
appBuilder.UseSwaggerUI((SwaggerUIOptions options) => {
    // Configure Swagger UI
});

in the Configure method. The contents of these configuration functions remain mostly equivalent and required changes are clear enough with the aid of the Swashbuckle documentation.

One issue that did present itself was a change in the way Swashbuckle generated the ‘OperationId’ for controller actions; previously the name of the action, now an amalgamation of the actions route elements. For example, an action at /api/examplecontroller/exampleaction would have an operationId of ‘apiexamplecontrollerexampleaction’. Maybe not a problem for some, but our preferred method of client generation (NSwag, muliple clients from operation ID) uses operationId as function names. Swashbuckle provides an attribute, SwaggerOperationAttribute, which can be used to manually override the operation id, but the idea of requiring this attribute on every action of every controller wasn’t very appealing.

Instead, we corrected the operationId using an operation filter:

public class OperationIdCorrectionFilter : IOperationFilter
{
    public void Apply(Operation operation, OperationFilterContext context)
    {
        if (context.ApiDescription.ActionDescriptor is ControllerActionDescriptor actionDescriptor)
        {
            SwaggerOperationAttribute swaggerOpAttr = actionDescriptor.MethodInfo
                .GetCustomAttributes()
                .OfType<SwaggerOperationAttribute>()
                .SingleOrDefault();

           if (swaggerOpAttr == null || !operation.OperationId.Equals(swaggerOpAttr.OperationId))
               operation.OperationId = actionDescriptor.ActionName;
        }
    }
}

which assigns the operations action name as the operationId unless the SwaggerOperationAttribute is present. At the point the filter executes, the id override will have already been applied so a check for this is performed to preserve the functionality of SwaggerOperationAttrubute.

Wrap Up

Though not without frustration and not exactly straightforward at times, the migration to ASP.NET Core was well-worth the effort. The flexibility of platform independence will provide us with many exciting opportunities going forward, not to mention the benefits like performance and code maintainability. Even so, .NET Core is still in its infancy compared to .NET Framework and it will definitely be an interesting journey as it moves towards maturity.