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

  • 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.

  • 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)

  • 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?

  • 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 ?

Recent Posts

DevEx Series 02: From Catalog to Copilots. Boosting Backstage with MCP Server

In the first part of this DevEx series, I tried to explain Platform Engineering and…

4 ay ago

DevEx Series 01: Creating Golden Paths with Backstage, Developer Self-Service Without Losing Control

As an architect involved in platform engineering and DevEx transformation within a large-scale organization for…

5 ay ago

Event-Driven Architecture’larda Conditional Claim-Check Pattern’ı ile Event Boyut Sınırlarının Üstesinden Gelmek

{:en}In today’s technological age, we typically build our application solutions on event-driven architecture in order…

1 yıl ago

Containerized Uygulamaların Supply Chain’ini Güvence Altına Alarak Güvenlik Risklerini Azaltma (Güvenlik Taraması, SBOM’lar, Artifact’lerin İmzalanması ve Doğrulanması) – Bölüm 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.…

2 yıl ago

Identity & Access Management İşlemlerini Azure AD B2C ile .NET Ortamında Gerçekleştirmek

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

2 yıl ago