An ASP.NET 5 MVC controller handles user requests to your site. Recall that the routing engine uses the URL convention to route a request to your controller and on to a method on that controller. For example, a request for ./customer/edit/5
will, by default, route to your CustomerController.Edit(id)
method; the value 5 will be passed as the id
parameter.
The controller method is then responsible for connecting the request to your model and returning the appropriate view result. The Edit(id)
method, for example, will likely use a DbContext
(created in the prior section) to find a specific customer from the database.
The controller would pass the Customer
instance from the model to the Customer/Edit view page and return a ViewResult
. Finally, the runtime would then likely look for a view called Edit inside the Views/Customer
folder. As part returning results, ASP.NET would generate on the server, based on the view code, the appropriate HTML and JavaScript to be sent back as the response to the user machine.
This section covers creating controllers, working with the model, and returning an IActionResult
to let MVC generate the right web response.
An action method on an ASP.NET 5 MVC controller should return results that can be processed by ASP.NET and sent back to the user’s browser as a valid HTTP response. For the most part, these methods should return an object that implements either the IActionResult
or IViewComponentResult
interface (found in the Microsoft.AspNet.Mvc
namespace). The former includes classes used to return HTML and JavaScript (ViewResults
, PartialViewResult
), JSON formatted messages (JsonResult
), Files (FileResult
, FileContentResult
), and other result objects. The latter is new to ASP.NET 5 MVC. It is used to return a portion of the response. (See the section “View Components, View Models, and Partial Views.”)
You typically define your controller action methods as returning one of these interfaces (IActionResult
being the most common). You then return an actual object that implements the interface as the response to the MVC runtime. When you do so, you use a method on the Controller
class from which your controller inherits. It contains method names that match the action result objects, minus the word Result, as in View()
, Json()
, Redirect()
, Content()
, and so on.
These methods know how to act on your behalf. They find the view, often find a model, and return the related action result object. The MVC framework defines many of these action result classes as listed here:
ContentResult
—Used to return a custom content type as the result of the action method. This is an HTTP content type, such as text/plain.
EmptyResult
—Used to return nothing (void) as the result.
FileResult (FileContentResult, FilePathResult, FileStreamResult)
—Used to send back binary output (and files) as the response.
HttpNotFoundResult
—Used to send an HTTP status indicating that the requested resource was not found.
HttpStatusCodeResult
—Used to send a specific HTTP status code (from the many available) as the result.
JsonResult
—Used to return a message formatted as JSON.
NoContentResult
—Used to indicate that the response has no actual content.
ObjectResult
—Used to return an object as the result. (See Chapter 19.)
RedirectResult
—Used to redirect to another URI.
RedirectToRouteResult (RedirectToActionResult)
—Used to redirect to another action method.
PartialViewResult
—Used to send a section of a partial view (portion of HTML) to be rendered inside another view.
ViewViewcomponentResult
—Used to return a view component (portion of the response) as a result (implements IViewComponentResult
).
ViewResult
—Used to return a web page generated from a view.
Let’s now take a look at using these action result objects inside a controller.
Visual Studio 2015 provides an ASP.NET 5 MVC template for adding a controller to your project. This template already derives from Controller
. You store controllers in the Controllers
folder. When creating a new controller, you use the naming standard FeatureAreaController
, where FeatureArea
is the name of your feature (in our example, CustomerController
). The following walks you through creating a controller:
1. Right-click the Controllers
folder in Solution Explorer. Choose Add, New Item.
2. From the Add New Item dialog (see Figure 17.22), select MVC Controller Class. Name your controller CustomerController.cs
. Click the Add button to continue.
3. Visual Studio creates a basic controller with an Index()
method that returns an IActionResult
(in this case, a ViewResult
). Listing 17.7 shows an example.
using Microsoft.AspNet.Mvc;
namespace AspNet5AppSample.Controllers
{
public class CustomerController : Controller
{
//GET: /<controller>/
public IActionResult Index()
{
return View();
}
}
}
The CustomerController
class will work closely with the model (your data access classes) to handle requests to read and write customer data. Therefore, the class needs an instance of your DbContext
object, CustomerDbContext
. Recall that you added this to the request pipeline earlier using the Startup.cs
file ConfigureServices
method. ASP.NET will pass this content to your controller provided you request it in the constructor. The following walks you through creating this code:
1. Open the CustomerController
class file from Solution Explorer.
2. Add a using
statement at the top of the class file for AspNet5Unleashed.Models
.
3. Define a class-level variable to hold an instance of CustomerDbContext
. Name this variable db
, as in the following:
private CustomerDbContext db;
4. Create a constructor at the top of the class (under the class definition). You can use the snippet ctor
as a shortcut.
Indicate that the constructor takes an instance of CustomerDbContext
and then assigns that to the class-level variable db
as in the following:
public CustomerController(CustomerDbContext context)
{
db = context;
}
ASP.NET will now provide the database context object as part of the request when creating an instance of your controller. The next step is to define actual action methods on the controller for returning, editing, creating, and deleting customers. The following sections examine each in turn.
We will use the Index
action method of the controller to return a list of customers from the model and pass it to a view page called Index.cshtml
(which we will create later). This is a simple method. Remember, we already set up the database context inside a variable called db
. Therefore, the following is all that is needed for the Index()
action method.
public IActionResult Index()
{
return View(db.Customers);
}
This method is set to return an IActionResult
instance. This is common for most of your action methods (see the “The Result Objects” section) and indicates the result should be an action. In this case, the action will be to create a view. Therefore, the code uses the Controller.View()
method to return a ViewResult
instance. The actual view page is not named because you can rely on the framework to follow convention and look for a view called Index
in the Views/Customer
folder.
Notice that a list of customers is passed to the view using the model and data context. The view, as you’ll see later, will be strongly typed to expect a list of customers.
Likely, the most simplistic controller method is one that only returns a view. In our example, a user requests to create a new customer. The data context is not needed for this (until the form is filled out and posted back to the server anyway). Therefore, you simply return a View. The following shows an example.
public IActionResult Create()
{
return View();
}
In this case, ASP.NET will look for a view called Create.cshtml
in the Views/Customer
folder. The view itself will know to bind to the Customer
class for outputting validation rules and error messages. It will then return a view with essentially a blank customer so the user can enter new customer information.
We will now create an Edit(id)
method to find a customer in the database using the Id
property and return an Edit.cshtml
view page (which we will also create later). This id
parameter is passed to the action method from the URL using the ASP.NET routing engine convention. For example, a user may the request ./customer/edit/2
. The value 2 will be passed to CustomerController.Edit(id)
as a parameter.
The first step is to add a using
statement to the top of the CustomerController
class to add support for the LINQ query engine. The following shows an example.
using System.Linq;
Next, we write the method. The method should use LINQ to look up a customer from the DbContext
. If it exists, it will return a ViewResult
object. If not, it returns the HttpNotFound
error (resulting in an HTTP 404 error page). An example of the code is shown here.
public IActionResult Edit(int id)
{
Customer customer =
db.Customers.FirstOrDefault(x => x.Id == id);
if (customer == null)
{
return HttpNotFound();
}
return View(customer);
}
So far we have looked at action methods that simply return views. These action methods are called as part of HTTP GET
requests. In the case of Edit(id)
and Create()
, a user must fill out a form. The form is then sent back to the server as an HTTP POST
. What is needed then are action methods to handle these POST
requests.
The convention is to define these action methods with the same name as their corresponding request action method. The difference is that these action methods include the attribute HttpPost
to indicate the method should be called as part of an HTTP POST
request. These action methods also take different parameters. The Create
and Edit
methods, for example, take a Customer
object.
The following code shows an example of the additional Edit
method.
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(
[Bind(new string[] { "Id", "Name", "Email", "OptInEmail", "Notes"})]
Customer customer)
{
if (ModelState.IsValid)
{
db.Customers.Update(customer);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(customer);
}
The Edit
method uses the Bind
model binder attribute to map form post fields to the Customer
instance properties explicitly. This is a security precaution to prevent something called over posting. Over posting occurs when you maintain a property on your object that you do not expose on the form. This allows someone to post extra data to your form and, if lucky enough, set properties on your object you did not intend for him to be able to set. Using Bind
is essentially whitelisting which properties you allow to be set; only those form fields will be bound to the Customer
instance passed as a parameter to the action
method. That is not required in this case because we allow all properties to be written. However, it is a good practice, and consistency is key. You may add a property later you do not want to bind, for example.
Notice that inside the method we first check to confirm the model state is valid. This executes the business rules you added to the model class as data annotations. If not valid, the Edit.cshtml
view is returned and the errors are shown to the user. If the model is valid, we use the Update
method to mark the customer for update and SaveChanges
to commit the update to the database. Finally, upon completion, we return the user to the Customer/Index.cshtml
page using the RedirectToAction
action method.
Cross Site Request Forgery
Notice that the Edit
method is decorated with the ValidateAntiForgeryToken
attribute. This is to help prevent the security threat Cross Site Request Forgery (CSRF). CSRF is when another application tries to post to your controller. This attribute, along with the AntiForgeryToken()
HTML helper in the view, helps validate calls to your controller.
Handling a HTTP POST for a new customer is nearly the same as handling a POST for customer edit. In fact, the only difference is the call to the DbContext
; instead of Update
, we use Add
. The following shows the Create
method for the Customer
controller.
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(
[Bind(new string[] { "Name", "Email", "OptInEmail", "Notes"})]
Customer customer)
{
if (ModelState.IsValid)
{
db.Customers.Add(customer);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(customer);
}
The last method we will add to the CustomerController
is a method to handle a request to delete a customer. The method is similar to the one that simply returns a customer based on Id
. In this case, the call will take an id
parameter, use it to find the customer instance, and then use the DbContext
to remove this customer. The following shows the code.
public IActionResult Delete(int id)
{
Customer customer =
db.Customers.FirstOrDefault(x => x.Id == id);
db.Customers.Remove(customer);
db.SaveChanges();
return RedirectToAction("Index");
}
Note that in an actual application, you may not want to make it this easy to delete data. Instead, you might require this via a form submit (and HTTP POST
), only by an authorized user, and provide an “are you sure” message for committing the data deletion.
You now have a model, a database, and a controller for working with customer objects. We have not, however, created views yet. Therefore, you cannot simply run this code and see your results. Let’s take a look at creating the views.