English · 한국어
Transpile C# types and pure logic into TypeScript — with cross-validation that proves the two stay in lockstep.
Existing C#-to-TypeScript generators stop at type shape: enums, records, DTO interfaces. That leaves logic mirrors — validation rules, pricing and tax math, permission checks, compact wire-format codecs — duplicated by hand on both sides of the JS/.NET boundary. Mirrorgen targets the half that's actually expensive to maintain.
// C# (source of truth)
[Transpile]
public static bool IsWithinDistance(int x1, int y1, int x2, int y2, int radius)
{
var dx = x2 - x1;
var dy = y2 - y1;
return dx * dx + dy * dy <= radius * radius;
}// _generated/rules.ts — emitted at build time
export function isWithinDistance(x1: number, y1: number, x2: number, y2: number, radius: number): boolean {
const dx = (x2 - x1) | 0;
const dy = (y2 - y1) | 0;
return Math.imul(dx, dx) + Math.imul(dy, dy) <= Math.imul(radius, radius);
}And the part nobody else does — Mirrorgen emits a fixture cross-test: C# generates random inputs and expected outputs at build time, the TypeScript test consumes the same fixture, and CI fails the moment the two implementations diverge by a single bit.
Any project with a .NET backend (ASP.NET, SignalR, Blazor Server, a custom simulation) plus a TypeScript client ends up maintaining two copies of the same logic — and the cost is silent drift, not duplication itself. A constant changes on one side, the other forgets, a bug ships. Existing tools can't help because they only mirror data shape.
Mirrorgen exists to make logic mirrors as cheap as type mirrors:
- One source of truth: C#
- One opt-in marker:
[Transpile] - Generated TypeScript ships next to the consuming code
- Cross-validation fixtures keep the two byte-exact, forever
Existing tools cluster into two groups: type-only generators that mirror DTO shape but not logic (TypeGen, Tapper, Reinforced.Typings, NSwag), and full-app C# → JS compilers that let you write the whole client in C# at the cost of a substantial in-browser runtime (Bridge.NET, H5, JSIL, Blazor WASM). F# teams have Fable for the equivalent F# → TS story. Mirrorgen targets the gap: plain .ts output that mirrors selected C# logic alongside a hand-written TypeScript client.
| Mirrorgen | Type-only generators | Full-app C# → JS | Fable (F# → TS) | |
|---|---|---|---|---|
| C# logic body | narrow subset | ❌ | full C# | n/a (F#) |
| Output | TypeScript | TypeScript | JavaScript | TypeScript or JS |
| Client runtime cost | KB (tiny helper) | none | hundreds of KB+ | varies |
| Cross-validation fixtures | ✅ | ❌ | ❌ | ❌ |
| Subset-enforcing analyzer | ✅ | ❌ | n/a | n/a |
| Use model | mirror logic next to hand-written TS | mirror DTOs | write whole client in C# | write whole client in F# |
If you want to write your whole client in C# or F#, use H5 / Blazor WASM / Fable. If you only need DTO shapes mirrored, TypeGen or Tapper is smaller and battle-tested. Mirrorgen exists because hand-mirroring rules / pricing / validation / codecs while keeping them in lockstep with C# is what burns time, and none of those columns targets it. The closest C# → TS method transpiler attempt (Rosetta by andry-tino) is explicitly dead in its README; lessons from why it stalled inform Mirrorgen's design and are written up in docs/CONCEPT.md.
Mirrorgen is a subset transpiler. It does not try to translate arbitrary C#. The supported subset is intentionally narrow so the output stays predictable, debuggable, and byte-exact:
- ❌
async/await,Task, threading - ❌ LINQ, deferred enumerables
- ❌
Span<T>,ref,unsafe, pointers - ❌ Exceptions (return result types instead)
- ❌ Reflection
- ❌ Inheritance and virtual dispatch
A Roslyn analyzer marks any [Transpile] member that crosses these lines as a build error — you find out in your IDE, not at codegen time.
See docs/CONCEPT.md for the precise subset spec and roadmap.
v0.3.1 — published on NuGet (all five packages). The walker subset is feature-complete, plugin discovery works end-to-end, and the cross-validation harness keeps the C# and TS sides byte-equivalent on every push. The API may still shift while early adopters shake it out.
Watch / star to be pinged on new releases.
<!-- YourProject.Rules.csproj -->
<ItemGroup>
<PackageReference Include="Mirrorgen.Attributes" Version="0.3.1" />
<PackageReference Include="Mirrorgen.Analyzers" Version="0.3.1" PrivateAssets="all" />
<PackageReference Include="Mirrorgen.MSBuild" Version="0.3.1" PrivateAssets="all" />
</ItemGroup>
<PropertyGroup>
<MirrorgenOutput>$(MSBuildThisFileDirectory)../YourProject.Client/src/_generated/</MirrorgenOutput>
<MirrorgenConfig>YourProject.MirrorgenConfig</MirrorgenConfig>
</PropertyGroup>// Domain type mapping plugin
public sealed class MirrorgenConfig : IMirrorgenExtension
{
public void Configure(IMirrorgenBuilder b)
{
b.MapType<OrderId>().AsPrimitive("number");
b.MapType<Money>().RuntimeImport("Money");
}
}The TypeScript side gets a tiny runtime helper package (@mirrorgen/runtime) for integer arithmetic, equality, and pluggable domain helpers — kept under a few KB so the generated code stays clean.
MIT.
mirrorgen/
src/
Mirrorgen.Core/ # Roslyn-based transpiler engine
Mirrorgen.Attributes/ # [Transpile], [GenerateCrossTest] — zero-dep, ref'd by users
Mirrorgen.Analyzers/ # Roslyn analyzer enforcing the subset
Mirrorgen.MSBuild/ # MSBuild target wrapper
Mirrorgen.Cli/ # `dotnet mirrorgen` tool
runtime-ts/ # @mirrorgen/runtime npm package
cross/ # checked-in TS emit + fixture JSON for cross-validation
cross-fixtures/ # C# subject methods consumed by the cross-validation flow
scripts/
regen-cross.sh # rebuilds runtime-ts/cross/{subject.ts,subject.fixtures.json}
samples/
minimal/ # smallest possible C# -> TS example
pricing-rules/ # non-trivial sample with domain-type mapping
tests/
Mirrorgen.Tests/ # walker / operator / fixture-generator unit tests
docs/
CONCEPT.md # this is where the design lives
SUBSET.md # exact subset spec
ROADMAP.md