به طور معمول برای ویرایش اطلاعات از HttpPut استفاده میکنیم. در این حالت باید تمامی فیلدهای مربوط به موجودیت را ارسال کنیم. اما اگر فقط یکی دوتا از فیلدها نیاز به ویرایش داشته باشند چه نیازی به ارسال مابقی فیلدهاست؟

با استفاده از HttpPatch میتوانیم فقط فیلدهایی را که نیاز به ویرایش دارند را به API ارسال کنیم. در این مطلب میخواهیم عملیات Patch را برای یک موجودیت User ایجاد کنیم. موجودیت User : 

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Family { get; set; }
    public DateTime BirthDay { get; set; }
}

سپس یک کلاس برای انجام عملیات موردنیاز User ایجاد میکنیم به نام UserService :

public class UserService : IUserService
{
    private readonly ApplicationDbContext _context;

    public UserService(ApplicationDbContext context)
    {
        _context = context;
    }
    public async Task<User> GetUserByIdAsync(int id, CancellationToken cancellationToken)
	{
   		 var user = await _context.Users.FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
   		 return user;
	}

    public async Task SaveChanges(CancellationToken cancellationToken)
    {
        await _context.SaveChangesAsync(cancellationToken);
    }
}

برای مپ کردن اطلاعات بین ویومدل و موجودیت ها از Automapper استفاده کرده ایم. که برای رجیستر کردن Automapper باید پکیج زیر را نصب نمایید:

<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />

با نصب پکیج AutoMapper.Extensions.Microsoft.DependencyInjection به صورت خودکار پکیج Automapper هم به پروژه اضافه میشود. سپس باید یک پروفایل برای رجیستر کردن ویومدل و مدل ها ایجاد کنیم. برای این کار یک کلاس به نام UserProfile ایجاد میکنیم:

public class UserProfile : Profile
{
    public UserProfile()
    {
        CreateMap<CreateUserViewModel, User>();
        CreateMap<User, SelectUserViewModel>()
            .ForMember(a => a.Age, map => map.MapFrom(src => DateTime.Now.Year - src.BirthDay.Year));
    }
}

ویومدل های CreateUserViewModel و SelectUserViewModel :

public class SelectUserViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Family { get; set; }
    public int Age { get; set; }
}
public class CreateUserViewModel
{
    [Required]
    [MaxLength(20)]
    public string Name { get; set; }
    [Required]
    [MaxLength(20)]
    public string Family { get; set; }
    public DateTime BirthDay { get; set; }
}

در ادامه یک کنترلر برای موجودیت User ایجاد میکنیم:

public class UserController : ControllerBase
{
    private readonly IUserService _userRepository;
    private readonly IMapper _mapper;

    public UserController(IUserService userRepository, IMapper mapper)
    {
        _userRepository = userRepository;
        _mapper = mapper;
    }
    [HttpGet("{id}", Name = "GetUser")]
    public async Task<IActionResult> GetUser(int id, CancellationToken cancellationToken)
    {
        var user = await _userRepository.GetUserByIdAsync(id, cancellationToken);
        if (user is null)
            return NotFound();
        var mappedUser = _mapper.Map<SelectUserViewModel>(user);
        return Ok(mappedUser);
    }
    [HttpPatch("{id}")]
    public async Task<IActionResult> Patch(int id, JsonPatchDocument<User> document, CancellationToken cancellationToken)
    {
        if (document is null)
        {
            return BadRequest();
        }
        var user = await _userRepository.GetUserByIdAsync(id, cancellationToken);
        document.ApplyTo(user, ModelState);
        if (ModelState.IsValid)
        {
            await _userRepository.SaveChanges(cancellationToken);
            var result = _mapper.Map<SelectUserViewModel>(user);
            return CreatedAtRoute("GetUser", new { id = result.Id }, result);
        }
        return BadRequest();
    }
}

اکشن Patch در ورودی یک JsonPatchDocument دریافت میکند که باید پکیج Microsoft.AspNetCore.JsonPatch را نصب نمایید.

<PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="5.0.9" />

کلاس JsonPatchDocument که جنریک میباشد, یک تایپ قبول میکند که مشخص میکند چه فیلدهایی را میتوانیم به اکشن Patch ارسال کنیم. برای مثال در اکشن Patch که ورودی کلاس JsonPatchDocument آن User است میتوانیم فیلدهایی را ارسال کنیم که در کلاس User وجود داشته باشد. برای مثال json زیر مشخص میکند که باید اسم کاربر از فرهاد به مجتبی تغییر کند:

[
  {
    "op": "replace",
    "path": "/name",
    "value": "مجتبی"
  }
]

این json به صورت array میباشد و به همین دلیل میتوانیم چند فیلد را برای ویرایش مشخص کنیم. برای مثال json زیر مشخص میکند که مقدار پراپرتی Name برابر مجتبی و Family آن برابر خادم صداقت شود.

[
  {
    "op": "replace",
    "path": "/name",
    "value": "مجتبی"
  },
  {
    "op": "replace",
    "path": "/family",
    "value": "خادم صداقت"
  }
]

سپس برای اعمال تغییرات ارسال شده باید ابتدا رکورد مورد نظر را از دیتابیس بخوانیم و سپس با استفاده از متد ApplyTo تغییرات ارسال شده را بر روی رکورد خوانده شده اعمال کنیم. در پارامتر دوم متد ApplyTo پراپرتی ModelState را قرار داده ایم که اگر مقدار نامعتبری ارسال شود عملیات ویرایش را انجام نمیدهد. برای فعال سازی این ویژگی که ModelState را به متد ApplyTo ارسال کنید باید پکیج زیر را نصب نمایید:

<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.9" />

در ادامه برای ثبت اطلاعات فقط متد SaveChangesAsync را برای اعمال تغییرات فراخوانی میکنیم. به دلیل اینکه به طورپیشفرض رکوردهای خوانده شده توسط ChangeTracker ردیابی میشوند و تغییرات با استفاده از متد ApplyTo بر روی متغییر user اعمال شده است. در نهایت رجیستر کردن سرویس ها در متد ConfigureServices:

services.AddDbContext<ApplicationDbContext>(options =>
{
    options.UseSqlServer(Configuration.GetConnectionString(nameof(ApplicationDbContext)));
});
services.AddScoped<IUserService, UserService>();
services.AddControllers(setupAction =>
{
    setupAction.ReturnHttpNotAcceptable = true;

}).AddNewtonsoftJson(setupAction =>
{
    setupAction.SerializerSettings.ContractResolver =
       new CamelCasePropertyNamesContractResolver();
});
services.AddAutoMapper(options =>
{
    options.AddProfile(new UserProfile());
});

انجام این کار با استفاده از سایر متدهای GET,POST,PUT نیز قابل اجراست, اما به عنوان Best practice برای ویرایش بخشی از اطلاعات باید از متد Patch استفاده کنیم.

در ادامه یک پروژه REST API با Maturity level 2 ایجاد میکنیم ;)

Powered by Froala Editor

نظرات