در پست قبلی، ما درخواستهای متفاوتی از GET را با کمک یک آبجکت handle ،DTO کردیم. در این پست، میخواهیم درخواستهای Put ،Post و Delete را ایجاد کنیم و با این کار، میخواهیم سمت سرور این سری از دوره (قسمت NET Core.) را تکمیل کنیم.
این مقاله، قسمتی از مجموعه آموزشی زیر میباشد:
- ایجاد یک دیتابیس برای پروژه
- پیاده سازی کدهای پایه
- log سفارشی در ASP.NET Core
- الگوی Repository با Entity Framework Core
- استفاده از Repository برای درخواستهای GET
- استفاده از Repository برای درخواستهای POST، PUT و DELETE(همین مقاله)
اگر می خواهید تمام آموزشهای لازم و پایه مربوط به این دوره آموزشی را ببینید ، لطفاً روی این لینک کلیک کنید: صفحه مقدمه برای این آموزش.
این پست، شامل قسمتهای زیر میباشد:
Handle کردن درخواست POST
اول از همه، attribute را دربالای اکشن متد GetOwnerById در کنترلر Owner تغییر میدهیم:
1 |
[HttpGet("{id}", Name = "OwnerById")] |
با این تغییر، ما نام OwnerById را برای اکشن متد GetOwnerById ست کردیم. این نام را قرار است که در اکشن متد ایجاد owner جدید استفاده کنیم که نام مفیدی است.
قبل از اینکه ادامه دهیم، باید کلاس DTO دیگری ایجاد کنیم. همانطور که در قسمت قبلی گفتیم، ما از کلاس مدل، فقط برای واکشی داده از دیتابیس استفاده میکنیم و برای برگرداندن نتیجه، به یک DTO نیاز داریم. برای اکشن create هم، کار به همین صورت است. پس کلاس OwnerForCreationDto را در پوشه Entities/DataTransferObjects ایجاد میکنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class OwnerForCreationDto { [Required(ErrorMessage = "Name is required")] [StringLength(60, ErrorMessage = "Name can't be longer than 60 characters")] public string Name { get; set; } [Required(ErrorMessage = "Date of birth is required")] public DateTime DateOfBirth { get; set; } [Required(ErrorMessage = "Address is required")] [StringLength(100, ErrorMessage = "Address cannot be loner then 100 characters")] public string Address { get; set; } } |
همانطور که میبینید، اینجا خصوصیات Id و Accounts را نداریم.
حال خط آخر را به اینترفیس IOwnerRepository اضافه میکنیم:
1 2 3 4 5 6 7 |
public interface IOwnerRepository : IRepositoryBase<Owner> { IEnumerable<Owner> GetAllOwners(); Owner GetOwnerById(Guid ownerId); Owner GetOwnerWithDetails(Guid ownerId); void CreateOwner(Owner owner); } |
بعد از تغییر اینترفیس، ما آن اینترفیس را پیاده سازی میکنیم:
1 2 3 4 |
public void CreateOwner(Owner owner) { Create(owner); } |
قبل از اینکه OwnerController را تغییر دهیم، باید یک قانون map دیگر ایجاد کنیم:
1 |
CreateMap<OwnerForCreationDto, Owner>(); |
در آخر، کنترلر را به صورت زیر تغییر میدهیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
[HttpPost] public IActionResult CreateOwner([FromBody]OwnerForCreationDto owner) { try { if (owner == null) { _logger.LogError("Owner object sent from client is null."); return BadRequest("Owner object is null"); } if (!ModelState.IsValid) { _logger.LogError("Invalid owner object sent from client."); return BadRequest("Invalid model object"); } var ownerEntity = _mapper.Map<Owner>(owner); _repository.Owner.CreateOwner(ownerEntity); _repository.Save(); var createdOwner = _mapper.Map<OwnerDto>(ownerEntity); return CreatedAtRoute("OwnerById", new { id = createdOwner.Id }, createdOwner); } catch (Exception ex) { _logger.LogError($"Something went wrong inside CreateOwner action: {ex.Message}"); return StatusCode(500, "Internal server error"); } } |
حالا زمان آن است که این کد را با فرستادن درخواست POST با استفاده از Postman تست کنیم.
نتیجه را بررسی کنیم:
شرح کد
حالا کمی در مورد این کد توضیح میدهیم. قسمتهای interface و repository که کاملا واضح هستند، بنابراین در مورد آن صحبت نمیکنیم. اما کد درون کنترلر، شامل مواردی است که قابل ذکر میباشد.
متد CreateOwner با ویژگی [HttpPost] مزین شده است که آنرا با درخواستهای POST محدود میکند. علاوه بر این، به پارامتر owner که از سمت کلاینت می آید دقت داشته باشید. ما آن را از Uri نمیگیریم بلکه از request body میگیریم. به همین دلیل از ویژگی [FromBody] استفاده کرده ایم. همچنین آبجکت owner، یک complex type است و به این دلیل باید از [FromBody] استفاده کنیم.
اگر بخواهیم، میتوانیم در این اکشن متد صراحتا مشخص کنیم که این پارامتر را از Uri بگیرد، که برای این کار، باید پارامتر را با ویژگی [FromUri]، مزین کنیم. اگرچه من به دلایل امنیتی و پیچیدگی request ، چنین کاری را پیشنهاد نمی کنم.
از آنجایی که پارامتر owner، از سمت کلاینت می آید، امکان دارد این اتفاق بیفتد که کلاینت اصلا این پارامتر را ارسال نکند. به عبارتی، ما باید آن را در برابر مقدار پیشفرض reference type که null میباشد اعتبارسنجی کنیم.
در قسمت پایینتر در کد، شما میتوانید متوجه این قسمت از کد اعتبارسنجی شوید: if(!ModelState.IsValid) . اگر به خصوصیات مدل owner نگاه کنید: Name
, Address
و DateOfBirth
، متوجه میشوید که همه آنها با Validation Attribute ها مزین شده اند. اگر به هر دلیل، اعتبارسنجی fail شود، در نتیجه، ModelState.IsValid مقدار false برمیگرداند که به این معنی است که مشکلی با آبجکت creation DTO پیش آمده است. در غیر اینصورت، آن true برمیگرداند که به این معنی است که مقادیر تمام خصوصیات معتبر میباشد.
ما دو map action نیز داریم. اولی، نوع OwnerForCreationDto را به نوع Owner مپ میکند، زیرا ما آبجکت OwnerForCreationDto را از کلاینت میگیریم و باید از آبجکت Owner برای اکشن create استفاده کنیم. دومین map action، نوع Owner را به نوع OwnerDto مپ میکند که نوعی است که به عنوان نتیجه برمیگردانیم.
آخرین چیزی که باید ذکر شود این قسمت از کد است:
1 |
CreatedAtRoute("OwnerById", new { id = owner.Id}, owner); |
CreatedAtRoute یک کد وضعیت 201 که بیانگر Created است را برمیگرداند. همچنین، این کد، بدنه response را با آبجکت جدید owner و همچنین، ویژگی Location در داخل response header را با آدرسی ست میکند که مربوط به دریافت owner ایجاد شده میباشد. در این متد، باید نام اکشن متد مورد نظر، که از آن برای دریافت موجودیت ایجاد شده میتوانیم استفاده کنیم را ارائه دهیم:
اگر این آدرس را در Postman کپی کنیم، زمانیکه درخواست GET را ارسال میکنیم، در پاسخ، آبجکت owner جدیدی که ایجاد شده است را دریافت میکنیم.
Handle کردن درخواست PUT
عالی.
حالا کارمان را با درخواست PUT برای آپدیت کردن موجودیت owner ادامه میدهیم.
اول از همه،باید یک کلاس DTO دیگر اضافه کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class OwnerForUpdateDto { [Required(ErrorMessage = "Name is required")] [StringLength(60, ErrorMessage = "Name can't be longer than 60 characters")] public string Name { get; set; } [Required(ErrorMessage = "Date of birth is required")] public DateTime DateOfBirth { get; set; } [Required(ErrorMessage = "Address is required")] [StringLength(100, ErrorMessage = "Address cannot be loner then 100 characters")] public string Address { get; set; } } |
ما همان کاری را انجام دادیم که با کلاس OwnerForCreationDto انجام دادیم. حتی اگرچه این کلاس همان OwnerForCreationDto به نظر برسد ، اما یکسان نیستند. اول از همه ، آنها یک تفاوت معنایی دارند. این کلاس، برای اکشن update و قبلی برای creation میباشد. علاوه بر این، قوانین اعتبارسنجی که برای creation DTO اعمال شده است نباید برای update DTO نیز اعمال شود. بنابراین، جداسازی آنها، یک شیوه درست میباشد.
به عنوان یک نکته، اگر میخواهید کد تکراری را از OwnerForCreationDto
و OwnerForUpdateDto
حذف کنید، میتوانید یک کلاس abstract ایجاد کنید، خصوصیات را از آن دو کلاس برداشته و درون این کلاس abstract قرار دهید و سپس، این دو کلاس را از این کلاس abstract مشتق کنید. اما ما این کار را به دلیل پیچیده نکردن سناریو انجام ندادیم.
حالا در مرحله بعد، باید یک قانون map جدید ایجاد کنیم:
1 |
CreateMap<OwnerForUpdateDto, Owner>(); |
سپس، خط آخر را به اینترفیس IOwnerRepository اضافه میکنیم:
1 2 3 4 5 6 7 8 |
public interface IOwnerRepository : IRepositoryBase<Owner> { IEnumerable<Owner> GetAllOwners(); Owner GetOwnerById(Guid ownerId); Owner GetOwnerWithDetails(Guid ownerId); void CreateOwner(Owner owner); void UpdateOwner(Owner owner); } |
البته که باید OwnerRepository.cs را نیز تغییر دهیم:
1 2 3 4 |
public void UpdateOwner(Owner owner) { Update(owner); } |
در آخر، OwnerController را تغییر میدهیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
[HttpPut("{id}")] public IActionResult UpdateOwner(Guid id, [FromBody]OwnerForUpdateDto owner) { try { if (owner == null) { _logger.LogError("Owner object sent from client is null."); return BadRequest("Owner object is null"); } if (!ModelState.IsValid) { _logger.LogError("Invalid owner object sent from client."); return BadRequest("Invalid model object"); } var ownerEntity = _repository.Owner.GetOwnerById(id); if (ownerEntity == null) { _logger.LogError($"Owner with id: {id}, hasn't been found in db."); return NotFound(); } _mapper.Map(owner, ownerEntity); _repository.Owner.UpdateOwner(ownerEntity); _repository.Save(); return NoContent(); } catch (Exception ex) { _logger.LogError($"Something went wrong inside UpdateOwner action: {ex.Message}"); return StatusCode(500, "Internal server error"); } } |
همانطور که باید متوجه شده باشید، اکشن متد با ویژگی [HttpPut] مزین شده است. علاوه بر این، این اکشن متد، دو پارامتر دریافت میکند: id موجودیتی که میخواهیم آپدیت کنیم و موجودیت با فیلدهای ویرایش شده که از body درخواست گرفته میشود. مابقی کد کاملا ساده میباشد. بعد از اعتبارسنجی، ما owner را از دیتابیس بیرون میکشیم و عملیات به روزرسانی را بر روی آن اجرا میکنیم. در آخر، ما NoContent را که بیانگر کد وضعیت 204 میباشد برمیگردانیم:
Handle کردن درخواست DELETE
برای درخواست Delete، ما باید این مراحل را دنبال کنیم:
اینترفیس:
1 2 3 4 5 6 7 8 9 |
public interface IOwnerRepository : IRepositoryBase<Owner> { IEnumerable<Owner> GetAllOwners(); Owner GetOwnerById(Guid ownerId); Owner GetOwnerWithDetails(Guid ownerId); void CreateOwner(Owner owner); void UpdateOwner(Owner owner); void DeleteOwner(Owner owner); } |
OwnerRepository:
1 2 3 4 |
public void DeleteOwner(Owner owner) { Delete(owner); } |
OwnerController:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
[HttpDelete("{id}")] public IActionResult DeleteOwner(Guid id) { try { var owner = _repository.Owner.GetOwnerById(id); if(owner == null) { _logger.LogError($"Owner with id: {id}, hasn't been found in db."); return NotFound(); } _repository.Owner.DeleteOwner(owner); _repository.Save(); return NoContent(); } catch (Exception ex) { _logger.LogError($"Something went wrong inside DeleteOwner action: {ex.Message}"); return StatusCode(500, "Internal server error"); } } |
اجازه دهید یک مورد دیگر را نیز handle کنیم. اگر شما بخواهید یک owner ای که دارای account میباشد را حذف کنید سپس خطای 500 internal دریافت میکنید، زیرا ما مجوز allow cascade delete را در پیکربندی دیتابیسمان نداریم. حالا کاری که ما باید انجام دهیم این است که در همچین موقعیتی، یک BadRequest برگردانیم. پس برای این کار، مقداری تغییرات ایجاد میکنیم.
اینترفیس IAccountRepository را تغییر دهید:
1 2 3 4 5 6 7 8 9 10 11 |
using Entities.Models; using System; using System.Collections.Generic; namespace Contracts { public interface IAccountRepository { IEnumerable<Account> AccountsByOwner(Guid ownerId); } } |
سپس فایل AccountRepository را با افزودن یک متد جدید تغییر دهید:
1 2 3 4 |
public IEnumerable<Account> AccountsByOwner(Guid ownerId) { return FindByCondition(a => a.OwnerId.Equals(ownerId)).ToList(); } |
در آخر، اکشن DeleteOwner در OwnerController را با افزودن یک مرحله از اعتبارسنجی دیگر قبل از حذف owner، تغییر دهید:
1 2 3 4 5 |
if(_repository.Account.AccountsByOwner(id).Any()) { _logger.LogError($"Cannot delete owner with id: {id}. It has related accounts. Delete those accounts first"); return BadRequest("Cannot delete owner. It has related accounts. Delete those accounts first"); } |
کل کار همین است. حالا درخواست Delete را از طریق Postman بفرستید و نتیجه را ببینید. آبجکت owner باید از دیتابیس، حذف شده باشد.
نتیجه گیری
حال که همه اینها را یاد گرفتید، تمام این اکشنها را برای موجودیت Account تکرار کنید. زیرا این شیوه، هیچ حرف و حدیثی در آن نیست. درسته؟😉
با در اختیار داشتن این کد، ما یک web API ای داریم که تمام ویژگی های مربوط به handle کردن عملیات CRUD را شامل میشود.
با مطالعه این پست، شما موارد زیر را باید یاد گرفته باشید:
- شیوه handle کردن درخواست POST
- شیوه handle کردن درخواست PUT
- نحوه کدنویسی بهتر و reusable تر
- و شیوه handle کردن درخواست DELETE
از مطالعه این مقاله از شما تشکر میکنیم و امیدواریم که برای شما مفید واقع قرار گرفته باشد.
بسیار عالی
درود بر شما