Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added new SetItemAsString[Async] methods #142

Merged
merged 2 commits into from May 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
}
}
}