Tips and Tricks for NancyFx and .NET Core

In my current assignment, we’re using Nancy on ASP.NET Core and .NET Core 2.0. Nancy for .NET Core is currently in alpha, and since documentation is lacking, and since this is not exactly the “super-duper-happy-path”, I thought I would write down some discoveries.

Major Differences from v. 1

You still define routes in module constructors, but the syntax has changed. Instead of e.g

Get["/products/{id}"] = args => SomeMethod(args.id);

you write e.g.

Get("/products/{id}", args => SomeMethod(args.id));

I usually use async and name my routes, then this becomes:

Get("/products/{id}", async args => await SomeMethodAsync(args.id), name: "GetProduct");

private async Task<dynamic> SomeMethod(string productId) { ... }

Application Settings

With WebAPI, you define an options class, in my case ConnectionStrings, and add this to the Startup class:

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.Configure(Configuration.GetSection("ConnectionStrings"));
    // ...
}

But that is an extension method in OptionsConfigurationServiceCollectionExtensions which only works with WebAPI’s DI container. And the Configuration property in the Startup class is set by the runtime, but Nancy is using another bootsrapper. So the question was how to (1) pass the configuration to the Nancy bootstrapper and (2) how to adapt this for TinyIoC which is used in Nancy by default.

The solution for (1) is to have a Custom bootstrapper and pass the configuration to it’s constructor. To solve (2) I wrote my own extension method for TinyIoC.

public class Startup
{
    // ...
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
            app.UseDeveloperExceptionPage();
        app.UseOwin(x => x.UseNancy(options => options.Bootstrapper = new CustomBootstrapper(_configuration)));
    }
}

public class CustomBootstrapper : DefaultNancyBootstrapper
{
    private readonly IConfiguration _configuration;

    public CustomBootstrapper(IConfiguration configuration)
    {
        _configuration = configuration;
    }

	protected override void ConfigureApplicationContainer(TinyIoCContainer container)
    {
        base.ConfigureApplicationContainer(container);
        container.Configure(_configuration.GetSection("NameOfAppsettingsSection"));
    }
}

internal static class ContainerExtensions
{
    public static void Configure(this TinyIoCContainer container, IConfiguration config) where TOptions : class, new()
    {
        var options = new TOptions();
        config.Bind(options);
        container.Register(options);
    }
}

Then, just add your options class as a parameter to classes where it is needed:

public class MyClass
{
    public MyClass(MyOptions options)
    {
        // ...
    }
    // ...
}

Swagger Generation

Swagger generation with Nancy is not as convenient as I would wish, partly because of the dynamic nature of Nancy, and partly because of the not so well implemented Nancy.Swagger package. In my experience, what works best is to use the builder methods, but it is still not perfect. I use Postman to test my API rather than Swagger.

Testing

Marcus Hammarberg has a series of blog posts that are very useful as an introduction to testing with Nancy. Here is a summary and some additional notes.

A nice feature of Nancy is that it has built-in unit test support, where you can create a “browser” that takes two parameters: a bootstrapper (use ConfigurableBootsrapper to specify the module that is the subject of test, including mocked dependencies) and an action that specifies the browser context to use for all requests. Example:

var browser = new Browser(
    new ConfigurableBootstrapper(configuration =>
    {
        configuration.Module<MyModule>();
        configuration.Dependency(myMock.Object);
    }),
    context => context.Accept("application/json"));

For integration tests, where you want to test all layers, you use your ordinary bootstrapper instead. If it takes an IConfiguration parameter, you have to write code for getting configuration from appsettings.json in the test project. Here is an example of a static helper class:

using Microsoft.Extensions.Configuration;

internal static class ConfigurationHelper
{
    public static IConfiguration Configuration;

    static ConfigurationHelper()
    {
        var configBuilder = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json")
            //.AddJsonFile($"appsettings.{environment}.json", true)
            .AddEnvironmentVariables();
        Configuration = configBuilder.Build();
    }
}

Make sure that appsettings.json has build action Content and Copy if newer. Having an override for the current environment (the commented line above) is not trivial, since the test project is not a ASP.NET Core project. You could use the same environment variable with the following line:

var environment = System.Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");

Error Handling

Global error handling can be setup in the bootstrapper by overriding ApplicationStartup. Example for an API that returns JSON:

protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
{
    base.ApplicationStartup(container, pipelines);
    pipelins.OnError += OnError;
}

private Nance.Response OnError(NancyContext ctx, Exception ex)
{
    _logger.Error(ex, "An unhandled error occurred.");
    var negotiator = ApplicationContainer.Resolve<IResponseNegotiator>();
    return negotiator.NegotiateResponse(new {Error = "Internal error. Please see server log for details."}, ctx);
}

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.