Gå til indholdet

Async HTTP

HTTP (HyperText Transfer Protocol) er det mest udbredte protokol til at overføre data over internettet. I .NET er System.Net.Http.HttpClient den bedste måde at kommunikere over HTTP, og der bør kun være en statisk instans for hele applikationen.

private static readonly HttpClient httpClient = new HttpClient();
// eller initialisering i constructor

Warning

Der bør kun være en statisk instans af HttpClient for hele applikationen. Hver HttpClient opretter en egen connection pool, og hvis der oprettes mange instanser kan det resultere i en udtømt pool og en masse fejl.

Metoder

HttpClient består en af del metoder hvoraf følgende er threadsafe:

  • CancelPendingRequests
  • DeleteAsync
  • GetAsync
  • GetByteArrayAsync
  • GetStreamAsync
  • GetStringAsync
  • PostAsync
  • PostAsJsonAsync
  • PutAsync
  • SendAsync

Hvis man blot skal hente text er GetASync eller GetStringAsync nemme

using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace Demo
{
    class Program
    {
        private static readonly HttpClient httpClient = new HttpClient();

        static async Task Main(string[] args)
        {
            // Henter et tilfældigt tal som text
            httpClient.DefaultRequestHeaders.Clear();
            httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html"));

            // Enten via den nemme GetStringAsync
            var response1 = await httpClient.GetStringAsync("https://www.random.org/integers/?num=1&min=1&max=6&col=1&base=10&format=plain&rnd=new");
            Console.WriteLine(response1);

            // Eller via Get
            var response2 = await httpClient.GetAsync("https://www.random.org/integers/?num=1&min=1&max=6&col=1&base=10&format=plain&rnd=new");
            if (response2.IsSuccessStatusCode)
            {
                string tal = await response2.Content.ReadAsStringAsync();
                Console.WriteLine(tal);
            }
        }
    }
}

Info

Brug httpbin til at teste alle mulige former for httpkald.

Det er bedst lokalt gennem Docker

docker run -p 8080:80 kennethreitz/httpbin

Herefter kan tjenesten tilgås på http://localhost:8080

Brug af HttpClient til JSON

Et kald mod en JSON-service kræver en ændring af accept-header samt en deserialisering.

Info

Se Awesome REST på marmelab/awesome-rest for en liste over ressourcer. Hvorfor ikke spille kort via HTTP? Se også public-apis/public-apis.

Eksempelvis vil et kald mod “Danmarks Adressers Web API” for at finde regioner i Danmark fra adressen https://api.dataforsyningen.dk/regioner/, der returnerer:

[
  {
    "ændret": "2019-08-27T14:05:49.915Z",
    "geo_version": 20,
    "geo_ændret": "2019-07-01T21:11:51.749Z",
    "bbox": [8.18951662, 56.53454667, 11.22599137, 57.76025478],
    "visueltcenter": [10.11282907, 57.30715928],
    "dagi_id": "389098",
    "kode": "1081",
    "navn": "Region Nordjylland",
    "nuts2": "DK05",
    "href": "https://api.dataforsyningen.dk/regioner/1081"
  },
  {
    "ændret": "2019-08-27T14:05:49.915Z",
    "geo_version": 24,
    "geo_ændret": "2018-11-02T22:06:29.440Z",
    "bbox": [8.07887623, 55.64437916, 11.66419061, 56.84325702],
    "visueltcenter": [9.60503157, 56.23399042],
    "dagi_id": "389101",
    "kode": "1082",
    "navn": "Region Midtjylland",
    "nuts2": "DK04",
    "href": "https://api.dataforsyningen.dk/regioner/1082"
  },
  ...
]

kunne skrives som:

using System.Net.Http.Headers;

namespace ConsoleApp6
{
    internal class Program
    {

        private static readonly HttpClient httpClient = new HttpClient();

        static async Task Main(string[] args)
        {
            httpClient.DefaultRequestHeaders.Accept.Clear();
            httpClient.DefaultRequestHeaders.Accept.Add(
                new MediaTypeWithQualityHeaderValue("application/json"));
            var response = await httpClient.GetAsync("https://api.dataforsyningen.dk/regioner/");
            if (response.IsSuccessStatusCode)
            {                
                string? json = await response.Content.ReadAsStringAsync();
                List<Region>? lst = System.Text.Json.JsonSerializer.Deserialize<List<Region>>(json);
                lst?.ForEach(i => Console.WriteLine($"{i.kode} {i.navn}"));
            }
        }

    }
    class Region
    {
        public string? navn { get; set; }
        public string? href { get; set; }
        public string? kode { get; set; }
    }
}

Resultatet vil være noget ala:

1081 Region Nordjylland
1082 Region Midtjylland
1083 Region Syddanmark
1084 Region Hovedstaden
1085 Region Sjælland

POST data

Hvis du ønsker at POST’e standard formdata over HTTP kan du bruge:

using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace Demo
{
    class Program
    {
        private static readonly HttpClient httpClient = new HttpClient();

        static async Task Main(string[] args)
        {
            var formContent = new FormUrlEncodedContent(new[]
            {
                new KeyValuePair<string, string>("name", "abc")
            });
            // Brug docker i stedet
            var response = await httpClient.PostAsync("https://httpbin.org/post", formContent);
            response.EnsureSuccessStatusCode();
            // retur fra server
            string content = await response.Content.ReadAsStringAsync();
            await Console.Out.WriteLineAsync(content);
        }
    }
}

Det vil skabe følgende POST:

POST https://httpbin.org/forms/post HTTP/1.1
Host: httpbin.org
Content-Type: application/x-www-form-urlencoded
Content-Length: 8

name=abc

POST JSON

Hvis du ønsker at POSTe til et standard JSON API kræver det en header:

using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace Demo
{
    class Program
    {
        private static readonly HttpClient httpClient = new HttpClient();

        static async Task Main(string[] args)
        {
            HttpResponseMessage response = await
                httpClient.PostAsJsonAsync("https://httpbin.org/post",
                new Customer { UserId = 1, Title = "a", Body = "b" });
            response.EnsureSuccessStatusCode();
            System.Console.WriteLine(response.StatusCode);
            // retur fra server
            string content = await response.Content.ReadAsStringAsync();
        }
    }

    class Customer {
        public string Title { get; set; }
        public string Body { get; set; }
        public int UserId { get; set; }
    }
}

Det vil skabe en POST som følger:

POST https://httpbin.org/posts HTTP/1.1
Host: jsonplaceholder.typicode.com
Content-Type: application/json; charset=utf-8
Content-Length: 35

{"Title":"a","Body":"b","UserId":1}

og returnere en “201 Created” statuskode.

Info

Hvis du skal sende en stor mængde data, kan det være en god ide at bruge SendAsync i stedet for PostAsJsonAsync da sidstnævnte vil læse hele JSON’en ind i hukommelsen før afsendelse.

Warning

Ved en bruge en statisk HttpClient (singleton) kan du principelt set få problemer med DNS cache. Dette kan løses på flere måder - se evt. Stop using the HttpClient the wrong way in .NET

RestSharp

Der findes et hav af pakke relateret til at simplificere HTTP kald. Den største er nok RestSharp som tilbyder en masse yderligere funktionalitet. Her er et simpelt eksempel der henter alle kommuner i DK:

using RestSharp;

namespace Demo
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var client = new RestClient("https://api.dataforsyningen.dk/");
            var request = new RestRequest("kommuner", Method.Get);
            var kommuner = await client.GetAsync<List<Kommune>>(request);
            kommuner.ForEach(k => Console.WriteLine(k));
        }
    }

    class Kommune
    {
        public string Kode { get; set; }
        public string Navn { get; set; }
        public override string ToString()
        {
            return $"{Kode} {Navn}";
        }
    }
}
Leg med et REST API

Det er nemt at “lege” med et HTTP Rest API gennem https://jsonplaceholder.typicode.com. Her er en C# app der benytter GET, POST, PUT, PATCH og DELETE (bemærk - kræver NuGet pakkerne RestSharp og Newtonsoft):

using RestSharp;
using Newtonsoft.Json;

namespace RestSharpDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            var client = new RestClient("https://jsonplaceholder.typicode.com");
            List<Post>? posts = null;

            var request = new RestRequest("/posts", Method.Get);
            var response = client.Execute(request);
            if (response.IsSuccessful)
            {
                posts = JsonConvert.DeserializeObject<List<Post>>(response.Content!);
                foreach (var post in posts!)
                {
                    Console.WriteLine($"Title: {post.title}");
                }
            }
            else
            {
                Console.WriteLine("Error fetching posts: " + response.ErrorMessage);
            }


            var newPost = new Post { userId = 1, title = "New Post", body = "This is the body of the new post." };
            request = new RestRequest("/posts", Method.Post);
            request.AddJsonBody(newPost);
            response = client.Execute(request);
            if (response.IsSuccessful)
            {
                var createdPost = JsonConvert.DeserializeObject<Post>(response.Content!);
                Console.WriteLine($"New post created with id: {createdPost?.id}");
            }
            else
            {
                Console.WriteLine("Error creating post: " + response.ErrorMessage);
            }


            if (posts?.Count > 0)
            {
                var postToUpdate = posts[0];
                postToUpdate.title = "Updated Title";
                request = new RestRequest($"/posts/{postToUpdate.id}", Method.Put);
                request.AddJsonBody(postToUpdate);
                response = client.Execute(request);
                if (response.IsSuccessful)
                {
                    var updatedPost = JsonConvert.DeserializeObject<Post>(response.Content!);
                    Console.WriteLine($"Post updated successfully: {updatedPost!.title}");
                }
                else
                {
                    Console.WriteLine("Error updating post: " + response.ErrorMessage);
                }
            }

            if (posts?.Count > 0)
            {
                var postToUpdate = posts[0];
                postToUpdate.title = "Updated Title (patch)";
                request = new RestRequest($"/posts/{postToUpdate.id}", Method.Patch);
                request.AddJsonBody(postToUpdate);
                response = client.Execute(request);
                if (response.IsSuccessful)
                {
                    var updatedPost = JsonConvert.DeserializeObject<Post>(response.Content!);
                    Console.WriteLine($"Post updated successfully: {updatedPost?.title}");
                }
                else
                {
                    Console.WriteLine("Error updating post: " + response.ErrorMessage);
                }
            }

            if (posts!.Count > 0)
            {
                var postToDelete = posts[0];
                request = new RestRequest($"/posts/{postToDelete.id}", Method.Delete);
                response = client.Execute(request);
                if (response.IsSuccessful)
                {
                    Console.WriteLine("Post deleted successfully.");
                }
                else
                {
                    Console.WriteLine("Error deleting post: " + response.ErrorMessage);
                }
            }
        }
    }

    public class Post
    {
        public int userId { get; set; }
        public int id { get; set; }
        public string? title { get; set; }
        public string? body { get; set; }
    }
}

ReFit

En nyere pakke er ReFit som bruge definition af et interface til automatisk generering af en HTTP klient. Her er et simpelt eksempel der henter alle kommuner i DK:

using Refit;

namespace Demo
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var client = RestService.For<IKommuneService>("https://api.dataforsyningen.dk/");
            var kommuner = await client.GetKommuner();
            kommuner.ForEach(k => Console.WriteLine(k));
        }
    }

    interface IKommuneService
    {
        [Get("/kommuner")]
        Task<List<Kommune>> GetKommuner();
    }

    class Kommune
    {
        public string Kode { get; set; }
        public string Navn { get; set; }
        public override string ToString()
        {
            return $"{Kode} {Navn}";
        }
    }
}

Dit eget API

I .NET kan du nemt lave dit eget API. Nemmest er at benytte ASP.NET Core Web API som er en del af ASP.NET Core. Skab et projekt i Visual Studio og vælg “ASP.NET Core Web API” eller benyt dotnet CLI:

dotnet new webapi -n MyApi

Det vil skabe en simpel API med et par simple metoder. Du kan tilføje flere metoder og endpoints efter behov.

Info

Du kan vælge at projektet automatisk skal indeholde Swagger for at dokumentere og prøve API’et.

Swagger, nu kendt som OpenAPI Specification, er et åbent kildekode-projekt, der definerer en standard, maskinlæsbar måde at beskrive RESTful webtjenester på. Den blev oprindeligt udviklet af Wordnik, og senere doneret til Linux Foundation. Swagger giver udviklere, API-designere og projektansvarlige et sæt værktøjer til at designe, bygge, dokumentere og forbruge RESTful webtjenester. Dette gør det muligt for mennesker og computere at opdage og forstå funktionaliteten af en service uden adgang til kildekoden, dokumentationen eller gennem netværkstrafikanalyse.

Med Swagger kan du beskrive dine API’er struktureret med JSON eller YAML. Denne beskrivelse omfatter information om API-ens endepunkter, deres operationsmetoder (GET, POST, osv.), parametre, svarbeskeder og andre detaljer. Swagger-specifikationen er ikke kun en API-dokumentation, men kan også bruges til at generere klientbiblioteker, server stubs, og interaktive API-dokumentationer direkte fra API-beskrivelsen. Dette reducerer mængden af manuelt arbejde betydeligt og forbedrer konsistensen på tværs af API-dokumentation og klient/serverkode.

Swagger tilbyder et økosystem af værktøjer, herunder Swagger Editor, en browserbaseret editor hvor du kan skrive og validere Swagger-specifikationer; Swagger UI, som genererer en interaktiv API-dokumentation, der lader udviklere udforske API’en og udføre opkald direkte fra browseren; og Swagger Codegen, der genererer server stubs og klientbiblioteker i forskellige programmeringssprog fra en Swagger-specifikation.

I de senere år er Swagger blevet en del af OpenAPI Initiative (OAI), og Swagger-specifikationen er blevet omdøbt til OpenAPI Specification. Dette skift understreger dens rolle som en industri-standard for API-design og dokumentation. OpenAPI og Swagger’s værktøjer er fortsat centrale i udviklingen af moderne web- og cloud-baserede applikationer, hvilket gør det lettere for udviklere at skabe, dele og forbruge API’er på en effektiv og standardiseret måde.

Opgaver