diff --git a/.build/definitions.cake b/.build/definitions.cake index b0cca17e7..e8f1c7d8a 100644 --- a/.build/definitions.cake +++ b/.build/definitions.cake @@ -1,21 +1,35 @@ // ADDINS -#addin nuget:?package=Cake.Coveralls&version=4.0.0 -#addin nuget:?package=Cake.FileHelpers&version=7.0.0 -#addin nuget:?package=Cake.AppVeyor&version=6.0.0 +#addin nuget:?package=Cake.Coverlet&version=6.0.1 +#addin nuget:?package=Cake.FileHelpers&version=9.0.0 +#addin nuget:?package=Cake.AppVeyor&version=10.0.0 // TOOLS -#tool nuget:?package=GitReleaseManager&version=0.20.0 -#tool nuget:?package=GitVersion.CommandLine&version=5.12.0 -#tool nuget:?package=OpenCover&version=4.7.1221 +#tool nuget:?package=GitReleaseManager.Tool&version=0.20.0 +#tool "dotnet:?package=GitVersion.Tool&version=6.0.0" #tool nuget:?package=ReportGenerator&version=5.4.8 public class CodeCoverageSettings { - public string ExcludeByFile { get; set; } = "*/*Designer.cs"; - public string ExcludeByAttribute { get; set; } = "*.ExcludeFromCodeCoverage*"; - public string ExcludeFilter { get; set; } = "-[Tests*]*;-[*]Microsoft.CodeAnalysis*;-[*]System.Runtime.CompilerServices.*"; - public string IncludeFilter { get; set; } + /// + /// Glob pattern to exclude source files from coverage (Coverlet format, e.g. "**/*Designer.cs"). + /// + public string ExcludeByFile { get; set; } = "**/*Designer.cs"; + + /// + /// Attribute short name used to exclude members from coverage (e.g. "ExcludeFromCodeCoverage"). + /// + public string ExcludeByAttribute { get; set; } = "ExcludeFromCodeCoverage"; + + /// + /// Coverlet exclude filters in [Assembly]Type format, e.g. "[Tests*]*". + /// + public List ExcludeFilter { get; set; } = new List { "[Tests*]*", "[*]Microsoft.CodeAnalysis*", "[*]System.Runtime.CompilerServices.*" }; + + /// + /// Coverlet include filters in [Assembly]Type format, e.g. "[SharpArch.*]*". + /// + public List IncludeFilter { get; set; } = new List(); } // params @@ -40,7 +54,7 @@ public class ProjectSettings { SolutionName = solutionName; CodeCoverage = new CodeCoverageSettings { - IncludeFilter = $"+[solutionName*]*" + IncludeFilter = new List { $"[{solutionName}*]*" } }; } } @@ -101,25 +115,26 @@ public class Paths { public DirectoryPath RootDir { get; } public string SrcDir { get; set; } public string ArtifactsDir { get; set; } - public string TestCoverageOutputFile { get; set; } + /// Glob matching the per-test-project, per-TargetFramework Cobertura XML files Coverlet writes + /// (e.g. coverage.SharpArch.XunitTests.net10.0.cobertura.xml). Each test project is run separately with a + /// unique CoverletOutputName so projects/TFMs don't overwrite each other. Consumed both by ReportGenerator + /// (local HTML report) and by the Coveralls upload loop. + public string TestCoverageGlobPattern { get; set; } public string TestCoverageReportDir { get; set; } public string PackagesDir { get; set; } public string BuildPropsFile { get; set; } - public string TestsRootDir { get; set; } - public string SamplesRootDir { get; set; } public string CommonAssemblyVersionFile { get; set; } public Paths(ICakeContext context) { RootDir = context.MakeAbsolute(context.Directory("./")); - SrcDir = RootDir.Combine("src").ToString(); + SrcDir = RootDir.Combine("Src").ToString(); ArtifactsDir = RootDir.Combine("artifacts").ToString(); - TestCoverageOutputFile = ArtifactsDir + "/OpenCover.xml"; + TestCoverageGlobPattern = ArtifactsDir + "/coverage.*.cobertura.*.xml"; TestCoverageReportDir = ArtifactsDir + "/CodeCoverageReport"; PackagesDir = ArtifactsDir + "/packages"; BuildPropsFile = SrcDir + "/Directory.Build.props"; - TestsRootDir = SrcDir + "/tests"; - CommonAssemblyVersionFile = SrcDir + "/common/AssemblyVersion.cs"; + CommonAssemblyVersionFile = SrcDir + "/Common/AssemblyVersion.cs"; } } @@ -133,6 +148,9 @@ public class BuildInfo { public bool IsRelease {get; protected set;} public bool IsLocal { get; protected set; } + + public bool IsPullRequest { get; protected set; } + public string AppVeyorJobId { get; protected set; } public BuildVersion Version { get; protected set; } @@ -151,18 +169,39 @@ public class BuildInfo { throw new ArgumentNullException(nameof(context)); var target = context.Argument("target", "Default"); var config = context.Argument("buildConfig", "Release"); - var buildSystem = context.BuildSystem(); - // Calculate version and commit hash - GitVersion semVersion = context.GitVersion(); - var version = new BuildVersion( - semVersion.NuGetVersion, - semVersion.FullBuildMetaData, - semVersion.InformationalVersion, - $"{semVersion.Major+1}.0.0", - semVersion.Sha, - semVersion.MajorMinorPatch - ); + var buildSystem = context.BuildSystem(); + var repositoryInfo = RepositoryInfo.Get(buildSystem, settings); + BuildVersion version; + + if (repositoryInfo.IsPullRequest) { + // GitVersion fails on PR builds, use 0.PullRequestId.BuildNumber as a version number + var buildVersion = $"0.{buildSystem.AppVeyor.Environment.PullRequest.Number}.{buildSystem.AppVeyor.Environment.Build.Number}"; + var commitHash = buildSystem.AppVeyor.Environment.Repository.Commit.Id; + + version = new BuildVersion( + buildVersion, + $"{buildVersion}/{commitHash}-PR-{buildSystem.AppVeyor.Environment.PullRequest.Title}", + buildVersion, + $"{buildVersion}.0", + commitHash, + buildVersion + ); + } + else { + // Calculate version and commit hash + // GitVersion 6.0 removed the NuGetVersion variable; SemVer is its SemVer2-compatible + // replacement and is accepted by modern NuGet (v3+). + GitVersion semVersion = context.GitVersion(); + version = new BuildVersion( + semVersion.SemVer, + semVersion.FullBuildMetaData, + semVersion.InformationalVersion, + $"{semVersion.Major+1}.0.0", + semVersion.Sha, + semVersion.MajorMinorPatch + ); + } var gitHubToken = context.EnvironmentVariable("GITHUB_TOKEN"); @@ -172,6 +211,7 @@ public class BuildInfo { IsDebug = string.Equals(config, "Debug", StringComparison.OrdinalIgnoreCase), IsRelease = string.Equals(config, "Release", StringComparison.OrdinalIgnoreCase), IsLocal = buildSystem.IsLocalBuild, + IsPullRequest = repositoryInfo.IsPullRequest, AppVeyorJobId = buildSystem.AppVeyor.Environment.JobId, Version = version, Repository = RepositoryInfo.Get(buildSystem, settings), @@ -181,5 +221,3 @@ public class BuildInfo { }; } } - - diff --git a/.build/tasks.cake b/.build/tasks.cake index b151fd209..e4be9e7fd 100644 --- a/.build/tasks.cake +++ b/.build/tasks.cake @@ -39,62 +39,69 @@ Task("Restore") Task("RunXunitTests") .Does(build => { - var projectPath = build.Paths.SrcDir; - var projectFilename = build.Settings.SolutionName; - // keep in sync with src/Directory.Build.props var solutionFullPath = new DirectoryPath(build.Paths.SrcDir).Combine(build.Settings.SolutionName) + ".sln"; + + // Build DotNetTestSettings for a given configuration and log-file name. + DotNetTestSettings BuildTestSettings(string buildCfg, string logFilename) { - Func buildProcessArgs = (buildCfg) => { - var pb = new ProcessArgumentBuilder() - .AppendSwitch("--configuration", buildCfg) - .AppendSwitch("--filter", "Category!=ManualTests") - .AppendSwitch("--results-directory", build.Paths.ArtifactsDir) - .Append("--no-restore") - .Append("--no-build"); - if (!build.IsLocal) { - pb.AppendSwitch("--test-adapter-path", ".") - .AppendSwitch("--logger", "AppVeyor"); - } - else { - pb.AppendSwitch("--logger", $"trx;LogFileName={projectFilename}.trx"); + var ts = new DotNetTestSettings + { + Configuration = buildCfg, + Filter = "Category!=ManualTests", + ResultsDirectory = new DirectoryPath(build.Paths.ArtifactsDir), + NoRestore = true, + NoBuild = true, + ArgumentCustomization = args => + { + if (!build.IsLocal) + return args.AppendSwitch("--test-adapter-path", ".") + .AppendSwitch("--logger", "AppVeyor"); + return args.AppendSwitch("--logger", $"trx;LogFileName={logFilename}.trx"); } - return pb; }; + return ts; + } - Information("Calculating code coverage for {0} ...", projectFilename); + // Coverlet's MSBuild integration runs per test project. Testing the whole solution in one call makes + // every project write to the same CoverletOutputDirectory with the same CoverletOutputName, so each TFM + // slot is overwritten by the *last* project that runs and only that project's coverage survives. Instead, + // test each project individually with a unique CoverletOutputName; Coverlet still appends the TFM for + // multi-targeted projects, producing files like coverage...cobertura.xml. UploadCoverage + // then sends each file to Coveralls as a distinct parallel job so the full SharpArch.* surface is covered + // instead of just the last project's slice. + var testProjects = GetFiles($"{build.Paths.SrcDir}/**/*.csproj") + .Where(f => f.GetFilenameWithoutExtension().ToString().Contains("Tests")) + .OrderBy(f => f.GetFilenameWithoutExtension().ToString()) + .ToList(); - var openCoverSettings = new OpenCoverSettings + foreach (var project in testProjects) + { + var projectName = project.GetFilenameWithoutExtension().ToString(); + var coverletSettings = new CoverletSettings { - OldStyle = true, - ReturnTargetCodeOffset = 0, - ArgumentCustomization = args => args.Append("-mergeoutput").Append("-hideskipped:File;Filter;Attribute"), - WorkingDirectory = projectPath, - } - .WithFilter($"{build.Settings.CodeCoverage.IncludeFilter} {build.Settings.CodeCoverage.ExcludeFilter}") - .ExcludeByAttribute(build.Settings.CodeCoverage.ExcludeByAttribute) - .ExcludeByFile(build.Settings.CodeCoverage.ExcludeByFile); - - // run open cover for debug build configuration - OpenCover( - tool => tool.DotNetTool( - projectPath.ToString(), - "test", - buildProcessArgs("Debug") - ), - build.Paths.TestCoverageOutputFile, - openCoverSettings + CollectCoverage = true, + CoverletOutputFormat = CoverletOutputFormat.cobertura, + CoverletOutputDirectory = new DirectoryPath(build.Paths.ArtifactsDir), + CoverletOutputName = $"coverage.{projectName}.cobertura.xml", + ExcludeByFile = new List { build.Settings.CodeCoverage.ExcludeByFile }, + ExcludeByAttribute = new List { build.Settings.CodeCoverage.ExcludeByAttribute }, + Include = build.Settings.CodeCoverage.IncludeFilter, + Exclude = build.Settings.CodeCoverage.ExcludeFilter, + }; + + Information("Calculating code coverage for {0} ...", projectName); + DotNetTest( + project.FullPath, + BuildTestSettings("Debug", projectName), + coverletSettings ); + } - // run tests again if Release mode was requested - if (build.IsRelease) - { - Information("Running Release mode tests for {0} ...", projectFilename); - DotNetTool( - solutionFullPath, - "test", - buildProcessArgs("Release") - ); - } + // Run Release-mode tests (no coverage) when a Release build was requested. + if (build.IsRelease) + { + Information("Running Release mode tests for {0} ...", build.Settings.SolutionName); + DotNetTest(solutionFullPath, BuildTestSettings("Release", build.Settings.SolutionName)); } }) .DeferOnError(); @@ -102,8 +109,7 @@ Task("RunXunitTests") Task("CleanPreviousTestResults") .Does(build => { - if (FileExists(build.Paths.TestCoverageOutputFile)) - DeleteFile(build.Paths.TestCoverageOutputFile); + DeleteFiles(build.Paths.ArtifactsDir + "/coverage.*.cobertura.xml"); DeleteFiles(build.Paths.ArtifactsDir + "/*.trx"); if (DirectoryExists(build.Paths.TestCoverageReportDir)) DeleteDirectory(build.Paths.TestCoverageReportDir, new DeleteDirectorySettings @@ -117,17 +123,90 @@ Task("GenerateCoverageReport") .WithCriteria((ctx, build) => build.IsLocal) .Does(build => { - ReportGenerator((FilePath)build.Paths.TestCoverageOutputFile, build.Paths.TestCoverageReportDir); + ReportGenerator(new GlobPattern(build.Paths.TestCoverageGlobPattern), build.Paths.TestCoverageReportDir); }); Task("UploadCoverage") - .WithCriteria((ctx, build) => !build.IsLocal) + .WithCriteria((ctx, build) => build.IsPullRequest == false && build.IsLocal == false) .Does(build => { - CoverallsNet(build.Paths.TestCoverageOutputFile, CoverallsNetReportType.OpenCover, new CoverallsNetSettings() + // Uses the self-contained coveralls-windows.exe "coverage-reporter" binary (no .NET runtime + // dependency) instead of the abandoned, .NET-6-only coveralls.net/Cake.Coveralls tool. + // + // Each per-project/per-TFM Cobertura file is uploaded as a DISTINCT parallel job. The + // coverage-reporter does NOT merge overlapping coverage within a single job (its + // SourceFiles.add just concatenates source-file entries), so a single multi-file `report` — + // or several `report` calls sharing the auto-detected AppVeyor service_job_id — would + // concatenate and lose data. Distinct --job-id per upload makes Coveralls treat each file as + // a separate parallel job; the final `coveralls done` fires the completion webhook that + // aggregates all jobs for this build (service_number, auto-detected from AppVeyor) with + // server-side per-line-max merging. `done` MUST run even when individual uploads fail, + // otherwise Coveralls never finalizes the merge — hence the try/finally. + var reporterExe = EnvironmentVariable("COVERALLS_REPORTER_EXE") ?? "coveralls-windows.exe"; + var buildNumber = EnvironmentVariable("APPVEYOR_BUILD_NUMBER"); + + var coverageFiles = GetFiles(build.Paths.TestCoverageGlobPattern) + .OrderBy(f => f.GetFilename().ToString()) + .ToList(); + + if (coverageFiles.Count == 0) { - RepoTokenVariable = "COVERALLS_REPO_TOKEN" - }); + Warning("No coverage files found matching {0}; skipping Coveralls upload.", build.Paths.TestCoverageGlobPattern); + return; + } + + var failures = new List(); + var uploaded = 0; + try + { + foreach (var file in coverageFiles) + { + // Distinct job id per project+TFM (prefixed with the build number for cross-build + // uniqueness) so each upload is a separate parallel job rather than overwriting the + // auto-detected AppVeyor service_job_id that all calls in this job share. + var jobId = string.IsNullOrEmpty(buildNumber) + ? file.GetFilenameWithoutExtension().ToString() + : $"{buildNumber}-{file.GetFilenameWithoutExtension()}"; + + Information("Uploading coverage {0} to Coveralls (parallel job {1}) ...", file.GetFilename(), jobId); + + var exitCode = StartProcess(reporterExe, new ProcessSettings + { + Arguments = new ProcessArgumentBuilder() + .Append("report") + .AppendQuoted(file.FullPath) + .Append("--format=cobertura") + .Append("--parallel") + .Append($"--job-id={jobId}") + .Append("--no-logo") + }); + + if (exitCode != 0) + failures.Add($"{file.GetFilename()} (exit code {exitCode})"); + else + uploaded++; + } + } + finally + { + // Fire the completion webhook so Coveralls aggregates the parallel jobs for this build, + // regardless of whether some uploads failed. Only finalize if at least one job was sent. + if (uploaded > 0) + { + Information("Finalizing Coveralls parallel build ..."); + var doneExitCode = StartProcess(reporterExe, new ProcessSettings + { + Arguments = new ProcessArgumentBuilder() + .Append("done") + .Append("--no-logo") + }); + if (doneExitCode != 0) + failures.Add($"coveralls done (exit code {doneExitCode})"); + } + } + + if (failures.Count > 0) + throw new Exception("Coveralls upload reported failures:\n" + string.Join("\n", failures)); }); Task("RunUnitTests") diff --git a/.clinerules b/.clinerules new file mode 100644 index 000000000..794f1e7d6 --- /dev/null +++ b/.clinerules @@ -0,0 +1,116 @@ +# .clinerules + +This file provides guidance to Cline when working with code in this repository. + +## Project Settings + +- **Stack:** dotnet + +## What this is + +S#arp Architecture is a framework (a set of NuGet libraries, not an application) for building +maintainable ASP.NET Core web applications using Domain-Driven Design on top of NHibernate. +The shipped product is the set of `SharpArch.*` packages under `Src/Lib/`; everything else +(`Src/Tests/`, `Src/Samples/`) exists to test and demonstrate them. + +**Ignore the `Old/` folder** — it holds legacy (pre-v5, ASP.NET MVC / Castle Windsor) samples kept +for reference only. Do not modify, build, or use it as a pattern for new work. + +## Commands + +All commands run from the repository root unless noted. There is a single solution: `Src/SharpArch.sln`. + +```bash +# Restore + build (Debug or Release) +cd Src && dotnet build -c Release + +# Run the full unit-test suite (excludes DB integration & functional tests) +cd Src && dotnet test -c Release --filter "Category!=IntegrationTests&Category!=Functional" + +# Run a single test project +dotnet test Src/Tests/SharpArch.XunitTests/SharpArch.XunitTests.csproj + +# Run a single test by name (substring match on fully-qualified name) +dotnet test Src/SharpArch.sln --filter "FullyQualifiedName~EntityTests" + +# Full CI pipeline locally (build + tests + coverage + pack) via Cake +dotnet tool install Cake.Tool --global # once +dotnet cake # runs the "Default" target from build.cake +dotnet cake --target=RunUnitTests # build + tests + coverage only +``` + +### Test categories & the database + +- Unit tests use an **in-memory SQLite** database and need no setup. +- Tests tagged `[Trait("Category","IntegrationTests")]` / `Functional` / `ManualTests` hit a **real SQL + Server** and are excluded from the default run. To run them locally, start SQL Server first: + `pwsh Docker/start-mssql.ps1` (SQL Server 2019 container on port **2433**, sa password `Password12!`). + +## Target frameworks + +Defined centrally in `Src/Directory.Build.props`: +- **Libraries** (`Src/Lib/`): `netstandard2.1;net8.0;net9.0` +- **Apps & test projects**: `net8.0;net9.0` + +## Architecture + +Strict DDD layering enforced by project references — the dependency direction is the point of the framework. + +- **`SharpArch.Domain`** — the core, with **no infrastructure dependencies**. Contains the domain + building blocks and, crucially, the *persistence-support interfaces* (`IRepository<,>`, + `ILinqRepository<,>`, `ITransactionManager`, `IEntityDuplicateChecker`) that the domain depends on + but does not implement. This inversion lets domain and application code stay ORM-agnostic. + - `Entity` / `IEntity` — identity-based equality. Two entities are equal if they share a + non-default `Id`; transient entities fall back to comparing *domain signature* properties. + - `[DomainSignature]` — marks the properties that define business identity; consumed by + `Entity.GetHashCode`/`Equals` and by `HasUniqueDomainSignatureAttribute` validation. + - `ValueObject`, `BaseObject`, `ValidatableObject` — value-equality and validation base types. + - `Specifications/` — `ILinqSpecification` / `QuerySpecification` (Specification pattern) used + by the LINQ repository. +- **`SharpArch.NHibernate`** — the NHibernate implementation of the domain's persistence interfaces: + `NHibernateRepository`/`LinqRepository`, `TransactionManager`, and `NHibernateSessionFactoryBuilder` + (fluent builder for `ISessionFactory`, supporting external `hibernate.cfg.xml`, Fluent + auto-persistence models, 2nd-level cache, and data-annotation validators). +- **`SharpArch.NHibernate.DependencyInjection`** — `AddNHibernateWithSingleDatabase(...)` wires the + above into `IServiceCollection`. Lifetimes matter: **`ISessionFactory` = Singleton**, + **`ISession` = Scoped** (per HTTP request), **`IStatelessSession` = Transient**, `TransactionManager` + = Scoped. +- **`SharpArch.Web.AspNetCore`** — MVC integration. `[Transaction]` is a *marker* attribute + (class/method/global); it does nothing unless `AutoTransactionHandler` is registered as an MVC + filter. The handler opens a transaction per action and commits/rolls back based on the outcome + (rollback on unhandled exception, and optionally on model-validation errors). +- **`SharpArch.Infrastructure`** — cross-cutting helpers (`CodeBaseLocator`, logging wrappers). +- **`SharpArch.Testing[.Xunit|.NUnit][.NHibernate]`** — base classes for consumers' tests, e.g. + `RepositoryTestsBase` / `TransientDatabaseTests` that spin up a throwaway DB per test. + +Typical consumer flow: define entities in a Domain layer → depend only on `IRepository`/`ILinqRepository` +→ compose an ASP.NET Core app that calls `AddNHibernateWithSingleDatabase` and registers +`AutoTransactionHandler`. The `Src/Samples/TardisBank` project is the end-to-end reference. + +## Conventions + +- **File-scoped namespaces**, with `using` directives placed **inside** the namespace block. +- `Nullable` and `ImplicitUsings` are **enabled**; `LangVersion` is `preview`. Global usings include + `JetBrains.Annotations` and `System.Diagnostics.CodeAnalysis`. +- **Public API is annotated**: `[PublicAPI]` marks exported surface, and `GenerateDocumentationFile` + is on — public members need XML doc comments (missing-doc warning 1591 is *not* suppressed for + libraries). +- **Test frameworks**: xUnit is primary (`Shouldly` for assertions, `Moq` for mocking). One legacy + project, `SharpArch.Tests.NHibernate`, still uses NUnit — the framework is selected by project name + in `Src/Tests/Directory.Build.props`, so match the surrounding project when adding tests. +- **Versioning/branching**: GitFlow (`develop` = pre-release, `master` = stable) with GitVersion; + package version is derived from git, not hardcoded. + +## Code exploration + +A CodeGraph index (`.codegraph/`) is present — prefer structural exploration of the codebase +(searching symbols, call graphs, where a symbol is defined, blast radius) for structural questions +over broad grep/read loops. + +## Auto-approve / safe commands + +The following read-only commands are pre-approved as safe to run without confirmation +(migrated from the previous Claude `settings.local.json` permissions): + +- `dotnet --list-sdks` +- `dotnet --version` diff --git a/.codegraph/.gitignore b/.codegraph/.gitignore index 9de0f1690..d20c0fe4b 100644 --- a/.codegraph/.gitignore +++ b/.codegraph/.gitignore @@ -1,16 +1,5 @@ -# CodeGraph data files -# These are local to each machine and should not be committed - -# Database -*.db -*.db-wal -*.db-shm - -# Cache -cache/ - -# Logs -*.log - -# Hook markers -.dirty +# CodeGraph data files — local to each machine, not for committing. +# Ignore everything in .codegraph/ except this file itself, so transient +# files (the database, daemon.pid, sockets, logs) never show up in git. +* +!.gitignore diff --git a/.gitignore b/.gitignore index 0f008e753..047847196 100644 --- a/.gitignore +++ b/.gitignore @@ -290,4 +290,5 @@ Artefacts/Documentation/build/ /DropsTestOutput.xml /Src/Common/AssemblyVersion.cs /Docker/mssql-data +/Docker/postgres-data /tmp/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..49466de3e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.preferCSharpExtension": true +} diff --git a/Artefacts/Documentation/ARCHITECTURE.md b/Artefacts/Documentation/ARCHITECTURE.md new file mode 100644 index 000000000..eaa31a1a1 --- /dev/null +++ b/Artefacts/Documentation/ARCHITECTURE.md @@ -0,0 +1,925 @@ +# Sharp-Architecture — Component Architecture + +> Generated from source analysis of `Src/Lib` and `Src/Samples`. +> Last updated: June 2026 + +--- + +## Table of Contents + +- [Sharp-Architecture — Component Architecture](#sharp-architecture--component-architecture) + - [Table of Contents](#table-of-contents) + - [Overview](#overview) + - [Package Structure](#package-structure) + - [Domain Model Layer](#domain-model-layer) + - [Class Hierarchy](#class-hierarchy) + - [Key Interfaces — Domain Model](#key-interfaces--domain-model) + - [Domain Signature](#domain-signature) + - [Equality \& Comparison Utilities](#equality--comparison-utilities) + - [Reflection Cache](#reflection-cache) + - [Persistence Abstractions](#persistence-abstractions) + - [Repository Contracts](#repository-contracts) + - [Transaction Contracts](#transaction-contracts) + - [Duplicate Detection Contract](#duplicate-detection-contract) + - [Validation Attributes](#validation-attributes) + - [`[HasUniqueDomainSignature]`](#hasuniquedomainsignature) + - [Specification Pattern](#specification-pattern) + - [Repository Extensions](#repository-extensions) + - [NHibernate Implementations](#nhibernate-implementations) + - [Interface Inheritance](#interface-inheritance) + - [Class Implementations](#class-implementations) + - [`TransactionManager`](#transactionmanager) + - [`NHibernateRepository`](#nhibernaterepositorytentity-tid) + - [`LinqRepository`](#linqrepositorytentity-tid) + - [`EntityDuplicateChecker`](#entityduplicatechecker) + - [Session \& Query Utilities](#session--query-utilities) + - [`Enums.LockMode`](#enumslockmode) + - [Session Factory Setup](#session-factory-setup) + - [FluentNHibernate Integration](#fluentnhibernate-integration) + - [Data Annotation Validation Hook](#data-annotation-validation-hook) + - [Infrastructure Utilities](#infrastructure-utilities) + - [`LogWrapper` (struct, in `SharpArch.Infrastructure.Logging`)](#logwrapper-struct-in-sharparchinfrastructurelogging) + - [`CodeBaseLocator`](#codebaselocator) + - [ASP.NET Core Web Layer](#aspnet-core-web-layer) + - [Declarative Transaction Management](#declarative-transaction-management) + - [Filter Pipeline](#filter-pipeline) + - [Dependency Injection Wiring](#dependency-injection-wiring) + - [End-to-End Request Flow](#end-to-end-request-flow) + - [Testing Infrastructure](#testing-infrastructure) + - [Base Test Classes (xUnit)](#base-test-classes-xunit) + - [`TransientDatabaseTests` (`SharpArch.Testing.Xunit.NHibernate`)](#transientdatabaseteststdatabaseinitializer-sharparchtestingxunitnhibernate) + - [`LiveDatabaseTests` (`SharpArch.Testing.Xunit.NHibernate`)](#livedatabaseteststdatabasesetup-sharparchtestingxunitnhibernate) + - [Base Test Classes (NUnit)](#base-test-classes-nunit) + - [General Test Helpers](#general-test-helpers) + - [`SetCultureAttribute` (`SharpArch.Testing.Xunit`)](#setcultureattribute-sharparchtestingxunit) + - [Sample Application — TardisBank](#sample-application--tardisbank) + - [Project Structure](#project-structure) + - [Layer → Framework Dependency Mapping](#layer--framework-dependency-mapping) + - [Component Dependency Graph](#component-dependency-graph) + - [NuGet Package Dependencies](#nuget-package-dependencies) + - [`SharpArch.Domain.csproj`](#sharparchdomaincsproj) + - [`SharpArch.NHibernate.csproj`](#sharparchnhibernatecsproj) + - [`SharpArch.NHibernate.Extensions.DependencyInjection.csproj`](#sharparchnhibernateextensionsdependencyinjectioncsproj) + - [`SharpArch.Web.AspNetCore.csproj`](#sharparchwebaspnetcorecsproj) + +--- + +## Overview + +Sharp-Architecture is a .NET framework built around **Domain-Driven Design (DDD)** principles. It provides: + +- A clean **domain model base** (entities, value objects, specifications) with **zero external dependencies**. +- **Repository and Transaction Manager abstractions** defined in the domain layer and implemented against NHibernate. +- **ASP.NET Core MVC filter** support for declarative, attribute-driven transaction management. +- **Dependency Injection extensions** to wire everything together with minimal boilerplate. +- **Testing base classes** for both xUnit and NUnit, supporting in-memory and live-database scenarios. + +The architecture enforces a strict **dependency direction**: only outer layers depend on inner layers. The domain layer has no knowledge of any ORM, web framework, or infrastructure library. + +``` +┌────────────────────────────────────────────────────────────────┐ +│ Web / Application │ +│ SharpArch.Web.AspNetCore │ +├────────────────────────────────────────────────────────────────┤ +│ ORM / Infrastructure │ +│ SharpArch.NHibernate │ +│ SharpArch.NHibernate.DependencyInjection │ +├────────────────────────────────────────────────────────────────┤ +│ Domain (Core — no external deps) │ +│ SharpArch.Domain │ +├────────────────────────────────────────────────────────────────┤ +│ Cross-cutting Utilities │ +│ SharpArch.Infrastructure │ +└────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Package Structure + +| Package | Responsibility | +|---|---| +| `SharpArch.Domain` | Core DDD building blocks: entities, value objects, repository contracts, specifications, validation. **Zero external dependencies.** | +| `SharpArch.Infrastructure` | Cross-cutting utilities: allocation-free logging wrapper (`LogWrapper`), code-base locator. Depends only on `Microsoft.Extensions.Logging`. | +| `SharpArch.NHibernate` | NHibernate implementations of domain repository/transaction contracts. Includes FluentNHibernate automapping support and Data Annotation validation hooks. | +| `SharpArch.NHibernate.DependencyInjection` | ASP.NET Core `IServiceCollection` extension `AddNHibernateWithSingleDatabase` that registers session factory, sessions, and transaction manager with correct lifetimes. | +| `SharpArch.Web.AspNetCore` | `[Transaction]` attribute and `AutoTransactionHandler` MVC action filter for declarative transaction management. | +| `SharpArch.Testing` | Framework-agnostic test base types and helper utilities shared between NUnit and xUnit packages. | +| `SharpArch.Testing.NUnit` | NUnit base classes: `RepositoryTestsBase` (transient DB) and `DatabaseRepositoryTestsBase` (live DB). | +| `SharpArch.Testing.Xunit` | xUnit attribute `SetCultureAttribute` for per-test/per-class culture control. | +| `SharpArch.Testing.Xunit.NHibernate` | xUnit base classes: `TransientDatabaseTests` (in-memory/rollback) and `LiveDatabaseTests` (live DB, rollback per test). | + +--- + +## Domain Model Layer + +### Class Hierarchy + +``` +System.Object + └── BaseObject (abstract) + │ ▸ Signature-based Equals / GetHashCode + │ ▸ HasSameObjectSignatureAs(BaseObject) + │ ▸ GetTypeUnproxied() — NHibernate proxy-safe type resolution + │ ▸ Uses static ITypePropertyDescriptorCache to cache PropertyInfo[] + │ + ├── ValueObject (abstract) + │ ▸ Signature = ALL properties (by convention) + │ ▸ [DomainSignature] MUST NOT be used on value object properties + │ ▸ Throws InvalidOperationException if [DomainSignature] detected + │ ▸ == / != operators + │ + └── ValidatableObject (abstract) + ▸ DataAnnotations validation via Validator.TryValidateObject + ▸ IsValid(ValidationContext) → bool + ▸ ValidationResults(ValidationContext) → ICollection + │ + └── Entity (abstract, generic) + where TId : IEquatable + ▸ Id : TId (virtual, protected setter, [XmlIgnore]) + ▸ IsTransient() — true when Id is null or equals default(TId) + ▸ GetId() — untyped object? accessor (avoids boxing where possible) + ▸ GetTypeUnproxied() — virtual, overridable for proxy detection + ▸ IEquatable>, == / != operators + ▸ Implements IEntity and IEntity + ▸ Hash code cached after first computation +``` + +### Key Interfaces — Domain Model + +| Interface | Purpose | +|---|---| +| `IEntity` | Non-generic entity contract: `GetId()`, `GetSignatureProperties()`, `IsTransient()`, `GetTypeUnproxied()` | +| `IEntity` | Generic entity contract: typed `Id` property (covariant `out TId`) | +| `IHasAssignedId` | Marker for entities with **manually assigned** IDs. Use when IDs are not auto-generated by the database (e.g., natural keys). `SetAssignedIdTo(TId)` must be implemented. | + +### Domain Signature + +`[DomainSignature]` is a property-level attribute that marks properties forming the **business identity** of an entity — the set of properties that determine whether two instances represent the same real-world concept (e.g., a `User`'s `Email`, a `Product`'s `Sku`). + +**How it works:** + +1. `BaseObject.GetSignatureProperties()` uses reflection to find all properties decorated with `[DomainSignature]` on the concrete type. +2. The result is cached in `TypePropertyDescriptorCache` (static, thread-safe) to avoid repeated reflection. +3. `HasSameObjectSignatureAs(BaseObject)` compares all signature property values. +4. `Entity.Equals()` first tries ID comparison, then falls back to signature comparison for transient objects. +5. `EntityDuplicateChecker.DoesDuplicateExistWithTypedIdOf(IEntity)` queries the database for entities with matching signature property values. + +```csharp +// Example usage: +public class User : Entity +{ + [DomainSignature] + public virtual string Email { get; set; } + + public virtual string Name { get; set; } // Not part of domain signature +} +``` + +**Rules:** +- `ValueObject` subclasses use **all** properties as signature — `[DomainSignature]` must not be applied. +- `Entity` subclasses should have at least one `[DomainSignature]` property. + +### Equality & Comparison Utilities + +| Type | Purpose | +|---|---| +| `BaseObjectEqualityComparer` | Implements `IEqualityComparer` for use with LINQ set operators (`Intersect`, `Union`, `Distinct`). Delegates to `BaseObject.Equals` and `GetHashCode`. Necessary because LINQ set operators use `IEqualityComparer.GetHashCode` rather than `object.GetHashCode`. | +| `DomainSignatureAttribute` | Attribute applied to entity properties to include them in domain signature comparison. | + +```csharp +// Example: using BaseObjectEqualityComparer with LINQ +var distinct = users.Distinct(new BaseObjectEqualityComparer()); +var intersection = listA.Intersect(listB, new BaseObjectEqualityComparer()); +``` + +### Reflection Cache + +``` +ITypePropertyDescriptorCache + └── TypePropertyDescriptorCache (thread-safe, static instance in BaseObject) + └── TypePropertyDescriptor (wraps Type + PropertyInfo[]) +``` + +- `TypePropertyDescriptorCache` is a concurrent dictionary keyed by `Type`. +- `BaseObject` holds a single static instance shared across all object instances. +- `GetOrAdd(type, factory)` is used to prevent redundant allocations on the first access. + +--- + +## Persistence Abstractions + +All persistence contracts live in `SharpArch.Domain.PersistenceSupport`. They carry **no ORM dependency** — they reference only `SharpArch.Domain.DomainModel` types. + +### Repository Contracts + +``` +IRepository + where TEntity : class, IEntity + where TId : IEquatable + │ + ├── TransactionManager : ITransactionManager + ├── GetAsync(TId, CancellationToken) → Task + ├── GetAllAsync(CancellationToken) → Task> + ├── SaveAsync(entity, CancellationToken) → Task + ├── SaveOrUpdateAsync(entity, CancellationToken) → Task + ├── EvictAsync(entity, CancellationToken) → Task + ├── DeleteAsync(entity, CancellationToken) → Task + └── DeleteAsync(TId, CancellationToken) → Task + +ILinqRepository extends IRepository + ├── FindOneAsync(TId, CancellationToken) → Task + ├── FindOneAsync(ILinqSpecification, CancellationToken) → Task + ├── FindAll() → IQueryable + └── FindAll(ILinqSpecification) → IQueryable +``` + +### Transaction Contracts + +``` +ITransactionManager + ├── BeginTransaction(IsolationLevel) → IDisposable + ├── CommitTransactionAsync(CancellationToken) → Task + └── RollbackTransactionAsync(CancellationToken) → Task + +ISupportsTransactionStatus + └── IsActive : bool +``` + +`ISupportsTransactionStatus` is an **optional extension interface**. `AutoTransactionHandler` checks whether the resolved `ITransactionManager` also implements this interface, and if so, verifies `IsActive` before attempting commit/rollback. This prevents double-commit errors if the action manually managed the transaction. + +### Duplicate Detection Contract + +``` +IEntityDuplicateChecker + └── DoesDuplicateExistWithTypedIdOf(IEntity) : bool +``` + +Implemented by `EntityDuplicateChecker` in `SharpArch.NHibernate`. Used internally by the `[HasUniqueDomainSignature]` validator attribute. + +### Validation Attributes + +The `SharpArch.Domain.Validation` namespace provides Data Annotation validators that integrate with the entity lifecycle: + +#### `[HasUniqueDomainSignature]` + +Applied to an entity **class** to validate that no other persisted entity has the same domain signature. + +``` +HasUniqueDomainSignatureAttributeBase (abstract base) + └── HasUniqueDomainSignatureAttribute (concrete, resolves IEntityDuplicateChecker from ValidationContext) +``` + +**Flow:** +1. Applied to an entity class: `[HasUniqueDomainSignature]` +2. When `entity.IsValid(validationContext)` or `entity.ValidationResults(...)` is called: +3. `HasUniqueDomainSignatureAttribute.IsValid(entity, context)` is invoked. +4. It retrieves `IEntityDuplicateChecker` from `validationContext.GetService()`. +5. Calls `IEntityDuplicateChecker.DoesDuplicateExistWithTypedIdOf(entity)`. +6. Returns a validation error if a duplicate exists. + +```csharp +[HasUniqueDomainSignature] +public class User : Entity +{ + [DomainSignature] + public virtual string Email { get; set; } +} +``` + +### Specification Pattern + +Specifications encapsulate and name query logic, enabling reuse and composition. + +``` +ISpecification + └── IsSatisfiedBy(T) : bool ← in-memory evaluation + +ILinqSpecification + └── SatisfyingElementsFrom(IQueryable) → IQueryable ← DB query + +QuerySpecification (abstract base — implements ILinqSpecification) + └── override SatisfyingElementsFrom(candidates) + +AdHoc (extends QuerySpecification) + └── Created via lambda: new AdHoc(q => q.Where(u => u.IsActive)) +``` + +`QuerySpecificationExtensions` provides LINQ-style extension methods for composing specifications. + +**Usage pattern:** + +```csharp +// Named specification +public class ActiveUsersSpec : QuerySpecification +{ + public override IQueryable SatisfyingElementsFrom(IQueryable candidates) + => candidates.Where(u => u.IsActive); +} + +// Inline ad-hoc specification +var spec = new AdHoc(q => q.Where(u => u.Email.EndsWith("@example.com"))); + +// Usage with repository +IQueryable activeUsers = linqRepo.FindAll(new ActiveUsersSpec()); +User? user = await linqRepo.FindOneAsync(spec, cancellationToken); +``` + +### Repository Extensions + +`RepositoryExtensions` provides convenience extension methods on `IRepository`: + +- Helpers for common patterns such as checking existence or combining operations. +- Reduces boilerplate in application code that uses `IRepository`. + +--- + +## NHibernate Implementations + +### Interface Inheritance + +``` +Domain Layer NHibernate Layer +───────────────── ──────────────────────────────────────── +ITransactionManager ◄───── INHibernateTransactionManager + └── + Session : ISession + └── + FlushChangesAsync() + +IRepository ◄───── INHibernateRepository + └── + FindAllAsync(propertyValuePairs) + └── + FindAllAsync(exampleInstance) + └── + FindOneAsync(exampleInstance) + └── + FindOneAsync(propertyValuePairs) + └── + GetAsync(id, lockMode) + └── + LoadAsync(id) + └── + LoadAsync(id, lockMode) + └── + MergeAsync(entity) + └── + UpdateAsync(entity) + +ILinqRepository ◄───── LinqRepository (class) +``` + +### Class Implementations + +#### `TransactionManager` + +``` +TransactionManager + ├── implements INHibernateTransactionManager + ├── implements ISupportsTransactionStatus + ├── constructor: TransactionManager(ISession session) + ├── Session : ISession → exposes raw NHibernate session + ├── BeginTransaction(IsolationLevel) → Session.BeginTransaction(...) + ├── CommitTransactionAsync(ct) → Session.GetCurrentTransaction().CommitAsync(ct) + ├── RollbackTransactionAsync(ct) → Session.GetCurrentTransaction().RollbackAsync(ct) + ├── FlushChangesAsync(ct) → Session.FlushAsync(ct) + └── IsActive → Session.GetCurrentTransaction()?.IsActive ?? false +``` + +#### `NHibernateRepository` + +``` +NHibernateRepository + ├── implements INHibernateRepository + ├── constructor: NHibernateRepository(INHibernateTransactionManager transactionManager) + ├── Session : ISession → TransactionManager.Session + ├── GetAsync(id, ct) → Session.GetAsync(id, ct) + ├── GetAllAsync(ct) → Session.CreateCriteria(...).ListAsync(ct) + ├── SaveAsync(entity, ct) → Session.SaveAsync(entity, ct) + ├── SaveOrUpdateAsync(entity, ct) → Session.SaveOrUpdateAsync(entity, ct) + ├── EvictAsync(entity, ct) → Session.EvictAsync(entity, ct) + ├── DeleteAsync(entity, ct) → Session.DeleteAsync(entity, ct) + ├── DeleteAsync(id, ct) → GetAsync then DeleteAsync + ├── FindAllAsync(propertyValuePairs, max, ct) → Criteria with Restrictions.Eq / IsNull + ├── FindAllAsync(example, exclusions, max, ct) → Criteria with Example.Create(...) + ├── FindOneAsync(example, ct, exclusions) → FindAllAsync (maxResults=2) + uniqueness check + ├── FindOneAsync(propertyValuePairs, ct) → FindAllAsync (maxResults=2) + uniqueness check + ├── GetAsync(id, lockMode, ct) → Session.GetAsync(id, lockMode, ct) + ├── LoadAsync(id, ct) → Session.LoadAsync(id, ct) + ├── LoadAsync(id, lockMode, ct) → Session.LoadAsync(id, lockMode, ct) + ├── MergeAsync(entity, ct) → Session.MergeAsync(entity, ct) + └── UpdateAsync(entity, ct) → Session.UpdateAsync(entity, ct) +``` + +#### `LinqRepository` + +``` +LinqRepository + ├── extends NHibernateRepository + ├── implements ILinqRepository + ├── FindOneAsync(id, ct) → Session.GetAsync(id, ct) + ├── FindOneAsync(ILinqSpecification, ct) → spec.SatisfyingElementsFrom(Session.Query()).SingleOrDefaultAsync() + ├── FindAll() → Session.Query() + └── FindAll(ILinqSpecification) → spec.SatisfyingElementsFrom(Session.Query()) +``` + +#### `EntityDuplicateChecker` + +``` +EntityDuplicateChecker + ├── implements IEntityDuplicateChecker + ├── constructor: EntityDuplicateChecker(ISession session) + ├── Temporarily sets FlushMode to Manual during check (avoids flushing pending changes) + ├── Builds ICriteria excluding the current entity's ID + ├── Appends criteria for each [DomainSignature] property: + │ ├── IEntity<> properties → compared by .id + │ ├── ValueObject properties → recursively expanded to scalar comparisons + │ ├── DateTime properties → Eq or IsNull (skips uninitialized dates) + │ ├── string properties → InsensitiveLike (case-insensitive exact match) + │ ├── enum properties → Eq (as int) + │ └── value types → Eq or IsNull + └── Returns true if at least one duplicate found (SetMaxResults(1)) +``` + +### Session & Query Utilities + +| Type | Purpose | +|---|---| +| `NHibernateQuery` | Base class / helpers for constructing HQL or Criteria queries; provides common query patterns. | +| `SessionExtensions` | Extension methods on `ISession`, e.g., helpers for common session operations. | +| `LockModeConvertExtensions` | Converts `SharpArch.Domain.Enums.LockMode` to the NHibernate `LockMode` enum, keeping the domain layer free of NHibernate references. | +| `ISessionFactoryKeyProvider` | Contract for providing a named key for multi-database session factory scenarios. | + +#### `Enums.LockMode` + +Defined in `SharpArch.Domain` (no NHibernate dependency), maps to NHibernate lock modes via `LockModeConvertExtensions`: + +| SharpArch LockMode | NHibernate LockMode | +|---|---| +| `None` | `LockMode.None` | +| `Read` | `LockMode.Read` | +| `Upgrade` | `LockMode.Upgrade` | +| `UpgradeNoWait` | `LockMode.UpgradeNoWait` | +| `Force` | `LockMode.Force` | +| `Write` | `LockMode.Write` | + +### Session Factory Setup + +`NHibernateSessionFactoryBuilder` is a **fluent builder** (instantiated as transient) that produces an `ISessionFactory` (registered as singleton in DI). + +``` +NHibernateSessionFactoryBuilder + │ + ├── UseConfigFile(string path) + │ Loads hibernate.cfg.xml from specified path. + │ Default: looks for "hibernate.cfg.xml" in working directory. + │ + ├── UseProperties(IEnumerable> props) + │ Sets NHibernate configuration properties programmatically. + │ + ├── UsePersistenceConfigurer(IPersistenceConfigurer configurer) + │ FluentNHibernate database configuration + │ e.g., SQLiteConfiguration.Standard.InMemory() + │ or MsSqlConfiguration.MsSql2012.ConnectionString(...) + │ + ├── AddMappingAssemblies(IEnumerable assemblies) + │ Scans assemblies for: + │ - HBM XML mappings (AddFromAssembly) + │ - FluentNHibernate class maps (AddFromAssembly + Conventions) + │ + ├── UseAutoPersistenceModel(AutoPersistenceModel model) + │ Adds FluentNHibernate auto-persistence model for convention-based mapping. + │ + ├── UseCache(Action cacheConfig) + │ Configures NHibernate second-level cache. + │ + ├── UseDataAnnotationValidators(bool enable) + │ When true, registers DataAnnotationsEventListener as pre-insert + │ and pre-update event listener on the Configuration. + │ + ├── ExposeConfiguration(Action config) + │ Callback invoked after all settings applied; allows direct NHibernate + │ Configuration manipulation (e.g., schema export, schema update). + │ + ├── BuildConfiguration() → NHibernate.Cfg.Configuration + └── BuildSessionFactory() → ISessionFactory +``` + +### FluentNHibernate Integration + +Located in `SharpArch.NHibernate/FluentNHibernate/`: + +| Type | Role | +|---|---| +| `IAutoPersistenceModelGenerator` | Contract: implement to provide a customised `AutoPersistenceModel` for auto-mapping. Pass result to `NHibernateSessionFactoryBuilder.UseAutoPersistenceModel(...)`. | +| `IMapGenerator` | Contract: implement to generate `ClassMap` instances for explicit FluentNHibernate mappings. | +| `AutomappingConfiguration` | Base `DefaultAutomappingConfiguration` subclass pre-configured to map only `Entity` subtypes, ignoring value objects and non-entity types. | +| `FluentNHibernateExtensions` | Extension methods simplifying common FluentNHibernate configuration tasks. | +| `GeneratorHelper` | Helpers for configuring ID generation strategies (identity, assigned, hilo, etc.) in FluentNHibernate convention-based mappings. | +| `Conventions/` | Pre-built FluentNHibernate conventions (e.g., table naming, foreign key naming, cascade defaults). | + +### Data Annotation Validation Hook + +`DataAnnotationsEventListener` implements NHibernate's `IPreInsertEventListener` and `IPreUpdateEventListener`: + +- Invoked automatically by NHibernate **before** every `INSERT` and `UPDATE`. +- Uses `Validator.TryValidateObject` to run all Data Annotation validators on the entity. +- Throws a `ValidationException` if validation fails, preventing the database write. +- Registered via `NHibernateSessionFactoryBuilder.UseDataAnnotationValidators(true)`. + +--- + +## Infrastructure Utilities + +### `LogWrapper` (struct, in `SharpArch.Infrastructure.Logging`) + +An allocation-free logging helper that skips the `ILogger.Log` call (and avoids lambda captures / string interpolation allocations) when a log level is disabled: + +```csharp +public readonly struct LogWrapper +{ + // Returns EnabledLogger? — non-null only when the level is enabled + public EnabledLogger? Trace { get; } + public EnabledLogger? Debug { get; } + public EnabledLogger? Information { get; } + public EnabledLogger? Warning { get; } + public EnabledLogger? Error { get; } + public EnabledLogger? Critical { get; } +} + +// Usage: +var logger = new LogWrapper(loggerFactory.CreateLogger("...")); +logger.Debug?.Log("Session created: {Id}", session.GetSessionImplementation().SessionId); +// ^^^^ short-circuits if Debug is disabled — no string formatting occurs +``` + +`EnabledLogger` wraps the actual `ILogger` and exposes a `Log(string message, params object[] args)` method. + +### `CodeBaseLocator` + +Provides utilities for locating the application code base directory at runtime — useful for finding configuration files (e.g., `hibernate.cfg.xml`) relative to the assembly location rather than the working directory. + +--- + +## ASP.NET Core Web Layer + +### Declarative Transaction Management + +The `[Transaction]` attribute is a **marker** — it carries configuration but does not apply any behaviour itself. Behaviour is provided by `AutoTransactionHandler`. + +```csharp +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public sealed class TransactionAttribute : Attribute, IFilterMetadata +{ + IsolationLevel IsolationLevel { get; } // default: ReadCommitted + bool RollbackOnModelValidationError { get; } // default: true +} +``` + +Apply at **controller class** level (all actions use it) or override per **action method**: + +```csharp +[Transaction] // all actions: ReadCommitted, rollback on validation error +public class OrdersController : ControllerBase +{ + [Transaction(IsolationLevel.Serializable)] // this action: Serializable + public Task Create([FromBody] CreateOrderRequest request) { ... } + + [Transaction(rollbackOnModelValidationError: false)] // this action: don't rollback on bad model + public Task Update(...) { ... } +} +``` + +### Filter Pipeline + +``` +ApplyTransactionFilterBase (abstract base) + └── Thread-safe action-descriptor-ID → TransactionAttribute? cache + Uses copy-on-write dictionary pattern (lock + new dict copy) for thread safety + +AutoTransactionHandler + ├── extends ApplyTransactionFilterBase + ├── implements IAsyncActionFilter + │ + ├── BEFORE action execution: + │ 1. GetTransactionAttribute(context) — looks up cache, reads effective policy + │ 2. If [Transaction] present: + │ → context.HttpContext.RequestServices.GetRequiredService() + │ → transactionManager.BeginTransaction(attribute.IsolationLevel) + │ + └── AFTER action execution: + 1. If ITransactionManager also implements ISupportsTransactionStatus: + → if !IsActive: log "Transaction already closed" and return (no double-commit) + 2. if (executedContext.Exception != null + || attribute.RollbackOnModelValidationError && !context.ModelState.IsValid): + → RollbackTransactionAsync() (no cancellation token on error path) + 3. else: + → CommitTransactionAsync(context.HttpContext.RequestAborted) +``` + +**Registration:** + +```csharp +// In Program.cs / Startup.cs +builder.Services.AddControllers(options => +{ + options.Filters.Add(); +}); +``` + +--- + +## Dependency Injection Wiring + +`NHibernateRegistrationExtensions.AddNHibernateWithSingleDatabase` (in `SharpArch.NHibernate.DependencyInjection`) registers the complete NHibernate infrastructure stack: + +| Service | Lifetime | Notes | +|---|---|---| +| `ISessionFactory` | **Singleton** | Built once at startup by `NHibernateSessionFactoryBuilder`. Hash code logged on creation. | +| `ISession` | **Scoped** (per HTTP request) | Opened from `ISessionFactory.OpenSession()` or via optional `sessionConfigurator` callback. Session ID logged on creation. | +| `IStatelessSession` | **Scoped** | Opened from `ISessionFactory.OpenStatelessSession()`. Stateless — no identity map or change tracking. Caller is responsible for disposal. | +| `TransactionManager` | **Scoped** | Wraps the scoped `ISession`. One per request. | +| `INHibernateTransactionManager` | Transient (→ `TransactionManager`) | | +| `ITransactionManager` | Transient (→ `TransactionManager`) | | + +**Signature:** + +```csharp +public static IServiceCollection AddNHibernateWithSingleDatabase( + this IServiceCollection services, + Func configureSessionFactory, + Func? sessionConfigurator = null, + Func? statelessSessionConfigurator = null +) +``` + +**Full setup example:** + +```csharp +// Program.cs +builder.Services.AddNHibernateWithSingleDatabase(sp => + new NHibernateSessionFactoryBuilder() + .UsePersistenceConfigurer( + MsSqlConfiguration.MsSql2012.ConnectionString( + builder.Configuration.GetConnectionString("Default"))) + .AddMappingAssemblies(new[] { typeof(ProductMap).Assembly }) + .UseDataAnnotationValidators(true) + .ExposeConfiguration(cfg => new SchemaUpdate(cfg).Execute(false, true)) +); + +builder.Services.AddControllers(options => +{ + options.Filters.Add(); +}); + +// Repository registrations (not auto-registered) +builder.Services.AddTransient, NHibernateRepository>(); +builder.Services.AddTransient, LinqRepository>(); +builder.Services.AddTransient(); +``` + +--- + +## End-to-End Request Flow + +``` +HTTP Request + │ + ▼ +┌──────────────────────────────────────────────────────┐ +│ AutoTransactionHandler — BEFORE │ +│ │ +│ 1. Find [Transaction] attribute on action/class │ +│ 2. Resolve ITransactionManager from DI │ +│ → TransactionManager (scoped to this request) │ +│ 3. TransactionManager.BeginTransaction(isolation) │ +│ → ISession.BeginTransaction(isolation) │ +└──────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────┐ +│ Controller Action │ +│ │ +│ Constructor-injects ILinqRepository │ +│ → LinqRepository │ +│ → NHibernateRepository (base) │ +│ → INHibernateTransactionManager │ +│ → TransactionManager (scoped) │ +│ → ISession (scoped) │ +│ → ISessionFactory (singleton) │ +│ │ +│ repo.GetAsync(id) → Session.GetAsync(id) │ +│ repo.SaveAsync(e) → Session.SaveAsync(e) │ +│ repo.FindAll(spec) → Session.Query()... │ +└──────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────┐ +│ AutoTransactionHandler — AFTER │ +│ │ +│ Check ISupportsTransactionStatus.IsActive │ +│ ├── false → already closed, skip (log debug) │ +│ └── true → │ +│ ├── exception || invalid model │ +│ │ → RollbackTransactionAsync() │ +│ └── success │ +│ → CommitTransactionAsync(ct) │ +└──────────────────────────────────────────────────────┘ + │ + ▼ +HTTP Response +``` + +--- + +## Testing Infrastructure + +### Base Test Classes (xUnit) + +#### `TransientDatabaseTests` (`SharpArch.Testing.Xunit.NHibernate`) + +- Designed for **in-memory or transient databases** (e.g., SQLite in-memory). +- Creates the database schema before the test class runs (`TDatabaseInitializer`). +- Wraps **each test** in a transaction that is **rolled back** after the test completes. +- Ensures complete test isolation — no test leaves data behind. +- Inherits from xUnit `IAsyncLifetime` for setup/teardown. + +#### `LiveDatabaseTests` (`SharpArch.Testing.Xunit.NHibernate`) + +- Designed for tests against a **real, pre-existing development database**. +- Does **not** run schema export — assumes schema is already up-to-date. +- Wraps each test in a transaction that is **rolled back** after the test. +- Useful for integration tests against a real SQL Server / PostgreSQL instance. +- Provided primarily for **backward compatibility**. + +**Usage pattern:** + +```csharp +public class OrderRepositoryTests : TransientDatabaseTests +{ + readonly ISession _session; + + public OrderRepositoryTests(SqliteTestDatabaseInitializer dbInit) + : base(dbInit) { } + + [Fact] + public async Task Can_save_and_retrieve_order() + { + var repo = new NHibernateRepository(TransactionManager); + var order = new Order { /* ... */ }; + await repo.SaveAsync(order); + Session.Flush(); + + var loaded = await repo.GetAsync(order.Id); + Assert.NotNull(loaded); + } +} +``` + +### Base Test Classes (NUnit) + +Located in `SharpArch.Testing.NUnit/NHibernate/`: + +| Class | Purpose | +|---|---| +| `RepositoryTestsBase` | NUnit equivalent of `TransientDatabaseTests`. Runs schema export before tests; each test rolls back. For in-memory / SQLite databases. | +| `DatabaseRepositoryTestsBase` | NUnit equivalent of `LiveDatabaseTests`. Tests against a live database; each test rolls back. | + +Both inherit from NUnit's `[TestFixture]` pattern and use `[SetUp]` / `[TearDown]` for transaction management. + +### General Test Helpers + +Located in `SharpArch.Testing/Helpers/`: + +- Framework-agnostic base types and utility helpers shared between NUnit and xUnit packages. +- Provides the core NHibernate session/transaction management logic that both `SharpArch.Testing.NUnit` and `SharpArch.Testing.Xunit.NHibernate` build upon. + +#### `SetCultureAttribute` (`SharpArch.Testing.Xunit`) + +An xUnit `BeforeAfterTestAttribute` that sets and restores `Thread.CurrentCulture` and `Thread.CurrentUICulture` for a specific test or test class: + +```csharp +[SetCulture("fr-FR")] +public class PriceFormattingTests +{ + [Fact] + public void Price_formats_with_french_locale() { /* ... */ } +} +``` + +--- + +## Sample Application — TardisBank + +TardisBank is a pocket-money / savings application that demonstrates all Sharp-Architecture patterns in a complete end-to-end scenario. + +### Project Structure + +``` +Samples/TardisBank/Src/ + ├── Suteki.TardisBank.Domain/ ← Domain layer + │ ├── User.cs, Account.cs, ... ← Entities extending Entity + │ ├── Money.cs, ... ← Value objects extending ValueObject + │ └── [DomainSignature] properties ← Business identity attributes + │ + ├── Suteki.TardisBank.Infrastructure/ ← ORM / persistence layer + │ ├── NHibernate mappings ← FluentNHibernate ClassMap or HBM + │ └── Repository implementations ← Concrete repos using NHibernateRepository + │ + ├── Suteki.TardisBank.Tasks/ ← Application services layer + │ └── Use-case orchestration ← Commands / handlers that coordinate + │ classes calling repositories domain objects and repositories + │ + ├── Suteki.TardisBank.Api/ ← Shared API contracts + │ └── DTOs / request-response types (not MVC controllers) + │ + ├── Suteki.TardisBank.WebApi/ ← ASP.NET Core Web API + │ ├── Controllers/ ← [Transaction] attributed controllers + │ └── Program.cs / Startup ← AddNHibernateWithSingleDatabase wiring + │ + └── Suteki.TardisBank.Tests/ ← Integration & unit tests + └── Uses TransientDatabaseTests ← In-memory DB, rollback per test +``` + +### Layer → Framework Dependency Mapping + +| Sample Layer | Sharp-Architecture Package Used | +|---|---| +| Domain entities | `SharpArch.Domain` (`Entity`, `ValueObject`, `[DomainSignature]`) | +| Infrastructure / ORM | `SharpArch.NHibernate` (`NHibernateRepository`, `LinqRepository`, `EntityDuplicateChecker`) | +| Web API | `SharpArch.Web.AspNetCore` (`[Transaction]`, `AutoTransactionHandler`) | +| DI wiring | `SharpArch.NHibernate.DependencyInjection` (`AddNHibernateWithSingleDatabase`) | +| Tests | `SharpArch.Testing.Xunit.NHibernate` (`TransientDatabaseTests`) | + +--- + +## Component Dependency Graph + +``` +SharpArch.Web.AspNetCore + └─→ SharpArch.Domain + (ITransactionManager, ISupportsTransactionStatus) + +SharpArch.NHibernate.DependencyInjection + ├─→ SharpArch.NHibernate + │ (TransactionManager, NHibernateSessionFactoryBuilder) + ├─→ SharpArch.Domain + │ (ITransactionManager) + └─→ SharpArch.Infrastructure + (LogWrapper) + +SharpArch.NHibernate + ├─→ SharpArch.Domain + │ (IEntity, IRepository, ILinqRepository, + │ ITransactionManager, IEntityDuplicateChecker, + │ ILinqSpecification, DomainSignatureAttribute, ValueObject, Enums) + └─→ NHibernate + FluentNHibernate (external NuGet) + +SharpArch.Infrastructure + └─→ Microsoft.Extensions.Logging (external NuGet) + +SharpArch.Domain + └─→ (none — pure .NET BCL only) + +SharpArch.Testing.Xunit.NHibernate + ├─→ SharpArch.Testing + ├─→ SharpArch.NHibernate + └─→ xUnit (external NuGet) + +SharpArch.Testing.NUnit + ├─→ SharpArch.Testing + ├─→ SharpArch.NHibernate + └─→ NUnit (external NuGet) +``` + +--- + +## NuGet Package Dependencies + +### `SharpArch.Domain.csproj` + +```xml + + +``` + +### `SharpArch.NHibernate.csproj` + +```xml + + + + +``` + +### `SharpArch.NHibernate.Extensions.DependencyInjection.csproj` + +```xml + + + + +``` + +### `SharpArch.Web.AspNetCore.csproj` + +```xml + + + + +``` + +--- + +*This document was generated from source-code analysis of the Sharp-Architecture repository (`Src/Lib` and `Src/Samples`). Keep this document in sync when adding new packages or significant API changes.* diff --git a/Docker/create_database_postgres.sql b/Docker/create_database_postgres.sql new file mode 100644 index 000000000..62a9b9206 --- /dev/null +++ b/Docker/create_database_postgres.sql @@ -0,0 +1,12 @@ +-- PostgreSQL equivalent of create_database.sql +-- Compatible with the official postgres Docker Linux image +-- +-- Usage options: +-- 1. Place in /docker-entrypoint-initdb.d/ for automatic execution on first container start +-- 2. Run manually: psql -U postgres -f create_database_postgres.sql + +-- Create TardisBank database if it does not already exist +SELECT 'CREATE DATABASE "TardisBank"' +WHERE NOT EXISTS ( + SELECT FROM pg_database WHERE datname = 'TardisBank' +)\gexec diff --git a/Docker/start-mssql.ps1 b/Docker/start-mssql.ps1 index c226cf8a5..53b5d5aa1 100644 --- a/Docker/start-mssql.ps1 +++ b/Docker/start-mssql.ps1 @@ -1 +1,8 @@ -docker.exe run --name sharparch-sql --user root -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Password12!' -p 2433:1433 -d -v./mssql-data:/var/opt/mssql/data mcr.microsoft.com/mssql/server:2019-latest \ No newline at end of file +docker.exe run ` + --name sharparch-sql ` + --user root ` + -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Password12!' ` + -p 2433:1433 ` + -d ` + -v./mssql-data:/var/opt/mssql/data ` + mcr.microsoft.com/mssql/server:2019-latest diff --git a/Docker/start-postgres.ps1 b/Docker/start-postgres.ps1 new file mode 100755 index 000000000..d68f57000 --- /dev/null +++ b/Docker/start-postgres.ps1 @@ -0,0 +1,7 @@ +docker run --name sharparch-postgres ` + -e POSTGRES_USER=postgres ` + -e POSTGRES_PASSWORD=Password12! ` + -p 5432:5432 ` + -v ${PWD}/postgres-data:/var/lib/postgresql ` + -v ${PWD}/create_database_postgres.sql:/docker-entrypoint-initdb.d/create_database_postgres.sql ` + -d postgres:latest diff --git a/GitVersion.yml b/GitVersion.yml index a02cbd49e..ede654002 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -2,10 +2,10 @@ branches: release: mode: ContinuousDeployment - tag: rc + label: rc develop: mode: ContinuousDeployment - tag: alpha + label: alpha ignore: sha: [] diff --git a/SharpArchitecture.code-workspace b/SharpArchitecture.code-workspace index 362d7c25b..e68d1d58c 100644 --- a/SharpArchitecture.code-workspace +++ b/SharpArchitecture.code-workspace @@ -3,5 +3,8 @@ { "path": "." } - ] -} \ No newline at end of file + ], + "settings": { + "dotnet.preferCSharpExtension": true + } +} diff --git a/Src/Common/NHibernate/TestDatabaseSetup.cs b/Src/Common/NHibernate/TestDatabaseSetup.cs index f05357cf4..ccf7f304b 100644 --- a/Src/Common/NHibernate/TestDatabaseSetup.cs +++ b/Src/Common/NHibernate/TestDatabaseSetup.cs @@ -69,7 +69,8 @@ public TestDatabaseSetup( if (!typeof(IAutoPersistenceModelGenerator).IsAssignableFrom(persistenceModelGenerator)) throw new ArgumentException($"Type {persistenceModelGenerator.FullName} must implement {nameof(IAutoPersistenceModelGenerator)}."); - if (mappingAssemblies == null) throw new ArgumentNullException(nameof(mappingAssemblies)); + if (mappingAssemblies == null) + throw new ArgumentNullException(nameof(mappingAssemblies)); _mappingAssemblies = mappingAssemblies.Distinct().ToArray(); } @@ -116,7 +117,8 @@ public virtual void Dispose() /// public Configuration GetConfiguration() { - if (_configuration != null) return _configuration; + if (_configuration != null) + return _configuration; var autoPersistenceModel = GenerateAutoPersistenceModel(); @@ -167,7 +169,8 @@ protected virtual void Customize(NHibernateSessionFactoryBuilder builder) /// Session builder. protected virtual ISessionBuilder ConfigureSession(ISessionBuilder sessionBuilder) { - if (sessionBuilder == null) throw new ArgumentNullException(nameof(sessionBuilder)); + if (sessionBuilder == null) + throw new ArgumentNullException(nameof(sessionBuilder)); SessionConfigurator?.Invoke(sessionBuilder); return sessionBuilder; } @@ -178,7 +181,8 @@ protected virtual ISessionBuilder ConfigureSession(ISessionBuilder sessionBuilde /// Session builder. protected virtual IStatelessSessionBuilder ConfigureStatelessSession(IStatelessSessionBuilder statelessSessionBuilder) { - if (statelessSessionBuilder == null) throw new ArgumentNullException(nameof(statelessSessionBuilder)); + if (statelessSessionBuilder == null) + throw new ArgumentNullException(nameof(statelessSessionBuilder)); StatelessSessionConfigurator?.Invoke(statelessSessionBuilder); return statelessSessionBuilder; } @@ -189,7 +193,8 @@ protected virtual IStatelessSessionBuilder ConfigureStatelessSession(IStatelessS /// public ISessionFactory GetSessionFactory() { - if (_sessionFactory != null) return _sessionFactory; + if (_sessionFactory != null) + return _sessionFactory; _sessionFactory = GetConfiguration().BuildSessionFactory(); return _sessionFactory!; } @@ -217,7 +222,8 @@ public ISession InitializeSession() /// NHibernate Session public IStatelessSession CreateStatelessSessionForConnection(DbConnection connection) { - if (connection == null) throw new ArgumentNullException(nameof(connection)); + if (connection == null) + throw new ArgumentNullException(nameof(connection)); var sessionBuilder = GetSessionFactory().WithStatelessOptions(); sessionBuilder.Connection(connection); var statelessSession = ConfigureStatelessSession(sessionBuilder).OpenStatelessSession(); diff --git a/Src/Directory.Build.props b/Src/Directory.Build.props index 07448a0e3..7bb5d264e 100644 --- a/Src/Directory.Build.props +++ b/Src/Directory.Build.props @@ -1,7 +1,7 @@  - net8.0;net9.0 + net8.0;net9.0;net10.0 netstandard2.1;$(AppTargetFrameworks) @@ -38,10 +38,6 @@ - - full - - True @@ -68,6 +64,23 @@ true false $(NoWarn);0618;1591 + + false + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + $(NoWarn);xunit1051 @@ -91,7 +104,7 @@ - + diff --git a/Src/Lib/Directory.Build.props b/Src/Lib/Directory.Build.props index 4b9686446..19d2e1a8e 100644 --- a/Src/Lib/Directory.Build.props +++ b/Src/Lib/Directory.Build.props @@ -4,7 +4,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Src/Lib/SharpArch.Domain/DomainModel/BaseObject.cs b/Src/Lib/SharpArch.Domain/DomainModel/BaseObject.cs index 4084d2811..e991e6844 100644 --- a/Src/Lib/SharpArch.Domain/DomainModel/BaseObject.cs +++ b/Src/Lib/SharpArch.Domain/DomainModel/BaseObject.cs @@ -153,7 +153,7 @@ public virtual bool HasSameObjectSignatureAs(BaseObject compareTo) } if (valueOfThisObject == null ^ valueToCompareTo == null || - !(valueOfThisObject!.Equals(valueToCompareTo))) + !valueOfThisObject!.Equals(valueToCompareTo)) { return false; } diff --git a/Src/Lib/SharpArch.Domain/DomainModel/BaseObjectEqualityComparer.cs b/Src/Lib/SharpArch.Domain/DomainModel/BaseObjectEqualityComparer.cs index 44468262d..60aed7cc1 100644 --- a/Src/Lib/SharpArch.Domain/DomainModel/BaseObjectEqualityComparer.cs +++ b/Src/Lib/SharpArch.Domain/DomainModel/BaseObjectEqualityComparer.cs @@ -49,7 +49,8 @@ public bool Equals(T? firstObject, T? secondObject) /// public int GetHashCode(T obj) { - if (obj is null) throw new ArgumentNullException(nameof(obj)); + if (obj is null) + throw new ArgumentNullException(nameof(obj)); return obj.GetHashCode(); } } diff --git a/Src/Lib/SharpArch.Domain/DomainModel/Entity.cs b/Src/Lib/SharpArch.Domain/DomainModel/Entity.cs index 8d1221d52..e9d97fbb3 100644 --- a/Src/Lib/SharpArch.Domain/DomainModel/Entity.cs +++ b/Src/Lib/SharpArch.Domain/DomainModel/Entity.cs @@ -1,6 +1,5 @@ namespace SharpArch.Domain.DomainModel; -using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Xml.Serialization; @@ -30,7 +29,8 @@ public abstract class Entity : ValidatableObject, IEntity, IEntity, public virtual object? GetId() { // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (Id is null) return null; + if (Id is null) + return null; return Id.Equals(default!) // ReSharper disable once RedundantCast ? (object?)null @@ -180,9 +180,7 @@ protected override PropertyInfo[] GetTypeSpecificSignatureProperties() /// default ID value; otherwise; false. /// bool HasSameNonDefaultIdAs(Entity compareTo) - { - return !IsTransient() && !compareTo.IsTransient() && Id!.Equals(compareTo.Id!); - } + => !IsTransient() && !compareTo.IsTransient() && Id!.Equals(compareTo.Id!); /// /// Check whether entities are equal. diff --git a/Src/Lib/SharpArch.Domain/DomainModel/IEntity.cs b/Src/Lib/SharpArch.Domain/DomainModel/IEntity.cs index 84a7aa25f..2c72ff800 100644 --- a/Src/Lib/SharpArch.Domain/DomainModel/IEntity.cs +++ b/Src/Lib/SharpArch.Domain/DomainModel/IEntity.cs @@ -67,6 +67,7 @@ public interface IEntity /// /// Gets the ID which uniquely identifies the entity instance within its type's bounds. /// - [AllowNull] [MaybeNull] + [AllowNull] + [MaybeNull] TId Id { get; } } diff --git a/Src/Lib/SharpArch.Domain/DomainModel/ValidatableObject.cs b/Src/Lib/SharpArch.Domain/DomainModel/ValidatableObject.cs index 85e2abc84..e86d9133e 100644 --- a/Src/Lib/SharpArch.Domain/DomainModel/ValidatableObject.cs +++ b/Src/Lib/SharpArch.Domain/DomainModel/ValidatableObject.cs @@ -1,7 +1,6 @@ namespace SharpArch.Domain.DomainModel; using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; /// diff --git a/Src/Lib/SharpArch.Domain/DomainModel/ValueObject.cs b/Src/Lib/SharpArch.Domain/DomainModel/ValueObject.cs index 3cc75a6a6..45e6f8664 100644 --- a/Src/Lib/SharpArch.Domain/DomainModel/ValueObject.cs +++ b/Src/Lib/SharpArch.Domain/DomainModel/ValueObject.cs @@ -1,6 +1,5 @@ namespace SharpArch.Domain.DomainModel; -using System.Diagnostics.CodeAnalysis; using System.Reflection; @@ -27,7 +26,8 @@ public abstract class ValueObject : BaseObject /// The result of the operator. public static bool operator ==(ValueObject? valueObject1, ValueObject? valueObject2) { - if (ReferenceEquals(valueObject1, null)) return ReferenceEquals(valueObject2, null); + if (ReferenceEquals(valueObject1, null)) + return ReferenceEquals(valueObject2, null); return valueObject1.Equals(valueObject2); } diff --git a/Src/Lib/SharpArch.Domain/PersistenceSupport/RepositoryExtensions.cs b/Src/Lib/SharpArch.Domain/PersistenceSupport/RepositoryExtensions.cs index 61d8d57bd..1e6becf4b 100644 --- a/Src/Lib/SharpArch.Domain/PersistenceSupport/RepositoryExtensions.cs +++ b/Src/Lib/SharpArch.Domain/PersistenceSupport/RepositoryExtensions.cs @@ -21,8 +21,10 @@ public static async Task SaveAndEvictAsync( where TEntity : class, IEntity where TId : IEquatable { - if (repository == null) throw new ArgumentNullException(nameof(repository)); - if (entity == null) throw new ArgumentNullException(nameof(entity)); + if (repository == null) + throw new ArgumentNullException(nameof(repository)); + if (entity == null) + throw new ArgumentNullException(nameof(entity)); var saved = await repository.SaveAsync(entity, CancellationToken.None).ConfigureAwait(false); await repository.EvictAsync(saved, cancellationToken).ConfigureAwait(false); } diff --git a/Src/Lib/SharpArch.Domain/Reflection/TypePropertyDescriptor.cs b/Src/Lib/SharpArch.Domain/Reflection/TypePropertyDescriptor.cs index b42891761..ac1465a10 100644 --- a/Src/Lib/SharpArch.Domain/Reflection/TypePropertyDescriptor.cs +++ b/Src/Lib/SharpArch.Domain/Reflection/TypePropertyDescriptor.cs @@ -35,7 +35,8 @@ public class TypePropertyDescriptor : IEquatable /// public TypePropertyDescriptor(Type ownerType, PropertyInfo[]? properties) { - if (ownerType == null) throw new ArgumentNullException(nameof(ownerType)); + if (ownerType == null) + throw new ArgumentNullException(nameof(ownerType)); _ownerType = ownerType; if (properties != null && properties.Length > 0) @@ -53,8 +54,10 @@ public TypePropertyDescriptor(Type ownerType, PropertyInfo[]? properties) /// public bool Equals(TypePropertyDescriptor? other) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; + if (ReferenceEquals(null, other)) + return false; + if (ReferenceEquals(this, other)) + return true; return OwnerType == other.OwnerType; } @@ -76,8 +79,10 @@ public bool HasProperties() /// public override bool Equals(object? obj) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; return obj is TypePropertyDescriptor other && Equals(other); } diff --git a/Src/Lib/SharpArch.Domain/Reflection/TypePropertyDescriptorCache.cs b/Src/Lib/SharpArch.Domain/Reflection/TypePropertyDescriptorCache.cs index 91f92ef13..348a2cfda 100644 --- a/Src/Lib/SharpArch.Domain/Reflection/TypePropertyDescriptorCache.cs +++ b/Src/Lib/SharpArch.Domain/Reflection/TypePropertyDescriptorCache.cs @@ -38,8 +38,10 @@ public class TypePropertyDescriptorCache : ITypePropertyDescriptorCache /// public TypePropertyDescriptor GetOrAdd(Type type, Func factory) { - if (type == null) throw new ArgumentNullException(nameof(type)); - if (factory == null) throw new ArgumentNullException(nameof(factory)); + if (type == null) + throw new ArgumentNullException(nameof(type)); + if (factory == null) + throw new ArgumentNullException(nameof(factory)); return _cache.GetOrAdd(type, factory); } diff --git a/Src/Lib/SharpArch.Domain/Specifications/QuerySpecification.cs b/Src/Lib/SharpArch.Domain/Specifications/QuerySpecification.cs index 79687a030..6cb029683 100644 --- a/Src/Lib/SharpArch.Domain/Specifications/QuerySpecification.cs +++ b/Src/Lib/SharpArch.Domain/Specifications/QuerySpecification.cs @@ -24,7 +24,8 @@ public abstract class QuerySpecification : ILinqSpecification /// is . public virtual IQueryable SatisfyingElementsFrom(IQueryable candidates) { - if (candidates == null) throw new ArgumentNullException(nameof(candidates)); + if (candidates == null) + throw new ArgumentNullException(nameof(candidates)); if (MatchingCriteria != null) { return candidates.Where(MatchingCriteria); diff --git a/Src/Lib/SharpArch.Domain/Specifications/QuerySpecificationExtensions.cs b/Src/Lib/SharpArch.Domain/Specifications/QuerySpecificationExtensions.cs index 5b10f5f60..f4dc78dec 100644 --- a/Src/Lib/SharpArch.Domain/Specifications/QuerySpecificationExtensions.cs +++ b/Src/Lib/SharpArch.Domain/Specifications/QuerySpecificationExtensions.cs @@ -21,8 +21,10 @@ public static QuerySpecification And( this QuerySpecification specification1, QuerySpecification specification2) { - if (specification1 == null) throw new ArgumentNullException(nameof(specification1)); - if (specification2 == null) throw new ArgumentNullException(nameof(specification2)); + if (specification1 == null) + throw new ArgumentNullException(nameof(specification1)); + if (specification2 == null) + throw new ArgumentNullException(nameof(specification2)); InvocationExpression invokedExpr = Expression.Invoke(specification2.MatchingCriteria!, specification1.MatchingCriteria!.Parameters); Expression> dynamicClause = Expression.Lambda>( diff --git a/Src/Lib/SharpArch.Domain/Validation/HasUniqueDomainSignatureAttributeBase.cs b/Src/Lib/SharpArch.Domain/Validation/HasUniqueDomainSignatureAttributeBase.cs index ed80bec22..4247907fb 100644 --- a/Src/Lib/SharpArch.Domain/Validation/HasUniqueDomainSignatureAttributeBase.cs +++ b/Src/Lib/SharpArch.Domain/Validation/HasUniqueDomainSignatureAttributeBase.cs @@ -48,7 +48,7 @@ protected HasUniqueDomainSignatureAttributeBase() if (entityToValidate == null) throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "This validator must be used at the class level of an " + nameof(IEntity) + ". The type you provided was '{0}'.", - (value?.GetType() as object) ?? "null")); + value?.GetType() as object ?? "null")); var duplicateChecker = (IEntityDuplicateChecker?)validationContext.GetService(typeof(IEntityDuplicateChecker)); diff --git a/Src/Lib/SharpArch.Infrastructure/CodeBaseLocator.cs b/Src/Lib/SharpArch.Infrastructure/CodeBaseLocator.cs index 5bfe82d98..ce06f6bc4 100644 --- a/Src/Lib/SharpArch.Infrastructure/CodeBaseLocator.cs +++ b/Src/Lib/SharpArch.Infrastructure/CodeBaseLocator.cs @@ -17,11 +17,12 @@ public class CodeBaseLocator /// is public static string GetAssemblyCodeBasePath(Assembly assembly) { - if (assembly == null) throw new ArgumentNullException(nameof(assembly)); + if (assembly == null) + throw new ArgumentNullException(nameof(assembly)); #if NET6_0_OR_GREATER - return Path.GetDirectoryName(assembly.Location) - ?? Directory.GetCurrentDirectory(); + return Path.GetDirectoryName(assembly.Location) + ?? Directory.GetCurrentDirectory(); #else var uri = new UriBuilder(assembly.CodeBase); diff --git a/Src/Lib/SharpArch.Infrastructure/SharpArch.Infrastructure.csproj b/Src/Lib/SharpArch.Infrastructure/SharpArch.Infrastructure.csproj index e87f26938..f13d56257 100644 --- a/Src/Lib/SharpArch.Infrastructure/SharpArch.Infrastructure.csproj +++ b/Src/Lib/SharpArch.Infrastructure/SharpArch.Infrastructure.csproj @@ -27,4 +27,8 @@ + + + + diff --git a/Src/Lib/SharpArch.NHibernate.DependencyInjection/NHibernateRegistrationExtensions.cs b/Src/Lib/SharpArch.NHibernate.DependencyInjection/NHibernateRegistrationExtensions.cs index 1199219f1..2c3c4097d 100644 --- a/Src/Lib/SharpArch.NHibernate.DependencyInjection/NHibernateRegistrationExtensions.cs +++ b/Src/Lib/SharpArch.NHibernate.DependencyInjection/NHibernateRegistrationExtensions.cs @@ -48,8 +48,10 @@ public static IServiceCollection AddNHibernateWithSingleDatabase( Func? statelessSessionConfigurator = null ) { - if (services == null) throw new ArgumentNullException(nameof(services)); - if (configureSessionFactory == null) throw new ArgumentNullException(nameof(configureSessionFactory)); + if (services == null) + throw new ArgumentNullException(nameof(services)); + if (configureSessionFactory == null) + throw new ArgumentNullException(nameof(configureSessionFactory)); services.AddSingleton(sp => { diff --git a/Src/Lib/SharpArch.NHibernate.DependencyInjection/SharpArch.NHibernate.Extensions.DependencyInjection.csproj b/Src/Lib/SharpArch.NHibernate.DependencyInjection/SharpArch.NHibernate.Extensions.DependencyInjection.csproj index deb8d78f8..194bb6de3 100644 --- a/Src/Lib/SharpArch.NHibernate.DependencyInjection/SharpArch.NHibernate.Extensions.DependencyInjection.csproj +++ b/Src/Lib/SharpArch.NHibernate.DependencyInjection/SharpArch.NHibernate.Extensions.DependencyInjection.csproj @@ -27,6 +27,10 @@ + + + + diff --git a/Src/Lib/SharpArch.NHibernate/EntityDuplicateChecker.cs b/Src/Lib/SharpArch.NHibernate/EntityDuplicateChecker.cs index b149f99a7..3390604c1 100644 --- a/Src/Lib/SharpArch.NHibernate/EntityDuplicateChecker.cs +++ b/Src/Lib/SharpArch.NHibernate/EntityDuplicateChecker.cs @@ -16,7 +16,7 @@ [PublicAPI] public class EntityDuplicateChecker : IEntityDuplicateChecker { - static readonly DateTime _uninitializedDatetime = default; + static readonly DateTime _uninitializedDatetime; readonly ISession _session; /// @@ -39,7 +39,8 @@ public EntityDuplicateChecker(ISession session) /// entity is null. public bool DoesDuplicateExistWithTypedIdOf(IEntity entity) { - if (entity == null) throw new ArgumentNullException(nameof(entity)); + if (entity == null) + throw new ArgumentNullException(nameof(entity)); var sessionForEntity = GetSessionFor(entity); @@ -102,7 +103,7 @@ static string GetPropertyName(string parentPropertyName, PropertyInfo signatureP return signatureProperty.Name; } - if (parentPropertyName.EndsWith(".") == false) + if (!parentPropertyName.EndsWith(".")) { parentPropertyName += "."; } @@ -113,7 +114,7 @@ static string GetPropertyName(string parentPropertyName, PropertyInfo signatureP static void AppendDateTimePropertyCriteriaTo(ICriteria criteria, string propertyName, object? propertyValue) { criteria.Add( - (propertyValue is null) || (DateTime)propertyValue > _uninitializedDatetime + propertyValue is null || (DateTime)propertyValue > _uninitializedDatetime ? Restrictions.Eq(propertyName, propertyValue) : Restrictions.IsNull(propertyName)); } @@ -126,8 +127,7 @@ static void AppendSignaturePropertyCriteriaTo(ICriteria criteria, IEntity entity var propertyValue = signatureProperty.GetValue(entity, null); var propertyName = signatureProperty.Name; - if (propertyType.GetInterfaces().Any( - x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEntity<>))) + if (propertyType.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEntity<>))) { AppendEntityIdCriteriaTo(criteria, propertyName, propertyValue); } diff --git a/Src/Lib/SharpArch.NHibernate/FluentNHibernate/FluentNHibernateExtensions.cs b/Src/Lib/SharpArch.NHibernate/FluentNHibernate/FluentNHibernateExtensions.cs index f060c34a2..57cabac13 100644 --- a/Src/Lib/SharpArch.NHibernate/FluentNHibernate/FluentNHibernateExtensions.cs +++ b/Src/Lib/SharpArch.NHibernate/FluentNHibernate/FluentNHibernateExtensions.cs @@ -26,7 +26,8 @@ public static class FluentNHibernateExtensions /// is null. public static FluentMappingsContainer AddFromNamespaceOf(this FluentMappingsContainer mappingsContainer) { - if (mappingsContainer == null) throw new ArgumentNullException(nameof(mappingsContainer)); + if (mappingsContainer == null) + throw new ArgumentNullException(nameof(mappingsContainer)); string ns = typeof(T).Namespace!; var types = typeof(T).Assembly.GetTypes() .Where(t => !t.IsAbstract && t.Namespace == ns) diff --git a/Src/Lib/SharpArch.NHibernate/LockModeConvertExtensions.cs b/Src/Lib/SharpArch.NHibernate/LockModeConvertExtensions.cs index fda31e9c4..70d561157 100644 --- a/Src/Lib/SharpArch.NHibernate/LockModeConvertExtensions.cs +++ b/Src/Lib/SharpArch.NHibernate/LockModeConvertExtensions.cs @@ -45,7 +45,8 @@ public static LockMode ToNHibernate(this Enums.LockMode lockMode) /// public static Enums.LockMode ToNHibernate(this LockMode lockMode) { - if (_nHibernateToSharpArchMap.TryGetValue(lockMode, out var convertedMode)) return convertedMode; + if (_nHibernateToSharpArchMap.TryGetValue(lockMode, out var convertedMode)) + return convertedMode; throw new ArgumentOutOfRangeException(nameof(lockMode), lockMode, $"The provided lock mode , '{lockMode}', could not be translated into an SharpArch.LockMode. " + "This is probably because NHibernate was updated and now has different lock modes " diff --git a/Src/Lib/SharpArch.NHibernate/NHibernateQuery.cs b/Src/Lib/SharpArch.NHibernate/NHibernateQuery.cs index f142419b4..7c03ee463 100644 --- a/Src/Lib/SharpArch.NHibernate/NHibernateQuery.cs +++ b/Src/Lib/SharpArch.NHibernate/NHibernateQuery.cs @@ -22,7 +22,8 @@ public abstract class NHibernateQuery /// is null. protected NHibernateQuery(ISession session) { - if (session == null) throw new ArgumentNullException(nameof(session)); + if (session == null) + throw new ArgumentNullException(nameof(session)); Session = session; } diff --git a/Src/Lib/SharpArch.NHibernate/NHibernateRepositoryBase.cs b/Src/Lib/SharpArch.NHibernate/NHibernateRepositoryBase.cs index b74feca38..cf52474ba 100644 --- a/Src/Lib/SharpArch.NHibernate/NHibernateRepositoryBase.cs +++ b/Src/Lib/SharpArch.NHibernate/NHibernateRepositoryBase.cs @@ -80,7 +80,8 @@ public Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = de public async Task DeleteAsync(TId id, CancellationToken cancellationToken = default) { var entity = await GetAsync(id, cancellationToken).ConfigureAwait(false); - if (entity != null) await DeleteAsync(entity, cancellationToken).ConfigureAwait(false); + if (entity != null) + await DeleteAsync(entity, cancellationToken).ConfigureAwait(false); } ITransactionManager IRepository.TransactionManager => TransactionManager; @@ -91,7 +92,8 @@ public virtual Task> FindAllAsync( int? maxResults = null, CancellationToken cancellationToken = default) { - if (propertyValuePairs == null) throw new ArgumentNullException(nameof(propertyValuePairs)); + if (propertyValuePairs == null) + throw new ArgumentNullException(nameof(propertyValuePairs)); if (propertyValuePairs.Count == 0) throw new ArgumentException("No properties specified. Please specify at least one property/value pair.", nameof(propertyValuePairs)); @@ -104,7 +106,8 @@ public virtual Task> FindAllAsync( : Restrictions.IsNull(key)); } - if (maxResults.HasValue) criteria.SetMaxResults(maxResults.Value); + if (maxResults.HasValue) + criteria.SetMaxResults(maxResults.Value); return criteria.ListAsync(cancellationToken); } @@ -115,11 +118,13 @@ public Task> FindAllAsync( ICriteria criteria = Session.CreateCriteria(typeof(TEntity)); Example example = Example.Create(exampleInstance); - foreach (string propertyToExclude in propertiesToExclude) example.ExcludeProperty(propertyToExclude); + foreach (string propertyToExclude in propertiesToExclude) + example.ExcludeProperty(propertyToExclude); criteria.Add(example); - if (maxResults.HasValue) criteria.SetMaxResults(maxResults.Value); + if (maxResults.HasValue) + criteria.SetMaxResults(maxResults.Value); return criteria.ListAsync(cancellationToken); } @@ -128,7 +133,8 @@ public Task> FindAllAsync( public virtual async Task FindOneAsync(TEntity exampleInstance, CancellationToken ct, params string[] propertiesToExclude) { IList foundList = await FindAllAsync(exampleInstance, propertiesToExclude, 2, ct).ConfigureAwait(false); - if (foundList.Count > 1) throw new NonUniqueResultException(foundList.Count); + if (foundList.Count > 1) + throw new NonUniqueResultException(foundList.Count); return foundList.Count == 1 ? foundList[0] : default; } @@ -139,7 +145,8 @@ public Task> FindAllAsync( CancellationToken cancellationToken = default) { var foundList = await FindAllAsync(propertyValuePairs, 2, cancellationToken).ConfigureAwait(false); - if (foundList.Count > 1) throw new NonUniqueResultException(foundList.Count); + if (foundList.Count > 1) + throw new NonUniqueResultException(foundList.Count); return foundList.Count == 1 ? foundList[0] : default; } diff --git a/Src/Lib/SharpArch.NHibernate/NHibernateSessionFactoryBuilder.cs b/Src/Lib/SharpArch.NHibernate/NHibernateSessionFactoryBuilder.cs index 872163562..9621db12d 100644 --- a/Src/Lib/SharpArch.NHibernate/NHibernateSessionFactoryBuilder.cs +++ b/Src/Lib/SharpArch.NHibernate/NHibernateSessionFactoryBuilder.cs @@ -94,7 +94,8 @@ bool ShouldExposeConfiguration() /// Mapping assemblies are not specified. public NHibernateSessionFactoryBuilder AddMappingAssemblies(IEnumerable mappingAssemblies) { - if (mappingAssemblies == null) throw new ArgumentNullException(nameof(mappingAssemblies), "Mapping assemblies are not specified."); + if (mappingAssemblies == null) + throw new ArgumentNullException(nameof(mappingAssemblies), "Mapping assemblies are not specified."); _mappingAssemblies.AddRange(mappingAssemblies); return this; @@ -122,9 +123,11 @@ public NHibernateSessionFactoryBuilder UseAutoPersistenceModel( /// is null. public NHibernateSessionFactoryBuilder UseProperties(IEnumerable> properties) { - if (properties == null) throw new ArgumentNullException(nameof(properties)); + if (properties == null) + throw new ArgumentNullException(nameof(properties)); - if (_properties == null) _properties = new Dictionary(64); + if (_properties == null) + _properties = new Dictionary(64); foreach (var pair in properties) { _properties[pair.Key] = pair.Value; diff --git a/Src/Lib/SharpArch.NHibernate/SessionExtensions.cs b/Src/Lib/SharpArch.NHibernate/SessionExtensions.cs index 64db28650..44ab303c6 100644 --- a/Src/Lib/SharpArch.NHibernate/SessionExtensions.cs +++ b/Src/Lib/SharpArch.NHibernate/SessionExtensions.cs @@ -21,8 +21,10 @@ public static class SessionExtensions /// public static void FlushAndEvict(this ISession session, object entity) { - if (session == null) throw new ArgumentNullException(nameof(session)); - if (entity == null) throw new ArgumentNullException(nameof(entity)); + if (session == null) + throw new ArgumentNullException(nameof(session)); + if (entity == null) + throw new ArgumentNullException(nameof(entity)); // Commits any changes up to this point to the database session.Flush(); @@ -42,8 +44,10 @@ public static void FlushAndEvict(this ISession session, object entity) /// public static async Task FlushAndEvictAsync(this ISession session, object entity, CancellationToken cancellationToken) { - if (session == null) throw new ArgumentNullException(nameof(session)); - if (entity == null) throw new ArgumentNullException(nameof(entity)); + if (session == null) + throw new ArgumentNullException(nameof(session)); + if (entity == null) + throw new ArgumentNullException(nameof(entity)); // Commits any changes up to this point to the database await session.FlushAsync(cancellationToken).ConfigureAwait(false); @@ -63,8 +67,10 @@ public static async Task FlushAndEvictAsync(this ISession session, object entity /// public static async Task FlushAndEvictAsync(this ISession session, CancellationToken cancellationToken, params object[] entities) { - if (session == null) throw new ArgumentNullException(nameof(session)); - if (entities == null) throw new ArgumentNullException(nameof(entities)); + if (session == null) + throw new ArgumentNullException(nameof(session)); + if (entities == null) + throw new ArgumentNullException(nameof(entities)); // Commits any changes up to this point to the database await session.FlushAsync(cancellationToken).ConfigureAwait(false); @@ -73,7 +79,8 @@ public static async Task FlushAndEvictAsync(this ISession session, CancellationT { var entity = entities[i]; - if (entity == null) throw new ArgumentNullException(nameof(entities), $"Item at index {i} it null."); + if (entity == null) + throw new ArgumentNullException(nameof(entities), $"Item at index {i} it null."); await session.EvictAsync(entity, cancellationToken).ConfigureAwait(false); } } @@ -89,12 +96,15 @@ public static async Task FlushAndEvictAsync(this ISession session, CancellationT /// is null. public static Task IncrementVersionAsync(this ISession session, object? entity, CancellationToken cancellationToken) { - if (session == null) throw new ArgumentNullException(nameof(session)); - if (entity == null) return Task.CompletedTask; + if (session == null) + throw new ArgumentNullException(nameof(session)); + if (entity == null) + return Task.CompletedTask; // don't process deleted entity. var entry = session.GetSessionImplementation().PersistenceContext.GetEntry(entity); - if (entry == null || entry.Status is Status.Deleted or Status.Gone) return Task.CompletedTask; + if (entry == null || entry.Status is Status.Deleted or Status.Gone) + return Task.CompletedTask; return session.LockAsync(entity, LockMode.Force, cancellationToken); } @@ -108,12 +118,15 @@ public static Task IncrementVersionAsync(this ISession session, object? entity, /// is null. public static void IncrementVersion(this ISession session, object? entity) { - if (session == null) throw new ArgumentNullException(nameof(session)); - if (entity == null) return; + if (session == null) + throw new ArgumentNullException(nameof(session)); + if (entity == null) + return; // ignore deleted entity var entry = session.GetSessionImplementation().PersistenceContext.GetEntry(entity); - if (entry == null || entry.Status is Status.Deleted or Status.Gone) return; + if (entry == null || entry.Status is Status.Deleted or Status.Gone) + return; session.Lock(entity, LockMode.Force); } @@ -130,15 +143,20 @@ public static void IncrementVersion(this ISession session, object? entity) /// public static bool IsModified(this ISession session, object entity) { - if (session == null) throw new ArgumentNullException(nameof(session)); - if (entity == null) throw new ArgumentNullException(nameof(entity)); + if (session == null) + throw new ArgumentNullException(nameof(session)); + if (entity == null) + throw new ArgumentNullException(nameof(entity)); var sessionImplementation = session.GetSessionImplementation(); var entry = sessionImplementation.PersistenceContext.GetEntry(entity); - if (entry == null) return false; + if (entry == null) + return false; var persister = entry.Persister; - if (entry.Status == Status.Deleted) return true; - if (!entry.RequiresDirtyCheck(entity)) return false; + if (entry.Status == Status.Deleted) + return true; + if (!entry.RequiresDirtyCheck(entity)) + return false; var currentValue = persister.GetPropertyValues(entity); var dirtyPropertyIndexes = persister.FindDirty(currentValue, entry.LoadedState, entity, sessionImplementation); diff --git a/Src/Lib/SharpArch.NHibernate/SharpArch.NHibernate.csproj b/Src/Lib/SharpArch.NHibernate/SharpArch.NHibernate.csproj index 3afc7f4f0..70710347e 100644 --- a/Src/Lib/SharpArch.NHibernate/SharpArch.NHibernate.csproj +++ b/Src/Lib/SharpArch.NHibernate/SharpArch.NHibernate.csproj @@ -16,7 +16,7 @@ - + diff --git a/Src/Lib/SharpArch.Testing.NUnit/NHibernate/DatabaseRepositoryTestsBase.cs b/Src/Lib/SharpArch.Testing.NUnit/NHibernate/DatabaseRepositoryTestsBase.cs index 949d16065..8148c049b 100644 --- a/Src/Lib/SharpArch.Testing.NUnit/NHibernate/DatabaseRepositoryTestsBase.cs +++ b/Src/Lib/SharpArch.Testing.NUnit/NHibernate/DatabaseRepositoryTestsBase.cs @@ -5,6 +5,7 @@ using global::NUnit.Framework; using Testing.NHibernate; + /// /// /// Initiates a transaction before each test is run and rolls back the transaction after @@ -67,7 +68,8 @@ protected DatabaseRepositoryTestsBase(TestDatabaseSetup initializer) [OneTimeSetUp] public void OneTimeSetUp() { - if (Initializer == null) throw new InvalidOperationException($"{nameof(Initializer)} is not set."); + if (Initializer == null) + throw new InvalidOperationException($"{nameof(Initializer)} is not set."); UpdateConfiguration(Initializer.GetConfiguration()); } @@ -85,7 +87,8 @@ protected virtual void UpdateConfiguration(Configuration configuration) [SetUp] public virtual void SetUp() { - if (Initializer == null) throw new InvalidOperationException($"{nameof(Initializer)} is not set."); + if (Initializer == null) + throw new InvalidOperationException($"{nameof(Initializer)} is not set."); Session = Initializer.GetSessionFactory().OpenSession(); Session.BeginTransaction(); } @@ -99,7 +102,8 @@ public virtual void TearDown() if (Session != null) { var currentTransaction = Session.GetCurrentTransaction(); - if (currentTransaction is { IsActive: true }) currentTransaction.Rollback(); + if (currentTransaction is { IsActive: true }) + currentTransaction.Rollback(); Session.Dispose(); Session = null; } diff --git a/Src/Lib/SharpArch.Testing.NUnit/NHibernate/RepositoryTestsBase.cs b/Src/Lib/SharpArch.Testing.NUnit/NHibernate/RepositoryTestsBase.cs index de3a7bce0..219918329 100644 --- a/Src/Lib/SharpArch.Testing.NUnit/NHibernate/RepositoryTestsBase.cs +++ b/Src/Lib/SharpArch.Testing.NUnit/NHibernate/RepositoryTestsBase.cs @@ -71,7 +71,8 @@ public virtual void TearDown() /// is protected void FlushSessionAndEvict(object instance) { - if (instance == null) throw new ArgumentNullException(nameof(instance)); + if (instance == null) + throw new ArgumentNullException(nameof(instance)); Session.FlushAndEvict(instance); } @@ -82,7 +83,8 @@ protected void FlushSessionAndEvict(object instance) /// is protected void SaveAndEvict(object instance) { - if (instance == null) throw new ArgumentNullException(nameof(instance)); + if (instance == null) + throw new ArgumentNullException(nameof(instance)); Session.Save(instance); FlushSessionAndEvict(instance); } diff --git a/Src/Lib/SharpArch.Testing.NUnit/SharpArch.Testing.NUnit.csproj b/Src/Lib/SharpArch.Testing.NUnit/SharpArch.Testing.NUnit.csproj index 4bc5f10b0..0a0a00a2f 100644 --- a/Src/Lib/SharpArch.Testing.NUnit/SharpArch.Testing.NUnit.csproj +++ b/Src/Lib/SharpArch.Testing.NUnit/SharpArch.Testing.NUnit.csproj @@ -24,7 +24,7 @@ - + diff --git a/Src/Lib/SharpArch.Testing.Xunit.NHibernate/LiveDatabaseTests.cs b/Src/Lib/SharpArch.Testing.Xunit.NHibernate/LiveDatabaseTests.cs index ec01c115d..368a4dbe1 100644 --- a/Src/Lib/SharpArch.Testing.Xunit.NHibernate/LiveDatabaseTests.cs +++ b/Src/Lib/SharpArch.Testing.Xunit.NHibernate/LiveDatabaseTests.cs @@ -53,7 +53,8 @@ public virtual void Dispose() if (Session != null) { var currentTransaction = Session.GetCurrentTransaction(); - if (currentTransaction != null && currentTransaction.IsActive) currentTransaction.Rollback(); + if (currentTransaction != null && currentTransaction.IsActive) + currentTransaction.Rollback(); Session.Dispose(); Session = null; } diff --git a/Src/Lib/SharpArch.Testing.Xunit.NHibernate/SharpArch.Testing.Xunit.NHibernate.csproj b/Src/Lib/SharpArch.Testing.Xunit.NHibernate/SharpArch.Testing.Xunit.NHibernate.csproj index cb6ee70f4..7a791ca40 100644 --- a/Src/Lib/SharpArch.Testing.Xunit.NHibernate/SharpArch.Testing.Xunit.NHibernate.csproj +++ b/Src/Lib/SharpArch.Testing.Xunit.NHibernate/SharpArch.Testing.Xunit.NHibernate.csproj @@ -16,10 +16,14 @@ - + + + + + diff --git a/Src/Lib/SharpArch.Testing.Xunit.NHibernate/TransientDatabaseTests.cs b/Src/Lib/SharpArch.Testing.Xunit.NHibernate/TransientDatabaseTests.cs index 766ef4b4f..d17e3b6ae 100644 --- a/Src/Lib/SharpArch.Testing.Xunit.NHibernate/TransientDatabaseTests.cs +++ b/Src/Lib/SharpArch.Testing.Xunit.NHibernate/TransientDatabaseTests.cs @@ -51,12 +51,21 @@ protected TransientDatabaseTests(TestDatabaseSetup dbSetup) } /// - public Task InitializeAsync() - => LoadTestData(CancellationToken.None); +#if NET8_0_OR_GREATER + public async ValueTask InitializeAsync() +#else + public async Task InitializeAsync() +#endif + => await LoadTestData(CancellationToken.None).ConfigureAwait(false); /// +#if NET8_0_OR_GREATER + public ValueTask DisposeAsync() + => new ValueTask(Task.CompletedTask); +#else public Task DisposeAsync() - => Task.CompletedTask; + => Task.CompletedTask; +#endif /// public virtual void Dispose() @@ -81,7 +90,8 @@ protected Task FlushSessionAndEvict(object instance, CancellationToken cancellat /// is protected async Task SaveAndEvict(object instance, CancellationToken cancellationToken = default) { - if (instance == null) throw new ArgumentNullException(nameof(instance)); + if (instance == null) + throw new ArgumentNullException(nameof(instance)); await Session.SaveAsync(instance, cancellationToken).ConfigureAwait(false); await FlushSessionAndEvict(instance, cancellationToken).ConfigureAwait(false); } diff --git a/Src/Lib/SharpArch.Testing.Xunit/SetCultureAttribute.cs b/Src/Lib/SharpArch.Testing.Xunit/SetCultureAttribute.cs index 0e4ed3251..abd864d60 100644 --- a/Src/Lib/SharpArch.Testing.Xunit/SetCultureAttribute.cs +++ b/Src/Lib/SharpArch.Testing.Xunit/SetCultureAttribute.cs @@ -2,7 +2,12 @@ using System.Globalization; using System.Reflection; +#if NET8_0_OR_GREATER +using global::Xunit.v3; + +#else using global::Xunit.Sdk; +#endif /// @@ -36,8 +41,10 @@ public SetCultureAttribute(string culture) /// The name of the UI culture. public SetCultureAttribute(string culture, string uiCulture) { - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture)); - if (string.IsNullOrWhiteSpace(uiCulture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(uiCulture)); + if (string.IsNullOrWhiteSpace(culture)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture)); + if (string.IsNullOrWhiteSpace(uiCulture)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(uiCulture)); _culture = new Lazy(() => new CultureInfo(culture, false)); _uiCulture = new Lazy(() => new CultureInfo(uiCulture, false)); } @@ -47,7 +54,11 @@ public SetCultureAttribute(string culture, string uiCulture) /// and /// and replaces them with the new cultures defined in the constructor. /// +#if NET8_0_OR_GREATER + public override void Before(MethodInfo methodUnderTest, IXunitTest test) +#else public override void Before(MethodInfo methodUnderTest) +#endif { _originalCulture = CultureInfo.CurrentCulture; _originalUiCulture = CultureInfo.CurrentUICulture; @@ -66,10 +77,16 @@ static void ClearCachedData() /// Restores the original and /// /// +#if NET8_0_OR_GREATER + public override void After(MethodInfo methodUnderTest, IXunitTest test) +#else public override void After(MethodInfo methodUnderTest) +#endif { - if (_originalCulture != null) CultureInfo.CurrentCulture = _originalCulture; - if (_originalUiCulture != null) CultureInfo.CurrentUICulture = _originalUiCulture; + if (_originalCulture != null) + CultureInfo.CurrentCulture = _originalCulture; + if (_originalUiCulture != null) + CultureInfo.CurrentUICulture = _originalUiCulture; ClearCachedData(); } } diff --git a/Src/Lib/SharpArch.Testing.Xunit/SharpArch.Testing.Xunit.csproj b/Src/Lib/SharpArch.Testing.Xunit/SharpArch.Testing.Xunit.csproj index 419666c95..868a5554d 100644 --- a/Src/Lib/SharpArch.Testing.Xunit/SharpArch.Testing.Xunit.csproj +++ b/Src/Lib/SharpArch.Testing.Xunit/SharpArch.Testing.Xunit.csproj @@ -14,10 +14,14 @@ - + + + + + false diff --git a/Src/Lib/SharpArch.Testing/Helpers/EntityIdSetter.cs b/Src/Lib/SharpArch.Testing/Helpers/EntityIdSetter.cs index a5e0f3c17..555c54b47 100644 --- a/Src/Lib/SharpArch.Testing/Helpers/EntityIdSetter.cs +++ b/Src/Lib/SharpArch.Testing/Helpers/EntityIdSetter.cs @@ -22,11 +22,13 @@ public static class EntityIdSetter public static void SetIdOf(IEntity entity, TId id) where TId : IEquatable { - if (entity == null) throw new ArgumentNullException(nameof(entity)); + if (entity == null) + throw new ArgumentNullException(nameof(entity)); // Set the data property reflectively PropertyInfo? idProperty = entity.GetType().GetProperty("Id", BindingFlags.Public | BindingFlags.Instance); - if (idProperty == null) throw new InvalidOperationException("Property with name 'Id' could not be found."); + if (idProperty == null) + throw new InvalidOperationException("Property with name 'Id' could not be found."); idProperty.SetValue(entity, id, null); } diff --git a/Src/Lib/SharpArch.Web.AspNetCore/Transaction/AutoTransactionHandler.cs b/Src/Lib/SharpArch.Web.AspNetCore/Transaction/AutoTransactionHandler.cs index fa2240d9a..e02d99b88 100644 --- a/Src/Lib/SharpArch.Web.AspNetCore/Transaction/AutoTransactionHandler.cs +++ b/Src/Lib/SharpArch.Web.AspNetCore/Transaction/AutoTransactionHandler.cs @@ -39,7 +39,7 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE } if (executedContext.Exception != null || - transactionAttribute!.RollbackOnModelValidationError && context.ModelState.IsValid == false) + transactionAttribute!.RollbackOnModelValidationError && !context.ModelState.IsValid) { // don't use cancellation token to ensure transaction is rolled back on error await transactionManager.RollbackTransactionAsync().ConfigureAwait(false); diff --git a/Src/Lib/SharpArch.Web.AspNetCore/Transaction/TransactionAttribute.cs b/Src/Lib/SharpArch.Web.AspNetCore/Transaction/TransactionAttribute.cs index 1169bec88..6ceed07a7 100644 --- a/Src/Lib/SharpArch.Web.AspNetCore/Transaction/TransactionAttribute.cs +++ b/Src/Lib/SharpArch.Web.AspNetCore/Transaction/TransactionAttribute.cs @@ -62,8 +62,10 @@ public TransactionAttribute(IsolationLevel isolationLevel = IsolationLevel.ReadC /// public bool Equals(TransactionAttribute? other) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; + if (ReferenceEquals(null, other)) + return false; + if (ReferenceEquals(this, other)) + return true; return RollbackOnModelValidationError == other.RollbackOnModelValidationError && IsolationLevel == other.IsolationLevel; } diff --git a/Src/Samples/Directory.Build.props b/Src/Samples/Directory.Build.props index 6d4e9699c..13afd9181 100644 --- a/Src/Samples/Directory.Build.props +++ b/Src/Samples/Directory.Build.props @@ -4,6 +4,7 @@ $(AppTargetFrameworks) + $(NoWarn);NU1903 diff --git a/Src/Samples/TardisBank/Src/Directory.Build.props b/Src/Samples/TardisBank/Src/Directory.Build.props index 7cbfe1490..9e6165c05 100644 --- a/Src/Samples/TardisBank/Src/Directory.Build.props +++ b/Src/Samples/TardisBank/Src/Directory.Build.props @@ -3,7 +3,7 @@ - net9.0 + net9.0;net10.0 diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Api/Announcements/AnnouncementModel.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Api/Announcements/AnnouncementModel.cs index a2cee8635..c81f43094 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Api/Announcements/AnnouncementModel.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Api/Announcements/AnnouncementModel.cs @@ -1,7 +1,7 @@ namespace Suteki.TardisBank.Api.Announcements; using System.ComponentModel.DataAnnotations; -using Newtonsoft.Json; +using System.Text.Json.Serialization; /// @@ -9,15 +9,15 @@ /// public class AnnouncementSummary { - [JsonProperty("id")] + [JsonPropertyName("id")] public int Id { get; set; } - [JsonProperty("date")] + [JsonPropertyName("date")] [DataType(DataType.Date)] public DateTime Date { get; set; } [Required] - [JsonProperty("title")] + [JsonPropertyName("title")] public string Title { get; set; } = null!; } @@ -27,7 +27,7 @@ public class AnnouncementSummary /// public class AnnouncementModel : AnnouncementSummary { - [JsonProperty("content")] + [JsonPropertyName("content")] [Required] public string Content { get; set; } = null!; } diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Api/Announcements/NewAnnouncement.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Api/Announcements/NewAnnouncement.cs index 207a9feba..9dd955242 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Api/Announcements/NewAnnouncement.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Api/Announcements/NewAnnouncement.cs @@ -1,20 +1,20 @@ namespace Suteki.TardisBank.Api.Announcements; using System.ComponentModel.DataAnnotations; -using Newtonsoft.Json; +using System.Text.Json.Serialization; public class NewAnnouncement { - [JsonProperty("date")] + [JsonPropertyName("date")] [DataType(DataType.Date)] public DateTime Date { get; set; } [Required] - [JsonProperty("title")] + [JsonPropertyName("title")] public string Title { get; set; } = null!; - [JsonProperty("content")] + [JsonPropertyName("content")] [Required] public string Content { get; set; } = null!; } diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Api/Suteki.TardisBank.Api.csproj b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Api/Suteki.TardisBank.Api.csproj index 3e03f86ed..61718a1f8 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Api/Suteki.TardisBank.Api.csproj +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Api/Suteki.TardisBank.Api.csproj @@ -1,7 +1,3 @@ - - - - diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/Account.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/Account.cs index 8176fe7a8..8f7f79c99 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/Account.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/Account.cs @@ -1,4 +1,5 @@ // ReSharper disable MissingXmlDoc + namespace Suteki.TardisBank.Domain; using SharpArch.Domain.DomainModel; @@ -34,7 +35,8 @@ public virtual void AddTransaction(string? description, decimal amount) void RemoveOldTransactions() { - if (Transactions.Count <= MaxTransactions) return; + if (Transactions.Count <= MaxTransactions) + return; var oldestTransaction = Transactions.First(); Transactions.Remove(oldestTransaction); @@ -59,7 +61,8 @@ public virtual void TriggerScheduledPayments(DateTime now) public virtual void RemovePaymentSchedule(int paymentScheduleId) { var scheduleToRemove = PaymentSchedules.SingleOrDefault(x => x.Id == paymentScheduleId); - if (scheduleToRemove == null) return; + if (scheduleToRemove == null) + return; PaymentSchedules.Remove(scheduleToRemove); } diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/Announcement.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/Announcement.cs index 9848b874a..a2c8b8271 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/Announcement.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/Announcement.cs @@ -1,4 +1,5 @@ // ReSharper disable MissingXmlDoc + namespace Suteki.TardisBank.Domain; using System.ComponentModel.DataAnnotations; diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/Parent.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/Parent.cs index a64c01e30..1ea0fd1bf 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/Parent.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/Parent.cs @@ -1,4 +1,5 @@ // ReSharper disable MissingXmlDoc + namespace Suteki.TardisBank.Domain; using Events; @@ -24,7 +25,8 @@ protected Parent() // should be called when parent is first created. public virtual Parent Initialise(IMediator mediator) { - if (mediator == null) throw new ArgumentNullException(nameof(mediator)); + if (mediator == null) + throw new ArgumentNullException(nameof(mediator)); ActivationKey = Guid.NewGuid().ToString(); mediator.Publish(new NewParentCreatedEvent(this)); diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/Suteki.TardisBank.Domain.csproj b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/Suteki.TardisBank.Domain.csproj index c2902264c..8dd7604b2 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/Suteki.TardisBank.Domain.csproj +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/Suteki.TardisBank.Domain.csproj @@ -2,7 +2,6 @@ - diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/User.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/User.cs index 4b039c72c..b832fb8b6 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/User.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Domain/User.cs @@ -1,4 +1,5 @@ // ReSharper disable MissingXmlDoc + namespace Suteki.TardisBank.Domain; using Events; @@ -48,7 +49,8 @@ protected User(string name, string userName, string password) public virtual void SendMessage(string text, IMediator mediator) { - if (mediator == null) throw new ArgumentNullException(nameof(mediator)); + if (mediator == null) + throw new ArgumentNullException(nameof(mediator)); Messages.Add(new Message(DateTime.Now.Date, text, this)); RemoveOldMessages(); @@ -58,7 +60,8 @@ public virtual void SendMessage(string text, IMediator mediator) void RemoveOldMessages() { - if (Messages.Count <= MaxMessages) return; + if (Messages.Count <= MaxMessages) + return; var oldestMessage = Messages.First(); Messages.Remove(oldestMessage); diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Infrastructure/Configuration/AnnouncementMappingProfile.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Infrastructure/Configuration/AnnouncementMappingProfile.cs index 404d500b3..7757af3d1 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Infrastructure/Configuration/AnnouncementMappingProfile.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Infrastructure/Configuration/AnnouncementMappingProfile.cs @@ -2,7 +2,6 @@ using Api.Announcements; using AutoMapper; -using AutoMapper.Internal; using Domain; diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Infrastructure/Suteki.TardisBank.Infrastructure.csproj b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Infrastructure/Suteki.TardisBank.Infrastructure.csproj index 596e15c17..5a553623c 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Infrastructure/Suteki.TardisBank.Infrastructure.csproj +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Infrastructure/Suteki.TardisBank.Infrastructure.csproj @@ -2,7 +2,7 @@ - + diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tasks/EmailService.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tasks/EmailService.cs index 719cc28bd..ad807fb71 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tasks/EmailService.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tasks/EmailService.cs @@ -36,7 +36,8 @@ public void SendEmail(string toAddress, string subject, string body) throw new ArgumentNullException(nameof(body)); } - if (string.IsNullOrWhiteSpace(_configuration.EmailSmtpServer)) return; + if (string.IsNullOrWhiteSpace(_configuration.EmailSmtpServer)) + return; var message = new MailMessage( _configuration.EmailFromAddress, @@ -44,11 +45,7 @@ public void SendEmail(string toAddress, string subject, string body) subject, body); - var client = new SmtpClient(_configuration.EmailSmtpServer) - { - EnableSsl = _configuration.EmailEnableSsl, - Port = _configuration.EmailPort, - }; + var client = new SmtpClient(_configuration.EmailSmtpServer) { EnableSsl = _configuration.EmailEnableSsl, Port = _configuration.EmailPort, }; if (!string.IsNullOrWhiteSpace(_configuration.EmailCredentialsUserName) && !string.IsNullOrWhiteSpace(_configuration.EmailCredentialsPassword)) diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tasks/UserService.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tasks/UserService.cs index 0cc1dec39..b20848996 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tasks/UserService.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tasks/UserService.cs @@ -22,7 +22,8 @@ public UserService(IHttpContextService context, ILinqRepository par public Task GetCurrentUser(CancellationToken cancellationToken) { - if (!_context.UserIsAuthenticated) return Task.FromResult(null); + if (!_context.UserIsAuthenticated) + return Task.FromResult(null); return GetUserByUserName(_context.UserName, cancellationToken); } @@ -54,14 +55,16 @@ public UserService(IHttpContextService context, ILinqRepository par public Task SaveUser(User user, CancellationToken cancellationToken) { - if (user == null) throw new ArgumentNullException(nameof(user)); + if (user == null) + throw new ArgumentNullException(nameof(user)); return _userRepository.SaveAsync(user, cancellationToken); } public bool AreNullOrNotRelated(Parent? parent, Child? child) { - if (parent == null || child == null) return true; + if (parent == null || child == null) + return true; if (!parent.HasChild(child)) { @@ -73,8 +76,8 @@ public bool AreNullOrNotRelated(Parent? parent, Child? child) public async Task IsNotChildOfCurrentUser(Child? child, CancellationToken cancellationToken) { - var parent = (await GetCurrentUser(cancellationToken).ConfigureAwait(false)) as Parent; - return (child == null) || (parent == null) || (!parent.HasChild(child)); + var parent = await GetCurrentUser(cancellationToken).ConfigureAwait(false) as Parent; + return child == null || parent == null || !parent.HasChild(child); } public Task DeleteUser(int userId, CancellationToken cancellationToken) diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Functional/AnnouncementControllerTests.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Functional/AnnouncementControllerTests.cs index 746fed1fd..a9553044c 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Functional/AnnouncementControllerTests.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Functional/AnnouncementControllerTests.cs @@ -1,10 +1,11 @@ namespace Suteki.TardisBank.Tests.Functional; using Api.Announcements; -using Shouldly; using Setup; +using Shouldly; using Xunit; + [Trait("Category", "Functional")] public class AnnouncementControllerTests : IClassFixture, IDisposable { @@ -33,12 +34,7 @@ public async Task CanSaveAnnouncement() { var today = DateTime.Now.Date; var uid = Guid.NewGuid().ToString("N"); - var newAnnouncement = new NewAnnouncement - { - Date = today, - Content = "New announcement " + today, - Title = uid - }; + var newAnnouncement = new NewAnnouncement { Date = today, Content = "New announcement " + today, Title = uid }; var response = await _setup.Client.PostAsJsonAsync("announcements", newAnnouncement); response.EnsureSuccessStatusCode(); @@ -52,4 +48,4 @@ public async Task CanSaveAnnouncement() await DeleteAnnouncement(_newAnnouncementUri); } -} \ No newline at end of file +} diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Helpers/RunnableInDebugOnlyAttribute.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Helpers/RunnableInDebugOnlyAttribute.cs index e52c4ae70..2b146f490 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Helpers/RunnableInDebugOnlyAttribute.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Helpers/RunnableInDebugOnlyAttribute.cs @@ -1,6 +1,7 @@ namespace Suteki.TardisBank.Tests.Helpers; using System.Diagnostics; +using System.Runtime.CompilerServices; using Xunit; @@ -11,11 +12,16 @@ [AttributeUsage(AttributeTargets.Method)] public sealed class RunnableInDebugOnlyAttribute : FactAttribute { +#if NET8_0_OR_GREATER + public RunnableInDebugOnlyAttribute( + [CallerFilePath] string? sourceFilePath = null, + [CallerLineNumber] int sourceLineNumber = -1) + : base(sourceFilePath, sourceLineNumber) +#else public RunnableInDebugOnlyAttribute() +#endif { if (!Debugger.IsAttached) - { Skip = "Only running in interactive mode."; - } } } diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/ChildTests.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/ChildTests.cs index ce7afd8a9..63481aeb3 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/ChildTests.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/ChildTests.cs @@ -1,9 +1,9 @@ namespace Suteki.TardisBank.Tests.Model; using Domain; -using Shouldly; using SharpArch.NHibernate; using SharpArch.Testing.Xunit.NHibernate; +using Shouldly; using Xunit; @@ -68,4 +68,4 @@ public async Task Should_be_able_to_create_and_retrieve_a_child() child.Password.ShouldBe("xxx"); child.Account.ShouldNotBeNull(); } -} \ No newline at end of file +} diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/MessageCountLimitTests.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/MessageCountLimitTests.cs index 186dae839..00f233e0e 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/MessageCountLimitTests.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/MessageCountLimitTests.cs @@ -1,9 +1,9 @@ namespace Suteki.TardisBank.Tests.Model; using Domain; -using Shouldly; using MediatR; using Moq; +using Shouldly; using Xunit; @@ -38,4 +38,4 @@ public void Number_of_messages_should_be_limited() _user.Messages.First().Text.ShouldBe("Message2"); _user.Messages.Last().Text.ShouldBe("New2"); } -} \ No newline at end of file +} diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/MessageTests.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/MessageTests.cs index a99ca940a..c99c34115 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/MessageTests.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/MessageTests.cs @@ -1,11 +1,11 @@ namespace Suteki.TardisBank.Tests.Model; using Domain; -using Shouldly; using MediatR; using Moq; using SharpArch.NHibernate; using SharpArch.Testing.Xunit.NHibernate; +using Shouldly; using Xunit; @@ -42,4 +42,4 @@ public async Task Should_be_able_to_add_a_message_to_a_user() Parent parent = (await parentRepository.GetAsync(_userId))!; parent.Messages.Count.ShouldBe(1); } -} \ No newline at end of file +} diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/ParentTests.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/ParentTests.cs index cd212f979..d56dc45a8 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/ParentTests.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/ParentTests.cs @@ -1,9 +1,9 @@ namespace Suteki.TardisBank.Tests.Model; using Domain; -using Shouldly; using SharpArch.NHibernate; using SharpArch.Testing.Xunit.NHibernate; +using Shouldly; using Xunit; @@ -53,4 +53,4 @@ public async Task Should_be_able_to_create_and_retrieve_Parent() parent.UserName.ShouldBe("mike@yahoo.com"); parent.Children.ShouldNotBeNull(); } -} \ No newline at end of file +} diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/PaymentSchedulingQueryTests.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/PaymentSchedulingQueryTests.cs index 7228712bf..a9a3d0090 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/PaymentSchedulingQueryTests.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/PaymentSchedulingQueryTests.cs @@ -1,9 +1,9 @@ namespace Suteki.TardisBank.Tests.Model; using Domain; -using Shouldly; using NHibernate.Linq; using SharpArch.Testing.Xunit.NHibernate; +using Shouldly; using Tasks; using Xunit; diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/PaymentSchedulingTests.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/PaymentSchedulingTests.cs index 3df321b1b..b6aa5c7f4 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/PaymentSchedulingTests.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/PaymentSchedulingTests.cs @@ -1,8 +1,8 @@ namespace Suteki.TardisBank.Tests.Model; using Domain; -using Shouldly; using Humanizer; +using Shouldly; using Xunit; @@ -70,4 +70,4 @@ public void Triggering_payment_before_next_run_causes_nothing_to_happen() _child.Account.Transactions.Count.ShouldBe(0); _child.Account.PaymentSchedules[0].NextRun.ShouldBe(_startDate); } -} \ No newline at end of file +} diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/TransactionCountLimitTests.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/TransactionCountLimitTests.cs index 2b3219a32..bb822cf31 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/TransactionCountLimitTests.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/TransactionCountLimitTests.cs @@ -43,4 +43,4 @@ public void When_more_than_max_transactions_created_transactions_should_be_trunc _child.Account.Transactions.First().Description.ShouldBe("payment2"); _child.Account.Transactions.Last().Description.ShouldBe("payment_new2"); } -} \ No newline at end of file +} diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/TransientDatabaseSetup.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/TransientDatabaseSetup.cs index 42c57fcd5..53d148a04 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/TransientDatabaseSetup.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/TransientDatabaseSetup.cs @@ -8,10 +8,8 @@ public class TransientDatabaseSetup : TestDatabaseSetup { /// public TransientDatabaseSetup() - : base(typeof(TransientDatabaseSetup).Assembly, typeof(AutoPersistenceModelGenerator), new[] - { - typeof(AutoPersistenceModelGenerator).Assembly - }) + : base(typeof(TransientDatabaseSetup).Assembly, typeof(AutoPersistenceModelGenerator), + new[] { typeof(AutoPersistenceModelGenerator).Assembly }) { } -} \ No newline at end of file +} diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/UserActivationTests.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/UserActivationTests.cs index aa581a8b4..3cd5ede99 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/UserActivationTests.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/UserActivationTests.cs @@ -2,9 +2,9 @@ namespace Suteki.TardisBank.Tests.Model; using Domain; using Domain.Events; -using Shouldly; using MediatR; using Moq; +using Shouldly; using Xunit; @@ -41,4 +41,4 @@ public void ParentShouldNotBeActiveWhenCreated() User parent = new Parent("Dad", "mike@mike.com", "xxx"); parent.IsActive.ShouldBeFalse(); } -} \ No newline at end of file +} diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/UserTests.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/UserTests.cs index 9b6800e8a..ffa2e6261 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/UserTests.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/UserTests.cs @@ -1,8 +1,8 @@ namespace Suteki.TardisBank.Tests.Model; using Domain; -using Shouldly; using SharpArch.Testing.Xunit.NHibernate; +using Shouldly; using Xunit; @@ -45,4 +45,4 @@ public void Should_be_able_to_treat_Parents_and_Children_Polymorphically() users[3].GetType().Name.ShouldBe("Parent"); users[4].GetType().Name.ShouldBe("Child"); } -} \ No newline at end of file +} diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/WithdrawlCashTests.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/WithdrawlCashTests.cs index 3f56c5f71..647dbf392 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/WithdrawlCashTests.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Model/WithdrawlCashTests.cs @@ -2,10 +2,10 @@ namespace Suteki.TardisBank.Tests.Model; using Domain; using Domain.Events; -using Shouldly; using MediatR; using Moq; using SharpArch.Testing.Xunit; +using Shouldly; using Xunit; @@ -27,14 +27,6 @@ public WithdrawlCashTests() _somebodyElsesParent = new Parent("Not Dad", "jon@jon.com", "zzz"); } - [Fact] - public void Child_should_not_be_able_to_withdraw_from_some_other_parent() - { - Action withdraw = () => _child.WithdrawCashFromParent(_somebodyElsesParent, 2.30M, "for toys", _mediator.Object); - var exception = withdraw.ShouldThrow(); - exception.Message.ShouldBe("Not Your Parent"); - } - [Fact] [SetCulture("en-GB")] public void Child_should_be_able_to_withdraw_cash() @@ -49,6 +41,14 @@ public void Child_should_be_able_to_withdraw_cash() _parent.Messages[0].Text.ShouldBe("Leo would like to withdraw £2.30"); } + [Fact] + public void Child_should_not_be_able_to_withdraw_from_some_other_parent() + { + Action withdraw = () => _child.WithdrawCashFromParent(_somebodyElsesParent, 2.30M, "for toys", _mediator.Object); + var exception = withdraw.ShouldThrow(); + exception.Message.ShouldBe("Not Your Parent"); + } + [Fact] [SetCulture("en-GB")] public void Child_should_not_be_able_to_withdraw_more_than_their_balance() diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Services/EmailServiceTests.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Services/EmailServiceTests.cs index 5d6c9fb3a..6fd9d4281 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Services/EmailServiceTests.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Services/EmailServiceTests.cs @@ -37,4 +37,4 @@ public void Should_be_able_to_send_an_email() _emailService.SendEmail(toAddress, subject, body); } -} \ No newline at end of file +} diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Suteki.TardisBank.Tests.csproj b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Suteki.TardisBank.Tests.csproj index a73db13fc..c3a3a51c1 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Suteki.TardisBank.Tests.csproj +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.Tests/Suteki.TardisBank.Tests.csproj @@ -2,31 +2,31 @@ - - - - + + + + - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers - - - + + + - - - + + + diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.WebApi/Controllers/AnnouncementsController.cs b/Src/Samples/TardisBank/Src/Suteki.TardisBank.WebApi/Controllers/AnnouncementsController.cs index 3e5a8f852..453229888 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.WebApi/Controllers/AnnouncementsController.cs +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.WebApi/Controllers/AnnouncementsController.cs @@ -1,7 +1,6 @@ #pragma warning disable 1573 namespace Suteki.TardisBank.WebApi.Controllers; -using System.Data; using Api.Announcements; using AutoMapper; using AutoMapper.QueryableExtensions; @@ -14,7 +13,7 @@ namespace Suteki.TardisBank.WebApi.Controllers; [ApiController] [Route("[controller]")] -[Transaction(IsolationLevel.ReadCommitted)] +[Transaction()] public class AnnouncementsController : ControllerBase { readonly ILinqRepository _announcementRepository; @@ -61,7 +60,8 @@ public async Task>> Get() public async Task> Get(int id) { var announcement = await _announcementRepository.GetAsync(id).ConfigureAwait(false); - if (announcement != null) return _mapper.Map(announcement)!; + if (announcement != null) + return _mapper.Map(announcement)!; return NotFound(new { id }); } diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.WebApi/NHibernate-Postgres.config b/Src/Samples/TardisBank/Src/Suteki.TardisBank.WebApi/NHibernate-Postgres.config new file mode 100644 index 000000000..40d999fc7 --- /dev/null +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.WebApi/NHibernate-Postgres.config @@ -0,0 +1,27 @@ + + + + + + + + Host=localhost;Port=5432;Database=TardisBank;Username=postgres;Password=Password12!; + + + + NHibernate.Dialect.PostgreSQLDialect + + + NHibernate.Connection.DriverConnectionProvider + NHibernate.Driver.NpgsqlDriver + + + true + auto + 500 + + + update + + + diff --git a/Src/Samples/TardisBank/Src/Suteki.TardisBank.WebApi/Suteki.TardisBank.WebApi.csproj b/Src/Samples/TardisBank/Src/Suteki.TardisBank.WebApi/Suteki.TardisBank.WebApi.csproj index bf2292929..497d81b1b 100644 --- a/Src/Samples/TardisBank/Src/Suteki.TardisBank.WebApi/Suteki.TardisBank.WebApi.csproj +++ b/Src/Samples/TardisBank/Src/Suteki.TardisBank.WebApi/Suteki.TardisBank.WebApi.csproj @@ -5,14 +5,14 @@ - - - - + + + + - + - + diff --git a/Src/Samples/TransactionAttribute/App/Controllers/OverridesController.cs b/Src/Samples/TransactionAttribute/App/Controllers/OverridesController.cs index d4dfe1a68..395bb660a 100644 --- a/Src/Samples/TransactionAttribute/App/Controllers/OverridesController.cs +++ b/Src/Samples/TransactionAttribute/App/Controllers/OverridesController.cs @@ -8,7 +8,7 @@ [Route("api/[controller]")] [ApiController] -[Transaction(IsolationLevel.ReadCommitted)] +[Transaction()] public class OverridesController : ControllerBase { static readonly ILogger Log = Serilog.Log.ForContext(); diff --git a/Src/Samples/TransactionAttribute/App/Stubs/TransactionManagerStub.cs b/Src/Samples/TransactionAttribute/App/Stubs/TransactionManagerStub.cs index 5d7f6e818..fc588191b 100644 --- a/Src/Samples/TransactionAttribute/App/Stubs/TransactionManagerStub.cs +++ b/Src/Samples/TransactionAttribute/App/Stubs/TransactionManagerStub.cs @@ -32,7 +32,8 @@ public IDisposable BeginTransaction(IsolationLevel isolationLevel = IsolationLev public Task CommitTransactionAsync(CancellationToken cancellationToken) { - _httpContextAccessor.HttpContext!.Response.Headers.Append(TransactionIsolationLevel, _transaction?.IsolationLevel.ToString() ?? "unknown"); + _httpContextAccessor.HttpContext!.Response.Headers.Append(TransactionIsolationLevel, + _transaction?.IsolationLevel.ToString() ?? "unknown"); _httpContextAccessor.HttpContext.Response.Headers.Append(TransactionState, "committed"); _transaction?.Commit(); return Task.CompletedTask; @@ -40,7 +41,8 @@ public Task CommitTransactionAsync(CancellationToken cancellationToken) public Task RollbackTransactionAsync(CancellationToken cancellationToken) { - _httpContextAccessor.HttpContext!.Response.Headers.Append(TransactionIsolationLevel, _transaction?.IsolationLevel.ToString() ?? "unknown"); + _httpContextAccessor.HttpContext!.Response.Headers.Append(TransactionIsolationLevel, + _transaction?.IsolationLevel.ToString() ?? "unknown"); _httpContextAccessor.HttpContext.Response.Headers.Append(TransactionState, "rolled-back"); _transaction?.Rollback(); return Task.CompletedTask; diff --git a/Src/Samples/TransactionAttribute/App/TransactionAttribute.WebApi.csproj b/Src/Samples/TransactionAttribute/App/TransactionAttribute.WebApi.csproj index f67006db3..d7f9ed515 100644 --- a/Src/Samples/TransactionAttribute/App/TransactionAttribute.WebApi.csproj +++ b/Src/Samples/TransactionAttribute/App/TransactionAttribute.WebApi.csproj @@ -5,22 +5,26 @@ - - + + - + - + - + + + + + diff --git a/Src/Samples/TransactionAttribute/Tests/TransactionAttribute.Tests.csproj b/Src/Samples/TransactionAttribute/Tests/TransactionAttribute.Tests.csproj index 9986dd4ac..9508a8020 100644 --- a/Src/Samples/TransactionAttribute/Tests/TransactionAttribute.Tests.csproj +++ b/Src/Samples/TransactionAttribute/Tests/TransactionAttribute.Tests.csproj @@ -9,6 +9,10 @@ + + + + all @@ -16,6 +20,16 @@ + + + + + + all + runtime; build; native; contentfiles; analyzers + + + @@ -24,6 +38,10 @@ + + + + diff --git a/Src/Samples/TransactionAttribute/Tests/UnitOfWorkAttributeOverrideTests.cs b/Src/Samples/TransactionAttribute/Tests/UnitOfWorkAttributeOverrideTests.cs index ceadc1961..3fa849019 100644 --- a/Src/Samples/TransactionAttribute/Tests/UnitOfWorkAttributeOverrideTests.cs +++ b/Src/Samples/TransactionAttribute/Tests/UnitOfWorkAttributeOverrideTests.cs @@ -1,13 +1,17 @@ using Xunit; -[assembly: AssemblyTrait("Category", "Samples")] +#if NET8_0_OR_GREATER +[assembly: Trait("Category", "Samples")] +#else +[assembly: Xunit.AssemblyTrait("Category", "Samples")] +#endif namespace TransactionAttribute.Tests; using System.Data; using System.Net; -using Shouldly; using Setup; +using Shouldly; using WebApi.Stubs; using Xunit; diff --git a/Src/SharpArch.Library.DotSettings.AutoLoad b/Src/SharpArch.Library.DotSettings.AutoLoad deleted file mode 100644 index 89316e414..000000000 --- a/Src/SharpArch.Library.DotSettings.AutoLoad +++ /dev/null @@ -1,2 +0,0 @@ - - Library \ No newline at end of file diff --git a/SharpArch.AutoLoad.DotSettings b/Src/SharpArch.sln.DotSettings similarity index 100% rename from SharpArch.AutoLoad.DotSettings rename to Src/SharpArch.sln.DotSettings diff --git a/Src/Tests/Directory.Build.props b/Src/Tests/Directory.Build.props index 61302408e..de7e049a5 100644 --- a/Src/Tests/Directory.Build.props +++ b/Src/Tests/Directory.Build.props @@ -1,4 +1,5 @@  + @@ -9,14 +10,16 @@ - + - - - + + + + all runtime; build; native; contentfiles; analyzers - \ No newline at end of file + diff --git a/Src/Tests/SharpArch.Tests.NHibernate/HasUniqueDomainSignatureValidatorTests.cs b/Src/Tests/SharpArch.Tests.NHibernate/HasUniqueDomainSignatureValidatorTests.cs index 4ad2c4fdb..84f2a33fd 100644 --- a/Src/Tests/SharpArch.Tests.NHibernate/HasUniqueDomainSignatureValidatorTests.cs +++ b/Src/Tests/SharpArch.Tests.NHibernate/HasUniqueDomainSignatureValidatorTests.cs @@ -1,8 +1,8 @@ namespace Tests.SharpArch.NHibernate; using Domain; -using Shouldly; using NUnit.Framework; +using Shouldly; [TestFixture] @@ -40,4 +40,4 @@ public void WhenEntityWithDuplicateStringIdExists_Should_MarkEntityAsInvalid() duplicateUser.IsValid(ValidationContextFor(duplicateUser)) .ShouldBeFalse(); } -} \ No newline at end of file +} diff --git a/Src/Tests/SharpArch.Tests.NHibernate/NHibernateSessionFactoryBuilderTests.cs b/Src/Tests/SharpArch.Tests.NHibernate/NHibernateSessionFactoryBuilderTests.cs index fa708c41b..6915aba4d 100644 --- a/Src/Tests/SharpArch.Tests.NHibernate/NHibernateSessionFactoryBuilderTests.cs +++ b/Src/Tests/SharpArch.Tests.NHibernate/NHibernateSessionFactoryBuilderTests.cs @@ -1,10 +1,10 @@ namespace Tests.SharpArch.NHibernate; -using Shouldly; using FluentNHibernate.Cfg.Db; using global::NHibernate.Cfg; using global::SharpArch.NHibernate; using NUnit.Framework; +using Shouldly; [TestFixture] @@ -21,7 +21,8 @@ public void TearDown() { try { - if (File.Exists(_tempFileName)) File.Delete(_tempFileName); + if (File.Exists(_tempFileName)) + File.Delete(_tempFileName); } // ReSharper disable once CatchAllClause catch @@ -130,4 +131,4 @@ public void WhenUsingDataAnnotationValidators_ShouldKeepRegisteredPreUpdateEvent configuration.EventListeners.PreUpdateEventListeners.ShouldContain(l => l is PreUpdateListener); } -} \ No newline at end of file +} diff --git a/Src/Tests/SharpArch.Tests.NHibernate/RepositoryTests.cs b/Src/Tests/SharpArch.Tests.NHibernate/RepositoryTests.cs index d8622a8de..b2891b20d 100644 --- a/Src/Tests/SharpArch.Tests.NHibernate/RepositoryTests.cs +++ b/Src/Tests/SharpArch.Tests.NHibernate/RepositoryTests.cs @@ -2,13 +2,13 @@ namespace Tests.SharpArch.NHibernate; -using Shouldly; using global::NHibernate; using global::SharpArch.Domain.DomainModel; using global::SharpArch.Domain.PersistenceSupport; using global::SharpArch.NHibernate; using Moq; using NUnit.Framework; +using Shouldly; [TestFixture] @@ -31,4 +31,4 @@ public void CanCastConcreteLinqRepositoryToInterfaceILinqRepository() public class MyEntity : Entity { string? Name { get; set; } -} \ No newline at end of file +} diff --git a/Src/Tests/SharpArch.Tests.NHibernate/SharpArch.Tests.NHibernate.csproj b/Src/Tests/SharpArch.Tests.NHibernate/SharpArch.Tests.NHibernate.csproj index 78a7f6e14..9dd67297b 100644 --- a/Src/Tests/SharpArch.Tests.NHibernate/SharpArch.Tests.NHibernate.csproj +++ b/Src/Tests/SharpArch.Tests.NHibernate/SharpArch.Tests.NHibernate.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/Src/Tests/SharpArch.XunitTests.NHibernate/HasUniqueDomainSignatureValidatorTests.cs b/Src/Tests/SharpArch.XunitTests.NHibernate/HasUniqueDomainSignatureValidatorTests.cs index 799520043..ab1838b96 100644 --- a/Src/Tests/SharpArch.XunitTests.NHibernate/HasUniqueDomainSignatureValidatorTests.cs +++ b/Src/Tests/SharpArch.XunitTests.NHibernate/HasUniqueDomainSignatureValidatorTests.cs @@ -42,4 +42,4 @@ public async Task WhenEntityWithDuplicateStringIdExists_Should_MarkEntityAsInval duplicateUser.IsValid(ValidationContextFor(duplicateUser)) .ShouldBeFalse(); } -} \ No newline at end of file +} diff --git a/Src/Tests/SharpArch.XunitTests.NHibernate/NHibernateRepositoryTests.cs b/Src/Tests/SharpArch.XunitTests.NHibernate/NHibernateRepositoryTests.cs index a9560e4d3..bb4a758a0 100644 --- a/Src/Tests/SharpArch.XunitTests.NHibernate/NHibernateRepositoryTests.cs +++ b/Src/Tests/SharpArch.XunitTests.NHibernate/NHibernateRepositoryTests.cs @@ -1,9 +1,9 @@ namespace Tests.SharpArch.NHibernate; using Domain; -using Shouldly; using global::SharpArch.NHibernate; using global::SharpArch.Testing.Xunit.NHibernate; +using Shouldly; using Xunit; @@ -25,10 +25,7 @@ protected override Task LoadTestData(CancellationToken cancellationToken) [Fact] public async Task CanSaveAsync() { - var entity = new Contractor - { - Name = "John Doe" - }; + var entity = new Contractor { Name = "John Doe" }; var res = await _repo.SaveAsync(entity); res.IsTransient().ShouldBeFalse(); @@ -38,10 +35,7 @@ public async Task CanSaveAsync() [Fact] public async Task CanSaveOrUpdate() { - var entity = new Contractor - { - Name = "John Doe" - }; + var entity = new Contractor { Name = "John Doe" }; var res = await _repo.SaveOrUpdateAsync(entity); res.IsTransient().ShouldBeFalse(); @@ -49,4 +43,4 @@ public async Task CanSaveOrUpdate() res = await _repo.SaveOrUpdateAsync(entity); res.Name.ShouldBe("John Doe Jr"); } -} \ No newline at end of file +} diff --git a/Src/Tests/SharpArch.XunitTests.NHibernate/NHibernateTestsSetup.cs b/Src/Tests/SharpArch.XunitTests.NHibernate/NHibernateTestsSetup.cs index 962e801f9..0a6519dc8 100644 --- a/Src/Tests/SharpArch.XunitTests.NHibernate/NHibernateTestsSetup.cs +++ b/Src/Tests/SharpArch.XunitTests.NHibernate/NHibernateTestsSetup.cs @@ -27,10 +27,6 @@ protected override void Customize(NHibernateSessionFactoryBuilder builder) { base.Customize(builder); builder.UsePersistenceConfigurer(new SQLiteConfiguration().InMemory()); - builder.UseProperties(new SortedList - { - [Environment.ReleaseConnections] = "on_close", - [Environment.Hbm2ddlAuto] = "create" - }); + builder.UseProperties(new SortedList { [Environment.ReleaseConnections] = "on_close", [Environment.Hbm2ddlAuto] = "create" }); } } diff --git a/Src/Tests/SharpArch.XunitTests.NHibernate/RepositoryTests.cs b/Src/Tests/SharpArch.XunitTests.NHibernate/RepositoryTests.cs index f584b448e..85a03ca72 100644 --- a/Src/Tests/SharpArch.XunitTests.NHibernate/RepositoryTests.cs +++ b/Src/Tests/SharpArch.XunitTests.NHibernate/RepositoryTests.cs @@ -2,12 +2,12 @@ namespace Tests.SharpArch.NHibernate; -using Shouldly; using global::NHibernate; using global::SharpArch.Domain.DomainModel; using global::SharpArch.Domain.PersistenceSupport; using global::SharpArch.NHibernate; using Moq; +using Shouldly; using Xunit; diff --git a/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/DataAnnotationsValidator/HasUniqueEntitySignatureValidatorTests.cs b/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/DataAnnotationsValidator/HasUniqueEntitySignatureValidatorTests.cs index 893791284..1ac794ca2 100644 --- a/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/DataAnnotationsValidator/HasUniqueEntitySignatureValidatorTests.cs +++ b/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/DataAnnotationsValidator/HasUniqueEntitySignatureValidatorTests.cs @@ -2,11 +2,11 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics; -using Shouldly; using global::SharpArch.Domain.DomainModel; using global::SharpArch.Domain.PersistenceSupport; using global::SharpArch.Domain.Validation; using Moq; +using Shouldly; using Xunit; diff --git a/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/DomainModel/BaseObjectEqualityComparerTests.cs b/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/DomainModel/BaseObjectEqualityComparerTests.cs index 7de90e8c5..1bcd4e8db 100644 --- a/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/DomainModel/BaseObjectEqualityComparerTests.cs +++ b/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/DomainModel/BaseObjectEqualityComparerTests.cs @@ -3,9 +3,9 @@ namespace Tests.SharpArch.Domain.DomainModel; using System.Reflection; -using Shouldly; using global::SharpArch.Domain.DomainModel; using global::SharpArch.Testing.Helpers; +using Shouldly; using Xunit; @@ -151,4 +151,4 @@ public void CannotSuccessfullyCompareDifferentlyTypedObjectsThatDeriveFromBaseOb comparer.Equals(obj1, obj2).ShouldBeFalse(); } -} \ No newline at end of file +} diff --git a/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/DomainModel/EntityTests.cs b/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/DomainModel/EntityTests.cs index 25d87fa9c..287534ff3 100644 --- a/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/DomainModel/EntityTests.cs +++ b/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/DomainModel/EntityTests.cs @@ -4,9 +4,9 @@ namespace Tests.SharpArch.Domain.DomainModel; -using Shouldly; using global::SharpArch.Domain.DomainModel; using global::SharpArch.Testing.Helpers; +using Shouldly; using Xunit; @@ -26,42 +26,12 @@ public class EntityTests public EntityTests() { - _obj = new MockEntityObjectWithDefaultId - { - FirstName = "FName1", - LastName = "LName1", - Email = @"testus...@mail.com" - }; - _sameObj = new MockEntityObjectWithDefaultId - { - FirstName = "FName1", - LastName = "LName1", - Email = @"testus...@mail.com" - }; - _diffObj = new MockEntityObjectWithDefaultId - { - FirstName = "FName2", - LastName = "LName2", - Email = @"testuse...@mail.com" - }; - _objWithId = new MockEntityObjectWithSetId - { - FirstName = "FName1", - LastName = "LName1", - Email = @"testus...@mail.com" - }; - _sameObjWithId = new MockEntityObjectWithSetId - { - FirstName = "FName1", - LastName = "LName1", - Email = @"testus...@mail.com" - }; - _diffObjWithId = new MockEntityObjectWithSetId - { - FirstName = "FName2", - LastName = "LName2", - Email = @"testuse...@mail.com" - }; + _obj = new MockEntityObjectWithDefaultId { FirstName = "FName1", LastName = "LName1", Email = @"testus...@mail.com" }; + _sameObj = new MockEntityObjectWithDefaultId { FirstName = "FName1", LastName = "LName1", Email = @"testus...@mail.com" }; + _diffObj = new MockEntityObjectWithDefaultId { FirstName = "FName2", LastName = "LName2", Email = @"testuse...@mail.com" }; + _objWithId = new MockEntityObjectWithSetId { FirstName = "FName1", LastName = "LName1", Email = @"testus...@mail.com" }; + _sameObjWithId = new MockEntityObjectWithSetId { FirstName = "FName1", LastName = "LName1", Email = @"testus...@mail.com" }; + _diffObjWithId = new MockEntityObjectWithSetId { FirstName = "FName2", LastName = "LName2", Email = @"testuse...@mail.com" }; } @@ -353,12 +323,7 @@ public void CanCompareObjectsWithComplexProperties() obj1.Equals(obj2).ShouldBeTrue(); - obj1.Address = new AddressBeingDomainSignatureComparable - { - Address1 = "123 Smith Ln.", - Address2 = "Suite 201", - ZipCode = 12345 - }; + obj1.Address = new AddressBeingDomainSignatureComparable { Address1 = "123 Smith Ln.", Address2 = "Suite 201", ZipCode = 12345 }; obj1.Equals(obj2).ShouldBeFalse(); // Set the address of the 2nd to be different to the address of the first @@ -641,4 +606,4 @@ public void WontGetConfusedWithOutsideCases() obj2.Name = @"Supercalifragilisticexpialidociouz"; obj1.Equals(obj2).ShouldBeFalse(); } -} \ No newline at end of file +} diff --git a/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/DomainModel/ValueObjectTests.cs b/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/DomainModel/ValueObjectTests.cs index 85d185597..4da054356 100644 --- a/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/DomainModel/ValueObjectTests.cs +++ b/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/DomainModel/ValueObjectTests.cs @@ -1,8 +1,7 @@ namespace Tests.SharpArch.Domain.DomainModel; -using System.Diagnostics.CodeAnalysis; -using Shouldly; using global::SharpArch.Domain.DomainModel; +using Shouldly; using Xunit; @@ -133,4 +132,4 @@ public void ShouldNotBeEqualWithDifferentReferencesAndDifferentIds() var val2 = new DummyValueType { Id = 10, Name = "Luis" }; val2.Equals(val1).ShouldBeFalse(); } -} \ No newline at end of file +} diff --git a/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/Reflection/TypePropertyDescriptorCacheTests.cs b/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/Reflection/TypePropertyDescriptorCacheTests.cs index aec0df1a4..6d2905c4d 100644 --- a/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/Reflection/TypePropertyDescriptorCacheTests.cs +++ b/Src/Tests/SharpArch.XunitTests/SharpArch.Domain/Reflection/TypePropertyDescriptorCacheTests.cs @@ -1,7 +1,7 @@ namespace Tests.SharpArch.Domain.Reflection; -using Shouldly; using global::SharpArch.Domain.Reflection; +using Shouldly; using Xunit; diff --git a/Src/Tests/SharpArch.XunitTests/SharpArch.Infrastructure/CodeBaseLocatorTests.cs b/Src/Tests/SharpArch.XunitTests/SharpArch.Infrastructure/CodeBaseLocatorTests.cs index c5c2b14f0..e42d5cda1 100644 --- a/Src/Tests/SharpArch.XunitTests/SharpArch.Infrastructure/CodeBaseLocatorTests.cs +++ b/Src/Tests/SharpArch.XunitTests/SharpArch.Infrastructure/CodeBaseLocatorTests.cs @@ -1,10 +1,9 @@ namespace Tests.SharpArch.Infrastructure; using System.Reflection; -using Shouldly; using global::SharpArch.Infrastructure; +using Shouldly; using Xunit; -using Xunit.Abstractions; public class CodeBaseLocatorTests diff --git a/VersionHistory.txt b/VersionHistory.txt index b74da78bd..71e24a121 100644 --- a/VersionHistory.txt +++ b/VersionHistory.txt @@ -1,8 +1,43 @@ +vNext ======================== -vNext + + +9.1.0 ======================== +ENHANCEMENTS: +* Add .NET 10 support (net10.0 added to AppTargetFrameworks) +* TardisBank sample now targets net9.0 and net10.0 + +BUILD: +* Upgrade xUnit to v3 for .NET 8+ targets (xunit.v3 3.2.2 / xunit.v3.extensibility.core 3.2.2); + keep xunit v2 (xunit.core 2.9.3) for netstandard2.1 and net6.0 +* Upgrade Microsoft.SourceLink.GitHub 8.0.0 -> 10.0.300 + +DEPENDENCIES: +* NHibernate upgraded 5.5.3 -> 5.6.1 +* NUnit upgraded 4.4.0 -> 4.6.1 +* NUnit3TestAdapter upgraded 5.1.0 -> 6.2.0 +* xunit.runner.visualstudio upgraded 3.1.4 -> 3.1.5 +* Microsoft.NET.Test.Sdk upgraded 17.14.1 -> 18.6.0 +* JetBrains.Annotations upgraded 2025.2.2 -> 2026.2.0 +* Microsoft.Extensions.Logging.Abstractions 10.0.0 added for net10.0 target +* Microsoft.Extensions.DependencyInjection.Abstractions 10.0.0 added for net10.0 target +* Autofac upgraded 8.4.0 -> 9.3.0 (samples) +* Autofac.Extensions.DependencyInjection upgraded 10.0.0 -> 11.0.1 (samples) +* Microsoft.Data.SqlClient upgraded 6.1.1 -> 7.0.1 (samples) +* Humanizer.Core upgraded 2.14.1 -> 3.0.10 (TardisBank sample) +* Newtonsoft.Json upgraded 13.0.3 -> 13.0.4 (TardisBank sample) +* Serilog.AspNetCore now TFM-conditional per target (8.0.0/9.0.0/10.0.0) +* Serilog.Sinks.Console upgraded 6.0.0 -> 6.1.1 (samples) +* Serilog.Sinks.Seq upgraded 9.0.0 -> 9.1.0 (samples) +* Serilog upgraded 4.3.0 -> 4.3.1 (TardisBank sample) +* SQLitePCLRaw.bundle_e_sqlite3 upgraded 3.0.2 -> 3.0.3 (TardisBank sample) +* NHibernate.Extensions.Sqlite now TFM-conditional (9.0.11 / net9.0, 10.0.2 / net10.0) +* Microsoft.AspNetCore.TestHost now TFM-conditional for net9.0 and net10.0 (samples) + 9.0.0 +======================== BREAKING: * Drop .NET 5 support @@ -26,7 +61,7 @@ DEPENDENCIES: 8.0.1 ======================== * .NET 6 support (upgrade from RC to release) - + 8.0.0 ======================== @@ -215,7 +250,7 @@ S#arp Architecture 2.0.3 for Visual Studio 2010 Issues: * #42: Show the expected/requested TResult in the exception message (Thanks Joel Purra @joelpurra). -* #41: Decorated the attribute classes with the AttributeUsageAttribute, to restrict usage of the attributes to their intended targets. (Thanks Sandor Drienhuizen @sandord) +* #41: Decorated the attribute classes with the AttributeUsageAttribute, to restrict usage of the attributes to their intended targets. (Thanks Sandor Drieënhuizen @sandord) ------- diff --git a/appveyor.yml b/appveyor.yml index 239b6541d..f6f2c8982 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,8 +7,8 @@ nuget: disable_publish_on_pr: true image: - - Ubuntu2204 - - Visual Studio 2022 + - Ubuntu2404 + - Visual Studio 2026 environment: CAKE_SETTINGS_SKIPVERIFICATION: true @@ -25,11 +25,11 @@ matrix: for: -# Linux build + # Linux build - matrix: only: - - image: Ubuntu2204 + - image: Ubuntu2404 clone_folder: ~/work/sharp-arch @@ -48,7 +48,7 @@ for: - matrix: only: - - image: Visual Studio 2022 + - image: Visual Studio 2026 clone_folder: c:\work\sharp-arch @@ -56,21 +56,23 @@ for: - c:\tmp\cake\tools -> build\*.cake, appveyor.yml services: - - mssql2019 + - mssql2025 install: - ps: dotnet --info - ps: ./mssql-setup.ps1 - - ps: dotnet tool install Cake.Tool --version 4.0.0 --global - - ps: dotnet tool install coveralls.net --version 4.0.1 --global + - ps: dotnet tool install Cake.Tool --version 6.1.0 --global + - ps: New-Item -ItemType Directory -Force -Path C:\tools | Out-Null + - ps: Invoke-WebRequest -Uri "https://github.com/coverallsapp/coverage-reporter/releases/latest/download/coveralls-windows.exe" -OutFile "C:\tools\coveralls-windows.exe" build_script: - ps: dotnet cake environment: - coveralls_repo_token: - secure: FgZlD1O2ilcGB6nF7cIgNcF6f8wWJk//ish6EG800QfuMd0y3BQWUXkl9u7TB4yG - appveyor_cache_entry_zip_args: -t7z -m0=lzma -mx=5 -ms=on + COVERALLS_REPO_TOKEN: + secure: Yf2HHc4ObFerbXits621royUrbahaAPNsUHdvJVnl7y1tTG/chj5wXKDnk0V71aP + COVERALLS_REPORTER_EXE: C:\tools\coveralls-windows.exe + APPVEYOR_CACHE_ENTRY_ZIP_ARGS: -t7z -m0=lzma -mx=5 -ms=on CAKE_PATHS_TOOLS: c:\tmp\cake\tools\ GITHUB_TOKEN: secure: 21bpvob1W59q2H3tgdQ4c6pkZVzTTZTTWUNkkLJ/1bo2xY4K160RtHw08cBRsQOd diff --git a/build.cake b/build.cake index 77bf2a8fc..d9c5ca551 100644 --- a/build.cake +++ b/build.cake @@ -11,10 +11,11 @@ ProjectSettings settings = new ProjectSettings("sharparchitecture", "Sharp-Archi { CodeCoverage = { - IncludeFilter = "+[SharpArch.*]*" + // Coverlet filter syntax: [Assembly]TypeName + IncludeFilter = new List { "[SharpArch.*]*" } } }; -settings.CodeCoverage.ExcludeFilter += ";-[Suteki*]*;-[TransactionAttribute*]*"; +settings.CodeCoverage.ExcludeFilter.AddRange(new[] { "[Suteki*]*", "[TransactionAttribute*]*" }); // SETUP / TEARDOWN diff --git a/global.json b/global.json new file mode 100644 index 000000000..512142d2b --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + } +} diff --git a/mssql-setup.ps1 b/mssql-setup.ps1 index abd225390..e7224af99 100644 --- a/mssql-setup.ps1 +++ b/mssql-setup.ps1 @@ -5,7 +5,7 @@ [reflection.assembly]::LoadWithPartialName("Microsoft.SqlServer.SqlWmiManagement") | Out-Null $serverName = $env:COMPUTERNAME -$instanceName = 'SQL2019' +$instanceName = 'SQL2025' $smo = 'Microsoft.SqlServer.Management.Smo.' $wmi = new-object ($smo + 'Wmi.ManagedComputer')