نظرات مطالب
شروع به کار با EF Core 1.0 - قسمت 14 - لایه بندی و تزریق وابستگی‌ها
ممنون بابت پاسختون...
اگر لایه Service به عنوان ماژول سطح بالا   فرض کنیم  و لایه DataAccsessLayer رو به عنوان   ماژول سطج پایین  فرض کنیم. مگر طبق تعریف قانون DIP  نباید رابط IUnitOfWork رو در لایه Service تعریف کرده و  لایه DAl موظف باشد ویژگی‌های مورد نیاز Service را  پیاده سازی کند و دیگر لازم نیست applicationDbContext رو به صورت public کنیم و میتوانیم اون رو به صورت internal  تعریف کنیم.

مانند پروژه  eShopOnContainers .


نظرات مطالب
طراحی و پیاده سازی زیرساختی برای مدیریت خطاهای حاصل از Business Rule Validationها در ServiceLayer
بنده قبلا در پروژه‌ها در بین لایه‌های Service و Presentation ، از لایه دیگری استفاده میکردم که الگویی به همین صورت داشت.
البته مواردی مثل متدهای OnFailure و OnBoth و اینگونه متدها خب جنبه سلیقه ای در پیاده سازی الگو داره.
ولی در کل ایجاد یک درخواست و پاسخ به اون، به صورت زیر میتونه باشه:
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; } } 
این کلاس به ازای هر درخواست برای انجام کاری، مشخص میکنه موفق آمیز بود یا خیر، چه پیام هایی برای کاربر قابل نمایش هست، و در صورتیکه نتیجه ای برای نمایش داشت هم در مشخصه Result میتونست قرار بگیره. و همچنین برای اپلیکیشن‌های WebApi میتونه از HttpResponse به عنوان یک بسته پاسخ استفاده بشه که شامل موارد مورد نیاز به علاوه StatusCode برای بررسی و ارسال به سمت کلاینت هست.
این حالت کلی این الگو هست که میتونه موارد دیگه ای هم بسته به نیاز بهش اضافه بشه.
مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 18 - کار با ASP.NET Web API
در ASP.NET Core، برخلاف نگارش‌های قبلی ASP.NET که ASP.NET Web API مجزای از ASP.NET MVC و همچنین وب فرم‌ها ارائه شده بود، اکنون جزئی از ASP.NET MVC است و با آن یکپارچه می‌باشد. بنابراین پیشنیازهای راه اندازی Web API با ASP.NET Core شامل سه مورد ذیل هستند که پیشتر آن‌ها را بررسی کردیم:
الف) فعال سازی ارائه‌ی فایل‌های استاتیک
ب) فعال سازی 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;
    }
[controller] هم در اینجا یک توکن پیش فرض است که با نام کنترلر جاری یا همان Test، به صورت خودکار جایگزین می‌شود.
در مورد سرویس ثبت وقایع نیز در مطلب «ارتقاء به 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();
        }
    } 
}
بنابراین در اینجا اگر می‌خواهید یک کنترلر ASP.NET Web API 2.x را به ASP.NET Core 1.0 ارتقاء دهید، تمام متدهای Get و Put و امثال آن هنوز معتبر هستند و مانند سابق عمل می‌کنند:
    [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)
        {
        }
    }
}
در مورد ویژگی FromBody در ادامه بیشتر بحث خواهد شد.

یک نکته: اگر می‌خواهید خروجی 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; }
    }
}
و همچنین قصد دارید اطلاعات آن‌را از کاربر توسط یک عملیات POST دریافت کرده و به شکل JSON نمایش دهید:
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);
        }
    }
}
برای اینکار، از jQuery به نحو ذیل استفاده می‌کنیم (از این جهت که بیشتر ارسال‌های به سرور جهت کار با Web API نیز Ajax ایی هستند):
@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 را مشخص می‌کنیم، آنگاه اطلاعات دریافتی از request body دریافتی، به شیء 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();
اطلاعات فرم سریالایز می‌شوند و data type مخصوصی هم برای آن‌ها تنظیم خواهد شد. در این حالت، ارسال یک چنین اطلاعاتی به سمت اکشن متد فوق، با خطای 415 unsupported media type متوقف می‌شود. برای رفع این مشکل باید از ویژگی دیگری به نام FromForm استفاده کرد:
 [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; }
}
این تزریق وابستگی‌ها برای اینکه تکمیل شود، نیاز به ویژگی FromServices خواهد داشت:
 public async Task<IActionResult> GetProduct([FromServices]ProductModel product)
{
}
وجود ویژگی FromServices به این معنا است که سرویس‌های مدل یاد شده را از تنظیمات ابتدایی IoC Container خود خوانده و سپس در اختیار مدل جاری قرار بده. به این ترتیب حتی تزریق وابستگی‌ها در مدل‌های برنامه هم میسر می‌شود.


تغییر تنظیمات اولیه‌ی خروجی‌های 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;
    });
همچنین اگر خواستید تنظیمات ابتدایی JSON.NET را تغییر داده و برای مثال خروجی JSON تولیدی را camel case کنید، این‌کار را توسط متد AddJsonOptions به نحو فوق می‌توان انجام داد.
مطالب
Long Polling در WCF
به صورت پیش فرض سرویس‌های WCF به صورت Sync اجرا خواهند شد، یعنی هر گاه درخواستی از سمت کلاینت به سرور ارسال شود سرور بعد از پردازش درخواست پاسخ مورد نظر را به کلاینت باز می‌گرداند. اما حالتی را در نظر بگیرید که بعد از دریافت Request از کلاینت بنا به دلایلی امکان پاسخ گویی سمت سرور در آن لحظه وجود ندارد. خوب چه اتفاقی خواهد افتاد؟
در این حالت 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
    }
در این حالت شما می‌توانید حداکثر زمان مورد نیاز برای درخواست را به عنوان پارامتر از طریق سازنده کلاس بالا تعیین نمایید. اگر این زمان بیش از زمان تعیین شده در خواص SendTimeout و ReceiveTimeout بود بعد از منقضی شدن زمان پردازش درخواست، به جای دریافت TimeoutException عملیات پردازش به کار خود ادامه خواهد داد.
برای استفاده از کلاس تهیه شده ابتدا باید عملیات خود را به صورت Async پیاده سازی نمایید که در این مقاله به صورت کامل شرح داده شده است.
یک مثال
قصد داریم Operation زیر را به صورت Long Polling پیاده سازی نماییم:
[OperationContract]
public string GetNotification();
ابتدا متد زیر باید به صورت Async تبدیل شود:
[OperationContract(AsyncPattern = true)]
public IAsyncResult BeginWaitNotification(AsyncCallback callback, object state);
 
public string EndWaitNotification(IAsyncResult result);
حال نوع بازگشتی سرویس مورد نظر را با استفاده از کلاس LongPollingAsyncResult به صورت زیر ایجاد خواهیم کرد:
public class MyNotificationResult : LongPollingAsyncResult<string>
{
   protected override DoWork()
    {
        کد‌های مورد نظر را اینجا قرار دهید
        base.Complete(...)
    }
}
در نهایت پیاده سازی متد‌های Begin و End همانند ذیل خواهد بود:
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;
}
در این حالت کلاینت می‌تواند یک درخواست به صورت LongPolling به سرور ارسال نماید و البته مدیریت این درخواست در یک thread دیگر انجام می‌گیرد که نتیجه آن از عدم تداخل پردازش این درخواست با سایر قسمت‌های برنامه است.

مطالب
بهبود کارآیی Reflection در دات نت 7
استفاده‌ی از Reflection در زیر ساخت‌های دات نت و ASP.NET Core، بسیار گسترده‌است؛ به همین جهت هرگونه بهبود کارآیی در این زمینه، نه فقط بر روی خود فریم‌ورک، بلکه تمام برنامه‌هایی که از آن استفاده می‌کنند هم تاثیر گذار است. از این لحاظ دات نت 7 شاهد تغییرات گسترده‌ای است تا حدی که کارآیی برنامه‌های مبتنی بر دات نت 7 ای که از Reflection استفاده می‌کنند، نسبت به نگارش‌های قبلی دات نت، حداقل 2 برابر شده‌است و این برنامه‌ها تنها کاری را که باید انجام دهند، صرفا تغییر target framework مورد استفاده‌ی در آن‌ها به نگارش جدید است. در این مطلب نحوه‌ی رسیدن به این کارآیی بالاتر را بررسی خواهیم کرد.


تدارک یک آزمایش برای بررسی میزان افزایش کارآیی 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;
}
همانطور که مشاهده می‌کنید، سازنده‌ی این کلاس، internal است و همچنین دو متد private هم دارد که اگر بخواهیم از آن در جای  دیگری استفاده کنیم، یکی از روش‌های متداول جهت دسترسی به این امکانات خصوصی، استفاده از Reflection است.
به همین جهت ابتدا کتابخانه‌ی 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>
در ادامه، یک کلاس آزمایش کارآیی برنامه را که با استفاده از Reflection، به امکانات خصوصی کلاس Person دسترسی پیدا می‌کند، مشاهده می‌کنید:
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 دات نت 6 به صورت زیر است:



و با target framework دات نت 7 به صورت زیر:


همانطور که مشاهده می‌کنید، در اکثر موارد، کارآیی Reflection در دات نت 7، حداقل 2 برابر نمونه‌ی مشابه دات نت 6 است.


چه تغییری در دات نت 7 سبب بهبود قابل ملاحظه‌ی کارآیی Reflection شده‌است؟

جزئیات تغییرات صورت گرفته‌ی در Reflection دات نت 7 را می‌توانید در این pull request مشاهده کنید که در حقیقت از امکانات سطح پایین IL Emit استفاده کرده‌اند. در این مورد پیشتر تعدادی مطلب ذیل عنوان «آشنایی با Reflection.Emit» در این سایت منتشر شده‌اند که می‌توانید آن‌ها را بررسی کنید.
در کل هرچند تغییرات جدید دات نت مانند ارائه‌ی انواع و اقسام source generators، در تعدادی از موارد نیاز به Reflection را کمتر کرده‌اند و کارآیی بیشتری را ارائه داده‌اند، اما Reflection هیچگاه منسوخ نخواهد شد و هرگونه بهبود کارآیی در این زمینه، بر روی کل فریم‌ورک و برنامه‌های مشتق شده‌ی از آن، تاثیر قابل توجهی را خواهد گذاشت.
مطالب
ساخت یک Web API که از عملیات CRUD پشتیبانی می کند
در این مقاله با استفاده از ASP.NET Web API یک سرویس HTTP خواهیم ساخت که از عملیات CRUD پشتیبانی می‌کند. CRUD مخفف Create, Read, Update, Delete است که عملیات پایه دیتابیسی هستند. بسیاری از سرویس‌های HTTP این عملیات را بصورت REST API هم مدل سازی می‌کنند. در مثال جاری سرویس ساده ای خواهیم ساخت که مدیریت لیستی از محصولات (Products) را ممکن می‌سازد. هر محصول شامل فیلدهای شناسه (ID)، نام، قیمت و طبقه بندی خواهد بود.

سرویس ما متدهای زیر را در دسترس قرار می‌دهد.

 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);
    }
}
حال کلاس دیگری با نام ProductRepository در پوشه Models ایجاد کنید. این کلاس قرارداد IProductRepository را پیاده سازی خواهد کرد. کد زیر را به این کلاس اضافه کنید.

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;
یک فیلد هم برای نگهداری وهله ای از IProductRepository اضافه کنید.
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();
    }
    // ....
}
نام این متد با "Get" شروع می‌شود، پس بر اساس قراردادهای توکار پیش فرض به درخواست‌های HTTP GET نگاشت خواهد شد. همچنین از آنجا که این متد پارامتری ندارد، به URl ای نگاشت می‌شود که هیچ قسمتی با نام مثلا id نداشته باشد.

برای دریافت محصولی مشخص بر اساس شناسه آن متد زیر را اضافه کنید.
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;
}
توجه کنید که حالا نوع بازگشتی این متد HttpResponseMessage است. با بازگشت دادن این نوع داده بجای Product، می‌توانیم جزئیات پیام HTTP response را کنترل کنیم. مانند تغییر کد وضعیت و مقدار دهی Location header.

متد 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);
    }
}
نام این متد با "Put" شروع می‌شود، پس Web API آن را به درخواست‌های HTTP PUT نگاشت خواهد کرد. این متد دو پارامتر می‌پذیرد، یکی شناسه محصول مورد نظر و دیگری آبجکت محصول آپدیت شده. مقدار پارامتر id از مسیر (route) دریافت می‌شود و پارامتر محصول با deserialize کردن بدنه درخواست.


حذف یک منبع

برای حذف یک محصول متد زیر را به کلاس ProductsController اضافه کنید.

public void DeleteProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }

    repository.Remove(id);
}
اگر یک درخواست DELETE با موفقیت انجام شود، می‌تواند کد وضعیت 200 (OK) را بهمراه بدنه موجودیتی که وضعیت فعلی را نمایش می‌دهد برگرداند. اگر عملیات حذف هنوز در حال اجرا است (Pending) می‌توانید کد 202 (Accepted) یا 204 (No Content) را برگردانید.

در مثال جاری متد DeleteProduct نوع void را بر می‌گرداند، که فریم ورک Web API آن را بصورت خودکار به کد وضعیت 204 (No Content) ترجمه می‌کند.
مطالب
xamarin.android قسمت سوم
Theme
برای اینکه بتوانیم ظاهر گرافیکی layout‌ها را کنترل نماییم، از Theme که مجموعه‌ای از styleهای گرافیکی می‌باشد، استفاده می‌کنیم. در اندروید مجموعه‌ای از تم‌های از پیش ساخته شده که به آنها Builtin Theme نیز گفته می‌شود می‌توانیم استفاده کنیم. تم‌ها ظاهر گرافیکی کلیه کنترل‌های Layout را با نام‌های زیر، کنترل می‌کنند:
statusBarColor
textColorPrimary
colorAccent
ColorPrimary
WindowBackground

اگر ساختار زیر را در یک صفحه استاندارد برنامه‌های موبایل را در نظر بگیریم، styleها هر بخش، یک نام منحصر به فرد دارد:



اگر بخواهیم از style‌های از پیش طراحی شده‌ی اندروید استفاده نماییم، ابتدا میتوانیم در صفحه‌ی layout در زامارین، style مربوطه را از بخش Theme استفاده کرده و نتیجه را مشاهده کنیم. ولی تغییر style سبب تغییر layout در زمان اجرا نمی‌شود. هرگاه بخواهیم از styleهای استاندارد یا builtin اندروید استفاده نماییم، در Activity توسط خصوصیت Theme با فرمت:
[Activity(Theme = "@style/NameThem")]
تم را به‌عنوان تم داخلی وسپس نام کامل تم را می‌نویسیم.
 
CustomTheme
در طراحی فرم‌ها ممکن است بخواهیم از یک استایل خاص builtin استفاده کنیم؛ ولی ممکن است بعضی از استایل‌های آن را استفاده نکنیم، مانند تمی که از قبل استفاده شده‌است، از روش زیر استفاده می‌کنیم:
- بر روی دایرکتوری value راست کلیک میکنیم. گزینه add new item را انتخاب و یک فایل xmlfile را با نام style ایجاد میکنیم.
- style‌های جدید منابع application می‌باشند که در بخش resource از آن‌ها استفاده میکنیم. هر استایل جدید را توسط Style Tag مشخص میکنیم و در خصوصیت Name، نام Style را مشخص میکنیم.
ممکن است در یک Style نتوانیم و یا نخواهیم تمامی Style‌های مورد نیاز را تامین کنیم. از این رو توسط Parent، یک StyleBuition تعریف نموده که این Styleها از آن مشتق می‌شوند. اگر در Theme جدید گزینه‌ها مشخص شوند، تم اصلی تغییر نمی‌کند. در غیر اینصورت تمامی گزینه‌های تعریف شده در تم جدید از ThemParent مقدار دهی می‌شود.
توسط item می‌توان یک style را تعریف نمود. در Activity توسط
 [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
کتابخانه متریال دیزاین کتابخانه جدیدی می‌باشد که به اندروید اضافه شده‌است. توسط  آن می‌توانیم کنترل‌های جدید را با استایل‌های جدید برای Appهای اندروید، تولید کنیم. ابتدا نیاز به نصب Component‌های ذیل در زامارین می‌باشد.
 
استفاده که از کتابخانه متریال دیزاین
برای اینکه بتوانیم Navigation menu را ایجاد کنیم، باید از نیوگت، کتابخانه‌های Appcompat و Designlibrary را انتخاب و نصب نماییم و اگر نگارش ویژوال استودیوی شما 15.7.3باشد، از ابتدا بصورت اتوماتیک نصب شده است و احتیاجی به این مراحل نمی‌باشد. بدیهی است در زمان نصب باید از نرم افزار‌های تحریم گذر نیز استفاده کرد.
  
ساخت Menu
NavigationMenu، منوی اصلی منو می‌باشد که با Swipping از گوشه راست به چپ، باز یا بسته می‌شود یا با کلیک بر روی دکمه‌ی Menu بر روی Toolbar، منو را باز یا بسته میکند.
قدم اول: نصب منو
منظور همان اضافه کردن کتابخانه‌ها است.
قدم دوم:
ابتدا باید گزینه‌های منو را در یک فایل xml تعریف نمود. هر گزینه‌ی آن از دو بخش متن اصلی منو و ID منو تشکیل شده‌است.
بر روی دایرکتوری Resource راست کلیک کرده و یک دایرکتوری یا پوشه را به نام Menu ایجاد می‌کنیم. بر روی دایرکتوری منو، راست کلیک کرده و یک فایل Xml را به آن اضافه می‌کنیم. برای آنکه بتوانیم در این فایل دستورات ساخت منو را نوشته و به نحوی که توسط اندروید قابل خواندن و تبدیل به منو باشد، ساختار منو را از آدرس زیر ویژوال استودیو دانلود میکنیم.
 <menu xmlns:android="http://schemas.android.com/apk/res/android">

توسط Group مجموعه گزینه‌های منو را را معرفی میکنیم.
هر گزینه در منو ی اصلی توسط یک آیتم مشخص میشود. برای آنکه هنگام کلیک بر روی هر گزینه از طریق برنامه نویسی بتوانیم گزینه انتخاب شده را شناسایی کنیم، یک آی دی منحصر بفرد را به هر گزینه اختصاص میدهیم. زمانیکه بر روی یک گزینه کلیک میشود، توسط این ID‌ها میتوانیم شناسایی کنیم کدام گزینه انتخاب شده‌است.
خصوصیت 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>
در Linearlayout ریشه، گزینه Fitssystemwindow را true میکنیم که سایز linearlayout را با سایز موبایل جدید اندازه می‌کند. سپس از toolbox، کنترل Toolbarرا به پنجره اضافه میکنیم که در بالای صفحه قرار می‌گیرد.
toolbar اضافه شده، toolbar استاندارد قبل از متریال دیزاین میباشد. در واقع toolbar اول، Toolbar استاندارد اندروید می‌باشد. برای آنکه از Toolbar متریال دیزاین استفاده کنیم، کنترل‌های متریال دیزاین در بخش supportlibrary اضافه میشود و Toolbar متریال دیزاین را اضافه میکنیم. علامت ؟ یعنی اینکه می‌خواهیم از اندازه سیستمی استفاده کنیم. اگر بخواهیم حداقل سایز Toolbarبر اساس پیش فرض در دستگاه‌های مختلف باشد، از علامت Android :attr ? استفاده میکنیم. اگر بخواهیم حداقل ارتفاع پیشنهادی اندروید در هر موبایل متصل شود، از خصوصیت Action Bar Size  استفاده میکنیم. این خصوصیت زمانی عمل میکند که Height  آن Wrapcontent باشد.
گذاشتن دکمه منو: برای آنکه بتوانیم دکمه منو را به Toolbar اضافه کنیم، از دکمه Image Button استفاده میکنیم که یک دکمه‌ی معمولی می‌باشد ولی خلاصه‌ی آن عکس است. در خصوصیت Back ground دکمه، بصورت زیر نام فایل آیکن منو را در دایرکتوری Drawable، مشخص میکنیم و خصوصیت src آن‌را null می‌کنیم تا تصویری بجز تصویر انتخابی نباشد.
برای آنکه بتوانیم پنجره اصلی منو را به صورتیکه دارای قابلیت حرکت به راست و چپ باشد، ایجاد کنیم، از کنترلی به‌نام  Drowerlayout استفاده میکنیم که بر روی صفحه قرار میگیرد. DrawerLayout در linearlayout ریشه قرار میگیرد و یا بعد از ToolBar و حتما باید خصوصیت fitsystemwindow کنترل Drawer را True کنیم. جهت نمایش گزینه‌های اصلی در Drawer از کنترل NavigationٰView استفاده می‌کنیم.
گزینه‌های منو در کنترلی به نام Navigationview قرار دارد. این کنترل باید در Drawerlayout قرار گیرد. توسط فضای نام منو، محل فایل xml را که منو درون آن قرار گرفته است، مشخص می‌کنیم. آدرس این دستور در این مسیر می‌باشد:
 xmlns:app="http://schemas.android.com/apk/res-auto"
Layout gravity  آن را end  قرار میدهیم که از سمت راست قرار بگیرد. Fit system Window را هم True میکنیم تا گزینه‌های داخل آن‌را هم Fit کند. Theme باید از نوع تم‌های متریال دیزاین و با کلمه Them . App Compact. ligth.NoActionBar باشد. برای آنکه اکتیویتی‌ها، متریال دیزاین را ساپورت کنند، میتوان از کلاس  App compact Activity  استفاده کنیم. Tool bar بصورت پیش فرض لیبل اکتیویتی را نشان می‌دهد و دستور زیر عنوان Toolbar را حذف میکند.
 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
با انتخاب گزینه Menu باید اکتیویتی مربوطه انتخاب شود. بنابراین برای هر گزینه‌ی منو یک Layout و اکتیویتی را ایجاد می‌کنیم و اجرا میکنیم. ولی در اکتیویتی جدید Toolbar وجود ندارد.
 
تکنیک ادغام:
برای آنکه در 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) { }
        }
بنابراین دستورات xmlTollbar  و darawer layout در تمامی Layoutها و دستورات سی شارپ، کنترل کننده Toolbar و منو در تمامی اکتیویتی‌ها تکرار شده‌اند.
 
حل مشکلات Layout
یک فایل Xml را به Layout  اضافه می‌کنیم و درون آن Tag merge و کد‌های مشترک Drawer out و Toolbar را داخل تگ Merge اضافه می‌کنیم. جهت استفاده از کدهای (مقدار فایل ایکس ام ال ساخته شده که Tag merge داخل آن است)  Merge، در layout های دیگر، از دستور Include  استفاده می‌کنیم.
نام لی‌آوت را در خصوصیت 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);
            }
        }
    }
 
اگر بخواهیم یک تم در تمامی اکتیویتی‌ها  به صورت سراسری استفاده شود، از فایل تنظمیات اندروید بنام AndroidManifest در دایرکتوری Properties استفاده می‌کنیم و در  بخش Application Theme، نام تم را مشخص میکنیم:
 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);
        }
    }
ابتدا باید viewpager در Layout اصلی را پیدا کرده و با دستور زیر به Tablayout متصل کنیم:
 var tablayout = FindViewById<Android.Support.Design.Widget.TabLayout>(Resource.Id.tabLayout1);
var viewpager = FindViewById<ViewPager>(Resource.Id.viewPager1);
tablayout.SetupWithViewPager(viewpager);
زمانیکه آیکن را در TabLayout انتخاب میکنیم یا با انگشت Swipe میکنیم، به ترتیب بین صفحات که از Position صفحه آغاز شده‌اند، حرکت می‌کنیم، باید فرگمنت همان Position را نشان دهیم و این مدیریت توسط بخشی به‌نام Adapter انجام میشود. یک Adapter را به کنترلر اضافه میکنیم و از کلاس Fragment pager adapter به ارث می‌بریم. بر روی کلاس Fragment pager adapter ، دکمه‌های کنترل و نقطه را میزنیم و سپس کلاس را پیاده سازی می‌کنیم. در این حالت دو تابع را به ما می‌دهد: تابع Get item .count مجددا بر روی کلاس پدر راست کلیک میکنیم. در تابع کانت تعداد کل صفحه‌ها را (Layout ها) را انتخاب میکنیم. هرگاه از یک صفحه به صفحه‌ی دیگری انتقال پیدا کنیم، موقعیت صفحه جدیدی که از یک شروع میشود را به تابع Get Item بر اساس موقعیت Object  از fragment مربوطه new کرده و بعنوان خروجی باز می‌گرداند.
    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;
    }
و در اکتیویتی اصلی، کد زیر را برای Load فرگمنت‌ها نیز قرار می‌دهیم:
 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
مطالب
مقدمه ای بر AutoMapper
AutoMapper کتابخانه ای ساده و سبک برای نگاشت اطلاعات یک شی به شی دیگر به صورت خودکار هست و...
اگه این پست رو مطالعه کرده باشید یه مشکل امنیتی بنام «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; }
    }
و ViewModel ما
 public class UserViewModel
    {
        public string FirstName { get; set; }

        public string LastName { get; set; }

        public string Password { get; set; }
    }
باشه(توجه کنید در واقع من برای View ی مورد نظرم فقط به نام , نام خانوادگی و پسورد نیاز دارم)
برای استفاده 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 شد...
برای استفاده از AutoMapper از نوگت استفاده میکنیم.
PM> Install-Package AutoMapper
در شروع برنامه نگاشت‌ها رو تعریف میکنم.یک روش ابداعی تعریف نگاشت‌ها در یک کلاس استاتیک و فراخوانی اون تو متد  Application_Start هست.
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);
        }
همنطور که میبنید با نوشتن چندین خط کد عملیات نگاشت اطلاعات یک شی به شی دیگه انجام شد.
ادامه دارد...
نظرات مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت هشتم - دریافت اطلاعات از سرور
- خواص را از شیء json دریافت و دستی نگاشت کنید:
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);
    })
- این مورد بیشتر بحث طراحی سرویس‌ها و جداسازی وظایف هست (و یک best practice). می‌توانید کلا کلاس سرویس را حذف کنید و تمام عملیات مرتبط را داخل همان کامپوننت هم مدیریت کنید. اما در +Angular2، مرسوم است کار طراحی لایه کار با HTTP، در یک کلاس سرویس مجزا انجام شود و استفاده کننده‌ها در کامپوننت‌ها، مشترک آن شوند.
مطالب
تغییرات Logging در ASP.NET Core 6x
فرض کنید با استفاده از روش متداول زیر، کار ثبت یک واقعه را انجام داده‌اید:
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!";
    }
}
در یک برنامه‌ی متداول ASP.NET Core، زیرساخت کار با ILogger از پیش تنظیم شده‌است. برای کار با آن فقط کافی است به نمونه‌های ILogger و یا <ILogger<T از طریق سیستم تزریق وابستگی‌ها دسترسی یافت و سپس متدهای الحاقی آن‌را مانند LogInformation فراخوانی کرد.

اگر یک چنین برنامه‌ای را به دات نت 6 ارتقاء دهید، با پیام اخطار زیر مواجه خواهید شد:
CA1848: For improved performance, use the LoggerMessage delegates instead of calling LogInformation
به صورت خلاصه، تمام متدهای پیشین LogInformation، LogDebug و امثال آن در دات نت 6 منسوخ شده درنظر گرفته می‌شوند! دلیل آن‌را در ادامه بررسی خواهیم کرد.


استفاده‌ی گسترده از 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 }
دقت کنید که رشته‌ی ارسالی به LogInformation به همراه $ نیست. یعنی از string interpolation استفاده نشده‌است و نام پارامتر تعریف شده (placeholder name) با حروف بزرگ شروع شده‌است.

اگر در اینجا مانند مثال زیر از string interpolation استفاده شود:
_logger.LogInformation($"hello world to {person}"); // Using interpolation instead of structured logging
هرچند کار با آن ساده‌تر است از string.Format، اما برای عملیات ثبت وقایع با کارآیی بالا توصیه نمی‌شود؛ به این دلایل:
- ویژگی «لاگ‌های ساختار یافته» را از دست می‌دهیم و دیگر توسط نرم افزارهای ثالث لاگ خوان، به سادگی پردازش نخواهند شد.
- ویژگی «قالب ثابت» پیام را نیز از دست خواهیم داد که باز هم یافتن پیام‌های مشابه را در بین انبوهی از لاگ‌های رسیده مشکل می‌کند.
-  کار serialization شیء ارسالی به آن، پیش از عملیات ثبت وقایع رخ می‌دهد. اما ممکن است سطح لاگ سیستم در این حد نباشد و اصلا این پیام لاگ نشود. در این حالت یک کار اضافی صورت گرفته و بر روی کارآیی برنامه تاثیر منفی خواهد گذاشت.

برای جلوگیری از serialization و همچنین تخصیص حافظه‌ی اضافی و مشکلات عدم ساختار یافته بودن لاگ‌ها، توصیه شده‌است که ابتدا سطح لاگ مدنظر بررسی شود و همچنین از string interpolation استفاده نشود:
if (_logger.IsEnabled(LogLevel.Information))
{
   _logger.LogInformation("hello world to {Person}", person);
}
البته مشکل این روش، تکرار این if/else‌ها در تمام برنامه‌است و همچنین باید دقت داشت که LogLevel انتخابی، با متد لاگ، هماهنگی دارد.
مشکل دیگر لاگ‌های ساختار یافته، امکان فراموش کردن یکی از پارامترها است که با یک خطای زمان اجرا گوشزد خواهد شد؛ مانند مثال زیر:
_logger.LogInformation("hello world to {Person} because {Reason}", person);
اکنون در دات نت 6 با پیام اخطار CA1848 که در ابتدای بحث مشاهده کردید، توصیه می‌کنند که اگر قالب نهایی خاصی را مدنظر دارید، آن‌را توسط متد LoggerMessage.Define دقیقا مشخص کنید:
private static readonly Action<ILogger, Person, Exception?> _logHelloWorld =
    LoggerMessage.Define<Person>(
        logLevel: LogLevel.Information,
        eventId: 0,
        formatString: "hello world to {Person}");
در این روش جدید باید یک Action را برای لاگ کردن پیام‌ها تهیه کرد که از همان ابتدا LogLevel آن مشخص است (و نیازی به بررسی مجزا ندارد؛ یعنی خودش logger.IsEnabled را فراخوانی می‌کند) و همچنین از روش لاگ ساختار یافته استفاده می‌کند. مزیت این روش کش شدن قالب لاگ، در بار اول فراخوانی آن است ( برخلاف متدهای الحاقی مانند LogInformation که هربار باید این قالب‌ها را پردازش کنند) و همچنین در اینجا دیگر خبری از boxing و تبدیل نوع پارامترها نیست.

اکنون روش فراخوانی این Action با کارآیی بالا به صورت زیر است:
[HttpGet("/")]
public string Get()
{
    var person = new Person(123, "Test");
    _logHelloWorld(_logger, person, null);
      return "Hello world!";
}
همانطور که مشاهده می‌کنید اینبار دیگر حتی امکان فراموش کردن پارامتری وجود ندارد (مشکلی که می‌تواند با LogInformation متداول رخ دهد).


معرفی [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);
}
این قطعه کد، LoggerMessage.Define را به صورت خودکار برای ما تولید می‌کند. برای اینکار باید یک متد partial را تهیه کرد و سپس آن‌را به ویژگی جدید LoggerMessage مزین کرد. پس از آن source generator، مابقی کارها را در زمان کامپایل برنامه انجام می‌دهد.
ویژگی 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);
برای مثال در اینجا متد LogHelloWorld یک پارامتر دارد اما LoggerMessage آن به همراه دو پارامتر تعریف شده‌است که این مشکل در زمان کامپایل تشخیص داده شده و گوشزد می‌شود (برخلاف روش‌های پیشین که در زمان اجرا این نوع مشکلات نمایان می‌شدند).

در این روش، امکان ذکر پارامتر اختیاری LogLevel هم وجود دارد؛ اگر نیاز است مقدار آن به صورت پویا تغییر کند:
[LoggerMessage(Message = "hello world to {Person}")]
partial void LogHelloWorld(LogLevel logLevel, Person person);