This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Restore, build, and test (standard workflow)
dotnet restore
dotnet build
dotnet test
# Run a single test
dotnet test --filter "FullyQualifiedName~TestMethodName"
# Build specific configuration
dotnet build -c ReleaseThis is a .NET library (ktsu.Essentials) providing high-performance interfaces and implementations for common cross-cutting concerns: compression, encoding, obfuscation, encryption, hashing, serialization, caching, persistence, validation, logging, navigation, randomness, probability distributions, command execution, and filesystem access. The solution uses:
- ktsu.Sdk - Custom SDK providing shared build configuration
- MSTest.Sdk - Test project SDK with Microsoft Testing Platform
- Multi-targeting: net10.0, net9.0, net8.0, net7.0, net6.0, netstandard2.1
Essentials/ICompressionProvider.cs- Compression/decompression interface with Span, Stream, and string supportEssentials/IEncodingProvider.cs- Format/transport encoding interface (Base64, Hex)Essentials/IObfuscationProvider.cs- Reversible obfuscation interface (NOT encryption); implementations compose encoding transforms and simple byte operationsEssentials/IEncryptionProvider.cs- Encryption/decryption interface with key/IV managementEssentials/IHashProvider.cs- Hashing interface with configurable output lengthEssentials/IIncrementalHash.cs- Chunk-by-chunk hashing contract;IHashProvider.CreateIncremental()returns oneEssentials/IncrementalHashAdapter.cs- Public adapter overSystem.Security.Cryptography.IncrementalHash, shared by the cryptographic hash providersEssentials/BufferingIncrementalHash.cs- Internal buffering fallback behind theCreateIncremental()default bodyShared/NonCryptoIncrementalHash.cs- Adapter overNonCryptographicHashAlgorithm, linked into the sixSystem.IO.Hashingproviders rather than placed in the interfaces-only packageEssentials/IKeyedHashProvider.cs- Keyed hashing (HMAC) interface for authenticating data with a secret keyEssentials/FixedTimeComparison.cs- Static fixed-time byte comparison for tags obtained outsideIKeyedHashProvider.VerifyShared/HmacKeyedHashCore.cs- HMAC implementation shared across algorithms, linked into the three keyed hash provider projects rather than placed in the interfaces packageEssentials/IRandomProvider.cs- Uniform randomness interface.NextBytes(Span<byte>)is the only required member; the integer, floating-point, shuffle, choice and sampling members are default implementations over it. Deliberately has no async tierEssentials/RandomHelpers.cs- Internal helpers behind those defaults: unbiased bounded draws (Lemire for 32 bits, rejection for 64), finite/probability/weight validationEssentials/IDistribution.cs-IDistribution<T>, the shared contract of a univariate distribution: moments, support, CDF, survival function, quantile, median and samplingEssentials/IContinuousDistribution.cs-IDistribution<double>plusPdf/LogPdfEssentials/IDiscreteDistribution.cs-IDistribution<int>plusPmf/LogPmf, and a defaultQuantilethat bisects the CDF. That default is an explicit reimplementation ofIDistribution<int>.Quantile, which CA1033 misreads as an explicit implementation on a class; the suppression on it says whyShared/DeflateBound.cs- zlib'sdeflateBoundplus container overhead, linked into the Deflate, Gzip and ZLib providers as theirGetMaxCompressedLengthShared/SpecialFunctions.cs- Incomplete gamma, incomplete beta, error function, normal quantile and cancellation-freeExpM1/Log1P, linked into the Normal, LogNormal, Exponential, Binomial, Poisson and Geometric providers rather than placed in the interfaces-only packageShared/DistributionArguments.cs- Parameter validation shared by every distribution provider, linked in the same wayShared/PersistenceDefaults.cs- TheIPersistenceProviderdefaultRetrieveOrCreateAsyncbody, linked into the FileSystem and Temp providers, which declare that member themselves and so hide the defaultEssentials/ISerializationProvider.cs- Object serialization/deserialization interfaceEssentials/ISerializationOptions.cs- Configurable serialization options (naming, inclusion, boxing policies)Essentials/ICacheProvider.cs- Generic cache interface with expiration and get-or-addEssentials/IPersistenceProvider.cs- Object persistence interface with pluggable backendsEssentials/IValidationProvider.cs- Validation interface with structured resultsEssentials/ILoggingProvider.cs- Logging interface with six severity levelsEssentials/UserDirectories.cs- Public XDG base directory helpers: data, config, state and cache homes, each with an application-directory variant. The persistence providers use the first twoEssentials/INavigationProvider.cs- Browser-like back/forward navigation interfaceEssentials/ICommandExecutor.cs- Shell command execution interface;Execute(command, environmentVariables, workingDirectory, cancellationToken)is the synchronous primitive that every other synchronous member composes overEssentials/IFileSystemProvider.cs- Filesystem abstraction extending Testably.AbstractionsEssentials/ProviderHelpers.cs- Internal utilities for async wrapping, stream bridging, UTF8 transformsEssentials/PersistenceProviderUtilities.cs- Shared utilities for persistence providers (safe filenames, culture-independent key formatting viaFormatKey, and key conversion back from a filename)Essentials/PersistenceProviderException.cs- Custom exception for persistence operations
Each provider implementation ships as its own project/package named Essentials.<Category>.<Impl> (NuGet id ktsu.Essentials.<Category>.<Impl>):
- CompressionProviders: Gzip, Brotli, Deflate, ZLib (ZLib targets net6.0+ only, not netstandard2.1)
- EncodingProviders: Base64, Hex
- ObfuscationProviders: Xor, Caesar, Reverse, BitRotate, Base64, Hex, Composite
- EncryptionProviders: Aes
- HashProviders: MD5, SHA1, SHA256, SHA384, SHA512, FNV1_32, FNV1a_32, FNV1_64, FNV1a_64, CRC32, CRC64, XxHash32, XxHash64, XxHash3, XxHash128
- KeyedHashProviders: HmacSha256, HmacSha384, HmacSha512
- RandomProviders: Native (System.Random), Crypto (RandomNumberGenerator), Xoshiro (xoshiro256**), Pcg (PCG-XSH-RR). Xoshiro and Pcg write the algorithm out, so a seed replays identically across platforms and framework versions, which
System.Randomdoes not promise. The three stateful providers register as transients because they are not thread-safe; Crypto is stateless and registers as a singleton - DistributionProviders: Uniform, Normal, Exponential, LogNormal, Triangular (continuous); Bernoulli, Binomial, Poisson, Geometric, Categorical (discrete). Triangular, Binomial and Categorical need parameters with no standard default and are excluded from
AddEssentials(), like the composite obfuscator - SerializationProviders: Json (System.Text.Json; takes a
JsonSerializerOptions, copied, or defaults to indented output), NewtonsoftJson, Yaml, Toml - FileSystemProviders: Native, InMemory (Testably.Abstractions.Testing's
MockFileSystem; excluded fromAddEssentials()because resolving oneIFileSystemProviderreturns the last registered, and an app that silently wrote to memory would lose its data) - CommandExecutors: Native
- LoggingProviders: Console, File (rolling, one line per entry, through
IFileSystemProvider; excluded fromAddEssentials()because it needs a directory). See The file logger - CacheProviders: InMemory
- NavigationProviders: InMemory
- PersistenceProviders: DataHome, ConfigHome, FileSystem, InMemory, Temp (DataHome and ConfigHome resolve XDG paths —
$XDG_DATA_HOMEelse~/.local/share/<app>,$XDG_CONFIG_HOMEelse~/.config/<app>— with the same layout on every platform;~is%USERPROFILE%on Windows. Both delegate storage toFileSystemPersistenceProvider.)
The Essentials.All project (ktsu.Essentials.All) is a meta-package that references every provider implementation for a one-install "batteries-included" experience; consumers can otherwise cherry-pick individual provider packages.
Interfaces are defined in the ktsu.Essentials namespace (in the Essentials/ directory). Each provider implementation lives in its own directory Essentials.<Category>.<Impl>/, in namespace ktsu.Essentials.<Category>.<Impl>, with the class named <Impl><Category-singular>Provider. For example, the SHA-256 hash provider is class SHA256HashProvider in namespace ktsu.Essentials.HashProviders.SHA256, and the XOR obfuscator is class XorObfuscationProvider in ktsu.Essentials.ObfuscationProviders.Xor. Higher-level concerns compose primitives rather than duplicating them: configuration is an IPersistenceProvider<TKey> over a serializer, and each obfuscator composes an encoding transform or a simple reversible byte operation. Obfuscators that wrap an IEncodingProvider (Base64, Hex) keep both a parameterless and an encoder-accepting constructor public and are registered via a DI factory lambda to avoid greedy-constructor selection.
- Testably.Abstractions - Base
IFileSysteminterface for filesystem abstraction - Polyfill - Backports of newer .NET APIs for older target frameworks
- Microsoft.SourceLink.GitHub / AzureRepos.Git - Source link support for debugging
All provider interfaces follow a consistent three-tier pattern:
-
Core Try* methods: Buffer-based methods over
Span<byte>orStream. Span overloads arebool TryX(source, destination, out int bytesWritten), paired with aGetMax…Lengthbound per category so callers can size buffers. These are the only methods implementers must provide. -
Convenience methods: Self-allocating methods that call Try* methods and manage buffers automatically. Provided via default interface implementations.
-
Async variants: Task-based async versions with
CancellationTokensupport. The stream paths of the compression providers, ofAesEncryptionProviderand of both encoding providers, along withIHashProvider.TryHashAsync(Stream, ...)andIKeyedHashProvider.TryHashAsync(ReadOnlyMemory<byte>, Stream, ...), are genuinely asynchronous — realReadAsync/WriteAsync, no thread held. The rest — obfuscation, serialization, and everything operating on buffers already in memory — are stillTask.Runwrappers over synchronous work viaProviderHelpers.RunAsync(); see issue #8.The two encoding providers each mirror their own synchronous memory profile rather than sharing one shape.
HexEncodingProvidertransforms a chunk at a time, because hex maps one byte to two and a chunk boundary never splits a pair; its decoder tops each read up to an even length first, since aReadAsyncmay legally return fewer bytes than asked for and treating that as end-of-stream would truncate.Base64EncodingProviderbuffers the whole input, because Base64 maps three bytes to four and a chunked transform would have to carry a partial group across every boundary — which is what its synchronous path does too. A provider makes its stream paths genuine by declaring the twoTry…Async(Stream, Stream, ...)primitives itself, which replaces the default implementation; the four derived stream defaults compose over those primitives, so overriding two members converts all six. Span-destination async overloads do not exist — anoutparameter cannot cross an async boundary.
IRandomProvider and the distribution interfaces have no async tier at all, and that is a decision
rather than an omission. The other interfaces offer async variants because they front I/O; a draw from a
generator or an evaluation of a CDF is a handful of arithmetic instructions over in-memory state, so a
Task.Run wrapper would cost orders of magnitude more than the work it wraps. Do not add one.
Where a distribution's tail is small, SurvivalFunction is declared rather than inherited. The default
subtracts the CDF from one, which discards the tail to cancellation: the normal survival function at 8
sigma is 6.2e-16, and 1 - 0.9999999999999994 keeps about one digit of it. All ten
providers compute the tail directly, and the tests assert the difference.
ICommandExecutor is the one interface with the mirror-image concern: synchronous methods layered over an
asynchronous one. It declares a synchronous primitive, Execute(string, IReadOnlyDictionary<string, string>?, string?, CancellationToken), that the other two synchronous members compose over. Its default body bridges to
ExecuteAsync with GetAwaiter().GetResult() — not .Result, which wraps the failure in an
AggregateException — and still blocks a thread for the child process's lifetime. NativeCommandExecutor
declares the primitive itself and drives System.Diagnostics.Process synchronously (a WaitForExit(timeout)
poll that honours the cancellation token, with a dedicated thread per redirected stream reading it to the end),
so no pool thread is held; declaring that one member converts all three synchronous overloads. See issue #17.
The reader threads read raw rather than going through the line-splitting OutputDataReceived callbacks, so the
synchronous path returns byte-for-byte what the child wrote — the same text ExecuteAsync returns — instead of
rewriting the terminators and adding one of its own. See issue #23.
On Unix the command reaches /bin/sh -c as one argument, byte for byte: through ProcessStartInfo.ArgumentList on
netcoreapp2.1+, and through the MSVCRT-correct QuoteArgument on netstandard2.1, which has no ArgumentList. See issue #54.
Common patterns are centralized in ProviderHelpers.cs:
RunAsync()- Wraps sync methods inTask.Runwith cancellation. Used by the in-memory async variants and by any stream path whose provider has not declared its own asynchronous primitives.ExecuteToByteArray()- Calls a try-operation with a MemoryStream destinationSpanToStreamBridge()- Bridges Span input to Stream-based operationsUtf8Transform()- Applies byte operations to UTF8 strings
FileLoggingProvider opens, appends to and closes the file for each entry rather than holding it open. That
costs a little per entry and buys three things: another program can read or delete the log while the app
runs, nothing needs disposing, and an I/O failure on one entry cannot leave a broken handle for the next.
Failures (IOException, UnauthorizedAccessException) are swallowed and counted in DroppedEntryCount,
because a logger that throws when the disk fills turns a warning into a crash. Entries are serialized by a
lock within one instance; rolling is not coordinated across processes.
Every entry is one line: the message and exception text escape \, \r, \n, U+0085, U+2028 and U+2029.
Write those escapes in C# source as '\u2028', never as the literal character: a raw U+2028 in source ends
the line, including inside a /// comment, which is how the doc comment broke while this was written.
Rolling happens before an entry that would push a non-empty file past MaxFileSizeBytes, so no file exceeds
the limit unless a single entry does, and such an entry is written whole to a fresh file. MaxFileCount
counts the current file, so 1 means a full file is deleted and started again.
Directory.Build.targets names a PackageValidationBaselineVersion for every project, and a package that
has never shipped has no such version on nuget.org, so its restore fails with NU1101. A new provider
project sets DisablePackageBaselineValidation in its own csproj, with a comment, and drops it once the
baseline version moves past its first release. Essentials.FileSystemProviders.InMemory and
Essentials.LoggingProviders.File carry it today.
The codebase uses [SuppressMessage] attributes for APIs not available in netstandard2.1 (e.g., CA1510 for ArgumentNullException throw helpers). The Polyfill package backports newer APIs where possible. When adding new code, verify availability in netstandard2.1 and use #if directives if needed.
Tests use MSTest.Sdk targeting net10.0 only. The test project (Essentials.Tests/) references all provider implementations and tests them through the interface contracts. Key test files:
HashProviderTests.cs- Tests all 15 hash provider implementationsIncrementalHashTests.cs- TestsCreateIncremental()and async stream hashing across all 15 hash providers, asserting incremental output equals one-shot outputKeyedHashProviderTests.cs- Tests all 3 HMAC keyed hash providers,Verify, andFixedTimeComparisonRandomProviderTests.cs- Contract tests over all 4 random providers (bounds, validation, shuffle and sampling invariants, uniformity of a range that does not divide 2^32), plus reference sequences for Xoshiro and Pcg produced by an independent transcription of each published algorithm. Those vectors make a seeded sequence part of the package contract: changing one is a breaking change and this is where it surfacesDistributionProviderTests.cs- Tests all 10 distributions three ways: contract properties every distribution must have (CDF bounded and non-decreasing, quantile inverts it, density integrates to one, masses sum to one, survival function complements the CDF), reference values computed outside the codebase, and empirical checks that seeded samples match the analytic moments and decilesCacheProviderTests.cs- Tests cache operations including expiration, and that a legitimately cachednullis honoured rather than treated as a miss:Get/GetAsyncreturn it instead of throwing, andGetOrAdd/GetOrAddAsyncreturn it without re-invoking the factory.TryGet'sboolis the sole authority on presence — a null check on the value is not a proxy for it. It also pins that an expired entry's removal leaves a value set concurrently under the same key in place (a key type that runs theSetbetweenTryGet's read and its remove makes that interleaving deterministic), and that aTimeSpan.MaxValueorTimeSpan.MinValuetime-to-live saturates rather than overflowingCommandExecutorTests.cs- Tests command execution, including the synchronous path, cancellation before and during a run (and that both paths kill a cancelled command rather than leaving it to finish, issue #53), a working directory that does not exist, that the synchronous and asynchronous paths capture the same bytes for a command whose output ends without a terminator, and thatExecuteAndGetOutputthrows unwrapped.ICommandExecutor's own synchronous defaults are reached through a test double that declares only the asynchronous members, sinceNativeCommandExecutorreplaces themEncodingProviderTests.cs- Tests Base64 and Hex encodingObfuscationProviderTests.cs- Tests all obfuscation providers via round-trip (obfuscate → deobfuscate)FileSystemProviderTests.cs- Tests filesystem operationsLoggingProviderTests.cs- Tests the console logging provider throughAddEssentials()FileLoggingProviderTests.cs- Tests the rolling file logger againstInMemoryFileSystemProvider: line format and escaping, rolling and retention, concurrency, dropped writes, DIInMemoryFileSystemProviderTests.cs- Isolation from disk and between instances; the shared contract inFileSystemProviderTests.csruns over it tooJsonSerializationOptionsTests.cs- TheJsonSerializerOptionsconstructor and DI overloadUserDirectoriesTests.cs- State and cache homes (data and config are covered inPersistenceProviderTests.cs)NavigationProviderTests.cs- Tests navigation stack behavior, including a nullable destination type where null is a legitimate step in the history rather than an unset current destinationPersistenceProviderTests.cs- Tests all persistence backendsSerializationProviderTests.cs- Tests serialization provider implementationsRoundTripTests.cs- Tests compression/encoding/encryption round-tripsDiTests.cs- Tests dependency injection registration
ktsu.Sdk.Analyzers requires a newer Roslyn than some installed SDKs carry. When it does not match,
every build fails with CSC : error CS9057: Analyzer assembly ... references version '5.9.0.0' of the compiler, which is newer than the currently running version — and the analyzers never run, so
KTSU**** findings are invisible until CI reports them. CI uses SDK 10.0.400, which carries Roslyn
5.9.
Rather than chase the SDK, override the compiler from NuGet. Put this in a file outside the repository and point MSBuild at it:
<Project>
<ItemGroup>
<PackageReference Include="Microsoft.Net.Compilers.Toolset" VersionOverride="5.9.0" PrivateAssets="all" />
</ItemGroup>
</Project>dotnet build Essentials.slnx -p:CustomAfterMicrosoftCommonProps=/path/to/roslyn59.propsNote After, not Before: projects here declare their SDK with <Sdk Name="..." /> elements
rather than the <Project Sdk="..."> attribute, which CustomBeforeMicrosoftCommonProps does not
reach. Keep the file out of the repository so normal builds, CI and packaging are unaffected.
KTSU0001 requires a System.Memory reference from every project using Span<T>/Memory<T>, and
the 47 projects targeting netstandard2.1 do. That reference cannot be added: NuGet rejects it
during solution restore with NU1510 — "This package is automatically available and does not need
to be referenced explicitly. Remove the PackageReference item." NoWarn metadata on the item does
not reach that check, so the two rules cannot both be satisfied. NuGet is the one describing
reality — the framework supplies the package — so KTSU0001 is suppressed instead, in
Directory.Build.targets, scoped to netstandard2.1.
It has to be Directory.Build.targets, not .props: ktsu.Sdk assigns NoWarn outright, and props
is imported before the SDK, so an addition there is silently overwritten. Check with
dotnet msbuild <proj> -p:TargetFramework=netstandard2.1 -getProperty:NoWarn if it ever stops
working.
CA1859 (use concrete types) is likewise wrong for Essentials.Tests and is in its NoWarn: the
providers are built on default interface implementations, which are only callable through the
interface, so binding a test to the concrete type would change or break what it dispatches to.
Uses scripts/PSBuild.psm1 PowerShell module for CI pipeline. Version increments are controlled by commit message tags: [major], [minor], [patch], [pre].
Do not add global suppressions for warnings. Use explicit suppression attributes with justifications when needed, with preprocessor defines only as fallback. Make the smallest, most targeted suppressions possible.