Moving to ASP.NET MVC Core 1
Introduction
The new version of ASP.NET MVC, Core 1, brought a number of changes. Some were additions, but others were removals.The structure is similar and will be mostly familiar to anyone who has worked with ASP.NET MVC, but a few stuff is very different. Let’s visit all of them, in a lightweight way – more posts coming soon.
Unified API
MVC and Web API have been merged together, which is a good thing. The new API is based on OWIN. There is a single Controller base class and an IActionResult serves as the base class for the responses, if we so want it – we can also return POCO objects.
Application Events
Application_Start and Application_End events are now gone, as is IRegisteredObject interface and HostingEnvironment.RegisterObject. If you want to be notified (and possibly cancel) hosting event, get a handle to the IApplicationLifetime instance using the built-in service provider and hook to the ApplicationStarted, ApplicationStopping or ApplicationStopped properties.
Dependency Injection
The dependency resolution APIs of both MVC and Web API has been dropped in favor of the new .NET Core mechanism, which is usually configured in the Startup class. You can plug your own IoC/DI framework as well. I wrote a post on this, which you can find here. There are now several new services that you can use to query information from the executing context, experienced users will perhaps find it complex. I’d say this is matter for a full post soon.
Routing
Attribute routing comes out of the box, no need to explicitly configure it. There are two new tokens, [controller] and [action], which can be used to represent the controller or action method they are appled to. For example, [Route("api/[controller]")], when applied to the HomeController, will be the same as "api/home". Same for [Route("[action]/{id}")]: if applied to the Post action method, will be the same as "post/{id}". This allows you to define constants, if you like.
There is no global routing table (RouteTable.Routes), so we need to configure it in the Startup class, in the UseMvc method:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Route handlers (IRouteHandler) are not directly usable. Remember, route handlers are called when the route is matched. Instead, we need to use an IRoute:
public class MyRouter: IRouter
{
private readonly IRouteHandler _handler;
public MyRouter(IRouteHandler handler)
{
this._handler = handler;
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
return null;
}
public Task RouteAsync(RouteContext context)
{
context.Handler = this._handler.GetRequestHandler(context.HttpContext, context.RouteData);
return Task.FromResult(0);
}
}
Routes are added in Startup as well:
app.UseMvc(routes =>
{
var routeBuilder = routes.MapRoute(
name: "default",
defaults: new {},
template: "{controller=Home}/{action=Index}/{id?}");
routeBuilder.DefaultHandler = new MyRouter();
});
There is already an handler so in this example we are overriding it. You better know what you’re doing if you are going to do something like this!
Route constraints (IRouteConstraint) are still available, just configured in a different way. These allow us to define if a route’s parameter should be matched:
services.Configure<RouteOptions>(options => options.ConstraintMap.Add("my", typeof(MyRouteConstraint)));
And then on the route template attribute we use the constraint:
[Route("home/action/{id:myconstraint}")]
Now there are also conventions (IApplicationModelConvention) that we can use:
public class MyConvention : IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
//...
}
}
Registration is done in the AddMvc method too:
services.AddMvc(options => options.Conventions.Insert(0, new MyConvention()));
Configuration
The Web.config file is gone, now configuration is done in a totally different way, preferably through JSON. You can either make the IConfigurationRoot instance available through the services collection:
public IConfigurationRoot Configuration { get; set; }
public void ConfigureServices(IServiceCollection services)
{
//build configuration and store it in Configuration
services.AddSingleton(this.Configuration);
//...
}
or you can build a strongly typed wrapper class for the configuration. For example, for this JSON file:
{
"ConnectionString": "Data Source=.;Integrated Security=SSPI; Initial Catalog=MyDb",
"Timeout": 300
}
we could have this code:
class MySettings
{
public string ConnectionString { get; set; }
public int Timeout { get; set; }
}
public class Startup
{
public IConfigurationRoot Configuration { get; set; }
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions();
services.Configure<AppSettings>(this.Configuration);
//...
}
}
public MyController : Controller
{
public MyController(IOptions<AppSettings> appSettings)
{
//...
}
}
Public Folder
Now the public folder is not the same as the project folder: by default, there’s a wwwroot folder below the project folder which is where all the “physical” assets will be copied to at runtime.
Virtual Path Providers
Again, virtual path providers are gone, but there is a similar mechanism. You need to get a hold of the IHostingEnvironment instance and use or change its ContentRootFileProvider or WebRootFileProvider properties. There’s a new file provider interface, IFileProvider, that you can leverage to provide your own behavior:
public class MyFileProvider : IFileProvider
{
public IDirectoryContents GetDirectoryContents(string subpath)
{
//...
}
public IFileInfo GetFileInfo(string subpath)
{
//...
}
public IChangeToken Watch(string filter)
{
//...
}
}
If you do not wish to or cannot implement one of the IDirectoryContents, IFileInfo or IChangeToken, just return null.
The difference between ContentRootFileProvider and WebRootFileProvider is that the first is used for files inside IHostingEnvironment.ContentRootPath and the latter for those inside IHostingEnvironment.WebRootPath.
OWIN
MVC Core is now based on the OWIN standard, which means you can add your own middleware to the pipeline. This replaces both HTTP Modules and HTTP Handlers. OWIN middleware sits on the pipeline, like this:
public class MyMiddleware
{
private readonly RequestDelegate _next;
public MyMiddleware(RequestDelegate next)
{
this._next = next;
}
public async Task Invoke(HttpContext context)
{
await context.Response.WriteAsync("Hello from a middleware class!");
await this._next.Invoke(context);
}
}
We register middleware components in the IApplicationBuilder instance:
app.UseMiddleware<MyMiddleware>();
A middleware class is just a simple POCO class that follows two conventions:
-
The constructor receives a RequestDelegate instance
-
It has an Invoke method taking an HttpContext and returning a Task
The constructor can also take any service that is registered in the service provider. Pay attention, the order by which we add add our middleware has importance. Make sure you add yours soon enough to encapsulate whatever logic you wish to wrap.
Logging and Tracing
Logging and tracing is now only supported as part of the new unified logging framework. You can also write your own middleware that wraps the MVC processing and add your own logging logic. You gain access to the ILoggerFactory or ILogger/ILogger<T> instances through the service provider or using Dependency Injection:
public class MyController : Controller
{
public MyController(ILogger<MyController> logger)
{
}
}
Custom Errors
The custom errors setting is also gone. In order to have similar behavior, enable developer exception page in the Startup class.
Controllers and Views
Controllers stay the same with one addition: we can have POCO controllers, that is, controllers that do not inherit from a base class (other than Object, that is). In order to make proper use of them, for example, if we want to access the context, we need to inject some properties into the controller class – the HttpContext.Current property is no more:
public class HomeController// : Controller
{
//automatically injected
[ActionBindingContext]
public ActionBindingContext BindingContext { get; set; }
//automatically injected
[ViewDataDictionary]
public ViewDataDictionary ViewData { get; set; }
//automatically injected
[ActionContext]
public ActionContext ActionContext { get; set; }
//constructor-set
public IUrlHelper Url { get; private set; }
public dynamic ViewBag
{
get { return new DynamicViewData(() => this.ViewData); }
}
public HomeController(IServiceProvider serviceProvider)
{
this.Url = serviceProvider.GetService(typeof(IUrlHelper)) as IUrlHelper;
}
}
The serviceProvider instance will come from the ASP.NET MVC Core dependency injection mechanism.
In views, we now only have the Razor engine. We can now inject components into views:
@inject IMyClass MyClass
@MyClass.MyMethod()
and also define functions in the markup:
@functions {
string MyMethod() {
return "Something";
}
}
Another new thing is the _ViewImports.cshtml file. Here we can specify stuff that will apply to all views. The following directives are supported:
-
addTagHelper
-
removeTagHelper
-
tagHelperPrefix
-
using
-
model
-
inherits
-
inject
Layouts files stay the same.
On the other hand, mobile views no longer exist. Of course, you can add logic to find if the current browser is mobile and then serve an appropriate view. Again, the Browser and IsMobileDevice properties are now gone (with the browser capabilities database), so you will have to do your own sniffing.
You can still add view engines (currently, only Razor is supported), you do that when you register MVC services (no more ViewEngines.Engines property):
services
.AddMvc()
.AddViewOptions(x =>
{
x.ViewEngines.Add(new MyViewEngine());
});
Model validation providers used to be extensible through the IClientValidatable interface. Now, we have IClientModelValidatorProvider, and we need to add our providers to a collection also when we register MVC services:
services
.AddMvc()
.AddViewOptions(x =>
{
x.ClientModelValidatorProviders.Add(new MyClientModelValidationProvider());
});
A client model validation provider needs to implement IClientModelValidatorProvider:
public class MyClientModelValidationProvider : IClientModelValidatorProvider
{
public void CreateValidators(ClientValidatorProviderContext context)
{
//...
}
}
Finally, views now cannot be precompiled. In the early days of ASP.NET MVC Core, it was possible to precompile them, but not anymore.
Filters
There is no longer a static property for holding the global filers (GlobalFilters.Filters), instead, they can be added to the services collection, normally through the AddMvc overload that takes a lambda:
services.AddMvc(mvc =>
{
mvc.Filters.Add(typeof(MyActionFilter));
});
Of course, it is still possible to scope filters at the class or method level, using either an attribute inheriting from a *FilterAttribute class (like ActionFilterAttribute) or using TypeFilterAttribute, for dependency injection.
View Components and Tag Helpers
These are new in Core 1. I wrote about View Components here and on Tag Helpers here, so I’m not delving into it again. Two very welcome additions indeed!
Bundling
In the old days, you would normally use the Microsoft ASP.NET Web Optimization package to bundle and minify your JavaScript and CSS files. Now, by default, Gulp is used for that purpose. You can also use BundleMinifier.Core from Mads Kristensen, this needs to be added as a tool and configured through a bundleconfig.json file. BundleMinifier is installed by default starting with Visual Studio 2015 Tooling Preview 1.
Maintaining State
In pre-Core days, one could store state in the Application, which would be available to all requests. Unfortunately, this led to several problems, and the application storage was dropped. Of course, it is still possible to use static members and classes.
Likewise, the Cache storage was also dropped, this time in favor of a more flexible and extensible mechanism. You’ll need the Microsoft.Extensions.Caching.Abstractions Nuget package for the base contracts plus a specific implementation (see Memory or Redis, for example):
public void ConfigureServices(IServiceCollection services)
{
services.AddDistributedMemoryCache();
//...
}
public class MyController : Controller
{
public MyController(IDistributedCache cache)
{
//...
}
}
Sessions are still around, but they need to be explicitly configured. You need to add a reference to the Microsoft.AspNetCore.Session package, and then register the services and middleware (a distributed memory cache is also required):
public void ConfigureServices(IServiceCollection services)
{
services.AddDistributedMemoryCache();
services.AddSession();
}
public void Configure(IApplicationBuilder app)
{
app.UseSession();
app.UseMvc();
}
After this, the ISession instance can be accessed through the HttpContext instance exposed, for example, by the Controller class.
There is no longer support for automatically storing the session in a SQL Server database or the ASP.NET State Service, but it is possible to use Redis, a very popular distributed cache technology.
Publishing
Publish profiles are still around, but now you have other options, such as deploy to Docker. There’s also the dotnet publish command, which places all deployable artifacts in a folder, ready to be copied to the server.
Conclusion
You can see that even though this is still MVC, a lot has changed. In my next post, I will talk a bit about some of the new interfaces that were introduced. In the meantime, hope this gets you up and running! Do let me know if I skipped something or you wish to know more about this. Your feedback is always greatly appreciated!