From 589f7dee065d752e84ef2d647187fb4762f196f3 Mon Sep 17 00:00:00 2001 From: Jan Johansson Date: Thu, 21 May 2020 19:50:43 +0200 Subject: [PATCH 1/3] Added configuration option for JsonSerializer, added TimespanJsonConverter and amended documentation --- README.md | 78 +++++++++++++++---- .../Blazored.SessionStorage.csproj | 7 +- .../ISessionStorageService.cs | 2 +- .../ISyncSessionStorageService.cs | 2 +- .../JsonConverters/TimespanJsonConverter.cs | 46 +++++++++++ .../ServiceCollectionExtensions.cs | 23 +++++- .../SessionStorageService.cs | 49 +++++++----- .../StorageOptions/SessionStorageOptions.cs | 18 +++++ 8 files changed, 180 insertions(+), 45 deletions(-) create mode 100644 src/Blazored.SessionStorage/JsonConverters/TimespanJsonConverter.cs create mode 100644 src/Blazored.SessionStorage/StorageOptions/SessionStorageOptions.cs diff --git a/README.md b/README.md index aedfe9d..8a08178 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Blazored SessionStorage -A library to provide access to session storage in Blazor applications +A library to provide access to local storage in Blazor applications [![Build Status](https://dev.azure.com/blazored/SessionStorage/_apis/build/status/Blazored.SessionStorage?branchName=master)](https://dev.azure.com/blazored/SessionStorage/_build/latest?definitionId=1&branchName=master) -![Nuget](https://img.shields.io/nuget/v/blazored.sessionstorage.svg) +[![Nuget](https://img.shields.io/nuget/v/blazored.localstorage.svg)](https://www.nuget.org/packages/Blazored.SessionStorage/) ### Installing @@ -11,11 +11,11 @@ You can install from NuGet using the following command: `Install-Package Blazored.SessionStorage` -Or via the Visual Studio package manger. +Or via the Visual Studio package manager. ### Setup -You will need to register the session storage services with the service collection in your _startup.cs_ file. +You will need to register the local storage services with the service collection in your _Startup.cs_ file in Blazor Server. ```c# public void ConfigureServices(IServiceCollection services) @@ -24,34 +24,75 @@ public void ConfigureServices(IServiceCollection services) } ``` +Or in your _Program.cs_ file in Blazor WebAssembly. + +```c# +public static async Task Main(string[] args) +{ + var builder = WebAssemblyHostBuilder.CreateDefault(args); + builder.RootComponents.Add("app"); + + builder.Services.AddBlazoredSessionStorage(); + + await builder.Build().RunAsync(); +} +``` + +### Configuration + +The local storage provides options that can be modified by you at registration in your _Startup.cs_ file in Blazor Server. + + +```c# +public void ConfigureServices(IServiceCollection services) +{ + services.AddBlazoredSessionStorage(config => + config.JsonSerializerOptions.WriteIndented = true); +} +``` +Or in your _Program.cs_ file in Blazor WebAssembly. + +```c# +public static async Task Main(string[] args) +{ + var builder = WebAssemblyHostBuilder.CreateDefault(args); + builder.RootComponents.Add("app"); + + builder.Services.AddBlazoredSessionStorage(config => + config.JsonSerializerOptions.WriteIndented = true); + + await builder.Build().RunAsync(); +} +``` + ### Usage (Blazor WebAssembly) To use Blazored.SessionStorage in Blazor WebAssembly, inject the `ISessionStorageService` per the example below. ```c# -@inject Blazored.SessionStorage.ISessionStorageService sessionStorage +@inject Blazored.SessionStorage.ISessionStorageService localStorage @code { protected override async Task OnInitializedAsync() { - await sessionStorage.SetItemAsync("name", "John Smith"); - var name = await sessionStorage.GetItemAsync("name"); + await localStorage.SetItemAsync("name", "John Smith"); + var name = await localStorage.GetItemAsync("name"); } } ``` -With Blazor WebAssembly you also have the option of a synchronous API, if your use case requires it. You can swap the `ISessionStorageService` for `ISyncSessionStorageService` which allows you to avoid use of `async`/`await`. For either interface, the method names are the same. +With Blazor WebAssembly you also have the option of a synchronous API, if your use case requires it. You can swap the `ISessionStorageService` for `ISyncStorageService` which allows you to avoid use of `async`/`await`. For either interface, the method names are the same. ```c# -@inject Blazored.SessionStorage.ISyncSessionStorageService sessionStorage +@inject Blazored.SessionStorage.ISyncStorageService localStorage @code { protected override void OnInitialized() { - sessionStorage.SetItem("name", "John Smith"); - var name = sessionStorage.GetItem("name"); + localStorage.SetItem("name", "John Smith"); + var name = localStorage.GetItem("name"); } } @@ -62,14 +103,14 @@ With Blazor WebAssembly you also have the option of a synchronous API, if your u **NOTE:** Due to pre-rendering in Blazor Server you can't perform any JS interop until the `OnAfterRender` lifecycle method. ```c# -@inject Blazored.SessionStorage.ISessionStorageService sessionStorage +@inject Blazored.SessionStorage.ISessionStorageService localStorage -@functions { +@code { - protected override async Task OnAfterRenderAsync() + protected override async Task OnAfterRenderAsync(bool firstRender) { - await sessionStorage.SetItemAsync("name", "John Smith"); - var name = await sessionStorage.GetItemAsync("name"); + await localStorage.SetItemAsync("name", "John Smith"); + var name = await localStorage.GetItemAsync("name"); } } @@ -84,12 +125,15 @@ The APIs available are: - ClearAsync() - LengthAsync() - KeyAsync() -- synchronous via `ISyncSessionStorageService`: + - ContainsKeyAsync() + +- synchronous via `ISyncStorageService` (Synchronous methods are **only** available in Blazor WebAssembly): - SetItem() - GetItem() - RemoveItem() - Clear() - Length() - Key() + - ContainsKey() **Note:** Blazored.SessionStorage methods will handle the serialisation and de-serialisation of the data for you. diff --git a/src/Blazored.SessionStorage/Blazored.SessionStorage.csproj b/src/Blazored.SessionStorage/Blazored.SessionStorage.csproj index 35c85c1..c6c6963 100644 --- a/src/Blazored.SessionStorage/Blazored.SessionStorage.csproj +++ b/src/Blazored.SessionStorage/Blazored.SessionStorage.csproj @@ -4,7 +4,6 @@ netstandard2.0 3.0 Blazored.SessionStorage - 3.2 Blazored.SessionStorage 1.0.11 @@ -18,10 +17,10 @@ Blazor SessionStorage Blazored Razor Components - + - - + + diff --git a/src/Blazored.SessionStorage/ISessionStorageService.cs b/src/Blazored.SessionStorage/ISessionStorageService.cs index f8d85cd..97724f7 100644 --- a/src/Blazored.SessionStorage/ISessionStorageService.cs +++ b/src/Blazored.SessionStorage/ISessionStorageService.cs @@ -15,7 +15,7 @@ public interface ISessionStorageService Task RemoveItemAsync(string key); - Task SetItemAsync(string key, object data); + Task SetItemAsync(string key, T data); event EventHandler Changing; event EventHandler Changed; diff --git a/src/Blazored.SessionStorage/ISyncSessionStorageService.cs b/src/Blazored.SessionStorage/ISyncSessionStorageService.cs index b85ee84..a994f6a 100644 --- a/src/Blazored.SessionStorage/ISyncSessionStorageService.cs +++ b/src/Blazored.SessionStorage/ISyncSessionStorageService.cs @@ -14,7 +14,7 @@ public interface ISyncSessionStorageService void RemoveItem(string key); - void SetItem(string key, object data); + void SetItem(string key, T data); event EventHandler Changing; event EventHandler Changed; diff --git a/src/Blazored.SessionStorage/JsonConverters/TimespanJsonConverter.cs b/src/Blazored.SessionStorage/JsonConverters/TimespanJsonConverter.cs new file mode 100644 index 0000000..f2436e3 --- /dev/null +++ b/src/Blazored.SessionStorage/JsonConverters/TimespanJsonConverter.cs @@ -0,0 +1,46 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +namespace Blazored.SessionStorage.JsonConverters +{ + /// + /// The new Json.NET doesn't support Timespan at this time + /// https://github.com/dotnet/corefx/issues/38641 + /// + public class TimespanJsonConverter : JsonConverter + { + /// + /// Format: Days.Hours:Minutes:Seconds:Milliseconds + /// + public const string TimeSpanFormatString = @"d\.hh\:mm\:ss\:FFF"; + + public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var s = reader.GetString(); + if (string.IsNullOrWhiteSpace(s)) + { + return TimeSpan.Zero; + } + else + { + TimeSpan parsedTimeSpan; + if (!TimeSpan.TryParseExact(s, TimeSpanFormatString, null, out parsedTimeSpan)) + { + throw new FormatException($"Input timespan is not in an expected format : expected {Regex.Unescape(TimeSpanFormatString)}. Please retrieve this key as a string and parse manually."); + } + else + { + return parsedTimeSpan; + } + } + } + + public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) + { + var timespanFormatted = $"{value.ToString(TimeSpanFormatString)}"; + writer.WriteStringValue(timespanFormatted); + } + } +} diff --git a/src/Blazored.SessionStorage/ServiceCollectionExtensions.cs b/src/Blazored.SessionStorage/ServiceCollectionExtensions.cs index a76e37f..4bb05e7 100644 --- a/src/Blazored.SessionStorage/ServiceCollectionExtensions.cs +++ b/src/Blazored.SessionStorage/ServiceCollectionExtensions.cs @@ -1,4 +1,7 @@ -using Microsoft.Extensions.DependencyInjection; +using System; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.StorageOptions; +using Microsoft.Extensions.DependencyInjection; namespace Blazored.SessionStorage { @@ -8,7 +11,23 @@ public static IServiceCollection AddBlazoredSessionStorage(this IServiceCollecti { return services .AddScoped() - .AddScoped(); + .AddScoped() + .Configure(configureOptions => + { + configureOptions.JsonSerializerOptions.Converters.Add(new TimespanJsonConverter()); + }); } + + public static IServiceCollection AddBlazoredSessionStorage(this IServiceCollection services, Action configure) + { + return services + .AddScoped() + .AddScoped() + .Configure(configureOptions => + { + configure?.Invoke(configureOptions); + configureOptions.JsonSerializerOptions.Converters.Add(new TimespanJsonConverter()); + }); + } } } diff --git a/src/Blazored.SessionStorage/SessionStorageService.cs b/src/Blazored.SessionStorage/SessionStorageService.cs index beb04d1..efb0922 100644 --- a/src/Blazored.SessionStorage/SessionStorageService.cs +++ b/src/Blazored.SessionStorage/SessionStorageService.cs @@ -1,4 +1,6 @@ -using Microsoft.JSInterop; +using Blazored.SessionStorage.StorageOptions; +using Microsoft.Extensions.Options; +using Microsoft.JSInterop; using System; using System.Text.Json; using System.Threading.Tasks; @@ -9,14 +11,19 @@ public class SessionStorageService : ISessionStorageService, ISyncSessionStorage { private readonly IJSRuntime _jSRuntime; private readonly IJSInProcessRuntime _jSInProcessRuntime; + private readonly JsonSerializerOptions _jsonOptions; - public SessionStorageService(IJSRuntime jSRuntime) + public event EventHandler Changing; + public event EventHandler Changed; + + public SessionStorageService(IJSRuntime jSRuntime, IOptions options) { - _jSRuntime = jSRuntime; - _jSInProcessRuntime = jSRuntime as IJSInProcessRuntime; + _jSRuntime = jSRuntime; + _jsonOptions = options.Value.JsonSerializerOptions; + _jSInProcessRuntime = jSRuntime as IJSInProcessRuntime; } - public async Task SetItemAsync(string key, object data) + public async Task SetItemAsync(string key, T data) { if (string.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); @@ -24,9 +31,11 @@ public async Task SetItemAsync(string key, object data) var e = await RaiseOnChangingAsync(key, data); if (e.Cancel) - return; - - await _jSRuntime.InvokeAsync("sessionStorage.setItem", key, JsonSerializer.Serialize(data)); + return; + + var serialisedData = JsonSerializer.Serialize(data, _jsonOptions); + + await _jSRuntime.InvokeVoidAsync("sessionStorage.setItem", key, serialisedData); RaiseOnChanged(key, e.OldValue, data); } @@ -39,9 +48,9 @@ public async Task GetItemAsync(string key) var serialisedData = await _jSRuntime.InvokeAsync("sessionStorage.getItem", key); if (serialisedData == null) - return default(T); - - return JsonSerializer.Deserialize(serialisedData); + return default; + + return JsonSerializer.Deserialize(serialisedData, _jsonOptions); } public async Task RemoveItemAsync(string key) @@ -58,7 +67,7 @@ public async Task RemoveItemAsync(string key) public async Task KeyAsync(int index) => await _jSRuntime.InvokeAsync("sessionStorage.key", index); - public void SetItem(string key, object data) + public void SetItem(string key, T data) { if (string.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); @@ -71,7 +80,9 @@ public void SetItem(string key, object data) if (e.Cancel) return; - _jSInProcessRuntime.Invoke("sessionStorage.setItem", key, JsonSerializer.Serialize(data)); + var serialisedData = JsonSerializer.Serialize(data, _jsonOptions); + + _jSInProcessRuntime.InvokeVoid("sessionStorage.setItem", key, serialisedData); RaiseOnChanged(key, e.OldValue, data); } @@ -87,9 +98,9 @@ public T GetItem(string key) var serialisedData = _jSInProcessRuntime.Invoke("sessionStorage.getItem", key); if (serialisedData == null) - return default(T); - - return JsonSerializer.Deserialize(serialisedData); + return default; + + return JsonSerializer.Deserialize(serialisedData, _jsonOptions); } public void RemoveItem(string key) @@ -100,7 +111,7 @@ public void RemoveItem(string key) if (_jSInProcessRuntime == null) throw new InvalidOperationException("IJSInProcessRuntime not available"); - _jSInProcessRuntime.Invoke("sessionStorage.removeItem", key); + _jSInProcessRuntime.InvokeVoid("sessionStorage.removeItem", key); } public void Clear() @@ -108,7 +119,7 @@ public void Clear() if (_jSInProcessRuntime == null) throw new InvalidOperationException("IJSInProcessRuntime not available"); - _jSInProcessRuntime.Invoke("sessionStorage.clear"); + _jSInProcessRuntime.InvokeVoid("sessionStorage.clear"); } public int Length() @@ -127,7 +138,6 @@ public string Key(int index) return _jSInProcessRuntime.Invoke("sessionStorage.key", index); } - public event EventHandler Changing; private async Task RaiseOnChangingAsync(string key, object data) { var e = new ChangingEventArgs @@ -156,7 +166,6 @@ private ChangingEventArgs RaiseOnChangingSync(string key, object data) return e; } - public event EventHandler Changed; private void RaiseOnChanged(string key, object oldValue, object data) { var e = new ChangedEventArgs diff --git a/src/Blazored.SessionStorage/StorageOptions/SessionStorageOptions.cs b/src/Blazored.SessionStorage/StorageOptions/SessionStorageOptions.cs new file mode 100644 index 0000000..b005f1a --- /dev/null +++ b/src/Blazored.SessionStorage/StorageOptions/SessionStorageOptions.cs @@ -0,0 +1,18 @@ +using System.Text.Json; + +namespace Blazored.SessionStorage.StorageOptions +{ + public class SessionStorageOptions + { + public JsonSerializerOptions JsonSerializerOptions { get; } = new JsonSerializerOptions + { + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + IgnoreNullValues = true, + IgnoreReadOnlyProperties = true, + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + ReadCommentHandling = JsonCommentHandling.Skip, + WriteIndented = false + }; + } +} From e4ef5c0e2402748f3661db84168442b1171fc3f8 Mon Sep 17 00:00:00 2001 From: Jan Johansson Date: Thu, 21 May 2020 19:53:14 +0200 Subject: [PATCH 2/3] Fixed typo in documentation --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8a08178..eeaece1 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Blazored SessionStorage -A library to provide access to local storage in Blazor applications +A library to provide access to session storage in Blazor applications [![Build Status](https://dev.azure.com/blazored/SessionStorage/_apis/build/status/Blazored.SessionStorage?branchName=master)](https://dev.azure.com/blazored/SessionStorage/_build/latest?definitionId=1&branchName=master) -[![Nuget](https://img.shields.io/nuget/v/blazored.localstorage.svg)](https://www.nuget.org/packages/Blazored.SessionStorage/) +[![Nuget](https://img.shields.io/nuget/v/blazored.sessionstorage.svg)](https://www.nuget.org/packages/Blazored.SessionStorage/) ### Installing @@ -15,7 +15,7 @@ Or via the Visual Studio package manager. ### Setup -You will need to register the local storage services with the service collection in your _Startup.cs_ file in Blazor Server. +You will need to register the session storage services with the service collection in your _Startup.cs_ file in Blazor Server. ```c# public void ConfigureServices(IServiceCollection services) @@ -40,7 +40,7 @@ public static async Task Main(string[] args) ### Configuration -The local storage provides options that can be modified by you at registration in your _Startup.cs_ file in Blazor Server. +The session storage provides options that can be modified by you at registration in your _Startup.cs_ file in Blazor Server. ```c# @@ -69,14 +69,14 @@ public static async Task Main(string[] args) To use Blazored.SessionStorage in Blazor WebAssembly, inject the `ISessionStorageService` per the example below. ```c# -@inject Blazored.SessionStorage.ISessionStorageService localStorage +@inject Blazored.SessionStorage.ISessionStorageService sessionStorage @code { protected override async Task OnInitializedAsync() { - await localStorage.SetItemAsync("name", "John Smith"); - var name = await localStorage.GetItemAsync("name"); + await sessionStorage.SetItemAsync("name", "John Smith"); + var name = await sessionStorage.GetItemAsync("name"); } } @@ -85,14 +85,14 @@ To use Blazored.SessionStorage in Blazor WebAssembly, inject the `ISessionStorag With Blazor WebAssembly you also have the option of a synchronous API, if your use case requires it. You can swap the `ISessionStorageService` for `ISyncStorageService` which allows you to avoid use of `async`/`await`. For either interface, the method names are the same. ```c# -@inject Blazored.SessionStorage.ISyncStorageService localStorage +@inject Blazored.SessionStorage.ISyncStorageService sessionStorage @code { protected override void OnInitialized() { - localStorage.SetItem("name", "John Smith"); - var name = localStorage.GetItem("name"); + sessionStorage.SetItem("name", "John Smith"); + var name = sessionStorage.GetItem("name"); } } @@ -103,14 +103,14 @@ With Blazor WebAssembly you also have the option of a synchronous API, if your u **NOTE:** Due to pre-rendering in Blazor Server you can't perform any JS interop until the `OnAfterRender` lifecycle method. ```c# -@inject Blazored.SessionStorage.ISessionStorageService localStorage +@inject Blazored.SessionStorage.ISessionStorageService sessionStorage @code { protected override async Task OnAfterRenderAsync(bool firstRender) { - await localStorage.SetItemAsync("name", "John Smith"); - var name = await localStorage.GetItemAsync("name"); + await sessionStorage.SetItemAsync("name", "John Smith"); + var name = await sessionStorage.GetItemAsync("name"); } } From c39b8068c765168e236d8c144c8ba56d0accd358 Mon Sep 17 00:00:00 2001 From: Jan Johansson Date: Thu, 21 May 2020 19:56:15 +0200 Subject: [PATCH 3/3] Fixed another typo in documentation --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index eeaece1..fa8fafb 100644 --- a/README.md +++ b/README.md @@ -82,10 +82,10 @@ To use Blazored.SessionStorage in Blazor WebAssembly, inject the `ISessionStorag } ``` -With Blazor WebAssembly you also have the option of a synchronous API, if your use case requires it. You can swap the `ISessionStorageService` for `ISyncStorageService` which allows you to avoid use of `async`/`await`. For either interface, the method names are the same. +With Blazor WebAssembly you also have the option of a synchronous API, if your use case requires it. You can swap the `ISessionStorageService` for `ISyncSessionStorageService` which allows you to avoid use of `async`/`await`. For either interface, the method names are the same. ```c# -@inject Blazored.SessionStorage.ISyncStorageService sessionStorage +@inject Blazored.SessionStorage.ISyncSessionStorageService sessionStorage @code { @@ -127,7 +127,7 @@ The APIs available are: - KeyAsync() - ContainsKeyAsync() -- synchronous via `ISyncStorageService` (Synchronous methods are **only** available in Blazor WebAssembly): +- synchronous via `ISyncSessionStorageService` (Synchronous methods are **only** available in Blazor WebAssembly): - SetItem() - GetItem() - RemoveItem()