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.

results of endpoint

Conclusion and Checklist

When adding a new endpoint, we should go through the following steps:

  1. Check that we are registering our controllers and routes into our ASP.NET Core application.
  2. Inheriting from the appropriate Controller base class. While not necessary, it makes things much more straightforward.
  3. Decorate our endpoint methods with appropriate attributes of Route and HttpGet, HttpPost, etc.
  4. Make sure our Views are using appropriate models.
  5. 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.
  6. 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.