Skip to content

ExternalTypeResolution

Edgar Mesquita edited this page Jan 31, 2026 · 2 revisions

External Type Resolution in eQuantic.UI Compiler

Overview

The eQuantic.UI compiler can now resolve types defined in external files within the same project. This enables components to reference models, DTOs, and other classes without requiring them to be in the same file.

Problem

Previously, the compiler used a minimal SemanticModel that only included the component file being compiled. This meant:

Before: Cannot reference external classes

// 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' not resolved - generates incorrect JavaScript

    protected override HtmlNode Render()
    {
        return Text(_currentUser.Name); // ❌ _currentUser.Name conversion fails
    }
}

Solution

The compiler now supports passing the full project Roslyn Compilation, which includes all source files and references.

After: External classes work perfectly

// 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
    }
}

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.

Backwards Compatibility

The feature is fully backwards compatible:

  • Without SetProjectCompilation(): Works as before (isolated files only)
  • With SetProjectCompilation(): Gains full project type resolution

No breaking changes to existing code.

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.

Future Enhancements

Potential improvements:

  • Automatic detection: Auto-discover .csproj from component file location
  • Caching: Cache compilations for faster repeated builds
  • Incremental: Support incremental compilation for changed files only
  • Diagnostics: Report type resolution failures with actionable errors

Summary

What's New:

  • Components can reference classes from other files
  • Full type information available during compilation
  • Correct JavaScript generation for external type members

Benefits:

  • Better code organization (models in separate files)
  • Type-safe property access
  • More accurate transpilation

How to Use:

var compilation = await ProjectCompilationHelper.GetProjectCompilationAsync(projectPath);
compiler.SetProjectCompilation(compilation);

Tests: 294 tests passing (5 new external type tests)

Clone this wiki locally