Categories: Asp.Net Web API

Asp.NET Web API Response’larında Tutarlılığı (Consistency) Sağlamak

Merhaba arkadaşlar, bir süredir bloğuma çok fazla vakit ayıramıyorum. Gerek iş yerimdeki yoğunluğumdan, gerek hazırlanıyor olduğum sertifikasyon sınavlarından, gereksede üzerinde çalışıyor olduğumuz Asp.NET Web API kitabından dolayı fazla vakit bulamamaktayım.

Konusu küçük fakat etkisi büyük olan bu konuyu paylaşmak istedim. Evet konumuz Asp.NET Web API üzerindeki response’larımızın tutarlılığını (Consistency) sağlamak. Bu konunun önemli olmasının sebebi ise: Asp.NET Web API ile RESTful mimarisinde bir servis geliştirdiğimiz için, client ilgili servisimizi tüketirken GET, PUT, POST gibi HTTP verbs‘lerini kullanacaktır. İlgili servisimiz response olarak bazen istenilen DTO (Data Transfer Object)’yu geriye dönerken, bazende response body’i boş dönerek bununla birlikte Header üzerinden bir bilgi geriye dönüyor olabilir. Bazende client’ın beklenmedik bir request göndermesi üzerine geriye bir hata mesajı da dönüyor olabiliriz. İşte bu gibi durumları tutarlı bir şekilde handle edebilmek için, client’a her seferinde farklı bir response structure‘ı sunmamamız gerekir.

Öyle bir response structure’ı oluşturalım ki client ilgili servisin versiyon numarasını (versiyonlama işlemleri için), işlemlerin http status kodlarını ve hatta bir hata meydana geldi ise hangi alanı kontrol etmesi gerektiği gibi bilgileri verebilmeliyiz.

Haydi şimdi biraz kodlamaya geçelim. 🙂 Öncelikle yukarıda bahsetmiş olduğumuz bu sihirli response structure’ımızı oluşturalım (Best practice’ler üzerinde de görebilirsiniz.)

[DataContract]
public class ApiResponse<T>
{
    public ApiResponse(HttpStatusCode statusCode, T result, string errorMessage = null)
    {
        StatusCode = (int)statusCode;
        Result = result;
        ErrorMessage = errorMessage;
    }

    public ApiResponse()
    {

    }

    [DataMember]
    public string Version { get { return "1.0"; } }

    [DataMember]
    public int StatusCode { get; set; }

    [DataMember(EmitDefaultValue = false)]
    public string ErrorMessage { get; set; }

    [DataMember(EmitDefaultValue = false)]
    public T Result { get; set; }
}

ApiResponse<T> isminde generic olarak sınıfımızı oluşturuyoruz. Kod üzerinde de görebildiğimiz gibi, “Version”, “StatusCode”, “ErrorMessage” ve ilgili response DTO’sunu veriyor olacağımız “Result” parametrelerini, constructor aracılığı ile doldurulabilmesini sağladık. Dikkat ederseniz ApiResponse<T> class’ını ve property’leri “DataContract” ve “DataMember” attribute’leri ile donattık. Opt-out olarak donattığımız bu attribute’ler, response’un serialization işlemi sırasında dilediğimiz propety’lerin serialize işlemine tabi olmasını ve belirtmiş olduğumuz “EmitDefaultValue” değerinin (yani herhangi bir value yoksa default value’su ile bunları yaratma) geçerli olabilmesini sağlamaktadır.

Evet response structure’ımızı belirlediğimize göre şimdi bunu Asp.NET Web API içerisine öyle bir implemente edelim ki, hiçbir şey yapmamıza gerek kalmadan, Web API handler’ları bu işlemi her response için gerçekleştirsin.

Evet Handler ipucunu verdiğimize göre hemen handler’ı oluşturmaya başlayalım.

public class ApiResponseHandler : DelegatingHandler
{
    protected override async System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
    {
        var response = await base.SendAsync(request, cancellationToken);

        return BuildApiResponse(request, response);
    }

    private static HttpResponseMessage BuildApiResponse(HttpRequestMessage request, HttpResponseMessage response)
    {
        object content = null;
        string errorMessage = string.Empty;

        ValidateResponse(response, ref content, ref errorMessage);

        // Yeni response'u custom olarak oluşturmuş olduğumuz wrapper sınıf ile baştan oluşturuyoruz.
        var newResponse = CreateHttpResponseMessage(request, response, content, errorMessage);

        // Header key'lerini baştan set et.
        foreach (var loopHeader in response.Headers)
        {
            newResponse.Headers.Add(loopHeader.Key, loopHeader.Value);
        }

        return newResponse;
    }

    private static HttpResponseMessage CreateHttpResponseMessage<T>(HttpRequestMessage request, HttpResponseMessage response, T content, string errorMessage)
    {
        return request.CreateResponse(response.StatusCode, new ApiResponse<T>(response.StatusCode, content, errorMessage));
    }

    private static void ValidateResponse(HttpResponseMessage response, ref object content, ref string errorMessage)
    {
        if (response.TryGetContentValue(out content) && !response.IsSuccessStatusCode)
        {
            HttpError error = content as HttpError;

            if (error != null)
            {
                content = null;
                StringBuilder sb = new StringBuilder();

                foreach (var loopError in error)
                {
                    sb.Append(string.Format("{0}: {1} ", loopError.Key, loopError.Value));
                }

                errorMessage = sb.ToString();
            }
        }
    }
}

ApiResponse structure’ının handler’ını, DelegatingHandler abstract class’ından yararlanarak oluşturduk. SendAsync method’unu override ederek, BuildApiResponse method’u ile asıl wrapping işlemi başlamış oluyor. BuildApiResponse method’u içerisinde yaptığımız ana işlem ise, request üzerinden ilgili content‘i veya hata‘yı yakalayıp CreateHttpResponseMessage method’u aracılığı ile request üzerinden tekrardan bir Response yaratmaktır. Yaratmış olduğumuz yeni response’un content’ini ApiResponse<T> structure’ı ile belirliyoruz. Bu sayede client response’u belirlemiş olduğumuz structure doğrultusunda alıyor olacaktır.

Şimdi geriye kalan son adım ise oluşturduğumuz bu ApiResponseHandler’ı, WebApiConfig içerisinde bulunan MessageHandlers içerisine eklemektir.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Custom oluşturduğumuz message handler'ları burada register ediyoruz.
        config.MessageHandlers.Add(new ApiResponseHandler());
    }
}

İşte hepsi bu kadar. Artık api controller içerisindeki her bir action method, ApiResponse<T> generic sınıfı ile wrapping işleminden geçerek aşağıdaki gibi örnek bir Json çıktısı üretecektir.

{
    Version: "1.0",
    StatusCode: 200,
    Result: {
        Id: 1,
        Username: "GokhanGokalp",
        Fullname: "Gökhan Gökalp",
        Mail: "gok.gokalp@yahoo.com"
    }
}
Kaynak:

http://www.devtrends.co.uk/blog/wrapping-asp.net-web-api-responses-for-consistency-and-to-provide-additional-information

Gökhan Gökalp

View Comments

  • Merhaba, yazınız için teşekkürler ancak test amaçlı bir deneme proje yazmıştım ve aynı projede ekstreden birde outputCache uygulamıştım ve yukarıda ki implementasyonlardan sonra projeyi çalıştırdığımda ilk requestten sonra cache duration içerisinde gelinen diğer requestlerde MessageHandler içerisindeki if(response.TryGetContentValue(out content) geriye content i null veriyor. Sebep olarak ise response içerisindeki Content alanında artık controller dan dönen obje değilde onun ram de tutulan byteContent hali bulunmakta. Bu nedenle ApiResponse objesindeki Result alanı cacheden objeyi alamadığı için client'a null gidiyor. Konu ile ilgili bir çözüm öneriniz var mıdır ?

    • Merhaba, bu konuyu test edip en kısa sürede yanıt vermeye çalışacağım. Şu link'ide incelemenizi tavsiye ederim Web API'da output caching için. Çünkü Web API'ın buil-in olarak bir OutputCache attribute'üne sahip değil. Tamamen stateless bir yapıda inşa edildiği için.

  • Merhaba,

    Bu site bile tek başına mükemmel bir kaynak. Çıkaracağınız kitabı hayal bile edemiyorum. :)

    Makale ve paylaşım için çok teşekkürler.

    • Teşekkür ederim güzel yorumun için. Bu ay içinde kitap basılıyor. İyi günler dilerim.

  • Merhaba hocam,
    Ben ApiController içerisinde Task Get(int id) şeklinde bir action kullanıyorum. id değerli veri bulunamaz ise NotFound() return ediyorum. Bu yöntemi uyguladığımda NotFound() sonucunda
    { Version: "1.0", StatusCode: 404 } şeklinde sonuç veriyor. ErrorMessage kısmı gelmiyor. Bulunamadı şeklinde bir hata mesajı verdirmek nasıl olur acaba teşekkürler.

    • Merhaba, error kısmı için benim oluşturmuş olduğum herhangi bir exception gerçekleştiğinde alınacak message kısmı. Eğer bulunamadı gibi custom message'lar dönmek istiyorsanız ErrorMessage dışında sizde bir message property'si tanımlayıp, ilgili durum bilgilerini oradan expose edebilirsiniz.

  • Sizce Best Practice hangisi olur :

    -HTTP Status Code'lara göre hata kodlarımızı belirlemek mi ? (Buradaki dezavantaj eğer HTTP Status Code listesinde olmayan, sisteme göre bambaşka bir kod kullanılacaksa, bunu dönememek olur sanırım)
    -Custome Code yapısı oluşturup bu code'ları mı dönmek ? (Burada da, zaten HTTP Status Code listesinde tanımlı kodları tekrar tanımlamak ve karşı tarafın da bu listeden haberdar olmak zorunda olması dezavantaj sanırım)

    • Merhaba,

      Açıkcası bu konu biraz karışık. Tamamen kurumunuzun kararlarına göre değişiklik gösterebilir aslında. Olması gereken standart olarak HTTP Status code'larına göre davranmak olacaktır elbette (bana göre). Tabi gerçek anlamıyla RESTful kafası ile design yapıyorsak. Eğer bunu yapıyorsak da, RESTful'a göre response'ları wraplememek gerekir aslında. Consistency için wraplemek de ayrı bir karar. O yüzden kendi kurumunuzun kendisine has tailored bir şekilde belirlediği bir standart olmalıdır derim gerek wrapping, gerekse de HTTP Status Code'larına göre veya custom olarak kendi rule'ları ile hataları belirlemek. Tamamen tercih diyebilirim. Birde bu API'ın nereye hizmet edeceği ile de alakalı bir konu.

      • Kurumun bu konuda almış olduğu bir karar yok ama şunu söyleyebilirim istekte bulunacak tüm client uygulamalar da kurum tarafından yazılacak.

        Yine de benim de isteğim standartlara mümkün olduğunca uymaktan yana. Bu doğrultuda HTTP Status Code listesini kullanıp ama birkaç metot için HTTP Status Code listesinde olmayan bir kod dönme ihtiyacı olursa nasıl bir yol izlemek lazım var mı kabul görmüş bir seçenek ?

        • Yalnız buradaki status code kavramlarını da karıştırmayalım. En basitinden size gelen bir requestteki işlem, gerçekleşemese bile aslında bunun status code'u 200'dür. Çünkü sunucuya başarılı bir şekilde gelip, business'sal işlemlerden dolayı gerçekleşememiştir. Bunları iyi ayırt etmek gerek. Siz ise wraplediğiniz context üzerinde farklı bir statu kodları elbette kullanabilir, error message'ları da karşıya iletebilirsiniz. Ben şu ana kadar tek bir doğrusunu görmedim bu işin.

  • Merhabalar,

    Bu Api response unu consistency icin wrap etme yaklasimi oldukca basarili fakat ModelState validation resultlari icin bu wrapping dogru bir bicimde calisir mi ? Bence direkt objenin namespaceini response a ekler gibi duruyor.

    iyi calismalar

  • Asp.net core 3.0 için de bir örnek mevcut mu? Core tarafında, yukarıda kullandığınız bazı extension metodlar çalışmıyor. Ayrıca delegate handler yerine middleware kullanılabilir mi?

    • Merhaba evet, .NET Core için middleware kullanabilirsiniz. Uygun bir vakit bulursam, makaleyi güncelleyeceğim. Teşekkür ederim.

Recent Posts

Securing the Supply Chain of Containerized Applications to Reduce Security Risks (Policy Enforcement-Automated Governance with OPA Gatekeeper and Ratify) – Part 2

{:tr} Makalenin ilk bölümünde, Software Supply Chain güvenliğinin öneminden ve containerized uygulamaların güvenlik risklerini azaltabilmek…

6 months ago

Securing the Supply Chain of Containerized Applications to Reduce Security Risks (Security Scanning, SBOMs, Signing&Verifying Artifacts) – Part 1

{:tr}Bildiğimiz gibi modern yazılım geliştirme ortamında containerization'ın benimsenmesi, uygulamaların oluşturulma ve dağıtılma şekillerini oldukça değiştirdi.…

8 months ago

Delegating Identity & Access Management to Azure AD B2C and Integrating with .NET

{:tr}Bildiğimiz gibi bir ürün geliştirirken olabildiğince farklı cloud çözümlerinden faydalanmak, harcanacak zaman ve karmaşıklığın yanı…

1 year ago

How to Order Events in Microservices by Using Azure Service Bus (FIFO Consumers)

{:tr}Bazen bazı senaryolar vardır karmaşıklığını veya eksi yanlarını bildiğimiz halde implemente etmekten kaçamadığımız veya implemente…

2 years ago

Providing Atomicity for Eventual Consistency with Outbox Pattern in .NET Microservices

{:tr}Bildiğimiz gibi microservice architecture'ına adapte olmanın bir çok artı noktası olduğu gibi, maalesef getirdiği bazı…

2 years ago

Building Microservices by Using Dapr and .NET with Minimum Effort – 02 (Azure Container Apps)

{:tr}Bir önceki makale serisinde Dapr projesinden ve faydalarından bahsedip, local ortamda self-hosted mode olarak .NET…

2 years ago