Optional Type
Optional<T> is a container which may or may not contain a value. Nullable<T> type can work with value types only. Optional<T>
data type can work with reference and value type both.
The following example demonstrates usage of this type:
using DotNext;
using DotNext.Collections.Generic;
var value = (int)i; //cast is supported
if(i.TryGet(out value))
{
//if i has value
}
if(i) //if i has value
{
value = i.Value;
}
value = i.OrThrow<ArgumentException>();
value = i | -1; //returns -1 if i has no value
value = i.ValueOrDefault; //returns default(int) if i has no value
value = i.OrInvoke(() => 10); //calls lambda if i has no value
Nullable<T>
and Optional<T>
are mutually convertible types with help of extension methods.
Null vs Undefined
Let's take a look at the following code:
using DotNext;
using System;
static Optional<string> FirstOrNone(string[] array)
=> array.Length > 0 ? array[0] : Optional<string>.None;
string[] array1 = { null };
Optional<string> first1 = FirstOrNone(array1);
string[] array2 = Array.Empty<string>();
Optional<string> first2 = FirstOrNone(array2);
HasValue
property of both values first1
and first2
is false. However, first1
actually represents the first element from the array. But the element is null. first2
is empty because the array is empty. This situation is equivalent to the following code:
using DotNext;
var first1 = new Optional<string>(null);
var first2 = Optional.None<string>(); //or default(Optional<string>)
Is it possible to distinguish the absence of value from null value? The answer is yes. There are two additional properties:
IsNull
returns true if underlying value is nullIsUndefined
returns true if underlying value is not defined
Now it's possible to apply additional logic to the optional result:
Optional<string> first = FirstOrNone(array);
switch (first)
{
case { HasValue: true }:
// value is present
string result = first.Value;
break;
case { IsNull: true }:
// value is null
break;
default:
// value is undefined
break;
}
Undefined Optional<T>
instance can be produced only by None
static property or by default value:
using DotNext;
Optional<string>.None; // IsUndefined == true, IsNull == false
new Optional<string>(); // IsUndefined == true, IsNull == false
default(Optional<string>); // IsUndefined == true, IsNull == false
new Optional<string>(null); // IsUndefined == false, IsNull == true
There is also convenient factory methods for creating optional values:
using DotNext;
Optional<string> nullValue = Optional.Null<string>(); // undefined
Optional<string> undefinedValue = Optional.None<string>(); // null
Optional<string> value = Optional.Some("Hello, world!"); // not null
Behavior of Equals
method and equality operators depend on underlying value and its presence. Undefined and null values behave similarily to JavaScript.
using DotNext;
Optional.Null<string>() == Optional.Null<string>(); // true
Optional.None<string>() == Optional.None<string>(); // true
Optional.None<string>() == Optional.Null<string>(); // false
The following table describes relationship between HasValue
, IsNull
and IsUndefined
properties for nullable type T
(reference type, Nullable<T>
or Optional<T>
):
HasValue | IsNull | IsUndefined |
---|---|---|
true | false | false |
false | true | false |
false | false | true |
The following table describes relationship between HasValue
, IsNull
and IsUndefined
properties for non-nullable type T
(all value types except Nullable<T>
and Optional<T>
):
HasValue | IsNull | IsUndefined |
---|---|---|
true | false | false |
false | false | true |
JSON serialization
Optional<T> is compatible with JSON serialization provided by System.Text.Json
namespace. You can enable support of this type using OptionalConverterFactory converter. The converter can be applied to the property or field directly using JsonConverterAttribute attribute or it can be registered via Converters property.
If the value of Optional<T>
is undefined, then property will be completely removed from serialized JSON document. This is useful when you want to describe Data Transfer Object for your resource in REST API that allows partial updates with PATCH HTTP method. To make this magic work, you also need to apply JsonIgnoreAttribute with condition equal to JsonIgnoreCondition.WhenWritingDefault. This condition tells JSON serializer to drop the field if its value is equal to default value. As we know, the default value of Optional<T>
is always undefined. The following example demonstrates how to design DTO with optional JSON fields:
using DotNext.Text.Json;
using System.Text.Json;
public sealed class JsonObject
{
[JsonConverter(typeof(OptionalConverterFactory))]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Optional<int> IntegerValue { get; set; } // optional field
[JsonConverter(typeof(OptionalConverterFactory))]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Optional<string> StringField { get; set; } // optional field
public bool BoolField{ get; set; } // required field which is always presented in JSON
}