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, Guid
s 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.