Practical ASP.NET

Configuring Your Application-Wide Objects in ASP.NET Core

ASP.NET Core's support for sharing objects defined at startup is great ... but what if you need to set options on those objects? Here's a case study that starts off great and then descends into over-engineered madness (but only if you want to go that far).

In a previous article, I showed how to add your own objects to ASP.NET Core's IServiceCollection during your site's startup process (and also how to retrieve those objects when you wanted them, of course). Some objects -- your DbContext object, for example -- you just use "as is." However some objects, especially those that can be used in multiple scenarios, require some configuration before they can be used. Fortunately, ASP.NET Core provides a standard way of setting those configuration options during the startup process so that, when your object is finally created, those options will be applied.

For example, let's say that I have a class called OrderService that manages retrieving information from Orders and that I use that class in several Controllers. To make that class available through the IServiceCollection provided by ASP.NET Core, I would add that class to the collection of available service objects in the Startup class' ConfigureServices method. If I wanted the class to be created new, every time it was requested by a Controller, I would use code like this:

public void ConfigureServices(IServiceCollection services)
{
  services.AddTransient<OrdersService, OrdersService>();

Now, to be handed an instantiated OrdersService object, I would just ask for it in a Controller's constructor and stuff it into a field to be used elsewhere in the Controller. That code might look like this:

public class HomeController : Controller
{
   public OrdersService OrdersService;

   public HomeController(OrdersService ordersService)
   {
      OrdersService = ordersService;
   }

But, it turns out, when this class is finally created, I want to specify what kind of Orders the class will be working with: Expedited, Recent, Archived or All. My first steps would be to set up an enumerated value of the available options called OrderTypes and add a property called OrderType to the OrderService class.

Using a property to hold this option lets me take advantage of the IServiceCollection's Configure method to specify properties to be set when an object in the services collection is created. All I have to do is pass a lambda expression to the Configure method, specifying which properties I want set and the values to use. These two lines of code would add my class to the collection of available service options and configure its OrderType option:

services.AddTransient<OrdersService, OrdersService>();
services.Configure<OrdersService>(options => options.OrderType = OrderTypes.Archived);

I didn't pick using a property to configure this object just because it works well with the Configure method (though that had a lot to do with it). Using a property also lets me override this setting if, for example, I have a Controller that must work with "recent" orders.

Enhancing the Solution
If I was only using this class in one project, I'd probably stop there. However, if the OrderService class was going to be used in multiple projects, I'd want to simplify adding it to a project's IServiceCollection. I would do that by creating an extension method that wraps up those two lines of code and accepts an OrderType (and I'd probably put that extension method in the same file as my OrderService class so that it would be available whenever the OrderService class was used). The class with that extension method would look like this:

public static class OrderServiceExtensions
{
  public static void AddOrderService(this IServiceCollection services, OrderTypes orderType)
  {
     services.AddTransient<OrdersService, OrdersService>();
     services.Configure<OrdersService>(ordersService => orderService.OrderType = orderType);
  }
}

Now, in the ConfigureServices method, when I want to use my OrderServices class, I can just use code like this:

services.AddOrderService(OrderTypes.Archived);

Supporting Multiple Options
And there's nothing wrong with this ... as long as I have only one option to set. If I add more options -- an orders cutoff date, for example -- I'm going to have to keep adding more parameters to my extension method. So, instead, I'll move my options out to a separate class.

My first step in this process is to create that new class with a property for each of my OrdersService options. I can even give this class a constructor that sets default values for those properties.

Again, I'd probably put this class in the same file as my OrderService class:

public class OrderOptions
{
   public OrderTypes OrderType { get; set; }
   public DateTime CutOffDate { get; set; }
   public OrderOptions()
   {
      OrderType = OrderTypes.Recent;
      CutOffDate = DateTime.Now.AddDays(-7);
   }
}

To set these properties, I'll alter my extension method to accept a lambda expression as its second parameter. Developers who want to configure options on my OrdersService expression will put code in that lambda expression to set the properties on the OrdersOption object. In my extension method, therefore, I'll need to pass a configurable OrderOptions object to that lambda expression (the lambda expression doesn't need to return anything so I'll use the Action keyword to declare the expression).

With that change, my extension method would now begin like this:

public static void AddOrderService(this IServiceCollection services, 
             Action<OrderOptions> optionsSetter)
{

Within my extension method I'll pass that lambda expression an OrderOptions object (as I promised when I declared the parameter) when I call the code passed in the expression:

OrderOptions opt= new OrderOptions();
optionsSetter(opt);

After the lambda code finishes running, I'll pass whatever settings were made to my OrderOptions object in the lambda expression to my OrderService object:

services.Configure<OrdersService>(options => {
                options.OrderType = opt.OrderType;
                options.CutoffDate = opt.CutoffDate;
              });

The code in my ConfigureServices method that uses my extension method will now pass a lambda expression that sets properties on the OrderOptions object. That code ends up looking like this:

services.AddOrderService(options => { 
                                      options.OrderType = OrderTypes.Archived;
                                      options.CutoffDate = DateTime.Now;
                                    });

And this is all fine if, perhaps, a little over-engineered. But what if I wanted to be able to set these options from, for example, a configuration file? The good news here is that .NET Core provides a standard way to create options objects like the one I used here, and that process lets me load options from a variety of sources (including configuration files). In fact, the real reason I moved my options into a separate object was to take advantage of that feature. I'll look at that in a later column.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus
Upcoming Events

.NET Insight

Sign up for our newsletter.

Terms and Privacy Policy consent

I agree to this site's Privacy Policy.