Skip to content

Commit

Permalink
Added new SetItemAsString[Async] methods (#142)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrissainty authored May 15, 2021
1 parent 7c8ec70 commit 050cb65
Show file tree
Hide file tree
Showing 8 changed files with 478 additions and 9 deletions.
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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):
Expand Down Expand Up @@ -117,6 +123,7 @@ The APIs available are:

- asynchronous via `ILocalStorageService`:
- SetItemAsync()
- SetItemAsStringAsync()
- GetItemAsync()
- GetItemAsStringAsync()
- RemoveItemAsync()
Expand All @@ -127,6 +134,7 @@ The APIs available are:

- synchronous via `ISyncLocalStorageService` (Synchronous methods are **only** available in Blazor WebAssembly):
- SetItem()
- SetItemAsString()
- GetItem()
- GetItemAsString()
- RemoveItem()
Expand All @@ -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.
Expand Down
12 changes: 10 additions & 2 deletions src/Blazored.LocalStorage/ILocalStorageService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading.Tasks;

namespace Blazored.LocalStorage
Expand Down Expand Up @@ -58,7 +58,15 @@ public interface ILocalStorageService
/// <param name="key">A <see cref="string"/> value specifying the name of the storage slot to use</param>
/// <param name="data">The data to be saved</param>
/// <returns>A <see cref="ValueTask"/> representing the completion of the operation.</returns>
ValueTask SetItemAsync<T>(string key, T data);
ValueTask SetItemAsync<T>(string key, T data);

/// <summary>
/// Sets or updates the <paramref name="data"/> in local storage with the specified <paramref name="key"/>. Does not serialize the value before storing.
/// </summary>
/// <param name="key">A <see cref="string"/> value specifying the name of the storage slot to use</param>
/// <param name="data">The string to be saved</param>
/// <returns></returns>
ValueTask SetItemAsStringAsync(string key, string data);

event EventHandler<ChangingEventArgs> Changing;
event EventHandler<ChangedEventArgs> Changed;
Expand Down
10 changes: 9 additions & 1 deletion src/Blazored.LocalStorage/ISyncLocalStorageService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;

namespace Blazored.LocalStorage
{
Expand Down Expand Up @@ -56,6 +56,14 @@ public interface ISyncLocalStorageService
/// <param name="data">The data to be saved</param>
void SetItem<T>(string key, T data);

/// <summary>
/// Sets or updates the <paramref name="data"/> in local storage with the specified <paramref name="key"/>. Does not serialize the value before storing.
/// </summary>
/// <param name="key">A <see cref="string"/> value specifying the name of the storage slot to use</param>
/// <param name="data">The string to be saved</param>
/// <returns></returns>
void SetItemAsString(string key, string data);

event EventHandler<ChangingEventArgs> Changing;
event EventHandler<ChangedEventArgs> Changed;
}
Expand Down
38 changes: 37 additions & 1 deletion src/Blazored.LocalStorage/LocalStorageService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Text.Json;
using System.Threading.Tasks;
using Blazored.LocalStorage.Serialization;
Expand Down Expand Up @@ -32,6 +32,24 @@ public async ValueTask SetItemAsync<T>(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<T> GetItemAsync<T>(string key)
{
if (string.IsNullOrWhiteSpace(key))
Expand Down Expand Up @@ -98,6 +116,24 @@ public void SetItem<T>(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<T>(string key)
{
if (string.IsNullOrWhiteSpace(key))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Text.Json;
using System.Threading.Tasks;
using Blazored.LocalStorage.JsonConverters;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IOptions<LocalStorageOptions>>();
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<ArgumentNullException>(action);
}

[Fact]
public void ThrowsArgumentNullException_When_DataIsNull()
{
// arrange / act
var data = (string)null;
var action = new Action(() => _sut.SetItemAsString("MyValue", data));

// assert
Assert.Throws<ArgumentNullException>(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);
}
}
}
Loading

0 comments on commit 050cb65

Please sign in to comment.