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

Feature : Enable variant Serializers - Newtonsoft, System.Text.Json #882

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/Akavache.Core/Akavache.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Newtonsoft.Json.Bson" Version="1.0.2" />
<PackageReference Include="System.Reactive" Version="6.0.0" />
<PackageReference Include="Splat" Version="14.*" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
Expand Down
3 changes: 1 addition & 2 deletions src/Akavache.Core/BlobCache/BlobCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

using System.Diagnostics.CodeAnalysis;
using System.Reactive.Threading.Tasks;
using Newtonsoft.Json.Bson;
using Splat;

namespace Akavache;
Expand Down Expand Up @@ -135,7 +134,7 @@ public static ISecureBlobCache Secure
/// </summary>
/// <remarks>
/// <para>
/// By default, <see cref="BsonReader"/> uses a <see cref="DateTimeKind"/> of <see cref="DateTimeKind.Local"/> and <see cref="BsonWriter"/>
/// By default, uses a <see cref="DateTimeKind"/> of <see cref="DateTimeKind.Local"/> and
/// uses <see cref="DateTimeKind.Utc"/>. Thus, DateTimes are serialized as UTC but deserialized as local time. To force BSON readers to
/// use some other <c>DateTimeKind</c>, you can set this value.
/// </para>
Expand Down
1 change: 0 additions & 1 deletion src/Akavache.Core/BlobCache/CacheEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ namespace Akavache;
/// <param name="expiresAt">The date and time when the entry expires.</param>
public class CacheEntry(string? typeName, byte[] value, DateTimeOffset createdAt, DateTimeOffset? expiresAt)
{

/// <summary>
/// Gets or sets the date and time when the entry was created.
/// </summary>
Expand Down
6 changes: 2 additions & 4 deletions src/Akavache.Core/BlobCache/IBlobCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using Newtonsoft.Json.Bson;

namespace Akavache;

/// <summary>
Expand All @@ -30,7 +28,7 @@ public interface IBlobCache : IDisposable
/// </summary>
/// <remarks>
/// <para>
/// By default, <see cref="BsonReader"/> uses a <see cref="DateTimeKind"/> of <see cref="DateTimeKind.Local"/> and <see cref="BsonWriter"/>
/// By default, uses a <see cref="DateTimeKind"/> of <see cref="DateTimeKind.Local"/> and
/// uses <see cref="DateTimeKind.Utc"/>. Thus, DateTimes are serialized as UTC but deserialized as local time. To force BSON readers to
/// use some other <c>DateTimeKind</c>, you can set this value.
/// </para>
Expand Down Expand Up @@ -103,4 +101,4 @@ public interface IBlobCache : IDisposable
/// </summary>
/// <returns>A signal indicating when the operation is complete.</returns>
IObservable<Unit> Vacuum();
}
}
89 changes: 24 additions & 65 deletions src/Akavache.Core/BlobCache/InMemoryBlobCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

using System.Diagnostics.CodeAnalysis;
using System.Reactive.Disposables;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
using Splat;

namespace Akavache;
Expand All @@ -21,7 +19,7 @@ public class InMemoryBlobCache : ISecureBlobCache, IObjectBlobCache, IEnableLogg
private readonly AsyncSubject<Unit> _shutdown = new();
private readonly IDisposable? _inner;
private readonly Dictionary<string, CacheEntry> _cache = [];
private readonly JsonDateTimeContractResolver _jsonDateTimeContractResolver = new(); // This will make us use ticks instead of json ticks for DateTime.
private readonly IDateTimeContractResolver _jsonDateTimeContractResolver; // This will make us use ticks instead of json ticks for DateTime.
private bool _disposed;
private DateTimeKind? _dateTimeKind;

Expand Down Expand Up @@ -58,6 +56,8 @@ public InMemoryBlobCache(IEnumerable<KeyValuePair<string, byte[]>> initialConten
/// <param name="initialContents">The initial contents of the cache.</param>
public InMemoryBlobCache(IScheduler? scheduler, IEnumerable<KeyValuePair<string, byte[]>>? initialContents)
{
BlobCache.EnsureInitialized();
_jsonDateTimeContractResolver = Locator.Current.GetService<IDateTimeContractResolver>() ?? throw new Exception("Could not resolve IDateTimeContractResolver");
Scheduler = scheduler ?? CurrentThreadScheduler.Instance;
foreach (var item in initialContents ?? Enumerable.Empty<KeyValuePair<string, byte[]>>())
{
Expand Down Expand Up @@ -155,7 +155,7 @@ public IObservable<Unit> Insert(string key, byte[] data, DateTimeOffset? absolut
{
if (_disposed)
{
return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>("InMemoryBlobCache");
return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>(nameof(InMemoryBlobCache));
}

lock (_cache)
Expand All @@ -168,15 +168,15 @@ public IObservable<Unit> Insert(string key, byte[] data, DateTimeOffset? absolut

/// <inheritdoc />
public IObservable<Unit> Flush() => _disposed ?
ExceptionHelper.ObservableThrowObjectDisposedException<Unit>("InMemoryBlobCache") :
ExceptionHelper.ObservableThrowObjectDisposedException<Unit>(nameof(InMemoryBlobCache)) :
Observable.Return(Unit.Default);

/// <inheritdoc />
public IObservable<byte[]> Get(string key)
{
if (_disposed)
{
return ExceptionHelper.ObservableThrowObjectDisposedException<byte[]>("InMemoryBlobCache");
return ExceptionHelper.ObservableThrowObjectDisposedException<byte[]>(nameof(InMemoryBlobCache));
}

CacheEntry? entry;
Expand Down Expand Up @@ -211,7 +211,7 @@ public IObservable<byte[]> Get(string key)
{
if (_disposed)
{
return ExceptionHelper.ObservableThrowObjectDisposedException<DateTimeOffset?>("InMemoryBlobCache");
return ExceptionHelper.ObservableThrowObjectDisposedException<DateTimeOffset?>(nameof(InMemoryBlobCache));
}

CacheEntry? entry;
Expand All @@ -233,7 +233,7 @@ public IObservable<IEnumerable<string>> GetAllKeys()
{
if (_disposed)
{
return ExceptionHelper.ObservableThrowObjectDisposedException<List<string>>("InMemoryBlobCache");
return ExceptionHelper.ObservableThrowObjectDisposedException<List<string>>(nameof(InMemoryBlobCache));
}

lock (_cache)
Expand All @@ -250,7 +250,7 @@ public IObservable<Unit> Invalidate(string key)
{
if (_disposed)
{
return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>("InMemoryBlobCache");
return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>(nameof(InMemoryBlobCache));
}

lock (_cache)
Expand All @@ -266,7 +266,7 @@ public IObservable<Unit> InvalidateAll()
{
if (_disposed)
{
return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>("InMemoryBlobCache");
return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>(nameof(InMemoryBlobCache));
}

lock (_cache)
Expand All @@ -282,7 +282,7 @@ public IObservable<Unit> InsertObject<T>(string key, T value, DateTimeOffset? ab
{
if (_disposed)
{
return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>("InMemoryBlobCache");
return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>(nameof(InMemoryBlobCache));
}

var data = SerializeObject(value);
Expand All @@ -300,7 +300,7 @@ public IObservable<T> GetObject<T>(string key)
{
if (_disposed)
{
return ExceptionHelper.ObservableThrowObjectDisposedException<T>("InMemoryBlobCache");
return ExceptionHelper.ObservableThrowObjectDisposedException<T>(nameof(InMemoryBlobCache));
}

CacheEntry? entry;
Expand All @@ -327,7 +327,7 @@ public IObservable<T> GetObject<T>(string key)
return ExceptionHelper.ObservableThrowKeyNotFoundException<T>(key);
}

var obj = DeserializeObject<T>(entry.Value);
var obj = DeserializeObject<T>(entry.Value)!;

return Observable.Return(obj, Scheduler);
}
Expand All @@ -340,31 +340,31 @@ public IObservable<IEnumerable<T>> GetAllObjects<T>()
{
if (_disposed)
{
return ExceptionHelper.ObservableThrowObjectDisposedException<IEnumerable<T>>("InMemoryBlobCache");
return ExceptionHelper.ObservableThrowObjectDisposedException<IEnumerable<T>>(nameof(InMemoryBlobCache));
}

lock (_cache)
{
return Observable.Return(
_cache
.Where(x => x.Value.TypeName == typeof(T).FullName && (x.Value.ExpiresAt is null || x.Value.ExpiresAt >= Scheduler.Now))
.Select(x => DeserializeObject<T>(x.Value.Value))
.Select(x => DeserializeObject<T>(x.Value.Value)!)
.ToList(),
Scheduler);
}
}

/// <inheritdoc />
public IObservable<Unit> InvalidateObject<T>(string key) => _disposed ?
ExceptionHelper.ObservableThrowObjectDisposedException<Unit>("InMemoryBlobCache") :
ExceptionHelper.ObservableThrowObjectDisposedException<Unit>(nameof(InMemoryBlobCache)) :
Invalidate(key);

/// <inheritdoc />
public IObservable<Unit> InvalidateAllObjects<T>()
{
if (_disposed)
{
return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>("InMemoryBlobCache");
return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>(nameof(InMemoryBlobCache));
}

lock (_cache)
Expand All @@ -384,7 +384,7 @@ public IObservable<Unit> Vacuum()
{
if (_disposed)
{
return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>("InMemoryBlobCache");
return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>(nameof(InMemoryBlobCache));
}

lock (_cache)
Expand Down Expand Up @@ -434,55 +434,14 @@ protected virtual void Dispose(bool isDisposing)
_disposed = true;
}

private byte[] SerializeObject<T>(T value)
{
var serializer = GetSerializer();
using var ms = new MemoryStream();
using var writer = new BsonDataWriter(ms);
serializer.Serialize(writer, new ObjectWrapper<T>(value));
return ms.ToArray();
}

private T DeserializeObject<T>(byte[] data)
{
#pragma warning disable CS8603 // Possible null reference return.
var serializer = GetSerializer();
using var reader = new BsonDataReader(new MemoryStream(data));
var forcedDateTimeKind = BlobCache.ForcedDateTimeKind;

if (forcedDateTimeKind.HasValue)
{
reader.DateTimeKindHandling = forcedDateTimeKind.Value;
}

try
{
var wrapper = serializer.Deserialize<ObjectWrapper<T>>(reader);
private byte[] SerializeObject<T>(T value) => GetSerializer().Serialize(value);

return wrapper is null ? default : wrapper.Value;
}
catch (Exception ex)
{
this.Log().Warn(ex, "Failed to deserialize data as boxed, we may be migrating from an old Akavache");
}

return serializer.Deserialize<T>(reader);
#pragma warning restore CS8603 // Possible null reference return.
}
private T? DeserializeObject<T>(byte[] data) => GetSerializer().Deserialize<T>(data);

private JsonSerializer GetSerializer()
private ISerializer GetSerializer()
{
var settings = Locator.Current.GetService<JsonSerializerSettings>() ?? new JsonSerializerSettings();
JsonSerializer serializer;

lock (settings)
{
_jsonDateTimeContractResolver.ExistingContractResolver = settings.ContractResolver;
settings.ContractResolver = _jsonDateTimeContractResolver;
serializer = JsonSerializer.Create(settings);
settings.ContractResolver = _jsonDateTimeContractResolver.ExistingContractResolver;
}

return serializer;
var s = Locator.Current.GetService<ISerializer>() ?? throw new Exception("ISerializer is not registered");
s.CreateSerializer(() => _jsonDateTimeContractResolver);
return s;
}
}
2 changes: 2 additions & 0 deletions src/Akavache.Core/DependencyResolverMixin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public static void InitializeAkavache(this IMutableDependencyResolver resolver,
"Akavache.Deprecated",
"Akavache.Mobile",
"Akavache.Sqlite3",
"Akavache.NewtonsoftJson",
"Akavache.Json",
"Akavache.Drawing"
};

Expand Down
50 changes: 50 additions & 0 deletions src/Akavache.Core/ISerializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

namespace Akavache;

/// <summary>
/// Determines how to serialize to and from a byte.
/// </summary>
public interface ISerializer
{
/// <summary>
/// Gets the serializer.
/// </summary>
/// <param name="getJsonDateTimeContractResolver">The json date time contract resolver.</param>
void CreateSerializer(Func<IDateTimeContractResolver> getJsonDateTimeContractResolver);

/// <summary>
/// Deserializes from bytes.
/// </summary>
/// <typeparam name="T">The type to deserialize to.</typeparam>
/// <param name="bytes">The bytes.</param>
/// <returns>The type.</returns>
T? Deserialize<T>(byte[] bytes);

/// <summary>
/// Deserializes the object.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="x">The x.</param>
/// <returns>An Observable of T.</returns>
IObservable<T?> DeserializeObject<T>(byte[] x);

/// <summary>
/// Serializes to an bytes.
/// </summary>
/// <typeparam name="T">The type of serialize.</typeparam>
/// <param name="item">The item to serialize.</param>
/// <returns>The bytes.</returns>
byte[] Serialize<T>(T item);

/// <summary>
/// Serializes the object.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="value">The value.</param>
/// <returns>The bytes.</returns>
byte[] SerializeObject<T>(T value);
}
18 changes: 18 additions & 0 deletions src/Akavache.Core/Internal/ExceptionMixins.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

namespace Akavache
{
internal static class ExceptionMixins
{
public static void ThrowArgumentNullExceptionIfNull<T>(this T? value, string name)
{
if (value is null)
{
throw new ArgumentNullException(name);
}
}
}
}
21 changes: 21 additions & 0 deletions src/Akavache.Core/Json/IDateTimeContractResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

namespace Akavache
{
/// <summary>
/// IDateTimeContractResolver.
/// </summary>
public interface IDateTimeContractResolver
{
/// <summary>
/// Gets or sets the force date time kind override.
/// </summary>
/// <value>
/// The force date time kind override.
/// </value>
DateTimeKind? ForceDateTimeKindOverride { get; set; }
}
}
Loading
Loading