Skip to content

SymbolTestContext

Query and extract symbols from compiled source code in tests.

See also: RoslynTestBase | Roslyn Queries

Overview

SymbolTestContext wraps a Compilation and provides convenient methods for finding and querying symbols. It's the foundation for symbol-based testing.

var ctx = SymbolsFor(@"
    public class Customer 
    {
        public string Name { get; set; }
        public void Save() { }
    }");

var type = ctx.RequireNamedType("Customer");
var prop = ctx.RequireProperty("Name");
var method = ctx.RequireMethod("Save");

Get vs Require

Every symbol accessor has two variants:

Pattern Returns On Not Found
Get* OptionalSymbol<T> Returns empty
Require* ValidSymbol<T> Throws exception
// Optional - check before use
var maybeType = ctx.GetNamedType("Customer");
if (maybeType.IsNotValid(out var valid))
{
    // Handle missing type
    return;
}
// Use valid.Value

// Required - throws if not found
var type = ctx.RequireNamedType("Customer");
// Use type.Value directly

Symbol Accessors

Types

// Named types (classes, structs, interfaces, records, enums)
OptionalSymbol<INamedTypeSymbol> type = ctx.GetNamedType("Customer");
ValidSymbol<INamedTypeSymbol> type = ctx.RequireNamedType("Customer");

// Any type (includes type parameters, arrays, etc.)
OptionalSymbol<ITypeSymbol> type = ctx.GetType("Customer");
ValidSymbol<ITypeSymbol> type = ctx.RequireType("Customer");

Namespaces

OptionalSymbol<INamespaceSymbol> ns = ctx.GetNamespace("MyApp.Services");
ValidSymbol<INamespaceSymbol> ns = ctx.RequireNamespace("MyApp.Services");

Members

These search across all types in the compilation:

// Methods
ValidSymbol<IMethodSymbol> method = ctx.RequireMethod("ProcessAsync");

// Properties
ValidSymbol<IPropertySymbol> prop = ctx.RequireProperty("Name");

// Fields
ValidSymbol<IFieldSymbol> field = ctx.RequireField("_logger");

// Parameters (from all methods)
ValidSymbol<IParameterSymbol> param = ctx.RequireParameter("customerId");

Fluent Type Queries

Query Members on a Specific Type

// Get all public methods on Customer
var methods = ctx.Type("Customer")
    .Methods()
    .ThatArePublic()
    .GetAll();

// Get required properties
var required = ctx.Type("Customer")
    .Properties()
    .ThatAreRequired()
    .GetAll();

// Get constructors
var ctors = ctx.Type("Customer")
    .Constructors()
    .GetAll();

Query Types in Source Code

The Types() method queries only types defined in your source (excludes referenced assemblies):

// All public classes in source
var classes = ctx.Types()
    .ThatAreClasses()
    .ThatArePublic()
    .GetAll();

// Interfaces with a specific attribute
var marked = ctx.Types()
    .ThatAreInterfaces()
    .WithAttribute("ServiceContract")
    .GetAll();

Query All Types in Compilation

Use AllTypesInCompilation() when you need to include types from referenced assemblies:

var allWithAttribute = ctx.AllTypesInCompilation()
    .ThatAreClasses()
    .WithAttribute("Serializable")
    .Query()
    .GetAll();

Projections

Map Types

Transform a symbol into a custom model:

var model = ctx.Map("Customer", typeSymbol => 
    new CustomerModel
    {
        Name = typeSymbol.OrNull()?.Name,
        PropertyCount = typeSymbol.QueryProperties().GetAll().Length
    });

Query with Compilation Context

Some projections need both the symbol and compilation:

var info = ctx.Query(
    symbols => symbols.RequireNamedType("MyRuntime"),
    (symbol, compilation) => symbol.QuerySystemInfo(compilation));

Accessing the Compilation

var ctx = SymbolsFor(source);

// Get the underlying compilation
Compilation compilation = ctx.Compilation;

// Use for advanced scenarios
var semanticModel = compilation.GetSemanticModel(compilation.SyntaxTrees.First());

Common Patterns

Testing Symbol Properties

[Test]
public async Task CustomerHasExpectedProperties()
{
    var ctx = SymbolsFor(@"
        public class Customer 
        {
            public required string Name { get; set; }
            public int Age { get; set; }
        }");

    var props = ctx.Type("Customer").Properties().GetAll();

    await Assert.That(props).HasCount(2);
    await Assert.That(props.Any(p => p.Name == "Name" && p.IsRequired)).IsTrue();
}

Testing Method Signatures

[Test]
public async Task ProcessAsyncHasCorrectSignature()
{
    var ctx = SymbolsFor(@"
        public class Service 
        {
            public async Task<bool> ProcessAsync(string input, CancellationToken ct) 
                => true;
        }");

    var method = ctx.RequireMethod("ProcessAsync");

    await Assert.That(method.Value.IsAsync).IsTrue();
    await Assert.That(method.Value.Parameters).HasCount(2);
    await Assert.That(method.Value.ReturnType.Name).IsEqualTo("Task");
}

Testing Attribute Data

[Test]
public async Task TypeHasExpectedAttribute()
{
    var ctx = SymbolsFor(@"
        [Obsolete(""Use CustomerV2"")]
        public class Customer { }");

    var type = ctx.RequireNamedType("Customer");
    var attr = type.Value.GetAttribute("Obsolete");

    await Assert.That(attr.IsValid).IsTrue();
}

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.