DEV Community

Ziv Ben-Or
Ziv Ben-Or

Posted on

Different ways to return data in C#

Alt Text

When we write a function with complex logic or when the function calls for a third party resource - aside from the result, we need to be able to receive an indication of whether the function succeeded or failed.

1️⃣ Try/Catch

A common way is the try/catch approach. The critical logic is written in a try block, and if something is wrong you send the appropriate Exception (it is recommended to create your own custom Exception for specific scenarios) and it is handled in the catch block:

[HttpPost]
[Route("customers" )]
public IHttpActionResult CreateCustomer([FromBody] CreateUserRequest request)
{
    try
    {
        var newCustomer = Customer.From(request);
        var result = _customerService.CreateCustomer(newCustomer);
        return Ok(newCustomer);
    }
    catch (CustomerExistsException)
    {
        return BadRequest(Errors.IllegalEmail);
    }
    catch (IllegalEmailException)
    {
        return BadRequest(Errors.CustomerExists);
    }
    catch (Exception e)
    {
        return InternalServerError(e);
    }
}
Enter fullscreen mode Exit fullscreen mode

I don’t like this approach for several reasons:

  1. Exception are meant to handle... Exceptions and not business logic and error handling.
  2. Using Exceptions for business logic is, in fact, goto pattern, which is a big no-no pattern ❌
  3. Exception impact performance 😕

2️⃣ TryParse

TryParse is a .Net implementation for type conversion.

Basically, the function returns a boolean indication if the conversion has succeeded, and gets an out parameter to be converted if it succeeds.

You can implement your own TryParse (or other operations that your function needs to do: TryExecute, TryUpdate etc…):

[HttpPost]
[Route("customers" )]
public IHttpActionResult CreateCustomer([FromBody] CreateUserRequest request)
{
    var newCustomer = Customer.From(request);
    var succeed = _customerService.TryCreateCustomer(newCustomer, out var customer);

    return succeed
        ? Ok(customer)
        : InternalServerError(new Exception(Errors.InternalError));
}
Enter fullscreen mode Exit fullscreen mode

This approach is fine, but there are developers that don’t like the idea of sending an out parameter to the function (it contradicts the functional programming principles).


3️⃣ Tuple

With the Tuple type you can achieve the same idea without sending the out parameter. The only thing you need to do is to return a Tuple< bool, T>, where the bool is an indication if the operation has succeeded and the T is the type of the desired value:

[HttpPost]
[Route("customers" )]
public IHttpActionResult CreateCustomer([FromBody] CreateUserRequest request)
{
    var newCustomer = Customer.From(request);
    var (succeed, customer) = _customerService.CreateCustomer(newCustomer);

    return succeed
        ? Ok(customer)
        : InternalServerError(new Exception(Errors.InternalError));
}
Enter fullscreen mode Exit fullscreen mode

The problem with the Tuple and the TryParse way, is that while you have a boolean indication if the logic has failed, but you don’t know why.


4️⃣ Result

Another approach is the Result. There are many implementations of this concept such as in the CSharpFunctionalExtensions nuget, or in the LanguageExt nuget, and I even wrote a post about this approach a few years ago: Using Result concept.

This idea is like the Tuple but in a dedicated class/struct that has a Value parameter for the result, Error parameter – a message text if failed, and boolean parameters that indicates if success or failure:

public Result< Customer> CreateCustomer(Customer customer)
{
    if (!Common.IsValidEmail(customer))         return Result.Fail(Errors.IllegalEmail);
    if (_users.ContainsKey(customer.IdNumber))  return Result.Fail(Errors.CustomerExists);

    _users.Add(customer.IdNumber, customer);
    return Result.Ok(customer);
}
Enter fullscreen mode Exit fullscreen mode
[HttpPost]
[Route("customers" )]
public IHttpActionResult CreateCustomer([FromBody] CreateUserRequest request)
{
    var newCustomer = Customer.From(request);
    var result = _customerService.CreateCustomer(newCustomer);

    return result.IsSuccess
        ? Ok(result.Value)
        : BadRequest(result.Error);
}
Enter fullscreen mode Exit fullscreen mode

5️⃣ OneOf

OneOf is a cool 😎 nugget I was introduced to in the past year. OneOf solves the problem of all the other patterns we saw, when it is not enough to know that the function/process failed and you also want to know why. For example, in controller in a Web API service you want to know what happened: if it is our failure – to return an internal server error (500), or if it is a data problem to return a Bad Request (400), Also, and also if it is a Bad Request, we want more details about what part of the request is bad.

OneOf is a struct that enables you to declare, with generics, how many and what types we expect to get back:

public OneOf<Customer, IllegalEmail, CustomerExists> CreateCustomer(Customer customer)
{
   if (!Common.IsValidEmail(customer))        return new IllegalEmail();
   if (_users.ContainsKey(customer.IdNumber)) return new CustomerExists();

   _users.Add(customer.IdNumber, customer);

   return customer;
}
Enter fullscreen mode Exit fullscreen mode

After you get back the result, you need do Match/switch-case to handle each of the type scenarios that you can get back:

[HttpPost]
[Route("customers")]
public IHttpActionResult CreateCustomer([FromBody] CreateUserRequest request)
{
   var newCustomer = Customer.From(request);
   var result = _customerService.CreateCustomer(newCustomer);

   return result.Match<IHttpActionResult>(
       customer       => Ok(customer),
       illegalEmail   => BadRequest(Errors.IllegalEmail),
       customerExists => BadRequest(Errors.CustomerExists)
   );
}
Enter fullscreen mode Exit fullscreen mode

6️⃣ Polymorphism

Another way is to use the C# language support of Polymorphism. We can create an empty interface, e.g. IResult, and any type we want to return inherits from this interface:

public interface IResult { }
public struct IllegalEmail: IResult { }
public struct CustomerExists: IResult { }
public struct Customer : IResult { /**...**/ }
Enter fullscreen mode Exit fullscreen mode
public IResult CreateCustomer(Customer customer)
{
   if (!Common.IsValidEmail(customer))        return new IllegalEmail();
   if (_users.ContainsKey(customer.IdNumber)) return new CustomerExists();

   _users.Add(customer.IdNumber, customer);
   return customer;
}
Enter fullscreen mode Exit fullscreen mode

Now you can handle any type with switch statement/expression:

[HttpPost]
[Route("customers")]
public IHttpActionResult CreateCustomer([FromBody] CreateUserRequest request)
{
   var newCustomer = Customer.From(request);
   var result = _customerService.CreateCustomer(newCustomer);
   return result switch
   {
       Customer       => Ok(result),
       IllegalEmail   => BadRequest(Errors.IllegalEmail),
       CustomerExists => BadRequest(Errors.CustomerExists)
   };
}
Enter fullscreen mode Exit fullscreen mode

For primitive types (int, bool, float...) or 3rd-party classes we can use a struct wrapper:

public struct ResultValue<T> : IResult
{
   private ResultValue(T value) => Value = value;
   public T Value { get; set; }
   public static ResultValue<T> From(T value) => new ResultValue<T>(value);
   public static implicit operator T(ResultValue<T> result) => result.Value;
}
Enter fullscreen mode Exit fullscreen mode
public IResult UpdateCustomerId(int number)
{
   if (!Common.IsValidIdNumber(number)) return new IllegalIdNumber();
   if (!Common.IsValidIdNumberLength(number)) return new IllegalIdNumberLength();

   Customer.UpdateIdNumber(number);

   return ResultValue<int>.From(number);

}
Enter fullscreen mode Exit fullscreen mode

Summary

In this article we have seen several ways to return data​: Try/Catch, TryParse, Tuple, Result, OneOf and Polymorphism. Some solutions are better and some are less good, but there isn’t only one right way. You must not be a fossil - always search for the best solution for your given code 💪

Thanks,

Ziv Ben-Or

ZivBenOr@Gmail.com

+972545882011

Check my article in Linkedin: Different ways to return data in C#

Top comments (0)