TurtleRoute.Azure 0.18.0

dotnet add package TurtleRoute.Azure --version 0.18.0
                    
NuGet\Install-Package TurtleRoute.Azure -Version 0.18.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TurtleRoute.Azure" Version="0.18.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TurtleRoute.Azure" Version="0.18.0" />
                    
Directory.Packages.props
<PackageReference Include="TurtleRoute.Azure" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TurtleRoute.Azure --version 0.18.0
                    
#r "nuget: TurtleRoute.Azure, 0.18.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TurtleRoute.Azure@0.18.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TurtleRoute.Azure&version=0.18.0
                    
Install as a Cake Addin
#tool nuget:?package=TurtleRoute.Azure&version=0.18.0
                    
Install as a Cake Tool

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

PRs Welcome

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

License

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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