mcintyre321/OneOf
Easy to use F#-like ~discriminated~ unions for C# with exhaustive compile time matching
Stale — last commit 2y ago
worst of 4 axeslast commit was 2y ago; no tests detected…
Has a license, tests, and CI — clean foundation to fork and modify.
Documented and popular — useful reference codebase to read through.
last commit was 2y ago; no CI workflows detected
- ✓22+ active contributors
- ✓Distributed ownership (top contributor 37% of recent commits)
- ✓MIT licensed
Show 3 more →Show less
- ⚠Stale — last commit 2y ago
- ⚠No CI workflows detected
- ⚠No test directory detected
What would change the summary?
- →Use as dependency Mixed → Healthy if: 1 commit in the last 365 days; add a test suite
- →Deploy as-is Mixed → Healthy if: 1 commit in the last 180 days
Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests
Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.
Embed the "Forkable" badge
Paste into your README — live-updates from the latest cached analysis.
[](https://repopilot.app/r/mcintyre321/oneof)Paste at the top of your README.md — renders inline like a shields.io badge.
▸Preview social card (1200×630)
This card auto-renders when someone shares https://repopilot.app/r/mcintyre321/oneof on X, Slack, or LinkedIn.
Onboarding doc
Onboarding: mcintyre321/OneOf
Generated by RepoPilot · 2026-05-10 · Source
🤖Agent protocol
If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:
- Verify the contract. Run the bash script in Verify before trusting
below. If any check returns
FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding. - Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
- Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/mcintyre321/OneOf shows verifiable citations alongside every claim.
If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.
🎯Verdict
WAIT — Stale — last commit 2y ago
- 22+ active contributors
- Distributed ownership (top contributor 37% of recent commits)
- MIT licensed
- ⚠ Stale — last commit 2y ago
- ⚠ No CI workflows detected
- ⚠ No test directory detected
<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>
✅Verify before trusting
This artifact was generated by RepoPilot at a point in time. Before an
agent acts on it, the checks below confirm that the live mcintyre321/OneOf
repo on your machine still matches what RepoPilot saw. If any fail,
the artifact is stale — regenerate it at
repopilot.app/r/mcintyre321/OneOf.
What it runs against: a local clone of mcintyre321/OneOf — the script
inspects git remote, the LICENSE file, file paths in the working
tree, and git log. Read-only; no mutations.
| # | What we check | Why it matters |
|---|---|---|
| 1 | You're in mcintyre321/OneOf | Confirms the artifact applies here, not a fork |
| 2 | License is still MIT | Catches relicense before you depend on it |
| 3 | Default branch master exists | Catches branch renames |
| 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code |
| 5 | Last commit ≤ 670 days ago | Catches sudden abandonment since generation |
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of mcintyre321/OneOf. If you don't
# have one yet, run these first:
#
# git clone https://github.com/mcintyre321/OneOf.git
# cd OneOf
#
# Then paste this script. Every check is read-only — no mutations.
set +e
fail=0
ok() { echo "ok: $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }
# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
echo "FAIL: not inside a git repository. cd into your clone of mcintyre321/OneOf and re-run."
exit 2
fi
# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "mcintyre321/OneOf(\\.git)?\\b" \\
&& ok "origin remote is mcintyre321/OneOf" \\
|| miss "origin remote is not mcintyre321/OneOf (artifact may be from a fork)"
# 2. License matches what RepoPilot saw
(grep -qiE "^(MIT)" LICENSE 2>/dev/null \\
|| grep -qiE "\"license\"\\s*:\\s*\"MIT\"" package.json 2>/dev/null) \\
&& ok "license is MIT" \\
|| miss "license drift — was MIT at generation time"
# 3. Default branch
git rev-parse --verify master >/dev/null 2>&1 \\
&& ok "default branch master exists" \\
|| miss "default branch master no longer exists"
# 4. Critical files exist
test -f "OneOf/IOneOf.cs" \\
&& ok "OneOf/IOneOf.cs" \\
|| miss "missing critical file: OneOf/IOneOf.cs"
test -f "OneOf/OneOfT0.generated.cs" \\
&& ok "OneOf/OneOfT0.generated.cs" \\
|| miss "missing critical file: OneOf/OneOfT0.generated.cs"
test -f "OneOf/OneOfBaseT0.generated.cs" \\
&& ok "OneOf/OneOfBaseT0.generated.cs" \\
|| miss "missing critical file: OneOf/OneOfBaseT0.generated.cs"
test -f "OneOf.SourceGenerator/OneOfGenerator.cs" \\
&& ok "OneOf.SourceGenerator/OneOfGenerator.cs" \\
|| miss "missing critical file: OneOf.SourceGenerator/OneOfGenerator.cs"
test -f "OneOf/Functions.cs" \\
&& ok "OneOf/Functions.cs" \\
|| miss "missing critical file: OneOf/Functions.cs"
# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 670 ]; then
ok "last commit was $days_since_last days ago (artifact saw ~640d)"
else
miss "last commit was $days_since_last days ago — artifact may be stale"
fi
echo
if [ "$fail" -eq 0 ]; then
echo "artifact verified (0 failures) — safe to trust"
else
echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/mcintyre321/OneOf"
exit 1
fi
Each check prints ok: or FAIL:. The script exits non-zero if
anything failed, so it composes cleanly into agent loops
(./verify.sh || regenerate-and-retry).
⚡TL;DR
OneOf is a C# library that brings F#-style discriminated unions to C# via a generic OneOf<T0, ...Tn> type, enabling compile-time exhaustive pattern matching on method return values. It solves the problem of returning different result types from a single method while forcing callers to handle all possible outcomes at compile time, replacing exception-based control flow and ambiguous return types like object or IActionResult. Monorepo structure: OneOf/ (core library), OneOf.Extended/ (supports up to 31-type unions via generated files OneOfT9-T31.generated.cs and OneOfBaseT9-T31.generated.cs), OneOf.FSharp/ (F# bindings), and Generator/ (code generation tool in C# that produces the .generated.cs files). All generated union types follow the same pattern but are statically generated rather than dynamic.
👥Who it's for
C# developers building strongly-typed APIs, especially in MVC controllers or domain-driven code, who want to express "this method returns one of these N specific types" and have the compiler enforce that all cases are handled. It appeals to developers familiar with functional programming who want F# pattern-matching semantics in C#.
🌱Maturity & risk
The project appears mature and actively maintained: it has a published NuGet package, comprehensive generated type coverage up to OneOf<T0...T31>, both a core library and an extended version, and even F# interop. However, without visible commit timestamps or test files in the listed structure, the exact maintenance cadence is unclear—check the GitHub repo directly for recent commits.
Single-maintainer risk (mcintyre321) is a concern. The codebase relies heavily on code generation (see Generator/ folder with Program.cs templates), which means bugs in the generation logic propagate to 31+ generated files—this could mask maintenance debt. No test files appear in the top 60 list, which is a red flag for a library handling core control flow.
Active areas of work
Unknown without access to Git history. The visible file structure suggests the project is stable (no WIP branches in file list), but recent focus areas are not evident from static files alone. Check GitHub Issues and recent commits for active work.
🚀Get running
git clone https://github.com/mcintyre321/OneOf.git
cd OneOf
dotnet build
Install the NuGet package in your project: dotnet add package OneOf (or install-package OneOf via Package Manager Console).
Daily commands:
For the library itself: dotnet build OneOf/ or dotnet build OneOf.Extended/. To regenerate the union types: execute Generator/Program.cs (the Generator.csproj builds the generation tool). No dev server; this is a library consumed by other projects.
🗺️Map of the codebase
OneOf/IOneOf.cs— Core interface that all OneOf types implement; defines the contract for discriminated unions and is essential to understanding the library's architecture.OneOf/OneOfT0.generated.cs— Template for single-type OneOf; shows the foundational pattern used to generate all other OneOf<T0...Tn> variants with Match methods and exhaustive checking.OneOf/OneOfBaseT0.generated.cs— Base class for all OneOf types; contains shared logic for value storage, equality, and ToString that all discriminated unions depend on.OneOf.SourceGenerator/OneOfGenerator.cs— Roslyn source generator that creates OneOf variants at compile-time; fundamental to how the library generates exhaustive Match overloads without reflection.OneOf/Functions.cs— Utility extension methods for creating and working with OneOf instances; commonly used API for library consumers.OneOf.Tests/MapTests.cs— Core tests validating the Match/Map behavior that ensures exhaustive compile-time checking works correctly.
🛠️How to make changes
Add support for a new union size (e.g., 32 types)
- Extend Generator/Program.cs to include T32 in the loop generating OneOfT and OneOfBaseT variants (
Generator/Program.cs) - Create OneOf.Extended/OneOfBaseT32.generated.cs by copying OneOfBaseT31.generated.cs and updating generic parameter count (
OneOf.Extended/OneOfBaseT32.generated.cs) - Create OneOf.Extended/OneOfT32.generated.cs with Match method overloads for all 32 type combinations (
OneOf.Extended/OneOfT32.generated.cs) - Update OneOf.SourceGenerator/OneOfGenerator.cs to generate Roslyn source for T32 unions (
OneOf.SourceGenerator/OneOfGenerator.cs) - Add test case in OneOf.SourceGenerator.Tests/SourceGeneratorTests.cs validating T32 generation (
OneOf.SourceGenerator.Tests/SourceGeneratorTests.cs)
Create a new predefined union type (semantic domain type)
- Create a new file in OneOf/Types/ (e.g., YesNoOrMaybe.cs) inheriting from OneOf<T0, T1, T2> (
OneOf/Types/YesNoOrMaybe.cs) - Define static factory properties matching the semantic meaning (
OneOf/Types/YesNoOrMaybe.cs) - Add unit tests in OneOf.Tests/ to validate the type's Match behavior (
OneOf.Tests/InterfaceTests.cs)
Customize JSON serialization for OneOf types
- Implement a custom JsonConverter in OneOf.Tests/OneOfJsonConverter.cs as template (
OneOf.Tests/OneOfJsonConverter.cs) - Register the converter with your JSON serializer (e.g., System.Text.Json or Newtonsoft.Json) (
OneOf.Tests/Serialization.cs) - Add test cases in OneOf.Tests/Serialization.cs validating round-trip serialization (
OneOf.Tests/Serialization.cs)
Add a diagnostic rule for compile-time OneOf validation
- Define a new diagnostic descriptor in OneOf.SourceGenerator/GeneratorDiagnosticDescriptors.cs (
OneOf.SourceGenerator/GeneratorDiagnosticDescriptors.cs) - Implement rule logic in OneOf.SourceGenerator/OneOfGenerator.cs as a context check (
OneOf.SourceGenerator/OneOfGenerator.cs) - Add analyzer test case in OneOf.SourceGenerator.AnalyzerTests/AnalyzerTests.cs (
OneOf.SourceGenerator.AnalyzerTests/AnalyzerTests.cs)
🔧Why these technologies
- Roslyn Incremental Source Generators — Enables compile-time code generation for exhaustive Match overloads without reflection, achieving type-safe discriminated unions with zero runtime overhead compared to polymorphism.
- Generic Type Parameters (T0...T31) — Supports unions of 2–32 types without requiring inheritance hierarchies or visitor patterns; each arity variant is independently compiled and matched.
- C# 7.3+ Pattern Matching (via switch expressions) — Aligns with modern C# syntax expectations for exhaustive matching; generated Match methods feel idiomatic to C# developers.
- Template-based Code Generation (Generator/Program.cs) — Pre-generates core OneOf<T0>...OneOf<T8> variants to avoid dependency on runtime code generation; reduces package size and startup time.
⚖️Trade-offs already made
-
Pre-generated variants (OneOf<T0>–T8) + extended variants (T9–T31) split across two NuGet packages
- Why: Keeps base library lean for 80% of use cases while offering extended support for complex domains without forcing bloat.
- Consequence: Developers must add separate OneOf.Extended package for >8-type unions; adds cognitive load but simplifies dependency management.
-
Exhaustive matching via delegate parameters rather than enum + switch
- Why: Provides type safety at compile time; impossible to forget a case or add a case without updating all callers.
- Consequence: Match methods have many overloads (exponential in arity); increases generated code size but prevents runtime errors from incomplete handling.
-
Value types + reference types mixed in single union
- Why: Enables real-world modeling (e.g., OneOf<string, int, Exception>).
- Consequence: Boxing overhead for value types; requires careful performance profiling in hot paths (mitigated by inlining).
-
No automatic JSON serialization in core library; requires custom converters
- Why: Avoids coupling to specific JSON frameworks (System.Text.Json vs. Newtonsoft.Json); lets consumers choose.
- Consequence: Serialization setup is manual; examples provided in test code rather than official docs.
🚫Non-goals (don't propose these)
- Does not provide async/await support for OneOf operations; users must await inside Match branches.
- Does not support recursive union types (e.g., Tree<T> = Leaf(T) | Branch(Tree<T>, Tree<T>)); requires explicit nesting.
- Does not offer runtime union reflection or dynamic type inspection; all matching is compile-time static.
- Does not provide discriminator fields or tagged unions in the runtime sense; identity is purely type-based.
🪤Traps & gotchas
- Code generation: The library relies on generated .cs files; manually editing OneOfT*.generated.cs will be overwritten on regeneration. Always modify Generator/Program.cs templates instead. 2. Generic arity: OneOf supports up to T31 out of the box; using OneOf<T0...T32> requires modifying the Generator and regenerating. 3. No visibility into test coverage from file listing—verify test project exists on GitHub before assuming production readiness. 4. Assembly versioning: OneOf.Extended and OneOf are separate NuGet packages; version mismatches can cause runtime issues.
🏗️Architecture
💡Concepts to learn
- Discriminated Union / Sum Type — Core concept of this library; understanding unions as 'a value that is exactly one of N types' is essential to using OneOf effectively, and it differs fundamentally from inheritance hierarchies
- Exhaustive Pattern Matching — OneOf enforces compile-time exhaustiveness via the Match() method signature—if you don't handle all union cases, code won't compile. This is the primary safety benefit over polymorphism.
- Code Generation via Templates — OneOf uses a code generator (Generator/Program.cs) to produce boilerplate for up to 31 union types; understanding how the generator works is critical for modifying or extending the library
- Generic Variance in C# — OneOf<T0...Tn> uses invariant generics to ensure type safety; understanding why you can't coerce OneOf<Derived, X> to OneOf<Base, X> prevents subtle casting bugs
- Railway-Oriented Programming — OneOf is commonly used to implement railway-oriented programming (success/failure tracks) as an alternative to exceptions; the README example demonstrates returning Either<Success, Error> implicitly
- Nominal Typing vs. Structural Typing — OneOf uses nominal typing (types are distinguished by name, not structure); empty marker classes like InvalidName and NameTaken in the README leverage this to create self-documenting return types
🔗Related repos
CommunityToolkit/WindowsCommunityToolkit— Uses discriminated unions internally for result handling; similar control-flow pattern but in a larger ecosystemmcintyre321/ValueOf— Companion library by the same author for one-line Value Object definitions; complements OneOf for strongly-typed domain modelsdotnet/csharplang— C# language spec repo; discriminated unions are being explored as a potential language feature (records, pattern matching enhancements) that OneOf anticipatedlouthy/language-ext— Major functional programming library for C# that provides Union types and pattern matching; direct alternative with richer ecosystem but more heavyweightfsharpforfunandprofit/book-of-fsharp— Educational resource on F# discriminated unions; useful background for understanding the paradigm OneOf brings to C#
🪄PR ideas
To work on one of these in Claude Code or Cursor, paste:
Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.
Add comprehensive source generator diagnostic tests in OneOf.SourceGenerator.AnalyzerTests
The OneOf.SourceGenerator.AnalyzerTests directory exists but only contains AnalyzerTests.cs. The GeneratorDiagnosticDescriptors.cs file in OneOf.SourceGenerator defines diagnostic rules, but there's no evidence of exhaustive test coverage for error conditions like invalid type arguments, missing Match() calls, or unsupported generic constraints. Adding tests for all diagnostic scenarios would catch regressions and guide users on proper usage.
- [ ] Review GeneratorDiagnosticDescriptors.cs to identify all defined diagnostics
- [ ] Create test cases in OneOf.SourceGenerator.AnalyzerTests/AnalyzerTests.cs for each diagnostic scenario (invalid types, exhaustiveness violations, etc.)
- [ ] Add test cases for edge cases like nullable reference types and generic constraints
- [ ] Verify tests run against the OneOfGenerator.cs implementation
Add integration tests for OneOf.Extended generic overloads (T9-T31)
The OneOf.Extended project contains 46 generated files (OneOfT9.generated.cs through OneOfT31.generated.cs) and corresponding base classes, but OneOf.Tests doesn't appear to have dedicated test coverage for these extended generic arities. The existing test files (MapTests.cs, EqualsTests.cs, etc.) likely only test T2-T8 variants, leaving extended types untested.
- [ ] Create OneOf.Tests/ExtendedGenericsTests.cs with test class covering OneOf<T0...T9> through OneOf<T0...T31>
- [ ] Add tests for Match(), Map(), and TryPick() methods across extended arities
- [ ] Add equality and default constructor tests for extended types
- [ ] Verify generated code patterns are consistent across all extended variants
Create benchmarking suite for Match() performance across arity levels
With 30+ different generic overloads in OneOf.Extended, there's no benchmarking infrastructure to ensure Match() performance doesn't degrade with increasing type arity. This would help catch performance regressions during future refactoring and provide data for documentation about scalability.
- [ ] Create OneOf.Benchmarks project using BenchmarkDotNet NuGet package
- [ ] Add benchmarks comparing Match() execution time for OneOf<T0,T1> vs OneOf<T0...T9> vs OneOf<T0...T31>
- [ ] Benchmark allocation patterns and boxing behavior across arities
- [ ] Document results in benchmark output to guide users on performance characteristics
🌿Good first issues
- Add unit tests for OneOf.Extended union types T9–T31: the generated classes are not tested in the visible file structure, creating risk of silent generation bugs. Create a test project that instantiates and matches on each union type.
- Document the code generation workflow: add a CONTRIBUTING.md or comment in Generator/Program.cs explaining how to regenerate the .generated.cs files after template changes, as it's non-obvious to newcomers.
- Extend the F# interop in OneOf.FSharp/FsOneOf.fs to support the full T9–T31 union types (currently it likely only supports lower arities); ensure F# discriminated union conversion works end-to-end.
⭐Top contributors
Click to expand
Top contributors
- @mcintyre321 — 37 commits
- [@Damian Romanowski](https://github.com/Damian Romanowski) — 16 commits
- @zspitz — 11 commits
- @romfir — 7 commits
- @thomhurst — 3 commits
📝Recent commits
Click to expand
Recent commits
6e02dbe— Better performance Source Generator (#172) (thomhurst)cfe1c4d— Fully Qualified Type names for the Source Generator (to avoid namespace clashes) (#160) (thomhurst)bfadb11— Add badges to README.md (#146) (ruan92)ece0dc7— Rewrite ISourceGenerator into IIncrementalGenerator (#148) (thomhurst)7cd022c— Ran the generator to generate readonly OneOf structs (#129) (XanderStoffels)2d1711a— Fix minor typo in project description (#126) (NxSoftware)02c690d— Bump Newtonsoft.Json from 10.0.2 to 13.0.2 in /OneOf.Tests (#134) (dependabot[bot])639de36— Remove the public constraint so that internal classes can also be (#141) (dnperfors)12745ca— Added source generator diagnostics tests and fixed bug while generating OneOfBase with interfaces (#123) (romfir)a1c3548— Simplify the generator's importation ofGenerateOneOfAttribute.g.cset al. (#116) (teo-tsirpanis)
🔒Security observations
The OneOf library is a low-risk utility library for C# discriminated unions. No critical or high-severity vulnerabilities were identified. The codebase appears well-structured with proper separation of concerns. Minor recommendations include securing build tooling and ensuring dependency updates.
LLM-derived; treat as a starting point, not a security audit.
👉Where to read next
- Open issues — current backlog
- Recent PRs — what's actively shipping
- Source on GitHub
Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.