This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This project uses Microsoft Testing Platform (MTP) with the TUnit testing framework. Test commands differ significantly from traditional VSTest.
See: https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-test?tabs=dotnet-test-with-mtp
# Check .NET installation
dotnet --info
# Restore NuGet packages
dotnet restore Splat.slnxNote: This repository uses SLNX (XML-based solution format) instead of the legacy SLN format.
CRITICAL: The working folder must be ./src folder. These commands won't function properly without the correct working folder.
# Build the solution
dotnet build Splat.slnx -c Release
# Build with warnings as errors (includes StyleCop violations)
dotnet build Splat.slnx -c Release -warnaserror
# Clean the solution
dotnet clean Splat.slnxCRITICAL: This repository uses MTP configured in global.json. All TUnit-specific arguments must be passed after --:
The working folder must be ./src folder. These commands won't function properly without the correct working folder.
IMPORTANT:
- Do NOT use
--no-buildflag when running tests. Always build before testing to ensure all code changes (including test changes) are compiled. Using--no-buildcan cause tests to run against stale binaries and produce misleading results. - Use
--output Detailedto see Console.WriteLine output from tests. This must be placed BEFORE any--separator:dotnet test --output Detailed -- --treenode-filter "..."
# Run all tests in the solution
dotnet test --solution Splat.slnx -c Release
# Run all tests in a specific project
dotnet test --project tests/Splat.Tests/Splat.Tests.csproj -c Release
# Run a single test method using treenode-filter
# Syntax: /{AssemblyName}/{Namespace}/{ClassName}/{TestMethodName}
dotnet test --project tests/Splat.Tests/Splat.Tests.csproj -- --treenode-filter "/*/*/*/MyTestMethod"
# Run all tests in a specific class
dotnet test --project tests/Splat.Tests/Splat.Tests.csproj -- --treenode-filter "/*/*/MyClassName/*"
# Run tests in a specific namespace
dotnet test --project tests/Splat.Tests/Splat.Tests.csproj -- --treenode-filter "/*/MyNamespace/*/*"
# Filter by test property (e.g., Category)
dotnet test --solution Splat.slnx -- --treenode-filter "/*/*/*/*[Category=Integration]"
# Run tests with code coverage (Microsoft Code Coverage)
dotnet test --solution Splat.slnx -- --coverage --coverage-output-format cobertura
# Run tests with detailed output
dotnet test --solution Splat.slnx -- --output Detailed
# List all available tests without running them
dotnet test --project tests/Splat.Tests/Splat.Tests.csproj -- --list-tests
# Fail fast (stop on first failure)
dotnet test --solution Splat.slnx -- --fail-fast
# Control parallel test execution
dotnet test --solution Splat.slnx -- --maximum-parallel-tests 4
# Generate TRX report
dotnet test --solution Splat.slnx -- --report-trx
# Disable logo for cleaner output
dotnet test --project tests/Splat.Tests/Splat.Tests.csproj -- --disable-logo
# Combine options: coverage + TRX report + detailed output
dotnet test --solution Splat.slnx -- --coverage --coverage-output-format cobertura --report-trx --output DetailedAlternative: Using dotnet run for single project
# Run tests using dotnet run (easier for passing flags)
dotnet run --project tests/Splat.Tests/Splat.Tests.csproj -c Release -- --treenode-filter "/*/*/*/MyTest"
# Disable logo for cleaner output
dotnet run --project tests/Splat.Tests/Splat.Tests.csproj -- --disable-logo --treenode-filter "/*/*/*/Test1"The --treenode-filter follows the pattern: /{AssemblyName}/{Namespace}/{ClassName}/{TestMethodName}
Examples:
- Single test:
--treenode-filter "/*/*/*/MyTestMethod" - All tests in class:
--treenode-filter "/*/*/MyClassName/*" - All tests in namespace:
--treenode-filter "/*/MyNamespace/*/*" - Filter by property:
--treenode-filter "/*/*/*/*[Category=Integration]" - Multiple wildcards:
--treenode-filter "/*/*/MyTests*/*"
Note: Use single asterisks (*) to match segments. Double asterisks (/**) are not supported in treenode-filter.
--treenode-filter- Filter tests by path pattern or properties (syntax:/{Assembly}/{Namespace}/{Class}/{Method})--list-tests- Display available tests without running--fail-fast- Stop after first failure--maximum-parallel-tests- Limit concurrent execution (default: processor count)--coverage- Enable Microsoft Code Coverage--coverage-output-format- Set coverage format (cobertura, xml, coverage)--report-trx- Generate TRX format reports--output- Control verbosity (Normal or Detailed)--no-progress- Suppress progress reporting--disable-logo- Remove TUnit logo display--diagnostic- Enable diagnostic logging (Trace level)--timeout- Set global test timeout--reflection- Enable reflection mode instead of source generation
See https://tunit.dev/docs/reference/command-line-flags for complete TUnit flag reference.
global.json- Specifies"Microsoft.Testing.Platform"as the test runnertestconfig.json- Configures test execution ("parallel": true) and code coverage (Cobertura format)Directory.Build.props- EnablesTestingPlatformDotnetTestSupportfor test projects.github/COPILOT_INSTRUCTIONS.md- Comprehensive development guidelines
Splat is a cross-platform utility library providing abstractions for common functionality that should work everywhere but often doesn't. It solves the problem of writing cross-platform mobile and desktop code riddled with #ifdefs.
Core Library (Splat/)
ServiceLocation/- Simple, flexible service location/dependency injectionAppLocator- Main service locator for registering and retrieving servicesPlatformModeDetector/- Detect unit test runners and design mode- Core abstractions and utilities
Specialized Libraries
Splat.Builder/- AOT-friendly configuration withAppBuilderandIModulepatternSplat.Core/- Core interfaces and abstractionsSplat.Logging/- Cross-platform logging abstraction withILoggerinterfaceSplat.Drawing/- Cross-platform image loading/saving and color/geometry primitives
Dependency Injection Adapters (Splat.*/)
Splat.Autofac/- Autofac container integrationSplat.DryIoc/- DryIoc container integrationSplat.Microsoft.Extensions.DependencyInjection/- Microsoft.Extensions.DependencyInjection integrationSplat.Ninject/- Ninject container integrationSplat.SimpleInjector/- SimpleInjector container integrationSplat.Prism/- Prism framework integration
Logging Adapters (Splat.*/)
Splat.Serilog/- Serilog logger integrationSplat.NLog/- NLog logger integrationSplat.Log4Net/- Log4Net logger integrationSplat.Microsoft.Extensions.Logging/- Microsoft.Extensions.Logging integration
Application Performance Monitoring (Splat.*/)
Splat.AppCenter/- Microsoft App Center APM integrationSplat.ApplicationInsights/- Azure Application Insights integrationSplat.Exceptionless/- Exceptionless integrationSplat.Raygun/- Raygun integration
Service Location
AppLocator.Current- Retrieve registered servicesAppLocator.CurrentMutable- Register new services at runtimeRegisterLazySingleton- Register singletons that are created on first accessRegisterConstant- Register singleton instancesRegister- Register factory functions for transient instances
Logging
IEnableLogger- Tag interface for classes that want logging supportthis.Log().Info()/this.Log().Warn()- Extension methods for loggingLogHost.Default- Static logger for static methods- Platform-specific adapters forward to chosen logging framework
Cross-Platform Drawing
IBitmap- Platform-agnostic bitmap interfaceToNative()/FromNative()- Convert between Splat and platform typesBitmapLoader.Current- Load/save images from streams, resources, or filesSplatColor- Cross-platform color abstractionPointF,SizeF,RectangleF- Cross-platform geometry primitives
AppBuilder and IModule (AOT-Friendly)
IModule- Define reusable service registration modulesAppBuilder- Compose modules and apply to resolverUseCurrentSplatLocator()- Target currentAppLocator.CurrentMutable- AOT-safe configuration without reflection
The project uses granular target framework definitions in Directory.Build.props:
SplatModernTargets- net8.0, net9.0, net10.0 (modern cross-platform)SplatLegacyTargets- net462, net472, net481 (Windows-only legacy .NET Framework)SplatCoreTargets- Combines modern + legacy targets for core librariesSplatWindowsTargets- net8.0/9.0/10.0-windows10.0.17763.0 and windows10.0.19041.0SplatAndroidTargets- net9.0-android, net10.0-androidSplatAppleTargets- iOS, tvOS, macOS, Mac CatalystSplatUiFinalTargetFrameworks- OS-aware composition for UI projects
OS-Aware Builds: The build system automatically selects appropriate target frameworks based on the host OS:
- Linux: Builds .NET 8/9/10 and Android targets
- Windows: Builds all targets including .NET Framework, Core, Android, Windows, and Apple
- macOS: Builds .NET 8/9/10, Android, and Apple targets
CRITICAL: Prefer Zero-Reflection Solutions First
When implementing new features, follow this priority order:
- Zero-reflection solutions - Design APIs that don't require reflection at all
- Source generators - Use Roslyn source generators to generate code at compile-time
- Compile-time code generation - Use tools like T4 templates or build-time code generation
- Strongly-typed expressions - Use expression trees that can be analyzed without runtime reflection
- Reflection with AOT attributes - Only as a last resort, and always with proper
DynamicallyAccessedMembersattributes
Examples of Preferred Approaches:
Good: Zero-Reflection with Explicit Registration
// Users explicitly register their services - no reflection needed
AppLocator.CurrentMutable.Register<IMyService>(() => new MyService());Good: Source Generator Pattern
// Use source generators to generate registration code at compile-time
// See: Splat.DI.SourceGenerator package
[RegisterService(typeof(IMyService))]
public partial class MyService : IMyService
{
// Generator creates registration code automatically
}Good: Expression-Based with Static Analysis
// Expression trees can be analyzed without runtime reflection
public void RegisterService<T>(Expression<Func<T>> factory)
{
// Analyze expression at compile-time or with source generators
}Avoid: Runtime Reflection (unless properly attributed)
// Only use reflection when absolutely necessary and with proper attributes
#if NET6_0_OR_GREATER
private static void RegisterFromType(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
Type serviceType)
#else
private static void RegisterFromType(Type serviceType)
#endif
{
var instance = Activator.CreateInstance(serviceType);
}All code targeting net8.0+ should be AOT-compatible:
Key AOT Patterns:
- Always prefer zero-reflection solutions - design APIs that work without reflection
- Use source generators for code that would traditionally require reflection
- Prefer
DynamicallyAccessedMembersAttributeoverUnconditionalSuppressMessage - Use specific
DynamicallyAccessedMemberTypesvalues rather thanAllwhen possible AppBuilderandIModulepattern for AOT-safe dependency injection configuration- Avoid reflection in hot paths
- Document any reflection usage and why it's necessary
- See
tests/Splat.Aot.Tests/for AOT test examples
Source Generator References:
- Splat.DI.SourceGenerator: https://github.com/reactivemarbles/Splat.DI.SourceGenerator
- .NET Source Generators: https://learn.microsoft.com/dotnet/csharp/roslyn-sdk/source-generators-overview
See .github/COPILOT_INSTRUCTIONS.md for comprehensive AOT patterns and examples.
CRITICAL: All code must comply with ReactiveUI contribution guidelines: https://www.reactiveui.net/contribute/index.html
- EditorConfig rules (
.editorconfig) - comprehensive C# formatting and naming conventions - StyleCop Analyzers - builds fail on violations
- Roslynator Analyzers - additional code quality rules
- Analysis level: latest with enhanced .NET analyzers
WarningsAsErrors: nullable- All public APIs require XML documentation comments (including protected methods of public classes)
- Braces: Allman style (each brace on new line)
- Indentation: 4 spaces, no tabs
- Fields:
_camelCasefor private/internal,readonlywhere possible,static readonly(notreadonly static) - Visibility: Always explicit (e.g.,
private string _foonotstring _foo), visibility first modifier - Namespaces: File-scoped preferred, imports outside namespace, sorted (system then third-party)
- Types: Use keywords (
int,string) not BCL types (Int32,String) - Modern C#: Use nullable reference types, pattern matching, switch expressions, records, init setters, target-typed new, collection expressions, file-scoped namespaces, primary constructors
- Avoid
this.unless necessary - Use
nameof()instead of string literals - Use
varwhen it improves readability or aids refactoring
See .github/COPILOT_INSTRUCTIONS.md for complete style guide.
- Unit tests use TUnit framework with Microsoft Testing Platform
- Test projects detected via naming convention (project name contains
Tests) - Coverage configured in
testconfig.json(Cobertura format, skip auto-properties) - Parallel test execution enabled (
"parallel": truein testconfig.json) - Always write unit tests for new features or bug fixes
- Follow existing test patterns in
tests/Splat.Tests/ - For AOT scenarios, reference patterns in
tests/Splat.Aot.Tests/ - Platform-specific test runners available for Android and UWP
- Design for zero-reflection first - avoid reflection if at all possible
- Consider source generators - use compile-time code generation when needed
- Create failing tests first
- Implement minimal functionality
- Ensure AOT compatibility
- Update documentation if needed
- Add XML documentation to all public APIs
- Prefer to create new methods than changing the signature of existing ones when changing APIs
- Run formatting validation before committing
- Create reproduction test
- Fix with minimal changes
- Verify AOT compatibility
- Ensure no regression in existing tests
- Create new project following naming convention (e.g.,
Splat.NewContainer) - Implement adapter interface (
IMutableDependencyResolverfor DI, logger factory for logging) - Prefer zero-reflection implementations - design adapters to work without runtime reflection
- Create corresponding test project (e.g.,
Splat.NewContainer.Tests) - Add extension methods for registration (e.g.,
UseNewContainer()) - Update README.md with new adapter information
- Ensure adapter works with
AppBuilderpattern for AOT scenarios
- Runtime reflection - prefer zero-reflection solutions or source generators
- Implicit service discovery - explicit registration is better for AOT
- Heavy reflection without proper AOT suppression attributes (if reflection is absolutely necessary)
- Platform-specific code in core Splat library (use platform extensions instead)
- Breaking changes to public APIs without proper versioning
- Large dependencies in core libraries (keep Splat lightweight)
- Implicit service registrations that might surprise users or break AOT
- Zero-Reflection First: Always design for zero-reflection solutions before considering alternatives
- Source Generators: Leverage source generators for compile-time code generation instead of runtime reflection
- No shallow clones: Repository requires full clone for git version information used by Nerdbank.GitVersioning
- Required .NET SDKs: .NET 8.0, 9.0, and 10.0 (all three required for full build)
- SLNX Format: Uses modern XML-based solution format instead of legacy SLN
- Comprehensive Instructions:
.github/COPILOT_INSTRUCTIONS.mdcontains detailed development guidelines - Code Formatting: Always run
dotnet format whitespaceanddotnet format stylebefore committing
Philosophy: Splat provides "leaky abstractions" that solve cross-platform problems while always allowing access to native platform types via ToNative() and FromNative(). Keep abstractions minimal and focused on solving real cross-platform pain points. Prefer zero-reflection solutions and source generators over runtime reflection. When in doubt, prefer simplicity over feature completeness, explicit registration over implicit discovery, and always consider the AOT implications of your changes.