سادهترین تعریف MVVM، نهایت استفاده از امکانات Binding موجود در WPF و Silverlight است. اما خوب، همیشه همه چیز بر وفق مراد نیست. مثلا کنترل WebBrowser را در WPF در نظر بگیرید. فرض کنید که میخواهیم خاصیت Source آنرا در ViewModel مقدار دهی کنیم تا صفحهای را نمایش دهد. بلافاصله با خطای زیر متوقف خواهیم شد:
A 'Binding' cannot be set on the 'Source' property of type 'WebBrowser'.
A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
بله؛ این خاصیت از نوع DependencyProperty نیست و نمیتوان چیزی را به آن Bind کرد. بنابراین این نکته مهم را توسعه دهندههای کنترلهای WPF و Silverlight همیشه باید بخاطر داشته باشند که اگر قرار است کنترلهای شما MVVM friendly باشند باید کمی بیشتر زحمت کشیده و بجای تعریف خواص ساده دات نتی، خواص مورد نظر را از نوع DependencyProperty تعریف کنید.
الان که تعریف نشده چه باید کرد؟
پاسخ متداول آن این است: مهم نیست؛ خودمان میتوانیم اینکار را انجام دهیم! یک Attached property یا به عبارتی یک Behavior را تعریف و سپس به کمک آن عملیات Binding را میسر خواهیم ساخت. برای مثال:
در این Attached property قصد داریم یک خاصیت جدید به نام BindableSource را جهت کنترل WebBrowser تعریف کنیم:
using System;
using System.Windows;
using System.Windows.Controls;
namespace WebBrowserSample.Behaviors
{
public static class WebBrowserBehaviors
{
public static readonly DependencyProperty BindableSourceProperty =
DependencyProperty.RegisterAttached("BindableSource",
typeof(object),
typeof(WebBrowserBehaviors),
new UIPropertyMetadata(null, BindableSourcePropertyChanged));
public static object GetBindableSource(DependencyObject obj)
{
return (string)obj.GetValue(BindableSourceProperty);
}
public static void SetBindableSource(DependencyObject obj, object value)
{
obj.SetValue(BindableSourceProperty, value);
}
public static void BindableSourcePropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
WebBrowser browser = o as WebBrowser;
if (browser == null) return;
Uri uri = null;
if (e.NewValue is string)
{
var uriString = e.NewValue as string;
uri = string.IsNullOrWhiteSpace(uriString) ? null : new Uri(uriString);
}
else if (e.NewValue is Uri)
{
uri = e.NewValue as Uri;
}
if (uri != null) browser.Source = uri;
}
}
}
یک مثال ساده از استفادهی آن هم به صورت زیر میتواند باشد:
ابتدا ViewModel مرتبط با فرم برنامه را تهیه خواهیم کرد. اینجا چون یک خاصیت را قرار است Bind کنیم، همینجا داخل ViewModel آنرا تعریف کردهایم. اگر تعداد آنها بیشتر بود بهتر است به یک کلاس مجزا مثلا GuiModel منتقل شوند.
using System;
using System.ComponentModel;
namespace WebBrowserSample.ViewModels
{
public class MainWindowViewModel : INotifyPropertyChanged
{
Uri _sourceUri;
public Uri SourceUri
{
get { return _sourceUri; }
set
{
_sourceUri = value;
raisePropertyChanged("SourceUri");
}
}
public MainWindowViewModel()
{
SourceUri = new Uri(@"C:\path\arrow.png");
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
void raisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}
در ادامه بجای استفاده از خاصیت Source که قابلیت Binding ندارد، از Behavior سفارشی تعریف شده استفاده خواهیم کرد. ابتدا باید فضای نام آن تعریف شود، سپس BindableSource مرتبط آن در دسترس خواهد بود:
<Window x:Class="WebBrowserSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:VM="clr-namespace:WebBrowserSample.ViewModels"
xmlns:B="clr-namespace:WebBrowserSample.Behaviors"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<VM:MainWindowViewModel x:Key="vmMainWindowViewModel" />
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource vmMainWindowViewModel}}">
<WebBrowser B:WebBrowserBehaviors.BindableSource="{Binding SourceUri}" />
</Grid>
</Window>
نمونه مشابه این مورد را در مثال «استفاده از کنترلهای Active-X در WPF» پیشتر در این سایت دیدهاید.
نگاهی به SignalR Hubs
Hubs به نوعی یک فریم ورک سطح بالای RPC نیز محسوب میشوند (Remote Procedure Calls) و آنرا برای انتقال انواع و اقسام دادهها بین سرور و کلاینت و یا فراخوانی متدی در سمت کلاینت یا سرور، بسیار مناسب میسازد. برای مثال اگر قرار باشد با persistent connection به صورت مستقیم کار کنیم، نیاز است تا بسیاری از مسایل serialization و deserialization اطلاعات را خودمان پیاده سازی و اعمال نمائیم.
قرار دادهای پیش فرض Hubs
- متدهای public کلاسهای Hubs از طریق دنیای خارج قابل فراخوانی هستند.
- ارسال اطلاعات به کلاینتها از طریق فراخوانی متدهای سمت کلاینت انجام خواهد شد. (نحوه تعریف این متدها در سمت سرور بر اساس قابلیتهای dynamic اضافه شده به دات نت 4 است که در ادامه در مورد آن بیشتر بحث خواهد شد)
مراحل اولیه نوشتن یک Hub
الف) یک کلاس Hub را تهیه کنید. این کلاس، از کلاس پایه Hub تعریف شده در فضای نام Microsoft.AspNet.SignalR باید مشتق شود. همچنین این کلاس میتواند توسط ویژگی خاصی به نام HubName نیز مزین گردد تا در حین برپایی اولیه سرویس، از طریق زیرساختهای SignalR به نامی دیگر (یک alias یا نام مستعار خاص) قابل شناسایی باشد. متدهای یک هاب میتوانند نوعهای ساده یا پیچیدهای را بازگشت دهند و همه چیز در اینجا نهایتا به فرمت JSON رد و بدل خواهد شد (فرمت پیش فرض که در پشت صحنه از کتابخانه معروف JSON.NET استفاده میکند؛ این کتابخانه سورس باز به دلیل کیفیت بالای آن، از زمان ارائه MVC4 به عنوان جزئی از مجموعه کارهای مایکروسافت قرار گرفته است).
ب) مسیریابی و Routing را تعریف و اصلاح نمائید.
و ... از نتیجه استفاده کنید.
تهیه اولین برنامه با SignalR
ابتدا یک پروژه خالی ASP.NET را آغاز کنید (مهم نیست MVC باشد یا WebForms). برای سادگی بیشتر، در اینجا یک ASP.NET Empty Web application درنظر گرفته شده است. در ادامه قصد داریم یک برنامه Chat را تهیه کنیم؛ از این جهت که توسط یک برنامه Chat بسیاری از مفاهیم مرتبط با SignalR را میتوان در عمل توضیح داد.
اگر از VS 2012 استفاده میکنید، گزینه SignalR Hub class جزئی از آیتمهای جدید قابل افزودن به پروژه است (منوی پروژه، گزینه new item آن) و پس از انتخاب این قالب خاص، تمامی ارجاعات لازم نیز به صورت خودکار به پروژه جاری اضافه خواهند شد.
و اگر از VS 2010 استفاده میکنید، نیاز است از طریق NuGet ارجاعات لازم را به پروژه خود اضافه نمائید:
PM> Install-Package Microsoft.AspNet.SignalR
using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Hubs; namespace SignalR02 { [HubName("chat")] public class ChatHub : Hub { public void SendMessage(string message) { Clients.All.hello(message); } } }
کلاس پایه Hub یک سری متد و خاصیت را در اختیار کلاسهای مشتق شده از آن قرار میدهد. سادهترین راه برای آشنایی با این متدها و خواص مهیا، کلیک راست بر روی نام کلاس پایه Hub و انتخاب گزینه Go to definition است.
برای نمونه در کلاس ChatHub فوق، از خاصیت Clients برای دسترسی به تمامی آنها و سپس فراخوانی متد dynamic ایی به نام hello که هنوز وجود خارجی ندارد، استفاده شده است.
اهمیتی ندارد که این کلاس در اسمبلی اصلی برنامه وب قرار گیرد یا مثلا در یک class library به نام Services. همینقدر که از کلاس Hub مشتق شود به صورت خودکار در ابتدای برنامه اسکن گردیده و یافت خواهد شد.
مرحله بعد، افزودن فایل global.asax به برنامه است. زیرا برای کار با SignalR نیاز است تنظیمات Routing و مسیریابی خاص آنرا اضافه نمائیم. پس از افرودن فایل global.asax، به فایل Global.asax.cs مراجعه کرده و در متد Application_Start آن تغییرات ذیل را اعمال نمائید:
using System; using System.Web; using System.Web.Routing; namespace SignalR02 { public class Global : HttpApplication { protected void Application_Start(object sender, EventArgs e) { // Register the default hubs route: ~/signalr RouteTable.Routes.MapHubs(); } } }
یک نکته مهم
اگر از ASP.NET MVC استفاده میکنید، این تنظیم مسیریابی باید پیش از تعاریف پیش فرض موجود قرار گیرد. در غیراینصورت مسیریابیهای SignalR کار نخواهند کرد.
اکنون برای آزمایش برنامه، برنامه را اجرا کرده و مسیر ذیل را فراخوانی کنید:
http://localhost/signalr/hubs
proxies.chat = this.createHubProxy('chat');
تا اینجا ما موفق شدیم اولین Hub خود را تشکیل دهیم.
بررسی پروتکل Hub
اکنون که اولین Hub خود را ایجاد کردهایم، بد نیست اندکی با زیر ساخت آن نیز آشنا شویم.
مطابق مسیریابی تعریف شده در Application_Start، مسیر ابتدایی دسترسی به SignalR با افزودن اسلش SignalR به انتهای مسیر ریشه سایت بدست میآید و اگر به این آدرس یک اسلش hubs را نیز اضافه کنیم، فایل js metadata مرتبط را نیز میتوان دریافت و مشاهده کرد.
زمانیکه یک کلاینت قصد اتصال به یک Hub را دارد، دو مرحله رخ خواهد داد:
الف) negotiate: در این حالت امکانات قابل پشتیبانی از طرف سرور مورد پرسش قرار میگیرند و سپس بهترین حالت انتقال، انتخاب میگردد. این انتخابها به ترتیب از چپ به راست خواهند بود:
Web socket -> SSE -> Forever frame -> long polling
به این معنا که اگر برای مثال امکانات Web sockets مهیا بود، در همینجا کار انتخاب نحوه انتقال اطلاعات خاتمه یافته و Web sockets انتخاب میشود.
تمام این مراحل نیز خودکار است و نیازی نیست تا برای تنظیمات آن کار خاصی صورت گیرد. البته در سمت کلاینت، امکان انتخاب یکی از موارد یاد شده به صورت صریح نیز وجود دارد.
ب) connect: اتصالی ماندگار برقرار میگردد.
در پروتکل Hub تمام اطلاعات JSON encoded هستند و یک سری مخففهایی را در این بین نیز ممکن است مشاهده نمائید که معنای آنها به شرح زیر است:
C: cursor M: Messages H: Hub name M: Method name A: Method args T: Time out D: Disconnect
روشهای مختلف ارسال اطلاعات به کلاینتها
به چندین روش میتوان اطلاعاتی را به کلاینتها ارسال کرد:
1) استفاده از خاصیت Clients موجود در کلاس Hub
2) استفاده از خواص و متدهای dynamic
در این حالت اطلاعات متد dynamic و پارامترهای آن به صورت JSON encoded به کلاینت ارسال میشوند (به همین جهت اهمیتی ندارند که در سرور وجود خارجی دارند یا خیر و به صورت dynamic تعریف شدهاند).
using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Hubs; namespace SignalR02 { [HubName("chat")] public class ChatHub : Hub { public void SendMessage(string message) { var msg = string.Format("{0}:{1}", Context.ConnectionId, message); Clients.All.hello(msg); } } }
حالت Clients.All به معنای ارسال پیامی به تمام کلاینتهای متصل به هاب ما هستند.
3) روشهای دیگر، استفاده از خاصیت dynamic دیگری به نام Caller است که میتوان بر روی آن متد دلخواهی را تعریف و فراخوانی کرد.
//این دو عبارت هر دو یکی هستند Clients.Caller.hello(msg); Clients.Client(Context.ConnectionId).hello(msg);
در اینجا پیامی صرفا به فراخوان جاری سرویس ارسال میگردد.
4) استفاده از خاصیت dynamic ایی به نام Clients.Others
Clients.Others.hello(msg);
5) استفاده از متد Clients.AllExcept
این متد میتواند آرایهای از ConnectionIdهایی را بپذیرد که قرار نیست پیام ارسالی ما را دریافت کنند.
6) ارسال اطلاعات به گروهها
تعداد مشخصی از ConnectionIdها یک گروه را تشکیل میدهند؛ مثلا اعضای یک chat room.
public void JoinRoom(string room) { this.Groups.Add(Context.ConnectionId, room); } public void SendMessageToRoom(string room, string msg) { this.Clients.Group(room).hello(msg); }
خاصیت Group در کلاس پایه Hub تعریف شده است.
نکته مهمی را که در اینجا باید درنظر داشت این است که اطلاعات گروهها به صورت دائمی در سرور ذخیره نمیشوند. برای مثال اگر سرور ری استارت شود، این اطلاعات از دست خواهند رفت.
آشنایی با مراحل طول عمر یک Hub
اگر به تعاریف کلاس پایه Hub دقت کنیم:
public abstract class Hub : IHub, IDisposable { protected Hub(); public HubConnectionContext Clients { get; set; } public HubCallerContext Context { get; set; } public IGroupManager Groups { get; set; } public void Dispose(); protected virtual void Dispose(bool disposing); public virtual Task OnConnected(); public virtual Task OnDisconnected(); public virtual Task OnReconnected(); }
تعدادی از این متدها را میتوان جهت مقاصد logging برنامه مورد استفاده قرار داد و یا در متد OnDisconnected اگر اطلاعاتی را در بانک اطلاعاتی ذخیره کردهایم، بر این اساس میتوان وضعیت نهایی را تغییر داد.
ارسال اطلاعات از یک Hub به Hub دیگر در برنامه
فرض کنید یک Hub دوم را به نام MinitorHub به برنامه اضافه کردهاید. اکنون قصد داریم از داخل ChatHub فوق، اطلاعاتی را به آن ارسال کنیم. روش کار به نحو زیر است:
public override System.Threading.Tasks.Task OnDisconnected() { sendMonitorData("OnDisconnected", Context.ConnectionId); return base.OnDisconnected(); } private void sendMonitorData(string type, string connection) { var ctx = GlobalHost.ConnectionManager.GetHubContext<MonitorHub>(); ctx.Clients.All.newEvenet(type, connection); }
مورد استفاده دیگر این روش، ارسال اطلاعات به کلاینتها از طریق کدهای یک برنامه تحت وب است (که در همان پروژه هاب واقع شده است). برای مثال در یک اکشن متد یا یک روال رویدادگردان کلیک نیز میتوان از GlobalHost.ConnectionManager استفاده کرد.
ASP.NET MVC #11
بررسی نکات تکمیلی Model binder در ASP.NET MVC
یک برنامه خالی جدید ASP.NET MVC را شروع کنید و سپس مدل زیر را به پوشه Models آن اضافه نمائید:
using System;
namespace MvcApplication7.Models
{
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }
public bool IsAdmin { set; get; }
}
}
از این مدل چند مقصود ذیل دنبال میشوند:
استفاده از Id به عنوان primary key برای edit و update رکوردها. استفاده از DateTime برای اینکه اگر کاربری اطلاعات بی ربطی را وارد کرد چگونه باید این مشکل را در حالت model binding خودکار تشخیص داد و استفاده از IsAdmin برای یادآوری یک نکته امنیتی بسیار مهم که اگر حین model binding خودکار به آن توجه نشود، سایت را با مشکلات حاد امنیتی مواجه خواهد کرد. سیستم پیشرفته است. میتواند به صورت خودکار ورودیهای کاربر را تبدیل به یک شیء حاضر و آماده کند ... اما باید حین استفاده از این قابلیت دلپذیر به یک سری نکات امنیتی هم دقت داشت تا سایت ما به نحو دلپذیری هک نشود!
در ادامه یک کنترلر جدید به نام UserController را به پوشه کنترلرهای پروژه اضافه نمائید. همچنین نام کنترلر پیش فرض تعریف شده در قسمت مسیریابی فایل Global.asax.cs را هم به User تغییر دهید تا در هربار اجرای برنامه در VS.NET، نیازی به تایپ آدرسهای مرتبط با UserController نداشته باشیم.
یک منبع داده تشکیل شده در حافظه را هم برای نمایش لیستی از کاربران، به نحو زیر به پروژه اضافه خواهیم کرد:
using System;
using System.Collections.Generic;
namespace MvcApplication7.Models
{
public class Users
{
public IList<User> CreateInMemoryDataSource()
{
return new[]
{
new User { Id = 1, Name = "User1", Password = "123", IsAdmin = false, AddDate = DateTime.Now },
new User { Id = 2, Name = "User2", Password = "456", IsAdmin = false, AddDate = DateTime.Now },
new User { Id = 3, Name = "User3", Password = "789", IsAdmin = true, AddDate = DateTime.Now }
};
}
}
}
در اینجا فعلا هدف آشنایی با زیر ساختهای ASP.NET MVC است و درک صحیح نحوه کارکرد آن. مهم نیست از EF استفاده میکنید یا NH یا حتی ADO.NET کلاسیک و یا از Micro ORMهایی که پس از ارائه دات نت 4 مرسوم شدهاند. تهیه یک ToList یا Insert و Update با این فریم ورکها خارج از بحث جاری هستند.
سورس کامل کنترلر User به شرح زیر است:
using System;
using System.Linq;
using System.Web.Mvc;
using MvcApplication7.Models;
namespace MvcApplication7.Controllers
{
public class UserController : Controller
{
[HttpGet]
public ActionResult Index()
{
var usersList = new Users().CreateInMemoryDataSource();
return View(usersList); // Shows the Index view.
}
[HttpGet]
public ActionResult Details(int id)
{
var user = new Users().CreateInMemoryDataSource().FirstOrDefault(x => x.Id == id);
if (user == null)
return View("Error");
return View(user); // Shows the Details view.
}
[HttpGet]
public ActionResult Create()
{
var user = new User { AddDate = DateTime.Now };
return View(user); // Shows the Create view.
}
[HttpPost]
public ActionResult Create(User user)
{
if (this.ModelState.IsValid)
{
// todo: Add record
return RedirectToAction("Index");
}
return View(user); // Shows the Create view again.
}
[HttpGet]
public ActionResult Edit(int id)
{
var user = new Users().CreateInMemoryDataSource().FirstOrDefault(x => x.Id == id);
if (user == null)
return View("Error");
return View(user); // Shows the Edit view.
}
[HttpPost]
public ActionResult Edit(User user)
{
if (this.ModelState.IsValid)
{
// todo: Edit record
return RedirectToAction("Index");
}
return View(user); // Shows the Edit view again.
}
[HttpPost]
public ActionResult Delete(int id)
{
// todo: Delete record
return RedirectToAction("Index");
}
}
}
توضیحات:
ایجاد خودکار فرمهای ورود اطلاعات
در قسمت قبل برای توضیح دادن نحوه ایجاد فرمها در ASP.NET MVC و همچنین نحوه نگاشت اطلاعات آنها به اکشن متدهای کنترلرها، فرمهای مورد نظر را دستی ایجاد کردیم.
اما باید درنظر داشت که برای ایجاد Viewها میتوان از ابزار توکار خود VS.NET نیز استفاده کرد و سپس اطلاعات و فرمهای تولیدی را سفارشی نمود. این سریعترین راه ممکن است زمانیکه مدل مورد استفاده کاملا مشخص است و میخواهیم Strongly typed views را ایجاد کنیم.
برای نمونه بر روی متد Index کلیک راست کرده و گزینه Add view را انتخاب کنید. در اینجا گزینهی create a strongly typed view را انتخاب کرده و سپس از لیست مدلها، User را انتخاب نمائید. Scaffold template را هم بر روی حالت List قرار دهید.
برای متد Details هم به همین نحو عمل نمائید.
برای ایجاد View متناظر با متد Create در حالت HttpGet، تمام مراحل یکی است. فقط Scaffold template انتخابی را بر روی Create قرار دهید تا فرم ورود اطلاعات، به صورت خودکار تولید شود.
متد Create در حالت HttpPost نیازی به View اضافی ندارد. چون صرفا قرار است اطلاعاتی را از سرور دریافت و ثبت کند.
برای ایجاد View متناظر با متد Edit در حالت HttpGet، باز هم مراحل مانند قبل است با این تفاوت که Scaffold template انتخابی را بر روی گزینه Edit قرار دهید تا فرم ویرایش اطلاعات کاربر به صورت خودکار به پروژه اضافه شود.
متد Edit در حالت HttpPost نیازی به View اضافی ندارد و کارش تنها دریافت اطلاعات از سرور و به روز رسانی بانک اطلاعاتی است.
به همین ترتیب متد Delete نیز، نیازی به View خاصی ندارد. در اینجا بر اساس primary key دریافتی، میتوان یک کاربر را یافته و حذف کرد.
سفارشی سازی Viewهای خودکار تولیدی
با کمک امکانات Scaffolding نامبرده شده، حجم قابل توجهی کد را در اندک زمانی میتوان تولید کرد. بدیهی است حتما نیاز به سفارشی سازی کدهای تولیدی وجود خواهد داشت. مثلا شاید نیازی نباشد فیلد پسود کاربر، در حین نمایش لیست کاربران، نمایش داده شود. میشود کلا این ستون را حذف کرد و از این نوع مسایل.
یک مورد دیگر را هم در Viewهای تولیدی حتما نیاز است که ویرایش کنیم. آن هم مرتبط است به لینک حذف اطلاعات یک کاربر در صفحه Index.cshtml:
@Html.ActionLink("Delete", "Delete", new { id=item.Id }
در قسمت قبل هم عنوان شد که اعمال حذف باید بر اساس HttpPost محدود شوند تا بتوان میزان امنیت برنامه را بهبود داد. متد Delete هم در کنترلر فوق تنها به حالت HttpPost محدود شده است. بنابراین ActionLink پیش فرض را حذف کرده و بجای آن فرم و دکمه زیر را قرار میدهیم تا اطلاعات به سرور Post شوند:
@using (Html.BeginForm(actionName: "Delete", controllerName: "User", routeValues: new { id = item.Id }))
{
<input type="submit" value="Delete"
onclick="return confirm ('Do you want to delete this record?');" />
}
در اینجا نحوه ایجاد یک فرم، که id رکورد متناظر را به سرور ارسال میکند، مشاهده میکنید.
علت وجود دو متد، به ازای هر Edit یا Create
به ازای هر کدام از متدهای Edit و Create دو متد HttpGet و HttpPost را ایجاد کردهایم. کار متدهای HttpGet نمایش Viewهای متناظر به کاربر هستند. بنابراین وجود آنها ضروری است. در این حالت چون از دو Verb متفاوت استفاده شده، میتوان متدهای هم نامی را بدون مشکل استفاده کرد. به هر کدام از افعال Get و Post و امثال آن، یک Http Verb گفته میشود.
بررسی معتبر بودن اطلاعات دریافتی
کلاس پایه Controller که کنترلرهای برنامه از آن مشتق میشوند، شامل یک سری خواص و متدهای توکار نیز هست. برای مثال توسط خاصیت this.ModelState.IsValid میتوان بررسی کرد که آیا Model دریافتی معتبر است یا خیر. برای بررسی این مورد، یک breakpoint را بر روی سطر this.ModelState.IsValid در متد Create قرار دهید. سپس به صفحه ایجاد کاربر جدید مراجعه کرده و مثلا بجای تاریخ روز، abcd را وارد کنید. سپس فرم را به سرور ارسال نمائید. در این حالت مقدار خاصیت this.ModelState.IsValid مساوی false میباشد که حتما باید به آن پیش از ثبت اطلاعات دقت داشت.
شبیه سازی عملکرد ViewState در ASP.NET MVC
در متدهای Create و Edit در حالت Post، اگر اطلاعات Model معتبر نباشند، مجددا شیء User دریافتی، به View بازگشت داده میشود. چرا؟
صفحات وب، زمانیکه به سرور ارسال میشوند، تمام اطلاعات کنترلهای خود را از دست خواهد داد (صفحه پاک میشود، چون مجددا یک صفحه خالی از سرور دریافت خواهد شد). برای رفع این مشکل در ASP.NET Web forms، از مفهومی به نام ViewState کمک میگیرند. کار ViewState ذخیره موقت اطلاعات فرم جاری است برای استفاده مجدد پس از Postback. به این معنا که پس از ارسال فرم به سرور، اگر کاربری در textbox اول مقدار abc را وارد کرده بود، پس از نمایش مجدد فرم، مقدار abc را در همان textbox مشاهده خواهد کرد (شبیه سازی برنامههای دسکتاپ در محیط وب). بدیهی است وجود ViewState برای ذخیره سازی این نوع اطلاعات، حجم صفحه را بالا میبرد (بسته به پیچیدگی صفحه ممکن است به چند صد کیلوبایت هم برسد).
در ASP.NET MVC بجای استفاده از ترفندی به نام ViewState، مجددا اطلاعات همان مدل متناظر با View را بازگشت میدهند. در این حالت پس از ارسال صفحه به سرور و نمایش مجدد صفحه ورود اطلاعات، تمام کنترلها با همان مقادیر قبلی وارد شده توسط کاربر قابل مشاهده خواهند بود (مدل مشخص است، View ما هم از نوع strongly typed میباشد. در این حالت فریم ورک میداند که اطلاعات را چگونه به کنترلهای قرار گرفته در صفحه نگاشت کند).
در مثال فوق، اگر اطلاعات وارد شده صحیح باشند، کاربر به صفحه Index هدایت خواهد شد. در غیراینصورت مجددا همان View جاری با همان اطلاعات model قبلی که کاربر تکمیل کرده است به او برای تصحیح، نمایش داده میشود. این مساله هم جهت بالا بردن سهولت کاربری برنامه بسیار مهم است. تصور کنید که یک فرم خالی با پیغام «تاریخ وارد شده معتبر نیست» مجدا به کاربر نمایش داده شود و از او درخواست کنیم که تمام اطلاعات دیگر را نیز از صفر وارد کند چون اطلاعات صفحه پس از ارسال به سرور پاک شدهاند؛ که ... اصلا قابل قبول نیست و فوقالعاده برنامه را غیرحرفهای نمایش میدهد.
خطاهای نمایش داده شده به کاربر
به صورت پیش فرض خطایی که به کاربر نمایش داده میشود، استثنایی است که توسط فریم ورک صادر شده است. برای مثال نتوانسته است abcd را به یک تاریخ معتبر تبدیل کند. میتوان توسط this.ModelState.AddModelError خطایی را نیز در اینجا اضافه کرد و پیغام بهتری را به کاربر نمایش داد. یا توسط یک سری data annotations هم کار اعتبار سنجی را سفارشی کرد که بحث آن به صورت جداگانه در یک قسمت مستقل بررسی خواهد شد.
ولی به صورت خلاصه اگر به فرمهای تولید شده توسط VS.NET دقت کنید، در ابتدای هر فرم داریم:
@Html.ValidationSummary(true)
در اینجا خطاهای عمومی در سطح مدل نمایش داده میشوند. برای اضافه کردن این نوع خطاها، در متد AddModelError، مقدار key را خالی وارد کنید:
ModelState.AddModelError(string.Empty, "There is something wrong with model.");
همچنین در این فرمها داریم:
@Html.EditorFor(model => model.AddDate)
@Html.ValidationMessageFor(model => model.AddDate)
EditorFor سعی میکند اندکی هوش به خرج دهد. یعنی اگر خاصیت دریافتی مثلا از نوع bool بود، خودش یک checkbox را در صفحه نمایش میدهد. همچنین بر اساس متادیتا یک خاصیت نیز میتواند تصمیم گیری را انجام دهد. این متادیتا منظور attributes و data annotations ایی است که به خواص یک مدل اعمال میشود. مثلا اگر ویژگی HiddenInput را به یک خاصیت اعمال کنیم، به شکل یک فیلد مخفی در صفحه ظاهر خواهد شد.
یا متد Html.DisplayFor، اطلاعات را به صورت فقط خواندنی نمایش میدهد. اصطلاحا به این نوع متدها، Templated Helpers هم گفته میشود. بحث بیشتر دربارهای این موارد به قسمتی مجزا و مستقل موکول میگردد. برای نمونه کل فرم ادیت برنامه را حذف کنید و بجای آن بنویسید Html.EditorForModel و سپس برنامه را اجرا کنید. یک فرم کامل خودکار ویرایش اطلاعات را مشاهده خواهید کرد (و البته نکات سفارشی سازی آن به یک قسمت کامل نیاز دارند).
در اینجا متد ValidationMessageFor کار نمایش خطاهای اعتبارسنجی مرتبط با یک خاصیت مشخص را انجام میدهد. بنابراین اگر قصد ارائه خطایی سفارشی و مخصوص یک فیلد مشخص را داشتید، در متد AddModelError، مقدار پارامتر اول یا همان key را مساوی نام خاصیت مورد نظر قرار دهید.
مقابله با مشکل امنیتی Mass Assignment در حین کار با Model binders
استفاده از Model binders بسیار لذت بخش است. یک شیء را به عنوان پارامتر اکشن متد خود معرفی میکنیم. فریم ورک هم در ادامه سعی میکند تا اطلاعات فرم را به خواص این شیء نگاشت کند. بدیهی است این روش نسبت به روش ASP.NET Web forms که باید به ازای تک تک کنترلهای موجود در صفحه یکبار کار دریافت اطلاعات و مقدار دهی خواص یک شیء را انجام داد، بسیار سادهتر و سریعتر است.
اما اگر همین سیستم پیشرفته جدید ناآگاهانه مورد استفاده قرار گیرد میتواند منشاء حملات ناگواری شود که به نام «Mass Assignment» شهرت یافتهاند.
همان صفحه ویرایش اطلاعات را درنظر بگیرید. چک باکس IsAdmin قرار است در قسمت مدیریتی برنامه تنظیم شود. اگر کاربری نیاز داشته باشد اطلاعات خودش را ویرایش کند، مثلا پسوردش را تغییر دهد، با یک صفحه ساده کلمه عبور قبلی را وارد کنید و دوبار کلمه عبور جدید را نیز وارد نمائید، مواجه خواهد شد. خوب ... اگر همین کاربر صفحه را جعل کند و فیلد چک باکس IsAdmin را به صفحه اضافه کند چه اتفاقی خواهد افتاد؟ بله ... مشکل هم همینجا است. در اینصورت کاربر عادی میتواند دسترسی خودش را تا سطح ادمین بالا ببرد، چون model binder اطلاعات IsAdmin را از کاربر دریافت کرده و به صورت خودکار به model ارائه شده، نگاشت کرده است.
برای مقابله با این نوع حملات چندین روش وجود دارند:
الف) ایجاد لیست سفید
به کمک ویژگی Bind میتوان لیستی از خواص را جهت به روز رسانی به model binder معرفی کرد. مابقی ندید گرفته خواهند شد:
public ActionResult Edit([Bind(Include = "Name, Password")] User user)
در اینجا تنها خواص Name و Password توسط model binder به خواص شیء User نگاشت میشوند.
به علاوه همانطور که در قسمت قبل نیز ذکر شد، متد edit را به شکل زیر نیز میتوان بازنویسی کرد. در اینجا متدهای توکار UpdateModel و TryUpdateModel نیز لیست سفید خواص مورد نظر را میپذیرند (اعمال دستی model binding):
[HttpPost]
public ActionResult Edit()
{
var user = new User();
if(TryUpdateModel(user, includeProperties: new[] { "Name", "Password" }))
{
// todo: Edit record
return RedirectToAction("Index");
}
return View(user); // Shows the Edit view again.
}
ب) ایجاد لیست سیاه
به همین ترتیب میتوان تنها خواصی را معرفی کرد که باید صرفنظر شوند:
public ActionResult Edit([Bind(Exclude = "IsAdmin")] User user)
در اینجا از خاصیت IsAdmin صرف نظر گردیده و از مقدار ارسالی آن توسط کاربر استفاده نخواهد شد.
و یا میتوان پارامتر excludeProperties متد TryUpdateModel را نیز مقدار دهی کرد.
لازم به ذکر است که ویژگی Bind را به کل یک کلاس هم میتوان اعمال کرد. برای مثال:
using System;
using System.Web.Mvc;
namespace MvcApplication7.Models
{
[Bind(Exclude = "IsAdmin")]
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }
public bool IsAdmin { set; get; }
}
}
این مورد اثر سراسری داشته و قابل بازنویسی نیست. به عبارتی حتی اگر در متدی خاصیت IsAdmin را مجددا الحاق کنیم، تاثیری نخواهد داشت.
یا میتوان از ویژگی ReadOnly هم استفاده کرد:
using System;
using System.ComponentModel;
namespace MvcApplication7.Models
{
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }
[ReadOnly(true)]
public bool IsAdmin { set; get; }
}
}
در این حالت هم خاصیت IsAdmin هیچگاه توسط model binder به روز و مقدار دهی نخواهد شد.
ج) استفاده از ViewModels
این راه حلی است که بیشتر مورد توجه معماران نرم افزار است و البته کسانی که پیشتر با الگوی MVVM کار کرده باشند این نام برایشان آشنا است؛ اما در اینجا مفهوم متفاوتی دارد. در الگوی MVVM، کلاسهای ViewModel شبیه به کنترلرها در MVC هستند یا به عبارتی همانند رهبر یک اکستر عمل میکنند. اما در الگوی MVC خیر. در اینجا فقط مدل یک View هستند و نه بیشتر. هدف هم این است که بین Domain Model و View Model تفاوت قائل شد.
کار View model در الگوی MVC، شکل دادن به چندین domain model و همچنین اطلاعات اضافی دیگری که نیاز هستند، جهت استفاده نهایی توسط یک View میباشد. به این ترتیب View با یک شیء سر و کار خواهد داشت و همچنین منطق شکل دهی به اطلاعات مورد نیازش هم از داخل View حذف شده و به خواص View model در زمان تشکیل آن منتقل میشود.
مشخصات یک View model خوب به شرح زیر است:
الف) رابطه بین یک View و View model آن، رابطهای یک به یک است. به ازای هر View، بهتر است یک کلاس View model وجود داشته باشد.
ب) View ساختار View model را دیکته میکند و نه کنترلر.
ج) View modelها صرفا یک سری کلاس POCO (کلاسهایی تشکیل شده از خاصیت، خاصیت، خاصیت ....) هستند که هیچ منطقی در آنها قرار نمیگیرد.
د) View model باید حاوی تمام اطلاعاتی باشد که View جهت رندر نیاز دارد و نه بیشتر و الزامی هم ندارد که این اطلاعات مستقیما به domain models مرتبط شوند. برای مثال اگر قرار است firstName+LastName در View نمایش داده شود، کار این جمع زدن باید حین تهیه View Model انجام شود و نه داخل View. یا اگر قرار است اطلاعات عددی با سه رقم جدا کننده به کاربر نمایش داده شوند، وظیفه View Model است که یک خاصیت اضافی را برای تهیه این مورد تدارک ببیند. یا مثلا اگر یک فرم ثبت نام داریم و در این فرم لیستی وجود دارد که تنها Id عنصر انتخابی آن در Model اصلی مورد استفاده قرار میگیرد، تهیه اطلاعات این لیست هم کار ViewModel است و نه اینکه مدام به Model اصلی بخواهیم خاصیت اضافه کنیم.
ViewModel چگونه پیاده سازی میشود؟
اکثر مقالات را که مطالعه کنید، این روش را توصیه میکنند:
public class MyViewModel
{
public SomeDomainModel1 Model1 { get; set; }
public SomeDomainModel2 Model2 { get; set; }
...
}
یعنی اینکه View ما به اطلاعات مثلا دو Model نیاز دارد. اینها را به این شکل محصور و کپسوله میکنیم. اگر View، واقعا به تمام فیلدهای این کلاسها نیاز داشته باشد، این روش صحیح است. در غیر اینصورت، این روش نادرست است (و متاسفانه همه جا هم دقیقا به این شکل تبلیغ میشود).
ViewModel محصور کننده یک یا چند مدل نیست. در اینجا حس غلط کار کردن با یک ViewModel را داریم. ViewModel فقط باید ارائه کننده اطلاعاتی باشد که یک View نیاز دارد و نه بیشتر و نه تمام خواص تمام کلاسهای تعریف شده. به عبارتی این نوع تعریف صحیح است:
public class MyViewModel
{
public string SomeExtraField1 { get; set; }
public string SomeExtraField2 { get; set; }
public IEnumerable<SelectListItem> StateSelectList { get; set; }
// ...
public string PersonFullName { set; set; }
}
در اینجا، View متناظری، قرار است نام کامل یک شخص را به علاوه یک سری اطلاعات اضافی که در domain model نیست، نمایش دهد. مثلا نمایش نام استانها که نهایتا Id انتخابی آن قرار است در برنامه استفاده شود.
خلاصه علت وجودی ViewModel این موارد است:
الف) Model برنامه را مستقیما در معرض استفاده قرار ندهیم (عدم رعایت این نکته به مشکلات امنیتی حادی هم حین به روز رسانی اطلاعات ممکن است ختم شود که پیشتر توضیح داده شد).
ب) فیلدهای نمایشی اضافی مورد نیاز یک View را داخل Model برنامه تعریف نکنیم (مثلا تعاریف عناصر یک دراپ داون لیست، جایش اینجا نیست. مدل فقط نیاز به Id عنصر انتخابی آن دارد).
با این توضیحات، اگر View به روز رسانی اطلاعات کلمه عبور کاربر، تنها به اطلاعات id آن کاربر و کلمه عبور او نیاز دارد، فقط باید همین اطلاعات را در اختیار View قرار داد و نه بیشتر:
namespace MvcApplication7.Models
{
public class UserViewModel
{
public int Id { set; get; }
public string Password { set; get; }
}
}
به این ترتیب دیگر خاصیت IsAdming اضافهای وجود ندارد که بخواهد مورد حمله واقع شود.
استفاده از model binding برای آپلود فایل به سرور
برای آپلود فایل به سرور تنها کافی است یک اکشن متد به شکل زیر را تعریف کنیم. HttpPostedFileBase نیز یکی دیگر از model binderهای توکار ASP.NET MVC است:
[HttpGet]
public ActionResult Upload()
{
return View(); // Shows the upload page
}
[HttpPost]
public ActionResult Upload(System.Web.HttpPostedFileBase file)
{
string filename = Server.MapPath("~/files/somename.ext");
file.SaveAs(filename);
return RedirectToAction("Index");
}
View متناظر هم میتواند به شکل زیر باشد:
@{
ViewBag.Title = "Upload";
}
<h2>
Upload</h2>
@using (Html.BeginForm(actionName: "Upload", controllerName: "User",
method: FormMethod.Post,
htmlAttributes: new { enctype = "multipart/form-data" }))
{
<text>Upload a photo:</text> <input type="file" name="photo" />
<input type="submit" value="Upload" />
}
اگر دقت کرده باشید در طراحی ASP.NET MVC از anonymously typed objects زیاد استفاده میشود. در اینجا هم برای معرفی enctype فرم آپلود، مورد استفاده قرار گرفته است. به عبارتی هر جایی که مشخص نبوده چه تعداد ویژگی یا کلا چه ویژگیها و خاصیتهایی را میتوان تنظیم کرد، اجازه تعریف آنها را به صورت anonymously typed objects میسر کردهاند. یک نمونه دیگر آن در متد routes.MapRoute فایل Global.asax.cs است که پارامتر سوم دریافت مقدار پیش فرضها نیز anonymously typed object است. یا نمونه دیگر آنرا در همین قسمت در جایی که لینک delete را به فرم تبدیل کردیم مشاهده نمودید. مقدار routeValues هم یک anonymously typed object معرفی شد.
سفارشی سازی model binder پیش فرض ASP.NET MVC
در همین مثال فرض کنید تاریخ را به صورت شمسی از کاربر دریافت میکنیم. خاصیت تعریف شده هم DateTime میلادی است. به عبارتی model binder حین تبدیل رشته تاریخ شمسی دریافتی به تاریخ میلادی با شکست مواجه شده و نهایتا خاصیت this.ModelState.IsValid مقدارش false خواهد بود. برای حل این مشکل چکار باید کرد؟
برای این منظور باید نحوه پردازش یک نوع خاص را سفارشی کرد. ابتدا با پیاده سازی اینترفیس IModelBinder شروع میکنیم. توسط bindingContext.ValueProvider میتوان به مقداری که کاربر وارد کرده در میانه راه دسترسی یافت. آنرا تبدیل کرده و نمونه صحیح را بازگشت داد.
نمونهای از این پیاده سازی را در ادامه ملاحظه میکنید:
using System;
using System.Globalization;
using System.Web.Mvc;
namespace MvcApplication7.Binders
{
public class PersianDateModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
var parts = valueResult.AttemptedValue.Split('/'); //ex. 1391/1/19
if (parts.Length != 3) return null;
int year = int.Parse(parts[0]);
int month = int.Parse(parts[1]);
int day = int.Parse(parts[2]);
actualValue = new DateTime(year, month, day, new PersianCalendar());
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
}
سپس برای معرفی PersianDateModelBinder جدید تنها کافی است سطر زیر را
ModelBinders.Binders.Add(typeof(DateTime), new PersianDateModelBinder());
به متد Application_Start قرار گرفته در فایل Global.asax.cs برنامه اضافه کرد. از این پس کاربران میتوانند تاریخها را در برنامه شمسی وارد کنند و model binder بدون مشکل خواهد توانست اطلاعات ورودی را به معادل DateTime میلادی آن تبدیل کند و استفاده نماید.
تعریف مدل بایندر سفارشی در فایل Global.asax.cs آنرا به صورت سراسری در تمام مدلها و اکشنمتدها فعال خواهد کرد. اگر نیاز بود تنها یک اکشن متد خاص از این مدل بایندر سفارشی استفاده کند میتوان به روش زیر عمل کرد:
public ActionResult Create([ModelBinder(typeof(PersianDateModelBinder))] User user)
همچنین ویژگی ModelBinder را به یک کلاس هم میتوان اعمال کرد:
[ModelBinder(typeof(PersianDateModelBinder))]
public class User
{
public class ProductService { public IEnumerable<Product> GetOrderedProducts() { using (var ctx = new Entites()) { return ctx.Products.OrderBy(x => x.Name).ToList(); } } }
public interface IUnitOfWork { IDbSet<TEntity> Set<TEntity>() where TEntity : class; int SaveAllChanges(); } public class Entites : DbContext, IUnitOfWork { public virtual DbSet<Product> Products { get; set; } // This is virtual because Moq needs to override the behaviour public new virtual IDbSet<TEntity> Set<TEntity>() where TEntity : class // This is virtual because Moq needs to override the behaviour { return base.Set<TEntity>(); } public int SaveAllChanges() { return base.SaveChanges(); } }
public class ProductService { private readonly IDbSet<Product> _products; private readonly IUnitOfWork _uow; public ProductService(IUnitOfWork uow) { _uow = uow; _products = _uow.Set<Product>(); } public IEnumerable<Product> GetOrderedProducts() { return _products.OrderBy(x => x.Name).ToList(); } }
[TestFixture] public class ProductServiceTest { private readonly ProductService _productService; public ProductServiceTest() { IQueryable<Product> data = GetRoadNetworks().AsQueryable(); var mockSet = new Mock<DbSet<Product>>(); mockSet.As<IQueryable<Product>>().Setup(m => m.Provider).Returns(data.Provider); mockSet.As<IQueryable<Product>>().Setup(m => m.Expression).Returns(data.Expression); mockSet.As<IQueryable<Product>>().Setup(m => m.ElementType).Returns(data.ElementType); mockSet.As<IQueryable<Product>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator()); var context = new Mock<Entites>(); context.Setup(c => c.Products).Returns(mockSet.Object); context.Setup(m => m.Set<Product>()).Returns(mockSet.Object); _productService = new ProductService(context.Object); } private IEnumerable<Product> GetRoadNetworks() { return new List<Product> { new Product { Id = 1, Name = "A" }, new Product { Id = 2, Name = "B" }, new Product { Id = 3, Name = "C" } }; } [Test] public void GetOrderedProductTest() { IEnumerable<Product> products = _productService.GetOrderedProducts(); List<string> names = products.Select(x => x.Name).ToList(); var expected = new List<string> {"A", "B", "C"}; CollectionAssert.AreEqual(names, expected); } }
خواندنیهای 17 مرداد
اس کیوال سرور
اس کیوال لایت
توسعه وب
دات نت فریم ورک
دلفی
سی و مشتقات
متفرقه
محیطهای مجتمع توسعه
مسایل انسانی، اجتماعی و مدیریتی برنامه نویسی
ویندوز
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; } }
public record ProductDTO(int Id, string Name, string Description);
public record ProductDTO { public int Id { get; init; } public string Name { get; init; } } var dto = new ProductDTO { Id = 1, Name = "some name" };
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 }
public class Product { public Product(int id) { Id = id; } private Product() { // required for EF } public int Id { get; private set; } // other properties and methods }
- برای اجرای وظایف خود به فریم ورک ثالثی وابسته نیست.
- به کلاس پایهای ( Base class) نیاز ندارد.
- وابستگی به متد استاتیکی ندارد.
- می تواند در هر جایی از پروژه، نمونه سازی شود.
- اصل Persistence Ignorant را بیشتر رعایت کرده، نه بطور کامل؛ چون یک سازنده دارد که به کتابخانهی ثالثی نیازمند است (سازندهی بدون پارامتر که مورد نیاز EF میباشد).
آشنایی با Jaeger
برای پیاده سازی distributed tracing، میتوانیم از ابزار متن باز و محبوب Jaeger (با تلفظ یِگِر) که ابتدا توسط شرکت Uber منتشر شد، استفاده کنیم. نحوه کارکرد Jaeger بصورت زیر میباشد:
سادهترین روش برای راهاندازی Jager، استفاده از داکر ایمیج All in one که شامل ماژول های agent ، collector، query و ui است. پورت 6831 مربوط به agent و پورت 16686 مربوط به ui میباشد. برای جزئیات مربوط به ماژولهای مختلف از این لینک استفاده کنید.
docker run -d -p 6831:6831/udp -p 6832:6832/udp -p 14268:14268 -p 14250:14250 -p 16686:16686 -p 5778:5778 --name jaeger jaegertracing/all-in-one:latest
بعد از اجرای دستور بالا، اطلاعات مربوط به سرویسها و trace ها در ماژول Jager UI با آدرس http://localhost:16686 قابل مشاهده است.
جهت استفاده از Jaeger از پروژه تستی که شامل دو سرویس User و Gateway میباشد، استفاده میکنیم. در سرویس User، متد AddUser در صورت عدم وجود کاربر در دیتابیس، اطلاعات کاربر از گیتهاب را دریافت و در دیتابیس ذخیره میکند. سرویس Gateway از Ocelot برای مسیردهی درخواستها استفاده میکند. برای آشنایی با ocelot این پست را مطالعه نمایید.
public async Task<ApiResult<Models.User>> AddUserAsync(string username) { var result = new ApiResult<Models.User>(); var user = await _applicationDbContext.Users.FirstOrDefaultAsync(x => x.Login == username); if (user is null) { try { var url = string.Format(_appConfig.Github.ProfileUrl, username); var apiResult = await _httpClient.GetStringAsync(url); var userDto = JsonSerializer.Deserialize<UserDto>(apiResult); user = _mapper.Map<Models.User>(userDto); await _applicationDbContext.Users.AddAsync(user); await _applicationDbContext.SaveChangesAsync(); result.Result = user; result.Message = "User successfully Created"; return result; } catch (Exception e) { result.Message = "User not found"; return result; } } result.Message = "User already exist"; result.Result = user; return result; }
برای ثبت Trace مربوط به درخواستها در Jaeger ، بعد از نصب پکیجهای Jaeger و OpenTracing.Contrib.NetCore در هر دو سرویس، در کانفیگ هریک از سرویسها مورد زیر را اضافه میکنیم:
"JaegerConfig": { "Host": "localhost", "Port": 6831, "IsEnabled": true, "SamplingRate": 0.5 }
و برای اضافه شدن tracer به برنامه از متد الحاقی زیر استفاده میکنیم:
public static class Extensions { public static void AddJaeger(this IServiceCollection services, IConfiguration configuration) { var config = configuration.GetSection("JaegerConfig").Get<JaegerConfig>(); if (!(config?.IsEnabled ?? false)) return; if (string.IsNullOrEmpty(config?.Host)) throw new Exception("invalid JaegerConfig"); services.AddSingleton<ITracer>(serviceProvider => { string serviceName = Assembly.GetEntryAssembly()?.GetName().Name; ILoggerFactory loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>(); var sampler = new ProbabilisticSampler(config.SamplingRate); var reporter = new RemoteReporter.Builder() .WithLoggerFactory(loggerFactory) .WithSender(new UdpSender(config.Host, config.Port, 0)) .WithFlushInterval(TimeSpan.FromSeconds(15)) .WithMaxQueueSize(300) .Build(); ITracer tracer = new Tracer.Builder(serviceName) .WithLoggerFactory(loggerFactory) .WithSampler(sampler) .WithReporter(reporter) .Build(); GlobalTracer.Register(tracer); return tracer; }); services.AddOpenTracing(); } }
برای ثبت traceها استراتژیهای متفاوتی وجود دارد. در اینجا از ProbabilisticSampler استفاده شدهاست که در سازندهی آن میتوان درصد ثبت Traceها را مقدار دهی کرد. در نهایت این متد الحاقی را در Startup اضافه میکنیم:
builder.Services.AddJaeger(builder.Configuration);
بعد از اجرای پروژه و فراخوانی https://localhost:6000/gateway/Users/Add ، سرویس Gateway، درخواست را به سرویس User ارسال میکند و این سرویسها در Jaeger UI قابل مشاهده هستند.
جهت مشاهده trace ها ، سرویس مورد نظر را انتخاب و روی Find Traces کلیک کنید. با کلیک روی Trace مورد نظر، جزئیات فعالیت هایی مثل فراخوانی سرویس و مراجعه به دیتابیس قابل مشاهده است.
برای اضافه کردن لاگ سفارشی به یک span، میتوان از اینترفیس ITracer استفاده کرد:
private readonly IUserService _userService; private readonly ITracer _tracer; public UsersController(IUserService userService, ITracer tracer) { _userService = userService; _tracer = tracer; }
[HttpPost] public async Task<ActionResult> AddUser(AddUserDto model) { var actionName = ControllerContext.ActionDescriptor.DisplayName; using var scope = _tracer.BuildSpan(actionName).StartActive(true); scope.Span.Log($"Add user log username: {model.Username}"); return Ok(await _userService.AddUserAsync(model.Username)); }
کدهای مربوط به این مطلب در اینجا قابل دسترسی است.
در C# 1.0 برای تعریف خواص، نیاز به نوشتن مقدار زیادی کد بود:
public class Person { public string _firstName; public string FirstName { get { return _firstName; } set { _firstName = value; } } }
در C# 2.0 از لحاظ ساده سازی این تعاریف، اتفاق خاصی رخنداد. فقط امکان تعریف سطوح دسترسی مانند private بر روی getterها و setterها میسر شد:
public string _firstName; public string FirstName { get { return _firstName; } private set { _firstName = value; } }
public class Person { public string FirstName { get; set; } }
در C# 6.0، امکان حذف private setterها از تعریف یک خاصیت میسر شد. یعنی مثال زیر را
public class User { public string Name { get; private set; } }
public class User { public string Name { get; } }
public class Foo { public string FirstName { get; set; } = "Initial Value"; }
همچنین در C# 6.0 با معرفی expression bodied members که بر روی خواص نیز قابل اعمال است، امکان تعریف خواص readonly محاسبه شدهی بر اساس مقدار سایر خواص نیز میسر شد:
public class Foo { public DateTime DateOfBirth { get; set; } public int Age => DateTime.Now.Year - DateOfBirth.Year; }
و در C# 9.0، با معرفی واژهی کلیدی init، امکان تعریف سادهتر خواص immutable ممکن شدهاست که در مطلب جاری به آن خواهیم پرداختیم.
روش غیرقابل مقدار دهی کردن خواص، در نگارشها پیش از C# 9.0
در بسیاری از موارد میخواهیم که خاصیتی از یک کلاس مدل، در خارج از آن قابل تغییر نباشد (مانند خواص شیءای که به محتوای فایل config ثابت برنامه اشاره میکند). راه حل فعلی آن تا پیش از C# 9.0 به صورت زیر است:
public class User { public string Name { get; private set; } }
var user = new User { Name = "User 1" // Compile Error };
The property or indexer 'User.Name' cannot be used in this context because the set accessor is inaccessible [CS9Features]csharp(CS0272)
- تنظیم مقدار خاصیت Name در سازندهی کلاس
- و یا تنظیم این مقدار در یک متد ثالث دیگر مانند SetName
public class User { public User(string name) { this.Name = name; } public void SetName(string name) { this.Name = name; } public string Name { get; private set; } }
معرفی خواص Init-Only در C# 9.0
برای رفع دو مشکل یاد شده (امکان تنظیم مقدار خاصیتها با همان روش متداول object initializer و همچنین غیرقابل تغییر شدن آنها)، اکنون در C# 9.0 میتوان بجای private set از واژهی کلیدی init استفاده کرد:
public class User { public string Name { get; init; } }
الف) امکان مقدار دهی خاصیت Name، در خارج بدنهی کلاس User و توسط روش متداول کار با object initializerها هنوز هم وجود دارد و در این حالت الزامی به تعریف یک سازنده و یا متد خاصی درون کلاس User برای مقدار دهی آن نیست:
var user = new User { Name = "User 1" };
// Compile Time Error // Init-only property or indexer 'User.Name' can only be assigned in an object initializer, // or on 'this' or 'base' in an instance constructor or an 'init' accessor. [CS9Features]csharp(CS8852) user.Name = "Test";
public class User { public string Name { get; init; } public User(string name) { this.Name = name; // Works fine } public void SetName(string name) { this.Name = name; // Compile Time Error } }
روش تعریف immutable properties در نگارشهای پیشین #C
با استفاده از واژهی readonly در نگارشهای قبلی #C نیز میتوان به صورت زیر، یک خاصیت را به صورت غیرقابل تغییر یا immutable در آورد:
public class Product { public Product(string name) { _name = name; } private readonly string _name; public string Name => _name; }
یک نکته: امکان استفادهی از فیلدهای readonly با خواص init-only هم وجود دارد؛ از این جهت که این نوع خواص تنها در زمان نمونه سازی اولیهی شیء، اجرا و مقدار دهی میشوند، با مفهوم readonly، سازگاری دارند:
public class Person { private readonly string _name; public string Name { get => _name; init => _name = value; } }
C# 7 - Out variables
یکی از تازههای 7 #C، ساده شدن تعریف out parameterها نسبت به قبل میباشد که باعث مرتب شدن کدها نیز میشود که در ادامه به آن میپردازیم.
تا قبل از ارائه 7 #C، برای ارسال یک پارامتر به صورت out میبایستی قبل از استفاده و ارسال آن به متد مورد نظر، ابتدا متغیر مربوطه را تعریف میکردیم که نمونهای از تعریف مذکور، قطعه کدهایی شبیه به زیر است:
int numericResult; if (int.TryParse(input, out numericResult)) WriteLine(numericResult); else WriteLine("Could not parse input");
قطعه کد فوق در 7 #C به صورت زیر در خواهد آمد:
if (int.TryParse(input, out int result)) WriteLine(result); else WriteLine("Could not parse input");
در مثال فوق نوع متغیر را به صورت صریح مشخص کردهایم؛ همچنین امکان تعریف متغیر به صورت ضمنی را هم داریم:
if (int.TryParse(input, out var answer)) WriteLine(answer); else WriteLine("Could not parse input");
اولین سوالی که ممکن است با دیدن نمونه کد فوق پیش بیاید، محدودهی متغیرهای out که به صورت inline تعریف میشوند، میباشد و احتمالا تصور کردهاید که محدوده دید آنها محدود به بلاک if میباشد؛ اما در عمل دامنهی دید متغیر result برابر است با بلاک بیرونی شرط، که میتوان از متغیر مورد نظر بعد از دستور if نیز استفاده نمود. قطعه کد زیر بیان کنندهی این موضوع میباشد:
if (!int.TryParse(input, out int result)) { return null; } return result;
out variables را میتوان یکی از سادهترین تازههای 7 #C دانست که باعث خواناتر شدن کدها میشود. در واقع دقیقا در جائیکه نیاز به متغیر است، تعریف میشود و اطمینان داریم که متغییر مربوطه قبل از ارسال به تابع، به صورت ناخواسته یا تصادفی مقداری اشتباه به آن انتساب داده نشده است.
قبل از 7#C:
class Program { static void Main(string[] args) { string firstName; string lastName; CreateName(out firstName, out lastName); Console.WriteLine($"Hello {firstName} {lastName}"); } private static void CreateName(out string firstName, out string lastName) { firstName = "Kevin"; lastName = "Griffin"; } }
و کد فوق در 7 #C :
class Program { static void Main(string[] args) { CreateName(out string firstName, out string lastName); Console.WriteLine($"Hello {firstName} {lastName}"); } private static void CreateName(out string firstName, out string lastName) { firstName = "Kevin"; lastName = "Griffin"; } }