Skip to content
On this page

Environment Checks

The big out of the box feature with Oakton.AspNetCore is the ability to expose environment checks or environment tests directly in your application. Most applications don't live in isolation. Your code probably has to interact with databases, the file system, configuration mechanisms, and other services. And since it's an imperfect world, it's quite possible that you've made a deployment in your career and later found out that configuration items were wrong, your dependencies were down, the database credentials you were using were wrong, or all kinds of sundry run of the mill problems.

Years ago I worked on a system that had worlds of environmental issues during deployments, and after learning about the technique of "environment tests" where you build in automatic checks in your deployment to exercise your system's integrations, we adapted the approach described here: Environment Tests and Self-Diagnosing Configuration.

Flash forward to 2019 (at the time of this release), and Oakton.AspNetCore allows you to build environment checks right into your ASP.Net Core application such that you can verify successful deployments to verify the system configuration quickly rather than waiting for user error reports or testers being blocked because the system is down (which happened to me today as a matter of fact).

Here's a quick start. Let's say that your ASP.Net Core application talks to a single Sql Server application database, and that you expect there to be a connection string in your appsettings.json like so:

{
    "connectionString": "some connection string using integrated security"
}

In a perfect world that just works and every thing is hunky dory. In the real world maybe the configuration is missing, the appsettings.json file didn't get copied over somehow, the service account your application is running under doesn't have access to the configured Sql Server database (this wasn't a random example), or the database just flat out doesn't exist (also not a random example).

To build in an environment check for database connectivity in this situation with Oakton.AspNetCore, get into your normal ASP.Net Core Startup class, and add an environment check in the Startup.ConfigureServices() method like this:

cs
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    // Other registrations we don't care about...
    
    // This extension method is in Oakton.AspNetCore
    services.CheckEnvironment<IConfiguration>("Can connect to the application database", config =>
    {
        var connectionString = config["connectionString"];
        using (var conn = new SqlConnection(connectionString))
        {
            // Just attempt to open the connection. If there's anything
            // wrong here, it's going to throw an exception
            conn.Open();
        }
    });
    
    // Ignore this please;)
    services.AddSingleton<IDescribedSystemPart, Describer1>();
    services.AddSingleton<IDescribedSystemPart, Describer2>();
    services.AddSingleton<IDescribedSystemPart, Describer3>();
}

snippet source | anchor

Now, during deployments or even just pulling down the code to run locally, we can run the environment checks on our application like so:

dotnet run -- check-env

Which in the case of our application above, blows up with output like this because I didn't add configuration for the database in the first place:

Running Environment Checks
   1.) Failed: Can connect to the application database
System.InvalidOperationException: The ConnectionString property has not been initialized.
   at System.Data.SqlClient.SqlConnection.PermissionDemand()
   at System.Data.SqlClient.SqlConnectionFactory.Permissi
onDemand(DbConnection outerConnection)
   at System.Data.ProviderBase.DbConnectionInternal.TryOpenConnectionInternal(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource`1
 retry, DbConnectionOptions userOptions)
   at System.Data.ProviderBase.DbConnectionClosed.TryOpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource`1 retry, 
DbConnectionOptions userOptions)
   at System.Data.SqlClient.SqlConnection.TryOpen(TaskCompletionSource`1 retry)
   at System.Data.SqlClient.SqlConnection.Open()
   at MvcApp.Startup.<>c.<ConfigureServices>b_
_4_0(IConfiguration config) in /Users/jeremydmiller/code/oakton/src/MvcApp/Startup.cs:line 41
   at Oakton.AspNetCore.Environment.EnvironmentCheckExtensions.<>c__DisplayClass2_0`1.<CheckEnvironment>b__0(IServ
iceProvider s, CancellationToken c) in /Users/jeremydmiller/code/oakton/src/Oakton.AspNetCore/Environment/EnvironmentCheckExtensions.cs:line 53
   at Oakton.AspNetCore.Environment.LambdaCheck.Assert(IServiceP
rovider services, CancellationToken cancellation) in /Users/jeremydmiller/code/oakton/src/Oakton.AspNetCore/Environment/LambdaCheck.cs:line 19
   at Oakton.AspNetCore.Environment.EnvironmentChecker.ExecuteAll
EnvironmentChecks(IServiceProvider services, CancellationToken token) in /Users/jeremydmiller/code/oakton/src/Oakton.AspNetCore/Environment/EnvironmentChecker.cs:line 31

If you ran this command during continuous deployment scripts, the command should cause your build to fail when it detects environment problems.

How it works

There's not much to it. Oakton.AspNetCore includes this interface:

cs
/// <summary>
///     Executed during bootstrapping time to carry out environment tests
///     against the application
/// </summary>
public interface IEnvironmentCheck
{
    /// <summary>
    ///     A textual description for command line output that describes
    ///     what is being checked
    /// </summary>
    string Description { get; }

    /// <summary>
    ///     Asserts that the current check is valid. Throw an exception
    ///     to denote a failure
    /// </summary>
    Task Assert(IServiceProvider services, CancellationToken cancellation);
}

snippet source | anchor

Oakton.AspNetCore is looking for service registrations of this interface in your application's IoC container. The CheckEnvironment() extension method above just adds a service registration for the IEnvironmentCheck interface that runs the supplied lambda. When executing the environment checks, Oakton.AspNetCore runs each check one within try/catch blocks. If the execution throws an exception, it's considered to be a failure. Otherwise, it's a passing check and comes out in the output color coded as green. Oakton.AspNetCore assumes that you take care of any necessary resource cleanup yourself.

If you ever want to run the environment checks outside of the command line, you can use the EnvironmentChecker.ExecuteAllEnvironmentChecks(IServiceProvider) method.

Running Environment Checks

To run the environment checks from the command line from your application, use this command:

dotnet run -- check-env

Just like the run command, you can also override the hosting environment or configuration items like so:

dotnet run -- check-env --environment UAT --config:filepath "some file path"

In addition, you can run the environment checks as part of the run command before starting Kestrel. If the environment checks fail, the application doesn't start and the Program.Main() method will throw an exception to fail fast.

In the case of successful environment checks (what you hope happens everytime), you'll see output like this listing off the successful checks. All environment checks are good! will be highlighted in green.

Running Environment Checks
   1.) Success: good
   2.) Success: also good
All environment checks are good!

Just to reiterate that all the normal ASP.Net Core options are available, check out the command line help for the check-env command like so:

dotnet run -- ? check-env

Which gives the following output:

 Usages for 'check-env' (Execute all environment checks against the application)
  check-env [-f, --file <file>] [-e, --environment <environment>] [-v, --verbose] [-l, --log-level <logleve>] [----config:<prop> <value>]

  ------------------------------------------------------------------------------------------------------------------
    Flags
  ------------------------------------------------------------------------------------------------------------------
                  [-f, --file <file>] -> Use to optionally write the results of the environment checks to a file
    [-e, --environment <environment>] -> Use to override the ASP.Net Environment name
                      [-v, --verbose] -> Write out much more information at startup and enables console logging
          [-l, --log-level <logleve>] -> Override the log level
          [----config:<prop> <value>] -> Overwrite individual configuration items
  ------------------------------------------------------------------------------------------------------------------

Adding Environment Checks

The easiest thing to do is to use the extension methods on IServiceCollection shown below:

cs
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    // Other registrations we don't care about...
    
    // This extension method is in Oakton.AspNetCore
    services.CheckEnvironment<IConfiguration>("Can connect to the application database", config =>
    {
        var connectionString = config["connectionString"];
        using (var conn = new SqlConnection(connectionString))
        {
            // Just attempt to open the connection. If there's anything
            // wrong here, it's going to throw an exception
            conn.Open();
        }
    });
    
    // Ignore this please;)
    services.AddSingleton<IDescribedSystemPart, Describer1>();
    services.AddSingleton<IDescribedSystemPart, Describer2>();
    services.AddSingleton<IDescribedSystemPart, Describer3>();
}

snippet source | anchor

There are various overloads of CheckEnvironment() for both synchronous and asynchronous checks, and specialized shortcuts as shown above that let you specify a specific service registered in your IoC container for the check. Otherwise, the overloads take in either Func<IServiceProvider, CancellationToken, Task> for asynchronous checks or Action<IServiceProvider> for synchronous code checks.

Custom Environment Checks

You can create custom, reusable environment checks by implementing the interface below:

cs
/// <summary>
///     Executed during bootstrapping time to carry out environment tests
///     against the application
/// </summary>
public interface IEnvironmentCheck
{
    /// <summary>
    ///     A textual description for command line output that describes
    ///     what is being checked
    /// </summary>
    string Description { get; }

    /// <summary>
    ///     Asserts that the current check is valid. Throw an exception
    ///     to denote a failure
    /// </summary>
    Task Assert(IServiceProvider services, CancellationToken cancellation);
}

snippet source | anchor

And adding your custom type to your service registrations when you configure your IoC container (Startup.ConfigureServices()).

Required Files

There's a specialized, built in check for required files like so:

cs
/// <summary>
///     Issue an environment check for the existence of a named file
/// </summary>
/// <param name="services"></param>
/// <param name="path"></param>
public static void CheckThatFileExists(this IServiceCollection services, string path)
{
    var check = new FileExistsCheck(path);
    services.AddSingleton<IEnvironmentCheck>(check);
}

snippet source | anchor

Required Service Registration

There's also a specialized check to ascertain that a required IoC service registration exists:

cs
/// <summary>
///     Issue an environment check for the registration of a service in the underlying IoC
///     container
/// </summary>
/// <param name="services"></param>
/// <typeparam name="T"></typeparam>
public static void CheckServiceIsRegistered<T>(this IServiceCollection services)
{
    services.CheckEnvironment($"Service {typeof(T).FullName} should be registered", s => s.GetRequiredService<T>());
}

/// <summary>
///     Issue an environment check for the registration of a service in the underlying IoC
///     container
/// </summary>
/// <param name="services"></param>
/// <param name="serviceType"></param>
public static void CheckServiceIsRegistered(this IServiceCollection services, Type serviceType)
{
    services.CheckEnvironment($"Service {serviceType.FullName} should be registered",
        s => s.GetRequiredService(serviceType));
}

snippet source | anchor