Integrating Naurt’s Geocoder with C#

August 20, 2024
Resources

Setting Up the Environment

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

Setting Up a Basic Web Server

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

  1. Program.cs which will be the program entry point
  2. Startup.cs which will be the web server configuration - it will map endpoints to handlers
  3. Handler.cs which will actually handle the request

Let’s get started. In Program.cs add

namespace NaurtWebServer
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

In Startup.cs add

namespace NaurtWebServer
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers(); // Add support for controllers

            services.AddSingleton<Handler>();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                var handler = app.ApplicationServices.GetRequiredService<Handler>();
                endpoints.MapGet("/", handler.Handle);
            });
        }

    }
}

And finally in Handler.cs add

namespace NaurtWebServer
{
    public class Handler
    {
        public async Task Handle(HttpContext context)
        {
            context.Response.ContentType = "text/plain";
            await context.Response.WriteAsync("Hello, World!");
        }
    }
}

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!”.

API Key

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.

Basic Naurt Request

To make a basic Naurt request, we just need to edit the Handler.cs file to include a simple POST request to Naurt

using System.Text;

namespace NaurtWebServer
{
    public class Handler
    {
        private readonly HttpClient _httpClient;
        private readonly String _apiKey;

        public Handler()
        {
            this._httpClient = new HttpClient();
            this._apiKey = File.ReadAllText("api.key");

            this._httpClient.DefaultRequestHeaders.Add("Authorization", this._apiKey);
        }

        public async Task Handle(HttpContext context)
        {


            string jsonContent = "{\"address_string\": \"The Grand Hotel, Brighton\"}";
            var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");

            var response = await this._httpClient.PostAsync("https://api.naurt.net/final-destination/v1", content);

            if (response.IsSuccessStatusCode)
            {
                var reply = await response.Content.ReadAsStringAsync();
                context.Response.ContentType = "application/json";
                context.Response.StatusCode = 200;
                await context.Response.WriteAsync(reply);
            }
            else
            {
                context.Response.StatusCode = 500;
                context.Response.ContentType = "application/json";
                var reply = await response.Content.ReadAsStringAsync();
                await context.Response.WriteAsync(reply);
            }
        }
    }
}

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

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.


namespace NaurtWebServer
{
    public class QueryParser
    {
        public string? addressString { get; set; }
        public bool? additionalMatches { get; set; }
        public float? latitude { get; set; }
        public float? longitude { get; set; }

        public QueryParser(IQueryCollection queryParams)
        {
            this.addressString = queryParams["address_string"];
            this.additionalMatches = this.ParseBool(queryParams["additional_matches"]);
            this.latitude = this.ParseFloat(queryParams["latitude"]);
            this.longitude = this.ParseFloat(queryParams["longitude"]);
        }

        private float? ParseFloat(string? query)
        {
            if (query == null)
            {
                return null;
            }

            if (float.TryParse(query!, out float floatParam))
            {
                return floatParam;
            }
            else
            {
                return null;
            }
        }

        private bool? ParseBool(string? query)
        {
            if (query == null)
            {
                return null;
            }

            if (bool.TryParse(query!, out bool boolParam))
            {
                return boolParam;
            }
            else
            {
                return null;
            }
        }
    }
}

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

using System.Text.Json;
using System.Text.Json.Serialization;

public async Task Handle(HttpContext context)
        {


            var queryParams = context.Request.Query;
            QueryParser queryParser = new QueryParser(queryParams);

            var options = new JsonSerializerOptions
            {
                DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
                PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
            };

            string jsonContent = JsonSerializer.Serialize(queryParser, options);
            
            // 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

Visualising the GeoJSON

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

<!DOCTYPE html>
<html>

<head>
    <title>Naurt Geocoder</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
    <style>
        #map {
            height: 100vh;
        }
    </style>
</head>

<body>
    <div id="map"></div>
    <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
    <script>

        function getColor(type) {
            switch (type) {
                case 'naurt_door': return '#FF0000';
                case 'naurt_building': return '#9d79f2';
                case 'naurt_parking': return '#00FF00';
                default: return '#000000';
            }
        }

        function style(feature) {
            return {
                fillColor: getColor(feature.properties.naurt_type), weight: 2, opacity: 1, color: 'white', dashArray: '5', fillOpacity: 0.4
            };
        }

        function onEachFeatureCurry(address) {
            return function onEachFeature(feature, layer) {
                if (feature.properties) {
                    var popupContent = 'Type: ' + feature.properties.naurt_type;
                    if (feature.properties.naurt_type == 'naurt_building') {
                        popupContent += '<br>Address: ' + address;
                    }
                    layer.bindPopup(popupContent).openPopup();
                }
            }
        }



        var map = L.map('map').setView([0, 0], 2);
        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' }).addTo(map);
        var response = #NaurtResponse;


        L.geoJSON(response["best_match"]["geojson"], { style: style, onEachFeature: onEachFeatureCurry(response["best_match"]["address"]) }).addTo(map);

        if ("additional_matches" in response) {
            for (const additionalMatch of response["additional_matches"]) {
                L.geoJSON(additionalMatch["geojson"], { style: style, onEachFeature: onEachFeatureCurry(additionalMatch["address"]) }).addTo(map);
            }
        }

        var bounds = L.geoJSON(response["best_match"]["geojson"]).getBounds();
        map.fitBounds(bounds);
    </script>
</body>


</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

    public class Handler
    {
        private readonly HttpClient _httpClient;
        private readonly string _apiKey;
        private readonly string template;


        public Handler()
        {
            this._httpClient = new HttpClient();
            this._apiKey = File.ReadAllText("api.key");

            this._httpClient.DefaultRequestHeaders.Add("Authorization", this._apiKey);

            string templatesPath = Path.Combine(Directory.GetCurrentDirectory(), "Templates");
            string templatePath = Path.Combine(templatesPath, "index.html");
            this.template = File.ReadAllText(templatePath);

        }
        // 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.

Subscribe To Our Newsletter - Sleek X Webflow Template

Subscribe to our newsletter

Sign up at Naurt for product updates, and stay in the loop!

Thanks for subscribing to our newsletter
Oops! Something went wrong while submitting the form.