Skip to content

ExternalTypeResolution

Edgar Mesquita edited this page Aug 8, 2026 · 2 revisions

External Type Resolution in eQuantic.UI Compiler

Overview

The eQuantic.UI compiler resolves types defined in external files within the same project. Components reference models, DTOs, and other classes without requiring them to be in the same file — the compiler works against the full project Roslyn Compilation, which includes all source files and references.

// Models/User.cs
public class User
{
    public string Name { get; set; }
    public string Email { get; set; }
}

// Pages/UserProfile.cs
[Page("/profile")]
public class UserProfile : StatefulComponent
{
    private User _currentUser; // ✅ Type 'User' fully resolved

    protected override HtmlNode Render()
    {
        return Text(_currentUser.Name); // ✅ Converts to: this._currentUser.name
    }
}

A full semantic model matters because member-access conversion depends on knowing the receiver's type: with it, _currentUser.Name lowers to this._currentUser.name; without it, the compiler cannot distinguish a property from a local or pick the right method mapping.

API Usage

Option 1: Using MSBuildWorkspace (Recommended)

using eQuantic.UI.Compiler;
using eQuantic.UI.Compiler.Services;

// Get the full project compilation
var compilation = await ProjectCompilationHelper
    .GetProjectCompilationAsync("path/to/MyApp.csproj");

// Create compiler and set project compilation
var compiler = new ComponentCompiler();
compiler.SetProjectCompilation(compilation);

// Now compile components - external types will be resolved
var results = compiler.CompileFile("Pages/UserProfile.cs");

Option 2: Manual Compilation from Sources

Useful in MSBuild tasks where MSBuildWorkspace might not be available:

using eQuantic.UI.Compiler.Services;

// Get all .cs files in the project
var sourceFiles = ProjectCompilationHelper
    .GetProjectSourceFiles("path/to/MyApp");

// Get assembly references
var assemblyPaths = new[]
{
    "path/to/eQuantic.UI.Core.dll",
    "path/to/other-dependencies.dll"
};

// Create compilation from sources
var compilation = ProjectCompilationHelper.CreateCompilationFromSources(
    sourceFiles,
    assemblyPaths,
    "MyApp");

var compiler = new ComponentCompiler();
compiler.SetProjectCompilation(compilation);

Option 3: Clearing Project Compilation

Revert to minimal compilation mode (isolated files):

compiler.ClearProjectCompilation();

How It Works

Architecture

┌─────────────────────────────────────────┐
│  MSBuild Project Compilation            │
│  - All .cs files in project             │
│  - All referenced assemblies            │
│  - Full type information                │
└────────────┬────────────────────────────┘
             │
             ↓ SetProjectCompilation()
┌────────────────────────────────────────┐
│  SemanticModelProvider                 │
│  - Stores project compilation          │
│  - Returns SemanticModel for each file │
└────────────┬───────────────────────────┘
             │
             ↓ GetSemanticModel(tree)
┌────────────────────────────────────────┐
│  CSharpToJsConverter                   │
│  - Uses SemanticModel for type info    │
│  - Converts _currentUser.Name → name   │
└────────────────────────────────────────┘

Key Classes

  1. SemanticModelProvider (SemanticModelProvider.cs)

    • SetProjectCompilation(Compilation) - Set full project compilation
    • GetSemanticModel(SyntaxTree) - Returns semantic model with full type info
  2. ProjectCompilationHelper (ProjectCompilationHelper.cs)

    • GetProjectCompilationAsync(string) - Load from .csproj
    • CreateCompilationFromSources(...) - Build from source files
    • GetProjectSourceFiles(string) - Find all .cs files
  3. ComponentCompiler (ComponentCompiler.cs)

    • SetProjectCompilation(Compilation) - Enable external type resolution
    • ClearProjectCompilation() - Revert to isolated mode

Examples

Example 1: Component with External Model

Models/Product.cs:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public bool InStock { get; set; }
}

Pages/ProductCard.cs:

[Component]
public class ProductCard : StatelessComponent
{
    public Product Item { get; set; }

    protected override HtmlNode Build()
    {
        return Container(
            Heading(Item.Name),
            Text($"${Item.Price:F2}"),
            Text(Item.InStock ? "In Stock" : "Out of Stock")
        );
    }
}

Generated JavaScript (with project compilation):

class ProductCard extends StatelessComponent {
    build() {
        return Container([
            Heading(this.item.name),
            Text(`$${this.item.price.toFixed(2)}`),
            Text(this.item.inStock ? 'In Stock' : 'Out of Stock')
        ]);
    }
}

Example 2: Multiple External Types

Models/Address.cs:

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public string ZipCode { get; set; }
}

Models/Customer.cs:

public class Customer
{
    public string Name { get; set; }
    public string Email { get; set; }
    public Address ShippingAddress { get; set; }
}

Pages/CheckoutPage.cs:

[Page("/checkout")]
public class CheckoutPage : StatefulComponent
{
    private Customer _customer;

    protected override HtmlNode Render()
    {
        return Container(
            Text($"Customer: {_customer.Name}"),
            Text($"Email: {_customer.Email}"),
            Text($"Shipping: {_customer.ShippingAddress.City}, {_customer.ShippingAddress.ZipCode}")
        );
    }
}

All property accesses are correctly resolved and converted to JavaScript.

Testing

Tests verify external type resolution works correctly:

[Fact]
public void SemanticModel_WithProjectCompilation_CanResolveExternalTypes()
{
    // Create compilation with User and Component
    var userTree = CSharpSyntaxTree.ParseText("public class User { ... }");
    var componentTree = CSharpSyntaxTree.ParseText("public class UserProfile { ... }");

    var compilation = CSharpCompilation.Create("Test", new[] { userTree, componentTree }, ...);

    var provider = new SemanticModelProvider();
    provider.SetProjectCompilation(compilation);

    var semanticModel = provider.GetSemanticModel(componentTree);

    // Assert: User type is resolved
    var userType = semanticModel.Compilation.GetTypeByMetadataName("User");
    userType.Should().NotBeNull();
}

See ExternalTypeResolutionTests.cs for complete test suite.

Modes

  • Without SetProjectCompilation(): minimal compilation — each file is resolved in isolation
  • With SetProjectCompilation(): full project type resolution

Performance

Minimal overhead:

  • Project compilation is created once at build time
  • Shared across all component files
  • No per-file compilation penalty

Memory efficient:

  • Single compilation instance
  • Reuses existing Roslyn infrastructure

Limitations

  1. Requires compilation before component compilation: The project must be compiled (or at least parsed) before running the component compiler.

  2. MSBuildWorkspace availability: GetProjectCompilationAsync requires MSBuild APIs, which may not be available in all contexts. Use CreateCompilationFromSources as fallback.

  3. Generated code: Auto-generated files in obj/ are excluded to avoid conflicts.

Summary

  • Components reference classes from other files; full type information is available during compilation, so JavaScript generation for external type members is correct.
  • The SDK build feeds the compiler the project's real assembly references (--refs), so receiver types resolve even across package boundaries.
var compilation = await ProjectCompilationHelper.GetProjectCompilationAsync(projectPath);
compiler.SetProjectCompilation(compilation);

Clone this wiki locally