Projections¶
Optional and validated wrappers that make null-checking less painful.
See also: Queries | Emit | Extensions | Roslyn Toolkit README
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 | Purpose |
|---|---|
OptionalSymbol<T> | A symbol that may or may not be present |
ValidSymbol<T> | 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 |
OptionalArgument<T> | An attribute argument that may or may not exist |
OptionalValue<T> | A general-purpose optional wrapper |
OptionalSyntax<T> | A syntax node that may or may not be present |
ValidSyntax<T> | A syntax node guaranteed to be non-null |
ValidTypeSyntax<T> | A type declaration syntax with rich helpers |
XmlDocumentation | Parsed XML documentation from a symbol |
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");
OptionalSymbol¶
Wraps a Roslyn symbol that may or may not be present.
Creating¶
OptionalSymbol<INamedTypeSymbol>.WithValue(typeSymbol)
OptionalSymbol<INamedTypeSymbol>.Empty()
OptionalSymbol<INamedTypeSymbol>.FromNullable(maybeNull)
Checking Presence¶
Extracting Values¶
// Validate to non-nullable wrapper (preferred pattern)
if (optional.IsValid(out var valid))
{
// valid is ValidSymbol<T> with guaranteed non-null
Console.WriteLine(valid.Name);
}
// Early exit pattern
if (optional.IsNotValid(out var valid))
return;
// valid is now ValidSymbol<T>
// Other extraction methods
var symbol = optional.OrThrow("Symbol required");
var maybeNull = optional.OrNull();
var validated = optional.Validate(); // OptionalSymbol → ValidSymbol?
var validated = optional.ValidateOrThrow(); // throws if empty
Transforming¶
// Map to a different type
OptionalValue<string> name = optional.Map(s => s.FullyQualifiedName);
// Filter
OptionalSymbol<T> filtered = optional.Where(s => s.IsPublic());
// Cast to derived type
OptionalSymbol<IMethodSymbol> method = optional.OfType<IMethodSymbol>();
// Select (alias for Map)
OptionalValue<int> count = optional.Select(s => s.GetMembers().Length);
Symbol Identity Properties¶
optional.Name // string? — symbol name
optional.Namespace // string? — containing namespace
optional.FullyQualifiedName // string? — e.g. "MyApp.Domain.Customer"
optional.GloballyQualifiedName // string? — e.g. "global::MyApp.Domain.Customer"
optional.DisplayName // string? — namespace.name format
optional.PropertyName // string? — suggested property name (PascalCase)
optional.ParameterName // string? — suggested parameter name (camelCase)
optional.Location // Location — primary source location
Accessibility Properties¶
optional.Accessibility // Accessibility? — enum value
optional.AccessibilityString // string? — "public", "private", etc.
optional.IsPublic // bool
optional.IsInternal // bool
optional.IsPrivate // bool
optional.IsProtected // bool
Modifier Properties¶
optional.IsStatic // bool
optional.IsAbstract // bool
optional.IsSealed // bool
optional.IsVirtual // bool
optional.IsOverride // bool
optional.IsReadOnly // bool
optional.IsPartial // bool
optional.IsImplicitlyDeclared // bool
optional.IsExtern // bool
Type Classification Properties¶
optional.IsGenericType // bool
optional.IsValueType // bool
optional.IsReferenceType // bool
optional.IsInterface // bool
optional.IsClass // bool
optional.IsStruct // bool
optional.IsRecord // bool
optional.IsEnum // bool
optional.IsDelegate // bool
optional.IsNullable // bool
optional.Kind // string? — "class", "struct", "interface", etc.
optional.SymbolTypeKind // TypeKind?
optional.SpecialType // SpecialType?
Method-Specific Properties¶
Type Hierarchy¶
optional.ContainingType // OptionalSymbol<INamedTypeSymbol>
optional.BaseType // OptionalSymbol<INamedTypeSymbol>
optional.Interfaces // ImmutableArray<INamedTypeSymbol>
// Get all base types in inheritance chain
optional.GetBaseTypes() // IEnumerable<ValidSymbol<INamedTypeSymbol>>
// Get interfaces
optional.GetInterfaces() // IEnumerable<ValidSymbol<INamedTypeSymbol>> — direct interfaces
optional.GetAllInterfaces() // IEnumerable<ValidSymbol<INamedTypeSymbol>> — includes inherited
// Check inheritance and interface implementation
optional.ImplementsInterface("IDisposable") // bool — checks all interfaces
optional.InheritsFrom("BaseClass") // bool — checks inheritance chain
Generic Type Support¶
optional.Arity // int — number of type parameters
optional.GetTypeArguments() // ImmutableArray<OptionalSymbol<INamedTypeSymbol>>
optional.GetTypeArgument(0) // OptionalArgument<INamedTypeSymbol>
optional.GetTypeArgumentSymbol(0) // OptionalSymbol<ITypeSymbol>
optional.GetFirstTypeArgument() // OptionalSymbol<ITypeSymbol>
optional.SingleTypeArgument // OptionalSymbol<ITypeSymbol> (for arity-1 generics)
optional.GetTypeParameters() // IEnumerable<OptionalSymbol<ITypeParameterSymbol>>
optional.GetMethodTypeParameters() // method-specific type parameters
Task Type Support¶
optional.IsTask // bool — Task, Task<T>, ValueTask, or ValueTask<T>
optional.InnerTaskType // OptionalSymbol<ITypeSymbol> — T in Task<T>
Attributes¶
optional.GetAttributes() // IEnumerable<OptionalAttribute>
optional.GetAttributes("MyAttribute") // IEnumerable<ValidAttribute>
optional.GetAttributes<ObsoleteAttribute>() // IEnumerable<ValidAttribute>
optional.GetAttribute("MyAttribute") // OptionalAttribute (first match)
optional.GetAttribute<ObsoleteAttribute>() // OptionalAttribute (first match)
optional.HasAttributes() // bool
optional.HasAttribute("MyAttribute") // bool
optional.LacksAttributes() // bool
optional.LacksAttribute("MyAttribute") // bool
XML Documentation¶
optional.XmlDocumentationRaw // string? — raw XML
optional.XmlDocumentation // XmlDocumentation — parsed structure
optional.HasXmlDocumentation // bool
Utility Methods¶
optional.Do(s => Console.WriteLine(s.Name));
optional.Match(
whenPresent: s => HandleSymbol(s),
whenEmpty: () => HandleEmpty());
optional.Equals(otherSymbol);
optional.DoesNotEqual(otherSymbol);
ValidSymbol¶
A validated symbol where the underlying value is guaranteed non-null. Created by validating an OptionalSymbol.
Creating¶
ValidSymbol<INamedTypeSymbol>.From(typeSymbol) // throws if null
ValidSymbol<INamedTypeSymbol>.TryFrom(typeSymbol) // returns null if input is null
// From OptionalSymbol validation (preferred)
if (optional.IsValid(out var valid)) { /* use valid */ }
Properties¶
Same properties as OptionalSymbol, but return non-nullable types:
valid.Value // TSymbol — the underlying symbol (guaranteed non-null)
valid.Name // string (not nullable)
valid.FullyQualifiedName // string (not nullable)
valid.Accessibility // Accessibility (not nullable)
Additional Task Properties¶
valid.IsValueTask // bool
valid.IsGenericTask // bool — Task<T>
valid.IsGenericValueTask // bool — ValueTask<T>
valid.IsNonGenericTask // bool — Task (no type argument)
valid.IsNonGenericValueTask // bool — ValueTask (no type argument)
Transforming¶
TResult result = valid.Map(s => s.Name);
ValidSymbol<IMethodSymbol> method = valid.MapTo(s => (IMethodSymbol)s);
ValidSymbol<T>? filtered = valid.Where(s => s.IsPublic);
ValidSymbol<IMethodSymbol>? method = valid.OfType<IMethodSymbol>();
valid.Do(s => Console.WriteLine(s.Name));
Type Hierarchy (INamedTypeSymbol)¶
valid.BaseType // OptionalSymbol<INamedTypeSymbol>
valid.GetBaseTypes() // IEnumerable<ValidSymbol<INamedTypeSymbol>>
valid.GetInterfaces() // IEnumerable<ValidSymbol<INamedTypeSymbol>>
valid.GetAllInterfaces() // IEnumerable<ValidSymbol<INamedTypeSymbol>>
valid.ImplementsInterface("IDisposable") // bool
valid.InheritsFrom("BaseClass") // bool
OptionalAttribute¶
Wraps an AttributeData that may or may not be present.
Creating¶
OptionalAttribute.WithValue(attributeData)
OptionalAttribute.Empty()
OptionalAttribute.FromNullable(maybeNull)
Getting Arguments¶
// Constructor arguments by index
OptionalArgument<string> name = attr.ConstructorArg<string>(0);
OptionalArgument<int> count = attr.ConstructorArg<int>(1);
// Named arguments
OptionalArgument<int> retries = attr.NamedArg<int>("MaxRetries");
OptionalArgument<string> message = attr.NamedArg<string>("Message");
Generic Attribute Type Arguments¶
For generic attributes like [MyAttribute<TRuntime, TEvent>]:
attr.GetTypeArguments() // ImmutableArray<OptionalSymbol<INamedTypeSymbol>>
attr.GetTypeArgument(0) // OptionalArgument<INamedTypeSymbol>
attr.GetTypeArgumentSymbol(0) // OptionalSymbol<ITypeSymbol>
attr.AttributeClass // OptionalSymbol<INamedTypeSymbol>
Transforming¶
// Map to a result type
OptionalArgument<MyConfig> config = attr.Map(a => new MyConfig(a));
// Extract multiple arguments at once
OptionalArgument<MyConfig> config = attr.WithArgs(a => new MyConfig
{
Name = a.ConstructorArg<string>(0).OrDefault("Default"),
Retries = a.NamedArg<int>("MaxRetries").OrDefault(3)
});
Validation¶
if (attr.IsValid(out var valid)) { /* use valid */ }
if (attr.IsNotValid(out var valid)) return;
attr.Validate(); // OptionalAttribute → ValidAttribute?
attr.ValidateOrThrow(); // throws if empty
attr.TryValidate(out var valid);
Other Methods¶
attr.Do(a => Console.WriteLine(a.AttributeClass?.Name));
attr.OrElse(() => fallbackAttribute);
attr.OrNull();
attr.OrThrow("Attribute required");
attr.OrDefault(fallbackValue);
attr.Match(whenPresent: ..., whenEmpty: ...);
attr.PropertyName // string? — suggested property name
attr.ParameterName // string? — suggested parameter name
ValidAttribute¶
A validated attribute with guaranteed non-null AttributeData.
// Same argument extraction methods as OptionalAttribute
OptionalArgument<string> arg = validAttr.ConstructorArg<string>(0);
OptionalArgument<int> retries = validAttr.NamedArg<int>("MaxRetries");
validAttr.GetNamedArgument<bool>("Enabled"); // alternate syntax
// Direct access
validAttr.Value // AttributeData
validAttr.AttributeClass // INamedTypeSymbol
// Generic attribute type arguments return ValidSymbol
ImmutableArray<ValidSymbol<INamedTypeSymbol>> typeArgs = validAttr.GetTypeArguments();
OptionalArgument¶
Wraps an attribute argument value that may or may not be present.
Extracting Values¶
string value = arg.OrDefault("fallback");
int count = arg.OrDefault(() => ComputeDefault());
string value = arg.OrThrow("Argument required");
string value = arg.OrThrow(() => new CustomException());
string? maybeNull = arg.OrNull();
Transforming¶
OptionalArgument<int> length = arg.Map(s => s.Length);
OptionalArgument<int> length = arg.Select(s => s.Length); // alias
OptionalArgument<MyEnum> enumValue = arg.ToEnum<MyEnum>(); // Roslyn stores enums as ints
Pattern Matching¶
string result = arg.Match(
whenPresent: v => $"Value: {v}",
whenEmpty: () => "No value");
if (arg.TryGetValue(out var value)) { /* use value */ }
if (arg.IsMissing(out var value)) return; // early exit pattern
State & Actions¶
arg.HasValue // bool
arg.IsEmpty // bool
arg.Value // T (throws if empty)
arg.Do(v => Console.WriteLine(v));
arg.OrElse(() => fallback);
OptionalValue¶
Generic optional wrapper for any value (not specific to Roslyn symbols). Same API as OptionalArgument:
OptionalValue<string>.WithValue("hello")
OptionalValue<string>.Empty()
value.Map(s => s.Length)
value.OrDefault("fallback")
value.OrThrow()
value.Match(whenPresent: ..., whenEmpty: ...)
OptionalSyntax / ValidSyntax¶
Wrappers for Roslyn syntax nodes.
OptionalSyntax¶
OptionalSyntax<ClassDeclarationSyntax>.WithValue(classDecl)
OptionalSyntax<ClassDeclarationSyntax>.Empty()
OptionalSyntax<ClassDeclarationSyntax>.FromNullable(maybeNull)
optional.Node // TSyntax?
optional.Location // Location
optional.Span // TextSpan
optional.FullSpan // TextSpan
optional.Map(n => n.Identifier.Text)
optional.Where(n => n.Modifiers.Any(SyntaxKind.PublicKeyword))
optional.OfType<RecordDeclarationSyntax>()
optional.Parent // OptionalSyntax<SyntaxNode>
optional.Ancestor<T>() // OptionalSyntax<T>
optional.Ancestors<T>() // IEnumerable<T>
if (optional.IsValid(out var valid)) { /* use valid */ }
ValidSyntax¶
ValidSyntax<ClassDeclarationSyntax>.From(classDecl)
ValidSyntax<ClassDeclarationSyntax>.TryFrom(maybeNull)
valid.Node // TSyntax (guaranteed non-null)
valid.Location // Location
valid.SyntaxTree // SyntaxTree
valid.Text // string — the node's text
valid.FullText // string — text with trivia
valid.Parent // ValidSyntax<SyntaxNode>?
valid.Ancestor<T>() // ValidSyntax<T>?
valid.Ancestors<T>() // IEnumerable<T>
valid.Descendant<T>() // ValidSyntax<T>?
valid.Descendants<T>() // IEnumerable<T>
valid.LeadingTrivia // SyntaxTriviaList
valid.TrailingTrivia // SyntaxTriviaList
// Implicit conversion to the underlying node
ClassDeclarationSyntax node = valid;
ValidTypeSyntax¶
Specialized wrapper for type declaration syntax (ClassDeclarationSyntax, RecordDeclarationSyntax, etc.) with rich helpers.
ValidTypeSyntax<ClassDeclarationSyntax>.From(classDecl)
syntax.Name // string
syntax.Identifier // SyntaxToken
syntax.Keyword // SyntaxToken (e.g., "class", "record")
syntax.Location // Location
syntax.IdentifierLocation // Location
// Modifiers
syntax.Modifiers // SyntaxTokenList
syntax.HasModifier(SyntaxKind.PublicKeyword)
syntax.IsPartial // bool
syntax.IsStatic // bool
syntax.IsAbstract // bool
syntax.IsSealed // bool
syntax.IsPublic // bool
syntax.IsInternal // bool
syntax.IsPrivate // bool
syntax.IsProtected // bool
syntax.IsReadOnly // bool
syntax.IsFile // bool
// Modifier manipulation (returns new syntax)
syntax.AddModifier(SyntaxKind.PartialKeyword)
syntax.RemoveModifier(SyntaxKind.SealedKeyword)
syntax.WithModifiers(newModifiers)
// Structure
syntax.BaseList // BaseListSyntax?
syntax.TypeParameterList // TypeParameterListSyntax?
syntax.ConstraintClauses // SyntaxList<TypeParameterConstraintClauseSyntax>
syntax.AttributeLists // SyntaxList<AttributeListSyntax>
syntax.Members // SyntaxList<MemberDeclarationSyntax>
syntax.HasBaseList // bool
syntax.IsGeneric // bool
syntax.Arity // int
// Navigation
syntax.ContainingType // ValidTypeSyntax<TypeDeclarationSyntax>?
syntax.ContainingNamespace // ValidSyntax<BaseNamespaceDeclarationSyntax>?
syntax.NestedTypes // IEnumerable<ValidTypeSyntax<TypeDeclarationSyntax>>
// Member access
syntax.Methods // IEnumerable<MethodDeclarationSyntax>
syntax.Properties // IEnumerable<PropertyDeclarationSyntax>
syntax.Fields // IEnumerable<FieldDeclarationSyntax>
syntax.Constructors // IEnumerable<ConstructorDeclarationSyntax>
// Conversions
TypeDeclarationSyntax node = syntax;
ValidSyntax<ClassDeclarationSyntax> validSyntax = syntax;
XmlDocumentation¶
Parsed XML documentation from a symbol.
var doc = symbol.XmlDocumentation; // from OptionalSymbol/ValidSymbol
var doc = XmlDocumentation.FromSymbol(symbol);
doc.HasValue // bool
doc.IsEmpty // bool
// Content
doc.Summary // string?
doc.Remarks // string?
doc.Returns // string?
doc.Value // string?
doc.Example // string?
doc.RawXml // string?
// Parameters
doc.Params // ImmutableDictionary<string, string>
doc.GetParam("name") // string?
// Type parameters
doc.TypeParams // ImmutableDictionary<string, string>
doc.GetTypeParam("T") // string?
// Exceptions and references
doc.Exceptions // ImmutableArray<(string Type, string Description)>
doc.SeeAlso // ImmutableArray<string>
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();
License¶
RPL-1.5 (Reciprocal Public License) — Real reciprocity, no loopholes.
You can use this code, modify it, and share it freely. But when you deploy it — internally or externally, as a service or within your company — you share your improvements back under the same license.
Why? We believe if you benefit from this code, the community should benefit from your improvements. That's the deal we think is fair.
Personal research and experimentation? No obligations. Go learn, explore, and build.
See LICENSE for the full legal text.