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
Configuremethod, like
ConfigureDevelopment, which would be invoked instead of
Configurewhen the .NET execution environment detected we were running in Development. Another approach we looked at was defining an entirely separate startup class called
StartupDevelopmentwhich would be used instead of the regular
Startupclass.
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
StartupDevelopmentclass 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.
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
Startupchild 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
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
HomeControllerand output a debug log entry in the
Indexaction. 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
Startupclass into a template. The first step is renaming it to something other than
Startupso 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_ENVenvironment variable from
Developmentto
Templateand having
StartupTemplateexecute directly. Instead, I want to keep
TemplatedStartupas a base class that should be inherited from.
Another good idea, which I didn’t do in this example, would be to mark
TemplatedStartupas
abstract, meaning it cannot be instantiated directly, only a non-abstract child class of
TemplatedStartupcould 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
Configuremethod 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.Informationdirectly, I pull it from the
MinimumLogLevelproperty instead. This will allow me to later override
MinimumLogLevelin my
StartupDevelopmentclass to provide a different level like
LogLevel.Debug.
Note: I also chose to pass in the minimum log level to
AddConsoleand
AddDebug. This is because these two providers will use
LogLevel.Informationby default unless we explicitly tell them to use a different log level. If we had set
MinimumLevelto
LogLevel.Debugbut left
AddConsoleand
AddDebugunchanged, 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
Configuremethod 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
ifblock, I decided to create a template method that defines the default error page behavior. This would allow me to default to the more restrictive
UseExceptionHandlerby 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
Startupto
TemplatedStartup, I needed to create a new
Startupclass that ASP.NET 5 can execute by default. ASP.NET 5 first looks for a startup class named
Startup{ASPNET_ENV}(where
ASPNET_ENVis 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
TemplatedStartupto be used.
7. Add StartupDevelopment
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
Configuremethod 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.Debuginstead of
LogLevel.Information.
Similarly, when the
Configuremethod 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
Debugtab we should set the value of the
ASPNET_ENVenvironment 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
Indexaction.
Now we can stop the project and change the value of
ASPNET_ENVto
Productionand 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.
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
TemplatedStartupclass, 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
IEmailServicefor 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.