Repository navigation
SupportedFeatures
🌐 This page in: English · Português
This document provides a comprehensive list of C# features, .NET APIs, and patterns supported by the eQuantic.UI compiler.
Legend
✅ Full Support: Transpiles to equivalent JavaScript behavior.
⚠️ Partial Support: Works with caveats or minor differences.❌ Not Supported: fundamentally incompatible logic (e.g., blocking I/O, unsafe pointers).
| Feature | Status | Notes |
|---|---|---|
| Classes & Structs | ✅ | Transpiled to ES6 Classes. A plain class has a module of its own whatever it declares, an empty one over an interface's defaults or a base included, and every module that names it imports it; a class whose chain of bases reaches an attribute or a [ServerOnly] type has none, three levels down as much as one (class Underline : Mark over class Mark : Attribute), a base from a referenced library included. An exception class of the app's is a class, with a module of its own (see try / catch / finally). The chain is read by what each base derives from and never by its name, so a class over an interface named like an attribute keeps its module, and a name is a module wherever any declaration of it is one. A class, a static class, a record or a struct declared inside another type is a module of its own too, named by the types that contain it and its own name joined by $ (Cart$Item, A$B$C), which no top-level type can take: every reference to it, inside its owner or out, names that twin, so a nested class beside a top-level class of its name is never taken for it, and one inside a server-only class or an attribute has none, as its owner has none: client code that names it is refused (EQ2010), and a page declared in it has no route, while one inside an exception class crosses as that class does (Since 0.2.0-preview.61). A record and a struct have a twin whatever they declare, unless marked [ServerOnly], a record over a server-only record included (Since 0.2.0-preview.61). |
| Interfaces | ✅ | Used for TypeScript type checking (erased at runtime). A default member (a property, an indexer or a method with a body; an indexer since 0.2.0-preview.61) reaches the twin of every class that takes it without declaring it: converted from the interface's source, or, for the SDK's own interfaces an app implements (a theme, a code language), delegated to the runtime's copy. An interface from any other compiled assembly has neither, and the build refuses the class (EQ1008) (Since 0.2.0-preview.59). |
| Enums | ✅ | Cross as their camelCase name (MainAlign.Start → 'start'), and a [Flags] enum as its number. .ToString(), interpolation and concatenation give the C# member name ("Start"), the same text the server prints, a flags combination's set flags ("Read, Write"), a value no member names as its digits, and nothing for a null nullable enum. ToString("D"), "X", "F" and "G", an interpolation's format and its alignment write what .NET writes, Enum.Parse, TryParse, GetName, GetNames, GetValues and IsDefined answer as .NET's, and a dictionary keyed by an enum crosses both ways, a name no member has being refused. Since 0.2.0-preview.15; the formats, the flags text, the statics of Enum and enum keys since 0.2.0-preview.60. |
| Generics | ✅ | Fully supported (erased at runtime). |
| Extension Methods | ✅ | Resolved via Semantic Model and transpiled to direct calls. |
| Async / Await | ✅ | Maps to async / await and Promise. |
| Lambda Expressions | ✅ | Maps to Arrow Functions () => {}. |
| Pattern Matching | ✅ |
is patterns, Property Patterns, Recursive Patterns. A type with nothing bound (o is int or long, int => …, case int:) tests the type, a long as a BigInt, an integer as a whole number and a char as a string of one code unit. A constant in a pattern, x is Limits.Max included, is tested by its value: a null matches any absence, a decimal and a NaN compare by value, and a long constant is a BigInt, as every long is (Since 0.2.0-preview.60). A positional pattern over a type whose Deconstruct the app wrote, an extension's included, calls it once for each value a test deconstructs, across the arms of a switch and the alternatives of an or too, as .NET does, and reads the parts it hands back, as a deconstruction does: it read the members the outs are named after, which a part the Deconstruct computes is not (Since 0.2.0-preview.61). |
| String Interpolation | ✅ | Maps to Template Literals `${var}`. A format with quoted text ({date:dd 'de' MMMM}) keeps its quotes, and a raw interpolated string keeps its braces ($$$"""{{x}}""" is {{x}}) (Since 0.2.0-preview.60). |
| String & char literals | ✅ | A string or a char keeps the value C# reads, whatever spells it: a lone surrogate ("x\uD83D"), a control character, and the escapes only C# has ('\a', '\e', '\x041', '\U00000041'). A UTF-8 literal ("ab"u8) has no twin and fails the build (EQ1004) (Since 0.2.0-preview.60). |
| Null-Coalescing | ✅ |
?? and ??= mapped to JS equivalents. |
| Object Initializers | ✅ |
new Obj { Prop = 1 } over a record, a struct, a plain class, an exception or a vocabulary type whose twin the runtime transpiles (the spreadsheet's and the forms' models, CellRef, FieldError, SheetController…), applied once the constructor has returned, as C# applies it (a plain class took it as a trailing config object, an exception dropped it, and a transpiled vocabulary type handed it to a slot its twin does not have, before 0.2.0-preview.61; a component takes it as its props): every initializer of the type has run, the member's own included, and the values are read after them. A nested collection initializer (Items = { 1, 2 }) adds to the collection the member holds, through the Add the bound tree binds (an extension method's through its home, and a list's, a set's or an ICollection<T> member's as a call to it is); a nested object initializer (Inner = { N = 3 }) assigns into the object the member holds; and an entry ([key] = value) is written through the type's indexer. A dictionary's pair (Map = { { "a", 1 } }) is its Add, which refuses a key already there. Every part is evaluated in the method it was written in, so an await in an element works, and each element is applied before the next one's parts are evaluated, the member it adds to or assigns into read before its own parts, as C# applies it. An element with no lowering fails the build (EQ1004) (Since 0.2.0-preview.61). |
field (C# 14) |
✅ | Guarded properties: the twin gets a $name slot (a name no C# field can take), a getter and the accessor's own body. A static one keeps its slot on the type, a record's included, and the slot starts as the property's initializer or its type's default (Since 0.2.0-preview.60). A record's and a struct's instance one runs its accessors' bodies over the slot, as a class's does, and one whose getter has a body is state its record compares and copies; it was held under the property's name, with its setter's body dropped, before 0.2.0-preview.61 (Since 0.2.0-preview.61). |
| Virtual and override properties | ✅ | A property a derived type can override or that overrides one (an auto-property declared virtual or override) keeps its value in a store of its own ($name) under accessors on the twin's prototype, in a class, a record and a struct alike, so every read reaches the override and no initializer reaches the accessor of another level: an auto-property over a computed one, or a computed one over an auto-property, threw at new or answered the base's value before 0.2.0-preview.61. An override that declares only the getter of a property whose base has both keeps the base's setter, as C# inherits it. A record compares, hashes and copies the store, as .NET compares a backing field. A twin with a store writes it in JSON under its property's name, so a server action receives the property by its C# name. One difference stays: in a type that overrides an auto-property with another, base.Name reads the override's value, where .NET reads the base's own field, since the two share one store (Since 0.2.0-preview.61). |
Members that hide (new) |
✅ | A method that hides an inherited member, with new or with the same signature and no override, is reached as C# reaches it: through the base type the base's method answers, and through the hiding type the hiding one, a call, a method group, a base call and ?. alike, a generic method over its own type parameter included, and so does a member the runtime reads by its .NET name, so ((object)c).GetHashCode() is object's over a public new int GetHashCode(). Its twin holds it under a name of its own (name$1), and an override of a new virtual method fills that method's place. So does a method beside an explicit interface implementation of its name, so a call through the class reaches it and one through the interface the explicit one. One that hides a member, or stands beside such an implementation, and answers an interface fails the build (EQ1007), and a property or a field that hides one still shares its name with it (#563, since 0.2.0-preview.61). |
init accessors |
✅ | Emitted as a JS setter, because an init body is where a type states its invariant, and dropping it would lose the check on the client. |
| Collection Initializers | ✅ |
new List<int> { 1, 2 } maps to [1, 2]. A list built with an argument and an initializer holds both, as C# builds it: a copy of the source, never the source itself, or nothing for a capacity, then each element in order, the source or the capacity read first. A capacity is still evaluated, and a negative one is refused as the constructor refuses it. So new List<int>(source) { 3 } is [...source, 3] and new List<int>(10) { 1, 2 } is [1, 2], written out or target-typed, and List<int> c = new(source) is a copy. A capacity dropped the elements, and a source was joined to them with a comma, before 0.2.0-preview.61 (Since 0.2.0-preview.61). |
IEnumerable<T> of your own |
✅ | A class, a record or a struct that implements IEnumerable<T>, or only IEnumerable, is enumerated as its own GetEnumerator() says, by foreach, a spread, string.Join and every LINQ operator, each through the one C# binds for it: a foreach over the class walks its own public GetEnumerator() (a struct enumerator's included), and LINQ, a spread, string.Join and a foreach over the interface walk the IEnumerable<T> one, explicit or not, so the two may walk different sequences, as in .NET. It is the sequence an iterator method yields, or an enumerator of your own, a class that is its own enumerator included, walked by its MoveNext and Current and disposed when the walk ends, a break included. A derived class's override answers, the explicit IEnumerable.GetEnumerator() beside the generic one is the same sequence, and so is an explicit Current beside a public one. A class with only a public GetEnumerator() is walked by foreach too. The twin has one iteration and one Current, so a type that implements IEnumerable<T>, or IEnumerator<T>, over two types fails the build (EQ1007), and so does an explicit IEnumerable.GetEnumerator() or Current that answers otherwise than the member the twin keeps, where one that hands back its answer (=> GetEnumerator();, => Current;) is held as one. A .NET collection's own GetEnumerator() (=> items.GetEnumerator()) has no translation and fails the build (EQ2004): yield the items instead (#612, since 0.2.0-preview.61). |
| Deconstruction | ✅ |
var (a, b) = tuple maps to [a, b] = tuple, and so do (var c, var d) = … and nested designations. A record or a struct deconstructs through the Deconstruct it calls, in every shape: a declaration, an assignment ((a, b) = point, (var a, b) = point), a foreach (var (a, b) in points) and a nested deconstruction. A record's own reads its members, and a Deconstruct the app wrote runs. The targets' receivers and indices are evaluated before the value, as C# evaluates them, so (xs[i++], xs[i++]) = point writes the elements C# writes (Since 0.2.0-preview.60). Each part reaches its target as C# puts it there: converted to the target's type (an int part into a long), and written by what the target is, a dictionary's entry through its class, in an assignment, a declaration and a loop (Since 0.2.0-preview.60). |
| Expression variables | ✅ | What a pattern, an out var or a deconstruction declares is declared where C# declares it, in every kind of member: in the enclosing block after an if or an expression statement, inside a loop or a using, and fresh on every loop iteration and every call, so a closure keeps its own value. A name JavaScript reserves (@class) is renamed once, at its declaration and every reference (Since 0.2.0-preview.59). |
| Local Functions | ✅ | Transpiled to inner functions. An out or ref parameter carries its value back to the caller, as a method's does (Since 0.2.0-preview.60). |
for loops |
✅ | 1:1. A loop's variable is one per loop, as in C#: a closure made in the body reads its current value, never a copy of its own iteration's, and so is what its initializer declares (out var n, var (i, j) = (0, 3)), a head of expressions included (for (Seed(out var n); …)) (Since 0.2.0-preview.60). |
| Delegates | ✅ | Called as values, by name, through a member, or from any expression (handlers[0](x), Make()(x), since 0.2.0-preview.60). |
Records (with) |
✅ | A copy of the record onto its own class, then the members the expression names: like .NET's copy, it runs no initializer and no constructor. A struct's with, and one on the code engine's records, go through the twin's own copy. A record whose chain declares a copy constructor is copied through it, as C#'s with is: a declared one starts its members at their zero, runs its base's copy with what : base(…) passes and then its body, and a synthesized level copies its own members (Since 0.2.0-preview.61). |
| Constructors | ✅ | A record's, a struct's or a plain class's twin constructor is the C# constructor the call binds, each one reached by the counts of arguments it takes, its optional parameters counted (a plain class kept its widest one before 0.2.0-preview.61). One that does its own work (the primary one, or an explicit one that does not chain) binds its parameters, runs every initializer in declaration order, a field's and a property's alike, a derived type's before its base's constructor, calls its base's constructor with its own arguments and runs its body; one that chains with : this(…) runs the one it chains to and then its own body, even when that one's body returns early. Each argument lands in its parameter's place and is evaluated in the order it is written, in new, : this(…), : base(…) and a base clause alike: a named one, an omitted optional one, and a params array passed whole (named or not) or packed from its elements; a variable an argument declares (out var n) is declared where it is evaluated, and a constructor's parameter is a variable its body may assign. A record's copy constructor is no branch. Two constructors that take the same count of arguments fail the build (EQ1009), and so do a chain to a constructor that chains in turn and : this() to a struct's implicit constructor beside constructors of its own. A struct's zero (default, an array's slot, new S() with no constructor of its own, a generic struct's, which holds each type argument's zero, and one from another assembly included) runs none of it, its static constructor included. A plain class's primary constructor binds its parameters as a record's does, and one a member reads is held on each instance under its name, so one that lands on another member's name fails the build (EQ1007). One difference stays: a base constructor that reads a derived class's member through a virtual member reads it unset, where .NET reads it initialized, since JavaScript lets nothing touch the instance before super() returns (Since 0.2.0-preview.61). |
| Indexers | ✅ | An instance indexer is its twin's item(…) and setItem(…, value), and every access to it calls them: a read, a write, a compound assignment, ++ on a value of any type, ??=, ?[…], a deconstruction's target, a key from the end (^1, at the count its type names) and an object initializer's entry, the receiver and the keys evaluated once each, each key in its parameter's place (a named key, an omitted optional key, a params key). An assignment answers the value it assigned, whatever the setter does with its copy, and an explicit implementation of an interface's indexer answers the interface's accesses. A second indexer in a type, or any member on Item or SetItem beside one, fails the build (EQ1007) (Since 0.2.0-preview.61). An access through a list's face (IList<T>, IReadOnlyList<T>, IList) reaches whatever the face holds when it runs, an array's or a list's element and a twin's item and setItem alike, in every one of those forms, and its Count counts a type of your own by its own Count, never by a Length beside it: the face read a subscript and a length, which a twin does not answer, before 0.2.0-preview.61 (Since 0.2.0-preview.61). |
| Ranges | ✅ |
x[a..b] over a string, an array or a list is JavaScript's slice, and an endpoint that may be ^0 resolves against the length as Index.GetOffset does. Over a type of your own with a Length (or a Count) and a Slice(int start, int length), it calls that Slice as C# lowers the range: the receiver once, then the endpoints in the order they are written, then the count, read only where an endpoint counts from the end or the end is left open. So strip[1..3] is Slice(1, 2), where it called the twin's slice(1, 3) before 0.2.0-preview.61. A range handed to an indexer that takes the Range itself fails the build (EQ2004), as a Range stored as a value does (Since 0.2.0-preview.61). |
| Static initialization | ✅ | A type's statics start at their zero, then the initializers run in declaration order, then the static constructor, once, the first time one of them is read or written: static int A = B + 1; static int B = 2; makes A 1, as in .NET, in a record, a struct, a class, a static class and a component. A type with a static constructor runs it before its first instance and the first use of any of its static members, a method, a computed property and an operator included. The initializers run in declaration order across fields, properties and field-like events, and the static constructor's body after them, a return in it ending it alone; one that throws makes every use of the type throw a TypeInitializationException, as .NET does; a static whose initializer C# folds to a constant is its value; and a component's static constructor runs once (Since 0.2.0-preview.61). |
typeof |
✅ | Maps to type name string literal. |
| Base types | ✅ | A class extends its base's twin however the base is written: bare, with its namespace, through an alias or with global:: (Since 0.2.0-preview.60). |
| Qualified type names | ✅ | A type reached through its namespace, part of it, the whole of it or global:: is imported as a type reached through a using is: inside App.Chat, Portal.Fold.Text(n) answers what Fold.Text(n) answers under using App.Portal; (Since 0.2.0-preview.61). |
base calls |
✅ | Maps to super keyword. |
using static |
✅ | A member reached by its simple name answers what its qualified spelling answers: PI is Math.PI, Join(…) is string.Join(…), an enum's High is Level.High, and Now under using static System.DateTime is DateTime.Now (Since 0.2.0-preview.61; until then a .NET member outside Math, MathF, string, a constant and an enum failed the build even where its qualified spelling translated). A .NET member whose qualified spelling has no translation either fails the build (EQ2004) instead of reaching the browser (Since 0.2.0-preview.60). |
A type's own Count |
✅ | A type the app or a library it references declares reads its own Count: a library's domain model read it as an array's length, undefined (Since 0.2.0-preview.60). |
cast & as |
✅ | Maps to JS passthrough/truncation. |
sizeof |
✅ | Maps to C# primitive sizes. |
| Anonymous Methods | ✅ |
delegate(...) { ... } maps to arrow functions. |
params |
✅ | A REST parameter (...xs). Both C# call forms work: expanded arguments and an array passed whole, which spreads. Since 0.2.0-preview.15. |
stackalloc |
✅ | Maps to Typed Arrays (e.g., Int32Array). |
yield return |
✅ | Maps to JS Generator Functions (function*). |
try / catch / finally |
✅ | The catch clauses are ONE JavaScript catch that tries them in C#'s order: each by its exception type, a type derived from it included, and by its when filter, which declares its own variables (out var n, a pattern) and answers false where it throws, as .NET's does. An exception no clause takes goes on, and throw; rethrows the exception caught. An exception is a JavaScript Error that carries its .NET types, an exception class of the app's own and Exception e = new("…") included, so e is ArgumentException, a switch over exceptions and as test the type too, and an exception the runtime throws is the type .NET throws there (an overflow, a missing key, a bad format, a refused cast); a JavaScript TypeError is a NullReferenceException. One difference stays, the platform's: .NET runs a filter BEFORE the finally blocks between the throw and the clause, and JavaScript after them. Since 0.2.0-preview.60. A framework exception's message is composed as .NET composes it: where no message or a null one was given, the text .NET writes for that constructor, read from .NET itself, then (Parameter 'x'), and the actual value and a disposed object's name on lines of their own, an aggregate's inner messages after its own and a type initializer's sentence, and ParamName, ActualValue, InnerException and TypeName read what the constructor took, an aggregate's InnerException being its first inner one, the runtime's own argument exceptions naming their parameter too (Since 0.2.0-preview.61). A null argument is refused as .NET refuses it, an ArgumentNullException naming the parameter, by LINQ's Max, Min and ToDictionary, by a LINQ operator handed a sequence that is not a list (Concat's second, Zip's first), by new Guid(text), char.GetUnicodeCategory(text, i), CancellationToken.Register and CreateLinkedTokenSource, and CompareTo(object) answers 1 for a null, as .NET's does. A member lowered to JavaScript's own method, LINQ over a list among them, still reads a null through null, a NullReferenceException: the conformance suite calls every member of the translated surface with a null for each reference parameter, and lists those in null-argument-gaps.baseline.txt with their reasons (Since 0.2.0-preview.61). An exception class of the app's is a class over the runtime's exception base, the Error that carries its types: its fields, its properties, its constructors and its methods are its own, and its base call hands what a new of its .NET base hands, so : base(message, inner) reaches Message and InnerException, : base("bad", name) over ArgumentException reads bad (Parameter 'x') and its ParamName, a class over InvalidOperationException that gives no message reads that type's own text, a missing message over Exception is .NET's default (Exception of type 'App.Failure' was thrown.), a member of its own under a name the base holds (a Name, an override of Message or of ParamName) answers for it, and a generic one is caught by its construction (catch (Failed<int>)) from the moment its constructor runs, a throw this there included. A missing message names the type as .NET names it at run time (App.Failed`1[System.Int32], App.Outer+Inner). It is transpiled as any class is: one the browser never sees, whose members reach what it cannot run, is marked [ServerOnly], and two of its constructors that take as many arguments are refused (EQ1009), save the one only .NET's serialization calls, (SerializationInfo, StreamingContext), which Visual Studio's template writes and the twin leaves out, and a call of that one the browser would make, a new or the base call of another constructor, fails the build (EQ1009). An exception built or tested where a type argument it carries is a type parameter fails the build too (EQ2014), since JavaScript erases it: new Failed<T>() in a generic method, catch (Failed<T>), new Missing() inside the generic class that declares it. One declared inside another type is a class too, its twin named by its owner (Holder$Nested), and so is what an exception class declares, a record of the reason it carries (Rejected$Reason). In a snippet compiled on its own, with no project to read its bases from, an exception class of the app's is still the runtime's Error with its types and none of its members (#611, since 0.2.0-preview.61). |
throw expressions |
✅ |
x ?? throw …, c ? a : throw … and => throw …, the exception evaluated only where it is thrown, an await in it included. Since 0.2.0-preview.60. |
lock |
Transpiled to the body alone (JS is single-threaded). Its expression runs once, in front of it, unless it only reads a variable, a field or this (Since 0.2.0-preview.60; one that declares a variable, since 0.2.0-preview.59). new object(), the usual gate, is a fresh object, with or without an empty initializer. |
The compiler includes specialized strategies for nearly all LINQ methods.
| Logic | Methods | Status |
|---|---|---|
| Filtering |
Where, OfType
|
✅ |
| Projection |
Select, SelectMany, Cast
|
✅ |
| Partitioning |
Skip, Take, SkipWhile, TakeWhile
|
✅ |
| Ordering |
OrderBy, OrderByDescending, ThenBy, Reverse
|
✅ |
| Aggregation |
Count, Sum, Min, Max, Average, Aggregate
|
✅ |
| Quantifiers |
Any, All, Contains
|
✅ |
| Sets |
Distinct, DistinctBy, Union, Intersect, Except, Concat
|
✅ |
| Elements |
First, FirstOrDefault, Single, Last, ElementAt
|
✅ |
| Utility |
SequenceEqual, DefaultIfEmpty
|
✅ |
| Generation |
Enumerable.Range, Enumerable.Repeat, Enumerable.Empty
|
✅ |
| Conversion |
ToList, ToArray, ToDictionary, ToHashSet
|
✅ |
| Grouping/Join |
GroupBy, Join, Zip
|
✅ |
Every operator reads its source as the sequence .NET enumerates, whatever the browser holds it as: an
array or a list as it is, a HashSet, a dictionary's pairs and the runtime's sorted set, sorted
dictionary, sorted list, queue, stack and linked list by their own enumeration (a stack from its top),
and a string by its chars, the UTF-16 code units. ToList and ToArray always make a new array, so a
copy and its source are two lists. A string's foreach and ToCharArray give code units too, a
surrogate pair being two chars, new string(chars) and new string(chars, start, length) build their
text, and a char static handed over as a delegate works, "a1b2".Count(char.IsDigit) and
Count(IsDigit) under using static System.Char alike (Since 0.2.0-preview.60).
Enumerable.Range and Repeat evaluate their arguments once, where C# does, an await among them
included: Range(Start(), 3) calls Start once, and Repeat(new List<int>(), 3) repeats one list. A
negative count, or a Range past int.MaxValue, throws .NET's ArgumentOutOfRangeException
(Since 0.2.0-preview.60).
A comparer handed to ToDictionary, ToLookup, GroupBy, Distinct or ToHashSet passes the fence a
collection's constructor passes. One that asks for the key type's default equality
(EqualityComparer<T>.Default, a null comparer, or StringComparer.Ordinal for a string) answers as
the operator does without it, each selector in its parameter whether it is named or not, and any other,
such as StringComparer.OrdinalIgnoreCase, fails the build with EQ2007. ToDictionary and GroupBy
refused every comparer, ToLookup called one as an element selector and Distinct dropped whatever
it was handed, before 0.2.0-preview.61 (Since 0.2.0-preview.61).
We map common .NET types to their JavaScript equivalents.
| .NET Type | JavaScript Equivalent |
|---|---|
string |
String |
int, double, float
|
Number |
bool |
Boolean |
object |
Object |
dynamic |
any |
- ✅
Join,Format.string.Joinreads any sequence (an array, a list, aHashSet, aLinkedList, a dictionary's keys, a sequence behind an interface) and writes each value as .NET'sToStringwrites it: a bool asTrue, an enum by its member's name, a float by its own digits, a double in .NET's notation (1E+21), a null as nothing; values passed one by one are each written by their own type, and a null separator writes nothing (Since 0.2.0-preview.61) - ✅
IsNullOrEmpty,IsNullOrWhiteSpace - ✅
Split,Replace,Substring,Trim - ✅
ToLower,ToUpper,StartsWith,EndsWith - ✅
Equals,StartsWith,EndsWith,Contains,IndexOf,LastIndexOfandReplacegiven aStringComparison, by that comparison, andCompareToby the current culture; a search by a culture comparison fails the build (EQ1004) (Since 0.2.0-preview.60)
The temporal types are backed by tick-precise compat types (100-ns ticks, proleptic Gregorian
calendar, not the lossy new Date() / numeric-milliseconds mapping). Ctors, components, Add*,
arithmetic (-→TimeSpan), comparisons, and .ToString() all match .NET, a date's text written
through one formatter in the culture in force (Since 0.2.0-preview.61); values cross the SSR wire as
ISO-8601 / "c" strings and are hydrated back into the compat type.
See the .NET BCL Coverage & Conformance table below for the per-type detail.
- ✅
List<T>→ Javascript Array[];Removeanswers the bool .NET does and compares asEqualityComparer<T>.Defaultdoes, a record by value, a tuple element by element and NaN by NaN (Since 0.2.0-preview.59).IndexOf,LastIndexOf,ContainsandRemove,Array.IndexOfandArray.LastIndexOf, and LINQ'sContainscompare by that one comparison, chosen from the element type: a NaN, a record, a tuple and a decimal found by value, a class by its ownEquals, a tuple's array member by reference, asValueTuple.Equalscompares it.SortandArray.Sortare .NET's introspective sort, unstable as .NET's is, so equal elements land where .NET's land, by the type's default comparer (a double's NaNs first), aComparison<T>, aStringComparer's six,Comparer<T>.Createor a comparer of your own, with .NET's exceptions; any other comparer fails the build (EQ2007).BinarySearchanswers the complement of the insertion point for a miss,FindandFindLastthe element type's default,RemoveAllhow many it removed,CopyTowrites into the array it is handed, and the ranges ofFindIndex,FindLastIndex,IndexOfandLastIndexOfare checked in .NET's words (Since 0.2.0-preview.61) - ✅
ICollection<T>'sAddandClearadd and empty as the collection behind the interface does when the call runs: a list appends, a set adds a value it does not hold, a linked list adds last, a dictionary adds the pair and refuses a key already there in .NET's words, and a type of your own runs its ownAddandClear, a member's collection initializer (Items = { 1, 2 }) included. They were an array'spushandsplice, which a set, a linked list and a dictionary lack, before 0.2.0-preview.61 (Since 0.2.0-preview.61) - ✅
Dictionary<TKey, TValue>,IDictionaryandIReadOnlyDictionary→ the runtime's dictionary class ($eq.collections.dictionary), which holds its entries by slot as .NET's does: it enumerates in insertion order, and an entry added after a removal takes the removed one's place, the last freed first, sod[3] = 30; d[1] = 10;enumerates3,1. Each key keeps its type (anintkey comes back a number, alonga BigInt, achara char) and is found as the key type's default comparer finds it: by value for a record, a struct, a tuple, a decimal or a date, by identity for a number, a string, a char, a bool, an enum or aGuid, and by the value's own equality where the type does not decide (object, an interface, a type parameter, a class), so two equal records underobjectare one key; a tuple key's array element by reference (Since 0.2.0-preview.61).new Dictionary<K, V>(other)copies, a deconstructingforeach (var (k, v) in d)works on every dictionary, the sorted ones included,TryAdd,ContainsValue,Remove(key, out value),Keys.ContainsandKeys.Countanswer as .NET's, andToDictionarybuilds the same class.Add, a collection initializer and the constructor that copies pairs refuse a key already there with .NET'sArgumentException, in each collection's words, while the indexer, an object initializer's[key] = valueandTryAddreplace or decline, andnew KeyValuePair<K, V>(key, value)andKeyValuePair.Createbuild the pair a dictionary yields (Since 0.2.0-preview.60). A key added while aforeachwalks the dictionary ends the walk with anInvalidOperationException, as .NET's enumerator does, while an overwrite, a removal andClearrun on, and a sorted dictionary refuses every change. A dictionary in server-rendered state, in a Server Action's result or arguments, or in a topic's payload crosses as its[key, value]pairs and arrives as the class, its keys in their type, in the order it enumerated, integer keys included: a JSON object would have listed those first and ascending (Since 0.2.0-preview.61, #437). A comparer that asks for what the browser already does (null,EqualityComparer<T>.Default,StringComparer.Ordinal) builds the dictionary, and a sorted one keeps the order it asks for (Since 0.2.0-preview.61), while one that changes equality, such asStringComparer.OrdinalIgnoreCase, fails the build withEQ2007. Since 0.2.0-preview.60. ItsKeysandValuesare live views, as .NET's are: one read after the dictionary changed shows the change, a key added while one is walked ends the walk, the keys' ownContainsfinds a key as the dictionary does, and each element comes back as it is, a delegate included. A LINQ query over a view, as over any source, reads it whole before its first callback (#685).EnsureCapacityanswers the prime .NET settles on, a capacity constructor and a copy size the dictionary as .NET's do, andTrimExcesspacks the entries so the next key goes after the others (Since 0.2.0-preview.61). - ✅
HashSet<T>,ISet<T>andIReadOnlySet<T>→ the runtime's set ($eq.collections.hashSet), built wherever one is made (a constructor, an initializer, a collection expression,ToHashSet, a value that crossed from the server). It holds its elements by slot in the table the runtime's dictionary keeps its keys in: an element added after a removal takes the removed one's slot, the last freed first, so{ 3, 1 }less 3, plus 5, enumerates5,1, and each set operation (UnionWith,IntersectWith,ExceptWith,SymmetricExceptWith,RemoveWhere), a copy andTrimExcessleave the slots where .NET's leave them. An element is found as its type's default comparer finds it, the rule a dictionary's keys take: a date, a decimal, a tuple and a record by value, NaN equal to NaN.Addanswers whether the element was new, the set predicates,CopyToandEnsureCapacityanswer as .NET's, and an element added during aforeachends the walk with anInvalidOperationException.TryGetValueis not handled yet and fails the build (EQ1004); a comparer that changes equality fails it too (EQ2007) (Since 0.2.0-preview.61). - ✅
ContainsKey/TryGetValuenever answer for"constructor","toString"or anything else onObject.prototype. Since 0.2.0-preview.15;GetValueOrDefaulttoo, since 0.2.0-preview.58. - ✅
TryGetValueandGetValueOrDefaultanswer a miss withdefault(TValue)on every dictionary, the sorted ones included:0for anint,falsefor abool,nullfor a reference, sod.TryGetValue(k, out var n); d[k] = n + 1;counts. The receiver, the key and an explicit default are each evaluated once, in the order written; anout _receives nothing, and anoutfield or element is written where C# writes it. The static form,CollectionExtensions.GetValueOrDefault(d, key), reads the dictionary its first argument passes. Since 0.2.0-preview.58. - ✅
Queue<T>,Stack<T>,LinkedList<T>,SortedSet<T>,SortedDictionary<K,V>,SortedList<K,V>→ runtime compat collections under$eq.collections.*. A sorted one orders asComparer<T>.Defaultorders its element type: a string in the current culture, a double with its NaN first, an enum by its value, a decimal, a date and a type of your own that implementsIComparableby itsCompareTo, andStringComparer.Ordinalby code unit. A queue's and a stack'sContainsfind a decimal, a date or a record by its value (Since 0.2.0-preview.60). Each writes the JSON .NET writes, an array in the order it enumerates, a stack from its top, so a Server Action argument reaches the server as the collection (Since 0.2.0-preview.61).
- ✅
Task,Task<T>→Promise. - ✅
Task.Delay→setTimeoutwrapper. - ✅
Task.WhenAll,Task.WhenAny. ⚠️ Task.Runexecutes on the main thread (microtask), NOT a background thread.
- ✅
Console.WriteLine→console.log. - ✅
Math.*(Min, Max, Abs, Round, etc.) →Math.*. - ✅
Guid(NewGuid,Empty,Parse,TryParse,new Guid(text)) →crypto.randomUUID()for a new one, and its canonical text, the lowercaseDformat, for one read from text in any format .NET reads, so two spellings of one Guid are one value to==, a dictionary and a set (Since 0.2.0-preview.60). - ✅
Regex→ JavaScriptRegExp.
eQuantic.UI.Lucide / Heroicons / RadixIcons / TablerIcons / Phosphor / SimpleIcons / BootstrapIcons / Iconoir / ...
Purpose: Comprehensive Icon Sets. Contains:
- SVG resolution logic
- Specialized icon components
- A generated
IconGlyphper icon, as a property so the trimmer can drop the ones nobody names
| Package | Purpose | Status |
|---|---|---|
| eQuantic.UI.Charts | Charts written once, drawn by every realizer. | ✅ |
| eQuantic.UI.Lottie | High-performance animations. | ✅ |
| eQuantic.UI.Image | Optimized (Lazy/Blur/Priority). | ✅ |
| Pattern | Interface | Description | Status |
|---|---|---|---|
| Metadata Management | IHandleMetadata |
SEO tags and head elements. | ✅ |
| Asset Management | IRequireAssets |
Dynamic script/style injection. | ✅ |
| Server Actions | [ServerAction] |
Secure RPC from Client to Server. | ✅ |
| Compound Components | N/A | Semantic sub-component patterns. | ✅ |
-
Blocking Code:
- ❌
.Wait(),.Resulton Tasks are NOT supported. You must useawait. Blocking the main thread freezes the browser UI.
- ❌
-
Reflection:
- ❌
System.Reflectionis largely unsupported. - ✅
typeof(T).Nameandnameof(...)are supported constants.
- ❌
-
File System:
- ❌
System.IO(File,Directory) in a client component is a build error (EQ2112): the browser has no file system to give it. - Use Server Actions to handle file operations.
- ❌
-
Numbers:
- ✅
int/double/floatmap to JS numbers. Integer division truncates (Math.trunc), and a division or remainder by zero, orMinValueby -1 onintandlong, throws the exception .NET throws (Since 0.2.0-preview.58); afloatis rounded to single precision wherever it is produced, exactly as RyuJIT rounds it, afloat?target's compound assignment and increment included (Since 0.2.0-preview.58), andMath.Roundis .NET's: an exact midpoint, everyMidpointRoundingmode. - ✅
decimalis exact end-to-end: compiled to the runtimeDecimalcompat type (BigInt mantissa + scale), so0.1m + 0.2m == 0.3mistrue. Decimals also cross the wire as JSON strings (EqJson) and are hydrated back intoDecimalon the client (the field'sDecimaldefault drives a type-preserving coercion, seehydrate-value.ts), so server-provided decimals keep all 28 digits instead of rounding through a double. A decimal read from text (decimal.Parse/TryParse,Convert.ToDecimal("…")) follows .NET's grammar under theNumberStylesthe call names and rounds as .NET's parser rounds. It is read in the invariant culture, and the call says so withCultureInfo.InvariantCulture: with no culture the build warns (EQ2110) and with another one it fails (EQ2108), since the browser reads no other culture's text. Adoubleor afloatconverted to a decimal keeps .NET's 15 or 7 significant digits:(decimal)(0.1 + 0.2)is0.3. Since 0.2.0-preview.58. - ✅ A constant is its value in its C# type:
decimal.MaxValue,MinValue,Zero,OneandMinusOneare exact decimals, spelleddecimalorDecimal, and alongconstant such asTimeSpan.TicksPerSecondis a BigInt, so it meets any other long. A decimal literal is its value, so1_000.5mworks, and a parameter's default filled in for an argument a named one skips keeps its type: a decimal, a long, a char, a string with a quote in it. A constant reached by its bare name under ausing staticis written the same way. A constant of an enum type is a value of that enum, not its number, and a decimal constant matches by value in a pattern, a switch arm or acase(d is 1m,case decimal.One:). Since 0.2.0-preview.60. - ✅ Every other numeric type reads text as .NET does too:
int,uint,long,ulong,short,ushort,byte,sbyte,doubleandfloatParse/TryParse, andConvert.ToInt32("…")and its siblings, read the whole text under the type's ownNumberStyles(Integer, orFloat | AllowThousandsfor a real) or the one the call names. Text that is not a number throws aFormatException("12abc","1e3"as an int,"0x1F"withoutHexNumber), a value the type cannot hold anOverflowException, a failedTryParseleaves0, a hex or binary style reads the type's bits ("FFFFFFFF"is-1for an int), a long's text is a BigInt, and a float is rounded once, from the digits. The culture is decimal's: EQ2110 with none, EQ2108 with any butCultureInfo.InvariantCulture. Since 0.2.0-preview.58. - ✅ A
double's and afloat's text is .NET's: the shortest digits that read back as the value, fixed notation from 1e-4 up to 1e16 for a double and 1e8 for a float, scientific outside (1E+17,1E-05),-0for a negative zero andInfinityfor an infinity, the same throughToString(),ToString(CultureInfo.InvariantCulture), a concatenation and an interpolation; a nullfloat?ordouble?writes nothing. Since 0.2.0-preview.58. - ✅ A number's text with no specifier is the culture's, wherever C# writes it: a
concatenation, a plain or an aligned interpolation hole,
ToString(),Convert.ToString,string.Format("{0}"),string.Concat,string.Join, aStringBuilder'sAppendandInsert, and a record's text, as .NET writes them in the culture in force:-1,5in pt-BR, a minus sign the culture spells (sv-SE's−5), a float in its own digits, a decimal with its scale, and never-0for an integer JavaScript holds as a negative zero. A value written for a machine says so withToString(CultureInfo.InvariantCulture), which keeps its shape in every culture. Since 0.2.0-preview.61. - ✅ Through the formatter too: a float formatted with an alignment or a specifier
(
$"{f,8}",$"{f:G}"),ToString("G"),ToString("R")and a float passed tostring.Formatwrite its own digits,GandRin .NET's notation (1E+21). Astring.Formatplaceholder aligns ({0,5},{0,-5}), a null in an aligned hole is padded, and a params array passed whole, astring[]or a collection expression included, is its values.string.Format's provider follows the policyToStringhas:CultureInfo.InvariantCultureformats invariantly, a date's patterns and the generic¤of a currency included,CultureInfo.CurrentCultureor a null as a call with none does, and any other is EQ2108. A bool'sToString()isTrueorFalse. One limit: a float boxed in anobjectthe compiler cannot see through (object o = 0.1f; $"{o}", or anobject[]built at run time) is the plain number underneath. Since 0.2.0-preview.58. - ✅ A number prints through its specifier as .NET prints it:
Eandewrite the mantissa's digits, the exponent's sign and at least three exponent digits ((12345.0).ToString("E2")is1.23E+004).F,N,P,C,Gand a custom picture round the number's EXACT value, a double's binary value included ((0.1).ToString("F20")is0.10000000000000000555) and a long's and a decimal's every digit (9007199254740993L.ToString("N0")is9,007,199,254,740,993), with an exact half rounded to even for a double and a float and away from zero for a decimal and an integer ((2.5).ToString("F0")is2,(2.5m).ToString("F0")is3,125.ToString("E1")is1.3E+002).N,F,CandPare laid out from the culture's ownNumberFormatInfo, which travels with every page: its patterns, symbols, separators, group sizes and default digits ((1234.5).ToString("N")is1,234.500in en-US on a server whose .NET reads ICU), in ASCII digits in every culture, ar-EG's included, as .NET writes them (Since 0.2.0-preview.61). A culture switched to with no server to ask is laid out byIntl.NumberFormatv3 (ES2023): Chrome and Edge 106, Firefox 116 and Safari 15.4 have it.XandB(binary) write a negative integer at its type's width (((short)-1).ToString("X")isFFFF, an int's isFFFFFFFF). A custom picture is drawn as .NET draws it: sections (0.00;(0.00);'zero'), quoted and escaped text,%and‰, exponents (0.00E+00), and the commas before the point that divide by a thousand (#,##0,). A precision can pass 100 digits, as .NET allows. Numbers are grouped as .NET groups them (es-ES writes1.234) and signed with the culture's minus (sv-SE's−), and a specifier the type does not take throws, as in .NET ((2.5).ToString("D")). Since 0.2.0-preview.59. A double underD,XorBthrows whatever it holds,nintandnuintare 64-bit integers, and the per mille sign (ar-SA's؉), an exponent's signs and the percent sign are the culture's own. Since 0.2.0-preview.61. - ✅
long/ulongare now exact: compiled to JS BigInt via thelongcompat helper.9007199254740993L + 1Lis9007199254740994(a plain JS number would round to…992). Literals become BigInt (5L→5n), and so does a literal with no suffix that only a long holds (637000000000000000→637000000000000000n, Since 0.2.0-preview.58); arithmetic/comparison operands are wrapped inlong()(which coercesnumber/string→bigint) so mixed expressions never throw. On the wire, 64-bit ints cross as JSON strings (Server Actions + SSR state, viaEqJson) so values beyond 2^53 survive the round trip. Other numeric type suffixes (1.5f,100u) are stripped.
- ✅
-
Thread Safety:
- Since JS is single-threaded,
lockstatements are compiled away (ignored). -
Thread.Sleepis not supported (useTask.Delay).
- Since JS is single-threaded,
Transpilation fidelity is enforced by a conformance harness (tests/eQuantic.UI.Conformance.Tests):
each case runs the same C# expression two ways, transpiled to JS (executed via the embedded Bun) and
evaluated directly in .NET (Roslyn scripting), and asserts identical results. The suite counts its cases (over a thousand at the time of writing, and growing with every construct); all of them are green.
(Most recent: every dictionary through the runtime's dictionary class, $eq.collections.dictionary.)
This covers both expressions and statement blocks (control flow: if/for/foreach/while/switch/
try-catch-finally/local functions; the block runs in an IIFE and its returned value is compared).
Every construct resolves via one of three mechanisms (see docs/DOTNET-COVERAGE-PROGRAM.md):
-
Native strategy: idiomatic JS when the runtime has an equivalent.
-
.NET-compat runtime helper: faithful .NET semantics where JS has none. The transpiler emits these under a single namespace
$eq(organised by domain), brought in with one import per module,import { $eq } from "@equantic/runtime"(resolved by the page's import map), instead of N loose helper imports, and$eq.*can never collide with a user identifier in the generated scope:-
$eq.num:dec(exact Decimal),decParse/decTryParse/decConvert(a decimal from text orConvert),intParse/intTryParse/intConvertandrealParse/realTryParse/realConvert(an integer, a double or a float from text orConvert),decFromDouble/decFromSingle(a binary number into a decimal),long(Int64 via BigInt),double/single(a number's text) -
$eq.math:round(banker's rounding) -
$eq.text:format(number/string formatting),stringBuilder -
$eq.time:dateTime,timeSpan,dateOnly,timeOnly,dateTimeOffset -
$eq.enums:text,parse,tryParse,zero,names,values,isDefined(an enum as .NET reads it, from the shape eqc writes at each call) -
$eq.collections:queue(FIFO),stack(LIFO),dictionary(held by slot as .NET's, keys found by identity or by value),linkedList,sortedSet/sortedDictionary/sortedList(key-sorted) -
$eq.nullable:arith,cmp,unary(liftedNullable<T>operators: null-propagating arithmetic and unary operators, false-on-null relational) -
$eq.equals: structural (value) equality for records/structs/tuples (backs==,.Equals,Contains,Distinct) -
$eq.hash:of(GetHashCode(), which agrees with$eq.equals, so valuesEqualsfinds equal hash equal, a vocabulary value type such asPointorTypeStyleincluded, and a type that overrides it answers its own),instance(the same call on a reference, refused when it is null as .NET throwsNullReferenceException, while an emptyNullable<T>answers 0),group(the method groupvalue.GetHashCode, which takes its receiver once and refuses a null one where the delegate is made),combine(HashCode.Combine, by its parameters even when they are named) andidentity(object.GetHashCodeof a class that does not override it, and an array's, however its items change). .NET's own numbers are not stable across processes, so the browser keeps the contract, never the server's number (Since 0.2.0-preview.60) -
$eq.css:styleBuilder,classBuilder,joinClasses,whenClass(the styling subsystem)
-
-
Fail-on-unsupported: a construct with no possible JS representation now raises a build error (with a stable
EQcode) instead of being silently emitted verbatim. Two layers:-
UnsupportedConstructStrategy(EQ2001): typed-reference intrinsics (__makeref,__refvalue,__reftype), pointer types, function pointers. -
goto/goto case/goto default(EQ2002): no JS equivalent; restructure with loops/conditionals. (unsafe/fixed/lockblocks unwrap to their body,lockbeing a single-threaded no-op, and a bare label drops to its inner statement.) -
SemanticValidatorclient/server boundary (EQ21xx): calls intoSystem.IO,System.Net.Http,System.Net.Sockets, EF Core /System.Data, OS threading (Thread/Monitor/Mutex),Process,InteropServices(P/Invoke),Reflection.Emitfrom a client component. (System.Threading.Tasksis not forbidden: async maps to Promise.) The fix is to move the call into a[ServerAction].
Any other construct that hits no strategy is an error (
EQ1001for an expression,EQ1002for a statement,EQ1003for anything else) and nothing is emitted for it. It used to pass through verbatim with a warning, which meant a build that succeeded and a page that threw — refusing what it cannot represent is the whole point. Diagnostics print in MSBuild-canonical form, so they faildotnet buildand the IDE links them. The full list is in Diagnostics. -
| Area | Status | Notes |
|---|---|---|
| Arithmetic / bitwise / comparison | ✅ | integer division truncates; %, shifts, `& |
Math.* / MathF.*
|
✅ | the same table as double.* / float.*, with .NET's own compositions; Round is .NET's (exact midpoint, every mode) |
decimal |
✅ | exact via Decimal (literals + + - * / % == != < > <= >=, the remainder with the dividend's sign, Since 0.2.0-preview.58 for %); the constants MaxValue, MinValue, Zero, One and MinusOne (Since 0.2.0-preview.60); decimal.Parse/TryParse by .NET's grammar and rounding, the NumberStyles a call names included; Convert.ToDecimal and the casts from double/float at .NET's 15 and 7 digits |
| Numeric constants | ✅ |
int.MaxValue, double.Epsilon, … → literals |
Parsing / Convert.*
|
✅ |
Parse/TryParse on every numeric type, and Convert.ToInt32/ToInt64/ToDouble/… of text, by .NET's grammar (Since 0.2.0-preview.58); bool.Parse/TryParse and Convert.ToBoolean of text as .NET's Boolean.TryParse reads it: "True" or "False" in any case, trimmed of white space and NULs, and .NET's exceptions, and Convert.ToBoolean of a number as whether it is not zero, of a bool as itself, and of a char or a date as .NET's InvalidCastException (Since 0.2.0-preview.59); Convert.ToString/…
|
| Strings | ✅ | Substring/IndexOf/Replace/Split/Pad/Trim/Trim(char)/Concat/Format/Join/IsNullOrEmpty/IsNullOrWhiteSpace; Trim(chars) and its two halves read the string and then the characters once, an await among them included, and no characters (an empty or a null array) trims white space, as .NET does (Since 0.2.0-preview.60); Trim(), its two halves, IsNullOrWhiteSpace and a bare Split() read .NET's white space, where JavaScript's leaves U+0085 and takes U+FEFF (Since 0.2.0-preview.58); a string's own Equals, StartsWith, EndsWith, Contains, IndexOf and LastIndexOf (with a start and a count) and Replace given a StringComparison answer by that comparison, written out or held in a variable: OrdinalIgnoreCase keeps .NET's casing (the Kelvin sign, the long s and the dotless i are not k, s and I, and a surrogate pair is read as its code point), a start or a count outside the string throws .NET's exception, Replace writes its replacement as text, and CompareTo orders by the current culture, as string.Compare does (Since 0.2.0-preview.60). IndexOf(char, startIndex[, count]) and LastIndexOf(char, startIndex[, count]) search that range and throw .NET's exception for a start or a count outside the string: LastIndexOf's start stands on a char of the string, and an empty string answers -1 (Since 0.2.0-preview.61, where the start was clamped and the count dropped). A SEARCH by a culture comparison, or with a CultureInfo, fails the build (EQ1004), because the browser has no culture-aware search: search by Ordinal or OrdinalIgnoreCase, or compare whole strings with Equals or string.Compare. The overloads without a comparison search ordinally in the browser and by the current culture in .NET (#532). |
char.* |
✅ |
ToUpper/ToLower/IsDigit/IsLetter/IsWhiteSpace/… (Unicode-aware; IsWhiteSpace is .NET's set, U+0085 in and U+FEFF out, Since 0.2.0-preview.58) |
StringBuilder |
✅ | compat type: Append(incl. bool→"True"/"False")/AppendLine("\n")/Insert/Remove/Replace/Clear/Length/ToString. The overloads that count or take a range write what .NET writes, Append('x', 3), Append(text, start, count), Insert(i, text, count), Replace over a range and ToString(start, length), a char[] as its characters and a null as nothing, and each refusal is .NET's (Since 0.2.0-preview.61). AppendFormat and AppendJoin append what string.Format and string.Join write, Capacity follows the chunks .NET allocates through every edit, MaxCapacity and EnsureCapacity answer, the Chars indexer reads and writes a char, Length cuts the text or fills it with \0, Equals(StringBuilder) compares the text and CopyTo copies it, each refusing what .NET refuses; an interpolated Append($"…") or AppendLine($"…") appends each part in turn, as .NET's handler does, so a hole that reads the builder sees the parts before it, a named argument is bound to its parameter, and a text longer than the browser's string can hold is .NET's OutOfMemoryException; a provider other than the current culture on Append, GetChunks and the span overloads fail the build (Since 0.2.0-preview.61) |
CancellationTokenSource / CancellationToken
|
✅ | the runtime's, as .NET's: a source (with a delay or none), its Token, IsCancellationRequested, Cancel, CancelAfter, Dispose and CreateLinkedTokenSource; a token's None (and default, and new CancellationToken(bool)), IsCancellationRequested, CanBeCanceled, ThrowIfCancellationRequested (an OperationCanceledException, "The operation was canceled.") and Register of a callback, whose registration Disposes and Unregisters. Callbacks run once, the last registered first, one registered late runs at once and is given back the default registration (its token is None), and the ones that throw are gathered into an AggregateException, as in .NET. A method of the three passed as a callback (token.Register(source.Cancel)) keeps its receiver, and a linked source, disposed, lets go of the tokens it follows. A disposed source lets go of its callbacks too, a registration still unregistering once after it, and new CancellationTokenSource(delay, timeProvider) fails the build with EQ2004: the runtime's source is timed by the real clock. A delay is taken up to 4294967294 ms, as .NET takes it, and a month is waited out though a browser's timer holds about 24.8 days. default(CancellationTokenRegistration) is the registration of nothing, whose token is None. Any other member of the three fails the build with EQ2004 (Since 0.2.0-preview.61) |
| LINQ | ✅ | Where/Select/SelectMany/Where-Select(indexed)/OrderBy/Distinct(By)/GroupBy/ToDictionary/ToLookup/Zip/Chunk/MinBy/MaxBy/Take(While)/Skip(While)/Aggregate/Sum/Min/Max/Average/Count/Any/All/First/Last/Concat/Reverse/Join/GroupJoin/ThenBy/ThenByDescending (Join/GroupJoin = order-preserving hash join over primitive keys; OrderBy+ThenBy = single stable composite sort, source copied). IGrouping from GroupBy/ToLookup is usable as a sequence (iterate, g.Select/g.Sum/g.Count()) and exposes g.Key; ILookup [key] indexer is not modelled. A comparer that asks for the default passes ToDictionary, ToLookup, GroupBy, Distinct and ToHashSet, and any other fails the build (EQ2007) (Since 0.2.0-preview.61). |
| Collections | ✅ | List, Dictionary, HashSet (incl. initializers and .Count; a set is the runtime's set, held by slot and finding its elements by value where its element type does, Since 0.2.0-preview.61); Queue/Stack/LinkedList compat; sorted family SortedSet/SortedDictionary/SortedList (key-sorted enumeration). Every dictionary is the runtime's dictionary class, $eq.collections.dictionary, enumerating by slot as .NET's does, a record/struct/tuple key found by value (construction and copy, d[k] get/set, ContainsKey/Add/Remove/Clear/TryGetValue/GetValueOrDefault/TryAdd/ContainsValue, Keys/Values/Count, foreach) (Since 0.2.0-preview.60). Reading a missing key throws KeyNotFoundException, and so do a compound assignment and an increment on one (Since 0.2.0-preview.58 for the increment). ILookup[key] indexer returns the group (or empty for an absent key). ICollection<T>'s Add and Clear reach the collection behind the interface, and IList<T> and IReadOnlyList<T> read and write a type of your own through its indexer and count it by its Count (Since 0.2.0-preview.61). |
enum |
✅ | member-name string, a [Flags] enum its number (equality/switch/ternary, text and formats, the statics of Enum) |
long/ulong
|
✅ | exact via BigInt (long helper); literals 5L→5n, and constants too (TimeSpan.TicksPerSecond → 10000000n, Since 0.2.0-preview.60), wire as JSON string |
DateTime |
✅ | tick-precise DateTime compat type: ctors, components, Add* to the tick as .NET 7+ lands on it (a fraction truncated, a count or a result out of range refused in .NET's words, AddMicroseconds included, Since 0.2.0-preview.59), -→TimeSpan, comparisons, .ToString()/format; ISO-8601 wire + hydration. ToString(), ToString(format) and ToString(format, provider) print as .NET prints them (no specifier, or a null or empty one, is the current culture's G, and no time zone moves the value's parts but U's): every one-letter standard specifier (d to Y, s, u and R from the value's own parts, o from them with the zone its kind names, U moved to UTC unless it is UTC already), a letter that is no standard specifier refused as .NET refuses it, a custom picture token by token with its fraction of a second (HH:mm:ss.fff), the culture's separators for / and : and the era for g (Since 0.2.0-preview.61), the invariant culture's patterns for CultureInfo.InvariantCulture or no culture in force, and a named culture refused at the build (EQ2108); ToShortDateString() and its siblings are d, D, t and T (Since 0.2.0-preview.60). Add, Subtract, AddMonths, AddYears and the + and - operators, compound forms included, refuse a result off the calendar in .NET's words, the operators naming their parameter t (Since 0.2.0-preview.60). Every constructor the compiler lets through builds the value .NET builds, each argument in its parameter's place, a named one included: the components with a millisecond, a microsecond and a kind, the ticks with a kind, and a DateOnly with a TimeOnly, a component, a kind or ticks out of range refused in .NET's words, and an overload that takes a Calendar refused at the build (EQ1004). A value carries its Kind (Now and Today local, UtcNow UTC, SpecifyKind), which arithmetic keeps and equality, ordering and the hash leave out; ToLocalTime() and ToUniversalTime() convert by it in the browser's time zone, Microsecond and Nanosecond answer, and the JSON writes a UTC value with Z and a local one with its offset and reads them back as System.Text.Json does (Z a UTC time, an offset moved to the browser's local time), while DateTime.Parse moves a written zone to the local time. A local time made from an instant keeps which of two repeated hours it is, as .NET keeps it beside the kind, through arithmetic until SpecifyKind, so its way back to UTC is that instant, and a date's text writes its kind: o and K end a UTC time with Z and a local one with its offset, and z, zz and zzz write the zone's offset for anything not UTC (Since 0.2.0-preview.61) |
TimeSpan |
✅ | tick-precise TimeSpan compat type: From*, ctors, components/totals, + -, comparisons, .NET "c" .ToString(); "c" wire + hydration |
DateOnly / TimeOnly
|
✅ | compat types (.NET 6+): ctors, components, Add*(+ TimeOnly wrap, one product converted as .NET 9+ converts a double, Since 0.2.0-preview.59, a DateOnly leaving the calendar refused in .NET's words, Since 0.2.0-preview.60), comparisons, ToString(), ToString(format) and ToString(format, provider) as .NET prints them in the culture in force: d and t with no format, the standard specifiers each type takes and a custom picture of its own parts, anything else .NET's FormatException, and ToShortDateString(), ToLongDateString(), ToShortTimeString() and ToLongTimeString() as d, D, t and T (Since 0.2.0-preview.61); ISO wire + hydration |
DateTimeOffset |
✅ | tick-precise compat type (wall-clock + offset, compared by the instant): ctors, components, MinValue/MaxValue (Since 0.2.0-preview.58), Offset/UtcDateTime/LocalDateTime, ToOffset, Add* to the tick with AddMilliseconds/AddMicroseconds, the clock time and then the UTC time refused in .NET's words (Since 0.2.0-preview.59), AddMonths and AddYears included (Since 0.2.0-preview.60), From/ToUnixTime* (ToUnixTimeSeconds and ToUnixTimeMilliseconds a long, rounded down before 1970 as .NET rounds it, Since 0.2.0-preview.61), - →TimeSpan, instant comparisons, ToString() and its overloads as .NET prints them in the culture in force: the short date, the long time and the offset with no format, every standard specifier but U, o and K writing the offset and u and R converting by it (Since 0.2.0-preview.61); ISO+offset wire + hydration. Every constructor builds the value .NET builds: the components with a millisecond and a microsecond, the ticks, a DateOnly with a TimeOnly, and a DateTime, which without an offset takes the browser's for a time that is not UTC; an offset that is not whole minutes, is past fourteen hours, or is not zero for a UTC DateTime is refused in .NET's words. Now is the instant at the browser's offset, LocalDateTime and ToLocalTime() read the browser's time zone, and Microsecond and Nanosecond answer (Since 0.2.0-preview.61) |
record / struct / value tuple |
✅ |
Value semantics. Records/structs are instances of their twin classes (positional new Point(1,2) calls the constructor C# calls, and an object initializer is applied after it), tuples are arrays with element access by position (t.Item1) and by declared name ((int X, int Y).X) → index. ==/!=, .Equals, Distinct compare structurally via $eq.equals, and Contains, IndexOf and a set by the element type's default comparer, a tuple's array member by reference (Since 0.2.0-preview.61); with copies-and-replaces. A mutable struct and a value tuple are copied where C# copies them, so a write through one name never shows through another the value was assigned, passed, returned or boxed to: the twin copies on write, and copies this where it leaves its struct's member (Since 0.2.0-preview.61). Deconstruction var (a, b) = … works for tuples (array destructuring, discard holes) and records (object destructuring by Deconstruct order). Records emit as named JS classes carrying their user instance methods + a structural equals, prototype-preserving with, and .NET toString; value semantics are unchanged. A record's equals compares its runtime type (a Dog never equals an Animal), what its base compares and every field it declares, a private one included, and its text names the members .NET's PrintMembers writes, in its order: its base's members first, then its own, a computed property and a property whose getter has an accessibility of its own included, an override left to the base, each once, and Name { } for none. Two members of a record or a struct that land on one name of the twin (struct S(int x) { public int X … }, a field celsius beside Celsius) fail the build (EQ1007); a primary constructor's parameter read only by an initializer (struct Point(int x) { public int X { get; } = x; }) is the constructor's, and builds; an abstract property holds nothing. Each value is written as .NET writes it: a bool as True, an enum by its member's name, a null as nothing and a number in the culture in force, and a plain class or struct that writes no text of its own writes its type's full name (My.App.Outer+In), where the browser wrote [object Object] or a record's text; a generic one does not yet (Since 0.2.0-preview.61). A positional parameter whose property the record declares (record Box(int X) { public int X { get; set; } = X; }) is one member, the declared one (Since 0.2.0-preview.61). A member starts as its declaration says, whatever the expression: a positional parameter, a property's or a field's initializer (= 1.5m, = 5 on a long, = new(), = "#" + Id), written once, in the class's constructor (Since 0.2.0-preview.59), which runs every initializer on every construction, whatever an object initializer then sets (since 0.2.0-preview.61; see Constructors above). A record's and a struct's members are lowered as a class's are: an async method, an iterator, an out or ref parameter, and the variable an expression body's pattern binds (o is P p && p.X == X) work in their methods, operators, conversions and computed properties, their own and a default an interface supplies, and a getter that yields returns its sequence in a class too (Since 0.2.0-preview.60). The build pipeline discovers records by scanning and emits each as its own module; components that reference a record import it automatically (reactive, no hardcoded list). SSR values are re-hydrated back into the record class on the client (recursively, restoring nested records and compat members), so methods/instanceof survive. Covers positional and body records and plain structs, plus record inheritance (extends + super) and generic records, whose equality compares the closed type a value was built as: a Box<int> never equals a Box<double>, and one built inside generic code, whose type arguments the build cannot know, is not taken for another type (Since 0.2.0-preview.61). Record-keyed dictionaries (Dictionary<RecordKey, V>) find a key by value, so equal-by-value keys collide as in .NET. Each member compares as EqualityComparer<T>.Default compares its type, so an array, a list or a collection's interface (IReadOnlyList<int>) by reference, and a tuple's Equals compares its elements the same way, a tuple of another arity unequal and a null pair equal only to another; a tuple seen only as object, or as an interface a tuple implements, is an array on the web and compares by its elements (Since 0.2.0-preview.61). |
Nullable<T> (T?) |
✅ |
HasValue/Value, GetValueOrDefault() (type-aware default: 0/false/$eq.num.dec(0)/enum zero-member/…) and GetValueOrDefault(fallback), ??; lifted operators via $eq.nullable.*: arithmetic propagates null, relational (< > <= >=) is false when either side is null (not a numeric coercion); a compound assignment (+=, *=, …), an increment and a unary -, ~ or + on a nullable number are lifted too, a null staying null and a value following its type's rule (a float? rounds, a byte? wraps, a decimal? stays exact; Since 0.2.0-preview.58). No-arg GetValueOrDefault() on DateTime?/Guid?/struct yields null, so use the fallback form there. |
Guid |
✅ |
Guid.NewGuid()→crypto.randomUUID(), Guid.Empty, Guid.Parse/TryParse/new Guid(text) → the canonical lowercase D text (Since 0.2.0-preview.60); string wire |
🌐 English · Português
🏁 Start here
🏗️ Architecture
- Architecture Overview
- Write-Once Components
- Declarative Surface
- Package Architecture
- Components
- Styling
- Localization
- Analytics & GTM
📱 Write-once
⚙️ Compilation
⚡ Runtime
🔌 Server
🎨 Ecosystem
🚀 Development