From a48d5f3247d615fa896e039f0354045bcf2ab70e Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sun, 26 Nov 2023 00:06:07 +0000 Subject: [PATCH 1/3] Feature : Enable variant Serializers - Newtonsoft, System.Text.Json --- src/Akavache.Core/Akavache.Core.csproj | 1 - src/Akavache.Core/BlobCache/BlobCache.cs | 3 +- src/Akavache.Core/BlobCache/CacheEntry.cs | 1 - src/Akavache.Core/BlobCache/IBlobCache.cs | 6 +- .../BlobCache/InMemoryBlobCache.cs | 85 ++------ src/Akavache.Core/DependencyResolverMixin.cs | 2 + src/Akavache.Core/ISerializer.cs | 68 ++++++ .../Json/IDateTimeContractResolver.cs | 21 ++ .../Json/JsonDateTimeOffsetTickConverter.cs | 46 ---- .../Json/JsonSerializationMixin.cs | 13 +- .../KeyedOperations/KeyedOperation.cs | 1 - src/Akavache.Core/LoginInfo.cs | 1 - src/Akavache.Core/Properties/AssemblyInfo.cs | 2 + src/Akavache.Json/Akavache.Json.csproj | 21 ++ src/Akavache.Json/Registrations.cs | 39 ++++ src/Akavache.Json/SystemJsonSerializer.cs | 101 +++++++++ src/Akavache.Mobile/Registrations.cs | 11 - .../Akavache.NewtonsoftJson.csproj | 36 ++++ .../JsonDateTimeContractResolver.cs | 14 +- .../JsonDateTimeOffsetTickConverter.cs | 94 +++++++++ .../JsonDateTimeTickConverter.cs | 31 ++- .../NewtonsoftSerializer.cs | 197 ++++++++++++++++++ .../Properties/AssemblyInfo.cs | 15 ++ src/Akavache.NewtonsoftJson/Registrations.cs | 44 ++++ .../SqlLiteCache/SqlRawPersistentBlobCache.cs | 57 +---- ...lTests.AkavacheCore.DotNet6_0.verified.txt | 16 ++ ...ovalTests.AkavacheCore.Net4_8.verified.txt | 16 ++ src/Akavache.Tests/Akavache.Tests.csproj | 1 + .../TestBases/ObjectBulkOperationsTestBase.cs | 2 +- src/Akavache.sln | 12 ++ 30 files changed, 768 insertions(+), 189 deletions(-) create mode 100644 src/Akavache.Core/ISerializer.cs create mode 100644 src/Akavache.Core/Json/IDateTimeContractResolver.cs delete mode 100644 src/Akavache.Core/Json/JsonDateTimeOffsetTickConverter.cs create mode 100644 src/Akavache.Json/Akavache.Json.csproj create mode 100644 src/Akavache.Json/Registrations.cs create mode 100644 src/Akavache.Json/SystemJsonSerializer.cs create mode 100644 src/Akavache.NewtonsoftJson/Akavache.NewtonsoftJson.csproj rename src/{Akavache.Core/Json => Akavache.NewtonsoftJson}/JsonDateTimeContractResolver.cs (80%) create mode 100644 src/Akavache.NewtonsoftJson/JsonDateTimeOffsetTickConverter.cs rename src/{Akavache.Core/Json => Akavache.NewtonsoftJson}/JsonDateTimeTickConverter.cs (67%) create mode 100644 src/Akavache.NewtonsoftJson/NewtonsoftSerializer.cs create mode 100644 src/Akavache.NewtonsoftJson/Properties/AssemblyInfo.cs create mode 100644 src/Akavache.NewtonsoftJson/Registrations.cs diff --git a/src/Akavache.Core/Akavache.Core.csproj b/src/Akavache.Core/Akavache.Core.csproj index 30f095330..667ebb7f6 100644 --- a/src/Akavache.Core/Akavache.Core.csproj +++ b/src/Akavache.Core/Akavache.Core.csproj @@ -11,7 +11,6 @@ - diff --git a/src/Akavache.Core/BlobCache/BlobCache.cs b/src/Akavache.Core/BlobCache/BlobCache.cs index 168ae51fe..598fdabda 100644 --- a/src/Akavache.Core/BlobCache/BlobCache.cs +++ b/src/Akavache.Core/BlobCache/BlobCache.cs @@ -5,7 +5,6 @@ using System.Diagnostics.CodeAnalysis; using System.Reactive.Threading.Tasks; -using Newtonsoft.Json.Bson; using Splat; namespace Akavache; @@ -135,7 +134,7 @@ public static ISecureBlobCache Secure /// /// /// - /// By default, uses a of and + /// By default, uses a of and /// uses . Thus, DateTimes are serialized as UTC but deserialized as local time. To force BSON readers to /// use some other DateTimeKind, you can set this value. /// diff --git a/src/Akavache.Core/BlobCache/CacheEntry.cs b/src/Akavache.Core/BlobCache/CacheEntry.cs index bdc23933f..4677a83b9 100644 --- a/src/Akavache.Core/BlobCache/CacheEntry.cs +++ b/src/Akavache.Core/BlobCache/CacheEntry.cs @@ -17,7 +17,6 @@ namespace Akavache; /// The date and time when the entry expires. public class CacheEntry(string? typeName, byte[] value, DateTimeOffset createdAt, DateTimeOffset? expiresAt) { - /// /// Gets or sets the date and time when the entry was created. /// diff --git a/src/Akavache.Core/BlobCache/IBlobCache.cs b/src/Akavache.Core/BlobCache/IBlobCache.cs index c340833c9..707a05fea 100644 --- a/src/Akavache.Core/BlobCache/IBlobCache.cs +++ b/src/Akavache.Core/BlobCache/IBlobCache.cs @@ -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; /// @@ -30,7 +28,7 @@ public interface IBlobCache : IDisposable /// /// /// - /// By default, uses a of and + /// By default, uses a of and /// uses . Thus, DateTimes are serialized as UTC but deserialized as local time. To force BSON readers to /// use some other DateTimeKind, you can set this value. /// @@ -103,4 +101,4 @@ public interface IBlobCache : IDisposable /// /// A signal indicating when the operation is complete. IObservable Vacuum(); -} \ No newline at end of file +} diff --git a/src/Akavache.Core/BlobCache/InMemoryBlobCache.cs b/src/Akavache.Core/BlobCache/InMemoryBlobCache.cs index 9c7dd27cd..c99f36e10 100644 --- a/src/Akavache.Core/BlobCache/InMemoryBlobCache.cs +++ b/src/Akavache.Core/BlobCache/InMemoryBlobCache.cs @@ -5,8 +5,6 @@ using System.Diagnostics.CodeAnalysis; using System.Reactive.Disposables; -using Newtonsoft.Json; -using Newtonsoft.Json.Bson; using Splat; namespace Akavache; @@ -21,7 +19,7 @@ public class InMemoryBlobCache : ISecureBlobCache, IObjectBlobCache, IEnableLogg private readonly AsyncSubject _shutdown = new(); private readonly IDisposable? _inner; private readonly Dictionary _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; @@ -58,6 +56,8 @@ public InMemoryBlobCache(IEnumerable> initialConten /// The initial contents of the cache. public InMemoryBlobCache(IScheduler? scheduler, IEnumerable>? initialContents) { + BlobCache.EnsureInitialized(); + _jsonDateTimeContractResolver = Locator.Current.GetService() ?? throw new Exception("Could not resolve IDateTimeContractResolver"); Scheduler = scheduler ?? CurrentThreadScheduler.Instance; foreach (var item in initialContents ?? Enumerable.Empty>()) { @@ -155,7 +155,7 @@ public IObservable Insert(string key, byte[] data, DateTimeOffset? absolut { if (_disposed) { - return ExceptionHelper.ObservableThrowObjectDisposedException("InMemoryBlobCache"); + return ExceptionHelper.ObservableThrowObjectDisposedException(nameof(InMemoryBlobCache)); } lock (_cache) @@ -168,7 +168,7 @@ public IObservable Insert(string key, byte[] data, DateTimeOffset? absolut /// public IObservable Flush() => _disposed ? - ExceptionHelper.ObservableThrowObjectDisposedException("InMemoryBlobCache") : + ExceptionHelper.ObservableThrowObjectDisposedException(nameof(InMemoryBlobCache)) : Observable.Return(Unit.Default); /// @@ -176,7 +176,7 @@ public IObservable Get(string key) { if (_disposed) { - return ExceptionHelper.ObservableThrowObjectDisposedException("InMemoryBlobCache"); + return ExceptionHelper.ObservableThrowObjectDisposedException(nameof(InMemoryBlobCache)); } CacheEntry? entry; @@ -211,7 +211,7 @@ public IObservable Get(string key) { if (_disposed) { - return ExceptionHelper.ObservableThrowObjectDisposedException("InMemoryBlobCache"); + return ExceptionHelper.ObservableThrowObjectDisposedException(nameof(InMemoryBlobCache)); } CacheEntry? entry; @@ -233,7 +233,7 @@ public IObservable> GetAllKeys() { if (_disposed) { - return ExceptionHelper.ObservableThrowObjectDisposedException>("InMemoryBlobCache"); + return ExceptionHelper.ObservableThrowObjectDisposedException>(nameof(InMemoryBlobCache)); } lock (_cache) @@ -250,7 +250,7 @@ public IObservable Invalidate(string key) { if (_disposed) { - return ExceptionHelper.ObservableThrowObjectDisposedException("InMemoryBlobCache"); + return ExceptionHelper.ObservableThrowObjectDisposedException(nameof(InMemoryBlobCache)); } lock (_cache) @@ -266,7 +266,7 @@ public IObservable InvalidateAll() { if (_disposed) { - return ExceptionHelper.ObservableThrowObjectDisposedException("InMemoryBlobCache"); + return ExceptionHelper.ObservableThrowObjectDisposedException(nameof(InMemoryBlobCache)); } lock (_cache) @@ -282,7 +282,7 @@ public IObservable InsertObject(string key, T value, DateTimeOffset? ab { if (_disposed) { - return ExceptionHelper.ObservableThrowObjectDisposedException("InMemoryBlobCache"); + return ExceptionHelper.ObservableThrowObjectDisposedException(nameof(InMemoryBlobCache)); } var data = SerializeObject(value); @@ -300,7 +300,7 @@ public IObservable GetObject(string key) { if (_disposed) { - return ExceptionHelper.ObservableThrowObjectDisposedException("InMemoryBlobCache"); + return ExceptionHelper.ObservableThrowObjectDisposedException(nameof(InMemoryBlobCache)); } CacheEntry? entry; @@ -340,7 +340,7 @@ public IObservable> GetAllObjects() { if (_disposed) { - return ExceptionHelper.ObservableThrowObjectDisposedException>("InMemoryBlobCache"); + return ExceptionHelper.ObservableThrowObjectDisposedException>(nameof(InMemoryBlobCache)); } lock (_cache) @@ -356,7 +356,7 @@ public IObservable> GetAllObjects() /// public IObservable InvalidateObject(string key) => _disposed ? - ExceptionHelper.ObservableThrowObjectDisposedException("InMemoryBlobCache") : + ExceptionHelper.ObservableThrowObjectDisposedException(nameof(InMemoryBlobCache)) : Invalidate(key); /// @@ -364,7 +364,7 @@ public IObservable InvalidateAllObjects() { if (_disposed) { - return ExceptionHelper.ObservableThrowObjectDisposedException("InMemoryBlobCache"); + return ExceptionHelper.ObservableThrowObjectDisposedException(nameof(InMemoryBlobCache)); } lock (_cache) @@ -384,7 +384,7 @@ public IObservable Vacuum() { if (_disposed) { - return ExceptionHelper.ObservableThrowObjectDisposedException("InMemoryBlobCache"); + return ExceptionHelper.ObservableThrowObjectDisposedException(nameof(InMemoryBlobCache)); } lock (_cache) @@ -434,55 +434,14 @@ protected virtual void Dispose(bool isDisposing) _disposed = true; } - private byte[] SerializeObject(T value) - { - var serializer = GetSerializer(); - using var ms = new MemoryStream(); - using var writer = new BsonDataWriter(ms); - serializer.Serialize(writer, new ObjectWrapper(value)); - return ms.ToArray(); - } - - private T DeserializeObject(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>(reader); + private byte[] SerializeObject(T value) => GetSerializer().SerializeObjectWrapper(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(reader); -#pragma warning restore CS8603 // Possible null reference return. - } + private T DeserializeObject(byte[] data) => GetSerializer().DeserializeObjectWrapper(data); - private JsonSerializer GetSerializer() + private ISerializer GetSerializer() { - var settings = Locator.Current.GetService() ?? 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() ?? throw new Exception("ISerializer is not registered"); + s.CreateSerializer(() => _jsonDateTimeContractResolver); + return s; } } diff --git a/src/Akavache.Core/DependencyResolverMixin.cs b/src/Akavache.Core/DependencyResolverMixin.cs index 0de5a8e42..1aff3f0ed 100644 --- a/src/Akavache.Core/DependencyResolverMixin.cs +++ b/src/Akavache.Core/DependencyResolverMixin.cs @@ -30,6 +30,8 @@ public static void InitializeAkavache(this IMutableDependencyResolver resolver, "Akavache.Deprecated", "Akavache.Mobile", "Akavache.Sqlite3", + "Akavache.NewtonsoftJson", + "Akavache.Json", "Akavache.Drawing" }; diff --git a/src/Akavache.Core/ISerializer.cs b/src/Akavache.Core/ISerializer.cs new file mode 100644 index 000000000..bf5e4687a --- /dev/null +++ b/src/Akavache.Core/ISerializer.cs @@ -0,0 +1,68 @@ +// 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; + +/// +/// Determines how to serialize to and from a byte. +/// +public interface ISerializer +{ + /// + /// Gets the serializer. + /// + /// The json date time contract resolver. + void CreateSerializer(Func getJsonDateTimeContractResolver); + + /// + /// Deserializes from bytes. + /// + /// The type to deserialize to. + /// The bytes. + /// The type. + T? Deserialize(byte[] bytes); + + /// + /// Deserializes the object. + /// + /// The type. + /// The x. + /// An Observable of T. + IObservable DeserializeObject(byte[] x); + + /// + /// Deserializes the object wrapper. + /// + /// The type. + /// The data. + /// A value of T. + /// You must call CreateSerializer before deserializing. + T DeserializeObjectWrapper(byte[] data); + + /// + /// Serializes to an bytes. + /// + /// The type of serialize. + /// The item to serialize. + /// The bytes. + byte[] Serialize(T item); + + /// + /// Serializes the object. + /// + /// The type. + /// The value. + /// The bytes. + byte[] SerializeObject(T value); + + /// + /// Serializes the object wrapper. + /// + /// The type. + /// The value. + /// A byte array. + /// You must call CreateSerializer before serializing. + byte[] SerializeObjectWrapper(T value); +} diff --git a/src/Akavache.Core/Json/IDateTimeContractResolver.cs b/src/Akavache.Core/Json/IDateTimeContractResolver.cs new file mode 100644 index 000000000..ef3b796ed --- /dev/null +++ b/src/Akavache.Core/Json/IDateTimeContractResolver.cs @@ -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 +{ + /// + /// IDateTimeContractResolver. + /// + public interface IDateTimeContractResolver + { + /// + /// Gets or sets the force date time kind override. + /// + /// + /// The force date time kind override. + /// + DateTimeKind? ForceDateTimeKindOverride { get; set; } + } +} diff --git a/src/Akavache.Core/Json/JsonDateTimeOffsetTickConverter.cs b/src/Akavache.Core/Json/JsonDateTimeOffsetTickConverter.cs deleted file mode 100644 index d60e35b74..000000000 --- a/src/Akavache.Core/Json/JsonDateTimeOffsetTickConverter.cs +++ /dev/null @@ -1,46 +0,0 @@ -// 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. - -using Newtonsoft.Json; - -namespace Akavache; - -internal class JsonDateTimeOffsetTickConverter : JsonConverter -{ - public static JsonDateTimeOffsetTickConverter Default { get; } = new(); - - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - if (value is DateTimeOffset dateTimeOffset) - { - serializer.Serialize(writer, new DateTimeOffsetData(dateTimeOffset)); - } - } - - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Date && reader.Value is not null) - { - return (DateTimeOffset)reader.Value; - } - - var data = serializer.Deserialize(reader); - - return data is null ? null : (DateTimeOffset)data; - } - - public override bool CanConvert(Type objectType) => objectType == typeof(DateTimeOffset) || objectType == typeof(DateTimeOffset?); - - internal class DateTimeOffsetData(DateTimeOffset offset) - { - public long Ticks { get; set; } = offset.Ticks; - - public long OffsetTicks { get; set; } = offset.Offset.Ticks; - - public static explicit operator DateTimeOffset(DateTimeOffsetData value) // explicit byte to digit conversion operator - => - new(value.Ticks, new(value.OffsetTicks)); - } -} diff --git a/src/Akavache.Core/Json/JsonSerializationMixin.cs b/src/Akavache.Core/Json/JsonSerializationMixin.cs index c1406619a..1ccaa7cae 100644 --- a/src/Akavache.Core/Json/JsonSerializationMixin.cs +++ b/src/Akavache.Core/Json/JsonSerializationMixin.cs @@ -6,7 +6,6 @@ using System.Collections.Concurrent; using System.Globalization; using System.Reactive.Threading.Tasks; -using Newtonsoft.Json; using Splat; namespace Akavache; @@ -398,20 +397,16 @@ public static IObservable InvalidateAllObjects(this IBlobCache blobCach internal static byte[] SerializeObject(T value) { - var settings = Locator.Current.GetService(); - return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value, settings)); + var serializer = Locator.Current.GetService(); + return serializer?.SerializeObject(value) ?? []; } internal static IObservable DeserializeObject(byte[] x) { - var settings = Locator.Current.GetService(); - try { - var bytes = Encoding.UTF8.GetString(x, 0, x.Length); - - var ret = JsonConvert.DeserializeObject(bytes, settings); - return Observable.Return(ret); + var serializer = Locator.Current.GetService(); + return serializer!.DeserializeObject(x) ?? default!; } catch (Exception ex) { diff --git a/src/Akavache.Core/KeyedOperations/KeyedOperation.cs b/src/Akavache.Core/KeyedOperations/KeyedOperation.cs index 3e985053f..b44dd6a6d 100644 --- a/src/Akavache.Core/KeyedOperations/KeyedOperation.cs +++ b/src/Akavache.Core/KeyedOperations/KeyedOperation.cs @@ -46,7 +46,6 @@ protected KeyedOperation(string key, int id) [SuppressMessage("StyleCop.Maintainability.CSharp", "SA1402: One type per file", Justification = "Same class name.")] internal class KeyedOperation(Func> func, string key, int id) : KeyedOperation(key, id) { - /// /// Gets the function which returns the observable. /// diff --git a/src/Akavache.Core/LoginInfo.cs b/src/Akavache.Core/LoginInfo.cs index 65cca1c4d..ee01d4b35 100644 --- a/src/Akavache.Core/LoginInfo.cs +++ b/src/Akavache.Core/LoginInfo.cs @@ -15,7 +15,6 @@ namespace Akavache; /// The password for the user. public class LoginInfo(string username, string password) { - /// /// Initializes a new instance of the class. /// diff --git a/src/Akavache.Core/Properties/AssemblyInfo.cs b/src/Akavache.Core/Properties/AssemblyInfo.cs index 26e7ebd78..26875b49f 100644 --- a/src/Akavache.Core/Properties/AssemblyInfo.cs +++ b/src/Akavache.Core/Properties/AssemblyInfo.cs @@ -8,6 +8,8 @@ [assembly: InternalsVisibleTo("Akavache.Tests")] [assembly: InternalsVisibleTo("Akavache.Sqlite3")] +[assembly: InternalsVisibleTo("Akavache.NewtonsoftJson")] +[assembly: InternalsVisibleTo("Akavache.Json")] [assembly: InternalsVisibleTo("Akavache.Mobile")] [assembly: InternalsVisibleTo("Akavache.Drawing")] [assembly: InternalsVisibleTo("Akavache")] diff --git a/src/Akavache.Json/Akavache.Json.csproj b/src/Akavache.Json/Akavache.Json.csproj new file mode 100644 index 000000000..5c278cc82 --- /dev/null +++ b/src/Akavache.Json/Akavache.Json.csproj @@ -0,0 +1,21 @@ + + + + $(AkavacheTargetFrameworks) + enable + enable + false + + + + + + + + + + + + + + diff --git a/src/Akavache.Json/Registrations.cs b/src/Akavache.Json/Registrations.cs new file mode 100644 index 000000000..830bac769 --- /dev/null +++ b/src/Akavache.Json/Registrations.cs @@ -0,0 +1,39 @@ +// 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. + +using System.Text.Json; +using Akavache.Core; +using Splat; + +namespace Akavache.Json; + +/// +/// Registrations for the mobile platform. Will register all our instances with splat. +/// +[Preserve(AllMembers = true)] +public class Registrations : IWantsToRegisterStuff +{ + /// + public void Register(IMutableDependencyResolver resolver, IReadonlyDependencyResolver readonlyDependencyResolver) + { +#if NETSTANDARD || XAMARINIOS || XAMARINMAC || XAMARINTVOS || TIZEN || MONOANDROID13_0 + if (resolver is null) + { + throw new ArgumentNullException(nameof(resolver)); + } +#else + ArgumentNullException.ThrowIfNull(resolver); +#endif + + resolver.Register( + () => new JsonSerializerOptions(), + typeof(JsonSerializerOptions), + null); + + resolver.Register(() => new SystemJsonSerializer(readonlyDependencyResolver.GetService()!), typeof(ISerializer), null); + + ////resolver.Register(() => new JsonDateTimeContractResolver(), typeof(IDateTimeContractResolver), null); + } +} diff --git a/src/Akavache.Json/SystemJsonSerializer.cs b/src/Akavache.Json/SystemJsonSerializer.cs new file mode 100644 index 000000000..e76db7876 --- /dev/null +++ b/src/Akavache.Json/SystemJsonSerializer.cs @@ -0,0 +1,101 @@ +// 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. + +using System.Text.Json; + +namespace Akavache.Json; + +/// +/// SystemJsonSerializer. +/// +/// +public class SystemJsonSerializer : ISerializer +{ + /// + /// Initializes a new instance of the class. + /// + /// The options. + public SystemJsonSerializer(JsonSerializerOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + Options = options; + } + + /// + /// Gets the options. + /// + /// + /// The options. + /// + public JsonSerializerOptions Options { get; } + + /// + /// Gets the serializer. + /// + /// The json date time contract resolver. + public void CreateSerializer(Func getJsonDateTimeContractResolver) + { + // TODO: Implement this + } + + /// + /// Deserializes from bytes. + /// + /// The type to deserialize to. + /// The bytes. + /// + /// The type. + /// + public T? Deserialize(byte[] bytes) => (T?)JsonSerializer.Deserialize(bytes, typeof(T), Options); + /// + /// Deserializes the object. + /// + /// The type. + /// The x. + /// + /// An Observable of T. + /// + public IObservable DeserializeObject(byte[] x) => throw new NotImplementedException(); + /// + /// Deserializes the object wrapper. + /// + /// The type. + /// The data. + /// + /// A value of T. + /// + public T DeserializeObjectWrapper(byte[] data) => throw new NotImplementedException(); + /// + /// Serializes to an bytes. + /// + /// The type of serialize. + /// The item to serialize. + /// + /// The bytes. + /// + public byte[] Serialize(T item) => JsonSerializer.SerializeToUtf8Bytes(item, Options); + /// + /// Serializes the object. + /// + /// The type. + /// The value. + /// + /// The bytes. + /// + public byte[] SerializeObject(T value) => throw new NotImplementedException(); + /// + /// Serializes the object wrapper. + /// + /// The type. + /// The value. + /// + /// A byte array. + /// + public byte[] SerializeObjectWrapper(T value) => throw new NotImplementedException(); +} diff --git a/src/Akavache.Mobile/Registrations.cs b/src/Akavache.Mobile/Registrations.cs index 2a926a647..4b245da9c 100644 --- a/src/Akavache.Mobile/Registrations.cs +++ b/src/Akavache.Mobile/Registrations.cs @@ -4,7 +4,6 @@ // See the LICENSE file in the project root for full license information. using Akavache.Core; -using Newtonsoft.Json; using ReactiveUI; using Splat; @@ -28,16 +27,6 @@ public void Register(IMutableDependencyResolver resolver, IReadonlyDependencyRes ArgumentNullException.ThrowIfNull(resolver); #endif - resolver.Register( - () => new JsonSerializerSettings - { - ObjectCreationHandling = ObjectCreationHandling.Replace, - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - TypeNameHandling = TypeNameHandling.All, - }, - typeof(JsonSerializerSettings), - null); - var akavacheDriver = new AkavacheDriver(); resolver.Register(() => akavacheDriver, typeof(ISuspensionDriver), null); diff --git a/src/Akavache.NewtonsoftJson/Akavache.NewtonsoftJson.csproj b/src/Akavache.NewtonsoftJson/Akavache.NewtonsoftJson.csproj new file mode 100644 index 000000000..66dfea422 --- /dev/null +++ b/src/Akavache.NewtonsoftJson/Akavache.NewtonsoftJson.csproj @@ -0,0 +1,36 @@ + + + + $(AkavacheTargetFrameworks) + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Akavache.Core/Json/JsonDateTimeContractResolver.cs b/src/Akavache.NewtonsoftJson/JsonDateTimeContractResolver.cs similarity index 80% rename from src/Akavache.Core/Json/JsonDateTimeContractResolver.cs rename to src/Akavache.NewtonsoftJson/JsonDateTimeContractResolver.cs index 111fcf196..59dc6692b 100644 --- a/src/Akavache.Core/Json/JsonDateTimeContractResolver.cs +++ b/src/Akavache.NewtonsoftJson/JsonDateTimeContractResolver.cs @@ -16,7 +16,7 @@ namespace Akavache; /// /// A inherited contract resolver. /// If we should override the . -internal class JsonDateTimeContractResolver(IContractResolver? contractResolver, DateTimeKind? forceDateTimeKindOverride) : DefaultContractResolver +public class JsonDateTimeContractResolver(IContractResolver? contractResolver, DateTimeKind? forceDateTimeKindOverride) : DefaultContractResolver, IDateTimeContractResolver { /// /// Initializes a new instance of the class. @@ -26,8 +26,20 @@ public JsonDateTimeContractResolver() { } + /// + /// Gets or sets the existing contract resolver. + /// + /// + /// The existing contract resolver. + /// public IContractResolver? ExistingContractResolver { get; set; } = contractResolver; + /// + /// Gets or sets the force date time kind override. + /// + /// + /// The force date time kind override. + /// public DateTimeKind? ForceDateTimeKindOverride { get; set; } = forceDateTimeKindOverride; /// diff --git a/src/Akavache.NewtonsoftJson/JsonDateTimeOffsetTickConverter.cs b/src/Akavache.NewtonsoftJson/JsonDateTimeOffsetTickConverter.cs new file mode 100644 index 000000000..eabdeaa2a --- /dev/null +++ b/src/Akavache.NewtonsoftJson/JsonDateTimeOffsetTickConverter.cs @@ -0,0 +1,94 @@ +// 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. + +using Newtonsoft.Json; + +namespace Akavache; + +/// +/// Json Date Time Offset Tick Converter. +/// +/// +public class JsonDateTimeOffsetTickConverter : JsonConverter +{ + /// + /// Gets the default. + /// + /// + /// The default. + /// + public static JsonDateTimeOffsetTickConverter Default { get; } = new(); + + /// + /// Writes the JSON representation of the object. + /// + /// The to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (serializer == null) + { + throw new ArgumentNullException(nameof(serializer)); + } + + if (value is DateTimeOffset dateTimeOffset) + { + serializer.Serialize(writer, new DateTimeOffsetData(dateTimeOffset)); + } + } + + /// + /// Reads the JSON representation of the object. + /// + /// The to read from. + /// Type of the object. + /// The existing value of object being read. + /// The calling serializer. + /// + /// The object value. + /// + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + if (reader == null) + { + throw new ArgumentNullException(nameof(reader)); + } + + if (serializer == null) + { + throw new ArgumentNullException(nameof(serializer)); + } + + if (reader.TokenType == JsonToken.Date && reader.Value is not null) + { + return (DateTimeOffset)reader.Value; + } + + var data = serializer.Deserialize(reader); + + return data is null ? null : (DateTimeOffset)data; + } + + /// + /// Determines whether this instance can convert the specified object type. + /// + /// Type of the object. + /// + /// true if this instance can convert the specified object type; otherwise, false. + /// + public override bool CanConvert(Type objectType) => objectType == typeof(DateTimeOffset) || objectType == typeof(DateTimeOffset?); + + internal class DateTimeOffsetData(DateTimeOffset offset) + { + public long Ticks { get; set; } = offset.Ticks; + + public long OffsetTicks { get; set; } = offset.Offset.Ticks; + + public static explicit operator DateTimeOffset(DateTimeOffsetData value) // explicit byte to digit conversion operator + => + new(value.Ticks, new(value.OffsetTicks)); + } +} diff --git a/src/Akavache.Core/Json/JsonDateTimeTickConverter.cs b/src/Akavache.NewtonsoftJson/JsonDateTimeTickConverter.cs similarity index 67% rename from src/Akavache.Core/Json/JsonDateTimeTickConverter.cs rename to src/Akavache.NewtonsoftJson/JsonDateTimeTickConverter.cs index 34ce0507a..8df47ae53 100644 --- a/src/Akavache.Core/Json/JsonDateTimeTickConverter.cs +++ b/src/Akavache.NewtonsoftJson/JsonDateTimeTickConverter.cs @@ -11,7 +11,7 @@ namespace Akavache; /// Since we use BSON at places, we want to just store ticks to avoid loosing precision. /// By default BSON will use JSON ticks. /// -internal class JsonDateTimeTickConverter(DateTimeKind? forceDateTimeKindOverride = null) : JsonConverter +public class JsonDateTimeTickConverter(DateTimeKind? forceDateTimeKindOverride = null) : JsonConverter { private readonly DateTimeKind? _forceDateTimeKindOverride = forceDateTimeKindOverride; @@ -25,8 +25,24 @@ internal class JsonDateTimeTickConverter(DateTimeKind? forceDateTimeKindOverride /// public static JsonDateTimeTickConverter LocalDateTimeKindDefault { get; } = new(DateTimeKind.Local); + /// + /// Determines whether this instance can convert the specified object type. + /// + /// Type of the object. + /// + /// true if this instance can convert the specified object type; otherwise, false. + /// public override bool CanConvert(Type objectType) => objectType == typeof(DateTime) || objectType == typeof(DateTime?); + /// + /// Reads the json. + /// + /// The reader. + /// Type of the object. + /// The existing value. + /// The serializer. + /// An object. + /// nameof(reader). public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { if (reader is null) @@ -54,8 +70,19 @@ internal class JsonDateTimeTickConverter(DateTimeKind? forceDateTimeKindOverride return null; } + /// + /// Writes the json. + /// + /// The writer. + /// The value. + /// The serializer. public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { + if (serializer == null) + { + throw new ArgumentNullException(nameof(serializer)); + } + if (value is DateTime dateTime) { switch (_forceDateTimeKindOverride) @@ -69,4 +96,4 @@ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer } } } -} \ No newline at end of file +} diff --git a/src/Akavache.NewtonsoftJson/NewtonsoftSerializer.cs b/src/Akavache.NewtonsoftJson/NewtonsoftSerializer.cs new file mode 100644 index 000000000..98ccc8217 --- /dev/null +++ b/src/Akavache.NewtonsoftJson/NewtonsoftSerializer.cs @@ -0,0 +1,197 @@ +// 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. + +using Newtonsoft.Json; +using Newtonsoft.Json.Bson; +using Splat; + +namespace Akavache; + +/// +/// Serializer for the Newtonsoft Serializer. +/// +public class NewtonsoftSerializer : ISerializer, IEnableLogger +{ + private JsonSerializer? _serializer; + + /// + /// Initializes a new instance of the class. + /// + /// The options. + public NewtonsoftSerializer(JsonSerializerSettings options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + Options = options; + } + + /// + /// Gets or sets the optional options. + /// + public JsonSerializerSettings Options { get; set; } + + /// + /// Gets the serializer. + /// + /// The get json date time contract resolver. + public void CreateSerializer(Func getJsonDateTimeContractResolver) + { + if (getJsonDateTimeContractResolver is null) + { + throw new ArgumentNullException(nameof(getJsonDateTimeContractResolver)); + } + + var jsonDateTimeContractResolver = getJsonDateTimeContractResolver() as JsonDateTimeContractResolver; + + lock (Options) + { + jsonDateTimeContractResolver!.ExistingContractResolver = Options.ContractResolver; + Options.ContractResolver = jsonDateTimeContractResolver; + _serializer = JsonSerializer.Create(Options); + Options.ContractResolver = jsonDateTimeContractResolver.ExistingContractResolver; + } + } + + /// + public T? Deserialize(byte[] bytes) + { + if (_serializer is null) + { + throw new InvalidOperationException("You must call CreateSerializer before deserializing"); + } + + using var reader = new BsonDataReader(new MemoryStream(bytes)); + var forcedDateTimeKind = BlobCache.ForcedDateTimeKind; + if (forcedDateTimeKind.HasValue) + { + reader.DateTimeKindHandling = forcedDateTimeKind.Value; + } + + try + { + var wrapper = _serializer.Deserialize>(reader); + 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(reader); + } + + /// + /// Deserializes the object. + /// + /// The type. + /// The x. + /// + /// An Observable of T. + /// + public IObservable DeserializeObject(byte[] x) + { + if (x is null) + { + throw new ArgumentNullException(nameof(x)); + } + + try + { + var bytes = Encoding.UTF8.GetString(x, 0, x.Length); + var ret = JsonConvert.DeserializeObject(bytes, Options); + return Observable.Return(ret); + } + catch (Exception ex) + { + return Observable.Throw(ex); + } + } + + /// + public byte[] Serialize(T item) + { + if (_serializer is null) + { + throw new InvalidOperationException("You must call CreateSerializer before serializing"); + } + + using var ms = new MemoryStream(); + using var writer = new BsonDataWriter(ms); + _serializer.Serialize(writer, new ObjectWrapper(item)); + return ms.ToArray(); + } + + /// + /// Serializes the object. + /// + /// The type. + /// The value. + /// + /// The bytes. + /// + public byte[] SerializeObject(T value) => + Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value, Options)); + + /// + /// Serializes the object wrapper. + /// + /// The type. + /// The value. + /// A byte array. + /// You must call CreateSerializer before serializing. + public byte[] SerializeObjectWrapper(T value) + { + if (_serializer is null) + { + throw new InvalidOperationException("You must call CreateSerializer before serializing"); + } + + using var ms = new MemoryStream(); + using var writer = new BsonDataWriter(ms); + _serializer.Serialize(writer, new ObjectWrapper(value)); + return ms.ToArray(); + } + + /// + /// Deserializes the object wrapper. + /// + /// The type. + /// The data. + /// A value of T. + /// You must call CreateSerializer before deserializing. + public T DeserializeObjectWrapper(byte[] data) + { + if (_serializer is null) + { + throw new InvalidOperationException("You must call CreateSerializer before deserializing"); + } + +#pragma warning disable CS8603 // Possible null reference return. + using var reader = new BsonDataReader(new MemoryStream(data)); + var forcedDateTimeKind = BlobCache.ForcedDateTimeKind; + + if (forcedDateTimeKind.HasValue) + { + reader.DateTimeKindHandling = forcedDateTimeKind.Value; + } + + try + { + var wrapper = _serializer.Deserialize>(reader); + + 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(reader); +#pragma warning restore CS8603 // Possible null reference return. + } +} diff --git a/src/Akavache.NewtonsoftJson/Properties/AssemblyInfo.cs b/src/Akavache.NewtonsoftJson/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..f08486b51 --- /dev/null +++ b/src/Akavache.NewtonsoftJson/Properties/AssemblyInfo.cs @@ -0,0 +1,15 @@ +// 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. + +using System.Runtime.CompilerServices; +using Akavache.Core; + +[assembly: InternalsVisibleTo("Akavache.Tests")] +[assembly: InternalsVisibleTo("Akavache.Sqlite3")] +[assembly: InternalsVisibleTo("Akavache.Core")] +[assembly: InternalsVisibleTo("Akavache.Mobile")] +[assembly: InternalsVisibleTo("Akavache.Drawing")] +[assembly: InternalsVisibleTo("Akavache")] +[assembly: Preserve] diff --git a/src/Akavache.NewtonsoftJson/Registrations.cs b/src/Akavache.NewtonsoftJson/Registrations.cs new file mode 100644 index 000000000..fbf5a6646 --- /dev/null +++ b/src/Akavache.NewtonsoftJson/Registrations.cs @@ -0,0 +1,44 @@ +// 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. + +using Akavache.Core; +using Newtonsoft.Json; +using Splat; + +namespace Akavache.NewtonsoftJson; + +/// +/// Registrations for the mobile platform. Will register all our instances with splat. +/// +[Preserve(AllMembers = true)] +public class Registrations : IWantsToRegisterStuff +{ + /// + public void Register(IMutableDependencyResolver resolver, IReadonlyDependencyResolver readonlyDependencyResolver) + { +#if NETSTANDARD || XAMARINIOS || XAMARINMAC || XAMARINTVOS || TIZEN || MONOANDROID13_0 + if (resolver is null) + { + throw new ArgumentNullException(nameof(resolver)); + } +#else + ArgumentNullException.ThrowIfNull(resolver); +#endif + + resolver.Register( + () => new JsonSerializerSettings + { + ObjectCreationHandling = ObjectCreationHandling.Replace, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + TypeNameHandling = TypeNameHandling.All, + }, + typeof(JsonSerializerSettings), + null); + + resolver.Register(() => new NewtonsoftSerializer(readonlyDependencyResolver.GetService()!), typeof(ISerializer), null); + + resolver.Register(() => new JsonDateTimeContractResolver(), typeof(IDateTimeContractResolver), null); + } +} diff --git a/src/Akavache.Sqlite3/SqlLiteCache/SqlRawPersistentBlobCache.cs b/src/Akavache.Sqlite3/SqlLiteCache/SqlRawPersistentBlobCache.cs index 4a065966b..d6a04f6a3 100644 --- a/src/Akavache.Sqlite3/SqlLiteCache/SqlRawPersistentBlobCache.cs +++ b/src/Akavache.Sqlite3/SqlLiteCache/SqlRawPersistentBlobCache.cs @@ -6,10 +6,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reactive.Disposables; - -using Newtonsoft.Json; -using Newtonsoft.Json.Bson; - using Splat; using SQLite; @@ -25,7 +21,7 @@ public class SqlRawPersistentBlobCache : IEnableLogger, IObjectBulkBlobCache private readonly IObservable _initializer; [SuppressMessage("Design", "CA2213: Dispose field", Justification = "Used to indicate disposal.")] private readonly AsyncSubject _shutdown = new(); - 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 SqliteOperationQueue? _opQueue; private IDisposable? _queueThread; private DateTimeKind? _dateTimeKind; @@ -39,8 +35,8 @@ public class SqlRawPersistentBlobCache : IEnableLogger, IObjectBulkBlobCache public SqlRawPersistentBlobCache(string databaseFile, IScheduler? scheduler = null) { Scheduler = scheduler ?? BlobCache.TaskpoolScheduler; - BlobCache.EnsureInitialized(); + _jsonDateTimeContractResolver = Locator.Current.GetService() ?? throw new Exception("Could not resolve IDateTimeContractResolver"); Connection = new(databaseFile, storeDateTimeAsTicks: true); _initializer = Initialize(); @@ -755,41 +751,26 @@ protected virtual IObservable AfterReadFromDiskFilter(byte[] data, ISche ExceptionHelper.ObservableThrowObjectDisposedException("SqlitePersistentBlobCache") : Observable.Return(data, scheduler); + private ISerializer GetSerializer() + { + var s = Locator.Current.GetService() ?? throw new Exception("ISerializer is not registered"); + s.CreateSerializer(() => _jsonDateTimeContractResolver); + return s; + } + private byte[] SerializeObject(T value) { var serializer = GetSerializer(); - using var ms = new MemoryStream(); - using var writer = new BsonDataWriter(ms); - serializer.Serialize(writer, new ObjectWrapper(value)); - return ms.ToArray(); + return serializer.Serialize(value); } private IObservable DeserializeObject(byte[] data) { var serializer = GetSerializer(); - using var reader = new BsonDataReader(new MemoryStream(data)); - var forcedDateTimeKind = BlobCache.ForcedDateTimeKind; - - if (forcedDateTimeKind.HasValue) - { - reader.DateTimeKindHandling = forcedDateTimeKind.Value; - } try { - try - { -#pragma warning disable CS8602 // Dereference of a possibly null reference. - var boxedVal = serializer.Deserialize>(reader).Value; -#pragma warning restore CS8602 // Dereference of a possibly null reference. - return Observable.Return(boxedVal); - } - catch (Exception ex) - { - this.Log().Warn(ex, "Failed to deserialize data as boxed, we may be migrating from an old Akavache"); - } - - var rawVal = serializer.Deserialize(reader); + var rawVal = serializer.Deserialize(data); return Observable.Return(rawVal); } catch (Exception ex) @@ -797,20 +778,4 @@ private byte[] SerializeObject(T value) return Observable.Throw(ex); } } - - private JsonSerializer GetSerializer() - { - var settings = Locator.Current.GetService() ?? new JsonSerializerSettings(); - JsonSerializer serializer; - - lock (settings) - { - _jsonDateTimeContractResolver.ExistingContractResolver = settings.ContractResolver; - settings.ContractResolver = _jsonDateTimeContractResolver; - serializer = JsonSerializer.Create(settings); - settings.ContractResolver = _jsonDateTimeContractResolver.ExistingContractResolver; - } - - return serializer; - } } diff --git a/src/Akavache.Tests/API/ApiApprovalTests.AkavacheCore.DotNet6_0.verified.txt b/src/Akavache.Tests/API/ApiApprovalTests.AkavacheCore.DotNet6_0.verified.txt index fbac23e95..b8b8b4821 100644 --- a/src/Akavache.Tests/API/ApiApprovalTests.AkavacheCore.DotNet6_0.verified.txt +++ b/src/Akavache.Tests/API/ApiApprovalTests.AkavacheCore.DotNet6_0.verified.txt @@ -1,6 +1,8 @@ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Drawing")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Json")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Mobile")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.NewtonsoftJson")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Sqlite3")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Tests")] [assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName=".NET 6.0")] @@ -114,6 +116,10 @@ namespace Akavache System.IObservable Insert(System.Collections.Generic.IDictionary keyValuePairs, System.DateTimeOffset? absoluteExpiration = default); System.IObservable Invalidate(System.Collections.Generic.IEnumerable keys); } + public interface IDateTimeContractResolver + { + System.DateTimeKind? ForceDateTimeKindOverride { get; set; } + } public interface IEncryptionProvider { System.IObservable DecryptBlock(byte[] block); @@ -152,6 +158,16 @@ namespace Akavache System.IObservable InvalidateObjects(System.Collections.Generic.IEnumerable keys); } public interface ISecureBlobCache : Akavache.IBlobCache, System.IDisposable { } + public interface ISerializer + { + void CreateSerializer(System.Func getJsonDateTimeContractResolver); + T? Deserialize(byte[] bytes); + System.IObservable DeserializeObject(byte[] x); + T DeserializeObjectWrapper(byte[] data); + byte[] Serialize(T item); + byte[] SerializeObject(T value); + byte[] SerializeObjectWrapper(T value); + } public class InMemoryBlobCache : Akavache.IBlobCache, Akavache.IObjectBlobCache, Akavache.ISecureBlobCache, Splat.IEnableLogger, System.IDisposable { public InMemoryBlobCache() { } diff --git a/src/Akavache.Tests/API/ApiApprovalTests.AkavacheCore.Net4_8.verified.txt b/src/Akavache.Tests/API/ApiApprovalTests.AkavacheCore.Net4_8.verified.txt index 01fdfccfe..f60b84dbb 100644 --- a/src/Akavache.Tests/API/ApiApprovalTests.AkavacheCore.Net4_8.verified.txt +++ b/src/Akavache.Tests/API/ApiApprovalTests.AkavacheCore.Net4_8.verified.txt @@ -1,6 +1,8 @@ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Drawing")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Json")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Mobile")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.NewtonsoftJson")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Sqlite3")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Tests")] [assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")] @@ -114,6 +116,10 @@ namespace Akavache System.IObservable Insert(System.Collections.Generic.IDictionary keyValuePairs, System.DateTimeOffset? absoluteExpiration = default); System.IObservable Invalidate(System.Collections.Generic.IEnumerable keys); } + public interface IDateTimeContractResolver + { + System.DateTimeKind? ForceDateTimeKindOverride { get; set; } + } public interface IEncryptionProvider { System.IObservable DecryptBlock(byte[] block); @@ -152,6 +158,16 @@ namespace Akavache System.IObservable InvalidateObjects(System.Collections.Generic.IEnumerable keys); } public interface ISecureBlobCache : Akavache.IBlobCache, System.IDisposable { } + public interface ISerializer + { + void CreateSerializer(System.Func getJsonDateTimeContractResolver); + T? Deserialize(byte[] bytes); + System.IObservable DeserializeObject(byte[] x); + T DeserializeObjectWrapper(byte[] data); + byte[] Serialize(T item); + byte[] SerializeObject(T value); + byte[] SerializeObjectWrapper(T value); + } public class InMemoryBlobCache : Akavache.IBlobCache, Akavache.IObjectBlobCache, Akavache.ISecureBlobCache, Splat.IEnableLogger, System.IDisposable { public InMemoryBlobCache() { } diff --git a/src/Akavache.Tests/Akavache.Tests.csproj b/src/Akavache.Tests/Akavache.Tests.csproj index 619693a09..4ebbde72a 100644 --- a/src/Akavache.Tests/Akavache.Tests.csproj +++ b/src/Akavache.Tests/Akavache.Tests.csproj @@ -34,6 +34,7 @@ + diff --git a/src/Akavache.Tests/TestBases/ObjectBulkOperationsTestBase.cs b/src/Akavache.Tests/TestBases/ObjectBulkOperationsTestBase.cs index 4bc091300..8d9ebda32 100644 --- a/src/Akavache.Tests/TestBases/ObjectBulkOperationsTestBase.cs +++ b/src/Akavache.Tests/TestBases/ObjectBulkOperationsTestBase.cs @@ -68,7 +68,7 @@ public async Task InsertShouldWorkWithMultipleKeys() var data = Tuple.Create("Foo", 4); var keys = new[] { "Foo", "Bar", "Baz", }; - await fixture.InsertObjects(keys.ToDictionary(k => k, v => data)).FirstAsync(); + await fixture.InsertObjects(keys.ToDictionary(k => k, _ => data)).FirstAsync(); Assert.Equal(keys.Length, (await fixture.GetAllKeys().FirstAsync()).Count()); diff --git a/src/Akavache.sln b/src/Akavache.sln index f04ed7394..e93ae965e 100644 --- a/src/Akavache.sln +++ b/src/Akavache.sln @@ -26,6 +26,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akavache", "Akavache\Akavac EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akavache.Drawing", "Akavache.Drawing\Akavache.Drawing.csproj", "{B24FE0E4-D602-45DC-8F3F-FE1C8F9AC63D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akavache.Json", "Akavache.Json\Akavache.Json.csproj", "{203F1A15-F6AE-4234-B208-BAB572734072}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akavache.NewtonsoftJson", "Akavache.NewtonsoftJson\Akavache.NewtonsoftJson.csproj", "{44E8D94A-E680-46C6-97CE-4854BC049BD1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -56,6 +60,14 @@ Global {B24FE0E4-D602-45DC-8F3F-FE1C8F9AC63D}.Debug|Any CPU.Build.0 = Debug|Any CPU {B24FE0E4-D602-45DC-8F3F-FE1C8F9AC63D}.Release|Any CPU.ActiveCfg = Release|Any CPU {B24FE0E4-D602-45DC-8F3F-FE1C8F9AC63D}.Release|Any CPU.Build.0 = Release|Any CPU + {203F1A15-F6AE-4234-B208-BAB572734072}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {203F1A15-F6AE-4234-B208-BAB572734072}.Debug|Any CPU.Build.0 = Debug|Any CPU + {203F1A15-F6AE-4234-B208-BAB572734072}.Release|Any CPU.ActiveCfg = Release|Any CPU + {203F1A15-F6AE-4234-B208-BAB572734072}.Release|Any CPU.Build.0 = Release|Any CPU + {44E8D94A-E680-46C6-97CE-4854BC049BD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44E8D94A-E680-46C6-97CE-4854BC049BD1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44E8D94A-E680-46C6-97CE-4854BC049BD1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44E8D94A-E680-46C6-97CE-4854BC049BD1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 2bbe8f258b1407de51a4f98eba753f71e2894050 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Mon, 27 Nov 2023 00:56:19 +0000 Subject: [PATCH 2/3] Add System.Text.Json Tests --- .../BlobCache/InMemoryBlobCache.cs | 8 +- src/Akavache.Core/ISerializer.cs | 18 - src/Akavache.Core/Properties/AssemblyInfo.cs | 1 + ...lTests.AkavacheCore.DotNet6_0.verified.txt | 298 +++++++ ...ovalTests.AkavacheCore.Net4_8.verified.txt | 298 +++++++ ...sts.AkavacheDrawing.DotNet6_0.verified.txt | 21 + ...lTests.AkavacheDrawing.Net4_8.verified.txt | 21 + ...sts.AkavacheProject.DotNet6_0.verified.txt | 18 + ...lTests.AkavacheProject.Net4_8.verified.txt | 18 + .../API/ApiApprovalTests.cs | 41 + src/Akavache.Json.Tests/API/ApiExtensions.cs | 39 + .../Akavache.Json.Tests.csproj | 51 ++ src/Akavache.Json.Tests/AsyncLockTests.cs | 40 + .../BasicEncryptionTests.cs | 53 ++ .../BlockingDisposeBulkCache.cs | 35 + .../BlockingDispose/BlockingDisposeCache.cs | 49 ++ .../BlockingDisposeObjectCache.cs | 21 + src/Akavache.Json.Tests/CoalescerTests.cs | 213 +++++ .../DateTimeResolverTests.cs | 39 + .../EncryptedSqliteBlobBulkExtensionsTest.cs | 21 + ...yptedSqliteBlobCacheBulkOperationsTests.cs | 15 + .../EncryptedSqliteBlobCacheDateTimeTests.cs | 15 + ...cryptedSqliteBlobCacheExtensionsFixture.cs | 19 + ...qliteBlobCacheObjectBulkOperationsTests.cs | 15 + .../Fixtures/DummyRoutedViewModel.cs | 44 + ...keDateTimeHighPrecisionContractResolver.cs | 26 + .../FakeDateTimeHighPrecisionJsonConverter.cs | 46 + .../Fixtures/ServiceProvider.cs | 15 + .../Fixtures/TestObjectDateTime.cs | 22 + .../Fixtures/TestObjectDateTimeOffset.cs | 22 + src/Akavache.Json.Tests/Fixtures/UserModel.cs | 31 + .../Fixtures/UserObject.cs | 27 + .../Helpers/IntegrationTestHelper.cs | 105 +++ src/Akavache.Json.Tests/Helpers/Utility.cs | 79 ++ .../InMemoryBlobCacheBulkOperationsTests.cs | 17 + .../InMemoryBlobCacheDateTimeTests.cs | 17 + .../InMemoryBlobCacheInterfaceTests.cs | 17 + ...emoryBlobCacheObjectBulkOperationsTests.cs | 17 + .../InMemoryBlobCacheTests.cs | 21 + .../Performance/PerfHelper.cs | 88 ++ .../Performance/ReadTests.cs | 151 ++++ .../Performance/Sqlite3ReadTests.cs | 17 + .../Performance/Sqlite3WriteTests.cs | 17 + .../Performance/WriteTests.cs | 107 +++ .../SqliteBlobCacheBulkExtensionsTest.cs | 21 + .../SqliteBlobCacheBulkOperationsTests.cs | 17 + .../SqliteBlobCacheDateTimeTests.cs | 17 + .../SqliteBlobCacheExtensionsTests.cs | 55 ++ .../SqliteBlobCacheInterfaceTests.cs | 17 + ...qliteBlobCacheObjectBulkOperationsTests.cs | 17 + .../TestBases/BlobCacheExtensionsTestBase.cs | 803 ++++++++++++++++++ .../TestBases/BlobCacheInterfaceTestBase.cs | 355 ++++++++ .../TestBases/BulkOperationsTestBase.cs | 111 +++ .../TestBases/DateTimeTestBase.cs | 161 ++++ .../TestBases/ObjectBulkOperationsTestBase.cs | 111 +++ src/Akavache.Json.Tests/UtilityTests.cs | 143 ++++ src/Akavache.Json.Tests/xunit.runner.json | 4 + src/Akavache.Json/SystemJsonSerializer.cs | 110 ++- .../NewtonsoftSerializer.cs | 108 +-- .../Properties/AssemblyInfo.cs | 1 + ...lTests.AkavacheCore.DotNet6_0.verified.txt | 3 +- ...ovalTests.AkavacheCore.Net4_8.verified.txt | 3 +- src/Akavache.sln | 6 + 63 files changed, 4173 insertions(+), 143 deletions(-) create mode 100644 src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheCore.DotNet6_0.verified.txt create mode 100644 src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheCore.Net4_8.verified.txt create mode 100644 src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheDrawing.DotNet6_0.verified.txt create mode 100644 src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheDrawing.Net4_8.verified.txt create mode 100644 src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheProject.DotNet6_0.verified.txt create mode 100644 src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheProject.Net4_8.verified.txt create mode 100644 src/Akavache.Json.Tests/API/ApiApprovalTests.cs create mode 100644 src/Akavache.Json.Tests/API/ApiExtensions.cs create mode 100644 src/Akavache.Json.Tests/Akavache.Json.Tests.csproj create mode 100644 src/Akavache.Json.Tests/AsyncLockTests.cs create mode 100644 src/Akavache.Json.Tests/BasicEncryptionTests.cs create mode 100644 src/Akavache.Json.Tests/BlockingDispose/BlockingDisposeBulkCache.cs create mode 100644 src/Akavache.Json.Tests/BlockingDispose/BlockingDisposeCache.cs create mode 100644 src/Akavache.Json.Tests/BlockingDispose/BlockingDisposeObjectCache.cs create mode 100644 src/Akavache.Json.Tests/CoalescerTests.cs create mode 100644 src/Akavache.Json.Tests/DateTimeResolverTests.cs create mode 100644 src/Akavache.Json.Tests/EncryptedSqliteBlobBulkExtensionsTest.cs create mode 100644 src/Akavache.Json.Tests/EncryptedSqliteBlobCacheBulkOperationsTests.cs create mode 100644 src/Akavache.Json.Tests/EncryptedSqliteBlobCacheDateTimeTests.cs create mode 100644 src/Akavache.Json.Tests/EncryptedSqliteBlobCacheExtensionsFixture.cs create mode 100644 src/Akavache.Json.Tests/EncryptedSqliteBlobCacheObjectBulkOperationsTests.cs create mode 100644 src/Akavache.Json.Tests/Fixtures/DummyRoutedViewModel.cs create mode 100644 src/Akavache.Json.Tests/Fixtures/FakeDateTimeHighPrecisionContractResolver.cs create mode 100644 src/Akavache.Json.Tests/Fixtures/FakeDateTimeHighPrecisionJsonConverter.cs create mode 100644 src/Akavache.Json.Tests/Fixtures/ServiceProvider.cs create mode 100644 src/Akavache.Json.Tests/Fixtures/TestObjectDateTime.cs create mode 100644 src/Akavache.Json.Tests/Fixtures/TestObjectDateTimeOffset.cs create mode 100644 src/Akavache.Json.Tests/Fixtures/UserModel.cs create mode 100644 src/Akavache.Json.Tests/Fixtures/UserObject.cs create mode 100644 src/Akavache.Json.Tests/Helpers/IntegrationTestHelper.cs create mode 100644 src/Akavache.Json.Tests/Helpers/Utility.cs create mode 100644 src/Akavache.Json.Tests/InMemoryBlobCacheBulkOperationsTests.cs create mode 100644 src/Akavache.Json.Tests/InMemoryBlobCacheDateTimeTests.cs create mode 100644 src/Akavache.Json.Tests/InMemoryBlobCacheInterfaceTests.cs create mode 100644 src/Akavache.Json.Tests/InMemoryBlobCacheObjectBulkOperationsTests.cs create mode 100644 src/Akavache.Json.Tests/InMemoryBlobCacheTests.cs create mode 100644 src/Akavache.Json.Tests/Performance/PerfHelper.cs create mode 100644 src/Akavache.Json.Tests/Performance/ReadTests.cs create mode 100644 src/Akavache.Json.Tests/Performance/Sqlite3ReadTests.cs create mode 100644 src/Akavache.Json.Tests/Performance/Sqlite3WriteTests.cs create mode 100644 src/Akavache.Json.Tests/Performance/WriteTests.cs create mode 100644 src/Akavache.Json.Tests/SqliteBlobCacheBulkExtensionsTest.cs create mode 100644 src/Akavache.Json.Tests/SqliteBlobCacheBulkOperationsTests.cs create mode 100644 src/Akavache.Json.Tests/SqliteBlobCacheDateTimeTests.cs create mode 100644 src/Akavache.Json.Tests/SqliteBlobCacheExtensionsTests.cs create mode 100644 src/Akavache.Json.Tests/SqliteBlobCacheInterfaceTests.cs create mode 100644 src/Akavache.Json.Tests/SqliteBlobCacheObjectBulkOperationsTests.cs create mode 100644 src/Akavache.Json.Tests/TestBases/BlobCacheExtensionsTestBase.cs create mode 100644 src/Akavache.Json.Tests/TestBases/BlobCacheInterfaceTestBase.cs create mode 100644 src/Akavache.Json.Tests/TestBases/BulkOperationsTestBase.cs create mode 100644 src/Akavache.Json.Tests/TestBases/DateTimeTestBase.cs create mode 100644 src/Akavache.Json.Tests/TestBases/ObjectBulkOperationsTestBase.cs create mode 100644 src/Akavache.Json.Tests/UtilityTests.cs create mode 100644 src/Akavache.Json.Tests/xunit.runner.json diff --git a/src/Akavache.Core/BlobCache/InMemoryBlobCache.cs b/src/Akavache.Core/BlobCache/InMemoryBlobCache.cs index c99f36e10..c7d39412c 100644 --- a/src/Akavache.Core/BlobCache/InMemoryBlobCache.cs +++ b/src/Akavache.Core/BlobCache/InMemoryBlobCache.cs @@ -327,7 +327,7 @@ public IObservable GetObject(string key) return ExceptionHelper.ObservableThrowKeyNotFoundException(key); } - var obj = DeserializeObject(entry.Value); + var obj = DeserializeObject(entry.Value)!; return Observable.Return(obj, Scheduler); } @@ -348,7 +348,7 @@ public IObservable> GetAllObjects() 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(x.Value.Value)) + .Select(x => DeserializeObject(x.Value.Value)!) .ToList(), Scheduler); } @@ -434,9 +434,9 @@ protected virtual void Dispose(bool isDisposing) _disposed = true; } - private byte[] SerializeObject(T value) => GetSerializer().SerializeObjectWrapper(value); + private byte[] SerializeObject(T value) => GetSerializer().Serialize(value); - private T DeserializeObject(byte[] data) => GetSerializer().DeserializeObjectWrapper(data); + private T? DeserializeObject(byte[] data) => GetSerializer().Deserialize(data); private ISerializer GetSerializer() { diff --git a/src/Akavache.Core/ISerializer.cs b/src/Akavache.Core/ISerializer.cs index bf5e4687a..26a49e9c8 100644 --- a/src/Akavache.Core/ISerializer.cs +++ b/src/Akavache.Core/ISerializer.cs @@ -32,15 +32,6 @@ public interface ISerializer /// An Observable of T. IObservable DeserializeObject(byte[] x); - /// - /// Deserializes the object wrapper. - /// - /// The type. - /// The data. - /// A value of T. - /// You must call CreateSerializer before deserializing. - T DeserializeObjectWrapper(byte[] data); - /// /// Serializes to an bytes. /// @@ -56,13 +47,4 @@ public interface ISerializer /// The value. /// The bytes. byte[] SerializeObject(T value); - - /// - /// Serializes the object wrapper. - /// - /// The type. - /// The value. - /// A byte array. - /// You must call CreateSerializer before serializing. - byte[] SerializeObjectWrapper(T value); } diff --git a/src/Akavache.Core/Properties/AssemblyInfo.cs b/src/Akavache.Core/Properties/AssemblyInfo.cs index 26875b49f..27ec3241c 100644 --- a/src/Akavache.Core/Properties/AssemblyInfo.cs +++ b/src/Akavache.Core/Properties/AssemblyInfo.cs @@ -7,6 +7,7 @@ using Akavache.Core; [assembly: InternalsVisibleTo("Akavache.Tests")] +[assembly: InternalsVisibleTo("Akavache.Json.Tests")] [assembly: InternalsVisibleTo("Akavache.Sqlite3")] [assembly: InternalsVisibleTo("Akavache.NewtonsoftJson")] [assembly: InternalsVisibleTo("Akavache.Json")] diff --git a/src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheCore.DotNet6_0.verified.txt b/src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheCore.DotNet6_0.verified.txt new file mode 100644 index 000000000..e9f123956 --- /dev/null +++ b/src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheCore.DotNet6_0.verified.txt @@ -0,0 +1,298 @@ +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Drawing")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Json")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Json.Tests")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Mobile")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.NewtonsoftJson")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Sqlite3")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Tests")] +[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName=".NET 6.0")] +namespace Akavache +{ + public class AkavacheHttpMixin : Akavache.IAkavacheHttpMixin + { + public AkavacheHttpMixin() { } + public System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, string key, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, string key, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, string key, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, string key, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + } + public static class BlobCache + { + public static string ApplicationName { get; set; } + public static System.DateTimeKind? ForcedDateTimeKind { get; set; } + public static Akavache.ISecureBlobCache InMemory { get; set; } + public static Akavache.IBlobCache LocalMachine { get; set; } + public static Akavache.ISecureBlobCache Secure { get; set; } + public static System.Reactive.Concurrency.IScheduler TaskpoolScheduler { get; set; } + public static Akavache.IBlobCache UserAccount { get; set; } + public static void EnsureInitialized() { } + public static System.Threading.Tasks.Task Shutdown() { } + } + public static class BulkOperationsMixin + { + public static System.IObservable> Get(this Akavache.IBlobCache blobCache, System.Collections.Generic.IEnumerable keys) { } + public static System.IObservable> GetCreatedAt(this Akavache.IBlobCache blobCache, System.Collections.Generic.IEnumerable keys) { } + public static System.IObservable> GetObjects(this Akavache.IBlobCache blobCache, System.Collections.Generic.IEnumerable keys) { } + public static System.IObservable Insert(this Akavache.IBlobCache blobCache, System.Collections.Generic.IDictionary keyValuePairs, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable InsertObjects(this Akavache.IBlobCache blobCache, System.Collections.Generic.IDictionary keyValuePairs, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable Invalidate(this Akavache.IBlobCache blobCache, System.Collections.Generic.IEnumerable keys) { } + public static System.IObservable InvalidateObjects(this Akavache.IBlobCache blobCache, System.Collections.Generic.IEnumerable keys) { } + } + public class CacheEntry + { + public CacheEntry(string? typeName, byte[] value, System.DateTimeOffset createdAt, System.DateTimeOffset? expiresAt) { } + public System.DateTimeOffset CreatedAt { get; set; } + public System.DateTimeOffset? ExpiresAt { get; set; } + public string? TypeName { get; set; } + public byte[] Value { get; set; } + } + public enum DataProtectionScope + { + CurrentUser = 0, + } + public class DefaultAkavacheHttpClientFactory : Akavache.IAkavacheHttpClientFactory + { + public DefaultAkavacheHttpClientFactory() { } + public System.Net.Http.HttpClient CreateClient(string name) { } + } + public static class DependencyResolverMixin + { + public static void InitializeAkavache(this Splat.IMutableDependencyResolver resolver, Splat.IReadonlyDependencyResolver readonlyDependencyResolver) { } + } + public class EncryptionProvider : Akavache.IEncryptionProvider + { + public EncryptionProvider() { } + public System.IObservable DecryptBlock(byte[] block) { } + public System.IObservable EncryptBlock(byte[] block) { } + } + public static class HttpMixinExtensions + { + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, string key, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, string key, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, string key, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, string key, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + } + public interface IAkavacheHttpClientFactory + { + System.Net.Http.HttpClient CreateClient(string name); + } + public interface IAkavacheHttpMixin + { + System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, string key, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, string key, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, string key, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, string key, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default); + } + public interface IBlobCache : System.IDisposable + { + System.DateTimeKind? ForcedDateTimeKind { get; set; } + System.Reactive.Concurrency.IScheduler Scheduler { get; } + System.IObservable Shutdown { get; } + System.IObservable Flush(); + System.IObservable Get(string key); + System.IObservable> GetAllKeys(); + System.IObservable GetCreatedAt(string key); + System.IObservable Insert(string key, byte[] data, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable Invalidate(string key); + System.IObservable InvalidateAll(); + System.IObservable Vacuum(); + } + public interface IBulkBlobCache : Akavache.IBlobCache, System.IDisposable + { + System.IObservable> Get(System.Collections.Generic.IEnumerable keys); + System.IObservable> GetCreatedAt(System.Collections.Generic.IEnumerable keys); + System.IObservable Insert(System.Collections.Generic.IDictionary keyValuePairs, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable Invalidate(System.Collections.Generic.IEnumerable keys); + } + public interface IDateTimeContractResolver + { + System.DateTimeKind? ForceDateTimeKindOverride { get; set; } + } + public interface IEncryptionProvider + { + System.IObservable DecryptBlock(byte[] block); + System.IObservable EncryptBlock(byte[] block); + } + public interface IFilesystemProvider + { + System.IObservable CreateRecursive(string path); + System.IObservable Delete(string path); + string? GetDefaultLocalMachineCacheDirectory(); + string? GetDefaultRoamingCacheDirectory(); + string? GetDefaultSecretCacheDirectory(); + System.IObservable OpenFileForReadAsync(string path, System.Reactive.Concurrency.IScheduler scheduler); + System.IObservable OpenFileForWriteAsync(string path, System.Reactive.Concurrency.IScheduler scheduler); + } + public interface IKeyedOperationQueue + { + System.IObservable EnqueueObservableOperation(string key, System.Func> asyncCalculationFunc); + System.IObservable EnqueueOperation(string key, System.Action action); + System.IObservable EnqueueOperation(string key, System.Func calculationFunc); + System.IObservable ShutdownQueue(); + } + public interface IObjectBlobCache : Akavache.IBlobCache, System.IDisposable + { + System.IObservable> GetAllObjects(); + System.IObservable GetObject(string key); + System.IObservable GetObjectCreatedAt(string key); + System.IObservable InsertObject(string key, T value, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable InvalidateAllObjects(); + System.IObservable InvalidateObject(string key); + } + public interface IObjectBulkBlobCache : Akavache.IBlobCache, Akavache.IBulkBlobCache, Akavache.IObjectBlobCache, System.IDisposable + { + System.IObservable> GetObjects(System.Collections.Generic.IEnumerable keys); + System.IObservable InsertObjects(System.Collections.Generic.IDictionary keyValuePairs, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable InvalidateObjects(System.Collections.Generic.IEnumerable keys); + } + public interface ISecureBlobCache : Akavache.IBlobCache, System.IDisposable { } + public interface ISerializer + { + void CreateSerializer(System.Func getJsonDateTimeContractResolver); + T? Deserialize(byte[] bytes); + System.IObservable DeserializeObject(byte[] x); + byte[] Serialize(T item); + byte[] SerializeObject(T value); + } + public class InMemoryBlobCache : Akavache.IBlobCache, Akavache.IObjectBlobCache, Akavache.ISecureBlobCache, Splat.IEnableLogger, System.IDisposable + { + public InMemoryBlobCache() { } + public InMemoryBlobCache(System.Collections.Generic.IEnumerable> initialContents) { } + public InMemoryBlobCache(System.Reactive.Concurrency.IScheduler scheduler) { } + public InMemoryBlobCache(System.Reactive.Concurrency.IScheduler? scheduler, System.Collections.Generic.IEnumerable>? initialContents) { } + public System.DateTimeKind? ForcedDateTimeKind { get; set; } + public System.Reactive.Concurrency.IScheduler Scheduler { get; set; } + public System.IObservable Shutdown { get; } + public void Dispose() { } + protected virtual void Dispose(bool isDisposing) { } + public System.IObservable Flush() { } + public System.IObservable Get(string key) { } + public System.IObservable> GetAllKeys() { } + public System.IObservable> GetAllObjects() { } + public System.IObservable GetCreatedAt(string key) { } + public System.IObservable GetObject(string key) { } + public System.IObservable GetObjectCreatedAt(string key) { } + public System.IObservable Insert(string key, byte[] data, System.DateTimeOffset? absoluteExpiration = default) { } + public System.IObservable InsertObject(string key, T value, System.DateTimeOffset? absoluteExpiration = default) { } + public System.IObservable Invalidate(string key) { } + public System.IObservable InvalidateAll() { } + public System.IObservable InvalidateAllObjects() { } + public System.IObservable InvalidateObject(string key) { } + public System.IObservable Vacuum() { } + public static Akavache.InMemoryBlobCache OverrideGlobals(System.Collections.Generic.IDictionary initialContents, System.Reactive.Concurrency.IScheduler? scheduler = null) { } + public static Akavache.InMemoryBlobCache OverrideGlobals(System.Collections.Generic.IDictionary initialContents, System.Reactive.Concurrency.IScheduler? scheduler = null) { } + public static Akavache.InMemoryBlobCache OverrideGlobals(System.Reactive.Concurrency.IScheduler? scheduler = null, params System.Collections.Generic.KeyValuePair[] initialContents) { } + } + public static class JsonSerializationMixin + { + public static System.IObservable> GetAllObjects(this Akavache.IBlobCache blobCache) { } + public static System.IObservable GetAndFetchLatest(this Akavache.IBlobCache blobCache, string key, System.Func> fetchFunc, System.Func? fetchPredicate = null, System.DateTimeOffset? absoluteExpiration = default, bool shouldInvalidateOnError = false, System.Func? cacheValidationPredicate = null) { } + public static System.IObservable GetAndFetchLatest(this Akavache.IBlobCache blobCache, string key, System.Func> fetchFunc, System.Func? fetchPredicate = null, System.DateTimeOffset? absoluteExpiration = default, bool shouldInvalidateOnError = false, System.Func? cacheValidationPredicate = null) { } + public static System.IObservable GetObject(this Akavache.IBlobCache blobCache, string key) { } + public static System.IObservable GetObjectCreatedAt(this Akavache.IBlobCache blobCache, string key) { } + public static System.IObservable GetOrCreateObject(this Akavache.IBlobCache blobCache, string key, System.Func fetchFunc, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable GetOrFetchObject(this Akavache.IBlobCache blobCache, string key, System.Func> fetchFunc, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable GetOrFetchObject(this Akavache.IBlobCache blobCache, string key, System.Func> fetchFunc, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable InsertAllObjects(this Akavache.IBlobCache blobCache, System.Collections.Generic.IDictionary keyValuePairs, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable InsertObject(this Akavache.IBlobCache blobCache, string key, T value, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable InvalidateAllObjects(this Akavache.IBlobCache blobCache) { } + public static System.IObservable InvalidateObject(this Akavache.IBlobCache blobCache, string key) { } + } + public class KeyedOperationQueue : Akavache.IKeyedOperationQueue, Splat.IEnableLogger, System.IDisposable + { + public KeyedOperationQueue(System.Reactive.Concurrency.IScheduler? scheduler = null) { } + public void Dispose() { } + protected virtual void Dispose(bool disposing) { } + public System.IObservable EnqueueObservableOperation(string key, System.Func> asyncCalculationFunc) { } + public System.IObservable EnqueueOperation(string key, System.Action action) { } + public System.IObservable EnqueueOperation(string key, System.Func calculationFunc) { } + public System.IObservable ShutdownQueue() { } + } + public class LoginInfo + { + public LoginInfo(string username, string password) { } + public string Password { get; } + public string UserName { get; } + } + public static class LoginMixin + { + public static System.IObservable EraseLogin(this Akavache.ISecureBlobCache blobCache, string host = "default") { } + public static System.IObservable GetLoginAsync(this Akavache.ISecureBlobCache blobCache, string host = "default") { } + public static System.IObservable SaveLogin(this Akavache.ISecureBlobCache blobCache, string user, string password, string host = "default", System.DateTimeOffset? absoluteExpiration = default) { } + } + public static class ProtectedData + { + public static byte[] Protect(byte[] originalData, byte[]? entropy, Akavache.DataProtectionScope scope = 0) { } + public static byte[] Unprotect(byte[] originalData, byte[]? entropy, Akavache.DataProtectionScope scope = 0) { } + } + public static class RelativeTimeMixin + { + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, string url, System.TimeSpan expiration, System.Collections.Generic.Dictionary? headers = null, bool fetchAlways = false) { } + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, System.Uri url, System.TimeSpan expiration, System.Collections.Generic.Dictionary? headers = null, bool fetchAlways = false) { } + public static System.IObservable Insert(this Akavache.IBlobCache blobCache, string key, byte[] data, System.TimeSpan expiration) { } + public static System.IObservable InsertObject(this Akavache.IBlobCache blobCache, string key, T value, System.TimeSpan expiration) { } + public static System.IObservable SaveLogin(this Akavache.ISecureBlobCache blobCache, string user, string password, string host, System.TimeSpan expiration) { } + } + public class SimpleFilesystemProvider : Akavache.IFilesystemProvider + { + public SimpleFilesystemProvider() { } + public System.IObservable CreateRecursive(string path) { } + public System.IObservable Delete(string path) { } + public string GetDefaultLocalMachineCacheDirectory() { } + public string GetDefaultRoamingCacheDirectory() { } + public string GetDefaultSecretCacheDirectory() { } + public System.IObservable OpenFileForReadAsync(string path, System.Reactive.Concurrency.IScheduler scheduler) { } + public System.IObservable OpenFileForWriteAsync(string path, System.Reactive.Concurrency.IScheduler scheduler) { } + protected static string GetAssemblyDirectoryName() { } + } +} +namespace Akavache.Core +{ + public class Registrations + { + public Registrations() { } + public void Register(Splat.IMutableDependencyResolver resolver, Splat.IReadonlyDependencyResolver readonlyDependencyResolver) { } + } +} +namespace Akavache.Internal +{ + [System.Flags] + public enum FileAccess + { + Read = 1, + Write = 2, + ReadWrite = 3, + } + public enum FileMode + { + CreateNew = 1, + Create = 2, + Open = 3, + OpenOrCreate = 4, + Truncate = 5, + Append = 6, + } + [System.Flags] + public enum FileShare + { + None = 0, + Read = 1, + Write = 2, + ReadWrite = 3, + Delete = 4, + Inheritable = 16, + } +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheCore.Net4_8.verified.txt b/src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheCore.Net4_8.verified.txt new file mode 100644 index 000000000..069214fc9 --- /dev/null +++ b/src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheCore.Net4_8.verified.txt @@ -0,0 +1,298 @@ +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Drawing")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Json")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Json.Tests")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Mobile")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.NewtonsoftJson")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Sqlite3")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Tests")] +[assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")] +namespace Akavache +{ + public class AkavacheHttpMixin : Akavache.IAkavacheHttpMixin + { + public AkavacheHttpMixin() { } + public System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, string key, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, string key, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, string key, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, string key, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + } + public static class BlobCache + { + public static string ApplicationName { get; set; } + public static System.DateTimeKind? ForcedDateTimeKind { get; set; } + public static Akavache.ISecureBlobCache InMemory { get; set; } + public static Akavache.IBlobCache LocalMachine { get; set; } + public static Akavache.ISecureBlobCache Secure { get; set; } + public static System.Reactive.Concurrency.IScheduler TaskpoolScheduler { get; set; } + public static Akavache.IBlobCache UserAccount { get; set; } + public static void EnsureInitialized() { } + public static System.Threading.Tasks.Task Shutdown() { } + } + public static class BulkOperationsMixin + { + public static System.IObservable> Get(this Akavache.IBlobCache blobCache, System.Collections.Generic.IEnumerable keys) { } + public static System.IObservable> GetCreatedAt(this Akavache.IBlobCache blobCache, System.Collections.Generic.IEnumerable keys) { } + public static System.IObservable> GetObjects(this Akavache.IBlobCache blobCache, System.Collections.Generic.IEnumerable keys) { } + public static System.IObservable Insert(this Akavache.IBlobCache blobCache, System.Collections.Generic.IDictionary keyValuePairs, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable InsertObjects(this Akavache.IBlobCache blobCache, System.Collections.Generic.IDictionary keyValuePairs, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable Invalidate(this Akavache.IBlobCache blobCache, System.Collections.Generic.IEnumerable keys) { } + public static System.IObservable InvalidateObjects(this Akavache.IBlobCache blobCache, System.Collections.Generic.IEnumerable keys) { } + } + public class CacheEntry + { + public CacheEntry(string? typeName, byte[] value, System.DateTimeOffset createdAt, System.DateTimeOffset? expiresAt) { } + public System.DateTimeOffset CreatedAt { get; set; } + public System.DateTimeOffset? ExpiresAt { get; set; } + public string? TypeName { get; set; } + public byte[] Value { get; set; } + } + public enum DataProtectionScope + { + CurrentUser = 0, + } + public class DefaultAkavacheHttpClientFactory : Akavache.IAkavacheHttpClientFactory + { + public DefaultAkavacheHttpClientFactory() { } + public System.Net.Http.HttpClient CreateClient(string name) { } + } + public static class DependencyResolverMixin + { + public static void InitializeAkavache(this Splat.IMutableDependencyResolver resolver, Splat.IReadonlyDependencyResolver readonlyDependencyResolver) { } + } + public class EncryptionProvider : Akavache.IEncryptionProvider + { + public EncryptionProvider() { } + public System.IObservable DecryptBlock(byte[] block) { } + public System.IObservable EncryptBlock(byte[] block) { } + } + public static class HttpMixinExtensions + { + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, string key, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, string key, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, string key, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, string key, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default) { } + } + public interface IAkavacheHttpClientFactory + { + System.Net.Http.HttpClient CreateClient(string name); + } + public interface IAkavacheHttpMixin + { + System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, string key, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, string key, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, string key, string url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable DownloadUrl(Akavache.IBlobCache blobCache, System.Net.Http.HttpMethod method, string key, System.Uri url, System.Collections.Generic.IDictionary? headers = null, bool fetchAlways = false, System.DateTimeOffset? absoluteExpiration = default); + } + public interface IBlobCache : System.IDisposable + { + System.DateTimeKind? ForcedDateTimeKind { get; set; } + System.Reactive.Concurrency.IScheduler Scheduler { get; } + System.IObservable Shutdown { get; } + System.IObservable Flush(); + System.IObservable Get(string key); + System.IObservable> GetAllKeys(); + System.IObservable GetCreatedAt(string key); + System.IObservable Insert(string key, byte[] data, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable Invalidate(string key); + System.IObservable InvalidateAll(); + System.IObservable Vacuum(); + } + public interface IBulkBlobCache : Akavache.IBlobCache, System.IDisposable + { + System.IObservable> Get(System.Collections.Generic.IEnumerable keys); + System.IObservable> GetCreatedAt(System.Collections.Generic.IEnumerable keys); + System.IObservable Insert(System.Collections.Generic.IDictionary keyValuePairs, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable Invalidate(System.Collections.Generic.IEnumerable keys); + } + public interface IDateTimeContractResolver + { + System.DateTimeKind? ForceDateTimeKindOverride { get; set; } + } + public interface IEncryptionProvider + { + System.IObservable DecryptBlock(byte[] block); + System.IObservable EncryptBlock(byte[] block); + } + public interface IFilesystemProvider + { + System.IObservable CreateRecursive(string path); + System.IObservable Delete(string path); + string? GetDefaultLocalMachineCacheDirectory(); + string? GetDefaultRoamingCacheDirectory(); + string? GetDefaultSecretCacheDirectory(); + System.IObservable OpenFileForReadAsync(string path, System.Reactive.Concurrency.IScheduler scheduler); + System.IObservable OpenFileForWriteAsync(string path, System.Reactive.Concurrency.IScheduler scheduler); + } + public interface IKeyedOperationQueue + { + System.IObservable EnqueueObservableOperation(string key, System.Func> asyncCalculationFunc); + System.IObservable EnqueueOperation(string key, System.Action action); + System.IObservable EnqueueOperation(string key, System.Func calculationFunc); + System.IObservable ShutdownQueue(); + } + public interface IObjectBlobCache : Akavache.IBlobCache, System.IDisposable + { + System.IObservable> GetAllObjects(); + System.IObservable GetObject(string key); + System.IObservable GetObjectCreatedAt(string key); + System.IObservable InsertObject(string key, T value, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable InvalidateAllObjects(); + System.IObservable InvalidateObject(string key); + } + public interface IObjectBulkBlobCache : Akavache.IBlobCache, Akavache.IBulkBlobCache, Akavache.IObjectBlobCache, System.IDisposable + { + System.IObservable> GetObjects(System.Collections.Generic.IEnumerable keys); + System.IObservable InsertObjects(System.Collections.Generic.IDictionary keyValuePairs, System.DateTimeOffset? absoluteExpiration = default); + System.IObservable InvalidateObjects(System.Collections.Generic.IEnumerable keys); + } + public interface ISecureBlobCache : Akavache.IBlobCache, System.IDisposable { } + public interface ISerializer + { + void CreateSerializer(System.Func getJsonDateTimeContractResolver); + T? Deserialize(byte[] bytes); + System.IObservable DeserializeObject(byte[] x); + byte[] Serialize(T item); + byte[] SerializeObject(T value); + } + public class InMemoryBlobCache : Akavache.IBlobCache, Akavache.IObjectBlobCache, Akavache.ISecureBlobCache, Splat.IEnableLogger, System.IDisposable + { + public InMemoryBlobCache() { } + public InMemoryBlobCache(System.Collections.Generic.IEnumerable> initialContents) { } + public InMemoryBlobCache(System.Reactive.Concurrency.IScheduler scheduler) { } + public InMemoryBlobCache(System.Reactive.Concurrency.IScheduler? scheduler, System.Collections.Generic.IEnumerable>? initialContents) { } + public System.DateTimeKind? ForcedDateTimeKind { get; set; } + public System.Reactive.Concurrency.IScheduler Scheduler { get; set; } + public System.IObservable Shutdown { get; } + public void Dispose() { } + protected virtual void Dispose(bool isDisposing) { } + public System.IObservable Flush() { } + public System.IObservable Get(string key) { } + public System.IObservable> GetAllKeys() { } + public System.IObservable> GetAllObjects() { } + public System.IObservable GetCreatedAt(string key) { } + public System.IObservable GetObject(string key) { } + public System.IObservable GetObjectCreatedAt(string key) { } + public System.IObservable Insert(string key, byte[] data, System.DateTimeOffset? absoluteExpiration = default) { } + public System.IObservable InsertObject(string key, T value, System.DateTimeOffset? absoluteExpiration = default) { } + public System.IObservable Invalidate(string key) { } + public System.IObservable InvalidateAll() { } + public System.IObservable InvalidateAllObjects() { } + public System.IObservable InvalidateObject(string key) { } + public System.IObservable Vacuum() { } + public static Akavache.InMemoryBlobCache OverrideGlobals(System.Collections.Generic.IDictionary initialContents, System.Reactive.Concurrency.IScheduler? scheduler = null) { } + public static Akavache.InMemoryBlobCache OverrideGlobals(System.Collections.Generic.IDictionary initialContents, System.Reactive.Concurrency.IScheduler? scheduler = null) { } + public static Akavache.InMemoryBlobCache OverrideGlobals(System.Reactive.Concurrency.IScheduler? scheduler = null, params System.Collections.Generic.KeyValuePair[] initialContents) { } + } + public static class JsonSerializationMixin + { + public static System.IObservable> GetAllObjects(this Akavache.IBlobCache blobCache) { } + public static System.IObservable GetAndFetchLatest(this Akavache.IBlobCache blobCache, string key, System.Func> fetchFunc, System.Func? fetchPredicate = null, System.DateTimeOffset? absoluteExpiration = default, bool shouldInvalidateOnError = false, System.Func? cacheValidationPredicate = null) { } + public static System.IObservable GetAndFetchLatest(this Akavache.IBlobCache blobCache, string key, System.Func> fetchFunc, System.Func? fetchPredicate = null, System.DateTimeOffset? absoluteExpiration = default, bool shouldInvalidateOnError = false, System.Func? cacheValidationPredicate = null) { } + public static System.IObservable GetObject(this Akavache.IBlobCache blobCache, string key) { } + public static System.IObservable GetObjectCreatedAt(this Akavache.IBlobCache blobCache, string key) { } + public static System.IObservable GetOrCreateObject(this Akavache.IBlobCache blobCache, string key, System.Func fetchFunc, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable GetOrFetchObject(this Akavache.IBlobCache blobCache, string key, System.Func> fetchFunc, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable GetOrFetchObject(this Akavache.IBlobCache blobCache, string key, System.Func> fetchFunc, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable InsertAllObjects(this Akavache.IBlobCache blobCache, System.Collections.Generic.IDictionary keyValuePairs, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable InsertObject(this Akavache.IBlobCache blobCache, string key, T value, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable InvalidateAllObjects(this Akavache.IBlobCache blobCache) { } + public static System.IObservable InvalidateObject(this Akavache.IBlobCache blobCache, string key) { } + } + public class KeyedOperationQueue : Akavache.IKeyedOperationQueue, Splat.IEnableLogger, System.IDisposable + { + public KeyedOperationQueue(System.Reactive.Concurrency.IScheduler? scheduler = null) { } + public void Dispose() { } + protected virtual void Dispose(bool disposing) { } + public System.IObservable EnqueueObservableOperation(string key, System.Func> asyncCalculationFunc) { } + public System.IObservable EnqueueOperation(string key, System.Action action) { } + public System.IObservable EnqueueOperation(string key, System.Func calculationFunc) { } + public System.IObservable ShutdownQueue() { } + } + public class LoginInfo + { + public LoginInfo(string username, string password) { } + public string Password { get; } + public string UserName { get; } + } + public static class LoginMixin + { + public static System.IObservable EraseLogin(this Akavache.ISecureBlobCache blobCache, string host = "default") { } + public static System.IObservable GetLoginAsync(this Akavache.ISecureBlobCache blobCache, string host = "default") { } + public static System.IObservable SaveLogin(this Akavache.ISecureBlobCache blobCache, string user, string password, string host = "default", System.DateTimeOffset? absoluteExpiration = default) { } + } + public static class ProtectedData + { + public static byte[] Protect(byte[] originalData, byte[]? entropy, Akavache.DataProtectionScope scope = 0) { } + public static byte[] Unprotect(byte[] originalData, byte[]? entropy, Akavache.DataProtectionScope scope = 0) { } + } + public static class RelativeTimeMixin + { + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, string url, System.TimeSpan expiration, System.Collections.Generic.Dictionary? headers = null, bool fetchAlways = false) { } + public static System.IObservable DownloadUrl(this Akavache.IBlobCache blobCache, System.Uri url, System.TimeSpan expiration, System.Collections.Generic.Dictionary? headers = null, bool fetchAlways = false) { } + public static System.IObservable Insert(this Akavache.IBlobCache blobCache, string key, byte[] data, System.TimeSpan expiration) { } + public static System.IObservable InsertObject(this Akavache.IBlobCache blobCache, string key, T value, System.TimeSpan expiration) { } + public static System.IObservable SaveLogin(this Akavache.ISecureBlobCache blobCache, string user, string password, string host, System.TimeSpan expiration) { } + } + public class SimpleFilesystemProvider : Akavache.IFilesystemProvider + { + public SimpleFilesystemProvider() { } + public System.IObservable CreateRecursive(string path) { } + public System.IObservable Delete(string path) { } + public string GetDefaultLocalMachineCacheDirectory() { } + public string GetDefaultRoamingCacheDirectory() { } + public string GetDefaultSecretCacheDirectory() { } + public System.IObservable OpenFileForReadAsync(string path, System.Reactive.Concurrency.IScheduler scheduler) { } + public System.IObservable OpenFileForWriteAsync(string path, System.Reactive.Concurrency.IScheduler scheduler) { } + protected static string GetAssemblyDirectoryName() { } + } +} +namespace Akavache.Core +{ + public class Registrations + { + public Registrations() { } + public void Register(Splat.IMutableDependencyResolver resolver, Splat.IReadonlyDependencyResolver readonlyDependencyResolver) { } + } +} +namespace Akavache.Internal +{ + [System.Flags] + public enum FileAccess + { + Read = 1, + Write = 2, + ReadWrite = 3, + } + public enum FileMode + { + CreateNew = 1, + Create = 2, + Open = 3, + OpenOrCreate = 4, + Truncate = 5, + Append = 6, + } + [System.Flags] + public enum FileShare + { + None = 0, + Read = 1, + Write = 2, + ReadWrite = 3, + Delete = 4, + Inheritable = 16, + } +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheDrawing.DotNet6_0.verified.txt b/src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheDrawing.DotNet6_0.verified.txt new file mode 100644 index 000000000..28b9950e6 --- /dev/null +++ b/src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheDrawing.DotNet6_0.verified.txt @@ -0,0 +1,21 @@ +[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName=".NET 6.0")] +namespace Akavache +{ + public static class BitmapImageMixin + { + public static System.IObservable LoadImage(this Akavache.IBlobCache blobCache, string key, float? desiredWidth = default, float? desiredHeight = default) { } + public static System.IObservable LoadImageFromUrl(this Akavache.IBlobCache blobCache, string url, bool fetchAlways = false, float? desiredWidth = default, float? desiredHeight = default, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable LoadImageFromUrl(this Akavache.IBlobCache blobCache, System.Uri url, bool fetchAlways = false, float? desiredWidth = default, float? desiredHeight = default, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable LoadImageFromUrl(this Akavache.IBlobCache blobCache, string key, string url, bool fetchAlways = false, float? desiredWidth = default, float? desiredHeight = default, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable LoadImageFromUrl(this Akavache.IBlobCache blobCache, string key, System.Uri url, bool fetchAlways = false, float? desiredWidth = default, float? desiredHeight = default, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable ThrowOnBadImageBuffer(byte[] compressedImage) { } + } +} +namespace Akavache.Drawing +{ + public class Registrations + { + public Registrations() { } + public void Register(Splat.IMutableDependencyResolver resolver, Splat.IReadonlyDependencyResolver readonlyDependencyResolver) { } + } +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheDrawing.Net4_8.verified.txt b/src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheDrawing.Net4_8.verified.txt new file mode 100644 index 000000000..9eb2a57a2 --- /dev/null +++ b/src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheDrawing.Net4_8.verified.txt @@ -0,0 +1,21 @@ +[assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")] +namespace Akavache +{ + public static class BitmapImageMixin + { + public static System.IObservable LoadImage(this Akavache.IBlobCache blobCache, string key, float? desiredWidth = default, float? desiredHeight = default) { } + public static System.IObservable LoadImageFromUrl(this Akavache.IBlobCache blobCache, string url, bool fetchAlways = false, float? desiredWidth = default, float? desiredHeight = default, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable LoadImageFromUrl(this Akavache.IBlobCache blobCache, System.Uri url, bool fetchAlways = false, float? desiredWidth = default, float? desiredHeight = default, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable LoadImageFromUrl(this Akavache.IBlobCache blobCache, string key, string url, bool fetchAlways = false, float? desiredWidth = default, float? desiredHeight = default, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable LoadImageFromUrl(this Akavache.IBlobCache blobCache, string key, System.Uri url, bool fetchAlways = false, float? desiredWidth = default, float? desiredHeight = default, System.DateTimeOffset? absoluteExpiration = default) { } + public static System.IObservable ThrowOnBadImageBuffer(byte[] compressedImage) { } + } +} +namespace Akavache.Drawing +{ + public class Registrations + { + public Registrations() { } + public void Register(Splat.IMutableDependencyResolver resolver, Splat.IReadonlyDependencyResolver readonlyDependencyResolver) { } + } +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheProject.DotNet6_0.verified.txt b/src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheProject.DotNet6_0.verified.txt new file mode 100644 index 000000000..22c33a136 --- /dev/null +++ b/src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheProject.DotNet6_0.verified.txt @@ -0,0 +1,18 @@ +[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName=".NET 6.0")] +namespace Akavache +{ + public class Registrations + { + public Registrations() { } + public void Register(Splat.IMutableDependencyResolver resolver, Splat.IReadonlyDependencyResolver readonlyDependencyResolver) { } + public static void Start(string applicationName) { } + } +} +namespace Akavache.Sqlite3 +{ + public static class LinkerPreserve { } + public class SQLitePersistentBlobCache : Akavache.Sqlite3.SqlRawPersistentBlobCache + { + public SQLitePersistentBlobCache(string databaseFile, System.Reactive.Concurrency.IScheduler? scheduler = null) { } + } +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheProject.Net4_8.verified.txt b/src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheProject.Net4_8.verified.txt new file mode 100644 index 000000000..5021077c2 --- /dev/null +++ b/src/Akavache.Json.Tests/API/ApiApprovalTests.AkavacheProject.Net4_8.verified.txt @@ -0,0 +1,18 @@ +[assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")] +namespace Akavache +{ + public class Registrations + { + public Registrations() { } + public void Register(Splat.IMutableDependencyResolver resolver, Splat.IReadonlyDependencyResolver readonlyDependencyResolver) { } + public static void Start(string applicationName) { } + } +} +namespace Akavache.Sqlite3 +{ + public static class LinkerPreserve { } + public class SQLitePersistentBlobCache : Akavache.Sqlite3.SqlRawPersistentBlobCache + { + public SQLitePersistentBlobCache(string databaseFile, System.Reactive.Concurrency.IScheduler? scheduler = null) { } + } +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/API/ApiApprovalTests.cs b/src/Akavache.Json.Tests/API/ApiApprovalTests.cs new file mode 100644 index 000000000..669929771 --- /dev/null +++ b/src/Akavache.Json.Tests/API/ApiApprovalTests.cs @@ -0,0 +1,41 @@ +// 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. + +using System.Diagnostics.CodeAnalysis; + +using Akavache.Sqlite3; + +namespace Akavache.APITests; + +/// +/// Tests for handling API approval. +/// +[ExcludeFromCodeCoverage] +[UsesVerify] +public class ApiApprovalTests +{ + /// + /// Tests to make sure the akavache project is approved. + /// + /// A representing the asynchronous unit test. + [Fact] + public Task AkavacheProject() => typeof(SQLitePersistentBlobCache).Assembly.CheckApproval(["Akavache"]); + + /// + /// Tests to make sure the akavache core project is approved. + /// + /// A representing the asynchronous unit test. + [Fact] + public Task AkavacheCore() => typeof(BlobCache).Assembly.CheckApproval(["Akavache"]); + +#if !NETSTANDARD + /// + /// Tests to make sure the akavache drawing project is approved. + /// + /// A representing the asynchronous unit test. + [Fact] + public Task AkavacheDrawing() => typeof(Akavache.Drawing.Registrations).Assembly.CheckApproval(["Akavache"]); +#endif +} diff --git a/src/Akavache.Json.Tests/API/ApiExtensions.cs b/src/Akavache.Json.Tests/API/ApiExtensions.cs new file mode 100644 index 000000000..7978fcb2f --- /dev/null +++ b/src/Akavache.Json.Tests/API/ApiExtensions.cs @@ -0,0 +1,39 @@ +// 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. + +using System.Reflection; +using System.Runtime.CompilerServices; +using PublicApiGenerator; + +namespace Akavache.APITests; + +/// +/// A helper for doing API approvals. +/// +public static class ApiExtensions +{ + /// + /// Checks to make sure the API is approved. + /// + /// The assembly that is being checked. + /// The namespaces. + /// The caller file path. + /// + /// A Task. + /// + public static async Task CheckApproval(this Assembly assembly, string[] namespaces, [CallerFilePath] string filePath = "") + { + var generatorOptions = new ApiGeneratorOptions { AllowNamespacePrefixes = namespaces }; + var apiText = assembly.GeneratePublicApi(generatorOptions); + var result = await Verify(apiText, null, filePath) + .UniqueForRuntimeAndVersion() + .ScrubEmptyLines() + .ScrubLines(l => + l.StartsWith("[assembly: AssemblyVersion(", StringComparison.InvariantCulture) || + l.StartsWith("[assembly: AssemblyFileVersion(", StringComparison.InvariantCulture) || + l.StartsWith("[assembly: AssemblyInformationalVersion(", StringComparison.InvariantCulture) || + l.StartsWith("[assembly: System.Reflection.AssemblyMetadata(", StringComparison.InvariantCulture)); + } +} diff --git a/src/Akavache.Json.Tests/Akavache.Json.Tests.csproj b/src/Akavache.Json.Tests/Akavache.Json.Tests.csproj new file mode 100644 index 000000000..8d8fc4d08 --- /dev/null +++ b/src/Akavache.Json.Tests/Akavache.Json.Tests.csproj @@ -0,0 +1,51 @@ + + + + net48;net6.0 + $(NoWarn);CA1307;CA2000;CA1062 + latest + disable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + diff --git a/src/Akavache.Json.Tests/AsyncLockTests.cs b/src/Akavache.Json.Tests/AsyncLockTests.cs new file mode 100644 index 000000000..5c43484e2 --- /dev/null +++ b/src/Akavache.Json.Tests/AsyncLockTests.cs @@ -0,0 +1,40 @@ +// 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. + +using AsyncLock = Akavache.Sqlite3.Internal.AsyncLock; + +namespace Akavache.Tests; + +/// +/// Tests the class. +/// +public class AsyncLockTests +{ + /// + /// Makes sure that the AsyncLock class handles cancellation correctly. + /// + [Fact] + public void HandlesCancellation() + { + var asyncLock = new AsyncLock(); + var lockOne = asyncLock.LockAsync(); + + var cts = new CancellationTokenSource(); + var lockTwo = asyncLock.LockAsync(cts.Token); + + Assert.True(lockOne.IsCompleted); + Assert.Equal(TaskStatus.RanToCompletion, lockOne.Status); + Assert.NotNull(lockOne.Result); + + Assert.False(lockTwo.IsCompleted); + + cts.Cancel(); + + Assert.True(lockTwo.IsCompleted); + Assert.True(lockTwo.IsCanceled); + + lockOne.Result.Dispose(); + } +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/BasicEncryptionTests.cs b/src/Akavache.Json.Tests/BasicEncryptionTests.cs new file mode 100644 index 000000000..eb476dc01 --- /dev/null +++ b/src/Akavache.Json.Tests/BasicEncryptionTests.cs @@ -0,0 +1,53 @@ +// 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. + +using Splat; + +namespace Akavache.Tests; + +/// +/// Makes sure that basic encryption works correctly. +/// +public class BasicEncryptionTests +{ + /// + /// Makes sure that encryption works. + /// + /// A task to monitor the progress. + [SkippableFact] + public async Task ShouldEncrypt() + { + // TODO: This test is failing on .NET 6.0. Investigate. + Skip.If(GetType().Assembly.GetTargetFrameworkName().StartsWith("net")); + + var provider = new EncryptionProvider(); + var array = Encoding.ASCII.GetBytes("This is a test"); + + var result = await AsArray(provider.EncryptBlock(array)); + Assert.True(array.Length < result.Length); // Encrypted bytes should be much larger. + Assert.NotEqual(array.ToList(), result); + + // the string should be garbage. + Assert.NotEqual(Encoding.ASCII.GetString(result), "This is a test"); + } + + /// + /// Makes sure the decryption works. + /// + /// A task to monitor the progress. + [Fact] + public async Task ShouldDecrypt() + { + var provider = new EncryptionProvider(); + var array = Encoding.ASCII.GetBytes("This is a test"); + + var encrypted = await AsArray(provider.EncryptBlock(array)); + var decrypted = await AsArray(provider.DecryptBlock(encrypted)); + Assert.Equal(array.ToList(), decrypted); + Assert.Equal(Encoding.ASCII.GetString(decrypted), "This is a test"); + } + + private static async Task AsArray(IObservable source) => await source.FirstAsync(); +} diff --git a/src/Akavache.Json.Tests/BlockingDispose/BlockingDisposeBulkCache.cs b/src/Akavache.Json.Tests/BlockingDispose/BlockingDisposeBulkCache.cs new file mode 100644 index 000000000..3356a9817 --- /dev/null +++ b/src/Akavache.Json.Tests/BlockingDispose/BlockingDisposeBulkCache.cs @@ -0,0 +1,35 @@ +// 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.Tests; + +internal class BlockingDisposeBulkCache(IBlobCache inner) : BlockingDisposeCache(inner), IObjectBulkBlobCache +{ + public IObservable Insert(IDictionary keyValuePairs, DateTimeOffset? absoluteExpiration = null) => Inner.Insert(keyValuePairs, absoluteExpiration); + + public IObservable> Get(IEnumerable keys) => Inner.Get(keys); + + public IObservable> GetCreatedAt(IEnumerable keys) => Inner.GetCreatedAt(keys); + + public IObservable Invalidate(IEnumerable keys) => Inner.Invalidate(keys); + + public IObservable InsertObjects(IDictionary keyValuePairs, DateTimeOffset? absoluteExpiration = null) => Inner.InsertObjects(keyValuePairs, absoluteExpiration); + + public IObservable> GetObjects(IEnumerable keys) => Inner.GetObjects(keys); + + public IObservable InvalidateObjects(IEnumerable keys) => Inner.InvalidateObjects(keys); + + public IObservable InsertObject(string key, T value, DateTimeOffset? absoluteExpiration = null) => Inner.InsertObject(key, value, absoluteExpiration); + + public IObservable GetObject(string key) => Inner.GetObject(key); + + public IObservable GetObjectCreatedAt(string key) => Inner.GetObjectCreatedAt(key); + + public IObservable> GetAllObjects() => Inner.GetAllObjects(); + + public IObservable InvalidateObject(string key) => Inner.InvalidateObject(key); + + public IObservable InvalidateAllObjects() => Inner.InvalidateAllObjects(); +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/BlockingDispose/BlockingDisposeCache.cs b/src/Akavache.Json.Tests/BlockingDispose/BlockingDisposeCache.cs new file mode 100644 index 000000000..6da56bd2c --- /dev/null +++ b/src/Akavache.Json.Tests/BlockingDispose/BlockingDisposeCache.cs @@ -0,0 +1,49 @@ +// 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.Tests; + +internal class BlockingDisposeCache : IBlobCache +{ + public BlockingDisposeCache(IBlobCache cache) + { + BlobCache.EnsureInitialized(); + Inner = cache; + } + + public IObservable Shutdown => Inner.Shutdown; + + public IScheduler Scheduler => Inner.Scheduler; + + public DateTimeKind? ForcedDateTimeKind + { + get => Inner.ForcedDateTimeKind; + set => Inner.ForcedDateTimeKind = value; + } + + protected IBlobCache Inner { get; } + + public virtual void Dispose() + { + Inner.Dispose(); + Inner.Shutdown.Wait(); + } + + public IObservable Insert(string key, byte[] data, DateTimeOffset? absoluteExpiration = null) => Inner.Insert(key, data, absoluteExpiration); + + public IObservable Get(string key) => Inner.Get(key); + + public IObservable> GetAllKeys() => Inner.GetAllKeys(); + + public IObservable GetCreatedAt(string key) => Inner.GetCreatedAt(key); + + public IObservable Flush() => Inner.Flush(); + + public IObservable Invalidate(string key) => Inner.Invalidate(key); + + public IObservable InvalidateAll() => Inner.InvalidateAll(); + + public IObservable Vacuum() => Inner.Vacuum(); +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/BlockingDispose/BlockingDisposeObjectCache.cs b/src/Akavache.Json.Tests/BlockingDispose/BlockingDisposeObjectCache.cs new file mode 100644 index 000000000..9758491d8 --- /dev/null +++ b/src/Akavache.Json.Tests/BlockingDispose/BlockingDisposeObjectCache.cs @@ -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.Tests; + +internal class BlockingDisposeObjectCache(IObjectBlobCache cache) : BlockingDisposeCache(cache), IObjectBlobCache +{ + public IObservable InsertObject(string key, T value, DateTimeOffset? absoluteExpiration = null) => ((IObjectBlobCache)Inner).InsertObject(key, value, absoluteExpiration); + + public IObservable GetObject(string key) => ((IObjectBlobCache)Inner).GetObject(key); + + public IObservable> GetAllObjects() => ((IObjectBlobCache)Inner).GetAllObjects(); + + public IObservable InvalidateObject(string key) => ((IObjectBlobCache)Inner).InvalidateObject(key); + + public IObservable InvalidateAllObjects() => ((IObjectBlobCache)Inner).InvalidateAllObjects(); + + public IObservable GetObjectCreatedAt(string key) => ((IObjectBlobCache)Inner).GetObjectCreatedAt(key); +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/CoalescerTests.cs b/src/Akavache.Json.Tests/CoalescerTests.cs new file mode 100644 index 000000000..8721590df --- /dev/null +++ b/src/Akavache.Json.Tests/CoalescerTests.cs @@ -0,0 +1,213 @@ +// 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. + +using Akavache.Sqlite3; + +using DynamicData; + +#pragma warning disable CS4014 // Await on awaitable items. -- We don't wait on the observables. + +namespace Akavache.Tests; + +/// +/// Tests associated with the method. +/// +public class CoalescerTests +{ + /// + /// Tests for a single item. + /// + /// A task to monitor the progress. + [Fact] + public async Task SingleItem() + { + var fixture = new SqliteOperationQueue(); + fixture.Select(new[] { "Foo" }); + + var queue = fixture.DumpQueue(); + var subj = queue[0].CompletionAsElements; + subj.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var output).Subscribe(); + Assert.Equal(0, output.Count); + + var result = SqliteOperationQueue.CoalesceOperations(queue); + + Assert.Equal(1, result.Count); + Assert.Equal(OperationType.BulkSelectSqliteOperation, result[0].OperationType); + + // Make sure the input gets a result when we signal the output's subject + var outSub = result[0].CompletionAsElements; + + Assert.Equal(0, output.Count); + outSub.OnNext(new[] { new CacheElement { Key = "Foo" } }); + outSub.OnCompleted(); + await Task.Delay(500).ConfigureAwait(false); + Assert.Equal(1, output.Count); + } + + /// + /// Tests for a unrelated items. + /// + /// A task to monitor the progress. + [Fact] + public async Task UnrelatedItems() + { + var fixture = new SqliteOperationQueue(); + fixture.Select(new[] { "Foo" }); + fixture.Insert(new[] { new CacheElement { Key = "Bar" } }); + fixture.Invalidate(new[] { "Baz" }); + + var queue = fixture.DumpQueue(); + var subj = queue[0].CompletionAsElements; + subj.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var output).Subscribe(); + Assert.Equal(0, output.Count); + + var result = SqliteOperationQueue.CoalesceOperations(queue); + + Assert.Equal(3, result.Count); + Assert.Equal(OperationType.BulkSelectSqliteOperation, result[0].OperationType); + + // Make sure the input gets a result when we signal the output's subject + var outSub = result[0].CompletionAsElements; + + Assert.Equal(0, output.Count); + outSub.OnNext(new[] { new CacheElement { Key = "Foo" } }); + outSub.OnCompleted(); + await Task.Delay(500).ConfigureAwait(false); + Assert.Equal(1, output.Count); + } + + /// + /// Tests for unrelated selected. + /// + /// A task to monitor the progress. + [Fact] + public async Task CoalesceUnrelatedSelects() + { + var fixture = new SqliteOperationQueue(); + + fixture.Select(new[] { "Foo" }); + fixture.Select(new[] { "Bar" }); + fixture.Invalidate(new[] { "Bamf" }); + fixture.Select(new[] { "Baz" }); + + var queue = fixture.DumpQueue(); + queue.Where(x => x.OperationType == OperationType.BulkSelectSqliteOperation) + .Select(x => x.CompletionAsElements) + .Merge() + .ToObservableChangeSet(ImmediateScheduler.Instance) + .Bind(out var output) + .Subscribe(); + var result = SqliteOperationQueue.CoalesceOperations(queue); + + Assert.Equal(2, result.Count); + + var item = result.Single(x => x.OperationType == OperationType.BulkSelectSqliteOperation); + Assert.Equal(OperationType.BulkSelectSqliteOperation, item.OperationType); + Assert.Equal(3, item.ParametersAsKeys.Count()); + + // All three of the input Selects should get a value when we signal + // our output Select + var outSub = item.CompletionAsElements; + var fakeResult = new[] + { + new CacheElement { Key = "Foo" }, + new CacheElement { Key = "Bar" }, + new CacheElement { Key = "Baz" }, + }; + + Assert.Equal(0, output.Count); + outSub.OnNext(fakeResult); + outSub.OnCompleted(); + await Task.Delay(1000).ConfigureAwait(false); + Assert.Equal(3, output.Count); + } + + /// + /// Tests to make sure the de-duplication of related selects works. + /// + /// A task to monitor the progress. + [Fact] + public async Task DedupRelatedSelects() + { + var fixture = new SqliteOperationQueue(); + fixture.Select(new[] { "Foo" }); + fixture.Select(new[] { "Foo" }); + fixture.Select(new[] { "Bar" }); + fixture.Select(new[] { "Foo" }); + + var queue = fixture.DumpQueue(); + queue.Where(x => x.OperationType == OperationType.BulkSelectSqliteOperation) + .Select(x => x.CompletionAsElements) + .Merge() + .ToObservableChangeSet(ImmediateScheduler.Instance) + .Bind(out var output) + .Subscribe(); + var result = SqliteOperationQueue.CoalesceOperations(queue); + + Assert.Equal(1, result.Count); + Assert.Equal(OperationType.BulkSelectSqliteOperation, result[0].OperationType); + Assert.Equal(2, result[0].ParametersAsKeys.Count()); + + var fakeResult = new[] + { + new CacheElement { Key = "Foo" }, + new CacheElement { Key = "Bar" }, + }; + + var outSub = result[0].CompletionAsElements; + + Assert.Equal(0, output.Count); + outSub.OnNext(fakeResult); + outSub.OnCompleted(); + await Task.Delay(1000).ConfigureAwait(false); + Assert.Equal(4, output.Count); + } + + /// + /// Tests to make sure that interpolated operations don't get de-duplicated. + /// + [Fact] + public void InterpolatedOpsDontGetDeduped() + { + var fixture = new SqliteOperationQueue(); + fixture.Select(new[] { "Foo" }); + fixture.Insert(new[] { new CacheElement { Key = "Foo", Value = [1, 2, 3] } }); + fixture.Select(new[] { "Foo" }); + fixture.Insert(new[] { new CacheElement { Key = "Foo", Value = [4, 5, 6] } }); + + var queue = fixture.DumpQueue(); + var result = SqliteOperationQueue.CoalesceOperations(queue); + + Assert.Equal(4, result.Count); + Assert.Equal(OperationType.BulkSelectSqliteOperation, result[0].OperationType); + Assert.Equal(OperationType.BulkInsertSqliteOperation, result[1].OperationType); + Assert.Equal(OperationType.BulkSelectSqliteOperation, result[2].OperationType); + Assert.Equal(OperationType.BulkInsertSqliteOperation, result[3].OperationType); + + Assert.Equal(1, result[1].ParametersAsElements.First().Value[0]); + Assert.Equal(4, result[3].ParametersAsElements.First().Value[0]); + } + + /// + /// Tests to make sure that grouped requests with different keys return empty results. + /// + /// A task to monitor the progress. + [Fact] + public async Task GroupedRequestsWithDifferentKeysReturnEmptyResultIfItemsDontExist() + { + using (Utility.WithEmptyDirectory(out var path)) + using (var cache = new SQLitePersistentBlobCache(Path.Combine(path, "sqlite.db"))) + { + var queue = new SqliteOperationQueue(cache.Connection, BlobCache.TaskpoolScheduler); + var request = queue.Select(new[] { "Foo" }); + var unrelatedRequest = queue.Select(new[] { "Bar" }); + + cache.ReplaceOperationQueue(queue); + + Assert.Equal(0, (await request).Count()); + Assert.Equal(0, (await unrelatedRequest).Count()); + } + } +} diff --git a/src/Akavache.Json.Tests/DateTimeResolverTests.cs b/src/Akavache.Json.Tests/DateTimeResolverTests.cs new file mode 100644 index 000000000..29acbb76e --- /dev/null +++ b/src/Akavache.Json.Tests/DateTimeResolverTests.cs @@ -0,0 +1,39 @@ +// 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. + +using Newtonsoft.Json.Serialization; + +namespace Akavache.Tests; + +/// +/// Tests associated with the class. +/// +public class DateTimeResolverTests +{ + /// + /// Checks to make sure that the JsonDateTime resolver validates correctly. + /// + [Fact] + public void JsonDateTimeContractResolverValidateConverter() + { + // Verify our converter used + var contractResolver = (IContractResolver)new JsonDateTimeContractResolver(null, null); + var contract = contractResolver.ResolveContract(typeof(DateTime)); + Assert.True(contract.Converter == JsonDateTimeTickConverter.Default); + contract = contractResolver.ResolveContract(typeof(DateTime)); + Assert.True(contract.Converter == JsonDateTimeTickConverter.Default); + contract = contractResolver.ResolveContract(typeof(DateTime?)); + Assert.True(contract.Converter == JsonDateTimeTickConverter.Default); + contract = contractResolver.ResolveContract(typeof(DateTime?)); + Assert.True(contract.Converter == JsonDateTimeTickConverter.Default); + + // Verify the other converter is used + contractResolver = new JsonDateTimeContractResolver(new FakeDateTimeHighPrecisionContractResolver(), null); + contract = contractResolver.ResolveContract(typeof(DateTime)); + Assert.True(contract.Converter is FakeDateTimeHighPrecisionJsonConverter); + contract = contractResolver.ResolveContract(typeof(DateTimeOffset)); + Assert.True(contract.Converter == JsonDateTimeOffsetTickConverter.Default); + } +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/EncryptedSqliteBlobBulkExtensionsTest.cs b/src/Akavache.Json.Tests/EncryptedSqliteBlobBulkExtensionsTest.cs new file mode 100644 index 000000000..d9ecfb047 --- /dev/null +++ b/src/Akavache.Json.Tests/EncryptedSqliteBlobBulkExtensionsTest.cs @@ -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. + +using Akavache.Sqlite3; + +namespace Akavache.Tests; + +/// +/// Tests bulk extensions. +/// +public class EncryptedSqliteBlobBulkExtensionsTest : BlobCacheExtensionsTestBase +{ + /// + protected override IBlobCache CreateBlobCache(string path) + { + BlobCache.ApplicationName = "TestRunner"; + return new BlockingDisposeBulkCache(new SQLiteEncryptedBlobCache(Path.Combine(path, "sqlite.db"))); + } +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/EncryptedSqliteBlobCacheBulkOperationsTests.cs b/src/Akavache.Json.Tests/EncryptedSqliteBlobCacheBulkOperationsTests.cs new file mode 100644 index 000000000..d885f22e3 --- /dev/null +++ b/src/Akavache.Json.Tests/EncryptedSqliteBlobCacheBulkOperationsTests.cs @@ -0,0 +1,15 @@ +// 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.Tests; + +/// +/// Encrypted tests for the class. +/// +public class EncryptedSqliteBlobCacheBulkOperationsTests : BulkOperationsTestBase +{ + /// + protected override IBlobCache CreateBlobCache(string path) => new BlockingDisposeBulkCache(new Sqlite3.SQLiteEncryptedBlobCache(Path.Combine(path, "sqlite.db"))); +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/EncryptedSqliteBlobCacheDateTimeTests.cs b/src/Akavache.Json.Tests/EncryptedSqliteBlobCacheDateTimeTests.cs new file mode 100644 index 000000000..d4c2024fb --- /dev/null +++ b/src/Akavache.Json.Tests/EncryptedSqliteBlobCacheDateTimeTests.cs @@ -0,0 +1,15 @@ +// 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.Tests; + +/// +/// Encrypted DateTime tests for the class. +/// +public class EncryptedSqliteBlobCacheDateTimeTests : DateTimeTestBase +{ + /// + protected override IBlobCache CreateBlobCache(string path) => new BlockingDisposeBulkCache(new Sqlite3.SQLiteEncryptedBlobCache(Path.Combine(path, "sqlite.db"))); +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/EncryptedSqliteBlobCacheExtensionsFixture.cs b/src/Akavache.Json.Tests/EncryptedSqliteBlobCacheExtensionsFixture.cs new file mode 100644 index 000000000..f81c514cc --- /dev/null +++ b/src/Akavache.Json.Tests/EncryptedSqliteBlobCacheExtensionsFixture.cs @@ -0,0 +1,19 @@ +// 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.Tests; + +/// +/// Tests for the class. +/// +public class EncryptedSqliteBlobCacheExtensionsFixture : BlobCacheExtensionsTestBase +{ + /// + protected override IBlobCache CreateBlobCache(string path) + { + BlobCache.ApplicationName = "TestRunner"; + return new BlockingDisposeObjectCache(new Sqlite3.SQLiteEncryptedBlobCache(Path.Combine(path, "sqlite.db"))); + } +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/EncryptedSqliteBlobCacheObjectBulkOperationsTests.cs b/src/Akavache.Json.Tests/EncryptedSqliteBlobCacheObjectBulkOperationsTests.cs new file mode 100644 index 000000000..e8fcbfaff --- /dev/null +++ b/src/Akavache.Json.Tests/EncryptedSqliteBlobCacheObjectBulkOperationsTests.cs @@ -0,0 +1,15 @@ +// 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.Tests; + +/// +/// Encrypted bulk operation tests for the class. +/// +public class EncryptedSqliteBlobCacheObjectBulkOperationsTests : ObjectBulkOperationsTestBase +{ + /// + protected override IBlobCache CreateBlobCache(string path) => new BlockingDisposeBulkCache(new Sqlite3.SQLiteEncryptedBlobCache(Path.Combine(path, "sqlite.db"))); +} diff --git a/src/Akavache.Json.Tests/Fixtures/DummyRoutedViewModel.cs b/src/Akavache.Json.Tests/Fixtures/DummyRoutedViewModel.cs new file mode 100644 index 000000000..9e834c375 --- /dev/null +++ b/src/Akavache.Json.Tests/Fixtures/DummyRoutedViewModel.cs @@ -0,0 +1,44 @@ +// 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. + +using System.Runtime.Serialization; + +using ReactiveUI; + +namespace Akavache.Tests; + +/// +/// A dummy object used in tests that replicates a routed view model. +/// +/// +/// Initializes a new instance of the class. +/// +/// The screen object to set. +[DataContract] +public class DummyRoutedViewModel(IScreen screen) : ReactiveObject, IRoutableViewModel +{ + private Guid _aRandomGuid; + + /// + /// Gets the url path segment. + /// + public string UrlPathSegment => "foo"; + + /// + /// Gets the host screen. + /// + [DataMember] + public IScreen HostScreen { get; private set; } = screen; + + /// + /// Gets or sets a guid value. + /// + [DataMember] + public Guid ARandomGuid + { + get => _aRandomGuid; + set => this.RaiseAndSetIfChanged(ref _aRandomGuid, value); + } +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/Fixtures/FakeDateTimeHighPrecisionContractResolver.cs b/src/Akavache.Json.Tests/Fixtures/FakeDateTimeHighPrecisionContractResolver.cs new file mode 100644 index 000000000..888bc892a --- /dev/null +++ b/src/Akavache.Json.Tests/Fixtures/FakeDateTimeHighPrecisionContractResolver.cs @@ -0,0 +1,26 @@ +// 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. + +using Newtonsoft.Json.Serialization; + +namespace Akavache.Tests; + +/// +/// A fake DateTime using high precision times. +/// +public class FakeDateTimeHighPrecisionContractResolver : DefaultContractResolver +{ + /// + protected override JsonContract CreateContract(Type objectType) + { + var contract = base.CreateContract(objectType); + if (objectType == typeof(DateTime) || objectType == typeof(DateTime?)) + { + contract.Converter = new FakeDateTimeHighPrecisionJsonConverter(); + } + + return contract; + } +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/Fixtures/FakeDateTimeHighPrecisionJsonConverter.cs b/src/Akavache.Json.Tests/Fixtures/FakeDateTimeHighPrecisionJsonConverter.cs new file mode 100644 index 000000000..6325e1814 --- /dev/null +++ b/src/Akavache.Json.Tests/Fixtures/FakeDateTimeHighPrecisionJsonConverter.cs @@ -0,0 +1,46 @@ +// 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. + +using Newtonsoft.Json; + +namespace Akavache.Tests; + +/// +/// A fake converter for the DateTime and high precision. +/// +public class FakeDateTimeHighPrecisionJsonConverter : JsonConverter +{ + /// + public override bool CanConvert(Type objectType) => objectType == typeof(DateTime) || objectType == typeof(DateTime?); + + /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType is not JsonToken.Integer and not JsonToken.Date) + { + return null; + } + + // If you need to deserialize already-serialized DateTimeOffsets, it would come in as JsonToken.Date, uncomment to handle + // Newly serialized values will come in as JsonToken.Integer + if (reader.TokenType == JsonToken.Date) + { + return (DateTime)reader.Value; + } + + var ticks = (long)reader.Value; + return new DateTime(ticks, DateTimeKind.Utc); + } + + /// + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value is not null) + { + var dateTime = value is DateTime dt ? dt : ((DateTime?)value).Value; + serializer.Serialize(writer, dateTime.ToUniversalTime().Ticks); + } + } +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/Fixtures/ServiceProvider.cs b/src/Akavache.Json.Tests/Fixtures/ServiceProvider.cs new file mode 100644 index 000000000..804beae62 --- /dev/null +++ b/src/Akavache.Json.Tests/Fixtures/ServiceProvider.cs @@ -0,0 +1,15 @@ +// 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.Tests; + +/// +/// A fixture for the service provider. +/// +public class ServiceProvider : IServiceProvider +{ + /// + public object GetService(Type serviceType) => serviceType == typeof(UserModel) ? new UserModel(new()) : null; +} diff --git a/src/Akavache.Json.Tests/Fixtures/TestObjectDateTime.cs b/src/Akavache.Json.Tests/Fixtures/TestObjectDateTime.cs new file mode 100644 index 000000000..9a3e0374a --- /dev/null +++ b/src/Akavache.Json.Tests/Fixtures/TestObjectDateTime.cs @@ -0,0 +1,22 @@ +// 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.Tests; + +/// +/// A fixture for when testing DateTime based tests. +/// +public class TestObjectDateTime +{ + /// + /// Gets or sets the time stamp. + /// + public DateTime Timestamp { get; set; } + + /// + /// Gets or sets a nullable time stamp. + /// + public DateTime? TimestampNullable { get; set; } +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/Fixtures/TestObjectDateTimeOffset.cs b/src/Akavache.Json.Tests/Fixtures/TestObjectDateTimeOffset.cs new file mode 100644 index 000000000..451940646 --- /dev/null +++ b/src/Akavache.Json.Tests/Fixtures/TestObjectDateTimeOffset.cs @@ -0,0 +1,22 @@ +// 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.Tests; + +/// +/// Test object for doing DateTimeOffset tests. +/// +public class TestObjectDateTimeOffset +{ + /// + /// Gets or sets a timestamp. + /// + public DateTimeOffset Timestamp { get; set; } + + /// + /// Gets or sets a nullable timestamp. + /// + public DateTimeOffset? TimestampNullable { get; set; } +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/Fixtures/UserModel.cs b/src/Akavache.Json.Tests/Fixtures/UserModel.cs new file mode 100644 index 000000000..cd1dcdac5 --- /dev/null +++ b/src/Akavache.Json.Tests/Fixtures/UserModel.cs @@ -0,0 +1,31 @@ +// 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.Tests; + +/// +/// A mock for the user models. +/// +/// +/// Initializes a new instance of the class. +/// +/// The user to abstract. +public class UserModel(UserObject user) +{ + /// + /// Gets or sets the name. + /// + public string Name { get; set; } + + /// + /// Gets or sets the age. + /// + public int Age { get; set; } + + /// + /// Gets or sets the user. + /// + public UserObject User { get; set; } = user; +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/Fixtures/UserObject.cs b/src/Akavache.Json.Tests/Fixtures/UserObject.cs new file mode 100644 index 000000000..38385b1c9 --- /dev/null +++ b/src/Akavache.Json.Tests/Fixtures/UserObject.cs @@ -0,0 +1,27 @@ +// 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.Tests; + +/// +/// A fixture of a user object. +/// +public class UserObject +{ + /// + /// Gets or sets the bio. + /// + public string Bio { get; set; } + + /// + /// Gets or sets the name. + /// + public string Name { get; set; } + + /// + /// Gets or sets the blog. + /// + public string Blog { get; set; } +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/Helpers/IntegrationTestHelper.cs b/src/Akavache.Json.Tests/Helpers/IntegrationTestHelper.cs new file mode 100644 index 000000000..aacf91440 --- /dev/null +++ b/src/Akavache.Json.Tests/Helpers/IntegrationTestHelper.cs @@ -0,0 +1,105 @@ +// 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. + +using System.Diagnostics; +using System.Globalization; +using System.Net; + +namespace Akavache.Tests; + +/// +/// Tests to help with the integration tests. +/// +public static class IntegrationTestHelper +{ + /// + /// Gets a single path combined from other paths. + /// + /// The paths to combine. + /// The combined path. + public static string GetPath(params string[] paths) + { + var ret = GetIntegrationTestRootDirectory(); + return new FileInfo(paths.Aggregate(ret, Path.Combine)).FullName; + } + + /// + /// Gets the root folder for the integration tests. + /// + /// The root folder. + public static string GetIntegrationTestRootDirectory() + { + // XXX: This is an evil hack, but it's okay for a unit test + // We can't use Assembly.Location because unit test runners love + // to move stuff to temp directories + var st = new StackFrame(true); + var di = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(st.GetFileName()))); + + return di.FullName; + } + + /// + /// Gets a response from a web service. + /// + /// The paths for the web service. + /// The response from the server. +#if NETFRAMEWORK + public static System.Net.Http.HttpResponseMessage GetResponse(params string[] paths) +#else + public static HttpResponseMessage GetResponse(params string[] paths) +#endif + { + var bytes = File.ReadAllBytes(GetPath(paths)); + + // Find the body + int bodyIndex; + for (bodyIndex = 0; bodyIndex < bytes.Length - 3; bodyIndex++) + { + if (bytes[bodyIndex] != 0x0D || bytes[bodyIndex + 1] != 0x0A || + bytes[bodyIndex + 2] != 0x0D || bytes[bodyIndex + 3] != 0x0A) + { + continue; + } + + goto foundIt; + } + + throw new InvalidOperationException("Couldn't find response body"); + + foundIt: + + var headerText = Encoding.UTF8.GetString(bytes, 0, bodyIndex); + var lines = headerText.Split('\n'); + var statusCode = (HttpStatusCode)int.Parse(lines[0].Split(' ')[1], CultureInfo.InvariantCulture); +#if NETFRAMEWORK + var ret = new System.Net.Http.HttpResponseMessage(statusCode) + { + Content = new System.Net.Http.ByteArrayContent(bytes, bodyIndex + 2, bytes.Length - bodyIndex - 2) + }; +#else + var ret = new HttpResponseMessage(statusCode) + { + Content = new ByteArrayContent(bytes, bodyIndex + 2, bytes.Length - bodyIndex - 2) + }; +#endif + + foreach (var line in lines.Skip(1)) + { + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + var separatorIndex = line.IndexOf(":", StringComparison.InvariantCulture); + var key = line[..separatorIndex]; + var val = line[(separatorIndex + 2)..].TrimEnd(); + + ret.Headers.TryAddWithoutValidation(key, val); + ret.Content.Headers.TryAddWithoutValidation(key, val); + } + + return ret; + } +} diff --git a/src/Akavache.Json.Tests/Helpers/Utility.cs b/src/Akavache.Json.Tests/Helpers/Utility.cs new file mode 100644 index 000000000..bc45ca70c --- /dev/null +++ b/src/Akavache.Json.Tests/Helpers/Utility.cs @@ -0,0 +1,79 @@ +// 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. + +using System.Reactive.Disposables; + +namespace Akavache.Tests; + +/// +/// A set of utility helper methods for use throughout tests. +/// +internal static class Utility +{ + /// + /// Deletes a directory. + /// + /// The path to delete. + public static void DeleteDirectory(string directoryPath) + { + // From http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true/329502#329502 + try + { + var di = new DirectoryInfo(directoryPath); + var files = di.EnumerateFiles(); + var dirs = di.EnumerateDirectories(); + + foreach (var file in files) + { + File.SetAttributes(file.FullName, FileAttributes.Normal); + new Action(() => file.Delete()).Retry(); + } + + foreach (var dir in dirs) + { + DeleteDirectory(dir.FullName); + } + + File.SetAttributes(directoryPath, FileAttributes.Normal); + Directory.Delete(directoryPath, false); + } + catch (Exception ex) + { + Console.Error.WriteLine("***** Failed to clean up!! *****"); + Console.Error.WriteLine(ex); + } + } + + public static IDisposable WithEmptyDirectory(out string directoryPath) + { + var di = new DirectoryInfo(Path.Combine(".", Guid.NewGuid().ToString())); + if (di.Exists) + { + DeleteDirectory(di.FullName); + } + + di.Create(); + + directoryPath = di.FullName; + return Disposable.Create(() => DeleteDirectory(di.FullName)); + } + + public static void Retry(this Action block, int retries = 2) + { + while (true) + { + try + { + block(); + return; + } + catch (Exception) when (retries != 0) + { + retries--; + Thread.Sleep(10); + } + } + } +} diff --git a/src/Akavache.Json.Tests/InMemoryBlobCacheBulkOperationsTests.cs b/src/Akavache.Json.Tests/InMemoryBlobCacheBulkOperationsTests.cs new file mode 100644 index 000000000..711650f8a --- /dev/null +++ b/src/Akavache.Json.Tests/InMemoryBlobCacheBulkOperationsTests.cs @@ -0,0 +1,17 @@ +// 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. + +using ReactiveUI; + +namespace Akavache.Tests; + +/// +/// Tests for operations associated with the class. +/// +public class InMemoryBlobCacheBulkOperationsTests : BulkOperationsTestBase +{ + /// + protected override IBlobCache CreateBlobCache(string path) => new BlockingDisposeBulkCache(new InMemoryBlobCache(RxApp.TaskpoolScheduler)); +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/InMemoryBlobCacheDateTimeTests.cs b/src/Akavache.Json.Tests/InMemoryBlobCacheDateTimeTests.cs new file mode 100644 index 000000000..e01dd1e9c --- /dev/null +++ b/src/Akavache.Json.Tests/InMemoryBlobCacheDateTimeTests.cs @@ -0,0 +1,17 @@ +// 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. + +using ReactiveUI; + +namespace Akavache.Tests; + +/// +/// Tests for DateTime operations associated with the class. +/// +public class InMemoryBlobCacheDateTimeTests : DateTimeTestBase +{ + /// + protected override IBlobCache CreateBlobCache(string path) => new BlockingDisposeBulkCache(new InMemoryBlobCache(RxApp.TaskpoolScheduler)); +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/InMemoryBlobCacheInterfaceTests.cs b/src/Akavache.Json.Tests/InMemoryBlobCacheInterfaceTests.cs new file mode 100644 index 000000000..dd0222acc --- /dev/null +++ b/src/Akavache.Json.Tests/InMemoryBlobCacheInterfaceTests.cs @@ -0,0 +1,17 @@ +// 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. + +using ReactiveUI; + +namespace Akavache.Tests; + +/// +/// Tests for the class. +/// +public class InMemoryBlobCacheInterfaceTests : BlobCacheInterfaceTestBase +{ + /// + protected override IBlobCache CreateBlobCache(string path) => new InMemoryBlobCache(RxApp.TaskpoolScheduler); +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/InMemoryBlobCacheObjectBulkOperationsTests.cs b/src/Akavache.Json.Tests/InMemoryBlobCacheObjectBulkOperationsTests.cs new file mode 100644 index 000000000..5efc01ae4 --- /dev/null +++ b/src/Akavache.Json.Tests/InMemoryBlobCacheObjectBulkOperationsTests.cs @@ -0,0 +1,17 @@ +// 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. + +using ReactiveUI; + +namespace Akavache.Tests; + +/// +/// Bulk operation tests associated with the class. +/// +public class InMemoryBlobCacheObjectBulkOperationsTests : ObjectBulkOperationsTestBase +{ + /// + protected override IBlobCache CreateBlobCache(string path) => new BlockingDisposeBulkCache(new InMemoryBlobCache(RxApp.TaskpoolScheduler)); +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/InMemoryBlobCacheTests.cs b/src/Akavache.Json.Tests/InMemoryBlobCacheTests.cs new file mode 100644 index 000000000..66c8ad450 --- /dev/null +++ b/src/Akavache.Json.Tests/InMemoryBlobCacheTests.cs @@ -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. + +using ReactiveUI; + +namespace Akavache.Tests; + +/// +/// Tests for the class. +/// +public class InMemoryBlobCacheTests : BlobCacheExtensionsTestBase +{ + /// + protected override IBlobCache CreateBlobCache(string path) + { + BlobCache.ApplicationName = "TestRunner"; + return new InMemoryBlobCache(RxApp.MainThreadScheduler); + } +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/Performance/PerfHelper.cs b/src/Akavache.Json.Tests/Performance/PerfHelper.cs new file mode 100644 index 000000000..e337570e0 --- /dev/null +++ b/src/Akavache.Json.Tests/Performance/PerfHelper.cs @@ -0,0 +1,88 @@ +// 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.Tests.Performance; + +/// +/// A set of classes related to handling performance testing. +/// +public static class PerfHelper +{ + private static readonly Random _randomNumberGenerator = new(); + + /// + /// Tests generating a database. + /// + /// The target blob cache. + /// The number of items to generate. + /// A list of generated items. + public static async Task> GenerateDatabase(IBlobCache targetCache, int size) + { + var ret = new List(); + + // Write out in groups of 4096 + while (size > 0) + { + var toWriteSize = Math.Min(4096, size); + var toWrite = GenerateRandomDatabaseContents(toWriteSize); + + await targetCache.Insert(toWrite); + + ret.AddRange(toWrite.Keys); + + size -= toWrite.Count; + Console.WriteLine(size); + } + + return ret; + } + + /// + /// Generate the contents of the database. + /// + /// The size of the database to write. + /// A dictionary of the contents. + public static Dictionary GenerateRandomDatabaseContents(int toWriteSize) => + Enumerable.Range(0, toWriteSize) + .Select(_ => GenerateRandomKey()) + .Distinct() + .ToDictionary(k => k, _ => GenerateRandomBytes()); + + /// + /// Generate random bytes for a value. + /// + /// The generated random bytes. + public static byte[] GenerateRandomBytes() + { + var ret = new byte[_randomNumberGenerator.Next(1, 256)]; + + _randomNumberGenerator.NextBytes(ret); + return ret; + } + + /// + /// Generates a random key for the database. + /// + /// The random key. + public static string GenerateRandomKey() + { + var bytes = GenerateRandomBytes(); + + // NB: Mask off the MSB and set bit 5 so we always end up with + // valid UTF-8 characters that aren't control characters + for (var i = 0; i < bytes.Length; i++) + { + bytes[i] = (byte)((bytes[i] & 0x7F) | 0x20); + } + + return Encoding.UTF8.GetString(bytes, 0, Math.Min(256, bytes.Length)); + } + + /// + /// Gets a series of size values to use in generating performance tests. + /// + /// The range of sizes. + public static IEnumerable GetPerfRanges() => new[] { 1, 10, 100, 1000, 10000, 100000, }; +} diff --git a/src/Akavache.Json.Tests/Performance/ReadTests.cs b/src/Akavache.Json.Tests/Performance/ReadTests.cs new file mode 100644 index 000000000..23583680b --- /dev/null +++ b/src/Akavache.Json.Tests/Performance/ReadTests.cs @@ -0,0 +1,151 @@ +// 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. + +using System.Diagnostics; + +namespace Akavache.Tests.Performance; + +/// +/// Performance read tests. +/// +public abstract class ReadTests +{ + private readonly Random _randomNumberGenerator = new(); + + /// + /// Tests the performance of sequential simple reads. + /// + /// A task to monitor the progress. + [Fact] + public Task SequentialSimpleReads() => + GeneratePerfRangesForBlock(async (cache, size, keys) => + { + var st = new Stopwatch(); + var toFetch = Enumerable.Range(0, size) + .Select(_ => keys[_randomNumberGenerator.Next(0, keys.Count - 1)]) + .ToArray(); + + st.Start(); + + foreach (var v in toFetch) + { + await cache.Get(v); + } + + st.Stop(); + return st.ElapsedMilliseconds; + }); + + /// + /// Tests the performance of sequential bulk reads. + /// + /// A task to monitor the progress. + [Fact] + public Task SequentialBulkReads() => + GeneratePerfRangesForBlock(async (cache, size, keys) => + { + var st = new Stopwatch(); + + var count = 0; + var toFetch = Enumerable.Range(0, size) + .Select(_ => keys[_randomNumberGenerator.Next(0, keys.Count - 1)]) + .GroupBy(_ => ++count / 32) + .ToArray(); + + st.Start(); + + foreach (var group in toFetch) + { + await cache.Get(group); + } + + st.Stop(); + return st.ElapsedMilliseconds; + }); + + /// + /// Tests the performance of parallel simple reads. + /// + /// A task to monitor the progress. + [Fact] + public Task ParallelSimpleReads() => + GeneratePerfRangesForBlock(async (cache, size, keys) => + { + var st = new Stopwatch(); + var toFetch = Enumerable.Range(0, size) + .Select(_ => keys[_randomNumberGenerator.Next(0, keys.Count - 1)]) + .ToArray(); + + st.Start(); + + await toFetch.ToObservable(BlobCache.TaskpoolScheduler) + .Select(x => Observable.Defer(() => cache.Get(x))) + .Merge(32) + .ToArray(); + + st.Stop(); + return st.ElapsedMilliseconds; + }); + + /// + /// Abstract method for generating the blob cache we want to test for. + /// + /// The path to the DB. + /// The created blob cache. + protected abstract IBlobCache CreateBlobCache(string path); + + /// + /// Generate performance block ranges for a block. + /// + /// The block to generate for. + /// A task to monitor the progress. + private async Task GeneratePerfRangesForBlock(Func, Task> block) + { + var results = new Dictionary(); + var dbName = default(string); + + using (Utility.WithEmptyDirectory(out var dirPath)) + using (var cache = await GenerateAGiantDatabase(dirPath).ConfigureAwait(false)) + { + var keys = await cache.GetAllKeys(); + dbName = cache.GetType().Name; + + foreach (var size in PerfHelper.GetPerfRanges()) + { + results[size] = await block(cache, size, keys.ToList()).ConfigureAwait(false); + } + } + + Console.WriteLine(dbName); + foreach (var kvp in results) + { + Console.WriteLine("{0}: {1}", kvp.Key, kvp.Value); + } + } + + /// + /// Generates a giant database. + /// + /// A path to use for generating it. + /// The blob cache. + private async Task GenerateAGiantDatabase(string path) + { + path ??= IntegrationTestHelper.GetIntegrationTestRootDirectory(); + + var giantDbSize = PerfHelper.GetPerfRanges().Last(); + var cache = CreateBlobCache(path); + + var keys = await cache.GetAllKeys(); + if (keys.Count() == giantDbSize) + { + return cache; + } + + await cache.InvalidateAll(); + await PerfHelper.GenerateDatabase(cache, giantDbSize).ConfigureAwait(false); + + return cache; + } +} diff --git a/src/Akavache.Json.Tests/Performance/Sqlite3ReadTests.cs b/src/Akavache.Json.Tests/Performance/Sqlite3ReadTests.cs new file mode 100644 index 000000000..13a74189f --- /dev/null +++ b/src/Akavache.Json.Tests/Performance/Sqlite3ReadTests.cs @@ -0,0 +1,17 @@ +// 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. + +using Akavache.Sqlite3; + +namespace Akavache.Tests.Performance; + +/// +/// Read tests for the class. +/// +public abstract class Sqlite3ReadTests : ReadTests +{ + /// + protected override IBlobCache CreateBlobCache(string path) => new SqlRawPersistentBlobCache(Path.Combine(path, "blob.db")); +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/Performance/Sqlite3WriteTests.cs b/src/Akavache.Json.Tests/Performance/Sqlite3WriteTests.cs new file mode 100644 index 000000000..9eba8d708 --- /dev/null +++ b/src/Akavache.Json.Tests/Performance/Sqlite3WriteTests.cs @@ -0,0 +1,17 @@ +// 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. + +using Akavache.Sqlite3; + +namespace Akavache.Tests.Performance; + +/// +/// Write performance tests for the class. +/// +public abstract class Sqlite3WriteTests : WriteTests +{ + /// + protected override IBlobCache CreateBlobCache(string path) => new SqlRawPersistentBlobCache(Path.Combine(path, "blob.db")); +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/Performance/WriteTests.cs b/src/Akavache.Json.Tests/Performance/WriteTests.cs new file mode 100644 index 000000000..0ee23e3c4 --- /dev/null +++ b/src/Akavache.Json.Tests/Performance/WriteTests.cs @@ -0,0 +1,107 @@ +// 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. + +using System.Diagnostics; + +namespace Akavache.Tests.Performance; + +/// +/// Performance write tests. +/// +public abstract class WriteTests +{ + /// + /// Do write tests for sequential simple reads. + /// + /// A task to monitor the progress. + [Fact] + public Task SequentialSimpleWrites() => + GeneratePerfRangesForBlock(async (cache, size) => + { + var toWrite = PerfHelper.GenerateRandomDatabaseContents(size); + + var st = new Stopwatch(); + st.Start(); + + foreach (var kvp in toWrite) + { + await cache.Insert(kvp.Key, kvp.Value); + } + + st.Stop(); + return st.ElapsedMilliseconds; + }); + + /// + /// Do write tests for sequential bulk writes. + /// + /// A task to monitor the progress. + [Fact] + public Task SequentialBulkWrites() => + GeneratePerfRangesForBlock(async (cache, size) => + { + var toWrite = PerfHelper.GenerateRandomDatabaseContents(size); + + var st = new Stopwatch(); + st.Start(); + + await cache.Insert(toWrite); + + st.Stop(); + return st.ElapsedMilliseconds; + }); + + /// + /// Do write tests for parallel simple writes. + /// + /// A task to monitor the progress. + [Fact] + public Task ParallelSimpleWrites() => + GeneratePerfRangesForBlock(async (cache, size) => + { + var toWrite = PerfHelper.GenerateRandomDatabaseContents(size); + + var st = new Stopwatch(); + st.Start(); + + await toWrite.ToObservable(BlobCache.TaskpoolScheduler) + .Select(x => Observable.Defer(() => cache.Insert(x.Key, x.Value))) + .Merge(32) + .ToArray(); + + st.Stop(); + return st.ElapsedMilliseconds; + }); + + /// + /// Generates the blob cache we want to do the performance tests against. + /// + /// The path to the cache. + /// The blob cache. + protected abstract IBlobCache CreateBlobCache(string path); + + private async Task GeneratePerfRangesForBlock(Func> block) + { + var results = new Dictionary(); + var dbName = default(string); + + using (Utility.WithEmptyDirectory(out var dirPath)) + using (var cache = CreateBlobCache(dirPath)) + { + dbName = cache.GetType().Name; + + foreach (var size in PerfHelper.GetPerfRanges()) + { + results[size] = await block(cache, size).ConfigureAwait(false); + } + } + + Console.WriteLine(dbName); + foreach (var kvp in results) + { + Console.WriteLine("{0}: {1}", kvp.Key, kvp.Value); + } + } +} diff --git a/src/Akavache.Json.Tests/SqliteBlobCacheBulkExtensionsTest.cs b/src/Akavache.Json.Tests/SqliteBlobCacheBulkExtensionsTest.cs new file mode 100644 index 000000000..036a931a5 --- /dev/null +++ b/src/Akavache.Json.Tests/SqliteBlobCacheBulkExtensionsTest.cs @@ -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. + +using Akavache.Sqlite3; + +namespace Akavache.Tests; + +/// +/// Tests for bulk wrapper. +/// +public class SqliteBlobCacheBulkExtensionsTest : BlobCacheExtensionsTestBase +{ + /// + protected override IBlobCache CreateBlobCache(string path) + { + BlobCache.ApplicationName = "TestRunner"; + return new BlockingDisposeBulkCache(new SqlRawPersistentBlobCache(Path.Combine(path, "sqlite.db"))); + } +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/SqliteBlobCacheBulkOperationsTests.cs b/src/Akavache.Json.Tests/SqliteBlobCacheBulkOperationsTests.cs new file mode 100644 index 000000000..7985862e9 --- /dev/null +++ b/src/Akavache.Json.Tests/SqliteBlobCacheBulkOperationsTests.cs @@ -0,0 +1,17 @@ +// 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. + +using Akavache.Sqlite3; + +namespace Akavache.Tests; + +/// +/// Bulk operation tests for the class. +/// +public class SqliteBlobCacheBulkOperationsTests : BulkOperationsTestBase +{ + /// + protected override IBlobCache CreateBlobCache(string path) => new BlockingDisposeBulkCache(new SqlRawPersistentBlobCache(Path.Combine(path, "sqlite.db"))); +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/SqliteBlobCacheDateTimeTests.cs b/src/Akavache.Json.Tests/SqliteBlobCacheDateTimeTests.cs new file mode 100644 index 000000000..2dc40a4e0 --- /dev/null +++ b/src/Akavache.Json.Tests/SqliteBlobCacheDateTimeTests.cs @@ -0,0 +1,17 @@ +// 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. + +using Akavache.Sqlite3; + +namespace Akavache.Tests; + +/// +/// DateTime operation tests for the class. +/// +public class SqliteBlobCacheDateTimeTests : BulkOperationsTestBase +{ + /// + protected override IBlobCache CreateBlobCache(string path) => new BlockingDisposeBulkCache(new SqlRawPersistentBlobCache(Path.Combine(path, "sqlite.db"))); +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/SqliteBlobCacheExtensionsTests.cs b/src/Akavache.Json.Tests/SqliteBlobCacheExtensionsTests.cs new file mode 100644 index 000000000..ea402b32f --- /dev/null +++ b/src/Akavache.Json.Tests/SqliteBlobCacheExtensionsTests.cs @@ -0,0 +1,55 @@ +// 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. + +using Akavache.Sqlite3; + +namespace Akavache.Tests; + +/// +/// Tests for the . +/// +public class SqliteBlobCacheExtensionsTests : BlobCacheExtensionsTestBase +{ + /// + /// Checks to make sure that vacuuming compacts the file size. + /// + [Fact] + public void VacuumCompactsDatabase() + { + using (Utility.WithEmptyDirectory(out var path)) + { + var dbPath = Path.Combine(path, "sqlite.db"); + + using (var fixture = new BlockingDisposeCache(CreateBlobCache(path))) + { + Assert.True(File.Exists(dbPath)); + + var buf = new byte[256 * 1024]; + var rnd = new Random(); + rnd.NextBytes(buf); + + fixture.Insert("dummy", buf).Wait(); + } + + var size = new FileInfo(dbPath).Length; + Assert.True(size > 0); + + using (var fixture = new BlockingDisposeCache(CreateBlobCache(path))) + { + fixture.InvalidateAll().Wait(); + fixture.Vacuum().Wait(); + } + + Assert.True(new FileInfo(dbPath).Length <= size); + } + } + + /// + protected override IBlobCache CreateBlobCache(string path) + { + BlobCache.ApplicationName = "TestRunner"; + return new BlockingDisposeObjectCache(new SqlRawPersistentBlobCache(Path.Combine(path, "sqlite.db"))); + } +} diff --git a/src/Akavache.Json.Tests/SqliteBlobCacheInterfaceTests.cs b/src/Akavache.Json.Tests/SqliteBlobCacheInterfaceTests.cs new file mode 100644 index 000000000..5871ce388 --- /dev/null +++ b/src/Akavache.Json.Tests/SqliteBlobCacheInterfaceTests.cs @@ -0,0 +1,17 @@ +// 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. + +using Akavache.Sqlite3; + +namespace Akavache.Tests; + +/// +/// Tests for the . +/// +public class SqliteBlobCacheInterfaceTests : BlobCacheInterfaceTestBase +{ + /// + protected override IBlobCache CreateBlobCache(string path) => new SqlRawPersistentBlobCache(Path.Combine(path, "sqlite.db")); +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/SqliteBlobCacheObjectBulkOperationsTests.cs b/src/Akavache.Json.Tests/SqliteBlobCacheObjectBulkOperationsTests.cs new file mode 100644 index 000000000..a30f63ace --- /dev/null +++ b/src/Akavache.Json.Tests/SqliteBlobCacheObjectBulkOperationsTests.cs @@ -0,0 +1,17 @@ +// 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. + +using Akavache.Sqlite3; + +namespace Akavache.Tests; + +/// +/// Object bulk operation tests for the class. +/// +public class SqliteBlobCacheObjectBulkOperationsTests : ObjectBulkOperationsTestBase +{ + /// + protected override IBlobCache CreateBlobCache(string path) => new BlockingDisposeBulkCache(new SqlRawPersistentBlobCache(Path.Combine(path, "sqlite.db"))); +} \ No newline at end of file diff --git a/src/Akavache.Json.Tests/TestBases/BlobCacheExtensionsTestBase.cs b/src/Akavache.Json.Tests/TestBases/BlobCacheExtensionsTestBase.cs new file mode 100644 index 000000000..0acfede53 --- /dev/null +++ b/src/Akavache.Json.Tests/TestBases/BlobCacheExtensionsTestBase.cs @@ -0,0 +1,803 @@ +// 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. + +using DynamicData; + +using FluentAssertions; + +using Microsoft.Reactive.Testing; + +using ReactiveUI.Testing; +using Splat; + +namespace Akavache.Tests; + +/// +/// Fixture for testing the blob cache extension methods. +/// +public abstract class BlobCacheExtensionsTestBase +{ + /// + /// Tests to make sure the download url extension methods download correctly. + /// + /// A task to monitor the progress. + [Fact] + public async Task DownloadUrlTest() + { + using (Utility.WithEmptyDirectory(out var path)) + using (var fixture = CreateBlobCache(path)) + { + var bytes = await fixture.DownloadUrl("http://httpbin.org/html").FirstAsync(); + bytes.Length.Should().BeGreaterThan(0); + } + } + + /// + /// Tests to make sure the download Uri extension method overload performs correctly. + /// + /// A task to monitor the progress. + [Fact] + public async Task DownloadUriTest() + { + using (Utility.WithEmptyDirectory(out var path)) + using (var fixture = CreateBlobCache(path)) + { + var bytes = await fixture.DownloadUrl(new Uri("http://httpbin.org/html")).FirstAsync(); + bytes.Length.Should().BeGreaterThan(0); + } + } + + /// + /// Tests to make sure the download with key extension method overload performs correctly. + /// + /// A task to monitor the progress. + [Fact] + public async Task DownloadUrlWithKeyTest() + { + using (Utility.WithEmptyDirectory(out var path)) + using (var fixture = CreateBlobCache(path)) + { + var key = Guid.NewGuid().ToString(); + await fixture.DownloadUrl(key, "http://httpbin.org/html").FirstAsync(); + var bytes = await fixture.Get(key); + bytes.Length.Should().BeGreaterThan(0); + } + } + + /// + /// Tests to make sure the download Uri with key extension method overload performs correctly. + /// + /// A task to monitor the progress. + [Fact] + public async Task DownloadUriWithKeyTest() + { + using (Utility.WithEmptyDirectory(out var path)) + using (var fixture = CreateBlobCache(path)) + { + var key = Guid.NewGuid().ToString(); + await fixture.DownloadUrl(key, new Uri("https://httpbin.org/html")).FirstAsync(); + var bytes = await fixture.Get(key); + bytes.Length.Should().BeGreaterThan(0); + } + } + + /// + /// Tests to make sure that getting non-existent keys throws an exception. + /// + /// A task to monitor the progress. + [Fact] + public async Task GettingNonExistentKeyShouldThrow() + { + using (Utility.WithEmptyDirectory(out var path)) + using (var fixture = CreateBlobCache(path)) + { + Exception thrown = null; + try + { + var result = await fixture.GetObject("WEIFJWPIEFJ") + .Timeout(TimeSpan.FromSeconds(3)); + } + catch (Exception ex) + { + thrown = ex; + } + + Assert.True(thrown.GetType() == typeof(KeyNotFoundException)); + } + } + + /// + /// Makes sure that objects can be written and read. + /// + /// A task to monitor the progress. + [Fact] + public async Task ObjectsShouldBeRoundtrippable() + { + var input = new UserObject { Bio = "A totally cool cat!", Name = "octocat", Blog = "http://www.github.com" }; + UserObject result; + + using (Utility.WithEmptyDirectory(out var path)) + { + using (var fixture = CreateBlobCache(path)) + { + if (fixture is InMemoryBlobCache) + { + return; + } + + await fixture.InsertObject("key", input).FirstAsync(); + } + + using (var fixture = CreateBlobCache(path)) + { + result = await fixture.GetObject("key").FirstAsync(); + } + } + + Assert.Equal(input.Blog, result.Blog); + Assert.Equal(input.Bio, result.Bio); + Assert.Equal(input.Name, result.Name); + } + + /// + /// Makes sure that arrays can be written and read. + /// + /// A task to monitor the progress. + [Fact] + public async Task ArraysShouldBeRoundtrippable() + { + var input = new[] { new UserObject { Bio = "A totally cool cat!", Name = "octocat", Blog = "http://www.github.com" }, new UserObject { Bio = "zzz", Name = "sleepy", Blog = "http://example.com" } }; + UserObject[] result; + + using (Utility.WithEmptyDirectory(out var path)) + { + using (var fixture = CreateBlobCache(path)) + { + if (fixture is InMemoryBlobCache) + { + return; + } + + await fixture.InsertObject("key", input).FirstAsync(); + } + + using (var fixture = CreateBlobCache(path)) + { + result = await fixture.GetObject("key").FirstAsync(); + } + } + + Assert.Equal(input[0].Blog, result[0].Blog); + Assert.Equal(input[0].Bio, result[0].Bio); + Assert.Equal(input[0].Name, result[0].Name); + Assert.Equal(input.Last().Blog, result.Last().Blog); + Assert.Equal(input.Last().Bio, result.Last().Bio); + Assert.Equal(input.Last().Name, result.Last().Name); + } + + /// + /// Makes sure that the objects can be created using the object factory. + /// + /// A task to monitor the progress. + [Fact] + public async Task ObjectsCanBeCreatedUsingObjectFactory() + { + var input = new UserModel(new()) { Age = 123, Name = "Old" }; + UserModel result; + + using (Utility.WithEmptyDirectory(out var path)) + { + using (var fixture = CreateBlobCache(path)) + { + if (fixture is InMemoryBlobCache) + { + return; + } + + await fixture.InsertObject("key", input).FirstAsync(); + } + + using (var fixture = CreateBlobCache(path)) + { + result = await fixture.GetObject("key").FirstAsync(); + } + } + + Assert.Equal(input.Age, result.Age); + Assert.Equal(input.Name, result.Name); + } + + /// + /// Makes sure that arrays can be written and read and using the object factory. + /// + /// A task to monitor the progress. + [Fact] + public async Task ArraysShouldBeRoundtrippableUsingObjectFactory() + { + var input = new[] { new UserModel(new()) { Age = 123, Name = "Old" }, new UserModel(new()) { Age = 123, Name = "Old" } }; + UserModel[] result; + using (Utility.WithEmptyDirectory(out var path)) + { + using (var fixture = CreateBlobCache(path)) + { + if (fixture is InMemoryBlobCache) + { + return; + } + + await fixture.InsertObject("key", input).FirstAsync(); + } + + using (var fixture = CreateBlobCache(path)) + { + result = await fixture.GetObject("key").FirstAsync(); + } + } + + Assert.Equal(input[0].Age, result[0].Age); + Assert.Equal(input[0].Name, result[0].Name); + Assert.Equal(input.Last().Age, result.Last().Age); + Assert.Equal(input.Last().Name, result.Last().Name); + } + + /// + /// Make sure that the fetch functions are called only once for the get or fetch object methods. + /// + /// A task to monitor the progress. + [SkippableFact] + public async Task FetchFunctionShouldBeCalledOnceForGetOrFetchObject() + { + // TODO: This test is failing on .NET 6.0. Investigate. + Skip.If(GetType().Assembly.GetTargetFrameworkName().StartsWith("net")); + + var fetchCount = 0; + var fetcher = new Func>>(() => + { + fetchCount++; + return Observable.Return(new Tuple("Foo", "Bar")); + }); + + using (Utility.WithEmptyDirectory(out var path)) + { + using (var fixture = CreateBlobCache(path)) + { + var result = await fixture.GetOrFetchObject("Test", fetcher).ObserveOn(ImmediateScheduler.Instance).FirstAsync(); + Assert.Equal("Foo", result.Item1); + Assert.Equal("Bar", result.Item2); + Assert.Equal(1, fetchCount); + + // 2nd time around, we should be grabbing from cache + result = await fixture.GetOrFetchObject("Test", fetcher).ObserveOn(ImmediateScheduler.Instance).FirstAsync(); + Assert.Equal("Foo", result.Item1); + Assert.Equal("Bar", result.Item2); + Assert.Equal(1, fetchCount); + + // Testing persistence makes zero sense for InMemoryBlobCache + if (fixture is InMemoryBlobCache) + { + return; + } + } + + using (var fixture = CreateBlobCache(path)) + { + var result = await fixture.GetOrFetchObject("Test", fetcher).ObserveOn(ImmediateScheduler.Instance).FirstAsync(); + Assert.Equal("Foo", result.Item1); + Assert.Equal("Bar", result.Item2); + Assert.Equal(1, fetchCount); + } + } + } + + /// + /// Makes sure the fetch function debounces current requests. + /// + [SkippableFact] + public void FetchFunctionShouldDebounceConcurrentRequests() => + new TestScheduler().With(sched => + { + // TODO: TestScheduler tests aren't gonna work with new SQLite. + Skip.If(GetType().Assembly.GetTargetFrameworkName().StartsWith("net")); + using (Utility.WithEmptyDirectory(out var path)) + { + var callCount = 0; + var fetcher = new Func>(() => + { + callCount++; + return Observable.Return(42).Delay(TimeSpan.FromMilliseconds(1000), sched); + }); + + var fixture = CreateBlobCache(path); + try + { + fixture.GetOrFetchObject("foo", fetcher).ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var result1).Subscribe(); + + Assert.Equal(0, result1.Count); + + sched.AdvanceToMs(250); + + // Nobody's returned yet, cache is empty, we should have called the fetcher + // once to get a result + fixture.GetOrFetchObject("foo", fetcher).ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var result2).Subscribe(); + Assert.Equal(0, result1.Count); + Assert.Equal(0, result2.Count); + Assert.Equal(1, callCount); + + sched.AdvanceToMs(750); + + // Same as above, result1-3 are all listening to the same fetch + fixture.GetOrFetchObject("foo", fetcher).ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var result3).Subscribe(); + Assert.Equal(0, result1.Count); + Assert.Equal(0, result2.Count); + Assert.Equal(0, result3.Count); + Assert.Equal(1, callCount); + + // Fetch returned, all three collections should have an item + sched.AdvanceToMs(1250); + Assert.Equal(1, result1.Count); + Assert.Equal(1, result2.Count); + Assert.Equal(1, result3.Count); + Assert.Equal(1, callCount); + + // Making a new call, but the cache has an item, this shouldn't result + // in a fetcher call either + fixture.GetOrFetchObject("foo", fetcher).ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var result4).Subscribe(); + sched.AdvanceToMs(2500); + Assert.Equal(1, result1.Count); + Assert.Equal(1, result2.Count); + Assert.Equal(1, result3.Count); + Assert.Equal(1, result4.Count); + Assert.Equal(1, callCount); + + // Making a new call, but with a new key - this *does* result in a fetcher + // call. Result1-4 shouldn't get any new items, and at t=3000, we haven't + // returned from the call made at t=2500 yet + fixture.GetOrFetchObject("bar", fetcher).ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var result5).Subscribe(); + sched.AdvanceToMs(3000); + Assert.Equal(1, result1.Count); + Assert.Equal(1, result2.Count); + Assert.Equal(1, result3.Count); + Assert.Equal(1, result4.Count); + Assert.Equal(0, result5.Count); + Assert.Equal(2, callCount); + + // Everything is done, we should have one item in result5 now + sched.AdvanceToMs(4000); + Assert.Equal(1, result1.Count); + Assert.Equal(1, result2.Count); + Assert.Equal(1, result3.Count); + Assert.Equal(1, result4.Count); + Assert.Equal(1, result5.Count); + Assert.Equal(2, callCount); + } + finally + { + // Since we're in TestScheduler, we can't use the normal + // using statement, we need to kick off the async dispose, + // then start the scheduler to let it run + fixture.Dispose(); + sched.Start(); + } + } + }); + + /// + /// Makes sure that the fetch function propogates thrown exceptions. + /// + /// A task to monitor the progress. + [Fact] + public async Task FetchFunctionShouldPropagateThrownExceptionAsObservableException() + { + var fetcher = new Func>>(() => throw new InvalidOperationException()); + + using (Utility.WithEmptyDirectory(out var path)) + using (var fixture = CreateBlobCache(path)) + { + var result = await fixture.GetOrFetchObject("Test", fetcher) + .Catch(Observable.Return(new Tuple("one", "two"))).FirstAsync(); + Assert.Equal("one", result.Item1); + Assert.Equal("two", result.Item2); + } + } + + /// + /// Makes sure that the fetch function propogates thrown exceptions. + /// + /// A task to monitor the progress. + [Fact] + public async Task FetchFunctionShouldPropagateObservedExceptionAsObservableException() + { + var fetcher = new Func>>(() => + Observable.Throw>(new InvalidOperationException())); + + using (Utility.WithEmptyDirectory(out var path)) + { + var fixture = CreateBlobCache(path); + using (fixture) + { + var result = await fixture.GetOrFetchObject("Test", fetcher) + .Catch(Observable.Return(new Tuple("one", "two"))).FirstAsync(); + Assert.Equal("one", result.Item1); + Assert.Equal("two", result.Item2); + } + } + } + + /// + /// Make sure that the GetOrFetch function respects expirations. + /// + [SkippableFact] + public void GetOrFetchShouldRespectExpiration() => + new TestScheduler().With(sched => + { + // TODO: TestScheduler tests aren't gonna work with new SQLite. + Skip.If(GetType().Assembly.GetTargetFrameworkName().StartsWith("net")); + using (Utility.WithEmptyDirectory(out var path)) + { + var fixture = CreateBlobCache(path); + using (fixture) + { + var result = default(string); + fixture.GetOrFetchObject( + "foo", + () => Observable.Return("bar"), + sched.Now + TimeSpan.FromMilliseconds(1000)) + .Subscribe(x => result = x); + + sched.AdvanceByMs(250); + Assert.Equal("bar", result); + + fixture.GetOrFetchObject( + "foo", + () => Observable.Return("baz"), + sched.Now + TimeSpan.FromMilliseconds(1000)) + .Subscribe(x => result = x); + + sched.AdvanceByMs(250); + Assert.Equal("bar", result); + + sched.AdvanceByMs(1000); + fixture.GetOrFetchObject( + "foo", + () => Observable.Return("baz"), + sched.Now + TimeSpan.FromMilliseconds(1000)) + .Subscribe(x => result = x); + + sched.AdvanceByMs(250); + Assert.Equal("baz", result); + } + } + }); + + /// + /// Makes sure that the GetAndFetchLatest invalidates objects on errors. + /// + /// A task to monitor the progress. + [Fact] + public async Task GetAndFetchLatestShouldInvalidateObjectOnError() + { + var fetcher = new Func>(() => Observable.Throw(new InvalidOperationException())); + + using (Utility.WithEmptyDirectory(out var path)) + { + var fixture = CreateBlobCache(path); + + using (fixture) + { + if (fixture is InMemoryBlobCache) + { + return; + } + + await fixture.InsertObject("foo", "bar").FirstAsync(); + + await fixture.GetAndFetchLatest("foo", fetcher, shouldInvalidateOnError: true) + .Catch(Observable.Return("get and fetch latest error")) + .ToList() + .FirstAsync(); + + var result = await fixture.GetObject("foo") + .Catch(Observable.Return("get error")) + .FirstAsync(); + + Assert.Equal("get error", result); + } + } + } + + /// + /// Makes sure that the GetAndFetchLatest calls the Fetch predicate. + /// + /// A task to monitor the progress. + [Fact] + public async Task GetAndFetchLatestCallsFetchPredicate() + { + var fetchPredicateCalled = false; + + bool FetchPredicate(DateTimeOffset _) + { + fetchPredicateCalled = true; + + return true; + } + + var fetcher = new Func>(() => Observable.Return("baz")); + + using (Utility.WithEmptyDirectory(out var path)) + { + var fixture = CreateBlobCache(path); + + using (fixture) + { + if (fixture is InMemoryBlobCache) + { + return; + } + + await fixture.InsertObject("foo", "bar").FirstAsync(); + + await fixture.GetAndFetchLatest("foo", fetcher, FetchPredicate).LastAsync(); + + Assert.True(fetchPredicateCalled); + } + } + } + + /// + /// Make sure that the GetAndFetchLatest method validates items already in the cache. + /// + /// A task to monitor the progress. + [Fact] + public async Task GetAndFetchLatestValidatesItemsToBeCached() + { + const string key = "tv1"; + var items = new List { 4, 7, 10, 11, 3, 4 }; + var fetcher = new Func>>(() => Observable.Return((List)null)); + + using (Utility.WithEmptyDirectory(out var path)) + { + var fixture = CreateBlobCache(path); + + using (fixture) + { + if (fixture is InMemoryBlobCache) + { + return; + } + + // GetAndFetchLatest will overwrite cache with null result + await fixture.InsertObject(key, items); + + await fixture.GetAndFetchLatest(key, fetcher).LastAsync(); + + var failedResult = await fixture.GetObject>(key).FirstAsync(); + + Assert.Null(failedResult); + + // GetAndFetchLatest skips cache invalidation/storage due to cache validation predicate. + await fixture.InsertObject(key, items); + + await fixture.GetAndFetchLatest(key, fetcher, cacheValidationPredicate: i => i?.Count > 0).LastAsync(); + + var result = await fixture.GetObject>(key).FirstAsync(); + + Assert.NotNull(result); + Assert.True(result.Count > 0, "The returned list is empty."); + Assert.Equal(items, result); + } + } + } + + /// + /// Tests to make sure that different key types work correctly. + /// + /// A task to monitor the progress. + [Fact] + public async Task KeysByTypeTest() + { + var input = new[] + { + "Foo", + "Bar", + "Baz" + }; + + var inputItems = input.Select(x => new UserObject { Name = x, Bio = "A thing", }).ToArray(); + var fixture = default(IBlobCache); + + using (Utility.WithEmptyDirectory(out var path)) + using (fixture = CreateBlobCache(path)) + { + foreach (var item in input.Zip(inputItems, (key, value) => new { Key = key, Value = value })) + { + fixture.InsertObject(item.Key, item.Value).Wait(); + } + + var allObjectsCount = await fixture.GetAllObjects().Select(x => x.Count()).FirstAsync(); + Assert.Equal(input.Length, (await fixture.GetAllKeys().FirstAsync()).Count()); + Assert.Equal(input.Length, allObjectsCount); + + fixture.InsertObject("Quux", new UserModel(null)).Wait(); + + allObjectsCount = await fixture.GetAllObjects().Select(x => x.Count()).FirstAsync(); + Assert.Equal(input.Length + 1, (await fixture.GetAllKeys().FirstAsync()).Count()); + Assert.Equal(input.Length, allObjectsCount); + + fixture.InvalidateObject("Foo").Wait(); + + allObjectsCount = await fixture.GetAllObjects().Select(x => x.Count()).FirstAsync(); + Assert.Equal(input.Length + 1 - 1, (await fixture.GetAllKeys().FirstAsync()).Count()); + Assert.Equal(input.Length - 1, allObjectsCount); + + fixture.InvalidateAllObjects().Wait(); + + allObjectsCount = await fixture.GetAllObjects().Select(x => x.Count()).FirstAsync(); + Assert.Equal(1, (await fixture.GetAllKeys().FirstAsync()).Count()); + Assert.Equal(0, allObjectsCount); + } + } + + /// + /// Tests to make sure that different key types work correctly. + /// + /// A task to monitor the progress. + [Fact] + public async Task KeysByTypeBulkTest() + { + var input = new[] + { + "Foo", + "Bar", + "Baz" + }; + + var inputItems = input.Select(x => new UserObject { Name = x, Bio = "A thing", }).ToArray(); + var fixture = default(IBlobCache); + + using (Utility.WithEmptyDirectory(out var path)) + using (fixture = CreateBlobCache(path)) + { + fixture.InsertObjects(input.Zip(inputItems, (key, value) => new { Key = key, Value = value }).ToDictionary(x => x.Key, x => x.Value)).Wait(); + + var allObjectsCount = await fixture.GetAllObjects().Select(x => x.Count()).FirstAsync(); + Assert.Equal(input.Length, (await fixture.GetAllKeys().FirstAsync()).Count()); + Assert.Equal(input.Length, allObjectsCount); + + fixture.InsertObject("Quux", new UserModel(null)).Wait(); + + allObjectsCount = await fixture.GetAllObjects().Select(x => x.Count()).FirstAsync(); + Assert.Equal(input.Length + 1, (await fixture.GetAllKeys().FirstAsync()).Count()); + Assert.Equal(input.Length, allObjectsCount); + + fixture.InvalidateObject("Foo").Wait(); + + allObjectsCount = await fixture.GetAllObjects().Select(x => x.Count()).FirstAsync(); + Assert.Equal(input.Length + 1 - 1, (await fixture.GetAllKeys().FirstAsync()).Count()); + Assert.Equal(input.Length - 1, allObjectsCount); + + fixture.InvalidateAllObjects().Wait(); + + allObjectsCount = await fixture.GetAllObjects().Select(x => x.Count()).FirstAsync(); + Assert.Equal(1, (await fixture.GetAllKeys().FirstAsync()).Count()); + Assert.Equal(0, allObjectsCount); + } + } + + /// + /// Tests to make sure that different key types work correctly. + /// + /// A task to monitor the progress. + [Fact] + public async Task CreatedAtTimeAccurate() + { + var input = new[] + { + "Foo", + "Bar", + "Baz" + }; + + var now = DateTimeOffset.Now.AddSeconds(-30); + + var inputItems = input.Select(x => new UserObject { Name = x, Bio = "A thing", }).ToArray(); + var fixture = default(IBlobCache); + + using (Utility.WithEmptyDirectory(out var path)) + using (fixture = CreateBlobCache(path)) + { + fixture.InsertObjects(input.Zip(inputItems, (key, value) => new { Key = key, Value = value }).ToDictionary(x => x.Key, x => x.Value)).Wait(); + var keyDates = await fixture.GetCreatedAt(input); + + Assert.Equal(keyDates.Keys.OrderBy(x => x), input.OrderBy(x => x)); + keyDates.Values.All(x => x > now).Should().BeTrue(); + } + } + + /// + /// Tests to make sure getting all keys works correctly. + /// + /// A task to monitor the progress. + [Fact] + public async Task GetAllKeysSmokeTest() + { + using (Utility.WithEmptyDirectory(out var path)) + { + IBlobCache fixture; + using (fixture = CreateBlobCache(path)) + { + await Observable.Merge( + fixture.InsertObject("Foo", "bar"), + fixture.InsertObject("Bar", 10), + fixture.InsertObject("Baz", new UserObject { Bio = "Bio", Blog = "Blog", Name = "Name" })) + .LastAsync(); + + var keys = await fixture.GetAllKeys().FirstAsync(); + Assert.Equal(3, keys.Count()); + Assert.True(keys.Any(x => x.Contains("Foo"))); + Assert.True(keys.Any(x => x.Contains("Bar"))); + } + + if (fixture is InMemoryBlobCache) + { + return; + } + + using (fixture = CreateBlobCache(path)) + { + var keys = await fixture.GetAllKeys().FirstAsync(); + Assert.Equal(3, keys.Count()); + Assert.True(keys.Any(x => x.Contains("Foo"))); + Assert.True(keys.Any(x => x.Contains("Bar"))); + } + } + } + + /// + /// Tests to make sure getting all keys works correctly. + /// + /// A task to monitor the progress. + [Fact] + public async Task GetAllKeysBulkSmokeTest() + { + using (Utility.WithEmptyDirectory(out var path)) + { + IBlobCache fixture; + using (fixture = CreateBlobCache(path)) + { + await fixture.InsertObjects(new Dictionary + { + ["Foo"] = "bar", + ["Bar"] = 10, + ["Baz"] = new UserObject { Bio = "Bio", Blog = "Blog", Name = "Name" } + }); + + var keys = await fixture.GetAllKeys().FirstAsync(); + Assert.Equal(3, keys.Count()); + Assert.True(keys.Any(x => x.Contains("Foo"))); + Assert.True(keys.Any(x => x.Contains("Bar"))); + } + + if (fixture is InMemoryBlobCache) + { + return; + } + + using (fixture = CreateBlobCache(path)) + { + var keys = await fixture.GetAllKeys().FirstAsync(); + Assert.Equal(3, keys.Count()); + Assert.True(keys.Any(x => x.Contains("Foo"))); + Assert.True(keys.Any(x => x.Contains("Bar"))); + } + } + } + + /// + /// Gets the we want to do the tests against. + /// + /// The path to the blob cache. + /// The blob cache for testing. + protected abstract IBlobCache CreateBlobCache(string path); +} diff --git a/src/Akavache.Json.Tests/TestBases/BlobCacheInterfaceTestBase.cs b/src/Akavache.Json.Tests/TestBases/BlobCacheInterfaceTestBase.cs new file mode 100644 index 000000000..6e85063a2 --- /dev/null +++ b/src/Akavache.Json.Tests/TestBases/BlobCacheInterfaceTestBase.cs @@ -0,0 +1,355 @@ +// 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. + +using System.Reactive.Threading.Tasks; +using Microsoft.Reactive.Testing; +using ReactiveUI.Testing; +using Splat; + +namespace Akavache.Tests; + +/// +/// A fixture for testing the blob cache interfaces. +/// +public abstract class BlobCacheInterfaceTestBase +{ + /// + /// Tests that the cache can get or insert blobs. + /// + /// A task to monitor the progress. + [Fact] + public async Task CacheShouldBeAbleToGetAndInsertBlobs() + { + using (Utility.WithEmptyDirectory(out var path)) + using (var fixture = CreateBlobCache(path)) + { + await fixture.Insert("Foo", [1, 2, 3]); + await fixture.Insert("Bar", [4, 5, 6]); + + await Assert.ThrowsAsync(async () => await fixture.Insert(null, [7, 8, 9]).FirstAsync()); + + var output1 = await fixture.Get("Foo"); + var output2 = await fixture.Get("Bar"); + + await Assert.ThrowsAsync(async () => + await fixture.Get(null).FirstAsync()); + + await Assert.ThrowsAsync(async () => + await fixture.Get("Baz").FirstAsync()); + + Assert.Equal(3, output1.Length); + Assert.Equal(3, output2.Length); + + Assert.Equal(1, output1[0]); + Assert.Equal(4, output2[0]); + } + } + + /// + /// Tests to make sure that cache's can be written then read. + /// + /// A task to monitor the progress. + [Fact] + public async Task CacheShouldBeRoundtrippable() + { + using (Utility.WithEmptyDirectory(out var path)) + { + var fixture = CreateBlobCache(path); + using (fixture) + { + // InMemoryBlobCache isn't round-trippable by design + if (fixture is InMemoryBlobCache) + { + return; + } + + await fixture.Insert("Foo", [1, 2, 3]); + } + + fixture.Shutdown.Wait(); + + using (var fixture2 = CreateBlobCache(path)) + { + var output = await fixture2.Get("Foo").FirstAsync(); + Assert.Equal(3, output.Length); + Assert.Equal(1, output[0]); + } + } + } + + /// + /// Checks to make sure that the property CreatedAt is populated and can be retrieved. + /// + public void CreatedAtShouldBeSetAutomaticallyAndBeRetrievable() + { + using (Utility.WithEmptyDirectory(out var path)) + { + var fixture = CreateBlobCache(path); + DateTimeOffset roughCreationTime; + using (fixture) + { + fixture.Insert("Foo", [1, 2, 3]).Wait(); + roughCreationTime = fixture.Scheduler.Now; + } + + fixture.Shutdown.Wait(); + + using (var fixture2 = CreateBlobCache(path)) + { + var createdAt = fixture2.GetCreatedAt("Foo").Wait(); + + Assert.InRange( + actual: createdAt.Value, + low: roughCreationTime - TimeSpan.FromSeconds(1), + high: roughCreationTime); + } + } + } + + /// + /// Tests to make sure that inserting an item twice only allows getting of the first item. + /// + /// A task to monitor the progress. + [Fact] + public async Task InsertingAnItemTwiceShouldAlwaysGetTheNewOne() + { + using (Utility.WithEmptyDirectory(out var path)) + using (var fixture = CreateBlobCache(path)) + { + fixture.Insert("Foo", [1, 2, 3]).Wait(); + + var output = await fixture.Get("Foo").FirstAsync(); + Assert.Equal(3, output.Length); + Assert.Equal(1, output[0]); + + fixture.Insert("Foo", [4, 5]).Wait(); + + output = await fixture.Get("Foo").FirstAsync(); + Assert.Equal(2, output.Length); + Assert.Equal(4, output[0]); + } + } + + /// + /// Checks to make sure that the cache respects expiration dates. + /// + [SkippableFact] + public void CacheShouldRespectExpiration() + { + // TODO: TestScheduler tests aren't gonna work with new SQLite. + Skip.If(GetType().Assembly.GetTargetFrameworkName().StartsWith("net")); + using (Utility.WithEmptyDirectory(out var path)) + { + new TestScheduler().With(sched => + { + bool wasTestCache; + + using (var fixture = CreateBlobCache(path)) + { + wasTestCache = fixture is InMemoryBlobCache; + fixture.Insert("foo", [1, 2, 3], TimeSpan.FromMilliseconds(100)); + fixture.Insert("bar", [4, 5, 6], TimeSpan.FromMilliseconds(500)); + + byte[] result = null; + sched.AdvanceToMs(20); + fixture.Get("foo").Subscribe(x => result = x); + + // Foo should still be active + sched.AdvanceToMs(50); + Assert.Equal(1, result[0]); + + // From 100 < t < 500, foo should be inactive but bar should still work + var shouldFail = true; + sched.AdvanceToMs(120); + fixture.Get("foo").Subscribe( + x => result = x, + _ => shouldFail = false); + fixture.Get("bar").Subscribe(x => result = x); + + sched.AdvanceToMs(300); + Assert.False(shouldFail); + Assert.Equal(4, result[0]); + } + + // NB: InMemoryBlobCache is not serializable by design + if (wasTestCache) + { + return; + } + + sched.AdvanceToMs(350); + sched.AdvanceToMs(351); + sched.AdvanceToMs(352); + + // Serialize out the cache and reify it again + using (var fixture = CreateBlobCache(path)) + { + byte[] result = null; + fixture.Get("bar").Subscribe(x => result = x); + sched.AdvanceToMs(400); + + Assert.Equal(4, result[0]); + + // At t=1000, everything is invalidated + var shouldFail = true; + sched.AdvanceToMs(1000); + fixture.Get("bar").Subscribe( + x => result = x, + _ => shouldFail = false); + + sched.AdvanceToMs(1010); + Assert.False(shouldFail); + } + + sched.Start(); + }); + } + } + + /// + /// Tests to make sure that InvalidateAll invalidates everything. + /// + /// A task to monitor the progress. + [Fact] + public async Task InvalidateAllReallyDoesInvalidateEverything() + { + using (Utility.WithEmptyDirectory(out var path)) + { + using (var fixture = CreateBlobCache(path)) + { + await fixture.Insert("Foo", [1, 2, 3]).FirstAsync(); + await fixture.Insert("Bar", [4, 5, 6]).FirstAsync(); + await fixture.Insert("Bamf", [7, 8, 9]).FirstAsync(); + + Assert.NotEqual(0, (await fixture.GetAllKeys().FirstAsync()).Count()); + + await fixture.InvalidateAll().FirstAsync(); + + Assert.Equal(0, (await fixture.GetAllKeys().FirstAsync()).Count()); + } + + using (var fixture = CreateBlobCache(path)) + { + Assert.Equal(0, (await fixture.GetAllKeys().FirstAsync()).Count()); + } + } + } + + /// + /// Tests to make sure that GetsAllKeys does not return expired keys. + /// + /// A task to monitor the progress. + [Fact] + public async Task GetAllKeysShouldntReturnExpiredKeys() + { + using (Utility.WithEmptyDirectory(out var path)) + { + using (var fixture = CreateBlobCache(path)) + { + var inThePast = BlobCache.TaskpoolScheduler.Now - TimeSpan.FromDays(1.0); + + await fixture.Insert("Foo", [1, 2, 3], inThePast).FirstAsync(); + await fixture.Insert("Bar", [4, 5, 6], inThePast).FirstAsync(); + await fixture.Insert("Bamf", [7, 8, 9]).FirstAsync(); + + Assert.Equal(1, (await fixture.GetAllKeys().FirstAsync()).Count()); + } + + using (var fixture = CreateBlobCache(path)) + { + if (fixture is InMemoryBlobCache) + { + return; + } + + Assert.Equal(1, (await fixture.GetAllKeys().FirstAsync()).Count()); + } + } + } + + /// + /// Make sure that the Vacuum method does not purge keys that should be there. + /// + /// A task to monitor the progress. + [Fact] + public async Task VacuumDoesntPurgeKeysThatShouldBeThere() + { + using (Utility.WithEmptyDirectory(out var path)) + { + using (var fixture = CreateBlobCache(path)) + { + var inThePast = BlobCache.TaskpoolScheduler.Now - TimeSpan.FromDays(1.0); + var inTheFuture = BlobCache.TaskpoolScheduler.Now + TimeSpan.FromDays(1.0); + + await fixture.Insert("Foo", [1, 2, 3], inThePast).FirstAsync(); + await fixture.Insert("Bar", [4, 5, 6], inThePast).FirstAsync(); + await fixture.Insert("Bamf", [7, 8, 9]).FirstAsync(); + await fixture.Insert("Baz", [7, 8, 9], inTheFuture).FirstAsync(); + + try + { + await fixture.Vacuum().FirstAsync(); + } + catch (NotImplementedException) + { + // NB: The old and busted cache will never have this, + // just make the test pass + } + + Assert.Equal(2, (await fixture.GetAllKeys().FirstAsync()).Count()); + } + + using (var fixture = CreateBlobCache(path)) + { + if (fixture is InMemoryBlobCache) + { + return; + } + + Assert.Equal(2, (await fixture.GetAllKeys().FirstAsync()).Count()); + } + } + } + + /// + /// Make sure that the Vacuum method purges entries that are expired. + /// + /// A task to monitor the progress. + [Fact] + public async Task VacuumPurgeEntriesThatAreExpired() + { + using (Utility.WithEmptyDirectory(out var path)) + using (var fixture = CreateBlobCache(path)) + { + var inThePast = BlobCache.TaskpoolScheduler.Now - TimeSpan.FromDays(1.0); + var inTheFuture = BlobCache.TaskpoolScheduler.Now + TimeSpan.FromDays(1.0); + + await fixture.Insert("Foo", [1, 2, 3], inThePast).FirstAsync(); + await fixture.Insert("Bar", [4, 5, 6], inThePast).FirstAsync(); + await fixture.Insert("Bamf", [7, 8, 9]).FirstAsync(); + await fixture.Insert("Baz", [7, 8, 9], inTheFuture).FirstAsync(); + + try + { + await fixture.Vacuum().FirstAsync(); + } + catch (NotImplementedException) + { + // NB: The old and busted cache will never have this, + // just make the test pass + } + + await Assert.ThrowsAsync(() => fixture.Get("Foo").FirstAsync().ToTask()); + await Assert.ThrowsAsync(() => fixture.Get("Bar").FirstAsync().ToTask()); + } + } + + /// + /// Gets the we want to do the tests against. + /// + /// The path to the blob cache. + /// The blob cache for testing. + protected abstract IBlobCache CreateBlobCache(string path); +} diff --git a/src/Akavache.Json.Tests/TestBases/BulkOperationsTestBase.cs b/src/Akavache.Json.Tests/TestBases/BulkOperationsTestBase.cs new file mode 100644 index 000000000..7e7abec1e --- /dev/null +++ b/src/Akavache.Json.Tests/TestBases/BulkOperationsTestBase.cs @@ -0,0 +1,111 @@ +// 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.Tests; + +/// +/// A base class for tests about bulk operations. +/// +public abstract class BulkOperationsTestBase +{ + /// + /// Tests if Get with multiple keys work correctly. + /// + /// A task to monitor the progress. + [Fact] + public async Task GetShouldWorkWithMultipleKeys() + { + using (Utility.WithEmptyDirectory(out var path)) + using (var fixture = CreateBlobCache(path)) + { + var data = new byte[] { 0x10, 0x20, 0x30, }; + var keys = new[] { "Foo", "Bar", "Baz", }; + + await Task.WhenAll(keys.Select(async v => await fixture.Insert(v, data).FirstAsync())).ConfigureAwait(false); + + Assert.Equal(keys.Length, (await fixture.GetAllKeys().FirstAsync()).Count()); + + var allData = await fixture.Get(keys).FirstAsync(); + + Assert.Equal(keys.Length, allData.Count); + Assert.True(allData.All(x => x.Value[0] == data[0] && x.Value[1] == data[1])); + } + } + + /// + /// Tests to make sure that Get invalidates all the old keys. + /// + /// A task to monitor the progress. + [Fact] + public async Task GetShouldInvalidateOldKeys() + { + using (Utility.WithEmptyDirectory(out var path)) + using (var fixture = CreateBlobCache(path)) + { + var data = new byte[] { 0x10, 0x20, 0x30, }; + var keys = new[] { "Foo", "Bar", "Baz", }; + + await Task.WhenAll(keys.Select(async v => await fixture.Insert(v, data, DateTimeOffset.MinValue).FirstAsync())).ConfigureAwait(false); + + var allData = await fixture.Get(keys).FirstAsync(); + Assert.Equal(0, allData.Count); + Assert.Equal(0, (await fixture.GetAllKeys().FirstAsync()).Count()); + } + } + + /// + /// Tests to make sure that insert works with multiple keys. + /// + /// A task to monitor the progress. + [Fact] + public async Task InsertShouldWorkWithMultipleKeys() + { + using (Utility.WithEmptyDirectory(out var path)) + using (var fixture = CreateBlobCache(path)) + { + var data = new byte[] { 0x10, 0x20, 0x30, }; + var keys = new[] { "Foo", "Bar", "Baz", }; + + await fixture.Insert(keys.ToDictionary(k => k, v => data)).FirstAsync(); + + Assert.Equal(keys.Length, (await fixture.GetAllKeys().FirstAsync()).Count()); + + var allData = await fixture.Get(keys).FirstAsync(); + + Assert.Equal(keys.Length, allData.Count); + Assert.True(allData.All(x => x.Value[0] == data[0] && x.Value[1] == data[1])); + } + } + + /// + /// Invalidate should be able to trash multiple keys. + /// + /// A task to monitor the progress. + [Fact] + public async Task InvalidateShouldTrashMultipleKeys() + { + using (Utility.WithEmptyDirectory(out var path)) + using (var fixture = CreateBlobCache(path)) + { + var data = new byte[] { 0x10, 0x20, 0x30, }; + var keys = new[] { "Foo", "Bar", "Baz", }; + + await Task.WhenAll(keys.Select(async v => await fixture.Insert(v, data).FirstAsync())).ConfigureAwait(false); + + Assert.Equal(keys.Length, (await fixture.GetAllKeys().FirstAsync()).Count()); + + await fixture.Invalidate(keys).FirstAsync(); + + Assert.Equal(0, (await fixture.GetAllKeys().FirstAsync()).Count()); + } + } + + /// + /// Gets the we want to do the tests against. + /// + /// The path to the blob cache. + /// The blob cache for testing. + protected abstract IBlobCache CreateBlobCache(string path); +} diff --git a/src/Akavache.Json.Tests/TestBases/DateTimeTestBase.cs b/src/Akavache.Json.Tests/TestBases/DateTimeTestBase.cs new file mode 100644 index 000000000..4be528b22 --- /dev/null +++ b/src/Akavache.Json.Tests/TestBases/DateTimeTestBase.cs @@ -0,0 +1,161 @@ +// 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.Tests; + +/// +/// Tests associated with the DateTime and DateTimeOffset. +/// +public abstract class DateTimeTestBase +{ + /// + /// Gets the date time offsets used in theory tests. + /// + public static IEnumerable DateTimeOffsetData => new[] + { + new object[] { new TestObjectDateTimeOffset { Timestamp = TestNowOffset, TimestampNullable = null } }, + [new TestObjectDateTimeOffset { Timestamp = TestNowOffset, TimestampNullable = TestNowOffset }], + }; + + /// + /// Gets the DateTime used in theory tests. + /// + public static IEnumerable DateTimeData => new[] + { + new object[] { new TestObjectDateTime { Timestamp = TestNow, TimestampNullable = null } }, + [new TestObjectDateTime { Timestamp = TestNow, TimestampNullable = TestNow }], + }; + + /// + /// Gets the DateTime used in theory tests. + /// + public static IEnumerable DateLocalTimeData => new[] + { + new object[] { new TestObjectDateTime { Timestamp = LocalTestNow, TimestampNullable = null } }, + [new TestObjectDateTime { Timestamp = LocalTestNow, TimestampNullable = LocalTestNow }], + }; + + /// + /// Gets the date time when the tests are done to keep them consistent. + /// + private static DateTime TestNow { get; } = DateTime.Now; + + /// + /// Gets the date time when the tests are done to keep them consistent. + /// + private static DateTime LocalTestNow { get; } = TimeZoneInfo.ConvertTimeFromUtc(TestNow.ToUniversalTime(), TimeZoneInfo.CreateCustomTimeZone("testTimeZone", TimeSpan.FromHours(6), "Test Time Zone", "Test Time Zone")); + + /// + /// Gets the date time off set when the tests are done to keep them consistent. + /// + private static DateTimeOffset TestNowOffset { get; } = DateTimeOffset.Now; + + /// + /// Makes sure that the DateTimeOffset are serialized correctly. + /// + /// The data in the theory. + /// A task to monitor the progress. + [Theory] + [MemberData(nameof(DateTimeOffsetData))] + public async Task GetOrFetchAsyncDateTimeOffsetShouldBeEqualEveryTime(TestObjectDateTimeOffset data) + { + using (Utility.WithEmptyDirectory(out var path)) + using (var blobCache = CreateBlobCache(path)) + { + var (firstResult, secondResult) = await PerformTimeStampGrab(blobCache, data); + Assert.Equal(firstResult.Timestamp, secondResult.Timestamp); + Assert.Equal(firstResult.Timestamp.UtcTicks, secondResult.Timestamp.UtcTicks); + Assert.Equal(firstResult.Timestamp.Offset, secondResult.Timestamp.Offset); + Assert.Equal(firstResult.Timestamp.Ticks, secondResult.Timestamp.Ticks); + Assert.Equal(firstResult.TimestampNullable, secondResult.TimestampNullable); + } + } + + /// + /// Makes sure that the DateTime are serialized correctly. + /// + /// The data in the theory. + /// A task to monitor the progress. + [Theory(Skip = "Serialiser Conversion needs completion")] + [MemberData(nameof(DateTimeData))] + public async Task GetOrFetchAsyncDateTimeShouldBeEqualEveryTime(TestObjectDateTime data) + { + using (Utility.WithEmptyDirectory(out var path)) + using (var blobCache = CreateBlobCache(path)) + { + var (firstResult, secondResult) = await PerformTimeStampGrab(blobCache, data); + Assert.Equal(secondResult.Timestamp.Kind, DateTimeKind.Utc); + Assert.Equal(firstResult.Timestamp.ToUniversalTime(), secondResult.Timestamp.ToUniversalTime()); + Assert.Equal(firstResult.TimestampNullable?.ToUniversalTime(), secondResult.TimestampNullable?.ToUniversalTime()); + } + } + + /// + /// Makes sure that the DateTime are serialized correctly. + /// + /// The data in the theory. + /// A task to monitor the progress. + [Theory(Skip = "Serialiser Conversion needs completion")] + [MemberData(nameof(DateLocalTimeData))] + public async Task GetOrFetchAsyncDateTimeWithForcedLocal(TestObjectDateTime data) + { + using (Utility.WithEmptyDirectory(out var path)) + using (var blobCache = CreateBlobCache(path)) + { + blobCache.ForcedDateTimeKind = DateTimeKind.Local; + var (firstResult, secondResult) = await PerformTimeStampGrab(blobCache, data); + Assert.Equal(secondResult.Timestamp.Kind, DateTimeKind.Local); + Assert.Equal(firstResult.Timestamp, secondResult.Timestamp); + Assert.Equal(firstResult.Timestamp.ToUniversalTime(), secondResult.Timestamp.ToUniversalTime()); + Assert.Equal(firstResult.TimestampNullable?.ToUniversalTime(), secondResult.TimestampNullable?.ToUniversalTime()); + BlobCache.ForcedDateTimeKind = null; + } + } + + /// + /// Tests to make sure that we can force the DateTime kind. + /// + /// A task to monitor the progress. + [Fact] + public async Task DateTimeKindCanBeForced() + { + using (Utility.WithEmptyDirectory(out var path)) + using (var fixture = CreateBlobCache(path)) + { + fixture.ForcedDateTimeKind = DateTimeKind.Utc; + + var value = DateTime.UtcNow; + await fixture.InsertObject("key", value).FirstAsync(); + var result = await fixture.GetObject("key").FirstAsync(); + Assert.Equal(DateTimeKind.Utc, result.Kind); + } + } + + /// + /// Gets the we want to do the tests against. + /// + /// The path to the blob cache. + /// The blob cache for testing. + protected abstract IBlobCache CreateBlobCache(string path); + + /// + /// Performs the actual time stamp grab. + /// + /// The type of data we are grabbing. + /// The blob cache to perform the operation against. + /// The data to grab. + /// A task with the data found. + private static async Task<(TData First, TData Second)> PerformTimeStampGrab(IBlobCache blobCache, TData data) + { + const string key = "key"; + + Task FetchFunction() => Task.FromResult(data); + + var firstResult = await blobCache.GetOrFetchObject(key, FetchFunction); + var secondResult = await blobCache.GetOrFetchObject(key, FetchFunction); + + return (firstResult, secondResult); + } +} diff --git a/src/Akavache.Json.Tests/TestBases/ObjectBulkOperationsTestBase.cs b/src/Akavache.Json.Tests/TestBases/ObjectBulkOperationsTestBase.cs new file mode 100644 index 000000000..8d9ebda32 --- /dev/null +++ b/src/Akavache.Json.Tests/TestBases/ObjectBulkOperationsTestBase.cs @@ -0,0 +1,111 @@ +// 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.Tests; + +/// +/// Base class for tests associated with object based bulk operations. +/// +public abstract class ObjectBulkOperationsTestBase +{ + /// + /// Tests to make sure that Get works with multiple key types. + /// + /// A task to monitor the progress. + [Fact] + public async Task GetShouldWorkWithMultipleKeys() + { + using (Utility.WithEmptyDirectory(out var path)) + using (var fixture = CreateBlobCache(path)) + { + var data = Tuple.Create("Foo", 4); + var keys = new[] { "Foo", "Bar", "Baz", }; + + await Task.WhenAll(keys.Select(async v => await fixture.InsertObject(v, data).FirstAsync())); + + Assert.Equal(keys.Length, (await fixture.GetAllKeys().FirstAsync()).Count()); + + var allData = await fixture.GetObjects>(keys).FirstAsync(); + + Assert.Equal(keys.Length, allData.Count); + Assert.True(allData.All(x => x.Value.Item1 == data.Item1 && x.Value.Item2 == data.Item2)); + } + } + + /// + /// Tests to make sure that Get works with multiple key types. + /// + /// A task to monitor the progress. + [Fact] + public async Task GetShouldInvalidateOldKeys() + { + using (Utility.WithEmptyDirectory(out var path)) + using (var fixture = CreateBlobCache(path)) + { + var data = Tuple.Create("Foo", 4); + var keys = new[] { "Foo", "Bar", "Baz", }; + + await Task.WhenAll(keys.Select(async v => await fixture.InsertObject(v, data, DateTimeOffset.MinValue).FirstAsync())); + + var allData = await fixture.GetObjects>(keys).FirstAsync(); + Assert.Equal(0, allData.Count); + Assert.Equal(0, (await fixture.GetAllKeys().FirstAsync()).Count()); + } + } + + /// + /// Tests to make sure that insert works with multiple keys. + /// + /// A task to monitor the progress. + [Fact] + public async Task InsertShouldWorkWithMultipleKeys() + { + using (Utility.WithEmptyDirectory(out var path)) + using (var fixture = CreateBlobCache(path)) + { + var data = Tuple.Create("Foo", 4); + var keys = new[] { "Foo", "Bar", "Baz", }; + + await fixture.InsertObjects(keys.ToDictionary(k => k, _ => data)).FirstAsync(); + + Assert.Equal(keys.Length, (await fixture.GetAllKeys().FirstAsync()).Count()); + + var allData = await fixture.GetObjects>(keys).FirstAsync(); + + Assert.Equal(keys.Length, allData.Count); + Assert.True(allData.All(x => x.Value.Item1 == data.Item1 && x.Value.Item2 == data.Item2)); + } + } + + /// + /// Invalidate should be able to trash multiple keys. + /// + /// A task to monitor the progress. + [Fact] + public async Task InvalidateShouldTrashMultipleKeys() + { + using (Utility.WithEmptyDirectory(out var path)) + using (var fixture = CreateBlobCache(path)) + { + var data = Tuple.Create("Foo", 4); + var keys = new[] { "Foo", "Bar", "Baz", }; + + await Task.WhenAll(keys.Select(async v => await fixture.InsertObject(v, data).FirstAsync())); + + Assert.Equal(keys.Length, (await fixture.GetAllKeys().FirstAsync()).Count()); + + await fixture.InvalidateObjects>(keys).FirstAsync(); + + Assert.Equal(0, (await fixture.GetAllKeys().FirstAsync()).Count()); + } + } + + /// + /// Gets the we want to do the tests against. + /// + /// The path to the blob cache. + /// The blob cache for testing. + protected abstract IBlobCache CreateBlobCache(string path); +} diff --git a/src/Akavache.Json.Tests/UtilityTests.cs b/src/Akavache.Json.Tests/UtilityTests.cs new file mode 100644 index 000000000..f10b3fc34 --- /dev/null +++ b/src/Akavache.Json.Tests/UtilityTests.cs @@ -0,0 +1,143 @@ +// 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. + +using System.Runtime.InteropServices; + +namespace Akavache.Tests; + +/// +/// Tests associated with our utilities. +/// +public class UtilityTests +{ + /// + /// Tests to make sure that create directories work. + /// + [Fact] + public void DirectoryCreateCreatesDirectories() + { + using (Utility.WithEmptyDirectory(out var path)) + { + var dir = new DirectoryInfo(Path.Combine(path, @"foo\bar\baz")); + dir.CreateRecursive(); + Assert.True(dir.Exists); + } + } + + /// + /// Gets to make sure we get exceptions on invalid network paths. + /// + [Fact] + public void DirectoryCreateThrowsIOExceptionForNonexistentNetworkPaths() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + + var exception = Assert.Throws(() => new DirectoryInfo(@"\\does\not\exist").CreateRecursive()); + Assert.StartsWith("The network path was not found", exception.Message); + } + + /// + /// Test to make sure we can split absolute paths. + /// + [Fact] + public void UtilitySplitsAbsolutePaths() + { + string path; + string expectedRoot; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + path = @"c:\foo\bar"; + expectedRoot = @"c:\"; + } + else + { + path = "/foo/bar"; + expectedRoot = "/"; + } + + Assert.Equal(new[] { expectedRoot, "foo", "bar" }, new DirectoryInfo(path).SplitFullPath()); + } + + /// + /// Tests to make sure we can resolve and split relative paths. + /// + [Fact] + public void UtilityResolvesAndSplitsRelativePaths() + { + var path = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"foo\bar" : "foo/bar"; + + var components = new DirectoryInfo(path).SplitFullPath().ToList(); + Assert.True(components.Count > 2); + Assert.Equal(new[] { "foo", "bar" }, components.Skip(components.Count - 2)); + } + + /// + /// Tests to make sure we can split on UNC paths. + /// + [Fact] + public void UtilitySplitsUncPaths() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + + Assert.Equal(new[] { @"\\foo\bar", "baz" }, new DirectoryInfo(@"\\foo\bar\baz").SplitFullPath()); + } + + /// + /// Test to make sure the operation queue shuts down. + /// + [Fact] + public void KeyedOperationQueueCorrectlyShutsDown() + { + var fixture = new KeyedOperationQueue(); + var op1 = new Subject(); + var op2 = new Subject(); + var op3 = new Subject(); + var isCompleted = false; + + int op1Result = 0, op2Result = 0, op3Result = 0; + + fixture.EnqueueObservableOperation("foo", () => op1).Subscribe(x => op1Result = x); + fixture.EnqueueObservableOperation("bar", () => op2).Subscribe(x => op2Result = x); + + // Shut down the queue, shouldn't be completed until op1 and op2 complete + fixture.ShutdownQueue().Subscribe(_ => isCompleted = true); + Assert.False(isCompleted); + + op1.OnNext(1); + op1.OnCompleted(); + Assert.False(isCompleted); + Assert.Equal(1, op1Result); + + op2.OnNext(2); + op2.OnCompleted(); + Assert.True(isCompleted); + Assert.Equal(2, op2Result); + + // We've already shut down, new ops should be ignored + fixture.EnqueueObservableOperation("foo", () => op3).Subscribe(x => op3Result = x); + op3.OnNext(3); + op3.OnCompleted(); + Assert.Equal(0, op3Result); + } + + /// + /// Test to make sure the static caches are initialized. + /// + [Fact] + public void ShouldInitializeDefaultCaches() + { + Assert.NotNull(BlobCache.LocalMachine); + Assert.NotNull(BlobCache.UserAccount); + Assert.NotNull(BlobCache.InMemory); + Assert.NotNull(BlobCache.Secure); + } +} diff --git a/src/Akavache.Json.Tests/xunit.runner.json b/src/Akavache.Json.Tests/xunit.runner.json new file mode 100644 index 000000000..c394df5a5 --- /dev/null +++ b/src/Akavache.Json.Tests/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "shadowCopy": false +} + diff --git a/src/Akavache.Json/SystemJsonSerializer.cs b/src/Akavache.Json/SystemJsonSerializer.cs index e76db7876..0a66660b2 100644 --- a/src/Akavache.Json/SystemJsonSerializer.cs +++ b/src/Akavache.Json/SystemJsonSerializer.cs @@ -4,6 +4,8 @@ // See the LICENSE file in the project root for full license information. using System.Text.Json; +using System.Text.Json.Serialization; +using Splat; namespace Akavache.Json; @@ -11,7 +13,7 @@ namespace Akavache.Json; /// SystemJsonSerializer. /// /// -public class SystemJsonSerializer : ISerializer +public class SystemJsonSerializer : ISerializer, IEnableLogger { /// /// Initializes a new instance of the class. @@ -44,33 +46,6 @@ public void CreateSerializer(Func getJsonDateTimeCont // TODO: Implement this } - /// - /// Deserializes from bytes. - /// - /// The type to deserialize to. - /// The bytes. - /// - /// The type. - /// - public T? Deserialize(byte[] bytes) => (T?)JsonSerializer.Deserialize(bytes, typeof(T), Options); - /// - /// Deserializes the object. - /// - /// The type. - /// The x. - /// - /// An Observable of T. - /// - public IObservable DeserializeObject(byte[] x) => throw new NotImplementedException(); - /// - /// Deserializes the object wrapper. - /// - /// The type. - /// The data. - /// - /// A value of T. - /// - public T DeserializeObjectWrapper(byte[] data) => throw new NotImplementedException(); /// /// Serializes to an bytes. /// @@ -79,7 +54,14 @@ public void CreateSerializer(Func getJsonDateTimeCont /// /// The bytes. /// - public byte[] Serialize(T item) => JsonSerializer.SerializeToUtf8Bytes(item, Options); + public byte[] Serialize(T item) + { + using var ms = new MemoryStream(); + using var writer = new Utf8JsonWriter(ms); + JsonSerializer.Serialize(writer, new ObjectWrapper(item)); + return ms.ToArray(); + } + /// /// Serializes the object. /// @@ -88,14 +70,74 @@ public void CreateSerializer(Func getJsonDateTimeCont /// /// The bytes. /// - public byte[] SerializeObject(T value) => throw new NotImplementedException(); + public byte[] SerializeObject(T value) => JsonSerializer.SerializeToUtf8Bytes(value, Options); + /// - /// Serializes the object wrapper. + /// Deserializes from bytes. + /// + /// The type to deserialize to. + /// The bytes. + /// + /// The type. + /// + public T? Deserialize(byte[] bytes) + { +#pragma warning disable CS8603 // Possible null reference return. + + ////var options = new JsonReaderOptions + ////{ + //// AllowTrailingCommas = true, + //// CommentHandling = JsonCommentHandling.Skip + ////}; + ////ReadOnlySpan jsonReadOnlySpan = bytes; + ////var reader = new Utf8JsonReader(jsonReadOnlySpan, options); + + var forcedDateTimeKind = BlobCache.ForcedDateTimeKind; + + ////if (forcedDateTimeKind.HasValue) + ////{ + //// reader.DateTimeKindHandling = forcedDateTimeKind.Value; + ////} + + try + { + var wrapper = JsonSerializer.Deserialize>(bytes); + + 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 JsonSerializer.Deserialize(bytes); +#pragma warning restore CS8603 // Possible null reference return. + } + + /// + /// Deserializes the object. /// /// The type. - /// The value. + /// The x. /// - /// A byte array. + /// An Observable of T. /// - public byte[] SerializeObjectWrapper(T value) => throw new NotImplementedException(); + public IObservable DeserializeObject(byte[] x) + { + if (x is null) + { + throw new ArgumentNullException(nameof(x)); + } + + try + { + var bytes = Encoding.UTF8.GetString(x, 0, x.Length); + var ret = JsonSerializer.Deserialize(bytes, Options); + return Observable.Return(ret); + } + catch (Exception ex) + { + return Observable.Throw(ex); + } + } } diff --git a/src/Akavache.NewtonsoftJson/NewtonsoftSerializer.cs b/src/Akavache.NewtonsoftJson/NewtonsoftSerializer.cs index 98ccc8217..80d3ac733 100644 --- a/src/Akavache.NewtonsoftJson/NewtonsoftSerializer.cs +++ b/src/Akavache.NewtonsoftJson/NewtonsoftSerializer.cs @@ -57,6 +57,31 @@ public void CreateSerializer(Func getJsonDateTimeCont } } + /// + public byte[] Serialize(T item) + { + if (_serializer is null) + { + throw new InvalidOperationException("You must call CreateSerializer before serializing"); + } + + using var ms = new MemoryStream(); + using var writer = new BsonDataWriter(ms); + _serializer.Serialize(writer, new ObjectWrapper(item)); + return ms.ToArray(); + } + + /// + /// Serializes the object. + /// + /// The type. + /// The value. + /// + /// The bytes. + /// + public byte[] SerializeObject(T value) => + Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value, Options)); + /// public T? Deserialize(byte[] bytes) { @@ -111,87 +136,4 @@ public void CreateSerializer(Func getJsonDateTimeCont return Observable.Throw(ex); } } - - /// - public byte[] Serialize(T item) - { - if (_serializer is null) - { - throw new InvalidOperationException("You must call CreateSerializer before serializing"); - } - - using var ms = new MemoryStream(); - using var writer = new BsonDataWriter(ms); - _serializer.Serialize(writer, new ObjectWrapper(item)); - return ms.ToArray(); - } - - /// - /// Serializes the object. - /// - /// The type. - /// The value. - /// - /// The bytes. - /// - public byte[] SerializeObject(T value) => - Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value, Options)); - - /// - /// Serializes the object wrapper. - /// - /// The type. - /// The value. - /// A byte array. - /// You must call CreateSerializer before serializing. - public byte[] SerializeObjectWrapper(T value) - { - if (_serializer is null) - { - throw new InvalidOperationException("You must call CreateSerializer before serializing"); - } - - using var ms = new MemoryStream(); - using var writer = new BsonDataWriter(ms); - _serializer.Serialize(writer, new ObjectWrapper(value)); - return ms.ToArray(); - } - - /// - /// Deserializes the object wrapper. - /// - /// The type. - /// The data. - /// A value of T. - /// You must call CreateSerializer before deserializing. - public T DeserializeObjectWrapper(byte[] data) - { - if (_serializer is null) - { - throw new InvalidOperationException("You must call CreateSerializer before deserializing"); - } - -#pragma warning disable CS8603 // Possible null reference return. - using var reader = new BsonDataReader(new MemoryStream(data)); - var forcedDateTimeKind = BlobCache.ForcedDateTimeKind; - - if (forcedDateTimeKind.HasValue) - { - reader.DateTimeKindHandling = forcedDateTimeKind.Value; - } - - try - { - var wrapper = _serializer.Deserialize>(reader); - - 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(reader); -#pragma warning restore CS8603 // Possible null reference return. - } } diff --git a/src/Akavache.Sqlite3/Properties/AssemblyInfo.cs b/src/Akavache.Sqlite3/Properties/AssemblyInfo.cs index 5f67cc443..638b1a3bf 100644 --- a/src/Akavache.Sqlite3/Properties/AssemblyInfo.cs +++ b/src/Akavache.Sqlite3/Properties/AssemblyInfo.cs @@ -6,3 +6,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Akavache.Tests")] +[assembly: InternalsVisibleTo("Akavache.Json.Tests")] diff --git a/src/Akavache.Tests/API/ApiApprovalTests.AkavacheCore.DotNet6_0.verified.txt b/src/Akavache.Tests/API/ApiApprovalTests.AkavacheCore.DotNet6_0.verified.txt index b8b8b4821..e9f123956 100644 --- a/src/Akavache.Tests/API/ApiApprovalTests.AkavacheCore.DotNet6_0.verified.txt +++ b/src/Akavache.Tests/API/ApiApprovalTests.AkavacheCore.DotNet6_0.verified.txt @@ -1,6 +1,7 @@ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Drawing")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Json")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Json.Tests")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Mobile")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.NewtonsoftJson")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Sqlite3")] @@ -163,10 +164,8 @@ namespace Akavache void CreateSerializer(System.Func getJsonDateTimeContractResolver); T? Deserialize(byte[] bytes); System.IObservable DeserializeObject(byte[] x); - T DeserializeObjectWrapper(byte[] data); byte[] Serialize(T item); byte[] SerializeObject(T value); - byte[] SerializeObjectWrapper(T value); } public class InMemoryBlobCache : Akavache.IBlobCache, Akavache.IObjectBlobCache, Akavache.ISecureBlobCache, Splat.IEnableLogger, System.IDisposable { diff --git a/src/Akavache.Tests/API/ApiApprovalTests.AkavacheCore.Net4_8.verified.txt b/src/Akavache.Tests/API/ApiApprovalTests.AkavacheCore.Net4_8.verified.txt index f60b84dbb..069214fc9 100644 --- a/src/Akavache.Tests/API/ApiApprovalTests.AkavacheCore.Net4_8.verified.txt +++ b/src/Akavache.Tests/API/ApiApprovalTests.AkavacheCore.Net4_8.verified.txt @@ -1,6 +1,7 @@ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Drawing")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Json")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Json.Tests")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Mobile")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.NewtonsoftJson")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Akavache.Sqlite3")] @@ -163,10 +164,8 @@ namespace Akavache void CreateSerializer(System.Func getJsonDateTimeContractResolver); T? Deserialize(byte[] bytes); System.IObservable DeserializeObject(byte[] x); - T DeserializeObjectWrapper(byte[] data); byte[] Serialize(T item); byte[] SerializeObject(T value); - byte[] SerializeObjectWrapper(T value); } public class InMemoryBlobCache : Akavache.IBlobCache, Akavache.IObjectBlobCache, Akavache.ISecureBlobCache, Splat.IEnableLogger, System.IDisposable { diff --git a/src/Akavache.sln b/src/Akavache.sln index e93ae965e..a5889c9e3 100644 --- a/src/Akavache.sln +++ b/src/Akavache.sln @@ -30,6 +30,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akavache.Json", "Akavache.J EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akavache.NewtonsoftJson", "Akavache.NewtonsoftJson\Akavache.NewtonsoftJson.csproj", "{44E8D94A-E680-46C6-97CE-4854BC049BD1}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akavache.Json.Tests", "Akavache.Json.Tests\Akavache.Json.Tests.csproj", "{04737766-EFB7-4652-94F0-2AC8D509371D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -68,6 +70,10 @@ Global {44E8D94A-E680-46C6-97CE-4854BC049BD1}.Debug|Any CPU.Build.0 = Debug|Any CPU {44E8D94A-E680-46C6-97CE-4854BC049BD1}.Release|Any CPU.ActiveCfg = Release|Any CPU {44E8D94A-E680-46C6-97CE-4854BC049BD1}.Release|Any CPU.Build.0 = Release|Any CPU + {04737766-EFB7-4652-94F0-2AC8D509371D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04737766-EFB7-4652-94F0-2AC8D509371D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04737766-EFB7-4652-94F0-2AC8D509371D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04737766-EFB7-4652-94F0-2AC8D509371D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 1abacb903793a43ec3404a2696d5ba339757c88c Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Wed, 3 Jan 2024 21:47:03 +0000 Subject: [PATCH 3/3] Update to use ThrowArgumentNullExceptionIfNull Encryption needs to be resolved or removed for --- src/Akavache.Core/Internal/ExceptionMixins.cs | 18 ++++ .../Json/JsonSerializationMixin.cs | 9 +- .../Platforms/shared/AkavacheHttpMixin.cs | 86 +++---------------- .../Platforms/shared/Registrations.cs | 9 +- .../Platforms/shared/StreamMixins.cs | 11 +-- src/Akavache.Drawing/Registrations.cs | 5 +- src/Akavache.Json/Registrations.cs | 14 +-- src/Akavache.Json/SystemJsonSerializer.cs | 5 +- src/Akavache.Mobile/Registrations.cs | 17 ++-- .../JsonDateTimeOffsetTickConverter.cs | 22 ++--- .../JsonDateTimeTickConverter.cs | 7 +- .../NewtonsoftSerializer.cs | 19 +--- src/Akavache.NewtonsoftJson/Registrations.cs | 15 +--- .../SqlLiteCache/ObjectWrapper.cs | 5 +- .../SqlLiteCache/SQLiteEncryptedBlobCache.cs | 18 ++-- 15 files changed, 66 insertions(+), 194 deletions(-) create mode 100644 src/Akavache.Core/Internal/ExceptionMixins.cs diff --git a/src/Akavache.Core/Internal/ExceptionMixins.cs b/src/Akavache.Core/Internal/ExceptionMixins.cs new file mode 100644 index 000000000..5db940d58 --- /dev/null +++ b/src/Akavache.Core/Internal/ExceptionMixins.cs @@ -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(this T? value, string name) + { + if (value is null) + { + throw new ArgumentNullException(name); + } + } + } +} diff --git a/src/Akavache.Core/Json/JsonSerializationMixin.cs b/src/Akavache.Core/Json/JsonSerializationMixin.cs index 1ccaa7cae..5e3c47190 100644 --- a/src/Akavache.Core/Json/JsonSerializationMixin.cs +++ b/src/Akavache.Core/Json/JsonSerializationMixin.cs @@ -242,14 +242,7 @@ public static IObservable> GetAllObjects(this IBlobCache blobC bool shouldInvalidateOnError = false, Func? cacheValidationPredicate = null) { -#if NETSTANDARD || XAMARINIOS || XAMARINMAC || XAMARINTVOS || TIZEN || MONOANDROID13_0 - if (blobCache is null) - { - throw new ArgumentNullException(nameof(blobCache)); - } -#else - ArgumentNullException.ThrowIfNull(blobCache); -#endif + blobCache.ThrowArgumentNullExceptionIfNull(nameof(blobCache)); #pragma warning disable CS8604 // Possible null reference argument. var fetch = Observable.Defer(() => blobCache.GetObjectCreatedAt(key)) diff --git a/src/Akavache.Core/Platforms/shared/AkavacheHttpMixin.cs b/src/Akavache.Core/Platforms/shared/AkavacheHttpMixin.cs index 2a53c5bad..c95d59ba5 100644 --- a/src/Akavache.Core/Platforms/shared/AkavacheHttpMixin.cs +++ b/src/Akavache.Core/Platforms/shared/AkavacheHttpMixin.cs @@ -17,14 +17,7 @@ public class AkavacheHttpMixin : IAkavacheHttpMixin /// public IObservable DownloadUrl(IBlobCache blobCache, string url, IDictionary? headers = null, bool fetchAlways = false, DateTimeOffset? absoluteExpiration = null) { -#if NETSTANDARD || XAMARINIOS || XAMARINMAC || XAMARINTVOS || TIZEN || MONOANDROID13_0 - if (blobCache is null) - { - throw new ArgumentNullException(nameof(blobCache)); - } -#else - ArgumentNullException.ThrowIfNull(blobCache); -#endif + blobCache.ThrowArgumentNullExceptionIfNull(nameof(blobCache)); return blobCache.DownloadUrl(url, url, headers, fetchAlways, absoluteExpiration); } @@ -32,20 +25,8 @@ public IObservable DownloadUrl(IBlobCache blobCache, string url, IDictio /// public IObservable DownloadUrl(IBlobCache blobCache, Uri url, IDictionary? headers = null, bool fetchAlways = false, DateTimeOffset? absoluteExpiration = null) { -#if NETSTANDARD || XAMARINIOS || XAMARINMAC || XAMARINTVOS || TIZEN || MONOANDROID13_0 - if (blobCache is null) - { - throw new ArgumentNullException(nameof(blobCache)); - } - - if (url is null) - { - throw new ArgumentNullException(nameof(url)); - } -#else - ArgumentNullException.ThrowIfNull(blobCache); - ArgumentNullException.ThrowIfNull(url); -#endif + blobCache.ThrowArgumentNullExceptionIfNull(nameof(blobCache)); + url.ThrowArgumentNullExceptionIfNull(nameof(url)); return blobCache.DownloadUrl(url.ToString(), url, headers, fetchAlways, absoluteExpiration); } @@ -53,14 +34,7 @@ public IObservable DownloadUrl(IBlobCache blobCache, Uri url, IDictionar /// public IObservable DownloadUrl(IBlobCache blobCache, string key, string url, IDictionary? headers = null, bool fetchAlways = false, DateTimeOffset? absoluteExpiration = null) { -#if NETSTANDARD || XAMARINIOS || XAMARINMAC || XAMARINTVOS || TIZEN || MONOANDROID13_0 - if (blobCache is null) - { - throw new ArgumentNullException(nameof(blobCache)); - } -#else - ArgumentNullException.ThrowIfNull(blobCache); -#endif + blobCache.ThrowArgumentNullExceptionIfNull(nameof(blobCache)); var doFetch = MakeWebRequest(HttpMethod.Get, new Uri(url), headers).SelectMany(x => ProcessWebResponse(x, url, absoluteExpiration)); var fetchAndCache = doFetch.SelectMany(x => blobCache.Insert(key, x, absoluteExpiration).Select(_ => x)); @@ -83,14 +57,7 @@ public IObservable DownloadUrl(IBlobCache blobCache, string key, string /// public IObservable DownloadUrl(IBlobCache blobCache, string key, Uri url, IDictionary? headers = null, bool fetchAlways = false, DateTimeOffset? absoluteExpiration = null) { -#if NETSTANDARD || XAMARINIOS || XAMARINMAC || XAMARINTVOS || TIZEN || MONOANDROID13_0 - if (blobCache is null) - { - throw new ArgumentNullException(nameof(blobCache)); - } -#else - ArgumentNullException.ThrowIfNull(blobCache); -#endif + blobCache.ThrowArgumentNullExceptionIfNull(nameof(blobCache)); var doFetch = MakeWebRequest(HttpMethod.Get, url, headers).SelectMany(x => ProcessWebResponse(x, url, absoluteExpiration)); var fetchAndCache = doFetch.SelectMany(x => blobCache.Insert(key, x, absoluteExpiration).Select(_ => x)); @@ -113,14 +80,7 @@ public IObservable DownloadUrl(IBlobCache blobCache, string key, Uri url /// public IObservable DownloadUrl(IBlobCache blobCache, HttpMethod method, string url, IDictionary? headers = null, bool fetchAlways = false, DateTimeOffset? absoluteExpiration = null) { -#if NETSTANDARD || XAMARINIOS || XAMARINMAC || XAMARINTVOS || TIZEN || MONOANDROID13_0 - if (blobCache is null) - { - throw new ArgumentNullException(nameof(blobCache)); - } -#else - ArgumentNullException.ThrowIfNull(blobCache); -#endif + blobCache.ThrowArgumentNullExceptionIfNull(nameof(blobCache)); return blobCache.DownloadUrl(method, url, url, headers, fetchAlways, absoluteExpiration); } @@ -128,20 +88,8 @@ public IObservable DownloadUrl(IBlobCache blobCache, HttpMethod method, /// public IObservable DownloadUrl(IBlobCache blobCache, HttpMethod method, Uri url, IDictionary? headers = null, bool fetchAlways = false, DateTimeOffset? absoluteExpiration = null) { -#if NETSTANDARD || XAMARINIOS || XAMARINMAC || XAMARINTVOS || TIZEN || MONOANDROID13_0 - if (blobCache is null) - { - throw new ArgumentNullException(nameof(blobCache)); - } - - if (url is null) - { - throw new ArgumentNullException(nameof(url)); - } -#else - ArgumentNullException.ThrowIfNull(blobCache); - ArgumentNullException.ThrowIfNull(url); -#endif + blobCache.ThrowArgumentNullExceptionIfNull(nameof(blobCache)); + url.ThrowArgumentNullExceptionIfNull(nameof(url)); return blobCache.DownloadUrl(method, url.ToString(), url, headers, fetchAlways, absoluteExpiration); } @@ -149,14 +97,7 @@ public IObservable DownloadUrl(IBlobCache blobCache, HttpMethod method, /// public IObservable DownloadUrl(IBlobCache blobCache, HttpMethod method, string key, string url, IDictionary? headers = null, bool fetchAlways = false, DateTimeOffset? absoluteExpiration = null) { -#if NETSTANDARD || XAMARINIOS || XAMARINMAC || XAMARINTVOS || TIZEN || MONOANDROID13_0 - if (blobCache is null) - { - throw new ArgumentNullException(nameof(blobCache)); - } -#else - ArgumentNullException.ThrowIfNull(blobCache); -#endif + blobCache.ThrowArgumentNullExceptionIfNull(nameof(blobCache)); var doFetch = MakeWebRequest(method, new Uri(url), headers).SelectMany(x => ProcessWebResponse(x, url, absoluteExpiration)); var fetchAndCache = doFetch.SelectMany(x => blobCache.Insert(key, x, absoluteExpiration).Select(_ => x)); @@ -179,14 +120,7 @@ public IObservable DownloadUrl(IBlobCache blobCache, HttpMethod method, /// public IObservable DownloadUrl(IBlobCache blobCache, HttpMethod method, string key, Uri url, IDictionary? headers = null, bool fetchAlways = false, DateTimeOffset? absoluteExpiration = null) { -#if NETSTANDARD || XAMARINIOS || XAMARINMAC || XAMARINTVOS || TIZEN || MONOANDROID13_0 - if (blobCache is null) - { - throw new ArgumentNullException(nameof(blobCache)); - } -#else - ArgumentNullException.ThrowIfNull(blobCache); -#endif + blobCache.ThrowArgumentNullExceptionIfNull(nameof(blobCache)); var doFetch = MakeWebRequest(method, url, headers).SelectMany(x => ProcessWebResponse(x, url, absoluteExpiration)); var fetchAndCache = doFetch.SelectMany(x => blobCache.Insert(key, x, absoluteExpiration).Select(_ => x)); diff --git a/src/Akavache.Core/Platforms/shared/Registrations.cs b/src/Akavache.Core/Platforms/shared/Registrations.cs index 08078d81f..ff6d5ffee 100644 --- a/src/Akavache.Core/Platforms/shared/Registrations.cs +++ b/src/Akavache.Core/Platforms/shared/Registrations.cs @@ -30,14 +30,7 @@ public class Registrations : IWantsToRegisterStuff #endif public void Register(IMutableDependencyResolver resolver, IReadonlyDependencyResolver readonlyDependencyResolver) { -#if NETSTANDARD || XAMARINIOS || XAMARINMAC || XAMARINTVOS || TIZEN || MONOANDROID13_0 - if (resolver is null) - { - throw new ArgumentNullException(nameof(resolver)); - } -#else - ArgumentNullException.ThrowIfNull(resolver); -#endif + resolver.ThrowArgumentNullExceptionIfNull(nameof(resolver)); #if XAMARIN_MOBILE var fs = new IsolatedStorageProvider(); diff --git a/src/Akavache.Core/Platforms/shared/StreamMixins.cs b/src/Akavache.Core/Platforms/shared/StreamMixins.cs index 05366eed7..989bc5022 100644 --- a/src/Akavache.Core/Platforms/shared/StreamMixins.cs +++ b/src/Akavache.Core/Platforms/shared/StreamMixins.cs @@ -3,6 +3,8 @@ // 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 Akavache; + namespace System { /// @@ -20,14 +22,7 @@ public static class StreamMixins /// An observable that signals when the write operation has completed. public static IObservable WriteAsyncRx(this Stream blobCache, byte[] data, int start, int length) { -#if NETSTANDARD || XAMARINIOS || XAMARINMAC || XAMARINTVOS || TIZEN || MONOANDROID13_0 - if (blobCache is null) - { - throw new ArgumentNullException(nameof(blobCache)); - } -#else - ArgumentNullException.ThrowIfNull(blobCache); -#endif + blobCache.ThrowArgumentNullExceptionIfNull(nameof(blobCache)); var ret = new AsyncSubject(); diff --git a/src/Akavache.Drawing/Registrations.cs b/src/Akavache.Drawing/Registrations.cs index 559312b59..73df10174 100644 --- a/src/Akavache.Drawing/Registrations.cs +++ b/src/Akavache.Drawing/Registrations.cs @@ -17,10 +17,7 @@ public class Registrations : IWantsToRegisterStuff /// public void Register(IMutableDependencyResolver resolver, IReadonlyDependencyResolver readonlyDependencyResolver) { - if (resolver is null) - { - throw new ArgumentNullException(nameof(resolver)); - } + resolver.ThrowArgumentNullExceptionIfNull(nameof(resolver)); #if !NETSTANDARD Locator.CurrentMutable.RegisterPlatformBitmapLoader(); diff --git a/src/Akavache.Json/Registrations.cs b/src/Akavache.Json/Registrations.cs index 830bac769..b455ece50 100644 --- a/src/Akavache.Json/Registrations.cs +++ b/src/Akavache.Json/Registrations.cs @@ -18,21 +18,13 @@ public class Registrations : IWantsToRegisterStuff /// public void Register(IMutableDependencyResolver resolver, IReadonlyDependencyResolver readonlyDependencyResolver) { -#if NETSTANDARD || XAMARINIOS || XAMARINMAC || XAMARINTVOS || TIZEN || MONOANDROID13_0 - if (resolver is null) - { - throw new ArgumentNullException(nameof(resolver)); - } -#else - ArgumentNullException.ThrowIfNull(resolver); -#endif - - resolver.Register( + resolver.ThrowArgumentNullExceptionIfNull(nameof(resolver)); + resolver?.Register( () => new JsonSerializerOptions(), typeof(JsonSerializerOptions), null); - resolver.Register(() => new SystemJsonSerializer(readonlyDependencyResolver.GetService()!), typeof(ISerializer), null); + resolver?.Register(() => new SystemJsonSerializer(readonlyDependencyResolver.GetService()!), typeof(ISerializer), null); ////resolver.Register(() => new JsonDateTimeContractResolver(), typeof(IDateTimeContractResolver), null); } diff --git a/src/Akavache.Json/SystemJsonSerializer.cs b/src/Akavache.Json/SystemJsonSerializer.cs index 0a66660b2..401298de1 100644 --- a/src/Akavache.Json/SystemJsonSerializer.cs +++ b/src/Akavache.Json/SystemJsonSerializer.cs @@ -21,10 +21,7 @@ public class SystemJsonSerializer : ISerializer, IEnableLogger /// The options. public SystemJsonSerializer(JsonSerializerOptions options) { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } + options.ThrowArgumentNullExceptionIfNull(nameof(options)); Options = options; } diff --git a/src/Akavache.Mobile/Registrations.cs b/src/Akavache.Mobile/Registrations.cs index 4b245da9c..e9a0da2b5 100644 --- a/src/Akavache.Mobile/Registrations.cs +++ b/src/Akavache.Mobile/Registrations.cs @@ -18,29 +18,22 @@ public class Registrations : IWantsToRegisterStuff /// public void Register(IMutableDependencyResolver resolver, IReadonlyDependencyResolver readonlyDependencyResolver) { -#if NETSTANDARD || XAMARINIOS || XAMARINMAC || XAMARINTVOS || TIZEN || MONOANDROID13_0 - if (resolver is null) - { - throw new ArgumentNullException(nameof(resolver)); - } -#else - ArgumentNullException.ThrowIfNull(resolver); -#endif + resolver.ThrowArgumentNullExceptionIfNull(nameof(resolver)); var akavacheDriver = new AkavacheDriver(); - resolver.Register(() => akavacheDriver, typeof(ISuspensionDriver), null); + resolver?.Register(() => akavacheDriver, typeof(ISuspensionDriver), null); // NB: These correspond to the hacks in Akavache.Http's registrations - resolver.Register( + resolver?.Register( () => readonlyDependencyResolver.GetService()?.ShouldPersistState ?? throw new InvalidOperationException("Unable to resolve ISuspensionHost, probably ReactiveUI is not initialized."), typeof(IObservable), "ShouldPersistState"); - resolver.Register( + resolver?.Register( () => readonlyDependencyResolver.GetService()?.IsUnpausing ?? throw new InvalidOperationException("Unable to resolve ISuspensionHost, probably ReactiveUI is not initialized."), typeof(IObservable), "IsUnpausing"); - resolver.Register(() => RxApp.TaskpoolScheduler, typeof(IScheduler), "Taskpool"); + resolver?.Register(() => RxApp.TaskpoolScheduler, typeof(IScheduler), "Taskpool"); } } diff --git a/src/Akavache.NewtonsoftJson/JsonDateTimeOffsetTickConverter.cs b/src/Akavache.NewtonsoftJson/JsonDateTimeOffsetTickConverter.cs index eabdeaa2a..aefe050c8 100644 --- a/src/Akavache.NewtonsoftJson/JsonDateTimeOffsetTickConverter.cs +++ b/src/Akavache.NewtonsoftJson/JsonDateTimeOffsetTickConverter.cs @@ -29,14 +29,11 @@ public class JsonDateTimeOffsetTickConverter : JsonConverter /// The calling serializer. public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { - if (serializer == null) - { - throw new ArgumentNullException(nameof(serializer)); - } + serializer.ThrowArgumentNullExceptionIfNull(nameof(serializer)); if (value is DateTimeOffset dateTimeOffset) { - serializer.Serialize(writer, new DateTimeOffsetData(dateTimeOffset)); + serializer?.Serialize(writer, new DateTimeOffsetData(dateTimeOffset)); } } @@ -52,22 +49,15 @@ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer /// public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { - if (reader == null) - { - throw new ArgumentNullException(nameof(reader)); - } - - if (serializer == null) - { - throw new ArgumentNullException(nameof(serializer)); - } + reader.ThrowArgumentNullExceptionIfNull(nameof(reader)); + serializer.ThrowArgumentNullExceptionIfNull(nameof(serializer)); - if (reader.TokenType == JsonToken.Date && reader.Value is not null) + if (reader?.TokenType == JsonToken.Date && reader.Value is not null) { return (DateTimeOffset)reader.Value; } - var data = serializer.Deserialize(reader); + var data = serializer?.Deserialize(reader); return data is null ? null : (DateTimeOffset)data; } diff --git a/src/Akavache.NewtonsoftJson/JsonDateTimeTickConverter.cs b/src/Akavache.NewtonsoftJson/JsonDateTimeTickConverter.cs index 8df47ae53..8036fd86f 100644 --- a/src/Akavache.NewtonsoftJson/JsonDateTimeTickConverter.cs +++ b/src/Akavache.NewtonsoftJson/JsonDateTimeTickConverter.cs @@ -45,12 +45,9 @@ public class JsonDateTimeTickConverter(DateTimeKind? forceDateTimeKindOverride = /// nameof(reader). public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { - if (reader is null) - { - throw new ArgumentNullException(nameof(reader)); - } + reader.ThrowArgumentNullExceptionIfNull(nameof(reader)); - if (reader.TokenType is not JsonToken.Integer and not JsonToken.Date) + if (reader?.TokenType is not JsonToken.Integer and not JsonToken.Date) { return null; } diff --git a/src/Akavache.NewtonsoftJson/NewtonsoftSerializer.cs b/src/Akavache.NewtonsoftJson/NewtonsoftSerializer.cs index 80d3ac733..08bf3b43c 100644 --- a/src/Akavache.NewtonsoftJson/NewtonsoftSerializer.cs +++ b/src/Akavache.NewtonsoftJson/NewtonsoftSerializer.cs @@ -22,11 +22,7 @@ public class NewtonsoftSerializer : ISerializer, IEnableLogger /// The options. public NewtonsoftSerializer(JsonSerializerSettings options) { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - + options.ThrowArgumentNullExceptionIfNull(nameof(options)); Options = options; } @@ -41,12 +37,8 @@ public NewtonsoftSerializer(JsonSerializerSettings options) /// The get json date time contract resolver. public void CreateSerializer(Func getJsonDateTimeContractResolver) { - if (getJsonDateTimeContractResolver is null) - { - throw new ArgumentNullException(nameof(getJsonDateTimeContractResolver)); - } - - var jsonDateTimeContractResolver = getJsonDateTimeContractResolver() as JsonDateTimeContractResolver; + getJsonDateTimeContractResolver.ThrowArgumentNullExceptionIfNull(nameof(getJsonDateTimeContractResolver)); + var jsonDateTimeContractResolver = getJsonDateTimeContractResolver?.Invoke() as JsonDateTimeContractResolver; lock (Options) { @@ -120,10 +112,7 @@ public byte[] SerializeObject(T value) => /// public IObservable DeserializeObject(byte[] x) { - if (x is null) - { - throw new ArgumentNullException(nameof(x)); - } + x.ThrowArgumentNullExceptionIfNull(nameof(x)); try { diff --git a/src/Akavache.NewtonsoftJson/Registrations.cs b/src/Akavache.NewtonsoftJson/Registrations.cs index fbf5a6646..2b485c74e 100644 --- a/src/Akavache.NewtonsoftJson/Registrations.cs +++ b/src/Akavache.NewtonsoftJson/Registrations.cs @@ -18,16 +18,9 @@ public class Registrations : IWantsToRegisterStuff /// public void Register(IMutableDependencyResolver resolver, IReadonlyDependencyResolver readonlyDependencyResolver) { -#if NETSTANDARD || XAMARINIOS || XAMARINMAC || XAMARINTVOS || TIZEN || MONOANDROID13_0 - if (resolver is null) - { - throw new ArgumentNullException(nameof(resolver)); - } -#else - ArgumentNullException.ThrowIfNull(resolver); -#endif + resolver.ThrowArgumentNullExceptionIfNull(nameof(resolver)); - resolver.Register( + resolver?.Register( () => new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Replace, @@ -37,8 +30,8 @@ public void Register(IMutableDependencyResolver resolver, IReadonlyDependencyRes typeof(JsonSerializerSettings), null); - resolver.Register(() => new NewtonsoftSerializer(readonlyDependencyResolver.GetService()!), typeof(ISerializer), null); + resolver?.Register(() => new NewtonsoftSerializer(readonlyDependencyResolver.GetService()!), typeof(ISerializer), null); - resolver.Register(() => new JsonDateTimeContractResolver(), typeof(IDateTimeContractResolver), null); + resolver?.Register(() => new JsonDateTimeContractResolver(), typeof(IDateTimeContractResolver), null); } } diff --git a/src/Akavache.Sqlite3/SqlLiteCache/ObjectWrapper.cs b/src/Akavache.Sqlite3/SqlLiteCache/ObjectWrapper.cs index 349f4c74c..4e291ccdf 100755 --- a/src/Akavache.Sqlite3/SqlLiteCache/ObjectWrapper.cs +++ b/src/Akavache.Sqlite3/SqlLiteCache/ObjectWrapper.cs @@ -15,10 +15,7 @@ public ObjectWrapper() { } - public ObjectWrapper(T value) - { - Value = value; - } + public ObjectWrapper(T value) => Value = value; public T? Value { get; set; } } diff --git a/src/Akavache.Sqlite3/SqlLiteCache/SQLiteEncryptedBlobCache.cs b/src/Akavache.Sqlite3/SqlLiteCache/SQLiteEncryptedBlobCache.cs index f21ef2836..cbe0b080b 100644 --- a/src/Akavache.Sqlite3/SqlLiteCache/SQLiteEncryptedBlobCache.cs +++ b/src/Akavache.Sqlite3/SqlLiteCache/SQLiteEncryptedBlobCache.cs @@ -28,32 +28,26 @@ public SQLiteEncryptedBlobCache(string databaseFile, IEncryptionProvider? encryp /// protected override IObservable BeforeWriteToDiskFilter(byte[] data, IScheduler scheduler) { - if (data is null) - { - throw new ArgumentNullException(nameof(data)); - } + data.ThrowArgumentNullExceptionIfNull(nameof(data)); - if (data.Length == 0) + if (data?.Length == 0) { return Observable.Return(data); } - return _encryption.EncryptBlock(data); + return _encryption.EncryptBlock(data!); } /// protected override IObservable AfterReadFromDiskFilter(byte[] data, IScheduler scheduler) { - if (data is null) - { - throw new ArgumentNullException(nameof(data)); - } + data.ThrowArgumentNullExceptionIfNull(nameof(data)); - if (data.Length == 0) + if (data?.Length == 0) { return Observable.Return(data); } - return _encryption.DecryptBlock(data); + return _encryption.DecryptBlock(data!); } }