Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
07c8636
feat(v1.7): add Condition and SkipWhen properties to [ForgeProperty]
superyyrrzz Apr 21, 2026
078c3bb
feat(v1.7): register FM0060-FM0064 diagnostics for Condition/SkipWhen
superyyrrzz Apr 21, 2026
416acf5
feat(v1.7): parse Condition/SkipWhen from [ForgeProperty] into Resolv…
superyyrrzz Apr 21, 2026
6188103
feat(v1.7): add ResolveConditionalForProperty helper for Condition/Sk…
superyyrrzz Apr 21, 2026
e8ae68c
feat(v1.7): wire Condition/SkipWhen into Forge new-instance path
superyyrrzz Apr 21, 2026
aa41c18
feat(v1.7): wire Condition/SkipWhen into ForgeInto existing-target path
superyyrrzz Apr 21, 2026
c4ee2ab
test(v1.7): cover Condition/SkipWhen across diagnostics, Forge, Forge…
superyyrrzz Apr 21, 2026
2f454ad
docs(v1.7): mark Feature 2 (Condition/SkipWhen) as Implemented
superyyrrzz Apr 21, 2026
0b71230
docs(v1.7): add Feature 2 implementation plan
superyyrrzz Apr 21, 2026
bc3fd83
fix(v1.7): address Copilot review feedback on Condition/SkipWhen
superyyrrzz Apr 21, 2026
7e0e761
fix(v1.7): tighten Condition/SkipWhen inheritance and guard emission
superyyrrzz Apr 21, 2026
b3120c2
fix(v1.7): apply Condition/SkipWhen guard to queued non-expression as…
superyyrrzz Apr 21, 2026
b0f9575
fix(v1.7): defer FM0064 to actual emission and remove stale comment
superyyrrzz Apr 21, 2026
d0a93c2
refactor(v1.7): extract conditional resolve+predicate-arg helper
superyyrrzz Apr 21, 2026
bb32d4e
fix(v1.7): align ForgeInto ConvertWith precedence with Forge
superyyrrzz Apr 21, 2026
8324689
fix(v1.7): avoid double-wrap in WrapQueuedEntriesWithConditionalGuard
superyyrrzz Apr 21, 2026
7c11c80
fix(v1.7): respect [Ignore] precedence in FM0062 pre-pass and ForgeIn…
superyyrrzz Apr 21, 2026
ca064e6
fix(v1.7): emit FM0062 for Condition/SkipWhen on init-only/required F…
superyyrrzz Apr 21, 2026
7185f2c
chore: reorder AnalyzerReleases.Unshipped.md by ascending rule ID
superyyrrzz Apr 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/SPEC-v1.7-projection-and-conditional.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ v1.7 closes three AutoMapper-parity gaps surfaced by the [Duende.IdentityServer.
| # | Feature | Issue | Effort | Status |
|---|---------|-------|--------|--------|
| 1 | Per-property LINQ projection (`SelectProperty`) | [#125](https://github.com/superyyrrzz/ForgeMap/issues/125) | Low | Planned |
| 2 | Conditional property assignment (`Condition`, `SkipWhen`) | [#126](https://github.com/superyyrrzz/ForgeMap/issues/126) | Medium | Planned |
| 2 | Conditional property assignment (`Condition`, `SkipWhen`) | [#126](https://github.com/superyyrrzz/ForgeMap/issues/126) | Medium | Implemented |
| 3 | Entity↔primitive mapping (`[ExtractProperty]`, `[WrapProperty]`) | [#127](https://github.com/superyyrrzz/ForgeMap/issues/127) | Medium | Planned |

Features 1 and 3 compose naturally: `SelectProperty` extracts a primitive from each element in a collection, while `[ExtractProperty]` / `[WrapProperty]` handle the single-element case. Both target the same join-table-entity pattern from opposite ends.
Expand Down
1,424 changes: 1,424 additions & 0 deletions docs/superpowers/plans/2026-04-21-v1.7-conditional-assignment.md

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions src/ForgeMap.Abstractions/ForgePropertyAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,24 @@ public ForgePropertyAttribute(string sourceProperty, string destinationProperty)
/// on the same destination property.
/// </summary>
public string? SelectProperty { get; set; }

/// <summary>
/// Name of a predicate method on the forger class. Called with the source property value;
/// when it returns false, the destination assignment is skipped.
/// Signature: <c>bool MethodName(TSourceProperty value)</c>. Static or instance, any accessibility.
/// Mutually exclusive with <see cref="SkipWhen"/> on the same <c>[ForgeProperty]</c>.
/// Cannot be combined with <c>[ForgeFrom]</c> or <c>[ForgeWith]</c> on the same destination property.
/// Not supported on properties bound to a constructor parameter, an <c>init</c> setter, or a <c>required</c> member.
/// </summary>
public string? Condition { get; set; }

/// <summary>
/// Name of a predicate method on the forger class. Called with the source object;
/// when it returns true, the destination assignment is skipped.
/// Signature: <c>bool MethodName(TSource source)</c>. Static or instance, any accessibility.
/// Mutually exclusive with <see cref="Condition"/> on the same <c>[ForgeProperty]</c>.
/// Cannot be combined with <c>[ForgeFrom]</c> or <c>[ForgeWith]</c> on the same destination property.
/// Not supported on properties bound to a constructor parameter, an <c>init</c> setter, or a <c>required</c> member.
/// </summary>
public string? SkipWhen { get; set; }
}
5 changes: 5 additions & 0 deletions src/ForgeMap.Generator/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ FM0056 | ForgeMap | Error | SelectPropertyMemberNotFound
FM0057 | ForgeMap | Error | SelectPropertyElementTypeIncompatible
FM0058 | ForgeMap | Error | SelectPropertyConflictsWithConverter
FM0059 | ForgeMap | Disabled | SelectPropertyApplied
FM0060 | ForgeMap | Error | ConditionAndSkipWhenBothSet
FM0061 | ForgeMap | Error | ConditionalPredicateInvalid
FM0062 | ForgeMap | Error | ConditionalNotSupportedOnInitOrCtor
FM0063 | ForgeMap | Error | ConditionalConflictsWithForgeFromOrWith
FM0064 | ForgeMap | Disabled | ConditionalAssignmentApplied
FM0072 | ForgeMap | Error | SelectPropertyConflictsWithForgeFromOrWith
FM0073 | ForgeMap | Error | SelectPropertyDestinationNotEnumerable
FM0075 | ForgeMap | Warning | SelectPropertyNotSupportedOnForgeInto
40 changes: 40 additions & 0 deletions src/ForgeMap.Generator/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -504,4 +504,44 @@ internal static class DiagnosticDescriptors
category: Category,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor ConditionAndSkipWhenBothSet = new(
id: "FM0060",
title: "Condition and SkipWhen are mutually exclusive",
messageFormat: "Property '{0}' has both Condition and SkipWhen set on the same [ForgeProperty] — choose one",
category: Category,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor ConditionalPredicateInvalid = new(
id: "FM0061",
title: "Conditional predicate method not found or has wrong signature",
messageFormat: "Predicate method '{0}' for property '{1}' was not found on the forger class or has the wrong signature; expected: bool {0}({2})",
category: Category,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor ConditionalNotSupportedOnInitOrCtor = new(
id: "FM0062",
title: "Condition/SkipWhen cannot be applied to init, required, or constructor-bound properties",
messageFormat: "Condition/SkipWhen cannot be applied to property '{0}' because it is set via constructor or init/required. Use [ForgeFrom] to choose between alternative constructions, or drop init if you need conditional post-construction assignment.",
category: Category,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor ConditionalConflictsWithForgeFromOrWith = new(
id: "FM0063",
title: "Condition/SkipWhen conflicts with [ForgeFrom] / [ForgeWith]",
messageFormat: "Property '{0}' has Condition/SkipWhen and is also targeted by [ForgeFrom] or [ForgeWith] — choose one",
category: Category,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor ConditionalAssignmentApplied = new(
id: "FM0064",
title: "Conditional assignment applied for property",
messageFormat: "Conditional assignment applied for property '{0}' via '{1}'",
category: Category,
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: false);
}
Loading
Loading