Automatically refresh the page when reconnection fails in ASP.NET Blazor Server

Pages/_Host.cshtml:

<body>
    ...

    <div id="reconnect-modal" style="display: none;"></div>
    <script src="_framework/blazor.server.js" autostart="false"></script>
    <script src="boot.js"></script>
</body>

wwwroot/boot.js:

(() => {
  const maximumRetryCount = 3;
  const retryIntervalMilliseconds = 5000;
  const reconnectModal = document.getElementById('reconnect-modal');

  const startReconnectionProcess = () => {
    reconnectModal.style.display = 'block';

    let isCanceled = false;

    (async () => {
      for (let i = 0; i < maximumRetryCount; i++) {
        reconnectModal.innerText = `Attempting to reconnect: ${i + 1} of ${maximumRetryCount}`;

        await new Promise(resolve => setTimeout(resolve, retryIntervalMilliseconds));

        if (isCanceled) {
          return;
        }

        try {
          const result = await Blazor.reconnect();
          if (!result) {
            // The server was reached, but the connection was rejected; reload the page.
            location.reload();
            return;
          }

          // Successfully reconnected to the server.
          return;
        } catch {
          // Didn't reach the server; try again.
        }
      }

      // Retried too many times; reload the page.
      location.reload();
    })();

    return {
      cancel: () => {
        isCanceled = true;
        reconnectModal.style.display = 'none';
      },
    };
  };

  let currentReconnectionProcess = null;

  Blazor.start({
    reconnectionHandler: {
      onConnectionDown: () => currentReconnectionProcess ??= startReconnectionProcess(),
      onConnectionUp: () => {
        currentReconnectionProcess?.cancel();
        currentReconnectionProcess = null;
      },
    },
  });
})();

References
https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/signalr?view=aspnetcore-7.0#automatically-refresh-the-page-when-reconnection-fails-blazor-server

Serilog.Enrichers.GlobalLogContext

A Serilog enricher to dynamically add properties to all log events of your application.

The main difference is that Serilog.Enrichers.GlobalLogContext allows you to add properties at a later time, long after your Serilog logging pipeline has been configured, which can be very useful in scenarios where the properties being added are not immediately available during the bootstrapping of the app. In addition, properties in the GlobalLogContext can be replaced or removed at any time during the execution of the app.

Install-Package Serilog.Enrichers.GlobalLogContext
Log.Logger = new LoggerConfiguration()
    .Enrich.FromGlobalLogContext()
    // ... other configuration ...
    .CreateLogger();
GlobalLogContext.PushProperty("AppVersion", GetThisAppVersion());
GlobalLogContext.PushProperty("OperatingSystem", GetCurrentOS());

References
https://github.com/serilog-contrib/serilog-enrichers-globallogcontext

Dynamically-rendered ASP.NET Core Razor components

<DynamicComponent Type="@componentType" Parameters="@parameters" />

@code {
    private Type componentType = ...;
    private IDictionary<string, object> parameters = ...;
}
<DynamicComponent Type="@typeof({COMPONENT})" @ref="dc" />

<button @onclick="Refresh">Refresh</button>

@code {
    private DynamicComponent? dc;

    private Task Refresh()
    {
        return (dc?.Instance as IRefreshable)?.Refresh();
    }
}

References
https://learn.microsoft.com/en-us/aspnet/core/blazor/components/dynamiccomponent?view=aspnetcore-7.0

Persist State for Prerendered Components in ASP.NET Blazor

Without persisting prerendered state, state used during prerendering is lost and must be recreated when the app is fully loaded. If any state is setup asynchronously, the UI may flicker as the prerendered UI is replaced with temporary placeholders and then fully rendered again.

<body>
    <component type="typeof(App)" render-mode="WebAssemblyPrerendered" />

    ...

    <persist-component-state />
</body>
@page "/weather-forecast-preserve-state"
@implements IDisposable
@using BlazorSample.Shared
@inject IWeatherForecastService WeatherForecastService
@inject PersistentComponentState ApplicationState

<PageTitle>Weather Forecast</PageTitle>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[] forecasts = Array.Empty<WeatherForecast>();
    private PersistingComponentStateSubscription persistingSubscription;

    protected override async Task OnInitializedAsync()
    {
        persistingSubscription = 
            ApplicationState.RegisterOnPersisting(PersistForecasts);

        if (!ApplicationState.TryTakeFromJson<WeatherForecast[]>(
            "fetchdata", out var restored))
        {
            forecasts = 
                await WeatherForecastService.GetForecastAsync(DateOnly.FromDateTime(DateTime.Now));
        }
        else
        {
            forecasts = restored!;
        }
    }

    private Task PersistForecasts()
    {
        ApplicationState.PersistAsJson("fetchdata", forecasts);

        return Task.CompletedTask;
    }

    void IDisposable.Dispose()
    {
        persistingSubscription.Dispose();
    }
}

References
https://learn.microsoft.com/en-us/aspnet/core/blazor/components/prerendering-and-integration?view=aspnetcore-7.0&pivots=server
https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/persist-component-state?view=aspnetcore-7.0

Define reusable RenderFragments in code in ASP.NET Blazor

@RenderWelcomeInfo

<p>Render the welcome content a second time:</p>

@RenderWelcomeInfo

@code {
    private RenderFragment RenderWelcomeInfo = @<p>Welcome to your new app!</p>;
}

To make RenderTreeBuilder code reusable across multiple components, declare the RenderFragment public and static:

public static RenderFragment SayHello = @<h1>Hello!</h1>;

RenderFragment delegates can accept parameters. The following component passes the message (message) to the RenderFragment delegate:

<div class="chat">
    @foreach (var message in messages)
    {
        @ChatMessageDisplay(message)
    }
</div>

@code {
    private RenderFragment<ChatMessage> ChatMessageDisplay = message =>
        @<div class="chat-message">
            <span class="author">@message.Author</span>
            <span class="text">@message.Text</span>
        </div>;
}

References
https://learn.microsoft.com/en-us/aspnet/core/blazor/performance?view=aspnetcore-7.0#define-reusable-renderfragments-in-code

Use HttpContext Object in Blazor Server to Retrieve Information About the User or User Agent

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    // . . .
    services.AddHttpContextAccessor();
}

Index.razor

@page "/"
@using Microsoft.AspNetCore.Http
@inject IHttpContextAccessor httpContextAccessor

<p>UserAgent = @UserAgent</p>
<p>IPAddress = @IPAddress</p>

@code {
    public string UserAgent { get; set; }
    public string IPAddress { get; set; }

    protected override void OnInitialized()
    {
        UserAgent = httpContextAccessor.HttpContext.Request.Headers["User-Agent"];
        IPAddress = httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString();
    }
}

Access HttpContext in Service

Don’t use IHttpContextAccessor/HttpContext directly or indirectly in the Razor components of Blazor Server apps. Blazor apps run outside of the ASP.NET Core pipeline context. The HttpContext isn’t guaranteed to be available within the IHttpContextAccessor, and HttpContext isn’t guaranteed to hold the context that started the Blazor app.

The recommended approach for passing request state to the Blazor app is through root component parameters during the app’s initial rendering. Alternatively, the app can copy the data into a scoped service in the root component’s initialization lifecycle event for use across the app.

namespace Get_HttpContext_ASP.NET_Core
{
    using Microsoft.AspNetCore.Http;

    public class UserService : IUserService
    {
        private readonly IHttpContextAccessor _httpContextAccessor;

        public UserService(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }

        public string GetLoginUserName()
        {
            return _httpContextAccessor.HttpContext.User.Identity.Name;
        }
    }
}

References
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-context?view=aspnetcore-7.0
https://www.syncfusion.com/faq/blazor/tips-and-tricks/how-do-you-use-the-httpcontext-object-in-blazor-server-side-to-retrieve-information-about-the-user-or-user-agent
https://www.telerik.com/blogs/how-to-get-httpcontext-asp-net-core
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/use-http-context?view=aspnetcore-7.0

Enrichment in Serilog

Log events can be enriched with properties in various ways. A number of pre-built enrichers are provided through NuGet:

Install-Package Serilog.Enrichers.Process
Install-Package Serilog.Enrichers.Environment
Install-Package Serilog.Enrichers.Thread

Configuration for enrichment is done via the Enrich configuration object:

var log = new LoggerConfiguration()
    .Enrich.WithMachineName()   
    .Enrich.WithThreadId()
    .Enrich.WithThreadName()
    .Enrich.WithProcessId()
    .Enrich.WithProcessName()
    .WriteTo.Console()
    .CreateLogger();

The LogContext

Serilog.Context.LogContext can be used to dynamically add and remove properties from the ambient “execution context”; for example, all messages written during a transaction might carry the id of that transaction, and so-on.

This feature must be added to the logger at configuration-time using .FromLogContext():

var log = new LoggerConfiguration()
    .Enrich.FromLogContext()

Then, properties can be added and removed from the context using LogContext.PushProperty():

log.Information("No contextual properties");

using (LogContext.PushProperty("A", 1))
{
    log.Information("Carries property A = 1");

    using (LogContext.PushProperty("A", 2))
    using (LogContext.PushProperty("B", 1))
    {
        log.Information("Carries A = 2 and B = 1");
    }

    log.Information("Carries property A = 1, again");
}

References
https://github.com/serilog/serilog/wiki/Enrichment

Form Validation Using Validator Component in ASP.NET Blazor

public class CustomValidator : ComponentBase
{
    private ValidationMessageStore messageStore;
    [CascadingParameter] public EditContext CurrentEditContext { get; set; }

    protected override void OnInitialized()
    {
        if (CurrentEditContext == null)
        {
            throw new InvalidOperationException(
                "To use validator component your razor page should have the edit component");
        }

        messageStore = new ValidationMessageStore(CurrentEditContext);
        
        // Clear Error Message On Raise Of OnValidationRequested Form Event
        CurrentEditContext.OnValidationRequested += (s, e) => messageStore.Clear();
        
        // Clear Error Message On Input Field Change Event
        CurrentEditContext.OnFieldChanged += (s, e) => messageStore.Clear(e.FieldIdentifier);
    }

    public void DisplayErrors(Dictionary<string, List<string>> errors)
    {
        foreach (var error in errors)
        {
            messageStore.Add(CurrentEditContext.Field(error.Key), error.Value);
        }

        CurrentEditContext.NotifyValidationStateChanged();
    }

    public void DisplayErrors(string fieldName, List<string> errors)
    {
        foreach (var error in errors)
        {
            messageStore.Add(CurrentEditContext.Field(fieldName), error);
        }

        CurrentEditContext.NotifyValidationStateChanged();
    }
}

App.razor

<EditForm Model="@_model" OnValidSubmit="ValidSubmit">
        <DataAnnotationsValidator/>
        <ValidationSummary/>
        <CustomValidator @ref="_customValidator"></CustomValidator>
...
@code {

    private CustomValidator _customValidator;

...
private async Task ValidSubmit(EditContext obj)
   {
       if (await CheckValidations())
       {
           if (OnSubmit.HasDelegate)
           {
               Customer.Customerr = _model.Customer.TrimText();
               Customer.ProvinceUUID = _model.ProvinceUUID;
               Customer.CityUUID = _model.CityUUID;

               await OnSubmit.InvokeAsync(new NewCustomerInfoEventArgs()
               {
                   Customer = Customer,
               });
           }
       }
   }

   private async Task<bool> CheckValidations()
   {
       int errors = 0;
       ApplicationDbContext context = await DbFactory.CreateDbContextAsync();
       var customer = _model.Customer.TrimText();
       var isExist = await context.DcCustomers.AnyAsync(x => x.Customerr == customer);
       await context.DisposeAsync();
       if (isExist)
       {
           _customValidator.DisplayErrors(nameof(_model.Customer), new List<string> { "نام مشتری تکراری است" });
           errors += 1;
       }

       if (errors == 0)
       {
           return true;
       }
       
       return false;
   }

References
https://www.learmoreseekmore.com/2021/01/blazor-server-forms-validator-component.html