Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
937fefc
add dotnet11 test project
christiandaley Aug 2, 2026
48c2dbf
start work on test
christiandaley Aug 2, 2026
e906861
add switch test
christiandaley Aug 2, 2026
b084d00
add union atttribute and interface
christiandaley Aug 2, 2026
03a375a
make test compile
christiandaley Aug 2, 2026
3e7f4d5
implement creation members
christiandaley Aug 2, 2026
450ddbb
make test compile
christiandaley Aug 2, 2026
716e807
update test
christiandaley Aug 2, 2026
c2441d4
update test again
christiandaley Aug 2, 2026
b3cea06
add test for hasvalue
christiandaley Aug 2, 2026
afa4530
make hasvalue test pass
christiandaley Aug 2, 2026
9b7af9d
small update to tests
christiandaley Aug 2, 2026
f86b391
start work on trygetvalue
christiandaley Aug 2, 2026
a2d32cd
add trygetvalue to interface
christiandaley Aug 2, 2026
49cf597
small change
christiandaley Aug 2, 2026
c3b9337
add trygetvalue generic stub and use it for equality
christiandaley Aug 3, 2026
d8038d9
get trygetvaluegeneric working
christiandaley Aug 3, 2026
8940e51
get trygetvalue working
christiandaley Aug 3, 2026
33d28ed
add test for createfailure
christiandaley Aug 3, 2026
9006507
make test pass
christiandaley Aug 3, 2026
f7f140f
add documentation to newly added functions
christiandaley Aug 3, 2026
6e582de
add test for switch on repeated types
christiandaley Aug 3, 2026
c74ad10
simplify trygetvalue<>
christiandaley Aug 4, 2026
d1f56be
add partial struct declarations for empty cases
christiandaley Aug 4, 2026
608223b
get empty case value implemented
christiandaley Aug 4, 2026
b984881
add trygetvalue to interface for empty cases
christiandaley Aug 4, 2026
4e3c2d5
fix docs and make test pass
christiandaley Aug 4, 2026
59241b1
add documentation to empty structs
christiandaley Aug 5, 2026
0be624c
update docs again
christiandaley Aug 5, 2026
c44693b
small improvement to equals implementation
christiandaley Aug 5, 2026
d82f079
optional -> option
christiandaley Aug 5, 2026
43f308f
start work on adding structs for all cases [skip ci]
christiandaley Aug 5, 2026
cdd5d56
get value and hasvalue implemented [skip ci]
christiandaley Aug 5, 2026
48c7006
get case struct names in a different way
christiandaley Aug 5, 2026
a0b2eeb
update tests
christiandaley Aug 5, 2026
fe27637
make test pass
christiandaley Aug 5, 2026
ec99701
make iunionmembers interface explicitly implemented [skip ci]
christiandaley Aug 5, 2026
78321a6
remove unneeded trygetvaluegeneric
christiandaley Aug 6, 2026
eb518d9
start work on complex generic
christiandaley Aug 6, 2026
7f4f71a
more work on generic test [skip ci]
christiandaley Aug 6, 2026
51f44dc
start work on typenameparser
christiandaley Aug 6, 2026
d96c785
get typename parser working [skip ci]
christiandaley Aug 6, 2026
b821735
small improvement to parser [skip ci]
christiandaley Aug 6, 2026
4d96e23
remove some unneeded variables [skip ci]
christiandaley Aug 6, 2026
74e76d7
add generated code attribute [skip ci]
christiandaley Aug 7, 2026
cb1185f
small fix [skip ci]
christiandaley Aug 7, 2026
3b991d7
update readme [skip ci]
christiandaley Aug 8, 2026
149e688
remove unneeded file [skip c]
christiandaley Aug 8, 2026
ff646b7
small fix to readme [skip ci]
christiandaley Aug 8, 2026
63fe511
add table of contenst entry [skip ci]
christiandaley Aug 8, 2026
ec5368b
update readme again [skip ci]
christiandaley Aug 8, 2026
c8c9704
fix issue with constraints on case structs [skip ci]
christiandaley Aug 8, 2026
66d6199
fix constraints
christiandaley Aug 8, 2026
06932c1
fix build and test workflow
christiandaley Aug 8, 2026
52e551e
small fix to readme
christiandaley Aug 8, 2026
8b0f0a1
small change
christiandaley Aug 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,16 @@ jobs:
dotnet-version: ${{ matrix.dotnet }}

- name: Restore dependencies
run: dotnet restore
# run: dotnet restore
run: |
dotnet restore ./Tests/Tests.csproj
dotnet restore ./Tests.AOT/Tests.AOT.csproj

- name: Build solution
run: dotnet build --configuration Release --no-restore
# run: dotnet build --configuration Release --no-restore
run: |
dotnet build ./Tests/Tests.csproj --configuration Release --no-restore
dotnet build ./Tests.AOT/Tests.AOT.csproj --configuration Release --no-restore

- name: Run unit tests
run: dotnet test ./Tests/Tests.csproj --configuration Release --no-build --verbosity normal
Expand Down
68 changes: 58 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ A highly configurable C\# discriminated union library

---

1. [Why use `SumSharp`?](#why-use-sumsharp)
1. [Why use SumSharp?](#why-use-sumsharp)
2. [Installation](#installation)
3. [Quick start](#quick-start)
- [Creating a DU type](#creating-a-du-type)
- [Empty cases](#empty-cases)
- [Generic cases](#generic-cases)
- [The `Match` function](#the-match-function)
- [.NET 11 union types and pattern matching](#net-11-union-types-and-pattern-matching)
4. [Motivation](#motivation)
- [SumSharp vs .NET 11 union types](#sumsharp-vs-net-11-union-types)
- [What about `OneOf`?](#what-about-oneof)
- [Typical DU implementation approaches](#typical-du-implementation-approaches)
- [SumSharp's approach](#sumsharps-approach)
Expand All @@ -39,12 +41,13 @@ A highly configurable C\# discriminated union library

Discriminated unions, also known as sum types, are an invaluable tool for working with heterogenous data types in code. They help ensure safe data access patterns and can [make illegal states unrepresentable.](https://fsharpforfunandprofit.com/posts/designing-with-types-making-illegal-states-unrepresentable/)

There are many discriminated union libraries available for C\#, such as [`OneOf`](https://github.com/mcintyre321/OneOf) which has received tens of millions of downloads. In my experience, all of them lack features commonly offered by discriminated union types in other languages.
There are many discriminated union libraries available for C\#, such as [`OneOf`](https://github.com/mcintyre321/OneOf) which has received tens of millions of downloads. In my experience, all of them lack features commonly offered by discriminated union types in other languages. Union types are being added to C# with the [.NET 11 release](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/union), but these are not true DUs because they lack case names and thus cannot support multiple cases of the same type.

`SumSharp` aims to be **the most powerful, expressive, and configurable C\# discriminated union library available**. Its goal is to provide features and syntax comparable to the discriminated union types natively offered by languages such as F\#, Rust, and Haskell. Although it's impossible to exactly replicate the functionality these other languages offer, `SumSharp` strives to get as close as possible.

### Features

- **Integration with .NET 11 union types, allowing use of C#'s built-in pattern matching syntax**
- Unlimited number of cases
- Support for class, struct, record, and record struct unions
- Support for generic unions
Expand Down Expand Up @@ -88,6 +91,7 @@ partial class StringOrDouble

That's it! `SumSharp` will generate members for the `StringOrDouble` class that allow it to be used as a discriminated union type. These members include:

- `Value` and `HasValue` properties, and `TryGetValue` methods to satisfy requirements for a non-boxing .NET 11 union type
- `String` and `Double` static functions that construct instances of `StringOrDouble`
- `AsString` and `AsDouble` properties that return either the underlying string/double value or throw an `InvalidOperationException`
- `IsString` and `IsDouble` boolean properties
Expand Down Expand Up @@ -141,7 +145,7 @@ Case types can be generic. To define a generic case you must supply the **name**
```csharp
[UnionCase("Some", "T")]
[UnionCase("None")]
partial class Optional<T>
partial class Option<T>
{

}
Expand All @@ -151,20 +155,22 @@ Note that generic types in general *must be fully qualified names unless you hav

### The `Match` function

`SumSharp` unions have a `Match` member function that provides functionality similar to the match statement in F\# (with the limitation that `SumSharp` does not offer partial matching). The parameters to `Match` are the handler functions for each case, in order. Each parameter has the same name as its corresponding case, allowing the use of named parameters to improve code readability and for the handlers to be specified out of order. To illustrate this, compare the syntax of performing a match on the `Optional<T>` type defined in the last section to equivalent F\# code.
**If you are using .NET 11 or higher, `SumSharp` unions satisfy the compiler's requirements for a union type. In most cases using built-in C# pattern matching will be easier than using the `Match` function. See [.NET 11 union types and pattern matching](#net-11-union-types-and-pattern-matching)**

`SumSharp` unions have a `Match` member function that provides functionality similar to the match statement in F\# (with the limitation that `SumSharp` does not offer partial matching). The parameters to `Match` are the handler functions for each case, in order. Each parameter has the same name as its corresponding case, allowing the use of named parameters to improve code readability and for the handlers to be specified out of order. To illustrate this, compare the syntax of performing a match on the `Option<T>` type defined in the last section to equivalent F\# code.

```csharp
// Here myOptionalValue is an Optional<string>
// Here myOptionValue is an Option<string>
// The "None" handler can come before the "Some" handler as long as they're both named
var result = myOptionalValue.Match(
var result = myOptionValue.Match(
None: () => "",
Some: x => x);
```

Corresponding F\# code would look like:

```fsharp
let result = match myOptionalValue with
let result = match myOptionValue with
| None -> ""
| Some x -> x
```
Expand All @@ -174,26 +180,68 @@ Handling each case is not required, but a warning will be emitted by the `SumSha
If you only want to handle some subset of cases, you can provide a default handler to prevent a warning from being emitted.

```csharp
var result = myOptionalValue.Match(
var result = myOptionValue.Match(
Some: x => x,
_: () => "");
```

Again, the corresponding F\# code would look like:

```fsharp
let result = match myOptionalValue with
let result = match myOptionValue with
| Some x -> x
| _ -> ""
```

The `SumSharp` analyzer will emit a warning if a default handler is provided for a `Match` that is already exhaustive. It will also emit a warning if any case handlers are specified by position rather than name. Specifying by name is preferred because it makes the code clearer and prevents bugs/compilation errors if the case ordering changes.

### .NET 11 union types and pattern matching

If you are using .NET 11 or higher, `SumSharp` unions satisfy the compiler's requirements for a union type. All `SumSharp` unions implement [the non-boxing access pattern](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/union#non-boxing-access-pattern) and [union member providers](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/union#union-member-providers).

Because C# union types do not support case names, `SumSharp` generates wrapper structs for each case in the union and places them as the same namespace/nested type level as the union itself. Empty cases get empty `partial` structs. This means that two `SumSharp` unions that share the same namespace/type heirarchy **cannot share identical non-empty case names**. These structs are used when pattern matching using built-in C# syntax such as `switch` or `is`. An example using the `Option<T>` type that was defined above:

```csharp
var x = Option<int>.Some(4);

var value = x switch
{
Some<int>(var i) => i,
None => 0,
};

// prints "value is 4"
Console.WriteLine($"value is {value}");

// prints "x is 4"
if (x is Some<int>(4))
{
Console.WriteLine("x is 4");
}
else if (x is None)
{
Console.WriteLine("x is none");
}

```

#### Type union implementation details

* `SumSharp` unions are *never null*. A non-null `SumSharp` union will never match with the `null` pattern, even if the underlying data it stores is null.
* The `IUnionMembers.Value` property is never null and will always return a boxed instance of one of the case structs.
* The `IUnionMembers.HasValue` property always returns true.
* The various `TryGetValue` overloads will wrap the underlying data in one of the case structs.
* `SumSharp` unions implement their corresponding `IUnionMembers` interface explicitly. This means that the `Value` and `HasValue` properties and the `TryGetValue` methods cannot be used unless you explicitly cast it to an `IUnionMembers`. In general you should not need to use any of these: they exist to satisfy the compiler's requirements for custom union types.

---

## Motivation

C\# unfortunately does not offer discriminated unions as a language feature. Although [a proposal](https://github.com/dotnet/csharplang/blob/18a527bcc1f0bdaf542d8b9a189c50068615b439/proposals/TypeUnions.md) has existed for a while, this feature doesn't seem to be coming in the near future.
### `SumSharp` vs .NET 11 union types

The union types introduced by .NET 11 are not true disrciminated unions because they lack the ability to define case names, thus not allowing for multiple cases of the same type. They also always box value types by default, which is unnecessary and often undesireable. They do, however, provide highly convenient pattern matching syntax using C\#'s built-in pattern matching operations such as `switch` and `is`.

As mentioned in the quick start guide, `SumSharp` unions satisfy the requirements for .NET 11 union types. Wrapper structs are defined for each case, allowing for pattern matching behavior that is similar to languages with first class DUs such as F\#. Thus, `SumSharp` works synergistically with C\#'s unions types. You don't need to choose between the two: using `SumSharp` gives you the best of both.

### What about `OneOf`?

Expand Down
Loading
Loading