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
//9 minute read
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.cshtmlThis 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.cshtmlThis 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.cshtmlThe _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.