مطالب
پیاده سازی یک MediaTypeFormatter برای پشتیبانی از MultiPart/form-data در Web API

Media Type یا MIME Type نشان دهنده فرمت یک مجموعه داده است. در HTTP، مدیا تایپ بیان کننده فرمت message body یک درخواست / پاسخ است و به دریافت کننده اعلام می‌کند که چطور باید پیام را بخواند. محل استاندارد تعیین Mime Type در هدر Content-Type است. درخواست کننده می‌تواند با استفاده از هدر Accept لیستی از MimeType‌های قابل قبول را به عنوان پاسخ، به سرور اعلام کند.

Asp.net Web API  از MimeType برای تعیین نحوه serialize یا deserialize کردن محتوای دریافتی / ارسالی استفاده می‌کند


MediaTypeFormatter

Web API برای خواندن/درج پیام در بدنه درخواست/پاسخ از MediaTypeFormmater‌‌ها استفاده می‌کند. اینها کلاس‌هایی هستند که نحوه‌ی Serialize کردن و deserialize کردن اطلاعات به فرمت‌های خاص را تعیین می‌کنند. Web API به صورت توکار دارای formatter هایی برای نوع‌های XML ، JSON، BSON و Form-UrlEncoded می‌باشد. همه این‌ها کلاس پایه MediaTypeFormatter را پیاده سازی می‌کنند. 


مسئله

یک پروژه Web API بسازید و view model زیر را در آن تعریف کنید:

public class NewProduct
    {
        [Required]
        public string Name { get; set; }

        public double Price { get; set; }

        public byte[] Pic { get; set; }
    }

همانطور که می‌بینید یک فیلد از نوع byte[] برای تصویر محصول در نظر گرفته شده است.

حالا یک کنترلر API  ساخته و اکشنی برای دریافت اطلاعات محصول جدید از کاربر می‌نویسیم :
public class ProductsController : ApiController
    {
        [HttpPost]
        public HttpResponseMessage PostProduct(NewProduct model)
        {
            if (ModelState.IsValid)
            {
                //  ثبت محصول 

                return new HttpResponseMessage(HttpStatusCode.Created);
            }

            return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
        }

    }

و یک صفحه html به نام index.html که حاوی یک فرم برای ارسال اطلاعات باشد :

<!DOCTYPE html>
<html>
<head>
    <title></title>
</head>
<body>
    <h1>ساخت MediaTypeFormatter برای Multipart/form-data</h1>
    <h2>محصول جدید</h2>

    <form id="newProduct" method="post" action="/api/products" enctype="multipart/form-data">
        <div>
            <label for="name">نام محصول : </label>
            <input type="text" id="name" name="name" />
        </div>
        <div>
            <label for="price">قیمت : </label>
            <input type="number" id="price"  name="price" />
        </div>
        <div>
            <label for="pic">تصویر : </label>
            <input type="file" id="pic" name="pic" />
        </div>
        <div>
            <button type="submit">ثبت</button>
        </div>
    </form>
</body>
</html>

زمانی که فرم حاوی فایلی برای آپلود باشد مشخصه encType باید برابر با Multipart/form-data مقداردهی شود تا اطلاعات فایل به درستی کد شوند. در زمان ارسال فرم Content-type درخواست برابر با Multipart/form-data و فرمت اطلاعات درخواست ارسالی به شکل زیر خواهد بود :


همانطور که می‌بینید هر فیلد در فرم، در یک بخش جداگانه قرار گرفته است که با خط چین هایی از هم جدا شده اند. هر بخش، header‌های جداگانه خود را دارد.

- Content-Disposition که نام فیلد و نام فایل را شامل می‌شود .

- content-type که mime type مخصوص آن بخش از داده‌ها را مشخص می‌کند.

پس از اینکه فرم را تکمیل کرده و ارسال کنید ، با پیام خطای زیر مواجه می‌شوید :

خطای روی داده اعلام می‌کند که Web API فاقد MediaTypeFormatter برای خواندن اطلاعات ارسال شده با فرمتMultiPart/Form-data  است.  Web API برای خواندن و بایند کردن پارامترهای complex Type از درون بدنه پیام یک درخواست از MediaTypeFormatter استفاده می‌کند و همانطور که گفته شد Web API فاقد Formatter توکار برای deserialize کردن داده‌های با فرمت Multipart/form-data است.

راه حل‌ها :

روشی که در سایت asp.net برای آپلود فایل در web api استفاده شده، عدم استفاده از پارامترها و خواندن محتوای Request در درون کنترلر است. که به طبع در صورتی که بخواهیم کنترلرهای تمیز و کوچکی داشته باشیم روش مناسبی نیست. از طرفی امتیاز parameter binding و modelstate را هم از دست خواهیم داد.

روش دیگری که می‌خواهیم در اینجا پیاده سازی کنیم ساختن یک MediaTypeFormatter برای خواندن فرمت Multipart/form-data است. با این روش کد موردنیاز کپسوله شده و امکان استفاده از binding و modelstate را خواهیم داشت.

برای ساختن یک MediaTypeFormatter یکی از 2 کلاس MediaTypeFormatter یا  BufferedMediaTypeFormatter  را باید پیاده سازی کنیم . تفاوت این دو در این است که BufferedMediaTypeFormatter برخلاف MediaTypeFormatter از متدهای synchronous استفاده می‌کند.

 
پیاده سازی :

یک کلاس به نام MultiPartMediaTypeFormatter می‌سازیم و کلاس MediaTypeFormatter را به عنوان کلاس پایه آن قرار می‌دهیم .

public class MultiPartMediaTypeFormatter : MediaTypeFormatter
{
    ...
}

ابتدا در تابع سازنده کلاس فرمت هایی که می‌خواهیم توسط این کلاس خوانده شوند را تعریف می‌کنیم :

        public MultiPartMediaTypeFormatter()
        {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data"));
        }
در اینجا Multipart/form-data را به عنوان تنها نوع مجاز تعریف کرده ایم.

سپس با پیاده سازی توابع CanReadType و CanWriteType مربوط به کلاس MediaTypeFormatter مشخص می‌کنیم که چه مدل‌هایی را می‌توان توسط این کلاس  serialize / deserialize کرد. در اینجا چون می‌خواهیم این کلاس محدود به یک مدل خاص نباشد، از یک اینترفیس برای شناسایی کلاس‌های مجاز استفاده می‌کنیم .

    public interface INeedMultiPartMediaTypeFormatter
    {
    }

و آنرا به کلاس newProduct اضافه می‌کنیم :

public class NewProduct : INeedMultiPartMediaTypeFormatter
{
 ...
}
از آنجا که تنها نیاز به خواندن اطلاعات داریم و قصد نوشتن نداریم، در متد CanWriteType مقدار false را برمی گردانیم .
        public override bool CanReadType(Type type)
        {
            return typeof(INeedMultiPartMediaTypeFormatter).IsAssignableFrom(type);
        }

        public override bool CanWriteType(Type type)
        {
            return false;
        }

و اما تابع ReadFromStreamAsync که کار خواندن محتوای ارسال شده و بایند کردن آنها به پارامترها را برعهده دارد 

public async override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContent content, IFormatterLogger formatterLogger)
که در آن پارامتر type مربوط به مدل مشخص شده به عنوان پارامتر اکشن (NewProduct)است و پارامتر content محتوای درخواست را در خود دارد.

ابتدا محتوای ارسال شده را خوانده و اطلاعات فرم را استخراج می‌کنیم و از طرف دیگر با استفاده از کلاس Activator یک نمونه از مدل جاری را ساخته و لیست property‌های آنرا استخراج می‌کنیم.

            MultipartMemoryStreamProvider provider = await content.ReadAsMultipartAsync();
            IEnumerable<HttpContent> formData = provider.Contents.AsEnumerable();

            var modelInstance = Activator.CreateInstance(type);
            IEnumerable<PropertyInfo> properties = type.GetProperties();

سپس در یک حلقه به ترتیب برای هر property متعلق به مدل، در میان اطلاعات فرم جستجو می‌کنیم. برای پیدا کردن اطلاعات متناظر با هر property در هدر Content-Disposition که در بالا توضیح داده شد، به دنبال فیلد همنام با property می‌گردیم .

            foreach (PropertyInfo prop in properties)
            {
                var propName = prop.Name.ToLower();
                var propType = prop.PropertyType;

                var data = formData.FirstOrDefault(d => d.Headers.ContentDisposition.Name.ToLower().Contains(propName));
در صورتی که فیلدی وجود داشته باشد کار را ادامه می‌دهیم .

 گفتیم که هر فیلد یک هدر، Content-Type هم می‌تواند داشته باشد. این هدر به صورت پیش فرض معادل text/plain است و برای فیلدهای عادی قرار داده نمی‌شود . در این مثال چون فقط یک فیلد غیر رشته ای داریم فرض را بر این گرفته ایم که در صورت وجود Content-Type، فیلد مربوط به تصویر است. در صورتیکهContentType  وجود داشته باشد، محتوای فیلد را به شکل Stream خوانده به byte[] تبدیل و با استفاده از متد SetValue در property مربوطه قرار می‌دهیم.

                if (data != null)
                {
                    if (data.Headers.ContentType != null)
                    {
                        using (var fileStream = await data.ReadAsStreamAsync())
                        {
                            using (MemoryStream ms = new MemoryStream())
                            {
                                fileStream.CopyTo(ms);
                                prop.SetValue(modelInstance, ms.ToArray());
                            }                            
                        }
                    }

در صورتی که Content-Type غایب باشد بدین معنی است که محتوای فیلد از نوع رشته است ( عدد ، تاریخ ، guid ، رشته ) و باید به نوع مناسب تبدیل شود. ابتدا آن را به صورت یک رشته می‌خوانیم و با استفاده از Convert.ChangeType آنرا به نوع مناسب تبدیل می‌کنیم و در property متناظر قرار می‌دهیم .

                if (data != null)
                {
                    if (data.Headers.ContentType != null)
                    {
                        //...
                    }
                    else
                    {
                        string rawVal = await data.ReadAsStringAsync();
                        object val = Convert.ChangeType(rawVal, propType);

                        prop.SetValue(modelInstance, val);
                    }
                }
و در نهایت نمونه ساخته شده از مدل را برگشت می‌دهیم.
return modelInstance;
برای فعال کردن این Formatter باید آنرا به لیست formmater‌های web api اضافه بکنیم. فایل WebApiConfig در App_Start را باز کرده و خط زیر را به آن اضافه می‌کنیم:
config.Formatters.Add(new MultiPartMediaTypeFormatter());
حال اگر مجددا فرم را به سرور ارسال کنیم، با پیام خطایی، مواجه نشده و عمل binding با موفقیت انجام می‌گیرد.

مطالب
نگاهی به تغییرات فایل Program.cs در نگارش‌های مختلف ASP.NET Core تا نگارش 6
در طی سال‌های قبل، نحوه‌ی آغاز برنامه‌های ASP.NET Core دچار تغییرات زیادی شده‌است که حداقل سه مورد مهم آن‌ها به صورت زیر است:
- استفاده از WebHost.CreateDefaultBuilder: این روش جهت تنظیم شروع به کار یک برنامه‌ی ASP.NET Core 2x مورد استفاده قرار می‌گرفت.
- استفاده از Host.CreateDefaultBuilder: روش پیش‌فرض آغاز برنامه‌های وب NET Core 3x. و NET 5x. که با معرفی generic host، امکان تهیه‌ی Worker services را میسر کردند.
- استفاده از WebApplication.CreateBuilder: روش جدید شروع به کار با برنامه‌های وب مبتنی بر NET 6.


استفاده از WebHost.CreateDefaultBuilder در ASP.NET Core 2.x

در نگارش اول ASP.NET Core، مفهومی به نام هاست پیش‌فرض (default host) وجود نداشت. یکی از مهم‌ترین نکات درنظر گرفته شده در طراحی ASP.NET Core، مفهوم «هزینه کردن به ازای احتیاج» است. یعنی اگر نیاز به قابلیتی نیست، نباید وجود داشته باشد. به این ترتیب یک قالب آغازین نباید به همراه تعداد زیادی بسته‌‌های NuGet و مقدار زیادی کد برای تنظیم باشد؛ زمانیکه واقعا قرار نیست از تمام آن‌ها استفاده شود. به همین جهت از نگارش 2، شروع به ساده‌کردن این قالب اولیه کردند و در این زمان، WebHost.CreateDefaultBuilder ارائه شد. این تنظیم به ظاهر ساده، کدهای پیش‌فرض مورد نیاز قابل توجهی را جهت ساخت ساده‌ی IWebHostBuilder و IWebHost به همراه دارد. در قالب به همراه آن، بین مفاهیم تنظیمات application و host تفاوت قائل شده‌اند؛ یعنی دو فایل Program.cs را برای تنظیمات application و فایل Startup.cs را برای ارائه‌ی تنظیمات هاست، تدارک دیدند.
در این طراحی، بین میدان دید Program و Startup تفاوت وجود دارد. هدف از Program، ارائه‌ی تنظیمات زیرساختی برنامه، مانند تنظیمات HTTP Server، یکپارچگی با IIS و امثال آن شد و عموما در طول عمر برنامه ثابت است. اما برای تغییر رفتار برنامه، بیشتر تغییرات و تنظیمات، به Startup محول شدند؛ مانند تنظیمات تزریق وابستگی‌ها، تنظیمات میان‌افزارها، مسیریابی‌ها و غیره.
public class Program
{
    public static void Main(string[] args)
    {
        BuildWebHost(args).Run();
    }

     public static IWebHost BuildWebHost(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .Build();
}
نمونه‌ا‌ی از فایل Program.cs برنامه‌های ASP.NET Core 2x را در اینجا ملاحظه می‌کنید این تنظیم ساده، کار خواندن فایل appsettings.json و برپایی تنظیمات برنامه، تنظیمات logging و همچنین تنظیمات یکپارچگی با Kestrel/IIS را در پشت صحنه انجام می‌دهد. همچنین ارجاعی را نیز به کلاس Startup برنامه دارد. متد UseStartup به صورت خودکار به دنبال متدهای ویژه‌ی ConfigureServices و Configure در کلاس آغازین برنامه گشته و آن‌ها را فراخوانی می‌کند تا تنظیمات تزریق وابستگی‌ها، مسیریابی‌ها و میان‌افزارها صورت گیرند.


معرفی Generic Host در ASP.NET Core 3.x/5

ASP.NET Core 3.x به همراه تغییرات بزرگی در کدهای آغازین برنامه‌های ASP.NET Core بود. تا پیش از آن، امکانات پایه‌ی ASP.NET Core تنها برای مقاصد وب قابل استفاده بود. اما در نگارش‌های 3x، با ارائه‌ی یک هاست عمومی، امکان پشتیبانی از سایر برنامه‌ها، مانند worker services را نیز میسر کرد؛ برای مثال پیشتیبانی از کارهای طولانی پس زمینه، هاست سرویس‌های gRPC، هاست سرویس‌های ویندوز و غیره. هدف اصلی از این تغییرات، به اشتراک گذاری فریم‌ورک پایه‌ی ASP.NET Core که تنها برای برنامه‌های وب ساخته شده بود و به همراه امکاناتی مانند تنظیمات، ثبت وقایع، تزریق وابستگی‌ها و غیره بود، با سایر انواع برنامه‌های یاد شده است. برای رسیدن به این هدف، Web Host نگارش 2x، به Generic Host نگارش 3x تغییر کرد و سپس ASP.NET Core برفراز آن بنا شد. اینبار بجای IWebHostBuilder، نمونه‌ی جدید IHostBuilder را داریم:
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            }; 
    }
}
در اینجا نمونه‌ا‌ی از فایل Program.cs برنامه‌های ASP.NET Core 3x را ملاحظه می‌کنید. با توجه به عمومی بودن این روش، قسمت ConfigureWebHostDefaults آن، همان کار WebHost.CreateDefaultBuilder نگارش 2 ASP.NET Core را انجام می‌دهد.
ASP.NET Core 5 به همراه تغییرات مهمی در این زمینه نبود و تنها با تغییر target framework در فایل csproj و به روز رسانی بسته‌های نیوگت مرتبط، کار ارتقاء به نگارش جدید در آن صورت می‌گرفت.


معرفی  WebApplicationBuilder در ASP.NET Core 6x  

در دات نت 6، روش آغاز برنامه‌های وب بطور کامل تغییر کرده‌است. در تمام نگارش‌های پیشین ASP.NET Core، همواره شاهد دو فایل Program.cs و Startup.cs بودیم؛ اما در اینجا فقط یک فایل Program.cs بیشتر وجود ندارد. هر چند همانطور که در مطلب «ارتقاء فایل‌های آغازین برنامه‌های ASP.NET Core 5x به 6x» نیز عنوان شد، می‌توان همان سبک و سیاق پیشین را نیز برگرداند و از این لحاظ محدودیتی وجود ندارد.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
   app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.MapGet("/", () => "Hello World!");
app.MapRazorPages();
app.Run();
در اینجا نمونه‌ای از فایل Program.cs برنامه‌های دات نت 6 را مشاهده می‌کنید که تغییرات قابل ملاحظه‌ای داشته‌است:
- استفاده از top level statements
- استفاده از usingهای سراسری و سایر قابلیت‌های C# 10.0
- حذف کامل کلاس Startup؛ اینبار همه چیز فقط یک فایل است.

دیدگاهی را که در اینجا بکار گرفته‌اند شامل این موارد است و بیشتر تازه‌واردان را مدنظر دارند:
- برای شروع به کار، using statements اضافی هستند؛ پس حذف شده‌اند!
- برای شروع به کار، namespaces اضافی هستند؛ پس حذف شده‌اند!
- برای شروع به کار، Program.Main ... هم اضافی است!
- تنظیمات بین دو فایل Program.cs و Startup.cs تقیسم نشده‌اند و همه‌چیز یکجا است و این هم نیازی به توضیح اضافی به تازه‌واردان، ندارد.
- همچنین همانطور که عنوان شد، « ... متد UseStartup به صورت خودکار به دنبال متدهای ویژه‌ی ConfigureServices و Configure در کلاس آغازین برنامه گشته و آن‌ها را فراخوانی می‌کند تا تنظیمات تزریق وابستگی‌ها، مسیریابی‌ها و میان‌افزارها صورت گیرند ...»
نکته‌ی مهم این توضیح، این است که کلاس Startup، هیچ اینترفیسی را پیاده سازی نمی‌کند. یعنی نام این متدها، دقیقا باید به همین صورت باشند (بدون اینکه قرار دادی توسط یک اینترفیس برای آن‌ها وضع شده باشد) و ... چرا واقعا باید به این صورت باشد؟! به همین جهت این‌ها هم حذف شده‌اند!
- در اینجا WebApplication هم مشاهده می‌شود؛ اما آیا واقعا نیازی به آن است؟
پاسخ: خیر! WebApplication.CreateBuilder ای که در اینجا ملاحظه می‌کنید در حقیقت ساده شده‌ی قطعه کد زیر از کدهای ASP.NET Core 3x/5x است:
var hostBuilder = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services => 
    {
        services.AddRazorPages();
    })
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.Configure((ctx, app) => 
        {
            if (ctx.HostingEnvironment.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseStaticFiles();
            app.UseRouting();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", () => "Hello World!");
                endpoints.MapRazorPages();
            });
        });
    }); 
hostBuilder.Build().Run();
که ... WebApplication.CreateBuilder بدون شک ساده‌تر از قطعه کد فوق است. همچنین پس از یک سطر زیر:
 var builder = WebApplication.CreateBuilder(args);
با استفاده از خاصیت builder.Configuration، می‌توان به تنظیمات برنامه دسترسی یافت و یا با استفاده از builder.Services می‌توان سرویس‌های جدیدی را ثبت کرد:
builder.Services.AddRazorPages();
builder.Services.AddSingleton<MyThingy>();
و یا با استفاده از خاصیت builder.Logging می‌توان تنظیمات ثبت وقایع برنامه را تغییر داد:
builder.Logging.AddFile();
همچنین در اینجا زمانیکه کار تنظیمات به پایان رسید، نمونه‌ای از WebApplication را به صورت زیر تولید کرده:
var app = builder.Build();
و اکنون می‌توان تنظیمات میان‌افزارها و یا مسیریابی‌ها را انجام داد:
app.UseStaticFiles();
app.MapRazorPages();
که در مقایسه با نگارش‌های قبلی، بسیار ساده‌تر شده‌، هرچند مرز بین این‌ها و ترتیب آن‌ها اندکی کم‌رنگ‌تر شده‌است.
مطالب
مفاهیم برنامه نویسی ـ مروری بر کلاس و شیء
من قصد دارم در قالب چند مطلب برخی از مفاهیم پایه و مهم برنامه نویسی را که پیش نیازی برای درک اکثر مطالب موجود در وب سایت است به زبان ساده بیان کنم تا دایره افرادی که می‌توانند از مطالب ارزشمند این وب سایت استفاده کنند وسعت بیشتری پیدا کند. لازم به توضیح است از آنجا که علاقه ندارم اینجا تبدیل به نسخه فارسی MSDN یا کتاب آنلاین آموزش برنامه نویسی شود این سری آموزش‌ها بیشتر شامل مفاهیم کلیدی خواهند بود.
این مطلب به عنوان اولین بخش از این سری مطالب منتشر می‌شود.

هدف این نوشته بررسی جزییات برنامه نویسی در رابطه با کلاس و شیء نیست. بلکه دریافتن چگونگی شکل گرفتن ایده شیء گرایی و علت مفید بودن آن است.

مشاهده مفاهیم شیء گرایی در پیرامون خود

حتماً در دنیای برنامه نویسی شیء گرا بارها با کلمات کلاس و شیء روبرو شده اید. درک صحیح از این مفاهیم بسیار مهم و البته بسیار ساده است. کار را با یک مثال شروع می‌کنیم. به تصویر زیر نگاه کنید.
 



در سمت راست بخشی از نقشه یک ساختمان و در سمت چپ ساختمان ساخته شده بر اساس این نقشه را می‌بینید. ساختمان همان شیء است. و نقشه ساختمان کلاس آن است چراکه امکان ایجاد اشیائی که تحت عنوان ساختمان طبقه بندی (کلاس بندی) می‌شوند را فراهم می‌کند. به همین سادگی. کلاس‌ها طرح اولیه، نقشه یا قالبی هستند که جزییات یک شی را توصیف می‌کنند.
حتماً با من موافق هستید اگر بگویم:
  • در نقشه ساختمان نمی‌توانید زندگی کنید اما در خود ساختمان می‌توانید.
  • از روی یک نقشه می‌توان به تعداد دلخواه ساختمان ساخت.
  • هنگامی که در یک ساختمان زندگی می‌کنید نیازی نیست تا دقیقاً بدانید چگونه ساخته شده و مثلاً سیم کشی یا لوله کشی‌های آن چگونه است! تنها کافیست بدانید برای روشن شدن لامپ باید کلید آن را بزنید.
  • ساختمان دارای ویژگی هایی مانند متراژ، ضخامت دیوار، تعداد پنجره و ابعاد هر یک و ... است که در هنگام ساخت و بر اساس اطلاعات موجود در نقشه تعیین شده اند.
  • ساختمان دارای کارکرد هایی است. مانند بالا و پایین رفتن آسانسور و یا باز و بسته شدن درب پارکینگ. هر یک از این کارکرد‌ها نیز بر اساس اطلاعات موجود در نقشه پیاده سازی و ساخته شده اند.
  • ساختمان تمام اجزای لازم برای اینکه از آن بتوانیم استفاده کنیم و به عبارتی در آن بتوانیم زندگی کنیم را در خود دارد.
در محیط پیرامون ما تقریباً هر چیزی را می‌توان در یک دیدگاه شیء تصور کرد. به عبارتی هر چیزی که بتوانید به صورت مستقل در ذهن بیاورید و سپس برخی ویژگی‌ها و رفتارها یا کارکردهای آن‌را برشمارید تا آن چیز را قابل شناسایی کند شیء است. مثلاً من به شما می‌گویم موجودی چهار پا دارد، مو... مو... می‌کند و شیر می‌دهد و ... . شما خواهید گفت گاو! و نمی‌گویید گربه. چرا؟ چون توانستید در ذهن خود موجودیتی را به صورت مستقل تصور کنید و از روی ویژگی‌ها و رفتارش آن‌را دقیقاً شناسایی کنید.
سوال: کلاس یا نقشه ایجاد گاو چیست؟ اگر از من بپرسید خواهم گفت طرح اولیه گاو هم ممکن است وجود داشته باشد البته در اختیار خداوند و با سطح دسترسی ملکوت!
اتومبیل، تلویزیون و ... همگی مثال هایی از اشیاء پیرامون ما در دنیای واقعی هستند که حتماً می‌توانید کلاس یا نقشه ایجاد آن‌ها را نیز بدست آورید و یا ویژگی‌ها و کارکرد‌های آن‌ها را برشمارید.

مفاهیم شیء گرایی در مهندسی نرم افزار

مفاهیمی که تاکنون در مورد دنیای واقعی مرور کردیم همان چیزی است که در دنیای برنامه نویسی ـ به عقیده من دنیای واقعی‌تر از دنیای واقعی ـ با آن سر و کار داریم. علت این امر آن است که اصولاً ایده روش برنامه نویسی شیء گرا با مشاهده محیط پیرامون ما به وجود آمده است.
برای نوشتن برنامه جهت حل یک مسئله بزرگ باید بتوان آن مسئله را به بخش‌های کوچکتری تقسیم نمود. در این رابطه مفهوم شیء و کلاس با همان کیفیتی که در محیط پیرامون ما وجود دارد به صورت مناسبی امکان تقسیم یه مسئله بزرگ به بخش‌های کوچکتر را فراهم می‌کند. و سبب می‌شود هماهنگی و تقارن و تناظر خاصی بین اشیاء برنامه و دنیای واقعی بوجود آید که یکی از مزایای اصلی روش شیء گراست.
از آنجا که در یک برنامه اصولاً همه چیز و همه مفاهیم در قالب کدها و دستورات برنامه معنا دارد، کلاس و شیء نیز چیزی بیش از قطعاتی کد نیستند. قطعه کد هایی که بسته بندی شده اند تا تمام کار مربوط به هدفی که برای آن‌ها در نظر گرفته شده است را انجام دهند.
همان طور که در هر زبان برنامه نویسی دستوراتی برای کارهای مختلف مانند تعریف یک متغیر یا ایجاد یک حلقه و ... در نظر گرفته شده است، در زبان‌های برنامه نویسی شیء گرا نیز دستوراتی وجود دارد تا بتوان قطعه کدی را بر اساس مفهوم کلاس بسته بندی کرد.
به طور مثال قطعه کد زیر را در زبان برنامه نویسی سی شارپ در نظر بگیرید.
class Player
{
   public string Name;
   public int Age;
   public void Walk()
   {
      // کدهای مربوط به پیاده سازی راه رفتن
   }
   public void Run()
   {
      // کدهای مربوط به پیاده سازی دویدن
   }
}
در این قطعه کد با استفاده از کلمه کلیدی class در زبان سی شارپ کلاسی ایجاد شده است که دارای دو ویژگی نام و سن و دو رفتار راه رفتن و دویدن است.
این کلاس به چه دردی می‌خورد؟ کجا می‌توانیم از این کلاس استفاده کنیم؟
پاسخ این است که این کلاس ممکن است برای ما هیچ سودی نداشته باشد و هیچ کجا نتوانیم از آن استفاده کنیم. اما بیایید فرض کنیم برنامه نویسی هستیم که قصد داریم یک بازی فوتبال بنویسیم. به جای آنکه قطعات کد مجزایی برای هر یک از بازیکنان و کنترل رفتار و ویژگی‌های آنان بنویسیم با اندکی تفکر به این نکته پی می‌بریم که همه بازیکنان مشترکات بسیاری دارند و به عبارتی در یک گروه یا کلاس قابل دسته بندی هستند. پس سعی می‌کنیم نقشه یا قالبی برای بازیکن‌ها ایجاد کنیم که دربردارنده ویژگی‌ها و رفتارهای آن‌ها باشد.
همان طور که در نقشه ساختمان نمی‌توانیم زندگی کنیم این کلاس هم هنوز آماده انجام کارهای واقعی نیست. چراکه برخی مقادیر هنوز برای آن تنظیم نشده است. مانند نام بازیکن و سن و ....
و همان طور که برای سکونت لازم است ابتدا یک ساختمان از روی نقشه ساختمان بسازیم برای استفاده واقعی از کلاس یاد شده نیز باید از روی آن شیء بسازیم. به این فرآیند وهله سازی یا نمونه سازی نیز می‌گویند. یک زبان برنامه نویسی شیء گرا دستوراتی را برای وهله سازی نیز در نظر گرفته است. در C# کلمه کلیدی new این وظیفه را به عهده دارد.
Player objPlayer = new Player();
objPlayer.Name = “Ali Karimi”;
objPlayer.Age = 30;
objPlayer.Run();
وقتی فرآیند وهله سازی صورت می‌گیرد یک نمونه یا شیء از آن کلاس در حافظه ساخته می‌شود که در حقیقت می‌توانید آنرا همان کدهای کلاس تصور کنید با این تفاوت که مقداردهی‌های لازم صورت گرفته است. به دلیل تعیین مقادیر لازم، حال شیء تولید شده می‌تواند به خوبی اعمال پیش بینی شده را انجام دهد. توجه نمایید در اینجا پیاده سازی داخلی رفتار دویدن و اینکه مثلاً در هنگام فراخوانی آن چه کدی باید اجرا شود تا تصویر یک بازیکن در حال دویدن در بازی نمایش یابد مد نظر و موضوع بحث ما نیست. بحث ما چگونگی سازماندهی کد‌ها توسط مفهوم کلاس و شیء است. همان طور که مشاهده می‌کنید ما تمام جزییات بازیکن‌ها را یکبار در کلاس پیاده سازی کرده ایم اما به تعداد دلخواه می‌توانیم از روی آن بازیکن‌های مختلف را ایجاد کنیم. همچنین به راحتی رفتار دویدن یک بازیکن را فراخوانی میکنیم بدون آنکه پیاده سازی کامل آن در اختیار و جلوی چشم ما باشد.
تمام آنچه که بازیکن برای انجام امور مربوط به خود نیاز دارد در کلاس بازیکن کپسوله می‌شود. بدیهی است در یک برنامه واقعی ویژگی‌ها و رفتارهای بسیار بیشتری باید برای کلاس بازیکن در نظر گرفته شود. مانند پاس دادن، شوت زدن و غیره.
به این ترتیب ما برای هر برنامه می‌توانیم مسئله اصلی را به تعدادی مسئله کوچکتر تقسیم کنیم و وظیفه حل هر یک از مسائل کوچک را به یک شیء واگذار کنیم. و بر اساس اشیاء تشخیص داده شده کلاس‌های مربوطه را بنویسیم. برنامه نویسی شیء گرا سبب می‌شود تا مسئله توسط تعدادی شیء که دارای نمونه‌های متناظری در دنیای واقعی هستند حل شود که این امر زیبایی و خوانایی و قابلیت نگهداری و توسعه برنامه را بهبود می‌دهد.
احتمالاً تاکنون متوجه شده اید که برای نگهداری ویژگی‌های اشیاء از متغیر‌ها و برای پیاده سازی رفتارها یا کارکرد‌های اشیاء از توابع استفاده میکنیم.
با توجه به این که هدف این مطلب بررسی مفهوم شیء گرائی بود و نه جزییات برنامه نویسی، بنابراین بیان برخی مفاهیم در این رابطه را که بیشتر در مهندسی نرم افزار معنا دارند تا در دنیای واقعی در مطالب بعدی بررسی می‌کنیم.
مطالب
بررسی تفاوت بین DTO و POCO
در ابتدا اجازه بدهید تعریف درستی از این دو واژه، ارائه کنیم.

DTO (Data Transfer Object)
به بیان خیلی ساده، DTO‌ها برای انتقال اطلاعات استفاده می‌شوند؛ پس هیچ منطق و رفتاری در این اشیاء تعریف نمی‌شود .اگر در DTO منطقی پیاده سازی شود، دیگر به آن DTO گفته نمی‌شود. اجازه بدید منظورمان را از منطق یا رفتار مشخص کنیم. منطق یا رفتار، همان متدهایی هستند که در نوع داده خود تعریف میکنیم. در #C، یک DTO تنها از خصوصیت‌ها (Properties) که از بلوک‌های Get و Set تشکیل شده‌اند، ساخته می‌شود. البته بدون کدهایی جهت اعتبار سنجی (Validation) مقادیر.

سؤال: وضعیت attribute ‌ها و Metadata‌ها چه می‌شود؟
خیلی غیر معمول نیست که از metadata‌ها در DTO، به‌منظور اعتبار سنجی یا اهداف خاص، استفاده کنیم. بعضی از attribute‌ها هیچ رفتاری را به DTO‌ها اضافه نمی‌کنند؛ ولی استفاده از DTO‌ها را در بخش‌های دیگر سیستم، ساده‌تر می‌کنند. در نتیجه هیچکدام از attribute ‌ها و metadata‌ها، شرایط DTO بودن را نقض نمی‌کنند.

مدل‌های دیگری مثل ViewModels‌ها و API Model‌ها چه می‌شوند؟
واژه DTO خیلی مبهم است. تنها چیزی که بیان می‌کند این است که شیء است و فقط و فقط شامل اطلاعات است و رفتاری ندارد. در این تعریف درباره‌ی کاربرد مورد نظر یک DTO چیزی گفته نشده. در بسیاری از معماری‌ها، DTO نقش خاصی را ایفا می‌کند. بطور مثال در معماری MVC، از DTO‌ها برای انقیاد داده (Binding) و ارسال اطلاعات به یک View استفاده میکنند. به همین خاطر این DTO‌ها بعنوان ViewModel، در معماری MVC شناخته می‌شوند که رفتاری را در خود تعریف نمی‌کنند و تنها فرمت اطلاعات مورد انتظار یک View را مهیا می‌کنند.
پس در این سناریوی خاص، ViewModel نوعی DTO می‌باشد. اما باید دقت داشته باشید، همه ViewModel‌‌ها را نمی‌توان DTO محسوب کرد؛ مثلا در معماری MVVM، ویوو مدل‌های تعریف شده، شامل رفتار هم می‌باشند. حتی در معماری MVC نیز گاهی اوقات منطقی به  ViewModel‌‌ها اضافه می‌شود که دیگر به آنها DTO نمی‌گوییم.

 
در صورت امکان، نام DTO‌ها را بر اساس استفاده‌ی آنها تعیین کنید. بطور مثال کلاسی با نام FoodDTO، مشخص نمی‌کند که این نوع، کجا و چگونه قرار است در معماری برنامه شما مورد استفاده قرار  بگیرید؛ برعکس نامگذاری به صورت FoodViewModels کاربرد آن را صراحتا بیان می‌کند.
مثالی از DTO در زبان سی شارپ :
public class ProductViewModel
{
  public int ProductId { get; set; }
  public string Name { get; set; }
  public string Description { get; set; }
  public string ImageUrl { get; set; }
  public decimal UnitPrice { get; set; }
}

کپسوله سازی و DTO ها 
کپسوله سازی، یکی از اصول برنامه نویسی شیءگرا می‌باشد. اما این کپسوله سازی به DTO‌ها اعمال نمی‌شوند. به این علت که هدف کپسوله سازی، پنهان کردن فرآیند پشت صحنه‌ی ذخیره سازی اطلاعات است؛ اما در DTO هیچ فرآیندی پیاده سازی نشده و نباید هیچ State پنهانی وجود داشته باشد. پس بحث Encapsulation در DTO منتفی است. پس کار را برای خودتان سخت نکنید؛ با تعریف private setter ‌ها یا تبدیل کردن DTO به یک شیء غیرقابل تغییر (immutable). شما باید به‌راحتی بتوانید عملیات ایجاد، نوشتن و خواندن DTO‌‌ها را انجام دهید؛ همچنین باید بتوانید عملیات سریالایز کردن بر روی DTO‌‌ها را بدون فرآیند سفارشی اضافه‌ای، انجام دهید.

Field ها  یا Property ها 
سؤالی که مطرح می‌شود این است که وقتی کپسوله سازی در DTO مفهومی ندارد، چرا باید همیشه از property ‌ها استفاده کنیم؟ چرا از فیلد‌ها استفاده نکنیم (فیلد‌های public )؟
ما می‌توانیم هم از property استفاده کنیم و هم از field‌ها؛ اما بعضی از فریم ورک‌ها که کار Serialization را انجام می‌دهند، فقط با property ‌ها کار می‌کنند. بنابراین بسته به نیاز خودتان، از field‌های عمومی یا property‌ها استفاده کنید. اما عموما از Property استفاده میکنند. البته در این پیوند، پرسش و پاسخ مفصلی در این رابطه وجود دارد.

غیرقابل تغییر بودن (Immutability) و نوع رکورد ( Record Type )
غیرقابل تغییر بودن، یکی از مزیت‌های مهم در توسعه نرم افزار است. اما همانطور که در مثل بالا بیان شد، نیازی به غیرقابل تغییر کردن DTO‌‌ها نیست. با ارائه رکورد در سی شارپ 9  شرایط کمی تغییر کرد. شاید عبارت مخفف دیگری که اضافه شده Data transfer Records یا (DTRs) است. یکی از روش‌های تعریف DTR در سی شارپ 9، به شکل زیر است:
public record ProductDTO(int Id, string Name, string Description);

البته روش دیگری هم وجود دارد که شما property‌ها را تعریف کنید و از طریق سازنده، مقدار دهی شوند. ویژگی جدید init-only این امکان را فراهم می‌کند که فقط در زمان مقدار دهی اولیه (initialization)، خصوصیات مقداردهی شوند و در ادامه‌ی چرخه حیات شیء، property ‌ها فقط خواندنی هستند. این ویژگی، record‌ها را غیر قابل تغییر می‌کند.
مثال:
public record ProductDTO
{
  public int Id { get; init; }
  public string Name { get; init; }
}
var dto = new ProductDTO { Id = 1, Name = "some name" };

کلاس‌های POCO یا همان Plain Old CLR/C# Object
شی Plain Old چیست؟ هر شیءای که Plain Old باشد، می‌تواند در هر جایی از برنامه‌ی ما مورد استفاده قرار بگیرد؛ حتی در کلاس‌های Test برنامه. این اشیاء هیچگونه وابستگی برای اجرا وظایف خود، به بانک‌های اطلاعاتی و کتابخانه‌های ثالت ندارند.
برای درک بهتر این نوع کلاس‌ها، به مثال زیر دقت کنید:
public class Product : DataObject<Product>
{
  public Product(int id)
  {
    Id = id;
    InitializeFromDatabase();
  }
  private void InitializeFromDatabase()
  {
    DataHelpers.LoadFromDatabase(this);
  }
  public int Id { get; private set; }
  // other properties and methods
}
همانطور که مشاهده میکنید، این کلاس به متد استاتیکی برای کار با دیتابیس وابسته است؛ در نتیجه باعث میشود که کل کلاس، به وجود بانک اطلاعاتی وابسته شود. همچنین با ارث بری از کلاس پایه‌ی دیگری، وابستگی به یک کتابخانه‌ی ثالث ایجاد شده‌است. اجرای آزمون واحد برای چنین کلاسی، سبب fail شدن عملیات می‌شود. به این علت که ارتباط با بانک اطلاعاتی مورد نیاز متد DataHelpers، تامین نشده‌است. این شرایط، مثالی از الگوی Active Record Pattern می‌باشند. همچنین این کلاس دسترسی به منبع داده را در درون خود گنجانده است که این به معنای نقض اصل Persistence Ignorant (اصل Persistence Ignorance به طور خلاصه بیان می‌کند که در تحلیل و طراحی Business Logic به موضوع ذخیره‌سازی (Persistence) فکر نکنید (تا جای ممکن) یا به عبارت دیگر، ذهن خود را درگیر پیچیدگی‌های ذخیره سازی نکنید. برگرفته شده از breakpoint.blog.ir : روح الله دلپاک)می باشد. یکی از ویژگی‌های POCO عدم نقض الگوی فوق است.

مثالی از POCO : 
public class Product
{
  public Product(int id)
  {
    Id = id;
  }

  private Product()
  {
    // required for EF
  }

  public int Id { get; private set; }
  // other properties and methods
}
این کلاس یک POCO است:
  • برای اجرای وظایف خود به فریم ورک ثالثی وابسته نیست.
  • به کلاس پایه‌ای ( Base class) نیاز ندارد.
  • وابستگی به متد استاتیکی ندارد.
  • می تواند در هر جایی از پروژه، نمونه سازی شود.
  • اصل Persistence Ignorant را بیشتر رعایت کرده، نه بطور کامل؛ چون یک سازنده دارد که به کتابخانه‌ی ثالثی نیازمند است (سازنده‌ی بدون پارامتر که مورد نیاز EF می‌باشد).

POCO و DTO :

شاید این دو مفهموم گیج کننده باشند، ولی DTO همان POCO هست. اگر یک کلاس، DTO باشد، حتما POCO نیز هست. (مرور ویژگی‌های دو مورد در بخش‌های قبلی) ولی برعکس این وضعیت ممکن است صادق نباشد؛ مثال قبلی که در آن وابستگی به کتابخانه‌ی ثالثی در سازنده‌ی بدون پارامتر وجود داشت، DTO بودن را نقض می‌کرد. پس اگر هر دو حالت صادق بود، میتوان گفت این دو مفهوم یکی است.
مطالب
استفاده از فایل Json برای ذخیره و بازیابی تنظیمات برنامه
قطعا شرایطی پیش خواهد آمد که شما مجبور شوید داده‌هایی را به عنوان تنظیمات برنامه در محلی ذخیره کنید و مجددا آن‌ها را فراخوانی کنید. روش‌های مختلفی برای این کار وجود دارند که معروف‌ترین و ساده‌ترین راه، استفاده از Settings خود پروژه می‌باشد. اما این به منزله بهترین راه نیست! در این مطلب قصد داریم تنظیمات برنامه را در یک فایل json، با همان ساختار استانداردش ذخیره و بازیابی کنیم.
برای اینکار نیاز به سریالایز و دیسریالایز کردن مدل داریم. اگر از دات نت کور استفاده میکنید، کتابخانه توکار جیسون، در فضای نام System.Text.Json از عهده این کار بر میاد و اگر از نسخه‌های دات نت فریمورک استفاده میکنید، باید پکیج newtonsoft.json را نصب کنید.
برای شروع یک کلاس را به نام GlobalData (یا هر نام دلخواه دیگری) ایجاد کنید.
چون قرار هست این کلاس هر نوع مدلی را برای ما سریالایز و دیسریالایز کند، پس کلاس را بصورت جنریک تعریف کنید.
public abstract class GlobalData<T> where T : GlobalData<T>, new()
حالا برای دسترسی به این کلاس، یک متغیر جنریک را به نام Config ایجاد میکنیم:
public static T Config { get; set; }
همینطور برای خواندن یک فایل جیسون از محلی مشخص، یک متغیر دیگر را به نام filename ایجاد میکنیم:
private static string _filename { get; set; }
در این کلاس به 2 متد نیاز داریم. متد اول برای دیسریالایز کردن فایل جیسون و متد دوم برای سریالایز کردن اطلاعات:
public static void Init(string FileName = "AppConfig.json")
        {
            _filename = FileName;
            if (File.Exists(FileName))
            {
                string json = File.ReadAllText(FileName);

                Config = (string.IsNullOrEmpty(json) ? new T() : JsonSerializer.Deserialize<T>(json)) ?? new T();
            }
            else
            {
                Config = new T();
            }
        }
بصورت پیشفرض محل خواندن فایل جیسون را در کنار فایل اجرایی exe و با نام AppConfig.json در نظر میگیریم. در صورتی که فایل ما موجود بود، به کمک ReadAllText محتوای فایل جیسون را میخوانیم و در صورتی که خالی نبود، اقدام به دیسریالایز کردن آن میکنیم.
در متد Save نیز:
public static void Save()
        {
            JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = true, IgnoreNullValues = true };

            string json = JsonSerializer.Serialize(Config, options);
            File.WriteAllText(_filename, json);
        }
پراپرتی Config را که شامل اطلاعات ما میباشد، در محل موردنظر سریالایز میکنیم.
حالا به سراغ پروژه‌ی دمو می‌رویم. یک کلاس را ایجاد کرده و از کلاس GlobalData ارث بری میکنیم:
internal class AppConfig : GlobalData<AppConfig>
حالا پراپرتی‌های دلخواه خود را ایجاد میکنیم:
 public string ServerUrl { get; set; } = "https://sub.deltaleech.com";
 public bool IsShowNotification { get; set; } = true;
 public NavigationViewPaneDisplayMode PaneDisplayMode { get; set; } = NavigationViewPaneDisplayMode.Left;

 public SkinType Skin { get; set; } = SkinType.Default;
دقت کنید که قبل از خواندن تنظیمات، باید ابتدا فایل تنظیمات را دیسریالایز کرده باشید. پس هنگام اجرای پروژه، متد Init را فراخوانی کنید:
protected override void OnStartup(StartupEventArgs e) {
GlobalData<AppConfig>.Init();
}
برای خواندن تنظیمات به این صورت عمل کنید:
var skin = GlobalData<AppConfig>.Config.Skin;
و برای ذخیره کردن :
GlobalData<AppConfig>.Config.Skin = Skin.Dark;
GlobalData<AppConfig>.Save();

دقت داشته باشید که اگر بعد از ذخیره کردن تنظیمات، قصد داشته باشید اطلاعات جدید را دریافت کنید، باید حتما قبل از دریافت اطلاعات، متد Init را یکبار دیگر فراخوانی کنید تا اطلاعات جدید، نمایش داده شود.

مطالب
لینک‌های هفته‌ی آخر اسفند

وبلاگ‌ها ، سایت‌ها و مقالات ایرانی (داخل و خارج از ایران)

امنیت

Visual Studio

ASP. Net

طراحی و توسعه وب

PHP

اس‌کیوال سرور

سی شارپ

VB

عمومی دات نت

جاوا

ویندوز

مسایل اجتماعی و انسانی برنامه نویسی

SVN

متفرقه

مطالب
ASP.NET Web API - قسمت پنجم
مدیریت کدهای وضعیت در Web API
تمامی پاسخ‌های دریافتی از Web API توسط Client، باید در قالب کدهای وضعیت HTTP باشند. دو کلاس جدید با نام‌های HttpResponseMessage و HttpResponseException همراه با ASP.NET MVC 4 معرفی شده اند که ارسال کدهای وضعیت پردازش درخواست به Client را آسان می‌سازند. به عنوان مثال، ارسال وضعیت برای چهار عمل اصلی بازیابی، ایجاد، آپدیت و حذف رکورد را بررسی می‌کنیم.

بازیابی رکورد
بر اساس مستندات پروتوکل HTTP، در صورتی که منبع درخواستی Client پیدا نشد، باید کد وضعیت 404 برگشت داده شود. این حالت را در متد ذیل پیاده سازی کرده ایم.
public Product GetProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound)); 
    }
    return item;
}
در صورتی که رکوردی با مشخصه‌ی درخواستی پیدا نشد، با استفاده از کلاس HttpResponseException، خطایی به Client ارسال خواهد شد. پارامتر سازنده‌ی این کلاس، شی ای از نوع کلاس HttpResponseMessage است. سازنده‌ی کلاس HttpResponseMessage، مقداری از یک enum با نام HttpStatusCode را می‌پذیرد. مقدار NotFound، نشان از خطای 404 است و زمانی به کار می‌رود که منبع درخواستی وجود نداشته باشد. اگر محصول درخواست شده یافت شد، در قالب JSON برگشت داده می‌شود. در شکل ذیل، پاسخ دریافتی در زمان درخواست محصولی که وجود ندارد را ملاحظه می‌کنید.

ایجاد رکورد
برای ایجاد رکورد، Client درخواستی از نوع POST را همراه با داده‌های رکورد در بدنه‌ی درخواست به Server ارسال می‌کند. در ذیل، پیاده سازی ساده ای از این حالت را مشاهده می‌کنید.
public Product PostProduct(Product item)
{
    item = repository.Add(item);
    return item;
}
 این پیاده سازی کار می‌کند اما کمبودهایی دارد:
  • کد وضعیت پردازش درخواست: به طور پیش فرض، Web API، کد 200 را در پاسخ ارسال می‌کند، اما بر اساس مستندات پروتوکل HTTP، زمانی که یک درخواست از نوع POST منجر به تولید منبعی می-شود، Server باید کد وضعیت 201 را به Client برگشت بدهد.
  • آدرس منبع جدید ایجاد شده: بر اساس مستندات پروتوکل HTTP، زمانی که منبعی بر روی Server ایجاد می‌شود، باید آدرس منبع جدید ایجاد شده از طریق هدر Location به Client ارسال شود.
با توجه به این توضیحات، متد قبل به صورت ذیل در خواهد آمد.
public HttpResponseMessage PostProduct(Product item)
{
    item = repository.Add(item);
    var response = Request.CreateResponse(HttpStatusCode.Created, item);

    string uri = Url.Link("DefaultApi", new { id = item.Id });
    response.Headers.Location = new Uri(uri);
    return response;
}
همان طور که ملاحظه می‌کنید، خروجی متد از نوع کلاس HttpResponseMessage است، چون با استفاده از این نوع می‌توانیم جزئیات مورد نیاز را در مورد نتیجه‌ی پردازش درخواست به مرورگر ارسال کنیم. همچنین، داده‌های رکورد جدید نیز در بدنه‌ی پاسخ، با یک فرمت مناسب مانند XML یا JSON برگشت داده می‌شوند. با استفاده از متد CreateResponse کلاس Request و پاس دادن کد وضعیت و شی ای که قصد داریم به Client ارسال شود به این متد، شی ای از نوع کلاس HttpResponseMessage ایجاد می‌کنیم. آدرس منبع جدید نیز با استفاده از response.Headers.Location مشخص شده است. نمونه ای از پاسخ دریافت شده در سمت Client به صورت ذیل است.


آپدیت رکورد
آپدیت با استفاده از درخواست‌های از نوع PUT انجام می‌گیرد. یک مثال ساده در این مورد.

public void PutProduct(int id, Product product)
{
    product.Id = id;
    if (!repository.Update(product))
    {
        throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound));
    }
}
نام متد با عبارت Put آغاز شده است. بنابراین توسط Web API برای پردازش درخواست‌های از نوع PUT در نظر گرفته می‌شود. متد قبل، دو پارامتر ورودی دارد. id برای مشخصه‌ی محصول، و محصول آپدیت شده که در پارامتر دوم قرار می‌گیرد. مقدار پارامتر id از آدرس دریافت می‌شود و مقدار پارامتر product از بدنه‌ی درخواست. به طور پیش فرض، Web API، مقدار داده هایی با نوع ساده مانند int، string و bool را از طریق route، و مقدار نوع‌های پیچیده‌تر مانند داده‌های یک کلاس را از بدنه‌ی درخواست می‌خواند.

حذف یک رکورد
حذف یک رکورد، با استفاده از درخواست‌های از نوع DELETE انجام می‌گیرد. یک مثال ساده در این مورد.
public HttpResponseMessage DeleteProduct(int id)
{
    repository.Remove(id);
    return new HttpResponseMessage(HttpStatusCode.NoContent);
}
بر اساس مستندات پروتکل HTTP، اگر منبعی که Client قصد حذف آن را دارد از پیش حذف شده است، نباید خطایی به وی گزارش شود. معمولاً در متدهایی که وظیفه‌ی حذف منبع را بر عهده دارند، کد 204 مبنی بر پردازش کامل درخواست و پاسخ خالی برگشت داده می‌شود. این کد با استفاد از مقدار NoContent برای HttpStatusCode مشخص می‌شود.

فراخوانی متدها و مدیریت کدهای وضعیت HTTP در سمت Client
حال ببینیم چگونه می‌توان از متدهای قبل در سمت Client استفاده و خطاهای احتمالی آنها را مدیریت کرد.
بهتر است مثال را برای حالتی که در آن رکوردی آپدیت می‌شود بررسی کنیم. کدهای مورد نیاز برای فراخوانی متد PutProduct در سمت Client به صورت ذیل است.
var id = $("#myTextBox").val();

$.ajax({
  url: "/api/Test/" + id,
   type: 'PUT',
   data: { Id: "1", Name: "Tomato Soup", Category: "Groceries", Price: "1.39M" },
   cache: false,
   statusCode: {
       200: function (data) {
           alert("آپدیت انجام شد");
        },
       404:
            function () {
                alert("خطا در آپدیت");
            }
      }
});
از متدهای get، getJson یا post در jQuery نمی‌توان برای عمل آپدیت استفاده نمود، چون Web API انتظار دارد تا نام فعل درخواستی، PUT باشد. اما با استفاده از متد ajax و ذکر نام فعل در پارامتر type آن می‌توان نوع درخواست را PUT تعریف کرد. خط 5 بدین منظور است. از طریق خصیصه‌ی statusCode نیز می‌توان کدهای وضعیت مختلف HTTP را بررسی کرد. دو کد 200 و 404 که به ترتیب نشان از موفقیت و عدم موفقیت در آپدیت رکورد هستند تعریف شده و پیغام مناسب به کاربر نمایش داده می‌شود.
در حالتی که آپدیت با موفقیت همراه باشد، بدنه‌ی پاسخ به شکل ذیل است.


  و در صورتی که خطایی رخ دهد، بدنه‌ی پاسخ دریافتی به صورت ذیل خواهد بود.


مطالب
پیاده سازی کنترلرهای Angular با استفاده از Typescript
پیشتر با ویژگی ها  و نحوه کد نویسی این زبان آشنا شدید. از طرفی دیگر، نحوه تعریف کنترلرها در Angular نیز آموزش داده شد. در این پست قصد دارم طی یک مثال ساده با استفاده از زبان Typescript یک کنترلر Angular را ایجاد  و سپس از آن در یک پروژه Asp.Net MVC استفاده نمایم. از آن جا که به صورت پیش فرض در VS.Net امکانات TypeScript نصب نشده است، برای شروع ابتدا TypeScript را از اینجا دانلود نمایید. بعد از نصب یک پروژه Asp.Net MVC ایجاد نمایید و سپس با استفاده از nuget فایل‌های مربوط به AngularJs  را نصب نمایید. در این پست به تفصیل این مورد بررسی شده است (عملیات BundleConfig فایل‌های مورد نیاز به عهده خودتان). در پوشه scripts یک فولدر به نام app ساخته، سپس یک فایل TypeScript به نام ProductController.ts ایجاد کنید. (بعد از نصب TypeScript گزینه TypeScript File مشاهده خواهد شد)
 

در فایل ProductController.ts کد‌های زیر را کپی نمایید: 

module Product {
    export interface Scope {
        message: string;
    }

    export class Controller {
        constructor($scope: Scope) {
            $scope.message = "Hello from Masoud";
        }
    }
}
توضیح کد‌ها بالا :

ابتدا یک ماژول به نام Product ایجاد می‌کنیم. سپس یک اینترفیس برای پیاده سازی آبجکت Scope که جهت مقید سازی عناصر DOM به آبجکت‌های کنترلر مورد استفاده قرار می‌گیرد، ایجاد می‌کنیم. در داخل این اینترفیس متغیری به نام message از نوع string داریم. قصد داریم این متغیر را به یک  عنصر مقید کنیم. حال یک کلاس به نام کنترلر ایجاد می‌کنیم که در تابع سازنده آن تزریق وابستگی برای scope$ از نوع اینترفیس Scope تعیین شده است. در نتیجه در بدنه سازنده می‌توانیم به متغیر message مقدار مورد نظر را نسبت دهیم .

کلمه کلیدی export برای تعریف عمومی کلاس استفاده شده است .
یک View ایجاد و کد‌های زیر را در آن کپی کنید :

<script type="text/javascript" src="~/scripts/app/ProductController.js"></script>
<div ng-app>
    <div ng-controller="Product.Controller">
        <p>{{message}}</p>
    </div>
</div>

اولین نکته در تگ script است که فراخوانی فایل TypeScript باید با پسوند   js. انجام گیرد. به دلیل اینکه فایل‌های TypeScript بعد از کامپایل تبدیل به فایل‌های JavaScript خواهند شد؛ در نتیجه پسوند آن نیز js. است. دومین نکته در فراخوانی کنترلر مورد نظر است که  از ترکیب نام ماژول و نام کلاس است. بعد از اجرای پروژه خروجی به صورت زیر خواهد بود :

 

نظرات مطالب
کاهش تعداد بار تعریف using ها در C# 10.0 و NET 6.0.
یک نکته‌ی تکمیلی: ترفندی برای معرفی Stubs توسط ویژگی global using statements در unit tests

برای نمونه متد زیر را درنظر بگیرید:
public static string CurrentInvariantMonthName()
    {
        var month = DateTimeOffset.UtcNow.Month;
        return CultureInfo
            .InvariantCulture
            .DateTimeFormat
            .GetMonthName(month);
    }
در کل نوشتن آزمون واحد برای متدهایی که با زمان و خواصی مانند UtcNow و یا Now، کار می‌کنند، مشکل است. در آزمون‌های واحد نیاز داریم تا یک خروجی مشخص را با مقداری از پیش معلوم، مقایسه کنیم تا اطمینان حاصل شود که عملیات صورت گرفته، صحیح است. اما UtcNow هر بار متغیر است.
برای حل این مشکل، با استفاده از ویژگی global using statements و compiler directives، می‌توان دو مفهوم متفاوت را برای زمان ارائه داد:
#if MOCK_TIME
global using DateTimeOffset = StubDateTimeOffset; 
#else
global using DateTimeOffset = System.DateTimeOffset;
#endif

public static class StubDateTimeOffset
{
    private static System.DateTimeOffset? value;
    
    public static System.DateTimeOffset UtcNow 
        => value ?? System.DateTimeOffset.UtcNow ;
    
    public static void Set(System.DateTimeOffset dateTimeOffset) {
        value = dateTimeOffset;
    }

    public static void Reset() => value = null;
}
در اینجا یک فایل خالی cs. ایجاد شده و قطعه کد فوق در آن درج می‌شود. در این حالت اگر توسط تنظیمات کامپایلر در فایل csproj برنامه، MOCK_TIME فعال شود، مفهوم DateTimeOffset، دیگر همان System.DateTimeOffset استاندارد نخواهد بود؛ بلکه از یک کلاس بدلی به نام StubDateTimeOffset تامین می‌شود. برای فعالسازی MOCK_TIME هم می‌توان به صورت زیر عمل کرد که این تعریف، در فایل csproj پروژه‌ی آزمایشی قرار می‌گیرد:
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
   <DefineConstants>MOCK_TIME</DefineConstants>
</PropertyGroup>
پس از این تغییر، اینبار کلاس StubDateTimeOffset، تامین کننده‌ی DateTimeOffset در آزمون‌های واحد خواهد بود و در این آزمون‌ها می‌توان با مقدار دهی DateTimeOffset به صورت زیر، هربار آزمایشات را بر اساس یک UtcNow «مشخص» انجام داد:
// set time
StubDateTimeOffset.Set(new(new(2022, 7, 1)));
مطالب
کامپوننت‌های جنریک در Blazor
طرح مشکل! نیاز به دریافت انواع و اقسام مقادیر یک جنس (مانند اعداد و یا تاریخ) در کامپوننت‌های Blazor

فرض کنید می‌خواهید عددی را در کامپوننتی، دریافت کنید. می‌توان اینکار را با تعریف یک پارامتر عمومی به صورت زیر انجام داد:
[Parameter] public int Value { get; set; }
و ... مشکل از همینجا شروع می‌شود! خوب، برای نوع‌های double ، decimal ، float ، long و غیره چه باید کرد؟ آیا باید به ازای هر کدام، یک پارامتر مخصوص را تعریف کنیم؟ و یا اگر خواستیم زمان و تاریخ را از کاربر دریافت کنیم، آیا باید او را مجبور به ارائه‌ی فقط DateTime کنیم و یا ... شاید بهتر باشد به او بگوییم اگر «رشته‌ای» را در اختیار ما قرار دهید، ما یکبار دیگر خودمان کار تبدیل آن‌را به نوع تاریخ انجام خواهیم داد!
برای حل این نوع مشکلات، می‌توان در Blazor پارامترها را جنریک تعریف کرد. برای نمونه اگر به طراحی یک سری کامپوننت‌های پایه‌ای Blazor دقت کنیم، امکان دریافت مستقیم انواع و اقسام اعداد و یا انواع و اقسام نوع‌های مرتبط با زمان را دارند؛ بدون اینکه این اطلاعات را به صورت رشته‌ای دریافت کنند و یا به ازای هر نوع عددی، یک پارامتر جداگانه را تعریف و یا اینکه استفاده کننده را محدود به ورود تنها یک نوع عددی و یا زمانی خاص کرده باشند.


نحوه‌ی جنریک تعریف کردن یک کامپوننت در Blazor

Blazor فایل‌های razor. را در حین پروسه‌ی build، به cs. تبدیل می‌کند و از آنجائیکه فایل‌های razor. الزامی به تعریف نام کلاس مرتبط را ندارند و این نام به صورت خودکار دقیقا از نام خود فایل، دریافت می‌شود، نیاز است راهی را یافت تا بتوان کلاس مرتبط را به صورت Generic تعریف کرد. برای این منظور، دایرکتیو ویژه‌ای به نام typeparam@ پیش‌ بینی شده‌است که توسط آن می‌توان یک یا چند نوع/پارامتر جنریک جدا شده‌ی توسط کاما را تعریف کنیم (تعریف شده‌ی در فایل فرضی MyGenericComponent.razor):
@typeparam T

@code {
    [Parameter] public T Value { get; set; }
}
در این مثال کامپوننتی را مشاهده می‌کنید که مقدار دریافتی خود را به صورت جنریک از نوع T دریافت می‌کند. این T باید توسط دایرکتیو typeparam@ دقیقا قید شود تا در حین ساخت خودکار کلاس معادل این فایل، مورد استفاده قرار گیرد.


نحوه‌ی جنریک تعریف کردن کامپوننت‌های دارای code-behind

اگر قسمت code@ فایل‌های razor. را به فایل‌های code-benind منتقل می‌کنید و داخل فایل razor.، کد #C نمی‌نویسید، روش جنریک تعریف کردن یک کامپوننت، به صورت زیر خواهد بود (برای مثال دو فایل MyGenericComponentWithCodeBehind.razor و MyGenericComponentWithCodeBehind.razor.cs تعریف شده‌اند):
using Microsoft.AspNetCore.Components;

namespace BlazorGenericComponents.Pages;

public partial class MyGenericComponentWithCodeBehind<T>
{
    [Parameter] public T Value { get; set; }
}
در اینجا ذکر دایرکتیو typeparam@ در فایل razor. متناظر هم باید صورت گیرد:
@typeparam T
یعنی هر دو کلاس نهایی حاصل از این فایل‌ها که به صورت partial تعریف شده‌اند (و در آخر یکی می‌شوند)، باید به صورت جنریک تعریف شوند.


روش مقید کردن پارامترهای جنریک در کامپوننت‌ها

از زمان دات نت 6 به بعد، امکان محدود کردن بازه‌ی نوع‌های قابل پذیرش T نیز میسر شده‌است:
@typeparam T where T : IMyInterface


روش مشخص کردن صریح نوع پارامترهای جنریک، در حین استفاده‌ی از آن‌ها

عموما نیازی به مشخص کردن نوع پارامترهای جنریک، در حین استفاده‌ی از آن‌ها نیست و کامپایلر بر اساس نوع مقدار ورودی، سعی خواهد کرد این نوع را به صورت خودکار تشخیص دهد؛ اما اگر به هر دلیلی چنین امری ممکن نبود و خطایی را دریافت کردید، می‌توان نوع پارامتر جنریک را به صورت زیر مشخص کرد:
<MyGenericComponent Value="10" T="int"/>
در اینجا نام پارامتر جنریک، ذکر شده و سپس نوعی به آن انتساب داده می‌شود.


روش پردازش پارامترهای جنریک در کامپوننت‌ها

تا اینجا امکان پذیرش نوع‌های جنریک را توسط استفاده کننده میسر کردیم؛ اما قسمت دیگر آن، نحوه‌ی کار با نوع T، درون یک کامپوننت است:
using Microsoft.AspNetCore.Components;

namespace BlazorGenericComponents.Pages;

public partial class MyGenericComponentWithCodeBehind<T>
{
    private string FormattedValue { set; get; }

    [Parameter] public T Value { get; set; }

    [Parameter] public string Format { get; set; }

    protected override void OnParametersSet()
    {
        FormattedValue = ConvertNumberToText();
    }

    private string ConvertNumberToText()
    {
        return Value switch
        {
            short shortValue => shortValue.ToString(Format),
            ushort ushortValue => ushortValue.ToString(Format),
            int intValue => intValue.ToString(Format),
            uint uintValue => uintValue.ToString(Format),
            byte byteValue => byteValue.ToString(Format),
            sbyte sbyteValue => sbyteValue.ToString(Format),
            decimal decimalValue => decimalValue.ToString(Format),
            double doubleValue => doubleValue.ToString(Format),
            float floatValue => floatValue.ToString(Format),
            long longValue => longValue.ToString(Format),
            ulong ulongValue => ulongValue.ToString(Format),
            _ => string.Empty
        };
    }
}
برای مثال فرض کنید کامپوننت جنریک ما قرار است انواع و اقسام اعداد را بپذیرد و سپس بر اساس Format مشخص شده، آن‌ها را نمایش دهد. در اینجا جهت واکنش نشان دادن به تغییرات Value، می‌توان از روال رخ‌داد گردان OnParametersSet استفاده کرد. سپس در متد ConvertNumberToText، بر اساس هر نوع میسر عددی، باید منطق ویژه‌ای را تهیه کرد که نمونه‌ای از آن‌را در اینجا مشاهده می‌کنید.

و در آخر نمایش نهایی آن هم در فایل razor. متناظر، به این صورت خواهد بود:
@typeparam T

@FormattedValue