ASP.NET Core 2.2 For Beginners (Part 4): MVC Controllers

In this chapter, you will learn about MVC, which is a popular design pattern for the user interface layer in applications, where M stands for Model, V stands for View, and C stands for Controller. In larger applications, MVC is typically combined with other design patterns, like data access and messaging patterns, to create a full application stack. This book will focus on the MVC fundamentals.

The controller is responsible for handling any HTTP requests that come to the application. It could be a user browsing to the /videos URL of the application. The controller’s responsibility is then to gather and combine all the necessary data and package it in model objects, which act as data carriers to the views.

The model is sent to the view, which uses the data when it’s rendered into HTML. The HTML is then sent back to the client browser as an HTML response.

The MVC pattern creates a separation of concerns between the model, view, and con­troller. The sole responsibility of the controller is to handle the request and to build a model. The model’s responsibility is to transport data and logic between the controller and the view, and the view is responsible for transforming that data into HTML.

For this to work, there must be a way to send HTTP requests to the correct controller. That is the purpose of ASP.NET MVC routing.

  1. The user sends an HTTP request to the server by typing in a URL.
  2. The controller on the server handles the request by fetching data and creating a model object.
  3. The model object is sent to the view.
  4. The view uses the data to render HTML.
  5. The view is sent back to the user’s browser in an HTTP response.

Routing

The ASP.NET middleware you implemented in the previous chapter must be able to route incoming HTTP requests to a controller, since you are building an ASP.NET Core MVC appli­cation. The decision to send the request to a controller action is determined by the URL, and the configuration information you provide.

It is possible to define multiple routes. ASP.NET will evaluate them in the order they have been added. You can also combine convention-based routing with attribute routing if you need. Attribute routing is especially useful in edge cases where convention-based routing is hard to use.

One way to provide the routing configuration is to use convention-based routing in the Startup class. With this type of configuration, you tell ASP.NET how to find the controller’s name, action’s name, and possibly parameter values in the URL. The controller is a C# class, and an action is a public method in a controller class. A parameter can be any value that can be represented as a string, such as an integer or a GUID.

The configuration can be done with a Lambda expression, as an inline method:

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

ASP.NET looks at the route template to determine how to pull apart the URL. If the URL contains /Home, it will locate the HomeController class by convention, because the name begins with Home. If the URL contains /Home/Index, ASP.NET will look for a public action method called Index inside the HomeController class. If the URL contains /Home/Index/
123
, ASP.NET will look for a public action method called Index with an Id parameter inside the HomeController class. The Id is optional when defined with a question mark after its name. The controller and action names can also be omitted, because they have default values in the Route template.

Another way to implement routing is to use attribute routing, where you assign attributes to the controller class and its action methods. The metadata in those attributes tell ASP.NET when to call a specific controller and action.

Attribute routing requires a using statement to the Microsoft.AspNetCore.Mvc name­space.

[Route("[controller]/[action]")]
public class HomeController : Controller
{
}

Convention-Based Routing

In the previous chapter, you created a C# controller class named HomeController. A controller doesn’t have to inherit from any other class when returning basic data such as strings. You also implemented routing using the UseMvcWithDefaultRoute method, which comes with built-in support for default routing for the HomeController. When building an application with multiple controllers, you want to use convention-based routing, or attribute routing to let ASP.NET know how to handle the incoming HTTP re­quests.

Let’s implement the default route explicitly, first with a method and then with a Lambda expression. To set this up you replace the UseMvcWithDefaultRoute method with the UseMvc method in the Startup class. In the UseMvc method, you then either call a method or add a Lambda expression for an inline method.

Implement Routing

  1. Open the Startup class and locate the Configure
  2. Replace the UseMvcWithDefaultRoute method with the UseMvc method and add a Lambda expression with the default route template.

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

  1. Save the file and refresh the application in the browser. As you can see, the Index action was reached with the explicit URL /home/index.
  2. Now change to a URL without the action’s name, and only the controller’s name (/Home). You should still get the message from the action method, because you specified the Index action method as the default action in the routing template.
  3. Now call the root URL. A root URL is a URL with only the localhost and the port specified (https://localhost:xxxxx). This should also call the Index action because both Home and Index are declared as default values for the controller and the action in the routing template.

Adding Another Controller

Now that you have implemented default routing, it’s time to add another controller and see how you can reach that controller.

  1. Right click on the Controllers folder and select Add-Class.
  2. Name the controller EmployeeController and click the Add

public class EmployeeController
{
}

  1. Add an action method called Name that returns a string to the controller. Return your name from the method.

public string Name()
{
    return "Jonas";
}

  1. Add another action method called Country that also returns a string. Return your country of residence from the method.
  2. Save the file and switch to the browser. Try with the root URL first. This should take you to /Home/Index as defined in the default route.
  3. Change the URL to /Employee/Name; this should display your name in the browser. In my case Jonas.
  4. Change the URL to /Employee/Country; this should display your country of residence in the browser. In my case Sweden.
  5. Change the URL to /Employee. ASP.NET passes the request on to the Run middleware, which returns the string Hello from configuration, using the ConfigurationMessageService that you implemented earlier. The reason is that the EmployeeController class has no action method called Index, which is the name defined as the default action in the default route you added earlier to the Startup
  6. Add a new method called Index that returns the string Hello from Employee to the EmployeeController
  7. Save the file and refresh the application in the browser, or use the /Employee Now the text Hello from Employee should be displayed.

The complete code for the EmployeeController class:

public class EmployeeController
{
    public string Name()
    {
        return "Jonas";
    }

    public string Country()
    {
        return "Sweden";
    }

    public string Index()
    {
        return "Hello from Employee";
    }
}

Attribute Routing

Let’s implement an example of attribute routing, using the EmployeeController and its actions.

  1. Open the EmployeeController
  2. If you want the controller to respond to /Employee with attribute routing, you add the Route attribute above the controller class, specifying employee as its parameter value. You will have to bring in the AspNetCore.Mvc namespace for the Route attribute to be available.

[Route("employee")]
public class EmployeeController

  1. Save the file and navigate to the /Employee An exception is displayed in the browser. The reason for this exception is that ASP.NET can’t determine which of the three actions is the default action.
  2. To solve this, you can specify the Route attribute for each of the action methods, and use an empty string for the default action. Let’s make the Index action the default action, and name the routes for the other action methods the same as the methods.

[Route("")]
public string Index()
{
    return "Hello from Employee";
}

[Route("name")]
public string Name()
{
    return "Jonas";
}

[Route("country")]
public string Country()
{
    return "Sweden";
}

  1. Save the file and refresh the application in the browser. Make sure that the URL ends with /Employee. You should see the message Hello from Employee in the browser.
  2. Navigate to the other actions by tagging on the route name of the specific actions to the /Employee URL, for instance /Employee/Name. You should be able to navigate to them and see their information.
  3. Let’s clean up the controller and make its route more reusable. Instead of using a hardcoded value for the controller’s route, you can use the [controller] token that represents the name of the controller class (Employee in this case). This makes it easier if you need to rename the controller for some reason.

[Route("[controller]")]
public class EmployeeController

  1. You can do the same for the action methods, but use the [action] token instead. ASP.NET will then replace the token with the action’s name. Keep the empty Route attribute on the Index action and add the [action] token to a second Route attribute so that it has two routes; this will make it possible to use either the base route /Employees or the /Employees/Index route to reach the Index

[Route("")]
[Route("[action]")]
public string Index()
{
    return "Hello from Employee";
}

[Route("[action]")]
public string Name()
{
    return "Jonas";
}

  1. Save the file and refresh the application in the browser. Make sure that the URL ends with /Employee/Name. You should see your name in the browser. Test the other URLs as well, to make sure that they work properly.
  2. You can also use literals in the route. Let’s say that you want the route for the EmployeeController to be Company/Employee; you could then prepend the controller’s route with Company/.

[Route("company/[controller]")]
public class EmployeeController

  1. Save the file and refresh the application in the browser. Make sure that the URL ends with /Employee/Name. You will not see your name in the browser; instead ASP.NET displays the text from the Run The reason for this is that there isn’t a route to /Employee/Name anymore; it has changed to /Company/Employee/Name. Change the URL in the browser to /Company/Employee/Name. You should now see your name again.
  2. If you don’t want a default route in your controller, you can clean it up even more by removing all the action attributes and changing the controller route to include the [action] This means that you no longer can go to /Company/Employee and reach the Index action; you will have to give an explicit URL in the browser to reach each action.

[Route("company/[controller]/[action]")]
public class EmployeeController

  1. Remove all the Route attributes from the action methods and change the controller’s Route attribute to include the [action] Save the file and refresh the browser with the URL /Company/Employee/Name. You should now see your name.
  2. Now navigate to the /Company/Employee You should see the message from the Run middleware because ASP.NET couldn’t find any default action in the EmployeeController. Remember, you must give a specific URL with an action specified.

The complete code in the EmployeeController class:

[Route("company/[controller]/[action]")]
public class EmployeeController
{
    public string Name() { return "Jonas"; }
    public string Country() { return "Sweden"; }
    public string Index() { return "Hello from Employee"; }
}

IActionResult

The controller actions that you have seen so far have all returned strings. When working with actions, you rarely return strings. Most of the time you use the IActionResult return type, which can return many types of data, such as objects and views. To gain access to IActionResult or derivations thereof, the controller class must inherit the Controller class.

There are more specific implementations of that interface, for instance the ContentResult class, which can be used to return simple content such as strings. Using a more specific return type can be beneficial when unit testing, because you get a specific data type to test against.

Another return type is ObjectType, which often is used in Web API applications because it turns the result into an object that can be sent over HTTP. JSON is the default return type, making the result easy to use from JavaScript on the client. The data carrier can be config­ured to deliver the data in other formats, such as XML.

A specific data type helps the controller decide what to do with the data returned from an action. The controller itself does not do anything with the data, and does not write any­thing into the response. It is the framework that acts on that decision, and transforms the data into something that can be sent over HTTP. That separation of letting the con­troller decide what should be returned, and the framework doing the actual transformation, gives you flexibility and makes the controller easier to test.

Implementing ContentResult

Let’s change the Name action to return a ContentResult.

  1. Open the EmployeeController
  2. Remove the company/ prefic in the class’ route attribute.
  3. Have the EmployeeController class inherit the Controller

public class EmployeeController : Controller

  1. Change the Name action’s return type to ContentResult.

public ContentResult Name()

  1. Change the return statement to return a content object by calling the Content method, and pass in the string to it.

public ContentResult Name()
{
    return Content("Jonas");
}

  1. Save all files, open the browser, and navigate to the Employees/Name
  2. Your name should be returned to the browser, same as before.

Using a Model Class and ObjectResult

Using a model class, you can send objects with data and logic to the browser. By conven­tion, model classes should be stored in a folder called Models, but in larger applications it’s not uncommon to store models in a separate project, which is referenced from the application. A model is a POCO (Plain Old CLR Object or Plain Old C# Object) class that can have attributes specifying how the browser should behave when using it, such as checking the length of a string or displaying data with a certain control.

Let’s add a Video model class that holds data about a video, such as a unique id and a title. Typically you don’t hardcode a model into a controller action; the objects are usually fetched from a data source such as a database (which you will do in another chapter).

  1. Right click on the project node in the Solution Explorer and select Add-New Folder.
  2. Name the folder Models.
  3. Right click on the Models folder and select Add-Class.
  4. Name the class Video and click the Add
  5. Add an int property called Id. This will be the unique id when it is used as an entity in the database later.
  6. Add a string property called Title. Let’s keep it simple for now; you will add more properties later.

public class Video
{
    public int Id { get; set; }
    public string Title { get; set; }
}

  1. Open the HomeController
  2. Instead of returning a string from the Index action, you will change the return type to ObjectResult.

public ObjectResult Index()

  1. You need to add a using statement to the Models namespace to get access to the Video

using AspNetCore22Intro.Models;

  1. Create an instance of the Video model class and store it in a variable called model. Assign values to its properties when you instantiate it.

var model = new Video { Id = 1, Title = "Shreck" };

  1. Return an instance of the ObjectResult class passing in the model object as its parameter.

return new ObjectResult(model);

  1. Save all the files.
  2. Browse to the root URL or /Home. As you can see, the object has been sent to the client as a JSON object.

The complete code for the HomeController class:

public class HomeController : Controller
{
    public ObjectResult Index()
    {
        var model = new Video { Id = 1, Title = "Shreck" };
        return new ObjectResult(model);
    }
}

Introduction to Views

The most popular way to render a view from an ASP.NET Core MVC application is to use the Razor view engine. To render the view, a ViewResult is returned from the controller action using the View method. It carries with it the name of the view in the filesystem, and a model object if needed.

The framework receives that information and produces the HTML that is sent to the browser.

Let’s implement a view for the Index action and pass in a Video object as its model.

  1. Open the HomeController
  2. Change the return type of the Index action to ViewResult.

public ViewResult Index()

  1. Call the View method and pass in the model object that you created earlier.

return View(model);

  1. Save the file and refresh the application in the browser.
  2. By convention ASP.NET will look for a view with the same name as the action that produced the result. It will look in two places, both subfolders, to a folder called Views: the first is a folder with the same name as the controller class, the second a folder named Shared. In this case, there is no view for the Index action, so an exception will be thrown.
  3. To fix this you must add a view called Index. Right click on the project node in the Solution Explorer and select Add-New Folder to add a folder named Views.
  4. Right click on the Views folder and select Add-New Folder; name it Home.
  5. Right click on the Home folder and select Add-New Item.
  6. Select the Razor View template and click the Add button (it should be named Index by default).
  7. Delete everything in the cshtml view that was added.
  8. Type html and press the Tab key on the keyboard to insert a skeleton for the view.
  9. Add the text Video to the <title> element.

<title>Video</title>

  1. Although you can use the passed-in model and have it inferred from the actual object, it is in most cases better to explicitly specify it to gain access to IntelliSense and pre-compilation errors. You specify the model using the @model directive at the top of the view. Note that it should be declared with a lowercase m.

@model AspNetCore22Intro.Models.Video

  1. To display the value from the Title property in the <body> element, you use the @Model object (note the capital letter M, and that it is prefixed with the @-sign to specify that it is Razor syntax). The IntelliSense will show all properties available in the model object passed to the view.

<body>@Model.Title</body>

  1. Save the Index view and refresh the application in the browser. You should now see the video title in the browser and the text Video in the browser tab.

A View with a Data Collection

Now that you know how to display one video, it’s time to display a collection of videos. To achieve this, you’ll first have to create the video collection and then pass it to the view displaying the data. In the view, you’ll use a Razor foreach loop to display the data as HTML.

  1. Open the HomeController
  2. Replace the single Video object with a list of Video

var model = new List<Video>
{
    new Video { Id = 1, Title = "Shreck" },
    new Video { Id = 2, Title = "Despicable Me" },
    new Video { Id = 3, Title = "Megamind" }
};

  1. Switch to the browser and navigate to /Home/Index, or start the application without debugging (Ctrl+F5), if it’s not already started.
  2. An error message will appear, telling you that you are sending in a collection (list) of Video objects to the Index view, when it is designed for a single Video.
  3. To solve this, you will have to change the @model directive in the Index You can use the IEnumerable interface, which is a nice abstraction to many different collections.

@model IEnumerable<AspNetCore22Intro.Models.Video>

  1. When you change the @model directive, the @Model object no longer is a single instance of the Video class; you therefore must implement a loop to display the data in the model. Remove the @Model.Title property and add a table by typing table in the <body> element and press the Tab

<table>
    <tr>
        <td></td>
    </tr>
</table>

  1. Add a foreach loop around the <tr> element with Razor to loop over the Model Using Razor makes it possible to mix C# and HTML. Note that you don’t add the @-sign when already inside Razor code, but you use it when in HTML. Use the loop variable to add the Id and Title properties to the table row. The video variable in the loop doesn’t have an @-sign because the foreach loop has one. When the video variable is used in HTML, however, the @-sign must be used.

@foreach (var video in Model)
{
    <tr>
        <td>@video.Id</td>
        <td>@video.Title</td>
    </tr>
}

  1. Save all files, switch to the browser, and refresh the application. The three films should now be displayed.

The full code for the Index action:

public ViewResult Index()
{
    var model = new List<Video>
    {
        new Video { Id = 1, Title = "Shreck" },
        new Video { Id = 2, Title = "Despicable Me" },
        new Video { Id = 3, Title = "Megamind" }
    };

    return View(model);
}

The full markup for the Index view:

@model IEnumerable<AspNetCore22Intro.Models.Video>

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Video</title>
</head>
<body>
    <table>
        @foreach (var video in Model)
        {
            <tr>
                <td>@video.Id</td>
                <td>@video.Title</td>
            </tr>
        }
    </table>
</body>
</html>

Adding a Data Service

Hardcoding data in a controller is not good practice. Instead you want to take advantage of dependency injection to make data available in a constructor, using a service compo­nent, like the Message service you added earlier.

One big benefit of implementing a service is that its interface can be used to implement different components. In this book you will implement one for Mock data and one for a SQL Server database.

In this section, you will implement a MockVideoData component that implements an interface called IVideoData.

The data will be implemented as a List<Video>. Note that a List collection isn’t thread safe, and should be used with caution in web applications; but this code is for experimental purposes, and the component will only ever be accessed by one user at a time.

To begin with, the interface will only define one method, called GetAll, which will return an IEnumerable<Video> collection.

  1. Right click on the Services folder and select Add-New Item.
  2. Select the Interface template, name it IVideoData, and click the Add
  3. Add the public access modifier to the interface to make it publicly available.

public interface IVideoData
{
}

  1. Add a using statement to the Models namespace to get access to the Video

using AspNetCore22Intro.Models;

  1. Add a method called GetAll that returns an IEnumerable<Video>

IEnumerable<Video> GetAll();

  1. Right click on the Services folder and select Add-Class.
  2. Name the class MockVideoData and click the Add
  3. Add a using statement to the Models namespace to get access to the Video

using AspNetCore22Intro.Models;

  1. Implement the IVideoData interface in the class.

public class MockVideoData : IVideoData
{
    public IEnumerable<Video> GetAll()
    {
        throw new NotImplementedException();
    }
}

  1. Add a private read-only field called _videos of type IEnumerable<Video> to the class. This field will hold the video data, loaded from a constructor.

private readonly IEnumerable<Video> _videos;

  1. Add a constructor below the _videos field in the class. You can use the ctor snippet and hit the Tab

public MockVideoData()
{
}

  1. Open the HomeController class and copy the video list, then paste it into the MockVideoData Remove the var keyword and rename the model variable _videos to assign the list to the field you just added.

_videos = new List<Video>
{
    new Video { Id = 1, Title = "Shreck" },
    new Video { Id = 2, Title = "Despicable Me" },
    new Video { Id = 3, Title = "Megamind" }
};

  1. Remove the throw statement in the GetAll method and return the _videos

public IEnumerable<Video> GetAll()
{
    return _videos;
}

  1. Now that the service is complete, you must add it to the services collection in the Startup class’s ConfigureServices Previously you registered the IMessageService interface with the services collection using the AddSingleton method; this would ensure that only one instance of the defined class would exist. Let’s use another method this time. Register the IVideoData interface using the AddScoped method; this will ensure that one object is created for each HTTP request. The HTTP request can then flow through many services that share the same instance of the MockVideoData class.

services.AddScoped<IVideoData, MockVideoData>();

  1. Open the HomeController class and a using statement to the Services

using AspNetCore22Intro.Services;

  1. Add a private read-only field of type IVideoData called _videos on class level. This field will hold the data fetched from the service.

private readonly IVideoData _videos;

  1. Add a constructor to the HomeController class and inject the IVideoData interface into it. Name the parameter videos. Assign the videos parameter to the _videos field, inside the constructor. This will make the video service available throughout the controller.

public HomeController(IVideoData videos)
{
    _videos = videos;
}

  1. Replace the hardcoded List<Video> collection assigned to the model variable in the Index action, with a call to the GetAll method on the service.

var model = _videos.GetAll();

  1. Save all the files.
  2. Switch to the browser and refresh the application. You should now see the list of videos.

The complete code for the IVideoData interface:

public interface IVideoData
{
    IEnumerable<Video> GetAll();
}

 

The complete code for the MockVideoData class:

public class MockVideoData : IVideoData
{
    Private readonly List<Video> _videos;

    public MockVideoData()
    {
        _videos = new List<Video>
        {
            new Video { Id = 1, Genre = Models.Genres.Romance,
                Title = "Shreck" },

            new Video { Id = 2, Genre = Models.Genres.Comedy,
                Title = "Despicable Me" },

            new Video { Id = 3, Genre = Models.Genres.Action,
                Title = "Megamind" }
        };
    }

    public IEnumerable<Video> GetAll() { return _videos; }
}

 

The complete code for the ConfigureServices method in the Startup class:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddSingleton<IMessageService, ConfigurationMessageService>();
    services.AddScoped<IVideoData, MockVideoData>();
}

 

The complete code for the HomeController class:

public class HomeController : Controller
{
    private readonly IVideoData _videos;

    public HomeController(IVideoData videos)
    {
        _videos = videos;
    }

    public ViewResult Index()
    {

        var model = _videos.GetAll();
        return View(model);
    }
}

 

Summary

In this chapter, you learned about the MVC (Model-View-Controller) design pattern, and how the controller receives an HTTP request, gathers data from various sources, and creates a model, which is then processed into HTML by the view, along with its own markup.

You will continue to use MVC throughout the book and create Razor Views and more sophisticated views and models that can be used to view and edit data.

Stay connected with news and updates!

Join our mailing list to receive the latest news and updates from our team.
Don't worry, your information will not be shared.

Subscribe
Close

50% Complete

Two Step

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.