Derinlemesine yazılım eğitimleri için kanalımı takip edebilirsiniz...

Asp.NET Core Web API – Custom Formatters(Özel Biçimlendiriciler)

Merhaba,

Asp.NET Core Web API uygulamaları, client’lar ve diğer uygulamalar ile JSON, XML veya düz/sade metin gibi dahili/mevcut biçimleri kullanarak iletişim kurarlar. Bizler Asp.NET Core uygulamalarında varsayılan olarak desteklenen JSON, XML ve düz metin yerine özel biçimlendiricileri(custom formatters) kullanarak kendimize ait bir iletişim format standardı oluşturabilir ve uygulamalarımızda bunu esas olarak kullanabiliriz. Bu içeriğimizde Asp.NET Core mimarisinde Özel Biçimlendiricilerin(Custom Formatters) nasıl ve hangi prensiplerle oluşturulacağını inceleyecek ve tarafımızca geliştirilmiş dahili olarak desteklenmeyen özel bir iletişim türünün nasıl kullanılabilir hale getirildiğini göreceğiz.

Öncelikle özel biçimlendirici oluşturmanın üç temel adımını tanımlayarak başlayalım…

  1. Çıktı Biçimlendirici – Output Formatter
    Verileri client’a göndermek ve serileştirmek için output formatter oluşturulur.
  2. Girdi Biçimlendirici – Input Formatter
    Client’tan alınan verilerin deserialize süreçleri için input formatter oluşturulur.
  3. Biçimlendiricilerin Uygulamaya Eklenmesi
    Oluşturulan formatter’ların uygulama tarafından varsayılan olarak kullanılabilmesi için Asp.NET Core temel konfigürasyonlarına eklenir.
Özel Biçimlendirici Oluşturma

Şimdide teknik açıdan Asp.NET Core uygulaması için özel biçimlendirici oluşturmanın temel adımlarını tanımlayalım…

  1. Özel biçimlendiriciyi temsil edecek bir sınıf oluşturulur.
  2. Ardından bu sınıf XmlDataContractSerializerInputFormatter, TextInputFormatter, JsonInputFormatter vb. gibi bir temel/base sınıftan türetilir. Bu sınıflar girdi biçimlendiricileri için kullanılabilen birçok dahili biçimlendiricilerden sadece birkaçıdır. Bunların dışında XmlDataContractSerializerOutputFormatter, TextOutputFormatter, JsonOutputFormatter vb. gibi birçok yerleşik çıktı biçimlendiricilerde mevcuttur. Hazır lafı gelmişken InputFormatter türü tüm girdi biçimlendiricilerinin temel sınıfıdır ve ayrıca tüm giriş biçimlendiriciler IInputFormatter arayüzünü uygular.
  3. Oluşturulan sınıf içerisinde geçerli medya türleri ve kodlamalar tasarlanır.

Şimdi gelin örnek olarak TextInputFormatter ve TextOutputFormatter sınıflarından türeyen iki özelleştirici sınıf oluşturalım. İlk olarak TextOutputFormatter‘dan türeyecek olan sınıf ile başlayalım.

    public class UserOutputFormatter : TextOutputFormatter
    {
        public UserOutputFormatter()
        {
            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/customFormat"));

            SupportedEncodings.Add(Encoding.UTF8);
            SupportedEncodings.Add(Encoding.Unicode);
        }
        protected override bool CanWriteType(Type type)
        {
            return typeof(User).IsAssignableFrom(type) || typeof(IEnumerable<User>).IsAssignableFrom(type);
        }
        public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
        {
            IServiceProvider serviceProvider = context.HttpContext.RequestServices;
            HttpResponse response = context.HttpContext.Response;

            StringBuilder buffer = new();
            if (context.Object is IEnumerable<User>)
                foreach (User user in context.Object as IEnumerable<User>)
                    FormatData(buffer, user);
            else
                FormatData(buffer, context.Object as User);

            await response.WriteAsync(buffer.ToString());
        }

        void FormatData(StringBuilder stringBuilder, User user)
        {
            stringBuilder.AppendLine("****");
            stringBuilder.Append("Employee : ");
            stringBuilder.Append(user.Name);
            stringBuilder.Append(" ");
            stringBuilder.AppendLine(user.Surname);
        }
    }

Yukarıdaki kod bloğunu incelerseniz eğer ‘TextOutputFormatter’ abstract class’ından türeyen ‘UserOutputFormatter ‘ adında bir sınıf oluşturulmuştur. Oluşturulan bu sınıfın constructor’ına göz atarsanız eğer ‘SupportedMediaTypes’ ve ‘SupportedEncodings’ koleksiyonları aracılığıyla geçerli media türü ve kodlamalar belirtilmektedir. ‘CanWriteType’ metodunda serilize edilecek olan datanın tür kontrolü gerçekleştirilmektedir. Yani output olarak gönderilecek datanın ‘User’ ya da ‘List<User>’ olup olmadığı denetlenmektedir. ‘WriteResponseBodyAsync’ metodunda ise özel biçime uygun serilizasyon operasyonu gerçekleştirilmektedir.

Bazı durumlarda ‘CanWriteType’ metodu yerine ‘CanWriteResult’ metodunu kullanmamız gerekebilir. Bu;

  • Action’ın model sınıfı döndürmesi gerektiği,
  • Runtime’da derived class’ın döndürülmesi gerektiği,
  • Runtime’da Action tarafından hangi derived class’ın döndürüldüğünün bilinmesi gerektiği

durumlar olarak ifade edilebilir.

Bu durumu şöyle örneklendirebiliriz. ‘User’ nesnesinin ‘Employee’ ve ‘Customer’ sınıflarının base class’ı olduğunu varsayalım. Normal şartlarda yukarıdaki gibi ‘CanWriteType’ metodu ile tür kontrolü yaparsak ‘Employee’ ve ‘Customer’ nesnelerinin her ikisi de ‘User’ nesnesinden türeyeceği için özel biçimlendirici tarafından şekillendirilecek ve formatlandırılacaktır. Ancak bizler burada özel biçimlendirici açısından ‘Customer’ nesnenin çalışmasını istemeyebilir ve sadece ‘Employee’ nesnelerinde devreye girmesini isteyebiliriz. İşte böyle bir durumda ‘CanWriteType’ metodundan ziyade ‘CanWriteResult’ metodu devreye girecek ve içerisinde almış olduğu ‘OutputFormatterCanWriteContext’ türünden parametredeki ‘Object’ property’si üzerinden bu ayrım gerçekleştirilebilecek. Peki ‘Customer’ nesnesi geldiğinde hangi biçimde sonuç dönecek? diye sorduğunuzu duyar gibiyim… Tabi ki de varsayılan biçimlendirme türü olan JSON türünde sonuç üretilip, döndürülecektir.

    public class UserOutputFormatter : TextOutputFormatter
    {
        .
        .
        .
        public override bool CanWriteResult(OutputFormatterCanWriteContext context)
        {
            return
                ((context.Object is Employee || context.Object is List<Employee>)
                    ||
                (context.Object is User || context.Object is List<User>))
                        &&
                (context.Object is not Customer || context.Object is not List<Customer>);
        }
        .
        .
        .
    }

Bakın! Yukarıdaki biçimlendiricinin ‘User’ ve ‘Employee’ nesnelerinde çalışabilirliğini ayırt edebilmek için ‘CanWriteType’ yerine ‘CanWriteResult’ metodunun override edilerek kullanıldığına dikkatinizi çekerim.

Velhasıl, artık geliştirilen bu biçimlendiriciyi Asp.NET Core uygulamasına dahil etmemiz gerekmektedir. Bunun için ilgili uygulamanın ‘Startup.cs’ dosyasındaki ‘ConfigureServices’ metodunda aşağıdaki çalışmanın yapılması yeterli olacaktır.

    public class Startup
    {
        .
        .
        .
        public void ConfigureServices(IServiceCollection services)
        {
            .
            .
            .
            services.AddControllers(options =>
            {
                options.OutputFormatters.Insert(0, new UserOutputFormatter());
                options.FormatterMappings.SetMediaTypeMappingForFormat("customFormat", MediaTypeHeaderValue.Parse("text/customFormat"));
            });
            .
            .
            .
        }
        .
        .
        .
    }

Yukarıdaki kod bloğunu incelerseniz eğer ‘AddController’ metodu içerisinde ‘OutputFormatters’ özelliğine ilgili biçimlendirici eklenmektedir. Neden OutputFormatters? diye sorarsanız eğer oluşturduğumuz biçimlendirici bir output/çıktı biçimlendiriciydi de ondan!

Şimdi ‘UsersController.cs’ adında bir controller oluşturalım ve içeriğini aşağıdaki gibi dolduralım.

    [Route("api/[controller]")]
    [ApiController]
    public class UsersController : ControllerBase
    {
        [HttpGet("[action]")]
        public IActionResult GetUsers()
        {
            List<User> employees = new()
            {
                new() { Age = 30, Name = "Gençay", Surname = "Yıldız" },
                new() { Age = 35, Name = "Mehmet", Surname = "Cümbül" },
                new() { Age = 40, Name = "Mehmet", Surname = "Sakarya" }
            };

            return Ok(employees);
        }

        [HttpGet("[action]")]
        public IActionResult GetEmployees()
        {
            List<Employee> employees = new()
            {
                new() { Age = 30, Name = "Gençay", Surname = "Yıldız" },
                new() { Age = 35, Name = "Mehmet", Surname = "Cümbül" },
                new() { Age = 40, Name = "Mehmet", Surname = "Sakarya" }
            };

            return Ok(employees);
        }

        [HttpGet("[action]")]
        public IActionResult GetCustomers()
        {
            List<Customer> employees = new()
            {
                new() { Age = 30, Name = "Gençay", Surname = "Yıldız" },
                new() { Age = 35, Name = "Mehmet", Surname = "Cümbül" },
                new() { Age = 40, Name = "Mehmet", Surname = "Sakarya" }
            };

            return Ok(employees);
        }
    }

Görüldüğü üzere ilgili controller’da ‘GetUsers’, ‘GetEmployees’ ve ‘GetCustomers’ olmak üzere üç action metot oluşturulmuştur. Şimdi bu metotlara client üzerinden istek yapıldığında ‘CanWriteResult’ metodundan dolayı dönüş türüne göre özel biçimlendirici devreye girecek ve verisel dönüşümü bizim formatımıza göre gerçekleştirecektir.

User Employee Customer
Asp.NET Core Web API - Custom Formatters(Özel Biçimlendiriciler) Asp.NET Core Web API - Custom Formatters(Özel Biçimlendiriciler) Asp.NET Core Web API - Custom Formatters(Özel Biçimlendiriciler)

Benzer şekilde client’tan gelecek olan verileri özelleştirilmiş bir şekilde işlememizi sağlayacak olan TextInputFormatter‘dan türeyecek sınıfıda geliştirelim.

    public class UserInputFormatter : TextInputFormatter
    {
        public UserInputFormatter()
        {
            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/customFormat"));

            SupportedEncodings.Add(Encoding.UTF8);
            SupportedEncodings.Add(Encoding.Unicode);
        }
        protected override bool CanReadType(Type type)
        {
            return type == typeof(User);
        }
        public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
        {
            HttpRequest request = context.HttpContext.Request;
            using StreamReader streamReader = new(request.Body, encoding);
            try
            {
                string data = await streamReader.ReadToEndAsync();
                if (!data.StartsWith("****"))
                {
                    string errorMessage = "Bozuk format! Veri **** ifadesi ile başlamalı!";
                    context.ModelState.TryAddModelError(context.ModelName, errorMessage);
                    throw new Exception(errorMessage);
                }

                string[] nameSurname = data.Substring(data.IndexOf(":") + 1, data.Length - (data.IndexOf(":") + 1)).Trim().Split(" ");
                User user = new()
                {
                    Name = nameSurname[0],
                    Surname = nameSurname[1]
                };

                return await InputFormatterResult.SuccessAsync(user);
            }
            catch
            {
                return await InputFormatterResult.FailureAsync();
            }
        }
    }

Yukarıdaki input formatter sınıfını incelerseniz eğer yine constructor’da media türü ve kodlama bilgileri verilmektedir. ‘CanReadType’ metodunda ise client’ın gönderdiği veriyi karşılayacak olan metottaki parametre türü belirlenmektedir. Eğer bu parametre türü ‘User’ ise bu biçimlendirici gelen datayı özel formata uygun olarak çözümleyecek yok eğer değilse varsayılan olan JSON türünden dönüşüm sağlanacaktır. ‘ReadRequestBodyAsync’ metodunda ise gelen özel formattaki datayı uygun algoritmayla geri çözümleyip misal olarak ‘User’ türüne dönüştürmekteyiz. Sizler burada daha farklı ve kompleks bir algoritma kullanabilirsiniz. O yüzden farazi bir çalışma gerçekleştirilmiştir. Eğer yukarıdaki dönüşüm başarıyla gerçekleştiriliyorsa bu durum InputFormatterResult.SuccessAsync komutuyla bildirilmektedir. Aksi taktirde InputFormatterResult.FailureAsync komutu ile süreçte bir hatanın meydana geldiği bildirilerek client’a gerekli dönüş yapılmaktadır.

Geliştirdiğimiz bu özel biçimlendiriciyi kullanabilmek için yine uygulamanın ‘Startup.cs’ dosyasında aşağıdaki bildirinin yapılması gerekmektedir.

    public class Startup
    {
        .
        .
        .
        public void ConfigureServices(IServiceCollection services)
        {
            .
            .
            .
            services.AddControllers(options =>
            {
                options.OutputFormatters.Insert(0, new UserOutputFormatter());
                options.InputFormatters.Insert(0, new UserInputFormatter());
                options.FormatterMappings.SetMediaTypeMappingForFormat("customFormat", MediaTypeHeaderValue.Parse("text/customFormat"));
            });
            .
            .
            .
        }
        .
        .
        .
    }

Evet… Şimdide deneme amaçlı ‘UsersController.cs’ isimli controller’ı aşağıdaki gibi değiştirelim ve gerekli testlerimizi gerçekleştirelim.

    [Route("api/[controller]")]
    [ApiController]
    public class UsersController : ControllerBase
    {
        [HttpPost]
        public void Post(User model)
        {

        }
    }

Dikkat ederseniz ‘Post’ action’ına client’tan gelecek olan daha ‘User’ türündedir. Haliyle bu türdeki tüm datalar için bizim oluşturduğumuz input biçimlendirici devreye girecektir.
Asp.NET Core Web API - Custom Formatters(Özel Biçimlendiriciler)Asp.NET Core Web API - Custom Formatters(Özel Biçimlendiriciler)
Eğer post edilen data yanlış bir formatta gönderilirse aşağıdaki gibi client uyarılacaktır!
Asp.NET Core Web API - Custom Formatters(Özel Biçimlendiriciler)Bizim oluşturduğumuz formata göre başlangıç değeri dört adet yıldız olması gerekirken burada bir yıldız mevcuttur. Haliyle bu tanımsız bir format olacağından dolayı dönüşüm sürecinde algoritmik bir hatadır.

Haliyle alınan sonuç;
Asp.NET Core Web API - Custom Formatters(Özel Biçimlendiriciler)şeklinde olacaktır.

Nihai olarak;
Bu makalede Asp.NET Core’un JSON yahut XML gibi veri formatlarına istinaden kendimize ait özelleştirilmiş biçimlendiricilerimizi oluşturmamızı sağlayan ve böylece özelleştirilmiş formatlara sahip üçüncü taraf sistemlerle uygulama bazında rahatlıkla iletişim kurabilmemize imkan tanıyan harika bir mimari olduğunu ve ihtiyaç doğrultusunda özel biçimlendiricilerin mimarisel açıdan biçilmiş kaftan olduğunu görmüş olduk.

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Bunlar da hoşunuza gidebilir...

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.