Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/mcintyre321/OneOf
Easy to use F#-like ~discriminated~ unions for C# with exhaustive compile time matching
https://github.com/mcintyre321/OneOf
c-sharp discriminated-unions dotnetcore f-sharp
Last synced: about 1 month ago
JSON representation
Easy to use F#-like ~discriminated~ unions for C# with exhaustive compile time matching
- Host: GitHub
- URL: https://github.com/mcintyre321/OneOf
- Owner: mcintyre321
- License: mit
- Created: 2016-01-12T22:31:19.000Z (almost 9 years ago)
- Default Branch: master
- Last Pushed: 2024-08-07T14:03:29.000Z (4 months ago)
- Last Synced: 2024-11-05T15:52:48.743Z (about 1 month ago)
- Topics: c-sharp, discriminated-unions, dotnetcore, f-sharp
- Language: C#
- Homepage:
- Size: 840 KB
- Stars: 3,479
- Watchers: 49
- Forks: 164
- Open Issues: 50
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
- RSCG_Examples - https://github.com/mcintyre321/OneOf
- awesome-reference-tools - OneOf
- awesome-dotnet - OneOf - OneOf provides discriminated unions for C# with exhaustive compile time matching. (Algorithms and Data structures)
README
# OneOf [![NuGet](https://img.shields.io/nuget/v/OneOf?logo=nuget)](https://www.nuget.org/packages/OneOf/) [![GitHub](https://img.shields.io/github/license/mcintyre321/OneOf)](licence.md)
> "Ah! It's like a compile time checked switch statement!" - Mike Giorgaras
## Getting Started
> `install-package OneOf`
This library provides F# style ~discriminated~ unions for C#, using a custom type `OneOf`. An instance of this type holds a single value, which is one of the types in its generic argument list.
I can't encourage you enough to give it a try! Due to exhaustive matching DUs provide an alternative to polymorphism when you want to have a method with guaranteed behaviour-per-type (i.e. adding an abstract method on a base type, and then implementing that method in each type). It's a really powerful tool, ask any f#/Scala dev! :)
PS If you like OneOf, you might want to check out [ValueOf](https://github.com/mcintyre321/valueof), for one-line Value Object Type definitions.
## Use cases
### As a method return value
The most frequent use case is as a return value, when you need to return different results from a method. Here's how you might use it in an MVC controller action:
```csharp
public OneOf CreateUser(string username)
{
if (!IsValid(username)) return new InvalidName();
var user = _repo.FindByUsername(username);
if(user != null) return new NameTaken();
var user = new User(username);
_repo.Save(user);
return user;
}[HttpPost]
public IActionResult Register(string username)
{
OneOf createUserResult = CreateUser(username);
return createUserResult.Match(
user => new RedirectResult("/dashboard"),
invalidName => {
ModelState.AddModelError(nameof(username), $"Sorry, that is not a valid username.");
return View("Register");
},
nameTaken => {
ModelState.AddModelError(nameof(username), "Sorry, that name is already in use.");
return View("Register");
}
);
}
```#### As an 'Option' Type
It's simple to use OneOf as an `Option` type - just declare a `OneOf`. OneOf comes with a variety of useful Types in the `OneOf.Types` namespace, including `Yes`, `No`, `Maybe`, `Unknown`, `True`, `False`, `All`, `Some`, and `None`.
#### Benefits
- True strongly typed method signature
- No need to return a custom result base type e.g `IActionResult`, or even worse, a non-descriptive type (e.g. object)
- The method signature accurately describes all the potential outcomes, making it easier for consumers to understand the code
- Method consumer HAS to handle all cases (see 'Matching', below)
- You can avoid using ["Exceptions for control flow"](http://softwareengineering.stackexchange.com/questions/189222/are-exceptions-as-control-flow-considered-a-serious-antipattern-if-so-why) antipattern by returning custom Typed error objects
### As a method parameter valueYou can use also use `OneOf` as a parameter type, allowing a caller to pass different types without requiring additional overloads. This might not seem that useful for a single parameter, but if you have multiple parameters, the number of overloads required increases rapidly.
```csharp
public void SetBackground(OneOf backgroundColor) { ... }//The method above can be called with either a string, a ColorName enum value or a Color instance.
```## Matching
You use the `TOut Match(Func f0, ... Func fn)` method to get a value out. Note how the number of handlers matches the number of generic arguments.
### Advantages over `switch` or `if` or `exception` based control flow:
This has a major advantage over a switch statement, as it
- requires every parameter to be handled
- No fallback - if you add another generic parameter, you HAVE to update all the calling code to handle your changes.In brown-field code-bases this is incredibly useful, as the default handler is often a runtime `throw NotImplementedException`, or behaviour that wouldn't suit the new result type.
E.g.
```csharp
OneOf backgroundColor = ...;
Color c = backgroundColor.Match(
str => CssHelper.GetColorFromString(str),
name => new Color(name),
col => col
);
_window.BackgroundColor = c;
```There is also a .Switch method, for when you aren't returning a value:
```csharp
OneOf dateValue = ...;
dateValue.Switch(
str => AddEntry(DateTime.Parse(str), foo),
int => AddEntry(int, foo)
);
```### TryPickđť‘Ą method
As an alternative to `.Switch` or `.Match` you can use the `.TryPickđť‘Ą` methods.
```csharp
//TryPickđť‘Ą methods for OneOf
public bool TryPickT0(out T0 value, out OneOf remainder) { ... }
public bool TryPickT1(out T1 value, out OneOf remainder) { ... }
public bool TryPickT2(out T2 value, out OneOf remainder) { ... }
```The return value indicates if the OneOf contains a Tđť‘Ą or not. If so, then `value` will be set to the inner value from the OneOf. If not, then the remainder will be a OneOf of the remaining generic types. You can use them like this:
```csharp
IActionResult Get(string id)
{
OneOf thingOrNotFoundOrError = GetThingFromDb(string id);if (thingOrNotFoundOrError.TryPickT1(out NotFound notFound, out var thingOrError)) //thingOrError is a OneOf
return StatusCode(404);if (thingOrError.TryPickT1(out var error, out var thing)) //note that thing is a Thing rather than a OneOf
{
_logger.LogError(error.Message);
return StatusCode(500);
}return Ok(thing);
}
```### Reusable OneOf Types using OneOfBase
You can declare a OneOf as a type, either for reuse of the type, or to provide additional members, by inheriting from `OneOfBase`. The derived class will inherit the `.Match`, `.Switch`, and `.TryPickđť‘Ą` methods.
```csharp
public class StringOrNumber : OneOfBase
{
StringOrNumber(OneOf _) : base(_) { }// optionally, define implicit conversions
// you could also make the constructor public
public static implicit operator StringOrNumber(string _) => new StringOrNumber(_);
public static implicit operator StringOrNumber(int _) => new StringOrNumber(_);public (bool isNumber, int number) TryGetNumber() =>
Match(
s => (int.TryParse(s, out var n), n),
i => (true, i)
);
}StringOrNumber x = 5;
Console.WriteLine(x.TryGetNumber().number);
// prints 5x = "5";
Console.WriteLine(x.TryGetNumber().number);
// prints 5x = "abcd";
Console.WriteLine(x.TryGetNumber().isNumber);
// prints False
```### OneOfBase Source Generation
You can automatically generate `OneOfBase` hierarchies using `GenerateOneOfAttribute` and partial class that extends `OneOfBase` using
a Source Generator (thanks to @romfir for the contribution :D). Install it via> Install-Package OneOf.SourceGenerator
and then define a stub like so:
```csharp
[GenerateOneOf]
public partial class StringOrNumber : OneOfBase { }
```During compilation the source generator will produce a class implementing the OneOfBase boiler plate code for you. e.g.
```csharp
public partial class StringOrNumber
{
public StringOrNumber(OneOf.OneOf _) : base(_) { }public static implicit operator StringOrNumber(System.String _) => new StringOrNumber(_);
public static explicit operator System.String(StringOrNumber _) => _.AsT0;public static implicit operator StringOrNumber(System.Int32 _) => new StringOrNumber(_);
public static explicit operator System.Int32(StringOrNumber _) => _.AsT1;
}
```