eQuantic.UIeQuantic.UI
Docs
Playground
GitHub
DocsKompilierung
Externe Typauflösung im eQuantic.UI-Compiler
Edit this page
2 min read
Überblick
Der eQuantic.UI-Compiler löst Typen auf, die in externen Dateien innerhalb desselben Projekts definiert sind. Komponenten referenzieren Modelle, DTOs und andere Klassen, ohne dass diese in derselben Datei liegen müssen: Der Compiler arbeitet gegen die vollständige Roslyn-Compilation des Projekts, die alle Quelldateien und Referenzen enthält.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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; //
✅
Der Typ 'User' wird vollständig aufgelöst
protected override HtmlNode Render()
{
return Text(_currentUser.Name); //
✅
Wird zu: this._currentUser.name
}
}
Ein vollständiges semantisches Modell zählt, weil die Umwandlung des Mitgliederzugriffs davon abhängt, den Typ des Empfängers zu kennen: Mit ihm senkt sich _currentUser.Name zu this._currentUser.name ab; ohne ihn kann der Compiler eine Eigenschaft nicht von einer lokalen Variable unterscheiden und nicht die richtige Methodenabbildung wählen.
Nutzung der API
Variante 1: MSBuildWorkspace nutzen (empfohlen)
1
2
3
4
5
6
7
8
9
10
11
12
13
using eQuantic.UI.Compiler;
using eQuantic.UI.Compiler.Services;
// Die vollständige Compilation des Projekts holen
var compilation = await ProjectCompilationHelper
.GetProjectCompilationAsync("path/to/MyApp.csproj");
// Compiler erzeugen und die Projekt-Compilation setzen
var compiler = new ComponentCompiler();
compiler.SetProjectCompilation(compilation);
// Nun die Komponenten kompilieren: Externe Typen werden aufgelöst
var results = compiler.CompileFile("Pages/UserProfile.cs");
Variante 2: Manuelle Compilation aus den Quellen
Nützlich in MSBuild-Tasks, wo MSBuildWorkspace möglicherweise nicht verfügbar ist:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using eQuantic.UI.Compiler.Services;
// Alle .cs-Dateien des Projekts holen
var sourceFiles = ProjectCompilationHelper
.GetProjectSourceFiles("path/to/MyApp");
// Die Assembly-Referenzen holen
var assemblyPaths = new[]
{
"path/to/eQuantic.UI.Primitives.dll",
"path/to/other-dependencies.dll"
};
// Die Compilation aus den Quellen erzeugen
var compilation = ProjectCompilationHelper.CreateCompilationFromSources(
sourceFiles,
assemblyPaths,
"MyApp");
var compiler = new ComponentCompiler();
compiler.SetProjectCompilation(compilation);
Variante 3: Die Projekt-Compilation löschen
Zurück zum Modus minimaler Compilation (isolierte Dateien):
1
compiler.ClearProjectCompilation();
Wie es funktioniert
Architektur
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌─────────────────────────────────────────┐
│ Compilation des MSBuild-Projekts │
│ - Alle .cs-Dateien des Projekts │
│ - Alle referenzierten Assemblys │
│ - Vollständige Typinformation │
└────────────┬────────────────────────────┘
│
↓ SetProjectCompilation()
┌────────────────────────────────────────┐
│ SemanticModelProvider │
│ - Hält die Projekt-Compilation │
│ - Gibt je Datei ein SemanticModel │
└────────────┬───────────────────────────┘
│
↓ GetSemanticModel(tree)
┌────────────────────────────────────────┐
│ CSharpToJsConverter │
│ - Nutzt das SemanticModel für Typen │
│ - Wandelt _currentUser.Name → name │
└────────────────────────────────────────┘
Wichtige Klassen
1.
SemanticModelProvider (SemanticModelProvider.cs)
•
SetProjectCompilation(Compilation) - setzt die vollständige Projekt-Compilation
•
GetSemanticModel(SyntaxTree) - gibt das semantische Modell mit vollständiger Typinformation zurück
2.
ProjectCompilationHelper (ProjectCompilationHelper.cs)
•
GetProjectCompilationAsync(string) - lädt aus der .csproj
•
CreateCompilationFromSources(...) - baut aus Quelldateien
•
GetProjectSourceFiles(string) - findet alle .cs-Dateien
3.
ComponentCompiler (ComponentCompiler.cs)
•
SetProjectCompilation(Compilation) - schaltet die externe Typauflösung ein
•
ClearProjectCompilation() - kehrt zum isolierten Modus zurück
Beispiele
Beispiel 1: Komponente mit einem externen Modell
Models/Product.cs:
1
2
3
4
5
6
7
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[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")
);
}
}
Erzeugtes JavaScript (mit der Projekt-Compilation):
1
2
3
4
5
6
7
8
9
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')
]);
}
}
Beispiel 2: Mehrere externe Typen
Models/Address.cs:
1
2
3
4
5
6
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string ZipCode { get; set; }
}
Models/Customer.cs:
1
2
3
4
5
6
public class Customer
{
public string Name { get; set; }
public string Email { get; set; }
public Address ShippingAddress { get; set; }
}
Pages/CheckoutPage.cs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[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}")
);
}
}
Alle Zugriffe auf Eigenschaften werden korrekt aufgelöst und nach JavaScript umgewandelt.
Tests
Die Tests prüfen, dass die externe Typauflösung korrekt arbeitet:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[Fact]
public void SemanticModel_WithProjectCompilation_CanResolveExternalTypes()
{
// Die Compilation mit User und der Komponente erzeugen
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);
// Behaupten: Der Typ User wird aufgelöst
var userType = semanticModel.Compilation.GetTypeByMetadataName("User");
userType.Should().NotBeNull();
}
Siehe ExternalTypeResolutionTests.cs für die vollständige Testsuite.
Modi
•
Ohne SetProjectCompilation(): minimale Compilation, jede Datei wird also isoliert aufgelöst
•
Mit SetProjectCompilation(): Typauflösung über das ganze Projekt
Performance
Minimaler Mehraufwand:
•
Die Projekt-Compilation wird einmal zur Build-Zeit erzeugt
•
Sie wird von allen Komponentendateien geteilt
•
Keine Kompilierungsstrafe je Datei
Speichereffizient:
•
Eine einzige Compilation-Instanz
•
Nutzt die bestehende Roslyn-Infrastruktur wieder
Grenzen
1.
Verlangt eine Compilation vor der Komponenten-Kompilierung: Das Projekt muss kompiliert (oder mindestens geparst) sein, bevor der Komponenten-Compiler läuft.
2.
Verfügbarkeit von MSBuildWorkspace: GetProjectCompilationAsync verlangt die MSBuild-APIs, die nicht in jedem Kontext verfügbar sind. Nutzen Sie CreateCompilationFromSources als Rückfall.
3.
Erzeugter Code: Automatisch erzeugte Dateien in obj/ werden ausgeschlossen, um Konflikte zu vermeiden.
Zusammenfassung
•
Komponenten referenzieren Klassen aus anderen Dateien; die vollständige Typinformation ist während der Kompilierung verfügbar, also ist die JavaScript-Erzeugung für Mitglieder externer Typen korrekt.
•
Der SDK-Build gibt dem Compiler die echten Assembly-Referenzen des Projekts (--refs), also lösen sich Empfängertypen auch über Paketgrenzen hinweg auf.
1
2
var compilation = await ProjectCompilationHelper.GetProjectCompilationAsync(projectPath);
compiler.SetProjectCompilation(compilation);