NOTE: Apart from
(and even then it's questionable, I'm Scottish). These are machine translated in languages I don't read. If they're terrible please contact me.
You can see how this translation was done in this article.
Friday, 16 August 2024
//Less than a minute
You can find all the source code for the blog posts on GitHub
Parts 1 & 2 of the series on adding Entity Framework to a .NET Core project.
Part 1 can be found here.
Part 2 can be found here.
In the previous parts we set up the database and the context for our blog posts, and added the services to interact with the database. In this post, we will detail how these services now work with the existing controllers and views.
Out controllers for Blogs are really pretty simple; in line with avoiding the 'Fat Controller' antipattern (a pattern we ideintified early in the ASP.NET MVC days).
I MVC frameworks a good practice is to do as little as possible in your controller methods. This is because the controller is responsible for handling the request and returning a response. It should not be responsible for the business logic of the application. This is the responsibility of the model.
The 'Fat Controller' antipattern is where the controller does too much. This can lead to a number of problems, including:
The blog controller is relatively simple. It has 4 main actions (and one 'compat action' for the old blog links). These are:
Task<IActionResult> Index(int page = 1, int pageSize = 5)
Task<IActionResult> Show(string slug, string language = BaseService.EnglishLanguage)
Task<IActionResult> Category(string category, int page = 1, int pageSize = 5)
Task<IActionResult> Language(string slug, string language)
IActionResult Compat(string slug, string language)
In turn these actions call the IBlogService
to get the data they need. The IBlogService
is detailed in the previous post.
In turn these actions are as follows
page
and pageSize
as parameters. This is for pagination. of the results.slug
of the post and the language
as parameters. THis is the method you're currently using for reading this blog post.category
, page
and pageSize
as parameters.slug
and language
as parameters.slug
and language
as parameters.As mentioned in an earlier post we implement OutputCache
and ResponseCahce
to cache the results of the blog posts. This improves the user experience and reduces the load on the server.
These are implemented using the appropriate Action decorators which specify the parameters used for the Action (as well as hx-request
for HTMX requests). For exampel with Index
we specify these:
[ResponseCache(Duration = 300, VaryByHeader = "hx-request", VaryByQueryKeys = new[] {nameof(page), nameof(pageSize)}, Location = ResponseCacheLocation.Any)]
[OutputCache(Duration = 3600, VaryByHeaderNames = new[] {"hx-request"} ,VaryByQueryKeys = new[] { nameof(page), nameof(pageSize)})]
The views for the blog are relatively simple. They are mostly just a list of blog posts, with a few details for each post. The views are in the Views/Blog
folder. The main views are:
_PostPartial.cshtml
This is the partial view for a single blog post. It is used within our Post.cshtml
view.
@model Mostlylucid.Models.Blog.BlogPostViewModel
@{
Layout = "_Layout";
}
<partial name="_PostPartial" model="Model"/>
_BlogSummaryList.cshtml
This is the partial view for a list of blog posts. It is used within our Index.cshtml
view as well as in the homepage.
@model Mostlylucid.Models.Blog.PostListViewModel
<div class="pt-2" id="content">
@if (Model.TotalItems > Model.PageSize)
{
<pager
x-ref="pager"
link-url="@Model.LinkUrl"
hx-boost="true"
hx-push-url="true"
hx-target="#content"
hx-swap="show:none"
page="@Model.Page"
page-size="@Model.PageSize"
total-items="@Model.TotalItems"
class="w-full"></pager>
}
@if(ViewBag.Categories != null)
{
<div class="pb-3">
<h4 class="font-body text-lg text-primary dark:text-white">Categories</h4>
<div class="flex flex-wrap gap-2 pt-2">
@foreach (var category in ViewBag.Categories)
{
<a hx-controller="Blog" hx-action="Category" hx-push-url="true" hx-get hx-target="#contentcontainer" hx-route-category="@category" href>
<span class="inline-block rounded-full dark bg-blue-dark px-2 py-1 font-body text-sm text-white outline-1 outline outline-green-dark dark:outline-white">@category</span>
</a>
}
</div>
</div>
}
@foreach (var post in Model.Posts)
{
<partial name="_ListPost" model="post"/>
}
</div>
This uses the _ListPost
partial view to display the individual blog posts along with the paging tag helper which allows us to page the blog posts.
_ListPost.cshtml
The _Listpost partial view is used to display the individual blog posts in the list. It is used within the _BlogSummaryList
view.
@model Mostlylucid.Models.Blog.PostListModel
<div class="border-b border-grey-lighter pb-8 mb-8">
<a asp-controller="Blog" asp-action="Show" hx-boost="true" hx-swap="show:window:top" hx-target="#contentcontainer" asp-route-slug="@Model.Slug"
class="block font-body text-lg font-semibold transition-colors hover:text-green text-blue-dark dark:text-white dark:hover:text-secondary">@Model.Title</a>
<div class="flex space-x-2 items-center py-4">
@foreach (var category in Model.Categories)
{
<a hx-controller="Blog" hx-action="Category" class="rounded-full bg-blue-dark font-body text-sm text-white px-2 py-1 outline outline-1 outline-white" hx-push-url="true" hx-get hx-target="#contentcontainer" hx-route-category="@category" href>@category
</a>
}
@{ var languageModel = (Model.Slug, Model.Languages, Model.Language); }
<partial name="_LanguageList" model="languageModel"/>
</div>
<div class="block font-body text-black dark:text-white">@Model.Summary</div>
<div class="flex items-center pt-4">
<p class="pr-2 font-body font-light text-primary light:text-black dark:text-white">
@Model.PublishedDate.ToString("f")
</p>
<span class="font-body text-grey dark:text-white">//</span>
<p class="pl-2 font-body font-light text-primary light:text-black dark:text-white">
@Model.ReadingTime
</p>
</div>
</div>
As you'll se here we have a link to the individual blog post, the categories for the post, the languages the post is available in, the summary of the post, the published date and the reading time.
We also have HTMX link tags for the categories and the languages to allow us to display the localized posts and the posts for a given category.
We have two ways of using HTMX here, one which gives the full URL and one which is 'HTML only' (i.e. no URL). This is because we want to use the full URL for the categories and the languages, but we don't need the full URL for the individual blog post.
<a asp-controller="Blog" asp-action="Show" hx-boost="true" hx-swap="show:window:top" hx-target="#contentcontainer" asp-route-slug="@Model.Slug"
This approach populates a full URL for the individual blog post and uses hx-boost
to 'boost' the request to use HTMX (this sets the hx-request
header to true
).
<a hx-controller="Blog" hx-action="Category" class="rounded-full bg-blue-dark font-body text-sm text-white px-2 py-1 outline outline-1 outline-white" hx-push-url="true" hx-get hx-target="#contentcontainer" hx-route-category="@category" href>@category
</a>
Alternatively this approach uses the HTMX tags to get the categories for the blog posts. This uses the hx-controller
, hx-action
, hx-push-url
, hx-get
, hx-target
and hx-route-category
tags to get the categories for the blog posts while hx-push-url
is set to true
to push the URL to the browser history.
It is also used within our Index
Action method for the HTMX requests.
public async Task<IActionResult> Index(int page = 1, int pageSize = 5)
{
var posts =await blogService.GetPagedPosts(page, pageSize);
if(Request.IsHtmx())
{
return PartialView("_BlogSummaryList", posts);
}
posts.LinkUrl = Url.Action("Index", "Blog");
return View("Index", posts);
}
Where it enables us to either return the full view or just the partial view for HTMX requests, giving a 'SPA' like experience.
In the HomeController
we also refer to these blog services to get the latest blog posts for the home page. This is done in the Index
action method.
public async Task<IActionResult> Index(int page = 1,int pageSize = 5)
{
var authenticateResult = GetUserInfo();
var posts =await blogService.GetPagedPosts(page, pageSize);
posts.LinkUrl= Url.Action("Index", "Home");
if (Request.IsHtmx())
{
return PartialView("_BlogSummaryList", posts);
}
var indexPageViewModel = new IndexPageViewModel
{
Posts = posts, Authenticated = authenticateResult.LoggedIn, Name = authenticateResult.Name,
AvatarUrl = authenticateResult.AvatarUrl
};
return View(indexPageViewModel);
}
As you'll see in here we use the IBlogService
to get the latest blog posts for the home page. We also use the GetUserInfo
method to get the user information for the home page.
Again this has an HTMX request to return the partial view for the blog posts to allow us to page the blog posts in the home page.
In our next part we'll go into excruciating detail of how we use the IMarkdownBlogService
to populate the database with the blog posts from the markdown files. This is a key part of the application as it allows us to use the markdown files to populate the database with the blog posts.