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.