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 aglobal.json
file in the root (or any other parent directory) that contains the following:
{
"msbuild-sdks": {
"Awesome.Sdk": "1.0.0"
}
}