Projections¶
Optional and validated wrappers that make null-checking less painful.
See also: Queries | Emit | Extensions
Overview¶
Roslyn symbols are often nullable, requiring constant null checks. Projections wrap these nullable values in types that provide safe access and fluent transformations:
| Type | Description |
|---|---|
| OptionalSymbol | A symbol that may or may not be present |
| ValidSymbol | A symbol guaranteed to be non-null |
| OptionalAttribute | An attribute that may or may not be present |
| ValidAttribute | An attribute guaranteed to be non-null |
| AttributeQuery | Base for strongly-typed attribute query wrappers |
| OptionalArgument | An attribute argument that may or may not exist |
| OptionalValue | A general-purpose optional wrapper |
| Syntax Wrappers | Optional/Valid wrappers for syntax nodes |
| XmlDocumentation | Parsed XML documentation from a symbol |
| EquatableArray | Drop-in ImmutableArray<T> replacement with sequence equality |
| Snapshots | Pipeline-safe materializations of Roslyn symbols |
| PipelineModel | [PipelineModel] attribute and DSRK analyzers |
The Pattern¶
// Without projections — null checks everywhere
var attr = symbol.GetAttributes().FirstOrDefault(a => a.AttributeClass?.Name == "MyAttribute");
if (attr == null) return;
var value = attr.ConstructorArguments.FirstOrDefault().Value;
if (value is not string s) return;
// finally use s
// With projections — fluent, null-safe operations
var value = symbol
.GetAttribute("MyAttribute")
.ConstructorArg<string>(0)
.OrDefault("fallback");
Real-World Examples¶
Extract Attribute Configuration¶
var config = symbol
.GetAttribute("RetryAttribute")
.WithArgs(a => new RetryConfig
{
MaxRetries = a.NamedArg<int>("MaxRetries").OrDefault(3),
DelayMs = a.NamedArg<int>("DelayMs").OrDefault(1000),
ExponentialBackoff = a.NamedArg<bool>("Exponential").OrDefault(false)
})
.OrDefault(RetryConfig.Default);
Early Exit Pattern in Analyzers¶
protected override bool ShouldReport(ValidSymbol<INamedTypeSymbol> type)
{
var target = GetFirstInvalidTarget(type);
return target.HasValue;
}
private static OptionalSymbol<INamedTypeSymbol> GetFirstInvalidTarget(ValidSymbol<INamedTypeSymbol> type)
{
return OptionalSymbol<INamedTypeSymbol>.FromNullable(
type.GetAttributes("EffectsModule")
.FirstOrDefault(t => !t.TargetType.IsInterface)
?.TargetType.Value
);
}
Safe Type Navigation¶
var elementType = typeSymbol
.AsOptional()
.Where(t => t.IsGenericType && t.Name == "List")
.Map(t => t.SingleTypeArgument)
.OrDefault(OptionalSymbol<ITypeSymbol>.Empty());
Validate Before Processing¶
public void Process(OptionalSymbol<IMethodSymbol> method)
{
if (method.IsNotValid(out var valid))
{
ReportError("Method symbol required");
return;
}
// valid is ValidSymbol<IMethodSymbol> — no null checks needed
var name = valid.Name;
var isAsync = valid.IsAsync;
var parameters = valid.Value.Parameters;
}
Chain Optional Operations¶
var serviceName = symbol
.GetAttribute("ServiceAttribute")
.ConstructorArg<INamedTypeSymbol>(0)
.Map(t => t.Name)
.OrDefault(() => symbol.Name + "Service");
Work with Generic Attributes¶
// For [Handler<TRequest, TResponse>]
var attr = symbol.GetAttribute("Handler");
var requestType = attr.GetTypeArgument(0).OrThrow("Request type required");
var responseType = attr.GetTypeArgument(1).OrThrow("Response type required");
Check Type Hierarchy¶
// Check if a type implements IDisposable
if (typeSymbol.ImplementsInterface("IDisposable"))
{
// Generate dispose pattern
}
// Check if inherits from a specific base class
if (typeSymbol.InheritsFrom("ControllerBase"))
{
// Handle controller-specific generation
}
// Iterate all base types
foreach (var baseType in typeSymbol.GetBaseTypes())
{
Console.WriteLine($"Inherits from: {baseType.Name}");
}
// Get all interfaces including inherited ones
var allInterfaces = typeSymbol.GetAllInterfaces()
.Select(i => i.FullyQualifiedName)
.ToList();