On this page

Generics and constraints

A generic declaration keeps a type relationship without replacing values with Object. ProcessingBatch <of Customer> holds Customers; ImportDraftFactory <of CustomerImportDraft> returns CustomerImportDraft. Constraints describe which operations the generic body may perform and which actual types callers may supply.

Declare parameters and supply arguments

Put the delimited <of ...> list on the declaration name. In class ImportDraftFactory <of T>, T is a parameter of the declaration; in ImportDraftFactory <of CustomerImportDraft>, CustomerImportDraft is an argument constructing a CLR type. Generic action parameters use the same delimiters. Nested type arguments keep their own delimiters, as in Dictionary <of String, List <of Customer>>.

The space before the opening angle bracket is formatter style. Use the same bounded spelling in declarations, calls, fields and needs. This does not introduce a separate generic runtime or change ordinary CLR identity.

Method inference uses supplied arguments and their generic relationships. A type appearing only in a return position does not become inferable from an assignment target. Choose explicit action arguments when needed, as explained in call binding.

Construct typed import drafts

An import needs a fresh draft for each row, a batch identifier and the draft's own row initialization. The factory needs three separate capabilities:

Restriction What the body can rely on
where T inherits ImportDraft The actual base-class contract, including writable BatchId
where T implements IImportDraft The actual interface contract for SetSourceRow
where T has constructor Ordinary public parameterless construction, expressed as new T()

The following is a complete, independent declaration example using ordinary classes and an interface:

import Global.System

public interface IImportDraft
    action SetSourceRow(row as Integer)
end interface

public class ImportDraft
    public BatchId as Guid = Guid.Empty
end class

public class CustomerImportDraft
    inherits ImportDraft
    implements IImportDraft

    public SourceRow as Integer with private set = 0
    public Email as String = ""

    public action SetSourceRow(row as Integer)
        implements IImportDraft.SetSourceRow
        SourceRow = row
    end action
end class

public class ImportDraftFactory <of T>
    where T inherits ImportDraft
    where T implements IImportDraft
    where T has constructor

    public action Create(batchId as Guid, row as Integer)
        returns T
        require row > 0
        let draft = new T()
        draft.BatchId = batchId
        draft.SetSourceRow(row)
        return draft
    end action
end class

public class CustomerImports
    public static action Prepare(batchId as Guid, row as Integer, email as String)
        returns CustomerImportDraft
        let factory = new ImportDraftFactory <of CustomerImportDraft>()
        let draft = factory.Create(batchId, row)
        draft.Email = email
        return draft
    end action
end class

The factory returns the concrete T. Prepare therefore receives CustomerImportDraft and can access Email without an Object cast. Each successful Create makes a fresh instance; the factory does not cache one draft for every row. The entry requirement runs before construction. The example does not catch construction, assignment or interface-body failures, return absence on failure or supply rollback for side effects.

SetSourceRow has explicit interface intent. A same-named method on an unrelated class is insufficient: these are CLR inheritance/interface constraints, not a structural shape test. The three where lines combine into one legal constraint set. The draft retains the supplied email for later business validation; the factory only establishes batch/row structure.

Reference C# contract and body

The following member declarations match the public nested types in the isolated native fixture, with ordinary System imports. The CustomerImports caller shows how application code uses those declarations.

public interface IImportDraft
{
    void SetSourceRow(int row);
}

public class ImportDraft
{
    public Guid BatchId { get; set; } = Guid.Empty;
}

public class CustomerImportDraft : ImportDraft, IImportDraft
{
    public int SourceRow { get; private set; }
    public string Email { get; set; } = "";
    public void SetSourceRow(int row) => SourceRow = row;
}

public class ImportDraftFactory<T>
    where T : ImportDraft, IImportDraft, new()
{
    public T Create(Guid batchId, int row)
    {
        if (row <= 0)
            throw new ArgumentException("Source row must be positive: row > 0", nameof(row));
        T draft = new T();
        draft.BatchId = batchId;
        draft.SetSourceRow(row);
        return draft;
    }
}

The C# backend places the base constraint first and new() last. That backend ordering does not turn the source's separate restrictions into runtime validation. For a statically invalid source type argument, the compiler reports the constraint failure before execution; it must not construct an object to discover whether its methods happen to work.

Constructor capability is a precise contract

A public parameterless constructor satisfies the ordinary class requirement. A private constructor does not. A constructor with one optional argument is still a one-parameter constructor; being callable with omitted arguments does not make it the zero-parameter constructor required by new(). Ordinary implicit constructor and value-type rules remain the CLR/C# rules.

An abstract class cannot be instantiated through a C# new-constrained generic parameter. The constraint does not authorize DI to fill constructor arguments, select a different factory or bypass accessibility. It also does not mean that execution of a valid constructor is guaranteed to succeed.

Use a constructed type in needs

A constructed generic dependency is still an ordinary dependency:

public class CustomerImportSession
    needs
        drafts as ImportDraftFactory <of CustomerImportDraft>
    end needs

    public action Prepare(batchId as Guid, row as Integer)
        returns CustomerImportDraft
        return drafts.Create(batchId, row)
    end action
end class

This class borrows its supplied factory. A host/composition must provide it under the existing registration and lifetime rules; the needs declaration creates no registration or open-generic policy. A generic class can likewise request dependencies involving its own declared parameters, and nested generic dependencies retain their actual CLR types. See services and ownership.

Diagnostics and cases

Case Required distinction
Type has a BatchId member but does not inherit ImportDraft Reject the base-class constraint; matching shape is insufficient
Type has SetSourceRow but does not implement IImportDraft Reject the interface constraint
Type has only a private constructor or a constructor taking optional inputs Reject the ordinary class constructor capability
Restrictions cannot form a legal C#/.NET constraint set Diagnose the declaration; do not invent another runtime type model
A required method type argument occurs only in its return type Require explicit arguments rather than infer from the assignment target
Valid type, invalid row value at execution Runtime entry-contract failure before construction
Type/dependency declaration alone No constructor/factory activation or service registration
A reached SetSourceRow body throws Failure prevents a successful result; it is not missing input or an empty selection

The normative rules retain normal imported CLR constraints too. Advanced variance/unsafe/ref-struct/static-abstract syntax is not implied by this application model.