Discriminated union type source generator
C# doesn't support discriminated unions yet. This source generator helps automate writing union types with set of helper methods.
Add package reference to N.SourceGenerators.UnionTypes
dotnet add package N.SourceGenerators.UnionTypesCreate a partial class or struct that will be used as a union type
publicpartialclassFooResult{}Add types you want to use in a discriminated union
publicrecordSuccess(intValue);publicrecordValidationError(stringMessage);publicrecordNotFoundError;publicpartialclassFooResult{}Add N.SourceGenerators.UnionTypes.UnionTypeAttribute to a union type.
usingN.SourceGenerators.UnionTypes;publicrecordSuccess(intValue);publicrecordValidationError(stringMessage);publicrecordNotFoundError;[UnionType(typeof(Success))][UnionType(typeof(ValidationError))][UnionType(typeof(NotFoundError))]publicpartialclassFooResult{}Or you can use generic type.
publicpartialclassOperationDataResult<[GenericUnionType]TResult,[GenericUnionType]TError>{}// extend generic type union with additional Int32 type[UnionType(typeof(int))]publicpartialclassExtendedOperationDataResult<[GenericUnionType]TResult,[GenericUnionType]TError>{}Null values are not allowed by default. This behavior can be overriden by AllowNull = true parameter.
[UnionType(typeof(int?),AllowNull=true)][UnionType(typeof(string),AllowNull=true)]publicpartialclassResultNullable<[GenericUnionType(AllowNull=true)]T>{}All examples can be found in examples project
Implicit conversion
publicFooResultImplicitReturn(){// you can return any union type variation without creating FooResultreturnnewNotFoundError();}Explicit conversion
publicValidationErrorExplicitCast(FooResultresult){return(ValidationError)result;}Checking value type
publicvoidValueTypeProperty(){FooResultfoo=GetFoo();TypevalueType=foo.ValueType;// returns typeof(NotFoundError)staticFooResultGetFoo(){returnnewNotFoundError();}}TryGet method is used to check if union contains a specific type
publicvoidTryGetValue(){FooResultfoo=GetFoo();if(foo.TryGetNotFoundError(outvarnotFoundError)){// make something with notFoundError}staticFooResultGetFoo(){returnnewNotFoundError();}}Alias for each variant is generated based on type name. Use alias parameter to override it.
[UnionType(typeof(int))][UnionType(typeof(string))]// default alias is 'ArrayOfTupleOfIntAndString' but it is overriden by alias parameter[UnionType(typeof(Tuple<int,string>[]),alias:"Items")]publicpartialclassAliasResult{}Match and MatchAsync methods are used to convert union type to another type. These methods force you to handle all possible variations.
publicIActionResultMatchMethod(FooResultresult){returnresult.Match<IActionResult>(
success =>newOkResult(),
validationError =>newBadRequestResult(),
notFoundError =>newNotFoundResult());}publicasyncTask<IActionResult>MatchAsyncMethod(FooResultresult,CancellationTokencancellationToken){returnawaitresult.MatchAsync<IActionResult>(staticasync(success,ct)=>{awaitSomeWork(success,ct);returnnewOkResult();},staticasync(validationError,ct)=>{awaitSomeWork(validationError,ct);returnnewBadRequestResult();},staticasync(notFoundError,ct)=>{awaitSomeWork(notFoundError,ct);returnnewNotFoundResult();},cancellationToken);staticTaskSomeWork<T>(Tvalue,CancellationTokenct){returnTask.Delay(100,ct);}}Switch and SwitchAsync methods are used to execute some work based on inner type
publicvoidSwitchMethod(FooResultresult){result.Switch(
success =>SomeWork(success),
validationError =>SomeWork(validationError),
notFoundError =>SomeWork(notFoundError));staticvoidSomeWork<T>(Tvalue){thrownewNotImplementedException();}}publicasyncTaskSwitchAsyncMethod(FooResultresult,CancellationTokencancellationToken){awaitresult.SwitchAsync(staticasync(success,ct)=>{awaitSomeWork(success,ct);},staticasync(validationError,ct)=>{awaitSomeWork(validationError,ct);},staticasync(notFoundError,ct)=>{awaitSomeWork(notFoundError,ct);},cancellationToken);staticTaskSomeWork<T>(Tvalue,CancellationTokenct){returnTask.Delay(100,ct);}}To add JSON support
- add
JsonPolymorphicUnionattribute to union type - add
TypeDiscriminatorto each type variant
- .NET 7 or newer
- only complex type variants
[UnionType(typeof(JsonTestsFooJ),TypeDiscriminator="Foo")][UnionType(typeof(JsonTestsBarJ),TypeDiscriminator="Bar")][JsonPolymorphicUnion]publicpartialclassJsonTestsUnion{}When one union type's variants is subset of another union type's variants use one of the following attributes to convert one type to another: UnionConverterTo, UnionConverterFrom, or UnionConverter.
[UnionConverterFrom(typeof(DataAccessResult))]// use this attributepublicpartialclassBusinessLogicResult{}[UnionConverterTo(typeof(BusinessLogicResult))]// OR thispublicpartialclassDataAccessResult{}[UnionConverter(typeof(DataAccessResult),typeof(BusinessLogicResult))]// OR thispublicstaticpartialclassConverters{}publicclassRepository{publicDataAccessResultUpdateItem(){returnnewNotFoundError();}}publicclassService{privatereadonlyRepository_repository;publicBusinessLogicResultUpdate(){varisValid=IsValid();if(!isValid){returnnewValidationError("the item is not valid");}varrepositoryResult=_repository.UpdateItem();// implicit conversion DataAccessResult to BusinessLogicResult when `UnionConverterTo` or `UnionConverterFrom` attribute is usedreturnrepositoryResult;// OR extension method when UnionConverter attribute is usedreturnrepositoryResult.Convert();}privateboolIsValid()=>thrownewNotImplementedException();}| Property | Default | Description |
|---|---|---|
| UnionTypesGenerator_ExcludeFromCodeCoverage | true | Add ExcludeFromCodeCoverage attribute when true |
<ProjectSdk="Microsoft.NET.Sdk">
<PropertyGroup>
<UnionTypesGenerator_ExcludeFromCodeCoverage>false</UnionTypesGenerator_ExcludeFromCodeCoverage>
</PropertyGroup>
</Project>