TurtleRoute.Azure
0.18.0
dotnet add package TurtleRoute.Azure --version 0.18.0
NuGet\Install-Package TurtleRoute.Azure -Version 0.18.0
<PackageReference Include="TurtleRoute.Azure" Version="0.18.0" />
<PackageVersion Include="TurtleRoute.Azure" Version="0.18.0" />
<PackageReference Include="TurtleRoute.Azure" />
paket add TurtleRoute.Azure --version 0.18.0
#r "nuget: TurtleRoute.Azure, 0.18.0"
#:package TurtleRoute.Azure@0.18.0
#addin nuget:?package=TurtleRoute.Azure&version=0.18.0
#tool nuget:?package=TurtleRoute.Azure&version=0.18.0
Turtle Route
This is a project that supports the map functionalities in Dime.Scheduler. Most notably, the geocoding service is essential to the performance of the map as it eliminates the need for ad-hoc geocoding.
Installation
Turtle Route is split into a core package with the contracts and the geocoding pipeline, and one package per vendor:
dotnet add package TurtleRoute # IGeocoder, IRoutePlanner, IRouteMatrix, GeocodingPipeline
dotnet add package TurtleRoute.OpenCage # OpenCage geocoding
dotnet add package TurtleRoute.Azure # Azure Maps geocoding and routing
dotnet add package TurtleRoute.Here # HERE routing
Usage
Register the providers in the calling project. Geocoders are tried in the order they are registered: when a layer throws (outage, invalid key, quota exceeded), finds nothing, or returns a result below the minimum confidence, the next layer is tried. Routing providers work the same way (see Routing).
builder.Services.AddTurtleRoute()
.AddOpenCageGeocoder(o => o.ApiKey = config["OpenCage:ApiKey"]) // primary
.AddAzureMapsGeocoder(o => o.ApiKey = config["AzureMaps:ApiKey"]) // fallback
.ConfigureGeocoding(o => o.MinimumConfidence = GeocodeConfidence.Medium)
.AddAzureMapsRouting(o => o.ApiKey = config["AzureMaps:ApiKey"]);
Then inject IGeocoder, IRoutePlanner or IRouteMatrix:
GeocodeResult? result = await geocoder.GeocodeAsync(new Address("Katwilgweg", "2", "2050", "Antwerpen", "", "BE"));
// result.Coordinate, result.Confidence, result.Provider ("OpenCage" or "AzureMaps")
The pipeline returns null when no layer produced an acceptable result, and throws a GeocodingException only when every layer failed, so callers can tell "address not found" apart from "try again later".
OpenCage returns its best text match even when it lies elsewhere, such as a same-named street in another city or a building somewhere in the country for random letters. OpenCageGeocoder therefore takes the first of its top 5 results on the requested street and in the requested postcode or city, and scores the result Low when none is, so the pipeline falls back. It asks for local names (Language defaults to "native") to compare them with the address.
Failures, timeouts and retries
A failing layer is logged once with its exception; while it keeps failing, one summary line per layer is logged every 5 minutes (e.g. OpenCageGeocoder failed 1482 more times in the last 00:05:00 ... Last error: OpenCage returned 503), and the first call that succeeds again logs that it recovered. To change the interval, register new LayerFailureLog(TimeSpan.FromMinutes(1)) as a singleton before AddTurtleRoute().
OpenCage and HERE requests time out after Timeout (10 and 30 seconds by default), so a hanging provider falls back instead of waiting for the default HttpClient timeout of 100 seconds. For a retry and a circuit breaker, add a resilience handler through configureHttpClient, e.g. with Microsoft.Extensions.Http.Resilience. Keep it short, as the next layer is waiting, and within Timeout, which bounds the whole call including retries:
.AddOpenCageGeocoder(o => o.ApiKey = config["OpenCage:ApiKey"], http => http.AddStandardResilienceHandler(r =>
{
r.Retry.MaxRetryAttempts = 1;
r.AttemptTimeout.Timeout = TimeSpan.FromSeconds(4);
r.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(10);
r.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
}))
While the circuit is open, calls fail at once and go straight to the next layer. Errors that a retry cannot fix, such as an invalid key (401) or an exceeded quota (402), fall back without a retry: watch the failure summaries, or the share of results whose Provider is the fallback, so they do not go unnoticed.
Without DI, the providers and the pipeline can be created directly:
IGeocoder geocoder = new GeocodingPipeline([
new OpenCageGeocoder(httpClient, new OpenCageOptions { ApiKey = "..." }),
new AzureGeocoder("...")
]);
To add another vendor, implement IGeocoder and register it with builder.AddGeocoder<MyGeocoder>().
Shadow mode
To compare a new provider with the current one on real traffic, run it in shadow mode. Callers always get the configured pipeline's result; a sample of requests is repeated against the shadow geocoder in a background worker (so a host is required), and the shadow's results are never returned or stored.
builder.Services.AddTurtleRoute()
.AddAzureMapsGeocoder(o => o.ApiKey = config["AzureMaps:ApiKey"])
.AddShadowGeocoder(shadow => shadow.AddOpenCageGeocoder(o => o.ApiKey = config["OpenCage:ApiKey"]), o =>
{
o.SampleRate = 0.10; // share of requests repeated against the shadow
o.SummaryInterval = TimeSpan.FromHours(1); // one summary log line per interval
o.DisagreementThresholdInMeters = 250;
o.MaxDisagreementsPerInterval = 5; // individually logged, with their address
o.MaxRequestsPerSecond = 1; // stay within the shadow provider's plan
});
The comparisons are aggregated in memory and logged as one line per interval, e.g.:
Geocoding shadow summary OpenCage vs AzureMaps over 60 min: 154 comparisons, accepted 87.0%, within 50 m 71.4%,
within 250 m 92.1%, within 1 km 97.4%, p50 25 m, p90 250 m, max 3200 m, not found 3.2%, errors 0.6%,
disagreements 3, dropped 0, 180 ms vs 140 ms, countries: BE 120 (89% accepted), DE 30 (80% accepted), other 4 (75% accepted)
A disagreement is a shadow result that would have been accepted but lies further than the threshold from the primary result; up to MaxDisagreementsPerInterval of them are logged as warnings with the address, the rest are only counted. Nothing is logged for an interval without traffic. To send the comparisons elsewhere, register your own IGeocodingComparisonSink.
Routing
GeoCoordinate start = new(51.219501, 4.359231);
GeoCoordinate end = new(51.214625, 4.382112);
Route route = await routePlanner.GetRouteAsync(start, end);
Routing providers are layers too, tried in the order they are registered: when a route planner or matrix throws (outage, invalid key, quota exceeded, no route found), the next one is tried, and a RoutingException is thrown only when every layer failed. Invalid input is not retried. For example, HERE with Azure Maps as the fallback:
builder.Services.AddTurtleRoute()
.AddHereRouting(o => o.ApiKey = config["Here:ApiKey"])
.AddAzureMapsRouting(o => o.ApiKey = config["AzureMaps:ApiKey"]);
Routes use the HERE Routing API v8 and return the polyline of each leg in Leg.Waypoints. With OptimizeWaypointOrder, the order is first calculated by the HERE Waypoints Sequence API, which is a second billable transaction; it is skipped when there are fewer than two intermediate stops. The matrix uses the HERE Matrix Routing API v8 in a single region-mode request when all coordinates fit within 400 km, and in chunks of 15 origins otherwise.
Contributing
Pull requests are welcome. Please check out the contribution and code of conduct guidelines.
Local development
- You must have Visual Studio 2022 Community or higher.
- The dotnet cli is also highly recommended.
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Azure.Maps.Routing (>= 1.0.0-beta.4)
- Azure.Maps.Search (>= 2.0.0-beta.5)
- TurtleRoute (>= 0.18.0)
-
net8.0
- Azure.Maps.Routing (>= 1.0.0-beta.4)
- Azure.Maps.Search (>= 2.0.0-beta.5)
- TurtleRoute (>= 0.18.0)
-
net9.0
- Azure.Maps.Routing (>= 1.0.0-beta.4)
- Azure.Maps.Search (>= 2.0.0-beta.5)
- TurtleRoute (>= 0.18.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.18.0 | 81 | 10/31/2026 |
| 0.18.0-beta.3 | 73 | 10/22/2026 |
| 0.18.0-beta.2 | 44 | 10/22/2026 |
| 0.18.0-beta.1 | 56 | 10/22/2026 |