I’ve designed this post for folks new to ASP.NET Core web development and want to learn the basic steps it takes to add a new endpoint to an existing ASP.NET Core MVC application. Along the way, we’ll explain the purpose of each step until we have a new endpoint. Developers can also use this guide to diagnose issues where an endpoint isn’t accessible, and they’re not sure why.
Note: This post assumes you’ve started from the ASP.NET Core Web App template for MVC.
Step 0. The Project SDK
A lot has changed in .NET development, especially how we pull dependencies into our solution. Starting from a template, we’ll get a running web application. Still, we may accidentally begin from a console application or class library that we wanted to be an ASP.NET Core web application.
To ensure that our project is ready to be a web application, we need to import the proper SDK. To do that, inside our project’s .csproj
file, we need to set the appropriate value for the Sdk
attribute on our Project
element to Microsoft.NET.Sdk.Web
.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
</Project>
Issues of missing types and namespaces in a project can be related to this particular mistake.
Step 1. In Startup.cs
An ASP.NET Core MVC is a convention-based approach to building web applications. While there are default conventions that come with the framework, we can change them to suit our needs.
First, since all controllers are built (newed up) by the service locator within ASP.NET Core, we need to have the framework scan our project and register all Controller
types. Registering controllers is accomplished in the ConfigureServices
method in our Startup
class.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
}
Next, we’ll want to change how routes are registered. By default, there is a conventional route pattern.
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
While this works, I do not recommend it as the simplicity of this route definition becomes a liability as a web application grows in complexity. We’ll want to change this line to the following.
endpoints.MapControllers();
Let’s get to defining a new route in our web application!
Step 2. Routes In A Controller
We removed the default controller route in favor of explicit routes defined in our Controller
classes. A controller is a particular class that typically inherits from Controller
and has at least one public method that maps to public endpoints. Most starting applications will have a HomeController
.
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel {RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier});
}
}
Let’s start by fixing the HomeController
to have explicit routes for Index
, Privacy
, and Error
. Here we’ll be using two attributes on each method. First, we’ll apply a Route
attribute to each specify the path of each.
public class HomeController : Controller
{
[Route("")]
public IActionResult Index()
{
return View();
}
[Route("privacy")]
public IActionResult Privacy()
{
return View();
}
[Route("error")]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel {RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier});
}
}
We could stop here, but for the sake of clarity, we’ll also add explicit method attributes to limit the kinds of requests that can access these endpoints. All these endpoints are accessible only through GET
web requests, so we’ll use the HttpGet
attribute to limit it to that method.
public class HomeController : Controller
{
[HttpGet, Route("")]
public IActionResult Index()
{
return View();
}
[HttpGet, Route("privacy")]
public IActionResult Privacy()
{
return View();
}
[HttpGet, Route("error")]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel {RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier});
}
}
Cool! Let’s create a brand new endpoint that accepts information from our Index
view via an HTML Form.
Step 3. New POST Endpoint
Since we’re building a web application, we’ll want users to interact with our user interface. Let’s start by creating a brand new class that represents the data we want from a user. In this case, we want the name of their best friend.
Under the Models
directory, we can create a new file called BestFriend.cs
with the following contents.
public record BestFriend(string Name);
Next, in our HomeController
, we’ll add a new endpoint using the same constructs in upgrading our controller. The new endpoint will use an HttpPost
as opposed to HttpGet
. We’ll also pull the value for our best friend’s name on the initial GET
request.
[HttpGet, Route("")]
public IActionResult Index()
{
var model = new BestFriend(TempData["BestFriend"] as string);
return View(model);
}
[HttpPost, Route("")]
public IActionResult Index([FromForm] BestFriend friend)
{
// storing the value
TempData["BestFriend"] = friend.Name;
return RedirectToAction("Index");
}
ASP.NET Core requires us to specify what sources model binding will use to populate our method arguments. In this case, the friend
argument will come from a form sent from an HTML page. We use the FromForm
attribute on the argument we want populated.
Let’s see how we set that up.
Step 4. Updating Our View With A Form
Since we are passing our Index
view our BestFriend
model, we’ll want to tell our view to expect that type. We can do that using the @model
directive at the top of the page. In the Index.cshtml
file under the path of Views/Home
, we’ll add the following to the top of the file.
@model BestFriend
Next, we’ll need to construct an HTML form to POST
to our new endpoint. Let’s modify our view to add a form, text input, and a submit button. We’ll also take this time to output the value once submitted to our POST
endpoint.
@model BestFriend
@{
ViewData["Title"] = "Home Page";
}
@if (!string.IsNullOrWhiteSpace(Model?.Name))
{
<h1>My Best Friend is @Model.Name!</h1>
}
<form asp-action="Index" method="post">
<label asp-for="Name"></label>
<input type="text" asp-for="Name" />
<button type="submit">Submit</button>
</form>
That’s it! We should now have the ability to start our application and send that information to our new endpoint.
Conclusion and Checklist
When adding a new endpoint, we should go through the following steps:
- Check that we are registering our controllers and routes into our ASP.NET Core application.
- Inheriting from the appropriate
Controller
base class. While not necessary, it makes things much more straightforward. - Decorate our endpoint methods with appropriate attributes of
Route
andHttpGet
,HttpPost
, etc. - Make sure our Views are using appropriate models.
- Our HTML forms (or clients) are sending back the appropriate information in a protocol we expect. In our case, we used an HTML form, but we could also use JSON.
- Our endpoints that accept data specify the location to look for values. In this example, we used
FromForm
.
From here, the sky’s the limit to building an ASP.NET Core MVC application as the typical patterns are tediously similar. I wish you luck on your ASP.NET Core journey, and if you have any questions, please let me know.