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>();
}
}
}
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