Dereference of a possibly null reference despite checking that the value is not null in the Linq Where() call

766 Views Asked by At

I have prepared a simple C# Fiddle, which calls an OSRM map matching service URL and parses the JSON response.

My problem is, that there is a line producing the warning CS8602: Dereference of a possibly null reference:

if (osrmResponse?.code == "Ok" && osrmResponse.matchings != null)
{
    return osrmResponse.matchings
        .Where(matching => matching != null)
        .SelectMany(matching => matching.legs ?? Enumerable.Empty<Leg>())
        .Where(leg => leg != null && leg.annotation != null && leg.annotation.nodes != null)
        // How to fix dereference of a possibly null value in the next line?
        .SelectMany(leg => leg.annotation.nodes ?? Enumerable.Empty<long>())
        // eliminate duplicate node ids by converting to an ISet
        .ToHashSet()
        .ToList();
}

I do not understand, why does the compiler consider the leg or leg.annotaion to be null if I check for that in .Where call just before the problematic line?

Is the reason maybe the signature of the .Where() call and how to solve it then?

The complete test case is copied below:

namespace OsrmMapMatch
{
    public class OsrmResponse
    {
        public string? code { get; set; }
        public Matching[]? matchings { get; set; }
    }

    public class Matching
    {
        public Leg[]? legs { get; set; }
    }
    public class Leg
    {
        public Annotation? annotation { get; set; }
    }

    public class Annotation
    {
        public long[]? nodes { get; set; }
    }

    internal class Program
    {
        const string OsrmUri = "?overview=simplified&generate_hints=false&skip_waypoints=true&gaps=ignore&annotations=nodes&geometries=geojson&radiuses=";

        readonly static (double lng, double lat)[] Locations = 
        {
            (10.757938, 52.437444),
            (10.764379, 52.437314),
            (10.770562, 52.439067),
            (10.773268, 52.436633),
        };

        static async Task Main(string[] args)
        {
            const string HttpClientMapMatch = "HttpClientMapMatch";

            ServiceProvider serviceProvider = new ServiceCollection()
                .AddHttpClient(HttpClientMapMatch, httpClient =>
                {
                    httpClient.BaseAddress = new Uri("https://router.project-osrm.org/match/v1/driving/");
                }).Services.BuildServiceProvider();

            IHttpClientFactory? httpClientFactory = serviceProvider.GetService<IHttpClientFactory>();
            HttpClient? httpClient = httpClientFactory?.CreateClient(HttpClientMapMatch);
            if (httpClient == null)
            {
                Console.WriteLine("Error httpClient is null");
                return;
            }

            IEnumerable<long> nodes = await GetOsmNodesAsync(httpClient, Locations);
            Console.WriteLine($"Map matched OSM node ids: {JsonSerializer.Serialize(nodes.OrderBy(node => node))}\n");
        }

        private static async Task<IEnumerable<long>> GetOsmNodesAsync(HttpClient httpClient, IEnumerable<(double lng, double lat)> locations)
        {
            IEnumerable<string> lngLats = locations
                .Select(location => $"{location.lng:F6},{location.lat:F6}")
                .ToList();

            IEnumerable<int> radiuses = locations
                .Select(location => 50)
                .ToList();

            string requestUri = string.Join(";", lngLats) + OsrmUri + string.Join(";", radiuses);
            OsrmResponse? osrmResponse = await httpClient.GetFromJsonAsync<OsrmResponse>(requestUri);
            if (osrmResponse?.code == "Ok" && osrmResponse.matchings != null)
            {
                return osrmResponse.matchings
                    .Where(matching => matching != null)
                    .SelectMany(matching => matching.legs ?? Enumerable.Empty<Leg>())
                    .Where(leg => leg != null && leg.annotation != null && leg.annotation.nodes != null)
                    // How to fix dereference of a possibly null value in the next line?
                    .SelectMany(leg => leg.annotation.nodes ?? Enumerable.Empty<long>())
                    // eliminate duplicate node ids by converting to an ISet
                    .ToHashSet()
                    .ToList();
            }

            return Enumerable.Empty<long>();
        }
    }
}
2

There are 2 best solutions below

0
Eric J. On BEST ANSWER

Current linters cannot analyze every possible execution path and so this linter is detecting a false positive possible null dereference.

Your RI (real intelligence) is superior in this case.

The language provides a mechanism, the null forgiving operator, to avoid this linter warning.

Without the null-forgiving operator, the compiler generates the following warning for the preceding code: Warning CS8625: Cannot convert null literal to non-nullable reference type. By using the null-forgiving operator, you inform the compiler that passing null is expected and shouldn't be warned about.

Application here:

leg!.annotation!.nodes
0
Ben Voigt On

Simply, the language rules know nothing about the relationship between the lambda condition passed to Where and the data passed to the lambda in SelectMany. It only knows the data type of the lambda argument, and that data type is still class Leg which has an Annotation? member.

Even though all the null values have been removed/resolved, that fact isn't known to the compiler null checker.