Practical .NET

Finding Your Objects in ASP.NET Core

Central to ASP.NET Core is the collection of objects that give you access to ASP.NET Core functionality. Here's how to access it, how to add to it, and an example of how to use this technology with the "difficult" cases.

In your existing ASP.NET MVC applications there are objects you must instantiate with local values -- if the client sends your Controller some customer information, you have to instantiate a Customer object in that Controller with the values from the client. There are, however, also objects that you use in multiple Controllers that don't require local values -- your DbContext object (or, really, any repository class), for example.

ASP.NET Core provides a consistent approach to handling the objects in that second category. ASP.NET Core creates a collection (called IServiceCollection) and adds useful objects to that collection (typically in the Startup class in the project's Startup.cs file). You can then pull objects from that collection, as you need them, in the rest of your application. The Startup class even has a special method, cleverly called ConfigureServices, where you're supposed to add the objects that your application's Controllers will need.

The Basics
There are, potentially, a lot of objects in ASP.NET Core that you might want to use: Session, Cache, HttpContext and so on. Fortunately, the ASP.NET Core team has provided a helper method that adds most of the objects that you'll need. The method is called AddMvc and you call it from the IServiceCollection object passed to the ConfigureServices method. If you use the Web Application (Model-View-Controller) template that comes with Visual Studio 2017, you'll get a default implementation of that:

public void ConfigureServices(IServiceCollection services)
{
  services.AddMvc();
}

Some of these objects, however, are sort of special: They need to be instantiated with values that reflect your site (your site's routing rules, for example) and, often, with values that represent the current request. To put it another way, these are objects that need to be up-to-date. That creates some special problems when adding these objects and I'll look at a typical example (the UrlHelper object) at the end of this column.

To retrieve one of these objects, later in your application, you just need to provide a parameter with the object's type in the constructor for your controller. Typically, you'll put that object in a field or property so that it can be used elsewhere in your controller.

The following example shows how a Controller might grab MVC Core's IHostingEnvironment object, which is added to the IServiceCollection by the AddMvc method. Once the constructor is passed the object, as is typical, the code stores the object in a property. The IHostingEnvironment object (among other things) tells you whether you're running in development or production and, in this code, an Action method uses the object to check for that:

public class HomeController 
{
  private IHostingEnvironment Environment { get; };
  public HomeController(IHostingEnivronment Environment)
  {
    this.Environment = Environment;
  }
  public IActionResult GetCustomers()
  {
    if (Environment.IsEnvironment("Development"))
    {

However, if you need multiple objects, the number of parameters to your constructor could get large. An alternative is to accept the IServiceProvider object that gives you access to the services collection. Using IServiceProvider's GetRequiredService method, you can retrieve the individual objects you want.

Rewriting the previous code to use IServiceProvider, gives this version of the Controller:

public class HomeController 
{
  private IHostingEnvironment Environment { get; };
  public HomeController(IServiceProvider sprovider)
  {
    this.Environment = sprovider.GetRequiredService<IHostingEnvironment>();
  }

GetRequiredService will throw an exception if the object isn't found in the services collection. If that bothers you then you can, instead, use GetService which returns null:

this.ShippingCalculator = sprovider.GetService<IShipCalc>();
if (this.ShippingCalculator == null)
{
  ShippingCost = 5.35;

Adding Your Own Objects
As my last example suggests, there's nothing stopping you from adding your own objects to the services collection. To do that, you use one of three methods available from the IServiceCollection class: AddScoped, AddTransient and AddSingleton. The syntax for all three is identical so, assuming you have a class called ShippingCalculator that implements the IShipCalc interface, you'd use code like this to add the class to the collection:

IServiceCollection.AddScoped<IShipCalc, ShippingCalculation>();

The difference between the three methods is apparent when the class passed to your code is instantiated. With AddSingleton your object is created just once and is shared among all requests by all users -- obviously creating concerns with initialization (data left over from previous requests) and thread safety. Of the three methods, AddTransient is the least "concerning" because it creates your object fresh, every time it's requested from the services collection ... but now you're paying for all those recreations. AddScoped provides a middle-ground, creating the object just once in a request for any user (though, if a user might use the object multiple times in a single request, initialization errors can occur).

You can control how your class is instantiated by providing a factory method to any of the Add* methods. The Add* method will pass an IServiceProvider object to your factory method for you to use when creating your object. Part of your code gets a little simpler with this approach because, once you've provided a factory method to the Add* method, the Add* method doesn't need to know what class to instantiate. As a result, you only have to reference the interface when calling the Add* method.

The following example provides a lambda expression to the AddScoped method that creates a new ShippingCalculator object, passing two parameters to the object's constructor. The first parameter required by the ShippingCalculator is a string but the second parameter is ASP.NET's IConfiguration object, so the code retrieves it through the IServiceProvider that the AddScoped method passes to the lambda expression. Now that IServiceCollection doesn't need to know what class to instantiate (that's taken care of by my factory method), you can omit the class name from the call to AddScoped:

services.AddScoped<IShipCalc>(
 (sp) => new ShipCalc("International",
                      sp.GetRequiredService<IConfiguration>())); 

One last note: While defining an interface and having a class implement it is certainly a best practice, it's not necessary. All the Add* methods will accept a class name without an interface name. This code adds the ShippingCalculator to the services collection without referencing an interface:

services.AddScoped<ShippingCalculator>();

And this constructor accepts the instantiated ShippingCalculator object:

public HomeController(ShippingCalculator shipCalculator)
{

An Awkward Case
As an example of how you might use this technology, let's look at MVC Core's UrlHelper class. The UrlHelper class' Action method generates a valid URL for your site when passed an Action name and a Controller name. This code is typical of how you might use a UrlHelper object:

string url = urlHelper.Action("Index", "Home");

In MVC, you could just instantiate a UrlHelper by passing it one of MVC's context objects. The process is more complicated in MVC Core (I imagine you're used to that by now).

The UrlHelper must now be retrieved from a UrlHelperFactory. The good news here is that there's an IUrlHelperFactory object already in the services collection, waiting for you to use it (it's added in the AddMvc method). However, when using the factory's GetUrlHelper method, you must pass the method an ActionContext object ... which is not in the services collection. What is in the service collection is an IActionContextAccessor object that has an ActionContext method that returns the ActionContext object you need.

You can't say I didn't warn you.

However, all this indirection does ensure that your UrlHelper is up-to-date with the current state of the request on whose behalf you're generating URLs. So, if you only wanted to use the UrlHelper in a single Controller, you might add this code to the constructor of the Controller where you need it:

public class HomeController : Controller
{
  IUrlHelper urlHelper;
  public HomeController(IServiceProvider serviceProvider)
  {			
    IActionContextAccessor actionAccess =
                serviceProvider.GetRequiredService<IActionContextAccessor>();
    IUrlHelperFactory factory = serviceProvider.GetRequiredService<IUrlHelperFactory>();

    ActionContext actionContext = actionAccess.ActionContext;
    urlHelper = factory.GetUrlHelper(actionContext);
}

However, if you wanted to use the UrlHelper in multiple Controllers, rather than repeat this code in each Controller, it might make sense to add the UrlHelper to the services collection. The following code is, essentially, my previous code wrapped up in a lambda expression and passed to the AddScoped method. But, because my lambda expression has multiple lines, I've had to add curly braces and the return keyword to make C# happy. To compensate, I've combined some statements, shortened variable names and used the var keyword more freely:

services.AddScoped<IUrlHelper>(sp => {
 var ac = sp.GetRequiredService<IActionContextAccessor>().ActionContext;
 var f = sp.GetRequiredService<IUrlHelperFactory>();
 return f.GetUrlHelper(ac);
});

Now any of my Controllers can retrieve the UrlHelper from the services collection with code like this:

public class HomeController : Controller
{
  IUrlHelper urlHelper;
  public HomeController(IUrlHelper urlHelper)
  {
   this.urlHelper = urlHelper;

That still leaves one question open: What if you're creating some object that other developers will use and you want to give those developers the option of configuring your object? I'll look at that task in a later column. Let's just say that if you can arrange to be paid by the keystroke, you'll be pleased with the process.

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

Featured

Subscribe on YouTube