ASP.NET Core Web API Course

Build RESTful services using ASP.NET Core 3.1

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

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

للتواصل

9. Versioning

2020-05-30






الخدمات services، كباقي البرمجيات، يطرأ عليها الحاجة الى التعديل والتطوير ويجب علينا كمطورين لهذه الخدمة أن نقوم بهذه التعديلات بطريقة لا تؤثر على المستفيدين من هذه الخدمة.

هنالك ثلاث طرق رئيسية لإصدار نسخ versions من الخدمة، وهي:

الطريقة مثال
HTTP Header X-API-Version: 2
Url /v2/employees
Query String /employees?api-version=2

في هذا الشرح سوف نعتمد طريقة تحديد النسخة في الـ Url.

التعديل على الـ Controller

إنشئ مجلد جديد بإسم v1 بداخل Controllers والذي سنقوم بوضع جميع الـ controllers في النسخة 1 version بداخله:

نعدل الآن على الـ namespace الخاص بـ EmployeesController ليصبح:

namespace aspnetcorewebapiproject.Controllers.v1
{

التعديل على الـ Models

نقوم أيضاً بإنشاء مجلد جديد بإسم v1 داخل المجلد Models ونضع المجلد Employees الموجود سابقاً بداخله:

نعدل الـ namespace لجميع الـ classes لتصبح كما يلي:

namespace aspnetcorewebapiproject.Models.Employees.v1
{

تصحيح الأخطاء

عند بناء المشروع ستجد أن هنالك بعض الأخطاء موجودة في الملفات التالية:

وذلك لأن هذه الملفات لم يعد بإمكانها إيجاد الـ classes السابقة وذلك لأننا قمنا بتغيير الـ namespace وكل ما عليك فعله هو كتابة الـ namespace الصحيح.

ملاحظة: في الملف MiddlewareExtensions.cs نعيد EmployeResponse من النسخة 1 دائماً حتى وإن طلبت نسخة مختلفة. قد يكون هذا ما تريده وبإمكانك أيضاً أن تعيد Response خاص بالنسخة المطلوبة.

قم الآن ببناء المشروع ومن المفترض أن تختفي جميع الأخطاء.

إضافة مكتبة الـ Versioning

نقوم الآن بإضافة المكتبتين التالية والتي ستساعدنا على تطبيق الـ versioning في الخدمة المقدمة:

dotnet add package Microsoft.AspNetCore.Mvc.Versioning
dotnet add package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer

التعديل على الـ EmployeesController

أضف ApiVersion و عدل Route لتصبح كالتالي:

...

namespace aspnetcorewebapiproject.Controllers.v1
{
    [ApiVersion("1.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    [EnableCors("CorsPolicy")]
    [ApiController]
    public class EmployeesController : ControllerBase
    {
		...

الـ ApiVersion attribute تحدد نسخة الـ controller وعدلنا على Route ليأخذ النسخة في العنوان url.

التعديل على ()ConfigureServices في Setup.cs

نضيف على هذه الدالة ما يلي:

services.AddApiVersioning(o => {
	o.DefaultApiVersion = new ApiVersion(1, 0);                
	o.AssumeDefaultVersionWhenUnspecified = true;
	o.ReportApiVersions = true;      
});

إختبار ما قمنا به

للتأكد من أن ما قمنا به سينفذ بشكل صحيح وسيعود الينا رقم النسخة الصحيح فإنه بإمكاننا إضافة العملية التالية:

[HttpGet("version")]
public string GetVersion() => HttpContext.GetRequestedApiVersion().ToString();

في Postman قم بإستدعاء العناوين التالية:

<!-- 1 -->
https://localhost:5001/api/v1/employees/version 

<!-- 1.0 -->
https://localhost:5001/api/v1.0/employees/version

في المرة الأولى ستعود الينا القيمة 1 وفي الثانية 1.0 ولكنها فعلياً تشير الى نفس النسخة.

التعديل على عملية GetEmployees

لنفترض أنه لدينا الآن متطلب جديد للعملية GetEmployees وهي أن تعيد لنا تاريخ الإنضمام بالتاريخ الميلادي بالإضافة الى الهجري. بإمكاننا الآن أن نعدل على هذه العملية وسيكتشف المستفيدين من الخدمة بأنه تم إضافة property جديد في القيمة المسترجعة. في مثالنا هذا الموضوع لا يشكل مسألة كبيرة ولكن لو أفترضنا أنا حذفنا property أو دمجنا أكثر من property معاً (مثل أن تصبح FirstName و LastName الى FullName في التعديل الجديد). هذه الأمور ستسبب بلا شك مشكلة للمستفيد من الخدمة حيث أنه بنى برنامجه على شكل معين للقيمة المسترجعة ثم يكتشف بأنه شكل آخر.

ولذلك سننشى action جديد وسنجعله يعيد DTO جديد يحمل التاريخ الهجري وذلك بإتباع ما يلي:

التعديل على الـ Models

نقوم بإنشاء مجلد جديد بإسم v1_1 داخل المجلد Models وننشئ مجلد آخر جديد بداخله بإسم Employees والذي بداخله أيضاً الملف EmployeeDetailsDto.cs:


using System;

namespace aspnetcorewebapiproject.Models.Employees.v1_1
{
    public class EmployeeDetailsDto
    {
        public int Id { get; set; }
        public string FirstName { get; set; }        
        public string LastName { get; set; }
        public bool IsManager { get; set; }
        public DateTime EnrollmentDate { get; set; }
        public string EnrollmentDateHijri { get; set; }
    }
}

نلاحظ أنه مشابه للـ class في النسخة 1 ماعدا في الـ namespace والـ property الأخيرة EnrollmentDateHijri.

التعديل على EmployeesProfile

يجب علينا أن نوضح لـ AutoMapper كيف يقوم بالتحويل من Entities.Employee الى Models.Employees.v1_1.EmployeeDetailsDto حيث أن جميع التحويلات السابقة كانت سهلة لأن القيمة المحول منها source والمحول اليها destination كانتا متطابقتان في الـ properties ولكن في حالتنا هذه يوجد property إضافي في المحول اليه ليس موجود في المحول منه.

using System;
using System.Globalization;
using AutoMapper;

namespace aspnetcorewebapiproject.Profiles
{
    public class EmployeesProfile : Profile
    {
        public EmployeesProfile()
        {
            // v 1.0
            //            

            // Map from Employee to EmployeeDetailsDto
            CreateMap<Entities.Employee, Models.Employees.v1.EmployeeDetailsDto>();

            // Map from EmployeeInsertDto to Employee
            CreateMap<Models.Employees.v1.EmployeeInsertDto, Entities.Employee>();

            // Map from EmployeeUpdateDto to Employee
            CreateMap<Models.Employees.v1.EmployeeUpdateDto, Entities.Employee>();

            // v 1.1
            // 

            // Map from Employee to EmployeeDetailsDto
            Calendar umAlQura = new UmAlQuraCalendar();

            CreateMap<Entities.Employee, Models.Employees.v1_1.EmployeeDetailsDto>()
                .ForMember( dest => dest.EnrollmentDateHijri,
                    map => map.MapFrom(
                        src => 
                            new String($"{umAlQura.GetDayOfMonth(src.EnrollmentDate)}/{umAlQura.GetMonth(src.EnrollmentDate)}/{umAlQura.GetYear(src.EnrollmentDate)}")
                    ));
        }
    }
}

إنشاء العملية Action الجديدة

ننسخ العملية GetEmployee ونلصقها بالتعديلات التالية:

[HttpGet("{id}")]
[MapToApiVersion("1.1")]
public async Task<ActionResult<EmployeesResponse<EmployeeDetailsDto>>> GetEmployeeV1_1(int id)
{            
	_logger.LogInformation("GetEmployee requested");

	string key = "v1.1__" + id;

	if( !_cache.TryGetValue(key, out Models.Employees.v1_1.EmployeeDetailsDto employeeDetailsDto) )
	{
		var employeeEntity = await _repo.GetAsync(id);

		if (employeeEntity != null)                
			employeeDetailsDto = _mapper.Map<Models.Employees.v1_1.EmployeeDetailsDto>(employeeEntity);                

		_cache.Set(
			key,
			employeeDetailsDto,
			new MemoryCacheEntryOptions {
				SlidingExpiration = TimeSpan.FromMinutes(1)
			}
		);
	}

	var response = new EmployeesResponse<Models.Employees.v1_1.EmployeeDetailsDto>()
	{
		IsSuccessful = true,
		Status = (employeeDetailsDto == null) ? 404 : 200,
		Message = (employeeDetailsDto == null) ? "Employee not found" : string.Empty,
		Data = employeeDetailsDto
	};

	if (response.Status == 404) 
		return NotFound( response );
	
	return Ok( response );                
}

التعديل على الـ EmployeesController

نضيف الآن ApiVersion إضافي الى EmployeesController يشير الى أن الـ controller يدعم نسخة 1.1 أيضاً لتصبح كالتالي:

...

namespace aspnetcorewebapiproject.Controllers.v1
{
    [ApiVersion("1.0")]
    [ApiVersion("1.1")]
    [Route("api/v{version:apiVersion}/[controller]")]
    [EnableCors("CorsPolicy")]
    [ApiController]
    public class EmployeesController : ControllerBase
    {
		...

التجربة في Postman

نقوم أولاً بتجربة النسخة الأصلية 1.0 من GetEmployees:

ثم نجرب النسخة الجديدة 1.1:

نلاحظ أن القيمة المسترجعة أخذت بالإعتبار التعديلات الجديدة.

ملاحظة مهمة، ما قمنا به فعلياً هو توفير نسختين من EmployeesController الأولى بالنسخة 1 أو 1.0 والثانية بالنسخة 1.1 ومعنى ذلك أنه بإمكاني إستدعاء جميع العمليات بأحد النسختين وسيعيد الينا نفس القيمة ولكن GetEmployee هي الوحيدة التي تختلف بين النسختين.

فلو قمنا بتجربة GetEmployees بالنسخة 1.0:

هي نفسها لو أستدعينا بالنسخة 1.1 حيث أننا لم نقم بإضافة تطبيق جديد للعملية في نسخة 1.1: