Adding Entity Framework for Blog Posts (Part 5) (English)

Adding Entity Framework for Blog Posts (Part 5)

Comments

NOTE: Apart from English (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.

Sunday, 18 August 2024

//

Less than a minute

See parts 1 and 2 and 3 and 4 for the previous steps.

Introduction

In previous parts we covered how to set up the database, how our controllers and views are structured, how our services worked, and how to seed the database with some initial data. In this part we'll cover details on how the EF Based services work and how we can use them in our controllers.

As usual you can see all the source for this on my GitHub here, in the Mostlylucid/Blog folder.

Blog Services

File Based Services

Previously we used a MarkdownBlogService to get our blog posts and languages. This service was injected into our controllers and views. This service was a simple service that read markdown files from disk and returned them as BlogViewModels.

This used a static Dictionary to hold the blog posts then returned results from that Dictionary.

  public async Task<PostListViewModel> GetPagedPosts(int page = 1, int pageSize = 10, string language = EnglishLanguage)
    {
        var model = new PostListViewModel();
        var posts = GetPageCache().Where(x => x.Value.Language == language)
            .Select(x => GetListModel(x.Value)).ToList();
        model.Posts = posts.OrderByDescending(x => x.PublishedDate).Skip((page - 1) * pageSize).Take(pageSize).ToList();
        model.TotalItems = posts.Count();
        model.PageSize = pageSize;
        model.Page = page;
        return await Task.FromResult(model);
    }

This is the GetPagedPosts method from the MarkdownBlogService. This method gets the blog posts from the cache and returns them as a PostListViewModel.

Using Files for storing Markdown files is still a good approach, it makes it simple to add posts (I just save markdown files to disk and check them in) and it's easy to manage. But we want to use the database to store the posts and languages.

flowchart LR A[Write New Markdown File] -->|Checkin To Github| B(Github Action Triggers) --> C(Builds Docker Image) --> D(Watchtower Pulls new Image) --> E(Site Updated)

EF Based Services

In the [previous part]((/blog/addingentityframeworkforblogpostspt4) I showed how we seeded the database with the blog data. This updates each time we redeploy and restart the docker container (using watchtower ) We used a EFBlogPopulator class to do this.

Now our flow looks like this

flowchart TD A[Write New Markdown File] -->|Checkin To Github| B(Github Action Triggers) B --> C(Builds Docker Image) C --> D(Watchtower Pulls new Image) D --> E{New Files Added?} E -->|Yes| F(Add File to Database) E -->|No| G(Skip Database Seeding) F --> H(Site Updated) G --> H(Site Updated)

Now that we have the blog posts in our database we use the EFBlogService to supply the implementation for out IBlogService interface:

public interface IBlogService
{
   Task<List<string>> GetCategories();
    Task<List<BlogPostViewModel>> GetPosts(DateTime? startDate = null, string category = "");
    Task<PostListViewModel> GetPostsByCategory(string category, int page = 1, int pageSize = 10, string language = MarkdownBaseService.EnglishLanguage);
    Task<BlogPostViewModel?> GetPost(string slug, string language = "");
    Task<PostListViewModel> GetPagedPosts(int page = 1, int pageSize = 10, string language = MarkdownBaseService.EnglishLanguage);
    
    Task<List<PostListModel>> GetPostsForLanguage(DateTime? startDate = null, string category = "", string language = MarkdownBaseService.EnglishLanguage);
}

This is the IBlogService interface. This is the interface that our controllers use to get the blog posts. The EFBlogService implements this interface and uses the BlogContext to get the data from the database. As with out FileBased service above we can get posts by category, by language, by date, and paged.

GetPostList

    private async Task<PostListViewModel> GetPostList(int count, List<BlogPostEntity> posts, int page, int pageSize)
    {
        var languages = await NoTrackingQuery().Select(x =>
                new { x.Slug, x.LanguageEntity.Name }
            ).ToListAsync();

        var postModels = new List<PostListModel>();

        foreach (var postResult in posts)
        {
            var langArr = languages.Where(x => x.Slug == postResult.Slug).Select(x => x.Name).ToArray();

            postModels.Add(postResult.ToListModel(langArr));
        }

        var postListViewModel = new PostListViewModel
        {
            Page = page,
            PageSize = pageSize,
            TotalItems = count,
            Posts = postModels
        };

        return postListViewModel;
    }

Here we use our common PostsQuery but we add NoTrackingQuery which is a simple method that returns a queryable of the BlogPostEntity but with AsNoTrackingWithIdentityResolution added. This means that the entities are not tracked by the context and are read only. This is useful when we are just reading data and not updating it.

     protected IQueryable<BlogPostEntity> PostsQuery()=>Context.BlogPosts.Include(x => x.Categories)
        .Include(x => x.LanguageEntity);
     
         private IQueryable<BlogPostEntity> NoTrackingQuery() => PostsQuery().AsNoTrackingWithIdentityResolution();

You can see that we also get the languages for the posts and then create a PostListViewModel which is a structure which accepts paging information (Page, PageSize and TotalItems) and is returned to the controller.

GetPost

Our main method is the GetPost method which gets a single post by its Slug and Language. This is a simple method that uses the PostsQuery to get the post and then returns it as a BlogPostViewModel. You can see that it also has an optional Language parameter which defaults to EnglishLanguage which is a constant in our MarkdownBaseService class.

  public async Task<BlogPostViewModel?> GetPost(string slug, string language = "")
    {
        if (string.IsNullOrEmpty(language)) language =MarkdownBaseService.EnglishLanguage;
        var post = await NoTrackingQuery().FirstOrDefaultAsync(x => x.Slug == slug && x.LanguageEntity.Name == language);
        if (post == null) return null;
        var langArr = await GetLanguagesForSlug(slug);
        return post.ToPostModel(langArr);
    }

This also uses our common method GetLanguagesForSlug which gets the languages for a post. This is a simple method that returns the languages for a post.

    private async Task<List<string>> GetLanguagesForSlug(string slug)=> await NoTrackingQuery()
        .Where(x => x.Slug == slug).Select(x=>x.LanguageEntity.Name).ToListAsync();

GetPostsByCategory

This method gets the posts by category (like ASP.NET & Entity Framework for this post). It uses the PostsQuery to get the posts and then filters them by the category. It then returns the posts as a PostListViewModel.

    public async Task<PostListViewModel> GetPostsByCategory(string category, int page = 1, int pageSize = 10,
        string language = MarkdownBaseService.EnglishLanguage)
    {
        
        var count = await NoTrackingQuery()
            .Where(x => x.Categories.Any(c => c.Name == category) && x.LanguageEntity.Name == language).CountAsync();
        var posts = await PostsQuery()
            .Where(x => x.Categories.Any(c => c.Name == category) && x.LanguageEntity.Name == language)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync();

        var languages = await GetLanguagesForSlugs(posts.Select(x => x.Slug).ToList());
        var postListViewModel = new PostListViewModel
        {
            Page = page,
            PageSize = pageSize,
            TotalItems = count,
            Posts = posts.Select(x => x.ToListModel(
                languages.FirstOrDefault(entry => entry.Key == x.Slug).Value.ToArray())).ToList()
        };
        return postListViewModel;
    }

In Conclusion

You can see that the EF Based services are a bit more complex than the File Based services but they are more flexible and can be used in more complex scenarios. We can use the EF Based services in our controllers and views to get the blog posts and languages. In future we'll build on these and add services like inline editing and comments. We'll also look at how we might synchronize these across multiple systems.

logo

©2024 Scott Galloway