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:
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.
publicstaticclassEntityEntryExtensions
{
publicstaticasync Task LoadAllReferences<T>(this EntityEntry<T> entry) where T : class
{
foreach (var reference in entry.References)
{
await reference.LoadAsync();
}
}
}
publicstaticclassEntityExtensions
{
publicstaticasync 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.
*/
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
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
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 :
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;
namespaceHelloBlazorLocalization
{
publicstaticclassWebAssemblyHostExtensions
{
publicasyncstatic 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", outvar 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 {
protectedoverrideasync Task OnParametersSetAsync()
{
var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
var queryStrings = QueryHelpers.ParseQuery(uri.Query);
if (queryStrings.TryGetValue("culture", outvar 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.
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.
This article will look at running http client calls used by Blazor apps using bUnit. First off, bUnit is a library to perform unit tests for Blazor apps.
We will look at mocking http client calls in this article.
I have added a Github repo with the sample code in this article here :
We will use the Nuget package RichardSzalay.MockHttp to do much of the mocking of http client.
The following helper extension methods allow us to easier add mocking of http client calls.
Helper extension methods for http client using bUnit - MockHttpClientBunitHelpers.cs
using Bunit;
using Microsoft.Extensions.DependencyInjection;
using RichardSzalay.MockHttp;
using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
namespaceBlazorHttpClientMocking.Test.Helpers
{
publicstaticclassMockHttpClientBunitHelpers
{
publicstatic MockHttpMessageHandler AddMockHttpClient(this TestServiceProvider services, string baseAddress = @"http://localhost")
{
var mockHttpHandler = new MockHttpMessageHandler();
var httpClient = mockHttpHandler.ToHttpClient();
httpClient.BaseAddress = new Uri(baseAddress);
services.AddSingleton<HttpClient>(httpClient);
return mockHttpHandler;
}
publicstatic T? FromResponse<T>(this HttpResponseMessage? response, JsonSerializerOptions? options = null)
{
if (response == null)
{
returndefault(T);
}
if (options == null)
{
options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
}
string responseString = response.Content.ReadAsStringAsync().Result;
var result = JsonSerializer.Deserialize<T>(responseString, options);
return result;
}
publicstaticasyncTask<T?> FromResponseAsync<T>(this HttpResponseMessage? response, JsonSerializerOptions? options = null)
{
if (response == null)
{
returnawait Task.FromResult(default(T));
}
if (options == null)
{
options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
}
string responseString = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<T>(responseString, options);
return result;
}
publicstatic MockedRequest RespondJson<T>(this MockedRequest request, T content)
{
request.Respond(req =>
{
var response = new HttpResponseMessage(HttpStatusCode.OK);
response.Content = new StringContent(JsonSerializer.Serialize(content));
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
return response;
});
return request;
}
publicstatic MockedRequest RespondJson<T>(this MockedRequest request, Func<T> contentProvider)
{
request.Respond(req =>
{
var response = new HttpResponseMessage(HttpStatusCode.OK);
response.Content = new StringContent(JsonSerializer.Serialize(contentProvider()));
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
return response;
});
return request;
}
}
}
The method AddMockHttpClient, which is an extension method on TestServiceProvider adds the mocked client. In the code above we read the response into a string and deserialize with System.Text.Json,
defaulting to case insensitive property naming, since this is default System.Text.Json on web, but not elsewhere, such as in test projects.
Let's look at a unit test which then sets up a mocked http client response that is used in the Blazor sample app on the FetchData page.
using BlazorHttpClientMocking.Test.Helpers;
using Bunit;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using RichardSzalay.MockHttp;
usingstatic BlazorHttpClientMocking.Pages.FetchData;
namespaceBlazorHttpClientMocking.Test
{
publicclassFetchDataTests
{
[Fact]
publicasync Task FetchData_HttpClient_Request_SuccessResponse()
{
//Arrange usingvar ctx = new TestContext();
var httpMock = ctx.Services.AddMockHttpClient();
string knownUrl = @"/sample-data/weather.json";
var sampleData = await SerializationHelpers.DeserializeJsonAsync<WeatherForecast[]>(knownUrl.TrimStart('/')); //trimming start of url since we need a physical path
httpMock.When(knownUrl).RespondJson(sampleData);
//Actvar httpClient = ctx.Services.BuildServiceProvider().GetService<HttpClient>();
var httpClientResponse = await httpClient!.GetAsync(knownUrl);
httpClientResponse.EnsureSuccessStatusCode();
var forecasts = await httpClientResponse.FromResponseAsync<WeatherForecast[]>();
//Assert
forecasts.Should().NotBeNull();
forecasts.Should().HaveCount(5);
}
}
}
In the arrange part of the unit test above, we create a TestContext and add a mocked http client using the extension method shown earlier. We read out the sample json data and set up using the When method
and remember to add "/" to the path as this is expected since we have a baseAddress specified on the http client, set to @"http://localhost" default.
We retrieve http client via the Services collection on the TestContext and call BuildServiceProvider and GetService method to get the http client with the mocking. The mocking must be done via the When method and then we get the client. The mocked http client is a singleton service here.
We can also do parameters in the mocking of http client calls.
Using parameters in http client calls
Lets first add parameter support for the Fetchdata razor page.
Fetchdata.razor
Let's now look at using parameters in mocked http client calls in another unit test.
[Fact]
publicasync Task FetchData_HttpClient_With_Parameter_Request_SuccessResponse()
{
//Arrange usingvar ctx = new TestContext();
var httpMock = ctx.Services.AddMockHttpClient();
string knownUrl = @"/sample-data/weather.json/0";
string fileUrl = @"sample-data/weather.json";
var sampleData = await SerializationHelpers.DeserializeJsonAsync<WeatherForecast[]>(fileUrl); //trimming start of url since we need a physical path
httpMock.When(knownUrl).RespondJson(sampleData);
//Actvar renderComponent = ctx.RenderComponent<FetchData>(p => p
.Add(fd => fd.Id, 0));
//Assert
renderComponent.Instance.forecasts.Should().NotBeNull();
renderComponent.Instance.forecasts.Should().HaveCount(1);
}
Here we use bUnit's capabilities in rendering Blazor components using the RenderComponent method and we also set the Id parameter here to the value 0 which now will prepare our component
with the right forecasts, here only one forecast will be shown. We use the Instance property to look at the forecasts field of the component.
internal WeatherForecast[]? forecasts;
So bUnit can be used both the mock http client calls and also render Blazor components and also support parametrized calls of mocked http client calls.
Finally a tip concerning letting your internal fields to be available for test project. In the csproj file of the application we can set it up like in this example :
Here we set up that the test project can see the internals of the blazor app project. This allows the test project to see internal methods and internal fields, internal classes and so on. This allows you to avoid changing parameters or fields in your components from private to public for example and instead change access modifier to internal so the tests can access those members.
This article presents some source code of how to write and read cookies from Blazor WebAssembly - WASM. For Blazor WASM, we are going to use Javascript to write these cookies. Blazor WASM has not an easy way to write
these cookies programatically, as the use of HttpContext accessor is discouraged and not available, i.e. you cannot just add cookies without round trips to backend services.
But via Js, the client can write cookies. I looked into a helper lib to write such cookies and do so using different attribute values for the cookies.
The following Mozilla Developer Network (MDN) page is helpful in detailing cookies, which attribute values can be set on them. Cookies are used to give user experience since they track user's on a web site and give tailored user experience - and advertising - and also can track the users accross servers / web sites as third-party cookies. They are small string values that are either stored on clients inside cookie storage in the browser's folder on the user's hard drive or in memory or other place such as partitioned cookies.
Interface for cookie handling for Blazor.wasm, IAltairCABlazorCookieUtil.cs
using AltairCA.Blazor.WebAssembly.Cookie.Models;
namespaceAltairCA.Blazor.WebAssembly.Cookie
{
publicinterfaceIAltairCABlazorCookieUtil
{
///<summary>/// Set a object in the cookie///</summary>///<param name="key">The key for the cookie (name)</param>///<param name="value">Cookie value</param>///<param name="span">TimeSpan that will be set to the 'expires' attribute value</param>///<param name="path">Path in the request url which must exist for the cookie to be sent in requests </param>///<param name="domain">The host to which the cookie will be sent</param>///<param name="secure">Specifies that the cookie will be sent only over secure protocols</param>///<param name="isSession">Flags the cookie as a session cookie (temporal) by setting the 'expires' attribute value to ''</param>///<param name="partitioned">Requires that the browser has activated partitioned cookies</param>///<param name="maxAgeInSeconds">Maximum age in seconds</param>///<returns></returns>Task SetValueAsync(string key, objectvalue, TimeSpan? span = null, string? path = null,
string? domain = null, bool? secure = null, SameSite? sameSite = null, bool? partitioned = null,
bool? isSession = null, int? maxAgeInSeconds = null);
///<summary>/// Set a string in the cookie///</summary>///<param name="key">The key for the cookie (name)</param>///<param name="value">Cookie value</param>///<param name="span">TimeSpan that will be set to the 'expires' attribute value</param>///<param name="path">Path in the request url which must exist for the cookie to be sent in requests </param>///<param name="domain">The host to which the cookie will be sent</param>///<param name="secure">Specifies that the cookie will be sent only over secure protocols</param>///<param name="isSession">Flags the cookie as a session cookie (temporal) by setting the 'expires' attribute value to ''</param>///<param name="partitioned">Requires that the browser has activated partitioned cookies</param>///<param name="maxAgeInSeconds">Maximum age in seconds</param>///<returns></returns>Task SetValueAsync(string key, stringvalue, TimeSpan? span = null, string? path=null, string? domain=null,
bool? secure = null, SameSite? sameSite = null, bool? partitioned = null, bool? isSession = null,
int? maxAgeInSeconds = null);
Task<string> GetValueAsync(string key);
Task<T> GetValueAsync<T>(string key) where T : class;
Task RemoveAsync(string key, string? path = null);
}
}
And here is the implementation.
Implementation for cookie handling for Blazor.wasm, AltairCABlazorCookieUtil.cs
using System.ComponentModel;
using AltairCA.Blazor.WebAssembly.Cookie.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
using Newtonsoft.Json;
namespaceAltairCA.Blazor.WebAssembly.Cookie
{
internalclassAltairCABlazorCookieUtil : IAltairCABlazorCookieUtil
{
readonly IJSRuntime JSRuntime;
privatereadonly AltairCABlazorCookieConfigOptions _settings;
publicAltairCABlazorCookieUtil(IJSRuntime jsRuntime,IOptions<AltairCABlazorCookieConfigOptions> options)
{
JSRuntime = jsRuntime;
_settings = options.Value;
}
public Task SetValueAsync(string key, objectvalue, TimeSpan? span = null, string? path = null,
string? domain = null, bool? secure = null, SameSite? sameSite = null, bool? partitioned = null, bool? isSession = null,
int? maxAgeInSeconds = null)
{
return SetValueAsync(key, JsonConvert.SerializeObject(value), span, path, domain, secure, sameSite, partitioned,
isSession);
}
publicasync Task SetValueAsync(string key, stringvalue, TimeSpan? span = null, string? path=null, string? domain=null,
bool? secure = null, SameSite? sameSite = null, bool? partitioned = null, bool? isSession = null,
int? maxAgeInSeconds = null)
{
if (string.IsNullOrWhiteSpace(path))
path = _settings.Path;
if (!span.HasValue)
span = _settings.DefaultExpire;
if (string.IsNullOrWhiteSpace(domain))
domain = _settings.Domain;
if (!secure.HasValue)
secure = _settings.IsSecure;
var curExp = span.HasValue && span.Value.Ticks > 0 && isSession != true && !maxAgeInSeconds.HasValue ? DateToUTC(span.Value) : "";
List<string> keyvals = new List<string>();
keyvals.Add($"{key}={value}");
keyvals.Add($"expires={curExp}");
keyvals.Add($"path={path}");
if(!string.IsNullOrWhiteSpace(domain))
keyvals.Add($"domain={domain}");
if(secure.HasValue && secure.Value)
keyvals.Add("secure");
if (maxAgeInSeconds.HasValue && isSession != true)
{
keyvals.Add($"max-age={maxAgeInSeconds.Value}");
}
if (sameSite.HasValue){
DescriptionAttribute desc = (DescriptionAttribute) typeof(SameSite).GetMember(sameSite.Value.ToString()).First().GetCustomAttributes(typeof(DescriptionAttribute), false).First();
keyvals.Add($"samesite={desc.Description}");
}
string cookieToSet = string.Join(";", keyvals);
if (partitioned == true){
cookieToSet += ";partitioned";
}
await SetCookie(cookieToSet);
}
publicasync Task RemoveAsync(string key,string path = null)
{
if (string.IsNullOrWhiteSpace(path))
path = _settings.Path;
List<string> keyvals = new List<string>();
keyvals.Add($"{key}=");
keyvals.Add($"Path={path}");
keyvals.Add($"expires=Thu, 01 Jan 1970 00:00:01 GMT;");
await SetCookie(string.Join(";", keyvals));
}
publicasyncTask<T> GetValueAsync<T>(string key) where T : class
{
var res = await GetValueAsync(key);
if (res == null)
returndefault(T);
return JsonConvert.DeserializeObject<T>(res);
}
publicasync Task<string> GetValueAsync(string key)
{
var cValue = await GetCookie();
if (string.IsNullOrEmpty(cValue)) returnnull;
var vals = cValue.Split(';');
foreach (var val in vals)
if(!string.IsNullOrEmpty(val) && val.IndexOf('=') > 0)
if(val.Substring(0, val.IndexOf('=')).Trim().Equals(key, StringComparison.OrdinalIgnoreCase))
return val.Substring(val.IndexOf('=') + 1);
returnnull;
}
privateasync Task SetCookie(stringvalue)
{
await JSRuntime.InvokeVoidAsync("eval", $"document.cookie = \'{value}\'");
}
privateasyncTask<string> GetCookie()
{
returnawait JSRuntime.InvokeAsync<string>("eval", $"document.cookie");
}
privatestaticstringDateToUTC(TimeSpan span) => DateTime.Now.Add(span).ToUniversalTime().ToString("R");
}
}
//we also have this enum used in cookie handler class aboveusing System.ComponentModel;
namespaceAltairCA.Blazor.WebAssembly.Cookie.Models;
publicenum SameSite {
[Description("lax")]
Lax = 0,
[Description("strict")]
Strict = 1,
[Description("none")]
None = 2
}
The MDN article details a lot around cookies and there are a lot of way of controlling these cookies via attributes. Most browsers limit Cookie sizes to be 4 kilobytes maximum length (4096 bytes) of all cookies
on a server. And a maxiumum of 50 cookies, still must be below the 4 kB limit.
https://stackoverflow.com/a/4604212
We set up the cookie handling via Program.cs of a Blazor WASM like this :
Implementation for cookie handling for Blazor.wasm, AltairCABlazorCookieUtil.cs
As we can see, we remove the cookie by setting it to expire at Unix Epoch zero (1970, 1st of January) in the cookie handling.
A good util to inspect Cookies and even edit them are available in this Google plugin:
Edit this Cookie
Counter.razor - using the Cookie util in Blazor WASM sample app
@inject IAltairCABlazorCookieUtil _cookieUtil;
privateasync Task IncrementCount()
{
currentCount++;
Content = await _cookieUtil.GetValueAsync("c");
ContentObj = await _cookieUtil.GetValueAsync<object>("d");
await _cookieUtil.SetValueAsync("c", "this is cookie with key c");
await _cookieUtil.SetValueAsync("d", new
{
hello = "hello world. i am a cookie with key d"
});
await _cookieUtil.SetValueAsync("cookieWithSameSiteSet", "Cookie which specified cross site request inclusion of the cookie : SameSite value (lax | strict | none)", sameSite: SameSite.Lax);
await _cookieUtil.SetValueAsync("cookiePartitioned", "Partitioned cookie", partitioned: true);
await _cookieUtil.SetValueAsync("cookieWithMaxAge", "Max age cookie", maxAgeInSeconds: 600);
await _cookieUtil.SetValueAsync("cookieWhichIsToBeSetToSessionCookie", "Session cookie = temporal cookie", isSession: true);
}
One important note about the cookie util here, observe the usage of the 'eval' method to set the cookie via Js in the util class and also retrieve cookies :
Implementation for cookie handling for Blazor.wasm, AltairCABlazorCookieUtil.cs
Using 'eval' we let Js run the Js code we pass in here in Blazor WASM app. This also means that we cannot write HttpOnly cookies, since we rely fully on Js here.
As some of you know, third party cookie support are planned by Chrome to be discontinued in support. The following article is interesting reading about this.
https://itrust-digital.com/cookieless-future/