Introduction
In modern C# development, we often find ourselves lost in “If-Else” chains, nested blocks of null checks and error handling which can obscure the actual business logic. Inspired by Rust and functional programming, the Esox.SharpAndRusty library introduces a way to write cleaner, more resilient code, using Railroad Oriented Programming.
Here is how you can transform your C# code into a fluent, type-safe pipeline.
Getting Rid of “If-Chains”
Tradition code can often look like staircase of if statements. If any steps fails, you return early or throw an exception as the case may be. This “Happy Path” is buried under boilerplate. Here is an example:
public string GetUserDisplay(int id) {
var user = database.Find(id);
if (user != null) {
var profile = service.LoadProfile(user);
if (profile != null) {
return $"User: {profile.DisplayName}";
}
}
return "Unknown";
}
The Solution: Mapping and Binding
Using Map and `Bind`, we treat the operation as a flow.
Maptransforms the value inside aResultorOptionif it is successful.Bindis used when the transformation itself could fail, returning anotherResult.
public string GetUserDisplay(int id) =>
database.Find(id) // Returns Option<User>
.Map(user => service.LoadProfile(user))
.Match(
some: profile => $"User: {profile.DisplayName}",
none: () => "Unknown"
);
Railroad Oriented Programming
Try to think of your logic as a two-track railroad. One track is the Success Track, and the other is the Failure Track.
When you use Bind(), you are moving along the Success Track. If a function returns an Err, the switch is flipped and the execution jumps to the Failure Track, bypassing all subsequent logic until it reaches an error handler.
Let’s have a look at a concrete example:
public Result<Invoice, Error> ProcessOrder(OrderRequest request) =>
ValidateRequest(request)
.Bind(req => CalculateTax(req))
.Bind(order => SubmitToGateway(order))
.Map(receipt => GenerateInvoice(receipt));
If CalculateTax() fails, SubmitToGateway() and GenerateInvoice() are never even called. The error slides down the failure track to the caller.
Side Effects without Breaking the Chain
Sometimes you need to perform an action – like logging or analytics – that does not change the data. Normally, this requires breaking your fluent chain to write a line of code.
Inspect() and Tap() allow you to perform these side effects, while keeping the track moving:
Inspect: Run an action on the success value.InspectErr: Run an action on the success value.Tap: Executes actions on both success and failure, again without transforming the result.
A concrete example:
var result = ParseInt(input)
.Inspect(val => Logger.Info($"Processing number: {val}"))
.Bind(val => Divide(100, val))
.InspectErr(err => Logger.Error($"Failed to divide: {err}"));
The Unit Type: Handling Void Elegantly
In standard C#, void is a special case that does not play well with generics or functional chains. In SharpAndRusty, we use the Unit type to represent “no value” while still maintaining the Result context.
This allows you to chain methods, that do something, but return nothing, for example saving to a file or a database, without losing the ability to track errors. Here is an example:
public Result<Unit, Error> SaveToDatabase(User user) {
try {
db.Save(user);
return Result<Unit, Error>.Ok(Unit.Default);
} catch (Exception ex) {
return Result<Unit, Error>.Err(Error.FromException(ex));
}
}
var finalResult = ValidateUser(user)
.Bind(u => SaveToDatabase(u)) // Returns Result<Unit, Error>
.Map(_ => "User saved successfully!");
A small cheat sheet
| Method | Purpose | Scenario |
| Map | Transforms the value inside. | You have a successful result and want to change its type or value (e.g., User → DisplayName). |
| Bind | Chains a new failable operation. | The next step could also fail (e.g., User → LoadProfileFromDB). Prevents nested Results. |
| Match | Resolves the flow. | The final step: define what to return for both the Success and Failure paths. |
| Inspect | Executes a side effect. | You want to log a value or trigger an event without changing the data in the pipeline. |
| Tap | Runs actions on both tracks. | Ideal for logging “Processing started” or “Operation finished,” regardless of the outcome. |
| Unit | Represents “void.” | Used when a method performs an action (like saving to a DB) but doesn’t return a value. |
Conclusion
Transitioning from imperative “if-else” chains to Railroad Oriented Programming is more than just a syntactic change; it is a shift in how we reason about the reliability of our software. By treating our logic as a continuous flow rather than a series of hurdles, we make the “Happy Path” the primary focus of our code while ensuring that error are handled with the same level of type-safety as our data.
Why this matters?
Adopting libraries like Esox.SharpAndRusty provides three (3!) immediate benefits to virtually any C# codebase:
- Readability: The business logic reads like a story from top to bottom, unencumbered by repetitive null checks or try-catch blocks. Mind you, exceptions are still needed for non-recoverable errors, like a stack overflow or an out of memory exception.
- Safety: By replacing
nullwithOptionand exceptions withResult, you force the compiler to remind you of potential failure states, reducing runtime crashes. - Maintainability: Side effects are isolated using
Inspect()andTap(), and theUnittype ensures that even “void” operations remain first-class citizens in your pipeline.
Functional C# does not require you to abandon everything you know about the language. Instead, it invites you to use tools, like Mapping, Binding, and Chaining to write code that is as expressive as it is robust. By switching the tracks from nested statements to a linear pipeline, you spend less time debugging the “staircase of doom” and more time delivering value.




