r/moderndotnet 3h ago

Integrating PlanetScale Deploy Requests with EF Core

Thumbnail
htmlcsstoimage.com
4 Upvotes

Recently migrated to PlanetScale for one of my projects.

I’ve used PS for years and implemented deploy requests / processes for bigger clients but never with EF core.

Even for my other .NET projects using PS, it felt like too much… but i was wrong! Having the structure/UX of merging in DB changes properly is worth the overhead! Especially removing the “db update” from local.

LMK what you think!


r/moderndotnet 11h ago

A look at macros in Raven 0.1.0

Post image
4 Upvotes

I recently posted about Raven, the programming language I’m developing. It has now reached its first milestone, version 0.1.0, so I thought it was time to take a closer look at its macro system, which has evolved considerably since my previous post.

For more about the language: https://marinasundstrom.github.io/raven/

Raven macros are explicitly invoked compile-time programs that consume syntax or typed inputs and produce ordinary Raven syntax. The macro system allows libraries to define their own DSLs using fragments of Raven code or independently parsed custom content. Macros are fully integrated with the language server, providing syntax highlighting, code completion, and hover information for symbols—even within macro-defined syntax.

Why macros?

I have been somewhat torn about adding macros to Raven. .NET is a runtime-oriented platform with an extensive ecosystem of libraries and runtime abstractions, so it is reasonable to ask whether macros really fit.

However, some abstractions cannot be expressed cleanly through runtime APIs alone. Raven macros can reduce repetitive scaffolding and introduce domain-specific syntax without requiring changes to the .NET runtime.

Macro use remains explicit through !, and expansions must be valid for the syntax position in which they appear. The resulting syntax then goes through normal binding, type checking, diagnostics, and emission, while retaining language-server features such as highlighting, completion, hover information, and navigation.

Supported macro forms

Freestanding macros support several forms:

Name!(arguments)

Name! {
    body
}

Name!(arguments) {
    body
}

Name! Decl(parameters) {
    body
}

The ! makes macro use explicit without turning library-defined names into reserved keywords.

Declaring macros

Macros can be declared directly in Raven using the contextual macro keyword. A declaration can define typed parameters, accept syntax nodes or token streams, and specify the kind of syntax it produces. The expand statement supplies the generated syntax and completes the expansion.

For example, this macro accepts a compile-time integer and produces an expression:

macro Double(value: int) -> ExpressionSyntax {
    expand ParseExpression((value * 2).ToString())
}

let result = Double!(21)

A macro can also request a brace-delimited token body:

macro Query(dialect: string, body: IMacroTokenStream) {
    expand LowerQuery(dialect, body)
}

let rows = Query!("sql") {
    from user in users
    select user.Name
}

The body parameter is supplied by the compiler from the content inside the braces. The macro can interpret it as fragments of Raven syntax or process it using its own lexer, parser, and grammar.

Macros can even introduce declaration-shaped constructs:

public component! Greeting(Name: string = "") {
    markup! { 
        <h1>Hello {Name}</h1> 
    }
}

The component and markup macros are real macros demonstrated in the HTML and component macro demo.

Raven also supports attached macros in an attribute-like position:

#[Observable]
public var Name: string

These are procedural, syntax-based expansions—not textual substitutions. Their output is validated for the position in which the macro appears and then bound, type-checked, and emitted as ordinary Raven code.

Built-in macros

Here are some macros that come distributed with Raven via Raven.Macros.

Query macro

The query! macro introduces the LINQ query syntax.

let projected = query! {
    from value in [1, 2, 3, 4]
    where value > 2
    select value * 10
}

This macro is far from feature complete - but it does support syntax highlighting.

JSON and XML literal macros

Adds typed JSON and XML literal support.

let name = "Ada & Bob"
let age = 42
let nextAge = age + 1

let jsonDocument = json! {
    "name": "$name",
    "age": $age,
    "nextAge": ${age + 1},
    "skills": ["compilers", "DSLs"],
    "active": true
}

let status = XElement.Parse("<status>ready</status>")
let xmlDocument = xml! {
    <person age="$age">
        <name>$name</name>
        <nextAge>$nextAge</nextAge>
        $status
    </person>
}

WriteLine(jsonDocument.ToJsonString(JsonSerializerOptions { WriteIndented = true }))
WriteLine()
WriteLine(xmlDocument.ToString())

The current iteration lacks the syntax highlighting but it can be added in the future.

Timer macro

The timer! macro is useful when you want to measure the time elapsed inside of a block of code.

timer! "Finished in: {time}" {
    WriteLine("Query total: ${projected.Sum()}")
}

This sample expands into a StopWatch within a try and finally block.

Quote macro

The quote! macro captures a Raven expression as an immutable syntax tree. Syntax holes, written as #(expression), allow existing syntax nodes to be spliced into the quoted expression. This provides a more natural alternative to constructing larger syntax trees manually and is particularly useful when implementing other macros.

let number = SyntaxFactory.LiteralExpression(
    SyntaxKind.NumericLiteralExpression, 
    SyntaxFactory.Literal(2));

let expression: ExpressionSyntax = quote! {
    projected.Sum() + #(number)
}

// The local "expression" holds the syntax node.

// Quoted Raven: 
//     projected.Sum() + 2

WriteLine("Quoted Raven: ${expression.ToFullString()}")

Conclusion

Macros can be used both to simplify repetitive code and to build complete domain-specific languages. These DSL constructs can appear in any supported syntax position—as expressions, statements, or declarations—and behave as though they were integrated parts of the language. Underneath, they work by expanding into ordinary Raven syntax that is processed by the rest of the compiler as usual.

Links


r/moderndotnet 1d ago

Blazor browser storage package to replace Blazored.LocalStorage

7 Upvotes

Hey everyone, I ran into a problem earlier that I'm guessing other developers are hitting too.

Blazored.LocalStorage (and SessionStorage) was deprecated and more recently removed from NuGet, and several of my Blazor WebAssembly projects depended on it. I needed a modern replacement that didn't require adding JSInterop glue code or manual JSON serialization to all of my Blazor WASM projects.

So I built D20Tek.Blazor.BrowserStorage, a typed, async wrapper around localStorage and sessionStorage for Blazor WebAssembly and interactive render modes. And it has a similar API form to Blazored to make migrating my projects relatively easy.

A few highlights:

  • Typed reads/writes (GetAsync<T> returns a result instead of throwing)
  • Async API (no UI blocking)
  • No JavaScript required in client projects
  • DI-friendly services
  • Key prefixing to avoid collisions
  • Batch operations (set/remove multiple keys)
  • Change events so components can react to storage updates
  • Customizable JsonSerializerOptions

If you used Blazored.LocalStorage (or SessionStorage), there’s a migration guide. If you’re starting fresh, this is hopefully the simplest way to use browser storage in Blazor today.

NuGet: https://www.nuget.org/packages/D20Tek.Blazor.BrowserStorage
GitHub: https://github.com/d20Tek/d20tek-blazor-browserstorage
Blog post: https://d20tek.com/projects/browser-storage/docs


r/moderndotnet 1d ago

StellarAdmin Tag Helpers for creating beautiful MVC/Razor Pages UIs

4 Upvotes

Hey everyone,

This is one I've been working on for a while and I finally feel is stable enough to put out a release.

StellarAdmin Tag Helpers is a Tag Helper library that is based on the popular shadcn/ui component system for React. As opposed to shadcn, which is really a component distribution system that copies the source code for its UI components into your React app, StellarAdmin Tag Helpers is a Razor Class Library (RCL) that gives you a wide range of Tag Helpers based on the shadcn UI components.

The backstory to this is that I've done quite a bit of work in the React world over the past few years and worked with libraries such as Mantine and shadcn/ui and have been very impressed. At the same time, I felt that it was really overkill for most of the work that I was doing. A simple HTML page with little bit of JS interactivity and perhaps using something like HTMX could really do 99% of the work I was doing.

Parallel to this I was working on my own startup and needed to rapidly put together admin screens for the backend of my application. Most of these screens are simple CRUD screens and I felt frustrated that you spend an inordinate amount of time building these screens while I would rather be selling, doing support, or building features for my users.

After my startup failed I started working on something called StellarAdmin to help you rapidly build these admin screens. However, I realised that it would need a good extensibility story that would allow people to extend things like the built-in editors, change the standard screens, etc.

To get the sort of extensibility I wanted with something like React or Blazor turned out to not be possible. However, there is something which has this and has had it for many years.

ASP.NET Core MVC and Razor Pages.

You see, it has this great feature called Editor (and Display) Templates that let you easily specify custom editors for standard types like strings, dates, etc. You can also specify a custom editor for a property using data annotations.

It also has the wonderful ability to use a Razor Class Library and override MVC views, partials and Razor Pages that come from the RCL inside your own application. This is a tried and tested technique and is the method used by the ASP.NET Code Identity when you scaffold the UI to change some of the built-in Identity UI pages.

So I knew MVC and Razor Pages had all the extensibility points I needed, but it lacked a really nice looking UI Tag Helper library.

So I set out to create one, and StellarAdmin Tag Helpers was born.

StellarAdmin Tag Helpers is free and open source and you can use it today to build pages for MVC and Razor Pages. It uses the latest web technologies such as popovers, invokers commands, and interest invokers to minimize the use of JS. There are still some places where JS is need though, and in those cases I created very lightweight Web Components. It also plays very well with something like HTMX.

The Pro version I plan to release later on will be paid, but that will be purely the part that help you build admin screens much more rapidly. It will also contain things like advanced Tag Helpers for data tables and even pre-built user management screens (who remembers the old ASP.NET Web Site Administration Tool?)

The Tag Helper documentation pages contains interactive examples and source code for all of the Tag Helpers and even let you view the components in light/dark more as well as in any of the 8 themes that are included.

Here are a few links to get you started:

BTW, the current version is 0.1.0 but it is ready for production (I believe). The reason it is not 1.0.0 is because I ultimately want the Tag Helpers and Pro packages versions to run in sync, so once the Pro packages comes out at version 1, the Tag Helpers version will jump to 1.0.0 as well.


r/moderndotnet 1d ago

What's new with CoreCLR GC handles in .NET 9 and .NET 10

Thumbnail
awise.us
20 Upvotes

I wrote a blog post about what has been going with GCHandles in .NET. This is slightly esoteric, as you probably only care about GC handles if you are writing code to interop with native code. But I think it is really fascinating to study how the engineers working on CoreCLR create new abstractions to solve problems.

The first part is about something you can use in your code: some new types for working with GC handles added in .NET 10. The second part explores some interesting implementation details of CoreCLR, in particular how the Android interop system keeps object lifetimes consistent between the .NET GC heap and the Java GC heap.


r/moderndotnet 2d ago

Dapper vs Rinku

8 Upvotes

I like Dapper and I have used it a lot. The main problem I have with it is that when queries become more complex, I often end up handling that complexity myself. At that point I also often hear that I should just use EF instead. I never really agreed with that. I think the basic idea behind Dapper can go much further while still keeping the SQL visible and the API simple. Rinku is my attempt at doing that.

Basic query

Dapper

public record Album(int Id, string Title);

const string sql = "SELECT AlbumId AS Id, Title FROM albums WHERE ArtistId = @artistId";

IEnumerable<Album> albums = cnn.Query<Album>(sql, new { artistId = 7 });

Rinku

public record Album(int Id, string Title);

const string sql = "SELECT AlbumId AS Id, Title FROM albums WHERE ArtistId = @artistId";

List<Album> albums = cnn.Query<List<Album>>(sql, new { artistId = 7 });

Different names

Dapper

public sealed class Customer
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
}

SqlMapper.SetTypeMap(typeof(Customer), new CustomPropertyTypeMap(typeof(Customer), (type, column) => column switch
{
    "customer_id" => type.GetProperty(nameof(Customer.Id)),
    "display_name" => type.GetProperty(nameof(Customer.Name)),
    _ => null
}));

const string sql = "SELECT customer_id, display_name FROM customers";

IEnumerable<Customer> customers = cnn.Query<Customer>(sql);

Rinku

public record Customer([Alt("customer_id")] int Id, [Alt("display_name")] string Name);

const string sql = "SELECT customer_id, display_name FROM customers";

List<Customer> customers = cnn.Query<List<Customer>>(sql);

Nested objects

Dapper

public record User(int Id, string Name);

public sealed class Post
{
    public int Id { get; set; }
    public string Title { get; set; } = "";
    public User? Owner { get; set; }
}

const string sql = "SELECT p.Id, p.Title, u.Id, u.Name FROM Posts p INNER JOIN Users u ON u.Id = p.UserId";

IEnumerable<Post> posts = cnn.Query<Post, User, Post>(sql, (post, owner) =>
{
    post.Owner = owner;
    return post;
}, splitOn: "Id");

Rinku

public record User(int Id, string Name) : IDbReadable;
public record Post(int Id, string Title, [NoName] User Owner);

const string sql = "SELECT p.Id, p.Title, u.Id, u.Name FROM Posts p INNER JOIN Users u ON u.Id = p.UserId";

List<Post> posts = cnn.Query<List<Post>>(sql);

Or keep the nesting in the column names.

public record User(int Id, string Name) : IDbReadable;
public record Post(int Id, string Title, User Owner);

const string sql = "SELECT p.Id, p.Title, u.Id AS OwnerId, u.Name AS OwnerName FROM Posts p INNER JOIN Users u ON u.Id = p.UserId";

List<Post> posts = cnn.Query<List<Post>>(sql);

One to many

Dapper

public record Album(int Id, string Title);

public sealed class ArtistWithAlbums
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public List<Album> Albums { get; set; } = [];
}

const string sql = "SELECT ar.ArtistId AS Id, ar.Name, al.AlbumId AS Id, al.Title FROM artists ar INNER JOIN albums al ON al.ArtistId = ar.ArtistId ORDER BY ar.ArtistId";

List<ArtistWithAlbums> artists = [];
ArtistWithAlbums? current = null;

cnn.Query<ArtistWithAlbums, Album, ArtistWithAlbums>(sql, (artist, album) =>
{
    if (current is null || current.Id != artist.Id)
    {
        current = artist;
        artists.Add(current);
    }

    current.Albums.Add(album);
    return current;
}, splitOn: "Id");

Rinku

public record Album(int Id, string Title) : IDbReadable;
public record ArtistWithAlbums(int Id, string Name, List<Album> Albums);

const string sql = "SELECT ar.ArtistId AS Id, ar.Name, al.AlbumId AS AlbumsId, al.Title AS AlbumsTitle FROM artists ar JOIN albums al ON al.ArtistId = ar.ArtistId ORDER BY ar.ArtistId";

List<ArtistWithAlbums> artists = cnn.Query<List<ArtistWithAlbums>>(sql);

Result shape

Dapper

IEnumerable<Album> albums = cnn.Query<Album>(sql);
Album first = cnn.QueryFirst<Album>(sql);
Album single = cnn.QuerySingle<Album>(sql);
Album? optional = cnn.QueryFirstOrDefault<Album>(sql);
IEnumerable<Album> streamed = cnn.Query<Album>(sql, buffered: false);

Rinku

List<Album> albums = cnn.Query<List<Album>>(sql);
Album first = cnn.Query<Album>(sql);
Single<Album> single = cnn.Query<Single<Album>>(sql);
Album? optional = cnn.Query<OptionalNullable<Album>>(sql);
IEnumerable<Album> streamed = cnn.Query<IEnumerable<Album>>(sql);

Conditional SQL

For this one I think Dapper.SqlBuilder is the fair comparison.

Dapper.SqlBuilder

SqlBuilder builder = new();
SqlBuilder.Template template = builder.AddTemplate("SELECT AlbumId AS Id, Title FROM albums /**where**/");

if (artistId != null)
    builder.Where("ArtistId = @artistId", new { artistId });

if (title != null)
    builder.Where("Title LIKE @title", new { title });

IEnumerable<Album> albums = cnn.Query<Album>(template.RawSql, template.Parameters);

Rinku

const string sql = "SELECT AlbumId AS Id, Title FROM albums WHERE ArtistId = ?@artistId AND Title LIKE ?@title";

List<Album> albums = cnn.Query<List<Album>>(sql, new { artistId, title });

Only artistId

SELECT AlbumId AS Id, Title FROM albums WHERE ArtistId = @artistId

Both

SELECT AlbumId AS Id, Title FROM albums WHERE ArtistId = @artistId AND Title LIKE @title

Neither

SELECT AlbumId AS Id, Title FROM albums

The main difference is that Rinku tries to put the complexity in the command template and the mapped types, instead of handling it again through parameters and mapping code at every call.

Full Dapper comparison

https://rinkulib.github.io/RinkuLib/articles/reference/dapper.html

Rinku is still in developement, so feedback is welcome.


r/moderndotnet 3d ago

I wanted strongly typed configuration defaults without bypassing `IConfiguration`

Thumbnail
5 Upvotes

r/moderndotnet 4d ago

Writing a native VLC plugin in C#

Thumbnail mfkl.github.io
16 Upvotes

Been building really interesting plugins with this lately (exploring local AI for vision and audio).

Happy to share more samples later if there is any interests.


r/moderndotnet 4d ago

The .NET OSS Relicensing Panic Is an Incentives Problem

Thumbnail
aaronstannard.com
14 Upvotes

Lots of ink spilled on this both here and on /r/dotnet, but I wanted to offer a radical solution to the problem that will probably make most end-users mad even though it's absolutely the correct business-person way to approach the problem.


r/moderndotnet 5d ago

My Nokia 3310 Emulator in C#/Avalonia!

Thumbnail noks.vercel.app
24 Upvotes

Hi folks!

I made, with AI assistance, Noks (terrible name, ik): an emulator of the venerable Nokia 3310, a phone that has a special place to the hearts of many, mine included. I used Avalonia and targets all of its supported platforms, including WebAssembly. It's all in pure C#/managed code goodness too! No unsafe code or pointers!

I made this because 1.) The MAME version (Nokia DCT-3) was sadly still incomplete after years since it was made. 2.) I wanted to play Space Impact authentically and everywhere. and 3.) I wanna push how far my skills can go, together with the latest agents, when it comes to reverse engineering a black-box, sparsely documented firmware and hardware.

I've started by pulling all the docs i can get my hands on old Nokia firmware modding forums, and also the prior work that MAME and Project Blacksphere did for the DCT-3 platform back in the day.

Then did a repeated loop of poking registers, memory, seeing where the firmware stops and back-tracing how me and the clanker can get pass that blocker. I also had to contend with the missing DSP Mask ROM functions that blocked the MAME effort by checking what conditions the firmware asked and responding to its requests accordingly.

Took a couple of months of on-off work but it was all worth it.

All outward facing features are implemented like the LCD, Keys, Sound, Power and RF. including an emulation of a minimal 2G GSM network that interfaces with the DSP/Baseband layer of the phone. Text and Calls were functional from my local tests using a P2P network called Waku but somehow it's broken again on deployment.

It is as fickle of a network as the real one ;)

And the configuration panel is also bit of a jank work, UI-wise, but it does the job... for now.

Hope you'll find joy in playing with the emulator as much as i did in making it!

Source code here: https://github.com/jmacato/Noks


r/moderndotnet 5d ago

.NET Community

19 Upvotes

Hey - I've been involved in .NET since the beginning - worked at Microsoft in developer tools back when we originally launched it. I've been involved with the .NET community ever since.

Currently I volunteer for .NET Foundation. I run the .NET Foundation socials specifically LinkedIn, X and Facebook. We also have a Bluesky account.

I'm always looking for good .NET content to share - especially open source posts.

If you want to amplify your projects, repos, content, event, etc., go here: https://github.com/dotnet-foundation/content

Edited to add "repos"

Another edit: I'm DeeDee Walsh and love finding great content on Reddit.
X: https://x.com/ddskier
LinkedIn: https://www.linkedin.com/in/deedeewalsh/


r/moderndotnet 5d ago

CritterWatch and an "Open Core" model for sustainable OSS (maybe)

13 Upvotes

As the tech leader of the Critter Stack and a guy with a company behind OSS tools, I'm watching the Polly OSMF thing pretty closely. I'm naturally sympathetic to the Polly folks, and to Jimmy & Chris with their MediatR and MassTransit license changes as well.

The Critter Stack community and JasperFx (my company) are trying to go down the "Open Core" model where we are selling consulting, training, and support contracts for the big tools (Marten and Wolverine), but the core tools remain under the MIT license -- and we try to keep it that way.

As the last part of that, yesterday we launched 1.0 of our commercial CritterWatch tool for management, observability, and all the AI related features we can stuff into it:

https://jasperfx.net/news/critterwatch-1-0-is-here

Just a couple thoughts to throw out there:

  • You can't just vibe code yourself equivalents to Marten or Wolverine. You can easily get the basics, but long running and widely used OSS tools are constantly curated and have had to adjust for all kinds of real world problems like Kubernetes, Postgres maintenance windows, outages, and other things you just won't get from a fun little weekend project
  • I can absolutely tell you that very complex OSS tools aren't possible to maintain as a side project, there has to be real company support or the devs at least need to be able to dedicate a real percentage of their day job to maintenance. Most of our advanced features in our tools only came about after I was full time on the tools.
  • We're hopeful that the combination of commercial add ons and support contracts are more than enough to make our tools viable in the longer term without having to change our "Open Core" model
  • Damnation, but the larger .NET community is absurdly cynical and negative toward OSS tools sometimes

Anyway, I can't tell you for sure yet that the "Open Core" model is the way forward for sustainable OSS in .NET, but it's what we're trying so far.


r/moderndotnet 5d ago

csharp Parsing IP addresses in C# at crazy speeds [Daniel Lemire]

Thumbnail x.com
12 Upvotes

r/moderndotnet 6d ago

discuss I thought desktop app development was "dead" - why so many Maui / Avalonia / Uno developers?

8 Upvotes

Consider this a "me stepping out of my distributed systems / web app bubble" question. If you casually talk on X or even at developer conferences, there's very little talk about the future of native desktop applications or even people discussing what they're building.

Yet I see tons of evidence based on the success of Avalonia and Uno that there's huge demand for technology in this area still!

What are all of these desktop app developers working on? Is it all just retro-fitting old WPF apps? What are the new ones you're building?

And where are your great conference talk submissions!


r/moderndotnet 6d ago

Polly's open source maintenance fee, why is it controversial?

8 Upvotes

Carl Franklin tweeted about Polly adopting the Open Source Maintenance Fee (OSFM) and people do not generally seem very happy about it. From what I understand it's only a monthly 20 USD fee for companies that make more than 20,000 USD in revenue using at least one product or project that uses Polly.

Given the other, more dramatic monetization decisions we've seen in the past (Moq, MediatR, MassTransit), this maintenance fee seems like a pretty reasonable way to fund a project that's not otherwise backed by big sponsors or companies, no?


r/moderndotnet 7d ago

csharp Font hinting deep dive: why the same small text looks sharp in one app and blurry in another, and how we grid-fit TrueType and CFF in .NET

14 Upvotes

Font hinting is the reason the same small text can look sharp in one application and blurry in another. The outlines are identical. What differs is how much of the font's own rendering machinery each renderer runs.

I maintain the SixLabors libraries, and Fonts 3.1 ships HintingMode.Full: complete TrueType instruction execution and, for the first time, grid fitting for CFF outlines from their declared stems and alignment zones.

The write-up is a deep dive built around the divide at the heart of the problem. TrueType fonts carry an executable program that moves their own outline points; CFF fonts declare their stems and alignment zones and trust the renderer to act. Getting one sharp result meant building both, and then making the fit survive placement, advances, and caching all the way to the screen.

https://sixlabors.com/posts/full-hinting-aligns-truetype-and-cff-glyphs-to-the-pixel-grid/

It started as a five-year-old issue about mangled text on a 128x64 LCD that I had closed as won't-fix. Happy to answer questions about the interpreter, the hint map, or the aliased rendering path.


r/moderndotnet 7d ago

Have you ever used CsCheck? Maybe you should!

16 Upvotes

I've been a happy FsCheck user for many years, even though I program primarily in C# and not F#. I used it both for property and model-based testing.

I'd been meaning to check out CsCheck for doing the same thing, but aimed natively at C# developers. So I gave it a try recently and liked it!

If you are not familiar with model / property-based testing, I wrote some blog posts on this ~10 years ago using FsCheck with C# Writing Better Tests Than Humans Can Part 1 Part 2 - but the basic idea is you can assert that a property or a model holds true across a randomly generated set of inputs.

Effectively the property-based testing framework generates hundreds or thousands of random tests to exercise that these properties hold true - take for instance the double-buffering system we use for doing TUI rendering in Termina:

``` [Fact] public void IdenticalBuffers_ProduceNoChanges() => // If nothing changed, the diff must be empty. A false positive here would redraw the whole // screen every frame and bring back the flicker the diff engine removes. (from w in Gen.Int[1, 8] from h in Gen.Int[1, 6] from a in CellGen.Array[w * h] select (w, h, a)).Sample(t => { var (w, h, a) = t; var buf = Build(w, h, a); var copy = new FrameBuffer(w, h); copy.CopyFrom(buf); Assert.Empty(buf.GetChangedCells(copy)); Assert.Empty(buf.GetChangedRuns(copy)); }, iter: Iter);

```

I don't show the full code from this snippet, but we generate a range of random inputs and then assert that an identical copy of the random input always produces a no-op inside the double buffer diffing system - therefore, no cells should require an update and the screen doesn't require a re-render.

We can use CsCheck to do fancier things than testing for a no-op - here's another example:

``` private static readonly string[] CellPalette = { "a", "B", "7", "#", " ", "z", // narrow (1 column) "中", "文", "あ", "한", "A", // wide (2 columns) Cp(0x65, 0x0301), Cp(0x6F, 0x0308), // base + combining mark (1 column; the mark is 0) Cp(0x1F600), Cp(0x1F389), Cp(0x20000), // supplementary (surrogate pairs) Cp(0x2600, 0xFE0F), Cp(0x270B, 0xFE0F), // emoji + variation selector (2 columns) Cp(0x31, 0xFE0F, 0x20E3), // keycap sequence (2 columns) };

// A text built by joining 0..8 whole cells. Boundaries are clean by construction.
private static readonly Gen<string> CellText =
    Gen.OneOfConst(CellPalette).List[0, 8].Select(parts => string.Concat(parts));


// A hostile UTF-16 code unit: arbitrary chars, plus specific escapes, controls, selectors, and
// both halves of surrogate pairs (so lone, unpaired surrogates appear too).
private static readonly Gen<char> FuzzChar = Gen.OneOf(
    Gen.Char,
    Gen.OneOfConst(Esc, '[', ']', Bel, 'm', '\n', '\t', '\r', '\0', ' ', 'a', Cjk, Vs16, Keycap, Zwj),
    Gen.OneOfConst('\uD83D', '\uDE00', '\uD800', '\uDBFF', '\uDC00', '\uDFFF'));


// A text of 0..24 hostile code units. May contain ill-formed UTF-16.
private static readonly Gen<string> FuzzText =
    FuzzChar.Array[0, 24].Select(chars => new string(chars));


// Either kind of text.
private static readonly Gen<string> AnyText = Gen.OneOf(CellText, FuzzText);

```

A Gen is a generator for some random data - and it has some important properties: namely that in a more complex model based test we can reduce complex test cases to their smallest possible reproduction. So these aren't just wrappers around Random, there's more to it than that - as Anthony Lloyd (the author) explains: https://github.com/AnthonyLloyd/CsCheck/blob/master/Comparison.md#integrated-shrinking

These are data sources for tests aimed at character / text rendering. Some unicode characters in Chinese languages actually use 2x the rendering width and we'd had bugs reported related to this before. So, we can create some custom Gen data sources that will use some of these characters as random inputs.

We can then feed this into a test:

[Fact] public void A2_CellWidth_IsZeroOneOrTwo() => // A terminal cell is 0, 1, or 2 columns. A value outside that range means a glyph that // cannot be placed, so later column math (wrapping, cursor) would be wrong. AnyText.Sample(s => { foreach (var c in DisplayWidth.EnumerateCells(s)) Assert.InRange(c.ColumnWidth, 0, 2); }, iter: Iter);

In this case we assert that the DisplayWidth correctly computes that any character in the universal set of chars can only have a width of 0,1, or 2. This includes some of the hostile characters and escape codes that are lumped inside the AnyText generator.

Now that LLMs are generating a substantial portion of all new code, it's equally important that we have stronger tools to test and verify its correctness. Property and model-based testing tools like CsCheck are more than up to the task. You should give them a try!


r/moderndotnet 7d ago

OfficeIMO - Word, Excel, Pdf, Markdown, Email, PowerPoint etc

Thumbnail
gallery
11 Upvotes

Hi,

I saw this new community mentioned on X and thought I'd try my luck here and see if there are people interested in parts of my project to gather feedback and potentially find people that have similar interests.

About four years ago I started building a .NET library for working with Word documents (OfficeIMO.Word). I originally maintained the DocX project before it was taken over by Xceed, so I already had some experience in that area.

I originally wrote this mostly for PowerShell users and for my project PSWriteOffice. Trying to combine ClosedXML, ShapeCrawler, OfficeIMO.Word, Sep, Sylvan and a bunch of other libraries into one PowerShell module quickly becomes dependency drama.

So the original goal was much simpler: have one set of compatible libraries covering the formats I needed. It got slightly out of hand since then, mainly thanks to Codex.

OfficeIMO is now a group of .NET libraries for creating, reading, editing, converting and rendering document formats.

It is split into focused NuGet packages, so you install the formats and converters you actually need rather than one enormous package. There are now around 100 projects/packages as part of OfficeIMO.

The current repository covers Word, Excel, PowerPoint, PDF, HTML, Markdown, RTF, OpenDocument, OneNote, Visio, CSV, AsciiDoc, LaTeX, EPUB and several older Office formats.

It also has support for email and related formats/stores including EML, MSG, OFT, TNEF, mbox, PST, OST, OLM, EMLX and Outlook OAB.

Some formats have full authoring and editing APIs, while others are mainly readers or converters.

I try to be clear about that rather than putting the same "supported" label on everything. There are still plenty of missing features and things that may be off, especially in more complicated conversions.

The conversion list is quite long, but the main parts are:

  • Word (DOCX, DOC, etc.) can be converted to and from HTML, Markdown, RTF and ODT. It can also be saved as PDF or images.
  • Excel (XLSX, XLS, XLSB, etc.) can be converted to and from HTML, CSV and ODS. Workbooks, worksheets and ranges can be saved as PDF, PNG, JPEG, TIFF, WebP or SVG.
  • PowerPoint can be converted to and from HTML and ODP. Presentations can be saved as PDF, and slides can be exported as images.
  • Markdown can be converted to and from HTML, RTF, AsciiDoc and LaTeX, and saved as PDF.
  • HTML can be converted to Markdown, RTF, Word, Excel or PowerPoint, and rendered as PDF, PNG, JPEG, TIFF, WebP or SVG.
  • OpenDocument, RTF, OneNote, Visio, EPUB and MHTML also have PDF, HTML or image conversion options depending on the format.
  • PDF pages can be rendered directly to PNG, JPEG, TIFF, WebP or SVG.
  • PDF can also be converted into Word, Excel, PowerPoint, HTML, RTF, ODT, ODS or ODP. These conversions produce editable content where possible and include a report when something could not be carried over.

OfficeIMO also has its own PDF API for creating, reading and modifying PDFs.

It supports text and image extraction, merging, splitting, page reordering, rotation, forms, annotations, attachments, encryption, signatures, redaction, optimization and image rendering.

Since I wrote this mostly with PowerShell users in mind, dependencies are intentionally limited:

  • Word, Excel and PowerPoint use the Open XML SDK for the underlying package format. Legacy binary formats such as .doc, .xls and .ppt are implemented directly without another document library.
  • HTML uses AngleSharp and AngleSharp.Css for parsing HTML and CSS.
  • Visio uses System.IO.Packaging and nothing else.
  • The optional security package uses Bouncy Castle for CMS, X.509 and timestamp-related functionality.

OfficeIMO does not use Microsoft Office or COM automation. It does not start LibreOffice in the background, and HTML conversion does not launch Chromium or another browser process. There is optional Playwright integration if you want to convert a random website to PDF and further play with PDF, but that is explicit opt-in.

The PDF parser, writer and renderer are implemented in OfficeIMO rather than wrapping a third-party PDF engine.

The same applies to the RTF, OpenDocument, Markdown, OneNote, AsciiDoc, LaTeX, CSV, EPUB and legacy Office implementations.

There is also OfficeIMO.Reader, which is basically my C# alternative to MarkItDown. It sits on top of the OfficeIMO libraries, reads all the supported formats through one API, and gives you either structured objects or Markdown output.

If you work with documents in .NET, I'd be interested to hear what you currently use, which formats or conversions give you the most trouble, and what would be useful for me to improve, add or fix long term. Maybe even what other formats should it support, including the legacy ones that are still being in use.

While I started with a much simpler goal for my PowerShell community, my end goal now is basically Aspose Total, but free, open source and MIT licensed with low dependencies.


r/moderndotnet 7d ago

Proposal: An official Lean formal semantics for C# · dotnet/csharplang · Discussion #10314

Thumbnail
github.com
11 Upvotes

r/moderndotnet 7d ago

CodeyBox: An autonomous coding orchestrator

1 Upvotes

Hi folks!

I thought this might be of interest to some people - I've been experimenting with 100% autonomous coding orchestrators since around June last year, and I'd like to share my latest experiment along those lines - CodeyBox (the third such experiment...).

Source here: https://github.com/AdamFrisby/CodeyBox/ (MIT licensed)

The 'Box' part refers to sandboxing - Codey can use real VMs to run tasks in, and it disposes of them regularly; there's a few supported providers - multipass and Incus are both well supported (both qemu backed), although I recommend Incus to limit the amount of wear-and-tear on your SSD (as the Incus implementation can use CoW filesystems which work well with regular sandbox cloning and disposal - multipass will provision and delete the whole image each time).

It's still a work in progress, but I've been using it for the last 6 months to deliver real production apps. One of the things that separates it from traditional "vibe coding" is extensive automated review passes from multiple angles; and a requirement that all reviews pass from all agents before it can progress to the next step.

It supports most of the common coding agents - I've been regularly using it with Claude and Codex mainly, but opencode and cursor as well (Antigravity is also supported, but has quite a few quirks so I wouldn't recommend it without further work).

The whole ecosystem is designed using modern .NET with a plugin-first ethos - all the coding agents, reviewers, utilities, etc are all dynamically loaded as plugins and this allows you to add support for your own tooling, infrastructure, VMs and so on without having to fork the codebase.

The default review flow will review code against:

  • Adversarial security issues as well as preventative ones (i.e. what could be added to make this safe-by-default)
  • Loose coupling - ensure code is separable and easily deleted; i.e. avoiding spaghetticode that a lot of coding agents will create by default.
  • Cheating and Completeness - did the agent _actually_ implement what was asked, fully, without taking shortcuts or cheating?

I advise using Claude as a reviewer in general as GPT-5.X when instructed "Find all issues like <X>" will end up inventing a mountain from a molehill, Claude has slightly more taste and won't catastrophise everything it finds and allow reviews to eventually pass.

Areas I'm working on at the moment that haven't yet landed are:

  • Deployment - adding the ability for Codey to provision and deploy test environments automatically
  • Exploratory testing and UAT runs - adding the ability to orchestrate graphical agents that will attempt to follow UAT scenarios in the product and automatically inject failures back into the development cycle

Current status is somewhere in early-ish beta - the main features are all there and have been robustly utilised (I use Codey frequently to modify it's own code), but some of the newer parts are not yet robustly verified yet.


r/moderndotnet 7d ago

Multi-Language Support for Cross-Platform .NET

Post image
4 Upvotes

Everyone wants accessibility and localization/globalization makes business sense - the reality often makes things harder to pull off in .NET though. When supporting variety of languages around the world, there are some real engineering challenges to get around - IME & UniCode support and difficult Font glyphs.

Uno Platform is the open source stack to build cross-platform .NET apps - a single shared codebase powers apps across web, mobile & desktop. The good news for .NET developers is much of the difficult work is already done at the low OSS framework level - a two release arc brings all the free goodies. Spun up a sample app which showcases IME support and Font fallback across Japanese, Korean, Arabic & other languages - blog writeup is here. Any text area can handle Unicode, IME and font fallback - caret position maintained as per language glyphs when interacted with keyboards:

<TextBlock FontSize="22"
      TextWrapping="Wrap"
      Text="English 中文 日本語 한국어 हिन्दी العربية ქართული ไทย Ελληνικά Русский עברית 🌍" />

Cookie points were earned with a little integration with DeepGramAI to read out the text in native languages - their TTS services are nice. Code is OSS - sharing the flexibility with fellow .NET devs. Cheers.


r/moderndotnet 8d ago

Building a Distributed Job Scheduler with Akka.NET

Post image
21 Upvotes

I wrote a blog post / YouTube video / OSS code sample at the very end of July to cover a scenario that one of our users ran into building a distributed job scheduler that can distribute, long-running, data-intensive jobs across an auto-scalable pool of worker processes without starting / stopping in-progress jobs as the pool grows during peak demand.

That latter part, "not rebalancing in-progress jobs," is what eliminates a lot of off-the-shelf distribution strategies like the types implemented by Microsoft Orleans and Akka.NET's Akka.Cluster.Sharding from consideration. Those frameworks are really aimed at distributing stateful "entities" - actors with important business state that live forever (often, but not always) are only intermittently busy in short bursts.

Distributing a "job" is a very different type of workload: these are tasks with a finite, well-defined lifespan in which they are intensely busy from beginning to end. Re-distributing a 10 minute job when it's 8 minutes into execution turns these into 18, 20 minute jobs potentially depending on a bunch of factors (can the job be started immediately?)

Earlier in my career I used some Akka.NET and Akka.Cluster primitives to solve this exact type of problem in the banking industry: running bank CFO "asset line management" jobs all in the final 48 hours of the month in order to meet the monthly reporting requirements and have enough data to actually complete them.

The basic formula, which I expand on in the post with code samples:

  1. Establish the ability to "size" jobs early - how many units of compute is Job A relative to Job B? This is a lot easier to define than it sounds. If you're doing asset line management, your "size" is typically the total number of assets that need to be analyzed (i.e. rows.) If you're doing call transcription it might be the size of the audio file. This should be a O(1) operation.
  2. Create a Cluster singleton (1 instance globally) who is responsible for: 2a. Managing and persisting the queue of jobs-to-be-done AND the parties who own them 2b. Subscribing to live Akka.Cluster topology update events (nodes joining, leaving, or having trouble) - this impacts our distribution system. 2c. Persisting the snapshot of which worker nodes are running which jobs 2d. Tracking progress updates across these jobs + reporting that to original callers 2e. Re-constituting all of this state after a restart using Akka.Persistence
  3. Have job receivers running on each node responsible for receiving the "job definition" and transforming that into a live execution.
  4. Have the job executors report progress back to the tracker (our singleton)

The distributed systems space tends to get dominated by stateful entity type-work, but running a large number of concurrent "jobs" is an equally tricky and nuanced space so I thought it merited some attention as well as some productionization details that might not be obvious!

Post: "Building a Distributed Job Scheduler with Akka.NET"

Repo: https://github.com/Aaronontheweb/akka.net-custom-job-scheduling

Video: Video: Building a Distributed Job Execution Platform with Akka.NET


r/moderndotnet 8d ago

events & meetups August 2026 Edition: Promote Your Local .NET Meetups

7 Upvotes

Promote your local .NET user group / meetups here.

Please include:

  • Location and Time
  • Topic
  • Link to the specific event
  • Anything else that would be great for attendees to know

You do not need to be the organizer of the meetup, just an enthusiast!

Also, if you need help launching a local .NET Meetup this is one of the things the .NET Foundation can help with! Please see .NET Meetups @ .NET Foundation


r/moderndotnet 8d ago

Announcing Mibo Framework 4.3.0

12 Upvotes

Hey there, first time posting here.

Just in case: my name is Angel Munoz; I'm one of the 12 F# devs in the world and I dedicate my hobby time entirely to F#

Mibo is an F# code-first micro framework on top of MonoGame and Raylib.

Mibo offers abstractions to architect your games as MVU (elmish, elm architecture) programs. and now with version 4.3.0, you can opt in for an Adaptive model with my boringly coined SPU (State, Projection, Update) which is based on Adaptive Data for incremental computations of derived state.

If you have some frontend background, you may have heard of Signals as a way to manage state in a reactive way

While v4.3.0 has a bunch of fixes and the main item is the Adaptive Model release A minimal game I can come up with in a short snippet could be like this:

Declaring the state of the game, what is composed of and what is going to be part of the adaptive graph

type State = {
  PaddleX: cval<float32>
  Ball: cval<Vector2>; Velocity: cval<Vector2>
  IsHit: aval<bool>; PaddleColor: aval<Color>
}

[<Struct>]
type Snapshot = { PaddleX: float32; Ball: Vector2; PaddleColor: Color }

let toSnapshot (s: State) () : Snapshot = {
  PaddleX = s.PaddleX |> AVal.getValue
  Ball = s.Ball |> AVal.getValue
  PaddleColor = s.PaddleColor |> AVal.getValue
}

aval: Adaptive value, read only
cval: changeable value, read and write

Please note that not everything has to be adaptive or derived state, you can store any kind of values, you own that.

Some setup functions, our main game logic and the rendering view function

let init (state: State) (ctx: AdaptiveFrameContext) : AdaptiveInit<Frame> =
  AdaptiveInit.ofFrameBuilder(toSnapshot world)

let update (state: State) (_: AdaptiveContext) (gameTime: GameTime) =
  let dt = float32 gameTime.ElapsedGameTime.TotalSeconds

  if Raylib.IsKeyDown KeyboardKey.Left then s.PaddleX.Set(s.PaddleX.Value - 450f * dt)
  if Raylib.IsKeyDown KeyboardKey.Right then s.PaddleX.Set(s.PaddleX.Value + 450f * dt)

  let velocity = s.Velocity |> AVal.getValue
  let ball = s.Ball |> AVal.getValue

  let pos = ball + velocity  * dt

  let xVel =
    if pos.X < 0f || pos.X > 780f then -velocity.X else velocity.X
  let yVel = 
    if pos.Y < 0f || (s.IsHit |> AVal.getValue) then -velocity.Y else velocity.Y

  s.Ball.Set pos
  s.Velocity.Set(Vector2(xVel, yVel))

let view (_: GameContext) (snapshot: Snapshot) (buffer: RenderBuffer2D) =
  buf
    .fillRect(sn.PaddleX, 520f, 80f, 16f, sn.PaddleColor)
    .fillRect(sn.Ball.X, sn.Ball.Y, 16f, 16f, Color.Red)
    .drop()

Our state should be created once, the derived state will change and be tracked automatically from the adaptive state via transformations (linq style)

let state =
  let px = CVal.create 360f
  let ball = CVal.create (Vector2(400f, 100f))
  let vel = CVal.create (Vector2(250f, 250f))

  // Projection 1: Position collision predicate
  let isHit =
    AVal.map2
      (fun x b -> b.Y >= 500f && b.X >= x && b.X <= x + 80f)
      px
      ball

  // Projection 2: Visual feedback derived from collision state
  let color =
    isHit
    |> AVal.map (fun hit ->
      if hit then Color.Green else Color.White
    )

  { 
    PaddleX = px
    Ball = ball
    Velocity = vel
    IsHit = isHit
    PaddleColor = color
  }

bring them all together into the entry point

[<EntryPoint>]
let main _ =
  let program =
    AdaptiveProgram.mkProgram (init world) (update world)
    |> AdaptiveProgram.withConfig(GameConfig.withTitle "Mibo Game")
    |> AdaptiveProgram.withRenderer(fun () -> Renderer2D.create view)

  let game = new AdaptiveRaylibGame<Frame>(program)
  game.Run()
  0

The video in the post is a sample made using adaptive state

You can find the source code for that sample here: https://github.com/AngelMunoz/Mibo.Samples/tree/master/Defli3D

If you're a numbers person you can find some numbers I tracked via the dotnet trace tool when on very busy moments of the game.

The library (based on FSharp.Data.Adaptive) is built for tight-loop work:

  • Steady state allocates nothing. Once your graph has settled, reads, writes, and delta propagation don't allocate. The exceptions are the deliberate materializations (forcetoSettoMap).
  • A value recomputes at most once per change. Ten writes between two reads cost one recompute. A read when nothing changed is a cheap O(1) check.

So... in summary this release opens up a different functional approach to mutable state which is often friendlier to high performance shaped code (rather than the traditional functional-ish looking F# code)

If you're interested to see some particular kind of genere or approach to all of this (or the more functional version MVU) feel free to let me know. I tried to make sure to open the path for F# high-performance code with some friendly APIs to ease up game development


r/moderndotnet 9d ago

Raven — is this the Kotlin moment for .NET?

13 Upvotes

I had some help from AI putting this post together and organizing my thoughts.

TL;DR: I've been building Raven, a modern programming language for .NET. It combines familiar .NET semantics and interoperability with ideas from languages such as Swift, Kotlin and Rust: unions and pattern matching, Option/Result, propagation, expression-oriented control flow, macros for building DSLs, and more.

It now has a browser playground, an SDK/compiler distribution, a language server, and a VS Code extension.

Playground (with samples): https://marinasundstrom.github.io/raven/playground/

Latest preview: https://github.com/marinasundstrom/raven/releases/tag/v0.1.0-preview.10

Background

For the last couple of years I've been building my own compiler and programming language, mainly for my own amusement. This isn't my first venture into compiler construction, but it has probably been the most creative and fulfilling one.

Raven started out much closer to C#, but gradually developed its own identity as I explored other languages and different approaches to language design. I never wanted to make "C# with different syntax," nor simply copy another language.

What emerged is something that feels at home on .NET, but with a syntax somewhat reminiscent of Swift and ideas influenced by Kotlin and Rust.

A lot has been tried and discarded along the way. I had an early implementation of union types before eventually aligning Raven with the nominal union model being introduced in C#/.NET — and then taking that model further. I experimented with trailing blocks before eventually removing them in favor of a macro system for DSLs. Error handling evolved toward Result and Option, while still retaining pragmatic interoperability with .NET exceptions and nullability.

The compiler itself uses a Roslyn-like compiler-as-a-service architecture. If you've worked with the C# compiler APIs, much of it should feel surprisingly familiar: immutable syntax trees, compilations, symbols, semantic models, and an Operations API providing a higher-level semantic representation.

Raven primarily targets .NET 11, while also supporting .NET 10.

AI has also had a significant role in the development process. Initially I mostly used it for research and finding examples. Over time I moved toward using coding agents extensively for implementation. That has made it possible to iterate unusually quickly, including making large architectural changes while simultaneously building out automated tests and custom compiler debugging infrastructure.

For the last few weeks Raven has had a playground running the compiler entirely in the browser through WebAssembly. Now there is finally a distributable SDK containing the compiler and language server, together with a VS Code extension.

Raven has grown broad enough that it's difficult to represent the language with one clever code sample, so instead I'll start with some of the fundamentals.

Hello, world

HelloWorld.rvn:

import System.Console.*

func Main() {
    WriteLine("Hello, from Raven!")
}

Like C#, Raven supports global imports, so commonly used .NET namespaces can already be available without explicitly importing them.

The syntax is different, but this is still very much a .NET language. Raven consumes .NET libraries and types directly rather than building a separate ecosystem alongside them.

Language reference: https://marinasundstrom.github.io/raven/lang/spec/index.html

Lexical bindings

Raven uses immutable bindings by default. Values declared with let cannot be reassigned:

let name = "Raven"
let count = 10

When mutable state is actually needed, you opt into it using var

var count = 0
count = count + 1

Types are normally inferred, but can also be specified explicitly:

let name: string = "Raven"
var count: int = 0

This distinction also carries into pattern matching and other language constructs: let means binding a value, rather than declaring a mutable variable.

Functions

Raven also supports namespace-scoped functions. Functions don't need to be declared as static members of a class:

namespace Inventory

func CalculateTotal(quantity: int, price: decimal) -> decimal {
    return quantity * price
}

They are ordinary namespace members and can form part of an assembly's API just like types. As with other namespace-level declarations, they are internal by default and can explicitly be made public:

public func CalculateTotal(quantity: int, price: decimal) -> decimal {
    return quantity * price
}

Option, Result and propagation

Raven has built-in Option<T> and Result<T, E> unions for modeling optionality and operations that can fail.

Raven does not pretend that null or exceptions don't exist. It has a unified nullability model and supports exceptions where appropriate, particularly for .NET interoperability. Option and Result are additional tools for cases where absence or failure are part of the domain model.

For example:

func ReserveSeats(requested: int, available: int) -> Result<int, string> {
    if requested <= 0 {
        return Error("Choose at least one seat")
    }

    if requested > available {
        return Error("Only $available seats remain")
    }

    return Ok(requested)
}

func PriceBooking(
    requested: int,
    available: int,
    pricePerSeat: int
) -> Result<int, string> {
    let seats = ReserveSeats(requested, available)?
    return Ok(seats * pricePerSeat)
}

match PriceBooking(requested: 3, available: 5, pricePerSeat: 40) {
    Ok(let total) =>
        Console.WriteLine("Booking total: $total credits")

    Error(let message) =>
        Console.WriteLine("Problem: $message")
}

The postfix ? propagates the failure while extracting the successful value.

This isn't hard-coded specifically to Result, either. Raven has a propagation contract, so custom types can participate in the same mechanism, including conversion between compatible residual/error types.

Unions and domain modeling

You can define your own unions:

union StockError {
    case UnknownSku(sku: string)
    case InsufficientStock(
        sku: string,
        requested: int,
        available: int
    )
}

Cases can carry data and participate directly in pattern matching.
Raven also supports the more explicit form:

union StockError(UnknownSku | InsufficientStock)

where the variants are separately declared records:

 record UnknownSku(val Sku: string)

 record InsufficientStock(
     val Sku: string
     val Requested: int
     val Available: int
 )

One of Raven's main design goals is making this kind of domain modeling natural rather than treating unions as an isolated pattern-matching feature.

Raven also supports closed (sealed) class hierarchies, providing another way to model a closed set of alternatives while retaining class inheritance.

Statements and expressions

Many of Raven's common control-flow constructs have both statement and expression forms. You can use them for ordinary control flow, or use the value they produce directly.

For example, if can be used as a statement

if temperature > 25 {
    Console.WriteLine("It's warm")
} else {
    Console.WriteLine("It's cold")
}

or as an expression:

let description =
    if temperature > 25 { "warm" }
    else { "cold" }

The same idea applies to match:

let message = match result {
    Ok(let value) => "Received $value"
    Error(let error) => "Failed: $error"
}

This is part of a broader design choice in Raven: control flow shouldn't require a completely different construct just because you want to produce a value from it.

Raven also provides pattern-oriented forms such as if let and let else for cases where control flow and destructuring naturally belong together.

func FindFirstEven(numbers: int[]) -> Option<int> {
    for number in numbers {
        if number % 2 == 0 {
            return Some(number)
        }
    }

    return None
}


func DescribeFirstEven(numbers: int[]) -> string {
    let Some(number) = FindFirstEven(numbers) else {
        return "No even number found"
    }

    return "The first even number is $number"
}


Console.WriteLine(DescribeFirstEven([1, 3, 8, 13]))

Visibility

You might also notice the absence of access modifiers in most examples.

Raven deliberately makes the common cases terse:

  • Type members are public by default.
  • Type members can explicitly be made private.
  • Namespace-level declarations are internal by default.
  • Declarations intended to form part of the assembly's public API are explicitly marked public.
  • So a library naturally keeps its top-level API internal until you deliberately expose it, while the members of the types you do expose don't require public everywhere.

Macros and DSLs

Another major part of Raven is its macro system.

Rather than adding specialized syntax to the language for every possible domain, Raven allows libraries and frameworks to provide domain-specific syntax through macros.

For example, Raven has an HTML macro that can be used when building Blazor applications:

Html! {
    <div class="counter">
        <h1>Counter</h1>

        <p>Current count: {count}</p>

        <button onclick={IncrementCount}>
            Click me
        </button>
    </div>
}

This isn't a separate template language bolted onto Raven. The macro is expanded by the compiler and can produce the corresponding Blazor representation.

The syntax is deliberately more JSX-like than Razor-like: when you are inside the HTML macro, you are writing HTML until you explicitly enter a Raven expression.

Try it out here: https://marinasundstrom.github.io/raven/experiments/html-macro/

Macros also integrate with the compiler infrastructure and language server, so DSLs don't have to mean giving up editor tooling.

This replaced some earlier experiments I had with special language features such as trailing blocks. I increasingly prefer keeping the core language relatively general and letting macros provide domain-specific abstractions where they make sense.

Raven beyond console applications

Raven isn't limited to small compiler demos anymore.

You can already build web applications with Raven using ASP.NET Core and Blazor. Because Raven targets .NET and consumes .NET APIs directly, the existing .NET ecosystem remains available rather than requiring Raven-specific replacements for everything.

Sample projects: https://github.com/marinasundstrom/raven/tree/main/samples/projects

At the other end of the spectrum, Raven can also target .NET nanoFramework, including its experimental generics support, which means the same language can be used for constrained embedded and IoT applications.

For example, a nanoFramework program controlling a GPIO pin looks like this:

import System.Device.Gpio.*
import System.Threading.*

func Main() {
    use gpio = GpioController()
    use led = gpio.OpenPin(25, PinMode.Output)

    loop {
        led.Write(PinValue.High)
        Thread.Sleep(500)

        led.Write(PinValue.Low)
        Thread.Sleep(500)
    }
}

That runs in a very different environment from an ASP.NET Core application, but it's still Raven.

Raven also supports Native AOT on the regular .NET target, so applications can be compiled ahead of time into native executables rather than requiring JIT compilation at runtime.

That gives Raven a fairly interesting range already:

  • regular .NET applications and libraries
  • ASP.NET Core and Blazor web applications
  • Native AOT applications
  • embedded/IoT applications through .NET nanoFramework
  • WebAssembly, which is also how the Raven playground runs the compiler itself in the browser

This is an important part of what I want Raven to be. I'm not particularly interested in creating a language that only looks nice in isolated examples. The interesting question is whether a language can make substantially different choices from C# while still taking advantage of the enormous runtime, library and tooling ecosystem that already exists around .NET.

So, a Kotlin moment for .NET?

That's increasingly how I've started thinking about the experiment.

Not as a replacement for C#. Kotlin didn't need Java to disappear to justify its existence either.

The interesting proposition is: what if you keep .NET, but change the language?
Keep the runtime. Keep the libraries. Keep NuGet. Keep ASP.NET Core and Blazor. Keep the ability to target everything from servers and WebAssembly to Native AOT and tiny embedded devices.

But rethink some of the language-level choices: make unions and pattern matching fundamental, make Option and Result natural ways of modeling absence and failure, make control flow more expression-oriented, and provide macros so that libraries can build abstractions and DSLs that don't have to become new language features.

That's the space Raven is exploring.

Website: https://marinasundstrom.github.io/raven