Dec 15, 2022
One of my latest side projects has been OpenModServer (which you can try out on mods.jacksonrakena.com), a kind of “all-in-one” mod website where users can download mods for many different games. It’s a bit of a learning project for me, and I’ve been using it to practice a lot of skills. I’m going to write about a few of those in this post.
One of the best features of ASP.NET, ever since those early days, has been its form validation suite. Right from the get-go, it’s an absolute breeze to use, and reduces the mental load of writing validation code. It’s also very powerful, and can be used to validate forms in a variety of ways.
Let’s go to an example:
<div class="mb-3">
<label asp-for="Changelog" class="form-label"></label>
<textarea asp-for="Changelog" class="form-control"></textarea>
<span asp-validation-for="Changelog" class="text-danger"></span>
</div>
This chunk of HTML hooks up a field, a label, and a validation span to the Changelog
field on the page model:
[BindProperty]
[MaxLength(2048)]
[Required]
public string Changelog { get; set; }
Let’s go through those attributes, and explain what they mean:
[BindProperty]
binds the field to the form. Without this, the field will not be populated when the form is submitted.[MaxLength(2048)]
limits the field to being no more than 2,048 characters.[Required]
means the field must have some content.You might think that these are done server-side, as they are expressed in the model. But you’d be wrong!
The asp-for
helper in the textarea
element actually generates code to tell the browser to validate the field:
<textarea class="form-control" data-val="true"
data-val-maxlength="The field Changelog must be a string or array type with a maximum length of '2048'."
data-val-maxlength-max="2048"
data-val-required="The Changelog field is required."
id="Changelog"
maxlength="2048"
name="Changelog">
</textarea>
So that’s cool. The browser does some validation (although the server does it too!), which saves us some server load from genuine users (by preventing this on the client), while preventing bad users from submitting bad data, by verifying it server-side too.
Entity Framework is a database ORM that is quite heavily abstracted - as in, it uses high-level C# language features and figures out what the developer wants to do, and converts that to raw SQL. It’s quite similar to ActiveRecord, from Ruby on Rails, but a fair bit more powerful and, more importantly, customizable.
Let’s take for example, this query:
// Search all mods
var modListings = await _database.ModListings
// Do an INNER JOIN on the users table where the Users.Id == ModListing.AuthorId
.Include(e => e.Creator)
// Sort by ModListings.DownloadCount, descending
.OrderByDescending(t => t.DownloadCount)
// Take the first 50 results
.Take(50)
// And finally, group by the game identifier
.GroupBy(d => d.GameIdentifier)
// This method actually sends the request to the database
.ToListAsync();
This is a rather extensive query, involving JOINs and filtering of the output data. One might think from the C# syntax that this is done client-side, but we can see from EF logs that it’s all done in the query:
SELECT [...]
FROM (
SELECT [...]
FROM mod_listings AS m
ORDER BY m.download_count DESC
LIMIT @__p_0
) AS t
INNER JOIN "AspNetUsers" AS a ON t.creator_id = a."Id"
ORDER BY t.game_identifier
This is a rather useful feature. Having to write SQL queries by hand can be a pain (although sometimes necessary to optimize), and switching mental contexts especially so. With EF, you can write your queries in C#, and EF will take care of the rest.
I made the executive decision to use Razor Pages more heavily instead of ASP.NET’s traditional Model-View-Controller pattern. Razor Pages somewhat combines the View and Controller parts of those, and allows for a more “page-oriented” approach to web development. This is a bit of a departure from the traditional MVC pattern, but I think it’s a good one.
This, however, posits an important problem. How do we have multiple actions on one page?
One answer with Razor Pages is called page handlers, and they’re pretty neat. They’re methods in your Razor Page model, that, when set in a <button>
, will be called when the button is clicked. ASP.NET automatically hooks up the required route and connects it to your method. Here’s an example:
These buttons are wired up using asp-page-handler
:
<button asp-page-handler="HandleApproval" type="submit"
class="btn-sm btn-success">Approve</button>
<button asp-page-handler="HandleDeletion" type="submit"
class="btn-sm btn-danger">Delete</button>
The referenced methods, HandleApproval
/HandleDeletion
, are defined in the page model:
public async Task<IActionResult> OnPostHandleDeletionAsync(string id)
{
// Parse the ID as a GUID
if (!Guid.TryParse(id, out var releaseId)) return RedirectToPage("/Files", new { Area = "Admin" });
// Search for the release
var release = await _database.ModReleases.FirstOrDefaultAsync(d => d.Id == releaseId);
if (release == null) return RedirectToPage("/Files", new { Area = "Admin" });
// Remove it from the database
_database.ModReleases.Remove(release);
// Tell the file manager to erase any stored data on disk
await _fileManagerService.DeleteModReleaseAsync(release);
// Save the database changes
await _database.SaveChangesAsync();
// Re-fetch the moderation queue, and re-render the same page
ModerationQueue = await _database.ModReleases
.Where(d => d.CurrentStatus == ModReleaseApprovalStatus.Unapproved)
.ToListAsync();
Releases = await _database.ModReleases.Include(c => c.ModListing).ToListAsync();
return Page();
}
The string id
parameter you see in the method is passed in through a hidden form field:
<input type="hidden" name="id" value="@release.Id" />
(Before you ask about authenticating the user, the page model already does that through [Authorize("Administrator")]
.)
left-pad
?ASP.NET isn’t perfect, and some of the things it does confuses me. Let’s go over them:
asp-route
attribute to a non-existent route, you’ll get a 404 and some odd behaviour, instead of a compile-time error. This is a bit of a pain, and I wish it was more strict on this. Rider does warn you about non-existent routes, though.So those are the big features that I’ve been enjoying in ASP.NET. After years of React/frontend-heavy development, it’s nice to be back in the minimal-to-no-JavaScript land. The page loading much quicker and not being as resource-intensive is a nice change of pace.