İçeriğe geç

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.

consistency

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

Kategori:Asp.Net Web API

13 Yorum

  1. Caner Caner

    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 ?

  2. Ahmet Kurt Ahmet Kurt

    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.

  3. Burhan Burhan

    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.

  4. Yunus Emre Yunus Emre

    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)

  5. cem basaranoglu cem basaranoglu

    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

  6. osman gön osman gön

    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?

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

This site uses Akismet to reduce spam. Learn how your comment data is processed.