{"id":13629427,"url":"https://github.com/domn1995/dunet","last_synced_at":"2026-01-27T10:01:26.033Z","repository":{"id":37049780,"uuid":"498053098","full_name":"domn1995/dunet","owner":"domn1995","description":"C# discriminated union source generator","archived":false,"fork":false,"pushed_at":"2026-01-25T07:51:36.000Z","size":576,"stargazers_count":828,"open_issues_count":4,"forks_count":26,"subscribers_count":10,"default_branch":"main","last_synced_at":"2026-01-25T16:54:06.534Z","etag":null,"topics":["csharp","csharp-sourcegenerator","discriminated-unions","dotnet","fp","functional","functional-programming","union"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/domn1995.png","metadata":{"files":{"readme":"Readme.md","changelog":null,"contributing":null,"funding":null,"license":"License.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2022-05-30T18:19:06.000Z","updated_at":"2026-01-25T16:19:29.000Z","dependencies_parsed_at":"2023-02-13T13:40:20.639Z","dependency_job_id":"f530355d-b792-4331-8624-868cfa305497","html_url":"https://github.com/domn1995/dunet","commit_stats":{"total_commits":172,"total_committers":3,"mean_commits":"57.333333333333336","dds":0.2441860465116279,"last_synced_commit":"bc3b6784906105cb237b3a4e8bf79b43933623e4"},"previous_names":[],"tags_count":34,"template":false,"template_full_name":null,"purl":"pkg:github/domn1995/dunet","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/domn1995%2Fdunet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/domn1995%2Fdunet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/domn1995%2Fdunet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/domn1995%2Fdunet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/domn1995","download_url":"https://codeload.github.com/domn1995/dunet/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/domn1995%2Fdunet/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28811495,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-27T07:41:26.337Z","status":"ssl_error","status_checked_at":"2026-01-27T07:41:08.776Z","response_time":168,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["csharp","csharp-sourcegenerator","discriminated-unions","dotnet","fp","functional","functional-programming","union"],"created_at":"2024-08-01T22:01:10.416Z","updated_at":"2026-01-27T10:01:25.997Z","avatar_url":"https://github.com/domn1995.png","language":"C#","funding_links":[],"categories":["Content","Source Generators","Source Generator","Identifiers"],"sub_categories":["15. [dunet](https://ignatandrei.github.io/RSCG_Examples/v2/docs/dunet) , in the [FunctionalProgramming](https://ignatandrei.github.io/RSCG_Examples/v2/docs/rscg-examples#functionalprogramming) category","Functional Programming","GUI - other"],"readme":"# Dunet\n\n[![Build](https://img.shields.io/github/actions/workflow/status/domn1995/dunet/main.yml?branch=main)](https://github.com/domn1995/dunet/actions)\n[![Package](https://img.shields.io/nuget/v/dunet.svg)](https://nuget.org/packages/dunet)\n\n**Dunet** is a simple source generator for [discriminated unions](https://en.wikipedia.org/wiki/Tagged_union) in C#.\n\n## Install\n\n- [NuGet](https://www.nuget.org/packages/Dunet/): `dotnet add package dunet`\n\n## Usage\n\n```cs\n// 1. Import the namespace.\nusing Dunet;\n\n// 2. Add the `Union` attribute to a partial record.\n[Union]\npartial record Shape\n{\n    // 3. Define the union variants as inner partial records.\n    partial record Circle(double Radius);\n    partial record Rectangle(double Length, double Width);\n    partial record Triangle(double Base, double Height);\n}\n```\n\n```cs\n// Optional: statically import the union for more terse code.\nusing static Shape;\n\n// 4. Use the union variants.\nvar shape = new Rectangle(3, 4);\n// Switch expression is checked for exhaustiveness.\nvar area = shape switch \n{\n    Circle(var radius) =\u003e 3.14 * radius * radius,\n    Rectangle(var length, var width) =\u003e length * width,\n    Triangle(var @base, var height) =\u003e @base * height / 2,\n};\nConsole.WriteLine(area); // \"12\"\n```\n\n## Match Method\n\nA `Match` method is also provided as a switch expression alternative:\n\n```cs\n// 1. Import the namespace.\nusing Dunet;\n\n// 2. Add the `Union` attribute to a partial record.\n[Union]\npartial record Shape\n{\n    // 3. Define the union variants as inner partial records.\n    partial record Circle(double Radius);\n    partial record Rectangle(double Length, double Width);\n    partial record Triangle(double Base, double Height);\n}\n```\n\n```cs\n// 4. Use the union variants.\nvar shape = new Shape.Rectangle(3, 4);\nvar area = shape.Match(\n    circle =\u003e 3.14 * circle.Radius * circle.Radius,\n    rectangle =\u003e rectangle.Length * rectangle.Width,\n    triangle =\u003e triangle.Base * triangle.Height / 2\n);\nConsole.WriteLine(area); // \"12\"\n```\n\n## Generics\n\nUse generics for more advanced union types. For example, an option monad:\n\n```cs\n// 1. Import the namespace.\nusing Dunet;\n// Optional: statically import the union for more terse code.\nusing static Option\u003cint\u003e;\n\n// 2. Add the `Union` attribute to a partial record.\n// 3. Add one or more type arguments to the union record.\n[Union]\npartial record Option\u003cT\u003e\n{\n    partial record Some(T Value);\n    partial record None();\n}\n```\n\n```cs\n// 4. Use the union variants.\nOption\u003cint\u003e ParseInt(string? value) =\u003e\n    int.TryParse(value, out var number)\n        ? number\n        : new None();\n\nstring GetOutput(Option\u003cint\u003e number) =\u003e\n    number switch\n    {\n        Some(var value) =\u003e value.ToString(),\n        None =\u003e \"Invalid input!\",\n    };\n\nvar input = Console.ReadLine(); // User inputs \"not a number\".\nvar result = ParseInt(input);\nvar output = GetOutput(result);\nConsole.WriteLine(output); // \"Invalid input!\"\n\ninput = Console.ReadLine(); // User inputs \"12345\".\nresult = ParseInt(input);\noutput = GetOutput(result);\nConsole.WriteLine(output); // \"12345\".\n```\n\n## Implicit Conversions\n\nDunet generates implicit conversions between union variants and the union type if your union meets all of the following conditions:\n\n- The union has no required properties.\n- No variant's property is an interface type.\n- Each non-empty variant has a single property.\n- Each non-empty variant's property is unique within the union.\n\nFor example, consider a `Result` union type that represents success as a `double` and failure as an `Exception`:\n\n```cs\n// 1. Import the namespace.\nusing Dunet;\n\n// 2. Define a union type with a single unique variant property:\n[Union]\npartial record Result\n{\n    partial record Success(double Value);\n    partial record Failure(Exception Error);\n}\n```\n\n```cs\n// 3. Return union variants directly.\nResult Divide(double numerator, double denominator)\n{\n    if (denominator is 0d)\n    {\n        // No need for `new Result.Failure(new InvalidOperationException(\"...\"));`\n        return new InvalidOperationException(\"Cannot divide by zero!\");\n    }\n\n    // No need for `new Result.Success(...);`\n    return numerator / denominator;\n}\n\nvar result = Divide(42, 0);\nvar output = result.Match(\n    success =\u003e success.Value.ToString(),\n    failure =\u003e failure.Error.Message\n);\n\nConsole.WriteLine(output); // \"Cannot divide by zero!\"\n```\n\n\u003e Note: Empty variants are ignored when generating implicit conversions.\n\n## Async Match\n\nDunet generates a `MatchAsync()` extension method for all `Task\u003cT\u003e` and `ValueTask\u003cT\u003e` where `T` is a union type. For example:\n\n```cs\n// Choice.cs\n\nusing Dunet;\n\nnamespace Core;\n\n// 1. Define a union type within a namespace.\n[Union]\npartial record Choice\n{\n    partial record Yes;\n    partial record No(string Reason);\n}\n```\n\n```cs\n// Program.cs\n\nusing Core;\nusing static Core.Choice;\n\n// 2. Define async methods like you would for any other type.\nstatic async Task\u003cChoice\u003e AskAsync()\n{\n    // Simulating network call.\n    await Task.Delay(1000);\n\n    // 3. Return unions from async methods like any other type.\n    return new No(\"because I don't wanna!\");\n}\n\n// 4. Asynchronously match any union `Task` or `ValueTask`.\nvar response = await AskAsync()\n    .MatchAsync(\n        yes =\u003e \"Yes!!!\",\n        no =\u003e $\"No, {no.Reason}\"\n    );\n\n// Prints \"No, because I don't wanna!\" after 1 second.\nConsole.WriteLine(response);\n```\n\n\u003e **Note**:\n\u003e `MatchAsync()` can only be generated for namespaced unions.\n\n## Specific Match\n\nDunet generates specific match methods for each union variant. This is useful when unwrapping a union and you only care about transforming a single variant. For example:\n\n```cs\n[Union]\npartial record Shape\n{\n    partial record Point(int X, int Y);\n    partial record Line(double Length);\n    partial record Rectangle(double Length, double Width);\n    partial record Sphere(double Radius);\n}\n```\n\n```cs\npublic static bool IsZeroDimensional(this Shape shape) =\u003e\n    shape.MatchPoint(\n        point =\u003e true,\n        () =\u003e false\n    );\n\npublic static bool IsOneDimensional(this Shape shape) =\u003e\n    shape.MatchLine(\n        line =\u003e true,\n        () =\u003e false\n    );\n\npublic static bool IsTwoDimensional(this Shape shape) =\u003e\n    shape.MatchRectangle(\n        rectangle =\u003e true,\n        () =\u003e false\n    );\n\npublic static bool IsThreeDimensional(this Shape shape) =\u003e\n    shape.MatchSphere(\n        sphere =\u003e true,\n        () =\u003e false\n    );\n```\n\n## Serialization/Deserialization\n\n```cs\nusing Dunet;\nusing System.Text.Json.Serialization;\n\n// Serialization and deserialization can be enabled with the `JsonDerivedType` attribute.\n[Union]\n[JsonDerivedType(typeof(Circle), typeDiscriminator: nameof(Circle))]\n[JsonDerivedType(typeof(Rectangle), typeDiscriminator: nameof(Rectangle))]\n[JsonDerivedType(typeof(Triangle), typeDiscriminator: nameof(Triangle))]\npublic partial record Shape\n{\n    public partial record Circle(double Radius);\n    public partial record Rectangle(double Length, double Width);\n    public partial record Triangle(double Base, double Height);\n}\n```\n\n```cs\nusing System.Text.Json;\nusing static Shape;\n\nvar shapes = new Shape[]\n{\n    new Circle(10),\n    new Rectangle(2, 3),\n    new Triangle(2, 1)\n};\n\nvar serialized = JsonSerializer.Serialize(shapes);\n\n// NOTE: The type discriminator must be the first property in each object.\nvar deserialized = JsonSerializer.Deserialize\u003cShape[]\u003e(\n    //lang=json\n    \"\"\"\n    [\n        { \"$type\": \"Circle\", \"radius\": 10 },\n        { \"$type\": \"Rectangle\", \"length\": 2, \"width\": 3 },\n        { \"$type\": \"Triangle\", \"base\": 2, \"height\": 1 }\n    ]\n    \"\"\",\n    // So we recognize camelCase properties.\n    new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }\n);\n```\n\n## Pretty Print\n\nTo control how union variants are printed with their `ToString()` methods, override and seal the union declaration's `ToString()` method. For example:\n\n```cs\n[Union]\npublic partial record QueryResult\u003cT\u003e\n{\n    public partial record Ok(T Value);\n    public partial record NotFound;\n    public partial record Unauthorized;\n\n    public sealed override string ToString() =\u003e\n        Match(\n            ok =\u003e ok.Value.ToString(),\n            notFound =\u003e \"Not found.\",\n            unauthorized =\u003e \"Unauthorized access.\"\n        );\n}\n```\n\n\u003e **Note**:\n\u003e You must seal the `ToString()` override to prevent the compiler from synthesizing a custom `ToString()` method for each variant.\n\u003e\n\u003e More info: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record#built-in-formatting-for-display\n\n## Shared Properties\n\nTo create a property shared by all variants, add it to the union declaration. For example, the following code requires all union variants to initialize the `StatusCode` property. This makes `StatusCode` available to anyone with a reference to `HttpResponse` without having to match.\n\n```cs\n[Union]\npublic partial record HttpResponse\n{\n    public partial record Success;\n    public partial record Error(string Message);\n    // 1. All variants shall have a status code.\n    public required int StatusCode { get; init; }\n}\n```\n\n```cs\nusing var client = new HttpClient();\nvar response = await CreateUserAsync(client, \"John\", \"Smith\");\n\n// 2. The `StatusCode` property is available at the union level.\nvar statusCode = response.StatusCode;\n\npublic static async Task\u003cHttpResponse\u003e CreateUserAsync(\n    HttpClient client, string firstName, string lastName\n)\n{\n    using var response = await client.PostJsonAsync(\n        \"/users\",\n        new { firstName, lastName }\n    );\n\n    var content = await response.Content.ReadAsStringAsync();\n\n    if (!response.IsSuccessStatusCode)\n    {\n        return new HttpResponse.Error(content)\n        {\n            StatusCode = (int)response.StatusCode,\n        };\n    }\n\n    return new HttpResponse.Success()\n    {\n        StatusCode = (int)response.StatusCode,\n    };\n}\n```\n\n## Unwrapping\n\nTo bypass exhaustive matching and access a variant directly, use the variant-specific `Unwrap` methods.\n\nThis can be useful if you're sure of the underlying value or if you don't care about a potential exception at runtime.\n\n```cs\nusing Dunet;\n\n[Union]\npartial record Option\u003cT\u003e\n{\n    partial record Some(T Value);\n    partial record None;\n}\n```\n\n```cs\nOption\u003cdouble\u003e option1 = new Option\u003cdouble\u003e.Some(3.14);\nvar some = option.UnwrapSome();\n// You can access `Value` directly here.\nConsole.WriteLine(some.Value); // Prints \"3.14\".\n\nOption\u003cdouble\u003e option2 = new Option\u003cdouble\u003e.None();\n// Throws `InvalidOperationException` because the underlying variant is `None`.\nvar bad = option.UnwrapSome();\n```\n\n\u003e **Note**:\n\u003e Unwrapping can be dangerous. Use only when runtime errors are ok \n\u003e or if you have checked that the value is the type that you expect.\n\n## Stateful Matching\n\nTo reduce memory allocations, use the `Match` overload that accepts a generic state parameter as its first argument.\n\nThis allows your match parameter lambdas to be `static` but still flow state through:\n\n```cs\nusing Dunet;\nusing static Expression;\n\nvar environment = new Dictionary\u003cstring, int\u003e()\n{\n    [\"a\"] = 1,\n    [\"b\"] = 2,\n    [\"c\"] = 3,\n};\n\nvar expression = new Add(new Variable(\"a\"), new Multiply(new Number(2), new Variable(\"b\")));\nvar result = Evaluate(environment, expression);\n\nConsole.WriteLine(result); // \"5\"\n\nstatic int Evaluate(Dictionary\u003cstring, int\u003e env, Expression exp) =\u003e\n    exp.Match(\n        // 1. Pass your state \"container\" as the first parameter.\n        state: env,\n        // 2. Use static lambdas for each variant's match method.\n        static (_, number) =\u003e number.Value,\n        // 3. Reference the state as the first argument of each lambda.\n        static (state, add) =\u003e Evaluate(state, add.Left) + Evaluate(state, add.Right),\n        static (state, mul) =\u003e Evaluate(state, mul.Left) * Evaluate(state, mul.Right),\n        static (state, var) =\u003e state[var.Value]\n    );\n\n[Union]\npublic partial record Expression\n{\n    public partial record Number(int Value);\n    public partial record Add(Expression Left, Expression Right);\n    public partial record Multiply(Expression Left, Expression Right);\n    public partial record Variable(string Value);\n}\n```\n\n## Nest Unions\n\nTo declare a union nested within a class or record, the class or record must be `partial`. For example:\n\n```cs\n// This type declaration must be partial.\npublic partial class Parent1\n{\n    // So must this one.\n    public partial class Parent2\n    {\n        // Unions must always be partial.\n        [Union]\n        public partial record Nested\n        {\n            public partial record Variant1;\n            public partial record Variant2;\n        }\n    }\n}\n```\n\n```cs\n// Access variants like any other nested type.\nvar variant1 = new Parent1.Parent2.Nested.Variant1();\n```\n\n## Samples\n\n- [Area Calculator](./samples/AreaCalculator/Program.cs)\n- [Serialization/Deserialization](./samples/Serialization/Program.cs)\n- [Option Monad](./samples/OptionMonad/Program.cs)\n- [Web Client](./samples/PokemonClient/PokeClient.cs)\n- [Recursive Expressions](./samples/ExpressionCalculator/Program.cs)\n- [Recursive Expressions with Stateful Matching](./samples/ExpressionCalculatorWithState/Program.cs)\n\n## Migration\n\n### Migrating from versions \u003c 1.11.0 to versions \u003e= 1.11.0\n\nFrom v1.11.0 this library now contains an assembly reference.\n\nBy default before this `dotnet add package dunet` will have generated:\n```xml\n\u003cPackageReference Include=\"dunet\" Version=\"1.10.0\"\u003e\n    \u003cIncludeAssets\u003eruntime; build; native; contentfiles; analyzers; buildtransitive\u003c/IncludeAssets\u003e\n    \u003cPrivateAssets\u003eall\u003c/PrivateAssets\u003e\n\u003c/PackageReference\u003e\n```\n\nWhen upgrading to dunet v1.11.0, this will need to be simplified to:\n```xml\n\u003cPackageReference Include=\"dunet\" Version=\"1.11.0\" /\u003e\n```\n\nOtherwise the assembly will not be included for compilation, leading to build failures when trying to reference\n the `Dunet` namespace or the `UnionAttribute` class.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdomn1995%2Fdunet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdomn1995%2Fdunet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdomn1995%2Fdunet/lists"}