ASP.NET Core Web API Course

Build RESTful services using ASP.NET Core 3.1

تعلم برمجة الـ RESTful services أو الـ Web APIs بإستخدام ASP.NET Core بطريقة مفصلة وتدريجية

الصفحة الرئيسية
خدمة RSS

للتواصل

5. Unified Response

2020-04-25






في هذا الدرس سنقوم بتوحيد النتائج المسترجعة response من الخدمة. نلاحظ أن بعض العمليات تعيد لنا قائمة (GetEmployees) وبعضها تعيد لنا قيمة واحدة (GetEmployee و PostEmployee و DeleteEmployee) بينما العملية الأخيرة لا تعيد أي قيمة على الإطلاق (PutEmployee).

إنشاء الـ Class الخاص بالـ Response

داخل المجلد Models\Employees نقوم بإنشاء ملف جديد بإسم EmployeesResponse.cs ويحتوي على الكود التالي:

using System;

namespace aspnetcorewebapiproject.Models.Employees
{
    public class EmployeesResponse<T>
    {
        public bool IsSuccessful { get; set; }
        public int Status { get; set; }
        public string Message { get; set; } 
        public T Data { get; set; }
    }
}

وفيما يلي شرح للـ properties المستخدمة:

Property الوصف
IsSuccessful هل تم تنفيذ العملية بنجاح؟
Status الـ Http Status Code المسترجع من تنفيذ العملية
Message في حالة لم ينجح التنفيذ فإنه يتم حفظ الرسالة هنا
Data البيانات المسترجعة

ونبدأ الآن إستخدامها في العمليات الخاصة بـ EmployeesController.

GetEmployees

نعدل على العملية لتصبح بالشكل التالي:

[HttpGet]
public async Task<ActionResult<EmployeesResponse<PaginatedList<EmployeeDetailsDto>>>> GetEmployees(string orderBy, string orderType, int? pageIndex, int? pageSize)
{
	var pagedEntities = await _repo.GetAllAsync(
			orderBy     ?? "FirstName", 
			orderType   ?? _config.GetValue<string>("ResponseDefaults:OrderType"),
			pageIndex   ?? _config.GetValue<int>("ResponseDefaults:PageIndex"), 
			pageSize    ?? _config.GetValue<int>("ResponseDefaults:PageSize")
		);

	var employeeDetailsDtos = _mapper.Map<List<EmployeeDetailsDto>>(pagedEntities.Items);

	var pagedDtos = new PaginatedList<EmployeeDetailsDto>(employeeDetailsDtos, pagedEntities.ItemsCount, pagedEntities.PageIndex, pagedEntities.PageSize);

	var response = new EmployeesResponse<PaginatedList<EmployeeDetailsDto>>
	{
		IsSuccessful = true,
		Status = 200,
		Message = string.Empty,
		Data = pagedDtos
	};

	return Ok( response );
}

ونلاحظ أن نوع القيمة المعادة أصبحت على الشكل التال:

Task<ActionResult<EmployeesResponse<PaginatedList<EmployeeDetailsDto>>>>

وفيما يلي مثال للقيمة المسترجعة بعد التنفيذ بواسطة Postman:

{
    "isSuccessful": true,
    "status": 200,
    "message": "",
    "data": {
        "pageIndex": 2,
        "pageSize": 3,
        "totalPages": 2,
        "itemsCount": 5,
        "items": [
            {
                "id": 2005,
                "firstName": "Ali",
                "lastName": "Saleh",
                "isManager": false,
                "enrollmentDate": "2012-10-03T00:00:00"
            },
            {
                "id": 2006,
                "firstName": "Alaa",
                "lastName": "Saleh",
                "isManager": false,
                "enrollmentDate": "2012-10-03T00:00:00"
            }
        ],
        "hasPreviousPage": true,
        "hasNextPage": false
    }
}

GetEmployee

نعدل على العملية لتصبح بالشكل التالي:

[HttpGet("{id}")]
public async Task<ActionResult<EmployeesResponse<EmployeeDetailsDto>>> GetEmployee(int id)
{
	var response = new EmployeesResponse<EmployeeDetailsDto>();
	response.IsSuccessful = true;

	var employeeEntity = await _repo.GetAsync(id);

	if (employeeEntity == null)
	{
		response.Status = 404;
		response.Message = "Employee not found";

		return NotFound( response );
	}

	var employeeDetailsDto = _mapper.Map<EmployeeDetailsDto>(employeeEntity);

	response.Status = 200;
	response.Data = employeeDetailsDto;

	return Ok( response );
}

ونلاحظ أن نوع القيمة المعادة أصبحت على الشكل التال:

Task<ActionResult<EmployeesResponse<EmployeeDetailsDto>>>

وفيما يلي مثال للقيمة المسترجعة بعد التنفيذ بواسطة Postman:

{
    "isSuccessful": true,
    "status": 200,
    "message": null,
    "data": {
        "id": 2003,
        "firstName": "Khalid",
        "lastName": "Alhabish",
        "isManager": false,
        "enrollmentDate": "2014-01-19T00:00:00"
    }
}

PutEmployee

سابقاً كانت هذه العملية لا تعيد لنا قيمة حيث أن Http Status Code كان 204 أي No Content. ولكن لو عدنا الى الـ method definitions في w3.org لرأينا أنه بإمكان الـ Put أن تعيد إما:

https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6

وعلى ذلك سوف نعدل على القيمة المسترجعه في حالة النجاح لتصبح Ok 200 في حالة نجاح العملية:

[HttpPut("{id}")]
public async Task<ActionResult<EmployeesResponse<EmployeeDetailsDto>>> PutEmployee(int id, EmployeeUpdateDto employeeUpdateDto)
{
	var response = new EmployeesResponse<EmployeeDetailsDto>();
	
	if ( (!ModelState.IsValid) || (id != employeeUpdateDto.Id) )
	{
		response.IsSuccessful = false;                
		response.Status = 400;
		response.Message = "Inputs are invalid";

		return BadRequest(response);
	}

	try
	{
		var employeeEntity = _mapper.Map<Employee>(employeeUpdateDto);
		await _repo.UpdateAsync(employeeEntity);

		var employeeDetailsDto = _mapper.Map<EmployeeDetailsDto>(employeeEntity);

		response.IsSuccessful = true;
		response.Status = 200;
		response.Data = employeeDetailsDto;

		return Ok( response );    
	}
	catch (Exception)
	{
		if (await _repo.GetAsync(id) == null)     
		{
			response.IsSuccessful = false;
			response.Status = 404;
			response.Message = "Employee not found";

			return NotFound(response);
		}

		response.IsSuccessful = false;
		response.Status = 500;
		response.Message = "Internal server error";

		return StatusCode(StatusCodes.Status500InternalServerError, response);                
	}            
}

ونلاحظ أن نوع القيمة المعادة أصبحت على الشكل التالي:

Task<ActionResult<EmployeesResponse<EmployeeDetailsDto>>>

PostEmployee

نعدل على العملية لتصبح بالشكل التالي:

[HttpPost]
public async Task<ActionResult<EmployeesResponse<EmployeeDetailsDto>>> PostEmployee(EmployeeInsertDto employeeInsertDto)
{
	var employeeEntity = _mapper.Map<Employee>(employeeInsertDto);

	await _repo.InsertAsync(employeeEntity);

	var employeeDetailsDto = _mapper.Map<EmployeeDetailsDto>(employeeEntity);

	var response = new EmployeesResponse<EmployeeDetailsDto>
	{
		IsSuccessful = true,
		Status = 201,
		Message = string.Empty,
		Data = employeeDetailsDto
	};

	return CreatedAtAction(nameof(GetEmployee), new { id = employeeDetailsDto.Id }, response);
}

ونلاحظ أن نوع القيمة المعادة أصبحت على الشكل التالي:

Task<ActionResult<EmployeesResponse<EmployeeDetailsDto>>>

DeleteEmployee

نعدل على العملية لتصبح بالشكل التالي:

[HttpDelete("{id}")]
public async Task<ActionResult<EmployeesResponse<EmployeeDetailsDto>>> DeleteEmployee(int id)
{
	var response = new EmployeesResponse<EmployeeDetailsDto>();

	Employee employeeEntity = await _repo.DeleteAsync(id);

	if (employeeEntity == null)
	{
		response.IsSuccessful = false;
		response.Status = 404;
		response.Message = "Employee not found";

		return NotFound(response);            
	}
	
	var employeeDetailsDto = _mapper.Map<EmployeeDetailsDto>(employeeEntity);

	response.IsSuccessful = true;
	response.Status = 200;
	response.Data = employeeDetailsDto;

	return Ok( response );
}

ونلاحظ أن نوع القيمة المعادة أصبحت على الشكل التالي:

Task<ActionResult<EmployeesResponse<EmployeeDetailsDto>>>

Binding to Query String

هذه خطوة لتنظيم الكود نوعاً ما، فبدلاً من أن يكون إسناد القيم من الـ query string كل متغير على حده كما هو الحال الآن:

public async Task<ActionResult<EmployeesResponse<PaginatedList<EmployeeDetailsDto>>>> GetEmployees(string orderBy, string orderType, int? pageIndex, int? pageSize)
{
  ...
}

سيصبح الكود بالطريقة التالية:

public async Task<ActionResult<EmployeesResponse<PaginatedList<EmployeeDetailsDto>>>> GetEmployees([FromQuery] EmployeeGetDto employeeGetDto)
{
  ...
}

ولعمل ذلك نقوم بالتالي:

داخل المجلد Models\Employees نقوم بإنشاء ملف جديد بإسم EmployeeGetDto.cs ويحتوي على الكود التالي:


using System;

namespace aspnetcorewebapiproject.Models.Employees
{
    public class EmployeeGetDto
    {
        public string orderBy { get; set; }        
        public string orderType { get; set; }
        public int? pageIndex { get; set; }
        public int? pageSize { get; set; }
    }
}

ثم نعدل على العملية GetEmployees في EmployeesController لتصبح:

[HttpGet]
public async Task<ActionResult<EmployeesResponse<PaginatedList<EmployeeDetailsDto>>>> GetEmployees([FromQuery] EmployeeGetDto employeeGetDto)
{
	var pagedEntities = await _repo.GetAllAsync(
			employeeGetDto.orderBy     ?? "FirstName", 
			employeeGetDto.orderType   ?? _config.GetValue<string>("ResponseDefaults:OrderType"),
			employeeGetDto.pageIndex   ?? _config.GetValue<int>("ResponseDefaults:PageIndex"), 
			employeeGetDto.pageSize    ?? _config.GetValue<int>("ResponseDefaults:PageSize")
		);

	var employeeDetailsDtos = _mapper.Map<List<EmployeeDetailsDto>>(pagedEntities.Items);

	var pagedDtos = new PaginatedList<EmployeeDetailsDto>(employeeDetailsDtos, pagedEntities.ItemsCount, pagedEntities.PageIndex, pagedEntities.PageSize);

	var response = new EmployeesResponse<PaginatedList<EmployeeDetailsDto>>
	{
		IsSuccessful = true,
		Status = 200,
		Message = string.Empty,
		Data = pagedDtos
	};

	return Ok( response );
}