diff --git a/README.md b/README.md
index 885485c..e12a4e3 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,9 @@
# Blazored LocalStorage
Blazored LocalStorage is a library that provides access to the browsers local 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 (v3 > v4): JsonSerializerOptions
+## Breaking Changes (v3 > v4)
+
+### 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 local storage with v3 to break things.
To retain the old settings use the following configuration when adding Blazored LocalStorage to the DI container:
@@ -21,6 +23,10 @@ builder.Services.AddBlazoredLocalStorage(config =>
);
```
+### SetItem[Async] method now serializes string values
+Prior to v4 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 `ILocalStorageService`:
- SetItemAsync()
+ - SetItemAsStringAsync()
- GetItemAsync()
- GetItemAsStringAsync()
- RemoveItemAsync()
@@ -127,6 +134,7 @@ The APIs available are:
- synchronous via `ISyncLocalStorageService` (Synchronous methods are **only** available in Blazor WebAssembly):
- SetItem()
+ - SetItemAsString()
- GetItem()
- GetItemAsString()
- RemoveItem()
@@ -135,7 +143,7 @@ The APIs available are:
- Key()
- ContainKey()
-**Note:** Blazored.LocalStorage methods will handle the serialisation and de-serialisation of the data for you, the exception is the `GetItemAsString[Async]` method which will return the raw string value from local storage.
+**Note:** Blazored.LocalStorage methods will handle the serialisation and de-serialisation of the data for you, the exceptions are the `SetItemAsString[Async]` and `GetItemAsString[Async]` methods which will save and return raw string values from local storage.
## Configuring JSON Serializer Options
You can configure the options for the default serializer (System.Text.Json) when calling the `AddBlazoredLocalStorage` method to register services.
diff --git a/src/Blazored.LocalStorage/ILocalStorageService.cs b/src/Blazored.LocalStorage/ILocalStorageService.cs
index cb5c9ab..cc6cef2 100644
--- a/src/Blazored.LocalStorage/ILocalStorageService.cs
+++ b/src/Blazored.LocalStorage/ILocalStorageService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Threading.Tasks;
namespace Blazored.LocalStorage
@@ -58,7 +58,15 @@ public interface ILocalStorageService
/// A value specifying the name of the storage slot to use
/// The data to be saved
/// A representing the completion of the operation.
- ValueTask SetItemAsync(string key, T data);
+ ValueTask SetItemAsync(string key, T data);
+
+ ///
+ /// Sets or updates the in local 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.LocalStorage/ISyncLocalStorageService.cs b/src/Blazored.LocalStorage/ISyncLocalStorageService.cs
index f21fefc..81dcf09 100644
--- a/src/Blazored.LocalStorage/ISyncLocalStorageService.cs
+++ b/src/Blazored.LocalStorage/ISyncLocalStorageService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
namespace Blazored.LocalStorage
{
@@ -56,6 +56,14 @@ public interface ISyncLocalStorageService
/// The data to be saved
void SetItem(string key, T data);
+ ///
+ /// Sets or updates the in local 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.LocalStorage/LocalStorageService.cs b/src/Blazored.LocalStorage/LocalStorageService.cs
index 6ece23c..2294112 100644
--- a/src/Blazored.LocalStorage/LocalStorageService.cs
+++ b/src/Blazored.LocalStorage/LocalStorageService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Text.Json;
using System.Threading.Tasks;
using Blazored.LocalStorage.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.LocalStorage.Tests/LocalStorageServiceTests/SetItem.cs b/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/SetItem.cs
index 14a7e5d..971749a 100644
--- a/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/SetItem.cs
+++ b/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/SetItem.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Text.Json;
using System.Threading.Tasks;
using Blazored.LocalStorage.JsonConverters;
@@ -82,7 +82,7 @@ public void OnChangingEventContainsNewValue_When_SavingNewData()
_sut.Changing += (_, args) => newValue = args.NewValue.ToString();
// act
- _sut.SetItemAsync("Key", data);
+ _sut.SetItem("Key", data);
// assert
Assert.Equal(data, newValue);
diff --git a/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/SetItemAsString.cs b/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/SetItemAsString.cs
new file mode 100644
index 0000000..3c8bf7b
--- /dev/null
+++ b/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/SetItemAsString.cs
@@ -0,0 +1,204 @@
+using System;
+using System.Text.Json;
+using Blazored.LocalStorage.JsonConverters;
+using Blazored.LocalStorage.Serialization;
+using Blazored.LocalStorage.StorageOptions;
+using Blazored.LocalStorage.Testing;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Blazored.LocalStorage.Tests.LocalStorageServiceTests
+{
+ public class SetItemAsString
+ {
+ private readonly LocalStorageService _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 LocalStorageOptions());
+ _serializer = new SystemTextJsonSerializer(mockOptions.Object);
+ _storageProvider = new InMemoryStorageProvider();
+ _sut = new LocalStorageService(_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.LocalStorage.Tests/LocalStorageServiceTests/SetItemAsStringAsync.cs b/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/SetItemAsStringAsync.cs
new file mode 100644
index 0000000..2399297
--- /dev/null
+++ b/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/SetItemAsStringAsync.cs
@@ -0,0 +1,205 @@
+using System;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Blazored.LocalStorage.JsonConverters;
+using Blazored.LocalStorage.Serialization;
+using Blazored.LocalStorage.StorageOptions;
+using Blazored.LocalStorage.Testing;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Blazored.LocalStorage.Tests.LocalStorageServiceTests
+{
+ public class SetItemAsStringAsync
+ {
+ private readonly LocalStorageService _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 LocalStorageOptions());
+ _serializer = new SystemTextJsonSerializer(mockOptions.Object);
+ _storageProvider = new InMemoryStorageProvider();
+ _sut = new LocalStorageService(_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);
+ }
+ }
+}
diff --git a/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/SetItemAsync.cs b/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/SetItemAsync.cs
index 4463f1d..6e0e9b8 100644
--- a/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/SetItemAsync.cs
+++ b/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/SetItemAsync.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Text.Json;
using System.Threading.Tasks;
using Blazored.LocalStorage.JsonConverters;