Skip to content

Constraints vs Alternatives

Are there alternatives beyond generic constraints? This appendix compares the interface constraint approach chosen by Functorium with other alternatives. By analyzing the pros and cons of each approach, we understand why the interface hierarchy + generic constraints is the best choice.


1. Interface Constraints (Functorium Approach)

Section titled “1. Interface Constraints (Functorium Approach)”
where TResponse : IFinResponse, IFinResponseFactory<TResponse>
ItemEvaluation
Type safetyGuaranteed at compile time
ReflectionNot required (0 sites)
PerformanceOptimal (static dispatch)
Code complexityRequires interface hierarchy design
ExtensibilityExtensible by adding new interfaces
IDE supportFull auto-completion and refactoring support
// Runtime type inspection inside Pipeline
var isSuccProp = typeof(TResponse).GetProperty("IsSucc");
var isSucc = (bool)isSuccProp!.GetValue(response)!;
// CreateFail also requires reflection
var createFail = typeof(TResponse).GetMethod("CreateFail", BindingFlags.Static | BindingFlags.Public);
var failResponse = (TResponse)createFail!.Invoke(null, [error])!;
ItemEvaluation
Type safetyValidated only at runtime (no compile-time guarantee)
ReflectionRequired in multiple places (3+ sites)
PerformanceReflection overhead (on every request)
Code complexityPipeline internals become complex
ExtensibilityReflection code must change when new properties/methods are added
IDE supportString-based, risk of missing during refactoring
public TResponse Handle(dynamic request, Func<TResponse> next)
{
dynamic response = next();
if (response.IsSucc) { ... }
return response;
}
ItemEvaluation
Type safetyNone (all checks at runtime)
ReflectionUses reflection internally
PerformanceReflection + DLR overhead
Code complexitySimple but unsafe
ExtensibilityCannot detect typos, runtime errors
IDE supportNo auto-completion
// Source Generator auto-generates Pipeline code
[GeneratePipeline]
public partial class ValidationPipeline<TResponse> { }
ItemEvaluation
Type safetyGenerated code is type-safe
ReflectionNot required
PerformanceOptimal (generated at compile time)
Code complexityGenerator itself is complex
ExtensibilityRequires modifying the generator (steep learning curve)
IDE supportVaries by generator
public object Handle(object request, Func<object> next)
{
var response = next();
if (response is IFinResponse fin && fin.IsSucc) { ... }
return response;
}
ItemEvaluation
Type safetyPartial (casting can fail)
ReflectionNot required but boxing occurs
PerformanceBoxing/unboxing overhead
Code complexityCasting code scattered throughout
ExtensibilityCasting code must change when new types are added
IDE supportLimited

A comparison of all five approaches by key criteria:

CriteriaInterface ConstraintsReflectiondynamicSource Genobject Casting
Compile-time safetyOXXOPartial
No reflectionOXXOO
Optimal performanceOXXOPartial
Design costMediumLowLowHighLow
MaintainabilityOXXMediumX
IDE supportOXXMediumPartial

Every request passes through Pipelines, so the performance overhead of reflection or dynamic accumulates.

If Pipeline constraints are wrong, runtime exceptions occur. Interface constraints prevent this at compile time.

Combining C# 11’s static abstract members with the CRTP pattern allows calling static factory methods through interfaces. This is the key to reflection-free CreateFail calls.

Each Pipeline requires only the capabilities it needs as constraints, so there are no unnecessary dependencies. The Validation Pipeline only needs CreateFail, so it constrains only IFinResponseFactory<TResponse>.


The following appendix examines the Railway Oriented Programming pattern implemented by FinResponse<A>’s Map and Bind chains, and the relationship between Pipelines and ROP.

Appendix C: Railway Oriented Programming Reference