EF Code First #12
آموزش Knockout.Js #1
در یک جمله Knockout.Js یک فریم ورک جاوا اسکریپ است که امکان پیاده سازی الگوی MVVM و مکانیزم data-binding را در پروژههای تحت وب به راحتی میسر میکند. به عبارت دیگر عناصر DOM را به data-model و آبجکتهای data-model را به عناصر DOM مقید میکند، به طوری که با هر تغییر در مقدار یا وضعیت این عناصر یا آبجکت ها، تغییرات به موارد مقید شده نیز اعمال میگردد. به تصاویر زیر دقت کنید!
به روز رسانی data-model بدون استفاده از KO
به روز رسانی data-model با استفاده از KO
ویژگیهای مهم KO
»ارائه یک راه حل بسیار ساده و واضح برای اتصال بخشهای مختلف UI به data-model
»به روز رسانی خودکار عناصر و بخشهای مختلف UI بر اساس تغییرات صورت گرفته در data-model
»به صورت کامل با کتابخانه و توابع javascript پیاده سازی شده است.
»حجم بسیار کم(سیزده کیلو بایت) بعد از فشرده سازی
»سازگار با تمام مروگرهای جدید(... ,IE 6+, Firefox 2+, Chrome, Safari )
»امکان استفاده راحت بدون اعمال تغییرات اساسی در معماری پروژه هایی که در فاز توسعه هستند و بخشی از مسیر توسعه را طی کرده اند
»و...
آیا KO برای تکمیل JQuery در نظر گرفته شده است یا جایگزین؟
»به دست آوردن تعداد divهای موجود با استفاده از یک کلاس مشخص css؛
» یا حتی به دست آوردن تعداد آیتمهای نمایشی در span هایی مشخص.
و البته سایر راه حل ها...
حال فرض کنید دکمههای دیگر نظیر Delete نیز مد نظر باشد که مراحل بالا تکرار خواهند شد. اما با استفاده از KO به راحتی میتوانیم تعداد آیتمهای موجود در یک آرایه را به یک عنصر مشخص bind کنیم به طور با هر تغییر در این مقدار، عنصر مورد نظر نیز به روز میشود یا به بیانی دیگر همواره تغییرات observe خواهند شد. برای مثال:
Number of items :<span data-bind="text: myList().count"></span>
در پست بعد، شروع به کار با KO آموزش داده خواهد شد.
ادامه دارد...
نحوهی سفارشی سازی کلاس پایهی تمام Viewهای برنامه و معرفی inherits@
در نگارشهای پیشین ASP.NET MVC، امکان تعویض کلاس پایهی Viewها، در فایل web.config واقع در پوشهی ریشهی Views وجود داشت. با حذف این فایل و ساده سازی و محول کردن مسئولیتهای آن به فایل جدید view imports، اینبار برای تعریف کلاس پایهی viewها میتوان به صورت ذیل عمل کرد:
using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Razor; namespace Core1RtmEmptyTest.StartupCustomizations { public abstract class MyCustomBaseView<TModel> : RazorPage<TModel> { public bool IsAuthenticated() { return Context.User.Identity.IsAuthenticated; } #pragma warning disable 1998 public override async Task ExecuteAsync() { } #pragma warning restore 1998 } }
پس از تعریف این کلاس، برای ثبت و معرفی آن به فایل ViewImports.cshtml_ مراجعه کنید و این یک سطر را به ابتدای آن اضافه نمائید:
@inherits Core1RtmEmptyTest.StartupCustomizations.MyCustomBaseView<TModel>
برای نمونه پس از سفارشی سازی صفحهی پایهی تمام Viewها، اکنون یک سطر ذیل را در هر view ایی میتوان تعریف و استفاده کرد:
Is Current User Authenticated? @IsAuthenticated()
معرفی functions@
دایرکتیو جدید functions@، بسیار شبیه است به دایرکتیو قدیمی و حذف شدهی helper@، که در نگارشهای پیشین Razor معرفی شده بود:
@functions { public string Test() { return message; } readonly string message = "test"; }
@Test() <br /> @message
معرفی inject@
توسط دایرکتیو جدید inject@، یک خاصیت عمومی به ASP.NET Core اعلام میشود و سپس مقدار دهی آن بر اساس تنظیمات IoC Container برنامه به صورت خودکار صورت خواهد گرفت. برای مثال زمانیکه میخواهیم به سرویس توکار HostingEnvironment در یک View دسترسی پیدا کنیم، میتوان در ابتدای آن نوشت:
@inject Microsoft.AspNetCore.Hosting.IHostingEnvironment Host;
[Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public Microsoft.AspNetCore.Hosting.IHostingEnvironment Host { get; private set; }
اکنون برای استفادهی از آن خواهیم داشت:
<div> Running in @Host.EnvironmentName </div>
به علاوه باید دقت داشت اگر تعریف inject@ فوق را در فایل view import قرار دهیم، این سرویس در اختیار تمام Viewهای برنامه قرار خواهد گرفت و دیگر نیازی به قرار دادن آن در یک کلاس پایهی سفارشی نیست.
یکی از مفیدترین استفادههای از قابلیت تزریق سرویسها در Viewها میتواند دسترسی به سرویس تامین تنظیمات برنامه باشد (که در مورد نحوهی تامین آن در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایلهای config» بیشتر بحث شد):
@inject IOptions<SmtpConfig> Settings;
Blazor 5x - قسمت 31 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 1 - انجام تنظیمات اولیه
using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Text.Json; namespace BlazorWasm.Client.Utils { public class JwtInfo { public IEnumerable<Claim> Claims { set; get; } public DateTime? ExpirationDateUtc { set; get; } public bool IsExpired { set; get; } public IEnumerable<string> Roles { set; get; } } /// <summary> /// From the Steve Sanderson’s Mission Control project: /// https://github.com/SteveSandersonMS/presentation-2019-06-NDCOslo/blob/master/demos/MissionControl/MissionControl.Client/Util/ServiceExtensions.cs /// </summary> public static class JwtParser { public static JwtInfo ParseClaimsFromJwt(string jwt) { var claims = new List<Claim>(); var payload = jwt.Split('.')[1]; var jsonBytes = getBase64WithoutPadding(payload); foreach (var keyValue in JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes)) { if (keyValue.Value is JsonElement element && element.ValueKind == JsonValueKind.Array) { foreach (var itemValue in element.EnumerateArray()) { claims.Add(new Claim(keyValue.Key, itemValue.ToString())); } } else { claims.Add(new Claim(keyValue.Key, keyValue.Value.ToString())); } } var roles = getRoles(claims); var expirationDateUtc = getDateUtc(claims, "exp"); var isExpired = getIsExpired(expirationDateUtc); return new JwtInfo { Claims = claims, Roles = roles, ExpirationDateUtc = expirationDateUtc, IsExpired = isExpired }; } private static IList<string> getRoles(IList<Claim> claims) => claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList(); private static byte[] getBase64WithoutPadding(string base64) { switch (base64.Length % 4) { case 2: base64 += "=="; break; case 3: base64 += "="; break; } return Convert.FromBase64String(base64); } private static bool getIsExpired(DateTime? expirationDateUtc) => !expirationDateUtc.HasValue || !(expirationDateUtc.Value > DateTime.UtcNow); private static DateTime? getDateUtc(IList<Claim> claims, string type) { var exp = claims.SingleOrDefault(claim => claim.Type == type); if (exp == null) { return null; } var expValue = getTimeValue(exp.Value); if (expValue == null) { return null; } var dateTimeEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); return dateTimeEpoch.AddSeconds(expValue.Value); } private static long? getTimeValue(string claimValue) { if (long.TryParse(claimValue, out long resultLong)) return resultLong; if (float.TryParse(claimValue, out float resultFloat)) return (long)resultFloat; if (double.TryParse(claimValue, out double resultDouble)) return (long)resultDouble; return null; } } }
روشهای اصلاح این کد بد بو
- لیست محصولات فروخته شده
- درصد کمیسیون خام
- تاریخ فروش
- شعبه فروش
- نوع پرداخت
public class Salesman { public void Method1() { return; } public void Method2() { return; } public void Method3() { return; } public decimal CalculateCommission(dynamic products, dynamic commissionRate, dynamic saleDate, dynamic branch, dynamic paymentType) { return decimal.MaxValue; } }
public class SalesmanV2 { public IEnumerable<dynamic> Products { get; set; } public dynamic CommisionRate { get; set; } public dynamic SaleDate { get; set; } public dynamic Branch { get; set; } public dynamic PaymentType { get; set; } public void Method1() { return; } public void Method2() { return; } public void Method3() { return; } public decimal CalculateCommission() { return decimal.MaxValue; } }
public class SalesmanV3 { public void Method1() { return; } public void Method2() { return; } public void Method3() { return; } }
public class CommissionCalculator { private IEnumerable<dynamic> _products; private dynamic _commisionRate; private dynamic _saleDate; private dynamic _branch; private dynamic _paymentType; public CommissionCalculator(IEnumerable<dynamic> products, dynamic commisionRate, dynamic saleDate, dynamic branch, dynamic paymentType) { _products = products; _commisionRate = commisionRate; _saleDate = saleDate; _branch = branch; _paymentType = paymentType; } }
جمع بندی
نحوه ذخیره شدن متن در فایلهای PDF
حتما نیاز است پیشنیاز فوق را یکبار مطالعه کنید تا علت خروجیهای متفاوتی را که در ادامه ملاحظه خواهید نمود، بهتر مشخص شوند. همچنین فایل PDF ایی که مورد بررسی قرار خواهد گرفت، همان فایلی است که توسط متد writePdf ذکر شده در پیشنیاز تهیه شده است.
دو کلاس متفاوت برای استخراج متن از فایلهای PDF در iTextSharp وجود دارند:
الف) SimpleTextExtractionStrategy
using System.Diagnostics; using System.IO; using iTextSharp.text; using iTextSharp.text.pdf; using iTextSharp.text.pdf.parser; namespace TestReaders { class Program { private static void readPdf1() { var reader = new PdfReader("test.pdf"); int intPageNum = reader.NumberOfPages; for (int i = 1; i <= intPageNum; i++) { var text = PdfTextExtractor.GetTextFromPage(reader, i, new SimpleTextExtractionStrategy()); File.WriteAllText("page-" + i + "-text.txt", text); } reader.Close(); } static void Main(string[] args) { readPdf1(); } } }
Test ld Wor llo He Hello People
ب) LocationTextExtractionStrategy
همان مثال قبل را درنظر بگیرید، اینبار به شکل زیر:
private static void readPdf2() { var reader = new PdfReader("test.pdf"); int intPageNum = reader.NumberOfPages; for (int i = 1; i <= intPageNum; i++) { var text = PdfTextExtractor.GetTextFromPage(reader, i, new LocationTextExtractionStrategy()); File.WriteAllText("page-" + i + "-text.txt", text); } reader.Close(); }
Test Hello World Hello People
استخراج متون فارسی از فایلهای PDF توسط iTextSharp
روشهای فوق با PDFهای فارسی هم کار میکنند اما خروجی حاصل آن مفهوم نیست و نیاز به پردازش ثانوی دارد. ابتدا مثال زیر را درنظر بگیرید:
static void writePdf2() { using (var document = new Document(PageSize.A4)) { var writer = PdfWriter.GetInstance(document, new FileStream("test.pdf", FileMode.Create)); document.Open(); FontFactory.Register("c:\\windows\\fonts\\tahoma.ttf"); var tahoma = FontFactory.GetFont("tahoma", BaseFont.IDENTITY_H); ColumnText.ShowTextAligned( canvas: writer.DirectContent, alignment: Element.ALIGN_CENTER, phrase: new Phrase("تست میشود", tahoma), x: 100, y: 100, rotation: 0, runDirection: PdfWriter.RUN_DIRECTION_RTL, arabicOptions: 0); } Process.Start("test.pdf"); }
ﺩﻮﺷﻲﻣ ﺖﺴﺗ
private static void readPdf2() { var reader = new PdfReader("test.pdf"); int intPageNum = reader.NumberOfPages; for (int i = 1; i <= intPageNum; i++) { var text = PdfTextExtractor.GetTextFromPage(reader, i, new LocationTextExtractionStrategy()); text = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(text)); File.WriteAllText("page-" + i + "-text.txt", text, Encoding.UTF8); } reader.Close(); }
ﺩﻮﺷﻲﻣ ﺖﺴﺗ
using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Runtime.InteropServices; using System.Security; namespace TestReaders { [SuppressUnmanagedCodeSecurity] class GdiMethods { [DllImport("GDI32.dll")] public static extern bool DeleteObject(IntPtr hgdiobj); [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern uint GetCharacterPlacement(IntPtr hdc, string lpString, int nCount, int nMaxExtent, [In, Out] ref GcpResults lpResults, uint dwFlags); [DllImport("GDI32.dll")] public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj); } [StructLayout(LayoutKind.Sequential)] struct GcpResults { public uint lStructSize; [MarshalAs(UnmanagedType.LPTStr)] public string lpOutString; public IntPtr lpOrder; public IntPtr lpDx; public IntPtr lpCaretPos; public IntPtr lpClass; public IntPtr lpGlyphs; public uint nGlyphs; public int nMaxFit; } public class UnicodeCharacterPlacement { const int GcpReorder = 0x0002; GCHandle _caretPosHandle; GCHandle _classHandle; GCHandle _dxHandle; GCHandle _glyphsHandle; GCHandle _orderHandle; public Font Font { set; get; } public string Apply(string lines) { if (string.IsNullOrWhiteSpace(lines)) return string.Empty; return Apply(lines.Split('\n')).Aggregate((s1, s2) => s1 + s2); } public IEnumerable<string> Apply(IEnumerable<string> lines) { if (Font == null) throw new ArgumentNullException("Font is null."); if (!hasUnicodeText(lines)) return lines; var graphics = Graphics.FromHwnd(IntPtr.Zero); var hdc = graphics.GetHdc(); try { var font = (Font)Font.Clone(); var hFont = font.ToHfont(); var fontObject = GdiMethods.SelectObject(hdc, hFont); try { var results = new List<string>(); foreach (var line in lines) results.Add(modifyCharactersPlacement(line, hdc)); return results; } finally { GdiMethods.DeleteObject(fontObject); GdiMethods.DeleteObject(hFont); font.Dispose(); } } finally { graphics.ReleaseHdc(hdc); graphics.Dispose(); } } void freeResources() { _orderHandle.Free(); _dxHandle.Free(); _caretPosHandle.Free(); _classHandle.Free(); _glyphsHandle.Free(); } static bool hasUnicodeText(IEnumerable<string> lines) { return lines.Any(line => line.Any(chr => chr >= '\u00FF')); } void initializeResources(int textLength) { _orderHandle = GCHandle.Alloc(new int[textLength], GCHandleType.Pinned); _dxHandle = GCHandle.Alloc(new int[textLength], GCHandleType.Pinned); _caretPosHandle = GCHandle.Alloc(new int[textLength], GCHandleType.Pinned); _classHandle = GCHandle.Alloc(new byte[textLength], GCHandleType.Pinned); _glyphsHandle = GCHandle.Alloc(new short[textLength], GCHandleType.Pinned); } string modifyCharactersPlacement(string text, IntPtr hdc) { var textLength = text.Length; initializeResources(textLength); try { var gcpResult = new GcpResults { lStructSize = (uint)Marshal.SizeOf(typeof(GcpResults)), lpOutString = new String('\0', textLength), lpOrder = _orderHandle.AddrOfPinnedObject(), lpDx = _dxHandle.AddrOfPinnedObject(), lpCaretPos = _caretPosHandle.AddrOfPinnedObject(), lpClass = _classHandle.AddrOfPinnedObject(), lpGlyphs = _glyphsHandle.AddrOfPinnedObject(), nGlyphs = (uint)textLength, nMaxFit = 0 }; var result = GdiMethods.GetCharacterPlacement(hdc, text, textLength, 0, ref gcpResult, GcpReorder); return result != 0 ? gcpResult.lpOutString : text; } finally { freeResources(); } } } }
در اینجا برای اصلاح متد readPdf2 خواهیم داشت:
private static void readPdf2() { var reader = new PdfReader("test.pdf"); int intPageNum = reader.NumberOfPages; for (int i = 1; i <= intPageNum; i++) { var text = PdfTextExtractor.GetTextFromPage(reader, i, new LocationTextExtractionStrategy()); text = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(text)); text = new UnicodeCharacterPlacement { Font = new System.Drawing.Font("Tahoma", 12) }.Apply(text); File.WriteAllText("page-" + i + "-text.txt", text, Encoding.UTF8); } reader.Close(); }
سؤال: آیا این روش با تمام PDFهای فارسی کار میکند؟
پاسخ: خیر! همانطور که در پیشنیاز مطلب جاری عنوان شد، در یک حالت خاص، PDF writer میتواند شماره Glyphها را کاملا عوض کرده و در فایل PDF نهایی ثبت کند. خروجی حاصل در برنامه Adobe reader خوانا است، چون نمایش را بر اساس اطلاعات هندسی Glyphها انجام میدهد؛ اما خروجی متنی آن به نوعی obfuscated است چون مثلا حرف A آن به کاراکتر مرسوم دیگری نگاشت شده است.
Target Typing در C# 9.0
مشکلی که بعضیها با واژهی کلیدی var دارند، این است که اندکی خوانایی کدها را کاهش میدهد و در این حالت بلافاصله مشخص نیست که نوع شیء در حال استفاده چیست. در C# 9.0 برای این دسته از برنامه نویسها راه حل دیگری را پیشنهاد دادهاند: نوع ابتدایی را مشخص کنید، اما نیازی به ذکر نوع پس از واژهی کلیدی new نیست و همانند var، خود کامپایلر آنرا حدس خواهد زد! برای توضیح آن دو کلاس سادهی زیر را درنظر بگیرید:
public class Person { public string FirstName { get; set; } } public class PersonWithCtor { public PersonWithCtor(string firstName) { this.FirstName = firstName; } public string FirstName { get; set; } }
Person person = new Person();
var person = new Person();
Person person = new();
Target Typing و پارامترهای سازندهی کلاسها در C# 9.0
در مثال فوق، کلاس PersonWithCtor به همراه یک سازندهی پارامتردار تعریف شدهاست. در این حالت Target Typing آن به صورت زیر خواهد بود:
Person person = new("User 1");
var personList = new List<Person> { new ("User 1"), new ("User 2"), // ... };
public void Adopt(Person p) { //... } public void CallerMethod() { this.Adopt(new Person("User 1")); // C# 9.0 this.Adopt(new("User 1")); }
XmlReader.Create(reader, new XmlReaderSettings() { IgnoreWhitespace = true }); // C# 9.0 XmlReader.Create(reader, new() { IgnoreWhitespace = true });
Target Typing و استفاده از خواص کلاسها در C# 9.0
در همان مثال اول، اگر بخواهیم خاصیت FirstName را مقدار دهی کنیم و همچنین از Target Typing نیز استفاده کنیم ... روش زیر کامپایل نخواهد شد:
Person person = new { FirstName = "User 2" };
Person person = new() { FirstName = "User 2" };
امکان استفادهی از Target typing با فیلدها در C# 9.0
امکان تعریف var با فیلدهای یک کلاس در زبان #C وجود ندارد. به همین جهت مجبور هستیم یک چنین تعاریف طولانی را در سطح کلاسها داشته باشیم:
private ConcurrentDictionary<string, ObservableList<Cat>> _catsBefore = new ConcurrentDictionary<string, ObservableList<Cat>>();
private ConcurrentDictionary<string, ObservableList<Cat>> _cats = new(); // C# 9.0
public ObservableCollection<Friend> Friends { get; } = new();
امکان ترکیب null-coalescing operator با target typing در C# 9.0
null-coalescing operator یا همان ?? به این معنا است که اگر متغیر سمت چپ آن نال نبود، همان مقدار درنظر گرفته شود و اگر نال بود، متغیر سمت راست آن بازگشت داده شود. در این حالت مثال زیر را در نظر بگیرید که در آن سگ و گربه از نوع پایهی حیوان تعریف شدهاند:
public interface IAnimal { } public class Dog : IAnimal { } public class Cat : IAnimal { }
Cat cat = null; Dog dog = new(); IAnimal animal = cat ?? dog;
دانستنیهایی در مورد Target Typing
- نوشتن ()throw new مجاز است و نوع پیشفرض آن، System.Exception در نظر گرفته میشود.
- در حالت کار با tuples، نوشتن new اضافی است:
(int a, int b) t = new(1, 2); // "new" is redundant
(int a, int b) t = new(); // OK; same as (0, 0)
محدودیتهای Target Typing در C# 9.0
- امکان نوشتن ()var dog = new وجود ندارد؛ چون نوع سمت راست این انتساب دیگر قابل حدس زدن نیست. نمونهی دیگر آن anonymous type properties است؛ مانند new { Prop = new() } که در آن برای مثال نوع خاصیت Prop قابل حدس زدن نیست.
- target typing با binary operators قابل استفاده نیست.
- به عنوان ref قابل استفاده نیست.
API Versioning
- URI-based versioning
- Header-based versioning
- Media type-based versioning
پیاده سازی URI-based versioning
public class ItemViewModel { public int Id { get; set; } public string Name { get; set; } public string Country { get; set; } } public class ItemV2ViewModel : ItemViewModel { public double Price { get; set; } }
public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" }; return Ok(viewModel); } } public class ItemsV2Controller : ApiController { [ResponseType(typeof(ItemV2ViewModel))] public IHttpActionResult Get(int id) { var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 }; return Ok(viewModel); } }
config.Routes.MapHttpRoute("ItemsV2", "api/v2/items/{id}", new { controller = "ItemsV2", id = RouteParameter.Optional }); config.Routes.MapHttpRoute("Items", "api/items/{id}", new { controller = "Items", id = RouteParameter.Optional }); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } );
config.Routes.MapHttpRoute( "defaultVersioned", "api/v{version}/{controller}/{id}", new { id = RouteParameter.Optional }, new { version = @"\d+" }); config.Routes.MapHttpRoute( "DefaultApi", "api/{controller}/{id}", new { id = RouteParameter.Optional } );
با این تنظیمات فعلا به مسیریابی ورژن بندی شدهای دست نیافتهایم. زیرا فعلا به هیچ طریق به Web API اشاره نکردهایم که به چه صورت از این پارامتر version برای پیدا کردن کنترلر ورژن بندی شده استفاده کند و به همین دلیل این دو مسیریابی نوشته شده در عمل نتیجه یکسانی را خواهند داشت. برای رفع مشکل مطرح شده باید فرآیند پیش فرض انتخاب کنترلر را کمی شخصی سازی کنیم.
IHttpControllerSelector مسئول پیدا کردن کنترلر مربوطه با توجه به درخواست رسیده میباشد. شکل زیر مربوط است به مراحل ساخت کنترلر بر اساس درخواست رسیده:
به جای پیاده سازی مستقیم این اینترفیس، از پیاده سازی کننده پیش فرض موجود (DefaultHttpControllerSelector) اسفتاده کرده و HttpControllerSelector جدید ما از آن ارث بری خواهد کرد.
public class VersionFinder { private static bool NeedsUriVersioning(HttpRequestMessage request, out string version) { var routeData = request.GetRouteData(); if (routeData != null) { object versionFromRoute; if (routeData.Values.TryGetValue(nameof(version), out versionFromRoute)) { version = versionFromRoute as string; if (!string.IsNullOrWhiteSpace(version)) { return true; } } } version = null; return false; } private static int VersionToInt(string versionString) { int version; if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version)) return 0; return version; } public static int GetVersionFromRequest(HttpRequestMessage request) { string version; return NeedsUriVersioning(request, out version) ? VersionToInt(version) : 0; } }
کلاس VersionFinder برای یافتن ورژن رسیده در درخواست جاری مورد استفاده قرار خواهد گرفت. با استفاده از متد NeedsUriVersioning بررسی صورت میگیرد که آیا در این درخواست پارامتری به نام version وجود دارد یا خیر که درصورت موجود بودن، مقدار آن واکشی شده و درون پارامتر out قرار میگیرد. در متد GetVersionFromRequest بررسی میشود که اگر خروجی متد NeedsUriVersioning برابر با true باشد، با استفاده از متد VersionToInt مقدار به دست آمده را به int تبدیل کند.
public class VersionAwareControllerSelector : DefaultHttpControllerSelector { public VersionAwareControllerSelector(HttpConfiguration configuration) : base(configuration) { } public override string GetControllerName(HttpRequestMessage request) { var controllerName = base.GetControllerName(request); var version = VersionFinder.GetVersionFromRequest(request); return version > 0 ? GetVersionedControllerName(request, controllerName, version) : controllerName; } private string GetVersionedControllerName(HttpRequestMessage request, string baseControllerName, int version) { var versionControllerName = $"{baseControllerName}v{version}"; HttpControllerDescriptor descriptor; if (GetControllerMapping().TryGetValue(versionControllerName, out descriptor)) { return versionControllerName; } throw new HttpResponseException(request.CreateErrorResponse( HttpStatusCode.NotFound, $"No HTTP resource was found that matches the URI {request.RequestUri} and version number {version}")); } }
متد GetControllerName وظیفه بازگشت دادن نام کنترلر را برعهده دارد. ما نیز با لغو رفتار پیش فرض این متد و تهیه نام ورژن بندی شده کنترلر و معرفی این پیاده سازی از IHttpControllerSelector به شکل زیر میتوانیم به Web API بگوییم که به چه صورت از پارامتر version موجود در درخواست استفاده کند.
config.Services.Replace(typeof(IHttpControllerSelector), new VersionAwareControllerSelector(config));
حال با اجرای برنامه به نتیجه زیر خواهیم رسید:
راه حل دوم: برای زمانیکه Attribute Routing مورد استفاده شما است میتوان به راحتی با تعریف قالبهای مناسب مسیریابی، API ورژن بندی شده را ارائه دهید.
[RoutePrefix("api/v1/Items")] public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] [Route("{id:int}")] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" }; return Ok(viewModel); } } [RoutePrefix("api/V2/Items")] public class ItemsV2Controller : ApiController { [ResponseType(typeof(ItemV2ViewModel))] [Route("{id:int}")] public IHttpActionResult Get(int id) { var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 }; return Ok(viewModel); } }
اگر توجه کرده باشید در مثال ما، نامهای کنترلرها متفاوت از هم میباشند. اگر بجای در نظر گرفتن نامهای مختلف برای یک کنترلر در ورژنهای مختلف، آن را با یک نام یکسان درون namespaceهای مختلف احاطه کنیم یا حتی آنها را درون Class Libraryهای جدا نگهداری کنیم، به مشکل "یافت شدن چندین کنترلر که با درخواست جاری مطابقت دارند" برخواهیم خورد. برای حل این موضوع به راه حل سوم توجه کنید.
راه حل سوم: استفاده از یک NamespaceControllerSelector که پیاده سازی دیگری از اینترفیس IHttpControllerSelector میباشد. فرض بر این است که قالب پروژه به شکل زیر میباشد:
کار با پیاده سازی اینترفیس IHttpRouteConstraint آغاز میشود:
public class VersionConstraint : IHttpRouteConstraint { public VersionConstraint(string allowedVersion) { AllowedVersion = allowedVersion.ToLowerInvariant(); } public string AllowedVersion { get; private set; } public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection) { object value; if (values.TryGetValue(parameterName, out value) && value != null) { return AllowedVersion.Equals(value.ToString().ToLowerInvariant()); } return false; } }
کلاس بالا در واقع برای اعمال محدودیت خاصی که در ادامه توضیح داده میشود، پیاده سازی شده است.
متد Match آن وظیفه چک کردن برابری مقدار کلید parameterName موجود در درخواست را با مقدار allowedVersion ای که API از آن پشتیبانی میکند، برعهده دارد. با استفاده از این Constraint مشخص کردهایم که دقیقا چه زمانی باید Route نوشته شده انتخاب شود.
به روش استفاده از این Constraint توجه کنید:
namespace UriBasedVersioning.Namespace.Controllers.V1 { using Models.V1; RoutePrefix("api/{version:VersionConstraint(v1)}/items")] public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] [Route("{id}")] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" }; return Ok(viewModel); } } } namespace UriBasedVersioning.Namespace.Controllers.V2 { using Models.V2; RoutePrefix("api/{version:VersionConstraint(v2)}/items")] public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] [Route("{id}")] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 }; return Ok(viewModel); } } }
با توجه به کد بالا میتوان دلیل استفاده از VersionConstraint را هم درک کرد؛ از آنجایی که ما دو Route شبیه به هم داریم، لذا باید مشخص کنیم که در چه شرایطی و کدام یک از این Routeها انتخاب شود. خوب، اگر برنامه را اجرا کرده و یکی از APIهای بالا را تست کنید، با خطا مواجه خواهید شد؛ زیرا فعلا این Constraint به سیستم Web API معرفی نشده است. تنظیمات زیر را انجام دهید:
var constraintsResolver = new DefaultInlineConstraintResolver(); constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof (VersionConstraint)); config.MapHttpAttributeRoutes(constraintsResolver);
مرحله بعدی کار، پیاده سازی IHttpControllerSelector میباشد:
public class NamespaceControllerSelector : IHttpControllerSelector { private readonly HttpConfiguration _configuration; private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers; public NamespaceControllerSelector(HttpConfiguration config) { _configuration = config; _controllers = new Lazy<Dictionary<string, HttpControllerDescriptor>>(InitializeControllerDictionary); } public HttpControllerDescriptor SelectController(HttpRequestMessage request) { var routeData = request.GetRouteData(); if (routeData == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } var controllerName = GetControllerName(routeData); if (controllerName == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } var version = GetVersion(routeData); if (version == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", version, controllerName); HttpControllerDescriptor controllerDescriptor; if (_controllers.Value.TryGetValue(controllerKey, out controllerDescriptor)) { return controllerDescriptor; } throw new HttpResponseException(HttpStatusCode.NotFound); } public IDictionary<string, HttpControllerDescriptor> GetControllerMapping() { return _controllers.Value; } private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary() { var dictionary = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase); var assembliesResolver = _configuration.Services.GetAssembliesResolver(); var controllersResolver = _configuration.Services.GetHttpControllerTypeResolver(); var controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver); foreach (var controllerType in controllerTypes) { var segments = controllerType.Namespace.Split(Type.Delimiter); var controllerName = controllerType.Name.Remove(controllerType.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length); var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", segments[segments.Length - 1], controllerName); if (!dictionary.Keys.Contains(controllerKey)) { dictionary[controllerKey] = new HttpControllerDescriptor(_configuration, controllerType.Name, controllerType); } } return dictionary; } private static T GetRouteVariable<T>(IHttpRouteData routeData, string name) { object result; if (routeData.Values.TryGetValue(name, out result)) { return (T)result; } return default(T); } private static string GetControllerName(IHttpRouteData routeData) { var subroute = routeData.GetSubRoutes().FirstOrDefault(); var dataTokenValue = subroute?.Route.DataTokens.First().Value; var controllerName = ((HttpActionDescriptor[])dataTokenValue)?.First().ControllerDescriptor.ControllerName.Replace("Controller", string.Empty); return controllerName; } private static string GetVersion(IHttpRouteData routeData) { var subRouteData = routeData.GetSubRoutes().FirstOrDefault(); return subRouteData == null ? null : GetRouteVariable<string>(subRouteData, "version"); } }
سورس اصلی کلاس بالا از این آدرس قابل دریافت است. در تکه کد بالا بخشی که مربوط به چک کردن تکراری بودن آدرس میباشد، برای ساده سازی کار حذف شده است. ولی نکتهی مربوط به SubRoutes که برای واکشی مقادیر پارامترهای مرتبط با Attribute Routing میباشد، اضافه شده است. روال کار به این صورت است که ابتدا RouteData موجود در درخواست را واکشی کرده و با استفاده از متدهای GetControllerName و GetVersion، پارامترهای controller و version را جستجو میکنیم. بعد با استفاده از مقادیر به دست آمده، controllerKey را تشکیل داده و درون کنترلرهای موجود در برنامه به دنبال کنترلر مورد نظر خواهیم گشت.
کارکرد متد InitializeControllerDictionary :
همانطور که میدانید به صورت پیشفرض Web API توجهی به فضای نام مرتبط با کنترلرها ندارد. از طرفی برای پیاده سازی اینترفیس IHttpControllerSelector نیاز است متدی با امضای زیر را داشته باشیم:
public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
لذا در کلاس پیاده سازی شده، خصوصیتی به نام _controllers را که از به صورت Lazy و از نوع بازگشتی متد بالا میباشد، تعریف کردهایم. متد InitializeControllerDictionary کار آماده سازی دادههای مورد نیاز خصوصیت _controllers میباشد. به این صورت که تمام کنترلرهای موجود در برنامه را واکشی کرده و این بار کلیدهای مربوط به دیکشنری را با استفاده از نام کنترلر و آخرین سگمنت فضای نام آن، تولید و درون دیکشنری مورد نظر ذخیره کردهایم.
حال تنظیمات زیر را اعمال کنید:
public static class WebApiConfig { public static void Register(HttpConfiguration config) { var constraintsResolver = new DefaultInlineConstraintResolver(); constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof (VersionConstraint)); config.MapHttpAttributeRoutes(constraintsResolver); config.Services.Replace(typeof(IHttpControllerSelector), new NamespaceControllerSelector(config)); } }
این بار برنامه را اجرا کرده و APIهای مورد نظر را تست کنید؛ بله بدون مشکل کار خواهد کرد.
نکته تکمیلی: سورس مذکور در سایت کدپلکس برای حالتی که از Centralized Routes استفاده میکنید آماده شده است. روش مذکور در این مقاله هم فقط قسمت Duplicate Routes آن را کم دارد که میتوانید اضافه کنید. پیاده سازی دیگری را از این راه حل هم میتوانید داشته باشید.
پیاده سازی Header-based versioning
public class HeaderVersionConstraint : IHttpRouteConstraint { private const string VersionHeaderName = "api-version"; public HeaderVersionConstraint(int allowedVersion) { AllowedVersion = allowedVersion; } public int AllowedVersion { get; } public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection) { if (!request.Headers.Contains(VersionHeaderName)) return false; var version = request.Headers.GetValues(VersionHeaderName).FirstOrDefault(); return VersionToInt(version) == AllowedVersion; } private static int VersionToInt(string versionString) { int version; if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version)) return 0; return version; } }
public sealed class HeaderVersionedRouteAttribute : RouteFactoryAttribute { public HeaderVersionedRouteAttribute(string template) : base(template) { Order = -1; } public int Version { get; set; } public override IDictionary<string, object> Constraints => new HttpRouteValueDictionary { {"", new HeaderVersionConstraint(Version)} }; }
[RoutePrefix("api/items")] public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] [HeaderVersionedRoute("{id}", Version = 1)] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" }; return Ok(viewModel); } } [RoutePrefix("api/items")] public class ItemsV2Controller : ApiController { [ResponseType(typeof(ItemV2ViewModel))] [HeaderVersionedRoute("{id}", Version = 2)] public IHttpActionResult Get(int id) { var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 }; return Ok(viewModel); } }
پیاده سازی Media type-based versioning
GET /api/Items HTTP 1.1 Accept: application/vnd.mediatype.versioning-v1+json GET /api/Items HTTP 1.1 Accept: application/vnd.mediatype.versioning-v2+json
public class MediaTypeVersionConstraint : IHttpRouteConstraint { private const string VersionMediaType = "vnd.mediatype.versioning"; public MediaTypeVersionConstraint(int allowedVersion) { AllowedVersion = allowedVersion; } public int AllowedVersion { get; } public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection) { if (!request.Headers.Accept.Any()) return false; var acceptHeaderVersion = request.Headers.Accept.FirstOrDefault(x => x.MediaType.Contains(VersionMediaType)); //Accept: application/vnd.mediatype.versioning-v1+json if (acceptHeaderVersion == null || !acceptHeaderVersion.MediaType.Contains("-v") || !acceptHeaderVersion.MediaType.Contains("+")) return false; var version = acceptHeaderVersion.MediaType.Between("-v", "+"); return VersionToInt(version)==AllowedVersion; } }
public class AA { public virtual ICollection<CC> Cs { get; set; } } public class BB { public virtual ICollection<CC> Cs { get; set; } } public class CC { public virtual AA AA { get; set; } public virtual long AAId { get; set; } public virtual BB BB { get; set; } public virtual long BBId { get; set; } public virtual string Value{get;set} }