-
Notifications
You must be signed in to change notification settings - Fork 0
Chaining With Map And Bind
One of the best parts of Resultron is that you can connect many steps into one clean "pipeline." If any step fails, the whole chain stops automatically, and the error flows to the end. You don't need if checks after every single line.
var result = Result.Success()
.Bind(ValidateName)
.Bind(ValidateEmail)
.Bind(SaveUser)
.Match(
onSuccess: () => "User created successfully.",
onFailure: error => $"Failed: {error.Description}"
);What happens here, step by step:
-
Result.Success()starts the chain in a successful state. -
.Bind(ValidateName)runsValidateName. If it fails, the chain stops here and skips the remaining steps. -
.Bind(ValidateEmail)only runs if step 2 succeeded. -
.Bind(SaveUser)only runs if step 3 succeeded. -
.Match(...)turns the final result (success or failure) into a plain string message.
This means you get one place (the Match at the end) to handle both success and failure, instead of writing if (result.IsFailure) return ... after every step.
This example is adapted from the project's sample app. It validates a name and email, checks for duplicates, and then saves the user — all in one chain:
public Result<User> Create(string name, string email)
{
return ValidateName(name)
.Bind(() => ValidateEmail(email))
.Bind(() => CheckEmailUniqueness(email))
.Bind(() => SaveUser(name, email));
}
private static Result ValidateName(string name)
{
return name.Length >= 2
? Result.Success()
: Result.Failure(UserErrors.InvalidName);
}
private static Result ValidateEmail(string email)
{
return email.Contains('@')
? Result.Success()
: Result.Failure(UserErrors.InvalidEmail);
}
private Result CheckEmailUniqueness(string email)
{
return _repository.EmailExists(email)
? Result.Failure(UserErrors.EmailAlreadyExists(email))
: Result.Success();
}
private Result<User> SaveUser(string name, string email)
{
var user = new User(Guid.NewGuid(), name, email, DateTime.UtcNow);
return _repository.Add(user);
}If name is too short, ValidateName fails, and ValidateEmail, CheckEmailUniqueness, and SaveUser are never called. The caller of Create simply gets back a failed Result<User> with a clear error, like "User.InvalidName".
Here is an example that mixes both Map and Bind to update a user's name:
public Result<User> UpdateName(Guid id, string newName)
{
return ValidateName(newName)
.Bind(() => _repository.GetById(id)) // Result -> Result<User>
.Map(user => new User(user.Id, newName, user.Email, user.CreatedAt)) // Result<User> -> Result<User>
.Bind(_repository.Update); // Result<User> -> Result<User>
}-
.Bind(() => _repository.GetById(id)): fetches the user. This step can fail (user not found), so it usesBind. -
.Map(user => ...): creates an updated copy of the user with the new name. This step cannot fail by itself, it's just a plain transformation, so it usesMap. -
.Bind(_repository.Update): saves the updated user. Saving can fail, so it usesBindagain.
Without chaining, the same code might look like this:
var nameCheck = ValidateName(newName);
if (nameCheck.IsFailure)
return Result<User>.Failure(nameCheck.Error);
var userResult = _repository.GetById(id);
if (userResult.IsFailure)
return Result<User>.Failure(userResult.Error);
var updatedUser = new User(userResult.Value.Id, newName, userResult.Value.Email, userResult.Value.CreatedAt);
var saveResult = _repository.Update(updatedUser);
if (saveResult.IsFailure)
return Result<User>.Failure(saveResult.Error);
return saveResult;The chained version does the exact same thing, but is much shorter and easier to read.
Next: see Handling Exceptions with Try to learn how Resultron wraps exceptions automatically.