DotNetCurry Logo

ASP.NET Core 2.0 – What’s New

Posted by: Daniel Jimenez Garcia , on 11/27/2017, in Category ASP.NET
Views: 7015
Abstract: ASP.NET Core 2.0 was released in August 2017. This article looks at the most important new features of ASP.NET Core 2.0 and improvements over its previous versions.

In August 2017, the ASP.NET team announced the public release of ASP.NET Core 2.0, released at the same time with .NET Core 2.0, Entity Framework Core 2.0 and the .NET Standard 2.0 specification.

This is great news for us developers as Microsoft’s cross-platform offering keeps maturing, improving and receiving new features.

Are you keeping up with new developer technologies? Advance your IT career with our Free Developer magazines covering ASP.NET Core, C#, Patterns, .NET Core, MVC, Azure, Angular, React, and more. Subscribe to the DotNetCurry (DNC) Magazine for FREE and download all previous, current and upcoming editions.

The release of .NET Standard 2.0 has also considerably increased the number of APIs that are now available in .NET Core, with the framework now covering a higher number of the use cases needed by existing teams and/or projects.

Editorial Note: If you want to stay updated with what’s going on in the .NET space, make sure to read http://www.dotnetcurry.com/dotnet/1391/dotnet-developer-what-is-new .

In this article, we will focus on ASP.NET Core 2.0 by taking a look at the most important new features and improvements over its previous versions. If you haven’t done so yet, I would recommend combining this reading with the linked announcements. I hope you will enjoy it!

You can find the companion source code on github.

Getting Started with ASP.NET Core 2.0

If you haven’t done it already, download and install the latest version of .NET Core which also includes ASP.NET Core from the official dotnet site. If you use Visual Studio, make sure you also install the latest updates.

Once you have done so, creating a new ASP.NET Core 2.0 is as simple as executing the dotnet new command using any of the web installed templates like dotnet new mvc or dotnet new react.

Since the list of templates has changed, simply execute dotnet new to get the full list:

dotnet-new

Figure 1, available project templates with dotnet new

Alternatively, you can start Visual Studio 2017 and execute File > New > Project then select the ASP .NET Core Web Application template in the dialog that appears:

visual-studio-2017-new-project

Figure 2, creating new ASP.NET Core project in Visual Studio

Once you do so, a second dialog will appear where you will be able to select which framework to target (by default the cross platform .NET Core is selected with version 2.0) and the specific ASP.NET Core template to use:

visual-studio-asp-core-template

Figure 3, selecting the framework and template for the new ASP.NET Core project in Visual Studio

Upgrading existing ASP.NET Core 1.x projects

If you already have an ASP.NET Core 1.X project and would like to upgrade, Microsoft has published a handy migration guide.

I do not wish to repeat the guide here, so I will just quickly summarize the most important steps:

  • Upgrade the target framework to either .NET Core 2.0 or the latest full .NET Framework (depending on whether you were targeting .NET Core or not before)
  • Upgrade the package references to their 2.0 versions and take advantage of the new Microsoft.AspNetCore.All metapackage
  • Update the Main method in Program.cs following the new host builder pattern (more later in the article)
  • Update the Startup class, moving the Configuration and Logging setup to the Main method (more about this also later in the article).

Additional steps might be needed if you use for example EF Core or want to take advantage of some of the new features.

One little warning before we move on.

In previous versions of the framework, you could create a global.json file on any folder and set the framework target version of that folder and sub-folders. If you have such a file in one of your existing projects or in any of its parent folders, make sure you update it to target the version 2.0

  • · If you forget to do so, once you upgrade visual studio to the latest 15.3 version, you will start getting the error “The SDK 'Microsoft.NET.Sdk.Web' specified could not be found” in visual studio when opening the project
  • · Andrew Lock’s wrote in detail about this issue in his blog.

With that last piece of advice, let’s move on to what’s new in ASP.NET Core 2.0!

ASP.NET Core 2.0 - What’s New

Razor pages

Razor pages is one of the biggest (if not THE biggest) new features introduced. However, you might be wondering how could that be, since we have been writing Razor pages for quite some years?

That’s because by Razor Pages, we are now referring at a new MVVM (Model-View-ViewModel) paradigm that’s available in ASP.NET Core projects.

Don’t be afraid, this is not a replacement of the previous MVC paradigm, in fact it’s frictionless to use both MVC and MVVM depending on the problem you are solving.

Look at it as another tool in your toolbox that will make your life easier when used in the right situations.

For example, have you ever looked at the HomeController created for the SPA (single page applications) templates like Angular or React and wondered what benefits is it bringing?

public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }

    public IActionResult Error()
    {
        return View();
    }
}

Wouldn’t it be great if all you had to do was create the View file without the need for this pure boilerplate controller?

This is just one of the examples where Razor pages and its MVVM paradigm might be a better approach than MVC. You will now have the flexibility to use one or the other for each specific use case!

Note: I have upgraded the SPA Vue.js project I used in an earlier article to use ASP.NET Core 2.0 and Razor Pages. Check out the commit with the relevant changes!

Getting started with razor pages

Once you have an ASP.NET Core 2.0 project, Razor pages are enabled by default when using services.AddMvc(); in your Startup.ConfigureServices method.

Let’s give it a try, create a folder named Pages at the root of your project (same place where the Views folder is located) and then create a new file HelloWorld.cshtml inside the folder with the following contents:

@page
<h1>Hello World!</h1>
<p>The server time is @DateTime.Now</p>

Apart from the @page directive, everything else is the same as any other Razor view you have seen so far.

Now build and run the project and navigate to /HelloWorld. Congratulations, you just rendered your first Razor page:

aspnet-core-2-first-page

Figure 4, first simple Razor page being rendered

Of course, that is rather ugly, and you would like to have a layout and some structure, like regular views do.

Since this is Razor, everything you know about Razor views can be applied to Razor pages. This is not just the syntax, for example the layouts and special _ViewStart.cshtml and _ViewImports.cshtml behave the same way.

Copy the _Layout.cshtml and _ViewStart.cshtml files located inside /Views/Shared into the /Pages folder. Now restart the project and navigate again to /HelloWorld, this time you will see how the default layout has also been applied to the Razor page:

first-page-with-layout

Figure 5, the simple Razor Page using the layout

You might be wondering what happens if your project doesn’t have Views and only has Razor Pages? Or if you want to use a different layout for Pages than the one for Views?

Well you can just create a file named _Layout.cshtml in the Pages folder and define the layout for the Pages.

Try that by copying the one located inside /Views/Shared and making some changes to it.

Before we start looking at Razor pages in more detail, I should mention there is a project template available at the CLI with dotnet new razor that will initialize the same as the typical MVC project, but using only razor views.

· If you are still unsure about what Razor Pages are, generate 2 projects using dotnet new razor and dotnet new mvc and compare the Razor Pages against the Controller+Views.

Routing

By default, routes to Razor Pages are based on the name and location of the page files:

file-name-route

As you can see there is almost a 1:1 mapping between routes and folder/file names, except for the special Index.cshtml case.

Even though Razor Pages are designed with this simple mapping in mind, it supports many of the features you are now used to when defining your application routes like route parameters, constraints or even your own conventions..

For example, using the @page directive, you can append additional segments and even parameters to the URLs we have seen before. Replace the contents of Pages/HelloWorld.cshtml with the following:

@page "foo/{id}"
<h1>Hello World!</h1>
<p>You have provided the id: @RouteData.Values["id"]</p>

You will now get a 404 if you navigate to /HelloWorld, but will see the page if you navigate to /HelloWorld/foo/42:

page-with-udpated-route

Figure 6, the Razor Page with the updated route

The entire range of route constraints are available when defining route parameters in Razor Pages.

Finally, you can update the default conventions or even add your own ones using the AddRazorPagesOptions in the Startup.ConfigureServices method.

For example, you can fully replace the automatically generated route with one of your own:

services.AddMvc()
    .AddRazorPagesOptions((opts) =>
    {
        opts.Conventions.AddPageRoute("/HelloWorld", "hello-world");
    });

The previous page will now be associated with the route /hello-world (notice how the override also affects the bits defined with the @page directive).

A more interesting example is a convention that replaces camel case names into kebab case, so /FooBar/HelloWorld.cshtml automatically becomes /foo-bar/hello-world:

services.AddMvc()
    .AddRazorPagesOptions((opts) =>
    {
        opts.Conventions.AddFolderRouteModelConvention("/", model =>
        {
            foreach (var selector in model.Selectors)
            {
                var attributeRouteModel = selector.AttributeRouteModel;
                attributeRouteModel.Template = ToKebabCase(attributeRouteModel.Template);
            }
        });
    });
private string ToKebabCase(string s)
{
    return Regex.Replace(
        s,
        "([a-z])([A-Z])",
        "$1-$2",
        RegexOptions.Compiled)
        .ToLower();
}

This has the added benefit of playing well with customizations made in the @page directive, so our /Hello-World.cshtml will now be associated with /hello-world/foo/{id}:

page-with-udpated-route-through-convention

Figure 7, route updated through convention and @page directive

Page Models

When using Razor Pages, sooner or later you will need to encapsulate and move the logic out from the view. Within a traditional MVVM architecture, you will be looking at moving this into the VM, while in the Razor Pages world, you will be creating a Page Model.

Let’s revert the previous routing changes and get back to our simple page /Pages/HelloWorld.cshtml with the following contents:

@page
<h1>Hello World!</h1>
<p>The server time is @DateTime.Now</p>

Let’s add a very simple page model for this page. Start by creating a new class named HelloWorld.cshtml.cs located in the same folder with the following contents:

public class HelloWorldModel: PageModel
{
    public DateTime ServerTime { get; set; }

    public void OnGet()
    {
        // This method is invoked when the page gets rendered in response to a GET request 
        ServerTime = DateTime.Now;
    }
}

Then update the Page itself to read like:

@page
@model ASPCore2.Pages.HelloWorldModel
<h1>Hello World!</h1>
<p>The server time is @Model.ServerTime</p>

As you can see, we have just created our model inheriting from PageModel and we are using the @model directive in the page to link our Razor Page with its model. You can then use any of its properties as in Model.ServerTime.

When you start creating multiple pages with corresponding models, you can avoid specifying the full namespace in the @model directive if instead, you add a @namespace directive to the _ViewStart.cshtml file like:

@namespace ASPCore2.Pages

Then you can simply use the model class name with the @model directive as in @model HelloWorldModel

It’s also worth mentioning that the model file doesn’t really have to be named like the page itself, since they are bound through the @page directive which is based on the model class name. However, if you follow the file naming convention, Visual Studio will recognize it and collapse the model inside the page:

page-and-its-model-in-visual-studio

Figure 8, page and its model grouped together in Visual Studio

This is in fact the convention followed by the Razor Page file template in Visual Studio! Try it by right clicking the Pages folder, then select Add > New Item and finally select the Razor Page template.

Dependency Injection in Page Models

A Page Model would be of limited use if it couldn’t communicate with the rest of the system. It is responsible not only for the data binding, but also for initiating the necessary calls to fetch/update your data in response to the different HTTP requests.

The good news is that they support dependency injection out of the box, all you need to do is to add a constructor receiving the desired dependencies.

For example, update the previous Page Model to read like:

public class HelloWorldModel: PageModel
{
    public HelloWorldModel(IHostingEnvironment env)
    {
        PhysicalPath = env.ContentRootPath;
    }

    public string PhysicalPath { get;}
    public DateTime ServerTime { get; set; }

    public void OnGet()
    {
        ServerTime = DateTime.Now;
    }
}

And now, we can update the view to show the physical location of our server:

<p>The server files are located at @Model.PhysicalPath</p>

Just like that, dependency injection works out of the box as you would expect in Razor Pages.

URL parameters

We saw earlier how it is possible to use the @page directive to define the url parameters part of the route associated with the page. For example:

  • When using @page "{id}" your page will be associated with the /HelloWorld/{id} URL, where the id parameter is required.
  • You can define an optional parameter adding a ‘?’ at the end as in @page "{id?}", so now both /HelloWorld and /HelloWorld/42 would render your page.
  • You can add any of the available constraints to the page URL parameters

Let’s update our simple page with the directive @page "{id:int}" so an integer id is now part of the route.

The question now is how do you read the value of the parameter in your page model?

While you could manually parse through the RouteData dictionary, a cleaner alternative is to add a parameter to the OnGet method:

public void OnGet(int id)
{
    Id = id;
    ServerTime = DateTime.Now;
}

Finally, a similar approach would be in using the [BindProperty] attribute with the Id property:

[BindProperty(SupportsGet = true)]
public int Id { get; set; }
public DateTime ServerTime { get; set; }

public void OnGet()
{
    ServerTime = DateTime.Now;
}

One last bit before we move on.

How does one generate a URL to a certain page, including any required parameters?

There are a couple of ways, both of which are very similar to how you generate a URL to a certain controller action:

- If you need to generate the URL itself, you can use the UrlHelper.Page method, for example in a razor page you can do:

@Url.Page("HelloWorld", new {id = 100})

- If you want to generate a full link tag pointing towards a specific razor page, then you would use the link tag helper setting the asp-page attribute and any required route attributes:

<a asp-page="/HelloWorld" asp-route-id="100">My Razor Page</a>

I hope all we have seen so far regarding Razor Pages is intuitive enough for you!

You might be wondering about the OnGet method of the Page Model though! Don’t worry we will see how pages can handle different types of requests in the next section

Form actions

So far, we have seen page models with an OnGet method, which as you might have guessed, is executed in response to an HTTP GET request.

But what if your page includes a form and you want the Page Model to handle additional requests like a POST?

The framework will look for a method OnPost (or OnPostAsync, the Async suffix is optional) in the Page Model class. The only restriction is that it should return an IActionResult (or Task<IActionResult> when using async methods)

Let’s see a very simple example using the well-known problem of the Todo list.

Start by adding a simple TodoModel to the models folder:

public class TodoModel
{
    [Required]
    public string Title { get; set; }
    [Required]
    public string Description { get; set; }
    public bool Completed { get; set; }
}

Let’s also add a static collection to the model itself, which will serve as a very simple in-memory repository.

Note: this is just to keep the example focused on the action handler without distracting the reader with repositories, entity framework or dependency injection. Please don’t do this in real applications!

public static ConcurrentBag<TodoModel> Todos = new ConcurrentBag<TodoModel>();

Let’s now create a new folder named Todo inside the Pages folder. Create a new Razor Page named Create.cshtml together with its model Create.cshtml.cs.

Next add an OnGet method where we initialize a new instance of the TodoModel:

[BindProperty]
public TodoModel Todo { get; set; }

public void OnGet()
{
    Todo = new TodoModel { Title = "What needs to be done?" };
}

Now update the page itself so it renders a form where users can enter the values of the new Todo:

@page
@model CreateModel

<h1>Add new Todo</h1>
<form method="post">
    <div asp-validation-summary="All"></div>    
    <div class="form-group">
        <label asp-for="Todo.Title"></label>
        <div>
            <input class="form-control" asp-for="Todo.Title" />
            <span asp-validation-for="Todo.Title"></span>
        </div>
    </div>
    <div class="form-group">
        <label asp-for="Todo.Description"></label>
        <div>
            <input class="form-control" asp-for="Todo.Description" />
            <span asp-validation-for="Todo.Description"></span>
        </div>
    </div>

    <button class="btn bg-primary" type="submit">Add</button>    
</form>

Finally, let’s add the OnPost method that will handle the POST request, adding the new Todo to the in-memory list of Todos and redirecting to the Index page:

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    TodoModel.Todos.Add(Todo);

    return RedirectToPage("./Index");
}

Simple, right?

It is not much different from your typical controller actions, except for the different method naming convention and the fact that the posted data is binded to a property of the page model class which has the [BindProperty] attribute.

Let’s complete our simple example by implementing the Index page. Update its Page Model so it gets the list of Todos from the in-memory repository:

public IEnumerable<TodoModel> Todos { get; set; }
public void OnGet()
{
    Todos = TodoModel.Todos.ToList();
}

Finally update its page so it renders each of the Todos and includes a link to the create page:

@page
@model IndexModel
<h1>Todo list</h1>
<ul class="list-unstyled">
    @foreach (var todo in Model.Todos)
    {
        <li>
            <p>@todo.Title - @todo.Description</p>
        </li>
    }
</ul>
<a asp-page="./Create">Add another</a>

page-for-creating-a-new-todo

Figure 9, create todo page

list-of-todos

Figure 10, the page rendering a list of todos

This is where our overview of Razor Pages finishes!

I hope you agree it is very straightforward to get started with them and get used to the MVVM pattern they propose.

That doesn’t mean they should be used everywhere (for example, I am not convinced about CRUD pages) but I can see how a combination of Razor Pages with GET-only pages and traditional controllers for APIs called by clients would be useful (particularly for SPAs).

If you want an even deeper look, I will recommend you to start from the official Microsoft documentation about Razor Pages.

Program and Startup classes

ASP.NET Core 2 simplifies the Program.cs once again, by streamlining the WebHostBuilder as part of its template.

Compare the default template you got when creating a 1.X project:

public class Program
{
    public static void Main(string[] args)
    {
        var host = new WebHostBuilder()
            .UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseIISIntegration()
            .UseStartup<Startup>()
            .UseApplicationInsights()
            .Build();

        host.Run();
    }
}

With the one you now get in a 2.0 project:

public class Program
{
    public static void Main(string[] args)
    {
        BuildWebHost(args).Run();
    }

    public static IWebHost BuildWebHost(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .Build();
}

It might not seem like a huge difference but there are a few details worth describing:

  • The most obvious one is that now it is recommended to separate the Main method that runs the application from the BuildWebHost which creates and configures an IWebHost but does not run it. And why would you care? This allows for tooling like scaffolding or EF migrations to inspect your IWebHost without running it
  • The second is that instead of creating a WebHostBuilder and configure it, you can start from the utility WebHost class which provides a handy CreateDefaultBuilder method. This is roughly equivalent to the previous methods except for:
    • It adds the default configuration (appsettings.json, appsettings.{env}.json, user secrets in dev environment, environment variables and command line args). More about it on the next section!
    • It adds the default logging (using settings from the logging configuration section it adds console and debug logging). More about it in the next section!
    • It doesn’t need to manually add application insights, since it now gets automatically started by means of the new IHostingStartup interface. More on it later!

You could check the actual definition of CreateDefaultBuilder in the aspnet MetaPackages repository, if you open the source code of the WebHost class.

  • You will see it looks very similar to the 1.X Main method, manually creating a WebHostBuilder and using the extension methods to configure it.

The fact that Configuration and Logging are now core services added in the Program class by the WebHostBuilder, means they no longer must be configured in the Startup class.

That way the startup class has also been simplified when compared to the default 1.X version:

  • The configuration is simply injected in the constructor, since it has already been configured by the WebHostBuilder and is ready to be injected in any class that needs it.
  • The Configure method no longer needs to receive an ILoggerFactory parameter and configure the logging, since logging has already been configured by the WebHostBuilder.

The default Startup class now looks like:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. 
    // Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    }

    // This method gets called by the runtime. 
    // Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseBrowserLink();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseStaticFiles();

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

You can read more about hosting and startup of 2.0 applications in the official Microsoft documentation.

Logging and Configuration

Both Logging and Configuration are now configured in the Program class by the WebHostBuilder and available as services that can be injected, so:

  • The interface IConfiguration is now available for dependency injection and can be injected anywhere (whereas before you would have to use the IOptions<T> or manually add the IConfiguration to the services)
  • The interfaces ILogging, ILoggingFactory and IHostingEnvironment are now available for dependency injection from earlier, which might come handy for some initialization code.

As we have seen in the previous section, the new WebHost.CreateDefaultBuilder used in the Main class will add Configuration and Logging using their default strategies. But of course, you can keep using the methods available in IWebHostBuilder to override or expand these defaults:

public static IWebHost BuildWebHost(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .ConfigureAppConfiguration((context, config) => {
            // If you want to override default sources, uncomment the next line
            //config.Sources.Clear();

            // Add new sources on top of the default config
            config.AddJsonFile("foo.json");
        })
        .ConfigureLogging((context, logging) => {
            // If you want to override default sources, uncomment the next line
            //logging.ClearProviders();

            // Add new logger providers on top of the default ones
            logging.AddEventSourceLogger();
        })
        .Build();

Logging Filtering

Another very interesting improvement to the logging framework is that in the configuration, now you can simply define that the logging level is applicable to all or specific loggers and categories. There is an application wide loggingFactory.AddConfiguration, conveniently called by the default WebHostBuilder.

  • · In earlier releases you would find yourself either passing the configuration to each logger registration or using the configuration within the loggerFactory.WithFilters helper.

For example, given the default logger (debug and console) added to a project, you can now update your configuration as follows in order to filter which event gets logged to each provider (example is taken from the official Microsoft docs about Logging):

{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    },
    "Debug": {
      "LogLevel": {
        "Default": "Information",
        "Microsoft.AspNetCore.Mvc.Internal": "Error",
        "Microsoft.AspNetCore.Hosting.Internal": "Error"
      }
    }
  }
}

The top-level LogLevel section applies to any logger while the LogLevel nested under Debug applies only to the Debug logger. (For a list of built-in providers check the Microsoft docs).

Since the logging configuration is now applied at the framework level, it will filter messages for any logger you add to your application. This means if you create your own logger or add a 3rd party one, you can use the configuration to define the filtering rules for that logger, as well with zero coding effort.

Let’s say you create your own logger, like the following (useless) logger:

public class FooLoggerProvider: ILoggerProvider
{
    public ILogger CreateLogger(string categoryName)
    {
        return new FooLogger(categoryName);
    }

    public void Dispose()
    {
    }
}

public class FooLogger: ILogger
{
    private readonly string _categoryName;

    public FooLogger(string categoryName)
    {
        _categoryName = categoryName;
    }       

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        var message = formatter(state, exception);
        System.Diagnostics.Debug.WriteLine(message, _categoryName);
    }

    public IDisposable BeginScope<TState>(TState state)
    {
        return null;
    }

    public bool IsEnabled(LogLevel logLevel)
    {
        return true;
    }
}

And you register it on your Main class:

public static IWebHost BuildWebHost(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .ConfigureLogging((context, logging) => 
logging.AddProvider(new FooLoggerProvider()))
        .Build();

..you can then update your configuration to define specific filters that apply to that logger only:

{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    },
    "ASPCore2.FooLoggerProvider": {
      "LogLevel": {
        "Default": "Information",
        "Microsoft.AspNetCore.Mvc.Internal": "Error",
        "Microsoft.AspNetCore.Hosting.Internal": "Error"
      }
    }
  }
}

Notice how the LogLevel section is nested under a section with the full type name of the logger provider. This might get a bit annoying, however you can add the [ProviderAlias("Foo")] attribute to the LoggerProvider and this way define it using the alias instead of the full type name:

{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    },
    "Foo": {
      "LogLevel": {
        "Default": "Information",
        "Microsoft.AspNetCore.Mvc.Internal": "Error",
        "Microsoft.AspNetCore.Hosting.Internal": "Error"
      }
    }
  }
}

For more information about logging using the latest 2.0 features, check the official Microsoft docs.

IHostingService

At first glance, IHostingService is a new and simple interface that only defines a pair of Start and Stop methods:

public class SampleHostedService: IHostedService
{
    public Task StartAsync(CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

The power comes from the fact that if you register this class as a service in your application, then the framework will call the Start method during application startup and the Stop method during application shutdown.

This is very useful if you intend to run some background process independently of your main application, like the periodical refresh/upload/download of data or the running of certain processes in the background.

Let’s update that simple process to basically just periodically add a logging (Warning, very simplistic code for the purposes of the article!):

private readonly ILogger<SampleHostedService> logger;
public SampleHostedService(ILogger<SampleHostedService> logger)
{
    this.logger = logger;
}

public Task StartAsync(CancellationToken cancellationToken)
{
    logger.LogInformation("Hosted service starting");

    return Task.Factory.StartNew(async () =>
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            logger.LogInformation("Hosted service executing - {0}", DateTime.Now);
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
            }
            catch (OperationCanceledException) { }
        }
    }, cancellationToken);
}

public Task StopAsync(CancellationToken cancellationToken)
{
    logger.LogInformation("Hosted service stopping");            
    return Task.CompletedTask;
}

Update the startup class to register SampleHostedService as an IHostedService:

services.AddSingleton<IHostedService, SampleHostedService>();

Now let’s start the application and we can see how our service is started as part of the application and keeps adding an entry every five seconds until the application is stopped (Try to use kestrel when starting the app instead of IISExpress or there will be no graceful shutdown, so StopAsync won’t be called):

hosted-service-running-example

Figure 11, the simple IHostedService implementation in action

I am sure you can come up with several ways to use this new feature, although many of those scenarios would probably benefit by their own independent service anyway. Still, this is a welcome addition to the framework that enables scenarios which earlier always required an independent service.

Would you like to read more about it? Check these articles by Steve Gordon and Maarten Balliauw. (Especially if you want to see how to properly handle the cancellation tokens and background tasks instead of my very simple code!)

IHostingStartup

This new feature looks similar to IHostedService but is quite different. While IHostedService is intended for you to use it as part of your application code if it would help towards solving some scenario; IHostingStartup is geared towards tooling.

The interface defines a single method:

public class SampleHostingStartup: IHostingStartup
{
    public void Configure(IWebHostBuilder builder)
    {
        throw new NotImplementedException();
    }
}

This means you can use any of the IWebHostBuilder methods (and extension methods) to add or update additional behaviors and features. For example, I could add here the custom logging provider added in one of the earlier sections:

[assembly: HostingStartup(typeof(ASPCore2.SampleHostingStartup))]
namespace ASPCore2
{
    public class SampleHostingStartup : IHostingStartup
    {
        public void Configure(IWebHostBuilder builder)
        {
            builder.ConfigureLogging((context, logging) => 
                logging.AddProvider(new FooLoggerProvider()));
        }
    }
}

And why would you do that?

It seems quite a bit of indirection instead of simply configuring the logging provider in the Startup function…

Well, the contract is that the framework will discover all the [HostingStartup] assembly attributes that point to a concrete IHostingStartup implementation, and call them while building the WebHost in your Main class. And now the interesting bit, it will look for IHostingStartup implementations in (check the source code):

  • Your main assembly (the one with the Main method)
  • Any additional assemblies deployed within the application which are part of the semi-colon separated list found in the Configuration with key hostingStartupAssemblies.

You can now drop any assembly in your application folder, make sure the assembly name is part of the comma-separated list hostingStartupAssemblies configuration entry (For example, setting the ASPNETCORE_HOSTINGSTARTUPASSEMBLIES environment variables) and the method will be called during startup, allowing you to add extra services, configuration, loggers, etc.

  • And an even more obscure setting based on the DOTNET_ADDITIONAL_DEPS environment variable would include extra assemblies, check Gergely’s article linked at the end of the section.

It’s quite an interesting feature although specially geared towards tooling like IIS-Express or Application Insights (more on the next section), rather than a general-purpose plugin system. Hence the by-design explicit list of assemblies to scan for the attribute rather than wide scan, as per David Fowler’s comment.

If you are interested in knowing more, then check this article from Gergely Kalapos.

Other new ASP.NET Core 2.0 Features

That’s not everything that has changed on ASP.NET Core 2.0! There are a few other changes worth reading about.

Razor compilation on publish by default

Once you upgrade or start a new project using ASP.NET Core 2.0, Razor compilation on publish will be enabled by default.

That means when you package and publish your application, all the Razor Views/Pages will be compiled and deployed as part of your application assembly rather than separated cshtml that have to be compiled on demand.

This results in quicker startup time and faster initial responses!

Although if you are targeting the full .NET framework, then you still need to reference a explicit NuGet package as per the official Microsoft docs.

If for some reason, you want to turn off this feature, you can do so on your csproj file:

<PropertyGroup>
  <TargetFramework>netcoreapp2.0</TargetFramework>
  <MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
</PropertyGroup>

The Runtime Package Store and the AspNetCore.All metapackage

Microsoft has created a new metapackage named Microsoft.AspNetCore.All which includes references to all the other ASP.NET Core assemblies:

  • The ASP.NET Core packages.
  • The Entity Framework Core packages.
  • Any additional dependencies needed either of those

The immediate benefit is that you need to maintain fewer references in your csproj and will have an easier time upgrading to a new baseline version (for example, from 2.0 to a future 2.X version).

In addition to that, by using the metapackage, you are also implicitly using the new .NET Core Runtime Package Store.

  • The ASP.NET framework assemblies are already installed in the store with their native images ready. Consider it the GAC (Global Assembly Cache) of the .NET Core world.
  • Your application package size is smaller since it doesn’t need to include the assemblies in the Runtime Package Store (Unless you explicitly publish for self-contained deployment)
  • Application startup can be improved when the Runtime Package Store already contains the native images of required assemblies.

TagHelperComponents

Did you use the MiniProfiler in any of your MVC applications?

This is a tool that collects profiling data from your server-side app and would include such information as part of the rendered page, so you could easily see it while running your app in development.

However, it required you to add the required JS/CSS as part of your layout by manually adding @MiniProfiler.RenderIncludes() before closing the body tag.

This is one of the scenarios where the TagHelperComponents can be of help!

These are services registered as singletons, that will be called when rendering the app and will let you modify the contents of some tags.

Out of the box, the framework calls for both the head and body tags any TagHelperComponent that has been registered, giving you a chance of modifying the contents of those tags.

For example, a simplistic implementation of a TagHelperComponent that would add the CSS/JS files needed by miniprofiler could look like:

public class SampleTagHelperComponent: TagHelperComponent
{
    public override int Order => 100;

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        if (string.Equals(context.TagName, "head", StringComparison.OrdinalIgnoreCase))
        {
            output.PostContent.AppendHtml("<link rel='stylesheet' type='text/css' href='miniprofiler.css'></link>");
        }
        if (string.Equals(context.TagName, "body", StringComparison.OrdinalIgnoreCase))
        {
            output.PostContent.AppendHtml("<script src='miniprofiler.js'></script>");
        }
    }
}

Next, register it as a service in your Startup class:

services.AddSingleton<ITagHelperComponent, SampleTagHelperComponent>();

Now it will automatically be called for both the head and body tags, thus modifying them to include the additional JS and CSS references:

script-body-taghelpercomponent

Figure 12, the script added by the TagHelperComponent at the end of body

What happens if you want to modify a tag other than head or body?

Then you need to create a special type of TagHelper that basically targets a given tag name and calls all the registered TagHelperComponents. And you do so by inheriting from the predefined TagHelperComponentTagHelper class.

For example, if you wanted to modify the footer component, then you would add the following tag helper:

[HtmlTargetElement("footer")]
public class FooterTagHelper: TagHelperComponentTagHelper
{
    public FooterTagHelper(
        ITagHelperComponentManager manager,
        ILoggerFactory logger): base(manager, logger) { }
}

Now any TagHelperComponent would also be called for footer elements, allowing you to modify them:

public override void Process(TagHelperContext context, TagHelperOutput output)
{
    if (string.Equals(context.TagName, "footer", StringComparison.OrdinalIgnoreCase))
    {
        output.PostContent.AppendHtml("<p>Modified in a TagHelperComponent</p>");
    }
}

The caveat is that they will be called for every footer without distinction, so take this into account when targeting elements that are not unique like head or body! (and double check you really need a TagHelperComponent in such a scenario, and not a regular tag helper, view component, partial, etc)

Application Insights

Finally, you might notice from the earlier discussion about the Program class that there is no longer a call to .AddApplicationInsights() added to your WebHostBuilder (nor is it added for you as part of CreateDefaultBuilder)

However, the feature works automatically when either debugging in Visual Studio or running in Azure, how is that possible?

Remember the earlier section about IHostingStartup Application Insights now:

  • Is included as part of the .All metapackage
  • Provides an IHostingStartup that registered the necessary services
  • When running from Visual Studio or Azure the ASPNETCORE_HOSTINGSTARTUPASSEMBLIES will contain the name of the assembly Microsoft.AspNetCore.ApplicationInsights.HostingStartup
  • A TagHelperComponent is registered to inject the JavaScript needed on the client side

That’s how application insights can work even when it is not referenced anywhere by your app!

That’s interesting, but it might also turn annoying when debugging in visual studio, especially since there is no way of turning the feature off. The only suggested workaround is modifying your launchSettings.json to include the environment variable ASPNETCORE_preventHostingStartup set as True:

"environmentVariables": {
    "ASPNETCORE_ENVIRONMENT": "Development",
    "ASPNETCORE_PREVENTHOSTINGSTARTUP": "True"
},

Be aware adding this environment variable completely disables IHostingStartup! (But since launchSettings.json is only used in development from Visual Studio, it might be a good compromise unless you have created your own IHostingStartup implementation)

Conclusion

There is no shortage of new features added to the framework on its 2.0 release!

Some like the Razor Pages are a significant new piece added to the framework. I am quite interested in seeing what its acceptance and usage by the wider development community is. Although I haven’t used them enough yet to have a better opinion, I can see a combination of pages handling GET request and a typical API controller handling additional client requests working well.

Others like TagHelperComponents, IHostingStartup or IHostedService enable some advanced and interesting scenarios that I am sure at least libraries and tools authors will welcome.

Of course, there is the usual number of smaller improvements and new features that contribute towards a more mature and pleasant framework. Not to forget the release of .NET Standard 2.0 which increases the number of APIs available in .NET Core and might make it viable for teams that were blocked because of missing dependencies.

I am curious to see what will come next! (Although you can always keep an eye on the roadmap).

This article was technically reviewed by David Pine.

Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+
Further Reading - Articles You May Like!
Author
Daniel Jimenez Garcia is a passionate software developer with 10+ years of experience. He started as a Microsoft developer and learned to love C# in general and ASP.NET MVC in particular. In the latter half of his career he worked on a broader set of technologies and platforms while these days he is particularly interested in .Net Core and Node.js. He is always looking for better practices and can be seen answering questions on Stack Overflow.


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!