1. What Is a Union Type?
Union Types are one of the important new/preview features on the C# side.
In short:
It is a type-safe structure that allows a variable to hold only one value from a specific and closed list of types.
For example:
public union Pet(Dog, Cat, Bird);
public record Dog(string Name);
public record Cat(string Name);
public record Bird(string Species);
Here, Pet means:
Pet can be a Dog,
or a Cat,
or a Bird.
Nothing else.
Usage:
Pet pet = new Dog("Max");
Here, the type of the pet variable is Pet, but the actual value it carries is Dog.
2. Why Do We Use Union Types?
Because in some cases, we want to show that a result, response, or model can be not just one type, but one of a limited number of different types.
Today in C#, we usually do things like this:
object result = GetResult();
or:
IResult result = GetResult();
But these can be weak solutions.
Because:
- Using
objectremoves type safety - Using an interface does not clearly express which concrete types can be returned
Union Type provides this:
public union PaymentResult(
PaymentSuccess,
PaymentFailed,
PaymentPending
);
Now this method can return only one of these three results.
3. Pattern Matching Usage
The strongest side of Union Type is using it together with pattern matching.
PaymentResult result = paymentService.Pay();
var message = result switch
{
PaymentSuccess success => $"Payment successful. Transaction No: {success.TransactionId}",
PaymentFailed failed => $"Payment failed. Error: {failed.ErrorMessage}",
PaymentPending pending => $"Payment pending. Estimated time: {pending.EstimatedCompletionTime}",
};
This approach:
- Reduces nullable property complexity
- Reduces boolean flag usage
- Provides a type-safe flow
- Improves code readability
4. What Problem Does Union Type Solve?
The most classic problem is this:
public class PaymentResponse
{
public bool IsSuccess { get; set; }
public string? TransactionId { get; set; }
public string? ErrorMessage { get; set; }
public bool IsPending { get; set; }
}
This model is dangerous.
Because invalid states can occur at the same time:
var response = new PaymentResponse
{
IsSuccess = true,
ErrorMessage = "Card limit is insufficient",
IsPending = true
};
This object produces a logically broken state.
Union Type prevents this.
public union PaymentResult(
PaymentSuccess,
PaymentFailed,
PaymentPending
);
Now a result cannot be both successful and failed at the same time.
5. API Response Scenario
public union GetUserResult(
UserFound,
UserNotFound,
UnauthorizedAccess
);
public record UserFound(int Id, string Name, string Email);
public record UserNotFound(int UserId);
public record UnauthorizedAccess(string Reason);
Service:
public GetUserResult GetUser(int id)
{
if (!currentUser.HasPermission)
return new UnauthorizedAccess("You are not authorized to view this user.");
var user = repository.GetById(id);
if (user is null)
return new UserNotFound(id);
return new UserFound(user.Id, user.Name, user.Email);
}
Controller:
var result = userService.GetUser(id);
return result switch
{
UserFound user => Ok(user),
UserNotFound notFound => NotFound(),
UnauthorizedAccess unauthorized => Forbid()
};
This approach clearly expresses which results the method can return.
6. Using Union Type Instead of Exceptions
Exception is not always the right solution.
Bad approach:
throw new Exception("User not found");
Better approach:
public union GetUserResult(
UserFound,
UserNotFound
);
Exception:
- Should be used for unexpected system errors
Union Type:
- Should be used for expected business results
7. Relation to Result Pattern
Many projects use this structure:
public class Result<T>
{
public bool IsSuccess { get; set; }
public T? Data { get; set; }
public string? Error { get; set; }
}
Union Type approach is stronger:
public union GetUserResult(
UserFound,
UserNotFound,
ValidationFailed
);
This structure provides a solution that is:
- More readable
- Safer
- More type-safe
- More maintainable
8. Difference from Enum
Enum only carries a state name.
public enum PaymentStatus
{
Success,
Failed,
Pending
}
Union Type can carry data specific to each state.
public record PaymentSuccess(string TransactionId);
public record PaymentFailed(string ErrorMessage);
public record PaymentPending(DateTime EstimatedCompletionTime);
So:
Enum: Carries the state name.
Union: Carries the state + data specific to that state.
9. Difference from Interface
Interface is open-ended.
public interface IPaymentResult { }
Any other type can implement this interface.
Union Type creates a closed set of types.
public union PaymentResult(
PaymentSuccess,
PaymentFailed,
PaymentPending
);
The compiler knows that:
PaymentResult can only be one of these three types.
10. When Should It Be Used?
Union Type is very suitable in these situations:
- When an operation can return multiple different results
- When boolean flag complexity exists
- When there are too many nullable properties
- When API response state management is needed
- When business rule results are being modeled
- When pattern matching will be used
11. When Should It Not Be Used?
It may be unnecessary in these situations:
- Simple entity models
- Methods returning a single type
- Simple DTO structures
- Service/repository classes
12. CQRS and MediatR Usage
Union Types are especially powerful on the CQRS side.
public union CreateProductResult(
ProductCreated,
ProductValidationFailed,
ProductAlreadyExists
);
This structure makes command handler results much cleaner.
13. Validation Scenario
public union CreateProductResult(
ProductCreated,
ProductValidationFailed,
ProductAlreadyExists
);
public record ProductValidationFailed(
IReadOnlyList<string> Errors
);
It allows validation states to be modeled much more cleanly.
14. Domain-Driven Design Usage
It is very powerful for state modeling on the DDD side.
public union CancelOrderResult(
OrderCancelled,
OrderAlreadyShipped,
OrderNotFound
);
This approach expresses business rules in a type-safe way.
15. Reduces Nullable Usage
Previously:
public UserDto? GetUser(int id)
{
}
What does null mean here?
- Does the user not exist?
- Is there no permission?
- Is validation invalid?
It is unclear.
Union Type makes this explicit.
16. Reduces Boolean Flag Usage
Bad approach:
public class LoginResponse
{
public bool IsSuccess { get; set; }
public bool RequiresTwoFactor { get; set; }
public bool IsLocked { get; set; }
}
Better approach:
public union LoginResult(
LoginSuccess,
TwoFactorRequired,
AccountLocked,
InvalidCredentials
);
17. Relation to the OneOf Library
Before native union support in C#, developers used libraries like OneOf.
public OneOf<UserDto, NotFound, ValidationError> GetUser(int id)
{
}
Union Type brings this approach to the language level.
18. Class, Record, or Union?
Simple decision rule:
Main object with behavior and identity -> class
Model that only carries data -> record
A result can be one of multiple different types -> union
19. Very Important Professional Rule
Union Type is especially powerful for state modeling.
Converting classes that can produce invalid states into union types often significantly improves code quality.
20. Conclusion
Union Type answers this question:
Which types can this value be?
Class answers this question:
Who is this object and what behavior does it have?
Record answers this question:
What values does this data consist of?
The clearest usage approach:
Entity, service, repository -> class
DTO, request, response -> record
Alternative result states -> union
When used in the right place in modern .NET projects, Union Types significantly improve code quality, readability, and type safety.