طراحی و پیاده سازی زیرساختی برای مدیریت خطاهای حاصل از Business Rule Validationها در ServiceLayer
public class Request<T> { public Request(T model) { Model = model. }
public T Model { get; } }
public class Response { public bool IsSuccess { get; set; } public MessageCollection Messages { get; set; } } public class HttpResponse : Response { public HttpStatusCode StatusCode { get; set; } }
public class Response<T> : Response { public T Result { get; set; } }
public class HttpResponse<T> : HttpResponse { public T Result { get; set; } }
الف) فعال سازی ارائهی فایلهای استاتیک
ب) فعال سازی ASP.NET MVC
ج) آشنایی با تغییرات مسیریابی
و مابقی آن صرفا یک سری نکات تکمیلی هستند که در ادامه آنها را بررسی خواهیم کرد.
تعریف مسیریابی کلی کنترلر
در اینجا همانند مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 9 - بررسی تغییرات مسیریابی»، میتوان در صورت نیاز، مسیریابی کلی کنترلر را توسط ویژگی Route بازنویسی کرد و برای مثال درخواستهای آنرا محدود به درخواستهایی کرد که با api/ شروع شوند:
[Route("api/[controller]")] // http://localhost:7742/api/test public class TestController : Controller { private readonly ILogger<TestController> _logger; public TestController(ILogger<TestController> logger) { _logger = logger; }
در مورد سرویس ثبت وقایع نیز در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 17 - بررسی فریم ورک Logging» بحث کردیم و از آن میتوان برای ثبت استثناءهای رخ داده استفاده کرد.
یک کنترلر ، اما با قابلیتهای متعدد
همانطور که ملاحظه میکنید، اینبار کلاس پایهی این کنترلر Test، همان Controller متداول ASP.NET MVC ذکر شدهاست و نه Api Controller سابق. تمام قابلیتهای موجود در ایندو توسط همان Controller ارائه میشوند.
هنوز پیش فرضهای سابق Web API برقرار هستند
در مثال ذیل که به نظر یک کنترلر ASP.NET MVC است،
- هنوز متد Get مربوط به Web API که به صورت پیش فرض به درخواستهای Get ختم شدهی به نام کنترلر پاسخ میدهد، برقرار است (متد IEnumerable<string> Get). برای مثال اگر شخصی در مرورگر، آدرس http://localhost:7742/api/test را درخواست دهد، متد Get اجرا میشود.
- در اینجا میتوان نوع خروجی متد را دقیقا از همان نوع اشیاء مدنظر، تعیین کرد؛ برای نمونه تعریف <IEnumerable<string در مثال زیر.
- مهم نیست که از return Json استفاده کنید و یا خروجی را مستقیما با فرمت <IEnumerable<string ارائه دهید.
- اگر نیاز به کنترل بیشتری بر روی HTTP Response Status بازگشتی داشتید، میتوانید از متدهایی مانند return Ok و یا return BadRequest در صورت بروز مشکلی استفاده نمائید. برای مثال در متد IActionResult GetEpisodes2، استثنای فرضی حاصل، ابتدا توسط سرویس ثبت وقایع ذخیره شده و در آخر یک BadRequest بازگشت داده میشود.
- تمام مسیریابیها را توسط ویژگی Route و یا نوعهای درخواستی مانند HttpGet، میتوان بازنویسی کرد؛ مانند مسیر /api/path1
- امکان محدود ساختن نوع پارامترهای دریافتی همانند متد Get(int page) ذیل، توسط ویژگیهای مسیریابی وجود دارد.
[Route("api/[controller]")] // http://localhost:7742/api/test public class TestController : Controller { private readonly ILogger<TestController> _logger; public TestController(ILogger<TestController> logger) { _logger = logger; } [HttpGet] public IEnumerable<string> Get() // http://localhost:7742/api/test { return new [] { "value1", "value2" }; } [HttpGet("{page:int}")] public IActionResult Get(int page) // http://localhost:7742/api/test/1 { return Json(new[] { "value3", "value4" }); } [HttpGet("/api/path1")] public IActionResult GetEpisodes1() // http://localhost:7742/api/path1 { return Json(new[] { "value5", "value6" }); } [HttpGet("/api/path2")] public IActionResult GetEpisodes2() // http://localhost:7742/api/path2 { try { // get data from the DB ... return Ok(new[] { "value7", "value8" }); } catch (Exception ex) { _logger.LogError("Failed to get data from the API", ex); return BadRequest(); } } }
[Route("api/[controller]")] public class ValuesController : Controller { // GET: api/values [HttpGet] public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } // GET api/values/5 [HttpGet("{id}")] public string Get(int id) { return "value"; } // POST api/values [HttpPost] public void Post([FromBody]string value) { } // PUT api/values/5 [HttpPut("{id}")] public void Put(int id, [FromBody]string value) { } // DELETE api/values/5 [HttpDelete("{id}")] public void Delete(int id) { } } }
یک نکته: اگر میخواهید خروجی Web API شما همواره JSON باشد، میتوانید ویژگی جدید Produces را به شکل ذیل به کلاس کنترلر اعمال کنید:
[Produces("application/json")] [Route("api/[controller]")] // http://localhost:7742/api/test public class TestController : Controller
تغییرات Model binding پیش فرض، برای پشتیبانی از ASP.NET MVC و ASP.NET Web API
فرض کنید مدل زیر را به برنامه اضافه کردهاید:
namespace Core1RtmEmptyTest.Models { public class Person { public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } } }
using Core1RtmEmptyTest.Models; using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class PersonController : Controller { public IActionResult Index() { return View(); } [HttpPost] public IActionResult Index(Person person) { return Json(person); } } }
@section scripts { <script type="text/javascript"> $(function () { $.ajax({ type: 'POST', url: '/Person/Index', dataType: 'json', contentType: 'application/json; charset=utf-8', data: JSON.stringify({ FirstName: 'F1', LastName: 'L1', Age: 23 }), success: function (result) { console.log('Data received: '); console.log(result); } }); }); </script> }
همانطور که مشاهده میکنید، اگر در ابتدای این متد یک break-point قرار دهیم، اطلاعاتی را از سمت کاربر دریافت نکردهاست و مقادیر دریافتی نال هستند.
این مورد یکی از مهمترین تغییرات Model binding این نگارش از ASP.NET MVC با نگارشهای قبلی آن است. در اینجا اشیاء پیچیده از request body دریافت و bind نمیشوند و باید به نحو ذیل، محل دریافت و تفسیر آنها را دقیقا مشخص کرد:
public IActionResult Index([FromBody]Person person)
نکتهی مهم: حتی اگر FromBody را ذکر کنید ولی از JSON.stringify در سمت کاربر استفاده نکنید، باز هم نال دریافت خواهید کرد. بنابراین در این نگارش ذکر JSON.stringify نیز الزامی است.
حالتهای دیگر تغییرات Model Binding در ASP.NET Core
تا اینجا مشخص شد که اگر یک درخواست Ajax ایی را به سمت سرور یک برنامهی ASP.NET Core ارسال کنیم، به صورت پیش فرض به اشیاء پیچیدهی سمت سرور bind نمیشود و باید حتما ویژگی FromBody را نیز مشخص کرد تا اطلاعات را از request body واکشی کند (محل دریافت اطلاعات پیش فرض آن نامشخص است).
یک سؤال: اگر به سمت یک چنین اکشن متدی، اطلاعات فرمی را به حالت معمول ارسال کنیم، چه اتفاقی رخ خواهد داد؟
ارسال اطلاعات فرمها به سرور، همواره شامل دو تغییر ذیل است:
var dataType = 'application/x-www-form-urlencoded; charset=utf-8'; var data = $('form').serialize();
[HttpPost] public IActionResult Index([FromForm]Person person)
علت این مساله نیز بالا رفتن میزان امنیت سیستم است. در نگارشهای قبلی، تمام مکانها و حالتهای میسر جستجو میشوند و اگر یکی از آنها قابلیت تطابق با خواص شیء مدنظر را داشته باشد، کار binding به پایان میرسد. اما در اینجا با مشخص شدن محل دقیق منبع اطلاعات، دیگر سایر حالات جستجو نشده و سطح حمله کاهش پیدا میکند.
در اینجا باید مشخص کرد که دقیقا اطلاعاتی که قرار است به یک شیء پیچیده Bind شوند، آیا از یک Form تامین میشوند، یا از Body و یا از هدر، کوئری استرینگ، مسیریابی و یا حتی از یک سرویس.
تمام این حالتها مشخص هستند (برای مثال دریافت اطلاعات از هدر درخواست HTTP و انتساب آنها به خواص متناظری در شیء مشخص شده)، منهای FromService آن که به نحو ذیل عمل میکند:
در این حالت میتوان در سازندهی کلاس مدل خود، سرویسی را تزریق کرد و توسط آن خاصیتی را مقدار دهی نمود:
public class ProductModel { public ProductModel(IProductService prodService) { Value = prodService.Get(productId); } public IProduct Value { get; private set; } }
public async Task<IActionResult> GetProduct([FromServices]ProductModel product) { }
تغییر تنظیمات اولیهی خروجیهای ASP.NET Web API
در اینجا حالت ارائهی خروجی XML به صورت پیش فرض فعال نیست. اگر علاقمند به افزودن آن نیز باشید، نحوهی کار را در متد ConfigureServices کلاس آغازین برنامه در کدهای ذیل مشاهده میکنید:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(options => { options.FormatterMappings.SetMediaTypeMappingForFormat("xml", new MediaTypeHeaderValue("application/xml")); }).AddJsonOptions(options => { options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); options.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Include; options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; });
Long Polling در WCF
در این حالت thread جاری سمت کلاینت نیز در حالت wait است و برنامه سمت کلاینت از کار میافتد تا زمانی که پاسخ از سرور دریافت نماید. اما در WCF به صورت پیش فرض هر درخواست ارسالی باید در طی یک دقیقه در اختیار سرور قرار گیرد و سرور نیز باید در طی یک دقیقه پاسخ مورد نظر را برگرداند(مقادیر خواص SendTimeout و ReceiveTimeout برای مدیریت این موارد به کار میروند). افزایش مقادیر این خواص کمک خاصی به این حالت نمیکند زیرا هم چنان کلاینت در حالت wait است و سرور نیز پاسخ خاصی ارسال نمیکند. حتی اگر کل عملیات را به صورت Async پیاده سازی نماییم باز ممکن است بعد از منقضی شدن زمان پردازش با یک TimeoutException برنامه از کار بیفتد. برای حل اینگونه موارد پیاده سازی سرویسها به صورت Long Polling به ما کمک خوبی خواهد کرد.
حال سناریو زیر را در نظر بگیرید:
سمت سرور:
»یک درخواست دریافت میشود؛
»سرور در حالت wait (البته توسط یک thread دیگر) منتظر تامین منابع برای پاسخ به کلاینت است؛
»در نهایت پاسخ مورد نظر ارسال خواهد شد.
سمت کلاینت:
»درخواست مورد نظر به سرور ارسال میشود؛
»کلاینت منتظر پاسخ از سمت سرور است(البته توسط یک Thread دیگر)؛
»اگر در حین انتظار برای پاسخ از سمت سرور، با یک TimeoutException روبرو شدیم به جای توقف برنامه و نمایش پیغام خطای Server is not available، باید عملیات به صورت خودکار restart شود.
»در نهایت پاسخ مورد نظر دریافت خواهد شد.
پیاده سازی این سناریو در WCF کار پیچیده ای نیست. بدین منظور میتوانید از کلاس زیر استفاده کنید( لینک دانلود ). سورس آن به صورت زیر است:
public abstract class LongPollingAsyncResult<TResult> : IAsyncResult where TResult : class { #region - Fields - private AsyncCallback _callback; private TimeSpan _timoutSpan; private TimeSpan _intervalWaitSpan; #endregion #region - Properties - public Exception Exception { get; private set; } public TResult Result { get; private set; } public object SyncRoot { get; private set; } #endregion #region - Ctor - public LongPollingAsyncResult(AsyncCallback callback, object asyncState, int timeoutSeconds = 300, int intervalWaitMilliseconds = 500) { SyncRoot = new object(); _callback = callback; AsyncState = asyncState; AsyncWaitHandle = new ManualResetEvent(IsCompleted); _timoutSpan = TimeSpan.FromSeconds(timeoutSeconds); _intervalWaitSpan = TimeSpan.FromMilliseconds(intervalWaitMilliseconds); ThreadPool.QueueUserWorkItem(new WaitCallback(LoopWithIntervalAndTimeout)); } #endregion #region - Private Helper Methods - private void LoopWithIntervalAndTimeout(object input) { try { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); while (!IsCompleted) { if (stopwatch.Elapsed > _timoutSpan) throw new TimeoutException(); DoWork(); if (!IsCompleted) Thread.Sleep(_intervalWaitSpan); } } catch (Exception e) { Complete(null, e); } } #endregion #region - Protected/Abstract Methods - protected void Complete(TResult result, Exception e = null, bool completedSynchronously = false) { lock (SyncRoot) { CompletedSynchronously = completedSynchronously; Result = result; Exception = e; IsCompleted = true; if (_callback != null) _callback(this); } } protected abstract void DoWork(); #endregion #region - Public Methods - public TResult WaitForResult() { if (!IsCompleted) AsyncWaitHandle.WaitOne(); if (Exception != null) { if (Exception is TimeoutException && WebOperationContext.Current != null) WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.RequestTimeout; throw Exception; } return Result; } #endregion #region - IAsyncResult Implementation - public object AsyncState { get; private set; } public WaitHandle AsyncWaitHandle { get; private set; } public bool CompletedSynchronously { get; private set; } public bool IsCompleted { get; private set; } #endregion }
برای استفاده از کلاس تهیه شده ابتدا باید عملیات خود را به صورت Async پیاده سازی نمایید که در این مقاله به صورت کامل شرح داده شده است.
یک مثال
قصد داریم Operation زیر را به صورت Long Polling پیاده سازی نماییم:
[OperationContract] public string GetNotification();
[OperationContract(AsyncPattern = true)] public IAsyncResult BeginWaitNotification(AsyncCallback callback, object state); public string EndWaitNotification(IAsyncResult result);
public class MyNotificationResult : LongPollingAsyncResult<string> { protected override DoWork() { کدهای مورد نظر را اینجا قرار دهید base.Complete(...) } }
public IAsyncResult BeginWaitNotification(AsyncCallback callback, object state) { return new MyNotificationResult(callback, state); } public string EndWaitNotification(IAsyncResult result) { MyNotificationResult myResult = result as MyNotificationResult; if(myResult == null) throw new ArgumentException("result was of the wrong type!"); myResult.WaitForResult(); return myResult.Result; }
تدارک یک آزمایش برای بررسی میزان افزایش کارآیی Reflection در دات نت 7
یک برنامهی کنسول جدید را ایجاد کرده و سپس کلاس Person را به صورت زیر به آن اضافه میکنیم:
namespace NET7Reflection; public class Person { private int _age; internal Person(int age) => _age = age; private int GetAge() => _age; private void SetAge(int age) => _age = age; }
به همین جهت ابتدا کتابخانهی BenchmarkDotNet را با TargetFramework دات نت 7 به صورت زیر به پروژه اضافه میکنیم:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net7.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="BenchmarkDotNet" Version="0.13.4" /> </ItemGroup> </Project>
using System.Reflection; using BenchmarkDotNet.Attributes; namespace NET7Reflection; [MemoryDiagnoser(false)] public class Benchmarks { private readonly object?[] _ageParams = { 30 }; private readonly ConstructorInfo _ctor = typeof(Person).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new[] { typeof(int) })!; private readonly MethodInfo _getAgeMethod = typeof(Person).GetMethod("GetAge", BindingFlags.NonPublic | BindingFlags.Instance)!; private readonly Person _person = new(10); private readonly MethodInfo _setAgeMethod = typeof(Person).GetMethod("SetAge", BindingFlags.NonPublic | BindingFlags.Instance)!; [Benchmark] public int GetAge() => (int)typeof(Person).GetMethod("GetAge", BindingFlags.NonPublic | BindingFlags.Instance)! .Invoke(_person, Array.Empty<object?>())!; [Benchmark] public int GetAgeCachedMethod() => (int)_getAgeMethod.Invoke(_person, Array.Empty<object?>())!; [Benchmark] public void SetAge() => typeof(Person).GetMethod("SetAge", BindingFlags.NonPublic | BindingFlags.Instance)! .Invoke(_person, new object?[] { 30 }); [Benchmark] public void SetAgeCachedMethod() => _setAgeMethod.Invoke(_person, new object?[] { 30 }); [Benchmark] public void SetAgeCachedMethodCachedParams() => _setAgeMethod.Invoke(_person, _ageParams); [Benchmark] public Person Ctor() => (Person)typeof(Person).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new[] { typeof(int) })! .Invoke(_person, new object?[] { 30 })!; [Benchmark] public Person CtorCachedCtorInfo() => (Person)_ctor.Invoke(_person, new object?[] { 30 })!; [Benchmark] public Person CtorCachedCtorInfoCachedParams() => (Person)_ctor.Invoke(_person, _ageParams)!; }
- در اینجا نحوهی کار با متدهای خصوصی کلاس Person را توسط Reflection مشاهده میکنید. برای مثال در متد GetAge، به نحو متداولی این کار صورت گرفتهاست. در متد GetAgeCachedMethod، قسمت دریافت اطلاعات متد، کش شدهاست و برای نمونه در متد SetAgeCachedMethodCachedParams، هم کش شدن قسمت دریافت اطلاعات متد را مشاهده میکنید و هم کش شدن پارامتر ارسالی به آنرا.
- این آزمایش را با فراخوانی زیر و تنظیم target framework به دات نت 6 و سپس دات نت 7، به صورت جداگانهای اجرا میکنیم:
using BenchmarkDotNet.Running; using NET7Reflection; BenchmarkRunner.Run<Benchmarks>();
و با target framework دات نت 7 به صورت زیر:
همانطور که مشاهده میکنید، در اکثر موارد، کارآیی Reflection در دات نت 7، حداقل 2 برابر نمونهی مشابه دات نت 6 است.
چه تغییری در دات نت 7 سبب بهبود قابل ملاحظهی کارآیی Reflection شدهاست؟
جزئیات تغییرات صورت گرفتهی در Reflection دات نت 7 را میتوانید در این pull request مشاهده کنید که در حقیقت از امکانات سطح پایین IL Emit استفاده کردهاند. در این مورد پیشتر تعدادی مطلب ذیل عنوان «آشنایی با Reflection.Emit» در این سایت منتشر شدهاند که میتوانید آنها را بررسی کنید.
در کل هرچند تغییرات جدید دات نت مانند ارائهی انواع و اقسام source generators، در تعدادی از موارد نیاز به Reflection را کمتر کردهاند و کارآیی بیشتری را ارائه دادهاند، اما Reflection هیچگاه منسوخ نخواهد شد و هرگونه بهبود کارآیی در این زمینه، بر روی کل فریمورک و برنامههای مشتق شدهی از آن، تاثیر قابل توجهی را خواهد گذاشت.
سرویس ما متدهای زیر را در دسترس قرار میدهد.
Relative URl | HTTP method | Action |
api/products/ | GET | گرفتن لیست تمام محصولات |
api/products/id/ | GET | گرفتن یک محصول بر اساس شناسه |
api/products?category=category/ | GET | گرفتن یک محصول بر اساس طبقه بندی |
api/products/ | POST | ایجاد یک محصول جدید |
api/products/id/ | PUT | بروز رسانی یک محصول |
api/products/id/ | DELETE | حذف یک محصول |
همانطور که مشاهده میکنید برخی از آدرس ها، شامل شناسه محصول هم میشوند. بعنوان مثال برای گرفتن محصولی با شناسه 28، کلاینت یک درخواست GET را به آدرس زیر ارسال میکند:
http://hostname/api/products/28
منابع
سرویس ما آدرس هایی برای دستیابی به دو نوع منبع (resource) را تعریف میکند:
URI | Resource |
api/products/ | لیست تمام محصولات |
api/products/id/ | یک محصول مشخص |
متد ها
چهار متد اصلی HTTP یعنی همان GET, PUT, POST, DELETE میتوانند بصورت زیر به عملیات CRUD نگاشت شوند:
- متد GET یک منبع (resource) را از آدرس تعریف شده دریافت میکند. متدهای GET هیچگونه تاثیری روی سرور نباید داشته باشند. مثلا حذف رکوردها با متد اکیدا اشتباه است.
- متد PUT یک منبع را در آدرس تعریف شده بروز رسانی میکند. این متد برای ساختن منابع جدید هم میتواند استفاده شود، البته در صورتی که سرور به کلاینتها اجازه مشخص کردن آدرسهای جدید را بدهد. در مثال جاری پشتیبانی از ایجاد منابع توسط متد PUT را بررسی نخواهیم کرد.
- متد POST منبع جدیدی میسازد. سرور آدرس آبجکت جدید را تعیین میکند و آن را بعنوان بخشی از پیام Response بر میگرداند.
- متد DELETE منبعی را در آدرس تعریف شده حذف میکند.
نکته: متد PUT موجودیت محصول (product entity) را کاملا جایگزین میکند. به بیان دیگر، از کلاینت انتظار میرود که آبجکت کامل محصول را برای بروز رسانی ارسال کند. اگر میخواهید از بروز رسانیهای جزئی/پاره ای (partial) پشتیبانی کنید متد PATCH توصیه میشود. مثال جاری متد PATCH را پیاده سازی نمیکند.
یک پروژه Web API جدید بسازید
ویژوال استودیو را باز کنید و پروژه جدیدی از نوع ASP.NET MVC Web Application بسازید. نام پروژه را به "ProductStore" تغییر دهید و OK کنید.
در دیالوگ New ASP.NET Project قالب Web API را انتخاب کرده و تایید کنید.
افزودن یک مدل
یک مدل، آبجکتی است که داده اپلیکیشن شما را نمایندگی میکند. در ASP.NET Web API میتوانید از آبجکتهای Strongly-typed بعنوان مدل هایتان استفاده کنید که بصورت خودکار برای کلاینت به فرمتهای JSON, XML مرتب (Serialize) میشوند. در مثال جاری، دادههای ما محصولات هستند. پس کلاس جدیدی بنام Product میسازیم.
در پوشه Models کلاس جدیدی با نام Product بسازید.
حال خواص زیر را به این کلاس اضافه کنید.
namespace ProductStore.Models { public class Product { public int Id { get; set; } public string Name { get; set; } public string Category { get; set; } public decimal Price { get; set; } } }
افزودن یک مخزن
ما نیاز به ذخیره کردن کلکسیونی از محصولات داریم، و بهتر است این کلکسیون از پیاده سازی سرویس تفکیک شود. در این صورت بدون نیاز به بازنویسی کلاس سرویس میتوانیم منبع دادهها را تغییر دهیم. این نوع طراحی با نام الگوی مخزن یا Repository Pattern شناخته میشود. برای شروع نیاز به یک قرارداد جنریک برای مخزنها داریم.
روی پوشه Models کلیک راست کنید و گزینه Add, New Item را انتخاب نمایید.
نوع آیتم جدید را Interface انتخاب کنید و نام آن را به IProductRepository تغییر دهید.
حال کد زیر را به این اینترفیس اضافه کنید.
namespace ProductStore.Models { public interface IProductRepository { IEnumerable<Product> GetAll(); Product Get(int id); Product Add(Product item); void Remove(int id); bool Update(Product item); } }
namespace ProductStore.Models { public class ProductRepository : IProductRepository { private List<Product> products = new List<Product>(); private int _nextId = 1; public ProductRepository() { Add(new Product { Name = "Tomato soup", Category = "Groceries", Price = 1.39M }); Add(new Product { Name = "Yo-yo", Category = "Toys", Price = 3.75M }); Add(new Product { Name = "Hammer", Category = "Hardware", Price = 16.99M }); } public IEnumerable<Product> GetAll() { return products; } public Product Get(int id) { return products.Find(p => p.Id == id); } public Product Add(Product item) { if (item == null) { throw new ArgumentNullException("item"); } item.Id = _nextId++; products.Add(item); return item; } public void Remove(int id) { products.RemoveAll(p => p.Id == id); } public bool Update(Product item) { if (item == null) { throw new ArgumentNullException("item"); } int index = products.FindIndex(p => p.Id == item.Id); if (index == -1) { return false; } products.RemoveAt(index); products.Add(item); return true; } } }
مخزن ما لیست محصولات را در حافظه محلی نگهداری میکند. برای مثال جاری این طراحی کافی است، اما در یک اپلیکیشن واقعی دادههای شما در یک دیتابیس یا منبع داده ابری ذخیره خواهند شد. همچنین استفاده از الگوی مخزن، تغییر منبع دادهها در آینده را راحتتر میکند.
افزودن یک کنترلر Web API
اگر قبلا با ASP.NET MVC کار کرده باشید، با مفهوم کنترلرها آشنایی دارید. در ASP.NET Web API کنترلرها کلاس هایی هستند که درخواستهای HTTP دریافتی از کلاینت را به اکشن متدها نگاشت میکنند. ویژوال استودیو هنگام ساختن پروژه شما دو کنترلر به آن اضافه کرده است. برای مشاهد آنها پوشه Controllers را باز کنید.
- HomeController یک کنترلر مرسوم در ASP.NET MVC است. این کنترلر مسئول بکار گرفتن صفحات وب است و مستقیما ربطی به Web API ما ندارد.
- ValuesController یک کنترلر نمونه WebAPI است.
کنترلر ValuesController را حذف کنید، نیازی به این آیتم نخواهیم داشت. حال برای اضافه کردن کنترلری جدید مراحل زیر را دنبال کنید.
در پنجره Solution Explorer روی پوشه Controllers کلیک راست کرده و گزینه Add, Controller را انتخاب کنید.
در دیالوگ Add Controller نام کنترلر را به ProductsController تغییر داده و در قسمت Scaffolding Options گزینه Empty API Controller را انتخاب کنید.
حال فایل کنترلر جدید را باز کنید و عبارت زیر را به بالای آن اضافه نمایید.
using ProductStore.Models;
public class ProductsController : ApiController { static readonly IProductRepository repository = new ProductRepository(); }
فراخوانی ()new ProductRepository طراحی جالبی نیست، چرا که کنترلر را به پیاده سازی بخصوصی از این اینترفیس گره میزند. بهتر است از تزریق وابستگی (Dependency Injection) استفاده کنید. برای اطلاعات بیشتر درباره تکنیک DI در Web API به این لینک مراجعه کنید.
گرفتن منابع
ProductStore API اکشنهای متعددی در قالب متدهای HTTP GET در دسترس قرار میدهد. هر اکشن به متدی در کلاس ProductsController مرتبط است.
Relative URl | HTTP Method | Action |
api/products/ | GET | دریافت لیست تمام محصولات |
api/products/id/ | GET | دریافت محصولی مشخص بر اساس شناسه |
api/products?category=category/ | GET | دریافت محصولات بر اساس طبقه بندی |
برای دریافت لیست تمام محصولات متد زیر را به کلاس ProductsController اضافه کنید.
public class ProductsController : ApiController { public IEnumerable<Product> GetAllProducts() { return repository.GetAll(); } // .... }
برای دریافت محصولی مشخص بر اساس شناسه آن متد زیر را اضافه کنید.
public Product GetProduct(int id) { Product item = repository.Get(id); if (item == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } return item; }
نام این متد هم با "Get" شروع میشود اما پارامتری با نام id دارد. این پارامتر به قسمت id مسیر درخواست شده (request URl) نگاشت میشود. تبدیل پارامتر به نوع داده مناسب (در اینجا int) هم بصورت خودکار توسط فریم ورک ASP.NET Web API انجام میشود.
متد GetProduct در صورت نامعتبر بودن پارامتر id استثنایی از نوع HttpResponseException تولید میکند. این استثنا بصورت خودکار توسط فریم ورک Web API به خطای 404 (Not Found) ترجمه میشود.
در آخر متدی برای دریافت محصولات بر اساس طبقه بندی اضافه کنید.
public IEnumerable<Product> GetProductsByCategory(string category) { return repository.GetAll().Where( p => string.Equals(p.Category, category, StringComparison.OrdinalIgnoreCase)); }
اگر آدرس درخواستی پارامترهای query string داشته باشد، Web API سعی میکند پارامترها را با پارامترهای متد کنترلر تطبیق دهد. بنابراین درخواستی به آدرس "api/products?category=category" به این متد نگاشت میشود.
ایجاد منبع جدید
قدم بعدی افزودن متدی به ProductsController برای ایجاد یک محصول جدید است. لیست زیر پیاده سازی ساده ای از این متد را نشان میدهد.
// Not the final implementation! public Product PostProduct(Product item) { item = repository.Add(item); return item; }
- نام این متد با "Post" شروع میشود. برای ساختن محصولی جدید کلاینت یک درخواست HTTP POST ارسال میکند.
- این متد پارامتری از نوع Product میپذیرد. در Web API پارامترهای پیچیده (complex types) بصورت خودکار با deserialize کردن بدنه درخواست بدست میآیند. بنابراین در اینجا از کلاینت انتظار داریم که آبجکتی از نوع Product را با فرمت XML یا JSON ارسال کند.
پیاده سازی فعلی این متد کار میکند، اما هنوز کامل نیست. در حالت ایده آل ما میخواهیم پیام HTTP Response موارد زیر را هم در بر گیرد:
- Response code: بصورت پیش فرض فریم ورک Web API کد وضعیت را به 200 (OK) تنظیم میکند. اما طبق پروتکل HTTP/1.1 هنگامی که یک درخواست POST منجر به ساخته شدن منبعی جدید میشود، سرور باید با کد وضعیت 201 (Created) پاسخ دهد.
- Location: هنگامی که سرور منبع جدیدی میسازد، باید آدرس منبع جدید را در قسمت Location header پاسخ درج کند.
ASP.NET Web API دستکاری پیام HTTP response را آسان میکند. لیست زیر پیاده سازی بهتری از این متد را نشان میدهد.
public HttpResponseMessage PostProduct(Product item) { item = repository.Add(item); var response = Request.CreateResponse<Product>(HttpStatusCode.Created, item); string uri = Url.Link("DefaultApi", new { id = item.Id }); response.Headers.Location = new Uri(uri); return response; }
متد CreateResponse آبجکتی از نوع HttpResponseMessage میسازد و بصورت خودکار آبجکت Product را مرتب (serialize) کرده و در بدنه پاسخ مینویسد. نکته دیگر آنکه مثال جاری، مدل را اعتبارسنجی نمیکند. برای اطلاعات بیشتر درباره اعتبارسنجی مدلها در Web API به این لینک مراجعه کنید.
بروز رسانی یک منبع
بروز رسانی یک محصول با PUT ساده است.
public void PutProduct(int id, Product product) { product.Id = id; if (!repository.Update(product)) { throw new HttpResponseException(HttpStatusCode.NotFound); } }
حذف یک منبع
برای حذف یک محصول متد زیر را به کلاس ProductsController اضافه کنید.
public void DeleteProduct(int id) { Product item = repository.Get(id); if (item == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } repository.Remove(id); }
در مثال جاری متد DeleteProduct نوع void را بر میگرداند، که فریم ورک Web API آن را بصورت خودکار به کد وضعیت 204 (No Content) ترجمه میکند.
برای اینکه بتوانیم ظاهر گرافیکی layoutها را کنترل نماییم، از Theme که مجموعهای از styleهای گرافیکی میباشد، استفاده میکنیم. در اندروید مجموعهای از تمهای از پیش ساخته شده که به آنها Builtin Theme نیز گفته میشود میتوانیم استفاده کنیم. تمها ظاهر گرافیکی کلیه کنترلهای Layout را با نامهای زیر، کنترل میکنند:
statusBarColor textColorPrimary colorAccent ColorPrimary WindowBackground
اگر بخواهیم از styleهای از پیش طراحی شدهی اندروید استفاده نماییم، ابتدا میتوانیم در صفحهی layout در زامارین، style مربوطه را از بخش Theme استفاده کرده و نتیجه را مشاهده کنیم. ولی تغییر style سبب تغییر layout در زمان اجرا نمیشود. هرگاه بخواهیم از styleهای استاندارد یا builtin اندروید استفاده نماییم، در Activity توسط خصوصیت Theme با فرمت:
[Activity(Theme = "@style/NameThem")]
در طراحی فرمها ممکن است بخواهیم از یک استایل خاص builtin استفاده کنیم؛ ولی ممکن است بعضی از استایلهای آن را استفاده نکنیم، مانند تمی که از قبل استفاده شدهاست، از روش زیر استفاده میکنیم:
- بر روی دایرکتوری value راست کلیک میکنیم. گزینه add new item را انتخاب و یک فایل xmlfile را با نام style ایجاد میکنیم.
- styleهای جدید منابع application میباشند که در بخش resource از آنها استفاده میکنیم. هر استایل جدید را توسط Style Tag مشخص میکنیم و در خصوصیت Name، نام Style را مشخص میکنیم.
[Activity(Theme = "@style/NameThem")]
<?xml version="1.0" encoding="utf-8" ?> <resources> <style name="MainTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="windowNoTitle">true</item> <item name="windowActionBar">false</item> </style> </resources>
Navigation Menu
ساخت Menu
منظور همان اضافه کردن کتابخانهها است.
قدم دوم:
ابتدا باید گزینههای منو را در یک فایل xml تعریف نمود. هر گزینهی آن از دو بخش متن اصلی منو و ID منو تشکیل شدهاست.
<menu xmlns:android="http://schemas.android.com/apk/res/android">
خصوصیت Android :Title متن اصلی منو را مشخص میکند.
<?xml version="1.0" encoding="utf-8" ?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <group android:checkableBehavior="single"> <item android:id="@+id/menuItemHome" android:title="صفحه اصلی"></item> <item android:id="@+id/menuItemInsertProduct" android:title="ورود کالا جدید" ></item> <item android:id="@+id/menuItemListProduct" android:title="مشاهده کالاها"></item> <item android:id="@+id/menuItemExit" android:title="خروج"></item> </group> </menu>
سپس باید در Layout مورد نظر همانند صفحه Main، ساختار اصلی برنامه شامل Toolbar و Menu را بصورت زیر تعریف نماییم:
<android.support.v7.widget.Toolbar android:layout_width="match_parent" android:id="@+id/toolbar1" android:background="#33B86C" android:minHeight="?android:attr/actionBarSize" android:layout_height="wrap_content"> </android.support.v7.widget.Toolbar>
ساختار منو به صورت زیر است:
<?xml version="1.0" encoding="utf-8" ?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <group android:checkableBehavior="single"> <item android:id="@+id/menuItemHome" android:title="صفحه اصلی"></item> <item android:id="@+id/menuItemInsertProduct" android:title="ورود کالا جدید" ></item> <item android:id="@+id/menuItemListProduct" android:title="مشاهده کالاها"></item> <item android:id="@+id/menuItemExit" android:title="خروج"></item> </group> </menu>
toolbar اضافه شده، toolbar استاندارد قبل از متریال دیزاین میباشد. در واقع toolbar اول، Toolbar استاندارد اندروید میباشد. برای آنکه از Toolbar متریال دیزاین استفاده کنیم، کنترلهای متریال دیزاین در بخش supportlibrary اضافه میشود و Toolbar متریال دیزاین را اضافه میکنیم. علامت ؟ یعنی اینکه میخواهیم از اندازه سیستمی استفاده کنیم. اگر بخواهیم حداقل سایز Toolbarبر اساس پیش فرض در دستگاههای مختلف باشد، از علامت Android :attr ? استفاده میکنیم. اگر بخواهیم حداقل ارتفاع پیشنهادی اندروید در هر موبایل متصل شود، از خصوصیت Action Bar Size استفاده میکنیم. این خصوصیت زمانی عمل میکند که Height آن Wrapcontent باشد.
گزینههای منو در کنترلی به نام Navigationview قرار دارد. این کنترل باید در Drawerlayout قرار گیرد. توسط فضای نام منو، محل فایل xml را که منو درون آن قرار گرفته است، مشخص میکنیم. آدرس این دستور در این مسیر میباشد:
xmlns:app="http://schemas.android.com/apk/res-auto"
SupportActionBar.SetDisplayShowTitleEnabled(false);
مدیریت گزینههای منو
به محض انتخاب یک گزینه درون NavigationView، رخدادی به نام NavigationItemSelected صادر میشود که توسط آن میتوانیم گزینهی انتخاب شده را از طریق برنامه نویسی مدیریت کنیم. این کنترل در Android.Support.V7.Widget و NameSpace بالا قرار میگیرد. سپس یک رخداد گردان را با نام navigationItemSelected پیاده سازی میکنیم. اطلاعات مربوط به گزینهی انتخاب شده، در پارامتر دوم از این تابع NavigationView.NavigationItemSelectedEventArgs ذخیره میشود. ID، آیتم انتخاب شده در فایل Menu را باز میگرداند.
var navigationview = this.FindViewById<NavigationView>(Resource.Id.navigationView1); navigationview.NavigationItemSelected += Navigationview_NavigationItemSelected; private void Navigationview_NavigationItemSelected(object sender, NavigationView.NavigationItemSelectedEventArgs e) { Intent intent = null; switch (e.MenuItem.ItemId) { case Resource.Id.menuItemHome: break; case Resource.Id.menuItemExit: Finish(); break; case Resource.Id.menuItemInsertProduct: break; case Resource.Id.menuItemListProduct: break; } }
مدیریت اکتیویتیها توسط Menu
برای آنکه در Layoutهای مختلف، تولبار و منو و یا هر View دیگری را بصورت مشترک استفاده کنیم، یک فایل xml را به دایرکتوری Layout اضافه میکنیم. دستور Merge میتواند تمام layoutها را به درون layoutهای دیگر مانند home,insert ادغام و یا تزریق کند. جهت استفاده از Merge در layoutهای دیگر نیاز به Id منحصر به فرد میباشد.
<?xml version="1.0" encoding="utf-8" ?> <merge xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/toolbarlayout"> <android.support.v7.widget.Toolbar android:layout_width="match_parent" android:id="@+id/toolbar1" android:background="#33B86C" android:minHeight="?android:attr/actionBarSize" android:layout_height="wrap_content"> <ImageButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/imageButton1" android:background="@drawable/mainmenu" android:layout_gravity="end" /> </android.support.v7.widget.Toolbar> <android.support.v4.widget.DrawerLayout android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/drawerLayout1" android:fitsSystemWindows="true"> <android.support.design.widget.NavigationView android:minWidth="25px" android:minHeight="25px" android:layout_width="200dp" android:layout_height="match_parent" android:layout_gravity="end" app:menu="@menu/menu" android:id="@+id/navigationView1" android:fitsSystemWindows="true" /> </android.support.v4.widget.DrawerLayout> </merge>
در اکتیویتیهای دیگر باید Toolbar و مدیریت گزینههای منو با کدهای مشابه Main انجام شود.
private void Navigationview_NavigationItemSelected(object sender, NavigationView.NavigationItemSelectedEventArgs e) { Intent intent = null; switch (e.MenuItem.ItemId) { case Resource.Id.menuItemHome: intent = new Intent(this, typeof(MainActivity)); break; case Resource.Id.menuItemExit: Finish(); break; case Resource.Id.menuItemInsertProduct: intent = new Intent(this, typeof(InsertActivity)); break; case Resource.Id.menuItemListProduct: intent = new Intent(this, typeof(ListProductsActivity)); break; } if (intent != null) { } }
نام لیآوت را در خصوصیت Layout اضافه میکنیم. برای آنکه کدهای سی شارپ کنترل کنندهی Toolbar و Menu چندین Toolbar وجود دارد که در یکی از آنها یک کلاس واسط از کلاس app compat Activity را به ارث میبریم. تابع Protected را از آن بازنویسی کرده و تمام کدهای مدیریت Toolbar و منو را در آن پیاده سازی میکنیم. تمام اکتیویتیهای برنامه را از این کلاس به ارث میبریم. بنابراین تابع InitieToolbar به تمامی فرزندان نیز به ارث برده میشود. در زمان اجرای دستورات، this ، اکتیویتی جاری میباشد.
public class BaseAcitivity : AppCompatActivity { protected void InitieToolbar() { var toolbar = this.FindViewById<widgetV7.Toolbar>(Resource.Id.toolbar1); this.SetSupportActionBar(toolbar); //SupportActionBar.SetDisplayShowTitleEnabled(false); var imagebutton = toolbar.FindViewById<ImageButton>(Resource.Id.imageButton1); imagebutton.Click += Imagebutton_Click; var navigationview = this.FindViewById<NavigationView>(Resource.Id.navigationView1); navigationview.NavigationItemSelected += Navigationview_NavigationItemSelected; } private void Navigationview_NavigationItemSelected(object sender, NavigationView.NavigationItemSelectedEventArgs e) { Intent intent = null; switch (e.MenuItem.ItemId) { case Resource.Id.menuItemHome: intent = new Intent(this, typeof(MainActivity)); break; case Resource.Id.menuItemExit: Finish(); break; case Resource.Id.menuItemInsertProduct: intent = new Intent(this, typeof(InsertActivity)); break; case Resource.Id.menuItemListProduct: intent = new Intent(this, typeof(ListProductsActivity)); break; } if (intent != null) StartActivity(intent); } private void Imagebutton_Click(object sender, EventArgs e) { var drawerlayout = this.FindViewById<DrawerLayout>(Resource.Id.drawerLayout1); if (drawerlayout.IsDrawerOpen(Android.Support.V4.View.GravityCompat.End) == false) { drawerlayout.OpenDrawer(Android.Support.V4.View.GravityCompat.End); } else { drawerlayout.CloseDrawer(Android.Support.V4.View.GravityCompat.End); } } }
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
ساخت TabPage
پیشنیاز: نصب کتابخانههای متریال دیزاین همانند قبل و طبق ورژن Sdk نصب شده
اگر بخواهیم چندین صفحه را بر روی یکدیگر Stack و یا Overload نماییم، از Tabpage استفاده میکنیم. صفحاتیکه از TabPage استفاده میکنند، با انگشت جابجا میشوند و همانند برنامهی واتساپ Fragment میباشند و هر Fragment دارای layout و اکتیویتی مربوط به خود میباشد. معماری layout آن بصورت زیر است:
ToolBar، در بالای فرم قرار میگیرد. TabLayou که بصورت TabPage آنها را به عهده دارد. Viewpager مدیریت Layoutها را به هنگام Swipe یا جابجایی به عهده دارد.
یک layout را برای Toolbar قرار میدهیم. سپس Layout اصلی main را طراحی میکنیم. پس از اضافه کردن ToolBar، ابزار TabLayout را در بخش SupportLibrary متریال دیزاین انتخاب و در صفحه میکشیم. TabLayout در پایین Toolbar قرار میگیرد و با انتخاب رنگ یکسان برای هر دو، متصل و یکنواخت به نظر میرسد. سپس از Layout از Toolbar آیتم ViewPager را بر روی صفحه قرار میدهیم. اگر LayoutWeight آن را یک قرار دهیم، تمام ارتفاع صفحه را به ما تخصیص میدهد. زمانیکه در TabLayout تبها جابجا میشوند یا بر روی یک آیکن کلیک میشود، صفحه مربوطه در بخش ViewPager به کاربر نمایش داده میشود. هر Page یک فرگمنت میباشد. به ازای هر فرگمنت یک Layout به دایرکتوری layout اضافه کرده و به ازای هر layoutFragment یک Activity Fragment را اضافه میکنیم. یک اکتیویتی از نوع را Android.Support.v4.AppFragment ایجاد میکنیم.
public class Fragment1 : Android.Support.V4.App.Fragment { public override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); } public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.Inflate(Resource.Layout.FragmentLayout1, container, false); } }
var tablayout = FindViewById<Android.Support.Design.Widget.TabLayout>(Resource.Id.tabLayout1); var viewpager = FindViewById<ViewPager>(Resource.Id.viewPager1); tablayout.SetupWithViewPager(viewpager);
class TabFragmentAdapter : FragmentPagerAdapter { public TabFragmentAdapter(FragmentManager fm) : base(fm) { } public override int Count => 3; public override Fragment GetItem(int position) { switch (position) { case 0: return new Fragment1(); case 1: return new Fragment2(); case 2: return new Fragment3(); default: return new Fragment1(); } } //int f1() { return 100; } //int f1 => 100; }
viewpager.Adapter = new TabFragmentAdapter(this.SupportFragmentManager);
آیکن برای TabPage
سپس اگر بخواهیم آیکنهای Tab را به ترتیب تعریف کنیم، از تابع Gettabat استفاده میکنیم. پارامتر ورودی آن موقعیت Tab page میباشد و Set icon هم آیکنهای دایرکتوری Drawable را انتخاب میکند.
tablayout.GetTabAt(0).SetIcon(Resource.Drawable.iconCall);
نمایش متن همراه با عکس
اگر بخواهیم آیکنهای تب پیج را سفارشی کنیم، از Layout استفاده میکنیم که عرض و ارتفاع آن wrap Content باشند و درون آن یک Text view که معادل Lable میباشند، قرار میدهند:
View iconlayout1 = LayoutInflater.Inflate(Resource.Layout.custom_TabIconLayout, null); var txt = iconlayout1.FindViewById<TextView>(Resource.Id.tabTextIcon); txt.Text = "تماس"; txt.SetCompoundDrawablesWithIntrinsicBounds(Resource.Drawable.iconCall, 0, 0, 0); tablayout.GetTabAt(0).SetCustomView(iconlayout1);
کدهای مطلب جاری برای دریافت: Navigation-TabPage-samples.zip
اگه این پست رو مطالعه کرده باشید یه مشکل امنیتی بنام «Mass Assignment» مطرح شد.برای رفع این مشکل یک روش استفاده از ViewModel بود.
فرض کنید Model ما
public class User { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string UserName { get; set; } public string Password { get; set; } public bool IsAdmin { get; set; } public virtual ICollection<BlogPost> BlogPosts { get; set; } }
public class UserViewModel { public string FirstName { get; set; } public string LastName { get; set; } public string Password { get; set; } }
برای استفاده UserViewModel بعنوان Model در View ی مورد نظر باید شی UserViewModel رو با اطلاعات شی User مقدار دهی کنیم مثلا با کدی مثل این در کنترلر.
public ActionResult Index(int id = 1) { var user = _userService.GetById(id); var userViewModel = new UserViewModel { FirstName = user.FirstName, LastName = user.LastName, Password = user.Password }; return View(userViewModel); }
برای استفاده از AutoMapper از نوگت استفاده میکنیم.
PM> Install-Package AutoMapper
public static class AutoMapperWebConfiguration { public static void Configure() { ConfigureUserMapping(); } private static void ConfigureUserMapping() { Mapper.CreateMap<User, UserViewModel>(); } }
اولین پارامتر نوع مبدا و دومین پارامتر نوع مقصد هست.
برای انجام نگاشت هم از متد Map استفاده میکنیم.
public ActionResult Index(int id=1) { var user = _userService.GetById(id); var userViewModel=new UserViewModel(); AutoMapper.Mapper.Map(user, userViewModel); return View(userViewModel); }
ادامه دارد...
export class Book { constructor( public id, public title:string, public pages:Array ){} } return this._http.get('getBook/1') .map(function(res){ var data = res.json(); return new Book(data.id, data.title, data.pages); })
public class TestController { private readonly ILogger<TestController> _logger; public TestController(ILogger<TestController> logger) { _logger = logger; } [HttpGet("/")] public string Get() { _logger.LogInformation("hello world"); return "Hello world!"; } }
اگر یک چنین برنامهای را به دات نت 6 ارتقاء دهید، با پیام اخطار زیر مواجه خواهید شد:
CA1848: For improved performance, use the LoggerMessage delegates instead of calling LogInformation
استفادهی گسترده از source generators در دات نت 6
source generators، امکان مداخله در عملیات کامپایل برنامه را میسر کرده و امکان تولید کدهای پویایی را در زمان کامپایل، فراهم میکنند. هرچند این قابلیت به همراه دات نت 5 ارائه شدند، اما تا زمان دات نت 6 استفادهی گستردهای از آن در خود دات نت صورت نگرفت. موارد زیر، تغییراتی است که بر اساس source generators در دات نت 6 رخ دادهاند:
- source generators مخصوص ILogger (موضوع این بحث؛ یعنی LoggerMessage source generator)
- source generators مخصوص System.Text.Json تا سربار تبدیل به JSON و یا برعکس کمتر شود.
- بازنویسی مجدد پروسهی کامپایل Blazor/Razor بر اساس source generators، بجای روش دو مرحلهای قبلی که امکان Hot Reload را فراهم کردهاست.
نوشتن یک source generator هرچند ساده نیست، اما چون نیاز به reflection را به حداقل میرساند، میتواند تغییرات کارآیی بسیار مثبتی را به همراه داشته باشد.
توصیه به استفاده از LoggerMessage.Define در دات نت 6
ILogger به همراه قابلیتهایی مانند structural logging نیز هست که امکان فرمت بهتر پیامهای ثبت شده را میسر میکند تا توسط برنامههای جانبی که قرار است این لاگها را پردازش کنند، به سادگی قابل خواندن باشند. برای مثال رکورد زیر را در نظر بگیرید:
public record Person (int Id, string Name);
var person = new Person(123, "Test");
_logger.LogInformation("hello to {Person}", person);
info: TestController[0] hello world to Person { Id = 123, Name = Test }
اگر در اینجا مانند مثال زیر از string interpolation استفاده شود:
_logger.LogInformation($"hello world to {person}"); // Using interpolation instead of structured logging
- ویژگی «لاگهای ساختار یافته» را از دست میدهیم و دیگر توسط نرم افزارهای ثالث لاگ خوان، به سادگی پردازش نخواهند شد.
- ویژگی «قالب ثابت» پیام را نیز از دست خواهیم داد که باز هم یافتن پیامهای مشابه را در بین انبوهی از لاگهای رسیده مشکل میکند.
- کار serialization شیء ارسالی به آن، پیش از عملیات ثبت وقایع رخ میدهد. اما ممکن است سطح لاگ سیستم در این حد نباشد و اصلا این پیام لاگ نشود. در این حالت یک کار اضافی صورت گرفته و بر روی کارآیی برنامه تاثیر منفی خواهد گذاشت.
برای جلوگیری از serialization و همچنین تخصیص حافظهی اضافی و مشکلات عدم ساختار یافته بودن لاگها، توصیه شدهاست که ابتدا سطح لاگ مدنظر بررسی شود و همچنین از string interpolation استفاده نشود:
if (_logger.IsEnabled(LogLevel.Information)) { _logger.LogInformation("hello world to {Person}", person); }
مشکل دیگر لاگهای ساختار یافته، امکان فراموش کردن یکی از پارامترها است که با یک خطای زمان اجرا گوشزد خواهد شد؛ مانند مثال زیر:
_logger.LogInformation("hello world to {Person} because {Reason}", person);
private static readonly Action<ILogger, Person, Exception?> _logHelloWorld = LoggerMessage.Define<Person>( logLevel: LogLevel.Information, eventId: 0, formatString: "hello world to {Person}");
اکنون روش فراخوانی این Action با کارآیی بالا به صورت زیر است:
[HttpGet("/")] public string Get() { var person = new Person(123, "Test"); _logHelloWorld(_logger, person, null); return "Hello world!"; }
معرفی [LoggerMessage] source generator در دات نت 6
هرچند LoggerMessage.Define، مزایای قابل توجهی مانند کش شدن قالب لاگ، عدم نیاز به بررسی ضرورت لاگ شدن پیام و ارسال تعداد پارامترهای صحیح را به همراه دارد، اما ... کار کردن با آن مشکل است و برای کار با آن باید کدهای زیادی را نوشت. به همین جهت با استفاده از قابلیت source generators، امکان تولید خودکار این نوع کدها در زمان کامپایل برنامه پیشبینی شدهاست:
public partial class TestController { [LoggerMessage(0, LogLevel.Information, "hello world to {Person}")] partial void LogHelloWorld(Person person); }
ویژگی partial method، امکان تعریف یک متد را در یک فایل و سپس ارائهی پیاده سازی آنرا در فایلی دیگر میسر میکند که البته در اینجا آن فایل دیگر، توسط source generator تولید میشود.
باید دقت داشت که در اینجا TestController را نیز باید به صورت partial تعریف کرد تا آن نیز قابلیت بسط در چند فایل را پیدا کند. همچنین متد فوق را به صورت static partial void نیز میتوان نوشت.
یکی از مزایای کار با source generator که خودش در اصل یک آنالایزر هم هست، بررسی تعداد پارامترهای ارسالی و تعریف شدهاست:
[LoggerMessage(0, LogLevel.Information, "hello world to {Person} with a {Reason}")] partial void LogHelloWorld(Person person);
در این روش، امکان ذکر پارامتر اختیاری LogLevel هم وجود دارد؛ اگر نیاز است مقدار آن به صورت پویا تغییر کند:
[LoggerMessage(Message = "hello world to {Person}")] partial void LogHelloWorld(LogLevel logLevel, Person person);