Is there an equivalent NotNullWhen C# pattern for async tuple return?
Asked Answered
D

3

9

In C# with nullable types it is possible to implement a 'TryGet' that is smart about null checking, e.g.,

bool TryGetById(int id, [NotNullWhen(returnValue: true)] out MyThing? myThing)

which allows the caller to skip null checking on the out var myThing.

Unfortunately, Async does not allow out parameters, and the pattern of using a Tuple return does not allow for this smart NotNull checking (at least, not so far as I have found). Is there an alternative?

Is there any way to use a 'NotNullWhen' equivalent on an async Tuple return type e.g.,

Task<(bool Ok, [NotNullWhen(returnValue: true)] MyThing? MyThing)> TryGetById(int id)
Displace answered 29/6, 2021 at 2:24 Comment(0)
F
8

There isn't an implementation of this for value Tuples (yet). However! From C#9 You could roll-your-own struct (or even better C#10 record struct) with MemberNotNullWhen.

MemberNotNullWhenAttribute Class

Specifies that the method or property will ensure that the listed field and property members have non-null values when returning with the specified return value condition.

Note : You will need to reimplement all the tupley goodness like equality etc.

Worlds most contrived example ensues

#nullable enable

public readonly struct Test
{
   [MemberNotNullWhen(returnValue: true, member: nameof(Value))]
   public bool IsGood => Value != null;

   public string? Value { get; init; }
}

public static Task<Test> TryGetAsync()
   => Task.FromResult(new Test {Value = "bob"});

public static void TestMethod(string bob)
   => Console.WriteLine(bob);

Usage

var result = await TryGetAsync();
if (result.IsGood)
   TestMethod(result.Value); // <= no warning
Feminize answered 29/6, 2021 at 2:56 Comment(2)
That's a good solution, thank you! I used your example and expanded to make 'Test' a generic 'Result<T>', which seems to work well. The only downside is losing the named member - now I have 'Value' instead of 'MyThing', but it's a reasonable compromise.Displace
@Displace nice. I'm guessing you will be able to attribute tuples sometime in the future, they are super convenient but missing features as you can see. Anyway good luck, glad it helpedFeminize
A
1

If you own the implementation of MyThing, you can do this

public class MyThing
{
    public static readonly MyThing Empty = new();
 // all other properties etc
}

then make your method signature that it can never return null Mything

public async Task<(bool Ok, MyThing MyThing)> TryGetById(int id)
{
    var something = await FindSomething(id);

    return (something == null) 
           ? (false, MyThing.Empty);
           : new (true, something);
}

var result = await TryGetById(420);
if(result.Ok) // whatever

Amboise answered 29/6, 2021 at 3:17 Comment(0)
B
0

I found another solution, inspired from @TheGeneral's answer that I think results in cleaner operation because you can still use the is keyword to pattern match a tuple.

The generic class

public class AsyncParseResult<T> : Tuple<bool, T?>
{
    public AsyncParseResult(bool item1, T? item2) : base(item1, item2) { }

    [MemberNotNullWhen(returnValue: true, member: nameof(Item2))]
    public new bool Item1 { get { return base.Item1; } }

    public new T? Item2 { get { return base.Item2; } }
}

Example Usage

public class Thing
{

}

public static class Test
{
    public static async Task<AsyncParseResult<Thing>> TryParseThingAsync(string value)
    {
        if(value == "thing")//you would replace this with your real parsing logic that uses async
        {
            return new AsyncParseResult<Thing>(true, new Thing());
        }
        else
        {
            return new AsyncParseResult<Thing>(false, null);
        }
    }

    public static async Task ExampleUsage()
    {
        if(await TryParseThingAsync("thing") is (true, var thing))
        {
            //thing will not be null here
        }
    }
}
Bales answered 8/8, 2023 at 19:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.