3

Managing State – Part 1

In this chapter, we will start looking at managing state. There is also a continuation of this chapter in Chapter 11, Managing State – Part 2.

There are many different ways of managing state or persisting data. Since this book focuses on Blazor, we will not explore how to connect to databases but create a simple JSON storage instead.

In the repo on GitHub, you can find more examples of storing data in databases such as RavenDB or MSSQL.

We will use a common pattern called the repository pattern.

We will also create an API to access the data from the JSON repository.

By the end of this chapter, we will have learned how to create a JSON repository and an API.

We will cover the following main topics:

  • Creating a data project
  • Adding the API to Blazor

Technical requirements

Make sure you have followed the previous chapters or use the Chapter02 folder as the starting point.

You can find the source code for this chapter’s result at https://github.com/PacktPublishing/Web-Development-with-Blazor-Second-Edition/tree/main/Chapter03.

Creating a data project

There are many ways of persisting data: document databases, relational databases, and files, to name a few. To remove complexity from the book, we will use the simplest way of creating blog posts by storing them as JSON in a folder.

The data will be accessible from both our Blazor WebAssembly project and the Blazor Server project, so we want to create a new project (not just put the code in one of the projects we created previously).

To save our blog posts, we will use JSON files stored in a folder, and to do so, we need to create a new project.

Creating a new project

We can also create a new project from within Visual Studio (to be honest, that’s how I would do it), but to get to know the .NET CLI, let’s do it from the command line instead.

To create a new project, follow these steps:

  1. Open a PowerShell prompt.
  2. Navigate to the MyBlog folder.
  3. Create a class library (classlib) by typing the following command:
    dotnet new classlib -o Data
    

    The dotnet tool should now have created a folder called Data.

  1. We also need to create a project where we can put our models:
    dotnet new classlib -o Data.Models
    
  2. Add the new projects to our solution by running the following command:
    dotnet sln add Data
    dotnet sln add Data.Models
    

It will look for any solution in the current folder.

We call the projects Data and Data.Models so their purpose will be easy to understand and they will be easy to find.

The default project has a class1.cs file – feel free to delete the file.

The next step is to create data classes to store our information.

Creating data classes

Now we need to create a class for our blog post. To do that, we will go back to Visual Studio:

  1. Open the MyBlog solution in Visual Studio (if it is not already open).

    We should now have a new project called Data in our solution. We might get a popup asking if we want to reload the solution; click Reload if so.

  1. Now we need to create three data classes. Right-click on Data.Models and select Add | New Folder. Name the folder Models.
  2. Right-click on the Models folder and select Add | Class. Name the class BlogPost.cs and press Add.
  3. Right-click on the Models folder and select Add | Class. Name the class Category.cs and press Add.
  4. Right-click on the Models folder and select Add | Class. Name the class Tag.cs and press Add.
  5. Open Category.cs and replace the content with the following code:
    namespace Data.Models;
    public class Category
    {
        public string? Id { get; set; }
        public string Name { get; set; } = string.Empty;
    }
    

    The Category class contains Id and Name. It might seem strange that the Id property is a string, but this is because we will support multiple data storage types, including MSSQL, RavenDB, and JSON.

    A string is a great datatype to support all of these. Id is also nullable, so if we create a new Category we send in null as an Id.

  1. Open Tag.cs and replace the content with the following code:
    namespace Data.Models;
    public class Tag
    {
        public string? Id { get; set; }
        public string Name { get; set; } = string.Empty;
    }
    

    The Tag class contains an Id and Name.

  1. Open BlogPost.cs and replace the content with the following code:
    namespace Data.Models;
    public class BlogPost
    {
        public string? Id { get; set; }
        public string Title { get; set; } = string.Empty;
        public string Text { get; set; } = string.Empty;
        public DateTime PublishDate { get; set; }
        public Category? Category { get; set; }
        public List<Tag> Tags { get; set; } = new();
    }
    

In this class, we define the content of our blog post. We need an Id to identify the blog post, a title, some text (the article), and the publishing date. We also have a Category property in the class, which is of the Category type. In this case, a blog post can have only one category, and A blog post can contain zero or more tags. We define the Tag property with List<Tag>.

By now, we have created a couple of classes that we will use. I have kept the complexity of these classes to a minimum since we are here to learn about Blazor.

Next, we will create a way to store and retrieve the information.

Creating an interface

In this section, we will create an API. Since we are currently working with Blazor Server, we can access the database directly, so the API we create here will have a direct connection to the database:

  1. Right-click on the Data.Models project and select Add | New Folder and name it Interfaces.
  2. Right-click in the Interfaces folder and select Add | Class.
  3. In the list of different templates, select Interface and name it IBlogApi.cs.
  4. Open IBlogApi.cs and replace its content with the following:
    namespace Data.Models.Interfaces;
    public interface IBlogApi
    {
        Task<int> GetBlogPostCountAsync();
        Task<List<BlogPost>?> GetBlogPostsAsync(int numberofposts, int startindex);
        Task<List<Category>?> GetCategoriesAsync();
        Task<List<Tag>?> GetTagsAsync();
        Task<BlogPost?> GetBlogPostAsync(string id);
        Task<Category?> GetCategoryAsync(string id);
        Task<Tag?> GetTagAsync(string id);
        Task<BlogPost?> SaveBlogPostAsync(BlogPost item);
        Task<Category?> SaveCategoryAsync(Category item);
        Task<Tag?> SaveTagAsync(Tag item);
        Task DeleteBlogPostAsync(string id);
        Task DeleteCategoryAsync(string id);
        Task DeleteTagAsync(string id);
        Task InvalidateCacheAsync();
    }
    

    The interface contains all the methods we need to get, save, and delete blog posts, tags, and categories.

Now we have an interface for the API with the methods we need to list blog posts, tags, and categories, as well as save (create/update) and delete them. Next, let’s implement the interface.

Implementing the interface

The idea is to create a class that stores our blog posts, tags, and categories as JSON files on our filesystem.

To implement the interface for the Blazor Server implementation, follow these steps:

  1. First, we need to add a reference to our Data models. Expand the Data project and right-click on the Dependencies node. Select Add Project reference and check the Data.Models project. Click OK.
  2. Right-click on the Dependencies node once again, but select Manage NuGet Packages. Search for Microsoft.Extensions.Options and click Install.
  3. Next, we need to create a class. Right-click on the Data project, select Add | Class, and name the class BlogApiJsonDirectAccess.cs.
  4. Open BlogApiJsonDirectAccess.cs and replace the code with the following:
    using Data.Models.Interfaces;
    using Microsoft.Extensions.Options;
    using System.Text.Json;
    using Data.Models;
    namespace Data;
    public class BlogApiJsonDirectAccess: IBlogApi
    {
    }
    

    The error list should contain many errors since we haven’t implemented the methods yet. We are inheriting from the IBlogApi, so we know what methods to expose.

  1. We need a class to hold our settings.

    In the Data project, add a new class called BlogApiJsonDirectAccessSetting.cs and replace its content with:

    namespace Data;
    public class BlogApiJsonDirectAccessSetting
    {
        public string BlogPostsFolder { get; set; } = string.Empty;
        public string CategoriesFolder { get; set; } = string.Empty;
        public string TagsFolder { get; set; } = string.Empty;
        public string DataPath { get; set; } = string.Empty;
    }
    

    IOptions is configured in program during the configuration of dependencies and is injected into all the classes that ask for a specific type.

  1. To be able to read settings, we also add a way to inject IOptions. By getting the settings this way we don’t have to add any code – it can come from a database, a setting file, or even be hard coded. This is my favorite way to get settings because this part of the code itself doesn’t know how to do it – instead, we add all our configurations by using dependency injection.

    Add the following code to the BlogApiJsonDirectAccess class:

        BlogApiJsonDirectAccessSetting _settings;
        public BlogApiJsonDirectAccess(IOptions<BlogApiJsonDirectAccessSetting> option)
        {
            _settings = option.Value;
            if (!Directory.Exists(_settings.DataPath))
            {
                Directory.CreateDirectory(_settings.DataPath);
            }
            if (!Directory.Exists($@"{_settings.DataPath}{_settings.BlogPostsFolder}"))
            {
                Directory.CreateDirectory($@"{_settings.DataPath}{_settings.BlogPostsFolder}");
            }
            if (!Directory.Exists($@"{_settings.DataPath}{_settings.CategoriesFolder}"))
            {
                Directory.CreateDirectory($@"{_settings.DataPath}{_settings.CategoriesFolder}");
            }
            if (!Directory.Exists($@"{_settings.DataPath}{_settings.TagsFolder}"))
            {
                Directory.CreateDirectory($@"{_settings.DataPath}{_settings.TagsFolder}");
            }
        }
    

    We get the injected setting and ensure we have the correct folder structure for our data.

  1. We need a couple of private variables where we can store the data. Add the following code in the BlogApiJsonDirectAccess class:
    private List<BlogPost>? _blogPosts;
    private List<Category>? _categories;
    private List<Tag>? _tags;
    
  2. Now it’s time to implement the API, but first, we need a couple of helper methods that can load the data from our filesystem and cache them. Let’s start with the methods for loading data from our filesystem by adding the following code to our class:
    private void Load<T>(ref List<T>? list, string folder)
    {
        if (list == null)
        {
            list = new();
            var fullpath = $@"{_settings.DataPath}{folder}";
            foreach (var f in Directory.GetFiles(fullpath))
            {
                var json = File.ReadAllText(f);
                var bp = JsonSerializer.Deserialize<T>(json);
                if (bp != null)
                {
                    list.Add(bp);
                }
            }
        }
    }
    private Task LoadBlogPostsAsync()
    {
        Load<BlogPost>(ref _blogPosts, _settings.BlogPostsFolder);
        return Task.CompletedTask;
    }
    private Task LoadTagsAsync()
    {
        Load<Tag>(ref _tags, _settings.TagsFolder);
        return Task.CompletedTask;
    }
    private Task LoadCategoriesAsync()
    {
        Load<Category>(ref _categories, _settings.CategoriesFolder);
        return Task.CompletedTask;
    }
    

    The Load method is a generic method that allows us to load blog posts, tags, and categories using the same method.

    It will only load data from the filesystem if we don’t already have any data. We also add separate methods that load each type: LoadBlogpostsAsync, LoadCategoriesAsync, and LoadTagsAsync.

  1. Next, we will add a couple of methods to help manipulate the data, namely SaveAsync and DeleteAsync. Add the following methods:
    private async Task SaveAsync<T>(List<T>? list, string folder, string filename, T item)
    {
        var filepath = $@"{_settings.DataPath}{folder}{filename}";
        await File.WriteAllTextAsync(filepath, JsonSerializer.Serialize<T>(item));
        if (list == null)
        {
            list = new();
        }
        if (!list.Contains(item))
        {
            list.Add(item);
        }
    }
    private void DeleteAsync<T>(List<T>? list, string folder, string id)
    {
        var filepath = $@"{_settings.DataPath}{folder}{id}.json";
        try
        {
            File.Delete(filepath);
        }
        catch { }
    }
    

    These methods are also generic to share as much code as possible and avoid repeating the code for every type of class (BlogPost, Category, and Tag).

  1. Next, it’s time to implement the API by adding the methods to get blog posts. Add the following code:
    public async Task<List<BlogPost>?> GetBlogPostsAsync(int numberofposts, int startindex)
    {
        await LoadBlogPostsAsync();
        return _blogPosts ?? new();
    }
    public async Task<BlogPost?> GetBlogPostAsync(string id)
    {
        await LoadBlogPostsAsync();
        if (_blogPosts == null)
            throw new Exception("Blog posts not found");
        return _blogPosts.FirstOrDefault(b => b.Id == id);
    }
    public async Task<int> GetBlogPostCountAsync()
    {
        await LoadBlogPostsAsync();
        if (_blogPosts == null)
            return 0;
        else
            return _blogPosts.Count();
    }
    

    The GetBlogPostsAsync method takes a couple of parameters we will use later for paging. We execute the LoadBlogPostsAsync at the start of each method to ensure we have loaded any data from our filesystem. This method will only be executed while the _blogposts list (in this case) is null.

    We also have a method that returns the current blog post count, which we will also use for paging.

  1. Now we need to add the same methods for categories, add the following code:
    public async Task<List<Category>?> GetCategoriesAsync()
    {
        await LoadCategoriesAsync();
        return _categories ?? new();
    }
    public async Task<Category?> GetCategoryAsync(string id)
    {
        await LoadCategoriesAsync();
        if (_categories == null)
            throw new Exception("Categories not found");
        return _categories.FirstOrDefault(b => b.Id == id);
    }
    

    The Category methods don’t have any support for paging. Otherwise, they should look familiar as they do almost the same as the blog post methods.

  1. Now it’s time to do the same thing for tags. Add the following code:
    public async Task<List<Tag>?> GetTagsAsync()
    {
        await LoadTagsAsync();
        return _tags ?? new();
    }
    public async Task<Tag?> GetTagAsync(string id)
    {
        await LoadTagsAsync();
        if (_tags == null)
            throw new Exception("Tags not found");
        return _tags.FirstOrDefault(b => b.Id == id);
    }
    

    As we can see, the code for tags is basically a copy of the one for categories.

  1. We also need a couple of methods for saving the data, so next up we’ll add methods for saving, blog posts, categories, and tags.

    Add the following code:

    public async Task<BlogPost?> SaveBlogPostAsync(BlogPost item)
    {
        if (item.Id == null)
        {
            item.Id = Guid.NewGuid().ToString();
        }
        await SaveAsync<BlogPost>(_blogPosts, _settings.BlogPostsFolder, $"{item.Id}.json", item);
        return item;
    }
    public async Task<Category?> SaveCategoryAsync(Category item)
    {
        if (item.Id == null)
        {
            item.Id = Guid.NewGuid().ToString();
        }
        await SaveAsync<Category>(_categories, _settings.CategoriesFolder, $"{item.Id}.json", item);
        return item;
    }
    public async Task<Tag?> SaveTagAsync(Tag item)
    {
        if (item.Id == null)
        {
            item.Id = Guid.NewGuid().ToString();
        }
        await SaveAsync<Tag>(_tags, _settings.TagsFolder, $"{item.Id}.json", item);
        return item;
    }
    

    The first thing we do is to check that the id of the item is not null. If it is, we create a new Guid. This is the id of the new item. And this is also going to be the name of the JSON files stored on the filesystem.

  1. We now have a method for saving items as well as getting items. But sometimes things don’t go as planned and we need a way to delete the items that we have created. Next up, we will add some delete methods. Add the following code:
    public Task DeleteBlogPostAsync(string id)
    {
        DeleteAsync(_blogPosts, _settings.BlogPostsFolder, id);
        if (_blogPosts != null)
        {
            var item = _blogPosts.FirstOrDefault(b => b.Id == id);
            if (item != null)
            {
                _blogPosts.Remove(item);
            }
        }
        return Task.CompletedTask;
    }
    public Task DeleteCategoryAsync(string id)
    {
        DeleteAsync(_categories, _settings.CategoriesFolder, id);
        if (_categories != null)
        {
            var item = _categories.FirstOrDefault(b => b.Id == id);
            if (item != null)
            {
                _categories.Remove(item);
            }
        }
        return Task.CompletedTask;
    }
    public Task DeleteTagAsync(string id)
    {
        DeleteAsync(_tags, _settings.TagsFolder, id);
        if (_tags != null)
        {
            var item = _tags.FirstOrDefault(b => b.Id == id);
            if (item != null)
            {
                _tags.Remove(item);
            }
        }
        return Task.CompletedTask;
    }
    

    The code we just added calls the DeleteAsync method that deletes the item and will also remove the item from the collection.

    1. Since we are updating the collections as we go, they should always be updated, but for good measure, let’s add a method for clearing the cache.

    Add the following method:

    public Task InvalidateCacheAsync()
    {
        _blogPosts = null;
        _tags = null;
        _categories = null;
        return Task.CompletedTask;
    }
    

Our JSON storage is done!

In the end, there will be three folders stored on the filesystem, one for blog posts, one for categories, and one for tags.

The next step is to add and configure the Blazor project to use our new storage.

Adding the API to Blazor

We now have a way to access JSON files stored on our filesystem. In the repo on GitHub, you can find more ways of storing our data with RavenDB or SQL server, but be mindful to keep the focus on what is important (Blazor).

Now it’s time to add the API to our Blazor Server project:

  1. In the BlazorServer project, add a project reference to the Data project. Open Program.cs and add the following namespaces:
    using Data;
    using Data.Models.Interfaces;
    
  2. Add the following code:
    builder.Services.AddOptions<BlogApiJsonDirectAccessSetting>()
        .Configure(options =>
        {
            options.DataPath = @"......Data";
            options.BlogPostsFolder = "Blogposts";
            options.TagsFolder = "Tags";
            options.CategoriesFolder = "Categories";
        });
    builder.Services.AddScoped<IBlogApi, BlogApiJsonDirectAccess>();
    

The snippet of code is the setting for where we want to store our files. You can change the data path property to where you want to store the files. Whenever we ask for IOptions<BlogApiJsonDirectAccessSetting>, the dependency injection will return an object populated with the information we have supplied above. This is an excellent place to load configuration from our .NET configuration, a key vault, or a database.

We are also saying that when we ask for an IBlogAPI we will get an instance of BlogApiJsonDirectAccess back from our dependency injection. We will return to dependency injection in Chapter 4, Understanding Basic Blazor Components.

Now we can use our API to access the database in our Blazor Server project.

Summary

This chapter taught us how to create a simple JSON repository for our data. We also learned that other alternatives could be found in the GitHub repo if you want to look at other options.

We also created an interface to access the data, which we will use more throughout the book.

In the next chapter, we will learn about components, particularly the built-in components in Blazor templates. We will also create our first component using the API and repository we made in this chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset