Monday 31 July 2023

Loading references (navigational properties) for an item in Azure Cosmos DB

The extensions methods shown here can be applied in general in Entity Framework Core. In Azure Cosmos DB, if you use the FindAsync method for example, you will not load the references of the item automatically. Instead, you must explicitly go via Entry method and then to LoadAsync on each Reference. A Reference is also called a navigational property in EF. Let's first consider this code finding some Trip data item (data is stored in json format in Azure Cosmos DB since it is schema less and non-relational db or a 'document DB') and related data inside the Driver, Address. The POCO for Trip looks like this:


#region Info and license

#endregion

namespace TransportApp.Domain
{
  public class Trip
  {
    public string TripId { get; set; }
    public DateTime BeginUtc { get; set; }
    public DateTime? EndUtc { get; set; }
    public short PassengerCount { get; set; }

    public string DriverId { get; set; }
    public Driver Driver { get; set; }
    public string VehicleId { get; set; }
    public Vehicle Vehicle { get; set; }
    public string FromAddressId { get; set; }
    public Address FromAddress { get; set; }
    public string ToAddressId { get; set; }
    public Address ToAddress { get; set; }
  }
}




We create a db context for Azure Cosmos DB like this :


 private readonly IDbContextFactory _contextFactory;

 public SomeService(IDbContextFactory contextFactory)
 {
  _contextFactory = contextFactory; 
  
  //..
  
 }
 
 public async Task RunSomeDemoCode(){
    using var context = await contextFactory.CreateDbContextAsync();
 
 }
 	


Once we have our context object we can get data from Azure Cosmos DB.

 
 public async Task RunSomeDemoCode(){
    using var context = await contextFactory.CreateDbContextAsync();
    
    var trip1 = await otherContext.Trips.FindAsync($"{nameof(Trip)}-1");
    await otherContext.LoadEntityWithAllReferences(trip1!);
 
 }
 	


Note that we probably want to check that trip1 object is not null here. We want to get the relations also here. This is not automatically loaded in Azure Cosmos DB ! We can load the relations or navigational properties using the following extensions methods listed below. If you use the method accepting an EntityEntry, you must use the Entry method first, like shown in the other method.


 public static class EntityEntryExtensions
    {
        public static async Task LoadAllReferences<T>(this EntityEntry<T> entry) where T : class
        {
            foreach (var reference in entry.References)
            {
                await reference.LoadAsync();
            }
        }
    }

    public static class EntityExtensions
    {
        public static async Task LoadEntityWithAllReferences<T>(this DbContext dbContext, T dataItem) where T : class
        {
            if (dataItem == null)
            {
                return;
            }
            var entity = dbContext.Entry(dataItem!);
            foreach (var reference in entity.References)
            {
                await reference.LoadAsync();
            }
        }
    }



I have added two screenshots here, first before calling the method LoadEntityWithAllReferences, and afterwards.
Before: 

After:



As we can see from the screen shots, the references has been loaded and now you avoid to manually have to load one and one reference property / navigation property. Note - about some of the sample code - it came from a Pluralsight Course. I have looked more into the extension methods here myself.


/*
  This demo application accompanies Pluralsight course 'Using EF Core 6 with Azure Cosmos DB', 
  by Jurgen Kevelaers. See https://pluralsight.pxf.io/efcore6-cosmos.

  MIT License

  Copyright (c) 2022 Jurgen Kevelaers

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  SOFTWARE.
*/


Saturday 29 July 2023

Forced cancellation of a CancellationToken

CancellationToken are used to signal that an asynchronous task should cancelled, at some given point in the code from here on a cancellation has been signaled and downstream. Methods downstream are methods and sub methods / sub routines. We can pass a cancellation token into for example Entity Framework Core to cancel a heavy database I/O process. Cancelling a method this way can be invoked on many different ways.


Examples of ways to cancel a cancellation tokens
1. By user interface actions. Like hitting a Cancel button in the UI. For example, the REST client Insomnia allows you to do this. 2. Other means of stopping a task. Inside a browser you can stop requests by reloading a window / tab. For example - If you use Swagger API in a browser, you can refresh the Swagger web page in a tab to indicate a cancellation is desired. The suggested way to programatically cancelling a cancellation token in code is to throw a OperationCanceledException in your code. If you have the Cancellation Token Source - the CTS - you can cancel the cancellation token as you like. Most often you do not have the CTS. You can always throw an OperationCanceledException. The source code can then listen to such a cancellation if you call ThrowIfCancellationIsRequested on the cancellation token. Another way to cancel a cancellation token is to create a linked cancellation token and cancellation the cancellation token source you created for it. This is an alternative way that directly updates the cancellation of a token to be cancelled downstream in case you have some logic further upstream that still should be called instead of directly throwing an OperationCancelledException. Is it a good approach to programatically create a new cancellation token and overwrite it or should you instead just throw an OperationCancelledException ? And why not just stick to the same object ? I am overwriting the token here using ref keyword, since CancellationToken is a struct object. This makes it harder to overwite, since structs are copied by value into methods, such as an extension method. However if this is a good idea or not - I include the code here for completeness.

Defining an extension method on cancellation tokens which can cancel them

CancellationTokenExtension.cs

namespace CarvedRock.Api
{
    public static class CancellationTokenExtensions
    {
        public static void ForceCancel(ref this CancellationToken cancellationToken,
        Func<bool>? condition = null)
        {
            if (condition == null || condition.Invoke())
            {
                var cts = CancellationTokenSource.CreateLinkedTokenSource(
                cancellationToken);
                cancellationToken = cts.Token;
                cts.Cancel();
            }
        }
    }
}


Here is some sample code that shows how we can use this extension method.

Using an extension method that is cancellation cancellation tokens

ProductController.cs

    [HttpGet]
    //[ResponseCache(Duration = 90, VaryByQueryKeys = new[] { "category" })]
    public async Task> Get(CancellationToken cancellationToken, string category = "all")
    {
        cancellationToken.ForceCancel(() => category == "kayak");
        using (_logger.BeginScope("ScopeCat: {ScopeCat}", category))
        {     
            _logger.LogInformation( "Getting products in API.");
            return await _productLogic.GetProductsForCategoryAsync(category, cancellationToken);
        }
    }


So there we have it, we can either use an approach like in this article, creating a temporary new cancellation token source and then created a linked cancellation token from the original cancellation token, overwriting it, and at the same time cancel it, possible by supplying a condition to decide if we want to cancel the cancellation token or not. Or we could just throw an OperationCanceledException. In the example code above I finally make the EF code communicating with the database and supply the cancelation token into a ToListAsync method here. This makes our code cancellable, in case we for example hit big data in the database that is slow and user wants to cancel.

Using an extension method that is cancellation cancellation tokens - downstream code making use of same cancellation token passed into sub method

CarvedRockRepository.cs


  public async Task<List<Product>> GetProductsAsync(string category, CancellationToken cancellationToken)
        { 

//.. Inside GetProductAsync method receiving token - more code above here inside the method

 var productsToSerialize = await _ctx.Products.Where(p => p.Category == category || category == "all")
  .Include(p => p.Rating).ToListAsync(cancellationToken);

// more code inside method below

}
  


Note ! Remember to pass down the cancellation token to your methods and sub methods /sub routines.

Sunday 9 July 2023

Localizing Blazor WASM applications with a language picker

This article presents code how to localize a Blazor WASM app with a language picker. This is part of globalizing an app. The sample app is in this sample app in GitHub: https://github.com/toreaurstadboss/HelloBlazorLocalization

First off, we need to add some Nuget package references, such as adding a capability of using local storage in a convenient way in the Blazor WASM app. The project file of the sample app has this setup :

Project file - HelloBlazorLocalization.csproj
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
	<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
  </PropertyGroup>

  <ItemGroup>
    <Compile Remove="Shared\Resources\**" />
    <Content Remove="Shared\Resources\**" />
    <EmbeddedResource Remove="Shared\Resources\**" />
    <None Remove="Shared\Resources\**" />
  </ItemGroup>

  <ItemGroup>
	  <PackageReference Include="Blazored.LocalStorage" Version="4.3.0" />
	  <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.3" />
	  <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.3" PrivateAssets="all" />
	  <PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.1.1" />
	  <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
	  <PackageReference Include="Microsoft.Extensions.Localization" Version="6.0.3" />
  </ItemGroup>

  <ItemGroup>
    <Folder Include="wwwroot\flag-icons\" />
  </ItemGroup>

  <ItemGroup>
    <None Include="HelloBlazorLocalization.sln" />
  </ItemGroup>

</Project>



Note the use of the property setting : BlazorWebAssemblyLoadAllGlobalizationData This is required to add localization to your Blazor WASM app ! Also note that we use Blazored.LocalStorage to write and access local storage. Let's look at the Program.cs file next how we set up the app.

Program.cs

using Blazored.LocalStorage;
using HelloBlazorLocalization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

builder.Services.Configure<RequestLocalizationOptions>(options =>
{
    string[] supportedCultures = new[] { "no", "en" };
    options
        .AddSupportedCultures(supportedCultures)
        .AddSupportedUICultures(supportedCultures)
        .SetDefaultCulture("no");
});

builder.Services.AddLocalization(options => 
    options.ResourcesPath = "Resources");

builder.Services.AddBlazoredLocalStorage();

await builder.Services.BuildServiceProvider().SetDefaultCultureAsync();

await builder.Build().RunAsync();


An extension method is added to ServiceProvider to load up selected culture from local storage. It also inspects the query string set, if any, since language picker component presented later on will reload the Blazor WASM app after selecting language.


WebAssemblyHostExtensions.cs

using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.WebUtilities;
using System.Globalization;

namespace HelloBlazorLocalization
{

    public static class WebAssemblyHostExtensions
    {
        public async static Task SetDefaultCultureAsync(this ServiceProvider serviceProvider)
        {
            var navigationManager = serviceProvider.GetService<NavigationManager>(); 
            var uri = navigationManager!.ToAbsoluteUri(navigationManager.Uri);
            var queryStrings = QueryHelpers.ParseQuery(uri.Query);
            var localStorage = serviceProvider.GetRequiredService<ILocalStorageService>();

            if (queryStrings.TryGetValue("culture", out var selectedCulture))
            {
                await localStorage.SetItemAsStringAsync("culture", selectedCulture);
            }

            var cultureString = await localStorage.GetItemAsync<string>("culture");
            CultureInfo cultureInfo;

            if (!string.IsNullOrWhiteSpace(cultureString))
            {
                cultureInfo = new CultureInfo(cultureString);
            }
            else
            {
                cultureInfo = new CultureInfo("en-US");
            }

            CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
            CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
        }
    }

}


Now, let's look at the Index.razor file where we repeat some of the code in the extension method shown above.


Index.razor

@page "/"
@using System.Globalization;

@inject NavigationManager NavigationManager
@inject Blazored.LocalStorage.ILocalStorageService LocalStorage
@inject IStringLocalizer<SharedResources> Localizer

<PageTitle>@Localizer["Home"]</PageTitle>

<h1>@Localizer["Home"]</h1>

@Localizer["HomeDescription"]

<SurveyPrompt Title="How is Blazor working for you?" />


@code {

    protected override async Task OnParametersSetAsync()
    {
        var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
        var queryStrings = QueryHelpers.ParseQuery(uri.Query);
        if (queryStrings.TryGetValue("culture", out var selectedCulture))
        {
            await LocalStorage.SetItemAsStringAsync("culture", selectedCulture);

        }
        else
        {
            selectedCulture = await LocalStorage.GetItemAsStringAsync("culture");
        }
        if (!string.IsNullOrWhiteSpace(selectedCulture))
        {
            var cultureInfo = new CultureInfo(selectedCulture);
            CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
            CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;            
        }

    }

}

To localize strings, we first do an inject of the IStringLocalizer as shown in the razor file. We also set the resource key when we fetch the localized text (Value). This is set up in the SharedResource files. This is done in the sample app in three files.
  • An empty class called SharedResources at the root level
  • Two resources files (.resx) called SharedResources.en.resx and SharedResources.no.resx
You can have multiple resource file in Blazor WASM. Note that we in Program.cs set up the ResourcesPath to the sub folder Resources, where we put the .resx files. See the sample app for details (clone the Github repo). Next up, let's look at the LanguagePicker.razor file that will show a language picker. The sample app got flag icons for all flags of countries so check out the folder flag-icons under wwwroot folder in the sample app.


LanguagePicker.razor

@using Microsoft.AspNetCore.Localization
@using Microsoft.Extensions.Options
@using System.Globalization
@inject IOptions<RequestLocalizationOptions> LocalizationOptions
@inject Blazored.LocalStorage.ILocalStorageService LocalStorage

<div class="mt-3 mb-3 mx-5">

    @foreach (var culture in LocalizationOptions.Value.SupportedCultures)
    { 
        
        <a style="cursor:pointer" onclick="location.href = '/?culture=@culture.ToString()';" class="text-decoration-none">
            <img style="width:20px" src="flag-icons/@(culture.Name).png" alt="@culture.Name" />
            <span class="badge rounded-pill mx-1 border border-primary 
            @((culture.ToString() == CultureInfo.CurrentCulture.ToString() || culture.ToString() == _selectedCulture) ?
                "btn btn-success" : "btn btn-info text-dark")">@culture</span>
        </a>  <br />
    }
</div>

@code {
    private string? _selectedCulture;
    protected override async Task OnParametersSetAsync()
    {
        _selectedCulture = await LocalStorage.GetItemAsStringAsync("culture");
    }
}


Note that Blazor WASM app should refresh entirely after choosing another language. Also note that you should set up multiple languages in your browser to get the expected results. You should have the supported languages set up in Blazor WASM, however it might still work to get the localization done if the language settings are not set up to include the specified languages. But if you do not see the expected results, check the language settings in your browser. And as can be seen, we use local storage to persist our selected language. The selected language is displayed with the green button to indicate selected. When the Blazor WASM reloads, the selected language is fetched from local storage. This can be seen in Application => Local Storage in F12 Developer Tools in Chrome for example, when running the app. Blazor WASM supports a reduced set of localization functionality, compared to Blazor server side apps.
A limited set of ASP.NET Core's localization features are supported:

✔️Supported: IStringLocalizer and IStringLocalizer are supported in Blazor apps.

❌Not supported: IHtmlLocalizer, IViewLocalizer, and Data Annotations localization are ASP.NET Core MVC features and not supported in Blazor apps.