For .NET 5 and later: Use the new MemberNotNullWhen
attribute.
The MemberNotNullWhenAttribute
type was introduced in .NET 5.0 and C# 9.0.
- (C# 9.0 also introduced
init
properties, which are useful for optional properties in immutable types without needing extraneous constructor parameters).
Use MemberNotNullWhen
by applying it to any Boolean
/bool
property where its true
/false
value asserts that certain fields and properties will be "notnull
" when that property is true
or false
(though you cannot yet directly assert that a property will be null
based on a property)
When a single bool
property indicates multiple properties are null
or notnull
then you can apply multiple [MemberNotNullWhen]
attributes - or you can use the params String[]
attribute ctor.
You might notice that your Status
property is not bool
- which means you will need to add a new property that adapts the Status
property to a bool
value for use with [MemberNotNullWhen]
.
...like so:
public class DecryptResponse
{
public DecryptStatus Status { get; init; }
[MemberNotNullWhen( returnValue: true , nameof(DecryptResponse.Stream))]
[MemberNotNullWhen( returnValue: false, nameof(DecryptResponse.ErrorMessage))]
private Boolean StatusIsOK => this.Status == DecryptStatus.Ok;
public Stream? Stream { get; init; }
public string? ErrorMessage { get; init; }
}
Of course there's a huge gaping hole with this approach: there's no way for the compiler to verify that Status
, Stream
, and ErrorMessage
are set correctly. There is nothing stopping your program from doing return new DecryptResponse();
without setting any properties. That means the object is in an invalid state.
You might think that's not a problem, but if you need to keep on adding or removing new properties to a class eventually you'll get careless and forget to set a required property you need and then your program explodes.
A better implementation of DecryptResponse
would use two separate constructors for the 2 mutually-exclusive valid states, like so:
public class DecryptResponse
{
public DecryptResponse( Stream stream )
{
this.Status = DecryptStatus.OK;
this.Stream = stream ?? throw new ArgumentNullException(nameof(stream));
this.ErrorMessage = null;
}
public DecryptResponse( DecryptStatus error, String errorMessage )
{
if( error == DecryptStatus.OK ) throw new ArgumentException( paramName: nameof(error), message: "Value cannot be 'OK'." );
this.Status = error;
this.Stream = null;
this.ErrorMessage = errorMessage ?? throw new ArgumentNullException(nameof(errorMessage ));
}
public DecryptStatus Status { get; }
[MemberNotNullWhen( returnValue: true , nameof(DecryptResponse.Stream))]
[MemberNotNullWhen( returnValue: false, nameof(DecryptResponse.ErrorMessage))]
private Boolean StatusIsOK => this.Status == DecryptStatus.Ok;
public Stream? Stream { get; }
public String? ErrorMessage { get; }
}
And then used like so:
DecryptResponse response = Decrypt( ... );
if( response.StatusIsOK )
{
DoSomethingWithStream( response.Stream ); // OK! The compiler "knows" that `response.Stream` is not `null` here.
}
else
{
ShowErrorMessage( response.ErrorMessage ); // ditto
}
Long-answer (for writing better classes in general):
The alternative to updating to .NET 5 + C# 9, and to avoid the invalid state problems described above is to use a better class design which makes invalid states unrepresentable.
I'm not a fan of mutable result objects (aka in-process DTOs) - i.e. those with get; set;
on every property) because without a primary-constructor there's no way to have hard assurances that an object instance will be initialized correctly.
(This is not to be confused with web-service DTOs, especially JSON DTOs, where there can be good reasons for making every property mutable, but that's another discussion)
If I were writing for an older .NET platform where MemberNotNullWhen
isn't available, then I'd design DecryptResponse
like below:
public abstract class DecryptResponse
{
public static implicit operator DecryptResponse( Stream okStream )
{
return new DecryptResponse.OK( okStream );
}
public static implicit operator DecryptResponse( ( DecryptStatus status, String errorMessage ) pair )
{
return new DecryptResponse.Failed( pair.status, pair.errorMessage );
}
private DecryptResponse( DecryptStatus status )
{
this.Status = status;
}
public DecryptStatus Status { get; }
public sealed class OK : DecryptResponse
{
public OK( Stream stream )
: base( DecryptStatus.OK )
{
this.Stream = stream ?? throw new ArgumentNullException(nameof(stream));
}
public Stream Stream { get; }
}
public sealed class Failed : DecryptResponse
{
public Failed ( DecryptStatus status, String errorMessage )
: base( status )
{
if( status == DecryptStatus.OK ) throw new ArgumentException( message: "Value cannot be " + nameof(DecryptStatus.OK) + "." );
this.ErrorMessage = errorMessage ?? throw new ArgumentNullException(nameof(errorMessage));
}
public String ErrorMessage { get; }
}
}
(From a CS-theoretical perspective, the above class is a union type).
The advantages of this design are numerous:
- The class design makes it clear there's only 2 possible "shapes" of the result data: "OK" or "Failed" - and each subclass owns its context-specific data members (
Stream
and ErrorMessage
respectively).
- The type heirarchy is closed (the base
abstract
type has a private
constructor) and its two child types are both sealed
, making it impossible for there to be a result other than OK
or Failed
.
- This is basically the same as an "enumerated (class) type", like Java's
enum
-classes. Whereas C# a enum
is more like a named constant and the compiler and language provides no guarantees that a C# enum
value is valid at runtime (e.g. you can always do MyEnum v = (MyEnum)123
even when 123
is not a defined value).
- Validation logic in the
OK
and Failed
constructors provide a guarantee that DecryptStatus.OK
always means the result type is DecryptResponse.OK
with a non-null
Stream
property. Similarly, if Status != DecryptStatus.OK
you have a DecryptResponse.Failed
object instead.
- The
implicit
operator definitions mean that methods that return a DecryptResponse
can directly return a Stream
or ValueTuple<DecryptStatus,String>
and the C# compiler will automagically perform the conversion for you.
Such a result-type is returned like so:
public DecryptResponse DecryptSomething()
{
Stream someStream = ... // do stuff
if( itWorked )
{
return someStream; // Returning a `Stream` invokes the DecryptResponse conversion operator method.
}
else
{
DecryptStatus errorStatus = ...
return ( errorStatus, "someErrorMessage" ); // ditto for `ValueTuple<DecryptStatus,String>`
}
}
or if you want to be explicit:
public DecryptResponse DecryptSomething()
{
Stream someStream = ... // do stuff
if( itWorked )
{
return new DecryptResponse.OK( someStream );
}
else
{
DecryptStatus errorStatus = ...
return new DecryptResponse.Failed( errorStatus, "someErrorMessage" );
}
}
And consumed like so:
DecryptResponse response = DecryptSomething();
if( response is DecryptResponse.OK ok )
{
using( ok.Stream )
{
// do stuff
}
}
else if( response is DecryptResponse.Failed fail )
{
Console.WriteLine( fail.ErrorMessage );
}
else throw new InvalidOperationException("This will never happen.");
(Unfortunately the C# compiler is not yet smart enough to recognize closed-type hierarchies, hence the need for the else throw new...
statement, but hopefully eventually that won't be necessary).
If you need support for serialization with JSON.net then you don't need to do anything, as JSON.NET supports serialization of these types just fine - but if you need to deserialize them then then you'll need a custom contract resolver, unfortunately - but writing a general-purpose contract-resolver for closed-types is straightforward and once you've written one you won't need to write another.
NotNullWhen
works only with method parameters, with property you can useNotNull
attirbute. Or convert it to method and use somethingNotNullIfNotNull
. You can find some ideas in this article Try out Nullable Reference Types – Sympetalous