Introduction to ASP.NET Core part 7: starting with MVC
January 30, 2017 Leave a comment
Introduction
In the previous post we took a look at how to work with various environments and their settings. With environment we now mean things like the development, alpha, beta, production etc. environments that a typical web application goes through. New things are tested in development, then the changes are deployed to beta/release/staging and when everyone is happy with the features then the application is deployed to the production environment. The most important point in our discussion was that an application will likely have different settings in different environments. A setting such as “RabbitMqServiceUrl” will have a certain value in the alpha environment but it will most definitely differ from the service URL in the production environment. We saw that the various configuration sources behave like cascading style sheets. We can load multiple settings sources in code. The first one will contain the generic values and the subsequent ones the values of a certain environment. The environment specific settings will override the generic ones.
In this post we’ll start looking at how to work with MVC in .NET Core.
The official documentation of ASP.NET Core MVC is available on the relevant Microsoft page here.
We’ll be working with the DotNetCoreBookstore demo application as before.
Model-View-Controller
For this discussion I’ll assume that you have at least some knowledge of the Model-View-Controller pattern and have worked with ASP.NET MVC before. Therefore I’ll only include some refresher here, not the fundamentals of MVC.
When an MVC-based website receives an HTTP call to the URL /customers then the MVC router will by convention look for a controller called CustomersController, i.e. the resource name followed by “controller”. A controller is a normal C# class, there is nothing fancy about it. Controllers are normally put into a folder called “Controllers” in the root of the web project. The URL can have an action extension like /customers/filter. In that case the router will look for a function called “Filter” in the CustomersController class. If the URL has no action then the action defaults to “index” so the CustomersController should have an Index function. The action can also be extended with additional parameters like /customers/filter?joinedAfter=[some-date] in which case the Filter function must have a parameter called joinedAfter.
Controller actions often return Models that the views can process for the resulting HTML. Models are also C# classes and they are normally very simple with no inherent logic. A model should only contain the properties that have been distilled for the purpose of showing the requested information in the browser. The models are served to the views that are rendered with some rendering engine. In ASP.NET this is most often Razor with the cshtml file extension. The Controller is free to call some backend services to filter the list of Customers from a database. The resulting list will be cleaned and transformed to models, such as a CustomerModel which only contains those properties that are necessary for the view. The corresponding view will take the injected customer model list and produce some HTML from it, such as a table. Views contain HTML elements with some embedded C# syntax to produce dynamic HTML such as table rows for the Customer models. The action name such as “Filter” should by convention have a view called Filter.cshtml in the Views/Customers folder in the web project root.
The View is not always HTML. When building an API such as a Web API then the response from the web site will most often be JSON or XML. The View then becomes a JSON or an XML string that doesn’t require any specific engine like Razor. The Controller can return this response to the caller without any further processing. Also, the model can be very simple: a list of objects and their properties, like a list of Customers and their names, phone numbers, addresses etc. We may not even need a dedicated object in the beginning of the project, we can just work with anonymous objects while we’re not sure what to return, i.e. what’s needed for the View. This means that when learning MVC we can start by looking at the role of the Controller and ignore the other two elements at first. That’s what we’ll do in this post as well.
Routing in MVC is a lot about mapping the HTTP request URL to a Controller and an action within the controller. The various sections in the URL must be translated to a combination of controller and action name and optionally some parameter names of an action. If there’s no corresponding controller or action then an exception is thrown. URLs can be simple like /home and more complex such as admin/customers/filter?nameFrom=b&nameTo=f&joinedAfter=2014 . Fortunately we can declare the routing maps in code in great detail.
MVC in ASP.NET Core is very similar to traditional ASP.NET MVC with .NET 4.5 and above. There are some new features and the setup is different but the underlying techniques such as routing and Razor views are almost the identical.
Setup
Our DotNetCoreBookstore application is currently not capable of implementing MVC. We’ll have to follow the usual recipe: add a NuGet package and install a middleware. This is again a good example of a modular architecture where we only include the dependencies we need instead of carrying around a huge library 80% of which we don’t need.
Add the following dependency to project.json:
“Microsoft.AspNetCore.Mvc”: “1.1.0”
We’ll need to make two changes in Startup.cs. First we’ll register the services and dependencies required for MVC in the ConfigureServices method:
services.AddMvc();
Next comes the routing declaration in the Configure method. In ASP.NET MVC we would do that either directly in Global.asax.cs or in a separate file and method such as RegisterRoutes which is called from Global.asax. There’s almost always a default route called “Default” that takes the controller and action parts of the URL like here:
private void RegisterRoutes(RouteCollection routes) { routes.MapRoute("Default", "{controller}/{action}", new { controller = "Home", action = "Index" }); }
This route means that if the URL is at its simplest like http://www.mysite.com/ with no controller and action names then the controller defaults to the Home controller and the action to Index. So if the HTTP request is for “/” then we must have a controller called HomeController and a function called Index that returns some type of HTTP response. If the URL request is /products/premium then we’ll need a ProductsController.cs file with a function called Premium. The routing engine will “understand” that the first string “products” corresponds to “controller” in the mapping rule and will search for a file called ProductsController.
A slight extension to the default route is to include a query string like “id” and make it optional:
private void RegisterRoutes(RouteCollection routes) { routes.MapRoute("Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional}); }
This will take care of extended queries that include a single ID parameter such as search term like in /products/premium?id=1234. In this case the Premium function will need an input parameter with the name id.
The .NET Core equivalent uses a middleware and a different way to declare the default values. Also, the RouteCollection object above has been replaced by the Microsoft.AspNetCore.Routing.IRouteBuilder interface which we’ll see shortly.
Currently we have the following middleware in our demo application in Configure:
app.Run(async (context) => { await context.Response.WriteAsync(stringFormatter.FormatMe(new { Message = "Hello World!" })); });
That bit of code can be removed or commented out. We’ll replace it with the following:
app.UseMvc(routeBuilder => { routeBuilder.MapRoute("Default", "{controller=Home}/{action=Index}/{id?}"); });
The UseMvc middleware accepts a function with a parameter of type Microsoft.AspNetCore.Routing.IRouteBuilder as mentioned above. The route builder has the MapRoute function where we can declare our routing rules. Note how the defaults can now be embedded in the template like controller=Home. The ? after “id” declares it optional.
We can declare as many routes as needed. It is seldom that the single default route will be enough for a large web application so there will definitely be more than the one above. Keep in mind, however, like in ASP.NET MVC, the first route that matches a URL request wins and the other routes are not considered.
The home controller
Add a folder called Controllers to the project root of our demo application. Add a new item of type MVC Controller Class called HomeController to the folder:
It will add a HomeController class with an Index method that returns a view. Let’s make it easy for us and rewrite the Index function to return a simple string instead so that we don’t get sidetracked with other new material:
public string Index() { return "This is a killer homepage from the Home controller!!!!"; }
Note that the controller derives from the Controller base class in the Microsoft.AspNetCore.Mvc homepage. It is actually not compulsory that a controller derive from the Controller class. However, this base class and ControllerBase further up the class hierarchy provide access to a wide variety of useful properties such as the http context, the current user, the route data and much more.
Web API controllers
We can take a short diversion at this point. If you’ve built APIs using the Web API technology then you’ll know that it also has controllers in traditional ASP.NET but they derive from a base class called ApiController which is different from the base class for MVC controllers.
In ASP.NET Core this distinction is gone. Both Web API and MVC controllers derive from the same Controller class. There’s a Web API controller template as well in Visual Studio:
We’ll take up Web API controllers in .NET Core in a different series later on.
Demo
We have a new home page, so let’s try it. Start the application and… …we see the following message in the browser:
Welcome to my default index page!
That’s not what we expected, right? That’s the default index.html page in the wwwroot folder that we installed previously in the series. Recall that we have the following middleware in the Configure method:
app.UseFileServer();
If the URL has no extension, like http://localhost:%5Bport%5D/ in our case then the above extension will look for a Home.htm(l) or Index.htm(l) in the wwwroot folder. Since there’s such a page we never come to the UseMvc middleware, it doesn’t get the chance. And why would it? How could MVC know which controller or static HTML file you really meant? This is an example of a “race” that was won by index.html and the HomeController was never called.
A quick solution is to either delete index.html from wwwroot or rename it to something like home_backup.html so that you have it for later reference.
Re-run the application and you should see the new message in the browser:
This is a killer homepage from the Home controller!!!!
If you don’t then clear the browser cache and try again, it must work.
We’ll stop here and continue our MVC exploration in the next post.
View the list of MVC and Web API related posts here.