Skip to content

feat: V12 API modernization, AOT-safe assembly config, V10→V11 migration, and full coverage#1178

Merged
glennawatson merged 27 commits intomainfrom
feature/akavache-v10-to-v11-compat
Apr 14, 2026
Merged

feat: V12 API modernization, AOT-safe assembly config, V10→V11 migration, and full coverage#1178
glennawatson merged 27 commits intomainfrom
feature/akavache-v10-to-v11-compat

Conversation

@glennawatson
Copy link
Copy Markdown
Contributor

@glennawatson glennawatson commented Apr 13, 2026

Summary

This PR is the V12 modernization pass for Akavache. The end-user surface is the four breaking-change sections below plus a migration guide; the rest — analyzer sweep, polyfills, cross-TFM build plumbing, test rewrite, coverage config — is consolidated under Maintainer notes.

  1. Reflection-based assembly discovery is deprecated — the builder no longer probes Assembly.GetEntryAssembly() at construction time. There is a new AOT-safe fluent API.
  2. A real V10→V11 migration pipeline — if you have an existing pre-V11 Akavache cache on disk, you can now read it and pull the contents into the V11 store without a custom migration script.
  3. Required application name on CacheDatabase.Initialize / CreateBuilder — the old parameterless overloads are marked [Obsolete].
  4. AOT-safe JsonTypeInfo serialization — exposed via extension methods in Akavache.SystemTextJson.Bson, so Akavache.Core itself stays serializer-agnostic and Newtonsoft-only consumers do not transitively pull in System.Text.Json.

A full user-facing migration walkthrough lives in docs/migration-v11-to-v12.md.


1. Reflection is out — use WithExecutingAssembly instead

What changed. IAkavacheInstance.ExecutingAssembly, ExecutingAssemblyName and Version are now [Obsolete]. The AkavacheBuilder constructor no longer calls Assembly.GetEntryAssembly() / Assembly.GetExecutingAssembly() / GetCustomAttribute<AssemblyFileVersionAttribute>(). By default these properties now return:

  • ExecutingAssembly → Akavache's own assembly (a sentinel)
  • ExecutingAssemblyNamenull
  • Versionnull

Why. Reflection-based assembly discovery does not reliably yield the caller's assembly in trimmed or NativeAOT-published apps — the entry assembly can go missing, attributes can be trimmed, and the resulting cache paths shift at runtime. Worse, relying on Assembly.GetEntryAssembly() adds a hidden reflection pin point that blocks trimming.

What to do. If your app reads any of those three properties, stop reading them from Akavache and hold your own Assembly reference instead:

var myAssembly = typeof(MyApp).Assembly;
var name       = myAssembly.GetName().Name;
var version    = myAssembly.GetName().Version;

If you want Akavache itself to track the value (so it flows through anything that still reads IAkavacheInstance), use the new AOT-safe fluent method on the builder:

CacheDatabase.CreateBuilder("MyApp")
    .WithSerializer<SystemJsonSerializer>()
    .WithExecutingAssembly(typeof(MyApp).Assembly) // AOT-safe — caller-owned reference
    .WithSqliteProvider()
    .WithSqliteDefaults()
    .Build();

Because the assembly reference is caller-owned, there is no runtime reflection discovery at all. WithExecutingAssembly reads the name and AssemblyFileVersionAttribute off the instance you pass, which is trim/AOT-safe.

The old behaviour is still available behind the legacy AssemblyInfoHelper (also marked [Obsolete]), but nothing in the builder calls it by default anymore.


2. V10 → V11 migration

Akavache V11 changed the on-disk format — the old sqlite3 CacheElement table layout was replaced by the new CacheEntry schema, and the per-cache file names changed. If you ship V11+ to users who already have V10 data on disk, you need to migrate it.

What ships in this PR.

  • A dedicated Akavache.V10toV11 project with a V10MigrationService that knows how to read the legacy CacheElement / CacheIndex tables and project them into the new shape.
  • An IAkavacheBuilder.WithV10ToV11Migration(...) extension that wires the migration into the builder pipeline so it runs the first time the V11 cache is opened.
  • Support for reading legacy V10 bytes in-place inside SqliteBlobCache / EncryptedSqliteBlobCache via TryGetLegacyValueAsync, so keys that never got migrated still resolve against the legacy table as a fallback.

How to opt in. If your app used V10 Akavache before upgrading, add the V10→V11 migration step when configuring the builder:

using Akavache.V10toV11;

CacheDatabase.CreateBuilder("MyApp")
    .WithSerializer<SystemJsonSerializer>()
    .WithV10ToV11Migration()   // reads legacy CacheElement rows on first open
    .WithSqliteProvider()
    .WithSqliteDefaults()
    .Build();

On first open, the migration service:

  1. Opens the legacy V10 cache files at the same on-disk locations they lived at before (governed by FileLocationOption.Legacy if your app is using the legacy directory layout).
  2. Reads the CacheElement rows plus the old file-name map.
  3. Writes each entry into the new V11 CacheEntry table on the corresponding V11 cache, preserving keys, expiration, and type metadata.
  4. Leaves the legacy file in place. Any keys that weren't copied are still readable through the TryGetLegacyValueAsync fallback inside SqliteBlobCache / EncryptedSqliteBlobCache.

Migration characteristics.

  • Safe on failure. If reading a V10 row throws, the migration moves on and the row remains accessible via the in-process legacy fallback.
  • Idempotent. Re-running WithV10ToV11Migration() on an already-migrated store is a no-op.
  • Encrypted caches. The EncryptedSqlite3 flavour migrates the same way — you pass the V10 password through the same builder path.
  • Timing. The migration runs at builder Build() time, synchronously from the caller's perspective, so it completes before your app starts hitting the cache for reads.

3. Required application name

CacheDatabase.Initialize<T>() and CacheDatabase.CreateBuilder() now require an explicit applicationName argument. The old parameterless CreateBuilder() overload is marked [Obsolete] but still compiles.

// V11 style (still works, but deprecated)
CacheDatabase.CreateBuilder();

// V12 style (required)
CacheDatabase.CreateBuilder("MyApp");
CacheDatabase.Initialize<SystemJsonSerializer>("MyApp");

This is the single biggest source of cache-path drift between reflection-based builds and NativeAOT — being explicit kills the ambiguity entirely.


4. AOT-safe JsonTypeInfo serialization lives in Akavache.SystemTextJson.Bson

ISerializer in Akavache.Core deliberately stays serializer-agnostic. It does not know about System.Text.Json and does not declare any JsonTypeInfo<T> overloads. The base interface is just the two reflection-based members:

[RequiresUnreferencedCode("...")]
[RequiresDynamicCode("...")]
T? Deserialize<T>(byte[] bytes);

[RequiresUnreferencedCode("...")]
[RequiresDynamicCode("...")]
byte[] Serialize<T>(T item);

The AOT-safe JsonTypeInfo<T> path is provided by extension methods that ship in Akavache.SystemTextJson.Bson (which transitively brings in Akavache.SystemTextJson). Importing using Akavache.SystemTextJson; gives you:

using Akavache.SystemTextJson;

// Call site looks identical to an instance method:
var bytes  = serializer.Serialize(myModel, AppJsonContext.Default.MyModel);
var result = serializer.Deserialize<MyModel>(bytes, AppJsonContext.Default.MyModel);

Under the hood the extension methods type-check the runtime serializer:

  • SystemJsonSerializer → routes to its static DeserializeAot / SerializeAot methods (pure JsonTypeInfo path, no reflection)
  • SystemJsonBsonSerializer → routes through the same path (BSON cannot be AOT-encoded, so the AOT overload emits plain JSON bytes)
  • Any other ISerializer (for example Newtonsoft-backed) → throws NotSupportedException. Use the non-typed Deserialize<T>(byte[]) / Serialize<T>(T) overloads on Newtonsoft.

This indirection keeps Akavache.Core free of a hard dependency on System.Text.Json, so Newtonsoft-only consumers do not transitively pull it in.


Other user-visible changes

  • SystemTextJson.Bson split out into its own Akavache.SystemTextJson.Bson NuGet package. If you were using SystemJsonBsonSerializer from Akavache.SystemTextJson, switch your package reference.
  • ISerializer.Deserialize(byte[], Type) was reverted. The non-generic overload is gone — use Deserialize<T>(byte[]).
  • SerializerExtensions.Get(IBlobCache, string, Type) (the reflection-based variant) was removed.
  • CheckpointMode enum added for explicit control over SQLite WAL checkpoint strategy.
  • New IAkavacheConnection / IAkavacheTransaction abstractions let you drive SqliteBlobCache / EncryptedSqliteBlobCache with a custom backing connection instead of the default SqliteAkavacheConnection / SqliteAkavacheTransaction implementations shipped in Akavache.Sqlite3.
  • Lock primitives upgraded to System.Threading.Lock on net9+. AkavacheBuilder, InMemoryBlobCacheBase, and NewtonsoftSerializer use C# 13's Lock type under #if NET9_0_OR_GREATER, which lets the JIT use tighter lock-acquire codegen than the legacy object-monitor path on those targets. NewtonsoftSerializer additionally stops locking on the user-supplied JsonSerializerSettings and uses a private lock object instead.
  • MAUI sample XAML compiles with x:DataType so MAUI's compiled-bindings analyzer is happy (no more XC0022 warnings on MainPage.xaml / EditTodoPage.xaml).
  • WPF and MAUI sample value converters return Binding.DoNothing / BindableProperty.UnsetValue from ConvertBack instead of throwing — the canonical one-way-converter idiom that the binding engine recognises without exception round-tripping.

Maintainer notes

Test count and coverage

Test count is up from ~1600 to 3940 across net8.0 and net9.0, with 0 skipped tests (down from 14 skipped — the chronically-flaky DateTimeSerializationShouldBeConsistentAndAccurate parameterised test and the duplicated SettingsStorageTests are gone). Production assemblies sit at 100% line / 100% branch coverage: 4438/4438 lines, 1714/1714 branches across Akavache.Core, Akavache.Drawing, Akavache.EncryptedSqlite3, Akavache.NewtonsoftJson, Akavache.Settings, Akavache.Sqlite3, Akavache.SystemTextJson, and Akavache.SystemTextJson.Bson.

Most of the new coverage came from extracting previously-private helpers (SqliteBlobCache.ToExpiryValue, GetOrCreateHttpService, TryGetLegacyValueAsync, InitializeDatabase; BitmapImageExtensions.ThrowOnBadImageBuffer / ThrowOnNullOrBadImageBuffer / BytesToImage; UniversalSerializer.CastAsDateTime / CastAsDateTimeOffset / FindKeyCandidates / TryDeserializeCandidate; the JsonTypeInfo dispatch helpers; etc.) to internal static methods with explicit parameters and adding direct unit tests for each. UniversalSerializerTests was moved into the shared CacheDatabaseState NotInParallel group to fix a pre-existing flaky race against the builder-extension tests that also mutate UniversalSerializer._registeredSerializerFactories.

VSTest → Microsoft.Testing.Platform (MTP) + TUnit

  • The entire test suite runs on Microsoft.Testing.Platform with TUnit instead of VSTest/xUnit. TestingPlatformDotnetTestSupport is enabled in Directory.Build.props, MTP is discovered via src/global.json, and src/tests.runsettings was deleted (legacy VSTest file).
  • Test isolation for anything touching CacheDatabase / AppLocator uses a shared AkavacheTestExecutor base together with TUnit's [NotInParallel("CacheDatabaseState")] so cross-test global-state mutation is serialized. Previously this was handled ad-hoc and leaked.
  • Native-SQLite tests use in-memory databases by default and are individually gated with NotInParallel groups — no disk contention.
  • A new TaskAssertionExtensions.ShouldThrowAsync<TException>(this Task) helper lets tests assert that a task faults with a specific exception without going through TUnit's Assert.That<T>(Func<Task<T?>>) overload (which fails to unify Task<T> with Task<T?> for non-nullable T and produces CS8619). The five test files exercising SQLite / encrypted / in-memory disposed-state and arg-validation paths use this helper end-to-end, so the suite builds clean with WarningsAsErrors for nullability.
  • InMemoryBlobCacheBaseTests consistently uses await using var cache = CreateCache(); instead of try { … } finally { await cache.DisposeAsync(); }.
  • Test-stub Assembly.GetCustomAttributes overrides return a typed static Attribute[] field — not a collection-expression literal [] — because the return type is object[] and [] would be inferred as object[], which breaks the framework's internal cast in CustomAttributeExtensions.GetCustomAttribute<T>(Assembly).
  • ConcurrencyTests is a sealed instance-method test class (TUnit's source generator instantiates fixtures, so static test methods would fail at codegen time).

Code coverage configuration (src/testconfig.json)

The previous testconfig.json used invalid top-level keys (platform, extensions[]) that are not part of the Microsoft.Testing.Platform schema and were silently ignored. The file has been rewritten to use the documented codeCoverage.Configuration.CodeCoverage schema with Attributes.Exclude for:

  • System.CodeDom.Compiler.GeneratedCodeAttribute
  • System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute
  • System.Diagnostics.DebuggerHiddenAttribute
  • System.Diagnostics.DebuggerNonUserCodeAttribute

This correctly excludes source-generator output (notably RegexGenerator.g.cs from [GeneratedRegex]) from the coverage numerator and denominator. ModulePaths.Include / Exclude are intentionally not specified: per the docs, empty Include clauses imply all, and MTP's default IncludeTestAssembly: false already drops the test assemblies without any custom filter.

Cross-TFM build plumbing

  • <EnableWindowsTargeting>true</EnableWindowsTargeting> added to Directory.Build.props. Combined with dropping the IsOsPlatform('Windows') guard on AkavacheWindowsDesktopTargets, this lets Linux / macOS CI and developer machines build net462 / net472 / net481 and net8.0/9.0/10.0-windows10.0.19041.0 TFMs, mirroring ReactiveUI's setup. Previously those targets only compiled on Windows runners, so TFM-specific regressions surfaced only at PR time.
  • Akavache.Core/Polyfills ships typed stand-ins for RequiresUnreferencedCodeAttribute, RequiresDynamicCodeAttribute, DoesNotReturnAttribute, NotNullAttribute, UnconditionalSuppressMessageAttribute, and CallerArgumentExpressionAttribute, gated #if !NET. InternalsVisibleTo is set for every satellite assembly, so Akavache.NewtonsoftJson / SystemTextJson / SystemTextJson.Bson / Sqlite3 / Settings / V10toV11 can apply [RequiresUnreferencedCode] and friends unconditionally — the old #if NET6_0_OR_GREATER gates around every AOT-annotated method are gone (six duplicated method declarations removed from Akavache.NewtonsoftJson/AkavacheBuilderExtensions.cs alone).
  • HashCode polyfill (Akavache.Core/Polyfills/HashCode.cs, #if NETFRAMEWORK) — xxHash32-based with Combine<T1..T4> and AddBytes(ReadOnlySpan<byte>). Size.GetHashCode, LoginInfo, and CacheEntry hash through it. The polyfill uses RandomNumberGenerator.Create() for the per-process seed (not System.Random, to satisfy CA5394) and its ToHashCode is marked readonly.

Source-generator regex split

DateTimeHelpers.Iso8601Regex() and SystemJsonBsonSerializer.GetDateRegex() both use [GeneratedRegex] on net7+, which doesn't exist on net462 / net472 / net481 / netstandard2.0. Each has been split into two sibling partial files — *.Regex.SourceGen.cs (#if NET7_0_OR_GREATER, [GeneratedRegex] partial method) and *.Regex.NetFramework.cs (#if !NET7_0_OR_GREATER, static readonly Regex field + body method returning it) — so the main file stays TFM-clean and callers are unchanged.

Netfx API parity fixes

Everything the netfx TFMs needed now that they actually compile cross-platform:

  • UniversalSerializer: ArgumentNullException.ThrowIfNull replaced with ArgumentExceptionHelper.ThrowIfNull (the BCL method is net6+). The "..."-quoted string trimming path stopped using string.StartsWith(char) + range indexer [1..^1] (both net6+) and uses index access + Substring instead.
  • RequestCache.GetOrCreateRequest: the 3-argument ConcurrentDictionary.GetOrAdd(key, factory, arg) overload is net-core-only — replaced with the 2-argument overload that closes over fetchFunc directly. Same visible behaviour.
  • InMemoryBlobCacheBase.VacuumExpiredEntries / CollectExpiredKeys and SqliteBlobCache.TryGetLegacyValueAsync drop the in DateTimeOffset parameter modifier (RCS1242 — DateTimeOffset is not a readonly struct, so in forces defensive copies at every member access).
  • Akavache.Settings/AkavacheBuilderExtensions.GetLoadedSettingsStore: Dictionary<TKey,TValue>.GetValueOrDefault is net6+ → switched to TryGetValue(..., out var) ? value : null.
  • HttpService.ProcessWebResponse: decorated with [SuppressMessage("Style", "IDE0200", ...)] — the lambda around ReadAsByteArrayAsync() cannot be collapsed to a method group because net6+ adds a CancellationToken overload that makes the method group ambiguous at the Observable.FromAsync call site.
  • BinaryHelpers XML doc: cref <see cref="BinaryPrimitives"/> → plain <c>System.Buffers.Binary.BinaryPrimitives</c> (net472/net481 don't ship the type, so the cref won't resolve).
  • samples/AkavacheTodoMaui/MauiProgram.cs: explicit using Microsoft.Extensions.Logging; so builder.Logging.AddDebug() resolves on net10.0-android, where the implicit-usings machinery doesn't pull it in.
  • Empty-body polyfill attributes (DoesNotReturnAttribute, NotNullAttribute) use the class Foo : Attribute; shorthand (RCS1251).
  • RequiresDynamicCodeAttribute polyfill's ctor has a <param name="message"> doc (SA1611).

Layout, analyzer enforcement, and code modernisation

  • Source tree restructured into src/tests/, src/benchmarks/, src/samples/, src/compat/ subfolders. Akavache.slnx, csproj ProjectReference paths, codecov.yml, and CLAUDE.md updated. BENCHMARK_REPORT.md and PERFORMANCE_SUMMARY.md live under docs/.
  • .editorconfig enforces the full Microsoft Code Analysis catalogue as error: CA Design / Performance / Reliability / Usage / Security / Maintainability / Globalization / Interoperability / Naming, plus SYSLIB0xxx runtime obsoletions and SYSLIB1xxx source-generator diagnostics.
  • IDE language / naming / miscellaneous style rules are enforced as error. <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> in Directory.Build.props runs them at compile time.
  • Roslynator simplification, code-quality, performance, maintainability, and documentation categories are enforced as error. Rules with a CA / SA equivalent are routed to none with # handled by … comments so we never double-report.
  • A curated ReSharper / Rider inspection set (~95 rules) at the bottom of .editorconfig is mapped 1:1 to the CA / SA / RCS / IDE rules where an equivalent exists, and disables the inspections that flag deliberate access-modifier or naming choices.
  • Test-only .editorconfig overrides in src/tests/: IDE0200 (TUnit Assert.That(...).Throws<T>() requires the explicit lambda — the method-group overload binds to a different Assert.That that does not invoke the delegate), RCS1079 (test fakes deliberately throw NotImplementedException), RCS1223 (no DebuggerDisplay on test fixtures).

Code modernisation sweep

With the analyzer catalogue promoted to error, the whole codebase was pulled forward to modern C#:

  • Collection expressions ([..xs], []) replace .ToArray() / .ToList() / Array.Empty<T>() where it compiles cleanly — except on the test stub classes mentioned above where the inferred type matters.
  • static lambdas on every anonymous function that captures nothing (IDE0320 enforced — ~250 call sites updated).
  • C# 13 field keyword for lazy-init property backing storage. SqliteBlobCache.HttpService is the canonical example: get => field ??= new HttpService(); with no manual backing field.
  • await using declarations replace await using (var x = ...) { ... } blocks where the scope allows.
  • Simplified Equals / GetHashCode on CacheEntry and LoginInfoReferenceEquals short-circuits, HashCode.AddBytes(ReadOnlySpan<byte>), and a private ValueEquals helper for byte-array content equality.
  • File-scoped namespaces throughout.
  • Pattern matching (is not, property patterns, is { Version: var v }) replaces nested null checks and as + null checks.
  • Compound assignments (+=, ??=), inferred tuple member names, and target-typed new().
  • Dead defensive code removed. SecureBlobCacheWrapper.Dispose / DisposeAsync no longer guard against inner is not IDisposable / IAsyncDisposable. Those checks were statically unreachable because IBlobCache : IDisposable, IAsyncDisposable — the compiler already enforces both interfaces on any inner the wrapper sees.
  • UniversalSerializer.TryBasicJsonDeserialization<T> restored to its original string/int/bool-only surface. An earlier refactor silently added double / long cases that were never covered by tests and broke TryBasicJsonDeserializationShouldReturnDefaultForDoubleType, which asserted that doubles return default(double).
  • PreserveAttribute and its tests are removed; SqliteBlobCache no longer references it.
  • ArgumentExceptionHelper, DateTimeHelpers, AkavacheBuilder.ApplyForcedDateTimeKind, and InMemoryBlobCacheBase.ForcedDateTimeKind setter use the early-return form (RCS1208).
  • Assembly.Location replaced with AppContext.BaseDirectory plus AssemblyFileVersionAttribute — trim/AOT-friendly and works on single-file publish.

Test plan

  • CI green on net462, net472, net481, net6.0, net8.0, net9.0, net10.0
  • Akavache.Tests passes (3940 tests, 0 failures, 0 skipped) on a fresh checkout
  • Akavache.Settings.Tests passes on a fresh checkout
  • Coverage run (dotnet test -- --coverage --coverage-output-format cobertura) reports 100% line / 100% branch (4438/4438 / 1714/1714) across production assemblies
  • Manual smoke test: load a V10 on-disk cache in a sample app with .WithV10ToV11Migration() and confirm legacy keys resolve
  • Manual smoke test: publish the AkavacheTodoMaui sample with NativeAOT and confirm cache paths are stable without ExecutingAssembly being touched

@glennawatson glennawatson requested review from ChrisPulman and Copilot and removed request for Copilot April 13, 2026 00:29
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR modernizes Akavache for a V12 release by removing implicit reflection-based assembly discovery in favor of explicit/AOT-safe configuration, introducing a V10→V11 migration pipeline, and expanding/strengthening test coverage across core and extension packages.

Changes:

  • Adds AOT-safe builder/config APIs (explicit applicationName, explicit executing assembly, connection abstractions) and updates related call sites.
  • Introduces new packages/abstractions (e.g., Akavache.SystemTextJson.Bson, IAkavacheConnection/IAkavacheTransaction) and refactors internals for testability.
  • Expands tests significantly and adjusts parallelization/isolation to reduce flakiness.

Reviewed changes

Copilot reviewed 89 out of 119 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/Akavache.Tests/RequestCacheTests.cs Adds coverage for in-flight request tracking/removal and disables parallel execution for isolation.
src/Akavache.Tests/RelativeTimeExtensionsTests.cs Adds broad coverage for TimeSpan-based extension overloads and null-guard branches.
src/Akavache.Tests/PreserveAttributeTests.cs Adds targeted unit tests for the internal PreserveAttribute surface.
src/Akavache.Tests/NewtonsoftJsonBuilderExtensionsTests.cs Adds tests for Newtonsoft builder extensions and factory lambda execution coverage.
src/Akavache.Tests/NewtonsoftDateTimeContractResolverTests.cs Adds tests for contract resolver converter behavior and recursion guard.
src/Akavache.Tests/Mocks/SerializerTestModel.cs Adds a shared model to support serializer tests.
src/Akavache.Tests/Mocks/SerializerTestContext.cs Adds source-gen JsonSerializerContext for AOT-style serializer tests.
src/Akavache.Tests/Mocks/InMemoryAkavacheTransaction.cs Adds in-memory transaction mock for connection abstraction tests.
src/Akavache.Tests/Mocks/InMemoryAkavacheConnection.cs Adds in-memory connection mock (incl. failure simulation) for sqlite cache logic tests.
src/Akavache.Tests/LoginInfoTests.cs Adds constructor coverage for LoginInfo.
src/Akavache.Tests/LoginExtensionsTests.cs Adds coverage for GetLogin null-deserialization branch.
src/Akavache.Tests/LegacyFileLocationTests.cs Disables parallel execution for shared CacheDatabase state scenarios.
src/Akavache.Tests/ISerializerDefaultMethodTests.cs Adds tests asserting default interface method behavior for new JsonTypeInfo overloads.
src/Akavache.Tests/HttpExtensionsTests.cs Adds tests for HTTP extension argument validation and stream error paths.
src/Akavache.Tests/Executors/AkavacheTestExecutor.cs Adds a test executor that resets global Akavache/Splat state for isolation.
src/Akavache.Tests/DownloadUrlExtensionsTests.cs Updates initialization to require application name; disables parallel execution.
src/Akavache.Tests/BinaryHelpersTests.cs Adds unit tests for BinaryHelpers little-endian decoders.
src/Akavache.Tests/BackwardCompatibilityTests.cs Updates initialization API usage and disables parallel execution for global state.
src/Akavache.Tests/AssemblyInfoHelperTests.cs Adds coverage for the obsolete reflection-based assembly helper.
src/Akavache.Tests/Akavache.Tests.csproj Adds CS0618 suppression and TUnit executor usings; adds new project references.
src/Akavache.SystemTextJson/SystemJsonBsonSerializer.cs Removes BSON serializer from base STJ package (split into dedicated project).
src/Akavache.SystemTextJson/AkavacheBuilderExtensions.cs Registers serializers with UniversalSerializer and removes BSON builder APIs from this package.
src/Akavache.SystemTextJson/Akavache.SystemTextJson.csproj Removes Newtonsoft deps and exposes internals to tests/BSON package.
src/Akavache.SystemTextJson.Bson/SystemJsonBsonSerializer.cs Introduces BSON serializer implementation in the split package.
src/Akavache.SystemTextJson.Bson/AkavacheBuilderExtensions.cs Adds builder extensions for configuring BSON serializer usage.
src/Akavache.SystemTextJson.Bson/Akavache.SystemTextJson.Bson.csproj Adds new BSON project with Newtonsoft.Json.Bson dependencies.
src/Akavache.Sqlite3/SqliteAkavacheTransaction.cs Adds sqlite implementation of the new transaction abstraction.
src/Akavache.Sqlite3/SqliteAkavacheConnection.cs Adds sqlite implementation of the new connection abstraction and legacy V10 reads.
src/Akavache.Sqlite3/AkavacheBuilderExtensions.cs Refactors sqlite cache creation and exposes internals for testing/migration.
src/Akavache.Sqlite3/Akavache.Sqlite3.csproj Adds InternalsVisibleTo for migration project.
src/Akavache.Settings/Akavache.Settings.csproj Adds InternalsVisibleTo for test projects.
src/Akavache.Settings.Tests/SettingsCacheTests.cs Updates nullability annotations for applicationName pass-through.
src/Akavache.Settings.Tests/SettingsBaseFallbackTests.cs Updates nullability annotations for applicationName pass-through.
src/Akavache.Settings.Tests/EncryptedSettingsCacheTests.cs Updates nullability; adjusts expectations for obsolete assembly metadata defaults.
src/Akavache.Settings.Tests/AkavacheBuilderExtensionsTests.cs Updates nullability annotations for applicationName pass-through.
src/Akavache.Settings.Tests/Akavache.Settings.Tests.csproj Adds CS0618 suppression.
src/Akavache.NewtonsoftJson/NewtonsoftSerializer.cs Refactors BSON/JSON fallback paths and exposes internals for tests.
src/Akavache.NewtonsoftJson/NewtonsoftDateTimeTickConverter.cs Simplifies DateTime kind switch logic/comments.
src/Akavache.NewtonsoftJson/NewtonsoftDateTimeOffsetTickConverter.cs Refactors object-token reading via JObject and exposes helper for tests.
src/Akavache.NewtonsoftJson/AkavacheBuilderExtensions.cs Registers Newtonsoft serializers with UniversalSerializer.
src/Akavache.NewtonsoftJson/Akavache.NewtonsoftJson.csproj Adds InternalsVisibleTo for tests.
src/Akavache.EncryptedSqlite3/Akavache.EncryptedSqlite3.csproj Adds InternalsVisibleTo; includes new connection/transaction compilation units.
src/Akavache.Drawing/ImageCacheExtensions.cs Replaces ad-hoc null checks with shared argument helper; refactors image buffer validation call.
src/Akavache.Drawing/BitmapImageExtensions.cs Refactors image buffer validation into internal helpers and improves docs.
src/Akavache.Drawing/Akavache.Drawing.csproj Adds InternalsVisibleTo for tests.
src/Akavache.Core/SerializerExtensions.cs Removes reflection-based APIs and standardizes null-guards; extracts ShouldRefetchCachedValue.
src/Akavache.Core/RelativeTimeExtensions.cs Standardizes null-guards via helper and expands method bodies for testability.
src/Akavache.Core/Polyfills/UnconditionalSuppressMessageAttribute.cs Adds polyfill to enable consistent attribute usage on older TFMs.
src/Akavache.Core/Polyfills/RequiresUnreferencedCodeAttribute.cs Adds polyfill to enable consistent attribute usage on older TFMs.
src/Akavache.Core/Polyfills/RequiresDynamicCodeAttribute.cs Adds polyfill to enable consistent attribute usage on older TFMs.
src/Akavache.Core/InMemoryBlobCacheBase.cs Extracts vacuum logic into internal static helpers for isolated testing.
src/Akavache.Core/ImageExtensions.cs Standardizes null-guards and consolidates image buffer validation helpers.
src/Akavache.Core/ISerializer.cs Adds JsonTypeInfo overloads as default interface methods for AOT-friendly usage.
src/Akavache.Core/IAkavacheTransaction.cs Adds new transaction abstraction for backend implementations.
src/Akavache.Core/IAkavacheInstance.cs Obsoletes assembly metadata properties and documents sentinel defaults.
src/Akavache.Core/IAkavacheConnection.cs Adds new async connection abstraction and operational methods (checkpoint/compact/etc.).
src/Akavache.Core/IAkavacheBuilder.cs Adds WithExecutingAssembly and updates serializer API surface docs.
src/Akavache.Core/HttpService.cs Extracts request/response helpers and opens MakeWebRequest for testing.
src/Akavache.Core/Helpers/DateTimeHelpers.cs Extracts DateTime conversion/recovery logic for shared reuse and testing.
src/Akavache.Core/Helpers/BinaryHelpers.cs Adds endian-explicit binary read helpers for legacy framework support.
src/Akavache.Core/Helpers/AssemblyInfoHelper.cs Adds obsolete reflection-based assembly helper retained for compat.
src/Akavache.Core/Helpers/ArgumentExceptionHelper.cs Adds shared argument validation helper for consistent guard patterns.
src/Akavache.Core/Core/SecurityUtilities.cs Removes redundant empty-name checks (relies on existing guards).
src/Akavache.Core/Core/RequestCache.cs Exposes internal removal helper for tests.
src/Akavache.Core/Core/AkavacheBuilder.cs Removes reflection-based discovery defaults; adds explicit executing assembly config and testable internals.
src/Akavache.Core/CheckpointMode.cs Adds public enum for checkpoint strength abstraction.
src/Akavache.Core/CacheDatabase.cs Requires explicit application name on initialization/builder creation; improves nullability invariants.
src/Akavache.Core/Akavache.csproj Adds InternalsVisibleTo for migration project.
src/Akavache.Benchmarks.V10/Akavache.Benchmarks.V10.csproj Marks benchmarks as not AOT-compatible and disables trim/AOT analyzers.
docs/migration-v11-to-v12.md Adds v11→v12 migration documentation, including package split and serializer API changes.
README.md Adds link to the new v11→v12 migration guide and clarifies v10→v11 wording.

@glennawatson glennawatson force-pushed the feature/akavache-v10-to-v11-compat branch 3 times, most recently from 754f088 to 47b4fba Compare April 13, 2026 01:23
@glennawatson glennawatson force-pushed the feature/akavache-v10-to-v11-compat branch from 47b4fba to 03d28d5 Compare April 13, 2026 04:00
@glennawatson glennawatson force-pushed the feature/akavache-v10-to-v11-compat branch from 03d28d5 to 8ec5fc9 Compare April 13, 2026 04:31
@glennawatson glennawatson force-pushed the feature/akavache-v10-to-v11-compat branch 4 times, most recently from f0ef88b to 2f7d4d0 Compare April 13, 2026 05:12
…nization

- Require explicit application name on CacheDatabase.Initialize and CreateBuilder APIs
- Deprecate parameterless CreateBuilder() overload via [Obsolete]
- Remove reflection-based SerializerExtensions.Get(IBlobCache, string, Type) method
- Revert non-generic Deserialize(byte[], Type) from ISerializer interface
- Split SystemTextJson.Bson into separate project (Akavache.SystemTextJson.Bson)
- Add Akavache.V10toV11 migration project
- Add polyfills for RequiresUnreferencedCode/RequiresDynamicCode on net462/net472/net481
- Replace Assembly.Location with AppContext.BaseDirectory + AssemblyFileVersionAttribute
- Introduce IAkavacheConnection / IAkavacheTransaction abstractions so SqliteBlobCache
  can be driven by an in-memory connection in unit tests without touching disk
- Add SqliteAkavacheConnection / SqliteAkavacheTransaction real-SQLite implementations
  and InMemoryAkavacheConnection / InMemoryAkavacheTransaction test mocks
- Extract pure helpers (DateTimeHelpers, BinaryHelpers, AssemblyInfoHelper,
  ArgumentExceptionHelper) from UniversalSerializer/NewtonsoftSerializer/
  SystemJsonBsonSerializer so the edge-case branches can be unit-tested directly
- Deprecate reflection-based IAkavacheInstance.ExecutingAssembly/Name/Version and
  add AOT-safe IAkavacheBuilder.WithExecutingAssembly(Assembly) for explicit
  caller-owned assembly metadata; the builder no longer probes
  Assembly.GetEntryAssembly during construction
- Convert method-group Select(...) calls in ImageExtensions/BitmapImageExtensions
  to explicit static lambdas and make ThrowOnBadImageBuffer/BytesToImage internal
  static so branch coverage instrumentation sees every path
- Promote SqliteBlobCache's private helpers (Initialize, TryGetLegacyValueAsync)
  plus new GetOrCreateHttpService/ToExpiryValue to internal static methods taking
  explicit IAkavacheConnection/IScheduler parameters, and add direct tests for both
  the Sqlite3 and EncryptedSqlite3 variants
- Refactor SystemJsonBsonSerializer wrapper unwrap to an internal static helper
  (TryUnwrapObjectWrapper) and NewtonsoftSerializer's equivalent
  (TryUnwrapSimpleObjectWrapper) with direct tests for both happy and error paths
- Add AkavacheTestExecutor + [NotInParallel(\"CacheDatabaseState\")] wiring so every
  test that mutates CacheDatabase / AkavacheBuilder / AppLocator / UniversalSerializer
  global state serialises against the same group and no longer races
- Fix flaky TryAlternativeSerializers test by moving UniversalSerializerTests into
  the shared CacheDatabaseState group
- Mark IsolatedStorage cache directory methods with [ExcludeFromCodeCoverage]
- Add [ExcludeFromCodeCoverage] to unreachable platform-dependent paths
- Add NoWarn CS0618 to test projects for intentional deprecated API testing
- Fix NewtonsoftSerializer.TryDeserializeFromOtherFormats value-type BSON check
- Increase test count from ~1600 to 3726 with 0 failures
- Reach 100% line and 100% branch coverage across every production assembly
- Build produces 0 warnings and 0 errors across all target frameworks
@glennawatson glennawatson force-pushed the feature/akavache-v10-to-v11-compat branch from 2f7d4d0 to 3740685 Compare April 13, 2026 05:21
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (ec39b90) to head (14c47fe).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##             main     #1178       +/-   ##
============================================
+ Coverage   56.34%   100.00%   +43.65%     
============================================
  Files          34        41        +7     
  Lines        3143      2864      -279     
  Branches      739       506      -233     
============================================
+ Hits         1771      2864     +1093     
+ Misses       1116         0     -1116     
+ Partials      256         0      -256     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…elpers

Introduce AkavacheTestExecutorBase following the ReactiveUI BuilderTestExecutorBase
pattern so global-state cleanup (CacheDatabase, AkavacheBuilder, Splat locator)
lives in one place with virtual ResetStateAsync/ConfigureBuilder hooks. The
standard AkavacheTestExecutor is now a sealed subclass. ResetStateAsync swaps in
a fresh ModernDependencyResolver so contract-based ISerializer registrations no
longer leak between tests.

Also splits SettingsBase.GetBlobCacheForClass into internal helpers and
SettingsStorage.InitializeAsync into an EagerLoadProperties helper so each
strategy can be unit tested directly, and adds coverage tests for both.
@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines:
1 pipeline(s) were filtered out due to trigger conditions.

AkavacheTestExecutorBase.ResetStateAsync swaps in a fresh ModernDependencyResolver
each run but previously abandoned the old one. Capture the previous locator via
AppLocator.GetLocator() before replacement and dispose it if it implements
IDisposable, so test runs don't accumulate orphaned resolver state.
…ttpServer

SettingsBase gains a second constructor that accepts explicit Func<IBlobCache>
resolvers for UserAccount / LocalMachine / InMemory, and GetBlobCacheForClass /
TryGetFromCacheDatabase now have overloads that take those delegates. The
parameterless constructor keeps wiring the ambient CacheDatabase resolvers, but
each resolver is wrapped in TryReadAmbientCache so one throwing getter no longer
short-circuits the remaining fallbacks (previously a bug: LocalMachine and
InMemory paths were unreachable when UserAccount threw). Adds direct coverage
tests for every branch plus EagerLoad, InitializeAsync, and the DeleteSettingsStore
catch path.

Replaces the HttpListener-based TestHttpServer with a TcpListener implementation.
On .NET 8 Linux the managed HttpListener races its own read completion against
Stop/Close and raises a NullReferenceException from HttpConnection.get_LocalEndPoint
on a ThreadPool worker — unhandled, process-terminating, and impossible to catch
from user code. A minimal TCP server eliminates the race entirely.
… gating

Drops the assembly-wide [NotInParallel] attribute on Akavache.Tests and
Akavache.Settings.Tests — it forced every test in the assembly to run serially,
which defeated TUnit's per-class parallel grouping and turned full runs into
3-minute jobs. Parallel gating is now per-class, scoped to the resource each
class actually contends for (CacheDatabaseState, NativeSqlite, BitmapLoader).

Converts every Sqlite/Encrypted blob cache test that previously opened a real
.db file to an InMemoryAkavacheConnection via the existing IAkavacheConnection
overload. This removes the native SQLCipher / SQLite provider from the parallel
path, which was the source of the intermittent AccessViolationException on CI
(the native provider is not safe under concurrent access from multiple managed
connections pointing at separate DBs). The genuine integration tests that still
need a real native DB — builder extensions, smoke tests, connection tests,
serialization compatibility, login extensions, update expiration — are marked
[NotInParallel("NativeSqlite")] so they serialize against each other.

Image/bitmap tests are marked [NotInParallel("BitmapLoader")] since they swap
the global BitmapLoader.Current for the duration of each test.

Main suite drops from ~2m50s to ~34s per TFM; coverage stays at 100%/100%.
- Repo restructure: tests/, benchmarks/, samples/, compat/ subfolders under src/; updated slnx, csproj refs, CLAUDE.md, codecov.yml; moved BENCHMARK_REPORT.md and PERFORMANCE_SUMMARY.md to docs/
- Enabled strict CA Design/Performance/Reliability/Usage/Security/Maintainability/Globalization/Interoperability/Naming, SYSLIB obsoletions, IDE language/naming rules, and a curated ReSharper inspection set in .editorconfig
- Comprehensive RCS rule pass: enabled simplification/quality/performance/maintainability/documentation categories, disabled rules with a CA/SA equivalent, downgraded RCS1223 to suggestion, disabled RCS1046/RCS1255/RCS1226/RCS1249 with rationale
- Added <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> to Directory.Build.props so IDE* analyzers fire at compile time
- Test-only editorconfig overrides: disabled IDE0200 (TUnit Assert.That method-group binding silently misses throw assertions), RCS1079 (intentional in fakes), RCS1223 (no DebuggerDisplay on test fixtures)
- Akavache.Sqlite3/SqliteBlobCache.cs cleanup: moved using inside #if ENCRYPTED, replaced lazy backing field with C# 13 'field' keyword on HttpService property
- Added Akavache.Core/Polyfills/HashCode.cs (xxHash32-based, NETFRAMEWORK gate, Combine<T1..T4> + AddBytes); Size.GetHashCode now delegates to HashCode.Combine
- Removed obsolete PreserveAttribute (pre-MAUI linker hint) and its tests; cleaned up its sole usage in SqliteBlobCache
- Suppressed CA2234 in tests where Akavache deliberately exercises the string-URL public API overloads; fixed Splat UnregisterAll(typeof) → UnregisterAll<>
- Configured CA1852 with api_surface=private,internal so it only flags non-public; sealed 5 mock/test types
- Inverted ArgumentExceptionHelper, DateTimeHelpers, AkavacheBuilder, and InMemoryBlobCacheBase null-guards to early-return form (RCS1208)
- Sample WPF/MAUI value converters: replaced NotImplementedException ConvertBack with Binding.DoNothing / BindableProperty.UnsetValue (canonical one-way-binding sentinels recognised by the binding engine)
- Test helper TaskAssertionExtensions.ShouldThrowAsync<TException> bypasses TUnit's Func<Task<T?>> overload (Task<T>/Task<T?> covariance); migrated 172 sites across 5 test files to fix all 344 CS8619 warnings
- InMemoryBlobCacheBaseTests: converted try/finally DisposeAsync patterns to await using
- Fixed test stub Assembly.GetCustomAttributes overrides to return Attribute[] (Array.Empty<Attribute>(), not [] which infers object[]) so System.Attribute.GetCustomAttribute<T>() succeeds
- Corrected expected exception types: WithInMemoryShouldThrowWhenNoSerializerConfigured and StringConstructorShouldThrowWhenSerializerNotRegistered now expect ArgumentNullException / InvalidOperationException to match production
- Restored using-statement preference and minor IDE0005/SA1515/SA1116 cleanups; ConcurrencyTests methods made instance (TUnit source-gen requires non-static), class sealed
- All 3814 tests pass (3730 Akavache.Tests + 84 Akavache.Settings.Tests on net8/net9)
- Removed obsolete `AdditionalConcurrencyTests.cs`
- Applied modern syntax improvements (e.g., target-typed `new()`, inline null checks, `static` lambdas)
- Replaced conditional logic with shorter return statements
- Consolidated redundant null and empty checks
- Added `DebuggerDisplay` to `Size` struct for improved debugging
- Updated SQLite connection checkpoint calls
- Removed unnecessary parameter validations; replaced repetitive guards with `ArgumentExceptionHelper.ThrowIfNull`
- Simplified test classes, cleanup, and structure to align with project standards
- Ensured full test pass against all configurations
- Deleted obsolete `RunCompatTest.ps1` and redundant `tests.runsettings`
- Simplified and modernized syntax across files (`u8.ToArray` for readability, inline validations, etc.)
- Consolidated null/empty checks into `ArgumentExceptionHelper`
- Added missing test cases for `IsWebP` functionality
- Optimized validation logic and helper utility functions
- Enhanced test reliability and reduced unnecessary overhead
- Ensured consistency with the latest codebase standards and conventions
- Updated syntax with range expressions ([..]), `static` lambdas, and modern inline validations
- Streamlined type references for consistency (e.g., `Akavache.*` → appropriate namespaces)
- Optimized object initialization and refactored repetitive code patterns
- Updated comments and XML docs to match project standards
- Enhanced overall readability and reduced potential code duplication
…d test for lazy initialization of HttpService property
@glennawatson glennawatson merged commit 6dee936 into main Apr 14, 2026
14 of 15 checks passed
@glennawatson glennawatson deleted the feature/akavache-v10-to-v11-compat branch April 14, 2026 21:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants