{"id":50148426,"url":"https://github.com/titan-fx/TitanFx.DUnion","last_synced_at":"2026-06-09T22:00:45.803Z","repository":{"id":233279396,"uuid":"749548864","full_name":"titan-fx/TitanFx.DUnion","owner":"titan-fx","description":"DUnion is a C# source generator which allows the easy creation of discriminated union types","archived":false,"fork":false,"pushed_at":"2026-05-13T21:22:48.000Z","size":2790,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-27T21:25:24.405Z","etag":null,"topics":[],"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/titan-fx.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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":"2024-01-28T22:07:12.000Z","updated_at":"2026-05-13T21:22:51.000Z","dependencies_parsed_at":"2025-06-07T07:37:34.062Z","dependency_job_id":"e8d19b3e-2ab0-4677-8880-1b376b8b8203","html_url":"https://github.com/titan-fx/TitanFx.DUnion","commit_stats":null,"previous_names":["danny-may/dunion","titan-fx/titanfx.dunion"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/titan-fx/TitanFx.DUnion","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/titan-fx%2FTitanFx.DUnion","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/titan-fx%2FTitanFx.DUnion/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/titan-fx%2FTitanFx.DUnion/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/titan-fx%2FTitanFx.DUnion/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/titan-fx","download_url":"https://codeload.github.com/titan-fx/TitanFx.DUnion/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/titan-fx%2FTitanFx.DUnion/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34127345,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-09T02:00:06.510Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":[],"created_at":"2026-05-24T07:00:38.572Z","updated_at":"2026-06-09T22:00:45.797Z","avatar_url":"https://github.com/titan-fx.png","language":"C#","funding_links":[],"categories":["Contributors Welcome for those"],"sub_categories":["1. [ThisAssembly](https://ignatandrei.github.io/RSCG_Examples/v2/docs/ThisAssembly) , in the [EnhancementProject](https://ignatandrei.github.io/RSCG_Examples/v2/docs/rscg-examples#enhancementproject) category"],"readme":"# TitanFx.DUnion\n\nEasy source generator for creating custom discriminated unions.\n\n# Define a new discriminated union\n\n```csharp\n[DUnion]\npublic static class CreateUserResult\n{\n    public readonly record struct Success(User User);\n    public readonly record struct NameInUse(Guid UserId);\n    public readonly record struct NameTooLong(int MaxLength);\n    public readonly record struct NameTooShort(int MinLength);\n}\n```\n\n# Use the generated discriminated union\n\n```csharp\npublic CreateUserResult CreateUser(User user) \n{\n    if (user.Name is null or { Length: \u003c 5 }) \n    {\n        return new CreateUserResult.NameTooShort(5);\n    }\n    \n    if (user.Name.Length \u003e 30) \n    {\n        return new CreateUserResult.NameTooLong(30);\n    }\n\n    var existingUser = userStore.FindByName(user.Name);\n    if (existingUser is not null)\n    {\n        return new CreateUserResult.NameInUse(existingUser.Id);\n    }\n\n    var createdUser = userStore.Add(user);\n    return new CreateUserResult.Success(createdUser);\n}\n\npublic IActionResult HandleSignUpRequest(User user) \n{\n    return CreateUser(user)\n        .Match(\n            caseSuccess: success =\u003e Ok(success.User.Id),\n            caseNameInUse: error =\u003e BadRequest($\"The name is already in use by user {error.UserId}.\"),\n            @default: () =\u003e BadRequest()\n        )\n}\n```\n\n# Supports generics\n\n```csharp\n// Looks like rust! https://doc.rust-lang.org/std/option/\n[DUnion]\npublic static class Option\n{\n    public readonly record struct Some\u003cT\u003e(T Value);\n    public readonly record struct None();\n}\n\npublic Option\u003cdouble\u003e Divide(double numerator, double denominator) \n{\n    if (denominator == 0)\n    {\n        return new Option.None();\n    }\n    else\n    {\n        return new Option.Some\u003cdouble\u003e(numerator / denominator);\n    }\n}\n\nDivide(2.0, 3.0)\n    .Switch(\n        caseSome: some =\u003e Console.WriteLine($\"Result: {some.Value}\"),\n        caseNone: none =\u003e Console.WriteLine(\"Cannot divide by 0\")\n    );\n```\n\n## Multiple generics are also supported\n\nBy default, type parameters with the same name across cases will be merged into the same type parameter on the union, but you can customize this by using the `[DUnionGeneric]` attribute.  \n\n```csharp\n// Also looks a bit like rust! https://doc.rust-lang.org/std/result/\n[DUnion]\npublic static class Result\n{\n    public readonly record struct Ok\u003cTOk\u003e(TOk Value);\n    public readonly record struct Err\u003cTErr\u003e(TErr Error);\n}\n```\nOr, if you want to use the same name for the type parameter on the cases\n```csharp\n[DUnion]\npublic static class Result\n{\n    public readonly record struct Ok\u003c[DUnionGeneric(\"TOk\")]T\u003e(T Value);\n    public readonly record struct Err\u003c[DUnionGeneric(\"TErr\")]T\u003e(T Error);\n}\n```\nThen you can use it like this:\n```csharp\n\npublic enum Version \n{\n    Version1,\n    Version2\n}\n\npublic Result\u003cVersion, string\u003e ParseVersion(int[] header) \n{\n    switch (header) \n    {\n        case []: return new Result.Err\u003cstring\u003e(\"Invalid header length\");\n        case [1]: return new Result.Ok\u003cVersion\u003e(Version.Version1);\n        case [2]: return new Result.Ok\u003cVersion\u003e(Version.Version2);\n        default: return new Result.Err\u003cstring\u003e(\"Invalid version\");\n    }\n}\n\nParseVersion([1, 2, 3, 4])\n    .Switch(\n        caseOk: ok =\u003e Console.WriteLine($\"Working with version: {ok.Value}\"),\n        caseErr: err =\u003e Console.WriteLine($\"Error parsing header: {err.Error}\")\n    );\n```\n\n# Unions can be extended\n\nYou can add methods onto the union type itself to add custom helper methods, making using the union easier.\n\n```csharp\npublic readonly record struct JsonValue\n{\n    public readonly record struct String(string Value);\n    public readonly record struct Number(double Value);\n    public readonly record struct Boolean(bool Value);\n    public readonly record struct Null();\n    public readonly record struct Array(IReadOnlyList\u003cJsonValue\u003e Values);\n    public readonly record struct Object(IReadOnlyDictionary\u003cstring, JsonValue\u003e Properties);\n\n    public string Stringify() \n    {\n        return Match(\n            caseString: str =\u003e Escape(str.Value),\n            caseBoolean: bool =\u003e bool.Value.ToString(),\n            caseNull: _ =\u003e \"null\",\n            caseArray: arr =\u003e $\"[{string.Join(\",\", arr.Values.Select(v =\u003e v.Stringify()))}]\",\n            caseObject: obj =\u003e $\"{{{string.Join(\",\", obj.Properties.Select(p =\u003e $\"{Escape(p.Key)}: {p.Value.Stringify()}\"))}}}\"\n        )\n    }\n\n    private static string Escape(string value) \n    {\n        return \"\\\"...\\\"\";\n    }\n}\n```\n\n# Using in multiple projects\n\nIn most situations, you will be fine to add this source generator to any of your projects, however it does come with a bit of duplication if you do so. Each place where you add this package will have a set of internal attributes added, namely `TitanFx.DUnion.DUnionAttribute`, `TitanFx.DUnion.DUnionCaseAttribute`, `TitanFx.DUnion.DUnionGenericAttribute`, and `TitanFx.DUnion.DUnionExcludeAttribute`. These might therefore be duplicated many times and slightly inflate your build output. Theres also an issue with the `[InternalsVisibleTo]` attribute. If two projects have the source generator installed, and one has its internals visible to the other, then the build will fail due to ambiguous references to the attributes.\n\nTo solve all these issues, you can install the [`TitanFx.DUnion.Attributes`](https://www.nuget.org/packages/TitanFx.DUnion.Attributes) package, and add the following to your `.csproj` files:\n\n```xml\n\u003cPropertyGroup\u003e\n    \u003cDefineConstants\u003eDUNION_OMIT_ATTRIBUTES\u003c/DefineConstants\u003e\n\u003c/PropertyGroup\u003e\n```\n\nEverything should work just fine after that!\n\n# Union members\n\nThe generated unions have some members defined on them to allow you to interact with the case they wrap:\n\n## Constructors\n\nThe union type will automatically contain a constructor for each of the cases it encompasses. These constructors can be used to wrap a case so that it can be used wherever the union type is required. Typically you wont need to use the constructors as there are implicit conversions from the cases to the union, but theyre there nonetheless!\n\n- `public MyUnion({Case} value)`\n\n## Switch\n\nThe `Switch` method is intended to mimic the c# `switch` statement, meaning you supply a number of handlers for each case you wish to accept, and optionally supply a default case, which can be null. To help reduce situations where a newly added case is missed, there are two overloads of the `Switch` method:\n\n- The `Switch` method with a `@default` parameter has all other parameters marked as optional. This is useful if you only ever want to handle some of the cases, and are confident that you will not need to handle any potential future ones.\n\n- The `Switch` method without a `@default` parameter requires that all possible cases have an argument supplied, although you are allowed to supply `null` if you do not wish to handle a specific case. This overload is useful for when you want to ensure that any future cases that may be added to the union are not forgotten.\n\n```csharp\n[DUnion]\npublic readonly record struct AccountType\n{\n    public readonly record struct User(string Email);\n    public readonly record struct Admin(string Email);\n    public readonly record struct Service(string Name, AccountType Owner);\n    public readonly record struct System(Guid Id);\n}\n\nAccountType account = GetAccountType(id);\n\n\naccount.Switch(\n    caseUser: user =\u003e { ... }, // Called if the account is of type AccountType.User\n    caseAdmin: user =\u003e { ... }, // Called if the account is of type AccountType.Admin\n    caseService: null // accounts of type AccountType.Service are ignored\n    // Error: value for caseSystem is not supplied\n)\n\naccount.Switch(\n    caseUser: user =\u003e { ... }, // Called if the account is of type AccountType.User\n    caseAdmin: user =\u003e { ... }, // Called if the account is of type AccountType.Admin\n    caseService: null, // accounts of type AccountType.Service are ignored\n    @default: null // accounts of type AccountType.System and any other ones added in the future are ignored\n)\n```\n\nThe name of the `Switch` method can be changed by setting the `SwitchName` property on the `[DUnion]` attribute.\n\n```csharp\n[DUnion(SwitchName = \"MyPreferredSwitchName\")]\npublic readonly record struct MyUnion\n{\n    ...\n}\n```\n\n## Match\n\nThe `Match` method is intended to mimic the c# `switch` expression, meaning you supply a number of handlers for each case you wish to accept, and optionally supply a default case. One of these handlers will be called, and its result will be returned. To help reduce situations where a newly added case is missed, there are two overloads of the `Match` method:\n\n- The `Match` method with a `@default` parameter has all other parameters marked as optional. This is useful if you only ever want to handle some of the cases, and are confident that you will not need to handle any potential future ones.\n\n- The `Match` method without a `@default` parameter requires that all possible cases have an argument supplied. This overload is useful for when you want to ensure that any future cases that may be added to the union are not forgotten.\n\n```csharp\n[DUnion]\npublic readonly record struct AccountType\n{\n    public readonly record struct User(string Email);\n    public readonly record struct Admin(string Email);\n    public readonly record struct Service(string Name, AccountType Owner);\n    public readonly record struct System(Guid Id);\n}\n\nAccountType account = GetAccountType(id);\n\naccount.Match(\n    caseUser: user =\u003e { return ... }, // Called if the account is of type AccountType.User\n    caseAdmin: user =\u003e { return ... }, // Called if the account is of type AccountType.Admin\n    caseService: null // Error: ArgumentNullException\n    // Error: value for caseSystem is not supplied\n)\n\naccount.Match(\n    caseUser: user =\u003e { return ... }, // Called if the account is of type AccountType.User\n    caseAdmin: user =\u003e { return ... }, // Called if the account is of type AccountType.Admin\n    @default: () =\u003e { return ... } // accounts of type AccountType.System, AccountType.Service and any other ones added in the future will cause the default to be called\n)\n```\n\nThe name of the `Match` method can be changed by setting the `MatchName` property on the `[DUnion]` attribute.\n\n```csharp\n[DUnion(MatchName = \"MyPreferredMatchName\")]\npublic readonly record struct MyUnion\n{\n    ...\n}\n```\n\n## Is{Case}\n\nThe `Is{Case}` method is intended to mimic the `x is Case` and `x is Case value` expressions. It returns `true` and sets `out value` to the value of the case if the current union represents the specified case; otherwise `false` and `default(Case)` will be used.\n\n```csharp\n[DUnion]\npublic static class Option\n{\n    public readonly record struct Some\u003cT\u003e(T Value);\n    public readonly record struct None();\n}\n\nOption\u003cint\u003e result = GetResult();\nif (result.IsSome(out var some)) \n{\n    // Do something with `some`\n}\n\nif (result.IsNone())\n{\n    // You dont have to pass an out parameter if you dont want to use it\n}\n```\n\nThe name of the `Is{Case}` method can be changed by setting the `IsCaseName` property on the `[DUnionCase]` attribute.\n\n```csharp\n[DUnion]\npublic readonly record struct MyUnion\n{\n    [DUnionCase(IsCaseName = \"IsSuccess\")]\n    public readonly record struct Ok();\n}\n```\n\n## As{Case}OrDefault\n\nThe `As{Case}OrDefault` method is intended to mimic the `x as Case` expression. It returns the value of the case if the union represents the specified case type; otherwise a default value will be returned.\n\nThere are three overloads for `As{Case}OrDefault`, allowing you to specify what should be used as the `default` value in the case.\n\n- No arguments will use `default(Case)` as the default return value.\n- A `Func\u003cCase\u003e` argument will use the result of invoking the delegate as the default return value.\n- A `Case` argument will use that value as the default return value.\n\n```csharp\n[DUnion]\npublic static class Option\n{\n    public record Some\u003cT\u003e(T Value);\n    public record None();\n}\n\nOption\u003cint\u003e result = GetResult();\nvar nullOrSome = result.AsSomeOrDefault();\nvar alwaysSome = result.AsSomeOrDefault(new Option.Some\u003cT\u003e(0));\nvar alsoAlwaysSome = result.AsSomeOrDefault(() =\u003e new Option.Some\u003cT\u003e(0));\n\nvar nullOrNone = result.AsNoneOrDefault();\nvar alwaysNone = result.AsNoneOrDefault(new Option.None());\nvar alsoAlwaysNone = result.AsNoneOrDefault(() =\u003e new Option.None());\n```\nThe name of the `As{Case}OrDefault` method can be changed by setting the `AsCaseOrDefault` property on the `[DUnionCase]` attribute.\n\n```csharp\n[DUnion]\npublic readonly record struct MyUnion\n{\n    [DUnionCase(AsCaseOrDefault = \"AsSuccessOrDefault\")]\n    public readonly record struct Ok();\n}\n```\n\n## IEquatable\u003cUnion\u003e\n\nAll unions are equatable to themselves. For two unions to be considered equal, both the type of their case, and the value of their case must be equal. The following methods are implemented which relate to this:\n\n- `static bool Equals(MyUnion left, MyUnion right)`\n- `bool Equals(MyUnion other)`\n- `bool IEquatable\u003cMyUnion\u003e.Equals(MyUnion other)`\n- `override bool Equals(object? other)`\n- `static bool operator ==(MyUnion left, MyUnion right)`\n- `static bool operator !=(MyUnion left, MyUnion right)`\n- `override int GetHashCode()`\n\n## Casting\n\nUnions can also be converted to their cases via casting, and vice versa. Going from a case to a union is an implicit cast, while going from a union to a case is explicit and may throw an exception if the cast is not valid. The following methods are implemented which relate to this:\n\n- `static implicit operator MyUnion({Case} value)`\n- `static explicit operator {Case}(MyUnion value)`\n\nNOTE: Conversions to and from an interface cannot be defined, so if a case is an interface you cannot cast between it and the union, and vice versa.\n\n\n## Fields\n\nThere are two `private readonly` fields located on the union instances. These fields generally should not be used for anything, but you can expose them through some readonly properties if you wish. I would advise not attepting to set these values yourself, for reasons detailed below.\n\n### `byte _discriminator`\n\nThis holds a number indicating which type of case the union is currently wrapping. The mapping from the value of this field to the case type is not stored anywhere, so you should not rely on the value of this. If the order in which you define the cases changes, the meaning of the values of this field will also change. This field may also be a `ushort` instead of a `byte` if there are more than 254 cases.\n\nThe only value whos meaning is built in is `0`. A value of 0 means this union instance has not been constructed properly. Ordinarily this can only happen if the union is a `struct` and is the default value.\n\n```csharp\n[DUnion]\npublic readonly struct class Option\n{\n    public readonly record struct Some(string Value);\n    public readonly record struct None();\n\n    public byte Discriminator =\u003e _discriminator;\n}\n\nOption value = default;\nvalue.Discriminator == 0; // true\n```\n\nThe name of the `_discriminator` field can be changed by setting the `DiscriminatorName` property on the `[DUnion]` attribute. This is mainly to allow mitigation of potential name collisions.\n\n```csharp\n[DUnion(DiscriminatorName = \"_someOtherDiscriminatorName\")]\npublic readonly record struct MyUnion\n{\n    ...\n}\n```\n\n### `object? _value`\n\nThis holds the current case the union is wrapping. You can access this value if you want to be able to access the type in a non-type-safe way. It will be your responsibility to perform any type checks on this before casting.\n\nThe name of the `_value` field can be changed by setting the `ValueName` property on the `[DUnion]` attribute. This is mainly to allow mitigation of potential name collisions.\n\n```csharp\n[DUnion(ValueName = \"_someOtherValueName\")]\npublic readonly record struct MyUnion\n{\n    ...\n}\n```\n\n# Type safety\n\nDue to the way c# works, under the hood all cases are stored in the [`object? _value`](#object-_value) field in the union. Converting to or from a strongly typed case to `object?` takes time, especially if the case is a value type like a `struct` or `enum`. To squeeze as much speed out of the union, there is an opt-in way to leverage the `System.Runtime.CompilerServices.Unsafe` class. This class allows us to skip a lot of the \"slow\" type checks when converting from `object?` to the strongly typed cases. Under normal usage of the unions, this is a safe process as all types are strongly checked before writing to the [`_value`](#object-_value) field. This means it is safe to enable the usage of the `Unsafe` class in almost all situations.\n\n`UseUnsafe` is turned off by default simply to reduce the chance of things being broken without realising, as it effectively turns off some normally unneeded checks. If you are having performance issues, and have identified that this will help alleviate them, and you have checked it is safe to do so, then feel free to turn this feature on at a per-union level.\n\nIf, however, any method is used to modify or set the [`_value`](#object-_value) or [`_discriminator`](#byte-_discriminator) fields yourself, then you must also maintain these checks yourself to ensure that the values at runtime are correctly set. \n\n\n```csharp\n[DUnion(UseUnsafe = true)]\npublic static class Option\n{\n    public record Some\u003cT\u003e(T Value);\n    public record None();\n}\n\npublic partial class Option\u003cT\u003e \n{\n    public Option(T value)\n    {\n        // Dangerous: UseUnsafe is enabled, but the values of _value and _discriminator might not align any more! If the order of Some and None got swapped, this would be incorrect.\n        this._value = new Option.Some\u003cT\u003e(value);\n        this._discriminator = 1; \n    }\n\n    public Option(T value) : this(new Option.Some\u003cT\u003e(value)) \n    {\n        // Safe: Setting of _value and _discriminator is delegated to the source generated code, so their relationship will be maintained.\n    }\n}\n\nOption\u003cint\u003e union = new Option.Some\u003cint\u003e(123);\n// Dangerous: UseUnsafe is enabled, but the values of _value and _discriminator might not align any more! If the order of Some and None got swapped, this would be incorrect.\ntypeof(Option\u003cint\u003e)\n    .GetField(\"_value\", BindingFlags.NonPublic | BindingFlags.Instance)!\n    .SetValue(new Option.None());\ntypeof(Option\u003cint\u003e)\n    .GetField(\"_discriminator\", BindingFlags.NonPublic | BindingFlags.Instance)!\n    .SetValue(2);\n\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftitan-fx%2FTitanFx.DUnion","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftitan-fx%2FTitanFx.DUnion","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftitan-fx%2FTitanFx.DUnion/lists"}