Quantcast
Channel: c# – .NET Liberty
Viewing all articles
Browse latest Browse all 10

ASP.NET 5 Startup Using Template Design Pattern

$
0
0

In my previous post on ASP.NET 5 startup logic, we talked about how we could execute different startup logic based on the environment our application is running in. For example, if we are running in Development, we might want to enable verbose error pages to help with tracking down issues. In Production however, we might want to use a less verbose log level so that potentially sensitive user information does not get persisted anywhere.

We talked about a couple of ways of overriding the startup logic per-environment. One way was to define a separate

Configure
method, like
ConfigureDevelopment
, which would be invoked instead of
Configure
when the .NET execution environment detected we were running in Development. Another approach we looked at was defining an entirely separate startup class called
StartupDevelopment
which would be used instead of the regular
Startup
class.

These approaches worked just fine, and they’re probably all I would need for very basic applications. In more complex applications, I might have substantially different startup logic and likely a handful of environments (Desktop, Development, Staging and Production for example). In this case our startup logic can quickly get quite complicated which can make it difficult to keep code organized.

Under our previous approach, our

StartupDevelopment
class had to know to call the appropriate shared methods in its parent class –
Startup
. This is error prone: if I forget to call a shared method where I should have called it, I might get weird behavior in one environment that I wouldn’t necessarily see on my desktop.

main-image

Rather than trusting a number of independent startup classes (

StartupDesktop
,
StartupDevelopment
,
StartupProduction
, etc) to call the appropriate shared methods at the correct time, we can make use of the template method design pattern to to ensure that common startup logic executes properly, and overridden startup logic gets executed at the right time.

In essence: rather than leaving the individual

Startup
child classes to decide what to do, I will keep all the control flow (algorithm) in a base startup template. The startup template will define a handful of hook points that my child startup classes can override to inject small bits of their own logic, while the bulk of the common startup logic remains in the control of the startup template.

Templated Startup in 8 Steps

In this post I’ll take you through how I used a templated startup pattern to override some logging and output options for Development and Production.

1. Create a new ASP.NET 5 Web Application project

00-asp-net-5-web-app

2. Add Logging to HomeController

I want to be able to adjust the log level based on whether my app is running in Development or Production. To test this out, I created a new logger in my

HomeController
and output a debug log entry in the
Index
action. I also threw an exception right after to ensure verbose error pages are only being displayed in Development since it could contain sensitive information.
using System;
using Microsoft.AspNet.Mvc;
using Microsoft.Framework.Logging;

namespace Template.Controllers
{
    public class HomeController : Controller
    {
        private ILogger _logger;

        public HomeController(ILoggerFactory loggerFactory)
        {
            _logger = loggerFactory.CreateLogger(typeof(HomeController).Name);
        }
        public IActionResult Index()
        {
            _logger.LogDebug("Starting Index page.");
            throw new Exception("Intentional");
            return View();
        }

        // ...
    }
}

Note: I didn’t have to inject the logger factory directly into my controller. Instead I could have requested an

ILogger<HomeController>
and let the Logging framework create that for me instead – the outcome would have been the same.

3. Convert Startup.cs to TemplatedStartup.cs

The next thing I needed to do was turn my default

Startup
class into a template. The first step is renaming it to something other than
Startup
so that it doesn’t get executed by ASP.NET 5. I intentionally did not call it
StartupTemplate
: if I had, there would be nothing stopping me from changing my
ASPNET_ENV
environment variable from
Development
to
Template
and having
StartupTemplate
execute directly. Instead, I want to keep
TemplatedStartup
as a base class that should be inherited from.

Another good idea, which I didn’t do in this example, would be to mark

TemplatedStartup
as
abstract
, meaning it cannot be instantiated directly, only a non-abstract child class of
TemplatedStartup
could be instantiated.
namespace Template
{
    public class TemplatedStartup // Used to be Startup
    {
        public TemplatedStartup(IHostingEnvironment env, IApplicationEnvironment appEnv)  // Used to be Startup
        {
            // Setup configuration sources.

            var builder = new ConfigurationBuilder()
                .SetBasePath(appEnv.ApplicationBasePath)
                .AddJsonFile("appsettings.json")
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
            // ...
         }

         // ...
    }
}

4. Template Property – MinimumLogLevel

If we jump down to the

Configure
method of
TemplatedStartup
, we can see the first few lines are responsible for setting log levels:
loggerFactory.MinimumLevel = LogLevel.Information;
loggerFactory.AddConsole();
loggerFactory.AddDebug();

I decided to create a new

protected
(can be seen by child classes)
virtual
(can be overridden by child classes) property to hold the
LogLevel
.
protected virtual LogLevel MinimumLogLevel
{
    get
    {
        return LogLevel.Information;
    }
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.MinimumLevel = MinimumLogLevel;
    loggerFactory.AddConsole(MinimumLogLevel);
    loggerFactory.AddDebug(MinimumLogLevel);
   
    // ...
}

Now rather than using

LogLevel.Information
directly, I pull it from the
MinimumLogLevel
property instead. This will allow me to later override
MinimumLogLevel
in my
StartupDevelopment
class to provide a different level like
LogLevel.Debug
.

Note: I also chose to pass in the minimum log level to

AddConsole
and
AddDebug
 . This is because these two providers will use
LogLevel.Information
by default unless we explicitly tell them to use a different log level. If we had set
MinimumLevel
to
LogLevel.Debug
but left
AddConsole
and
AddDebug
unchanged, we would not see our log entries because they would have been dropped by these providers as they are below the default level of
LogLevel.Information
.

5. Template Method – SetupErrorPages

The next part of the default

Configure
method is to configure error pages. On Development we want rich and verbose logging to help us diagnose development issues. In production, we want to keep that potentially sensitive information private.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    // ...
    
    if (env.IsDevelopment())
    {
        app.UseBrowserLink();
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage(DatabaseErrorPageOptions.ShowAll);
    }
    else
    {
        // Add Error handling middleware which catches all application specific errors and
        // sends the request to the following path or controller action.
        app.UseExceptionHandler("/Home/Error");
    }

    // ...
}

Rather than having this

if
block, I decided to create a template method that defines the default error page behavior. This would allow me to default to the more restrictive
UseExceptionHandler
by default, then override that for my Development environment later on.
protected virtual void SetupErrorPages(IApplicationBuilder app)
{
    app.UseExceptionHandler("/Home/Error");
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.MinimumLevel = MinimumLogLevel;
    loggerFactory.AddConsole(MinimumLogLevel);
    loggerFactory.AddDebug(MinimumLogLevel);

    SetupErrorPages(app);

    // ...
}

6. Add Startup

Since I renamed the default

Startup
to
TemplatedStartup
, I needed to create a new
Startup
class that ASP.NET 5 can execute by default. ASP.NET 5 first looks for a startup class named
Startup{ASPNET_ENV}
(where
ASPNET_ENV
is a convention based environment variable to indicate our environment to the .NET execution environment). If no such class is found, it will fall back to just
Startup
. Here’s that class:
using Microsoft.AspNet.Hosting;
using Microsoft.Dnx.Runtime;

namespace Template
{
    public class Startup : TemplatedStartup
    {
        public Startup(IHostingEnvironment env, IApplicationEnvironment appEnv)
            : base(env, appEnv)
        {

        }
    }
}

In this case, I’m not going to override any of the base properties or methods because I want default settings defined in

TemplatedStartup
to be used.

7. Add StartupDevelopment

sweet-giraffe

Now I want to create a separate startup class that will execute when our environment is set to Development. This will allow me to override the logging level and error page behavior.

using Microsoft.AspNet.Hosting;
using Microsoft.Dnx.Runtime;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Framework.Logging;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Diagnostics.Entity;

namespace Template
{
    public class StartupDevelopment : TemplatedStartup
    {
        public StartupDevelopment(IHostingEnvironment env, IApplicationEnvironment appEnv)
            : base(env, appEnv)
        {
                
        }

        protected override LogLevel MinimumLogLevel
        {
            get
            {
                return LogLevel.Debug;
            }
        }

        protected override void SetupErrorPages(IApplicationBuilder app)
        {
            app.UseBrowserLink();
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage(DatabaseErrorPageOptions.ShowAll);
        }
    }
}

When we are running in Development mode, ASP.NET 5 will use this class. ASP.NET 5 will call into the inherited methods (

ConfigureServices
,
Configure
) from
TemplatedStartup
. When the
Configure
method is called, it will fetch the minimum log level from the property
MinimumLogLevel
. Since I have overridden this in
StartupDevelopment
, it will use the value
LogLevel.Debug
instead of
LogLevel.Information
.

Similarly, when the

Configure
method calls
SetupErrorPages
, it will call into my overridden method instead of the default implementation in
TemplatedStartup
. This gives me a chance to enable browser link (automatic web page reloads), as well as enable verbose developer error pages to show detailed error information.

8. Test

First, let’s confirm that out application is running in Development mode. Right click on the project and go to

Properties
. In the
Debug
tab we should set the value of the
ASPNET_ENV
environment variable set to
Development
. If we run the app we should see our Debug log entry as well as a rich error page from the intentional exception we threw in
HomeController
’s
Index
  action.

02-run-as-development

01-debug-output-dev-mode

03-verbose-error-page

Now we can stop the project and change the value of

ASPNET_ENV
to
Production
and fire it up again. This time we should not see any Debug level log entries in the Debug output window, and we should get a generic error page that doesn’t give us detailed information.

02-change-to-production 04-minimal-error-page

Next Steps

This is just a simple example of how we can use the template method design pattern to structure our startup logic in such a way that all the important common startup logic executes properly, but gives us the flexibility to define small hooks to allow alternative logic to execute for single steps of our startup algorithm. The key here is the overall steps executed during startup is handled by the base

TemplatedStartup
class, which allows only small pieces to be overridden, rather than giving up control of the whole startup process to various classes.

In future posts I’ll talk about how I extended this pattern to do more advanced scenarios like using an in memory EntityFramework 7 provider rather than SQL when running in Development. We can also use this pattern to allow the injection of different services based on our environment. For example, we might have an

IEmailService
for sending emails to users. In development we could override that with an implementation that writes the emails out to disk instead.

Another thing to mention is we don’t have to use the template method design pattern only in our startup class. It is a general design pattern that you’ve probably seen in other applications. Let me know if you’ve used this design pattern in other places and what use cases you’ve found it the most useful for.

 


Viewing all articles
Browse latest Browse all 10

Trending Articles