DEV Community

Ahmed Fouad
Ahmed Fouad

Posted on • Originally published at Medium on

7 ways to improve code quality in c# using option type

Option is relatively new for imperative programming (c#, java, …) developers also it is now a primitive type in some of these multiparadigm languages like java it is not yet natively supported in C#.

I think the biggest misunderstanding regarding option type came from it is similarity was the nullable type. you will find a lot of online articles presenting Option type as a safer alternative to the nullable type or as a final solution to NullReferenceException while other material defends Nullable and say that option type is not the way to go as it simply replaces NullReferenceException with MissingValueException.

Here we will show you how to use Option Type Correctly and how it will improve your code dramatically and prevent some potential bugs.

Optional 4.0.0

1- Force you to handle the missing value case

Option type will force you to deal with the missing case when you actually need to get the value.

public string GetEmail(string id)
{
    var user= GetUser("id");
    return user.SomeNotNull()
         .Select(context=>context.Email)
         .ValueOr("Not found");
}

Here you have the GetUser Method which fitch the user that matches the provided id from the database and in case there no user with that Id it will return null.

This is one of the classics problems in all kinds of applications where the developer starts his code like that

var user= GetUser("id");
return user.Email;

and then when some NullReferenceExceptions Happens you can see something like this

var user= GetUser("id");
return user?.Email;

and then the NullRefrenceException will occur at the consumer of this method.

using the Option where you have to think about what to do when there is no value will have you to not to fall in the null propagation trap.

you can also you .ValueOrDefault() or .ValueOrFailure() if you want but it is not recommended unless you really wanna do that.

2- You define when it is none and when it is some

private static string GetEmail(string id)
        {
            var user = GetUser(id);
            var email = user.SomeNotNull()
                .Where(context=> IsEmail(context.Email))
                .Select(context => context.Email)
                .ValueOr("Not Found");
            return email;
        }

In some cases, you wanna ensure that the returned value satisfy some condition for example here if you have a case where some of the emails in the database are not incorrect format or not verified or anything else that you do not want to return.

you can write the previous code instead of something ugly like this

var user= GetUser("id");
return IsEmail(user?.Email)??NotFound;

In some cases, even this code could fail if the IsEmail do not accept null parameters so you could end up with something like

var user= GetUser("id");
return var user= GetUser("id");
return !string.IsNulOrEmpty(User?.Email)&&IsEmail(user?.Email)??NotFound;

wherein the option version it will be

private static string GetEmail(string id)
        {
            var user = GetUser(id);
            var email = user.SomeNotNull()
               .Select(context => context.Email)
               .Where(email=>!string.IsNullOrEmpty(email))
               .Where(email=> IsEmail(email))
               .ValueOr(string.Empty);
            return email;
        }

3- Execute Only When Value Exist

public static void NotifyUser(string id)
       {

        var user = GetUser(id)
                  .SomeNotNull()
                  .Where(context=>context.IsActive)
                  .Where(context=>!context.NoDeleted)
                  .Where(context=>context.AllowNotification);

        user
            .Where(context => !string.IsNullOrEmpty(context.Email))
            .Where(context => IsEmail(context.Email))            
            .Where(context=>context.SubscribedToEmailNotification)
            .MatchSome(SendNotificationEmail);

        user    
         .Where(context=>!string.IsNullOrEmpty(context.PhoneNumber))
         .Where(context=>context.SubscribedToPhoneNotification)
         .MatchSome(SendNotificationSMS);

          }
        private static void SendNotificationSMS(User context)
        {

        }
        private static void SendNotificationEmail(User context)
        {

        }

this code explains himself send an email notification if the email exists and send an SMS if the phone number exists.

vau user =GetUser(id);

if(user==null || !user.IsActive || !user.IsDeleted || !user.AllowNotification)
   return;

if(!string.IsNullOrEmpty(user.Email)
    &&IsEmail(user.Email)
    && user.SubscribedToEmailNotification)
            sendNotificationEmail(user);

if(!string.IsNullOrEmpty(user.PhoneNumber))
   &&user.SubscribedToSMSNotification)
    SendNotificationSMS(user);

I am sure it will take less time from you to understand the first code snipped with optional.

While both of them have the same number of conditions and checks but When we are using Where we are defining a rule so when we say “

var user = GetUser(id)
                  .SomeNotNull()
                  .Where(context=>context.IsActive)
                  .Where(context=>!context.NoDeleted)
                  .Where(context=>context.AllowNotification);

we are defining a contract no more than that with no actions so our user only exists if he satisfies the conditions and later on when needed we will do actions depend on he exists or not.

but with if conditions we are actually forcing an execution path with contains the condition and the action normally these are harder to understand and force upfront work which could not be needed.

I am sure you are familiar with this Joke

A programmer is going to the grocery store and his wife tells him, “Buy a gallon of milk, and if there are eggs, buy a dozen.” So the programmer goes, buys everything, and drives back to his house. Upon arrival, his wife angrily asks him, “Why did you get 13 gallons of milk?” The programmer says, “There were eggs!”

I am sure he never heard of functional programming and his code is full of if-else bulls***t that is why he used the if condition to build an action set while normal humans use the requirements to build rules like that.

var milikGallon=store.GetMilikGallon().SomeNotNull();
milikGallon.MatchSome(Buy);

var egges=store.GetEggs().SomeNotNull().Select(eggs=>eggs.Take(13));
egges.MatchSome(Buy);

4- Async Support

public static async Task NotifyUser(string id)
        {
            var user = GetUser(id)
                .SomeNotNull()
                .Where(context => context.IsActive)
                .Where(context => !context.NoDeleted)
                .Where(context => context.AllowNotification);

var sendEmailTask=
           user
            .Where(context =>context.SubscribedToEmailNotification)
            .Where(context => !string.IsNullOrEmpty(context.Email))
            .Where(context => IsEmail(context.Email))
            .Select(SendNotificationEmail)
            .ValueOr(Task.CompletedTask);

 var sendSmsTask=
           user
            .Where(context => context.SubscribedToSMSNotification)
            .Where(context   
                     =>!string.IsNullOrEmpty(context.PhoneNumber))
             .Select(SendNotificationSMS)
             .ValueOr(Task.CompletedTask);

      await Task.WhenAll(sendEmailTask, sendSmsTask);

        }

private static Task SendNotificationSMS(User context)
        {
            return Task.CompletedTask;

        }

private static Task SendNotificationEmail(User context)
        {
            return Task.CompletedTask;

        }

When it comes to async programming all we need is to select the task instead of invoking a method and if we are using the Task.CompletedTask as a replacement value for the none condition.

var sendEmailTask=
           user
            .Where(context =>context.SubscribedToEmailNotification)
            .Where(context => !string.IsNullOrEmpty(context.Email))
            .Where(context => IsEmail(context.Email))
            .Select(SendNotificationEmail)
            .ValueOr(Task.CompletedTask);

will return a completed Task (Task.CompletedTask) when the user does not exist or is not subscribed to email notification or does not have a valid email otherwise, it returns the SendNotificationEmail task.

and with Task.WhenAll we manage to run and wait for the tasks in parallel and here is the edge of functional programming when we changed the methods to async we were pushed too to run them in parallel but in the other alternative it will be

vau user =GetUser(id);

if(user==null || !user.IsActive || !user.IsDeleted || !user.AllowNotification)
   return;

if(!string.IsNullOrEmpty(user.Email)
    &&IsEmail(user.Email)
    && user.SubscribedToEmailNotification)
          await sendNotificationEmail(user);

if(!string.IsNullOrEmpty(user.PhoneNumber))
   &&user.SubscribedToSMSNotification)
   await SendNotificationSMS(user);

which of course can be refactored to run the 2 tasks in parallel but it is not part of the natural refactor path when we refactored sendNotificationEmail and SendNotificationSMS to be async.

5- Exception Handling

The get user method may throw an exception if there is a DB error or something like that, our previous code does not handle this in anyways while the normal try-catch block can work perfectly with our functional programming code Option type give us a better way for exception handling.

I will start by writing an extension method that wraps our GetUser method to return OptionWithException instead of T.

public static Option<T, Exception> ToOption<P,T>(this Func<P,T> context,P parameter)
        {

                try
                {
                    var result = context.Invoke(parameter);
                   return result.SomeNotNull(
() =>
(Exception) new NullReferenceException($"{context.Method.Name} returned null"));
                }
                catch (Exception e)
                {
                    return Option.None<T, Exception>(e);
                }
        }

and then we could use it like that

private static string GetEmail(string id)
  {
    var userFactory=new Func<string,User>(GetUser);

    var user = userFactory.ToOption(id);
    var email= 
     user.Filter(context =>!string.IsNullOrEmpty(context.Email,
                () =>new ConditionNotMatchedException("Emptyemail"))

          .Filter(context => IsEmail(context.Email), 
             () => new ConditionNotMatchedException("InvalidEmail"))

          .Select(context => context.Email);

     return email.ValueOr("Not found");
        }

This new code will be doing the same as the older one will return the email or not found.

but as you have noticed we replaced the Where(T=>bool) with Filter(T=>bool , ()=>Exception). That is because we will use the exception now as a reason when the value not existing so we can know why there is no email if we want and to do we must do an additional simple change

public struct EmailResult
   {
       public string Email { get; set; }
       public string Error { get; set; }

}
private static EmailResult GetEmail(string id)
   {
     var userFactory=new Func<string,User>(GetUser);

     var user = userFactory.ToOption(id);
     var emailResult = 
         user.Filter(context =>!string.IsNullOrEmpty(context.Email),   
              () => new ConditionNotMatchedException("Emptyemail"))

             .Filter(context => IsEmail(context.Email),
            () => new ConditionNotMatchedException("InvalidEmail"))

             .Select(context => context.Email)
             .MapException(ex=>new EmailResult(){Error =ex.Message})
             .Map(email=>new EmailResult(){Email = email});

     return emailResult.ValueOrException();
        }

As we need to know the reason in case of not finding the email so we will return a ResultObject in which we will map the result and the exception using

.MapException(ex=>new EmailResult(){Error =ex.Message})
.Map(email=>new EmailResult(){Email = email});

6- Integration With Other contracts Frameworks like Shoudly

Shouldly 3.0.2

Shoudly is an assertion framework and normally assertion frameworks are used for testing but here I would use it for filtering my value

private static EmailResult GetEmail(string id)
 {
   var userFactory=new Func<string,User>(GetUser);

   var user = userFactory.ToOption(id);
   var emailResult = user
         .Filter(context=>context.Email.ShouldNotBeNullOrEmpty())            
         .Filter(context=>context.Email.ShouldMatch(Patterns.Email))
         .Select(context => context.Email)
         .MapException(ex=>new EmailResult(){Error = ex})
         .Map(email=>new EmailResult(){Email = email});

   return emailResult.ValueOrException();
 }

but it requires me to implement a simple overload for filter

public static Option<T, Exception> Filter< T>(this Option<T, Exception> context,Action<T> condition)
        {
            try
            {
                if (!context.HasValue)
                    return context;
                condition.Invoke(context.ValueOrFailure());
                 return context;
            }
            catch (Exception e)
            {
                return Option.None<T, Exception>(e);
            }
        }

I believe this syntax is the most readable one you can achieve

var emailResult = user
         .Filter(context=>context.Email.ShouldNotBeNullOrEmpty())            
         .Filter(context=>context.Email.ShouldMatch(Patterns.Email))
         .Select(context => context.Email)

7- It can be your entry to functional Programming

Here I have to tell you that option type is one of the building blocks of functional programming and by using it as we mentioned in the previous 6 sections you are actually writing functional Programming cold.

So welcome on board, now I would suggest that you follow me on twitter and medium for more articles.

Ahmed Fouad

And you can buy me one coffee it will help me write more articles

Buy AhmedFouad a Coffee. ko-fi.com/ahmedfouad

And I would really recommend you to read Functional Programming in C#: How to write better C# code as it is one of few books that introduce functional programming thinking to c# developers read it and you will notice dramatic improvements in how you think and a strong boost in your code quality.

Top comments (0)