DEV Community

Cover image for Top Ingredients for Blazor Server Recipe
Ray_Dsz
Ray_Dsz

Posted on

Top Ingredients for Blazor Server Recipe

Table Of Contents

Introduction

Blazor is a powerful front-end framework, especially loved by C# developers who want to build modern web applications without switching to JavaScript-based stacks. It comes in two main flavors:

  • Blazor Server – uses server-side rendering, where the UI interactions are processed on the server via SignalR.

  • Blazor WebAssembly – runs directly in the browser (client-side) using WebAssembly.

Both of these are now part of the Blazor Web App model introduced in .NET 8, giving you the flexibility to mix and match rendering modes.

In this article, I’ll be focusing on Blazor Server and walk you through the key components and configurations that make your front-end secure, reliable, and production-ready—especially when integrating with Azure Entra Authentication.

So let’s dive in! 🚀

Project Structure

If you have seen my previous posts, I had created a template with clean architecture. You can view it here. Iam just going to continue the frontend part on the same project. Below is the project structure:

Blazor Template

Most of the code in this project is generated out of the box, and we won’t be focusing on the UI part in this blog. (For UI, I personally recommend using Radzen—a great component library that works seamlessly with Blazor.)

Our main focus will be everything except the Components folder.

Here’s a quick breakdown of the important folders and their roles:

  • BasicDataModels/

    This folder contains the models which are necessary to retrieve the json values from appSettings. Like jwt parameters, Api hosts etc.

  • Extensions/

    Contains HttpClientExtension to handle success, failure from the api.

  • Middleware/

    I wanted to generate an id that would be common across entire transaction. So that i could check the logs how the request has flown and where the error occured. Using this Id, I would be able to get the entire path of the request from UI -> Api -> Send Mail Api etc.

  • Models/

    This folder contains the models with structure matching to the model structure comming from api.

  • Services/

    This folder contains all the api calls. I used interface implementation pattern here. Each api host has its own folder like request, azure graph, workflow etc.

Now, We will go ahead to the most important part, Setting up the blazor server in a neat way..

The Ingredients

We will basically going through each ingredient and its set up.

Setting Up Azure Entra Authentication

Authentication plays a crucial role. Since, we had Azure user management. We can use Azure Entra Authentication into our application's. You can check my post here to set it up.


Logging for Blazor Server

Logging is a crucial process in web application development. This plays a crucial role for the developers to debug. Thanks to Serilog, We can now log in a seamless way.

Packages Required

Serilog.AspNetCore

In appSettings.json

        "Serilog": {
    "Using": [ "Serilog.Sinks.File" ],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "Serilog.AspNetCore.RequestLoggingMiddleware": "Warning",
        "Microsoft.AspNetCore.Components.RenderTree.Renderer": "Error"
      }
    },
    "WriteTo": [
      {
        "Name": "File",
        "Args": {
          "path": "D:\\ApplicationLogs\\templateUILogs.txt",
          "rollingInterval": "Month",
          "rollOnFileSizeLimit": true,
          "outputTemplate": "[CorrId:{CorrelationId}] [{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Level:u3}] {Message:lj}{NewLine}{Exception}"
        }
      }
    ],
    "Enrich": [ "WithProperty" ],
    "Properties": {
      "ApplicationName": "ISWebAppTemplate"
    }
  }
Enter fullscreen mode Exit fullscreen mode

In Program.cs

        builder.Host.UseSerilog((context, loggerConfig) =>
        {
            loggerConfig.ReadFrom.Configuration(context.Configuration)
            .Enrich.FromLogContext();
        });
Enter fullscreen mode Exit fullscreen mode

Setting up CorrelationId for Transaction

Now consider a situation, Where you have blazor server app, Web Api and a Mail Api which is called within Web Api. What if something fails in the middle?. Even though, We set up serilog in each of the project. How do we track it?.
For ex: If you submit an request in

blazor form -> It calls post request in Api -> Sends an mail to user via mail api that request is submitted

If any failure happens there is no way we can track the log as there is nothing common in them. To resolve this, We create a correlation Id in blazor and pass it via api calls throughout the transaction till it completes. We maintain a single Correlation Id for one complete transaction .i.e.

Generate CorrelationId in Blazor -> Pass it to Web Api -> Pass it to mail api

Let's see how we can set it up.

In Services Folder:

Create an interface, implementation classes like below:

// ICorrelationService.cs
public interface ICorrelationService
{
    string? CorrelationId { get; set; }
}

//CorrelationServiceState.cs
public class CorrelationServiceState : ICorrelationService
{

    public string? CorrelationId
    {
        get;
        set;
    }

}
Enter fullscreen mode Exit fullscreen mode

In Middleware Folder :

Create a CorrelationCircuitHandler File

public class CorrelationCircuitHandler : CircuitHandler
{
    private readonly ILogger<CorrelationCircuitHandler> _logger;
    private readonly ICorrelationService _correlationService;

    public CorrelationCircuitHandler(ILogger<CorrelationCircuitHandler> logger, ICorrelationService correlationService)
    {
        _logger = logger;
        _correlationService = correlationService;
    }

    public override Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        _correlationService.CorrelationId = circuit.Id;
        LogContext.PushProperty("CorrelationId", _correlationService.CorrelationId);
        _logger.LogInformation("Circuit connected");
        // Store or use circuit.Id as a unique connection/session ID
        return Task.CompletedTask;
    }

    public override Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        LogContext.PushProperty("CorrelationId", _correlationService.CorrelationId);
        _logger.LogInformation("Circuit disconnected");
        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

In DependecyInjection.cs :

        services.AddScoped<ICorrelationService, CorrelationServiceState>();
        services.AddScoped<CircuitHandler, CorrelationCircuitHandler>();
Enter fullscreen mode Exit fullscreen mode

Exception Handling in Blazor

Blazor by default catches the server rendering errors. But, it doesn't catch the errors that are thrown from Api like 403, 404, 401 etc. In order to catch these, We need to set up Custom Exception Logging Service. So Let's start...

In Services Folder:

Create an interface, implementation classes.

// IExceptionLoggingService.cs
public interface IExceptionLoggingService
{
    ProblemDetails Log(Exception ex);
}

// ExceptionLoggingService.cs
public class ExceptionLoggingService(ILogger<ExceptionLoggingService> logger, ICorrelationService correlationService) : DelegatingHandler, IExceptionLoggingService
{
    public ProblemDetails Log(Exception exception)
    {

        var stackTrace = new StackTrace(exception, true);
        var frame = stackTrace.GetFrame(0);
        var problemDetails = new ProblemDetails
        {
            StatusCode = StatusCodes.Status500InternalServerError,
            FileName = exception.TargetSite?.DeclaringType?.FullName,
            LineNumber = frame?.GetFileLineNumber(),
            MethodName = exception.TargetSite?.Name,
            Type = exception.GetType().Name,
            Title = exception.Message,
            Description = exception.InnerException?.Message,
            StackTrace = exception.StackTrace
        };

        LogContext.PushProperty("CorrelationId", correlationService.CorrelationId);

        logger.LogError(
    $"StatusCode: {problemDetails.StatusCode}, " +
    $"File : {problemDetails.FileName}, " +
    $"Line Number {problemDetails.LineNumber}, " +
    $"Method Name {problemDetails.MethodName}, " +
    $"Type: {problemDetails.Type}, " +
    $"Title: {problemDetails.Title}, " +
    $"Description: {problemDetails.Description}, " +
    $"StackTrace: {problemDetails.StackTrace}, " +
    $"TimeStamp: {DateTime.Now}, ");
        return problemDetails;
    }
}
Enter fullscreen mode Exit fullscreen mode
//Problem Details class in BasicDataModels Folder
public class ProblemDetails
{
    public int? StatusCode { get; set; }

    public string? FileName { get; set; }

    public int? LineNumber { get; set; }

    public string? MethodName { get; set; }
    public string? Type { get; set; }
    public string? Title { get; set; }
    public string? Description { get; set; }
    public string? StackTrace { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

In DependencyInjection.cs:

        services.AddScoped<IExceptionLoggingService, ExceptionLoggingService>();
Enter fullscreen mode Exit fullscreen mode

Retry Policy for Api in Blazor

What if the api fails or the app pool is failing and the Blazor app is not able to connect to api?. What if the api is temporary down. During these times, It's crucial that we retry the request to api for around 2 to 3 times and gracefully handle it in blazor. So hence, Retry Policy. Let's see how to set it up..

Package Required

Polly

In DependencyInjection.cs:

According to below code, The httpClient will retry 5 times with each time multiplying twice the seconds to previous request hit. Ex: If the first retry was done. It will retry after 2 seconds, then after 4 seconds and it will retry 5 times.

        services.AddHttpClient("TemplateClient", client =>
        {
            client.BaseAddress = new Uri(baseUrls.TemplateApiBaseUrl);
        })
                    .AddTransientHttpErrorPolicy(policyBuilder =>
            policyBuilder.WaitAndRetryAsync(5,
                retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))));
Enter fullscreen mode Exit fullscreen mode

Rate limiting in Blazor Application

Rate limiting is necessary if too many requests are hitting to a blazor at once. Usually as per my experience. IIS server can handle 5 million transaction a minute. But, if you want to limit it like 15 requests for 5 seconds. The requests comming after this will be kept in queue and the requests after will be rejected. So let's see how to set it up...

In DependencyInjection.cs:

 // In AddUIServices method
        services.AddRateLimiter(options =>
        {
            options.AddFixedWindowLimiter("fixed", opt =>
            {
                opt.PermitLimit = 12;
                opt.Window = TimeSpan.FromSeconds(24);
                opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
                opt.QueueLimit = 2;
            });
        });

//In UseUIServices method
        app.MapRazorComponents<App>()
            .AddInteractiveServerRenderMode()
            .RequireRateLimiting("policy");
Enter fullscreen mode Exit fullscreen mode

Content Security Policy (CSP)

Content Security Policy is crucial and one of the most important ingredient of Blazor. It allows only the requests from the domain we set up in CSP of our blazor app. This works similar to CORS in ASP .NET Core Web Api. It just blocks the requests comming from the other domain. Note that, If you want to block requests from particular set of machines, CSP doesnt help. Let's see how we can implement this..

In DependencyInjection.cs:

// In UseUIServices method
        app.Use(async (context, next) =>
        {

            // Add the CSP header
            context.Response.Headers.Add("Content-Security-Policy",
                $"base-uri 'self'; " +
                $"default-src 'self' https://localhost:* https://<domain>.com; " +
                $"img-src data: https://<domain>.com; " +
                $"object-src 'none'; " +
                $"script-src 'self' https://<domain>.com 'unsafe-eval'; " +
                $"style-src 'self' https://<domain>.com 'sha256-eLbuM5dktsItN/wyd3rBMDvH9/MlAz4tA5/eNpxjhsQ=' 'sha256-eLbuM5dktsItN/wyd3rBMDvH9/MlAz4tA5/eNpxjhsQ=' 'unsafe-hashes'; " +
                $"upgrade-insecure-requests;");

            await next();
        });
Enter fullscreen mode Exit fullscreen mode

Error handling in Api Call from Blazor

I spent a lot of time on how to handle the validation errors from api and api erros from api. So i came up approach that suits my situation. Of course, There might be easier and more reliable approach. But, This is the one i went with, So let's dig deep...

In MainLayout.razor:

 @if (hasError)
 {
     <Error problemDetails="@problemDetails" />
 }
 else
 {
     <CascadingValue Value="userToken" Name="Token">
         <Header @bind-empDetails="empDetails" />
     </CascadingValue>

     <RadzenBody class="carrental-body">
         @Body
     </RadzenBody>
 }

@code { 
    private bool hasError = false;

    private ProblemDetails? problemDetails;
                        var requestsCreatedByEmp = await requestService.GetAllRequestsByEmpId(samAccount);
                        if (!requestsCreatedByEmp.isApiFailure)
                        {
                            var data = requestsCreatedByEmp.Data;
                        }
                        else
                        {
                            hasError = true;
                            problemDetails = requestsCreatedByEmp.ApiFailureDetails;
                        }
}
Enter fullscreen mode Exit fullscreen mode

In Error.razor:

@page "/Error"
@inject ICorrelationService correlationService
@inject NavigationManager NavigationManager
@inject IExceptionLoggingService ExceptionLogging
<PageTitle>Error</PageTitle>
<RadzenCard Variant="Variant.Outlined" class="rz-my-12 rz-mx-auto ">
        <RadzenStack Orientation="Orientation.Horizontal" JustifyContent="JustifyContent.Start" Gap="1rem" class="rz-p-4">
        <RadzenIcon Icon="dangerous" IconColor="@Colors.Danger" />
            <RadzenStack Gap="0">
            <RadzenLabel>Something went wrong!! :(</RadzenLabel>
            <RadzenLabel>Correlation Id: @CorrelationId</RadzenLabel>
            </RadzenStack>
        </RadzenStack>
    </RadzenCard>
@code {
    [Parameter]
    public Exception? ErrorDetails { get; set; }

    [Parameter]
    public ProblemDetails? problemDetails { get; set; }
    private string CorrelationId;
    protected override void OnInitialized()
    {
        CorrelationId = correlationService.CorrelationId;
        //if the errors are not api errors
        if(ErrorDetails != null)
        {
           ExceptionLogging.Log(ErrorDetails);
        }

    }

    private void goToHome()
    {
        NavigationManager.NavigateTo("home", TemplateConstants.s_yes);
    }
}
Enter fullscreen mode Exit fullscreen mode

In RequestService.cs:

// This get's the custom token generated from MainLayout
    public async Task GetCustomToken()
    {

        var token = await _userToken.WaitForTokenAsync();
        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _userToken.Token);
    }

//Adds correlation id to api header to pass it to upcomming api
    private HttpRequestMessage AppendCorrelationId(HttpRequestMessage? request)
    {
        request.Headers.Add("X-Correlation-ID", _correlationService.CorrelationId ?? "unknown");
        return request;
    }

//This method is called from Mainlayout.razor
    public async Task<Result<IEnumerable<CreateRequest>>> GetAllRequestsByEmpId(string userId)
    {
        try
        {
            int empId = 0;
            await GetCustomToken();
            var request = new HttpRequestMessage(HttpMethod.Get, $"{basicApiUrls.TemplateApiBaseUrl}/requests/requestByEmployee/{empId}");
            request = AppendCorrelationId(request);
            var response = await _httpClient.SendAsync(request);
            return await response.HandleApiResponse<IEnumerable<CreateRequest>>(logger);
        }
        catch (HttpRequestException exception)
        {
            var problemDetails = exceptionLogger.Log(exception);

            // Handle network-related errors
            return Result<IEnumerable<CreateRequest>>.FailureStatus(problemDetails);
        }
    }

Enter fullscreen mode Exit fullscreen mode

Conclusion

There’s still a lot more that can be added to improve and expand this setup. What I’ve shared here is based on what I’ve learned and implemented so far.

Feel free to build on it, customize it to your needs, and make it even better.

Happy coding! 🚀

Top comments (0)