مطالب
روش دیگر نوشتن Model binderهای سفارشی در ASP.NET Core 7x با معرفی IParseable
ASP.NET MVC از روش بکارگیری binding providerها برای تدارک زیرساخت model binding استفاده می‌کند که در این روش، داده‌های پارامترهای یک action method از طریق هدرها، کوئری استرینگ‌ها، بدنه‌ی درخواست و غیره تهیه می‌شوند. در حالت پیش‌فرض اگر این پارامترها از نوع‌های ساده‌ای مانند اعداد و یا DateTime تشکیل شده باشند و یا به همراه یک TypeConverter باشند که امکان تبدیل این رشته را به آن نوع خاص بدهد، به صورت خودکار bind خواهند شد و هر نوع دیگری، به صورت یک نوع پیچیده درنظر گرفته می‌شود. نوع پیچیده یعنی bind برای مثال اطلاعات بدنه‌ی درخواست به تک تک خواص آن نوع. برای نمونه در کنترلر زیر:
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries =
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy",
        "Hot", "Sweltering", "Scorching",
    };

    // /WeatherForecast/GetForecast2?from=1&to=3
    [HttpGet("[action]")]
    public IEnumerable<WeatherForecast> GetForecast2(Duration days)
    {
        return Enumerable.Range(days.From, days.To)
                         .Select(index => new WeatherForecast
                                          {
                                              Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                                              TemperatureC = Random.Shared.Next(-20, 55),
                                              Summary = Summaries[Random.Shared.Next(Summaries.Length)],
                                          })
                         .ToArray();
    }
}
که از دو مدل زیر استفاده می‌کند:
public class Duration
{
    public int From { get; set; }
    public int To { get; set; }
}

public class WeatherForecast
{
    public DateOnly Date { get; set; }

    public int TemperatureC { get; set; }

    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

    public string? Summary { get; set; }
}
می‌توان خواص پارامتر days را از طریق کوئری استرینگ‌های HttpGet، برای مثال با ارائه‌ی آدرس WeatherForecast/GetForecast2?from=1&to=3 به صورت خودکار تامین کرد. زمانیکه اطلاعات رسیده چنین شکلی را داشته باشند، کار پردازش و bind آن‌ها در حالت HttpGet، خودکار است.


روش دیگر پردازش اطلاعات رشته‌ای رسیده و تشکیل یک Model Binder سفارشی در ASP.NET Core 7x

اکنون فرض کنید بجای آدرس WeatherForecast/GetForecast2?from=1&to=3 که اطلاعات را از طریق کوئری استرینگ مشخص و استانداردی دریافت می‌کند، می‌خواهیم اطلاعات آن‌را از طریق یک قالب سفارشی و غیراستاندارد مانند WeatherForecast/GetForecast3/1-3 دریافت کنیم:
// /WeatherForecast/GetForecast3/1-3
[HttpGet("[action]/{days}")]
public IEnumerable<WeatherForecast> GetForecast3(Days days)
    {
        return Enumerable.Range(days.From, days.To)
                         .Select(index => new WeatherForecast
                                          {
                                              Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                                              TemperatureC = Random.Shared.Next(-20, 55),
                                              Summary = Summaries[Random.Shared.Next(Summaries.Length)],
                                          })
                         .ToArray();
    }
یکی از راه‌های انجام اینکار، نوشتن model binderهای سفارشی مخصوص است و یا اکنون در ASP.NET Core 7x می‌توان با پیاده سازی اینترفیس IParsable به صورت خودکار و با روشی دیگر به این مقصود رسید:
using System.Diagnostics.CodeAnalysis;
using System.Globalization;

namespace NET7Mvc.Models;

public class Days : IParsable<Days>
{
    public Days(int from, int to)
    {
        From = from;
        To = to;
    }

    public int From { get; }
    public int To { get; }

    public static bool TryParse([NotNullWhen(true)] string? value,
                                IFormatProvider? provider,
                                [MaybeNullWhen(false)] out Days result)
    {
        var items = value?.Split('-', StringSplitOptions.RemoveEmptyEntries);
        if (items is { Length: 2 })
        {
            if (int.TryParse(items[0], NumberStyles.None, provider, out var from)
                && int.TryParse(items[1], NumberStyles.None, provider, out var to))
            {
                result = new Days(from, to);
                return true;
            }
        }

        result = default;
        return false;
    }

    public static Days Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
            throw new ArgumentException("Could not parse the given value.", nameof(value));
        }

        return result;
    }
}
- برای پیاده سازی این اینترفیس باید دو متد TryParse و Parse آن‌را به صورت فوق پیاده سازی کرد و توسط آن، روش تبدیل رشته‌ی دریافتی از کاربر را به شیء مدنظر، مشخص کرد.
- همینقدر که مدلی IParsable را پیاده سازی کرده باشد، از امکانات آن به صورت خودکار استفاده خواهد شد و نیازی به معرفی و یا تنظیمات خاص دیگری ندارد.
- البته این قابلیت جدید نیست و پشتیبانی از IParsable، پیشتر در Minimal API دات نت 6 ارائه شده بود؛ اما در دات نت 7 توسط ASP.NET Core MVC نیز قابل استفاده شده‌است.
مطالب
کتابخانه GMap.Net
نقشه گوگل در حال حاضر یکی از محبوب‌ترین و کاملترین نقشه‌های جهان است و امکانات خوبی هم دارد. در این راستا بسیاری از مردم سعی در استفاده از این نقشه‌ها و امکانات آن‌ها دارند. به همین دلیل گوگل در بسته‌های api خود نیز این مورد را گنجانده است. ولی استفاده از این api مستلزم نوشتن کدهای جاوا اسکرپیتی و شناخت توابع و ثابت‌های api گوگل است. اما در هر صورت این مستندات مورد مطالعه قرار می‌گیرند.

سال گذشته بود که به بررسی کتابخانه‌های موجود برای دات نت که به ساخت نقشه‌های گوگل (+ ) می‌پردازند پرداختم. ولی مشکلی که وجود داشت، همه آن‌ها در نهایت یک تصویر jpeg تحویل می‌دادند. ولی من می‌خواستم نقشه‌ی من زنده و واکنشگرا باشد تا کاربر بتواند روی آن حرکت کند، زوم کند، مارکر‌ها را جابجا کند و امکانات دیگری که در این نقشه در دسترس است را داشته باشد. برای همین شروع به ساخت یک class library کردم تا کاربر بتواند در محیط سی شارپ، تنظیمات را با اسامی قابل شناخت و یک intellisense قدرتمند بنویسد و در نهایت بر اساس اطلاعات کاربر، این کدها به صورت جاوا اسکریپت تولید شود. می‌توانید سورس نهایی کتابخانه‌ی GMap.Net را در گیت هاب، به همراه یک پروژه‌ی نمونه ببینید.

پروژه‌ی وابسته این کتابخانه،  MS Ajax Minifier جهت کم حجم کردن کدهای جاوا اسکریپت است. در مورد این کتابخانه در سایت جاری بحث شده است.
برای نصب این کتابخانه می‌توانید از طریق دستور زیر در Nuget عمل کنید:
Install-Package GMap.Net

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

برای شروع نیاز است که یک نمونه از کلاس GoogleMapApi را ایجاد کنید. بعد از آن با استفاده از خصوصیت SetLocation، مختصات مرکز نقشه را تنظیم نمایید. سپس با استفاده از خصوصیات دیگر نیز می‌توانید نقشه را تنظیم نمایید. تعدادی از این خصوصیات مثل SetZoomVisibility هستند که با استفاده از آن می‌توانید تنظیمات زوم را روی نقشه پیاده سازی کنید. البته فعال کردن این گزینه به تنهایی کافی نیست و باید از طریق خصوصیت ZoomControlOption پیکربندی کنترل زوم را نیز اینجام دهید که این پیکربندی شامل موقعیت قرارگیری کنترل‌های زوم و اندازه‌ی دکمه‌ها می‌باشد و مابقی تنظیمات هم بدین شکل هستند:

به غیر از تنظیمات نقشه، Overlayهای زیر در این کلاس پشتیبانی می‌شوند:
عنوان
توضیحات
 Marker  یک نشانه گذار که برای مشخص کردن یک محل بر روی نقشه به کار می‌رود. این علامت گذار شامل خصوصیت‌هایی چون نقطه‌ی قرارگیری، آیکن، عنوان و انیمیشنی برای نحوه‌ی نمایش آن می‌باشد. همچنین شامل یک خصوصیت دیگر از نوع InfoWindow است که به شما امکان نمایش یک پنجره‌ی توضیحات را نیز بر روی مارکر می‌دهد. این محتوا می‌تواند به صورت HTML نمایش یابد.
 Circle  در صورتیکه بخواهید ناحیه‌ای دایره‌ای شکل را بر روی نقشه مشخص کنید، کاربرد دارد. با دادن نقطه‌ی مرکزی و شعاع می‌توانید دایره را ترسیم کنید. همچنین شامل خصوصیات ظاهری چون رنگ داخل و حاشیه‌ها و میزان شفافیت نیز می‌باشد.
 Rectangle  به رسم یک مستطیل می‌پردازد و تنها لازم است دو مختصات را به آن بدهید و بر اساس این دو نقطه، ناحیه‌ی مستطیلی شکل ترسیم می‌گردد. در صورتیکه نقاط بیشتری را به آن اضافه کنید، فقط دوتای اولی در نظر گرفته می‌شوند. این گزینه شامل خصوصیات ظاهری نیز می‌گردد.
 Polyline  برای ترسیم مسیرها به صورت چند ضلعی به کار می‌رود و الزامی به بستن مسیرها نیست. دارای خصوصیات ظاهری نیز می‌باشد.
 
polygon
کاملا شبیه Ployline است؛ با این تفاوت که یک چند ضلعی بسته است و می‌تواند داخل آن با رنگ پر باشد. برای بستن این چند ضلعی لازم نیست تا کاری انجام دهید. خود کلاس، نقطه‌ی اول و آخر را به هم وصل می‌کند.

خصوصیات آیتم‌های بالا، شامل موارد زیر می‌شود:

 نام خصوصیت
توضیحات
 Id  در سازنده‌ی هر کدام به طور اجباری قرار گرفته است. این id برای زمانی است که بخواهید با استفاده از جاوااسکرپیت با آن ارتباط برقرار کنید.
 Editable  با فعال کردن این خاصیت، به کاربر این اجازه را می‌دهید که بتواند روی Overlay ویرایش انجام دهد.
 StrokeWeight  ضخامت لبه‌ها را مشخص می‌کند.
 StrokeColor  رنگ لبه را مشخص می‌کند.
 StrokeOpacity  میزان شفافیت لبه را بین 0 تا 1 مشخص می‌کند.
 FillColor  بعضی از المان‌ها مانند چند ضلعی‌های بسته و مستطیل که ناحیه‌ی داخلی دارند، شامل این گزینه هستند و رنگ داخل این ناحیه را مشخص می‌کنند.
 FillOpacity  میزان شفافیت خصوصیت بالا را از 0 تا 1 مشخص می‌کند.
 Points  با استفاده از این خاصیت می‌توانید مختصات را با استفاده از کلاس Location به آن اضافه کنید. برای دایره خصوصیت Point وجود دارد.
 Radius  برای دایره کاربرد دارد. با مقدار نوع Int می‌توانید شعاع آن را مقدار دهی کنید.
کد زیر و تصویر زیر نمونه‌ای از کاربرد این کلاس است:
public class MiladTower
    {
        public GoogleMapApi TestMarker()
        {
            var map=new GoogleMapApi(true);
            var location = new Location(35.7448416, 51.3753212);
            map.SetLocation(location);
            map.SetZoom(17);
            map.SetMapType(MapTypes.ROADMAP);
            map.SetBackgroundColor(Color.Aqua);
            map.ZoomControlVisibilty(true);
            map.ZoomOptions = new zoomControlOptions()
            {
                Position = Position.TOP_LEFT,
                ZoomStyle = ZoomStyle.SMALL
            };
  

            Marker marker=new Marker("mymarker1");
            marker.InfoWindow=new InfoWindow("iw1")
            {
                Content = "<b>Milad Tower</b><i>in Tehran</i><br/>Milad Tower is the highest tower in iran,many people and tourists visit it each year, but it's so expensive that i cant afford it as iranian citizen<br/>please see more info at  <a href=\"https://en.wikipedia.org/wiki/Milad_Tower\"><img width='16px' height='16px' src='https://en.wikipedia.org/favicon.ico'/>wikipedia</a>"
            };
            marker.MarkerPoint = location;
            map.Markers.Add(marker);

            var circle=new CircleMarker("mymarker2");
            circle.FillColor = Color.Green;
            circle.FillOpacity = 0.6f;
            circle.StrokeColor = Color.Red;
            circle.StrokeOpacity = 0.8f;
            circle.Point = location;
            circle.Radius = 30;
            circle.Editable = true;
            circle.StrokeWeight = 3;
            map.Circles.Add(circle);

            Rectangle rect=new Rectangle("rect1");
            rect.FillColor = Color.Black;
            rect.FillOpacity = 0.4f;
            rect.Points.Add(new Location(35.74728723483808, 51.37550354003906));
            rect.Points.Add(new Location(35.74668641224311, 51.376715898513794));
            map.Rectangles.Add(rect);

            Polyline polyline=new Polyline("poly1");
            polyline.Points.Add(new Location(35.74457043569041, 51.373915672302246));
            polyline.Points.Add(new Location(35.74470976097927, 51.37359380722046));
            polyline.Points.Add(new Location(35.744378863020074, 51.37337923049927));
            polyline.StrokeColor = Color.Blue;
            polyline.StrokeWeight = 2;
            map.Polylines.Add(polyline);

            Polygon polygon=new Polygon("poly2");
            polygon.Points.Add(new Location(35.746494844665094, 51.374655961990356));
            polygon.Points.Add(new Location(35.74635552250061, 51.37283205986023));
            polygon.Points.Add(new Location(35.74598109297522, 51.372681856155396));
            polygon.Points.Add(new Location(35.7454934611854, 51.37361526489258));
            polygon.FillColor = Color.Black;
            polygon.FillOpacity = 0.5f;
            polygon.StrokeColor = Color.Gray;
            polygon.StrokeWeight = 1;
            map.Polygons.Add(polygon);

            return map;
        }
    }
بعد از ایجاد چنین کلاسی نیاز است تا آن را در ویوو ترسیم کنیم. ابتدا یک div برای ناحیه‌ی نقشه ایجاد می‌کنیم و سپس خروجی متد ShowMapForMVC را در ناحیه‌ی Head صفحه چاپ می‌کنیم. این خروجی به طور خودکار یک MVCHTMLString را بر می‌گرداند. در صورتیکه از وب فرم استفاده می‌کنید، می‌توانید از گزینه‌ی ShowMap استفاده کنید.
@section javascript
{
    @{
        var map = new MiladTower().TestMarker();
        @map.ShowMapForMvc("mapdiv")
    }
}
<br/><br/>
<div id="mapdiv" style="width:600px;height:600px;"></div>

در نهایت نقشه‌ی زیر نمایش داده می‌شود:


کم حجم کردن کدها
در صورتیکه به سورس صفحه نگاهی بیندازید، می‌بینید که کدهای جاوا اسکریپت، داخل صفحه نوشته شده‌اند. اگر بخواهید برای کم حجم‌تر شدن کد، عملیات minify را انجام دهید، با true شدن خصوصیت minified با استفاده از کتابخانه‌ی وابسته‌اش (MS Ajax Minifier)  اینکار را انجام می‌دهد.

انتقال کدها به یک فایل خارجی
بسیاری از ما برای نوشتن کدهای جاوا اسکریپت، از یک فایل خارجی استفاده می‌کنیم. برای داشتن این قابلیت می‌توانید به جای ShowMapForMVC متد CallJs را صدا بزنید تا کتابخانه api گوگل را صدا بزند و سپس در یک اکشن متد، متد GiveJustJS را صدا بزنید و طبق مقاله‌ی موجود در سایت جاری محتوای آن را برگردانید و به عنوان یک فایل JS به این اکشن متد لینک بدهید. کدهای زیر به شما نحوه‌ی این کار را نشان می‌دهند:
ابتدا در یک اکشن متد، کد زیر را وارد می‌کنیم:
    public ActionResult MiladJs()
        {
            var output = new MiladTower().TestMarker().GiveJustJs("mapdiv");
            Response.ContentType = "text/javascript";
            return Content(output);
        }

بعد از آن در ویووی مربوطه کد زیر را داریم:
@section javascript
{
    @{
        var map = new MiladTower().TestMarker();
        @map.CallJs()
        <script type="text/javascript" src="@Url.Action("MiladJs","Home")"></script>
    }   
}
<br/><br/>
<div id="mapdiv" style="width:600px;height:600px;"></div>
بدین ترتیب کدهای شما داخل یک فایل خارجی قرار می‌گیرند.


نحوه‌ی کارکرد این کتابخانه
برای آشنایی با نحوه‌ی کارکرد آن باید بدانید که اساس کار این کتابخانه string interpolation است. این کتابخانه کلاسی را به صورت Partial دارد که بین چندین فایل تقسیم شده است و هر یک از فایل‌ها، با نام محتوای آن نامگذاری شده‌اند. Public methods متدهای عمومی، private methods متدهای خصوصی، Constants یا ثابت‌ها که حاوی تمام دستورات جاوا اسکریپتی هستند و در نهایت خود کلاس اصلی GoogleMapApi که حاوی کدهای اجرایی و تشکیل کد جاوا اسکریپت می‌باشد. در کنار کلاس اصلی، کلاس‌های Overlay هم قرار دارند که شامل اطلاعات اشیاء روی نقشه‌ها هستند؛ مثل مارکرها و چندضلعی‌ها و ... و در نهایت یک سری کلاس و نوع شمارشی Enum شامل خصوصیت‌هایی که برای تنظیمات و پیکربندی نقشه به کار می‌روند.
کلاس GoogleMapApi برای ایجاد کدها، داده‌هایی را که برای هر کلاس در نظر گرفته‌اید، با استفاده از interpolation و ثابت‌های حاوی کد جاوا اسکریپت، در یک رشته‌ی جدید قرار می‌دهند و این رشته‌ها با اتصال درست در موقعیت خود، کد نهایی جاوا اسکریپت را تولید می‌کنند.
نظرات مطالب
ASP.NET MVC #18
- نیازی نیست تمام متدهای RoleProvider دات نت پیاده سازی شوند. برای یک برنامه پیاده سازی دو متد IsUserInRole، GetRolesForUser کافی است. 
- سپس دو کلاس Role و User را باید تعریف کنید. این دو رابطه many-to-many با هم دارند؛ یعنی هر کدام با یک ICollection به دیگری ارتباط پیدا می‌کنند. سپس این دو کلاس را در کلاس Context برنامه مطابق معمول توسط DbSetها در معرض دید EF قرار می‌دهید. مابقی آن کارکردن معمولی با این دو جدول اضافه شده به برنامه است:
    public class EfRolesService : IRolesService
    {
        readonly IUnitOfWork _uow;
        readonly IDbSet<Role> _roles;
        public EfRolesService(IUnitOfWork uow)
        {
            _uow = uow;
            _roles = _uow.Set<Role>();
        }

        public IList<Role> FindUserRoles(int userId)
        {
            var query = from role in _roles
                        from user in role.Users
                        where user.Id == userId
                        select role;

            return query.OrderBy(x => x.Name).ToList();
        }

        public string[] GetRolesForUser(int userId)
        {
            var roles = FindUserRoles(userId);
            if (roles == null || !roles.Any())
            {
                return new string[] { };
            }

            return roles.Select(x => x.Name).ToArray();
        }

        public bool IsUserInRole(int userId, string roleName)
        {
            var query = from role in _roles
                        where role.Name == roleName
                        from user in role.Users
                        where user.Id == userId
                        select role;
            var userRole = query.FirstOrDefault();
            return userRole != null;
        }
    }
و در این حالت CustomRoleProvider به صورت زیر خواهد بود. در این روش فرض شده حین لاگین، user.Id در FormsAuthentication.SetAuthCookie تنظیم می‌شود؛ یعنی userName در این RoleProvider به id آن تنظیم شده:
    public class CustomRoleProvider : RoleProvider
    {
        public override bool IsUserInRole(string username, string roleName)
        {
            // Since the role provider, in this case the CustomRoleProvider is instantiated by 
            // the ASP.NET framework the best solution is to use the service locator pattern. 
            // The service locator pattern is normally considered to be an anti-pattern but 
            // sometimes you have to be pragmatic and accept the limitation on the framework 
            // that is being used (in this case the ASP.NET framework).

            var rolesService = ObjectFactory.GetInstance<IRolesService>();
            return rolesService.IsUserInRole(username.ToInt(), roleName);
        }

        public override string[] GetRolesForUser(string username)
        {
            var rolesService = ObjectFactory.GetInstance<IRolesService>();
            return rolesService.GetRolesForUser(username.ToInt());
        }
// مابقی نیازی نیست پیاده سازی شوند
مطالب
ASP.NET MVC #6

آشنایی با انواع ActionResult

در قسمت چهارم، اولین متد یا اکشنی که به صورت خودکار توسط VS.NET به برنامه اضافه شد، اینچنین بود:

using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
//
// GET: /Home/
public ActionResult Index()
{
return View();
}
}
}

توضیحات تکمیلی مرتبط با خروجی از نوع ActionResult ایی را که مشاهده می‌کنید، در این قسمت ارائه خواهد شد.
رفتار یک کنترلر توسط متدهایی که در آن کلاس تعریف می‌شوند، مشخص می‌گردد. هر متد هم از طریق یک URL مجزا قابل دسترسی و فراخوانی خواهد بود. این متدها که به آن‌ها اکشن نیز گفته می‌شود باید عمومی بوده، استاتیک یا متد الحاقی (extension method) نباشند و همچنین دارای پارامترهایی از نوع ref و out نیز نباشند.
هر درخواست رسیده، به یک کنترلر و متدی عمومی در آن توسط سیستم مسیریابی، نگاشت خواهد شد. اگر علاقمند باشید که در یک کلاس کنترلر، متدی عمومی را از این سیستم خارج کنید، تنها کافی است آن‌را با ویژگی (attribute) به نام NonAction مزین کنید:

using System.Web.Mvc;

namespace MvcApplication2.Controllers
{
public class HomeController : Controller
{
[NonAction]
public string ShowData()
{
return "Text";
}

public ActionResult Index()
{
ViewBag.Message = string.Format("{0}/{1}/{2}",
RouteData.Values["controller"],
RouteData.Values["action"],
RouteData.Values["id"]);
return View();
}

public ActionResult Search(string data = "*")
{
// do something ...
return View();
}
}
}

چند نکته در این مثال قابل ذکر است:
الف) در اینجا اگر شخصی آدرس http://localhost/home/showdata را درخواست نماید، با توجه به استفاده از ویژگی NonAction، با پیغام یافت نشد یا 404 مواجه می‌گردد.
ب) صرفنظر از پارامترهای یک متد و ساختار کلاس جاری، اطلاعات مسیریابی از طریق شیء RouteData.Values نیز در دسترس هستند که نمونه‌ای از آن‌را در اینجا بر اساس مقادیر پیش فرض تعاریف مسیریابی یک پروژه ASP.NET MVC ملاحظه می‌نمائید.
ج) در متد Search، از قابلیت امکان تعریف مقداری پیش فرض جهت آرگومان‌ها در سی شارپ 4 استفاده شده است. به این ترتیب اگر شخصی آدرس http://localhost/home/search را وارد کند، چون پارامتری را ذکر نکرده است، به صورت خودکار از مقدار پیش فرض آرگومان data استفاده می‌گردد.


انواع Action Results در ASP.NET MVC

در ASP.NET MVC بجای استفاده مستقیم از شیء Response، از شیء ActionResult جهت ارائه خروجی یک متد استفاده می‌شود و مهم‌ترین دلیل آن هم مشکل بودن نوشتن آزمون‌های واحد برای شیء Response است که وهله سازی آن مساوی است با به کار اندازی موتور ASP.NET و Http Runtime آن توسط یک وب سرور (بنابراین در ASP.NET MVC سعی کنید شیء Response را فراموش کنید).
سلسه مراتب ActionResult‌های قابل استفاده در ASP.NET در تصویر زیر مشخص شده‌اند:


و در مثال زیر تقریبا انواع و اقسام ActionResult‌های مهم و کاربردی ASP.NET MVC را می‌توانید مشاهده کنید:

using System.Web.Mvc;

namespace MvcApplication2.Controllers
{
public class ActionResultsController : Controller
{
//http://localhost/actionresults/welcome
public string Welcome()
{
return "Hello, World";
}

//http://localhost/actionresults/index
public ActionResult Index() // or ContentResult
{
return Content("Hello, World");
}

//http://localhost/actionresults/SendMail
public void SendMail()
{
}

public ActionResult SendMailCompleted() // or EmptyResult
{
// do whatever
return new EmptyResult();
}

public ActionResult GetFile() // or FilePathResult
{
return File(Server.MapPath("~/content/site.css"), "text/css", "mySite.css");
}

public ActionResult UnauthorizedStatus() // or HttpStatusCodeResult/HttpUnauthorizedResult
{
return new HttpUnauthorizedResult("You need to login first.");
}

public ActionResult Status() // or HttpStatusCodeResult
{
return new HttpStatusCodeResult(501, "Server Error");
}

public ActionResult GetJavaScript() // or JavaScriptResult
{
return JavaScript("...JavaScript...");
}

public ActionResult GetJson() // or JsonResult
{
var obj = new { prop1 = 1, prop2 = "data" };
return Json(obj, JsonRequestBehavior.AllowGet);
}

public ActionResult RedirectTo() // or RedirectResult
{
return RedirectPermanent("http://www.site.com");
//return RedirectToAction("Home", "Index");
}

public ActionResult ShowView() // or ViewResult
{
return View();
}
}
}

چند نکته در این مثال وجود دارد:
1) مثلا متد GetJavaScript را درنظر بگیرید. در این متد خاص، چه بنویسید public ActionResult GetJavaScript یا بنویسید public JavaScriptResult GetJavaScript تفاوتی نمی‌کند. در سایر موارد هم به همین ترتیب است. علت را در تصویر سلسله مراتبی ActionResult‌ها می‌توان جستجو کرد. تمام این کلاس‌ها نوعی ActionResult هستند و از یک کلاس پایه به ارث رسیده‌اند.
2) مثلا ContentResult شبیه به همان Response.Write سابق ASP.NET عمل می‌کند. علت وجودی آن هم عدم وابستگی مستقیم به شیء Response و ساده‌تر سازی نوشتن آزمون‌های واحد برای این نوع اکشن متدها است.
3) منهای متد آخری که نمایش داده شده (ShowView)، هیچکدام از متدهای دیگر نیازی به View متناظر ندارند. یعنی نیازی نیست تا روی متد کلیک راست کرده و Add view را انتخاب کنیم. چون در همین متد کنترلر، کار Response به پایان می‌رسد و مرحله بعدی ندارد. مثلا در حالت return File، یک فایل به درون مرورگر کاربر Flush خواهد شد و تمام.
4) متد Welcome و متد Index در اینجا به یک صورت تفسیر می‌شوند. به این معنا که اگر خروجی متد تعریف شده در یک کنترلر از نوع ActionResult نباشد، به صورت پیش فرض درون یک ContentResult محصور خواهد شد.
5) اگر خروجی متدی در اینجا از نوع void باشد، با ActionResult ایی به نام EmptyResult یکسان خواهد بود. بنابراین با متدهای SendMail و SendMailCompleted به یک نحو رفتار می‌گردد.
6) return Json یاد شده که خروجی‌اش از نوع JsonResultاست در پیاده سازی‌های Ajax ایی کاربرد دارد.
7) جهت بازگرداندن حالت وضعیت 403 یا غیرمجاز می‌توان از return new HttpUnauthorizedResult استفاده کرد.
8) یا جهت اعلام مشکلی در سمت سرور به کمک return new HttpStatusCodeResultکد ویژه‌ای را می‌توان به کاربر نمایش داد.
9) به کمک return RedirectToAction می‌توان به یک کنترلر و متدی خاص در آن، کاربر را هدایت کرد.

و خلاصه اینکه تمام کارهایی را که پیشتر در ASP.NET Web forms ، مستقیما به کمک شیء Response انجام می‌دادید (Response.Write، Response.End، Response.Redirect و غیره)، اینبار به کمک یکی از ActionResult‌های یاد شده انجام دهید تا بتوان بدون نیاز به راه اندازی یک وب سرور، برای متدهای کنترلرها آزمون واحد نوشت. برای مثال:

[TestMethod]
public void TestMethod1()
{
    // Arrange
    var controller = new ActionResultsController();

    // Act
    var result = controller.Index() as ContentResult;

    // Assert
    Assert.NotNull(result);
    Assert.AreEqual( "Hello, World", result.Content);
}



نظرات اشتراک‌ها
کتابخانه tinymce
Required کردن TinyMCE با اتریبیوت اعتبارسنجی سفارشی
 سمت سرور :
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)]
public class MakeTinyMceRequiredAttribute : ValidationAttribute, IClientModelValidator
{
    public MakeTinyMceRequiredAttribute()
    {
        ErrorMessage = "لطفا {0} را وارد نمایید";
    }

    protected override ValidationResult IsValid(object value,
        ValidationContext validationContext)
    {
        var displayName = validationContext.DisplayName;
        ErrorMessage = ErrorMessage.Replace("{0}", displayName);

        if (string.IsNullOrWhiteSpace(value?.ToString()))
        {
            return new ValidationResult(ErrorMessage);
        }
        return ValidationResult.Success;
    }

    public void AddValidation(ClientModelValidationContext context)
    {
        var displayName = context.ModelMetadata.ContainerMetadata
            .ModelType.GetProperty(context.ModelMetadata.PropertyName)
            .GetCustomAttributes(typeof(DisplayAttribute), false)
            .Cast<DisplayAttribute>()
            .FirstOrDefault()?.Name;
        ErrorMessage = ErrorMessage.Replace("{0}", displayName);

        MergeAttribute(context.Attributes, "data-val", "true");
        MergeAttribute(context.Attributes, "data-val-makeTinyMceRequired", ErrorMessage);
    }
    public bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
    {
        if (attributes.ContainsKey(key))
        {
            return false;
        }
        attributes.Add(key, value);
        return true;
    }
}
سمت کلاینت :
if (jQuery.validator) {
    // For hidden inputs
    $.validator.setDefaults({
        ignore: []
    });

    // makeTinyMceRequired
    jQuery.validator.addMethod('makeTinyMceRequired', function (value, element, param) {
        var editorId = $(element).attr('id');
        var editorContent = tinyMCE.get(editorId).getContent();
        $('body').append(`<div id="test-makeTinyMceRequired">${editorContent}</div>`);
        var result = isNullOrWhitespace($('#test-makeTinyMceRequired').text());
        $('#test-makeTinyMceRequired').remove();
        return !result;
    });
    jQuery.validator.unobtrusive.adapters.addBool('makeTinyMceRequired');
}
function isNullOrWhitespace(input) {
    if (typeof input === 'undefined' || input == null)
        return true;
    return input.replace(/\s/g, '').length < 1;
}
نحوه انجام کار :
متنِ ادیتور TinyMCE داخل یک div موقت ریخته میشه بعد text اون div با isNullOrWhitespace بررسی میشه، اگه کاربر مقداری رو وارد کرده باشه اعتبارسنجی پاس میشه در غیر اینصورت متن خطارو نمایش میده.
مطالب
Globalization در ASP.NET MVC - قسمت دوم

به‌روزرسانی فایلهای Resource در زمان اجرا

یکی از ویژگیهای مهمی که در پیاده سازی محصول با استفاده از فایلهای Resource باید به آن توجه داشت، امکان بروز رسانی محتوای این فایلها در زمان اجراست. از آنجاکه احتمال اینکه کاربران سیستم خواهان تغییر این مقادیر باشند بسیار زیاد است، بنابراین درنظر گرفتن چنین ویژگی‌ای برای محصول نهایی میتواند بسیار تعیین کننده باشد. متاسفانه پیاده سازی چنین امکانی درباره فایلهای Resource چندان آسان نیست. زیرا این فایلها همانطور که در قسمت قبل توضیح داده شد پس از کامپایل به صورت اسمبلی‌های ستلایت (Satellite Assembly) درآمده و دیگر امکان تغییر محتوای آنها بصورت مستقیم و به آسانی وجود ندارد.

نکته: البته نحوه پیاده سازی این فایلها در اسمبلی نهایی (و در حالت کلی نحوه استفاده از هر فایلی در اسمبلی نهایی) در ویژوال استودیو توسط خاصیت Build Action تعیین میشود. برای کسب اطلاعات بیشتر راجع به این خاصیت به اینجا رجوع کنید.

یکی از روشهای نسبتا من‌درآوردی که برای ویرایش و به روزرسانی کلیدهای Resource وجود دارد بدین صورت است:
- ابتدا باید اصل فایلهای Resource به همراه پروژه پابلیش شود. بهترین مکان برای نگهداری این فایلها فولدر App_Data است. زیرا محتویات این فولدر توسط سیستم FCN (همان File Change Notification) در ASP.NET رصد نمیشود.
نکته: علت این حساسیت این است که FCN در ASP.NET تقریبا تمام محتویات فولدر سایت در سرور (فولدر App_Data یکی از معدود استثناهاست) را تحت نظر دارد و رفتار پیشفرض این است که با هر تغییری در این محتویات، AppDomain سایت Unload میشود که پس از اولین درخواست دوباره Load میشود. این اتفاق موجب از دست دادن تمام سشن‌ها و محتوای کش‌ها و ... میشود (اطلاعات بیشتر و کاملتر درباره نحوه رفتار FCN در اینجا).
- سپس با استفاده یک مقدار کدنویسی امکاناتی برای ویرایش محتوای این فایلها فراهم شود. ازآنجا که محتوای این فایلها به صورت XML ذخیره میشود بنابراین براحتی میتوان با امکانات موجود این ویژگی را پیاده سازی کرد. اما در فضای نام System.Windows.Forms کلاسهایی وجود دارد که مخصوص کار با این فایلها طراحی شده اند که کار نمایش و ویرایش محتوای فایلهای Resource را ساده‌تر میکند. به این کلاسها در قسمت قبلی اشاره کوتاهی شده بود.
- پس از ویرایش و به روزرسانی محتوای این فایلها باید کاری کنیم تا برنامه از این محتوای تغییر یافته به عنوان منبع جدید بهره بگیرد. اگر از این فایلهای Rsource به صورت embed استفاده شده باشد در هنگام build پروژه محتوای این فایلها به صورت Satellite Assembly در کنار کتابخانه‌های دیگر تولید میشود. اسمبلی مربوط به هر زبان هم در فولدری با عنوان زبان مربوطه ذخیره میشود. مسیر و نام فایل این اسمبلی‌ها مثلا به صورت زیر است:
bin\fa\Resources.resources.dll
بنابراین در این روش برای استفاده از محتوای به روز رسانی شده باید عملیات Build این کتابخانه دوباره انجام شود و کتابخانه‌های جدیدی تولید شود. راه حل اولی که به ذهن میرسد این است که از ابزارهای پایه و اصلی برای تولید این کتابخانه‌ها استفاده شود. این ابزارها (همانطور که در قسمت قبل نیز توضیح داده شد) عبارتند از Resource Generator و Assembly Linker. اما استفاده از این ابزارها و پیاده سازی روش مربوطه سختتر از آن است که به نظر می‌آید. خوشبختانه درون مجموعه عظیم دات نت ابزار مناسبتری برای این کار نیز وجود دارد که کار تولید کتابخانه‌های موردنظر را به سادگی انجام میدهد. این ابزار با عنوان Microsoft Build شناخته میشود که در اینجا توضیح داده شده است. 

خواندن محتویات یک فایل resx.
همانطور که در بالا توضیح داده شد برای راحتی کار میتوان از کلاس زیر که در فایل System.Windows.Forms.dll قرار دارد استفاده کرد:
System.Resources.ResXResourceReader
این کلاس چندین کانستراکتور دارد که مسیر فایل resx. یا استریم مربوطه به همراه چند گزینه دیگر را به عنوان ورودی میگیرد. این کلاس یک Enumator دارد که یک شی از نوع IDictionaryEnumerator برمیگرداند. هر عضو این enumerator از نوع object است. برای استفاده از این اعضا ابتدا باید آنرا به نوع DictionaryEntry تبدیل کرد. مثلا بصورت زیر:
private void TestResXResourceReader()
{
  using (var reader = new ResXResourceReader("Resource1.fa.resx"))
  {
    foreach (var item in reader)
    {
      var resource = (DictionaryEntry)item;
      Console.WriteLine("{0}: {1}", resource.Key, resource.Value);
    }
  }
}
همانطور که ملاحظه میکنید استفاده از این کلاس بسیار ساده است. ازآنجاکه DictionaryEntry یک struct است، به عنوان یک راه حل مناسبتر بهتر است ابتدا کلاسی به صورت زیر تعریف شود:
public class ResXResourceEntry
{
  public string Key { get; set; }
  public string Value { get; set; }
  public ResXResourceEntry() { }
  public ResXResourceEntry(object key, object value)
  {
    Key = key.ToString();
    Value = value.ToString();
  }
  public ResXResourceEntry(DictionaryEntry dictionaryEntry)
  {
    Key = dictionaryEntry.Key.ToString();
    Value = dictionaryEntry.Value != null ? dictionaryEntry.Value.ToString() : string.Empty;
  }
  public DictionaryEntry ToDictionaryEntry()
  {
    return new DictionaryEntry(Key, Value);
  }
}
سپس با استفاده از این کلاس خواهیم داشت:
private static List<ResXResourceEntry> Read(string filePath)
{
  using (var reader = new ResXResourceReader(filePath))
  {
    return reader.Cast<object>().Cast<DictionaryEntry>().Select(de => new ResXResourceEntry(de)).ToList();
  }
}
حال این متد برای استفاده‌های آتی آماده است.

نوشتن در فایل resx.
برای نوشتن در یک فایل resx. میتوان از کلاس ResXResourceWriter استفاده کرد. این کلاس نیز در کتابخانه System.Windows.Forms در فایل System.Windows.Forms.dll قرار دارد:
System.Resources.ResXResourceWriter
متاسفانه در این کلاس امکان افزودن یا ویرایش یک کلید به تنهایی وجود ندارد. بنابراین برای ویرایش یا اضافه کردن حتی یک کلید کل فایل باید دوباره تولید شود. برای استفاده از این کلاس نیز میتوان به شکل زیر عمل کرد:
private static void Write(IEnumerable<ResXResourceEntry> resources, string filePath)
{
  using (var writer = new ResXResourceWriter(filePath))
  {
    foreach (var resource in resources)
    {
      writer.AddResource(resource.Key, resource.Value);
    }
  }
}
در متد فوق از همان کلاس ResXResourceEntry که در قسمت قبل معرفی شد، استفاده شده است. از متد زیر نیز میتوان برای حالت کلی حذف یا ویرایش استفاده کرد:
private static void AddOrUpdate(ResXResourceEntry resource, string filePath)
{
  var list = Read(filePath);
  var entry = list.SingleOrDefault(l => l.Key == resource.Key);
  if (entry == null)
  {
    list.Add(resource);
  }
  else
  {
    entry.Value = resource.Value;
  }
  Write(list, filePath);
}
در این متد از متدهای Read و Write که در بالا نشان داده شده‌اند استفاده شده است.

حذف یک کلید در فایل resx.
برای اینکار میتوان از متد زیر استفاده کرد:
private static void Remove(string key, string filePath)
{
  var list = Read(filePath);
  list.RemoveAll(l => l.Key == key); 
  Write(list, filePath);
}
در این متد، از متد Write که در قسمت معرفی شد، استفاده شده است.

راه حل نهایی
قبل از بکارگیری روشهای معرفی شده در این مطلب بهتر است ابتدا یکسری قرارداد بصورت زیر تعریف شوند:
- طبق راهنماییهای موجود در قسمت قبل یک پروژه جداگانه با عنوان Resources برای نگهداری فایلهای resx. ایجاد شود.
- همواره آخرین نسخه از محتویات موردنیاز از پروژه Resources باید درون فولدری با عنوان Resources در پوشه App_Data قرار داشته باشد.
- آخرین نسخه تولیدی از محتویات موردنیاز پروژه Resource در فولدری با عنوان Defaults در مسیر App_Data\Resources برای فراهم کردن امکان "بازگرداندن به تنظیمات اولیه" وجود داشته باشد.
برای فراهم کردن این موارد بهترین راه حل استفاده از تنظیمات Post-build event command line است. اطلاعات بیشتر درباره Build Eventها در اینجا.

برای اینکار من از دستور xcopy استفاده کردم که نسخه توسعه یافته دستور copy است. دستورات استفاده شده در این قسمت عبارتند از:
xcopy $(ProjectDir)*.* $(SolutionDir)MvcApplication1\App_Data\Resources /e /y /i /exclude:$(ProjectDir)excludes.txt
xcopy $(ProjectDir)*.* $(SolutionDir)MvcApplication1\App_Data\Resources\Defaults /e /y /i /exclude:$(ProjectDir)excludes.txt
xcopy $(ProjectDir)$(OutDir)*.* $(SolutionDir)MvcApplication1\App_Data\Resources\Defaults\bin /e /y /i 
در دستورات فوق آرگومان e/ برای کپی تمام فولدرها و زیرفولدرها، y/ برای تایید تمام کانفیرم ها، و i/ برای ایجاد خودکار فولدرهای موردنیاز استفاده میشود. آرگومان exclude/ نیز همانطور که از نامش پیداست برای خارج کردن فایلها و فولدرهای موردنظر از لیست کپی استفاده میشود. این آرگومان مسیر یک فایل متنی حاوی لیست این فایلها را دریافت میکند. در تصویر زیر یک نمونه از این فایل و مسیر و محتوای مناسب آن را مشاهده میکنید:

با استفاده از این فایل excludes.txt فولدرهای bin و obj و نیز فایلهای با پسوند user. و vspscc. (مربوط به TFS) و نیز خود فایل excludes.txt از لیست کپی دستور xcopy حذف میشوند و بنابراین کپی نمیشوند. درصورت نیاز میتوانید گزینه‌های دیگری نیز به این فایل اضافه کنید.
همانطور که در اینجا اشاره شده است، در تنظیمات Post-build event command line یکسری متغیرهای ازپیش تعریف شده (Macro) وجود دارند که از برخی از آنها در دستوارت فوق استفاده شده است:
(ProjectDir)$ : مسیر کامل و مطلق پروژه جاری به همراه یک کاراکتر \ در انتها
(SolutionDir)$ : مسیر کامل و مطلق سولوشن به همراه یک کاراکتر \ در انتها
(OutDir)$ : مسیر نسبی فولدر Output پروژه جاری به همراه یک کاراکتر \ در انتها

نکته: این دستورات باید در Post-Build Event پروژه Resources افزوده شوند.

با استفاده از این تنظیمات مطمئن میشویم که پس از هر Build آخرین نسخه از فایلهای موردنیاز در مسیرهای تعیین شده کپی میشوند. درنهایت با استفاده از کلاس ResXResourceManager که در زیر آورده شده است، کل عملیات را ساماندهی میکنیم:
public class ResXResourceManager
{
  private static readonly object Lock = new object();
  public string ResourcesPath { get; private set; }
  public ResXResourceManager(string resourcesPath)
  {
    ResourcesPath = resourcesPath;
  }
  public IEnumerable<ResXResourceEntry> GetAllResources(string resourceCategory)
  {
    var resourceFilePath = GetResourceFilePath(resourceCategory);
    return Read(resourceFilePath);
  }
  public void AddOrUpdateResource(ResXResourceEntry resource, string resourceCategory)
  {
    var resourceFilePath = GetResourceFilePath(resourceCategory);
    AddOrUpdate(resource, resourceFilePath);
  }
  public void DeleteResource(string key, string resourceCategory)
  {
    var resourceFilePath = GetResourceFilePath(resourceCategory);
    Remove(key, resourceFilePath);
  }
  private string GetResourceFilePath(string resourceCategory)
  {
    var extension = Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName == "en" ? ".resx" : ".fa.resx";
    var resourceFilePath = Path.Combine(ResourcesPath, resourceCategory.Replace(".", "\\") + extension);
    return resourceFilePath;
  }
  private static void AddOrUpdate(ResXResourceEntry resource, string filePath)
  {
    var list = Read(filePath);
    var entry = list.SingleOrDefault(l => l.Key == resource.Key);
    if (entry == null)
    {
      list.Add(resource);
    }
    else
    {
      entry.Value = resource.Value;
    }
    Write(list, filePath);
  }
  private static void Remove(string key, string filePath)
  {
    var list = Read(filePath);
    list.RemoveAll(l => l.Key == key); 
    Write(list, filePath);
  }
  private static List<ResXResourceEntry> Read(string filePath)
  {
    lock (Lock)
    {
      using (var reader = new ResXResourceReader(filePath))
      {
        var list = reader.Cast<object>().Cast<DictionaryEntry>().ToList();
        return list.Select(l => new ResXResourceEntry(l)).ToList();
      }
    }
  }
  private static void Write(IEnumerable<ResXResourceEntry> resources, string filePath)
  {
    lock (Lock)
    {
      using (var writer = new ResXResourceWriter(filePath))
      {
        foreach (var resource in resources)
        {
          writer.AddResource(resource.Key, resource.Value);
        }
      }
    }
  }
}
در این کلاس تغییراتی در متدهای معرفی شده در قسمتهای بالا برای مدیریت دسترسی همزمان با استفاده از بلاک lock ایجاد شده است.
با استفاده از کلاس BuildManager عملیات تولید کتابخانه‌ها مدیریت میشود. (در مورد نحوه استفاده از MSBuild در اینجا توضیحات کافی آورده شده است):
public class BuildManager
{
  public string ProjectPath { get; private set; }
  public BuildManager(string projectPath)
  {
    ProjectPath = projectPath;
  }
  public void Build()
  {
    var regKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\MSBuild\ToolsVersions\4.0");
    if (regKey == null) return;
    var msBuildExeFilePath = Path.Combine(regKey.GetValue("MSBuildToolsPath").ToString(), "MSBuild.exe");
    var startInfo = new ProcessStartInfo
    {
      FileName = msBuildExeFilePath,
      Arguments = ProjectPath,
      WindowStyle = ProcessWindowStyle.Hidden
    };
    var process = Process.Start(startInfo);
    process.WaitForExit();
  }
}
درنهایت مثلا با استفاده از کلاس ResXResourceFileManager مدیریت فایلهای این کتابخانه‌ها صورت میپذیرد:
public class ResXResourceFileManager
{
  public static readonly string BinPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase.Replace("file:///", ""));
  public static readonly string ResourcesPath = Path.Combine(BinPath, @"..\App_Data\Resources");
  public static readonly string ResourceProjectPath = Path.Combine(ResourcesPath, "Resources.csproj");
  public static readonly string DefaultsPath = Path.Combine(ResourcesPath, "Defaults");
  public static void CopyDlls()
  {
    File.Copy(Path.Combine(ResourcesPath, @"bin\debug\Resources.dll"), Path.Combine(BinPath, "Resources.dll"), true);
    File.Copy(Path.Combine(ResourcesPath, @"bin\debug\fa\Resources.resources.dll"), Path.Combine(BinPath, @"fa\Resources.resources.dll"), true);
    Directory.Delete(Path.Combine(ResourcesPath, "bin"), true);
    Directory.Delete(Path.Combine(ResourcesPath, "obj"), true);
  }
  public static void RestoreAll()
  {
    RestoreDlls();
    RestoreResourceFiles();
  }
  public static void RestoreDlls()
  {
    File.Copy(Path.Combine(DefaultsPath, @"bin\Resources.dll"), Path.Combine(BinPath, "Resources.dll"), true);
    File.Copy(Path.Combine(DefaultsPath, @"bin\fa\Resources.resources.dll"), Path.Combine(BinPath, @"fa\Resources.resources.dll"), true);
  }
  public static void RestoreResourceFiles(string resourceCategory)
  {
    RestoreFile(resourceCategory.Replace(".", "\\"));
  }
  public static void RestoreResourceFiles()
  {
    RestoreFile(@"Global\Configs");
    RestoreFile(@"Global\Exceptions");
    RestoreFile(@"Global\Paths");
    RestoreFile(@"Global\Texts");

    RestoreFile(@"ViewModels\Employees");
    RestoreFile(@"ViewModels\LogOn");
    RestoreFile(@"ViewModels\Settings");

    RestoreFile(@"Views\Employees");
    RestoreFile(@"Views\LogOn");
    RestoreFile(@"Views\Settings");
  }

  private static void RestoreFile(string subPath)
  {
    File.Copy(Path.Combine(DefaultsPath, subPath + ".resx"), Path.Combine(ResourcesPath, subPath + ".resx"), true);
    File.Copy(Path.Combine(DefaultsPath, subPath + ".fa.resx"), Path.Combine(ResourcesPath, subPath + ".fa.resx"), true);
  }
}
در این کلاس از مفهومی با عنوان resourceCategory برای استفاده راحتتر در ویوها استفاده شده است که بیانگر فضای نام نسبی فایلهای Resource و کلاسهای متناظر با آنهاست که براساس استانداردها باید برطبق مسیر فیزیکی آنها در پروژه باشد مثل Global.Texts یا Views.LogOn. همچنین در متد RestoreResourceFiles نمونه هایی از مسیرهای این فایلها آورده شده است.
پس از اجرای متد Build از کلاس BuildManager، یعنی پس از build پروژه Resource در زمان اجرا، باید ابتدا فایلهای تولیدی به مسیرهای مربوطه در فولدر bin برنامه کپی شده سپس فولدرهای تولیدشده توسط msbuild، حذف شوند. این کار در متد CopyDlls از کلاسResXResourceFileManager انجام میشود. هرچند در این قسمت فرض شده است که فایل csprj. موجود برای حالت debug تنظیم شده است.
نکته: دقت کنید که در این قسمت بلافاصله پس از کپی فایلها در مقصد با توجه به توضیحات ابتدای این مطلب سایت Restart خواهد شد که یکی از ضعفهای عمده این روش به شمار میرود.
سایر متدهای موجود نیز برای برگرداندن تنظیمات اولیه بکار میروند. در این متدها از محتویات فولدر Defaults استفاده میشود.
نکته: درصورت ساخت دوباره اسمبلی و یا بازگرداندن اسمبلی‌های اولیه، از آنجاکه وب‌سایت Restart خواهد شد، بنابراین بهتر است تا صفحه جاری بلافاصله پس از اتمام عملیات،دوباره بارگذاری شود. مثلا اگر از ajax برای اعمال این دستورات استفاده شده باشد میتوان با استفاده از کدی مشابه زیر در پایان فرایند صفحه را دوباره بارگذاری کرد:
window.location.reload();

در قسمت بعدی راه حل بهتری با استفاده از فراهم کردن پرووایدر سفارشی برای مدیریت فایلهای Resource ارائه میشود.
مطالب
طراحی و پیاده سازی مکانیزم مدیریت Transactionها در ServiceLayer

هدف ارائه راه حلی برای مدیریت Transactionها به عنوان یک Cross Cutting Concern، توسط ApplicationServiceها می‌باشد. 

پیش نیازها:

پیش فرض ما این است که شما از EF به عنوان OR-Mapper استفاده می‌کنید و الگوی Context Per Request را پیاده سازی کرده اید یا از طریق پیاده سازی الگوی Container Per Request به داشتن Context یکتا برای هر درخواست رسیده اید.
کتابخانه StructureMap.Mvc5 پیاده سازی از الگوی Container Per Request را با استفاده از امکانات Nested Container مربوط به StructureMap ارائه می‌دهد. اشیاء موجود در Nested Container طول عمر Singleton دارند.

ایده کار به این صورت هست که توسط یک Interceptor، به هنگام ورود به متد موجود در یک ApplicationService که با TransactionalAttribute تزئین شده است، یک تراکنش ایجاد شده و بعد از اتمام کار متد مورد نظر، در صورت عدم بروز استثناء، Commit و در غیر این صورت Rollback شود. حال اگر یک متد تراکنشی، داخل خود از متد تراکنشی دیگری استفاده کند، صرفا متد بیرونی تراکنش را ایجاد می‌کند و متد داخلی از همان تراکنش استفاده خواهد کرد و اصطلاحا  این تراکنش به صورت Ambient Transaction می باشد. 


واسط ITransaction

    public interface ITransaction : IDisposable
    {
        void Commit();
        void Rollback();
    }

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


واسط IUnitOfWork

    public interface IUnitOfWork : IDisposable
    {
        ...

        ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Snapshot);
        ITransaction Transaction { get; }
        IDbConnection Connection { get; }
        bool HasTransaction { get; }
    }

اعضای جدید واسط IUnitOfWork کاملا مشخص هستند.

پیاده سازی واسط ITransaction، توسط یک Nested Type در دل کلاس DbContextBase انجام می‌گیرد.

 public abstract class DbContextBase : DbContext
    {
        ...

        #region Fields
        
        private ITransaction _currenTransaction;

        #endregion

        #region NestedTypes

        private class DbContextTransactionAdapter : ITransaction
        {
            private DbContextTransaction _transaction;

            public DbContextTransactionAdapter(DbContextTransaction transaction)
            {
                Guard.NotNull(transaction, nameof(transaction));

                _transaction = transaction;
            }

            public void Commit()
            {
                _transaction?.Commit();
            }

            public void Rollback()
            {
                if (_transaction?.UnderlyingTransaction.Connection != null)
                    _transaction.Rollback();
            }

            public void Dispose()
            {
                _transaction?.Dispose();
                _transaction = null;
            }
        }

        #endregion

        #region Public Methods
        ...

        public ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted)
        {
            if (_currenTransaction != null) return _currenTransaction;

            return _currenTransaction = new DbContextTransactionAdapter(Database.BeginTransaction(isolationLevel));
        }
        #endregion

        #region Properties
        ...

        public ITransaction Transaction => _currenTransaction;
        public IDbConnection Connection => Database.Connection;
        public bool HasTransaction => _currenTransaction != null;

        #endregion
}

public class ApplicationDbContext : DbContextBase, IUnitOfWork, ITransientDependency
{
     
}

کلاس DbContextTransactionAdapter همانطور که از نام آن مشخص می‌باشد، پیاده سازی از الگوی Adapter برای وفق دادن DbContextTransaction با واسط ITransaction، می‌باشد. متد BeginTransaction در صورتی که تراکنشی برای وهله جاری DbContext ایجاد نشده باشد، تراکنشی را ایجاد کرده و فیلد currentTransaction_ را نیز مقدار دهی می‌کند. 


TransactionalAttribute

    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
    public sealed class TransactionalAttribute : Attribute
    {
        public IsolationLevel IsolationLevel { get; set; } = IsolationLevel.ReadCommitted;
        public TimeSpan? Timeout { get; set; }
    }

TransactionInterceptor

    public class TransactionInterceptor : ISyncInterceptionBehavior
    {
        private readonly IUnitOfWork _unitOfWork;

        public TransactionInterceptor(IUnitOfWork unitOfWork)
        {
            _unitOfWork = unitOfWork;
        }
        public IMethodInvocationResult Intercept(ISyncMethodInvocation methodInvocation)
        {
            var transactionAttribute = GetTransactionaAttributeOrNull(methodInvocation.InstanceMethodInfo);

            if (transactionAttribute == null || _unitOfWork.HasTransaction) return methodInvocation.InvokeNext();

            using (var transaction = _unitOfWork.BeginTransaction(transactionAttribute.IsolationLevel))
            {
                var result = methodInvocation.InvokeNext();

                if (result.Successful)
                    transaction.Commit();
                else
                {
                    transaction.Rollback();
                }

                return result;
            }
        }

        private static TransactionalAttribute GetTransactionaAttributeOrNull(MemberInfo methodInfo)
        {
            var transactionalAttribute =
                ReflectionHelper.GetAttributesOfMemberAndDeclaringType<TransactionalAttribute>(
                    methodInfo
                ).FirstOrDefault();

            return transactionalAttribute;
        }
    }

واسط ISyncInterceptionBehavior، مربوط می‌شود به کتابخانه جانبی دیگری که برای AOP توسط تیم StructureMap به نام StructureMap.DynamicInterception ارائه شده‌است. در متد Intercept، ابتدا چک می‌شود که که آیا این متد با TransactionAttribute تزئین شده و طی درخواست جاری برای Context جاری تراکنشی ایجاد نشده باشد؛ سپس تراکنش جدیدی ایجاد شده و بدنه اصلی متد اجرا می‌شود و نهایتا در صورت موفقیت آمیز بودن عملیات، تراکنش مورد نظر Commit می‌شود.

در آخر لازم است این Interceptor در تنظیمات اولیه StructureMap به شکل زیر معرفی شود:

Policies.Interceptors(new DynamicProxyInterceptorPolicy(
                type => typeof(IApplicationService).IsAssignableFrom(type), typeof(AuthorizationInterceptor), typeof(TransactionInterceptor), typeof(ValidationInterceptor)));



نکته: فرض کنید در بدنه اکشن متد یک کنترلر ASP.NET MVC یا ASP.NET Core، دو متد تراکنشی فراخوانی شود؛ در این صورت شاید لازم باشد که این دو متد طی یک تراکنش واحد به جای تراکنش‌های مجزا، اجرا شوند؛ بنابراین نیاز است از الگوی Transaction Per Request استفاده شود. برای این کار می‌توان یک ActionFilterAttribute سفارشی ایجاد کرد که ایجاد کننده تراکنش باشد و متدهای داخلی که هر کدام جدا تراکنشی بودند، نیز از تراکنش ایجاد شده استفاده کنند.

مطالب
بررسی مباحث Streaming در ‎‎‎.NET - مقدمه

هدف بررسی کامل مباحث Streaming در دات نت فریمورک می‌باشد.

Stream چیست؟

دنباله‌ای از بایت‌ها که می‌توان آنها را از یک backing store (انبار پشتیبان) خواند یا در آن نوشت.

Backing Store 

یک رسانه ذخیره سازی از جمله Disk-Drive، Memory و Network Location می‌باشد که به عنوان منبع یا مقصدی برای خواندن و نوشتن بایت‌ها به صورت دنباله‌ای، می‌توان از آن استفاده کرد.


زمانی که قرار است داده ذخیره شده به صورت Stream مصرف شود، مزیت مقیاس پذیری را نیز خواهید داشت. لذا لازم نیست با مشکل محدودیت حافظه نیز درگیر شوید.

آشنایی با معماری Streaming در دات نت

Streaming در دات نت، توسط سه مفهوم: backing store، decorators و adapters در برگرفته شده است. 

کلاسی به نام Stream در دات نت، برای ارائه یکسری متد مشترک برای Reading، Writing و Positioning در نظر گرفته شده است که همچنین کلاس پایه Backing Store Streams و Decorator Streams نیز می‌باشد. 

اعضای کلاس Stream را می‌توان به شکل زیر گروه بندی کرد:


در نظر داشته باشید که Stream ها، دارای اشاره گری به مکان جاری تحت عنوان Pointer نیز می‌باشند. مقدار پیش فرض آن «صفر» می‌باشد و زمانی که شروع به خواندن از Stream کنید، این خواندن از مکانی شروع می‌شود که Pointer به آنجا اشاره می‌کند. به شکل زیر توجه کنید:

اگر قرار باشد 3 بایت اول خوانده شود، لذا حالت زیر را خواهیم داشت: 

همانطور که مشخص است، Pointer مربوط به Stream به اولین خانه‌ای اشاره می‌کند که در Read‌های بعدی قرار است خوانده شود. در نهایت با خواندن دو بایت دیگر، حالت زیر را خواهیم داشت:

برای Reading و Writing متدهای زیر در کلاس System.IO.Stream در نظر گرفته شده‌اند:

(Read(byte[] buffer,int offset,int count

buffer: آرایه‌ای از بایت‌ها برای نگهداری داده‌ی خوانده شده از Stream
offset: برخلاف تصور، اندیسی است که مکان شروع ذخیره سازی در buffer را مشخص می‌کند و نه مکان شروع خواندن از Stream
count: بیشترین تعداد بایت برای خواندن از Stream می‌باشد. با توجه به اینکه ممکن است به انتهای Stream رسیده باشیم یا اینکه در شرایطی مثلا در Network Streamها چه بسا خود Stream تصمیم بگیرد تعداد بایت کمتری از این مقدار Count را برای ما ارائه دهد. از این رو همیشه مقداری که برای Count مشخص می‌کنید همان مقداری نیست که متد Read برای شما برگشت خواهد داد.
return: تعداد بایت‌هایی که خوانده شده است یا اگر به انتهای Stream رسیده باشیم «0» برگشت خواهد داد. از این رو تکه کد زیر برای خواندن کل داده به یکباره، قابل اطمینان نخواهد بود. 

byte[] dataToRead=new byte[stream.Length];
int bytesRead=stream.Read(dataToRead,0,dataToRead.Length);

راه حل جایگزین می‌تواند به شکل زیر باشد:

static byte[] ReadBytes(Stream stream)
    {
        // dataToRead will hold the data read from the stream
        byte[] dataToRead = new byte[stream.Length];
        //this is the total number of bytes read. this will be incremented 
        //and eventually will equal the bytes size held by the stream
        int totalBytesRead = 0;
        //this is the number of bytes read in each iteration (i.e. chunk size)
        int chunkBytesRead = 1;
        while (totalBytesRead < dataToRead.Length && chunkBytesRead > 0)
        {
            chunkBytesRead = stream.Read(dataToRead, totalBytesRead, 
                dataToRead.Length - totalBytesRead);
            totalBytesRead = totalBytesRead + chunkBytesRead;
        }
        return dataToRead;
    }
در کد بالا تا زمانیکه مجموع تعداد بایت‌های خوانده شده کوچکتر از طول Stream باشد و همچنین به انتهای Stream نرسیده باشیم (chunkBytesRead>0)، عملیات خواندن انجام خواهد گرفت. خوشبختانه در کلاس BinaryReader متدی برای این کار در نظر گرفته شده است که در آینده با آنها بیشتر آشنا خواهیم شد.
byte[] data = new BinaryReader (s).ReadBytes (1000);
ReadByte

return: یک بایت را از مکان فعلی که Pointer به آن اشاره می‌کند، می‌خواند. اگر خروجی «-1» باشد، به انتهای Stream رسیده اید.
برخلاف انتظار، خروجی این متد از نوع int می‌باشد؛ چرا که لازم است «-1» را نیز در برگیرد.

CanRead
ممکن است یک Stream از عملیات خواندن پشتیبانی نکند؛ این محدودیت از طریق حالت جاری Backing Store تعیین می‌شود. برای مثال:

با توجه به حالت FileStream که فقط برای Append کردن وهله سازی شده است، امکان خواندن را نخواهید داشت. بنابراین زمانیکه از کلاس شخص ثالثی برای خواندن از Stream استفاده می‌کنید، به‌صلاح است (به منظور Defensive Programming) که از متد CanRead قبل خواندن بهره ببرید.

(Write(byte[] array,int offset,int count

array: آرایه ای از بایت‌ها که قرار است در Stream درج شوند.
offset: اندیس شروع array برای درج کردن در Stream را مشخص می‌کند.
count: بیشترین تعداد بایتی که از array در Stream درج خواهد شد.

WriteByte

برای درج یک بایت در Stream استفاده می‌شود.

CanWrite

برای تشخص پشتیبانی کردن Stream از عملیات درج کردن مورد استفاده قرار خواهد گرفت.

عملیات Seeking 

با انجام هر یک از عملیات Read  و Write برروی Stream، باعث تغییر مکان Pointer مربوط به آن خواهید شد. در صورتیکه نیاز است به صورت انتخابی مکان خاصی از Stream را برای شروع درج کردن یا خواندن انتخاب کنید، Seeking کمک کننده خواهد بود.

باید توجه داشت که پشتیبانی از این عملیات به backing store مورد استفاده وابسته می‌باشد. از این رو باید دانست که MemoryStream و FileStream از Seeking پشتیبانی کرده ولی در مقابل NetworkStream، PipeStream و همچنین Decorator Streams به غیر از BufferedStream قابلیت Seeking را ندارند. BufferedStream با ایجاد پوششی برروی یک Stream به اصطلاح non-seekable، امکان Seeking درون Buffer داخلی خود را مهیا خواهد کرد.

برای عملیات Seeking نیز اعضایی در کلاس پایه System.IO.Stream در نظر گرفته شده است:

(Seek(long Offset,SeekOrigin origin

برای تنظیم مکان Pointer در Stream استفاده خواهد شد. 

(SetLength(long value

متدی برای تنظیم طول Stream، که اگر value ارسال شده کوچکتر از طول فعلی Stream باشد، آن را کوتاه کرده و در غیر این صورت، Stream موردنظر گسترش خواهد یافت. برای استفاده از این متد، Stream مورد نظر باید قابلیت Writing و Seeking را داشته باشد.

Length

پراپرتی فقط خواندنی که طول Stream را مشخص می‌کند. در صورتیکه Stream مورد نظر Seekable باشد، می‌توان از این پراپرتی بهر برد؛ این بدین معنی است که اگر با یک Stream از نوع non-seekable کار می‌کنید، در صورت استفاده از این خصوصیت، تمام بایت‌های Stream خوانده شده و بعد از قرار گرفتن در  یک buffer (به عنوان مثال در memory)، محاسبه خواهد شد.

Position

پراپرتی برای خواندن یا تنظیم مکان فعلی Pointer مربوط به Stream، می‌باشد. برای استفاده از آن لازم است Stream مورد استفاده Seekable باشد.

CanSeek

مشخص می‌کند که Stream مورد استفاده Seekable  می باشد یا خیر.

تفاوت متد Seek و پراپرتی Position برای عملیات Seeking

به طور خلاصه با استفاده از متد Seek انعطاف پذیری بالایی خواهید داشت. با مقدار دهی پراپرتی Position، این مقدار همیشه نسبت به ابتدای Stream در نظر گرفته خواهد شد (شکل زیر)؛ این در حالی است که با استفاده از متد Seek می‌توان مشخص کرد که مقدار Offset تنظیم شده نسبت به ابتدا، مکان جاری و یا انتهای Stream می‌باشد.

مثال:

        using (FileStream fs = File.Create(@"C:\files\testfile3.txt"))
        {
            // position is 0
            long pos = fs.Position;
            // sets the position to 1
            fs.Position = 1; 
         
            byte[] arrbytes = { 100, 101 };
            //writes the content of arrbytes into current position - which is 1
            fs.Write(arrbytes, 0, arrbytes.Length);
            //position is now 3 as its advanced by write
            pos = fs.Position;
            fs.Position = 0;
            byte[] readdata1 = ReadBytes(fs);
        }
در تکه کد بالا قصد داریم تعدادی بایت را در یک فایل متنی ذخیره کنیم. برای نشان دادن عملیات Seeking، ابتدا Position را با عداد «1» تنظیم کرده‌ایم. با استفاده از متد Write عمل درج بایت‌ها با شروع از مکان اندیس «1» را انجام داده‌ایم. در این لحظه، Position عدد «3» را نشان می‌دهد. حال برای خواندن Stream لازم است Position را با «0» مقدار دهی کنیم تا Pointer دوباره به ابتدای Stream اشاره کند و عملیات خواندن را انجام داده‌ایم. اگر تکه کد بالا را دیباگ کنیم، به نتیجه نشان داده شده در شکل زیر خواهیم رسید:

Closing and Flushing 

کلاس پایه System.IO.Stream اینترفیس IDisposable را پیاده سازی کرده است؛ لذا بهتر است برای آزاد سازی منابع از جمله: file handle در FileStream یا socket handle در NetworkStream، بعد از استفاده، متد Dispose آنها را فراخوانی کنید یا با وهله سازی آنها در بدنه using، این فراخوانی به صورت ضمنی انجام شود. 

نکته: باید توجه کنید که با Close (معادل Dispose) شدن decorator streamها ، backing store stream داخلی آنها نیز Close خواهد شد.

با توجه به اینکه I/O عملیات پرهزینه‌ای می‌باشد، برخی از انواع Stream‌ها به منظور بهبود کارآیی از یک مکانیزم بافر داخلی استفاده می‌کنند. به این شکل که عملیات Write، داده را به جای آنکه درون backing store ذخیره سازی کند، درون این بافر ذخیره سازی خواهد کرد. زمانیکه این بافر پر شود یا به صورت صریح متدهای Flush یا Close فراخوانی شده باشند، داده موجود در بافر درون backing store ذخیره خواهد شد. در نتیجه عملیات Read هم می‌تواند به بخشی از داده اصلی که هم اکنون درون بافر می‌باشد، دسترسی سریع‌تری داشته باشد. به عنوان مثال FileStream از این مکانیزم داخلی برخوردار است. سایز پیش فرض این بافر ‏‏4KB (قابل تنظیم است) می‌باشد. برای سایر مواردی که این امکان برایشان وجود ندارد، می‌توان از BufferedStream برای Decorate کردن Stream مورد نظر خود استفاده کرد.

نکته: به صورت پیش فرض، Streamها thread-safe نیستند و امکان خواندن و نوشتن همزمان توسط چند thread برروی یک stream مشترک را نخواهید داشت. برای حل این موضوع، متد استاتیکی در کلاس Stream تحت عنوان Synchronized در نظر گرفته شده است که یک thread-safe wrapper را به برروی stream ورودی در نظر گرفته و آن را به عنوان خروجی برگشت خواهد داد. 

 [HostProtection(SecurityAction.LinkDemand, Synchronization = true)]
    public static Stream Synchronized(Stream stream)
    {
      if (stream == null)
        throw new ArgumentNullException("stream");
      if (stream is Stream.SyncStream)
        return stream;
      return (Stream) new Stream.SyncStream(stream);
    }
مطالب
EF Code First #7

مدیریت روابط بین جداول در EF Code first به کمک Fluent API

EF Code first بجای اتلاف وقت شما با نوشتن فایل‌های XML تهیه نگاشت‌ها یا تنظیم آن‌ها با کد، رویه Convention over configuration را پیشنهاد می‌دهد. همین رویه، جهت مدیریت روابط بین جداول نیز برقرار است. روابط one-to-one، one-to-many، many-to-many و موارد دیگر را بدون یک سطر تنظیم اضافی، صرفا بر اساس یک سری قراردادهای توکار می‌تواند تشخیص داده و اعمال کند. عموما زمانی نیاز به تنظیمات دستی وجود خواهد داشت که قراردادهای توکار رعایت نشوند و یا برای مثال قرار است با یک بانک اطلاعاتی قدیمی از پیش موجود کار کنیم.


مفاهیمی به نام‌های Principal و Dependent

در EF Code first از یک سری واژه‌های خاص جهت بیان ابتدا و انتهای روابط استفاده شده است که عدم آشنایی با آن‌ها درک خطاهای حاصل را مشکل می‌کند:
الف) Principal : طرفی از رابطه است که ابتدا در بانک اطلاعاتی ذخیره خواهد شد.
ب) Dependent : طرفی از رابطه است که پس از ثبت Principal در بانک اطلاعاتی ذخیره می‌شود.
Principal می‌تواند بدون نیاز به Dependent وجود داشته باشد. وجود Dependent بدون Principal ممکن نیست زیرا ارتباط بین این دو توسط یک کلید خارجی تعریف می‌شود.


کدهای مثال مدیریت روابط بین جداول

در دنیای واقعی، همه‌ی مثال‌ها به مدل بلاگ و مطالب آن ختم نمی‌شوند. به همین جهت نیاز است یک مدل نسبتا پیچیده‌تر را در اینجا بررسی کنیم. در ادامه کدهای کامل مثال جاری را مشاهده خواهید کرد:

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Customer
{
public int Id { set; get; }
public string FirstName { set; get; }
public string LastName { set; get; }

public virtual AlimentaryHabits AlimentaryHabits { set; get; }
public virtual ICollection<CustomerAlias> Aliases { get; set; }
public virtual ICollection<Role> Roles { get; set; }
public virtual Address Address { get; set; }
}
}

namespace EF_Sample35.Models
{
public class CustomerAlias
{
public int Id { get; set; }
public string Aka { get; set; }

public virtual Customer Customer { get; set; }
}
}

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Role
{
public int Id { set; get; }
public string Name { set; get; }

public virtual ICollection<Customer> Customers { set; get; }
}
}

namespace EF_Sample35.Models
{
public class AlimentaryHabits
{
public int Id { get; set; }
public bool LikesPasta { get; set; }
public bool LikesPizza { get; set; }
public int AverageDailyCalories { get; set; }

public virtual Customer Customer { get; set; }
}
}

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Address
{
public int Id { set; get; }
public string City { set; get; }
public string StreetAddress { set; get; }
public string PostalCode { set; get; }

public virtual ICollection<Customer> Customers { set; get; }
}
}



همچنین تعاریف نگاشت‌های برنامه نیز مطابق کد‌های زیر است:

using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;

namespace EF_Sample35.Mappings
{
public class CustomerAliasConfig : EntityTypeConfiguration<CustomerAlias>
{
public CustomerAliasConfig()
{
// one-to-many
this.HasRequired(x => x.Customer)
.WithMany(x => x.Aliases)
.WillCascadeOnDelete();
}
}
}

using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;

namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// one-to-one
this.HasOptional(x => x.AlimentaryHabits)
.WithRequired(x => x.Customer)
.WillCascadeOnDelete();

// many-to-many
this.HasMany(p => p.Roles)
.WithMany(t => t.Customers)
.Map(mc =>
{
mc.ToTable("RolesJoinCustomers");
mc.MapLeftKey("RoleId");
mc.MapRightKey("CustomerId");
});

// many-to-one
this.HasOptional(x => x.Address)
.WithMany(x => x.Customers)
.WillCascadeOnDelete();
}
}
}


به همراه Context زیر:

using System.Data.Entity;
using System.Data.Entity.Migrations;
using EF_Sample35.Mappings;
using EF_Sample35.Models;

namespace EF_Sample35.DataLayer
{
public class Sample35Context : DbContext
{
public DbSet<AlimentaryHabits> AlimentaryHabits { set; get; }
public DbSet<Customer> Customers { set; get; }

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new CustomerConfig());
modelBuilder.Configurations.Add(new CustomerAliasConfig());

base.OnModelCreating(modelBuilder);
}
}

public class Configuration : DbMigrationsConfiguration<Sample35Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}

protected override void Seed(Sample35Context context)
{
base.Seed(context);
}
}
}


که نهایتا منجر به تولید چنین ساختاری در بانک اطلاعاتی می‌گردد:



توضیحات کامل کدهای فوق:

تنظیمات روابط one-to-one و یا one-to-zero

زمانیکه رابطه‌ای 0..1 و یا 1..1 است، مطابق قراردادهای توکار EF Code first تنها کافی است یک navigation property را که بیانگر ارجاعی است به شیء دیگر، تعریف کنیم (در هر دو طرف رابطه).
برای مثال در مدل‌های فوق یک مشتری که در حین ثبت اطلاعات اصلی او، «ممکن است» اطلاعات جانبی دیگری (AlimentaryHabits) نیز از او تنها در طی یک رکورد، دریافت شود. قصد هم نداریم یک ComplexType را تعریف کنیم. نیاز است جدول AlimentaryHabits جداگانه وجود داشته باشد.

namespace EF_Sample35.Models
{
public class Customer
{
// ...
public virtual AlimentaryHabits AlimentaryHabits { set; get; }
}
}

namespace EF_Sample35.Models
{
public class AlimentaryHabits
{
// ...
public virtual Customer Customer { get; set; }
}
}

در اینجا خواص virtual تعریف شده در دو طرف رابطه، به EF خواهد گفت که رابطه‌ای، 1:1 برقرار است. در این حالت اگر برنامه را اجرا کنیم، به خطای زیر برخواهیم خورد:

Unable to determine the principal end of an association between 
the types 'EF_Sample35.Models.Customer' and 'EF_Sample35.Models.AlimentaryHabits'.
The principal end of this association must be explicitly configured using either
the relationship fluent API or data annotations.

EF تشخیص داده است که رابطه 1:1 برقرار است؛ اما با قاطعیت نمی‌تواند طرف Principal را تعیین کند. بنابراین باید اندکی به او کمک کرد:

using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;

namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// one-to-one
this.HasOptional(x => x.AlimentaryHabits)
.WithRequired(x => x.Customer)
.WillCascadeOnDelete();
}
}
}


همانطور که ملاحظه می‌کنید در اینجا توسط متد WithRequired طرف Principal و توسط متد HasOptional، طرف Dependent تعیین شده است. به این ترتیب EF می‌توان یک رابطه 1:1 را تشکیل دهید.
توسط متد WillCascadeOnDelete هم مشخص می‌کنیم که اگر Principal حذف شد، لطفا Dependent را به صورت خودکار حذف کن.

توضیحات ساختار جداول تشکیل شده:
هر دو جدول با همان خواص اصلی که در دو کلاس وجود دارند، تشکیل شده‌اند.
فیلد Id جدول AlimentaryHabits اینبار دیگر Identity نیست. اگر به تعریف قید FK_AlimentaryHabits_Customers_Id دقت کنیم، در اینجا مشخص است که فیلد Id جدول AlimentaryHabits، به فیلد Id جدول مشتری‌ها متصل شده است (یعنی در آن واحد هم primary key است و هم foreign key). به همین جهت به این روش one-to-one association with shared primary key هم گفته می‌شود (کلید اصلی جدول مشتری با جدول AlimentaryHabits به اشتراک گذاشته شده است).


تنظیمات روابط one-to-many

برای مثال همان مشتری فوق را درنظر بگیرید که دارای تعدادی نام مستعار است:

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Customer
{
// ...
public virtual ICollection<CustomerAlias> Aliases { get; set; }
}
}

namespace EF_Sample35.Models
{
public class CustomerAlias
{
// ...
public virtual Customer Customer { get; set; }
}
}

همین میزان تنظیم کفایت می‌کند و نیازی به استفاده از Fluent API برای معرفی روابط نیست.
در طرف Principal، یک مجموعه یا لیستی از Dependent وجود دارد. در Dependent هم یک navigation property معرف طرف Principal اضافه شده است.
جدول CustomerAlias اضافه شده، توسط یک کلید خارجی به جدول مشتری مرتبط می‌شود.

سؤال: اگر در اینجا نیز بخواهیم CascadeOnDelete را اعمال کنیم، چه باید کرد؟
پاسخ: جهت سفارشی سازی نحوه تعاریف روابط حتما نیاز به استفاده از Fluent API به نحو زیر می‌باشد:

using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;

namespace EF_Sample35.Mappings
{
public class CustomerAliasConfig : EntityTypeConfiguration<CustomerAlias>
{
public CustomerAliasConfig()
{
// one-to-many
this.HasRequired(x => x.Customer)
.WithMany(x => x.Aliases)
.WillCascadeOnDelete();
}
}
}

اینکار را باید در کلاس تنظیمات CustomerAlias انجام داد تا بتوان Principal را توسط متد HasRequired به Customer و سپس dependent را به کمک متد WithMany مشخص کرد. در ادامه می‌توان متد WillCascadeOnDelete یا هر تنظیم سفارشی دیگری را نیز اعمال نمود.
متد HasRequired سبب خواهد شد فیلد Customer_Id، به صورت not null در سمت بانک اطلاعاتی تعریف شود؛ متد HasOptional عکس آن است.


تنظیمات روابط many-to-many

برای تنظیم روابط many-to-many تنها کافی است دو سر رابطه ارجاعاتی را به یکدیگر توسط یک لیست یا مجموعه داشته باشند:

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Role
{
// ...
public virtual ICollection<Customer> Customers { set; get; }
}
}

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Customer
{
// ...
public virtual ICollection<Role> Roles { get; set; }
}
}

همانطور که مشاهده می‌کنید، یک مشتری می‌تواند چندین نقش داشته باشد و هر نقش می‌تواند به چندین مشتری منتسب شود.
اگر برنامه را به این ترتیب اجرا کنیم، به صورت خودکار یک رابطه many-to-many تشکیل خواهد شد (بدون نیاز به تنظیمات نگاشت‌های آن). نکته جالب آن تشکیل خودکار جدول ارتباط دهنده واسط یا اصطلاحا join-table می‌باشد:

CREATE TABLE [dbo].[RolesJoinCustomers](
[RoleId] [int] NOT NULL,
[CustomerId] [int] NOT NULL,
)

سؤال: نام‌های خودکار استفاده شده را می‌خواهیم تغییر دهیم. چکار باید کرد؟
پاسخ: اگر بانک اطلاعاتی برای بار اول است که توسط این روش تولید می‌شود شاید این پیش فرض‌ها اهمیتی نداشته باشد و نسبتا هم مناسب هستند. اما اگر قرار باشد از یک بانک اطلاعاتی موجود که امکان تغییر نام فیلدها و جداول آن وجود ندارد استفاده کنیم، نیاز به سفارشی سازی تعاریف نگاشت‌ها به کمک Fluent API خواهیم داشت:

using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;

namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// many-to-many
this.HasMany(p => p.Roles)
.WithMany(t => t.Customers)
.Map(mc =>
{
mc.ToTable("RolesJoinCustomers");
mc.MapLeftKey("RoleId");
mc.MapRightKey("CustomerId");
});
}
}
}


تنظیمات روابط many-to-one

در تکمیل مدل‌های مثال جاری، به دو کلاس زیر خواهیم رسید. در اینجا تنها در کلاس مشتری است که ارجاعی به کلاس آدرس او وجود دارد. در کلاس آدرس، یک navigation property همانند حالت 1:1 تعریف نشده است:

namespace EF_Sample35.Models
{
public class Address
{
public int Id { set; get; }
public string City { set; get; }
public string StreetAddress { set; get; }
public string PostalCode { set; get; }
}
}

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Customer
{
// …
public virtual Address Address { get; set; }
}
}

این رابطه توسط EF Code first به صورت خودکار به یک رابطه many-to-one تفسیر خواهد شد و نیازی به تنظیمات خاصی ندارد.
زمانیکه جداول برنامه تشکیل شوند، جدول Addresses موجودیتی مستقل خواهد داشت و جدول مشتری با یک فیلد به نام Address_Id به جدول آدرس‌ها متصل می‌گردد. این فیلد نال پذیر است؛ به عبارتی ذکر آدرس مشتری الزامی نیست.
اگر نیاز بود این تعاریف نیز توسط Fluent API سفارشی شوند، باید خاصیت public virtual ICollection<Customer> Customers به کلاس Address نیز اضافه شود تا بتوان رابطه زیر را توسط کدهای برنامه تعریف کرد:

using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;

namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// many-to-one
this.HasOptional(x => x.Address)
.WithMany(x => x.Customers)
.WillCascadeOnDelete();
}
}
}

متد HasOptional سبب می‌شود تا فیلد Address_Id اضافه شده به جدول مشتری‌ها، null پذیر شود.

نظرات مطالب
C# 6 - The nameof Operator
یک نکته‌ی تکمیلی: تکامل اپراتور nameof در C# 12.0

همانطور که در این مطلب مشاهده کردید، اپراتور nameof، روشی بسیار مفید جهت دسترسی به نام متغیرها، نوع‌ها و یا اعضای یک کلاس است. در C# 12، این ویژگی اندکی بهبود یافته‌است و امکان دسترسی به اطلاعات اعضای یک کلاس را هم دارد:
public class NameofClass
{
    public string SomeProperty { get; set; }

    // Now legal with C# 12
    // would show "Length" on the console
    public const string NameOfSomePropertyLength = nameof(SomeProperty.Length); 
    
    public static int StaticField;
    public const string NameOfStaticFieldMinValue  = nameof(StaticField.MinValue);

    [Description($"String {nameof(SomeProperty.Length)}")]
    public int StringLength(string s)
    {
        return s.Length;
    }
}
در این مثال، اگر سعی کنیم مقدار NameOfSomePropertyLength را در کنسول نمایش دهیم، عبارت Length ظاهر خواهد شد. تا پیش از C# 12 برای دسترسی به یک چنین قابلیتی نیاز به نمونه سازی و تولید شیءای از کلاس NameofClass فوق وجود داشت تا بتوان اپراتور nameof را به خواص آن اعمال کرد. این محدودیت در C# 12 برطرف شده‌است.

همچنین همانطور که مشاهده می‌کنید، امکان دسترسی به اطلاعات فیلدهای استاتیک و یا بکارگیری این قابلیت در Attributes هم میسر شده‌است.