Creating Web Apps with Blazor And .NET Aspire - Part V

Creating Web Apps with Blazor And .NET Aspire - Part V

Hello, I hope you are doing great!

Thanks for reading.

Remember to share the article with your network and invite more people to subscribe.

If you haven't yet, go check the previous articles in the series: Creating Web Apps with Blazor And .NET Aspire

Last time we worked in showing the contacts list, so we can now create contacts and list them, we also mentioned there are a couple of issues, like the fact that we are retrieving all contacts at once, which is not recommended, so, today, we are going to work on pagination, however, before we do that, we want to show the issue so you can clearly see the benefits, so, the first thing we are going to do is to create Test Data, and for teaching purposes we are going to do so by using Background Service.

Right click your Visual Studio solution, then select Add -> New Project, search for Worker Service and select it, make sure you select the one with C#.


In the next window type the name of the project, I used BlazorAspireTutorial.TestDataGenerator, then click Next

In the next window, make sure you have .NET 9 selected as well as Enlist in .NET Aspire Orchestration checked.

By enlisting this project in the .NET Aspire Orchestration, we are making sure the services run every time we run the .NET Aspire AppHost, which is good for testing purposes, in real production scenarios, you do not want to have these kind of test data generator services enabled, in fact, not even deployed.

Now, in the new project, go to the Worker.cs file. We want to rename the file to TestContactsGeneratorService, right click over the file, select rename, type the new one and hit ENTER. Visual Studio will ask you if you want to modify the rest of the code, accordingly, let it do so, it will modify the name of the class too, as well as any references to that class.

This is the file content so far.

namespace BlazorAspireTutorial.TestDataGenerator;

public class TestContactsGeneratorService : BackgroundService
{
    private readonly ILogger<TestContactsGeneratorService> _logger;

    public TestContactsGeneratorService(ILogger<TestContactsGeneratorService> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            if (_logger.IsEnabled(LogLevel.Information))
            {
                _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
            }
            await Task.Delay(1000, stoppingToken);
        }
    }
}
        

Now, there are a couple of things we want to do, first we need to make sure the project has knowledge of the database connection string.

Go to the Program.cs file for the .NET Aspire AppHost project, and modify the line for the Test Data Generator like this

builder
    .AddProject<Projects.BlazorAspireTutorial_TestDataGenerator>("blazoraspiretutorial-testdatagenerator")
    .WithReference(sql)
    .WaitFor(sql);        

Now, go back to the Test Data generator project, right click on it, select Add -> .NET Aspire Package and install Aspire.Microsoft.EntityFrameworkCore.SqlServer

Now, go to the Program.cs of the Test Data Generator project, and include the following line after the call to AddServiceDefaults

builder.AddSqlServerDbContext<BlazorAspireTutorialDatabaseContext>("sqldb");        

You will see that there is an error trying to find the type BlazorAspireTutorialDatabaseContext, right click over it, select Quick Actions and Refactorings...


Then select the option to add a reference to the Data Access project, you could also have done that manually through the Dependencies in the project.

Make sure, the "using" gets added.

This is the file content so far

using BlazorAspireTutorial.DataAccess.Models;
using BlazorAspireTutorial.TestDataGenerator;

var builder = Host.CreateApplicationBuilder(args);

builder.AddServiceDefaults();
builder.AddSqlServerDbContext<BlazorAspireTutorialDatabaseContext>("sqldb");
builder.Services.AddHostedService<TestContactsGeneratorService>();

var host = builder.Build();
host.Run();
        

We can now use the Database Context through Dependency Injection for this project.

Go to the file TestContactsGeneratorService, include the dbContext parameter in the constructor

public TestContactsGeneratorService(ILogger<TestContactsGeneratorService> logger,
    BlazorAspireTutorialDatabaseContext dbContext)
{
    _logger = logger;
    _dbContext = dbContext;
}        

You also need to define the variable


And you need to make sure to import the namespace


This is the file content so far

using BlazorAspireTutorial.DataAccess.Models;

namespace BlazorAspireTutorial.TestDataGenerator;

public class TestContactsGeneratorService : BackgroundService
{
    private readonly ILogger<TestContactsGeneratorService> _logger;
    private readonly BlazorAspireTutorialDatabaseContext _dbContext;

    public TestContactsGeneratorService(ILogger<TestContactsGeneratorService> logger,
        BlazorAspireTutorialDatabaseContext dbContext)
    {
        _logger = logger;
        _dbContext = dbContext;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            if (_logger.IsEnabled(LogLevel.Information))
            {
                _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
            }
            await Task.Delay(1000, stoppingToken);
        }
    }
}
        

Now, you the application, you will get an error.


Background Services are a type of Hosted Services, the framework does not create a scope by default for these, so, we need to create the scope manually. More info on the official documentation

Change the constructor like this

public TestContactsGeneratorService(ILogger<TestContactsGeneratorService> logger,
    IServiceProvider serviceProvider)
{
    _logger = logger;
    _serviceProvider = serviceProvider;
}        

Also, make sure to remove the variable for the dbContext, and include a new one

private readonly IServiceProvider _serviceProvider;        

Now, go to the ExecuteAsync method, and modify it like this

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        using var scope = _serviceProvider.CreateScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<BlazorAspireTutorialDatabaseContext>();
        if (_logger.IsEnabled(LogLevel.Information))
        {
            _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
        }
        await Task.Delay(1000, stoppingToken);
    }
}        

Now, run the project again, now, you will not get any exceptions, the instance of the dbContext will be retrieved in the loop, and if you go to the logs, you will see the messages from the service.


Go back to the TestContactsGeneratorService class

Modify the ExecuteAsync method like this

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    if (!stoppingToken.IsCancellationRequested)
    {
        try
        {
            if (_logger.IsEnabled(LogLevel.Information))
            {
                _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
            }
            using var scope = _serviceProvider.CreateScope();
            var dbContext = scope.ServiceProvider.GetRequiredService<BlazorAspireTutorialDatabaseContext>();
            for (int i=0; i < 5000; i++)
            {
                Contact entity = new()
                {
                    EmailAddress = $"test-{i}@test.local",
                    Firstname = $"FN-{i}",
                    Lastname = "LN-{i}",
                    IntagramProfileUrl = "https://someurl.com",
                    LinkedInProfileUrl = "https://someurl.com",
                    WebsiteUrl = "https://someurl.com",
                    XformerlyTwitterUrl = "https://someurl.com"
                };
                await dbContext.AddAsync(entity, stoppingToken);
            }
            int totalItemsAdded = await dbContext.SaveChangesAsync(stoppingToken);
            if (_logger.IsEnabled(LogLevel.Information))
            {
                _logger.LogInformation("Contacts created: {qty}", totalItemsAdded);
            }
            await Task.Delay(1000, stoppingToken);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex.Message, "An error has occurred: {Message}", ex.Message);
        }
    }
}        

What we did just here is, we changed the while to an if, we only want the service to run once and finish, we don't want it to keep inserting contacts indefinitely.

Then, we created a loop, to insert 5000 records, check that we make sure to include the SaveChangeAsync out of the "for loop", we also include a line to log how many records where saved.

Note: These kind of Test Data Generator services can be improved with "Faker" libraries, these libraries usually allow you to create semi-realistic test data, like actual names, last names, url, email, etc.

Run the application and go to the service logs.

After a while you should see something like this


Now, you will be able to see all of those contacts in the Blazor Web app, first, though, we need to go and remove the intentional delay we left when loading data.

Go to the List.razor file in your Blazor Web App project

Remove the following lines

await Task.Delay(TimeSpan.FromSeconds(30));        
throw new Exception("test");        

Now, load the App, and navigate to the list of contacts page

After a while you will see something like this


All of the Test Data was retrieved, and despite remove the delay, it actually took a while rendering the items. The query was actually executed somewhat fast, you can verify that by going to the Blazor Web App logs


The query took only 41 milliseconds

So, now, let's make the rendering faster by using pagination.

Add the following variable to the page

private readonly PaginationState paginationState = new()
{
        ItemsPerPage = 10
};        

Modify the grid to enable pagination referencing this variable

<FluentDataGrid Items="@this.Items.AsQueryable()" Pagination="@paginationState">
    <PropertyColumn Property="@(p=>p.Firstname)"></PropertyColumn>
    <PropertyColumn Property="@(p=>p.Lastname)"></PropertyColumn>
</FluentDataGrid>        

Now, we need to include a paginator component, after the grid

<FluentPaginator State="@paginationState"></FluentPaginator>        

This is the code so far

@page "/Contacts/List"
@using Microsoft.EntityFrameworkCore

@inject IToastService toastService
@inject BlazorAspireTutorial.DataAccess.Models.BlazorAspireTutorialDatabaseContext dbContext
@inject ILogger<List> logger


@if (this.IsBusy)
{
    <FluentProgressRing></FluentProgressRing>
}

@if (this.Items != null)
{
    <FluentDataGrid Items="@this.Items.AsQueryable()" Pagination="@paginationState">
        <PropertyColumn Property="@(p=>p.Firstname)"></PropertyColumn>
        <PropertyColumn Property="@(p=>p.Lastname)"></PropertyColumn>
    </FluentDataGrid>

    <FluentPaginator State="@paginationState"></FluentPaginator>
}


@code
{
    private DataAccess.Models.Contact[]? Items { get; set; }
    private bool IsBusy { get; set; }
    private readonly PaginationState paginationState = new()
    {
            ItemsPerPage = 10
    };

    protected override async Task OnInitializedAsync()
    {
        try
        {
            this.IsBusy = true;
            this.Items = await dbContext.Contacts.ToArrayAsync();
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "An error has occurred: {ErrorMessage}", ex.Message);
            toastService.ShowError(ex.Message);
        }
        finally
        {
            this.IsBusy = false;
        }
    }
}        

Run the application again and navigate to the list of contacts.

The page contents will be rendered faster; however, we are still retrieving all the contacts from the database, which is not recommended, that can be solved using server-side pagination, which we will talk about in other articles.

That will be everything for today, wait for the next article of this series.

I hope you find this article useful.

We are currently working with "FairPlayCombined GitHub repository", part of the "FairPlay" Open-Source Software initiaitve, go to the GitHub repository today and give it a star!

Help the success of this initiative: Become a GitHub Sponsor Today!

For mutual-benefit business opportunities, feel free to send me an InMail, or schedule a meeting

Business Opportunities Talk

要查看或添加评论,请登录

Eduardo Fonseca的更多文章

  • Blazor How-To: Display Toast Notifications

    Blazor How-To: Display Toast Notifications

    Hello, I hope you are doing great! Thanks for reading. Remember to share the article with your network and invite more…

    3 条评论
  • How to use Instagram APIs with C# and .NET - Part I

    How to use Instagram APIs with C# and .NET - Part I

    Hello, I hope you are doing great! Thanks for reading. Remember to share the article with your network and invite more…

    3 条评论
  • Blazor How-To: Dynamically Set Page Render Mode

    Blazor How-To: Dynamically Set Page Render Mode

    Hello, I hope you are doing great! Thanks for reading. Remember to share the article with your network and invite more…

  • Features in the FairPlay platform

    Features in the FairPlay platform

    Hello, I hope you are doing great! Thanks for reading. Remember to share the article with your network and invite more…

  • Blazor How-To: Creating a Blog platform - Part 1

    Blazor How-To: Creating a Blog platform - Part 1

    Hello, I hope you are doing great! Thanks for reading. Remember to share the article with your network and invite more…

  • Progress Update on The FairPlay Platform

    Progress Update on The FairPlay Platform

    Hello, I hope you are doing great! Thanks for reading. Remember to share the article with your network and invite more…

  • How can Software Developers fight boredom

    How can Software Developers fight boredom

    Hello, I hope you are doing great! Thanks for reading. Remember to share the article with your network and invite more…

  • Using Artificial Intelligence to Improve Data Validations

    Using Artificial Intelligence to Improve Data Validations

    Hello, I hope you are doing great! Thanks for reading. Remember to share the article with your network and invite more…

  • How to Create Image Shares for LinkedIn Using C#

    How to Create Image Shares for LinkedIn Using C#

    Hello, I hope you are doing great! Thanks for reading. Remember to share the article with your network and invite more…

  • How to overcome legacy-fatigue?

    How to overcome legacy-fatigue?

    Hello, I hope you are doing great! Thanks for reading. Remember to share the article with your network and invite more…

社区洞察

其他会员也浏览了