Object Mapping (نگاشت شیء) و عملیات CRUD با وضعیت‌های HTTP

Object Mapping (نگاشت شیء) و عملیات CRUD با وضعیت‌های HTTP

Object Mapping (نگاشت شیء) و عملیات CRUD با وضعیت‌های HTTP

منبع: Coding Clean, Reliable, and Safe REST APIs with ASP.NET Core 8 — Anthony Giretti

اعتبار ترجمه: ترجمه با کمک هوش مصنوعی

تصویر منبع — صفحهٔ 140Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 140.

Object Mapping (نگاشت شیء) و عملیات CRUD با HTTP Statusهای صحیح

شکل ۴-۲۲. Validation موفق Object از نوع Country در POST /countries

FluentValidation Library قدرتمندی برای انواع Validation است و مثال‌های کتاب فقط بخش ساده‌ای از قابلیت‌های آن را نشان می‌دهند. مهم‌ترین اصل این بخش این است که هرگز نباید User Input را قابل اعتماد فرض کرد. Validation همهٔ ورودی‌ها هم Best Practice است و هم بخشی از دفاع در برابر حمله‌های مخرب.

Object Mapping

پس از Validation باید Data ورودی را وارد Layerهای دیگر Application کنیم. در فصل ۳ اصول Separation of Concerns (SoC) و Abstraction را دیدیم. Input Parameterهای Endpoint مخصوص Web/API Layer هستند و نباید مستقیماً در Domain یا Infrastructure مصرف شوند.

تصویر منبع — صفحهٔ 141Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 141.

برای انتقال اطلاعات از API Endpoint به Repository و Database، Objectهای ورودی را به Data Transfer Object (DTO) یا Domain Object Map می‌کنیم. DTO بخشی از Domain است و می‌تواند توسط Layerهای مختلف استفاده شود. مراحل مثال:

  1. ساخت Domain Layer و تعریف DTO در آن.
  2. ساخت Interface برای Abstract کردن Mapping بین API Input و DTO، همراه Implementation آن.

Interface و Implementation مربوط به Mapping در API Layer قرار می‌گیرند، زیرا Input Model متعلق به همین Layer است. Domain Layer نباید به API Layer وابسته شود؛ API Layer به Domain وابسته است.

شکل ۴-۲۳. مسئولیت‌های API Layer و Domain Layer
تصویر منبع — صفحهٔ 142Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 142.

ابتدا Class مربوط به CountryDto را در Domain Layer می‌سازیم.

Listing 4-17. کلاس CountryDto

namespace Domain.DTOs;

    public class CountryDto
    {
        public string Name { get; set; }
        public string Description { get; set; }
        public string FlagUri { get; set; }
    }

Signature این Class فعلاً با Country در API Layer یکسان است، اما این به معنی یکی‌کردن آن‌ها نیست. Responsibility آن‌ها متفاوت است و ممکن است مستقل از هم تغییر کنند. این تفاوت برای SoC بسیار مهم است.

شکل ۴-۲۴. API Layer و Domain Layer با عناصر اختصاصی خود

در Domain، Folderای با نام DTOs ساخته می‌شود و CountryDto در آن قرار می‌گیرد. سپس API Layer به Domain Reference می‌دهد. اکنون Mapper را در API می‌سازیم.

Listing 4-18. Interface مربوط به ICountryMapper

using AspNetCore8MinimalApis.Models;
    using Domain.DTOs;

    namespace AspNetCore8MinimalApis.Mapping.Interfaces;

    public interface ICountryMapper
    {
        public CountryDto? Map(Country country);
    }

Method برابر Map یک Country می‌گیرد و CountryDto? برمی‌گرداند.

Listing 4-19. کلاس CountryMapper

using AspNetCore8MinimalApis.Mapping.Interfaces;
    using AspNetCore8MinimalApis.Models;
    using Domain.DTOs;

    namespace AspNetCore8MinimalApis.Mapping;

    public class CountryMapper : ICountryMapper
    {
        public CountryDto? Map(Country country)
        {
    
            return country is not null ? new CountryDto
            {
                Name = country.Name,
                Description = country.Description,
                FlagUri = country.FlagUri,
            } : null;
        }
    }

هدف این مثال پیچیدگی Mapping نیست، بلکه نشان دادن Abstraction و SoC است. Manual Mapping در این مثال از نظر Performance ساده و سریع است، اما چون Operation از طریق Interface Abstract شده، می‌توان Implementation را با Libraryهایی مانند AutoMapper یا Mapster عوض کرد بدون آن‌که Contract تغییر کند.

Pair مربوط به ICountryMapper/CountryMapper در Dependency Injection به‌صورت Scoped ثبت می‌شود، زیرا نگه داشتن Instance تا پایان عمر Application ضروری نیست.

Listing 4-20. ثبت Mapper در Dependency Injection

var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddScoped<ICountryMapper, CountryMapper>();
    var app = builder.Build();

Listing 4-21. POST /countries با Mapper Inject‌شده

app.MapPost("/countries", ([FromBody] Country country,
    IValidator<Country> validator, ICountryMapper mapper) => {
        var validationResult = validator.Validate(country);

        if (validationResult.IsValid)
        {
            var countryDto = mapper.Map(country);

            //Do some work here
            return Results.Created();
        }
        return Results.ValidationProblem(validationResult.
         ToDictionary());
    });
تصویر منبع — صفحهٔ 146Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 146.
شکل ۴-۲۵. اجرای POST /countries با ICountryMapper Inject‌شده

مدیریت CRUD و HTTP Statusها

اکنون می‌توان Endpointهای متداول Create، Retrieve، Update و Delete را ساخت. در HTTP معمولاً این Mapping برقرار است:

  • POST: Create
  • GET: Retrieve یک Entity یا Collection
  • PUT: جایگزینی Entity و در برخی حالت‌ها ایجاد آن اگر وجود نداشته باشد؛ Update در CRUD
  • PATCH: Update جزئی Entity
  • DELETE: Delete Entity

Handling HTTP Statuses

Class ایستا Results Methodهای متعددی برای تولید HTTP Statusهای رایج در Minimal API فراهم می‌کند.

جدول ۴-۵. Methodهای Results و HTTP Status تولیدشده
MethodStatus
Accepted202
AcceptedAtRoute202
BadRequest400
Bytes200, 206, 416
Challenge401
Conflict409
Content200
Created201
CreatedAtRoute201
File200, 206, 416
Json200
Forbid403
Redirect301, 302, 307, 308
RedirectToRoute301, 302, 307, 308
SignIn200
SignOut200
Stream200, 206, 416
Text200
Unauthorized401
UnproccessableEntity422
ValidationProblem400 + ValidationProblemDetails
StatusCodeهر Status با Integer ورودی
Empty200

اگر Method اختصاصی برای Status مورد نیاز وجود نداشته باشد، StatusCode هر عدد Status را می‌پذیرد.

تصویر منبع — صفحهٔ 149Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 149.

ساخت Service برای CRUD

برای مدیریت CountryDto، Serviceای میان API و Infrastructure ایجاد می‌کنیم. با رعایت Abstraction و SoC، Interface با نام ICountryService در Domain و Implementation با نام CountryService در Business Logic Layer (BLL) قرار می‌گیرد. جزئیات Implementation در اینجا مهم نیست؛ تمرکز روی نحوهٔ مصرف Service توسط Endpointها است.

شکل ۴-۲۶. مسئولیت‌های API، Domain و BLL

Listing 4-22. Interface مربوط به ICountryService

using Domain.DTOs;

    namespace Domain.Services;

    public interface ICountryService
    {
        CountryDto Retrieve(int id);
        List<CountryDto> GetAll();
        int CreateOrUpdate(CountryDto country);
        bool UpdateDescription(int id, string description);
        bool Delete(int id);
    }

Service باید به‌صورت Scoped ثبت شود:

builder.Services.AddScoped<ICountryService, CountryService>();

برای CRUD، Property شناسه به CountryDto اضافه می‌شود و Nullable است، زیرا پیش از Creation ممکن است ID نداشته باشد.

Listing 4-23. CountryDto با ID Nullable

namespace Domain.DTOs;

    public class CountryDto
    {
        public int? Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public string FlagUri { get; set; }
    }

همین ID به Input Model مربوط به Country در API نیز اضافه می‌شود. Contract متدها:

  • Retrieve(id): CountryDto متناظر با ID را برمی‌گرداند.
  • GetAll(): Collection همهٔ CountryDtoها.
  • CreateOrUpdate(country): اگر ID برابر Null باشد Create و در غیر این صورت Update. مثال عمداً ساده فرض می‌کند ID موجود معتبر است.
  • UpdateDescription(id, description): Update جزئی Description و بازگرداندن Boolean نتیجه.
  • Delete(id): حذف بر اساس ID و بازگرداندن Boolean نتیجه.

ساخت Endpointهای CRUD

Validator و Mapper برای Ruleهای جدید Update شده‌اند و ICountryService در Endpointها Inject می‌شود.

Listing 4-24. پیاده‌سازی Endpointهای CRUD

// Create
    app.MapPost("/countries", (
                [FromBody] Country country,
                IValidator<Country> validator,
                ICountryMapper mapper,
                ICountryService countryService) => {
        var validationResult = validator.Validate(country);

        if (validationResult.IsValid)
        {
            var countryDto = mapper.Map(country);
            return Results.CreatedAtRoute(
                    "countryById",
                    new {
                           Id = countryService.CreateOrUpdate(
                                    countryDto
                                )
                        }
            );
        }
        return Results.ValidationProblem(
                  validationResult.ToDictionary()
    
               );
    });

    // Retrieve
    app.MapGet("/countries/{id}", (
               int id, ICountryMapper mapper,
               ICountryService countryService) => {
        var country = countryService.Retrieve(id);

        if (country is null)
            return Results.NotFound();

        return Results.Ok(mapper.Map(country));
    }).WithName("countryById");

    // Retrieve
    app.MapGet("/countries", (
               ICountryMapper mapper,
               ICountryService countryService) => {
        var countries = countryService.GetAll();
        return Results.Ok(mapper.Map(countries));
    });

    // Update
    app.MapPut("/countries", (
               [FromBody] Country country,
               IValidator<Country> validator,
               ICountryMapper mapper,
               ICountryService countryService) => {
        var validationResult = validator.Validate(country);

        if (validationResult.IsValid)
        {
            if (country.Id is null)
    
                return Results.CreatedAtRoute(
                          "countryById",
                          new {
                                  Id = countryService.CreateOrUpdate(
                                          mapper.Map(country)
                                       )
                          });
            return Results.NoContent();
        }
        return Results.ValidationProblem(
               validationResult.ToDictionary()
        );
    });

    // Update
    app.MapPatch("/countries/{id}", (
                 int id,
                 [FromBody] CountryPatch countryPatch,
                 IValidator<CountryPatch> validator,
                 ICountryMapper mapper,
                 ICountryService countryService) => {
        var validationResult = validator.Validate(countryPatch);

        if (validationResult.IsValid)
        {
            if (countryService.UpdateDescription(
                   id,
                   countryPatch.Description
                ))
                return Results.NoContent();
    
            return Results.NotFound();
        }
        return Results.ValidationProblem(
                  validationResult.ToDictionary()
        );
    });

    // Delete
    app.MapDelete("/countries/{id}", (
                  int id,
                  ICountryService countryService) => {
        if (countryService.Delete(id))
            return Results.NoContent();

        return Results.NotFound();
    });

POST /countries

چون POST برای Create است، Country ورودی باید ID برابر Null داشته باشد. پس از Validation و Mapping، CreateOrUpdate اجرا می‌شود. Response موفق 201 Created با CreatedAtRoute است و Validation ناموفق 400 Bad Request تولید می‌کند. CreatedAtRoute نام Route برابر countryById و ID ساخته‌شده را می‌گیرد تا Location منبع جدید ساخته شود.

تصویر منبع — صفحهٔ 156Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 156.
شکل ۴-۲۷. Response مورد انتظار هنگام Create با POST /countries

Location Header نشانی Resource تازه‌ساخته‌شده را به Client می‌دهد.

GET /countries/{id}

اگر Resource پیدا شود، JSON با 200 OK برگردانده می‌شود؛ در غیر این صورت 404 Not Found. Endpoint با WithName("countryById") نام‌گذاری شده تا CreatedAtRoute بتواند URL Location را بسازد.

تصویر منبع — صفحهٔ 157Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 157.
تصویر منبع — صفحهٔ 157Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 157.
شکل ۴-۲۸. Response موفق GET /countries/{id}

DELETE /countries/{id}

اگر Country وجود داشته و حذف شود، Response برابر 204 No Content است؛ اگر پیدا نشود، 404 Not Found.

شکل ۴-۲۹. Response موفق DELETE /countries/{id}
تصویر منبع — صفحهٔ 158Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 158.

PUT /countries

PUT در این Example هم Create و هم Update را پوشش می‌دهد. Validation ناموفق 400 است. اگر country.Id Null باشد Resource ایجاد و 201 Created برگردانده می‌شود؛ در غیر این صورت Update انجام شده و نویسنده 204 No Content را انتخاب کرده است، هرچند RFC اجازهٔ 200 OK را نیز می‌دهد.

شکل ۴-۳۰. Response موفق Update با PUT /countries

در Route مربوط به PUT، ID داخل URL نیست و در Body قرار دارد. PUT یک Operation Idempotent است و Route می‌تواند ثابت بماند؛ Payload تعیین می‌کند Create یا Update انجام شود.

PATCH /countries/{id}

PATCH فقط Description را Update می‌کند. Input Model جداگانهٔ CountryPatch و Validator مربوط به IValidator<CountryPatch> استفاده می‌شوند. Validation موفق و Update موفق به 204 No Content منجر می‌شود. اگر Resource وجود نداشته باشد، 404 است؛ PATCH در این مثال Resource جدید نمی‌سازد. Validation ناموفق 400 با ProblemDetails/ValidationProblem تولید می‌کند.

در ادامه Mapper و Validator مربوط به PATCH نشان داده می‌شوند.

Listing 4-25. CountryMapper به‌روزشده

using AspNetCore8MinimalApis.Mapping.Interfaces;
    using AspNetCore8MinimalApis.Models;
    using Domain.DTOs;

    namespace AspNetCore8MinimalApis.Mapping;

    public class CountryMapper : ICountryMapper
    {
        public CountryDto? Map(Country country)
        {
            return country != null ? new CountryDto
            {
                Id = country.Id,
                Name = country.Name,
                Description = country.Description,
                FlagUri = country.FlagUri,
            } : null;
        }

        public Country? Map(CountryDto country)
        {
            return country != null ? new Country
            {
                Id = country.Id,
                Name = country.Name,
                Description = country.Description,
                FlagUri = country.FlagUri,
            } : null;
        }
    
        public List<Country> Map(List<CountryDto> countries)
        {
            return countries.Select(Map).ToList();
        }
    }

Listing 4-26. CountryPatchValidator

using AspNetCore8MinimalApis.Models;
    using FluentValidation;
    using FluentValidation.Results;
    using System.Text.RegularExpressions;

    namespace AspNetCore8MinimalApis.Validators;

    public class CountryPatchValidator : AbstractValidator<CountryPatch>
    {
        public CountryPatchValidator()
        {
            RuleFor(x => x.Description)
            .NotEmpty()
            .WithMessage("{ParameterName} cannot be empty")
            .Custom((name, context) =>
            {
                Regex rg = new Regex("<.*?>"); // Matches HTML tags
               if (rg.Matches(name).Count > 0)
               {
                   // Raises an error
                   context.AddFailure(new ValidationFailure(
                   "Description",
                   "The description has invalid content")
                   );

Code این Listing در صفحهٔ بعد ادامه پیدا می‌کند و ادامهٔ آن همراه بخش بعدی حفظ خواهد شد.

منبع: Coding Clean, Reliable, and Safe REST APIs with ASP.NET Core 8 — Anthony Giretti.

فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.

امتیاز کاربران به این مقاله

☆☆☆☆☆

0 نفر امتیاز داده اند. میانگین: 0.0 از 5

 

0 نظر

نظر محترم شما در مورد مقاله های وب سایت برنامه نویسی و پایگاه داده

نظرات محترم شما در خدمات رسانی بهتر ما را یاری می نمایند. لطفا اگر مایل بودید یک نظر ما را مهمان فرمائید. آدرس ایمیل و وب سایت شما نمایش داده نخواهد شد.

0 / 500

اطلاعات تماس

  • آدرس:اصفهان-خیابان ام کلثوم غربی - بعد خیابان تخم چی - بیست متر بعد از پیتزا ننه شب - کوچه تعمیر گاه سمار زغالی - پلاک 354 - درب مشکی - طبقه هفتم
  • آدرس ایمیل:najafzade@gmail.com
  • وب سایت:http://www.a00b.com/
  • تلفن ثابت:(+98)9131253620
  • تلفن همراه:09131253620