First Look into ASP.NET Core MVC - Notes Management App

First Look into ASP.NET Core MVC - Notes Management App

This post showcases Noter - an ASP.NET Core MVC app for managing notes.

A note consists of a title and content.

This post comes from the perspective of a complete beginner in MVC. As such, certain details may be overlooked, while others explained excessively.

Styling (i.e. applying CSS) is intentionally left for later. Instead, the focus is on basic functionality: viewing, editing, and creating notes, as well as navigating between pages.

Access the project’s source code on GitHub 📁, check the initial state 0️⃣, explore the pull request ➡️, and view the final state 1️⃣.

Data Generation

Before diving into MVC, the app needs some sample data. This is accomplished in the first commit.

In brief, the commit goes through EF-Core routine: setting up entities, data context, and migrations. The database connection string is stored in user secrets.

The Noter.UI project is created using the ASP.NET Core Web App (Model-View-Controller) template. It is required at this stage only for the Microsoft.EntityFrameworkCore.Tools package.

The commit also sets up a script to generate 100k sample notes with Bogus and import them into the database. The data import script is borrowed from here - which comes from the exploration on data import into Cosmos DB.

100k is a good number. The data generation and import are relatively fast (~20 seconds), while leaving no choice but to implement pagination when retrieving data.

The result can verified by connecting to the database via, for example, Azure Data Studio and executing a query:

Views Step 1: Navigation

The second commit sets up the three components required for navigation - the view Index.cshmtl, the controller NoteController, and the anchor element enabling navigation:

<a asp-controller="Note" asp-action="Index">Notes</a>

The anchor element ensures that clicking on "Notes" calls the Index action in the Note controller, which then redirects the browser to the Index page.

"Index" is a conventional name for a view, which displays all entities of the relevant type. In this case, it will be a table of notes.

Here is the resulting look in the browser:

Views Step 2: Basic Table

The third commit sources the data and displays it in a table. As there are too many notes, only top 20 are retrieved.

The LINQ query is transformed to the following SQL:

SELECT TOP(@__p_0) [n].[Id], [n].[Title], SUBSTRING([n].[Content], 0 + 1, 200) AS [ContentPreview]
FROM [Notes] AS [n]

The Substring method ensures that only previews rather than full contents are retrieved from the database.

As of EF-Core version 9.0.1, the Substring method has to be inside the LINQ query.

In particular, adding a property such as public string ContentPreview => Content[..Math.Min(200, Content.Length)]; to the Note entity does not work. The generated SQL retrieves the full contents:

SELECT TOP(@__p_0) [n].[Id], [n].[Title], [n].[Content]
FROM [Notes] AS [n]

Here is the result:

Views Step 3: Pagination

The fourth commit adds pagination. When a page pageNumber is requested, (pageNumber - 1) * PageSize notes are skipped, and the next PageSize are retrieved.

Pagination requires sorting, which, in this case, is achieved by ordering Guid ids. However, Guids are not ideal for sorting because the inserted position of a newly created entity is unpredictable, making it difficult to find. The issue is of no importance at the moment, as such, it is noted, acknowledged, and ignored.

A notable feature of the Page action is redirection to itself when the page number is outside the bounds:

if (pageNumber < 1)
{
	return RedirectToAction("Page", new { pageNumber = 1 });
}

int notesCount = await dataContext.Notes.CountAsync();
int totalPages = notesCount == 0 ? 1 : (notesCount + PageSize - 1) / PageSize;
if (pageNumber > totalPages)
{
	return RedirectToAction("Page", new { pageNumber = totalPages });
}

Correcting the page number in this way rather through variable assignment ensures that the page number in the browser address bar is correct.

At this point, all 100k notes can be browsed page by page:

Actions

The fifth commit implements the note details view.

In general, "details" is a conventional name for a view, which displays everything about a single entity. For notes, it is the title and content:

<h1>@Model.Title</h1>

@Html.Raw(Model.Content?.Replace("\n", "<br/>"))

The sixth commit implements the edit view, and the seventh commit implements the create view. Both are nearly identical.

Edit view:

@model NoteViewModels.Edit

<a asp-action="Details" asp-route-id="@Model.Id">Back</a>

<form method="post" asp-action="Edit">
    <div asp-validation-summary="All"></div>
    <input type="submit" value="Update"/>
    <input type="hidden" asp-for="Id"/>
    <input asp-for="Title" style="width:100%"/>
    <textarea asp-for="Content" style="width:100%; height: 500px"></textarea>
</form>

Create view:

@model NoteViewModels.Create  
  
<form method="post" asp-action="Create">  
    <div asp-validation-summary="All"></div>  
    <input type="submit" value="Create"/>  
    <input asp-for="Title" style="width:100%"/>  
    <textarea asp-for="Content" style="width:100%; height: 500px"></textarea>  
</form>

Both edit and create views are implemented via forms. Forms require two controller actions - get to load the form and post to submit the form. Since controllers are stateless, the id property has to get passed to the view. It is marked "hidden" so that it remains unmodified.

Two different HTML elements are used for title and content inputs - input and textarea. The difference between them is that input is designed for a single-line input, while textarea for multi-line input. As a result, pressing the Enter/Return key within input submits the form, while pressing it within textarea simply moves the cursor to the next line.

The view model properties used in forms must be nullable. When the title or content inputs are empty, they remain unassigned instead of being set to an empty string. This causes type mismatches. Non-nullable objects end up being null, which leads to runtime errors. Making the properties nullable ensures they are checked for null in controller actions.

Conclusion

This post walks through the step-by-step creation of a simple MVC note management app. With the core functionality in place, possible future improvements may include styling and authentication.

Read more