diff --git a/.github/instructions/CodeReview.instructions.md b/.github/instructions/CodeReview.instructions.md
new file mode 100644
index 00000000..f36e381c
--- /dev/null
+++ b/.github/instructions/CodeReview.instructions.md
@@ -0,0 +1,201 @@
+---
+description: "Use when reviewing code, pull requests, active branch changes, current changes, local diffs, or PR comments. Trigger for requests such as review this code, review this PR, review the active branch, review current changes, review the diff, review reviewer feedback, or summarize review findings."
+---
+
+# Copilot Code Review Instructions (FastMoq)
+
+## Role
+
+Act as a senior .NET library engineer reviewing code, pull requests, active branch changes, and current diffs in FastMoq.
+
+Be strict, concise, and focus on meaningful issues.
+
+## Review Mode
+
+Default: **Deep**
+
+Allow override via:
+
+- `mode=deep` (default)
+- `mode=high`
+- `recommendation-only`
+
+### Mode Behavior
+
+#### Deep (default)
+
+- Full analysis of correctness, regression, and design
+- Include edge cases, downstream consumer impact, and provider or framework interactions
+
+#### High
+
+- Only high-impact issues, regressions, and breaking behavior changes
+
+#### Recommendation-only
+
+- Output ONLY recommendations
+- Follow user instructions strictly
+- Do NOT include an Issues section
+
+## Instruction Override
+
+If the user provides specific instructions:
+
+- Follow them over default behavior
+- Limit scope to requested areas only
+- Do not add unrelated review categories
+
+## Review Focus
+
+### Correctness & Logic
+
+- Validate behavior vs intent
+- Detect incorrect assumptions
+- Identify missing edge cases
+
+### Regression Risk (CRITICAL)
+
+- Behavior changes vs previous implementation
+- Breaking public API or contract changes
+- Silent changes impacting downstream consumers, migration paths, or package composition
+
+### Public API & Contract Safety
+
+- Extension methods, fluent APIs, builders, registration helpers, abstractions, analyzers, and generators
+- Constructor selection, known-type resolution, optional-parameter resolution, keyed-service, or DI behavior changes
+- Shared props, targets, or package changes that alter supported usage
+
+### Provider & Verification Semantics
+
+- Changes to `GetOrCreateMock(...)`, `Verify(...)`, `VerifyLogged(...)`, `VerifyNoOtherCalls(...)`, `TimesSpec`, or provider registration behavior
+- Setup, verify, callback, matcher, and expression behavior changes across `reflection`, `moq`, and `nsubstitute`
+- Provider-specific behavior being presented as provider-neutral, especially around matcher and verification semantics
+
+### Package & Multi-Target Compatibility
+
+- `net8.0`, `net9.0`, and `net10.0` behavior differences
+- Central package version changes in `Directory.Packages.props`
+- Packaging, analyzer, or generator asset changes in release projects
+
+### Analyzer & Generator Behavior
+
+- Diagnostic or code-fix behavior changes
+- Source-generator output, planning, bootstrap, or settings changes
+- Generated or analyzer-backed behavior drifting from documented public guidance
+
+### Documentation & Samples
+
+- Public behavior changes missing updates in repo docs, migration docs, samples, or release notes
+- Divergence from repo-local provider-first guidance in `README.md`, `docs/`, or migration docs
+
+### Code Quality
+
+- Maintainability and clarity
+- Duplication or unnecessary complexity
+
+### Performance
+
+- Unnecessary work in hot paths, reflection-heavy flows, or generator/runtime planning paths
+
+### Testing
+
+- Missing tests for changed public behavior
+- Missing cross-provider, cross-target, analyzer, or generator coverage
+- Missing regression coverage for migration or compatibility behavior
+
+### Diagnostics Confidence
+
+- Treat syntax or compile findings as unconfirmed until local diagnostics or a local build reproduces them
+- Be especially careful around preview C# features, generated code, and analyzer diagnostics
+
+## PR Comment Handling
+
+When reviewing a pull request and PR comment context is available:
+
+- Complete the agent's own code review before pulling PR comments
+- Then review GitHub PR review comments and review threads
+- Deduplicate and group related feedback
+- Identify outdated comments
+- Identify incorrect comments
+- Identify conflicting comments
+- Treat syntax or compile comments as unconfirmed until local diagnostics or a build reproduces them
+- Combine agent findings and PR comment findings into one ranked list
+- Before making code changes, stop and let the user choose which findings to address and provide any extra instructions or missing context
+- After selected comments are addressed, reply on the relevant GitHub comments with the fix or the reason the comment was rejected
+
+Assign status per issue when applicable:
+
+- Accept
+- Reject
+- Already Addressed
+
+Do NOT repeat comments verbatim.
+Consolidate them into high-signal issues.
+
+## Output Format
+
+If NOT `recommendation-only`:
+
+### ๐ด Issues (must fix)
+
+Use for must-fix defects, regressions, contract breaks, and materially risky behavior changes.
+
+### ๐ก Recommendations
+
+Use for non-blocking improvements, missing hardening, follow-up coverage, or unconfirmed findings that still merit investigation.
+
+### โ
Good
+
+Use sparingly for notable strengths only.
+
+If `recommendation-only`:
+
+### ๐ก Recommendations ONLY
+
+## Per-Issue Requirements
+
+Each item must include:
+
+- What: the problem
+- Where: file or behavior
+- Why: impact
+- Fix: concrete suggestion
+
+Include these when applicable:
+
+- Regression impact
+- Downstream consumer, provider, package, or target-framework impact
+- Comment status (`Accept`, `Reject`, or `Already Addressed`)
+- Confirmation state when the issue is syntax or compile related (`Confirmed` or `Unconfirmed`)
+
+## PR Comment Generation Mode
+
+- One issue per comment
+- Direct and actionable
+- No obvious restatement
+- Prefer fixes over explanations
+- When replying to an existing GitHub review comment, cite the code fix or the rejection rationale directly
+
+## Bias (Priority Order)
+
+1. Regression risk
+2. Public API and contract changes
+3. Downstream consumer impact
+4. Provider, verification, and package behavior changes
+5. Missing edge cases or incorrect assumptions
+
+Deprioritize:
+
+- Formatting
+- Naming unless harmful
+- Style-only issues
+
+## Constraints
+
+- No fluff
+- No full code summaries
+- Prefer fewer, high-value issues
+- Do not invent repo conventions or unsupported workflow rules
+- Do not present unconfirmed syntax or compile comments as must-fix findings
+- Do not start code changes until the user decides which findings to address
+- Do not use non-GitHub review tooling concepts for this repo
diff --git a/.github/instructions/FastMoq.Core.md b/.github/instructions/FastMoq.Core.instructions.md
similarity index 89%
rename from .github/instructions/FastMoq.Core.md
rename to .github/instructions/FastMoq.Core.instructions.md
index 37ac3d8c..526598f7 100644
--- a/.github/instructions/FastMoq.Core.md
+++ b/.github/instructions/FastMoq.Core.instructions.md
@@ -1,3 +1,9 @@
+---
+name: "FastMoq.Core"
+description: "Use when working in FastMoq.Core, Mocker, MockerTestBase internals, provider-first runtime code, constructor resolution, or shared public API changes."
+applyTo: "FastMoq.Core/**"
+---
+
# FastMoq.Core - Copilot Instructions
## ๐ Core Architecture Rules
diff --git a/.github/instructions/FastMoq.Tests.md b/.github/instructions/FastMoq.Tests.instructions.md
similarity index 93%
rename from .github/instructions/FastMoq.Tests.md
rename to .github/instructions/FastMoq.Tests.instructions.md
index 4d034b13..8d8d544a 100644
--- a/.github/instructions/FastMoq.Tests.md
+++ b/.github/instructions/FastMoq.Tests.instructions.md
@@ -1,3 +1,9 @@
+---
+name: "FastMoq.Tests"
+description: "Use when writing or updating tests in FastMoq.Tests, especially MockerTestBase patterns, verification behavior, coverage, and test naming."
+applyTo: "FastMoq.Tests/**"
+---
+
# FastMoq.Tests - Copilot Instructions
## ๐งช Test Writing Patterns
diff --git a/.github/instructions/FastMoq.Web.md b/.github/instructions/FastMoq.Web.instructions.md
similarity index 93%
rename from .github/instructions/FastMoq.Web.md
rename to .github/instructions/FastMoq.Web.instructions.md
index 727b118c..fc3245fe 100644
--- a/.github/instructions/FastMoq.Web.md
+++ b/.github/instructions/FastMoq.Web.instructions.md
@@ -1,3 +1,9 @@
+---
+name: "FastMoq.Web"
+description: "Use when working in FastMoq.Web or related Blazor and web-helper code, including HttpContext helpers, service registration, and component-testing flows."
+applyTo: "FastMoq.Web/**"
+---
+
# FastMoq.Web - Copilot Instructions
## ๐ Web & Blazor Development Guidelines
diff --git a/Directory.Packages.props b/Directory.Packages.props
index a4495acb..0d97ad44 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -14,7 +14,6 @@
-
diff --git a/FastMoq-Release.sln b/FastMoq-Release.sln
index 2cd896f7..448ea37c 100644
--- a/FastMoq-Release.sln
+++ b/FastMoq-Release.sln
@@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastMoq", "FastMoq\FastMoq.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastMoq.Abstractions", "FastMoq.Abstractions\FastMoq.Abstractions.csproj", "{970828D1-0EF2-4D0D-BF1B-BB85DEB38514}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastMoq.Generators", "FastMoq.Generators\FastMoq.Generators.csproj", "{5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastMoq.Azure", "FastMoq.Azure\FastMoq.Azure.csproj", "{CADD0874-D3E6-40B3-A8D5-EB04047597EC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastMoq.Core", "FastMoq.Core\FastMoq.Core.csproj", "{2E2FC5C0-5B64-48BA-92F6-DF0DD46298E0}"
@@ -37,6 +39,10 @@ Global
{970828D1-0EF2-4D0D-BF1B-BB85DEB38514}.Debug|Any CPU.Build.0 = Release|Any CPU
{970828D1-0EF2-4D0D-BF1B-BB85DEB38514}.Release|Any CPU.ActiveCfg = Release|Any CPU
{970828D1-0EF2-4D0D-BF1B-BB85DEB38514}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Debug|Any CPU.ActiveCfg = Release|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Debug|Any CPU.Build.0 = Release|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Release|Any CPU.Build.0 = Release|Any CPU
{CADD0874-D3E6-40B3-A8D5-EB04047597EC}.Debug|Any CPU.ActiveCfg = Release|Any CPU
{CADD0874-D3E6-40B3-A8D5-EB04047597EC}.Debug|Any CPU.Build.0 = Release|Any CPU
{CADD0874-D3E6-40B3-A8D5-EB04047597EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
diff --git a/FastMoq.Abstractions/Generators/FastMoqGeneratedTestTargetAttribute.cs b/FastMoq.Abstractions/Generators/FastMoqGeneratedTestTargetAttribute.cs
new file mode 100644
index 00000000..260a483d
--- /dev/null
+++ b/FastMoq.Abstractions/Generators/FastMoqGeneratedTestTargetAttribute.cs
@@ -0,0 +1,44 @@
+namespace FastMoq.Generators
+{
+ ///
+ /// Marks a partial MockerTestBase<TComponent> test base for FastMoq's explicit compile-time harness generation path.
+ ///
+ ///
+ /// Apply this attribute only to partial test-base types that derive from MockerTestBase<TComponent>.
+ /// The first generator slice keeps opt-in explicit and emits constructor-signature metadata for the selected component path rather than enabling blanket automatic generation for every eligible type in a project.
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
+ public sealed class FastMoqGeneratedTestTargetAttribute : Attribute
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The component under test that the generated harness path should target.
+ public FastMoqGeneratedTestTargetAttribute(Type componentType)
+ {
+ ComponentType = componentType ?? throw new ArgumentNullException(nameof(componentType));
+ ConstructorParameterTypes = Array.Empty();
+ }
+
+ ///
+ /// Initializes a new instance of the class with an explicit constructor signature.
+ ///
+ /// The component under test that the generated harness path should target.
+ /// The explicit constructor signature to use for the generated harness bootstrap. Pass an explicit empty array to target the parameterless constructor.
+ public FastMoqGeneratedTestTargetAttribute(Type componentType, params Type[] constructorParameterTypes)
+ : this(componentType)
+ {
+ ConstructorParameterTypes = constructorParameterTypes ?? throw new ArgumentNullException(nameof(constructorParameterTypes));
+ }
+
+ ///
+ /// Gets the component under test that the generated harness path should target.
+ ///
+ public Type ComponentType { get; }
+
+ ///
+ /// Gets the optional explicit constructor signature that the generator should use.
+ ///
+ public IReadOnlyList ConstructorParameterTypes { get; }
+ }
+}
\ No newline at end of file
diff --git a/FastMoq.Analyzers.Tests/AnalyzerTestHelpers.cs b/FastMoq.Analyzers.Tests/AnalyzerTestHelpers.cs
index 80a9826b..7e5b49f1 100644
--- a/FastMoq.Analyzers.Tests/AnalyzerTestHelpers.cs
+++ b/FastMoq.Analyzers.Tests/AnalyzerTestHelpers.cs
@@ -16,6 +16,19 @@ namespace FastMoq.Analyzers.Tests
{
internal static class AnalyzerTestHelpers
{
+ private static readonly HashSet ExcludedTrustedPlatformAssemblyNames = new(StringComparer.OrdinalIgnoreCase)
+ {
+ "FastMoq.Analyzers.Tests",
+ "FastMoq.Tests",
+ "FastMoq.Tests.Blazor",
+ "FastMoq.Tests.Web",
+ };
+
+ private static bool IsXunitAssemblyName(string assemblyName)
+ {
+ return assemblyName.StartsWith("xunit", StringComparison.OrdinalIgnoreCase);
+ }
+
public static async Task> GetDiagnosticsAsync(string source, params DiagnosticAnalyzer[] analyzers)
{
return await GetDiagnosticsAsync(source, includeAzureFunctionsHelpers: false, includeMoqProviderPackage: true, includeNSubstituteProviderPackage: true, includeWebHelpers: true, analyzers).ConfigureAwait(false);
@@ -132,18 +145,63 @@ public static string NormalizeCode(string source)
.ToFullString();
}
- public static Document CreateDocumentForTest(string source, bool includeAzureFunctionsHelpers = false, bool includeMoqProviderPackage = true, bool includeNSubstituteProviderPackage = true, bool includeWebHelpers = true)
+ public static Document CreateDocumentForTest(
+ string source,
+ bool includeAzureFunctionsHelpers = false,
+ bool includeMoqProviderPackage = true,
+ bool includeNSubstituteProviderPackage = true,
+ bool includeWebHelpers = true,
+ bool includeDatabaseHelpers = false,
+ bool includeAzureHelpers = false,
+ bool includeAggregatePackage = false,
+ bool includeXunit = true)
{
- return CreateDocument(source, includeAzureFunctionsHelpers, includeMoqProviderPackage, includeNSubstituteProviderPackage, includeWebHelpers);
+ return CreateDocument(
+ source,
+ includeAzureFunctionsHelpers,
+ includeMoqProviderPackage,
+ includeNSubstituteProviderPackage,
+ includeWebHelpers,
+ includeDatabaseHelpers,
+ includeAzureHelpers,
+ includeAggregatePackage,
+ includeXunit);
}
- private static Document CreateDocument(string source, bool includeAzureFunctionsHelpers = false, bool includeMoqProviderPackage = true, bool includeNSubstituteProviderPackage = true, bool includeWebHelpers = true)
+ private static Document CreateDocument(
+ string source,
+ bool includeAzureFunctionsHelpers = false,
+ bool includeMoqProviderPackage = true,
+ bool includeNSubstituteProviderPackage = true,
+ bool includeWebHelpers = true,
+ bool includeDatabaseHelpers = false,
+ bool includeAzureHelpers = false,
+ bool includeAggregatePackage = false,
+ bool includeXunit = true)
{
- var project = CreateProject([("Test.cs", source)], includeAzureFunctionsHelpers, includeMoqProviderPackage, includeNSubstituteProviderPackage, includeWebHelpers);
+ var project = CreateProject(
+ [("Test.cs", source)],
+ includeAzureFunctionsHelpers,
+ includeMoqProviderPackage,
+ includeNSubstituteProviderPackage,
+ includeWebHelpers,
+ includeDatabaseHelpers,
+ includeAzureHelpers,
+ includeAggregatePackage,
+ includeXunit);
return project.Documents.Single();
}
- private static Project CreateProject(IReadOnlyList<(string fileName, string source)> sources, bool includeAzureFunctionsHelpers = false, bool includeMoqProviderPackage = true, bool includeNSubstituteProviderPackage = true, bool includeWebHelpers = true)
+ private static Project CreateProject(
+ IReadOnlyList<(string fileName, string source)> sources,
+ bool includeAzureFunctionsHelpers = false,
+ bool includeMoqProviderPackage = true,
+ bool includeNSubstituteProviderPackage = true,
+ bool includeWebHelpers = true,
+ bool includeDatabaseHelpers = false,
+ bool includeAzureHelpers = false,
+ bool includeAggregatePackage = false,
+ bool includeXunit = true)
{
var workspace = new AdhocWorkspace();
var projectId = ProjectId.CreateNewId();
@@ -153,7 +211,15 @@ private static Project CreateProject(IReadOnlyList<(string fileName, string sour
.WithProjectParseOptions(projectId, new CSharpParseOptions(LanguageVersion.Preview))
.WithProjectCompilationOptions(projectId, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
- foreach (var metadataReference in GetMetadataReferences(includeAzureFunctionsHelpers, includeMoqProviderPackage, includeNSubstituteProviderPackage, includeWebHelpers))
+ foreach (var metadataReference in GetMetadataReferences(
+ includeAzureFunctionsHelpers,
+ includeMoqProviderPackage,
+ includeNSubstituteProviderPackage,
+ includeWebHelpers,
+ includeDatabaseHelpers,
+ includeAzureHelpers,
+ includeAggregatePackage,
+ includeXunit))
{
solution = solution.AddMetadataReference(projectId, metadataReference);
}
@@ -167,33 +233,78 @@ private static Project CreateProject(IReadOnlyList<(string fileName, string sour
return solution.GetProject(projectId)!;
}
- private static IEnumerable GetMetadataReferences(bool includeAzureFunctionsHelpers, bool includeMoqProviderPackage, bool includeNSubstituteProviderPackage, bool includeWebHelpers)
+ private static IEnumerable GetMetadataReferences(
+ bool includeAzureFunctionsHelpers,
+ bool includeMoqProviderPackage,
+ bool includeNSubstituteProviderPackage,
+ bool includeWebHelpers,
+ bool includeDatabaseHelpers,
+ bool includeAzureHelpers,
+ bool includeAggregatePackage,
+ bool includeXunit)
{
+ if (includeAggregatePackage)
+ {
+ includeAzureFunctionsHelpers = true;
+ includeAzureHelpers = true;
+ includeDatabaseHelpers = true;
+ includeWebHelpers = true;
+ }
+
var references = new HashSet(StringComparer.OrdinalIgnoreCase);
if (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string trustedPlatformAssemblies)
{
foreach (var assemblyPath in trustedPlatformAssemblies.Split(Path.PathSeparator))
{
+ var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath);
+ if (ExcludedTrustedPlatformAssemblyNames.Contains(assemblyName))
+ {
+ continue;
+ }
+
+ if (!includeXunit && IsXunitAssemblyName(assemblyName))
+ {
+ continue;
+ }
+
+ if (!includeAggregatePackage &&
+ string.Equals(assemblyName, "FastMoq", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ if (!includeDatabaseHelpers &&
+ string.Equals(assemblyName, "FastMoq.Database", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ if (!includeAzureHelpers &&
+ string.Equals(assemblyName, "FastMoq.Azure", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
if (!includeWebHelpers &&
- string.Equals(Path.GetFileNameWithoutExtension(assemblyPath), "FastMoq.Web", StringComparison.OrdinalIgnoreCase))
+ string.Equals(assemblyName, "FastMoq.Web", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (!includeAzureFunctionsHelpers &&
- string.Equals(Path.GetFileNameWithoutExtension(assemblyPath), "FastMoq.AzureFunctions", StringComparison.OrdinalIgnoreCase))
+ string.Equals(assemblyName, "FastMoq.AzureFunctions", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (!includeMoqProviderPackage &&
- string.Equals(Path.GetFileNameWithoutExtension(assemblyPath), "FastMoq.Provider.Moq", StringComparison.OrdinalIgnoreCase))
+ string.Equals(assemblyName, "FastMoq.Provider.Moq", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (!includeNSubstituteProviderPackage &&
- string.Equals(Path.GetFileNameWithoutExtension(assemblyPath), "FastMoq.Provider.NSubstitute", StringComparison.OrdinalIgnoreCase))
+ string.Equals(assemblyName, "FastMoq.Provider.NSubstitute", StringComparison.OrdinalIgnoreCase))
{
continue;
}
@@ -203,12 +314,32 @@ private static IEnumerable GetMetadataReferences(bool include
}
references.Add(typeof(FastMoq.Mocker).Assembly.Location);
+ references.Add(typeof(FastMoq.Providers.FastMoqDefaultProviderAttribute).Assembly.Location);
+
+ if (includeAggregatePackage)
+ {
+ var aggregateAssemblyPath = Path.Combine(Path.GetDirectoryName(typeof(FastMoq.Mocker).Assembly.Location)!, "FastMoq.dll");
+ if (File.Exists(aggregateAssemblyPath))
+ {
+ references.Add(aggregateAssemblyPath);
+ }
+ }
if (includeWebHelpers)
{
references.Add(typeof(FastMoq.Web.Extensions.TestWebExtensions).Assembly.Location);
}
+ if (includeDatabaseHelpers)
+ {
+ references.Add(typeof(FastMoq.DbContextMockerExtensions).Assembly.Location);
+ }
+
+ if (includeAzureHelpers)
+ {
+ references.Add(typeof(FastMoq.Azure.Pageable.PageableBuilder).Assembly.Location);
+ }
+
if (includeMoqProviderPackage)
{
references.Add(typeof(FastMoq.Providers.MoqProvider.IFastMockMoqExtensions).Assembly.Location);
@@ -221,6 +352,10 @@ private static IEnumerable GetMetadataReferences(bool include
references.Add(typeof(Moq.Mock).Assembly.Location);
references.Add(typeof(NSubstitute.Substitute).Assembly.Location);
+ if (includeXunit)
+ {
+ references.Add(typeof(global::Xunit.FactAttribute).Assembly.Location);
+ }
references.Add(typeof(Microsoft.Extensions.Logging.ILogger).Assembly.Location);
references.Add(typeof(Microsoft.AspNetCore.Http.DefaultHttpContext).Assembly.Location);
references.Add(typeof(Microsoft.AspNetCore.Mvc.ControllerContext).Assembly.Location);
@@ -229,7 +364,7 @@ private static IEnumerable GetMetadataReferences(bool include
{
references.Add(typeof(FastMoq.AzureFunctions.Extensions.FunctionContextTestExtensions).Assembly.Location);
references.Add(typeof(Microsoft.Azure.Functions.Worker.FunctionContext).Assembly.Location);
- references.Add(typeof(Azure.Core.Serialization.ObjectSerializer).Assembly.Location);
+ references.Add(typeof(global::Azure.Core.Serialization.ObjectSerializer).Assembly.Location);
}
return references.Select(path => MetadataReference.CreateFromFile(path));
diff --git a/FastMoq.Analyzers.Tests/FastMoq.Analyzers.Tests.csproj b/FastMoq.Analyzers.Tests/FastMoq.Analyzers.Tests.csproj
index 2cac3b1a..65acd632 100644
--- a/FastMoq.Analyzers.Tests/FastMoq.Analyzers.Tests.csproj
+++ b/FastMoq.Analyzers.Tests/FastMoq.Analyzers.Tests.csproj
@@ -10,6 +10,7 @@
+
@@ -31,8 +32,12 @@
+
+
+
+
diff --git a/FastMoq.Analyzers.Tests/GeneratedHarnessSourceGeneratorTests.cs b/FastMoq.Analyzers.Tests/GeneratedHarnessSourceGeneratorTests.cs
new file mode 100644
index 00000000..d85925a2
--- /dev/null
+++ b/FastMoq.Analyzers.Tests/GeneratedHarnessSourceGeneratorTests.cs
@@ -0,0 +1,1425 @@
+using System;
+using FastMoq.Generators;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace FastMoq.Analyzers.Tests
+{
+ public sealed class GeneratedHarnessSourceGeneratorTests
+ {
+ private const string RepresentativeConsumingScenarioSource = @"
+using FastMoq;
+using FastMoq.Generators;
+using FastMoq.Models;
+using System;
+using System.IO.Abstractions;
+
+namespace Demo.Tests;
+
+public sealed class ConstructorSelectionTarget
+{
+ public ConstructorSelectionTarget()
+ {
+ ConstructorKind = ""parameterless"";
+ }
+
+ public ConstructorSelectionTarget(IFileSystem fileSystem, string value)
+ {
+ ConstructorKind = ""selected"";
+ ValueWasNull = value is null;
+ }
+
+ public string ConstructorKind { get; }
+
+ public bool ValueWasNull { get; }
+}
+
+[FastMoqGeneratedTestTarget(typeof(ConstructorSelectionTarget), typeof(IFileSystem), typeof(string))]
+public partial class GeneratedConstructorHarness : MockerTestBase
+{
+ public string DescribeConstructorKind() => Component.ConstructorKind;
+
+ public bool DescribeValueWasNull() => Component.ValueWasNull;
+
+ public Type?[]? DescribeConstructorTypes() => ComponentConstructorParameterTypes;
+
+ public InstanceConstructionPlan DescribeComponentConstruction() => GetComponentConstructionPlan();
+}
+
+public sealed class ManualConstructorHarness : MockerTestBase
+{
+ protected override Type?[]? ComponentConstructorParameterTypes => new Type?[] { typeof(IFileSystem), typeof(string) };
+
+ public string DescribeConstructorKind() => Component.ConstructorKind;
+
+ public bool DescribeValueWasNull() => Component.ValueWasNull;
+
+ public Type?[]? DescribeConstructorTypes() => ComponentConstructorParameterTypes;
+
+ public InstanceConstructionPlan DescribeComponentConstruction() => GetComponentConstructionPlan();
+}
+";
+
+ [Fact]
+ public async Task GeneratedHarnessSourceGenerator_ShouldEmitHarnessMetadata_ForSinglePublicConstructorTarget()
+ {
+ const string source = @"
+using FastMoq;
+using FastMoq.Generators;
+
+namespace Demo.Tests;
+
+public interface IOrderGateway { }
+
+public sealed class OrderSubmitter
+{
+ public OrderSubmitter(IOrderGateway gateway)
+ {
+ }
+}
+
+[FastMoqGeneratedTestTarget(typeof(OrderSubmitter))]
+public partial class OrderSubmitterTests : MockerTestBase
+{
+}
+";
+
+ var result = await RunGeneratorAsync(source);
+
+ result.DriverDiagnostics.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
+ .Should()
+ .BeEmpty();
+ result.OutputCompilation.GetDiagnostics().Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
+ .Should()
+ .BeEmpty();
+
+ var generatedSource = result.GeneratedSources.Should().ContainSingle().Subject;
+ generatedSource.SourceText.ToString().Should().Contain("protected override global::System.Type?[]? ComponentConstructorParameterTypes =>");
+ generatedSource.SourceText.ToString().Should().Contain("typeof(global::Demo.Tests.IOrderGateway)");
+ generatedSource.SourceText.ToString().Should().Contain("\"gateway\"");
+ }
+
+ [Fact]
+ public async Task GeneratedHarnessSourceGenerator_ShouldUseExplicitConstructorSignature_ForMultiConstructorTarget()
+ {
+ const string source = @"
+using FastMoq;
+using FastMoq.Generators;
+
+namespace Demo.Tests;
+
+public interface IOrderGateway { }
+
+public sealed class OrderSubmitter
+{
+ public OrderSubmitter()
+ {
+ }
+
+ public OrderSubmitter(IOrderGateway gateway)
+ {
+ }
+}
+
+[FastMoqGeneratedTestTarget(typeof(OrderSubmitter), typeof(IOrderGateway))]
+public partial class OrderSubmitterTests : MockerTestBase
+{
+}
+";
+
+ var result = await RunGeneratorAsync(source);
+
+ result.DriverDiagnostics.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
+ .Should()
+ .BeEmpty();
+ result.OutputCompilation.GetDiagnostics().Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
+ .Should()
+ .BeEmpty();
+
+ var generatedSource = result.GeneratedSources.Should().ContainSingle().Subject.SourceText.ToString();
+ generatedSource.Should().Contain("typeof(global::Demo.Tests.IOrderGateway)");
+ generatedSource.Should().NotContain("new global::System.Type?[]\r\n {\r\n };");
+ }
+
+ [Fact]
+ public async Task GeneratedHarnessSourceGenerator_ShouldUseExplicitParameterlessConstructor_ForMultiConstructorTarget()
+ {
+ const string source = @"
+using FastMoq;
+using FastMoq.Generators;
+using System;
+
+namespace Demo.Tests;
+
+public interface IOrderGateway { }
+
+public sealed class OrderSubmitter
+{
+ public OrderSubmitter()
+ {
+ ConstructorKind = ""parameterless"";
+ }
+
+ public OrderSubmitter(IOrderGateway gateway)
+ {
+ ConstructorKind = ""dependency"";
+ }
+
+ public string ConstructorKind { get; }
+}
+
+[FastMoqGeneratedTestTarget(typeof(OrderSubmitter), new global::System.Type[] { })]
+public partial class OrderSubmitterTests : MockerTestBase
+{
+ public string DescribeConstructorKind() => Component.ConstructorKind;
+
+ public Type?[]? DescribeConstructorTypes() => ComponentConstructorParameterTypes;
+}
+";
+
+ var loadedAssembly = await LoadGeneratedAssemblyAsync(source);
+ var generatedHarness = CreateInstance(loadedAssembly, "Demo.Tests.OrderSubmitterTests");
+
+ Invoke(generatedHarness, "DescribeConstructorKind").Should().Be("parameterless");
+ Invoke(generatedHarness, "DescribeConstructorTypes").Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task GeneratedHarnessSourceGenerator_ShouldEmitHarnessMetadata_ForNestedPartialHarnessTarget()
+ {
+ const string source = @"
+using FastMoq;
+using FastMoq.Generators;
+using System;
+
+namespace Demo.Tests;
+
+public sealed class NestedTarget
+{
+ public NestedTarget()
+ {
+ }
+}
+
+public partial class OuterHarnessContainer
+{
+ [FastMoqGeneratedTestTarget(typeof(NestedTarget))]
+ public partial class GeneratedNestedHarness : MockerTestBase
+ {
+ public Type?[]? DescribeConstructorTypes() => ComponentConstructorParameterTypes;
+ }
+}
+";
+
+ var result = await RunGeneratorAsync(source);
+
+ result.DriverDiagnostics.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
+ .Should()
+ .BeEmpty();
+ result.OutputCompilation.GetDiagnostics().Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
+ .Should()
+ .BeEmpty();
+
+ var generatedSource = result.GeneratedSources.Should().ContainSingle().Subject.SourceText.ToString();
+ generatedSource.Should().Contain("public partial class OuterHarnessContainer");
+ generatedSource.Should().Contain("public partial class GeneratedNestedHarness");
+
+ var loadedAssembly = await LoadGeneratedAssemblyAsync(source);
+ var generatedHarness = CreateInstance(loadedAssembly, "Demo.Tests.OuterHarnessContainer+GeneratedNestedHarness");
+
+ Invoke(generatedHarness, "DescribeConstructorTypes").Should().BeEmpty();
+ generatedHarness.GetType().GetNestedType("FastMoqGeneratedHarnessMetadata", BindingFlags.NonPublic).Should().NotBeNull();
+ }
+
+ [Fact]
+ public async Task GeneratedHarnessSourceGenerator_ShouldReportDiagnostic_WhenNestedHarnessContainingTypeIsNotPartial()
+ {
+ const string source = @"
+using FastMoq;
+using FastMoq.Generators;
+
+namespace Demo.Tests;
+
+public sealed class NestedTarget
+{
+ public NestedTarget()
+ {
+ }
+}
+
+public class OuterHarnessContainer
+{
+ [FastMoqGeneratedTestTarget(typeof(NestedTarget))]
+ public partial class GeneratedNestedHarness : MockerTestBase
+ {
+ }
+}
+";
+
+ var result = await RunGeneratorAsync(source);
+
+ result.OutputCompilation.GetDiagnostics().Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
+ .Should()
+ .BeEmpty();
+ result.GeneratedSources.Should().BeEmpty();
+
+ var diagnostic = result.DriverDiagnostics.Single(static d => d.Id == GeneratedHarnessSourceGenerator.UnsupportedNestedGeneratedTargetDiagnosticId);
+ diagnostic.Severity.Should().Be(DiagnosticSeverity.Warning);
+ diagnostic.GetMessage().Should().Contain("OuterHarnessContainer");
+ diagnostic.GetMessage().Should().Contain("GeneratedNestedHarness");
+ }
+
+ [Fact]
+ public async Task GeneratedHarnessSourceGenerator_ShouldNotEmit_WhenMultiplePublicConstructorsRemainAmbiguous()
+ {
+ const string source = @"
+using FastMoq;
+using FastMoq.Generators;
+
+namespace Demo.Tests;
+
+public interface IOrderGateway { }
+public interface IAuditWriter { }
+
+public sealed class OrderSubmitter
+{
+ public OrderSubmitter(IOrderGateway gateway)
+ {
+ }
+
+ public OrderSubmitter(IAuditWriter auditWriter)
+ {
+ }
+}
+
+[FastMoqGeneratedTestTarget(typeof(OrderSubmitter))]
+public partial class OrderSubmitterTests : MockerTestBase
+{
+}
+";
+
+ var result = await RunGeneratorAsync(source);
+
+ result.DriverDiagnostics.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
+ .Should()
+ .BeEmpty();
+ result.OutputCompilation.GetDiagnostics().Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
+ .Should()
+ .BeEmpty();
+ result.GeneratedSources.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task GeneratedHarnessSourceGenerator_ShouldWorkInRepresentativeCompiledConsumerScenario()
+ {
+ var loadedAssembly = await LoadGeneratedAssemblyAsync(RepresentativeConsumingScenarioSource);
+
+ var generatedHarness = CreateInstance(loadedAssembly, "Demo.Tests.GeneratedConstructorHarness");
+
+ Invoke(generatedHarness, "DescribeConstructorKind").Should().Be("selected");
+ Invoke(generatedHarness, "DescribeConstructorTypes")
+ .Should()
+ .Equal([typeof(System.IO.Abstractions.IFileSystem), typeof(string)]);
+
+ var generatedPlan = Invoke(generatedHarness, "DescribeComponentConstruction");
+ generatedPlan.Parameters.Should().HaveCount(2);
+ generatedPlan.Parameters[0].Name.Should().Be("fileSystem");
+ generatedPlan.Parameters[0].ParameterType.Should().Be(typeof(System.IO.Abstractions.IFileSystem));
+ generatedPlan.Parameters[1].Name.Should().Be("value");
+ generatedPlan.Parameters[1].ParameterType.Should().Be(typeof(string));
+ }
+
+ [Fact]
+ public async Task GeneratedHarnessSourceGenerator_ShouldMatchManualRuntimeHarness_ForRepresentativeConsumerScenario()
+ {
+ var loadedAssembly = await LoadGeneratedAssemblyAsync(RepresentativeConsumingScenarioSource);
+
+ var generatedHarness = CreateInstance(loadedAssembly, "Demo.Tests.GeneratedConstructorHarness");
+ var manualHarness = CreateInstance(loadedAssembly, "Demo.Tests.ManualConstructorHarness");
+
+ var generatedPlan = Invoke(generatedHarness, "DescribeComponentConstruction");
+ var manualPlan = Invoke(manualHarness, "DescribeComponentConstruction");
+
+ Invoke(generatedHarness, "DescribeConstructorKind").Should().Be(Invoke(manualHarness, "DescribeConstructorKind"));
+ Invoke(generatedHarness, "DescribeValueWasNull").Should().Be(Invoke(manualHarness, "DescribeValueWasNull"));
+ Invoke(generatedHarness, "DescribeConstructorTypes").Should().Equal(Invoke(manualHarness, "DescribeConstructorTypes"));
+
+ generatedPlan.RequestedType.Should().Be(manualPlan.RequestedType);
+ generatedPlan.ResolvedType.Should().Be(manualPlan.ResolvedType);
+ generatedPlan.UsedNonPublicConstructor.Should().Be(manualPlan.UsedNonPublicConstructor);
+ generatedPlan.UsedPreferredConstructorAttribute.Should().Be(manualPlan.UsedPreferredConstructorAttribute);
+ generatedPlan.UsedAmbiguityFallback.Should().Be(manualPlan.UsedAmbiguityFallback);
+ generatedPlan.Parameters.Select(static parameter => parameter.Name)
+ .Should()
+ .Equal(manualPlan.Parameters.Select(static parameter => parameter.Name));
+ generatedPlan.Parameters.Select(static parameter => parameter.ParameterType)
+ .Should()
+ .Equal(manualPlan.Parameters.Select(static parameter => parameter.ParameterType));
+ generatedPlan.Parameters.Select(static parameter => parameter.Source)
+ .Should()
+ .Equal(manualPlan.Parameters.Select(static parameter => parameter.Source));
+
+ var metadataType = generatedHarness.GetType().GetNestedType("FastMoqGeneratedHarnessMetadata", BindingFlags.NonPublic);
+ metadataType.Should().NotBeNull();
+ var dependencyNames = (string[]) metadataType!.GetProperty("DependencyNames", BindingFlags.NonPublic | BindingFlags.Static)!.GetValue(null)!;
+ var dependencyTypes = (Type[]) metadataType.GetProperty("DependencyTypes", BindingFlags.NonPublic | BindingFlags.Static)!.GetValue(null)!;
+
+ dependencyNames.Should().Equal(generatedPlan.Parameters.Select(static parameter => parameter.Name));
+ dependencyTypes.Should().Equal(generatedPlan.Parameters.Select(static parameter => parameter.ParameterType));
+ }
+
+ [Fact]
+ public async Task GeneratedHarnessSourceGenerator_ShouldEmitExecutableScenarioScaffold_ForGeneratedHarnessTarget()
+ {
+ const string source = @"
+using FastMoq;
+using FastMoq.Generators;
+
+namespace Demo.Tests;
+
+public sealed class ScenarioCounter
+{
+ public int Count { get; private set; }
+
+ public bool WasVerified { get; private set; }
+
+ public void Increment()
+ {
+ Count++;
+ }
+
+ public void MarkVerified()
+ {
+ WasVerified = true;
+ }
+}
+
+[FastMoqGeneratedTestTarget(typeof(ScenarioCounter))]
+public partial class GeneratedScenarioHarness : MockerTestBase
+{
+ public int DescribeCount() => Component.Count;
+
+ public bool DescribeWasVerified() => Component.WasVerified;
+
+ partial void ActGeneratedScenario(ScenarioBuilder scenario)
+ {
+ scenario.When(component => component.Increment());
+ }
+
+ partial void AssertGeneratedScenario(ScenarioBuilder scenario)
+ {
+ scenario.Then(component =>
+ {
+ if (component.Count != 1)
+ {
+ throw new global::System.InvalidOperationException(""Expected exactly one increment."");
+ }
+ });
+ }
+
+ partial void VerifyGeneratedScenario(ScenarioBuilder scenario)
+ {
+ scenario.Then(component => component.MarkVerified());
+ }
+}
+";
+
+ var loadedAssembly = await LoadGeneratedAssemblyAsync(source);
+ var generatedHarness = CreateInstance(loadedAssembly, "Demo.Tests.GeneratedScenarioHarness");
+
+ Invoke
+
+
diff --git a/FastMoq.Benchmarks/GeneratedHarnessSetupBenchmarks.cs b/FastMoq.Benchmarks/GeneratedHarnessSetupBenchmarks.cs
new file mode 100644
index 00000000..fe292252
--- /dev/null
+++ b/FastMoq.Benchmarks/GeneratedHarnessSetupBenchmarks.cs
@@ -0,0 +1,112 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Order;
+using FastMoq.Generators;
+
+namespace FastMoq.Benchmarks;
+
+///
+/// Measures fresh harness setup for the generated single-constructor path versus the normal runtime fallback path.
+///
+[MemoryDiagnoser]
+[Orderer(SummaryOrderPolicy.FastestToSlowest)]
+[RankColumn]
+public partial class GeneratedHarnessSetupBenchmarks
+{
+ ///
+ /// Measures creating a fresh runtime-only harness and projecting the graph/bootstrap descriptor through constructor discovery.
+ ///
+ [Benchmark(Baseline = true)]
+ public int RuntimeFallbackBootstrapDescriptor()
+ {
+ using var harness = new RuntimeSingleConstructorHarness();
+ return harness.GetBenchmarkedNodeCount();
+ }
+
+ ///
+ /// Measures creating a fresh source-generated harness and projecting the same graph/bootstrap descriptor through generated constructor metadata.
+ ///
+ [Benchmark]
+ public int GeneratedHarnessBootstrapDescriptor()
+ {
+ using var harness = new GeneratedSingleConstructorHarness();
+ return harness.GetBenchmarkedNodeCount();
+ }
+
+ internal sealed class SingleConstructorTarget
+ {
+ public SingleConstructorTarget(
+ IAlphaDependency alphaDependency,
+ IBetaDependency betaDependency,
+ IGammaDependency gammaDependency,
+ IDeltaDependency deltaDependency,
+ IEpsilonDependency epsilonDependency,
+ IZetaDependency zetaDependency,
+ IEtaDependency etaDependency,
+ IThetaDependency thetaDependency,
+ IServiceProvider serviceProvider,
+ string value,
+ int retryCount)
+ {
+ AlphaDependency = alphaDependency;
+ BetaDependency = betaDependency;
+ GammaDependency = gammaDependency;
+ DeltaDependency = deltaDependency;
+ EpsilonDependency = epsilonDependency;
+ ZetaDependency = zetaDependency;
+ EtaDependency = etaDependency;
+ ThetaDependency = thetaDependency;
+ ServiceProviderWasResolved = serviceProvider is not null;
+ ValueWasNull = value is null;
+ RetryCount = retryCount;
+ }
+
+ public IAlphaDependency AlphaDependency { get; }
+
+ public IBetaDependency BetaDependency { get; }
+
+ public IGammaDependency GammaDependency { get; }
+
+ public IDeltaDependency DeltaDependency { get; }
+
+ public IEpsilonDependency EpsilonDependency { get; }
+
+ public IZetaDependency ZetaDependency { get; }
+
+ public IEtaDependency EtaDependency { get; }
+
+ public IThetaDependency ThetaDependency { get; }
+
+ public bool ServiceProviderWasResolved { get; }
+
+ public bool ValueWasNull { get; }
+
+ public int RetryCount { get; }
+ }
+
+ internal interface IAlphaDependency;
+
+ internal interface IBetaDependency;
+
+ internal interface IGammaDependency;
+
+ internal interface IDeltaDependency;
+
+ internal interface IEpsilonDependency;
+
+ internal interface IZetaDependency;
+
+ internal interface IEtaDependency;
+
+ internal interface IThetaDependency;
+
+ [FastMoqGeneratedTestTarget(typeof(SingleConstructorTarget))]
+ internal partial class GeneratedSingleConstructorHarness : MockerTestBase
+ {
+ public int GetBenchmarkedNodeCount() => GetComponentHarnessBootstrapDescriptor().Graph.Nodes.Count;
+ }
+
+ internal sealed class RuntimeSingleConstructorHarness : MockerTestBase
+ {
+ public int GetBenchmarkedNodeCount() => GetComponentHarnessBootstrapDescriptor().Graph.Nodes.Count;
+ }
+}
\ No newline at end of file
diff --git a/FastMoq.Core/KnownTypeRegistry.cs b/FastMoq.Core/KnownTypeRegistry.cs
index 8a84d326..3cf388d2 100644
--- a/FastMoq.Core/KnownTypeRegistry.cs
+++ b/FastMoq.Core/KnownTypeRegistry.cs
@@ -112,6 +112,34 @@ internal static bool TryGetManagedInstance(Mocker mocker, Type requestedType, ou
return false;
}
+ internal static bool HasManagedInstanceResolution(Mocker mocker, Type requestedType)
+ {
+ ArgumentNullException.ThrowIfNull(mocker);
+ ArgumentNullException.ThrowIfNull(requestedType);
+
+ if (mocker.KnownTypeRegistrations.Any(registration =>
+ registration.Matches(requestedType) && registration.ManagedInstanceFactory != null))
+ {
+ return true;
+ }
+
+ return HasBuiltInManagedInstanceResolution(mocker, requestedType);
+ }
+
+ internal static bool HasKnownParameterResolution(Mocker mocker, Type requestedType)
+ {
+ ArgumentNullException.ThrowIfNull(mocker);
+ ArgumentNullException.ThrowIfNull(requestedType);
+
+ if (mocker.KnownTypeRegistrations.Any(registration =>
+ registration.Matches(requestedType) && (registration.DirectInstanceFactory != null || registration.ManagedInstanceFactory != null)))
+ {
+ return true;
+ }
+
+ return HasBuiltInKnownParameterResolution(mocker, requestedType);
+ }
+
internal static bool TryGetCustomManagedInstance(Mocker mocker, Type requestedType, out object? instance)
{
foreach (var registration in mocker.KnownTypeRegistrations)
@@ -178,6 +206,68 @@ private static IEnumerable GetPostProcessingRegistrations
return BuiltInRegistrations.Concat(mocker.KnownTypeRegistrations);
}
+ private static bool HasBuiltInKnownParameterResolution(Mocker mocker, Type requestedType)
+ {
+ var enabledResolutions = mocker.Policy.EnabledBuiltInTypeResolutions;
+ if ((enabledResolutions & BuiltInTypeResolutionFlags.FileSystem) != 0 &&
+ requestedType.IsEquivalentTo(typeof(IFileSystem)) &&
+ !mocker.Contains())
+ {
+ return true;
+ }
+
+ if ((enabledResolutions & BuiltInTypeResolutionFlags.HttpClient) != 0 && requestedType.IsEquivalentTo(typeof(HttpClient)))
+ {
+ return true;
+ }
+
+ if ((enabledResolutions & BuiltInTypeResolutionFlags.Uri) != 0 && requestedType.IsEquivalentTo(typeof(Uri)))
+ {
+ return true;
+ }
+
+ if (requestedType.IsAssignableTo(typeof(MemberInfo)) || requestedType == typeof(ParameterInfo))
+ {
+ return true;
+ }
+
+ return HasBuiltInManagedInstanceResolution(mocker, requestedType);
+ }
+
+ private static bool HasBuiltInManagedInstanceResolution(Mocker mocker, Type requestedType)
+ {
+ var enabledResolutions = mocker.Policy.EnabledBuiltInTypeResolutions;
+ if ((enabledResolutions & BuiltInTypeResolutionFlags.FileSystem) != 0 &&
+ requestedType.IsEquivalentTo(typeof(IFileSystem)) &&
+ !mocker.Contains())
+ {
+ return true;
+ }
+
+ if (requestedType == typeof(HttpContext) && !mocker.Contains())
+ {
+ return true;
+ }
+
+ if (requestedType == typeof(ControllerContext) && !mocker.HasTypeRegistration(typeof(ControllerContext)))
+ {
+ return true;
+ }
+
+ if (typeof(IHttpContextAccessor).IsAssignableFrom(requestedType) && !mocker.Contains())
+ {
+ return true;
+ }
+
+ if (requestedType.IsAssignableTo(typeof(MemberInfo)) || requestedType == typeof(ParameterInfo))
+ {
+ return true;
+ }
+
+ return (enabledResolutions & BuiltInTypeResolutionFlags.DbContext) != 0 &&
+ DatabaseSupportBridge.IsEntityFrameworkDbContextType(requestedType);
+ }
+
private static object? TryGetBuiltInFileSystem(Mocker mocker, Type type)
{
if ((mocker.Policy.EnabledBuiltInTypeResolutions & BuiltInTypeResolutionFlags.FileSystem) == 0)
diff --git a/FastMoq.Core/Mocker.ConstructionPlan.cs b/FastMoq.Core/Mocker.ConstructionPlan.cs
new file mode 100644
index 00000000..4334d4d8
--- /dev/null
+++ b/FastMoq.Core/Mocker.ConstructionPlan.cs
@@ -0,0 +1,225 @@
+using FastMoq.Extensions;
+using FastMoq.Models;
+using System.Reflection;
+using PublicInstanceConstructionRequest = FastMoq.Models.InstanceConstructionRequest;
+
+namespace FastMoq
+{
+ public partial class Mocker
+ {
+ ///
+ /// Resolves constructor-selection metadata for the supplied request without creating the target instance.
+ ///
+ /// The constructor-selection request to resolve.
+ /// A read-only constructor plan describing the selected target type and parameter metadata.
+ /// Thrown when is .
+ /// Thrown when the requested type does not currently use the constructor-selection path.
+ public InstanceConstructionPlan CreateConstructionPlan(PublicInstanceConstructionRequest request)
+ {
+ ArgumentNullException.ThrowIfNull(request);
+
+ var requestedType = CleanType(request.RequestedType);
+ var model = GetTypeModel(requestedType);
+
+ if (model.CreateFunc != null)
+ {
+ throw new InvalidOperationException($"Type '{requestedType}' resolves through a custom factory registration and does not expose constructor-plan metadata.");
+ }
+
+ if (KnownTypeRegistry.HasKnownParameterResolution(this, requestedType))
+ {
+ throw new InvalidOperationException($"Type '{requestedType}' resolves through a known-type path and does not expose constructor-plan metadata.");
+ }
+
+ if (model.Arguments.Count > 0 && request.ConstructorParameterTypes == null)
+ {
+ throw new InvalidOperationException($"Type '{requestedType}' has stored constructor arguments. Specify ConstructorParameterTypes explicitly before requesting constructor-plan metadata.");
+ }
+
+ var targetType = model.InstanceType ?? requestedType;
+ if (targetType.IsInterface ||
+ targetType.IsAbstract ||
+ targetType.ContainsGenericParameters)
+ {
+ throw new InvalidOperationException($"Type '{requestedType}' does not resolve to a concrete constructor path.");
+ }
+
+ var constructionRequest = CreateConstructorSelectionRequest(
+ request.PublicOnly,
+ request.ConstructorParameterTypes,
+ request.OptionalParameterResolution,
+ request.ConstructorAmbiguityBehavior);
+ var selection = SelectConstructionPlanConstructor(targetType, constructionRequest);
+ var parameters = selection.Constructor
+ .GetParameters()
+ .Select(parameter => CreateConstructionParameterPlan(parameter, constructionRequest.OptionalParameterResolution));
+
+ return new InstanceConstructionPlan(
+ requestedType,
+ targetType,
+ usedNonPublicConstructor: !selection.Constructor.IsPublic,
+ selection.UsedPreferredConstructorAttribute,
+ selection.UsedAmbiguityFallback,
+ parameters);
+ }
+
+ internal InstanceConstructionGraph CreateConstructionGraph(PublicInstanceConstructionRequest request)
+ {
+ ArgumentNullException.ThrowIfNull(request);
+
+ var plan = CreateConstructionPlan(request);
+ var rootNode = new InstanceConstructionGraphNode(
+ id: 0,
+ nodeType: plan.ResolvedType,
+ kind: InstanceConstructionGraphNodeKind.Root,
+ plan: plan);
+
+ var nodes = new List { rootNode };
+ var edges = new List();
+
+ foreach (var parameter in plan.Parameters)
+ {
+ var dependencyNode = new InstanceConstructionGraphNode(
+ id: nodes.Count,
+ nodeType: parameter.ParameterType,
+ kind: InstanceConstructionGraphNodeKind.Dependency,
+ parameter: parameter);
+
+ nodes.Add(dependencyNode);
+ edges.Add(new InstanceConstructionGraphEdge(rootNode.Id, dependencyNode.Id, parameter.Position, parameter.Name));
+ }
+
+ return new InstanceConstructionGraph(request, rootNode, nodes, edges);
+ }
+
+ internal PublicInstanceConstructionRequest CreateConstructionPlanRequest(Type requestedType, InstanceCreationFlags flags, Type?[]? constructorParameterTypes)
+ {
+ ArgumentNullException.ThrowIfNull(requestedType);
+
+ var request = CreateConstructorSelectionRequest(flags, constructorParameterTypes);
+ return new PublicInstanceConstructionRequest(requestedType)
+ {
+ ConstructorParameterTypes = constructorParameterTypes,
+ PublicOnly = request.PublicOnly,
+ OptionalParameterResolution = request.OptionalParameterResolution,
+ ConstructorAmbiguityBehavior = request.ConstructorAmbiguityBehavior,
+ };
+ }
+
+ private ConstructionPlanSelection SelectConstructionPlanConstructor(Type targetType, ConstructorSelectionRequest request)
+ {
+ if (request.ConstructorParameterTypes != null)
+ {
+ var exactConstructor = FindConstructorByType(targetType, false, ShouldFallbackToNonPublicConstructors(request.PublicOnly), request.ConstructorParameterTypes);
+ return new ConstructionPlanSelection(exactConstructor, UsedPreferredConstructorAttribute: false, UsedAmbiguityFallback: false);
+ }
+
+ var constructorModel = FindPreferredConstructor(
+ targetType,
+ nonPublic: false,
+ fallbackToNonPublicConstructors: ShouldFallbackToNonPublicConstructors(request.PublicOnly),
+ constructorAmbiguityBehavior: request.ConstructorAmbiguityBehavior,
+ optionalParameterResolution: request.OptionalParameterResolution);
+
+ var constructor = constructorModel.ConstructorInfo
+ ?? throw GetConstructorResolutionException(targetType, "Constructor selection did not return a constructor.");
+ var usedPreferredConstructorAttribute = constructor.IsDefined(typeof(PreferredConstructorAttribute), inherit: false);
+ var usedAmbiguityFallback = !usedPreferredConstructorAttribute && WasAmbiguityFallbackUsed(targetType, constructor, request);
+ return new ConstructionPlanSelection(constructor, usedPreferredConstructorAttribute, usedAmbiguityFallback);
+ }
+
+ private bool WasAmbiguityFallbackUsed(Type targetType, ConstructorInfo selectedConstructor, ConstructorSelectionRequest request)
+ {
+ if (request.ConstructorAmbiguityBehavior != ConstructorAmbiguityBehavior.PreferParameterlessConstructor ||
+ selectedConstructor.GetParameters().Length != 0)
+ {
+ return false;
+ }
+
+ var constructors = selectedConstructor.IsPublic
+ ? GetConstructors(targetType, false, request.OptionalParameterResolution)
+ : GetConstructors(targetType, true, request.OptionalParameterResolution)
+ .Where(constructor => constructor.ConstructorInfo?.IsPublic == false)
+ .ToList();
+
+ constructors = constructors
+ .Where(constructor => constructor.ConstructorInfo?.IsDefined(typeof(PreferredConstructorAttribute), inherit: false) != true)
+ .ToList();
+
+ var testedConstructors = this.GetTestedConstructors(targetType, constructors);
+ if (testedConstructors.Count == 0)
+ {
+ return false;
+ }
+
+ var largestArity = testedConstructors.Max(constructor => constructor.ParameterList.Length);
+ return largestArity > 0 && testedConstructors.Count(constructor => constructor.ParameterList.Length == largestArity) > 1;
+ }
+
+ private InstanceConstructionParameterPlan CreateConstructionParameterPlan(ParameterInfo parameter, OptionalParameterResolutionMode optionalParameterResolution)
+ {
+ var hasServiceKey = TryGetServiceKey(parameter, out var serviceKey);
+ var source = ResolveConstructionParameterSource(parameter, optionalParameterResolution, hasServiceKey ? serviceKey : null);
+
+ return new InstanceConstructionParameterPlan(
+ parameter.Name ?? string.Empty,
+ parameter.ParameterType,
+ parameter.Position,
+ parameter.IsOptional,
+ optionalParameterResolution,
+ hasServiceKey ? serviceKey : null,
+ source);
+ }
+
+ private InstanceConstructionParameterSource ResolveConstructionParameterSource(ParameterInfo parameter, OptionalParameterResolutionMode optionalParameterResolution, object? serviceKey)
+ {
+ if (serviceKey != null)
+ {
+ return InstanceConstructionParameterSource.KeyedService;
+ }
+
+ if (optionalParameterResolution == OptionalParameterResolutionMode.UseDefaultOrNull && parameter.IsOptional)
+ {
+ return InstanceConstructionParameterSource.OptionalDefault;
+ }
+
+ var parameterType = parameter.ParameterType;
+ if (HasCustomRegistrationAffectingResolution(parameterType))
+ {
+ return InstanceConstructionParameterSource.CustomRegistration;
+ }
+
+ if (KnownTypeRegistry.HasKnownParameterResolution(this, parameterType))
+ {
+ return InstanceConstructionParameterSource.KnownType;
+ }
+
+ if (parameterType.IsClass || parameterType.IsInterface)
+ {
+ return !parameterType.IsSealed
+ ? InstanceConstructionParameterSource.AutoMock
+ : InstanceConstructionParameterSource.TypeDefault;
+ }
+
+ return InstanceConstructionParameterSource.TypeDefault;
+ }
+
+ private bool HasCustomRegistrationAffectingResolution(Type parameterType)
+ {
+ if (!HasTypeRegistration(parameterType))
+ {
+ return false;
+ }
+
+ var model = GetTypeModel(parameterType);
+ return model.CreateFunc != null ||
+ model.Arguments.Count > 0 ||
+ model.InstanceType != parameterType;
+ }
+
+ private readonly record struct ConstructionPlanSelection(
+ ConstructorInfo Constructor,
+ bool UsedPreferredConstructorAttribute,
+ bool UsedAmbiguityFallback);
+ }
+}
\ No newline at end of file
diff --git a/FastMoq.Core/Mocker.cs b/FastMoq.Core/Mocker.cs
index 91ef5c28..69bc26d8 100644
--- a/FastMoq.Core/Mocker.cs
+++ b/FastMoq.Core/Mocker.cs
@@ -81,7 +81,7 @@ public partial class Mocker : IDisposable, IAsyncDisposable
private static readonly EventId ConstructorAmbiguityEventId = new(21071, nameof(ConstructorAmbiguityEventId));
private static readonly ConcurrentDictionary AutoPopulatedPropertyCache = new();
- private readonly record struct InstanceConstructionRequest(
+ private readonly record struct ConstructorSelectionRequest(
bool? PublicOnly,
Type?[]? ConstructorParameterTypes,
OptionalParameterResolutionMode OptionalParameterResolution,
@@ -1132,7 +1132,7 @@ private static bool TryGetServiceKey(ParameterInfo parameter, out object? servic
/// Creates an instance of using the supplied per-call construction flags.
///
public T? CreateInstance(InstanceCreationFlags flags, params object?[] args) where T : class =>
- CreateInstanceCore(CreateInstanceConstructionRequest(flags, constructorParameterTypes: null), args);
+ CreateInstanceCore(CreateConstructorSelectionRequest(flags, constructorParameterTypes: null), args);
///
/// Legacy compatibility overload retained for callers that previously toggled file-system resolution per call.
@@ -1172,7 +1172,7 @@ private static bool TryGetServiceKey(ParameterInfo parameter, out object? servic
}
internal T? CreateInstance(bool? publicOnly, OptionalParameterResolutionMode optionalParameterResolution, params object?[] args) where T : class =>
- CreateInstanceCore(CreateInstanceConstructionRequest(publicOnly, null, optionalParameterResolution, Policy.DefaultConstructorAmbiguityBehavior), args);
+ CreateInstanceCore(CreateConstructorSelectionRequest(publicOnly, null, optionalParameterResolution, Policy.DefaultConstructorAmbiguityBehavior), args);
///
/// Creates an instance of by selecting a constructor that matches the supplied parameter types.
@@ -1186,20 +1186,20 @@ private static bool TryGetServiceKey(ParameterInfo parameter, out object? servic
/// Creates an instance of by selecting a constructor that matches the supplied parameter types and creation flags.
///
public T? CreateInstanceByType(InstanceCreationFlags flags, params Type?[] parameterTypes) where T : class =>
- CreateInstanceCore(CreateInstanceConstructionRequest(flags, parameterTypes), Array.Empty());
+ CreateInstanceCore(CreateConstructorSelectionRequest(flags, parameterTypes), Array.Empty());
internal T? CreateInstanceByType(bool? publicOnly, OptionalParameterResolutionMode optionalParameterResolution, params Type?[] parameterTypes) where T : class =>
- CreateInstanceCore(CreateInstanceConstructionRequest(publicOnly, parameterTypes, optionalParameterResolution, Policy.DefaultConstructorAmbiguityBehavior), Array.Empty());
+ CreateInstanceCore(CreateConstructorSelectionRequest(publicOnly, parameterTypes, optionalParameterResolution, Policy.DefaultConstructorAmbiguityBehavior), Array.Empty());
- private InstanceConstructionRequest CreateInstanceConstructionRequest(InstanceCreationFlags flags, Type?[]? constructorParameterTypes)
+ private ConstructorSelectionRequest CreateConstructorSelectionRequest(InstanceCreationFlags flags, Type?[]? constructorParameterTypes)
{
var publicOnly = ResolvePublicOnlyOverride(flags);
var optionalParameterResolution = ResolveOptionalParameterResolution(flags);
var constructorAmbiguityBehavior = ResolveConstructorAmbiguityBehavior(flags);
- return CreateInstanceConstructionRequest(publicOnly, constructorParameterTypes, optionalParameterResolution, constructorAmbiguityBehavior);
+ return CreateConstructorSelectionRequest(publicOnly, constructorParameterTypes, optionalParameterResolution, constructorAmbiguityBehavior);
}
- private InstanceConstructionRequest CreateInstanceConstructionRequest(bool? publicOnly, Type?[]? constructorParameterTypes, OptionalParameterResolutionMode optionalParameterResolution, ConstructorAmbiguityBehavior constructorAmbiguityBehavior) =>
+ private ConstructorSelectionRequest CreateConstructorSelectionRequest(bool? publicOnly, Type?[]? constructorParameterTypes, OptionalParameterResolutionMode optionalParameterResolution, ConstructorAmbiguityBehavior constructorAmbiguityBehavior) =>
new(publicOnly, constructorParameterTypes, optionalParameterResolution, constructorAmbiguityBehavior);
private static bool? ResolvePublicOnlyOverride(InstanceCreationFlags flags)
@@ -1261,7 +1261,7 @@ private ConstructorAmbiguityBehavior ResolveConstructorAmbiguityBehavior(Instanc
///
/// Centralized creation logic used by all public CreateInstance* methods.
///
- private T? CreateInstanceCore(InstanceConstructionRequest request, object?[] args) where T : class
+ private T? CreateInstanceCore(ConstructorSelectionRequest request, object?[] args) where T : class
{
var requestedType = typeof(T);
var model = GetTypeModel(requestedType);
diff --git a/FastMoq.Core/MockerTestBase_Constructors.cs b/FastMoq.Core/MockerTestBase_Constructors.cs
index 54f524f3..8b17c99c 100644
--- a/FastMoq.Core/MockerTestBase_Constructors.cs
+++ b/FastMoq.Core/MockerTestBase_Constructors.cs
@@ -1,4 +1,5 @@
-๏ปฟusing Microsoft.Extensions.Logging;
+๏ปฟusing FastMoq.Models;
+using Microsoft.Extensions.Logging;
namespace FastMoq
{
@@ -23,6 +24,38 @@ public partial class MockerTestBase where TComponent : class
///
protected virtual Type?[]? ComponentConstructorParameterTypes => null;
+ ///
+ /// Creates the constructor-planning request for the current component path.
+ /// Override this when a custom no longer matches the default constructor-selection hooks.
+ ///
+ protected virtual InstanceConstructionRequest CreateComponentConstructionRequest() =>
+ Mocks.CreateConstructionPlanRequest(typeof(TComponent), ComponentCreationFlags, ComponentConstructorParameterTypes);
+
+ ///
+ /// Resolves constructor-selection metadata for the current component path without creating a new component instance.
+ /// Generated or hand-written harnesses can use this to query the component-construction contract through the same request-only planning surface used by .
+ ///
+ /// A constructor plan for the current component-construction path.
+ protected InstanceConstructionPlan GetComponentConstructionPlan() =>
+ Mocks.CreateConstructionPlan(CreateComponentConstructionRequest());
+
+ internal InstanceConstructionGraph GetComponentConstructionGraph() =>
+ Mocks.CreateConstructionGraph(CreateComponentConstructionRequest());
+
+ internal ComponentHarnessBootstrapDescriptor GetComponentHarnessBootstrapDescriptor()
+ {
+ var componentCreationFlags = ComponentCreationFlags;
+ var componentConstructorParameterTypes = ComponentConstructorParameterTypes;
+ var defaultRequest = Mocks.CreateConstructionPlanRequest(typeof(TComponent), componentCreationFlags, componentConstructorParameterTypes);
+ var request = CreateComponentConstructionRequest();
+
+ return new ComponentHarnessBootstrapDescriptor(
+ Mocks.CreateConstructionGraph(request),
+ componentCreationFlags,
+ componentConstructorParameterTypes,
+ requiresExplicitConstructionRequestOverride: !RequestsAreEquivalent(defaultRequest, request));
+ }
+
private Func DefaultCreateAction =>
mocker => Component = CreateDefaultComponent(mocker) ?? throw CannotCreateComponentException;
@@ -206,6 +239,41 @@ protected MockerTestBase(Action setupMocksAction,
: mocker.CreateInstanceByType(ComponentCreationFlags, constructorParameterTypes);
}
+ private static bool RequestsAreEquivalent(InstanceConstructionRequest left, InstanceConstructionRequest right)
+ {
+ ArgumentNullException.ThrowIfNull(left);
+ ArgumentNullException.ThrowIfNull(right);
+
+ return left.RequestedType == right.RequestedType &&
+ left.PublicOnly == right.PublicOnly &&
+ left.OptionalParameterResolution == right.OptionalParameterResolution &&
+ left.ConstructorAmbiguityBehavior == right.ConstructorAmbiguityBehavior &&
+ ConstructorParameterTypesAreEquivalent(left.ConstructorParameterTypes, right.ConstructorParameterTypes);
+ }
+
+ private static bool ConstructorParameterTypesAreEquivalent(Type?[]? left, Type?[]? right)
+ {
+ if (left == null || right == null)
+ {
+ return left == right;
+ }
+
+ if (left.Length != right.Length)
+ {
+ return false;
+ }
+
+ for (var index = 0; index < left.Length; index++)
+ {
+ if (left[index] != right[index])
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
private static Func CreateActionWithTypes(params Type[] args) =>
m => m.CreateInstanceByType(args) ?? throw CannotCreateComponentException;
}
diff --git a/FastMoq.Core/Models/ComponentHarnessBootstrapDescriptor.cs b/FastMoq.Core/Models/ComponentHarnessBootstrapDescriptor.cs
new file mode 100644
index 00000000..ab151452
--- /dev/null
+++ b/FastMoq.Core/Models/ComponentHarnessBootstrapDescriptor.cs
@@ -0,0 +1,27 @@
+namespace FastMoq.Models
+{
+ internal sealed class ComponentHarnessBootstrapDescriptor
+ {
+ public ComponentHarnessBootstrapDescriptor(
+ InstanceConstructionGraph graph,
+ InstanceCreationFlags componentCreationFlags,
+ IEnumerable? componentConstructorParameterTypes,
+ bool requiresExplicitConstructionRequestOverride)
+ {
+ Graph = graph ?? throw new ArgumentNullException(nameof(graph));
+ ComponentCreationFlags = componentCreationFlags;
+ ComponentConstructorParameterTypes = componentConstructorParameterTypes == null
+ ? null
+ : [.. componentConstructorParameterTypes];
+ RequiresExplicitConstructionRequestOverride = requiresExplicitConstructionRequestOverride;
+ }
+
+ public InstanceConstructionGraph Graph { get; }
+
+ public InstanceCreationFlags ComponentCreationFlags { get; }
+
+ public IReadOnlyList? ComponentConstructorParameterTypes { get; }
+
+ public bool RequiresExplicitConstructionRequestOverride { get; }
+ }
+}
diff --git a/FastMoq.Core/Models/InstanceConstructionGraph.cs b/FastMoq.Core/Models/InstanceConstructionGraph.cs
new file mode 100644
index 00000000..fd86ad5a
--- /dev/null
+++ b/FastMoq.Core/Models/InstanceConstructionGraph.cs
@@ -0,0 +1,87 @@
+namespace FastMoq.Models
+{
+ internal sealed class InstanceConstructionGraph
+ {
+ public InstanceConstructionGraph(
+ InstanceConstructionRequest request,
+ InstanceConstructionGraphNode root,
+ IEnumerable nodes,
+ IEnumerable edges)
+ {
+ Request = request ?? throw new ArgumentNullException(nameof(request));
+ Root = root ?? throw new ArgumentNullException(nameof(root));
+ Nodes = [.. nodes ?? throw new ArgumentNullException(nameof(nodes))];
+ Edges = [.. edges ?? throw new ArgumentNullException(nameof(edges))];
+ }
+
+ public InstanceConstructionRequest Request { get; }
+
+ public InstanceConstructionGraphNode Root { get; }
+
+ public IReadOnlyList Nodes { get; }
+
+ public IReadOnlyList Edges { get; }
+ }
+
+ internal sealed class InstanceConstructionGraphNode
+ {
+ public InstanceConstructionGraphNode(
+ int id,
+ Type nodeType,
+ InstanceConstructionGraphNodeKind kind,
+ InstanceConstructionPlan? plan = null,
+ InstanceConstructionParameterPlan? parameter = null)
+ {
+ if (kind == InstanceConstructionGraphNodeKind.Root && plan == null)
+ {
+ throw new ArgumentException("Root graph nodes require a construction plan.", nameof(plan));
+ }
+
+ if (kind == InstanceConstructionGraphNodeKind.Dependency && parameter == null)
+ {
+ throw new ArgumentException("Dependency graph nodes require parameter metadata.", nameof(parameter));
+ }
+
+ Id = id;
+ NodeType = nodeType ?? throw new ArgumentNullException(nameof(nodeType));
+ Kind = kind;
+ Plan = plan;
+ Parameter = parameter;
+ }
+
+ public int Id { get; }
+
+ public Type NodeType { get; }
+
+ public InstanceConstructionGraphNodeKind Kind { get; }
+
+ public InstanceConstructionPlan? Plan { get; }
+
+ public InstanceConstructionParameterPlan? Parameter { get; }
+ }
+
+ internal sealed class InstanceConstructionGraphEdge
+ {
+ public InstanceConstructionGraphEdge(int fromNodeId, int toNodeId, int parameterPosition, string parameterName)
+ {
+ FromNodeId = fromNodeId;
+ ToNodeId = toNodeId;
+ ParameterPosition = parameterPosition;
+ ParameterName = parameterName ?? throw new ArgumentNullException(nameof(parameterName));
+ }
+
+ public int FromNodeId { get; }
+
+ public int ToNodeId { get; }
+
+ public int ParameterPosition { get; }
+
+ public string ParameterName { get; }
+ }
+
+ internal enum InstanceConstructionGraphNodeKind
+ {
+ Root = 0,
+ Dependency = 1,
+ }
+}
\ No newline at end of file
diff --git a/FastMoq.Core/Models/InstanceConstructionPlan.cs b/FastMoq.Core/Models/InstanceConstructionPlan.cs
new file mode 100644
index 00000000..780529d5
--- /dev/null
+++ b/FastMoq.Core/Models/InstanceConstructionPlan.cs
@@ -0,0 +1,168 @@
+namespace FastMoq.Models
+{
+ ///
+ /// Describes the constructor-selection outcome FastMoq resolved for a requested type.
+ ///
+ public sealed class InstanceConstructionPlan
+ {
+ ///
+ /// Initializes a new resolved constructor plan.
+ ///
+ /// The originally requested type.
+ /// The concrete type whose constructor was selected.
+ /// True when the selected constructor is non-public.
+ /// True when constructor selection was driven by .
+ /// True when ambiguity resolution fell back to a parameterless constructor.
+ /// The selected constructor parameters in declaration order.
+ public InstanceConstructionPlan(
+ Type requestedType,
+ Type resolvedType,
+ bool usedNonPublicConstructor,
+ bool usedPreferredConstructorAttribute,
+ bool usedAmbiguityFallback,
+ IEnumerable parameters)
+ {
+ RequestedType = requestedType ?? throw new ArgumentNullException(nameof(requestedType));
+ ResolvedType = resolvedType ?? throw new ArgumentNullException(nameof(resolvedType));
+ UsedNonPublicConstructor = usedNonPublicConstructor;
+ UsedPreferredConstructorAttribute = usedPreferredConstructorAttribute;
+ UsedAmbiguityFallback = usedAmbiguityFallback;
+ Parameters = [.. parameters ?? throw new ArgumentNullException(nameof(parameters))];
+ }
+
+ ///
+ /// Gets the originally requested service or concrete type.
+ ///
+ public Type RequestedType { get; }
+
+ ///
+ /// Gets the concrete type whose constructor was selected.
+ ///
+ public Type ResolvedType { get; }
+
+ ///
+ /// Gets a value indicating whether the selected constructor is non-public.
+ ///
+ public bool UsedNonPublicConstructor { get; }
+
+ ///
+ /// Gets a value indicating whether constructor selection used .
+ ///
+ public bool UsedPreferredConstructorAttribute { get; }
+
+ ///
+ /// Gets a value indicating whether ambiguity resolution fell back to a parameterless constructor.
+ ///
+ public bool UsedAmbiguityFallback { get; }
+
+ ///
+ /// Gets the selected constructor parameters in declaration order.
+ ///
+ public IReadOnlyList Parameters { get; }
+ }
+
+ ///
+ /// Describes one selected constructor parameter in an .
+ ///
+ public sealed class InstanceConstructionParameterPlan
+ {
+ ///
+ /// Initializes a new constructor-parameter plan entry.
+ ///
+ /// The constructor parameter name.
+ /// The constructor parameter type.
+ /// The zero-based parameter position.
+ /// True when the constructor parameter is optional.
+ /// The optional-parameter policy used for this parameter.
+ /// The DI-style service key when the parameter is keyed; otherwise .
+ /// The resolved source category FastMoq would use for the parameter.
+ public InstanceConstructionParameterPlan(
+ string name,
+ Type parameterType,
+ int position,
+ bool isOptional,
+ OptionalParameterResolutionMode optionalParameterResolution,
+ object? serviceKey,
+ InstanceConstructionParameterSource source)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ ParameterType = parameterType ?? throw new ArgumentNullException(nameof(parameterType));
+ Position = position;
+ IsOptional = isOptional;
+ OptionalParameterResolution = optionalParameterResolution;
+ ServiceKey = serviceKey;
+ Source = source;
+ }
+
+ ///
+ /// Gets the constructor parameter name.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets the constructor parameter type.
+ ///
+ public Type ParameterType { get; }
+
+ ///
+ /// Gets the zero-based constructor parameter position.
+ ///
+ public int Position { get; }
+
+ ///
+ /// Gets a value indicating whether the constructor parameter is optional.
+ ///
+ public bool IsOptional { get; }
+
+ ///
+ /// Gets the optional-parameter policy FastMoq would apply for this parameter.
+ ///
+ public OptionalParameterResolutionMode OptionalParameterResolution { get; }
+
+ ///
+ /// Gets the DI-style service key when the parameter is keyed.
+ ///
+ public object? ServiceKey { get; }
+
+ ///
+ /// Gets the resolved source category FastMoq would use for this parameter.
+ ///
+ public InstanceConstructionParameterSource Source { get; }
+ }
+
+ ///
+ /// Describes the source category FastMoq would use for a selected constructor parameter.
+ ///
+ public enum InstanceConstructionParameterSource
+ {
+ ///
+ /// The parameter resolves through a custom registration that provides its own instance or factory.
+ ///
+ CustomRegistration = 0,
+
+ ///
+ /// The parameter resolves through a built-in or custom known-type registration.
+ ///
+ KnownType = 1,
+
+ ///
+ /// The parameter resolves through keyed-service metadata.
+ ///
+ KeyedService = 2,
+
+ ///
+ /// The parameter resolves through FastMoq's tracked-mock creation path.
+ ///
+ AutoMock = 3,
+
+ ///
+ /// The parameter uses its declared optional default value or null.
+ ///
+ OptionalDefault = 4,
+
+ ///
+ /// The parameter falls back to the type default because FastMoq does not create a richer value.
+ ///
+ TypeDefault = 5,
+ }
+}
\ No newline at end of file
diff --git a/FastMoq.Core/Models/InstanceConstructionRequest.cs b/FastMoq.Core/Models/InstanceConstructionRequest.cs
new file mode 100644
index 00000000..b34f9b19
--- /dev/null
+++ b/FastMoq.Core/Models/InstanceConstructionRequest.cs
@@ -0,0 +1,45 @@
+namespace FastMoq.Models
+{
+ ///
+ /// Describes constructor-selection intent for a type that should be planned through FastMoq's runtime constructor-resolution pipeline.
+ ///
+ public sealed class InstanceConstructionRequest
+ {
+ ///
+ /// Initializes a new request for the supplied requested type.
+ ///
+ /// The requested service or concrete type whose constructor path should be planned.
+ public InstanceConstructionRequest(Type requestedType)
+ {
+ RequestedType = requestedType ?? throw new ArgumentNullException(nameof(requestedType));
+ }
+
+ ///
+ /// Gets the requested service or concrete type whose constructor path should be planned.
+ ///
+ public Type RequestedType { get; }
+
+ ///
+ /// Gets or sets the exact constructor parameter types to match.
+ /// Set this to to use FastMoq's preferred-constructor selection rules.
+ /// Set this to an empty array to request the parameterless constructor explicitly.
+ ///
+ public Type?[]? ConstructorParameterTypes { get; init; }
+
+ ///
+ /// Gets or sets whether constructor selection should stay on public constructors only.
+ /// Set this to to use the current policy.
+ ///
+ public bool? PublicOnly { get; init; }
+
+ ///
+ /// Gets or sets how optional parameters should be resolved when FastMoq needs to supply values automatically.
+ ///
+ public OptionalParameterResolutionMode OptionalParameterResolution { get; init; } = OptionalParameterResolutionMode.UseDefaultOrNull;
+
+ ///
+ /// Gets or sets how constructor ambiguity should be handled when multiple equally viable constructors remain.
+ ///
+ public ConstructorAmbiguityBehavior ConstructorAmbiguityBehavior { get; init; } = ConstructorAmbiguityBehavior.Throw;
+ }
+}
\ No newline at end of file
diff --git a/FastMoq.Generators/AnalyzerReleases.Shipped.md b/FastMoq.Generators/AnalyzerReleases.Shipped.md
new file mode 100644
index 00000000..e69de29b
diff --git a/FastMoq.Generators/AnalyzerReleases.Unshipped.md b/FastMoq.Generators/AnalyzerReleases.Unshipped.md
new file mode 100644
index 00000000..10f28760
--- /dev/null
+++ b/FastMoq.Generators/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,5 @@
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|--------------------
+FMOQGEN001 | FastMoq.Generators | Warning | Nested generated harness target requires partial containing types
diff --git a/FastMoq.Generators/FastMoq.Generators.csproj b/FastMoq.Generators/FastMoq.Generators.csproj
new file mode 100644
index 00000000..806081c4
--- /dev/null
+++ b/FastMoq.Generators/FastMoq.Generators.csproj
@@ -0,0 +1,40 @@
+
+
+
+ netstandard2.0
+ enable
+ preview
+ false
+ true
+ FastMoq source generators for explicit provider-first harness bootstrap and constructor-signature metadata.
+ $(FastMoqBasePackageTags);generators;roslyn;source-generator;provider-first
+ true
+ $(NoWarn);NU5128;RS1038
+
+
+
+
+ <_Parameter1>FastMoq.Analyzers.Tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+
+
+
+
\ No newline at end of file
diff --git a/FastMoq.Generators/GeneratedHarnessSourceGenerator.cs b/FastMoq.Generators/GeneratedHarnessSourceGenerator.cs
new file mode 100644
index 00000000..b9480acc
--- /dev/null
+++ b/FastMoq.Generators/GeneratedHarnessSourceGenerator.cs
@@ -0,0 +1,1009 @@
+using System;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System.Collections.Immutable;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+
+namespace FastMoq.Generators
+{
+ [Generator]
+ public sealed class GeneratedHarnessSourceGenerator : IIncrementalGenerator
+ {
+ internal const string GeneratedTestTargetAttributeMetadataName = "FastMoq.Generators.FastMoqGeneratedTestTargetAttribute";
+ internal const string UnsupportedNestedGeneratedTargetDiagnosticId = "FMOQGEN001";
+ private const string XUnitFactAttributeMetadataName = "Xunit.FactAttribute";
+ private const string FastMoqGeneratedTestFrameworkPropertyName = "build_property.FastMoqGeneratedTestFramework";
+ private const string FrameworkSettingNone = "none";
+ private const string ComponentConstructorParameterTypesPropertyName = "ComponentConstructorParameterTypes";
+ private const string SetupMocksActionPropertyName = "SetupMocksAction";
+ private const string CreatedComponentActionPropertyName = "CreatedComponentAction";
+ private const string ConfigureMockerPolicyPropertyName = "ConfigureMockerPolicy";
+ private const string MockerTestBaseTypeName = "MockerTestBase`1";
+ private const string MockerTestBaseNamespace = "FastMoq";
+ private const string ThreadingTasksNamespace = "System.Threading.Tasks";
+ private const string TaskMetadataName = "Task";
+ private const string GenericTaskMetadataName = "Task`1";
+ private const string ValueTaskMetadataName = "ValueTask";
+ private const string GenericValueTaskMetadataName = "ValueTask`1";
+ private static readonly DiagnosticDescriptor UnsupportedNestedGeneratedTargetDiagnostic = new(
+ UnsupportedNestedGeneratedTargetDiagnosticId,
+ "Nested generated harness target requires partial containing types",
+ "FastMoqGeneratedTestTarget on nested type '{0}' requires containing type '{1}' to be partial so FastMoq can emit matching nested declarations",
+ "FastMoq.Generators",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "FastMoq can emit generated harness members for nested targets only when each containing type is partial, allowing the generator to reopen the containing declaration chain.");
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var evaluations = context.SyntaxProvider
+ .ForAttributeWithMetadataName(
+ GeneratedTestTargetAttributeMetadataName,
+ static (node, _) => node is ClassDeclarationSyntax,
+ static (generatorContext, _) => EvaluateTarget(generatorContext));
+
+ context.RegisterSourceOutput(
+ evaluations,
+ static (productionContext, evaluation) =>
+ {
+ foreach (var diagnostic in evaluation.Diagnostics)
+ {
+ productionContext.ReportDiagnostic(diagnostic);
+ }
+ });
+
+ var targets = evaluations
+ .Where(static evaluation => evaluation.Target is not null)
+ .Select(static (evaluation, _) => evaluation.Target!);
+
+ var frameworkSetting = context.AnalyzerConfigOptionsProvider
+ .Select(static (options, _) =>
+ {
+ options.GlobalOptions.TryGetValue(FastMoqGeneratedTestFrameworkPropertyName, out var value);
+ return value?.Trim() ?? string.Empty;
+ });
+
+ context.RegisterSourceOutput(
+ targets.Combine(frameworkSetting),
+ static (productionContext, pair) => EmitSource(productionContext, pair.Left!, pair.Right));
+ }
+
+ private static GeneratedHarnessTargetEvaluation EvaluateTarget(GeneratorAttributeSyntaxContext context)
+ {
+ if (context.TargetNode is not ClassDeclarationSyntax classDeclaration ||
+ !classDeclaration.Modifiers.Any(static modifier => modifier.IsKind(SyntaxKind.PartialKeyword)))
+ {
+ return GeneratedHarnessTargetEvaluation.Empty;
+ }
+
+ var targetType = (INamedTypeSymbol)context.TargetSymbol;
+ var propertyNames = new global::System.Collections.Generic.HashSet(
+ targetType.GetMembers().OfType().Select(static property => property.Name));
+ if (targetType.Arity != 0 ||
+ propertyNames.Contains(ComponentConstructorParameterTypesPropertyName))
+ {
+ return GeneratedHarnessTargetEvaluation.Empty;
+ }
+
+ if (!TryCreateContainingTypeDeclarations(classDeclaration, targetType, out var containingTypeDeclarations, out var diagnostic))
+ {
+ return GeneratedHarnessTargetEvaluation.FromDiagnostic(diagnostic!);
+ }
+
+ var componentType = TryGetMockerTestBaseComponentType(targetType);
+ if (componentType is null)
+ {
+ return GeneratedHarnessTargetEvaluation.Empty;
+ }
+
+ var attribute = context.Attributes[0];
+ if (attribute.ConstructorArguments.Length == 0 ||
+ attribute.ConstructorArguments[0].Value is not ITypeSymbol attributeComponentType ||
+ !SymbolEqualityComparer.Default.Equals(attributeComponentType, componentType))
+ {
+ return GeneratedHarnessTargetEvaluation.Empty;
+ }
+
+ var explicitConstructorParameterTypes = GetExplicitConstructorParameterTypes(attribute);
+ if (!TryResolveConstructor(componentType, explicitConstructorParameterTypes, out var selectedConstructor))
+ {
+ return GeneratedHarnessTargetEvaluation.Empty;
+ }
+
+ return GeneratedHarnessTargetEvaluation.FromTarget(
+ new GeneratedHarnessTargetModel(
+ targetType.ContainingNamespace.IsGlobalNamespace
+ ? null
+ : targetType.ContainingNamespace.ToDisplayString(),
+ targetType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
+ CreateTypeDeclarationModel(classDeclaration),
+ containingTypeDeclarations,
+ componentType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
+ context.SemanticModel.Compilation.GetTypeByMetadataName(XUnitFactAttributeMetadataName) is not null,
+ GetGeneratedTestMethods(componentType),
+ !propertyNames.Contains(SetupMocksActionPropertyName),
+ !propertyNames.Contains(CreatedComponentActionPropertyName),
+ !propertyNames.Contains(ConfigureMockerPolicyPropertyName),
+ selectedConstructor!.Parameters
+ .Select(parameter => parameter.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))
+ .ToImmutableArray(),
+ selectedConstructor.Parameters
+ .Select(parameter => parameter.Name)
+ .ToImmutableArray()));
+ }
+
+ private static bool TryCreateContainingTypeDeclarations(
+ ClassDeclarationSyntax classDeclaration,
+ INamedTypeSymbol targetType,
+ out ImmutableArray containingTypeDeclarations,
+ out Diagnostic? diagnostic)
+ {
+ diagnostic = null;
+ var builder = ImmutableArray.CreateBuilder();
+
+ foreach (var containingType in classDeclaration.Ancestors().OfType().Reverse())
+ {
+ if (!containingType.Modifiers.Any(static modifier => modifier.IsKind(SyntaxKind.PartialKeyword)))
+ {
+ containingTypeDeclarations = default;
+ diagnostic = Diagnostic.Create(
+ UnsupportedNestedGeneratedTargetDiagnostic,
+ containingType.Identifier.GetLocation(),
+ targetType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
+ containingType.Identifier.Text);
+ return false;
+ }
+
+ builder.Add(CreateTypeDeclarationModel(containingType));
+ }
+
+ containingTypeDeclarations = builder.ToImmutable();
+ return true;
+ }
+
+ private static GeneratedTypeDeclarationModel CreateTypeDeclarationModel(TypeDeclarationSyntax declaration)
+ {
+ var modifiers = string.Join(" ", declaration.Modifiers.Select(static modifier => modifier.Text));
+ var nameWithTypeParameters = declaration.Identifier.Text + (declaration.TypeParameterList?.ToString() ?? string.Empty);
+ var declarationKeyword = GetDeclarationKeyword(declaration);
+ var headerText = string.IsNullOrWhiteSpace(modifiers)
+ ? declarationKeyword + " " + nameWithTypeParameters
+ : modifiers + " " + declarationKeyword + " " + nameWithTypeParameters;
+
+ return new GeneratedTypeDeclarationModel(
+ headerText,
+ declaration.ConstraintClauses.Select(static clause => clause.ToString()).ToImmutableArray());
+ }
+
+ private static string GetDeclarationKeyword(TypeDeclarationSyntax declaration)
+ {
+ return declaration switch
+ {
+ ClassDeclarationSyntax => "class",
+ StructDeclarationSyntax => "struct",
+ RecordDeclarationSyntax recordDeclaration when recordDeclaration.ClassOrStructKeyword.IsKind(SyntaxKind.StructKeyword) => "record struct",
+ RecordDeclarationSyntax recordDeclaration when recordDeclaration.ClassOrStructKeyword.IsKind(SyntaxKind.ClassKeyword) => "record class",
+ RecordDeclarationSyntax => "record",
+ _ => declaration.Keyword.Text,
+ };
+ }
+
+ private static INamedTypeSymbol? TryGetMockerTestBaseComponentType(INamedTypeSymbol targetType)
+ {
+ for (var current = targetType; current != null; current = current.BaseType)
+ {
+ if (current.IsGenericType &&
+ current.MetadataName == MockerTestBaseTypeName &&
+ string.Equals(current.ContainingNamespace.ToDisplayString(), MockerTestBaseNamespace, StringComparison.Ordinal))
+ {
+ return current.TypeArguments[0] as INamedTypeSymbol;
+ }
+ }
+
+ return null;
+ }
+
+ private static ImmutableArray GetExplicitConstructorParameterTypes(AttributeData attribute)
+ {
+ if (attribute.ConstructorArguments.Length < 2)
+ {
+ return default;
+ }
+
+ if (attribute.ConstructorArguments[1].Kind != TypedConstantKind.Array)
+ {
+ return default;
+ }
+
+ var builder = ImmutableArray.CreateBuilder();
+ foreach (var value in attribute.ConstructorArguments[1].Values)
+ {
+ if (value.Value is ITypeSymbol typeSymbol)
+ {
+ builder.Add(typeSymbol);
+ }
+ }
+
+ return builder.ToImmutable();
+ }
+
+ private static ImmutableArray GetGeneratedTestMethods(INamedTypeSymbol componentType)
+ {
+ var candidateMethods = componentType.GetMembers()
+ .OfType()
+ .Where(static method =>
+ method.MethodKind == MethodKind.Ordinary &&
+ method.DeclaredAccessibility == Accessibility.Public &&
+ !method.IsStatic &&
+ !method.IsImplicitlyDeclared &&
+ !IsObjectOverride(method))
+ .OrderBy(static method => method.Name, StringComparer.Ordinal)
+ .ThenBy(static method => method.Parameters.Length)
+ .ThenBy(static method => method.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), StringComparer.Ordinal);
+
+ var builder = ImmutableArray.CreateBuilder();
+ var ordinal = 1;
+ foreach (var method in candidateMethods)
+ {
+ builder.Add(CreateGeneratedTestMethodModel(method, ordinal));
+ ordinal++;
+ }
+
+ return builder.ToImmutable();
+ }
+
+ private static bool IsObjectOverride(IMethodSymbol method)
+ {
+ return method.IsOverride &&
+ method.OverriddenMethod?.ContainingType.SpecialType == SpecialType.System_Object;
+ }
+ private static string EscapeIdentifierIfKeyword(string identifier)
+ {
+ var keywordKind = SyntaxFacts.GetKeywordKind(identifier);
+ return keywordKind != SyntaxKind.None ? "@" + identifier : identifier;
+ }
+ private static GeneratedComponentTestMethodModel CreateGeneratedTestMethodModel(IMethodSymbol method, int ordinal)
+ {
+ var methodDisplayName = method.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
+ var escapedMethodName = EscapeIdentifierIfKeyword(method.Name);
+ var methodIdentifier = CreateGeneratedMethodIdentifier(method.Name);
+
+ if (method.IsGenericMethod)
+ {
+ return GeneratedComponentTestMethodModel.CreateDeferred(escapedMethodName, ordinal, methodIdentifier, methodDisplayName, "is generic.");
+ }
+
+ if (!TryCreateInvocationArguments(method, out var invocationArguments, out var deferredReasonSuffix))
+ {
+ return GeneratedComponentTestMethodModel.CreateDeferred(escapedMethodName, ordinal, methodIdentifier, methodDisplayName, deferredReasonSuffix);
+ }
+
+ if (method.ReturnsByRef || method.ReturnsByRefReadonly)
+ {
+ return GeneratedComponentTestMethodModel.CreateDeferred(escapedMethodName, ordinal, methodIdentifier, methodDisplayName, "returns by reference.");
+ }
+
+ if (TryGetUnsupportedReturnTypeReason(method.ReturnType, out var unsupportedReason))
+ {
+ return GeneratedComponentTestMethodModel.CreateDeferred(escapedMethodName, ordinal, methodIdentifier, methodDisplayName, unsupportedReason);
+ }
+
+ return GeneratedComponentTestMethodModel.CreateSupported(
+ escapedMethodName,
+ ordinal,
+ methodIdentifier,
+ invocationArguments,
+ IsAsyncReturnType(method.ReturnType),
+ ReturnsValue(method.ReturnType));
+ }
+
+ private static bool TryCreateInvocationArguments(IMethodSymbol method, out string invocationArguments, out string deferredReasonSuffix)
+ {
+ if (method.Parameters.Length == 0)
+ {
+ invocationArguments = string.Empty;
+ deferredReasonSuffix = string.Empty;
+ return true;
+ }
+
+ var argumentBuilder = ImmutableArray.CreateBuilder(method.Parameters.Length);
+ foreach (var parameter in method.Parameters)
+ {
+ if (parameter.RefKind != RefKind.None)
+ {
+ invocationArguments = string.Empty;
+ deferredReasonSuffix = "uses ref, in, or out parameters.";
+ return false;
+ }
+
+ if (!parameter.IsOptional)
+ {
+ invocationArguments = string.Empty;
+ deferredReasonSuffix = "requires non-optional parameters.";
+ return false;
+ }
+
+ if (!TryCreateDefaultArgumentExpression(parameter, out var argumentExpression))
+ {
+ invocationArguments = string.Empty;
+ deferredReasonSuffix = "has an unsupported optional-parameter default for '" + parameter.Name + "'.";
+ return false;
+ }
+
+ argumentBuilder.Add(argumentExpression);
+ }
+
+ invocationArguments = string.Join(", ", argumentBuilder);
+ deferredReasonSuffix = string.Empty;
+ return true;
+ }
+
+ private static bool TryCreateDefaultArgumentExpression(IParameterSymbol parameter, out string argumentExpression)
+ {
+ if (!parameter.HasExplicitDefaultValue)
+ {
+ argumentExpression = string.Empty;
+ return false;
+ }
+
+ if (TryFormatDefaultValue(parameter.Type, parameter.ExplicitDefaultValue, out var rawArgumentExpression))
+ {
+ argumentExpression = CreateTypedArgumentExpression(parameter.Type, rawArgumentExpression);
+ return true;
+ }
+
+ argumentExpression = string.Empty;
+ return false;
+ }
+
+ private static string CreateTypedArgumentExpression(ITypeSymbol parameterType, string rawArgumentExpression)
+ {
+ return "(" + parameterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + ")" + rawArgumentExpression;
+ }
+
+ private static bool TryFormatDefaultValue(ITypeSymbol parameterType, object? explicitDefaultValue, out string expression)
+ {
+ if (explicitDefaultValue is null)
+ {
+ if (parameterType.IsReferenceType || IsNullableValueType(parameterType))
+ {
+ expression = "null";
+ return true;
+ }
+
+ expression = string.Empty;
+ return false;
+ }
+
+ if (parameterType.TypeKind == TypeKind.Enum &&
+ TryFormatEnumDefaultValue(parameterType, explicitDefaultValue, out expression))
+ {
+ return true;
+ }
+
+ switch (explicitDefaultValue)
+ {
+ case bool booleanValue:
+ expression = booleanValue ? "true" : "false";
+ return true;
+ case char charValue:
+ expression = SymbolDisplay.FormatLiteral(charValue, quote: true);
+ return true;
+ case string stringValue:
+ expression = SymbolDisplay.FormatLiteral(stringValue, quote: true);
+ return true;
+ case sbyte sbyteValue:
+ expression = sbyteValue.ToString(CultureInfo.InvariantCulture);
+ return true;
+ case byte byteValue:
+ expression = byteValue.ToString(CultureInfo.InvariantCulture);
+ return true;
+ case short shortValue:
+ expression = shortValue.ToString(CultureInfo.InvariantCulture);
+ return true;
+ case ushort ushortValue:
+ expression = ushortValue.ToString(CultureInfo.InvariantCulture);
+ return true;
+ case int intValue:
+ expression = intValue.ToString(CultureInfo.InvariantCulture);
+ return true;
+ case uint uintValue:
+ expression = uintValue.ToString(CultureInfo.InvariantCulture) + "U";
+ return true;
+ case long longValue:
+ expression = longValue.ToString(CultureInfo.InvariantCulture) + "L";
+ return true;
+ case ulong ulongValue:
+ expression = ulongValue.ToString(CultureInfo.InvariantCulture) + "UL";
+ return true;
+ case float floatValue:
+ expression = floatValue.ToString("R", CultureInfo.InvariantCulture) + "F";
+ return true;
+ case double doubleValue:
+ expression = doubleValue.ToString("R", CultureInfo.InvariantCulture);
+ return true;
+ case decimal decimalValue:
+ expression = decimalValue.ToString(CultureInfo.InvariantCulture) + "M";
+ return true;
+ default:
+ expression = string.Empty;
+ return false;
+ }
+ }
+
+ private static bool TryFormatEnumDefaultValue(ITypeSymbol parameterType, object explicitDefaultValue, out string expression)
+ {
+ if (parameterType is not INamedTypeSymbol namedEnumType)
+ {
+ expression = string.Empty;
+ return false;
+ }
+
+ foreach (var field in namedEnumType.GetMembers().OfType())
+ {
+ if (!field.HasConstantValue)
+ {
+ continue;
+ }
+
+ if (Equals(field.ConstantValue, explicitDefaultValue))
+ {
+ expression = namedEnumType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + "." + EscapeIdentifierIfKeyword(field.Name);
+ return true;
+ }
+ }
+
+ expression = string.Empty;
+ return false;
+ }
+
+ private static bool IsNullableValueType(ITypeSymbol typeSymbol)
+ {
+ return typeSymbol is INamedTypeSymbol namedType &&
+ namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T;
+ }
+
+ private static string CreateGeneratedMethodIdentifier(string methodName)
+ {
+ return new string(methodName.Select(static character =>
+ char.IsLetterOrDigit(character) || character == '_'
+ ? character
+ : '_').ToArray());
+ }
+
+ private static bool TryGetUnsupportedReturnTypeReason(ITypeSymbol returnType, out string reason)
+ {
+ if (returnType.TypeKind == TypeKind.Pointer || returnType.TypeKind == TypeKind.FunctionPointer)
+ {
+ reason = "has unsupported return type '" + returnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + "'.";
+ return true;
+ }
+
+ reason = string.Empty;
+ return false;
+ }
+
+ private static bool IsAsyncReturnType(ITypeSymbol returnType)
+ {
+ return IsNonGenericTaskReturnType(returnType) ||
+ IsGenericTaskReturnType(returnType) ||
+ IsNonGenericValueTaskReturnType(returnType) ||
+ IsGenericValueTaskReturnType(returnType);
+ }
+
+ private static bool ReturnsValue(ITypeSymbol returnType)
+ {
+ return returnType.SpecialType != SpecialType.System_Void &&
+ !IsNonGenericTaskReturnType(returnType) &&
+ !IsNonGenericValueTaskReturnType(returnType);
+ }
+
+ private static bool IsNonGenericTaskReturnType(ITypeSymbol returnType)
+ {
+ return returnType is INamedTypeSymbol namedType &&
+ string.Equals(namedType.ContainingNamespace.ToDisplayString(), ThreadingTasksNamespace, StringComparison.Ordinal) &&
+ string.Equals(namedType.MetadataName, TaskMetadataName, StringComparison.Ordinal);
+ }
+
+ private static bool IsGenericTaskReturnType(ITypeSymbol returnType)
+ {
+ return returnType is INamedTypeSymbol namedType &&
+ string.Equals(namedType.ContainingNamespace.ToDisplayString(), ThreadingTasksNamespace, StringComparison.Ordinal) &&
+ string.Equals(namedType.MetadataName, GenericTaskMetadataName, StringComparison.Ordinal);
+ }
+
+ private static bool IsNonGenericValueTaskReturnType(ITypeSymbol returnType)
+ {
+ return returnType is INamedTypeSymbol namedType &&
+ string.Equals(namedType.ContainingNamespace.ToDisplayString(), ThreadingTasksNamespace, StringComparison.Ordinal) &&
+ string.Equals(namedType.MetadataName, ValueTaskMetadataName, StringComparison.Ordinal);
+ }
+
+ private static bool IsGenericValueTaskReturnType(ITypeSymbol returnType)
+ {
+ return returnType is INamedTypeSymbol namedType &&
+ string.Equals(namedType.ContainingNamespace.ToDisplayString(), ThreadingTasksNamespace, StringComparison.Ordinal) &&
+ string.Equals(namedType.MetadataName, GenericValueTaskMetadataName, StringComparison.Ordinal);
+ }
+
+ private static bool TryResolveConstructor(
+ INamedTypeSymbol componentType,
+ ImmutableArray explicitConstructorParameterTypes,
+ out IMethodSymbol? selectedConstructor)
+ {
+ var instanceConstructors = componentType.InstanceConstructors
+ .Where(static constructor => !constructor.IsStatic)
+ .ToImmutableArray();
+
+ if (!explicitConstructorParameterTypes.IsDefault)
+ {
+ selectedConstructor = instanceConstructors.FirstOrDefault(constructor =>
+ ParametersMatch(constructor, explicitConstructorParameterTypes));
+ return selectedConstructor is not null;
+ }
+
+ var publicConstructors = instanceConstructors
+ .Where(static constructor => constructor.DeclaredAccessibility == Accessibility.Public)
+ .ToImmutableArray();
+ if (publicConstructors.Length != 1)
+ {
+ selectedConstructor = null;
+ return false;
+ }
+
+ selectedConstructor = publicConstructors[0];
+ return true;
+ }
+
+ private static bool ParametersMatch(IMethodSymbol constructor, ImmutableArray explicitConstructorParameterTypes)
+ {
+ if (constructor.Parameters.Length != explicitConstructorParameterTypes.Length)
+ {
+ return false;
+ }
+
+ for (var index = 0; index < constructor.Parameters.Length; index++)
+ {
+ if (!SymbolEqualityComparer.Default.Equals(constructor.Parameters[index].Type, explicitConstructorParameterTypes[index]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static void EmitSource(SourceProductionContext context, GeneratedHarnessTargetModel target, string frameworkSetting)
+ {
+ var emitXUnitSmokeTests = target.EmitXUnitSmokeTests &&
+ !string.Equals(frameworkSetting, FrameworkSettingNone, StringComparison.OrdinalIgnoreCase);
+
+ var sourceBuilder = new StringBuilder();
+ sourceBuilder.AppendLine("// ");
+ sourceBuilder.AppendLine("#nullable enable");
+
+ if (!string.IsNullOrWhiteSpace(target.NamespaceName))
+ {
+ sourceBuilder.Append("namespace ")
+ .Append(target.NamespaceName)
+ .AppendLine();
+ sourceBuilder.AppendLine("{");
+ }
+
+ var currentTypeIndentLevel = 1;
+ foreach (var containingTypeDeclaration in target.ContainingTypeDeclarations)
+ {
+ AppendTypeDeclaration(sourceBuilder, currentTypeIndentLevel, containingTypeDeclaration);
+ currentTypeIndentLevel++;
+ }
+
+ AppendTypeDeclaration(sourceBuilder, currentTypeIndentLevel, target.TargetTypeDeclaration);
+
+ var memberIndentLevel = currentTypeIndentLevel + 1;
+ var blockIndentLevel = memberIndentLevel + 1;
+ var nestedBlockIndentLevel = blockIndentLevel + 1;
+
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "protected override global::System.Type?[]? ComponentConstructorParameterTypes =>");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "FastMoqGeneratedHarnessMetadata.ConstructorParameterTypes;");
+ sourceBuilder.AppendLine();
+ if (target.EmitConfigureMockerPolicyOverride)
+ {
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "protected override global::System.Action? ConfigureMockerPolicy =>");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "options =>");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "{");
+ AppendIndentedLine(sourceBuilder, nestedBlockIndentLevel, "base.ConfigureMockerPolicy?.Invoke(options);");
+ AppendIndentedLine(sourceBuilder, nestedBlockIndentLevel, "ConfigureGeneratedMockerPolicy(options);");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "};");
+ sourceBuilder.AppendLine();
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "partial void ConfigureGeneratedMockerPolicy(global::FastMoq.MockerPolicyOptions options);");
+ sourceBuilder.AppendLine();
+ }
+
+ if (target.EmitSetupMocksActionOverride)
+ {
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "protected override global::System.Action? SetupMocksAction =>");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "mocker =>");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "{");
+ AppendIndentedLine(sourceBuilder, nestedBlockIndentLevel, "base.SetupMocksAction?.Invoke(mocker);");
+ AppendIndentedLine(sourceBuilder, nestedBlockIndentLevel, "ConfigureGeneratedMocks(mocker);");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "};");
+ sourceBuilder.AppendLine();
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "partial void ConfigureGeneratedMocks(global::FastMoq.Mocker mocker);");
+ sourceBuilder.AppendLine();
+ }
+
+ if (target.EmitCreatedComponentActionOverride)
+ {
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, $"protected override global::System.Action<{target.ComponentTypeName}>? CreatedComponentAction =>");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "component =>");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "{");
+ AppendIndentedLine(sourceBuilder, nestedBlockIndentLevel, "base.CreatedComponentAction?.Invoke(component);");
+ AppendIndentedLine(sourceBuilder, nestedBlockIndentLevel, "AfterGeneratedComponentCreated(component);");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "};");
+ sourceBuilder.AppendLine();
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, $"partial void AfterGeneratedComponentCreated({target.ComponentTypeName} component);");
+ sourceBuilder.AppendLine();
+ }
+
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// ");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// Executes the generated arrange, act, assert, and verify scaffold synchronously.");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// ");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "public void ExecuteGeneratedScenarioScaffold() => CreateGeneratedScenarioScaffold().Execute();");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// ");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// Executes the generated arrange, act, assert, and verify scaffold asynchronously.");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// ");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// A task that completes when the generated scaffold finishes running.");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "public global::System.Threading.Tasks.Task ExecuteGeneratedScenarioScaffoldAsync() => CreateGeneratedScenarioScaffold().ExecuteAsync();");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// ");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// Executes the generated scaffold with an act phase that expects the specified exception type.");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// ");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// The exception type expected from the generated act phase.");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "public void ExecuteGeneratedExpectedExceptionScenarioScaffold() where TException : global::System.Exception => CreateGeneratedExpectedExceptionScenarioScaffold().Execute();");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// ");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// Executes the generated scaffold asynchronously with an act phase that expects the specified exception type.");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// ");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// The exception type expected from the generated act phase.");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// A task that completes when the generated scaffold finishes running.");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "public global::System.Threading.Tasks.Task ExecuteGeneratedExpectedExceptionScenarioScaffoldAsync() where TException : global::System.Exception => CreateGeneratedExpectedExceptionScenarioScaffold().ExecuteAsync();");
+ sourceBuilder.AppendLine();
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, $"private global::FastMoq.ScenarioBuilder<{target.ComponentTypeName}> CreateGeneratedScenarioScaffold()");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "{");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "var scenario = Scenario;");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "ArrangeGeneratedScenario(scenario);");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "ActGeneratedScenario(scenario);");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "AssertGeneratedScenario(scenario);");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "VerifyGeneratedScenario(scenario);");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "return scenario;");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "}");
+ sourceBuilder.AppendLine();
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, $"private global::FastMoq.ScenarioBuilder<{target.ComponentTypeName}> CreateGeneratedExpectedExceptionScenarioScaffold() where TException : global::System.Exception");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "{");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "var scenario = Scenario;");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "ArrangeGeneratedScenario(scenario);");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "ExpectedExceptionGeneratedScenario(scenario);");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "AssertGeneratedScenario(scenario);");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "VerifyGeneratedScenario(scenario);");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "return scenario;");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "}");
+ sourceBuilder.AppendLine();
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// ");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// Adds arrange steps to the generated scenario scaffold.");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// ");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// The scenario builder to configure.");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, $"partial void ArrangeGeneratedScenario(global::FastMoq.ScenarioBuilder<{target.ComponentTypeName}> scenario);");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// ");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// Adds act steps to the generated scenario scaffold.");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// ");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// The scenario builder to configure.");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, $"partial void ActGeneratedScenario(global::FastMoq.ScenarioBuilder<{target.ComponentTypeName}> scenario);");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// ");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// Adds an act step to the generated scaffold that expects the specified exception type.");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// ");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// The exception type expected from the generated act phase.");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// The scenario builder to configure.");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, $"partial void ExpectedExceptionGeneratedScenario(global::FastMoq.ScenarioBuilder<{target.ComponentTypeName}> scenario) where TException : global::System.Exception;");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// ");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// Adds assertion steps to the generated scenario scaffold.");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// ");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// The scenario builder to configure.");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, $"partial void AssertGeneratedScenario(global::FastMoq.ScenarioBuilder<{target.ComponentTypeName}> scenario);");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// ");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// Adds verification steps to the generated scenario scaffold.");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// ");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "/// The scenario builder to configure.");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, $"partial void VerifyGeneratedScenario(global::FastMoq.ScenarioBuilder<{target.ComponentTypeName}> scenario);");
+ sourceBuilder.AppendLine();
+ AppendGeneratedXUnitSmokeTests(sourceBuilder, target, emitXUnitSmokeTests, memberIndentLevel);
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "internal static class FastMoqGeneratedHarnessMetadata");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "{");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, $"internal static global::System.Type ComponentType {{ get; }} = typeof({target.ComponentTypeName});");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "internal static global::System.Type?[] ConstructorParameterTypes { get; } = new global::System.Type?[]");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "{");
+ foreach (var parameterTypeName in target.ConstructorParameterTypeNames)
+ {
+ AppendIndentedLine(sourceBuilder, nestedBlockIndentLevel, $"typeof({parameterTypeName}),");
+ }
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "};");
+ sourceBuilder.AppendLine();
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "internal static global::System.String[] DependencyNames { get; } = new global::System.String[]");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "{");
+ foreach (var parameterName in target.DependencyNames)
+ {
+ AppendIndentedLine(sourceBuilder, nestedBlockIndentLevel, SymbolDisplay.FormatLiteral(parameterName, quote: true) + ",");
+ }
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "};");
+ sourceBuilder.AppendLine();
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "internal static global::System.Type?[] DependencyTypes => ConstructorParameterTypes;");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "}");
+
+ for (var indentLevel = currentTypeIndentLevel; indentLevel >= 1; indentLevel--)
+ {
+ AppendIndentedLine(sourceBuilder, indentLevel, "}");
+ }
+
+ if (!string.IsNullOrWhiteSpace(target.NamespaceName))
+ {
+ sourceBuilder.AppendLine("}");
+ }
+
+ context.AddSource(GetHintName(target), sourceBuilder.ToString());
+ }
+
+ private static void AppendTypeDeclaration(StringBuilder builder, int indentLevel, GeneratedTypeDeclarationModel declaration)
+ {
+ AppendIndentedLine(builder, indentLevel, declaration.HeaderText);
+ foreach (var constraintClause in declaration.ConstraintClauses)
+ {
+ AppendIndentedLine(builder, indentLevel, constraintClause);
+ }
+
+ AppendIndentedLine(builder, indentLevel, "{");
+ }
+
+ private static void AppendIndentedLine(StringBuilder builder, int indentLevel, string text)
+ {
+ builder.Append(' ', indentLevel * 4)
+ .AppendLine(text);
+ }
+
+ private static void AppendGeneratedXUnitSmokeTests(StringBuilder sourceBuilder, GeneratedHarnessTargetModel target, bool emitXUnitSmokeTests, int memberIndentLevel)
+ {
+ if (!emitXUnitSmokeTests)
+ {
+ return;
+ }
+
+ var blockIndentLevel = memberIndentLevel + 1;
+
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "[global::Xunit.Fact]");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "public void FastMoqGeneratedSmokeTest_00_Component_ShouldCreateComponent()");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "{");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "_ = Component;");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "}");
+ sourceBuilder.AppendLine();
+
+ foreach (var generatedTestMethod in target.GeneratedTestMethods)
+ {
+ if (generatedTestMethod.IsDeferred)
+ {
+ var skipReasonLiteral = SymbolDisplay.FormatLiteral(generatedTestMethod.DeferredReason!, quote: true);
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "[global::Xunit.Fact(Skip = " + skipReasonLiteral + ")]");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "public void " + generatedTestMethod.GeneratedMethodName + "()");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "{");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "throw new global::System.NotSupportedException(" + skipReasonLiteral + ");");
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "}");
+ sourceBuilder.AppendLine();
+ continue;
+ }
+
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "[global::Xunit.Fact]");
+ if (generatedTestMethod.IsAsync)
+ {
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "public async global::System.Threading.Tasks.Task " + generatedTestMethod.GeneratedMethodName + "()");
+ }
+ else
+ {
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "public void " + generatedTestMethod.GeneratedMethodName + "()");
+ }
+
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "{");
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "var component = Component;");
+
+ if (generatedTestMethod.IsAsync)
+ {
+ if (generatedTestMethod.ReturnsValue)
+ {
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "_ = await component." + generatedTestMethod.ComponentMethodName + "(" + generatedTestMethod.InvocationArguments + ");");
+ }
+ else
+ {
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "await component." + generatedTestMethod.ComponentMethodName + "(" + generatedTestMethod.InvocationArguments + ");");
+ }
+ }
+ else if (generatedTestMethod.ReturnsValue)
+ {
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "_ = component." + generatedTestMethod.ComponentMethodName + "(" + generatedTestMethod.InvocationArguments + ");");
+ }
+ else
+ {
+ AppendIndentedLine(sourceBuilder, blockIndentLevel, "component." + generatedTestMethod.ComponentMethodName + "(" + generatedTestMethod.InvocationArguments + ");");
+ }
+
+ AppendIndentedLine(sourceBuilder, memberIndentLevel, "}");
+ sourceBuilder.AppendLine();
+ }
+ }
+
+ private static string GetHintName(GeneratedHarnessTargetModel target)
+ {
+ var sanitizedIdentifier = new string(target.TargetIdentityName.Select(static character =>
+ char.IsLetterOrDigit(character)
+ ? character
+ : '_').ToArray());
+ return sanitizedIdentifier + ".FastMoq.GeneratedHarness.g.cs";
+ }
+
+ private sealed class GeneratedHarnessTargetEvaluation
+ {
+ private GeneratedHarnessTargetEvaluation(GeneratedHarnessTargetModel? target, ImmutableArray diagnostics)
+ {
+ Target = target;
+ Diagnostics = diagnostics;
+ }
+
+ public static GeneratedHarnessTargetEvaluation Empty { get; } = new(null, ImmutableArray.Empty);
+
+ public static GeneratedHarnessTargetEvaluation FromTarget(GeneratedHarnessTargetModel target) =>
+ new(target, ImmutableArray.Empty);
+
+ public static GeneratedHarnessTargetEvaluation FromDiagnostic(Diagnostic diagnostic) =>
+ new(null, ImmutableArray.Create(diagnostic));
+
+ public GeneratedHarnessTargetModel? Target { get; }
+
+ public ImmutableArray Diagnostics { get; }
+ }
+
+ private sealed class GeneratedTypeDeclarationModel
+ {
+ public GeneratedTypeDeclarationModel(string headerText, ImmutableArray constraintClauses)
+ {
+ HeaderText = headerText;
+ ConstraintClauses = constraintClauses;
+ }
+
+ public string HeaderText { get; }
+
+ public ImmutableArray ConstraintClauses { get; }
+ }
+
+ private sealed class GeneratedHarnessTargetModel
+ {
+ public GeneratedHarnessTargetModel(
+ string? namespaceName,
+ string targetIdentityName,
+ GeneratedTypeDeclarationModel targetTypeDeclaration,
+ ImmutableArray containingTypeDeclarations,
+ string componentTypeName,
+ bool emitXUnitSmokeTests,
+ ImmutableArray generatedTestMethods,
+ bool emitSetupMocksActionOverride,
+ bool emitCreatedComponentActionOverride,
+ bool emitConfigureMockerPolicyOverride,
+ ImmutableArray constructorParameterTypeNames,
+ ImmutableArray dependencyNames)
+ {
+ NamespaceName = namespaceName;
+ TargetIdentityName = targetIdentityName;
+ TargetTypeDeclaration = targetTypeDeclaration;
+ ContainingTypeDeclarations = containingTypeDeclarations;
+ ComponentTypeName = componentTypeName;
+ EmitXUnitSmokeTests = emitXUnitSmokeTests;
+ GeneratedTestMethods = generatedTestMethods;
+ EmitSetupMocksActionOverride = emitSetupMocksActionOverride;
+ EmitCreatedComponentActionOverride = emitCreatedComponentActionOverride;
+ EmitConfigureMockerPolicyOverride = emitConfigureMockerPolicyOverride;
+ ConstructorParameterTypeNames = constructorParameterTypeNames;
+ DependencyNames = dependencyNames;
+ }
+
+ public string? NamespaceName { get; }
+
+ public string TargetIdentityName { get; }
+
+ public GeneratedTypeDeclarationModel TargetTypeDeclaration { get; }
+
+ public ImmutableArray ContainingTypeDeclarations { get; }
+
+ public string ComponentTypeName { get; }
+
+ public bool EmitXUnitSmokeTests { get; }
+
+ public ImmutableArray GeneratedTestMethods { get; }
+
+ public bool EmitSetupMocksActionOverride { get; }
+
+ public bool EmitCreatedComponentActionOverride { get; }
+
+ public bool EmitConfigureMockerPolicyOverride { get; }
+
+ public ImmutableArray ConstructorParameterTypeNames { get; }
+
+ public ImmutableArray DependencyNames { get; }
+ }
+
+ private sealed class GeneratedComponentTestMethodModel
+ {
+ private GeneratedComponentTestMethodModel(
+ string componentMethodName,
+ string generatedMethodName,
+ string invocationArguments,
+ bool isDeferred,
+ string? deferredReason,
+ bool isAsync,
+ bool returnsValue)
+ {
+ ComponentMethodName = componentMethodName;
+ GeneratedMethodName = generatedMethodName;
+ InvocationArguments = invocationArguments;
+ IsDeferred = isDeferred;
+ DeferredReason = deferredReason;
+ IsAsync = isAsync;
+ ReturnsValue = returnsValue;
+ }
+
+ public string ComponentMethodName { get; }
+
+ public string GeneratedMethodName { get; }
+
+ public string InvocationArguments { get; }
+
+ public bool IsDeferred { get; }
+
+ public string? DeferredReason { get; }
+
+ public bool IsAsync { get; }
+
+ public bool ReturnsValue { get; }
+
+ public static GeneratedComponentTestMethodModel CreateSupported(
+ string componentMethodName,
+ int ordinal,
+ string methodIdentifier,
+ string invocationArguments,
+ bool isAsync,
+ bool returnsValue)
+ {
+ return new GeneratedComponentTestMethodModel(
+ componentMethodName,
+ "FastMoqGeneratedSmokeTest_" + ordinal.ToString("D2") + "_" + methodIdentifier + "_ShouldExecuteWithoutThrowing",
+ invocationArguments,
+ isDeferred: false,
+ deferredReason: null,
+ isAsync: isAsync,
+ returnsValue: returnsValue);
+ }
+
+ public static GeneratedComponentTestMethodModel CreateDeferred(
+ string componentMethodName,
+ int ordinal,
+ string methodIdentifier,
+ string methodDisplayName,
+ string deferredReasonSuffix)
+ {
+ var deferredReason = "FastMoq generated smoke test deferred: method '" + methodDisplayName + "' " + deferredReasonSuffix;
+ return new GeneratedComponentTestMethodModel(
+ componentMethodName,
+ "FastMoqGeneratedPlaceholder_" + ordinal.ToString("D2") + "_" + methodIdentifier + "_IsDeferred",
+ string.Empty,
+ isDeferred: true,
+ deferredReason,
+ isAsync: false,
+ returnsValue: false);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/FastMoq.Tests.Web/GlobalUsings.cs b/FastMoq.Tests.Web/GlobalUsings.cs
index 10afe5ce..1e051f72 100644
--- a/FastMoq.Tests.Web/GlobalUsings.cs
+++ b/FastMoq.Tests.Web/GlobalUsings.cs
@@ -1,5 +1,7 @@
๏ปฟglobal using Bunit;
global using FastMoq.Web.Blazor;
-global using FluentAssertions;
+global using AwesomeAssertions;
+global using static AwesomeAssertions.AssertionExtensions;
+global using static AwesomeAssertions.EnumAssertionsExtensions;
global using Moq;
global using Xunit;
diff --git a/FastMoq.Tests/FastMoq.Tests.csproj b/FastMoq.Tests/FastMoq.Tests.csproj
index 14707a79..8344400e 100644
--- a/FastMoq.Tests/FastMoq.Tests.csproj
+++ b/FastMoq.Tests/FastMoq.Tests.csproj
@@ -11,9 +11,8 @@
+
-
-
all
diff --git a/FastMoq.Tests/GlobalUsings.cs b/FastMoq.Tests/GlobalUsings.cs
index 6cee727c..1e12edbb 100644
--- a/FastMoq.Tests/GlobalUsings.cs
+++ b/FastMoq.Tests/GlobalUsings.cs
@@ -1,3 +1,5 @@
-๏ปฟglobal using FluentAssertions;
+๏ปฟglobal using AwesomeAssertions;
+global using static AwesomeAssertions.AssertionExtensions;
+global using static AwesomeAssertions.EnumAssertionsExtensions;
global using Moq;
global using Xunit;
diff --git a/FastMoq.Tests/InstanceConstructionGraphTests.cs b/FastMoq.Tests/InstanceConstructionGraphTests.cs
new file mode 100644
index 00000000..79847b4d
--- /dev/null
+++ b/FastMoq.Tests/InstanceConstructionGraphTests.cs
@@ -0,0 +1,79 @@
+using FastMoq.Models;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.IO.Abstractions;
+using System.Linq;
+
+namespace FastMoq.Tests
+{
+ public sealed class InstanceConstructionGraphTests
+ {
+ [Fact]
+ public void CreateConstructionGraph_ShouldCreateRootAndOrderedDependencyNodes_FromConstructionPlan()
+ {
+ var mocker = new Mocker();
+
+ var graph = mocker.CreateConstructionGraph(new InstanceConstructionRequest(typeof(TargetWithGraphDependencies)));
+
+ graph.Request.RequestedType.Should().Be(typeof(TargetWithGraphDependencies));
+ graph.Root.Kind.Should().Be(InstanceConstructionGraphNodeKind.Root);
+ graph.Root.Plan.Should().NotBeNull();
+ graph.Root.Plan!.ResolvedType.Should().Be(typeof(TargetWithGraphDependencies));
+ graph.Nodes.Should().HaveCount(4);
+ graph.Edges.Should().HaveCount(3);
+ graph.Edges.Select(edge => edge.ParameterName).Should().Equal(["fileSystem", "dependency", "name"]);
+ graph.Edges.Select(edge => edge.ParameterPosition).Should().Equal([0, 1, 2]);
+
+ var dependencyNodes = graph.Nodes.Skip(1).ToArray();
+ dependencyNodes[0].Kind.Should().Be(InstanceConstructionGraphNodeKind.Dependency);
+ dependencyNodes[0].Parameter.Should().NotBeNull();
+ dependencyNodes[0].NodeType.Should().Be(typeof(IFileSystem));
+ dependencyNodes[0].Parameter!.Source.Should().Be(InstanceConstructionParameterSource.KnownType);
+ dependencyNodes[1].NodeType.Should().Be(typeof(IDependency));
+ dependencyNodes[1].Parameter!.Source.Should().Be(InstanceConstructionParameterSource.KeyedService);
+ dependencyNodes[1].Parameter!.ServiceKey.Should().Be("primary");
+ dependencyNodes[2].NodeType.Should().Be(typeof(string));
+ dependencyNodes[2].Parameter!.Source.Should().Be(InstanceConstructionParameterSource.OptionalDefault);
+ }
+
+ [Fact]
+ public void GetComponentConstructionGraph_ShouldMapHarnessHooksThroughGraphMetadata()
+ {
+ using var harness = new ConstructorTypesGraphHarness();
+
+ var graph = harness.GetComponentConstructionGraph();
+
+ graph.Request.ConstructorParameterTypes.Should().NotBeNull();
+ graph.Request.ConstructorParameterTypes!.Should().Equal([typeof(IFileSystem), typeof(string)]);
+ graph.Root.Plan.Should().NotBeNull();
+ graph.Root.Plan!.Parameters.Should().HaveCount(2);
+ graph.Edges.Select(edge => edge.ParameterName).Should().Equal(["fileSystem", "value"]);
+ graph.Nodes.Skip(1).Select(node => node.NodeType).Should().Equal([typeof(IFileSystem), typeof(string)]);
+ }
+
+ private sealed class ConstructorTypesGraphHarness : MockerTestBase
+ {
+ protected override Type?[]? ComponentConstructorParameterTypes => [typeof(IFileSystem), typeof(string)];
+ }
+
+ private sealed class ConstructorSelectionTarget
+ {
+ public ConstructorSelectionTarget()
+ {
+ }
+
+ public ConstructorSelectionTarget(IFileSystem fileSystem, string value)
+ {
+ }
+ }
+
+ public interface IDependency;
+
+ private sealed class TargetWithGraphDependencies
+ {
+ public TargetWithGraphDependencies(IFileSystem fileSystem, [FromKeyedServices("primary")] IDependency dependency, string name = "default")
+ {
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/FastMoq.Tests/InstanceConstructionPlanTests.cs b/FastMoq.Tests/InstanceConstructionPlanTests.cs
new file mode 100644
index 00000000..239007b9
--- /dev/null
+++ b/FastMoq.Tests/InstanceConstructionPlanTests.cs
@@ -0,0 +1,333 @@
+using FastMoq.Models;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.IO.Abstractions;
+using PublicInstanceConstructionRequest = FastMoq.Models.InstanceConstructionRequest;
+
+namespace FastMoq.Tests
+{
+ public sealed class InstanceConstructionPlanTests
+ {
+ [Fact]
+ public void CreateConstructionPlan_ShouldUseMappedConcreteType_WhenRequestedTypeIsRegisteredAbstraction()
+ {
+ var mocker = new Mocker();
+ mocker.AddType();
+
+ var plan = mocker.CreateConstructionPlan(new PublicInstanceConstructionRequest(typeof(IMappedService)));
+
+ plan.RequestedType.Should().Be(typeof(IMappedService));
+ plan.ResolvedType.Should().Be(typeof(MappedService));
+ plan.Parameters.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void CreateConstructionPlan_ShouldDescribeCustomRegistrationParameter_WhenFactoryRegistrationExists()
+ {
+ var mocker = new Mocker();
+ mocker.AddType(_ => new RegisteredDependency());
+
+ var plan = mocker.CreateConstructionPlan(new PublicInstanceConstructionRequest(typeof(TargetWithDependency)));
+
+ plan.Parameters.Should().HaveCount(1);
+ plan.Parameters[0].Source.Should().Be(InstanceConstructionParameterSource.CustomRegistration);
+ }
+
+ [Fact]
+ public void CreateConstructionPlan_ShouldDescribeKnownTypeParameter_WhenBuiltInKnownTypeApplies()
+ {
+ var mocker = new Mocker();
+
+ var plan = mocker.CreateConstructionPlan(new PublicInstanceConstructionRequest(typeof(TargetWithFileSystem)));
+
+ plan.Parameters.Should().HaveCount(1);
+ plan.Parameters[0].ParameterType.Should().Be(typeof(IFileSystem));
+ plan.Parameters[0].Source.Should().Be(InstanceConstructionParameterSource.KnownType);
+ }
+
+ [Fact]
+ public void CreateConstructionPlan_ShouldRejectKnownTypeRequests_WhenRuntimeUsesDirectKnownTypeResolution()
+ {
+ var mocker = new Mocker();
+
+ var action = () => mocker.CreateConstructionPlan(new PublicInstanceConstructionRequest(typeof(Uri)));
+
+ action.Should().Throw()
+ .WithMessage("*known-type path*");
+ }
+
+ [Fact]
+ public void CreateConstructionPlan_ShouldRejectAbstractTypeRequests_WhenResolvedTypeIsNotConstructible()
+ {
+ var mocker = new Mocker();
+
+ var action = () => mocker.CreateConstructionPlan(new PublicInstanceConstructionRequest(typeof(AbstractDependency)));
+
+ action.Should().Throw()
+ .WithMessage("*does not resolve to a concrete constructor path*");
+ }
+
+ [Fact]
+ public void CreateConstructionPlan_ShouldRejectOpenGenericRequests_WhenResolvedTypeIsNotConstructible()
+ {
+ var mocker = new Mocker();
+
+ var action = () => mocker.CreateConstructionPlan(new PublicInstanceConstructionRequest(typeof(GenericDependency<>)));
+
+ action.Should().Throw()
+ .WithMessage("*does not resolve to a concrete constructor path*");
+ }
+
+ [Fact]
+ public void CreateConstructionPlan_ShouldDescribeKeyedParameter_WhenFromKeyedServicesAttributeIsPresent()
+ {
+ var mocker = new Mocker();
+
+ var plan = mocker.CreateConstructionPlan(new PublicInstanceConstructionRequest(typeof(TargetWithKeyedDependency)));
+
+ plan.Parameters.Should().HaveCount(1);
+ plan.Parameters[0].Source.Should().Be(InstanceConstructionParameterSource.KeyedService);
+ plan.Parameters[0].ServiceKey.Should().Be("primary");
+ }
+
+ [Fact]
+ public void CreateConstructionPlan_ShouldDescribeOptionalDefault_WhenOptionalParametersUseDefaults()
+ {
+ var mocker = new Mocker();
+
+ var plan = mocker.CreateConstructionPlan(new PublicInstanceConstructionRequest(typeof(TargetWithOptionalDependency)));
+
+ plan.Parameters.Should().HaveCount(1);
+ plan.Parameters[0].IsOptional.Should().BeTrue();
+ plan.Parameters[0].Source.Should().Be(InstanceConstructionParameterSource.OptionalDefault);
+ }
+
+ [Fact]
+ public void CreateConstructionPlan_ShouldDescribeAutoMockAndTypeDefaultParameters_WhenNoHigherPriorityResolutionExists()
+ {
+ var mocker = new Mocker();
+
+ var plan = mocker.CreateConstructionPlan(new PublicInstanceConstructionRequest(typeof(TargetWithMixedDependencies)));
+
+ plan.Parameters.Should().HaveCount(2);
+ plan.Parameters[0].Source.Should().Be(InstanceConstructionParameterSource.AutoMock);
+ plan.Parameters[1].Source.Should().Be(InstanceConstructionParameterSource.TypeDefault);
+ }
+
+ [Fact]
+ public void CreateConstructionPlan_ShouldDescribeMappedTypeRegistrationParameter_AsCustomRegistration()
+ {
+ var mocker = new Mocker();
+ mocker.AddType();
+
+ var plan = mocker.CreateConstructionPlan(new PublicInstanceConstructionRequest(typeof(TargetWithDependency)));
+
+ plan.Parameters.Should().HaveCount(1);
+ plan.Parameters[0].ParameterType.Should().Be(typeof(IDependency));
+ plan.Parameters[0].Source.Should().Be(InstanceConstructionParameterSource.CustomRegistration);
+ }
+
+ [Fact]
+ public void CreateConstructionPlan_ShouldDescribeStoredConstructorArguments_AsCustomRegistration()
+ {
+ var mocker = new Mocker();
+ mocker.AddType(replace: false, args: 42);
+
+ var plan = mocker.CreateConstructionPlan(new PublicInstanceConstructionRequest(typeof(TargetWithConfiguredConcreteDependency)));
+
+ plan.Parameters.Should().HaveCount(1);
+ plan.Parameters[0].ParameterType.Should().Be(typeof(ConcreteDependencyWithValue));
+ plan.Parameters[0].Source.Should().Be(InstanceConstructionParameterSource.CustomRegistration);
+ }
+
+ [Fact]
+ public void CreateConstructionPlan_ShouldDescribeNonSealedConcreteDependencyAsAutoMock_WhenNoCustomRegistrationExists()
+ {
+ var mocker = new Mocker();
+
+ var plan = mocker.CreateConstructionPlan(new PublicInstanceConstructionRequest(typeof(TargetWithConcreteDependency)));
+
+ plan.Parameters.Should().HaveCount(1);
+ plan.Parameters[0].ParameterType.Should().Be(typeof(ConcreteDependency));
+ plan.Parameters[0].Source.Should().Be(InstanceConstructionParameterSource.AutoMock);
+ }
+
+ [Fact]
+ public void CreateConstructionPlan_ShouldDescribeSealedConcreteDependencyAsTypeDefault_WhenNoCustomRegistrationExists()
+ {
+ var mocker = new Mocker();
+
+ var plan = mocker.CreateConstructionPlan(new PublicInstanceConstructionRequest(typeof(TargetWithSealedConcreteDependency)));
+
+ plan.Parameters.Should().HaveCount(1);
+ plan.Parameters[0].ParameterType.Should().Be(typeof(SealedConcreteDependency));
+ plan.Parameters[0].Source.Should().Be(InstanceConstructionParameterSource.TypeDefault);
+ }
+
+ [Fact]
+ public void CreateConstructionPlan_ShouldFlagPreferredConstructorSelection_WhenAttributeIsPresent()
+ {
+ var mocker = new Mocker();
+
+ var plan = mocker.CreateConstructionPlan(new PublicInstanceConstructionRequest(typeof(TargetWithPreferredConstructor)));
+
+ plan.UsedPreferredConstructorAttribute.Should().BeTrue();
+ plan.Parameters.Should().HaveCount(1);
+ }
+
+ [Fact]
+ public void CreateConstructionPlan_ShouldFlagAmbiguityFallback_WhenConfiguredToPreferParameterlessConstructor()
+ {
+ var mocker = new Mocker();
+
+ var plan = mocker.CreateConstructionPlan(new PublicInstanceConstructionRequest(typeof(TargetWithAmbiguousConstructors))
+ {
+ ConstructorAmbiguityBehavior = ConstructorAmbiguityBehavior.PreferParameterlessConstructor,
+ });
+
+ plan.UsedAmbiguityFallback.Should().BeTrue();
+ plan.Parameters.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void CreateConstructionPlan_ShouldFlagNonPublicConstructor_WhenFallbackToNonPublicIsRequested()
+ {
+ var mocker = new Mocker();
+
+ var plan = mocker.CreateConstructionPlan(new PublicInstanceConstructionRequest(typeof(TargetWithPrivateConstructor))
+ {
+ PublicOnly = false,
+ });
+
+ plan.UsedNonPublicConstructor.Should().BeTrue();
+ plan.Parameters.Should().HaveCount(1);
+ }
+
+ private interface IMappedService;
+
+ private sealed class MappedService : IMappedService;
+
+ public interface IDependency;
+
+ private sealed class RegisteredDependency : IDependency;
+
+ private sealed class TargetWithDependency
+ {
+ public TargetWithDependency(IDependency dependency)
+ {
+ }
+ }
+
+ private sealed class TargetWithFileSystem
+ {
+ public TargetWithFileSystem(IFileSystem fileSystem)
+ {
+ }
+ }
+
+ private sealed class TargetWithKeyedDependency
+ {
+ public TargetWithKeyedDependency([FromKeyedServices("primary")] IDependency dependency)
+ {
+ }
+ }
+
+ private sealed class TargetWithOptionalDependency
+ {
+ public TargetWithOptionalDependency(string name = "default")
+ {
+ }
+ }
+
+ private sealed class TargetWithMixedDependencies
+ {
+ public TargetWithMixedDependencies(IDependency dependency, int retryCount)
+ {
+ }
+ }
+
+ private sealed class TargetWithConcreteDependency
+ {
+ public TargetWithConcreteDependency(ConcreteDependency dependency)
+ {
+ }
+ }
+
+ private sealed class TargetWithConfiguredConcreteDependency
+ {
+ public TargetWithConfiguredConcreteDependency(ConcreteDependencyWithValue dependency)
+ {
+ }
+ }
+
+ private sealed class TargetWithSealedConcreteDependency
+ {
+ public TargetWithSealedConcreteDependency(SealedConcreteDependency dependency)
+ {
+ }
+ }
+
+ public class ConcreteDependency
+ {
+ }
+
+ public abstract class AbstractDependency
+ {
+ protected AbstractDependency()
+ {
+ }
+ }
+
+ public class ConcreteDependencyWithValue
+ {
+ public ConcreteDependencyWithValue(int value)
+ {
+ }
+ }
+
+ public class GenericDependency
+ {
+ public GenericDependency(TValue value)
+ {
+ }
+ }
+
+ private sealed class SealedConcreteDependency
+ {
+ }
+
+ private sealed class TargetWithPreferredConstructor
+ {
+ public TargetWithPreferredConstructor()
+ {
+ }
+
+ [PreferredConstructor]
+ public TargetWithPreferredConstructor(IDependency dependency)
+ {
+ }
+ }
+
+ private sealed class TargetWithAmbiguousConstructors
+ {
+ public TargetWithAmbiguousConstructors()
+ {
+ }
+
+ public TargetWithAmbiguousConstructors(IDependency dependency)
+ {
+ }
+
+ public TargetWithAmbiguousConstructors(string name)
+ {
+ }
+ }
+
+ private sealed class TargetWithPrivateConstructor
+ {
+ private TargetWithPrivateConstructor(IDependency dependency)
+ {
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/FastMoq.Tests/InternalSampleServiceTests.cs b/FastMoq.Tests/InternalSampleServiceTests.cs
index 1dacac64..b885eaad 100644
--- a/FastMoq.Tests/InternalSampleServiceTests.cs
+++ b/FastMoq.Tests/InternalSampleServiceTests.cs
@@ -1,5 +1,4 @@
using System;
-using FluentAssertions;
using System.IO.Abstractions;
using Xunit;
diff --git a/FastMoq.Tests/MockerTestBaseConstructionPlanTests.cs b/FastMoq.Tests/MockerTestBaseConstructionPlanTests.cs
new file mode 100644
index 00000000..397007c6
--- /dev/null
+++ b/FastMoq.Tests/MockerTestBaseConstructionPlanTests.cs
@@ -0,0 +1,153 @@
+using FastMoq.Models;
+using System;
+using System.IO.Abstractions;
+
+namespace FastMoq.Tests
+{
+ public sealed class MockerTestBaseConstructionPlanTests
+ {
+ [Fact]
+ public void GetComponentConstructionPlan_ShouldUseComponentConstructorParameterTypesHook()
+ {
+ using var harness = new ConstructorTypesHarness();
+
+ var plan = harness.DescribeComponentConstruction();
+
+ plan.RequestedType.Should().Be(typeof(ConstructorSelectionTarget));
+ plan.ResolvedType.Should().Be(typeof(ConstructorSelectionTarget));
+ plan.Parameters.Should().HaveCount(2);
+ plan.Parameters[0].ParameterType.Should().Be(typeof(IFileSystem));
+ plan.Parameters[1].ParameterType.Should().Be(typeof(string));
+ }
+
+ [Fact]
+ public void GetComponentConstructionPlan_ShouldUseComponentCreationFlags()
+ {
+ using var harness = new NonPublicConstructorHarness();
+
+ var plan = harness.DescribeComponentConstruction();
+
+ plan.UsedNonPublicConstructor.Should().BeTrue();
+ plan.Parameters.Should().HaveCount(1);
+ plan.Parameters[0].Source.Should().Be(InstanceConstructionParameterSource.AutoMock);
+ }
+
+ [Fact]
+ public void GetComponentConstructionPlan_ShouldAllowExplicitRequestOverride_WhenCreateComponentActionDiverges()
+ {
+ using var harness = new CustomRequestHarness();
+
+ var plan = harness.DescribeComponentConstruction();
+
+ plan.Parameters.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void GetComponentHarnessBootstrapDescriptor_ShouldDescribeDefaultHookBootstrap()
+ {
+ using var harness = new ConstructorTypesHarness();
+
+ var descriptor = harness.DescribeComponentBootstrap();
+
+ descriptor.RequiresExplicitConstructionRequestOverride.Should().BeFalse();
+ descriptor.ComponentCreationFlags.Should().Be(InstanceCreationFlags.None);
+ descriptor.ComponentConstructorParameterTypes.Should().NotBeNull();
+ descriptor.ComponentConstructorParameterTypes!.Should().Equal([typeof(IFileSystem), typeof(string)]);
+ descriptor.Graph.Root.Plan.Should().NotBeNull();
+ descriptor.Graph.Root.Plan!.Parameters.Should().HaveCount(2);
+ }
+
+ [Fact]
+ public void GetComponentHarnessBootstrapDescriptor_ShouldPreserveComponentCreationFlags()
+ {
+ using var harness = new NonPublicConstructorHarness();
+
+ var descriptor = harness.DescribeComponentBootstrap();
+
+ descriptor.RequiresExplicitConstructionRequestOverride.Should().BeFalse();
+ descriptor.ComponentCreationFlags.Should().Be(InstanceCreationFlags.AllowNonPublicConstructorFallback);
+ descriptor.ComponentConstructorParameterTypes.Should().BeNull();
+ descriptor.Graph.Root.Plan.Should().NotBeNull();
+ descriptor.Graph.Root.Plan!.UsedNonPublicConstructor.Should().BeTrue();
+ }
+
+ [Fact]
+ public void GetComponentHarnessBootstrapDescriptor_ShouldRequireExplicitRequestOverride_WhenHooksDoNotMatchRequest()
+ {
+ using var harness = new CustomRequestHarness();
+
+ var descriptor = harness.DescribeComponentBootstrap();
+
+ descriptor.RequiresExplicitConstructionRequestOverride.Should().BeTrue();
+ descriptor.ComponentCreationFlags.Should().Be(InstanceCreationFlags.None);
+ descriptor.ComponentConstructorParameterTypes.Should().BeNull();
+ descriptor.Graph.Request.ConstructorParameterTypes.Should().NotBeNull();
+ descriptor.Graph.Request.ConstructorParameterTypes.Should().BeEmpty();
+ descriptor.Graph.Root.Plan.Should().NotBeNull();
+ descriptor.Graph.Root.Plan!.Parameters.Should().BeEmpty();
+ }
+
+ private sealed class ConstructorTypesHarness : MockerTestBase
+ {
+ protected override Type?[]? ComponentConstructorParameterTypes => [typeof(IFileSystem), typeof(string)];
+
+ public InstanceConstructionPlan DescribeComponentConstruction() => GetComponentConstructionPlan();
+
+ public ComponentHarnessBootstrapDescriptor DescribeComponentBootstrap() => GetComponentHarnessBootstrapDescriptor();
+ }
+
+ private sealed class NonPublicConstructorHarness : MockerTestBase
+ {
+ protected override InstanceCreationFlags ComponentCreationFlags => InstanceCreationFlags.AllowNonPublicConstructorFallback;
+
+ public InstanceConstructionPlan DescribeComponentConstruction() => GetComponentConstructionPlan();
+
+ public ComponentHarnessBootstrapDescriptor DescribeComponentBootstrap() => GetComponentHarnessBootstrapDescriptor();
+ }
+
+ private sealed class CustomRequestHarness : MockerTestBase
+ {
+ protected override Func CreateComponentAction => _ => new ManualConstructionTarget();
+
+ protected override InstanceConstructionRequest CreateComponentConstructionRequest() => new(typeof(ManualConstructionTarget))
+ {
+ ConstructorParameterTypes = [],
+ };
+
+ public InstanceConstructionPlan DescribeComponentConstruction() => GetComponentConstructionPlan();
+
+ public ComponentHarnessBootstrapDescriptor DescribeComponentBootstrap() => GetComponentHarnessBootstrapDescriptor();
+ }
+
+ private sealed class ConstructorSelectionTarget
+ {
+ public ConstructorSelectionTarget()
+ {
+ }
+
+ public ConstructorSelectionTarget(IFileSystem fileSystem, string value)
+ {
+ }
+ }
+
+ public interface IDependency;
+
+ private sealed class NonPublicConstructorTarget
+ {
+ private NonPublicConstructorTarget(IDependency dependency)
+ {
+ }
+ }
+
+ private sealed class ManualConstructionTarget
+ {
+ public ManualConstructionTarget()
+ {
+ }
+
+ public ManualConstructionTarget(IDependency dependency)
+ {
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/FastMoq.sln b/FastMoq.sln
index da762ad5..abd9ae9a 100644
--- a/FastMoq.sln
+++ b/FastMoq.sln
@@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastMoq.Core", "FastMoq.Cor
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastMoq.Abstractions", "FastMoq.Abstractions\FastMoq.Abstractions.csproj", "{B9131B12-63E3-4E14-B7D9-3CB808FB0C0F}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastMoq.Generators", "FastMoq.Generators\FastMoq.Generators.csproj", "{5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastMoq.Provider.Moq", "FastMoq.Provider.Moq\FastMoq.Provider.Moq.csproj", "{5EFD9F7E-E336-42B0-B45D-03C1AA0DFAE1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastMoq.Provider.NSubstitute", "FastMoq.Provider.NSubstitute\FastMoq.Provider.NSubstitute.csproj", "{A13D7FC4-E19C-41C7-8B52-898F983AA27A}"
@@ -121,6 +123,18 @@ Global
{B9131B12-63E3-4E14-B7D9-3CB808FB0C0F}.Release|x64.Build.0 = Release|Any CPU
{B9131B12-63E3-4E14-B7D9-3CB808FB0C0F}.Release|x86.ActiveCfg = Release|Any CPU
{B9131B12-63E3-4E14-B7D9-3CB808FB0C0F}.Release|x86.Build.0 = Release|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Debug|x64.Build.0 = Debug|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Debug|x86.Build.0 = Debug|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Release|x64.ActiveCfg = Release|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Release|x64.Build.0 = Release|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Release|x86.ActiveCfg = Release|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Release|x86.Build.0 = Release|Any CPU
{5EFD9F7E-E336-42B0-B45D-03C1AA0DFAE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5EFD9F7E-E336-42B0-B45D-03C1AA0DFAE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5EFD9F7E-E336-42B0-B45D-03C1AA0DFAE1}.Debug|x64.ActiveCfg = Debug|Any CPU
diff --git a/FastMoq/FastMoq.csproj b/FastMoq/FastMoq.csproj
index e8ed5f91..a24174dd 100644
--- a/FastMoq/FastMoq.csproj
+++ b/FastMoq/FastMoq.csproj
@@ -14,6 +14,8 @@
+
+
@@ -22,6 +24,7 @@
+
diff --git a/FastMoq/packages.lock.json b/FastMoq/packages.lock.json
index 2b662da3..b8f5ab6d 100644
--- a/FastMoq/packages.lock.json
+++ b/FastMoq/packages.lock.json
@@ -35,8 +35,8 @@
},
"Castle.Core": {
"type": "Transitive",
- "resolved": "5.1.1",
- "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==",
+ "resolved": "5.2.1",
+ "contentHash": "wHARzQA695jwwKreOzNsq54KiGqKP38tv8hi8e2FXDEC/sA6BtrX90tVPDkOfVu13PbEzr00TCV8coikl+D1Iw==",
"dependencies": {
"System.Diagnostics.EventLog": "6.0.0"
}
@@ -158,6 +158,18 @@
"resolved": "10.0.0",
"contentHash": "uMXReBNxJKabrdoxHn4LNDTlGQFGDPtntlVvqo/b0HaxJzeDAIMFXJJ64mufJCNRuQU/Pus8ngcTZErvnsh7vg=="
},
+ "Microsoft.Azure.DurableTask.Core": {
+ "type": "Transitive",
+ "resolved": "3.7.1",
+ "contentHash": "OGBnNs+bqrht4Rs9Uc6LB6GHTwBpoBOu0l5cWcQJwLmeGwRWZbnXGXR8t8tkZJt6ERK8KHt3XI8jXvao2gEOYg==",
+ "dependencies": {
+ "Castle.Core": "5.2.1",
+ "Microsoft.Extensions.Logging.Abstractions": "6.0.1",
+ "Newtonsoft.Json": "13.0.1",
+ "System.Reactive.Compatibility": "4.4.1",
+ "System.Reactive.Core": "4.4.1"
+ }
+ },
"Microsoft.Azure.Functions.Worker.Core": {
"type": "Transitive",
"resolved": "2.51.0",
@@ -190,53 +202,53 @@
},
"Microsoft.EntityFrameworkCore": {
"type": "Transitive",
- "resolved": "10.0.6",
- "contentHash": "eDy7bu3G+51FRC0cPtXTqUI9iAdDYl/XBQ5UguN8NFOA7QNmFvUEf36wA7PZ4ctsnxRN4t3dIvs2VKVE5H+EQQ==",
+ "resolved": "10.0.7",
+ "contentHash": "G6yclVO5/csPzzsymV0SemY2NDqE31CP5M3jprF5IuO9wJsh4aUOfYD8HCLuDmM1D1CfReegVic48O2r79d46Q==",
"dependencies": {
- "Microsoft.EntityFrameworkCore.Abstractions": "10.0.6",
- "Microsoft.EntityFrameworkCore.Analyzers": "10.0.6",
- "Microsoft.Extensions.Caching.Memory": "10.0.6",
- "Microsoft.Extensions.Logging": "10.0.6"
+ "Microsoft.EntityFrameworkCore.Abstractions": "10.0.7",
+ "Microsoft.EntityFrameworkCore.Analyzers": "10.0.7",
+ "Microsoft.Extensions.Caching.Memory": "10.0.7",
+ "Microsoft.Extensions.Logging": "10.0.7"
}
},
"Microsoft.EntityFrameworkCore.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.6",
- "contentHash": "4F+e6uxhVmyduu+Ve1INxek94adt4RAddWqykXNDnOOWQrJJ20izw/9qRpZdkLnIW9oj/3qnLWUtsv37U0xJCw=="
+ "resolved": "10.0.7",
+ "contentHash": "TuxExnfIS/bSq3z2CbH0LwZH1oyj9iHhSGneU4fpxl3ikjZGZdSae9gcfnImV1rufH8f/ab1NnHwyL2BLyeZOg=="
},
"Microsoft.EntityFrameworkCore.Analyzers": {
"type": "Transitive",
- "resolved": "10.0.6",
- "contentHash": "PIcmALdKzeSJNWmxsLDsS8XKFqiH5+9GzIM+qd3w1efYIwmO0w5304i37/SkfynctHZwkiiQjb2mkoIXU1CGZg=="
+ "resolved": "10.0.7",
+ "contentHash": "eZnMyiJzo249Ejg5CaFScvJS0u7neQfS9DXknAHTO6FHVMM99gO0byNXHGZmA/BOkZ13ngeVziQLHTMOtgescg=="
},
"Microsoft.EntityFrameworkCore.InMemory": {
"type": "Transitive",
- "resolved": "10.0.6",
- "contentHash": "yQQLR6s0NOBJvg/du/w/mJn9ESlQ0XkAQ0zJEPhtlS/Vsnay6LRSdh39Sxy9/SkpYLoNoI9c6FUyP+UIE+BWdg==",
+ "resolved": "10.0.7",
+ "contentHash": "RAns8RTPymM9GsIdY5Y8wWdpdMEXputTl24+95WhYzqF4HhT0i+jFmNgDxBTy386vU9ruoUC5gcPAiuJr8SZxg==",
"dependencies": {
- "Microsoft.EntityFrameworkCore": "10.0.6",
- "Microsoft.Extensions.Caching.Memory": "10.0.6",
- "Microsoft.Extensions.Logging": "10.0.6"
+ "Microsoft.EntityFrameworkCore": "10.0.7",
+ "Microsoft.Extensions.Caching.Memory": "10.0.7",
+ "Microsoft.Extensions.Logging": "10.0.7"
}
},
"Microsoft.Extensions.Caching.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.6",
- "contentHash": "Ilr690V+E1H116ncF00KIlvRloKXBdCExaNqcT9BvCcS5nFGR1pcTamSA2EI8pOXbNp0DHZm8K8h6Wl1hMSbIQ==",
+ "resolved": "10.0.7",
+ "contentHash": "pUDgQKEqNUFlerDIFRg7zzoDVRPEWIG7nR40h8Gzg8RXza4Ry0lWZ7u91bmwu3iUDCxw3Dv6TLHVFoAgY0gy7Q==",
"dependencies": {
- "Microsoft.Extensions.Primitives": "10.0.6"
+ "Microsoft.Extensions.Primitives": "10.0.7"
}
},
"Microsoft.Extensions.Caching.Memory": {
"type": "Transitive",
- "resolved": "10.0.6",
- "contentHash": "5godKXBBsObgl/dBQKgrFeHFd6vVVOMGK3TuKLPNlwJgabFKl5vISSHLw5hWUtd+zKcl/Llmw25dsGlySxXJJg==",
+ "resolved": "10.0.7",
+ "contentHash": "6eULH/sc97yfCEV31g7AgUzHc7dIm0DGBcofoE8GgBaXbdAPPhathN8rYcgi1TSiG1QucCdqKiVNaDEPAEXL5Q==",
"dependencies": {
- "Microsoft.Extensions.Caching.Abstractions": "10.0.6",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.6",
- "Microsoft.Extensions.Logging.Abstractions": "10.0.6",
- "Microsoft.Extensions.Options": "10.0.6",
- "Microsoft.Extensions.Primitives": "10.0.6"
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.7",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.7",
+ "Microsoft.Extensions.Options": "10.0.7",
+ "Microsoft.Extensions.Primitives": "10.0.7"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
@@ -310,8 +322,8 @@
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.6",
- "contentHash": "w+dX4SIr1X9yegX2yX2dU1XtP4JAUVNdvOG/Evn+H+ndn96YzfIPX52FALXChrRNWFR9l77FQyg1mB7WQo6iOA=="
+ "resolved": "10.0.7",
+ "contentHash": "Z6mfFEaFcwCfSboxJwOLfu7/31npCY9q70WUamHW/vRQhDvBKOT4Vf9YkZj5J6hLvJpb0oDEYfHunQZj0xxvKw=="
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
@@ -414,10 +426,10 @@
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.6",
- "contentHash": "1YgBO3wAy0dlpQyVTKWBSPND/t0yZHsvd3shGpbeEwH8JSb2hnFI2pNFrOOUi/stsp+T/dqwqmRIGh47ibo9bw==",
+ "resolved": "10.0.7",
+ "contentHash": "tIEcQ2gvERrH2KiCjdsVcHGhXt9lIsuDStfOIeZWr7/fP8IXhGiYfx0/80PNI7WPO2IYuFtlZLSlnTS8+/Mchw==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.6"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7"
}
},
"Microsoft.Extensions.Logging.Configuration": {
@@ -495,8 +507,8 @@
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
- "resolved": "10.0.6",
- "contentHash": "L8P21mqaG+CXvPheLndean/cHCOcItJqH8nx+0YQnK7wAiOR0G1IOC418ZSzTMD2D6Gmo0f2M5WR70XtpX2B8g=="
+ "resolved": "10.0.7",
+ "contentHash": "D5M0Jr551iTgwkZMN9rm0pSkgNLj5quUWQUmQPMZh7k/bnvZTnXRGfE2KuvXf1EEjt/ofD9yw9IumpgdP9QCnw=="
},
"Microsoft.Extensions.Validation": {
"type": "Transitive",
@@ -542,6 +554,11 @@
"Microsoft.JSInterop": "10.0.0"
}
},
+ "Newtonsoft.Json": {
+ "type": "Transitive",
+ "resolved": "13.0.1",
+ "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A=="
+ },
"System.ClientModel": {
"type": "Transitive",
"resolved": "1.8.0",
@@ -566,6 +583,63 @@
"resolved": "8.0.1",
"contentHash": "BVYuec3jV23EMRDeR7Dr1/qhx7369dZzJ9IWy2xylvb4YfXsrUxspWc4UWYid/tj4zZK58uGZqn2WQiaDMhmAg=="
},
+ "System.Reactive": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "iSTPeWR9HJhGoNV4WhVlvofuiTjpok1i4E3LPgMdbMqf3jKhFlT9HAlO32lb52NLppWC/4dZQFfUzTytvyXBmw=="
+ },
+ "System.Reactive.Compatibility": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "Ad2/TBOBV0/45pzccpbQj2vo3S85uipF/PfqkbQUnH0vOtBqrXd1eqWtky5YTXq/WIRU1HF62HFSOdXiNC+E4A==",
+ "dependencies": {
+ "System.Reactive.Core": "4.4.1",
+ "System.Reactive.Interfaces": "4.4.1",
+ "System.Reactive.Linq": "4.4.1",
+ "System.Reactive.PlatformServices": "4.4.1",
+ "System.Reactive.Providers": "4.4.1"
+ }
+ },
+ "System.Reactive.Core": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "YQHJOt8hZvwFelIIfs19t8Jhz5P6NS1ZZccbVUuE1LFyoFZjaUecdYIYykgXzpyj5Rl40XC3xkyuuhHRaAln2w==",
+ "dependencies": {
+ "System.Reactive": "4.4.1"
+ }
+ },
+ "System.Reactive.Interfaces": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "Pk++rL2lxe6yhBEzgc9eJWR57YxYxtAmcz9U0JdKyHEzTLqba3B7MhhYrpN2ToTPVRyJJzxMdPdLieIJvlsACg==",
+ "dependencies": {
+ "System.Reactive": "4.4.1"
+ }
+ },
+ "System.Reactive.Linq": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "wyOVuUyHmPV667REPwcZjFjOlRLX6qSGVcXMye0qUqBAWFB3bu1RO1XLGWZTnf0d67DQHV69kw7tAtTh+4EYyQ==",
+ "dependencies": {
+ "System.Reactive": "4.4.1"
+ }
+ },
+ "System.Reactive.PlatformServices": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "jVF40bEwEES1DpDxI8pRcVEkH/TztFCOSToMYIK6TNE5QojsQljfvZd6jaDG4jRZ9hkoMYyUbkeSDYG/1ldPDg==",
+ "dependencies": {
+ "System.Reactive": "4.4.1"
+ }
+ },
+ "System.Reactive.Providers": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "K9RDgMHsuceX8QTk5ALCgYpexA9qcMsIi4K97Eigqcz9hD6fa4Smrjy8F/m6YdVJZFsBGaZ3uwj+d0loAdMfbA==",
+ "dependencies": {
+ "System.Reactive": "4.4.1"
+ }
+ },
"System.Security.Cryptography.ProtectedData": {
"type": "Transitive",
"resolved": "10.0.6",
@@ -607,7 +681,8 @@
"type": "Project",
"dependencies": {
"FastMoq.Core": "[1.0.0, )",
- "Microsoft.Azure.Functions.Worker": "[2.51.0, )"
+ "Microsoft.Azure.Functions.Worker": "[2.51.0, )",
+ "Microsoft.DurableTask.Abstractions": "[1.24.0, )"
}
},
"fastmoq.core": {
@@ -736,6 +811,18 @@
"Microsoft.Azure.Functions.Worker.Grpc": "2.51.0"
}
},
+ "Microsoft.DurableTask.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[1.24.0, )",
+ "resolved": "1.24.0",
+ "contentHash": "3I59dHtTxPodvuqyxx94jahv6ohZecXdpydeozRM7jTD4+hkjCKntxNNMxtZ+IdpXKrk7SLgz5rqrtNHxDDXlQ==",
+ "dependencies": {
+ "Microsoft.Azure.DurableTask.Core": "3.7.1",
+ "Microsoft.Bcl.AsyncInterfaces": "8.0.0",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.3"
+ }
+ },
"Microsoft.Extensions.Configuration": {
"type": "CentralTransitive",
"requested": "[10.0.6, )",
@@ -749,31 +836,31 @@
"Microsoft.Extensions.DependencyInjection": {
"type": "CentralTransitive",
"requested": "[10.0.6, )",
- "resolved": "10.0.6",
- "contentHash": "poUvwtf92bEs8uBH3aRRs/ZgiAw+Z485EU7TtVPBt//MmD0uMPERe7+v3Ur7lpD8XgIEDL9sDoTBcW1LMG97CQ==",
+ "resolved": "10.0.7",
+ "contentHash": "91F/o3emPV/+xY/ip3s2LqDNF14kjttlVtq0BXgg6p4MnCzeSZxnUJm+t6WRrtD3JdGo88/oX+z7OwK4y8PZuw==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.6"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7"
}
},
"Microsoft.Extensions.Logging": {
"type": "CentralTransitive",
"requested": "[10.0.6, )",
- "resolved": "10.0.6",
- "contentHash": "ZjpnbMD88IcZQE2pE9lcGv3mkH2mlApPWNh88ya1wJpcxZLp7p4aN7twI2FpawGPAsXNpmMgtKaz3o796YWKWQ==",
+ "resolved": "10.0.7",
+ "contentHash": "hOeRIQ63GkgiYCB/MIFp+LQs8aXpJXpB55t6Aj37ab7t2/6WeFcPXxYM9hdy/o5tffzwf8mhqzLJP6mjGYCxjw==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection": "10.0.6",
- "Microsoft.Extensions.Logging.Abstractions": "10.0.6",
- "Microsoft.Extensions.Options": "10.0.6"
+ "Microsoft.Extensions.DependencyInjection": "10.0.7",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.7",
+ "Microsoft.Extensions.Options": "10.0.7"
}
},
"Microsoft.Extensions.Options": {
"type": "CentralTransitive",
"requested": "[10.0.6, )",
- "resolved": "10.0.6",
- "contentHash": "v5RTWm+3Gdub21ADJeRG5bunOOxutFNBZk6qGH6Az4L5nyRZoLe3Kse7jfAyUcdEoiKp72XpNw/wGR+9wP+MtQ==",
+ "resolved": "10.0.7",
+ "contentHash": "00SHUGTh2jSMvIr6x9Xwd2nE+B5/qFCO/9hDwUDhJsjYRDlADmaBZ7tqehXzBDsfjHSXJzuRHJzPYPPjphBQ7Q==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.6",
- "Microsoft.Extensions.Primitives": "10.0.6"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
+ "Microsoft.Extensions.Primitives": "10.0.7"
}
},
"Moq": {
@@ -849,8 +936,8 @@
},
"Castle.Core": {
"type": "Transitive",
- "resolved": "5.1.1",
- "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==",
+ "resolved": "5.2.1",
+ "contentHash": "wHARzQA695jwwKreOzNsq54KiGqKP38tv8hi8e2FXDEC/sA6BtrX90tVPDkOfVu13PbEzr00TCV8coikl+D1Iw==",
"dependencies": {
"System.Diagnostics.EventLog": "6.0.0"
}
@@ -971,6 +1058,18 @@
"resolved": "8.0.22",
"contentHash": "Ha5M7eC//ZyBzJTc7CmUs0RJkqfBRXc38xzewR8VqZov8jURWuyaSv2XNiokjt7H77cZjQ7sLL0I/RD5JnQ/nA=="
},
+ "Microsoft.Azure.DurableTask.Core": {
+ "type": "Transitive",
+ "resolved": "3.7.1",
+ "contentHash": "OGBnNs+bqrht4Rs9Uc6LB6GHTwBpoBOu0l5cWcQJwLmeGwRWZbnXGXR8t8tkZJt6ERK8KHt3XI8jXvao2gEOYg==",
+ "dependencies": {
+ "Castle.Core": "5.2.1",
+ "Microsoft.Extensions.Logging.Abstractions": "6.0.1",
+ "Newtonsoft.Json": "13.0.1",
+ "System.Reactive.Compatibility": "4.4.1",
+ "System.Reactive.Core": "4.4.1"
+ }
+ },
"Microsoft.Azure.Functions.Worker.Core": {
"type": "Transitive",
"resolved": "2.2.0",
@@ -1343,6 +1442,11 @@
"Microsoft.JSInterop": "8.0.22"
}
},
+ "Newtonsoft.Json": {
+ "type": "Transitive",
+ "resolved": "13.0.1",
+ "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A=="
+ },
"System.ClientModel": {
"type": "Transitive",
"resolved": "1.8.0",
@@ -1372,6 +1476,63 @@
"resolved": "8.0.1",
"contentHash": "BVYuec3jV23EMRDeR7Dr1/qhx7369dZzJ9IWy2xylvb4YfXsrUxspWc4UWYid/tj4zZK58uGZqn2WQiaDMhmAg=="
},
+ "System.Reactive": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "iSTPeWR9HJhGoNV4WhVlvofuiTjpok1i4E3LPgMdbMqf3jKhFlT9HAlO32lb52NLppWC/4dZQFfUzTytvyXBmw=="
+ },
+ "System.Reactive.Compatibility": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "Ad2/TBOBV0/45pzccpbQj2vo3S85uipF/PfqkbQUnH0vOtBqrXd1eqWtky5YTXq/WIRU1HF62HFSOdXiNC+E4A==",
+ "dependencies": {
+ "System.Reactive.Core": "4.4.1",
+ "System.Reactive.Interfaces": "4.4.1",
+ "System.Reactive.Linq": "4.4.1",
+ "System.Reactive.PlatformServices": "4.4.1",
+ "System.Reactive.Providers": "4.4.1"
+ }
+ },
+ "System.Reactive.Core": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "YQHJOt8hZvwFelIIfs19t8Jhz5P6NS1ZZccbVUuE1LFyoFZjaUecdYIYykgXzpyj5Rl40XC3xkyuuhHRaAln2w==",
+ "dependencies": {
+ "System.Reactive": "4.4.1"
+ }
+ },
+ "System.Reactive.Interfaces": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "Pk++rL2lxe6yhBEzgc9eJWR57YxYxtAmcz9U0JdKyHEzTLqba3B7MhhYrpN2ToTPVRyJJzxMdPdLieIJvlsACg==",
+ "dependencies": {
+ "System.Reactive": "4.4.1"
+ }
+ },
+ "System.Reactive.Linq": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "wyOVuUyHmPV667REPwcZjFjOlRLX6qSGVcXMye0qUqBAWFB3bu1RO1XLGWZTnf0d67DQHV69kw7tAtTh+4EYyQ==",
+ "dependencies": {
+ "System.Reactive": "4.4.1"
+ }
+ },
+ "System.Reactive.PlatformServices": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "jVF40bEwEES1DpDxI8pRcVEkH/TztFCOSToMYIK6TNE5QojsQljfvZd6jaDG4jRZ9hkoMYyUbkeSDYG/1ldPDg==",
+ "dependencies": {
+ "System.Reactive": "4.4.1"
+ }
+ },
+ "System.Reactive.Providers": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "K9RDgMHsuceX8QTk5ALCgYpexA9qcMsIi4K97Eigqcz9hD6fa4Smrjy8F/m6YdVJZFsBGaZ3uwj+d0loAdMfbA==",
+ "dependencies": {
+ "System.Reactive": "4.4.1"
+ }
+ },
"System.Security.Cryptography.ProtectedData": {
"type": "Transitive",
"resolved": "8.0.0",
@@ -1413,7 +1574,8 @@
"type": "Project",
"dependencies": {
"FastMoq.Core": "[1.0.0, )",
- "Microsoft.Azure.Functions.Worker": "[2.2.0, )"
+ "Microsoft.Azure.Functions.Worker": "[2.2.0, )",
+ "Microsoft.DurableTask.Abstractions": "[1.24.0, )"
}
},
"fastmoq.core": {
@@ -1542,6 +1704,18 @@
"Microsoft.Azure.Functions.Worker.Grpc": "2.2.0"
}
},
+ "Microsoft.DurableTask.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[1.24.0, )",
+ "resolved": "1.24.0",
+ "contentHash": "3I59dHtTxPodvuqyxx94jahv6ohZecXdpydeozRM7jTD4+hkjCKntxNNMxtZ+IdpXKrk7SLgz5rqrtNHxDDXlQ==",
+ "dependencies": {
+ "Microsoft.Azure.DurableTask.Core": "3.7.1",
+ "Microsoft.Bcl.AsyncInterfaces": "8.0.0",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.3"
+ }
+ },
"Microsoft.Extensions.Configuration": {
"type": "CentralTransitive",
"requested": "[8.0.0, )",
@@ -1655,8 +1829,8 @@
},
"Castle.Core": {
"type": "Transitive",
- "resolved": "5.1.1",
- "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==",
+ "resolved": "5.2.1",
+ "contentHash": "wHARzQA695jwwKreOzNsq54KiGqKP38tv8hi8e2FXDEC/sA6BtrX90tVPDkOfVu13PbEzr00TCV8coikl+D1Iw==",
"dependencies": {
"System.Diagnostics.EventLog": "6.0.0"
}
@@ -1776,6 +1950,18 @@
"resolved": "9.0.11",
"contentHash": "O0HzG5utNH6ihO632k0nHFZa8iNDmGphdgWWqeDSdN/T9n0ZOXlA5+q77DxY3nHTjNfA0KMfpykIhEI+Wmzosg=="
},
+ "Microsoft.Azure.DurableTask.Core": {
+ "type": "Transitive",
+ "resolved": "3.7.1",
+ "contentHash": "OGBnNs+bqrht4Rs9Uc6LB6GHTwBpoBOu0l5cWcQJwLmeGwRWZbnXGXR8t8tkZJt6ERK8KHt3XI8jXvao2gEOYg==",
+ "dependencies": {
+ "Castle.Core": "5.2.1",
+ "Microsoft.Extensions.Logging.Abstractions": "6.0.1",
+ "Newtonsoft.Json": "13.0.1",
+ "System.Reactive.Compatibility": "4.4.1",
+ "System.Reactive.Core": "4.4.1"
+ }
+ },
"Microsoft.Azure.Functions.Worker.Core": {
"type": "Transitive",
"resolved": "2.2.0",
@@ -2150,6 +2336,11 @@
"Microsoft.JSInterop": "9.0.11"
}
},
+ "Newtonsoft.Json": {
+ "type": "Transitive",
+ "resolved": "13.0.1",
+ "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A=="
+ },
"System.ClientModel": {
"type": "Transitive",
"resolved": "1.8.0",
@@ -2174,6 +2365,63 @@
"resolved": "8.0.1",
"contentHash": "BVYuec3jV23EMRDeR7Dr1/qhx7369dZzJ9IWy2xylvb4YfXsrUxspWc4UWYid/tj4zZK58uGZqn2WQiaDMhmAg=="
},
+ "System.Reactive": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "iSTPeWR9HJhGoNV4WhVlvofuiTjpok1i4E3LPgMdbMqf3jKhFlT9HAlO32lb52NLppWC/4dZQFfUzTytvyXBmw=="
+ },
+ "System.Reactive.Compatibility": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "Ad2/TBOBV0/45pzccpbQj2vo3S85uipF/PfqkbQUnH0vOtBqrXd1eqWtky5YTXq/WIRU1HF62HFSOdXiNC+E4A==",
+ "dependencies": {
+ "System.Reactive.Core": "4.4.1",
+ "System.Reactive.Interfaces": "4.4.1",
+ "System.Reactive.Linq": "4.4.1",
+ "System.Reactive.PlatformServices": "4.4.1",
+ "System.Reactive.Providers": "4.4.1"
+ }
+ },
+ "System.Reactive.Core": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "YQHJOt8hZvwFelIIfs19t8Jhz5P6NS1ZZccbVUuE1LFyoFZjaUecdYIYykgXzpyj5Rl40XC3xkyuuhHRaAln2w==",
+ "dependencies": {
+ "System.Reactive": "4.4.1"
+ }
+ },
+ "System.Reactive.Interfaces": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "Pk++rL2lxe6yhBEzgc9eJWR57YxYxtAmcz9U0JdKyHEzTLqba3B7MhhYrpN2ToTPVRyJJzxMdPdLieIJvlsACg==",
+ "dependencies": {
+ "System.Reactive": "4.4.1"
+ }
+ },
+ "System.Reactive.Linq": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "wyOVuUyHmPV667REPwcZjFjOlRLX6qSGVcXMye0qUqBAWFB3bu1RO1XLGWZTnf0d67DQHV69kw7tAtTh+4EYyQ==",
+ "dependencies": {
+ "System.Reactive": "4.4.1"
+ }
+ },
+ "System.Reactive.PlatformServices": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "jVF40bEwEES1DpDxI8pRcVEkH/TztFCOSToMYIK6TNE5QojsQljfvZd6jaDG4jRZ9hkoMYyUbkeSDYG/1ldPDg==",
+ "dependencies": {
+ "System.Reactive": "4.4.1"
+ }
+ },
+ "System.Reactive.Providers": {
+ "type": "Transitive",
+ "resolved": "4.4.1",
+ "contentHash": "K9RDgMHsuceX8QTk5ALCgYpexA9qcMsIi4K97Eigqcz9hD6fa4Smrjy8F/m6YdVJZFsBGaZ3uwj+d0loAdMfbA==",
+ "dependencies": {
+ "System.Reactive": "4.4.1"
+ }
+ },
"System.Security.Cryptography.ProtectedData": {
"type": "Transitive",
"resolved": "9.0.14",
@@ -2215,7 +2463,8 @@
"type": "Project",
"dependencies": {
"FastMoq.Core": "[1.0.0, )",
- "Microsoft.Azure.Functions.Worker": "[2.2.0, )"
+ "Microsoft.Azure.Functions.Worker": "[2.2.0, )",
+ "Microsoft.DurableTask.Abstractions": "[1.24.0, )"
}
},
"fastmoq.core": {
@@ -2344,6 +2593,18 @@
"Microsoft.Azure.Functions.Worker.Grpc": "2.2.0"
}
},
+ "Microsoft.DurableTask.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[1.24.0, )",
+ "resolved": "1.24.0",
+ "contentHash": "3I59dHtTxPodvuqyxx94jahv6ohZecXdpydeozRM7jTD4+hkjCKntxNNMxtZ+IdpXKrk7SLgz5rqrtNHxDDXlQ==",
+ "dependencies": {
+ "Microsoft.Azure.DurableTask.Core": "3.7.1",
+ "Microsoft.Bcl.AsyncInterfaces": "8.0.0",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.3"
+ }
+ },
"Microsoft.Extensions.Configuration": {
"type": "CentralTransitive",
"requested": "[9.0.14, )",
diff --git a/docs/benchmarks/README.md b/docs/benchmarks/README.md
index e1b23b9a..cbd16a4e 100644
--- a/docs/benchmarks/README.md
+++ b/docs/benchmarks/README.md
@@ -4,6 +4,8 @@ This repository includes a runnable BenchmarkDotNet suite in `FastMoq.Benchmarks
The published comparison in this folder focuses on a narrow question: once a test service graph already exists, how close is FastMoq to direct Moq during repeated business execution?
+The current branch also records a second narrow generator-facing question: for a richer single-constructor target, how does source-generated graph/bootstrap projection compare to the normal runtime fallback path?
+
## Run the published comparison
Published workflow comparison:
@@ -14,9 +16,17 @@ dotnet run -c Release --project .\FastMoq.Benchmarks\FastMoq.Benchmarks.csproj -
BenchmarkDotNet writes local artifacts to `BenchmarkDotNet.Artifacts/results/`.
+Generated harness comparison:
+
+```powershell
+dotnet run -c Release --project .\FastMoq.Benchmarks\FastMoq.Benchmarks.csproj -- -j short --filter "*GeneratedHarnessSetupBenchmarks*"
+```
+
## Latest checked-in results
-The latest checked-in results are in [results/latest-results-net8.md](./results/latest-results-net8.md).
+The latest checked-in invocation-only results are in [results/latest-results-net8.md](./results/latest-results-net8.md).
+
+The latest checked-in generated-harness setup results are in [results/generated-harness-setup-net8.md](./results/generated-harness-setup-net8.md).
The latest checked-in workflow comparison shows:
@@ -24,8 +34,10 @@ The latest checked-in workflow comparison shows:
| --- | --- |
| Simple invocation-only workflow | FastMoq and direct Moq are effectively tied across `10`, `50`, and `100` repeated invocations, with identical allocations throughout. |
| Complex invocation-only workflow | FastMoq and direct Moq are effectively tied across the measured invocation counts, again with identical allocations. |
+| Generated graph/bootstrap projection | On the richer single-constructor benchmark used for `#122`, the generated harness path holds a slight edge over the runtime fallback path with effectively identical allocations. |
## What the published results show
- Once setup is removed from the measurement, FastMoq and direct Moq are effectively tied in these workflow comparisons.
- The published comparison stays centered on invocation-only workflow execution.
+- The generated harness comparison stays centered on graph/bootstrap planning for the first explicit `MockerTestBase` generator slice rather than full generated tests.
diff --git a/docs/benchmarks/results/generated-harness-setup-net8.md b/docs/benchmarks/results/generated-harness-setup-net8.md
new file mode 100644
index 00000000..91bc179c
--- /dev/null
+++ b/docs/benchmarks/results/generated-harness-setup-net8.md
@@ -0,0 +1,35 @@
+# Generated Harness Setup Results
+
+These results were generated from the current branch on 2026-05-06 with the published generator-facing comparison below.
+
+```powershell
+dotnet run -c Release --project .\FastMoq.Benchmarks\FastMoq.Benchmarks.csproj -- -j short --filter "*GeneratedHarnessSetupBenchmarks*"
+```
+
+Environment:
+
+- BenchmarkDotNet v0.13.12
+- Windows 11 (10.0.26200.8246)
+- 13th Gen Intel Core i9-13900K
+- .NET SDK 10.0.300-preview.0.26177.108
+- Host runtime .NET 8.0.26
+- Job selected by the command above
+
+These numbers are local branch measurements, not a guarantee of the same timings on another machine.
+
+## Why this comparison exists
+
+This document records the first generator-facing setup-path measurement for `#122`: a richer single-public-constructor target where the benchmark projects the harness bootstrap descriptor and construction graph through either the normal runtime fallback path or the source-generated constructor-metadata path.
+
+## Generated harness bootstrap projection
+
+| Method | Mean | Error | StdDev | Ratio | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio |
+| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
+| GeneratedHarnessBootstrapDescriptor | 232.7 us | 47.78 us | 2.62 us | 0.98 | 1 | 20.0195 | 2.4414 | 375.91 KB | 0.99 |
+| RuntimeFallbackBootstrapDescriptor | 238.1 us | 81.33 us | 4.46 us | 1.00 | 2 | 20.5078 | 2.9297 | 380.57 KB | 1.00 |
+
+## Interpretation
+
+- On this richer graph/bootstrap comparison, the generated harness path still holds a slight edge over the runtime fallback path.
+- Allocations remain close, with the generated harness path now measuring slightly lower than the runtime fallback path.
+- The short-run confidence interval is intentionally lightweight, so treat this as recorded branch evidence for the first MVP slice rather than a blanket promise for every component shape.
diff --git a/docs/roadmap/README.md b/docs/roadmap/README.md
index a498ee00..19103642 100644
--- a/docs/roadmap/README.md
+++ b/docs/roadmap/README.md
@@ -118,44 +118,24 @@ Current public issue anchor in this bucket is [#141](https://github.com/cwinland
### Code generation and scaffolding
-First-party code generators do not exist today, but code-generation work is part of the current v5 direction.
+Code generation remains part of the current v5 direction.
The main value is not "generated mocks" in isolation. The stronger FastMoq-specific opportunity is compile-time provider-first test generation: generated test graphs, harness scaffolding, and framework-helper builders that reduce reflection, reduce boilerplate, and stay aligned with FastMoq-owned APIs.
-Current public issue crosswalk:
-
-- foundation and package-shape work: [#120](https://github.com/cwinland/FastMoq/issues/120), [#121](https://github.com/cwinland/FastMoq/issues/121), [#125](https://github.com/cwinland/FastMoq/issues/125), [#126](https://github.com/cwinland/FastMoq/issues/126), [#127](https://github.com/cwinland/FastMoq/issues/127), and [#134](https://github.com/cwinland/FastMoq/issues/134)
-- near-term helper preparation that can improve v4 authoring before generators ship: [#132](https://github.com/cwinland/FastMoq/issues/132), [#133](https://github.com/cwinland/FastMoq/issues/133), and [#135](https://github.com/cwinland/FastMoq/issues/135)
-- first implementation-facing outcomes, in planned order: [#122](https://github.com/cwinland/FastMoq/issues/122), [#136](https://github.com/cwinland/FastMoq/issues/136), [#123](https://github.com/cwinland/FastMoq/issues/123), [#137](https://github.com/cwinland/FastMoq/issues/137), and [#124](https://github.com/cwinland/FastMoq/issues/124)
-- later evaluation tracks after the provider-first generator story is stable: [#138](https://github.com/cwinland/FastMoq/issues/138) and [#139](https://github.com/cwinland/FastMoq/issues/139)
-
-Planned direction stays phased:
+Current direction stays phased:
1. Compile-time test graph and harness generation.
-2. Scenario and suite scaffolding.
-3. Full generated tests from existing supported classes.
-4. Framework-helper builders for repeated helper-heavy test patterns.
-5. Analyzer-guided generation flow and package suggestions.
-6. Provider-optimized or narrower generated-fake evaluation only after the shared contract is stable.
-
-Current package and MVP contract direction for [#120](https://github.com/cwinland/FastMoq/issues/120):
-
-- `FastMoq.Generators` is the dedicated source-generator package
-- `FastMoq.Analyzers` remains separate from the source-generator implementation
-- the aggregate `FastMoq` package should include the generator path once it ships, while `FastMoq.Core` stays the lighter provider-neutral runtime and does not include the source-generator implementation
-- generator capability is install-enabled but target-explicit, not blanket automatic for every eligible type in a project
-- unsupported, disabled, missing, or stale generated paths fall back to the supported FastMoq runtime path
-- the first implementation-facing MVP is generated graph metadata and harness bootstrap only; scenario scaffolding, full generated tests, helper builders, provider-optimized generation, and compile-time fake generation remain later work
-- broader project-level or suite-level generated-test preference settings are intentionally later than the first MVP
-
-Current constructor-contract direction for [#125](https://github.com/cwinland/FastMoq/issues/125):
-
-- [#121](https://github.com/cwinland/FastMoq/issues/121) remains the umbrella tracker, while [#125](https://github.com/cwinland/FastMoq/issues/125) is the active blocking contract before [#122](https://github.com/cwinland/FastMoq/issues/122)
-- the proposed public contract is `InstanceConstructionRequest`, `InstanceConstructionPlan`, `InstanceConstructionParameterPlan`, and `InstanceConstructionParameterSource`, with `Mocker.CreateConstructionPlan(InstanceConstructionRequest request)` as the preferred entry point
-- the proposed first-slice parameter-source enum members are `CustomRegistration`, `KnownType`, `KeyedService`, `AutoMock`, `ConstructedByMocker`, `OptionalDefault`, and `TypeDefault`
-- the new contract stays narrow while existing public diagnostics, models, creation APIs, and current public reflection-metadata resolution behavior remain part of the compatibility boundary
-
-For the current detailed direction, design constraints, and fuller generator issue mapping, see [Generator roadmap and design](./generator-roadmap.md).
+2. Shared generated-test settings and test-platform contracts.
+3. Stable scenario-scaffolding contracts and helper-boundary narrowing.
+4. Scenario and suite scaffolding.
+5. Broaden full generated tests and analyzer-guided generation beyond the current explicit-harness xUnit smoke-test slice.
+6. Framework-helper builders for repeated helper-heavy test patterns when they justify a separate layer.
+7. Provider-optimized or narrower generated-fake evaluation only after the shared contract is stable.
+
+For the current detailed direction, implementation status, scope boundaries, and fuller generator issue mapping, see [Generator roadmap and design](./generator-roadmap.md).
+For the shared generated-test settings contract behind [#162](https://github.com/cwinland/FastMoq/issues/162), see [Generated test settings design](./generated-test-settings.md).
+For the scenario-scaffolding contract behind [#126](https://github.com/cwinland/FastMoq/issues/126), see [Generated scenario scaffolding contract](./generated-scenario-scaffolding-contract.md).
+For the current helper-boundary contract behind [#134](https://github.com/cwinland/FastMoq/issues/134), see [Generated helper family matrix](./generated-helper-family-matrix.md).
### `MockOptional` retirement
diff --git a/docs/roadmap/generated-helper-family-matrix.md b/docs/roadmap/generated-helper-family-matrix.md
new file mode 100644
index 00000000..d6c00a3c
--- /dev/null
+++ b/docs/roadmap/generated-helper-family-matrix.md
@@ -0,0 +1,108 @@
+# Generated Helper Family Matrix
+
+This page captures the current helper-family narrowing contract for generator-facing FastMoq helper surfaces. It is the canonical repo-local design artifact for [#134](https://github.com/cwinland/FastMoq/issues/134), not a shipped feature guide.
+
+This page is intentionally docs-only. The repo does emit core scenario and suite scaffolding, but it does not yet emit helper-heavy or package-specific scaffold variants, and this page does not itself implement helper normalization.
+
+For the shared generated-test settings contract behind [#162](https://github.com/cwinland/FastMoq/issues/162), see [Generated test settings design](./generated-test-settings.md).
+For the scenario-scaffolding contract behind [#126](https://github.com/cwinland/FastMoq/issues/126), see [Generated scenario scaffolding contract](./generated-scenario-scaffolding-contract.md).
+
+## Purpose And Non-Goals
+
+This slice exists to answer one narrow question for later generated scenario or suite scaffolds: which current FastMoq helper-family entry points may the first scaffold implementation target safely, and under what package or provider conditions?
+
+The goals are:
+
+- classify the current helper-family matrix for the first consumer, [#136](https://github.com/cwinland/FastMoq/issues/136)
+- identify which helper paths are generator-stable as-is
+- identify which helper paths are usable only with explicit package or provider bounds
+- identify which helper paths stay deferred from the first generated scenario or suite scaffold path
+- split any truly missing normalization work into later follow-on issues instead of absorbing it into this docs pass
+
+This slice does not:
+
+- implement helper normalization in runtime code
+- redesign the settled `#126` scenario contract or the settled `#162` settings contract
+- reopen package-detection and target-shape logic already owned by [#127](https://github.com/cwinland/FastMoq/issues/127)
+- define helper-builder generation strategy for [#137](https://github.com/cwinland/FastMoq/issues/137)
+- define full generated-test behavior for [#123](https://github.com/cwinland/FastMoq/issues/123) or analyzer routing behavior for [#124](https://github.com/cwinland/FastMoq/issues/124)
+- widen into broader observability, tracing, or diagnostics architecture work that belongs elsewhere
+
+## How #136 Uses This Matrix
+
+The first generated scenario or suite scaffold implementation should use this matrix as a hard boundary.
+
+- the current supported `#136` scaffold slice stays inside core provider-neutral scenario and suite hooks and does not yet auto-emit helper-family-specific setup from this matrix
+- `#136` may target helper families classified as `generator-stable as-is` directly.
+- `#136` may target helper families classified as `generator-stable with explicit bounds` only when the documented package or provider conditions are satisfied.
+- `#136` should not target helper families classified as `deferred from first scenario scaffolds`.
+- If later generated output needs behavior outside the current stable helper entry points, that work should move to a later follow-on issue instead of quietly widening `#134`.
+
+## Classification Model
+
+Each helper family in this matrix is classified into one of four buckets.
+
+| Classification | Meaning for later generator consumers |
+| --- | --- |
+| `generator-stable as-is` | The current public helper surface is explicit enough for the first generated scenario or suite scaffolds to target directly. |
+| `generator-stable with explicit bounds` | The helper surface is usable for generation only under documented package, provider, or scenario-shape limits. |
+| `deferred from first scenario scaffolds` | The helper surface exists, but the first scaffold implementation should not rely on it. |
+| `requires follow-on implementation issue` | Later generated output may need this family, but the current public surface is not yet stable enough to target without new public-shape work. |
+
+## Helper-Family Matrix
+
+| Helper family | Primary entry points | Classification | First-slice guidance | Ownership notes |
+| --- | --- | --- | --- | --- |
+| Logging verification | `VerifyLogged(...)`, `VerifyLoggedOnce(...)`, `VerifyNotLogged(...)` | `generator-stable as-is` | Generated scaffolds may use the current provider-neutral logging verification helpers directly. | Reuses the shared verification line from `#133` and analyzer follow-up in `#147`. Broader observability remains outside this issue. |
+| Logger-factory composition | `CreateLoggerFactory(...)`, `AddLoggerFactory(...)`, `AddCapturedLoggerFactory(...)` | `generator-stable as-is` | Generated scaffolds may use the current capture-backed logger-factory helpers directly for logging setup. | Reuses existing logging capture and verification surfaces rather than introducing a second abstraction. |
+| Typed `IServiceProvider` and `IServiceScope` composition | `CreateTypedServiceProvider(...)`, `CreateTypedServiceScope(...)`, `AddTypedServiceProvider(...)`, `AddTypedServiceScope(...)` | `generator-stable as-is` | Generated scaffolds may rely on the typed provider and scope helpers when a real DI composition path is cleaner than direct registrations. | Reuses the narrow typed-DI quick-win line already stabilized before this slice. |
+| ASP.NET Core principal and `HttpContext` helpers | `SetupClaimsPrincipal(...)`, `CreateHttpContext(...)`, `AddHttpContext(...)`, `AddHttpContextAccessor(...)`, `CreateControllerContext(...)` | `generator-stable with explicit bounds` | Generated scaffolds may use these helpers when `FastMoq.Web` is referenced and the generated target actually needs web or controller context composition. | Keep this row scoped to web-context composition, not to broader UI interaction helpers. |
+| Azure configuration and typed provider convenience | `CreateAzureConfiguration(...)`, `AddAzureConfiguration(...)`, `CreateAzureServiceProvider(...)`, `AddAzureServiceProvider(...)` | `generator-stable with explicit bounds` | Generated scaffolds may use Azure configuration and typed-provider helpers when `FastMoq.Azure` is referenced. Prefer the Azure typed-provider convenience path over ad hoc lower-level composition where possible. | Package-aware targeting still belongs to `#127`. |
+| Azure client, storage, and pageable helpers | `AddAzureClient(...)`, storage client registration helpers, `CreatePageable(...)`, `CreateAsyncPageable(...)` | `generator-stable with explicit bounds` | Generated scaffolds may use the current stable client and pageable helpers for first-pass scenarios, but should not assume keyed or multi-client normalization beyond what the current public surface already expresses. | Later keyed or broader client-shaping needs should split to a later follow-on issue instead of widening this slice. |
+| Azure Functions HTTP and `FunctionContext` basics | `CreateHttpRequestData(...)`, `CreateHttpResponseData(...)`, `ReadBodyAsStringAsync(...)`, `ReadBodyAsJsonAsync(...)`, `AddFunctionContextInvocationId(...)`, `CreateFunctionContextInstanceServices()` | `generator-stable with explicit bounds` | Generated scaffolds may use these helpers for HTTP-trigger and basic worker-context scenarios when `FastMoq.AzureFunctions` is referenced. | Reuses the narrower landed Azure Functions helper work, including `#107`. |
+| Durable replay-safe logger creation via concrete orchestration context | `Mocker.AddTaskOrchestrationReplaySafeLogging(...)` before resolving `TaskOrchestrationContext` | `generator-stable with explicit bounds` | Generated scaffolds may use the concrete replay-safe logger helper path when the scenario only needs replay-safe logger creation and the helper is registered before resolution. | Keep the documented bounds explicit: logger creation is supported, broader orchestration activity behavior is not. |
+| Durable replay-safe logger creation via tracked orchestration mock | `IFastMock.AddTaskOrchestrationReplaySafeLogging(...)` | `deferred from first scenario scaffolds` | The first scaffold implementation should not rely on the tracked orchestration-mock path. It is provider-constrained and does not support replay-state suppression on the tracked path. | Later expansion should align with the provider-contract follow-up already noted around tracked-property configuration support. |
+| Blazor component interaction helpers | `MockerBlazorTestBase`, `IMockerBlazorTestHelpers` and related UI interaction helpers | `deferred from first scenario scaffolds` | The first helper matrix should not treat UI-component interaction helpers as required for the initial generated scenario or suite scaffold path. | Keep this for later web or UI expansion rather than mixing it into the first `#136` assumption set. |
+| Broader observability, tracing, and diagnostics architecture | Structured trace or diagnostics architecture beyond current logging capture and verification | `requires follow-on implementation issue` | Do not make the first generated scaffold depend on a broader observability stack than the current repo-owned logging capture and verification helpers already provide. | Broader structured diagnostics or tracing remains outside this issue. |
+
+## Package And Provider Constraints
+
+This matrix does not replace package-aware or provider-aware capability checks.
+
+- [#127](https://github.com/cwinland/FastMoq/issues/127) remains the authoritative package-detection and target-shape gate.
+- Package-specific helper families may only be emitted when the referenced FastMoq package set makes that helper family valid for the current project.
+- The Durable tracked-mock replay-safe logger path remains provider-constrained and should not be treated as provider-neutral scaffolding guidance.
+- The concrete replay-safe orchestration helper remains intentionally narrow: it supports logger creation and replay-state assertions, not full orchestration activity, timer, event, or sub-orchestrator behavior.
+
+## Dependency Boundaries
+
+`#134` exists to narrow helper-family assumptions, not to re-own adjacent work that already landed or already has a narrower issue.
+
+- [#132](https://github.com/cwinland/FastMoq/issues/132) owns the shared setup-helper expansion line for fixed returns, async returns, callbacks, and exception helpers.
+- [#133](https://github.com/cwinland/FastMoq/issues/133) owns the shared verification-helper expansion line, including the current logging verification story.
+- [#135](https://github.com/cwinland/FastMoq/issues/135) owns the earlier quick-win cleanup for logging, HTTP, and typed-DI entry points that this matrix now reuses rather than redesigns.
+- [#107](https://github.com/cwinland/FastMoq/issues/107) owns the landed Azure Functions InvocationId and replay-safe logger guidance that this matrix now classifies for generator-facing use.
+- [#145](https://github.com/cwinland/FastMoq/issues/145) or later observability work owns broader structured diagnostics or tracing concerns outside the current helper capture or verification surface.
+- [#137](https://github.com/cwinland/FastMoq/issues/137) remains later helper-builder generation and should not be designed inside this narrowing pass.
+
+## Split Criteria For Later Follow-On Work
+
+`#134` should stop and point to later follow-on work when a generator need exceeds the current stable helper surface.
+
+Open or route to a later issue when one of these becomes necessary:
+
+- a helper family needs a new public entry point instead of relying on the current repo-owned helper surface
+- the desired generated flow depends on keyed or multi-client semantics that the current helper API does not make explicit
+- the desired generated flow depends on provider-native or protected-member behaviors that are not part of the shared provider-first helper guidance
+- the desired generated flow depends on Azure Functions orchestration behavior beyond replay-safe logger creation
+- the desired generated flow depends on Blazor UI interaction helpers rather than simple web-context composition
+- the desired generated flow depends on broader structured observability or diagnostics architecture rather than the current logging capture helpers
+
+## Follow-On Boundaries
+
+The immediate follow-on order after this docs pass should remain explicit:
+
+- `#134` ends once the helper-family matrix, package or provider bounds, and split criteria are documented and mirrored into the roadmap.
+- the first `#136` implementation now emits generated scenario and suite scaffolding inside explicit partial harness targets while staying within the core contract and these matrix bounds.
+- `#123` and `#124` should consume the same helper assumptions later instead of inventing local defaults or re-triaging helper families inside their own slices.
+- `#137` remains later and conditional rather than part of the immediate `#126 -> #134 -> #136 -> #123/#124` chain.
diff --git a/docs/roadmap/generated-scenario-scaffolding-contract.md b/docs/roadmap/generated-scenario-scaffolding-contract.md
new file mode 100644
index 00000000..860257d0
--- /dev/null
+++ b/docs/roadmap/generated-scenario-scaffolding-contract.md
@@ -0,0 +1,156 @@
+# Generated Scenario Scaffolding Contract
+
+This page captures the current design contract for generator-stable scenario and suite scaffolding in FastMoq. It is the canonical repo-local design artifact for [#126](https://github.com/cwinland/FastMoq/issues/126), and the contract described here now has a narrow implementation in the repo.
+
+This page remains primarily a contract and API-design artifact, but the repo now emits a narrow first scenario/scaffold implementation for explicit partial `MockerTestBase` targets. The supported and deferred shapes below describe that current implementation boundary.
+
+For the shared generated-test settings and extensibility contract behind [#162](https://github.com/cwinland/FastMoq/issues/162), see [Generated test settings design](./generated-test-settings.md).
+
+## Current Supported Scope
+
+The current supported [#136](https://github.com/cwinland/FastMoq/issues/136) scaffold slice is narrow:
+
+- generated scenario entry points are emitted inside explicit partial `MockerTestBase` harness targets
+- generated suite scaffolding means shared setup composition inside those harnesses through `ConfigureGeneratedMockerPolicy`, `ConfigureGeneratedMocks`, and `AfterGeneratedComponentCreated`
+- generated sync, async, and continued-assertion expected-exception entry points build on the existing `ScenarioBuilder` pipeline
+- full generated test classes, standalone generated suite types, and consumer-configurable settings-driven scaffold shapes remain deferred
+
+## Purpose And Non-Goals
+
+This slice exists to define the stable runtime contract that later generated scenario scaffolds should target.
+
+The goals are:
+
+- map generated scenario phases to existing FastMoq-owned runtime APIs
+- define the first regeneration-safe customization seams for generated scenario output
+- keep generated scenario flow provider-first and centered on existing FastMoq surfaces
+- preserve the settled ownership boundaries from [#125](https://github.com/cwinland/FastMoq/issues/125), [#122](https://github.com/cwinland/FastMoq/issues/122), and [#162](https://github.com/cwinland/FastMoq/issues/162)
+
+This slice does not:
+
+- implement source generation for scenario or suite scaffolds
+- redesign `ScenarioBuilder` into a separate scenario runtime model
+- reopen constructor-selection, graph metadata, or harness bootstrap contracts already settled in `#125` and `#122`
+- settle the helper-family narrowing matrix for logging, HTTP, Azure, Azure Functions, or typed DI beyond the boundaries needed to keep the scenario contract narrow
+- replace the settings, hook-emission-style, or regeneration-policy ownership already documented in `#162`
+
+## Current Runtime Anchors
+
+The first scenario-scaffolding contract should target the existing public FastMoq runtime surface instead of inventing a second scenario abstraction.
+
+Primary anchors:
+
+- `ScenarioBuilder.With(...)` for arrange steps
+- `ScenarioBuilder.When(...)` for normal act steps
+- `ScenarioBuilder.WhenThrows(...)` for expected-exception act steps that still allow trailing assertions
+- `ScenarioBuilder.Then(...)` for assertion steps
+- `ScenarioBuilder.Verify(...)` and `VerifyNoOtherCalls()` for provider-neutral verification steps
+- `ScenarioBuilder.Execute()`, `ExecuteAsync()`, `ExecuteThrows()`, and `ExecuteThrowsAsync()` for execution semantics
+- `MockerTestBase.Scenario` as the default scenario builder entry point for generated test-base scaffolds
+- `MockerTestBase.SetupMocksAction`, `CreateComponentAction`, `CreatedComponentAction`, and `ConfigureMockerPolicy` as the surrounding component and mocker composition seams
+- `MockerTestBase.ComponentConstructorParameterTypes`, `CreateComponentConstructionRequest()`, `GetComponentConstructionPlan()`, and `GetComponentHarnessBootstrapDescriptor()` as the settled harness and construction metadata hooks beneath the scenario layer
+- `Mocks.VerifyLogged(...)`, `VerifyLoggedOnce(...)`, and `VerifyNotLogged(...)` as the existing provider-neutral logging verification surface when generated scaffolds need log assertions
+
+## First Supported Scaffold Shape
+
+The current supported scaffold shape stays narrow and method-centric:
+
+- generated scenario methods live inside generated partial `MockerTestBase`-based test types
+- generated suite scaffolding for [#136](https://github.com/cwinland/FastMoq/issues/136) means suite-level shared setup regions and post-creation hooks inside the same generated partial harness, not standalone scenario or suite container types
+- generated scenario methods build on the existing `Scenario` property or `Mocks.Scenario(Component)` flow instead of creating a new standalone scenario-class abstraction
+- current generated entry points cover sync, async, and continued-assertion expected-exception flows
+- generated scenario methods may consult `GetComponentConstructionPlan()` or `GetComponentHarnessBootstrapDescriptor()` when constructor or bootstrap metadata needs to shape the scaffold, but the scenario contract itself stays above the graph and harness layer
+
+This keeps the first contract aligned with the current runtime model and avoids creating a second compatibility-heavy scenario surface beside `ScenarioBuilder`.
+
+## Phase-To-API Mapping
+
+| Scaffold phase | Primary runtime APIs | Contract stance |
+| --- | --- | --- |
+| Arrange | `With(...)`, `SetupMocksAction`, `CreatedComponentAction`, `ConfigureMockerPolicy` | Generated scaffolds may use `With(...)` for scenario-local arrangement and existing test-base hooks for component-wide setup. |
+| Act | `When(...)` | The normal action path for generated scenario methods. |
+| Expected exception | `WhenThrows(...)`, `ExecuteThrows()`, `ExecuteThrowsAsync()` | The contract must preserve both continued-assertion and exception-object assertion flows. |
+| Assert | `Then(...)` | Generated assertions should stay inside the normal scenario assert pipeline. |
+| Verify | `Verify(...)`, `VerifyNoOtherCalls()`, `Mocks.VerifyLogged(...)` | Provider-neutral verification stays first-class. Logging stays on the existing mocker helper surface unless a later concrete generator need proves otherwise. |
+| Execute | `Execute()`, `ExecuteAsync()` | Sync versus async execution remains a consumer choice shaped by `#162` settings and later scaffold implementation, not by local ad hoc defaults. |
+
+## Regeneration-Safe Customization Model
+
+The scenario contract must match the first extensibility direction already settled in `#162`:
+
+- generated source remains generator-owned and replaceable
+- user customization belongs in companion partials or explicit generated hook seams, not in edited generated output
+- the first hook-emission model stays constrained to `GeneratedHookEmissionStyle.None` and `GeneratedHookEmissionStyle.CompanionPartialHooks`
+- `#126` defines the scenario hook roles that later scaffolds need; `#136` chooses the concrete emitted members that realize those roles while preserving regeneration safety
+
+The current implementation materializes those roles through companion partial members named `ConfigureGeneratedMockerPolicy`, `ConfigureGeneratedMocks`, `AfterGeneratedComponentCreated`, `ArrangeGeneratedScenario`, `ActGeneratedScenario`, `ExpectedExceptionGeneratedScenario`, `AssertGeneratedScenario`, and `VerifyGeneratedScenario`.
+
+The first minimal hook-role set should stay explicit:
+
+- `Arrange`
+- `Act`
+- `ExpectedException`
+- `Assert`
+- `Verify`
+
+These are contract roles, not a second runtime API. The later generated scaffold implementation may materialize them as companion-partial methods, generated delegates, or similarly narrow named seams, but it must not collapse them into one opaque user-editable block.
+
+## Verification And Logging Stance
+
+The first in-band verification path for generated scenarios is provider-neutral FastMoq verification:
+
+- use `ScenarioBuilder.Verify(...)` for explicit interaction assertions
+- use `ScenarioBuilder.VerifyNoOtherCalls()` for verification closure where appropriate
+- prefer `Mocks.VerifyLogged(...)` and related helpers for provider-neutral logging assertions instead of adding logging-specific methods to `ScenarioBuilder` in this design slice
+
+That means generated scenarios stay provider-first by default while still allowing hand-written custom hooks to use narrower helper or provider-specific behavior where a consuming suite intentionally opts into that path.
+
+## Async And Expected-Exception Semantics
+
+The current scenario contract already supports mixed sync and async phases. The generated scenario contract should preserve that behavior rather than redefining it.
+
+Key rules:
+
+- arrange, act, and assert phases can each be synchronous or asynchronous through the current `ScenarioBuilder` overload set
+- `WhenThrows(...)` is the expected-exception path when the act phase should fail but trailing assertions should still run
+- `ExecuteThrows()` and `ExecuteThrowsAsync()` are the path when the exception object itself is the main assertion target
+- sync versus async generated test-method syntax belongs to `#162` settings consumption and `#136` scaffold implementation, not to a separate `#126` runtime model
+- the current generated scaffold implementation emits `ExecuteGeneratedExpectedExceptionScenarioScaffold()` and `ExecuteGeneratedExpectedExceptionScenarioScaffoldAsync()` for the continued-assertion `WhenThrows(...)` path
+- generated wrappers for direct `ExecuteThrows()` and `ExecuteThrowsAsync()` exception-object inspection remain deferred; use a hand-written `Scenario` flow when the exception object itself is the primary assertion target
+
+## Composition With Existing Contracts
+
+`#126` depends on lower layers that are already settled and must not reopen them.
+
+- `#125` remains the constructor-selection and public construction-planning contract boundary.
+- `#122` remains the graph metadata and harness bootstrap implementation boundary.
+- `#162` remains the shared settings, hook-emission-style, and regeneration-policy boundary.
+
+In practice that means:
+
+- generated scenarios may consume harness metadata, but they do not redefine constructor signatures, parameter-source categories, or graph shape
+- generated scenarios must consume the shared `#162` settings vocabulary for hook emission, scaffold choice, framework syntax, runner/bootstrap targeting, naming, and regeneration behavior instead of inventing local defaults
+- the scenario contract should remain stable even if later scaffold implementations support more than one framework syntax target or runner mode
+
+## Explicit Deferrals
+
+The following work stays outside `#126` even when it is closely related:
+
+- helper-family narrowing and re-triage across logging, HTTP, Azure, Azure Functions, and typed DI stays in [#134](https://github.com/cwinland/FastMoq/issues/134) and is documented in [Generated helper family matrix](./generated-helper-family-matrix.md)
+- the first concrete generated scenario and suite scaffolding implementation now lives in [#136](https://github.com/cwinland/FastMoq/issues/136)
+- full generated tests from existing services and supported classes stays in [#123](https://github.com/cwinland/FastMoq/issues/123)
+- analyzer-guided generation routing and missing-package suggestions stays in [#124](https://github.com/cwinland/FastMoq/issues/124)
+- helper-builder generation stays in [#137](https://github.com/cwinland/FastMoq/issues/137)
+- standalone generated suite types, settings-driven scaffold variants, and direct exception-object-returning generated scaffold wrappers remain explicit deferred cases from the first `#136` slice
+
+If later discovery shows that one truly missing runtime seam blocks the contract, track that seam narrowly instead of silently widening `#126` into a broad implementation branch.
+
+## Follow-On Boundaries
+
+The immediate sequence after `#162` should stay explicit:
+
+- `#126` defines the stable scenario-scaffolding contract
+- `#134` classifies which helper families the scenario implementation may later depend on
+- `#136` is the current narrow implementation slice: generated scenario execution plus suite-level shared setup inside explicit partial harness targets
+- `#123` and `#124` follow only after the contract and scaffold layers are stable enough to consume instead of re-deriving local defaults
+- `#137` remains later and conditional rather than part of the immediate next-step chain
diff --git a/docs/roadmap/generated-test-settings.md b/docs/roadmap/generated-test-settings.md
new file mode 100644
index 00000000..ac3d6f22
--- /dev/null
+++ b/docs/roadmap/generated-test-settings.md
@@ -0,0 +1,350 @@
+# FastMoq Generated Test Settings Design
+
+This page captures the current `#162` design direction for generated-test authoring settings. It is design-level only. It does not imply shipped settings resolution, generator behavior changes, analyzer behavior changes, or external scaffolding support in the repo.
+
+## Purpose And Non-Goals
+
+Purpose:
+
+- define one shared authoring-time settings contract for later generated scenario scaffolds, broader generated-test expansion, analyzer-guided entry points, and any helper-builder output that explicitly opts into the same contract
+- keep `#123`, `#124`, `#126`, `#136`, and conditionally `#137` aligned to one vocabulary for naming, platform targeting, regeneration, and extensibility
+- settle where those settings live, how they override one another, and which parts remain capability-gated by existing package-shape rules
+
+Non-goals:
+
+- no generator, analyzer, runtime, schema parser, or settings-loader implementation work in this slice
+- no reopening of the `#122` harness MVP contract
+- no reopening of the `#127` package-layout and target-shape authority
+- no machine-local IDE-profile settings model
+- no arbitrary user-authored template system, AST injection point, or plugin callback model in the first design
+
+## Current Baseline
+
+The current repo state that drives this design is:
+
+- `#122` is complete for the narrow harness MVP, and `#136` now builds on that base for explicit partial `MockerTestBase` targets: the repo can emit constructor-signature metadata, harness bootstrap, generated scenario execution entry points, suite-level shared setup hooks, and the first narrow `#123` xUnit smoke-test slice inside those explicit harness partials, but it still does not emit standalone full generated test classes, settings-driven scaffold variants, or framework-helper builders.
+- `#127` is complete for package detection and target-shape rules: the analyzer layer already knows the supported generated target shapes for the referenced FastMoq package layout.
+- `GeneratedHarnessSourceGenerator` currently hard-codes the target attribute metadata name, the `MockerTestBase` requirement, the generated metadata type name, the `ComponentConstructorParameterTypes` override, the auto-generated header, `#nullable enable`, the `.FastMoq.GeneratedHarness.g.cs` hint-name suffix, and the current xUnit-gated smoke-test naming and placeholder strategy.
+- `GeneratedTestTargetShapeRule` and `FastMoqAnalysisHelpers` currently own the package matrix, target-shape list, required package per shape, default base type name, and default namespaces for each supported generated test shape.
+- `Directory.Packages.props` currently carries `xunit` `2.9.3` and `xunit.runner.visualstudio` `3.0.2`, while the repo documentation remains framework-agnostic. That mixed baseline is one reason syntax targeting and runner or bootstrap targeting must stay separate settings.
+- `FastMoq.Generators.csproj` now declares `CompilerVisibleProperty` for `FastMoqGeneratedTestFramework`, but it still does not declare `CompilerVisibleItemMetadata` or any broader custom bridge for the rest of the generated-test authoring settings.
+
+## Consumer Map
+
+- `#126` consumes the shared settings contract for regeneration-safe scenario-scaffolding hooks and customization boundaries, but it still owns the `ScenarioBuilder` contract surface rather than the settings carrier.
+- `#136` consumes the shared settings contract for concrete scenario and suite scaffolding implementation after `#126` settles the hook model.
+- `#123` consumes the shared settings contract for naming, scaffold choice, framework syntax targeting, runner or bootstrap targeting, and regeneration behavior when widening from harness metadata into broader generated-test coverage.
+- `#124` consumes the shared settings contract so analyzer-guided generation suggestions can reflect the same naming and platform choices without inventing analyzer-local defaults.
+- `#137` is conditional: if helper-builder output needs shared naming, output, or regeneration rules, it should consume this contract later. This design does not assume that relationship unless a later implementation explicitly chooses it.
+
+## Settings Taxonomy
+
+### Package Acquisition And Capability Settings
+
+- Auto-add package policy. Classification: capability-gated user setting. First consumer: `#124` or a later external scaffolder. Notes: the in-compilation source generator must not mutate project files.
+- Helper-family opt-in or opt-out. Classification: capability-gated user setting. First consumer: `#137` if it opts into the shared contract. Notes: the setting can only choose among helper families already supported by the `#127` package matrix.
+
+### Output Placement And Naming Settings
+
+- Destination project. Classification: deferred value placeholder. First consumer: a future external scaffolder, not the current in-compilation source generator.
+- Destination folder. Classification: deferred value placeholder. First consumer: a future external scaffolder, not the current in-compilation source generator.
+- Namespace template. Classification: true user setting. First consumer: `#123`, `#136`, and `#124`.
+- Type and member naming templates. Classification: true user setting. First consumer: `#123`, `#136`, and conditionally `#137`.
+
+### Provider And Bootstrap Settings
+
+- Preferred provider. Classification: true user setting. First consumer: `#123` and `#136`. Notes: provider choice remains constrained by the FastMoq packages referenced by the consuming project.
+- Provider bootstrap style. Classification: true user setting. First consumer: `#123` and `#136`. Notes: this covers how generated tests express or discover provider selection; it is separate from framework syntax or runner choice.
+
+### Test Syntax And Assertion Settings
+
+- Test framework syntax target. Classification: true user setting with explicit deferred values. First consumer: `#123`, `#136`, and `#124`.
+- Runner or bootstrap target. Classification: true user setting with explicit deferred values. First consumer: `#123`, `#136`, and `#124`.
+- Assertion style. Classification: true user setting. First consumer: `#123` and `#136`. Notes: the repo already uses more than one assertion style in documentation, so generated output should not silently hard-code one style forever.
+
+### Scaffold-Shape Settings
+
+- Preferred scaffold shape. Classification: true user setting. First consumer: `#123` and `#136`.
+- Hook emission style. Classification: structured extensibility point. First consumer: `#126` and `#136`.
+- Helper-builder generation preference. Classification: capability-gated user setting. First consumer: conditionally `#137`.
+
+### Regeneration-Safety Settings
+
+- Regeneration policy. Classification: true user setting. First consumer: `#123`, `#136`, and conditionally `#137`.
+- Partial-extension boundary policy. Classification: structured extensibility point. First consumer: `#126` and `#136`.
+- Hand-edited safe-zone policy. Classification: deferred value placeholder until a non-generator file-emission workflow exists.
+
+## Settings Carrier, Scope, And Precedence Model
+
+The first persisted carrier should be MSBuild-backed configuration authored at repo or project scope. The authoritative path is:
+
+1. built-in defaults derived from package matrix and target shape
+2. repo defaults in `Directory.Build.props`
+3. consuming project overrides in the project file
+4. future explicit tool or code-action arguments for external scaffolding flows only
+
+The first-wave property family should use a single `FastMoqGeneratedTest...` prefix so the settings are easy to discover in MSBuild and easy to surface through analyzer-config global options. Recommended first-wave property names are:
+
+- `FastMoqGeneratedTestFramework`
+- `FastMoqGeneratedTestRunner`
+- `FastMoqGeneratedTestProvider`
+- `FastMoqGeneratedTestProviderBootstrap`
+- `FastMoqGeneratedTestAssertionStyle`
+- `FastMoqGeneratedTestScaffold`
+- `FastMoqGeneratedTestHookEmission`
+- `FastMoqGeneratedTestRegenerationPolicy`
+- `FastMoqGeneratedTestAutoAddPackages`
+- `FastMoqGeneratedTestNamespace`
+- `FastMoqGeneratedTestTypeNamePattern`
+- `FastMoqGeneratedTestMethodNamePattern`
+- `FastMoqGeneratedTestHelperFamilies`
+
+Reserved later properties for external scaffolding flows are:
+
+- `FastMoqGeneratedTestOutputProject`
+- `FastMoqGeneratedTestOutputFolder`
+
+`.editorconfig` stays secondary in the first design. It can participate later for path-scoped analyzer behavior if a real need appears, but it should not replace repo/project-scoped MSBuild settings as the authoritative source for generated-test authoring choices.
+
+JSON or `AdditionalFiles` manifests are explicitly deferred. They should only be introduced if the structured settings shape demonstrably outgrows practical MSBuild properties or simple semicolon-delimited list values.
+
+Implementation note: although Roslyn analyzers and generators can observe `build_property.*` values through generated analyzer config, this repo does not currently expose any custom generated-test settings bridge in `FastMoq.Generators.csproj`. A future implementation slice must add the required compiler-visible-property or equivalent build plumbing before any custom settings can flow into analyzer or generator code. The current `#136` scaffold implementation and the first narrow `#123` smoke-test slice therefore consume the `#162` vocabulary as fixed generator-owned defaults rather than as user-configurable settings values.
+
+## Extensibility Model
+
+The first design should treat extensibility as constrained authoring control, not as an open plugin system.
+
+The surface breaks into three buckets:
+
+- Closed choices. Examples: framework syntax target, runner target, assertion style, provider bootstrap style, scaffold preference, regeneration policy.
+- Structured override points. Examples: hook emission style, named partial companion hooks, and regeneration-safe setup or verify seams that map back to existing FastMoq runtime surfaces.
+- Deferred open extensibility. Examples: arbitrary user-authored templates, AST injection, or plugin callbacks. These are intentionally out of scope for the first design.
+
+The first override mechanism should be companion partial hooks only. Do not introduce multiple parallel extension systems in the first design.
+
+The first `GeneratedHookEmissionStyle` choice set is:
+
+- `None`
+- `CompanionPartialHooks`
+
+The first named hook roles should stay minimal and map back to existing runtime surfaces instead of inventing a second runtime model:
+
+- `ConfigureGeneratedMockerPolicy` for `MockerTestBase.ConfigureMockerPolicy`
+- `ConfigureGeneratedMocks` for `MockerTestBase.SetupMocksAction`
+- `SelectGeneratedConstructor` for `ComponentConstructorParameterTypes` and `CreateComponentConstructionRequest()`-based customization
+- `AfterGeneratedComponentCreated` for `MockerTestBase.CreatedComponentAction`
+- `ArrangeGeneratedScenario` for `ScenarioBuilder.With(...)` and `When(...)`
+- `AssertGeneratedScenario` for `ScenarioBuilder.Then(...)`, `WhenThrows(...)`, and provider-neutral `Verify(...)` flows
+
+Custom overrides on test structure should come through scaffold selection plus those regeneration-safe named hooks, not through free-form template injection.
+
+## Supported And Deferred Framework And Runner Matrix
+
+Framework syntax targets:
+
+| Target | Status | Notes |
+| --- | --- | --- |
+| `XUnitV2` | Supported baseline | Matches the current repo package baseline most closely. |
+| `XUnitV3` | Supported with differences | Syntax stays similar for many tests, but fixture and runner choices differ enough that it must remain explicit. |
+| `NUnit` | Supported with differences | Attribute names, lifecycle hooks, and assertion guidance differ. |
+| `MSTest` | Supported with differences | Attribute names and suite structure differ. |
+| `Deferred` | Explicit placeholder | Use when the consuming flow cannot yet emit the requested framework cleanly. |
+
+Runner or bootstrap targets:
+
+| Target | Status | Notes |
+| --- | --- | --- |
+| `ProjectDefault` | Supported baseline | Uses the consuming test project's established runner/bootstrap story without trying to infer more than the project already declares. |
+| `VSTest` | Supported | Runner choice remains separate from syntax choice. |
+| `MTP` | Supported | Runner choice remains separate from syntax choice. |
+| `ModuleInitializer` | Deferred | Bootstrap-specific flow that should stay explicit instead of inferred. |
+| `AssemblyFixture` | Deferred | Framework-specific bootstrap behavior should remain explicit. |
+| `Deferred` | Explicit placeholder | Use when the requested bootstrap shape is not yet implemented. |
+
+The design requirement behind both tables is simple: syntax target and runner or bootstrap mode are related, but they are not the same setting and should not be collapsed into one inferred value.
+
+## Candidate API And Design Outline
+
+The first descriptive model should use authoring-time names rather than runtime metadata names:
+
+```csharp
+public sealed record GeneratedTestAuthoringSettings
+{
+ public GeneratedTestPackagePolicySettings PackagePolicy { get; init; }
+ public GeneratedTestPlacementSettings Placement { get; init; }
+ public GeneratedTestNamingTemplateSettings Naming { get; init; }
+ public GeneratedTestProviderSelectionSettings ProviderSelection { get; init; }
+ public GeneratedTestSyntaxTargetSettings SyntaxTarget { get; init; }
+ public GeneratedTestScaffoldPreferenceSettings ScaffoldPreference { get; init; }
+ public GeneratedTestRegenerationSettings Regeneration { get; init; }
+}
+
+public sealed record GeneratedTestPackagePolicySettings
+{
+ public GeneratedTestPackagePolicy AutoAddPackages { get; init; }
+ public string[] HelperFamilies { get; init; }
+}
+
+public sealed record GeneratedTestPlacementSettings
+{
+ public string? Namespace { get; init; }
+ public string? OutputProject { get; init; }
+ public string? OutputFolder { get; init; }
+}
+
+public sealed record GeneratedTestNamingTemplateSettings
+{
+ public string TestTypeNamePattern { get; init; }
+ public string TestMethodNamePattern { get; init; }
+ public string ScenarioTypeNamePattern { get; init; }
+}
+
+public sealed record GeneratedTestProviderSelectionSettings
+{
+ public string PreferredProvider { get; init; }
+ public GeneratedTestProviderBootstrapStyle BootstrapStyle { get; init; }
+}
+
+public sealed record GeneratedTestSyntaxTargetSettings
+{
+ public GeneratedTestFrameworkSyntaxTarget Framework { get; init; }
+ public GeneratedTestRunnerBootstrapMode Runner { get; init; }
+ public string AssertionStyle { get; init; }
+}
+
+public sealed record GeneratedTestScaffoldPreferenceSettings
+{
+ public GeneratedTestScaffoldKind PreferredScaffold { get; init; }
+ public GeneratedHookEmissionStyle HookEmission { get; init; }
+ public bool GenerateHelperBuildersWhenSupported { get; init; }
+}
+
+public sealed record GeneratedTestRegenerationSettings
+{
+ public GeneratedTestRegenerationPolicy Policy { get; init; }
+ public bool PreserveCompanionPartials { get; init; }
+ public string[] SafeZones { get; init; }
+}
+```
+
+Related enum and value-object families should stay separate for clarity:
+
+- `GeneratedTestFrameworkSyntaxTarget`
+- `GeneratedTestRunnerBootstrapMode`
+- `GeneratedTestProviderBootstrapStyle`
+- `GeneratedTestScaffoldKind`
+- `GeneratedHookEmissionStyle`
+- `GeneratedTestPackagePolicy`
+- `GeneratedTestRegenerationPolicy`
+
+Intended first-use classification by type:
+
+- `GeneratedTestAuthoringSettings`: top-level true user-setting aggregate; first carrier is MSBuild-backed analyzer-config values.
+- `GeneratedTestPackagePolicySettings`: capability-gated user settings; primary consumer is analyzer-guided or external scaffolding flows.
+- `GeneratedTestPlacementSettings`: mixed user setting plus deferred placeholders; `OutputProject` and `OutputFolder` are not first-class source-generator controls yet.
+- `GeneratedTestNamingTemplateSettings`: true user settings; consumed by generated scenario scaffolds and broader generated-test expansion.
+- `GeneratedTestProviderSelectionSettings`: true user settings; constrained by referenced FastMoq provider packages.
+- `GeneratedTestSyntaxTargetSettings`: true user settings with explicit deferred values.
+- `GeneratedTestScaffoldPreferenceSettings`: mixed user settings and structured extensibility point.
+- `GeneratedTestRegenerationSettings`: mixed user settings and structured extensibility point.
+
+## Illustrative MSBuild Configuration Examples
+
+Repo-level defaults in `Directory.Build.props`:
+
+```xml
+
+
+ XUnitV2
+ ProjectDefault
+ reflection
+ ProjectDefault
+ AwesomeAssertions
+ ScenarioScaffold
+ CompanionPartialHooks
+ PreserveCompanionPartials
+ SuggestOnly
+ $(RootNamespace).GeneratedTests
+ {ComponentName}GeneratedTests
+ {MemberName}_Should_{Outcome}
+ Web;Database
+
+
+```
+
+Project-level override in a consuming test project:
+
+```xml
+
+
+ NUnit
+ MTP
+ FullTestClass
+ $(RootNamespace).Generated
+
+
+```
+
+These examples are illustrative only. They document the intended first carrier and naming family, not working current implementation.
+
+## Regeneration And Safe-Zone Rules
+
+- Generated `.g.cs` output remains generator-owned and should be fully replaceable.
+- User customization should live in companion partials or named hook seams, not in edited generated output.
+- `PreserveCompanionPartials` should be the default regeneration stance when a file-emitting flow exists.
+- When a requested scaffold shape cannot provide a safe regeneration seam, the implementation should fail clearly or document the unsupported case instead of overwriting user edits silently.
+- Destination project and destination folder are future external-scaffolder concerns. They do not become meaningful first-class controls for the in-compilation source generator by themselves.
+- Hand-edited safe zones remain a deferred concept until a non-generator file-emission workflow exists.
+
+## Package-Aware Gating Rules
+
+Settings may choose among supported outputs, but settings must not override the package-shape authority already owned by `#127`.
+
+That means:
+
+- `FastMoqAnalysisHelpers.GetGeneratedTestPackageMatrix(...)` remains the authoritative package-layout summary.
+- `FastMoqAnalysisHelpers.SupportsGeneratedTestTargetShape(...)` remains the authoritative check for whether a requested generated target shape is valid for the current project.
+- `ResolveGeneratedTestPackageLayout(...)` and `BuildSupportedGeneratedTestTargetShapes(...)` remain analyzer-owned capability logic.
+- helper-family settings can only request helper output that is already valid for the referenced FastMoq package layout.
+- naming, scaffold, provider, or runner settings must never bypass unsupported target-shape or helper-package combinations.
+
+## Settings Versus Invariants
+
+The following items are explicitly not user settings in `#162`.
+
+| Item | Classification | Rationale |
+| --- | --- | --- |
+| `FastMoq.Generators.FastMoqGeneratedTestTargetAttribute` metadata name | Not a setting | This is the current generator target identity, not an authoring preference. |
+| Explicit partial-class requirement and `MockerTestBase` target eligibility | Not a setting | These are current MVP target-shape rules from `#122`. |
+| `FastMoqGeneratedHarnessMetadata` type name | Not a setting | Generated metadata naming is part of the current harness contract. |
+| `ComponentConstructorParameterTypes` hook name | Not a setting | This is the existing runtime and generated harness hook from `MockerTestBase`. |
+| `ConfigureGeneratedMockerPolicy`, `ConfigureGeneratedMocks`, `AfterGeneratedComponentCreated`, `ArrangeGeneratedScenario`, `ActGeneratedScenario`, `ExpectedExceptionGeneratedScenario`, `AssertGeneratedScenario`, and `VerifyGeneratedScenario` hook names | Not a setting | Current companion partial hook names are generator-owned implementation details today. |
+| `ExecuteGeneratedScenarioScaffold`, `ExecuteGeneratedScenarioScaffoldAsync`, `ExecuteGeneratedExpectedExceptionScenarioScaffold`, and `ExecuteGeneratedExpectedExceptionScenarioScaffoldAsync` member names | Not a setting | Current generated scenario entry-point names are generator-owned implementation details today. |
+| `.FastMoq.GeneratedHarness.g.cs` hint-name suffix | Not a setting | File hint naming is generator-owned implementation detail today. |
+| Current `// ` header and `#nullable enable` boilerplate | Not a setting | Generator-owned implementation detail today. |
+| `FastMoqGeneratedTestPackageLayout` enum values | Not a setting | Analyzer-owned capability boundary from `#127`. |
+| `GeneratedTestTargetShape` enum values | Not a setting | Analyzer-owned target-shape boundary from `#127`. |
+| Default required package per target shape | Not a setting | Capability mapping remains analyzer-owned. |
+| Default base-type and namespace lists per target shape | Not a setting | Capability mapping remains analyzer-owned. |
+
+The following items are candidate future settings or implementation concerns rather than settled invariants:
+
+| Item | Classification | Rationale |
+| --- | --- | --- |
+| Naming templates | Candidate future setting | Authoring-time concern consumed by later scaffold and full-test flows. |
+| Framework syntax target | Candidate future setting | Authoring-time concern, distinct from runner/bootstrap. |
+| Runner/bootstrap mode | Candidate future setting | Authoring-time concern, distinct from syntax. |
+| Provider bootstrap style | Candidate future setting | Authoring-time concern for generated output shape. |
+| Compiler-visible settings bridge | Future implementation concern | Required before custom settings can flow into Roslyn code, but not itself a user setting. |
+
+This section exists to keep `#162` from silently reopening `#122` or `#127`.
+
+## Explicit Follow-On Boundaries
+
+- `#162` ends once the shared settings contract, carrier model, precedence rules, support matrix, extensibility model, and invariants are documented and mirrored into the roadmap.
+- `#126` should then focus only on regeneration-safe scenario/scaffold hooks that consume this contract.
+- the first `#136` slice now implements scenario and suite scaffolding against the `#126` hook contract plus the `#162` settings vocabulary, but future settings wiring still needs the compiler-visible-property bridge above before consumers can override those defaults.
+- `#123` should implement broader generated-test coverage against this contract without inventing local defaults for framework syntax, runner/bootstrap mode, naming, or regeneration behavior.
+- `#124` should route analyzer suggestions into already-supported generation layers while using this contract for defaults and choices.
+- `#137` should only adopt this contract if helper-builder output actually needs the same naming, placement, regeneration, or framework-targeting choices.
+- provider-optimized generation evaluation in `#138` and narrower fake-generation evaluation in `#139` remain intentionally later.
diff --git a/docs/roadmap/generator-roadmap.md b/docs/roadmap/generator-roadmap.md
index b9e77d29..5b031705 100644
--- a/docs/roadmap/generator-roadmap.md
+++ b/docs/roadmap/generator-roadmap.md
@@ -2,19 +2,25 @@
This page captures the current v5 direction for FastMoq code generation. It is the detailed design companion to the main [roadmap summary](./README.md), not a shipped feature guide.
-This page is intentionally design-level only. It is appropriate for roadmap and implementation planning ahead of code, but it does not imply current shipped support.
+This page remains design-level heavy. It is appropriate for roadmap and implementation planning, but some sections also record the narrow generator slices currently supported in the repo.
-FastMoq does not currently include traditional Roslyn source generators. The current repo ships analyzers and code fixes, but it does not emit new `.g.cs` files for mocks, dependency graphs, or test scaffolding.
+For the shared generated-test settings contract behind [#162](https://github.com/cwinland/FastMoq/issues/162), see [Generated test settings design](./generated-test-settings.md).
+For the scenario-scaffolding contract behind [#126](https://github.com/cwinland/FastMoq/issues/126), see [Generated scenario scaffolding contract](./generated-scenario-scaffolding-contract.md).
+For the helper-family narrowing contract behind [#134](https://github.com/cwinland/FastMoq/issues/134), see [Generated helper family matrix](./generated-helper-family-matrix.md).
+
+FastMoq now contains narrow Roslyn source-generator slices for explicit `MockerTestBase` harness targets, the first generated scenario and suite scaffolding layer inside those targets, and the first narrow generated-test slice inside those same harness partials. The repo still does not emit standalone generated test classes or broader framework-helper builders.
## Current Baseline
Confirmed current state:
- `FastMoq.Analyzers` ships analyzers and code fixes for migration, provider-first authoring, package guidance, and helper adoption.
-- The repo does not currently contain `ISourceGenerator` or `IIncrementalGenerator` implementations.
-- There is no first-party compile-time generation of mocks, DI graphs, scenario scaffolding, or framework-helper builders.
+- `FastMoq.Generators` now contains the first `IIncrementalGenerator` implementation for explicit partial `MockerTestBase` targets.
+- The current generated output is intentionally narrow: constructor-signature metadata and harness bootstrap for explicitly selected component paths, generated scenario execution entry points and suite-level shared setup hooks for explicit partial harness targets, and xUnit-gated smoke-test emission inside those same harness partials.
+- The current generated-test slice is still bounded to public instance methods that are safe to execute with no inputs or with explicit compile-time default parameter values, with supported `void`, value-returning, `Task`, `Task`, `ValueTask`, and `ValueTask` return shapes. Unsupported methods stay compile-safe through skipped placeholders with explicit reasons.
+- There is still no first-party compile-time generation of standalone full generated test classes or broader framework-helper builders, and the current full-test slice remains intentionally limited to explicit partial harness targets.
-That means code generation in v5 is net-new product surface rather than a minor extension of the current analyzer package.
+That means code generation in v5 is now an early implementation-facing surface rather than roadmap-only prose, but the broader generator line is still net-new beyond these first harness, scaffold, and smoke-test slices.
## Public Issue Crosswalk
@@ -27,8 +33,9 @@ The current public backlog for this design is:
- [#125](https://github.com/cwinland/FastMoq/issues/125) graph metadata hooks and constructor-selection primitives
- [#126](https://github.com/cwinland/FastMoq/issues/126) `ScenarioBuilder` scaffolding hooks for generated output
- [#127](https://github.com/cwinland/FastMoq/issues/127) package-detection and target-test-shape rules for generated tests
-- [#134](https://github.com/cwinland/FastMoq/issues/134) blocking helper-surface normalization for logging, HTTP, Azure, Azure Functions, and typed DI
+- [#134](https://github.com/cwinland/FastMoq/issues/134) helper-family narrowing and re-triage matrix for logging, HTTP, Azure, Azure Functions, and typed DI
- [#122](https://github.com/cwinland/FastMoq/issues/122) compile-time graph and harness MVP
+- [#162](https://github.com/cwinland/FastMoq/issues/162) generated-test settings and test-platform targeting as the current gate before wider authoring flows
- [#136](https://github.com/cwinland/FastMoq/issues/136) generated scenario and suite scaffolding after the graph and harness MVP
- [#137](https://github.com/cwinland/FastMoq/issues/137) generator-backed framework-helper builders for repeated test patterns
- [#123](https://github.com/cwinland/FastMoq/issues/123) full provider-first test generation from existing services and supported classes
@@ -39,10 +46,16 @@ The current public backlog for this design is:
Crosswalk summary:
- `#121` is the runtime-prerequisite umbrella.
-- `#132`, `#133`, and `#135` are the narrower v4-style quick wins that are now implemented on the current milestone branch.
+- `#132`, `#133`, and `#135` are the narrower v4-style quick wins that are now implemented in the repo.
- `#146` and `#147` carry the near-term analyzer follow-up for those landed helper surfaces.
-- `#120`, `#125`, `#126`, `#127`, and `#134` are the pre-v5 contract and blocking prerequisite slices.
-- `#122`, `#136`, `#137`, `#123`, and `#124` are the phased implementation and authoring-flow outcomes once the prerequisites are stable enough.
+- `#120`, `#125`, `#126`, `#127`, and `#134` are the pre-v5 contract and helper-boundary prerequisite slices.
+- `#122` is the completed first implementation-facing MVP for compile-time graph metadata and harness bootstrap.
+- `#162` is the current shared settings and test-platform contract gate before wider generated scenario scaffolds, broader generated-test expansion, and analyzer entry points.
+- `#126` defines the stable scenario-scaffolding contract layer that the current generated scaffold slice now targets.
+- `#134` documents the helper-family bounds that the current scaffold slice stays within and that later helper-heavy scaffold expansion must continue to honor.
+- `#136` is now the first implementation slice for generated scenario and suite scaffolding: generated scenario execution helpers, companion partial hooks, and suite-level shared setup composition inside explicit partial harness targets.
+- `#123` now has its first narrow implementation slice inside explicit harness partials, while `#124` remains later analyzer-guided authoring flow after the current generated-test boundary is stable.
+- `#137` remains later and conditional rather than part of the immediate next-step chain.
- `#138` and `#139` are intentionally late evaluation tracks after the main provider-first generator story is already working.
## Product Positioning
@@ -112,14 +125,27 @@ Why it goes first:
### 2. Scenario and suite scaffolding
-This workstream should build on generated graph metadata rather than replace it.
+This workstream now has its first implementation slice and still has explicit bounds.
-Primary outputs:
+Current implemented outputs:
+
+- generated scenario execution entry points and companion partial hooks inside explicit partial `MockerTestBase` targets
+- generated suite-level shared setup regions through `ConfigureGeneratedMockerPolicy`, `ConfigureGeneratedMocks`, and `AfterGeneratedComponentCreated`
+- generated sync, async, and continued-assertion expected-exception scaffold entry points built on the existing `ScenarioBuilder` pipeline
+
+Current supported shape:
-- generated scenario shell types or partials
-- generated per-suite setup regions
-- generated default verify helpers or scenario hooks
-- generated migration-starting points for repeated patterns
+- generated members stay inside explicit partial harness types rather than standalone generated suite classes
+- suite scaffolding for `#136` means shared setup composition for the generated harness, not separate project-level test containers
+- verification stays provider-first through existing `ScenarioBuilder` and `Mocks` surfaces
+- the current slice uses the settled `#162` hook vocabulary and built-in defaults rather than consumer-configurable settings values
+
+Explicitly deferred from this slice:
+
+- standalone generated suite or scenario types outside the harness partial
+- settings-driven framework syntax, runner or bootstrap selection, naming templates, or helper-family-specific emission
+- generated wrappers that return the thrown exception object through `ExecuteThrows()` or `ExecuteThrowsAsync()`; use a hand-written `Scenario` flow when the exception object itself is the primary assertion target
+- full generated test classes and helper-builder expansion
Expected value:
@@ -129,7 +155,28 @@ Expected value:
### 3. Full-test generation from existing services and classes
-This workstream should generate complete starting tests for real existing code, not just partial harness fragments.
+This workstream now has its first implementation-facing slice, but the broader work still aims at complete starting tests for real existing code rather than partial harness fragments alone.
+
+Current implemented outputs:
+
+- xUnit smoke tests emitted inside explicit partial `MockerTestBase` harness targets when `Xunit.FactAttribute` is available in the compilation
+- one component-creation smoke test plus one generated smoke test per eligible public instance component method
+- generated calls for methods that take no parameters or only explicit compile-time defaulted parameters
+- skipped placeholders with explicit reasons for unsupported methods and shapes
+
+Current supported shape:
+
+- generated tests stay inside the existing explicit harness partial rather than emitting standalone generated test classes
+- method execution is limited to public instance methods with either no parameters or explicit compile-time default values that can be emitted safely
+- supported return shapes are `void`, value-returning sync methods, `Task`, `Task`, `ValueTask`, and `ValueTask`
+- generated smoke tests are currently xUnit-specific and only emit when the xUnit surface is actually available in the compilation
+
+Explicitly deferred from this slice:
+
+- standalone generated test classes for services, controllers, handlers, or other broader target shapes
+- methods that require non-optional parameters, generic methods, `ref` or `out` parameters, or optional defaults that cannot be emitted safely from metadata
+- settings-driven framework syntax, naming templates, runner selection, assertion-style selection, or helper-family-specific full-test emission
+- analyzer entry points and missing-package suggestion flows in `#124`
Primary outputs:
@@ -226,26 +273,29 @@ Without those runtime targets, generators would be forced to emit provider-nativ
Some runtime preparation work was narrow enough to land before v5 without forcing a wider public-contract redesign.
-Completed on the current milestone branch:
+Completed in the repo:
- expanded provider-first setup helpers for common simple arrangements
- expanded provider-first verification helpers where a shared abstraction is still clear and stable
- small helper-surface cleanups for logging, HTTP, or typed DI setup where the FastMoq-owned runtime surface already exists and only needed a more generator-friendly shape
-These landed early because they improve normal authoring even before source generators ship. The remaining near-term follow-up is narrower analyzer guidance in [#146](https://github.com/cwinland/FastMoq/issues/146) and [#147](https://github.com/cwinland/FastMoq/issues/147), plus broader helper normalization in [#134](https://github.com/cwinland/FastMoq/issues/134).
+These landed early because they improve normal authoring even before source generators ship. The remaining near-term follow-up is narrower analyzer guidance in [#146](https://github.com/cwinland/FastMoq/issues/146) and [#147](https://github.com/cwinland/FastMoq/issues/147), plus the helper-family narrowing matrix in [#134](https://github.com/cwinland/FastMoq/issues/134) after the `#126` scenario contract is explicit.
### Likely v5 blocking prerequisites
Some work is more foundational and should be treated as explicit prerequisites for the generator implementation itself.
-Blocking areas:
+Completed in the repo:
-- stable graph metadata hooks or reusable constructor-selection primitives for generator output
-- clearer scenario-builder extension points for generated scaffolding
+- stable graph metadata hooks and reusable constructor-selection primitives for generator output
- clear package-detection and target-test-shape rules so generated tests do not assume helper packages that are not referenced
+
+Still-open follow-up that composes with the generator line but no longer blocks the first real `#122` source-generator slice:
+
+- clearer scenario-builder extension points for generated scaffolding
- broader generator-friendly helper normalization across logging, HTTP, Azure, Azure Functions, and DI-heavy setup when generated output needs those shapes to stay consistent across projects
-Those pieces are less about convenience and more about preventing the generator from emitting unstable, provider-native, or package-guessing code.
+The completed groundwork is what prevents the first generator slice from emitting unstable, provider-native, or package-guessing code too early. The remaining follow-up still matters, but it is now later-slice scope rather than a blocker for the first implementation-facing `#122` output.
## Package Shape And MVP Contract
@@ -271,7 +321,7 @@ The install and opt-in story for the first generator slice is:
- package installation enables generation capability, but generation targets should still be explicit rather than blanket automatic for every eligible type in a project
- generator-triggering flow can come from supported markers, declared generation targets, or later analyzer-guided authoring, but the package alone should not imply broad surprise output across an existing suite
-Broader project-level or suite-level settings that express preferred generated test direction, scaffold style, or harness shape are later work. They should not be treated as part of the #125 constructor-contract slice or the first #122 graph and harness MVP.
+Broader project-level or suite-level settings that express preferred generated test direction, scaffold style, assertion style, or framework and runner targeting are later work in [#162](https://github.com/cwinland/FastMoq/issues/162). They should not be treated as part of the #125 constructor-contract slice or the first #122 graph and harness MVP.
This keeps the aggregate install convenient without making generation feel like unavoidable background behavior.
@@ -311,7 +361,7 @@ That MVP boundary is what issue `#122` is allowed to implement first.
## Current Constructor Contract Direction For #125
-Issue [#121](https://github.com/cwinland/FastMoq/issues/121) remains the umbrella tracker for prerequisite status. Issue [#125](https://github.com/cwinland/FastMoq/issues/125) is the active blocking contract slice before the graph and harness MVP in [#122](https://github.com/cwinland/FastMoq/issues/122).
+Issue [#121](https://github.com/cwinland/FastMoq/issues/121) remains the umbrella tracker for prerequisite status. Issue [#125](https://github.com/cwinland/FastMoq/issues/125) is complete, and its public constructor-planning contract is now the settled runtime boundary that the first real generator output in [#122](https://github.com/cwinland/FastMoq/issues/122) targets.
The current proposed public surface for that slice is:
@@ -343,7 +393,6 @@ The current proposed first-slice enum members for `InstanceConstructionParameter
- `KnownType`
- `KeyedService`
- `AutoMock`
-- `ConstructedByMocker`
- `OptionalDefault`
- `TypeDefault`
@@ -351,6 +400,9 @@ Boundary rules for this slice:
- the public request model captures constructor-selection intent only
- the public resolved plan captures stable constructor-selection output only
+- `Mocker.CreateConstructionPlan(InstanceConstructionRequest request)` stays the preferred public entry point for this slice; a companion generic convenience overload is not required for the first graph and harness MVP
+- the first harness-side consumer can live on `MockerTestBase` by mapping `ComponentCreationFlags` and `ComponentConstructorParameterTypes` through the same request-only planning surface
+- `ConstructedByMocker` is deferred out of the first-slice contract because the current runtime parameter path does not recursively construct dependency parameters as a distinct category; current concrete-parameter outcomes remain `AutoMock` or `TypeDefault` unless a custom or known registration applies
- the first slice does not commit to a public executable-plan API such as `CreateInstance(InstanceConstructionPlan plan)`
- the first slice does not add new reflection-heavy contract fields such as raw `ConstructorInfo`, `ParameterInfo`, or executable argument values to the new plan types
- existing public diagnostics and runtime behavior, including current public reflection-metadata resolution paths, remain part of the compatibility boundary and should not be demoted just to make the new contract cleaner
@@ -366,10 +418,38 @@ Closing issue [#125](https://github.com/cwinland/FastMoq/issues/125) should requ
- ambiguity behavior, including both throw and prefer-parameterless paths
- preferred-constructor selection and invalid multiple-preferred-constructor cases
- keyed or special dependency resolution paths
-- stable parameter-source categorization for `CustomRegistration`, `KnownType`, `KeyedService`, `AutoMock`, `ConstructedByMocker`, `OptionalDefault`, and `TypeDefault`
+- stable parameter-source categorization for `CustomRegistration`, `KnownType`, `KeyedService`, `AutoMock`, `OptionalDefault`, and `TypeDefault`
That parity matrix is part of the definition artifact for this slice. It does not require the generator implementation itself to land inside [#125](https://github.com/cwinland/FastMoq/issues/125), but it does require the contract text to make those expected behaviors explicit enough that later implementation issues can target them without reopening constructor-selection semantics.
+## Current `#122` runtime status after the first graph slice
+
+The first implementation step after [#125](https://github.com/cwinland/FastMoq/issues/125) is now in place.
+
+Done in the repo:
+
+- an internal `InstanceConstructionGraph` model now projects the selected root constructor plan plus ordered dependency nodes and edges from `Mocker.CreateConstructionPlan(...)`
+- `MockerTestBase` now exposes the first harness-side consumer through `GetComponentConstructionGraph()`
+- an internal harness-bootstrap descriptor now sits on top of the current graph model and captures the default `MockerTestBase` bootstrap knobs plus whether generated output would need an explicit `CreateComponentConstructionRequest()` override
+- focused tests cover direct graph creation, the harness-mapped component path, and the first harness-bootstrap descriptor cases without widening the public planning contract beyond `InstanceConstructionRequest` and `InstanceConstructionPlan`
+- representative generated consuming scenarios now compile against the real generator output rather than only generator-driver fixtures
+- parity tests now prove the generated harness path matches the supported runtime harness path for the same component shapes
+- measured evidence is now recorded in [generated harness setup benchmark results](../benchmarks/results/generated-harness-setup-net8.md), where the generated bootstrap-descriptor path holds a slight edge over the runtime fallback path with effectively identical allocations on the richer single-constructor benchmark
+
+What now moves past [#122](https://github.com/cwinland/FastMoq/issues/122):
+
+- broader generated-test settings and framework or runner targeting now live in [#162](https://github.com/cwinland/FastMoq/issues/162) rather than the graph and harness MVP
+- the first generated scenario or suite scaffolding slice now lives in [#136](https://github.com/cwinland/FastMoq/issues/136) through generated scenario execution helpers and suite-level shared setup hooks inside explicit partial harness targets
+- broader generated-test expansion and analyzer entry points still belong in [#123](https://github.com/cwinland/FastMoq/issues/123) and [#124](https://github.com/cwinland/FastMoq/issues/124)
+
+Preferred post-`#122` decision:
+
+- keep the public planning API unchanged
+- use [#162](https://github.com/cwinland/FastMoq/issues/162) to wire consumer-configurable generated-test settings before widening beyond the current fixed scaffold defaults
+- treat the current [#136](https://github.com/cwinland/FastMoq/issues/136) scaffold shape as the stable narrow baseline: explicit partial harness targets, companion partial hooks, and suite-level shared setup composition
+- only enrich the internal graph model further if a later generation layer proves that more dependency-order metadata is actually required for compilation or parity
+- move next into [#123](https://github.com/cwinland/FastMoq/issues/123) and [#124](https://github.com/cwinland/FastMoq/issues/124) only after the current scaffold boundary is stable enough to consume without re-deriving local defaults
+
## Suggested v5 Delivery Phases
### Phase 0: contract and package design
@@ -394,6 +474,8 @@ Ship:
### Phase 2: scenario and scaffold generation
+The first step of this phase is now in place for explicit partial harness targets.
+
Ship:
- generated suite or scenario scaffolds
@@ -463,10 +545,10 @@ The current doc plan now maps to these issue slices:
5. Tighten the existing logging, HTTP, and typed DI helper surfaces that are small enough to land as v4 quick wins.
6. Define graph metadata hooks and constructor-selection contracts for generator-targeted output.
7. Define `ScenarioBuilder` scaffolding hooks and regeneration-safe extension points for generated output.
-8. Define package-detection and target-test-shape rules for package-aware generation.
+8. Define package-detection and target-test-shape rules for package-aware generation. This is now implemented in the repo through the shared analyzer package matrix.
9. Normalize the broader blocking helper surfaces for logging, HTTP, Azure, Azure Functions, and typed DI-heavy setup.
10. Implement compile-time test graph and harness generation MVP.
-11. Implement generated scenario and suite scaffolding after the graph and harness MVP.
+11. Implement generated scenario and suite scaffolding after the graph and harness MVP. This is now in place for explicit partial harness targets through generated scenario execution helpers and suite-level shared setup hooks.
12. Implement generator-backed framework-helper builders for repeated test patterns.
13. Add full-test generation for supported existing services and other supported classes.
14. Add analyzer guidance for untested code plus package-aware suggestions before test generation.