diff --git a/README.md b/README.md
index b3ec8f8..e0f165b 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,9 @@
# Blazored SessionStorage
Blazored SessionStorage is a library that provides access to the browsers session storage APIs for Blazor applications. An additional benefit of using this library is that it will handle serializing and deserializing values when saving or retrieving them.
-## Breaking Change (v1 > v2): JsonSerializerOptions
+## Breaking Change (v1 > v2)
+
+### JsonSerializerOptions
From v4 onwards we use the default the `JsonSerializerOptions` for `System.Text.Json` instead of using custom ones. This will cause values saved to session storage with v3 to break things.
To retain the old settings use the following configuration when adding Blazored SessionStorage to the DI container:
@@ -21,6 +23,10 @@ builder.Services.AddBlazoredSessionStorage(config =>
);
```
+### SetItem[Async] method now serializes string values
+Prior to v2 we bypassed the serialization of string values as it seemed a pointless as string can be stored directly. However, this led to some edge cases where nullable strings were being saved as the string `"null"`. Then when retrieved, instead of being null the value was `"null"`. By serializing strings this issue is taken care of.
+For those who wish to save raw string values, a new method `SetValueAsString[Async]` is available. This will save a string value without attempting to serialize it and will throw an exception if a null string is attempted to be saved.
+
## Installing
To install the package add the following line to you csproj file replacing x.x.x with the latest version number (found at the top of this file):
@@ -117,6 +123,7 @@ The APIs available are:
- asynchronous via `ISessionStorageService`:
- SetItemAsync()
+ - SetItemAsStringAsync()
- GetItemAsync()
- GetItemAsStringAsync()
- RemoveItemAsync()
@@ -127,6 +134,7 @@ The APIs available are:
- synchronous via `ISyncSessionStorageService` (Synchronous methods are **only** available in Blazor WebAssembly):
- SetItem()
+ - SetItemAsString()
- GetItem()
- GetItemAsString()
- RemoveItem()
diff --git a/src/Blazored.SessionStorage/ISessionStorageService.cs b/src/Blazored.SessionStorage/ISessionStorageService.cs
index 476435f..f9d7a3f 100644
--- a/src/Blazored.SessionStorage/ISessionStorageService.cs
+++ b/src/Blazored.SessionStorage/ISessionStorageService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Threading.Tasks;
namespace Blazored.SessionStorage
@@ -60,6 +60,14 @@ public interface ISessionStorageService
/// A representing the completion of the operation.
ValueTask SetItemAsync(string key, T data);
+ ///
+ /// Sets or updates the in session storage with the specified . Does not serialize the value before storing.
+ ///
+ /// A value specifying the name of the storage slot to use
+ /// The string to be saved
+ ///
+ ValueTask SetItemAsStringAsync(string key, string data);
+
event EventHandler Changing;
event EventHandler Changed;
}
diff --git a/src/Blazored.SessionStorage/ISyncSessionStorageService.cs b/src/Blazored.SessionStorage/ISyncSessionStorageService.cs
index f1efb32..0017e89 100644
--- a/src/Blazored.SessionStorage/ISyncSessionStorageService.cs
+++ b/src/Blazored.SessionStorage/ISyncSessionStorageService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
namespace Blazored.SessionStorage
{
@@ -56,6 +56,14 @@ public interface ISyncSessionStorageService
/// The data to be saved
void SetItem(string key, T data);
+ ///
+ /// Sets or updates the in session storage with the specified . Does not serialize the value before storing.
+ ///
+ /// A value specifying the name of the storage slot to use
+ /// The string to be saved
+ ///
+ void SetItemAsString(string key, string data);
+
event EventHandler Changing;
event EventHandler Changed;
}
diff --git a/src/Blazored.SessionStorage/SessionStorageService.cs b/src/Blazored.SessionStorage/SessionStorageService.cs
index 8bdd847..64434c3 100644
--- a/src/Blazored.SessionStorage/SessionStorageService.cs
+++ b/src/Blazored.SessionStorage/SessionStorageService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Text.Json;
using System.Threading.Tasks;
using Blazored.SessionStorage.Serialization;
@@ -32,6 +32,24 @@ public async ValueTask SetItemAsync(string key, T data)
RaiseOnChanged(key, e.OldValue, data);
}
+ public async ValueTask SetItemAsStringAsync(string key, string data)
+ {
+ if (string.IsNullOrWhiteSpace(key))
+ throw new ArgumentNullException(nameof(key));
+
+ if (data is null)
+ throw new ArgumentNullException(nameof(data));
+
+ var e = await RaiseOnChangingAsync(key, data).ConfigureAwait(false);
+
+ if (e.Cancel)
+ return;
+
+ await _storageProvider.SetItemAsync(key, data).ConfigureAwait(false);
+
+ RaiseOnChanged(key, e.OldValue, data);
+ }
+
public async ValueTask GetItemAsync(string key)
{
if (string.IsNullOrWhiteSpace(key))
@@ -98,6 +116,24 @@ public void SetItem(string key, T data)
RaiseOnChanged(key, e.OldValue, data);
}
+ public void SetItemAsString(string key, string data)
+ {
+ if (string.IsNullOrWhiteSpace(key))
+ throw new ArgumentNullException(nameof(key));
+
+ if (data is null)
+ throw new ArgumentNullException(nameof(data));
+
+ var e = RaiseOnChangingSync(key, data);
+
+ if (e.Cancel)
+ return;
+
+ _storageProvider.SetItem(key, data);
+
+ RaiseOnChanged(key, e.OldValue, data);
+ }
+
public T GetItem(string key)
{
if (string.IsNullOrWhiteSpace(key))
diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/SetItemAsString.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/SetItemAsString.cs
new file mode 100644
index 0000000..a00c9d4
--- /dev/null
+++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/SetItemAsString.cs
@@ -0,0 +1,204 @@
+using System;
+using System.Text.Json;
+using Blazored.SessionStorage.JsonConverters;
+using Blazored.SessionStorage.Serialization;
+using Blazored.SessionStorage.StorageOptions;
+using Blazored.SessionStorage.Testing;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests
+{
+ public class SetItemAsString
+ {
+ private readonly SessionStorageService _sut;
+ private readonly IStorageProvider _storageProvider;
+ private readonly IJsonSerializer _serializer;
+
+ private const string Key = "testKey";
+
+ public SetItemAsString()
+ {
+ var mockOptions = new Mock>();
+ var jsonOptions = new JsonSerializerOptions();
+ jsonOptions.Converters.Add(new TimespanJsonConverter());
+ mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions());
+ _serializer = new SystemTextJsonSerializer(mockOptions.Object);
+ _storageProvider = new InMemoryStorageProvider();
+ _sut = new SessionStorageService(_storageProvider, _serializer);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData(null)]
+ public void ThrowsArgumentNullException_When_KeyIsInvalid(string key)
+ {
+ // arrange / act
+ const string data = "Data";
+ var action = new Action(() => _sut.SetItemAsString(key, data));
+
+ // assert
+ Assert.Throws(action);
+ }
+
+ [Fact]
+ public void ThrowsArgumentNullException_When_DataIsNull()
+ {
+ // arrange / act
+ var data = (string)null;
+ var action = new Action(() => _sut.SetItemAsString("MyValue", data));
+
+ // assert
+ Assert.Throws(action);
+ }
+
+ [Fact]
+ public void RaisesOnChangingEvent_When_SavingNewData()
+ {
+ // arrange
+ var onChangingCalled = false;
+ _sut.Changing += (_, _) => onChangingCalled = true;
+
+ // act
+ _sut.SetItemAsString("Key", "Data");
+
+ // assert
+ Assert.True(onChangingCalled);
+ }
+
+ [Fact]
+ public void OnChangingEventContainsEmptyOldValue_When_SavingData()
+ {
+ // arrange
+ var oldValue = "";
+ _sut.Changing += (_, args) => oldValue = args.OldValue?.ToString();
+
+ // act
+ _sut.SetItemAsString("Key", "Data");
+
+ // assert
+ Assert.Equal(default, oldValue);
+ }
+
+ [Fact]
+ public void OnChangingEventContainsNewValue_When_SavingNewData()
+ {
+ // arrange
+ const string data = "Data";
+ var newValue = "";
+ _sut.Changing += (_, args) => newValue = args.NewValue.ToString();
+
+ // act
+ _sut.SetItemAsString("Key", data);
+
+ // assert
+ Assert.Equal(data, newValue);
+ }
+
+ [Fact]
+ public void OnChangingEventIsCancelled_When_SettingCancelToTrue_When_SavingNewData()
+ {
+ // arrange
+ _sut.Changing += (_, args) => args.Cancel = true;
+
+ // act
+ _sut.SetItemAsString("Key", "Data");
+
+ // assert
+ Assert.Equal(0, _storageProvider.Length());
+ }
+
+ [Fact]
+ public void SavesDataToStore()
+ {
+ // Act
+ var valueToSave = "StringValue";
+ _sut.SetItemAsString(Key, valueToSave);
+
+ // Assert
+ var valueFromStore = _storageProvider.GetItem(Key);
+
+ Assert.Equal(1, _storageProvider.Length());
+ Assert.Equal(valueToSave, valueFromStore);
+ }
+
+ [Fact]
+ public void OverwriteExistingValueInStore_When_UsingTheSameKey()
+ {
+ // Arrange
+ const string existingValue = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJBZG1pbmlzdHJhdG9yIiwiZXhwIjoxNTg1NjYwNzEyLCJpc3MiOiJDb2RlUmVkQm9va2luZy5TZXJ2ZXIiLCJhdWQiOiJDb2RlUmVkQm9va2luZy5DbGllbnRzIn0.JhK1M1H7NLCFexujJYCDjTn9La0HloGYADMHXGCFksU";
+ const string newValue = "6QLE0LL7iw7tHPAwold31qUENt3lVTUZxDGqeXQFx38=";
+
+ _storageProvider.SetItem(Key, existingValue);
+
+ // Act
+ _sut.SetItemAsString(Key, newValue);
+
+ // Assert
+ var updatedValue = _storageProvider.GetItem(Key);
+
+ Assert.Equal(newValue, updatedValue);
+ }
+
+ [Fact]
+ public void RaisesOnChangedEvent_When_SavingData()
+ {
+ // arrange
+ var onChangedCalled = false;
+ _sut.Changed += (_, _) => onChangedCalled = true;
+
+ // act
+ _sut.SetItemAsString("Key", "Data");
+
+ // assert
+ Assert.True(onChangedCalled);
+ }
+
+ [Fact]
+ public void OnChangedEventContainsEmptyOldValue_When_SavingNewData()
+ {
+ // arrange
+ var oldValue = "";
+ _sut.Changed += (_, args) => oldValue = args.OldValue?.ToString();
+
+ // act
+ _sut.SetItemAsString("Key", "Data");
+
+ // assert
+ Assert.Equal(default, oldValue);
+ }
+
+ [Fact]
+ public void OnChangedEventContainsNewValue_When_SavingNewData()
+ {
+ // arrange
+ const string data = "Data";
+ var newValue = "";
+ _sut.Changed += (_, args) => newValue = args.NewValue.ToString();
+
+ // act
+ _sut.SetItemAsString("Key", data);
+
+ // assert
+ Assert.Equal(data, newValue);
+ }
+
+ [Fact]
+ public void OnChangedEventContainsOldValue_When_UpdatingExistingData()
+ {
+ // arrange
+ var existingValue = "Foo";
+ _storageProvider.SetItem("Key", existingValue);
+ var oldValue = "";
+ _sut.Changed += (_, args) => oldValue = args.OldValue?.ToString();
+
+ // act
+ _sut.SetItemAsString("Key", "Data");
+
+ // assert
+ Assert.Equal(existingValue, oldValue);
+ }
+ }
+}
diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/SetItemAsStringAsync.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/SetItemAsStringAsync.cs
new file mode 100644
index 0000000..7a82c1a
--- /dev/null
+++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/SetItemAsStringAsync.cs
@@ -0,0 +1,205 @@
+using System;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Blazored.SessionStorage.JsonConverters;
+using Blazored.SessionStorage.Serialization;
+using Blazored.SessionStorage.StorageOptions;
+using Blazored.SessionStorage.Testing;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests
+{
+ public class SetItemAsStringAsync
+ {
+ private readonly SessionStorageService _sut;
+ private readonly IStorageProvider _storageProvider;
+ private readonly IJsonSerializer _serializer;
+
+ private const string Key = "testKey";
+
+ public SetItemAsStringAsync()
+ {
+ var mockOptions = new Mock>();
+ var jsonOptions = new JsonSerializerOptions();
+ jsonOptions.Converters.Add(new TimespanJsonConverter());
+ mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions());
+ _serializer = new SystemTextJsonSerializer(mockOptions.Object);
+ _storageProvider = new InMemoryStorageProvider();
+ _sut = new SessionStorageService(_storageProvider, _serializer);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData(null)]
+ public void ThrowsArgumentNullException_When_KeyIsInvalid(string key)
+ {
+ // arrange / act
+ const string data = "Data";
+ var action = new Func(async () => await _sut.SetItemAsStringAsync(key, data));
+
+ // assert
+ Assert.ThrowsAsync(action);
+ }
+
+ [Fact]
+ public void ThrowsArgumentNullException_When_DataIsNull()
+ {
+ // arrange / act
+ var data = (string)null;
+ var action = new Func(async () => await _sut.SetItemAsStringAsync("MyValue", data));
+
+ // assert
+ Assert.ThrowsAsync(action);
+ }
+
+ [Fact]
+ public async Task RaisesOnChangingEvent_When_SavingNewData()
+ {
+ // arrange
+ var onChangingCalled = false;
+ _sut.Changing += (_, _) => onChangingCalled = true;
+
+ // act
+ await _sut.SetItemAsStringAsync("Key", "Data");
+
+ // assert
+ Assert.True(onChangingCalled);
+ }
+
+ [Fact]
+ public async Task OnChangingEventContainsEmptyOldValue_When_SavingData()
+ {
+ // arrange
+ var oldValue = "";
+ _sut.Changing += (_, args) => oldValue = args.OldValue?.ToString();
+
+ // act
+ await _sut.SetItemAsStringAsync("Key", "Data");
+
+ // assert
+ Assert.Equal(default, oldValue);
+ }
+
+ [Fact]
+ public async Task OnChangingEventContainsNewValue_When_SavingNewData()
+ {
+ // arrange
+ const string data = "Data";
+ var newValue = "";
+ _sut.Changing += (_, args) => newValue = args.NewValue.ToString();
+
+ // act
+ await _sut.SetItemAsStringAsync("Key", data);
+
+ // assert
+ Assert.Equal(data, newValue);
+ }
+
+ [Fact]
+ public async Task OnChangingEventIsCancelled_When_SettingCancelToTrue_When_SavingNewData()
+ {
+ // arrange
+ _sut.Changing += (_, args) => args.Cancel = true;
+
+ // act
+ await _sut.SetItemAsStringAsync("Key", "Data");
+
+ // assert
+ Assert.Equal(0, _storageProvider.Length());
+ }
+
+ [Fact]
+ public async Task SavesDataToStore()
+ {
+ // Act
+ var valueToSave = "StringValue";
+ await _sut.SetItemAsStringAsync(Key, valueToSave);
+
+ // Assert
+ var valueFromStore = _storageProvider.GetItem(Key);
+
+ Assert.Equal(1, _storageProvider.Length());
+ Assert.Equal(valueToSave, valueFromStore);
+ }
+
+ [Fact]
+ public async Task OverwriteExistingValueInStore_When_UsingTheSameKey()
+ {
+ // Arrange
+ const string existingValue = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJBZG1pbmlzdHJhdG9yIiwiZXhwIjoxNTg1NjYwNzEyLCJpc3MiOiJDb2RlUmVkQm9va2luZy5TZXJ2ZXIiLCJhdWQiOiJDb2RlUmVkQm9va2luZy5DbGllbnRzIn0.JhK1M1H7NLCFexujJYCDjTn9La0HloGYADMHXGCFksU";
+ const string newValue = "6QLE0LL7iw7tHPAwold31qUENt3lVTUZxDGqeXQFx38=";
+
+ _storageProvider.SetItem(Key, existingValue);
+
+ // Act
+ await _sut.SetItemAsStringAsync(Key, newValue);
+
+ // Assert
+ var updatedValue = _storageProvider.GetItem(Key);
+
+ Assert.Equal(newValue, updatedValue);
+ }
+
+ [Fact]
+ public async Task RaisesOnChangedEvent_When_SavingData()
+ {
+ // arrange
+ var onChangedCalled = false;
+ _sut.Changed += (_, _) => onChangedCalled = true;
+
+ // act
+ await _sut.SetItemAsStringAsync("Key", "Data");
+
+ // assert
+ Assert.True(onChangedCalled);
+ }
+
+ [Fact]
+ public async Task OnChangedEventContainsEmptyOldValue_When_SavingNewData()
+ {
+ // arrange
+ var oldValue = "";
+ _sut.Changed += (_, args) => oldValue = args.OldValue?.ToString();
+
+ // act
+ await _sut.SetItemAsStringAsync("Key", "Data");
+
+ // assert
+ Assert.Equal(default, oldValue);
+ }
+
+ [Fact]
+ public async Task OnChangedEventContainsNewValue_When_SavingNewData()
+ {
+ // arrange
+ const string data = "Data";
+ var newValue = "";
+ _sut.Changed += (_, args) => newValue = args.NewValue.ToString();
+
+ // act
+ await _sut.SetItemAsStringAsync("Key", data);
+
+ // assert
+ Assert.Equal(data, newValue);
+ }
+
+ [Fact]
+ public async Task OnChangedEventContainsOldValue_When_UpdatingExistingData()
+ {
+ // arrange
+ var existingValue = "Foo";
+ _storageProvider.SetItem("Key", existingValue);
+ var oldValue = "";
+ _sut.Changed += (_, args) => oldValue = args.OldValue?.ToString();
+
+ // act
+ await _sut.SetItemAsStringAsync("Key", "Data");
+
+ // assert
+ Assert.Equal(existingValue, oldValue);
+ }
+ }
+}