Jasper’s Configuration Story

A couple dozen times I’ve sat around a table at some kind of software conference swapping software horror stories with other developers, all of us trying to one up each other. The winning story usually ends up being some kind of twisted usage of stored procedures to build dynamic websites or something just as berserk. Just behind misuse of sprocs is stories of naive teams that hard coded some kind of value like a database connection string, a file path, or an email address that should really be in some kind of external configuration source. If you’re going to use Jasper to build applications, this post is all about how to avoid being the subject of one of those horror stories by using .Net configuration with Jasper.

This is meant to be the start of a series of related blog posts on Jasper that lead up to how we intend to support durable messaging within an ASP.Net Core application without forcing you to use distributed transactions. Not promising when they’ll be done, but I’m planning:

  1. Jasper’s Configuration Story (this one)
  2. Jasper’s Extension Model
  3. Integrating Marten into Jasper Applications
  4. Durable Messaging in Jasper
  5. Integrating Jasper into ASP.Net Core Applications
  6. Jasper’s HTTP Transport
  7. Jasper’s “Outbox” Support within ASP.Net Core Applications

I think this will be the longest post by far.

When I published my blog post introducing Jasper a couple weeks ago, I used this sample code to show a minimal Jasper application that would listen for incoming messages at port 2601:

    class Program
    {
        static int Main(string[] args)
        {
            return JasperAgent.Run(args, _ =>
            {
                _.Logging.UseConsoleLogging = true;

                _.Transports.LightweightListenerAt(2601);
            });
        }
    }

I wrote that sample purposely to be as simple as possible, but that led to the obvious question about how to pull the port number from configuration from folks reading the code sample. As of now, Jasper gives you a couple options. The first option is to use our integration with the configuration support from ASP.Net Core.

First though, I’m going to move the application bootstrapping code above into a JasperRegistry class (think Startup classes in ASP.Net Core applications) like this:

public class PongerApp : JasperRegistry
{
    public PongerApp()
    {
        Logging.UseConsoleLogging = true;
        Transports.LightweightListenerAt(2601);
    }
}

and then that application would be bootstrapped with code like this:

class Program
{
    static int Main(string[] args)
    {
        return JasperAgent.Run(args);
    }
}

Great, now let’s pull the port number from configuration, in this case from environment variables:

public class PongerApp : JasperRegistry
{
    public PongerApp()
    {
        // Declare what configuration sources we want
        Configuration
            .AddEnvironmentVariables()
            .AddJsonFile("ponger.settings.json");

        // Use the final product of the configuration root data
        // to configure the application
        Settings.WithConfig(root =>
        {
            var port = root.GetValue("INCOMING_PORT");
            Transports.LightweightListenerAt(port);
        });
    }
}

A couple quick things to note:

  1. JasperRegistry.Configuration is a ConfigurationBuilder object from ASP.Net Core
  2. The root argument to the nested closure in Settings.WithConfig() is the IConfigurationRoot object compiled from the JasperRegistry.Configuration object.

It’s admittedly a little cumbersome with the nested closure in the call to Settings.WithConfig(), but doing it that way helps Jasper order operations for you so that all additions to the ConfigurationBuilder are calculated once before you execute the nested closure. In the FubuMVC days I called the “Mongolian BBQ” approach where you declare the ingredients you want and let’s the framework figure out how to do things in order for you just like the cook at a Mongolian BBQ restaurant.

This is maybe the simplest conceptual way to add external configuration, but there are a couple other options in Jasper.

Uri Aliases

We’ve been using this code to direct Jasper to listen for messages at a certain port with its built in TCP transport in its “fire and forget” mode:

public class PongerApp : JasperRegistry
{
    public PongerApp()
    {
        Transports.LightweightListenerAt(2601);
    }
}

That code is just syntactical sugar for this code that instead supplies a meaningful Uri:

public class PongerApp : JasperRegistry
{
    public PongerApp()
    {
        Transports.ListenForMessagesFrom("tcp://localhost:2601");
    }
}

Now, let’s move the Uri for where and how the application should listen for incoming messages to an application json configuration file like this one:

{
    "incoming": "tcp://localhost:2601"
}

Finally, we can use Jasper’s concept of Uri aliases to configure the application like this:

public class PongerApp : JasperRegistry
{
    public PongerApp()
    {
        Configuration
            .AddJsonFile("ponger.settings.json");
        
        Transports.ListenForMessagesFrom("config://incoming");
    }
}

At runtime, Jasper will translate the Uri “config://incoming” to “the Uri string in the configuration with the key ‘incoming.'” There’s a couple other things to note about Uri aliases:

  • The aliasing strategies are pluggable, but the “config” option is the only one that comes out of the box. We also have an option to use Consul’s key/value storage in the Jasper.Consul extension.
  • The aliases are valid in any place where you use a Uri to send or route messages

Strong Typed Configuration

I’m a big fan of strong typed configuration going back to the “Settings” model in FubuMVC, and it’s even part of ASP.Net Core proper with their Options model. With quite a bit of help from my colleague Mark Wuthrich, Jasper brings the old Settings model forward with some lessons learned type improvements.

Using Jasper’s strong typed configuration, you can do this instead:

public class PongerSettings
{
    public Uri Incoming { get; set; }
}

public class PongerApp : JasperRegistry
{
    public PongerApp()
    {
        Configuration
            .AddJsonFile("ponger.settings.json");

        Settings.With(settings =>
        {
            Transports.ListenForMessagesFrom(settings.Incoming);
        });
    }
}

where the “ponger.settings.json” file would look something like:

{ "incoming": "tcp://localhost:2601" }

I like the Settings model personally because of the traceability between configuration elements and the places in the code that consumes those elements. I wouldn’t bother with the Settings model in this simple of an application, but it might make sense in a bigger application. Do note that the Settings objects are part of the underlying IoC registrations and are available to be injected into any kind of message or HTTP handler class in your application code.

It’s Just Code, Do Whatever You Want

Don’t like any of the options that Jasper provides out of the box? No worries, the JasperRegistry configuration is an example of an “internal DSL,” meaning that it’s fair game to use any kind of .Net code you want like maybe this:

public class PongerApp : JasperRegistry
{
    public PongerApp()
    {
        // Programmatically find the incoming Uri any way you want
        // with your own code
        string uri = Environment.GetEnvironmentVariable("PONGER_INCOMING_URI");
        Transports.ListenForMessagesFrom(uri);
    }
}

One thought on “Jasper’s Configuration Story

Leave a comment