ثبت استثناهای مدیریت شده توسط ELMAH
سورسشو از کجا می شه دانلود کرد؟
املای صحیح فناوریهای مرتبط با مایکروسافت
releasing-the-source-code
BloggerToCHM
توضیح مثال
متدی تحت عنوان ValidateEmail را تصور کنید. این متد از حیث بازگشت نتیجه به عنوان خروجی میتواند به اشکال مختلفی پیاده سازی شود که در ادامه مشاهده میکنیم:
متد ValidateEmail با خروجی Boolean
public bool ValidateEmail(string email) { var valid = true; if (string.IsNullOrWhiteSpace(email)) { valid = false; } var isValidFormat = true;//todo: using RegularExpression if (!isValidFormat) { valid = false; } var isRealDoamin = true;//todo: Code here that confirms whether domain exists. if (!isRealDoamin) { valid = false; } return valid; }
همانطور که در تکه کد زیر مشخص میباشد، استفاده کننده از متد بالا، امکان بررسی خروجی آن را در قالب یک شرط خواهد داشت و علاوه بر اینکه پیاده سازی آن ساده میباشد، خوانایی کد را نیز بالا میبرد؛ ولی با این حال نمیتوان متوجه شد مشکل اصلی آدرس ایمیل ارسالی به عنوان آرگومان، دقیقا چیست.
var email = "email@example.com"; var isValid = ValidateEmail(email); if(isValid) { //do something }
متد ValidateEmail با صدور استثناء
public void ValidateEmail(string email) { if (string.IsNullOrWhiteSpace(email)) throw new ArgumentNullException(nameof(email)); var isValidFormat = true;//todo: using RegularExpression if (!isValidFormat) throw new ArgumentException("email is not in a correct format"); var isRealDoamin = true;//todo: Code here that confirms whether domain exists. if (!isRealDoamin) throw new ArgumentException("email does not include a valid domain.") }
روش بالا هم جواب میدهد ولی بهتر است کلاس Exception سفارشی به عنوان مثال ValidationException برای این قضیه در نظر گرفته شود تا بتوان وهلههای صادر شده از این نوع را در لایههای بالاتر مدیریت کرد.
متد ValidateEmail با چندین خروجی
برای این منظور چندین راه حل پیش رو داریم.
با استفاده از پارامتر out:
public bool ValidateEmail(string email, out string message) { var valid = true; message = string.Empty; if (string.IsNullOrWhiteSpace(email)) { valid = false; message = "email is null."; } if (valid) { var isValidFormat = true;//todo: using RegularExpression if (!isValidFormat) { valid = false; message = "email is not in a correct format"; } } if (valid) { var isRealDoamin = true;//todo: Code here that confirms whether domain exists. if (!isRealDoamin) { valid = false; message = "email does not include a valid domain."; } } return valid; }
var email = "email@example.com"; var isValid = ValidateEmail(email, out string message); if (isValid) { //do something }
Tuple<bool, List<string>> result = Tuple.Create<bool, List<string>>(true, new List<string>());
public class OperationResult { public bool Success { get; set; } public IList<string> Messages { get; } = new List<string>(); public void AddMessage(string message) { Messages.Add(message); } }
public OperationResult ValidateEmail(string email) { var result = new OperationResult(); if (string.IsNullOrWhiteSpace(email)) { result.Success = false; result.AddMessage("email is null."); } if (result.Success) { var isValidFormat = true;//todo: using RegularExpression if (!isValidFormat) { result.Success = false; result.AddMessage("email is not in a correct format"); } } if (result.Success) { var isRealDoamin = true;//todo: Code here that confirms whether domain exists. if (!isRealDoamin) { result.Success = false; result.AddMessage("email does not include a valid domain."); } } return result; }
این بار خروجی متد مذکور از نوع OperationResult ای میباشد که هم موفقیت آمیز بودن یا عدم آن را مشخص میکند و همچنین امکان دسترسی به لیست پیغامهای مرتبط با اعتبارسنجیهای انجام شده، وجود دارد.
استفاده از Exception برای نمایش پیغام برای کاربر نهایی
با صدور یک استثناء و مدیریت سراسری آن در بالاترین (خارجی ترین) لایه و نمایش پیغام مرتبط با آن به کاربر نهایی، میتوان از آن به عنوان ابزاری برای ارسال هر نوع پیغامی به کاربر نهایی استفاده کرد. اگر قوانین تجاری با موفقیت برآورده نشدهاند یا لازم است به هر دلیلی یک پیغام مرتبط با یک اعتبارسنجی تجاری را برای کاربر نمایش دهید، این روش بسیار کارساز میباشد و با یکبار وقت گذاشتن برای توسعه زیرساخت برای این موضوع به عنوان یک Cross Cutting Concern تحت عنوان Exception Management آزادی عمل زیادی در ادامه توسعه سیستم خود خواهید داشت.
به عنوان مثال داشتن یک کلاس Exception سفارشی تحت عنوان UserFriendlyException در این راستا یک الزام میباشد.
[Serializable] public class UserFriendlyException : Exception { public string Details { get; private set; } public int Code { get; set; } public UserFriendlyException() { } public UserFriendlyException(SerializationInfo serializationInfo, StreamingContext context) : base(serializationInfo, context) { } public UserFriendlyException(string message) : base(message) { } public UserFriendlyException(int code, string message) : this(message) { Code = code; } public UserFriendlyException(string message, string details) : this(message) { Details = details; } public UserFriendlyException(int code, string message, string details) : this(message, details) { Code = code; } public UserFriendlyException(string message, Exception innerException) : base(message, innerException) { } public UserFriendlyException(string message, string details, Exception innerException) : this(message, innerException) { Details = details; } }
و همچنین لازم است در بالاترین لایه سیستم خود به عنوان مثال برای یک پروژه ASP.NET MVC یا ASP.NET Core MVC میتوان یک ExceptionFilter سفارشی نیز تهیه کرد که هم به صورت سراسری استثناءهای سفارشی شما را مدیریت کند و همچنین خروجی مناسب Json برای استفاده در سمت کلاینت را نیز مهیا کند. به عنوان مثال برای درخواستهای Ajax ای لازم است در سمت کلاینت نیز پاسخهای رسیده از سمت سرور به صورت سراسری مدیریت شوند و برای سایر درخواستها همان نمایش صفحات خطای پیغام مرتبط با استثناء رخ داده شده کفایت میکند.
یک مدل پیشنهادی برای تهیه خروجی مناسب برای ارسال جزئیات استثنا رخ داده در درخواستهای Ajax ای
[Serializable] public class MvcAjaxResponse : MvcAjaxResponse<object> { public MvcAjaxResponse() { } public MvcAjaxResponse(bool success) : base(success) { } public MvcAjaxResponse(object result) : base(result) { } public MvcAjaxResponse(ErrorInfo error, bool unAuthorizedRequest = false) : base(error, unAuthorizedRequest) { } } [Serializable] public class MvcAjaxResponse<TResult> : MvcAjaxResponseBase { public MvcAjaxResponse(TResult result) { Result = result; Success = true; } public MvcAjaxResponse() { Success = true; } public MvcAjaxResponse(bool success) { Success = success; } public MvcAjaxResponse(ErrorInfo error, bool unAuthorizedRequest = false) { Error = error; UnAuthorizedRequest = unAuthorizedRequest; Success = false; } /// <summary> /// The actual result object of AJAX request. /// It is set if <see cref="MvcAjaxResponseBase.Success" /> is true. /// </summary> public TResult Result { get; set; } } public class MvcAjaxResponseBase { public string TargetUrl { get; set; } public bool Success { get; set; } public ErrorInfo Error { get; set; } public bool UnAuthorizedRequest { get; set; } public bool __mvc { get; } = true; }
[Serializable] public class ErrorInfo { public int Code { get; set; } public string Message { get; set; } public string Detail { get; set; } public Dictionary<string, string> ValidationErrors { get; set; } public ErrorInfo() { } public ErrorInfo(string message) { Message = message; } public ErrorInfo(int code) { Code = code; } public ErrorInfo(int code, string message) : this(message) { Code = code; } public ErrorInfo(string message, string details) : this(message) { Detail = details; } public ErrorInfo(int code, string message, string details) : this(message, details) { Code = code; } }
public async Task CheckIsDeactiveAsync(long id) { if (await _organizationalUnits.AnyAsync(a => a.Id == id && !a.IsActive).ConfigureAwait(false)) throw new UserFriendlyException("واحد سازمانی جاری غیرفعال میباشد."); }
روش نام گذاری متدهایی که امکان بازگشت خروجی Null را دارند
public User GetById(long id);
[Serializable] public class EntityNotFoundException : Exception { public Type EntityType { get; set; } public object Id { get; set; } public EntityNotFoundException() { } public EntityNotFoundException(string message) : base(message) { } public EntityNotFoundException(string message, Exception innerException) : base(message, innerException) { } public EntityNotFoundException(SerializationInfo serializationInfo, StreamingContext context) : base(serializationInfo, context) { } public EntityNotFoundException(Type entityType, object id) : this(entityType, id, null) { } public EntityNotFoundException(Type entityType, object id, Exception innerException) : base($"There is no such an entity. Entity type: {entityType.FullName}, id: {id}", innerException) { EntityType = entityType; Id = id; } }
یک مثال واقعی
public async Task<UserOrganizationalUnitInfo> GetCurrentOrganizationalUnitInfoOrNullAsync(long userId) { return (await _setting.GetSettingValueForUserAsync( UserSettingNames.CurrentOrganizationalUnitInfo, userId).ConfigureAwait(false)) .FromJsonString<UserOrganizationalUnitInfo>(); }
برای خیلی ممکن است سوال پیش آمده باشد چطور یک برنامه نویس از پروژه ای که به صورت اوپن سورس منتشر میکند محافظت کرده و از سوء استفاده جلوگیری میکند ؟ بر اساس همین سوال شخص لینوس توروالدز Git را ایجاد کرد برای ذخیره پروژههای متن باز و حفظ حقوق برنامه نویس پروژه
سایت گیت هاب (github.com) بر پایه Git تشکیل شده و به همین منظور استفاده میشود. البته برنامه نویس میتواند پروژه را بصورت خصوصی ذخیره کند و از انتشار عمومی پروژه خودداری کند. با استفاده از این سیستم برنامه نویسان پروژههای متن باز را با خیال راحت و با حفظ حقوق منتشر کنند و به این ترتیب پروژه به نام آن برنامه نویس ثبت خواهد شد. در این سیکل برنامه نویس یک اکانت در این سایت ایجاد و برای هر پروژه متن باز که منتشر میکند یک صفحه (مخزن) ساخته و پروژه را در آن ذخیره میکند.
یکی دیگر از مواردی که ممکن است برای برنامههای متن باز پیش بیاید این است که اگر برنامه نویسی یک پروژه متن باز را از گیت هاب توسعه داد ، موارد اضافه شده بر عهده برنامه نویس اول گذاشته نشه و حق برنامه نویس اصلی رعایت شود ؛ برای این منظور سیکلی در سایت گیت هاب ایجاد شده با عنوان Forking که یک برنامه نویس میتواند پروژه را داشته باشد و پس از توسعه پروژه ، تغییرات ایجاد شده در برنامه را به برنامه نویس اصلی ارسال کند و پس از تایید ، تغییرات ایجاد شده در مخزن اصلی پروژه اعمال شود.
گیت هاب امکانات بیشتری را در خود پیاده کرده که این سایت را تبدیل به شبکه اجتماعی برای برنامه نویسان کرده است. موارد از قبیل انجمن برای پرسش و مشکلات ، ارسال پیغام خصوصی برای سایر اعضا و ….
نگاهی به 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
- انتشار نسخه نهایی راهنمای امنیتی WCF
- صنعت حل کپچای هند و راهحلهای مقابله با اسپم با تکیه بر تحلیل محتوا
- بالابردن ضریب موفقیت پیاده سازی ITIL در سازمان
- تکست باکس قابل ویرایش بوسیله ASP.NET AJAX
- لیستی از نرم افزارهای ORMapper
- استفاده مشترک از یک یا چند User Control در چندین IIS Application
Visual Studio
ASP. Net
طراحی و توسعه وب
- تعیین اعتبار زیباتر فیلدهای یک فرم
- آشنایی با یک سری تگ مجهور HTML !
- در انتخاب رنگهای مناسب و هماهنگ مشکل دارید؟
- معرفی 10 فریم ورک جاوا اسکریپتی
اسکیوال سرور
عمومی دات نت
ویندوز
متفرقه
معرفی الگوی Repository
روش متداول کار با فناوریهای مختلف دسترسی به دادهها عموما بدین شکل است:
الف) یافتن رشته اتصالی رمزنگاری شده به دیتابیس از یک فایل کانفیگ (در یک برنامه اصولی البته!)
ب) باز کردن یک اتصال به دیتابیس
ج) ایجاد اشیاء Command برای انجام عملیات مورد نظر
د) اجرا و فراخوانی اشیاء مراحل قبل
ه) بستن اتصال به دیتابیس و آزاد سازی اشیاء
اگر در برنامههای یک تازه کار به هر محلی از برنامه او دقت کنید این 5 مرحله را میتوانید مشاهده کنید. همه جا! قسمت ثبت، قسمت جستجو، قسمت نمایش و ...
مشکلات این روش:
1- حجم کارهای تکراری انجام شده بالا است. اگر قسمتی از فناوری دسترسی به دادهها را به اشتباه درک کرده باشد، پس از مطالعه بیشتر و مشخص شدن نحوهی رفع مشکل، قسمت عمدهای از برنامه را باید اصلاح کند (زیرا کدهای تکراری همه جای آن پراکندهاند).
2- برنامه نویس هر بار باید این مراحل را به درستی انجام دهد. اگر در یک برنامه بزرگ تنها قسمت آخر در یکی از مراحل کاری فراموش شود دیر یا زود برنامه تحت فشار کاری بالا از کار خواهد افتاد (و متاسفانه این مساله بسیار شایع است).
3- برنامه منحصرا برای یک نوع دیتابیس خاص تهیه خواهد شد و تغییر این رویه جهت استفاده از دیتابیسی دیگر (مثلا کوچ برنامه از اکسس به اس کیوال سرور)، نیازمند بازنویسی کل برنامه میباشد.
و ...
همین برنامه نویس پس از مدتی کار به این نتیجه میرسد که باید برای اینکارهای متداول، یک لایه و کلاس دسترسی به دادهها را تشکیل دهد. اکنون هر قسمتی از برنامه برای کار با دیتابیس باید با این کلاس مرکزی که انجام کارهای متداول با دیتابیس را خلاصه میکند، کار کند. به این صورت کد نویسی یک نواختی با حذف کدهای تکراری از سطح برنامه و همچنین بدون فراموش شدن قسمت مهمی از مراحل کاری، حاصل میگردد. در اینجا اگر روزی قرار شد از یک دیتابیس دیگر استفاده شود فقط کافی است یک کلاس برنامه تغییر کند و نیازی به بازنویسی کل برنامه نخواهد بود.
این روزها تشکیل این لایه دسترسی به دادهها (data access layer یا DAL) نیز مزموم است! و دلایل آن در مباحث چرا به یک ORM نیازمندیم برشمرده شده است. جهت کار با ORM ها نیز نیازمند یک لایه دیگر میباشیم تا یک سری اعمال متداول با آنهارا کپسوله کرده و از حجم کارهای تکراری خود بکاهیم. برای این منظور قبل از اینکه دست به اختراع بزنیم، بهتر است به الگوهای طراحی برنامه نویسی شیء گرا رجوع کرد و از رهنمودهای آن استفاده نمود.
الگوی Repository یکی از الگوهای برنامه نویسی با مقیاس سازمانی است. با کمک این الگو لایهای بر روی لایه نگاشت اشیاء برنامه به دیتابیس تشکیل شده و عملا برنامه را مستقل از نوع ORM مورد استفاه میکند. به این صورت هم از تشکیل یک سری کدهای تکراری در سطح برنامه جلوگیری شده و هم از وابستگی بین مدل برنامه و لایه دسترسی به دادهها (که در اینجا همان NHibernate میباشد) جلوگیری میشود. الگوی Repository (مخزن)، کار ثبت، حذف، جستجو و به روز رسانی دادهها را با ترجمه آنها به روشهای بومی مورد استفاده توسط ORM مورد نظر، کپسوله میکند. به این شکل شما میتوانید یک الگوی مخزن عمومی را برای کارهای خود تهیه کرده و به سادگی از یک ORM به ORM دیگر کوچ کنید؛ زیرا کدهای برنامه شما به هیچ ORM خاصی گره نخورده و این عملیات بومی کار با ORM توسط لایهای که توسط الگوی مخزن تشکیل شده، صورت گرفته است.
طراحی کلاس مخزن باید شرایط زیر را برآورده سازد:
الف) باید یک طراحی عمومی داشته باشد و بتواند در پروژههای متعددی مورد استفاده مجدد قرار گیرد.
ب) باید با سیستمی از نوع اول طراحی و کد نویسی و بعد کار با دیتابیس، سازگاری داشته باشد.
ج) باید امکان انجام آزمایشات واحد را سهولت بخشد.
د) باید وابستگی کلاسهای دومین برنامه را به زیر ساخت ORM مورد استفاده قطع کند (اگر سال بعد به این نتیجه رسیدید که ORM ایی به نام XYZ برای کار شما بهتر است، فقط پیاده سازی این کلاس باید تغییر کند و نه کل برنامه).
ه) باید استفاده از کوئریهایی از نوع strongly typed را ترویج کند (مثل کوئریهایی از نوع LINQ).
بررسی مدل برنامه
مدل این قسمت (برنامه NHSample4 از نوع کنسول با همان ارجاعات متداول ذکر شده در قسمتهای قبل)، از نوع many-to-many میباشد. در اینجا یک واحد درسی توسط چندین دانشجو میتواند اخذ شود یا یک دانشجو میتواند چندین واحد درسی را اخذ نماید که برای نمونه کلاس دیاگرام و کلاسهای متشکل آن به شکل زیر خواهند بود:
using System.Collections.Generic;
namespace NHSample4.Domain
{
public class Course
{
public virtual int Id { get; set; }
public virtual string Teacher { get; set; }
public virtual IList<Student> Students { get; set; }
public Course()
{
Students = new List<Student>();
}
}
}
using System.Collections.Generic;
namespace NHSample4.Domain
{
public class Student
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<Course> Courses { get; set; }
public Student()
{
Courses = new List<Course>();
}
}
}
کلاس کانفیگ برنامه جهت ایجاد نگاشتها و سپس ساخت دیتابیس متناظر
using FluentNHibernate.Automapping;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate.Tool.hbm2ddl;
namespace NHSessionManager
{
public class Config
{
public static FluentConfiguration GetConfig()
{
return
Fluently.Configure()
.Database(
MsSqlConfiguration
.MsSql2008
.ConnectionString(x => x.FromConnectionStringWithKey("DbConnectionString"))
)
.Mappings(
m => m.AutoMappings.Add(
new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample4.Domain.Course).Assembly))
.ExportTo(System.Environment.CurrentDirectory)
);
}
public static void CreateDb()
{
bool script = false;//آیا خروجی در کنسول هم نمایش داده شود
bool export = true;//آیا بر روی دیتابیس هم اجرا شود
bool dropTables = false;//آیا جداول موجود دراپ شوند
new SchemaExport(GetConfig().BuildConfiguration()).Execute(script, export, dropTables);
}
}
}
الف) با توجه به اینکه برنامه از نوع ویندوزی است، برای مدیریت صحیح کانکشن استرینگ، فایل App.Config را به برنامه افروده و محتویات آنرا به شکل زیر تنظیم میکنیم (تا کلید DbConnectionString توسط متد GetConfig مورد استفاده قرارگیرد ):
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<!--NHSessionManager-->
<add name="DbConnectionString"
connectionString="Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true"/>
</connectionStrings>
</configuration>
ب) در NHibernate سنتی (!) کار ساخت نگاشتها توسط یک سری فایل xml صورت میگیرد که با معرفی فریم ورک Fluent NHibernate و استفاده از قابلیتهای Auto Mapping آن، اینکار با سهولت و دقت هر چه تمامتر قابل انجام است که توضیحات نحوهی انجام آنرا در قسمتهای قبل مطالعه فرمودید. اگر نیاز بود تا این فایلهای XML نیز جهت بررسی شخصی ایجاد شوند، تنها کافی است از متد ExportTo آن همانگونه که در متد GetConfig استفاده شده، کمک گرفته شود. به این صورت پس از ایجاد خودکار نگاشتها، فایلهای XML متناظر نیز در مسیری که به عنوان آرگومان متد ExportTo مشخص گردیده است، تولید خواهند شد (دو فایل NHSample4.Domain.Course.hbm.xml و NHSample4.Domain.Student.hbm.xml را در پوشهای که محل اجرای برنامه است خواهید یافت).
با فراخوانی متد CreateDb این کلاس، پس از ساخت خودکار نگاشتها، database schema متناظر، در دیتابیسی که توسط کانکشن استرینگ برنامه مشخص شده، ایجاد خواهد شد که دیتابیس دیاگرام آنرا در شکل ذیل مشاهده مینمائید (جداول دانشجویان و واحدها هر کدام به صورت موجودیتی مستقل ایجاد شده که ارجاعات آنها در جدولی سوم نگهداری میشود).
پیاده سازی الگوی مخزن
اینترفیس عمومی الگوی مخزن به شکل زیر میتواند باشد:
using System;
using System.Linq;
using System.Linq.Expressions;
namespace NHSample4.NHRepository
{
//Repository Interface
public interface IRepository<T>
{
T Get(object key);
T Save(T entity);
T Update(T entity);
void Delete(T entity);
IQueryable<T> Find();
IQueryable<T> Find(Expression<Func<T, bool>> predicate);
}
}
سپس پیاده سازی آن با توجه به کلاس SingletonCore ایی که در قسمت قبل تهیه کردیم (جهت مدیریت صحیح سشن فکتوری)، به صورت زیر خواهد بود.
این کلاس کار آغاز و پایان تراکنشها را نیز مدیریت کرده و جهت سهولت کار اینترفیس IDisposable را نیز پیاده سازی میکند :
using System;
using System.Linq;
using NHSessionManager;
using NHibernate;
using NHibernate.Linq;
namespace NHSample4.NHRepository
{
public class Repository<T> : IRepository<T>, IDisposable
{
private ISession _session;
private bool _disposed = false;
public Repository()
{
_session = SingletonCore.SessionFactory.OpenSession();
BeginTransaction();
}
~Repository()
{
Dispose(false);
}
public T Get(object key)
{
if (!isSessionSafe) return default(T);
return _session.Get<T>(key);
}
public T Save(T entity)
{
if (!isSessionSafe) return default(T);
_session.Save(entity);
return entity;
}
public T Update(T entity)
{
if (!isSessionSafe) return default(T);
_session.Update(entity);
return entity;
}
public void Delete(T entity)
{
if (!isSessionSafe) return;
_session.Delete(entity);
}
public IQueryable<T> Find()
{
if (!isSessionSafe) return null;
return _session.Linq<T>();
}
public IQueryable<T> Find(System.Linq.Expressions.Expression<Func<T, bool>> predicate)
{
if (!isSessionSafe) return null;
return Find().Where(predicate);
}
void Commit()
{
if (!isSessionSafe) return;
if (_session.Transaction != null &&
_session.Transaction.IsActive &&
!_session.Transaction.WasCommitted &&
!_session.Transaction.WasRolledBack)
{
_session.Transaction.Commit();
}
else
{
_session.Flush();
}
}
void Rollback()
{
if (!isSessionSafe) return;
if (_session.Transaction != null && _session.Transaction.IsActive)
{
_session.Transaction.Rollback();
}
}
private bool isSessionSafe
{
get
{
return _session != null && _session.IsOpen;
}
}
void BeginTransaction()
{
if (!isSessionSafe) return;
_session.BeginTransaction();
}
public void Dispose()
{
Dispose(true);
// tell the GC that the Finalize process no longer needs to be run for this object.
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposeManagedResources)
{
if (_disposed) return;
if (!disposeManagedResources) return;
if (!isSessionSafe) return;
try
{
Commit();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
Rollback();
}
finally
{
if (isSessionSafe)
{
_session.Close();
_session.Dispose();
}
}
_disposed = true;
}
}
}
using System;
using System.Collections.Generic;
using NHSample4.Domain;
using NHSample4.NHRepository;
namespace NHSample4
{
class Program
{
static void Main(string[] args)
{
//ایجاد دیتابیس در صورت نیاز
//NHSessionManager.Config.CreateDb();
//ابتدا یک دانشجو را اضافه میکنیم
Student student = null;
using (var studentRepo = new Repository<Student>())
{
student = studentRepo.Save(new Student() { Name = "Vahid" });
}
//سپس یک واحد را اضافه میکنیم
using (var courseRepo = new Repository<Course>())
{
var course = courseRepo.Save(new Course() { Teacher = "Shams" });
}
//اکنون یک واحد را به دانشجو انتساب میدهیم
using (var courseRepo = new Repository<Course>())
{
courseRepo.Save(new Course() { Students = new List<Student>() { student } });
}
//سپس شماره دروس استادی خاص را نمایش میدهیم
using (var courseRepo = new Repository<Course>())
{
var query = courseRepo.Find(t => t.Teacher == "Shams");
foreach (var course in query)
Console.WriteLine(course.Id);
}
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
همانطور که ملاحظه میکنید در این سطح دیگر برنامه هیچ درکی از ORM مورد استفاده ندارد و پیاده سازی نحوهی تعامل با NHibernate در پس کلاس مخزن مخفی شده است. کار آغاز و پایان تراکنشها به صورت خودکار مدیریت گردیده و همچنین آزاد سازی منابع را نیز توسط اینترفیس IDisposable مدیریت میکند. به این صورت امکان فراموش شدن یک سری از اعمال متداول به حداقل رسیده، میزان کدهای تکراری برنامه کم شده و همچنین هر زمانیکه نیاز بود، صرفا با تغییر پیاده سازی کلاس مخزن میتوان به ORM دیگری کوچ کرد؛ بدون اینکه نیازی به بازنویسی کل برنامه وجود داشته باشد.
دریافت سورس برنامه قسمت هشتم
ادامه دارد ...