Skip to content

Generated Constructors

[GenerateConstructor] removes hand-written constructor, field-assignment, and guard-clause boilerplate for a partial class. The generator emits one public constructor whose parameters, in declaration order, come from the class's eligible private readonly fields, plus any requested guard clauses -- built-in or custom, always as a direct compile-time call, never through reflection. This is a source-generation-only feature.

Why Not a Primary Constructor?

C# primary constructors reduce constructor syntax, but they give you no constructor body to place a guard clause in. Enforcing an invariant with a primary constructor still means writing a field or property initializer that repeats the parameter, the storage member, and the validation expression -- and that repetition grows with every dependency.

Roslyn source generators are additive: they can contribute new members to a partial type, but they cannot rewrite or inject statements into a constructor you already wrote -- primary or otherwise. So Needlr generates a complete constructor from field declarations that remain in your source, instead of trying to splice guard statements into a primary constructor it does not own. See ADR-0005 for the full design rationale.

Prerequisites

Generated constructors require Needlr's source-generation setup. Add the source-generation packages described in Getting Started; reflection-only configuration does not run this generator.

Needlr's current packages and examples target net10.0. If NexusLabs.Needlr.Generators is consumed independently by a project targeting an older framework, the generated built-in guard calls impose these minimum runtime API versions:

Guard Generated API Minimum .NET version
NotNull and NonNullableReferences ArgumentNullException.ThrowIfNull .NET 6
NotNullOrEmpty ArgumentException.ThrowIfNullOrEmpty .NET 7
NotNullOrWhiteSpace ArgumentException.ThrowIfNullOrWhiteSpace .NET 8

Bare constructor generation and custom guards depend only on the APIs used by the generated assignments and the application-provided guard method.

Quick Start

using NexusLabs.Needlr.Generators;

[GenerateConstructor]
public partial class UserService
{
    private readonly IRepository _repository;
}

Generated:

public UserService(IRepository repository)
{
    _repository = repository;
}

No guard is emitted -- see Default: No Guards below. Every eligible field becomes a parameter in declaration order, and _camelCase field names map to camelCase parameter names.

Authored vs. Generated Code

You author the field declarations (and any guard attributes); Needlr generates the constructor:

// You write:
using NexusLabs.Needlr.Generators;

[GenerateConstructor(ConstructorNullGuardMode.NonNullableReferences)]
public partial class TenantService
{
    private readonly IRepository _repository;

    [ConstructorGuard(ConstructorGuardKind.NotNullOrWhiteSpace)]
    private readonly string _tenantName;

    public IRepository Repository => _repository;
    public string TenantName => _tenantName;
}
// Needlr generates (simplified):
partial class TenantService
{
    /// <summary>Initializes a new instance of the <see cref="TenantService"/> class.</summary>
    /// <param name="repository">The value used to initialize <c>_repository</c>.</param>
    /// <param name="tenantName">The value used to initialize <c>_tenantName</c>.</param>
    /// <exception cref="ArgumentNullException">
    /// <paramref name="repository"/> or <paramref name="tenantName"/> is null.
    /// </exception>
    /// <exception cref="ArgumentException">
    /// <paramref name="tenantName"/> is empty or consists only of white-space characters.
    /// </exception>
    public TenantService(IRepository repository, string tenantName)
    {
        global::System.ArgumentNullException.ThrowIfNull(repository);
        global::System.ArgumentNullException.ThrowIfNull(tenantName);
        global::System.ArgumentException.ThrowIfNullOrWhiteSpace(tenantName);

        _repository = repository;
        _tenantName = tenantName;
    }
}

The generated constructor carries full XML documentation, generated by the generator itself from each field's name -- you do not write it by hand.

Migrating Existing Constructors

From a hand-written constructor

Replace the constructor parameters, guards, and assignments with the generation attribute while retaining the fields:

// Before
public sealed class UserService
{
    private readonly IRepository _repository;

    public UserService(IRepository repository)
    {
        ArgumentNullException.ThrowIfNull(repository);
        _repository = repository;
    }
}

// After
[GenerateConstructor(ConstructorNullGuardMode.NonNullableReferences)]
public sealed partial class UserService
{
    private readonly IRepository _repository;
}

From a guarded primary constructor

A guarded primary constructor normally repeats each dependency in a field initializer:

// Before
public sealed class UserService(IRepository repository)
{
    private readonly IRepository _repository =
        repository ?? throw new ArgumentNullException(nameof(repository));
}

// After
[GenerateConstructor(ConstructorNullGuardMode.NonNullableReferences)]
public sealed partial class UserService
{
    private readonly IRepository _repository;
}

The generated form preserves an ordinary public constructor while removing the repeated parameter, guard, and assignment code.

Finding mechanically replaceable constructors

NDLRGEN063 reports an Info diagnostic when one public hand-written or primary constructor has an exact generated-constructor equivalent. The analyzer preserves the authored parameter names, types, and order, and recognizes only assignments and built-in guards that Needlr can reproduce without changing behavior.

The diagnostic deliberately does not propose architectural refactors. Constructors that normalize or derive values, copy collections, transfer ownership, perform side effects, chain to another constructor, pass base-constructor arguments, or expose multiple overloads remain authored code.

Projects that require generated constructors can promote the suggestion:

[*.cs]
dotnet_diagnostic.NDLRGEN063.severity = warning

Default: No Guards

[GenerateConstructor] with no argument is equivalent to [GenerateConstructor(ConstructorNullGuardMode.None)]: field assignments are generated as-is, with no automatic runtime guard, even for a non-nullable reference field:

[GenerateConstructor]
public partial class UserService
{
    private readonly IRepository _repository;
}

// Generated: no guard.
// public UserService(IRepository repository)
// {
//     _repository = repository;
// }

Automatic null guards require an explicit class-level opt-in:

[GenerateConstructor(ConstructorNullGuardMode.NonNullableReferences)]
public partial class UserService
{
    private readonly IRepository _repository;
}

// Generated:
// public UserService(IRepository repository)
// {
//     ArgumentNullException.ThrowIfNull(repository);
//     _repository = repository;
// }

ConstructorNullGuardMode.NonNullableReferences only ever affects non-nullable reference-type fields. Nullable reference fields and every value-type field (including Nullable<T>) are never guarded by the class-level default -- see Nullability.

Field-Triggered Generation

A positive field-level guard attribute enables constructor generation even when the class has no [GenerateConstructor] at all:

public partial class TenantService
{
    private readonly IRepository _repository;

    [ConstructorGuard(ConstructorGuardKind.NotNullOrWhiteSpace)]
    private readonly string _tenantName;
}

This behaves exactly as though the class had [GenerateConstructor] at the default ConstructorNullGuardMode.None:

  • Every eligible field becomes a constructor parameter -- not just the annotated one.
  • Only explicitly annotated fields receive a guard; _repository above is unguarded and accepts null.
// Generated:
// public TenantService(IRepository repository, string tenantName)
// {
//     ArgumentException.ThrowIfNullOrWhiteSpace(tenantName);
//
//     _repository = repository;
//     _tenantName = tenantName;
// }

A positive trigger is a built-in guard kind other than None, a custom guard type, or an alias attribute defined with [ConstructorGuardDefinition]. [ConstructorIgnore] and a bare [ConstructorGuard(ConstructorGuardKind.None)] are exclusion-only modifiers -- applying either one, on its own, with no other trigger present on the class, has no effect and is flagged by NDLRGEN045.

Eligible Fields, Order, and Naming

An eligible field is a private, instance, readonly field without a field initializer. Fields are considered in this order:

  1. Fields are read from the type's declaring syntax in deterministic declaration order (by source file path, then by position within the file), so the parameter list is stable across incremental builds.
  2. [ConstructorIgnore] removes an otherwise-eligible field from the parameter list entirely.
  3. Field names are normalized to parameter names by stripping a single leading underscore and lower-casing the first letter (_tenantName -> tenantName); a name that collides with a C# keyword is escaped with @.

The type itself must satisfy additional constraints:

  • Partial. Only a partial class can receive a generator-contributed constructor (NDLRGEN039).
  • Top-level, non-record class. Records and nested classes are unsupported (NDLRGEN040).
  • No existing explicit instance constructor. Generation is skipped entirely -- not merged -- when the class already declares its own constructor (NDLRGEN041).
  • A base type with an accessible parameterless constructor, including the common case of no explicit base constructors at all (e.g. BackgroundService), since the generated constructor relies on the implicit : base() call (NDLRGEN042).
  • At least one eligible field (NDLRGEN043), and no two eligible fields normalizing to the same parameter name (NDLRGEN044).

Generic classes are supported: the generated constructor repeats the class's own type parameter list.

Nullability

Only non-nullable reference-type fields are eligible for the class-level NonNullableReferences automatic guard. Every other field shape is left alone by the class-level default:

[GenerateConstructor(ConstructorNullGuardMode.NonNullableReferences)]
public partial class Widget
{
    private readonly IRepository _repository;          // guarded automatically (non-nullable reference)
    private readonly IRepository? _optionalRepository; // never guarded (nullable reference: null is explicitly valid)
    private readonly int _retryCount;                  // never guarded (non-nullable value type: null is impossible)
    private readonly int? _timeoutSeconds;              // never guarded by the default, but CAN take an explicit guard below
}

ConstructorGuardKind.NotNull can still be requested explicitly on a Nullable<T> value-type field, because a runtime null is possible for int? even though it is impossible for a non-nullable int:

[GenerateConstructor(ConstructorNullGuardMode.NonNullableReferences)]
public partial class Widget
{
    [ConstructorGuard(ConstructorGuardKind.NotNull)]
    private readonly int? _timeoutSeconds;
}

Requesting NotNull on a non-nullable value type (e.g. plain int) is rejected at compile time by NDLRGEN048, because the guard could never fail.

Built-In Guards

Kind Applies to Generated call Failure
None any eligible field (none) --
NotNull any reference type, Nullable<T>, or type parameter not constrained to a value type ArgumentNullException.ThrowIfNull(value) ArgumentNullException when null
NotNullOrEmpty string ArgumentException.ThrowIfNullOrEmpty(value) ArgumentNullException when null; ArgumentException when ""
NotNullOrWhiteSpace string ArgumentException.ThrowIfNullOrWhiteSpace(value) ArgumentNullException when null; ArgumentException when empty or white space

Every built-in guard emits the exact same standard .NET guard-clause call a hand-written constructor would use, so the reported exception type and ParamName are identical to what you would get by hand -- ParamName is always the generated constructor's parameter name.

The generated constructor's XML documentation also describes the exceptions implied by its effective built-in guards. Repeated guards and parameters are coalesced deterministically into one ArgumentNullException element and, when applicable, one ArgumentException element. None, a suppressed class-level default, and custom guards do not produce exception claims.

NotNullOrEmpty and NotNullOrWhiteSpace are rejected on a non-string field by NDLRGEN048. A guard kind not defined on ConstructorGuardKind (e.g. produced by casting an out-of-range integer) is rejected by NDLRGEN047.

NotEmpty is deferred

A generic, collection-agnostic NotEmpty guard (covering strings, collections, and arbitrary IEnumerable<T> uniformly) is deferred until its semantics are well-defined across those shapes. Use a direct custom guard type such as CollectionNotEmptyGuard shown below in the meantime.

Guard Composition and Suppression

A field can carry more than one [ConstructorGuard] -- guards are additive, applied in a deterministic order:

  1. The class-level automatic default (when applicable) is composed first.
  2. Every explicit [ConstructorGuard] on the field follows, in source declaration order.
  3. ConstructorGuardKind.None suppresses the class-level default for that field only -- it does not suppress any other explicit guard on the same field.
  4. Identical effective guard calls (the same built-in kind, or the same resolved custom guard type and method) are only emitted once, even if requested more than once.
[GenerateConstructor(ConstructorNullGuardMode.NonNullableReferences)]
public partial class OrderService
{
    // Suppresses the automatic NotNull default for this field only.
    [ConstructorGuard(ConstructorGuardKind.None)]
    private readonly IRepository _repository;

    // Class default (NotNull) applies first, then this explicit guard.
    [ConstructorGuard(ConstructorGuardKind.NotNullOrWhiteSpace)]
    private readonly string _orderId;
}

// Generated:
// public OrderService(IRepository repository, string orderId)
// {
//     ArgumentNullException.ThrowIfNull(orderId);
//     ArgumentException.ThrowIfNullOrWhiteSpace(orderId);
//
//     _repository = repository;
//     _orderId = orderId;
// }

Custom Guards

Custom guards use compile-time-resolved direct calls. Needlr never invokes a guard through reflection, and never splices an arbitrary string expression as source.

Direct Guard Types

Point a field at a custom guard type. The generator resolves and calls its conventional static void Validate(T value, string parameterName) method directly:

public static class CollectionNotEmptyGuard
{
    public static void Validate<T>(IReadOnlyCollection<T>? value, string parameterName)
    {
        ArgumentNullException.ThrowIfNull(value, parameterName);

        if (value.Count == 0)
        {
            throw new ArgumentException("Collection must not be empty.", parameterName);
        }
    }
}

public partial class OrderService
{
    [ConstructorGuard(typeof(CollectionNotEmptyGuard))]
    private readonly IReadOnlyCollection<Order> _orders;
}

// Generated call: CollectionNotEmptyGuard.Validate(orders, nameof(orders));

Explicit Method Selectors

Select an explicit method name -- instead of relying on the conventional Validate name -- with nameof:

public static class NumberGuards
{
    public static void ValidatePositive(int value, string parameterName)
    {
        if (value <= 0)
        {
            throw new ArgumentOutOfRangeException(parameterName, value, "Value must be positive.");
        }
    }
}

public partial class RetryPolicy
{
    [ConstructorGuard(typeof(NumberGuards), nameof(NumberGuards.ValidatePositive))]
    private readonly int _retryCount;
}

// Generated call: NumberGuards.ValidatePositive(retryCount, nameof(retryCount));

A custom guard method must be accessible, static, return void, and take exactly two parameters: a value compatible with the field's type (directly, or via generic type-parameter inference) and a trailing string parameter name -- no ref/out/in parameters. The analyzer resolves and validates the exact method at compile time; see NDLRGEN049 through NDLRGEN052 for every way this resolution can fail.

Custom Alias / Meta-Attributes

Wrap a custom guard behind your own domain-specific attribute by decorating it with [ConstructorGuardDefinition]:

[ConstructorGuardDefinition(typeof(CollectionNotEmptyGuard))]
[AttributeUsage(AttributeTargets.Field)]
public sealed class CollectionNotEmptyAttribute : Attribute
{
}

public partial class OrderService
{
    [CollectionNotEmpty]
    private readonly IReadOnlyCollection<Order> _orders;
}

// Generated call: CollectionNotEmptyGuard.Validate(orders, nameof(orders));

[CollectionNotEmpty] and [ConstructorGuard(typeof(CollectionNotEmptyGuard))] normalize to the exact same internal guard model and generated call. This works whether the alias attribute is declared in the current project or in a referenced assembly -- Roslyn can inspect the meta-attribute either way. An alias attribute is itself validated once, at its own declaration, by NDLRGEN053 and NDLRGEN054.

Parameterized Alias Arguments

A parameterized alias such as [MinCount(3)] automatically forwards every one of its own positional constructor arguments, in declared order, onto the resolved guard method call -- spliced between the guarded value and the trailing nameof parameter name:

public static class MinCountGuard
{
    public static void Validate<T>(IReadOnlyCollection<T>? value, int minimum, string parameterName)
    {
        ArgumentNullException.ThrowIfNull(value, parameterName);

        if (value.Count < minimum)
        {
            throw new ArgumentException($"Must contain at least {minimum} element(s).", parameterName);
        }
    }
}

[ConstructorGuardDefinition(typeof(MinCountGuard))]
[AttributeUsage(AttributeTargets.Field)]
public sealed class MinCountAttribute : Attribute
{
    public MinCountAttribute(int minimum) => Minimum = minimum;

    public int Minimum { get; }
}

public partial class BulkOrderRequest
{
    [MinCount(3)]
    private readonly IReadOnlyCollection<string> _lineItems;
}

// Generated call: MinCountGuard.Validate(lineItems, 3, nameof(lineItems));

The resolved guard method must declare exactly one middle parameter per forwarded argument -- value, then one parameter per alias constructor argument in order, then the trailing string parameterName -- and each middle parameter's type must be compatible with its corresponding forwarded argument's type (directly, or via generic method type-parameter unification with the field's type, as MinCountGuard.Validate<T> demonstrates above). A bare alias with no positional constructor arguments (e.g. [CollectionNotEmpty]) forwards zero arguments, so its generated call remains byte-for-byte identical to the two-argument shape the feature has always emitted.

Supported forwarded argument shapes

Every positional alias constructor argument is a compiler-validated attribute constant (a Roslyn TypedConstant), never raw source text or a runtime value. Only the following constant shapes can be forwarded:

Shape Example alias usage Rendered literal
null [Defaultable(null)] null
bool [Flagged(true)] true
Signed/unsigned integral (sbyte, byte, short, ushort, int, uint, long, ulong) [Bounds(7u, 123456789012L, 9999999999UL)] 7u, 123456789012L, 9999999999UL (the C# suffix required for the literal to round-trip to the same runtime type)
char [Label("prefix", ':')] ':'
string [Label("prefix", ':')] "prefix"
enum member [Risk(RiskLevel.High)] global::TestApp.RiskLevel.High (a fully qualified cast expression for a combined [Flags] value with no single matching member)
System.Type, including open generics [OfType(typeof(List<>))] typeof(global::System.Collections.Generic.List<>)

Optional alias constructor parameters

An omitted optional alias constructor argument still forwards -- Roslyn's AttributeData.ConstructorArguments always includes one entry per declared constructor parameter, filling in the parameter's effective default value for any argument the usage omits:

[ConstructorGuardDefinition(typeof(RetryGuard))]
[AttributeUsage(AttributeTargets.Field)]
public sealed class RetryAttribute : Attribute
{
    public RetryAttribute(int maxAttempts = 5) { }
}

[Retry]
private readonly int _value;

// Generated call: RetryGuard.Validate(value, 5, nameof(value));

Unsupported in this version

The following are diagnosed by NDLRGEN055 rather than silently dropped or approximated:

  • Arrays and params arguments -- forwarding an array as a single positional literal is inherently ambiguous.
  • float and double arguments -- floating-point literals can silently lose round-trip precision.
  • Named attribute arguments and properties -- only positional constructor arguments are ever forwarded, regardless of shape.

When an alias usage's forwarded argument is a supported shape but is incompatible with the resolved guard method's corresponding middle parameter -- either because the method's effective arity does not match the number of forwarded arguments, or because a middle parameter's type is incompatible with its forwarded argument -- NDLRGEN056 is reported instead. NDLRGEN051 and NDLRGEN052 retain their existing general invalid/ambiguous guard-method behavior for every other resolution failure.

Factory, DI, and AOT Integration

Generated-constructor generation shares one constructor-parameter model with Needlr's type registry, [GenerateFactory], and its Native AOT factory emission -- a generated constructor is never invisible to Needlr's own registration and never requires [DeferToContainer].

Automatic DI Registration

A type is automatically registered when every generated-constructor parameter is itself a container-resolvable service type:

[GenerateConstructor(ConstructorNullGuardMode.NonNullableReferences)]
public partial class UserService
{
    private readonly IRepository _repository;
}

var serviceProvider = new Syringe()
    .UsingSourceGen()
    .BuildServiceProvider();

var service = serviceProvider.GetRequiredService<UserService>(); // resolves the generated constructor's dependency

[GenerateFactory] Interoperability

A class with a mix of injectable and runtime (value type, string, delegate) fields is excluded from automatic registration -- the same rule applied to a hand-written constructor -- but works with [GenerateFactory] exactly as it would with a hand-written constructor:

[GenerateFactory]
[GenerateConstructor]
public partial class ReportBuilder
{
    private readonly IRepository _repository; // injectable
    private readonly string _templateName;    // runtime
}
var factory = serviceProvider.GetRequiredService<IReportBuilderFactory>();
var report = factory.Create("acme-template"); // _repository resolved from DI, templateName forwarded

The factory call site binds arguments by parameter name, not by the field declaration order relative to injectable/runtime grouping -- interleaving a runtime field before an injectable field still binds correctly. See Factory Delegates for the full factory story.

Native AOT

Needlr's type registry, factory emission, and generated constructor share one model, so the generated activation path adds no runtime reflection and uses direct, statically-known constructor calls. Custom guard implementations must independently remain compatible with their application's Native AOT and trimming requirements.

Primary-Constructor Limitation

Needlr's generator cannot add guard statements to a primary constructor you already declared, because Roslyn source generators can only add members to a partial type -- they cannot rewrite or inject statements into an existing member. This is why the feature generates an entirely new constructor from field declarations instead of trying to augment a primary constructor:

// NOT supported: a source generator cannot inject a guard into this constructor body.
public partial class UserService(IRepository repository)
{
}

Use [GenerateConstructor] with private readonly fields (not primary-constructor parameters) whenever you need generated guard clauses.

Unsupported and Deferred Scenarios

The following are intentionally out of scope for the initial implementation and are diagnosed rather than silently approximated:

  • Records and nested types (NDLRGEN040).
  • Classes with an existing explicit instance constructor (NDLRGEN041).
  • Base types that require constructor arguments (NDLRGEN042).
  • Keyed fields or properties -- generated-constructor generation only ever considers plain private readonly fields; there is no support for producing a [FromKeyedServices]-annotated generated-constructor parameter from a field or property annotation.
  • Properties as a source of this class constructor's parameters -- only fields are considered. Positional records can add explicitly marked properties through Generated Record Constructor Overloads.
  • NotEmpty as a built-in guard kind (see Built-In Guards above).
  • Array/params and floating-point (float/double) parameterized alias arguments, and every named attribute argument or property -- diagnosed by NDLRGEN055 (see Parameterized Alias Arguments above); only the scalar constant shapes in that section's table are forwarded.
  • IHubRegistrationPlugin implementations -- SignalR hub-registration plugins require parameterless activation and cannot use generated-constructor generation at all; see NDLRSIG003.
  • Raw expressions, cross-field validation, or reflection supplied as attribute arguments -- every guard is a typed guard kind or a compile-time-resolved direct method reference (with, at most, a fixed number of compiler-validated positional constants forwarded to it), never an arbitrary spliced expression or a runtime-reflected call.

Attribute Reference

Attribute / Type Target Description
[GenerateConstructor] Class Enables constructor generation. The parameterless form is equivalent to [GenerateConstructor(ConstructorNullGuardMode.None)].
[GenerateConstructor(ConstructorNullGuardMode)] Class Enables constructor generation with an explicit null-guard mode.
ConstructorNullGuardMode.None -- No automatic null guards (default).
ConstructorNullGuardMode.NonNullableReferences -- Automatically guards every eligible non-nullable reference-type field with NotNull.
[ConstructorGuard(ConstructorGuardKind)] Field Requests a built-in guard kind for the field. A positive kind also triggers generation on its own.
[ConstructorGuard(Type)] Field Requests a custom guard type using its conventional static void Validate(T, string) method. Also a positive trigger.
[ConstructorGuard(Type, string)] Field Requests a custom guard type and an explicit method name (typically via nameof). Also a positive trigger.
ConstructorGuardKind.None -- No guard; explicitly suppresses an applicable class-level default for this field only.
ConstructorGuardKind.NotNull -- Throws ArgumentNullException when null.
ConstructorGuardKind.NotNullOrEmpty -- Throws ArgumentNullException/ArgumentException for null/empty strings.
ConstructorGuardKind.NotNullOrWhiteSpace -- Throws ArgumentNullException/ArgumentException for null/empty/white-space-only strings.
[ConstructorIgnore] Field Excludes an otherwise-eligible field from generated-constructor parameters. Exclusion-only; has no effect without a generation trigger elsewhere on the class.
[ConstructorGuardDefinition(Type)] Attribute type Declares an application-defined field or property attribute as an alias for a direct custom guard, using its conventional Validate method. Property usages require [RecordConstructorOverloadParameter].
[ConstructorGuardDefinition(Type, string)] Attribute type Declares an alias attribute for a custom guard with an explicit method name.

[ConstructorGuard] allows multiple applications on the same field (AllowMultiple = true); the other attributes each allow exactly one application.

Analyzers

Diagnostic Severity Description
NDLRGEN039 Error Generated-constructor type must be partial
NDLRGEN040 Error Generated-constructor type shape is unsupported (record or nested type)
NDLRGEN041 Error Generated-constructor conflicts with an explicit constructor
NDLRGEN042 Error Generated-constructor base type requires a parameterless constructor
NDLRGEN043 Error No eligible field for generated-constructor generation
NDLRGEN044 Error Generated-constructor parameter names collide
NDLRGEN045 Warning Constructor guard attribute has no effect
NDLRGEN046 Error Constructor guard attribute applied to an ineligible field
NDLRGEN047 Error Invalid constructor guard enum value
NDLRGEN048 Error Constructor guard incompatible with field type
NDLRGEN049 Error Custom constructor guard type is invalid
NDLRGEN050 Error Custom constructor guard method name is invalid
NDLRGEN051 Error Custom constructor guard method is invalid
NDLRGEN052 Error Custom constructor guard method is ambiguous
NDLRGEN053 Error [ConstructorGuardDefinition] target is invalid
NDLRGEN054 Error [ConstructorGuardDefinition] guard contract is unresolved
NDLRGEN055 Error Constructor guard alias usage argument is unsupported
NDLRGEN056 Error Custom constructor guard method is incompatible with forwarded alias arguments
NDLRSIG003 Error IHubRegistrationPlugin implementation cannot use generated-constructor generation

Namespace Import

[GenerateConstructor], [ConstructorGuard], [ConstructorIgnore], [ConstructorGuardDefinition], ConstructorGuardKind, and ConstructorNullGuardMode are all in the NexusLabs.Needlr.Generators namespace:

using NexusLabs.Needlr.Generators;

This is intentional -- it signals that the feature is source-generation-only.

Full Runnable Example

src/Examples/SourceGen/GeneratedConstructorExample is a runnable console application exercising every scenario described on this page: bare generation, the NonNullableReferences mode resolved through a real Syringe, field-triggered implicit generation, both built-in string guards, nullable/value-type behavior, a direct custom guard type, an explicit method selector, an application-defined alias attribute, a parameterized alias attribute ([MinCount(3)]) that forwards its own positional argument onto the resolved guard call, and [GenerateFactory] interoperability.