The Problem with Empty Collections
Every C# developer has encountered this scenario: a method returns a collection that should contain data, but at runtime, it's empty. The result? InvalidOperationException, null reference errors, or subtle bugs that only surface in production.
public async Task<decimal> CalculateAverageOrderValueAsync(int customerId)
{
var orders = await _repository.GetOrdersAsync(customerId);
return orders.Average(o => o.Total); // Throws if orders is empty
}
The traditional solution involves defensive programming:
if (orders.Any())
{
return orders.Average(o => o.Total);
}
return 0m; // What does zero mean here? No orders? An error?
This approach has significant drawbacks:
- Silent failures: Returning default values masks underlying issues
- Code duplication: Empty checks scattered throughout the codebase
- Semantic ambiguity: Is zero a valid average or an error state?
A Type-System Solution
What if we could express "this collection must have at least one element" directly in our type signatures? This is precisely what NonEmptyList provides.
public async Task<decimal> CalculateAverageOrderValueAsync(
NonEmptyList<Order> orders) // Guaranteed non-empty at compile time
{
return orders.Average(o => o.Total); // Always safe
}
The compiler now enforces our business rule. Callers must provide a non-empty collection, shifting validation from runtime to compile time.
Installation
dotnet add package Masterly.NonEmptyList
Compatible with .NET 6.0 and .NET 8.0.
Core API Design
NonEmptyList draws inspiration from functional programming languages, particularly Scala's List type. It provides three fundamental properties for traversing collections:
| Property | Type | Description |
|---|---|---|
Head |
T |
First element (guaranteed to exist) |
Tail |
NonEmptyList<T>? |
Remaining elements, or null if single |
Init |
NonEmptyList<T>? |
All elements except last, or null if single |
var numbers = new NonEmptyList<int>(1, 2, 3, 4, 5);
var head = numbers.Head; // 1
var tail = numbers.Tail; // NonEmptyList<int> { 2, 3, 4, 5 }
var init = numbers.Init; // NonEmptyList<int> { 1, 2, 3, 4 }
var last = numbers.Last; // 5
Deconstruction Support
C# pattern matching integrates naturally:
var (first, rest) = numbers;
// first = 1, rest = NonEmptyList<int> { 2, 3, 4, 5 }
Functional Transformations
Map and FlatMap
Transform elements while preserving the non-empty guarantee:
var orders = new NonEmptyList<Order>(order1, order2, order3);
// Transform to DTOs
NonEmptyList<OrderDto> dtos = orders.Map(o => new OrderDto(o));
// Flatten nested collections
NonEmptyList<LineItem> allItems = orders.FlatMap(o => o.Items);
Reduce: Aggregation Without Edge Cases
Traditional Aggregate requires handling empty collections:
// Standard LINQ - throws on empty collection
decimal total = orders.Aggregate(0m, (sum, o) => sum + o.Total);
With NonEmptyList, Reduce requires no seed value because the collection is guaranteed non-empty:
// NonEmptyList - always safe, no seed required
decimal total = orders.Reduce((sum, o) => sum + o.Total);
This eliminates an entire category of runtime errors.
Fold for Type Transformations
When the result type differs from the element type:
var summary = orders.Fold(
new OrderSummary(),
(summary, order) => summary.AddOrder(order)
);
Partition and GroupBy
Separate collections based on predicates:
var (highValue, standard) = orders.Partition(o => o.Total > 1000m);
var byStatus = orders.GroupByNonEmpty(o => o.Status);
// Dictionary<OrderStatus, NonEmptyList<Order>>
Pattern Matching
Handle single-element and multi-element cases explicitly:
string ProcessOrders(NonEmptyList<Order> orders) =>
orders.Match(
single: order =>
$"Processing single order: {order.Id}",
multiple: (first, remaining) =>
$"Processing batch: {first.Id} and {remaining.Count} more"
);
This pattern is particularly useful for recursive algorithms and batch processing logic.
Asynchronous Operations
Modern applications require async support. NonEmptyList provides both sequential and parallel async operations:
Sequential Processing
var enrichedOrders = await orders.MapAsync(
async order => await _enrichmentService.EnrichAsync(order)
);
Parallel Processing with Controlled Concurrency
var results = await orders.MapParallelAsync(
async order => await _processor.ProcessAsync(order),
maxDegreeOfParallelism: 4
);
Async Aggregation
var totalRevenue = await orders.FoldAsync(
0m,
async (sum, order) => sum + await _calculator.CalculateRevenueAsync(order)
);
Immutable Variant
For concurrent scenarios or when immutability is required:
var immutable = new ImmutableNonEmptyList<int>(1, 2, 3);
// All operations return new instances
var appended = immutable.Append(4); // New list: [1, 2, 3, 4]
var prepended = immutable.Prepend(0); // New list: [0, 1, 2, 3]
// Original unchanged
Console.WriteLine(immutable.Count); // Still 3
Convert between variants as needed:
var immutable = mutableList.ToImmutable();
var mutable = immutableList.ToMutable();
Serialization
JSON Support
Built-in System.Text.Json converters handle serialization automatically:
var orders = new NonEmptyList<Order>(order1, order2);
string json = JsonSerializer.Serialize(orders);
var restored = JsonSerializer.Deserialize<NonEmptyList<Order>>(json);
Deserialization enforces the non-empty constraint:
// Throws JsonException with descriptive message
JsonSerializer.Deserialize<NonEmptyList<int>>("[]");
Entity Framework Core Integration
A dedicated package provides database persistence support:
dotnet add package Masterly.NonEmptyList.EntityFrameworkCore
Configure properties and relationships:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Store as JSON column
modelBuilder.Entity<Product>()
.Property(p => p.Tags)
.HasNonEmptyListConversion();
// Configure relationships with non-empty constraint
modelBuilder.Entity<Order>()
.HasNonEmptyManyWithOne(
o => o.LineItems,
li => li.Order,
li => li.OrderId
);
}
Practical Example: Order Processing Pipeline
public class OrderProcessingService
{
public async Task<ProcessingResult> ProcessBatchAsync(
NonEmptyList<Order> orders,
CancellationToken cancellationToken = default)
{
// Validate all orders in parallel
var validationResults = await orders.MapParallelAsync(
order => _validator.ValidateAsync(order, cancellationToken),
maxDegreeOfParallelism: 8
);
// Partition by validation result
var (valid, invalid) = orders
.Zip(validationResults)
.Partition(pair => pair.Item2.IsValid);
if (invalid.Any())
{
_logger.LogWarning("Found {Count} invalid orders", invalid.Count);
}
// Process valid orders
var processed = valid switch
{
null => new ProcessingResult(0, 0m),
var validOrders => await ProcessValidOrdersAsync(
validOrders.Map(pair => pair.Item1),
cancellationToken
)
};
return processed;
}
private async Task<ProcessingResult> ProcessValidOrdersAsync(
NonEmptyList<Order> orders,
CancellationToken cancellationToken)
{
// Calculate totals - Reduce is safe, no empty check needed
var totalRevenue = orders.Reduce((sum, o) => sum + o.Total);
// Process with pattern matching
await orders.Match(
single: async order =>
await _processor.ProcessSingleAsync(order, cancellationToken),
multiple: async (priority, remaining) =>
{
await _processor.ProcessPriorityAsync(priority, cancellationToken);
await remaining.ForEachAsync(
order => _processor.ProcessAsync(order, cancellationToken)
);
}
);
return new ProcessingResult(orders.Count, totalRevenue);
}
}
When to Use NonEmptyList
Ideal use cases:
- Domain models where empty collections are invalid (orders must have line items)
- API contracts that require at least one element
- Aggregation operations where empty input is an error
- Functional programming patterns (head/tail recursion)
Consider alternatives when:
- Empty collections are valid in your domain
- You need to gradually build collections from empty state
- Performance is critical and allocation overhead matters
Resources
NonEmptyList brings a well-established functional programming pattern to C#, enabling developers to express domain constraints directly in the type system. By shifting validation from runtime to compile time, we eliminate entire categories of bugs while making code more expressive and self-documenting.
If this library helps your project, consider giving it a star on GitHub.
Top comments (0)