Simplify your .NET with custom SDKs

By now we all are most probably building awesome microservices that can save the world from doom. Bootstrapping these microservices should be quick and painless so we can focus on the actual implementation.

By using the .NET Core SDK we can quickly bootstrap an API containing all the goodness we need to get started:

dotnet new webapi -n foo  

The example above will create a Webhost, a Startup configuration, as well as a .csproj file, and oh don't forget that sample ValuesController API endpoint.

If you're creating many web APIs, each with their own host, but with exact same configuration and pipeline components, one could argue that duplicating (and maintaining) the same plumbing code can become problematic.

Creating a custom SDKs

Inside a stock standard ASP.NET Core .csproj file, we will find an Sdk attribute which is pointing to Microsoft.NET.Sdk.Web, which is .NET's standard web dev kit.

From .NET Core 1.0, the new .csproj is much cleaner, but still can contain the same clutter for similar projects. We could solve this problem by creating a NuGet package containing all shared code, or we can create a custom SDK.

As from version 2.1, we are able to utilise the .NET SDK resolver to magically resolve custom SDKs from NuGet packages, given that these packages contain a special Sdk folder with the relevant MSBuild files.

Imagine the following .csproj:

<Project Sdk="Awesome.Sdk" />  

The Awesome.Sdk SDK in the code above extends the default Microsoft.NET.Sdk.Web SDK and can contain all the goodness we might need for a microservice.

When we execute dotnet restore, the .NET SDK resolver will install the a version of the Awesome.Sdk NuGet package, and as a result, bring in all the other required dependencies, including shared code, libraries and tools. Let's investigate the contents of the MSBuild files that makes up the Awesome.Sdk NuGet package:

The Sdk\Sdk.props file:

<?xml version="1.0" encoding="utf-8"?>  
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">  
  <Import Sdk="Microsoft.NET.Sdk.Web" Project="Sdk.targets" />
  <ItemGroup>
    <Compile Include="$(NuGetPackageRoot)\awesome.sdk\1.0.0\*.cs" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Awesome.Core" Version="1.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.3" />
  </ItemGroup>
  <ItemGroup>
    <DotNetCliToolReference Include="Microsoft.DotNet.Watcher.Tools" Version="2.0.0" />
  </ItemGroup>
</Project>  

The definition above includes some compile time *.cs files, references to the Awesome.Core and Microsoft.AspNetCore.All packages and the Microsoft.DotNet.Watcher.Tools CLI tool.

Here's the contents of the AwesomeApp.cs file in the root of the package, which contains code for bootstrapping a Webhost:

using Microsoft.AspNetCore;  
using Microsoft.AspNetCore.Hosting;  
using Microsoft.Extensions.DependencyInjection;  
using System.Reflection;  
using Microsoft.AspNetCore.Builder;  
using Awesome.Core;

namespace Awesome.Sdk  
{
    public class AwesomeApp
    {
        public static void Main(string[] args) => 
            WebHost.CreateDefaultBuilder(args)
                .UseSetting(WebHostDefaults.ApplicationKey, Assembly.GetAssembly(typeof(AwesomeApp)).GetName().Name)
                .ConfigureServices(services =>
                {
                    services
                        .AddAssemblyServices<AwesomeApp>()
                        .AddMvc();
                })
                .Configure(app=>{
                    app.UseMvc();
                })
                .Build()
                .Run();
    }
}

The most interesting bit of the code above is inside the ConfigureServices function. Here we not only resolve the dependencies for all the shared components (like MVC), but also the potential service dependencies that are set within the component assembly.

The AddAssemblyService is a extension method from in the Awesome.Core NuGet package and is responsible for wiring up all the dependencies that are configured in all classes implementing IDependencyContainer:

public static IServiceCollection AddAssemblyServices<T>(this IServiceCollection services)  
        {
            var deps = Assembly.GetAssembly(typeof(T))
                .GetTypes()
                .Where(x => typeof(IDependencyContainer).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract)
                .Select(x => (IDependencyContainer)Activator.CreateInstance(x));

            foreach (var dep in deps)
            {
                dep.ConfigureServices(services);
            }

            return services;
        }

The last thing we need to do is create a .nuspec file that contains the Sdk\Sdk.targets and AwesomeApp.cs files, and run nuget pack to create a NuGet package for our custom SDK.

Using a custom SDK

Assuming that the Awesome.Sdk package is on NuGet or available locally, we can create a new .csproj called microservice.csproj that contains the following:

<Project Sdk="Awesome.Sdk/1.0.0"/>  

The code above will be enough to build and compile a minimal ASP.NET Core application, but it contains no endpoints.

We add a new HelloController.cs file that contains one endpoint for responding with a greeting:

public class HelloController : ControllerBase  
{
   [HttpGet("~/hello")]
   public IActionResult Get([FromServices]IFoo foo) => Ok($"Hello {foo.GetName()}");
}

As you may have noticed, we are depending on an instance of IFoo that needs to be wired to the IServiceCollection of ASP.NET Core.

Application specific dependencies can be registered inside any class that implements IDependencyContainer:

public class Startup : IDependencyContainer  
{
   public void ConfigureServices(IServiceCollection services)
   {
      services.AddSingleton<IFoo, Foo>();
   }
}

After executing dotnet build and dotnet run, the application will have an endpoint at /hello that response with the appropriate greeting.

We can also get rid of the version of the Sdk specified in the .csproj file by creating a global.json file in the root (or any other parent directory) that contains the following:

{
   "msbuild-sdks": {
       "Awesome.Sdk": "1.0.0"
   }
}