Everything you need to know to set up a C# server to plotgeocoding responses
The set up will vary slightly depending on what platform you use. I will be setting this up on an ArchLinux machine for use with the command line. If you’re using Windows, or plan to use Visual Studio your set up might be a little different - see the .NET documentation.
We’ll install the required packages with pacman
. It’s important to have the ASP.NET runtime, too, as we’ll be using that for our webserver
sudo pacman -S dotnet-runtime dotnet-sdk aspnet-runtime
Then we can create the project with
dotnet new web -o NaurtExample
Before we get into using Naurt, we’re just going to create a simple web server that will always say “Hello World!”. To do this, we’re going to use three files
Program.cs
which will be the program entry pointStartup.cs
which will be the web server configuration - it will map endpoints to handlersHandler.cs
which will actually handle the requestLet’s get started. In Program.cs
add
1namespace NaurtWebServer
2{
3 public class Program
4 {
5 public static void Main(string[] args)
6 {
7 CreateHostBuilder(args).Build().Run();
8 }
9
10 public static IHostBuilder CreateHostBuilder(string[] args) =>
11 Host.CreateDefaultBuilder(args)
12 .ConfigureWebHostDefaults(webBuilder =>
13 {
14 webBuilder.UseStartup<Startup>();
15 });
16 }
17}
18
In Startup.cs add
1namespace NaurtWebServer
2{
3 public class Startup
4 {
5 public void ConfigureServices(IServiceCollection services)
6 {
7 services.AddControllers(); // Add support for controllers
8
9 services.AddSingleton<Handler>();
10 }
11
12 public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
13 {
14 if (env.IsDevelopment())
15 {
16 app.UseDeveloperExceptionPage();
17 }
18
19 app.UseRouting();
20
21 app.UseEndpoints(endpoints =>
22 {
23 var handler = app.ApplicationServices.GetRequiredService<Handler>();
24 endpoints.MapGet("/", handler.Handle);
25 });
26 }
27
28 }
29}
And finally in Handler.cs add
1namespace NaurtWebServer
2{
3 public class Handler
4 {
5 public async Task Handle(HttpContext context)
6 {
7 context.Response.ContentType = "text/plain";
8 await context.Response.WriteAsync("Hello, World!");
9 }
10 }
11}
If you run with dotnet run
and then go to http://localhost:5001/ (or the one that is auto assigned- can be seen in the logs) you should see the server return a “Hello World!”.
If you haven’t already, sign up at Naurt’s dashboard for a free key. The free key comes loaded with thousands of API requests per month, so there’s no cost to try it out. We don’t require a credit card for sign up, either!
For this post, I’ll be placing my API key in a file called api.key
. Make sure the file is in the NaurtExample
folder. In most backend applications, this will probably be injected via an environment variable or a secret.
To make a basic Naurt request, we just need to edit the Handler.cs
file to include a simple POST request to Naurt
1using System.Text;
2
3namespace NaurtWebServer
4{
5 public class Handler
6 {
7 private readonly HttpClient _httpClient;
8 private readonly String _apiKey;
9
10 public Handler()
11 {
12 this._httpClient = new HttpClient();
13 this._apiKey = File.ReadAllText("api.key");
14
15 this._httpClient.DefaultRequestHeaders.Add("Authorization", this._apiKey);
16 }
17
18 public async Task Handle(HttpContext context)
19 {
20
21
22 string jsonContent = "{\"address_string\": \"The Grand Hotel, Brighton\"}";
23 var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
24
25 var response = await this._httpClient.PostAsync("https://api.naurt.net/final-destination/v1", content);
26
27 if (response.IsSuccessStatusCode)
28 {
29 var reply = await response.Content.ReadAsStringAsync();
30 context.Response.ContentType = "application/json";
31 context.Response.StatusCode = 200;
32 await context.Response.WriteAsync(reply);
33 }
34 else
35 {
36 context.Response.StatusCode = 500;
37 context.Response.ContentType = "application/json";
38 var reply = await response.Content.ReadAsStringAsync();
39 await context.Response.WriteAsync(reply);
40 }
41 }
42 }
43}
Now, we’re reading in the API Key from the file and storing it in the class. We also create a HttpClient
which we will reuse to make the requests. Finally, we set the API Key header as a default, since every request we make will use it.
Handling query parameters in C# is really easy. To do it, I’m going to make a class to handle the actual implementation, in a new QueryParser.cs
file.
1
2namespace NaurtWebServer
3{
4 public class QueryParser
5 {
6 public string? addressString { get; set; }
7 public bool? additionalMatches { get; set; }
8 public float? latitude { get; set; }
9 public float? longitude { get; set; }
10
11 public QueryParser(IQueryCollection queryParams)
12 {
13 this.addressString = queryParams["address_string"];
14 this.additionalMatches = this.ParseBool(queryParams["additional_matches"]);
15 this.latitude = this.ParseFloat(queryParams["latitude"]);
16 this.longitude = this.ParseFloat(queryParams["longitude"]);
17 }
18
19 private float? ParseFloat(string? query)
20 {
21 if (query == null)
22 {
23 return null;
24 }
25
26 if (float.TryParse(query!, out float floatParam))
27 {
28 return floatParam;
29 }
30 else
31 {
32 return null;
33 }
34 }
35
36 private bool? ParseBool(string? query)
37 {
38 if (query == null)
39 {
40 return null;
41 }
42
43 if (bool.TryParse(query!, out bool boolParam))
44 {
45 return boolParam;
46 }
47 else
48 {
49 return null;
50 }
51 }
52 }
53}
The gist of this code is to pull out the relevant parameter from the query parameters, and parse it into the correct type. All of them are nullable, since, they might not be present. It’s important that they’re nullable, rather than assigned a default value as in the next step we will want to skip null values.
Speaking of, back in Handle
we can use this and the serialise the request to a JSON with
1using System.Text.Json;
2using System.Text.Json.Serialization;
3
4public async Task Handle(HttpContext context)
5 {
6
7
8 var queryParams = context.Request.Query;
9 QueryParser queryParser = new QueryParser(queryParams);
10
11 var options = new JsonSerializerOptions
12 {
13 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
14 PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
15 };
16
17 string jsonContent = JsonSerializer.Serialize(queryParser, options);
18
19 // rest of the method unchanged...
Now try going to http://localhost:5001/?address_string=The Grand Hotel, Brighton&additional_matches=true - you should see the new request
While what we’ve done so far technically works fine, the GeoJSON is not exactly meant for humans. It would be much better to visualise the GeoJSON on a map - for that we’ll use Leaflet.
Then create a Templates
folder and inside there an index.html
. The contents of that file should be
1<!DOCTYPE >
2<html>
3
4<head>
5 <title>Naurt Geocoder</title>
6 <meta charset="utf-8" />
7 <meta name="viewport" content="width=device-width, initial-scale=1.0">
8 <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
9 <style>
10 #map {
11 height: 100vh;
12 }
13 </style>
14</head>
15
16<body>
17 <div id="map"></div>
18 <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
19 <script>
20
21 function getColor(type) {
22 switch (type) {
23 case 'naurt_door': return '#FF0000';
24 case 'naurt_building': return '#9d79f2';
25 case 'naurt_parking': return '#00FF00';
26 default: return '#000000';
27 }
28 }
29
30 function style(feature) {
31 return {
32 fillColor: getColor(feature.properties.naurt_type), weight: 2, opacity: 1, color: 'white', dashArray: '5', fillOpacity: 0.4
33 };
34 }
35
36 function onEachFeatureCurry(address) {
37 return function onEachFeature(feature, layer) {
38 if (feature.properties) {
39 var popupContent = 'Type: ' + feature.properties.naurt_type;
40 if (feature.properties.naurt_type == 'naurt_building') {
41 popupContent += '<br>Address: ' + address;
42 }
43 layer.bindPopup(popupContent).openPopup();
44 }
45 }
46 }
47
48
49
50 var map = L.map('map').setView([0, 0], 2);
51 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' }).addTo(map);
52 var response = #NaurtResponse;
53
54
55 L.geoJSON(response["best_match"]["geojson"], { style: style, onEachFeature: onEachFeatureCurry(response["best_match"]["address"]) }).addTo(map);
56
57 if ("additional_matches" in response) {
58 for (const additionalMatch of response["additional_matches"]) {
59 L.geoJSON(additionalMatch["geojson"], { style: style, onEachFeature: onEachFeatureCurry(additionalMatch["address"]) }).addTo(map);
60 }
61 }
62
63 var bounds = L.geoJSON(response["best_match"]["geojson"]).getBounds();
64 map.fitBounds(bounds);
65 </script>
66</body>
67
68
69</html>
I won’t walk through every detail of the template, as it’s pretty self-explanatory. I’ll just note that the basic idea here is to plot each aspect of the GeoJSON - naurt_door
, naurt_parking
, naurt_building
- as its own layer. We also check to see if there are additional_matches
, and if there are, loop over them and plot them in the same way as the best_match
.
In terms of rendering this template, I tried a LOT of different alternatives, but nothing seemed like a good, simple solution. I tried Razor at first, which is the most popular, but it introduced way too much overhead and complexity. Razor is really designed for large, robust MVC applications, and it would have added more lines of code than the entire rest of the project. There were a few alternatives which were based on Razor but just allowed for rendering a single page, but all of them had some critical shortcoming or other which made them unusable. In the end, I simply added a #NaurtResponse
into the template, and used a string replace on it.
Modify the Handler
class to keep the template in memory
1 public class Handler
2 {
3 private readonly HttpClient _httpClient;
4 private readonly string _apiKey;
5 private readonly string template;
6
7
8 public Handler()
9 {
10 this._httpClient = new HttpClient();
11 this._apiKey = File.ReadAllText("api.key");
12
13 this._httpClient.DefaultRequestHeaders.Add("Authorization", this._apiKey);
14
15 string templatesPath = Path.Combine(Directory.GetCurrentDirectory(), "Templates");
16 string templatePath = Path.Combine(templatesPath, "index.html");
17 this.template = File.ReadAllText(templatePath);
18
19 }
20 // rest of the class...
Then modify the Handle method
And that’s it! If you run again and go to http://localhost:5001/?address_string=The Grand Hotel, Brighton&additional_matches=true you should see this
You can view the whole code and many more examples at our GitHub.