مطالب
Query Options در پروتکل OData
در قسمت قبل  با OData به صورت مختصر آشنا شدیم. در این قسمت به امکانات توکار OData و جزئیات query options پرداخته و همچنین قابلیت‌های امنیتی این پروتکل را بررسی مینماییم.
در قسمت قبلی، config مربوط به OData و همچنین Controller و Crud مربوط به آن entity پیاده سازی شد. در این قسمت ابتدا سه موجودیت را به نام‌های Product ، Category و همچنین Supplier، به صورت زیر تعریف مینماییم:

به این صورت مدل‌های خود را تعریف کرده و طبق مقاله‌ی قبلی، Controller‌های هر یک را پیاده سازی نمایید:

public class Supplier
{
    [Key]
    public string Key {get; set; }
    public string Name { get; set; }
}
public class Category
{
    public int ID { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Product> Products { get; set; }
}

public class Product
{
    public int ID { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }

    [ForeignKey("Category")]
    public int CategoryId { get; set; }
    public Category Category { get; set; }

    [ForeignKey("Supplier")]
    public string SupplierId { get; set; }
    public virtual Supplier Supplier { get; set; }
}

پکیج Microsoft.AspNet.OData به تازگی ورژن 6 آن به صورت رسمی منتشر شده و شامل تغییراتی نسبت به نسخه‌ی قبلی آن است. اولین نکته‌ی حائز اهمیت، Config آن است که به صورت زیر تغییر کرده و باید Option‌های مورد نیاز، کانفیگ شوند. در این نسخه DI نیز به Odata اضافه شده است:

public static void Register(HttpConfiguration config)
        {
            ODataModelBuilder odataModelBuilder = new ODataConventionModelBuilder();

            var product = odataModelBuilder.EntitySet<Product>("Products");
            var category = odataModelBuilder.EntitySet<Category>("Categories");
            var supplier = odataModelBuilder.EntitySet<Supplier>("Suppliers");

            var edmModel = odataModelBuilder.GetEdmModel();

            supplier.EntityType.Ignore(c => c.Name);

            config.Select(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.MaxTop(25);
            config.OrderBy(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.Count(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.Expand(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.Filter(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.Count(System.Web.OData.Query.QueryOptionSetting.Allowed);

            //config.MapODataServiceRoute("ODataRoute", "odata", edmModel); // کانفیگ به صورت معمولی
           config.MapODataServiceRoute("ODataRoute", "odata",
                builder =>
                {
                    builder.AddService(ServiceLifetime.Singleton, sp => edmModel);
                    builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefault());
                });
}
باید همه‌ی Query option‌هایی را که به آنها نیاز داریم، معرفی نماییم و فرض کنید که ProductsController و متد Get آن بدین صورت پیاده سازی شده باشد:
[EnableQuery]
        public IQueryable<Product> Get()
        {
            return new List<Product>
            {
                new Product { Id = 1, Name = "name 1", Price = 11, Category = new Category {Id =1, Name = "Cat1" } },
                new Product { Id = 2, Name = "name 2", Price = 12, Category = new Category {Id =2, Name = "Cat2" } },
                new Product { Id = 3, Name = "name 3", Price = 13, Category = new Category {Id =3, Name = "Cat3" } },
                new Product { Id = 4, Name = "name 4", Price = 14, Category = new Category {Id =4, Name = "Cat4" } },
            }.AsQueryable(); ;
        }
در این پروتکل به صورت توکار، optionهای زیر قابل استفاده است:

 Description Option 
 بسط دادن موجودیت مرتبط  $expand
 فیلتر کردن نتیجه، بر اساس شرط‌های Boolean ی  $filter
 فرمان به سرور که تعداد رکورد‌های بازگشتی را نیز نمایش دهد(مناسب برای پیاده سازی server-side pagging )  $count
 مرتب کردن نتیجه‌ی بازگشتی  $orderby
 select زدن روی پراپرتی‌های درخواستی  $select
 پرش کردن از اولین رکورد به اندازه‌ی n عدد  $skip
 فقط بازگرداندن n رکورد اول  $top
کوئری‌های زیر را در نظر بگیرید:

 در کوئری اول، فقط فیلد‌های Id,Name از Products برگشت داده خواهند شد و در کوئری دوم، از 2 رکورد اول، صرفنظر می‌شود و از بقیه‌ی آنها، فقط 3 رکورد بازگشت داده میشود:
/odata/Products?$select=Id,Name
/odata/Products?$top=3&$skip=2
/odata/Products?$count=true
در پاسخ کوئری فوق، تعداد رکورد‌های بازگشتی نیز نمایش داده میشوند:
{@odata.context: "http://localhost:4516/odata/$metadata#Products", @odata.count: 4,…}
@odata.context:"http://localhost:4516/odata/$metadata#Products"
@odata.count:4
value:[{Id: 1, Name: "name 1", Price: 11, SupplierId: 0, CategoryId: 0},…]
0:{Id: 1, Name: "name 1", Price: 11, SupplierId: 0, CategoryId: 0}
1:{Id: 2, Name: "name 2", Price: 12, SupplierId: 0, CategoryId: 0}
2:{Id: 3, Name: "name 3", Price: 13, SupplierId: 0, CategoryId: 0}
3:{Id: 4, Name: "name 4", Price: 14, SupplierId: 0, CategoryId: 0}
در response، این مقادیر به همراه تعداد رکورد بازگشتی، نمایش داده میشوند که برای پیاده سازی paging مناسب است.
/odata/Products?$filter=Id eq 1
در کوئری فوق eq مخفف equal و به معنای برابر است و بجای آن میتوان از gt به معنای بزرگتر و lt به معنای کوچکتر نیز استفاده کرد:
/odata/Products?$filter=Id gt 1 and Id lt 3
/odata/Products?$orderby=Id desc
در کوئری فوق نیز به صورت واضح، بر روی فیلد Id، مرتب سازی به صورت نزولی خواهد بود و در صورت وجود نداشتن کلمه کلیدی desc، به صورت صعودی خواهد بود.
بسط دادن موجودیت‌های دیگر نیز بدین شکل زیر میباشد:
/odata/Products?$expand=Category
برای اینکه چندین موجودیت دیگر نیز بسط داده شوند، اینگونه رفتار مینماییم:
/odata/Products?$expand=Category,Supplier
برای اینکه به صورت عمیق به موجودیت‌های دیگر بسط داده شود، بصورت زیر:
/odata/Categories(1)?$expand=Products/Supplier
و برای اینکه حداکثر تعداد رکورد بازگشتی را مشخص نماییم:
[EnableQuery(PageSize = 10)]


محدود کردن Query Options
به صورت زیر میتوانیم فقط option‌های دلخواه را فراخوانی نماییم. مثلا در اینجا فقط اجازه‌ی Skip و Top داده شده است و بطور مثال Select قابل فراخوانی نیست:
[EnableQuery (AllowedQueryOptions= AllowedQueryOptions.Skip | AllowedQueryOptions.Top)]
برای اینکه فقط اجازه‌ی logical function زیر را بدهیم (فقط eq):
[EnableQuery(AllowedLogicalOperators=AllowedLogicalOperators.Equal)]
برای اینکه Property خاصی در edm قابلیت نمایش نباشد، در config بطور مثال Price را ignore مینماییم:
var product = odataModelBuilder.EntitySet<Product>("Products");
product.EntityType.Ignore(e => e.Price);
برای اینکه فقط بر روی فیلد‌های خاصی بتوان از orderby استفاده نمود:
[EnableQuery(AllowedOrderByProperties = "Id,Name")]
یک option به نام value$ برای بازگرداندن تنها آن رکورد مورد نظر، به صورت مجزا میباشد. برای اینکار بطور مثال متد زیر را به کنترلر خود اضافه کنید:
public System.Web.Http.IHttpActionResult GetName(int key)
        {
            Product product = Get().Single(c => c.Id == key);            
            return Ok(product.Name);
        }
/odata/Products(1)/Name/$value
و حاصل کوئری فوق، مقداری بطور مثال برابر زیر خواهد بود و نه به صورت convention پاسخ‌های OData، فرمت بازگشتی "text/plain" خواهد بود و نه json:
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
DataServiceVersion: 3.0
Content-Length: 3

Ali

Attribute Convention
هایی هم برای اعتبارسنجی پراپرتی‌ها موجود است که نام آن‌ها واضح تعریف شده‌اند:
 Description  Attribute
 اجازه‌ی فیلتر زدن بر روی آن پراپرتی داده نخواهد شد  NotFilterable
 اجازه‌ی مرتب کردن بر روی آن پراپرتی داده نخواهد شد  NotSortable
 اجازه‌ی select زدن بر روی آن پراپرتی داده نمیشود  NotNavigable
 اجازه‌ی شمارش دهی بر روی آن Collection داده نمیشود  NotCountable
 اجازه‌ی بسط دادن آن Collection داده نمیشود  NotExpandable

و همچنین [AutoExpand] به صورت اتوماتیک آن موجودیت مورد نظر را بسط میدهد.

بطور مثال کد‌های زیر را در مدل خود میتوانید مشاهده نمائید:

public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }

        [NotFilterable, NotSortable]
        public decimal Price { get; set; }

        [ForeignKey(nameof(SupplierId))]
        [NotNavigable]
        public virtual Supplier Supplier { get; set; }
        public int SupplierId { get; set; }


        [ForeignKey(nameof(CategoryId))]
        public virtual Category Category { get; set; }
        public int CategoryId { get; set; }

        [NotExpandable]
        public virtual ICollection<TestEntity> TestEnities { get; set; }
    }

فرض کنید پراپرتی زیر را به مدل خود اضافه کرده اید

public DateTimeOffset CreatedOn { get; set; }

حال کوئری زیر را برای فیلتر زدن، بر روی آن در اختیار داریم:

/odata/Products?$filter=year(CreatedOn) eq 2016

در اینجا فقط Product هایی بازگردانده میشوند که در سال 2016 ثبت شده‌اند:

/odata/Products?$filter=CreatedOn lt cast(2017-04-01T04:11:31%2B08:00,Edm.DateTimeOffset)

کوئری فوق تاریخ مورد نظر را Cast کرده و همه‌ی Product هایی را که قبل از این تاریخ ثبت شده‌اند، باز می‌گرداند.

Nested Filter In Expand

/odata/Categories?$expand=Products($filter=Id gt 1 and Id lt 5)

همه‌ی Category‌ها به علاوه بسط دادن Product هایشان، در صورتیکه Id آنها بیشتر از 1 باشد

و یا حتی بر روی موجودیت بسط داده شده، select زده شود:

/odata/Categories?$expand=Products($select=Id,Name)


Custom Attribute

ضمنا به سادگی میتوان اتریبیوت سفارشی نوشت:

public class MyEnableQueryAttribute : EnableQueryAttribute
{
    public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
    {
       // Don't apply Skip and Top.
       var ignoreQueryOptions = AllowedQueryOptions.Skip | AllowedQueryOptions.Top;
       return queryOptions.ApplyTo(queryable, ignoreQueryOptions);
    }
}

روی هر متدی از کنترلر خود که اتریبیوت [MyEnableQuery] را قرار دهید، دیگر قابلیت Skip, Top را نخواهد داشت.

Dependency Injection در آخرین نسخه‌ی OData اضافه شده است. بطور پیشفرض OData بصورت case-sensitive رفتار میکند. برای تغییر دادن آن در نسخه‌های قدیمی، Extension Methodی به نام EnableCaseSensitive وجود داشت. اما در نسخه‌ی جدید شما میتوانید پیاده سازی خاص خود را از هر کدام از بخش‌های OData داشته باشید و با استفاده از تزریق وابستگی، آن را به config برنامه‌ی خود اضافه کنید؛ برای مثال:

 public class CaseInsensitiveResolver : ODataUriResolver
    {
        private bool _enableCaseInsensitive;

        public override bool EnableCaseInsensitive
        {
            get { return true; }
            set { _enableCaseInsensitive = value; }
        }
    }

اینجا پیاده سازی از ODataUriResolver انجام شده و متد EnableCaseInsensitive به صورت جدیدی override و در حالت default مقدار true را برمیگرداند.

حال به صورت زیر آن را می‌توان به وابستگی‌های config برنامه، اضافه نمود:

config.MapODataServiceRoute("ODataRoute", "odata",
                builder =>
                {
                    builder.AddService(ServiceLifetime.Singleton, sp => edmModel);
                    builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefault());
                    builder.AddService<ODataUriResolver>(ServiceLifetime.Singleton, sp => new CaseInsensitiveResolver()); // how enable case sensitive
                });

در قسمت بعدی به Action‌ها و Function‌ها در OData میپردازیم.

مطالب
اعمال تغییرات سفارشی به ویژگی AutoMapping در Fluent NHibernate

با کمک Fluent NHibernate می‌توان نگاشت‌ها را به دو صورت خودکار و یا دستی تعریف کرد. در حالت خودکار، روابط بین کلاس‌ها بررسی شده و بدون نیاز به تعریف هیچگونه ویژگی (attribute) خاصی بر روی فیلدها، امکان تشخیص خودکار حالت‌های کلید خارجی، روابط یک به چند، چند به چند و امثال آن وجود دارد. یا اگر نیاز باشد تا اسکریپت تولیدی جهت به روز رسانی بانک اطلاعاتی، طول خاصی را به فیلدی اعمال کند می‌توان از ویژگی‌های NHibernate validator استفاده کرد؛ مانند تعریف طول و نال نبودن یک فیلد که علاوه بر بکارگیری اطلاعات آن در حین تعیین اعتبار ورودی دریافتی، بر روی نحوه‌ی به روز رسانی بانک اطلاعاتی هم تاثیر گذار است:
public class Product
{
public virtual int Id { set; get; }

[Length(120)]
[NotNullNotEmpty]
public virtual string Name { get; set; }

public virtual decimal UnitPrice { get; set; }
}
این نگاشت خودکار یا AutoMapping ،‌ تقریبا در 90 درصد موارد کافی است. فیلد Id را بر اساس یک سری پیش فرض‌هایی که این مورد هم قابل تنظیم است به صورت primary key تعریف می‌کند، طول فیلدها و نحوه‌ی پذیرفتن نال آن‌ها، از ویژگی‌های NHibernate validator گرفته می‌شود و روابط بین کلاس‌ها به صورت خودکار به روابط یک به چند و مانند آن ترجمه می‌شود و نیازی نیست تا کلاسی جداگانه را جهت مشخص سازی صریح این موارد تهیه کرد، یا ویژگی مشخص کننده‌ی دیگری را به فیلدها افزود. اما اگر برای مثال بخواهیم در این کلاس فیلد Name را به صورت Unique معرفی کنیم چه باید کرد؟ به عبارتی تمام آنچه‌ که ویژگی AutoMapping در Fluent NHibernate انجام می‌دهد، بسیار هم عالی؛ اما فقط می‌خواهیم مقادیر یک فیلد منحصربفرد باشد. برای این منظور اینترفیس IAutoMappingOverride تدارک دیده شده است:
public class ProductCustomMappings : IAutoMappingOverride<Product>
{
public void Override(AutoMapping<Product> mapping)
{
mapping.Id(u => u.Id).GeneratedBy.Identity(); //ضروری است
mapping.Map(p => p.Name).Unique();
}
}
در حالت استفاده از اینترفیس IAutoMappingOverride مشخص سازی نحوه‌ی تولید primary key ضروری است و سپس برای نمونه، فیلد Name به صورت منحصربفرد تعریف می‌گردد. در اینجا کل عملیات هنوز از روش AutoMapping پیروی می‌کند اما فیلد Name علاوه بر اعمال ویژگی‌های NHibernate validator، به صورت منحصربفرد نیز معرفی خواهد شد.

مطالب
رمزنگاری و رمزگشایی خودکار خواص مدل‌ها در ASP.NET Core
فرض کنید قصد دارید خاصیت Id مدل مورد استفاده‌ی در یک View را رمزنگاری کنید تا در سمت کلاینت به سادگی قابل تغییر نباشد. همچنین این Id زمانیکه به سمت سرور ارسال شد، به صورت خودکار رمزگشایی شود و بدون نیاز به تغییرات خاصی در کدهای متداول اکشن متدها، اطلاعات نهایی آن قابل استفاده باشند. برای این منظور در ASP.NET Core می‌توان یک Action Result رمزنگاری کننده و یک Model binder رمزگشایی کننده را طراحی کرد.


نیاز به علامتگذاری خواصی که باید رمزنگاری شوند

می‌خواهیم خاصیت یا خاصیت‌های مشخصی، از یک مدل را رمزنگاری شده به سمت کلاینت ارسال کنیم. به همین جهت ویژگی خالی زیر را به پروژه اضافه می‌کنیم تا از آن تنها جهت علامتگذاری این نوع خواص، استفاده کنیم:
using System;

namespace EncryptedModelBinder.Utils
{
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class EncryptedFieldAttribute : Attribute { }
}


رمزنگاری خودکار مدل خروجی از یک اکشن متد

در ادامه کدهای کامل یک ResultFilter را مشاهده می‌کنید که مدل ارسالی به سمت کلاینت را یافته و سپس خواصی از آن‌را که با ویژگی EncryptedField مزین شده‌اند، به صورت خودکار رمزنگاری می‌کند:
namespace EncryptedModelBinder.Utils
{
    public class EncryptedFieldResultFilter : ResultFilterAttribute
    {
        private readonly IProtectionProviderService _protectionProviderService;
        private readonly ILogger<EncryptedFieldResultFilter> _logger;
        private readonly ConcurrentDictionary<Type, bool> _modelsWithEncryptedFieldAttributes = new ConcurrentDictionary<Type, bool>();

        public EncryptedFieldResultFilter(
            IProtectionProviderService protectionProviderService,
            ILogger<EncryptedFieldResultFilter> logger)
        {
            _protectionProviderService = protectionProviderService;
            _logger = logger;
        }

        public override void OnResultExecuting(ResultExecutingContext context)
        {
            var model = context.Result switch
            {
                PageResult pageResult => pageResult.Model, // For Razor pages
                ViewResult viewResult => viewResult.Model, // For MVC Views
                ObjectResult objectResult => objectResult.Value, // For Web API results
                _ => null
            };

            if (model is null)
            {
                return;
            }

            if (typeof(IEnumerable).IsAssignableFrom(model.GetType()))
            {
                foreach (var item in model as IEnumerable)
                {
                    encryptProperties(item);
                }
            }
            else
            {
                encryptProperties(model);
            }
        }

        private void encryptProperties(object model)
        {
            var modelType = model.GetType();
            if (_modelsWithEncryptedFieldAttributes.TryGetValue(modelType, out var hasEncryptedFieldAttribute)
                && !hasEncryptedFieldAttribute)
            {
                return;
            }

            foreach (var property in modelType.GetProperties())
            {
                var attribute = property.GetCustomAttributes(typeof(EncryptedFieldAttribute), false).FirstOrDefault();
                if (attribute == null)
                {
                    continue;
                }

                hasEncryptedFieldAttribute = true;

                var value = property.GetValue(model);
                if (value is null)
                {
                    continue;
                }

                if (value.GetType() != typeof(string))
                {
                    _logger.LogWarning($"[EncryptedField] should be applied to `string` proprties, But type of `{property.DeclaringType}.{property.Name}` is `{property.PropertyType}`.");
                    continue;
                }

                var encryptedData = _protectionProviderService.Encrypt(value.ToString());
                property.SetValue(model, encryptedData);
            }

            _modelsWithEncryptedFieldAttributes.TryAdd(modelType, hasEncryptedFieldAttribute);
        }
    }
}
توضیحات:
- در اینجا برای رمزنگاری از IProtectionProviderService استفاده شده‌است که در بسته‌ی DNTCommon.Web.Core تعریف شده‌است. این سرویس در پشت صحنه از سیستم Data Protection استفاده می‌کند.
- سپس رخ‌داد OnResultExecuting، بازنویسی شده‌است تا بتوان به مدل ارسالی به سمت کلاینت، پیش از ارسال نهایی آن، دسترسی یافت.
- context.Result می‌تواند از نوع PageResult صفحات Razor باشد و یا از نوع ViewResult مدل‌های متداول Viewهای پروژه‌های MVC و یا از نوع ObjectResult که مرتبط است به پروژه‌های Web Api بدون هیچ نوع View سمت سروری. هر کدام از این نوع‌ها، دارای خاصیت مدل هستند که در اینجا قصد بررسی آن‌را داریم.
- پس از مشخص شدن شیء Model، اکنون حلقه‌ای را بر روی خواص آن تشکیل داده و خواصی را که دارای ویژگی EncryptedFieldAttribute هستند، یافته و آن‌ها را رمزنگاری می‌کنیم.

روش اعمال این فیلتر باید به صورت سراسری باشد:
namespace EncryptedModelBinder
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDNTCommonWeb();
            services.AddControllersWithViews(options =>
            {
                options.Filters.Add(typeof(EncryptedFieldResultFilter));
            });
        }
از این پس مدل‌های تمام خروجی‌های ارسالی به سمت کلاینت، بررسی شده و در صورت لزوم، خواص آن‌ها رمزنگاری می‌شود.


رمزگشایی خودکار مدل دریافتی از سمت کلاینت

تا اینجا موفق شدیم خواص ویژه‌ای از مدل‌ها را رمزنگاری کنیم. مرحله‌ی بعد، رمزگشایی خودکار این اطلاعات در سمت سرور است. به همین جهت نیاز داریم تا در سیستم Model Binding پیش‌فرض ASP.NET Core مداخله کرده و منطق سفارشی خود را تزریق کنیم. بنابراین در ابتدا یک IModelBinderProvider سفارشی را تهیه می‌کنیم تا در صورتیکه خاصیت جاری در حال بررسی توسط سیستم Model Binding دارای ویژگی EncryptedFieldAttribute بود، از EncryptedFieldModelBinder برای پردازش آن استفاده کند:
namespace EncryptedModelBinder.Utils
{
    public class EncryptedFieldModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.IsComplexType)
            {
                return null;
            }

            var propName = context.Metadata.PropertyName;
            if (string.IsNullOrWhiteSpace(propName))
            {
                return null;
            }

            var propInfo = context.Metadata.ContainerType.GetProperty(propName);
            if (propInfo == null)
            {
                return null;
            }

            var attribute = propInfo.GetCustomAttributes(typeof(EncryptedFieldAttribute), false).FirstOrDefault();
            if (attribute == null)
            {
                return null;
            }

            return new BinderTypeModelBinder(typeof(EncryptedFieldModelBinder));
        }
    }
}
که این EncryptedFieldModelBinder به صورت زیر تعریف می‌شود:
namespace EncryptedModelBinder.Utils
{
    public class EncryptedFieldModelBinder : IModelBinder
    {
        private readonly IProtectionProviderService _protectionProviderService;

        public EncryptedFieldModelBinder(IProtectionProviderService protectionProviderService)
        {
            _protectionProviderService = protectionProviderService;
        }

        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            var logger = bindingContext.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>();
            var fallbackBinder = new SimpleTypeModelBinder(bindingContext.ModelType, logger);
            var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            if (valueProviderResult == ValueProviderResult.None)
            {
                return fallbackBinder.BindModelAsync(bindingContext);
            }

            bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

            var valueAsString = valueProviderResult.FirstValue;
            if (string.IsNullOrWhiteSpace(valueAsString))
            {
                return fallbackBinder.BindModelAsync(bindingContext);
            }

            var decryptedResult = _protectionProviderService.Decrypt(valueAsString);
            bindingContext.Result = ModelBindingResult.Success(decryptedResult);
            return Task.CompletedTask;
        }
    }
}
در اینجا مقدار ارسالی به سمت سرور به صورت یک رشته دریافت شده و سپس رمزگشایی می‌شود و بجای مقدار فعلی خاصیت، مورد استفاده قرار می‌گیرد. به این ترتیب دیگر نیازی به تغییر کدهای اکشن متدها برای رمزگشایی اطلاعات نیست.

پس از این تعاریف نیاز است EncryptedFieldModelBinderProvider را به صورت زیر به سیستم معرفی کرد:
namespace EncryptedModelBinder
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDNTCommonWeb();
            services.AddControllersWithViews(options =>
            {
                options.ModelBinderProviders.Insert(0, new EncryptedFieldModelBinderProvider());
                options.Filters.Add(typeof(EncryptedFieldResultFilter));
            });
        }


یک مثال

فرض کنید مدل‌های زیر تعریف شده‌اند:
namespace EncryptedModelBinder.Models
{
    public class ProductInputModel
    {
        [EncryptedField]
        public string Id { get; set; }

        [EncryptedField]
        public int Price { get; set; }

        public string Name { get; set; }
    }
}

namespace EncryptedModelBinder.Models
{
    public class ProductViewModel
    {
        [EncryptedField]
        public string Id { get; set; }

        [EncryptedField]
        public int Price { get; set; }

        public string Name { get; set; }
    }
}
که بعضی از خواص آن‌ها با ویژگی EncryptedField مزین شده‌اند.
اکنون کنترلر زیر زمانیکه رندر شود، View متناظر با اکشن متد Index آن، یکسری لینک را به اکشن متد Details، جهت مشاهده‌ی جزئیات محصول، تولید می‌کند. همچنین اکشن متد Products آن هم فقط یک خروجی JSON را به همراه دارد:
namespace EncryptedModelBinder.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            var model = getProducts();
            return View(model);
        }

        public ActionResult<string> Details(ProductInputModel model)
        {
            return model.Id;
        }

        public ActionResult<List<ProductViewModel>> Products()
        {
            return getProducts();
        }

        private static List<ProductViewModel> getProducts()
        {
            return new List<ProductViewModel>
            {
                new ProductViewModel { Id = "1", Name = "Product 1"},
                new ProductViewModel { Id = "2", Name = "Product 2"},
                new ProductViewModel { Id = "3", Name = "Product 3"}
            };
        }
    }
}
کدهای View اکشن متد Index به صورت زیر است:
@model List<ProductViewModel>

<h3>Home</h3>

<ul>
    @foreach (var item in Model)
    {
        <li><a asp-action="Details" asp-route-id="@item.Id">@item.Name</a></li>
    }
</ul>
در ادامه اگر برنامه را اجرا کنیم، می‌توان مشاهده کرد که تمام asp-route-id‌ها که به خاصیت ویژه‌ی Id اشاره می‌کنند، به صورت خودکار رمزنگاری شده‌اند:


و اگر یکی از لینک‌ها را درخواست کنیم، خروجی model.Id، به صورت معمولی و رمزگشایی شده‌ای مشاهده می‌شود (این خروجی یک رشته‌است که هیچ ویژگی خاصی به آن اعمال نشده‌است. به همین جهت، اینبار این خروجی معمولی مشاهده می‌شود). هدف از اکشن متد Details، نمایش رمزگشایی خودکار اطلاعات است.


و یا اگر اکشن متدی که همانند اکشن متدهای Web API، فقط یک شیء JSON را باز می‌گرداند، فراخوانی کنیم نیز می‌توان به خروجی رمزنگاری شده‌ی زیر رسید:



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: EncryptedModelBinder.zip
مطالب
توسعه سیستم مدیریت محتوای DNTCms - قسمت پنجم
در این قسمت به بررسی بخش Collections (امکان ساخت گروه‌های شخصی برای انتشار مطالب خود (توسط کاربران) با اعمال دسترسی‌های مختلف) ، بخش آگهی‌ها، سیستم لاگ عملیات کاربران و مدل‌های سیستمی می‌پردازیم.
در مدل‌های سیستم، یک تغییر کلی به منظور نگهداری آخرین تغییر دهنده و آخرین تاریخ تغییر در رکورد‌ها، ایجاد شده است. کلاس پایه‌ی زیر به منظور کپسوله کردن یکسری خصوصیات تکراری در نظر گرفته شده است.
  public abstract class BaseEntity
    {
        #region Properties
        /// <summary>
        /// gets or sets Identifier of this Entity
        /// </summary>
        public virtual long Id { get; set; }
        /// <summary>
        /// gets or sets date that this entity was created
        /// </summary>
        public virtual DateTime CreatedOn { get; set; }
        /// <summary>
        /// gets or sets Date that this entity was updated
        /// </summary>
        public virtual DateTime ModifiedOn { get; set; }
        /// <summary>
        /// indicate this entity is Locked for Modify
        /// </summary>
        public virtual bool ModifyLocked { get; set; }
        /// <summary>
        /// gets or sets date that this entity repoted last time
        /// </summary>
        public virtual DateTime? ReportedOn { get; set; }
        /// <summary>
        /// gets or sets counter for Content's report
        /// </summary>
        public virtual int ReportsCount { get; set; }
        /// <summary>
        /// gets or sets TimeStamp for prevent concurrency Problems
        /// </summary>
        public virtual byte[] RowVersion { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets ro sets User that Modify this entity
        /// </summary>
        public virtual User ModifiedBy { get; set; }
        /// <summary>
        /// gets ro sets Id of  User that modify this entity
        /// </summary>
        public virtual long? ModifiedById { get; set; }
        /// <summary>
        /// gets ro sets User that Create this entity
        /// </summary>
        public virtual User CreatedBy { get; set; }
        /// <summary>
        /// gets ro sets User that Create this entity
        /// </summary>
        public virtual long CreatedById { get; set; }
        #endregion
    }
با توجه به امکان تغییر نام کاربری توسط کاربر در سیستم، نگه داری صرفا نام کاربری آخرین تغییر دهنده، مفید نخواهد بود. شبیه به این کار را در سیستم Decision نیز می‌توانید مشاهده کنید. خصوصیاتی که نیاز به توضیح دارند :
  • ReportedOn : نگهداری آخرین تاریخ اخطار 
  • ModifyLocked : به منظور ممانعت از ویرایش 
  • CreatedBy,CreatedById : به منظور ایجاد ارتباط یک به چند بین کاربر و سایر موجودیت‌ها (به عنوان ایجاد کننده)
  • ModifiedBy , ModifiedById : به منظور ایجاد ارتباط یک به چند بین کاربر و سایر موجودیت‌ها (به عنوان آخرین تغییر دهنده)

مدل کلکسیون (کالکشن ،Collection)

بخش Collections می‌تواند برای کاربران امکان انتشار مطالب گروهبندی شده را بر اساس موضوع‌های مختلف، مهیا کند. کاربران بعد از کسب امتیازات لازم با استفاده از مهیا کردن محتوای وبلاگ یا فعالیت‌های آنها در انجمن و ... می‌توانند دسترسی لازم را برای ساخت Collections‌‌های خود، داشته باشند.  
 /// <summary>
    /// Represents the Collection for group posts by topic 
    /// </summary>
    public class Collection : GuidBaseEntity
    {
        #region Properties
        /// <summary>
        /// gets or sets name of collection
        /// </summary>
        public virtual string Name { get; set; }
        /// <summary>
        /// gets or sets Alternative SlugUrl
        /// </summary>
        public virtual string SlugUrl { get; set; }
        /// <summary>
        /// gets or sets some description of group
        /// </summary>
        public virtual string Description { get; set; }
        /// <summary>
        /// gets or sets description that indicate how to pay 
        /// </summary>
        public virtual string HowToPay { get; set; }
        /// <summary>
        /// gets or sets Visibility Type 
        /// </summary>
        public virtual CollectionVisibility Visibility { get; set; }
        /// <summary>
        /// gets or sets color of Collection's Cover
        /// </summary>
        public virtual string Color { get; set; }
        /// <summary>
        /// gets or sets Name of Image that used as Cover
        /// </summary>
        public virtual string Photo { get; set; }
        /// <summary>
        /// gets or sets name of tags seperated by comma that assosiated with this content fo increase performance
        /// </summary>
        public virtual string TagNames { get; set; }
        /// <summary>
        /// indicate this collection is active or not
        /// </summary>
        public virtual bool IsActive { get; set; }
        #endregion

        #region  NavigationProperties
       
        /// <summary>
        /// get or set collection of attachments that attached in this group
        /// </summary>
        public virtual ICollection<CollectionAttachment> Attachments { get; set; }
        /// <summary>
        /// get or set tags of collection
        /// </summary>
        public virtual ICollection<Tag> Tags { get; set; }
        /// <summary>
        /// get or set List Of Posts that Associated with this Collection
        /// </summary>
        public virtual ICollection<CollectionPost> Posts { get; set; }
        /// <summary>
        /// get or set Users that they are Member of this collection if visibility is Custom
        /// </summary>
        public virtual ICollection<User> Memebers { get; set; }
        #endregion
    }
   public enum  CollectionVisibility
    {
        Friends,
        OnlyMe,
        Public,
        NotFree,
        Custom
    }
مدل بالا نشان دهنده‌ی کلکسیون‌های ایجاد شده‌ی توسط کاربران می‌باشد. خصوصیاتی که نیاز به توضیح دارند:
  • Visibility : برای اعمال محدودیت دسترسی به یک کلکسیون در نظر گرفته شده است. از نوع، نوع داده‌ی شمارشی CollectionVisibility پیاده سازی شده‌ی دربالا، می‌باشد. حالت Custom برای زمانی است که نیاز است صرفا یک سری از کاربران بدون هزینه‌، دسترسی برای مطالعه‌ی این کلکسیون داشته باشند. حالت NotFree هم برای زمانی است که کاربرانی که هزینه‌ی مورد نظر را پرداخت کرده باشند، به عنوان عضو به این کلکسیون می‌توانند دسترسی داشته باشند.
  • Members : به منظور اعمال ارتباط چند به چند بین مدل کاربر و مدل کلکسیون، در نظر گرفته شده است و زمانی این لیست اعضا خالی نیست که حالت Visibility با NotFree یا Custom مقدار دهی شده باشد.
  • Tags : برای یافتن راحت‌تر کلکسیون‌های مورد نظر کاربران، یک ارتباط چند به چند بین کلکسیون‌ها و مخزن برچسب مطرح شده‌ی در مقاله اول، در نظر گرفته شده است. 
  • TagNames : برای افزایش کارآیی سیستم در نظر گرفته شده است و نام برچسب‌های در ارتباط با کلکسیون را در خود به صورت جدا شده با (,) نگهداری می‌کند.
  • Posts : لیست پست‌هایی است که توسط مدیر کلکسیون در آن ارسال می‌شود. لذا یک ارتباط یک به چند بین کلکسیون‌ها و CollectionPost در نظر گرفته شده است.
  • Attachments : لیست فایل‌هایی است که در کلکسیون بارگذاری شده‌اند (در ادامه پیاده سازی خواهد شد).
  • Owner , OwnerId : هر کلکسیون نیز یک صاحب خواهد داشت. بدین منظور یک ارتباط یک به چند بین کاربر و کلکسیون برقرار شده است.
  • HowToPay : توضیحاتی در مورد نحوه‌ی پرداخت هزینه (در صورتی که Visibility این کلکسیون NotFree باشد).

مدل پست‌های کلکسیون ها

 public class CollectionPost : GuidBaseEntity
    {
        #region Properties
      
        /// <summary>
        /// indicate this post should be pin
        /// </summary>
        public virtual bool IsPin { get; set; }
        /// <summary>
        /// gets or sets the blog pot body
        /// </summary>
        public virtual string Body { get; set; }
        /// <summary>
        /// gets or sets the content title
        /// </summary>
        public virtual string Title { get; set; }
        /// <summary>
        /// gets or sets value  indicating Custom Slug
        /// </summary>
        public virtual string SlugAltUrl { get; set; }
        /// <summary>
        /// gets or sets a value indicating whether the content comments are allowed 
        /// </summary>
        public virtual bool AllowComments { get; set; }
        /// <summary>
        /// indicate comments should be approved before display
        /// </summary>
        public virtual bool ModerateComments { get; set; }
        /// <summary>
        /// gets or sets viewed count 
        /// </summary>
        public virtual long ViewCount { get; set; }
        /// <summary>
        /// Gets or sets the total number of  approved comments
        /// <remarks>The same as if we run Item.Comments.Count()
        /// We use this property for performance optimization (no SQL command executed)
        /// </remarks>
        /// </summary>
        public virtual int ApprovedCommentsCount { get; set; }
        /// <summary>
        /// Gets or sets the total number of  unapproved comments
        /// <remarks>The same as if we run Item.Comments.Count()
        /// We use this property for performance optimization (no SQL command executed)
        /// </remarks>
        /// </summary>
        public virtual int UnApprovedCommentsCount { get; set; }
        /// <summary>
        /// gets or sets rating complex instance
        /// </summary>
        public virtual Rating Rating { get; set; }
        /// <summary>
        /// gets or sets information of User-Agent
        /// </summary>
        public virtual string Agent { get; set; }
        /// <summary>
        /// indicate users can share this post
        /// </summary>
        public virtual bool IsEnableForShare { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// get or set comments of this post
        /// </summary>
        public virtual ICollection<CollectionComment> Comments { get; set; }
       
        /// <summary>
        /// gets or sets collection that associated with this post
        /// </summary>
        public virtual Collection Collection { get; set; }
        /// <summary>
        /// gets or sets id of collection that associated with this post
        /// </summary>
        public virtual Guid CollectionId { get; set; }
        #endregion
    }
مدل بالا نشان دهنده‌ی پست‌های ارسالی در کلکسیون‌ها می‌باشد. خصوصیاتی که نیاز به توضیح دارند:
  • IsEnableForShare : اگر با مقدار True مقدار دهی شده باشد ، امکان به اشتراک گذاری آن درشبکه‌های اجتماعی وجود خواهد داشت. 
  • Comments : اگر مقدار AllowComments مربوط به پست ارسالی true باشد، در آن صورت امکان نظر دهی به پست  هم امکان پذیر خواهد بود. برای برقراری ارتباط یک به چند بین مدل پست کلکسیون و نظرات کلکسیون، این لیست در مدل بالا گنجانده شده است.
  • ModerateComments : اگر برای پست خاصی، با مقدار true مقدار دهی شده باشد، نظرات آن پست قبل از نمایش باید توسط مدیر آن کلکسیون تأیید شوند.
  • Author , AuthorId : در واقع ارسال کننده‌ی تمام پست‌ها، همان صاحب کلکسیون می‌باشد. ولی برای راحتی واکشی لیست پست‌های ارسالی کاربر مورد نظر یک ارتباط یک به چند بین کاربر و پست‌های ارسالی را در کلکسیون اعمال کرده‌ایم.
  • Collection , CollectionId : کلید خارجی ما در دیتابیس ایجاد شده خواهد بود که نشان دهنده‌ی ارتباط یک به چند بین کلکسیون و پست‌ها می‌باشد.
  • IsPin : اگر لازم است پستی به عنوان اولین پست در کلکسیون نمایش داده شود، این خصوصیت برای آن true خواهد بود.
  • ApprovedCommentsCount , UnApprovedCommentsCount: برای افزایش کارآیی سیستم در نظر گرفته شده است و هنگام درج نظر جدید یا حذف نظر، ویرایش خواهند شد. 

مدل نظرات ارسالی در کلکسیون ها

 public class CollectionComment 
    {
        #region Ctor
        public CollectionComment()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
            CreatedOn = DateTime.Now;

        }
        #endregion

        #region Properties
        /// <summary>
        /// get or set identifier of record
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets date of creation 
        /// </summary>
        public virtual DateTime CreatedOn { get; set; }
        /// <summary>
        /// gets or sets body of blog post's comment
        /// </summary>
        public virtual string Body { get; set; }
        /// <summary>
        /// gets or sets body of blog post's comment
        /// </summary>
        public virtual Rating Rating { get; set; }
        /// <summary>
        /// gets or sets informations of agent
        /// </summary>
        public virtual string UserAgent { get; set; }
        /// <summary>
        /// indicate this comment is Approved 
        /// </summary>
        public virtual bool IsApproved { get; set; }
        /// <summary>
        /// gets or sets Ip Address of Creator
        /// </summary>
        public virtual string CreatorIp { get; set; }
        /// <summary>
        /// gets or sets datetime that is modified
        /// </summary>
        public virtual DateTime? ModifiedOn { get; set; }
        /// <summary>
        /// gets or sets counter for report this comment
        /// </summary>
        public virtual int ReportsCount { get; set; }
        /// <summary>
        /// indicate this entity is Locked for Modify
        /// </summary>
        public virtual bool ModifyLocked { get; set; }
        /// <summary>
        /// gets or sets date that this entity repoted last time
        /// </summary>
        public virtual DateTime? ReportedOn { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets post that this comment added it
        /// </summary>
        public virtual CollectionPost Post { get; set; }
        /// <summary>
        /// gets or sets id of post that this comment added it
        /// </summary>
        public virtual Guid PostId { get; set; }
        /// <summary>
        /// get or set user that create this record
        /// </summary>
        public virtual User Creator { get; set; }
        /// <summary>
        /// get or set Id of user that create this record
        /// </summary>
        public virtual long CreatorId { get; set; }
        /// <summary>
        /// gets or sets CollectionComment's identifier for Replying and impelemention self referencing
        /// </summary>
        public virtual Guid? ReplyId { get; set; }
        /// <summary>
        /// gets or sets Collection's comment for Replying and impelemention self referencing
        /// </summary>
        public virtual CollectionComment Reply { get; set; }
        /// <summary>
        /// get or set collection of Collection's comment for Replying and impelemention self referencing
        /// </summary>
        public virtual ICollection<CollectionComment> Children { get; set; }
        #endregion
    }

مدل بالا نشان دهنده‌ی نظرات ارسالی برای پست‌های کلکسیون‌ها می‌باشد. صرفا کاربران عضو سیستم این اجازه را در صورتی خواهند داشت که برای پست مورد نظر خصوصیت AllowComments با مقدار true مقدار دهی شده باشد
حالت درختی آن مشخص است. برای اعمال ارتباط یک به چند بین پست‌ها و نظرات، از CollectionPost و CollectionPostId استفاده خواهد شد.

  • IsApproved : برای زمانی استفاده خواهد شد که خصوصیت ModerateComments پست مورد نظر با مقدار true مقدار دهی شده باشد. 
  • ReportsCount : به مانند بخش‌های قبل، تعداد اخطار‌های داده شده‌ی برای یک نظر را نشان خواهد داد. 
  • Creator,CreatorId : ارسال کننده‌ی نظر می‌باشد و برای ایجاد ارتباط یک به چند بین کاربر و نظرات کلکسیون‌ها در نظر گرفته شده‌اند. 
  • ReportedOn : نگه داری آخرین تاریخ اخطار 
  • ModifyLocked : به منظور ممانعت از ویرایش

مدل فایل‌های ضمیمه کلکسیون ها

public class CollectionAttachment : BaseAttachment
    {
        #region NavigationProperties
        /// <summary>
        /// gets or sets Collection  that this file attached 
        /// </summary>
        public virtual Collection Collection { get; set; }
        /// <summary>
        /// gets or sets Id of Collection  that this file attached
        /// </summary>
        public virtual Guid? CollectionId { get; set; }
        #endregion
    }
اگر یادتان باشد در مقاله‌ی دوم در مورد نحوه‌ی مدیریت فایل‌ها بحث شد و در نتیجه تصمیم گرفته شد که از ارث بری TPH استفاده کنیم. مدل بالا نیز یکی از SubClass‌های ما خواهد بود. با توجه به اینکه شاید Privacy فایل‌های ارسالی یک گروه مهم باشد (در صورت خصوصی بودن یا پولی بودن مطالعه‌ی کلکسیون)  نیاز شد مدل بالا را نیز داشته باشیم. برای اعمال ارتباط یک به چند بین مدل بالا و مدل کلکسیون، از خصوصیت‌های Colllection , CollectionId استفاده خواهیم کرد. دقت کنید که لازم است CollectionId به صورت نال پذیر درنظر گرفته شود.

مدل آگهی ها

/// <summary>
    /// Represents Announcement For Announcement Section
    /// </summary>
    public class Announcement : BaseContent
    {
        #region Properties
        /// <summary>
        /// gets or sets Date that this Announcement will Expire
        /// </summary>
        public virtual DateTime? ExpireOn { get; set; }
        /// <summary>
        /// indicate this accouncement is approved by admin if announcementSetting.Moderate==true
        /// </summary>
        public virtual bool IsApproved { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// get or set Collection of Comments for this Announcement
        /// </summary>
        public virtual ICollection<AnnouncementComment> Comments { get; set; }

        #endregion
    }
مدل بالا نشان دهنده‌ی آگهی‌ها سیستم خواهد بود. همان طور که مشخص است، این مدل نیز از کلاس پایه BaseContent ارث بری کرده و علاوه بر آن یکسری خصوصیت دیگر را به شرح زیر دارد:
  • ExpireOn : زمان انقضای آگهی 
  • IsApproved : به منظور اعمال مدیریتی در نظر گرفته شده است
  • Comments : اگر امکان ارسال نظرات برای آگهی از بخش تنظیمات فعال باشد، این لیست نظرات ما را نگه داری خواهد کرد. لذا یک رابطه‌ی یک به چند بین نظرات و آگهی‌ها خواهد بود.

مدل نظرات آگهی ها

/// <summary>
    /// Repersents Comment For Announcement
    /// </summary>
    public class AnnouncementComment : BaseComment
    {
        #region NavigationProperties
        /// <summary>
        /// gets or sets body of announcement's comment
        /// </summary>
        public virtual long? ReplyId { get; set; }
        /// <summary>
        /// gets or sets body of announcement's comment
        /// </summary>
        public virtual AnnouncementComment Reply { get; set; }
        /// <summary>
        /// gets or sets body of announcement's comment
        /// </summary>
        public virtual ICollection<AnnouncementComment> Children { get; set; }
        /// <summary>
        /// gets or sets announcement that this comment sent to it
        /// </summary>
        public virtual Announcement Announcement { get; set; }
        /// <summary>
        /// gets or sets announcement'Id that this comment sent to it
        /// </summary>
        public virtual long AnnouncementId { get; set; }
        #endregion
    }
مدل بالا  نشان دهنده‌ی نظرات ارسالی برای آگهی‌ها می‌باشد. از کلاس پایه‌ی مطرح شده‌ی در مقاله‌ی اول ارث بری کرده و علاوه بر آن ساختار درختی آن مشخص است و همچنین برای برقراری ارتباط یک به چند با مدل آگهی‌ها، خصوصیات Announcement  , AnnouncementId در نظر گرفته شده‌اند.

مدل سیستم لاگ عملیات کاربران

/// <summary>
    /// Represent The Operation's log
    /// </summary>
    public class AuditLog
    {
        #region Ctor
        /// <summary>
        /// 
        /// </summary>
        public AuditLog()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
            OperatedOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// sets or gets identifier of AuditLog
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// sets or gets description of Log
        /// </summary>
        public virtual string Description { get; set; }
        /// <summary>
        /// sets or gets when log is operated
        /// </summary>
        public virtual DateTime OperatedOn { get; set; }
        /// <summary>
        /// sets or gets log's section
        /// </summary>
        public virtual AuditSection Section { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// sets or gets log's creator
        /// </summary>
        public virtual User OperatedBy { get; set; }
        /// <summary>
        /// sets or gets identifier of log's creator
        /// </summary>
        public virtual long OperatedById { get; set; }
        #endregion
    }

  public enum AuditSection
    {
        Blog,
        News,
        Forum,
        ...
    }
مدل بالا نگهدارنده زمان اکشن انجام شده، توسط کاربری که انجام شده و یه سری توضیحات کلی می‌باشد. به منظور اعمال ارتباط یک به چند مابین کاربر و مدل بالا، خصوصیات OperatedBy و OperatedById را در نظر گرفته‌ایم. خصوصیت Section که از نوع، نوع داده شمارشی AuditSection می‌باشد، می‌تواند برای جداسازی این لاگ‌های ذخیره شده، مفید باشد.

مدل دامین‌های ممنوع

در پنل مدیریت امکانی را خواهیم داشت تا یکسری از دامین‌های مد نظر را که نمی‌خواهیم در محتوای سیستم، آدرس صفحات آنها دیده شوند و همچنین برای عدم ارسال و دریافت پینگ بک‌ها لحاظ شوند، ثبت کنیم.
 /// <summary>
    /// Represents Domain that is banned
    /// </summary>
    public class BannedDomain
    {
        #region Propertie
        /// <summary>
        /// gets or sets identifier of Domain
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets DomainName
        /// </summary>
        public virtual string Name { get; set; }
        /// <summary>
        /// gets or sets Date that this record added
        /// </summary>
        public virtual DateTime BannedOn { get; set; }
        #endregion
    }

مدل کلمات ممنوع 

 /// <summary>
    /// Represents the banned words
    /// </summary>
    public class BannedWord
    {
        #region Ctor
        /// <summary>
        /// Create one instance of <see cref="BannedWord"/>
        /// </summary>
        public BannedWord()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets identifier of BannedWord
        /// </summary>
        public Guid Id { get; set; }
        /// <summary>
        /// gets or sets Bad word
        /// </summary>
        public string BadWord { get; set; }
        /// <summary>
        /// gets or sets Good replaceword
        /// </summary>
        public string GoodWord { get; set; }
        /// <summary>
        /// indicating that this Word is spam
        /// </summary>
        public bool IsStopWord { get; set; }
        #endregion

    }
مدل بالا نشان دهنده‌ی کلماتی است که لازم است در متن مطالب سیستم حذف یا با کلمات جدید جایگزین شوند.
  • BadWord : کلمه مورد نظر که قرار است Ban شود.
  • IsStopWord : اگر لازم نیست جایگزینی برای کلمه استفاده شود و فقط لازم است حذف گردد، مقدار این خصوصیت true خواهد بود.
  • GoodWord : کلمه جایگزین 

مدل تنظیمات سیستم

 /// <summary>
    /// Represent The CMS setting
    /// </summary>
    public class Setting
    {
        #region Properties
        /// <summary>
        /// sets or gets name of setting
        /// </summary>
        public virtual string Name { get; set; }
        /// <summary>
        /// sets or gets value of setting
        /// </summary>
        public virtual string Value { get; set; }
        /// <summary>
        /// sets or gets Type of setting
        /// </summary>
        public virtual string Type { get; set; }
        #endregion
    }
مدل بالا کل تنظیمات سیستم را در قالب Key , Value نگهداری خواهد کرد. برای توضیحات این قسمت می‌توانید به مقاله‌ی "ذخیره تنظیمات متغیر مربوط به یک وب اپلیکیشن ASP.NET MVC با استفاده از EF" رجوع کنید.
پ.ن:در مقاله‌ی بعد با پیاده سازی مدل‌های مدیریت کاربران، سیستم پیام رسانی، سیستم ترفیع رتبه و ارتباط دوستی، DomainClasses به اتمام خواهد رسید.

نتیجه‌ی این قسمت 

با توجه به این که تعداد مدل‌ها زیاد است و از طرفی حجم تصویر را کاهش داده‌ایم ، تصویر بدست آماده کمی افت کیفیت دارد؛ بنابراین بهتر است از فایل EDMX زیر استفاده کنید.

مطالب
MVVM و الگوی ViewModel Locator

اگر ViewModel را همان فایل code behind عاری از ارجاعاتی به اشیاء بصری بدانیم، یک تفاوت مهم را علاوه بر مورد ذکر شده نسبت به Code behind متداول خواهد داشت: وهله سازی آن باید دستی انجام شود و خودکار نیست.
اگر به ابتدای کلاس‌های code behind‌ دقت کنید همیشه واژه‌ی partial قابل رویت است، به این معنا که این کلاس در حقیقت جزئی از همان کلاس متناظر با XAML ایی است که مشاهده می‌کنید؛ یا به عبارتی با آن یکی است. فقط جهت زیبایی یا مدیریت بهتر، در دو کلاس قرار گرفته‌اند اما واژه کلیدی partial این‌ها را نهایتا به صورت یکسان و یکپارچه‌ای به کامپایلر معرفی خواهد کرد. بنابراین وهله سازی code behind هم خودکار خواهد بود و به محض نمایش رابط کاربری،‌ فایل code behind آن هم وهله سازی می‌شود؛ چون اساسا و در پشت صحنه، از دیدگاه کامپایلر تفاوتی بین این دو وجود ندارد.

اکنون سؤال اینجا است که آیا می‌توان با ViewModel ها هم همین وهله سازی خودکار را به محض نمایش یک View متناظر، پیاده سازی کرد؟
البته صحیح آن این است که عنوان شود ViewModel متناظر با یک View و نه برعکس. چون روابط در الگوی MVVM از View به ViewModel به Model است و نه حالت عکس؛ مدل نمی‌داند که ViewModel ایی وجود دارد. ViewModel هم از وجود View ها در برنامه بی‌خبر است و این «بی‌خبری‌ها» اساس الگوهایی مانند MVC ، MVVM ، MVP‌ و غیره هستند. به همین جهت شاعر در وصف ViewModel فرموده‌اند که:

ای در درون برنامه‌ام و View از تو بی خبر_________وز تو برنامه‌ام پر است و برنامه از تو بی خبر :)

پاسخ:
بله. برای این منظور الگوی دیگری به نام ViewModel Locator طراحی شده است؛ روش‌های زیادی برای پیاده سازی این الگو وجود دارند که ساده‌ترین آن‌ها مورد زیر است:
فرض کنید ViewModel ساده زیر را قصد داریم به کمک الگوی ViewModel Locator به View ایی تزریق کنیم:

namespace WpfViewModelLocator.ViewModels
{
public class MainWindowViewModel
{
public string SomeText { set; get; }
public MainWindowViewModel()
{
SomeText = "Data ...";
}
}
}

برای این منظور ابتدا کلاس ViewModelLocatorBase زیر را تدارک خواهیم دید:

using WpfViewModelLocator.ViewModels;

namespace WpfViewModelLocator.ViewModelLocator
{
public class ViewModelLocatorBase
{
public MainWindowViewModel MainWindowVm
{
get { return new MainWindowViewModel(); }
}
}
}

در اینجا یک وهله از کلاس MainWindowViewModel توسط خاصیتی به نام MainWindowVm در دسترس قرار خواهد گرفت. برای اینکه بتوان این کلاس را در تمام Viewهای برنامه قابل دسترسی کنیم، آن‌را در App.Xaml تعریف خواهیم کرد:

<Application x:Class="WpfViewModelLocator.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vml="clr-namespace:WpfViewModelLocator.ViewModelLocator"
StartupUri="MainWindow.xaml">
<Application.Resources>
<vml:ViewModelLocatorBase x:Key="ViewModelLocatorBase" />
</Application.Resources>
</Application>

اکنون فقط کافی است در View خود DataContext را به نحو زیر مقدار دهی کنیم تا در زمان اجرا به صورت خودکار بتوان به خاصیت MainWindowVm یاد شده دسترسی یافت:

<Window x:Class="WpfViewModelLocator.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid DataContext="{Binding Path=MainWindowVm, Source={StaticResource ViewModelLocatorBase}}">
<TextBlock Text="{Binding SomeText}" VerticalAlignment="Top" Margin="5" />
</Grid>
</Window>

در مورد ViewModel ها و Viewهای دیگر هم به همین ترتیب خواهد بود. یک وهله از آن‌ها به کلاس ViewModelLocatorBase اضافه می‌شود. سپس Binding Path مرتبط به DataContext به نام خاصیتی که در کلاس ViewModelLocatorBase مشخص خواهیم کرد، Bind خواهد شد.

روش دوم:
اگر در اینجا بخواهیم Path را حذف کنیم و فقط دسترسی عمومی به ViewModelLocatorBase را ذکر کنیم، باید یک Converter نوشت (چون به این ترتیب می‌توان به اطلاعات Binding در متد Convert دسترسی یافت). سپس یک قرار داد را هم تعریف خواهیم کرد؛ به این صورت که ما در Converter به نام View دسترسی پیدا می‌کنیم (از طریق ریفلکشن). سپس نام viewModel ایی را که باید به دنبال آن گشت مثلا ViewName به علاوه کلمه ViewModel در نظر خواهیم گرفت. در حقیقت یک نوع Convection over configuration است:

using System;
using System.Globalization;
using System.Linq;
using System.Windows.Data;

namespace WpfViewModelLocator.ViewModelLocator
{
public class ViewModelLocatorBaseConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
//مقدار در اینجا همان مشخصات ویوو است
if (value == null) return null;
string viewTypeName = value.GetType().Name;

//قرار داد ما است
//ViewModel Name = ViewName + "ViewModel"
string viewModelName = string.Concat(viewTypeName, "ViewModel");

//یافتن اسمبلی که حاوی ویوو مدل ما است
var asms = AppDomain.CurrentDomain.GetAssemblies();
var viewModelAsmName = "WpfViewModelLocator"; //نام پروژه مرتبط
var viewModelAsm = asms.Where(x => x.FullName.Contains(viewModelAsmName)).First();

//یافتن این کلاس ویوو مدل مرتبط
var viewModelType = viewModelAsm.GetTypes().Where(x => x.FullName.Contains(viewModelName)).FirstOrDefault();
if (viewModelType == null)
throw new InvalidOperationException(string.Format("Could not find view model '{0}'", viewModelName));

//وهله سازی خودکار آن
return Activator.CreateInstance(viewModelType);
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

کار این تبدیلگر بسیار ساده و واضح است. Value‌ دریافتی، وهله‌ای از view است. پس به این ترتیب می‌توان نام آن‌را یافت. سپس قرارداد ویژه خودمان را اعمال می‌کنیم به این ترتیب که ViewModel Name = ViewName + "ViewModel" و سپس به دنبال اسمبلی که حاوی این نام است خواهیم گشت. آن‌را یافته، کلاس مرتبط را در آن پیدا می‌کنیم و در آخر، به صورت خودکار آن‌را وهله سازی خواهیم کرد.
اینبار تعریف عمومی این Conveter در فایل App.Xaml به صورت زیر خواهد بود:

<Application x:Class="WpfViewModelLocator.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vml="clr-namespace:WpfViewModelLocator.ViewModelLocator"
StartupUri="MainWindow.xaml">
<Application.Resources>
<vml:ViewModelLocatorBaseConverter x:Key="ViewModelLocatorBaseConverter" />
</Application.Resources>
</Application>

و استفاده‌ی آن در تمام View های برنامه به شکل زیر می‌باشد (بدون نیاز به ذکر هیچ نام خاصی و بدون نیاز به کلاس ViewModelLocatorBase یاد شده در ابتدای مطلب):

<Window x:Class="WpfViewModelLocator.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DataContext="{Binding RelativeSource={RelativeSource Self},
Converter={StaticResource ViewModelLocatorBaseConverter}}"
Title="MainWindow" Height="350" Width="525">
<Grid>
<TextBlock Text="{Binding SomeText}" VerticalAlignment="Top" Margin="5" />
</Grid>
</Window>


مطالب
پیاده سازی Option یا Maybe در #C

Options یا Maybe در یک زبان تابعی مثل #F، نشان دهنده‌ی این است که شیء (Object) ممکن است وجود نداشته باشد(Null Reference) که یکی از مهمترین ویژگی‌های یک زبان شیءگرا مثل #C و یا Java محسوب می‌شودما برنامه نویس‌ها (اغلب) از هرچیزی که باعث کرش برنامه می‌شود، بیزاریم و برای اینکه برنامه کرش نکند، مجبور میشویم تمام کد‌های خود  را از Null Reference محافظت کنیم. تمام این مشکلات توسط Tony Hoare مخترع ALOGL است که تنها دلیل وجود Null References را سادگی پیاده سازی آن می‌داند و او این مورد را یک «خطای  میلیون دلاری» نامیده‌است. 

به این مثال توجه بفرمایید: 

public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

public class UserService : IUserService
    {
        private IList<User> _userData;

        public UserService()
        {
            _userData = new List<User>
            {
                new User {Id = 1,Name = "ali"},
                new User {Id = 2,Name = "Karim"}
            };
        }

        public User GetById(int id)
        {
            return _userData.FirstOrDefault(x => x.Id == id);
        }
    }  

public class UserController : Controller
    {
        private readonly IUserService _userService;

        public UserController(IUserService  userService)
        {
            _userService = userService;
        }
        public ActionResult Details(int id)
        {
            var user=_userService.GetById(3); // این متد ممکن است مقداری برگرداند و یا مقدار نال برگرداند                           
            if( user == null)
                 return HttpNotFound();    
            return View(user);  
        }
    }

این کدی است که ما برنامه نویسان به صورت متداولی با آن سروکار داریم. اما چه چیزی درباره این کد اشکال دارد؟

مشکل از آن جایی هست که ما نمی‌دانیم متد GetById مقداری را برمیگرداند و یا Null را بر می‌گرداند. این متد هرگاه که امکان برگرداندن Null وجود داشته باشد، خطای  NullReferenceException را در زمان اجرا بر می‌گرداند و همان طور که میدانید، به ازای هر شرطی که به برنامه اضافه میکنیم، پیچیدگی برنامه هم افزایش می‌یابد و کد خوانایی خود را از دست می‌دهد. تصور کنید دنیایی بدون NullReferenceException چه دنیایی زیبایی می‌بود؛ ولی متاسفانه این مورد از ویژگی‌های زبان #C است. خوشبختانه راه‌حل‌های برای حل NRE ارائه شده‌اند که در ادامه به آن‌ها می‌پردازیم.

ما می‌خواهیم متد GetById همیشه چیزی غیر از نال را برگرداند و یکی از راه‌هایی که ما را به این هدف می‌رساند این است که این متد یک توالی را برگرداند.

به نگاری جدید کد توجه بفرمایید:
public class UserService : IUserService
    {
        private IList<User> _userData;

        public UserService()
        {
            _userData = new List<User>
            {
                new User {Id = 1,Name = "ali"},
                new User {Id = 2,Name = "Karim"}
            };
        }

        public IEnumerable<User> GetById(int id)
        {
            var user = _userData.FirstOrDefault(x => x.Id == id);
            if (user == null) return new User[0];
            return new[] { user };
        }
    } 

اگر به امضای متد GetById توجه کنید، به جای اینکه User را برگرداند، این متد یک توالی از User را بر می‌گرداند و اگر در اینجا کاربری یافت شد، این توالی دارای یک المان خواهد بود و در غیر این صورت اگر User یافت نشد، این متد یک توالی را بر می‌گرداند که دارای هیچ المانی نیست. در ادامه اگر کلاینت بخواهد از متد GetById استفاده کند، به صورت زیر خواهد بود:

 public ActionResult Details(int id)
        {
            var user = _userService
                            .GetById(3)
                            .DefaultIfEmpty(new User())
                            .Single();
            return View(user);
        }

 متد GetById دارای دو وجه است و وجه مثبت آن این است که اگر مجموعه دارای مقداری باشد، هیچ مشکلی نیست؛ ولی اگر مجموعه دارای المانی نباشد، باید یک شیء را به صورت پیش فرض به آن اختصاص دهیم که این کار را با استفاده از متد DefualtIfEmpty انجام داده‌ایم. 

 در اول مقاله هم اشاره کردیم که  Maybe یا Options، مجموعه‌ای است که دارای یک المان و یا هیچ المانی است. اگر به امضای متد GetById توجه کنید، متوجه خواهید شد که این متد می‌تواند مجموعه‌ای را برگرداند و نمی‌تواند گارانتی کند که حتما مجموعه‌ای را بر می‌گرداند که دارای یک المان و یا هیچ باشد. برای حل این مشکل می‌توانیم از کلاس Option استفاده کنیم:

public class Option<T> : IEnumerable<T>
    {
        private readonly T[] _data;

        private Option(T[] data)
        {
            _data = data;
        }

        public static Option<T> Create(T element) => new Option<T>(new[] { element });

        public static Option<T> CreateEmpty() => new Option<T>(new T[0]);

        public IEnumerator<T> GetEnumerator() => ((IEnumerable<T>) _data).GetEnumerator();

        IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
    }

تنها دلیل استفاده از متد‌های Create و CreateEmpty این است که به خوانایی برنامه کمک کنیم؛ نه بیشتر. در ادامه اگر بخواهیم از کلاس option استفاده کنیم، به صورت زیر خواهد بود:

 public class UserService : IUserService
    {
       ...
       ...
       public Option<User> GetById(int id)
        {
            var user = _userData.FirstOrDefault(x => x.Id == id);
            return user == null ? Option<User>.CreateEmpty() : Option<User>.Create(user);
        }
    }

 public class UserController : Controller
    {
       ...
       ...
       public ActionResult Details(int id)
        {
            var user = _userService
                            .GetById(3)
                            .DefaultIfEmpty(new User())
                            .Single();
            return View(user);
        }
    }


چکیده:

مدیریت کردن References کار بسیار پیچیده‌ای است. قبل از آن که تلاش کنیم مقداری را برگردانیم و یا عملیاتی را بر روی آن انجام دهیم، اول باید مطمئن شویم که این شیء به جایی اشاره می‌کند. نمونه‌های متفاوتی از Option و یا Maybe را می‌توانید در اینترنت پیدا کنید که هدف نهایی آن‌ها، حذف NullReferenceException است و آشنایی با این ایده، شما را به دنیای برنامه نویسی تابعی در#C هدایت می‌کند.

مطالب
Roslyn #4
بررسی API کامپایل Roslyn

Compilation API، یک abstraction سطح بالا از فعالیت‌های کامپایل Roslyn است. برای مثال در اینجا می‌توان یک اسمبلی را از Syntax tree موجود، تولید کرد و یا جایگزین‌هایی را برای APIهای قدیمی CodeDOM و Reflection Emit ارائه داد. به علاوه این API امکان دسترسی به گزارشات خطاهای کامپایل را میسر می‌کند؛ به همراه دسترسی به اطلاعات Semantic analysis. در مورد تفاوت Syntax tree و Semantics در قسمت قبل بیشتر بحث شد.
با ارائه‌ی Roslyn، اینبار کامپایلرهای خط فرمان تولید شده مانند csc.exe، صرفا یک پوسته بر فراز Compilation API آن هستند. بنابراین دیگر نیازی به فراخوانی Process.Start بر روی فایل اجرایی csc.exe مانند یک سری کتابخانه‌های قدیمی نیست. در اینجا با کدنویسی، به تمام اجزاء و تنظیمات کامپایلر، دسترسی وجود دارد.


کامپایل پویای کد توسط Roslyn

برای کار با API کامپایل، سورس کد، به صورت یک رشته در اختیار کامپایلر قرار می‌گیرد؛ به همراه تنظیمات ارجاعاتی به اسمبلی‌هایی که نیاز دارد. سپس کار کامپایلر شروع خواهد شد و شامل مواردی است مانند تبدیل متن دریافتی به Syntax tree و همچنین تبدیل مواردی که اصطلاحا به آن‌ها Syntax sugars گفته می‌شود مانند خواص get و set دار به معادل‌های اصلی آن‌ها. در اینجا کار Semantic analysis هم انجام می‌شود و شامل تشخیص حوزه‌ی دید متغیرها، تشخیص overloadها و بررسی نوع‌های بکار رفته‌است. در نهایت کار تولید فایل باینری اسمبلی، از اطلاعات آنالیز شده صورت می‌گیرد. البته خروجی کامپایلر می‌تواند اسمبلی‌های exe یا dll، فایل XML مستندات اسمبلی و یا فایل‌های .netmudule و .winmdobj مخصوص WinRT هم باشد.
در ادامه، اولین مثال کار با Compilation API را مشاهده می‌کنید. پیشنیاز اجرای آن همان مواردی هستند که در قسمت قبل بحث شدند. یک برنامه‌ی کنسول ساده‌ی .NET 4.6 را آغاز کرده و سپس بسته‌ی نیوگت Microsoft.CodeAnalysis را در آن نصب کنید. در ادامه کدهای ذیل را به پروژه‌ی آماده شده اضافه کنید:
static void firstCompilation()
{
    var tree = CSharpSyntaxTree.ParseText("class Foo { void Bar(int x) {} }");
 
    var mscorlibReference = MetadataReference.CreateFromFile(typeof (object).Assembly.Location);
 
    var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
 
    var comp = CSharpCompilation.Create("Demo")
                                .AddSyntaxTrees(tree)
                                .AddReferences(mscorlibReference)
                                .WithOptions(compilationOptions);
 
    var res = comp.Emit("Demo.dll");
 
    if (!res.Success)
    {
        foreach (var diagnostic in res.Diagnostics)
        {
            Console.WriteLine(diagnostic.GetMessage());
        }
    }
}
در اینجا نحوه‌ی کامپایل پویای یک قطعه کد متنی سی‌شارپ را به DLL معادل آن مشاهده می‌کنید. مرحله‌ی اول اینکار، تولید Syntax tree از رشته‌ی متنی دریافتی است. سپس متد CSharpCompilation.Create یک وهله از Compilation API مخصوص #C را آغاز می‌کند. این API به صورت Fluent طراحی شده‌است و می‌توان سایر قسمت‌های آن‌را به همراه یک دات پس از ذکر متد، به طول زنجیره‌ی فراخوانی، اضافه کرد. برای نمونه در این مثال، نحوه‌ی افزودن ارجاعی را به اسمبلی mscorlib که System.Object در آن قرار دارد و همچنین ذکر نوع خروجی DLL یا DynamicallyLinkedLibrary را ملاحظه می‌کنید. اگر این تنظیم ذکر نشود، خروجی پیش فرض از نوع .exe خواهد بود و اگر mscorlib را اضافه نکنیم، نوع int سورس کد ورودی، شناسایی نشده و برنامه کامپایل نمی‌شود.
متدهای تعریف شده توسط Compilation API به یک s جمع، ختم می‌شوند؛ به این معنا که در اینجا در صورت نیاز، چندین Syntax tree یا ارجاع را می‌توان تعریف کرد.
پس از وهله سازی Compilation API و تنظیم آن، اکنون با فراخوانی متد Emit، کار تولید فایل اسمبلی نهایی صورت می‌گیرد. در اینجا اگر خطایی وجود داشته باشد، استثنایی را دریافت نخواهید کرد. بلکه باید خاصیت Success نتیجه‌ی آن‌را بررسی کرده و درصورت موفقیت آمیز نبودن عملیات، خطاهای دریافتی را از مجموعه‌ی Diagnostics آن دریافت کرد. کلاس Diagnostic، شامل اطلاعاتی مانند محل سطر و ستون وقوع مشکل و یا پیام متناظر با آن است.


معرفی مقدمات Semantic analysis

Compilation API به اطلاعات Semantics نیز دسترسی دارد. برای مثال آیا Type A قابل تبدیل به Type B هست یا اصلا نیازی به تبدیل ندارد و به صورت مستقیم قابل انتساب هستند؟ برای درک بهتر این مفهوم نیاز است یک مثال را بررسی کنیم:
        static void semanticQuestions()
        {
            var tree = CSharpSyntaxTree.ParseText(@"
using System;
 
class Foo
{
    public static explicit operator DateTime(Foo f)
    {
        throw new NotImplementedException();
    }
 
    void Bar(int x)
    {
    }
}");
 
            var mscorlib = MetadataReference.CreateFromFile(typeof (object).Assembly.Location);
            var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
            var comp = CSharpCompilation.Create("Demo").AddSyntaxTrees(tree).AddReferences(mscorlib).WithOptions(options);
            // var res = comp.Emit("Demo.dll");
 
            // boxing
            var conv1 = comp.ClassifyConversion(
                comp.GetSpecialType(SpecialType.System_Int32),
                comp.GetSpecialType(SpecialType.System_Object)
                );
 
            // unboxing
            var conv2 = comp.ClassifyConversion(
                comp.GetSpecialType(SpecialType.System_Object),
                comp.GetSpecialType(SpecialType.System_Int32)
                );
 
            // explicit reference conversion
            var conv3 = comp.ClassifyConversion(
                comp.GetSpecialType(SpecialType.System_Object),
                comp.GetTypeByMetadataName("Foo")
                );
 
            // explicit user-supplied conversion
            var conv4 = comp.ClassifyConversion(
                comp.GetTypeByMetadataName("Foo"),
                comp.GetSpecialType(SpecialType.System_DateTime)
                );
        }
تا سطر CSharpCompilation.Create این مثال، مانند قبل است و تا اینجا به Compilation API دسترسی پیدا کرده‌ایم. پس از آن می‌خواهیم یک Semantic analysis مقدماتی را انجام دهیم. برای این منظور می‌توان از متد ClassifyConversion استفاده کرد. این متد یک نوع مبداء و یک نوع مقصد را دریافت می‌کند و بر اساس اطلاعاتی که از Compilation API بدست می‌آورد، می‌تواند مشخص کند که برای مثال آیا نوع کلاس Foo قابل تبدیل به DateTime هست یا خیر و اگر هست چه نوع تبدیلی را نیاز دارد؟


برای مثال نتیجه‌ی بررسی آخرین تبدیل انجام شده در تصویر فوق مشخص است. با توجه به تعریف public static explicit operator DateTime در سورس کد مورد آنالیز، این تبدیل explicit بوده و همچنین user defined. به علاوه متدی هم که این تبدیل را انجام می‌دهد، مشخص کرده‌است.
نظرات مطالب
بررسی تفصیلی رابطه Many-to-Many در EF Code first
سلام. من ساختاری شبیه به این دارم:
public class Person
{
        //...
        public virtual IList<Center> PreferedCenters { get; set; }
        public virtual IList<Center> ActiveCenters { get; set; }

        public Person()
        {
            PreferedCenters = new List<Center>();
            ActiveCenters = new List<Center>();
        }
}
و کلا Center هم به شکل زیره:
public class Center
{
    //...
    public virtual IList<Person> Persons { get; set; }

    public Center()
    {
        Persons = new List<Person>();
    }
}
مشکلی که دارم اینه که منطقا باید دوتا رابطه‌ی Many-to-many تشکیل بشه: ActiveCenters و PreferedCenters. ولی توی جدول خروجی EF، فقط ستونی به اسم Center_ID ساخته می‌شه و وقتی هم که می‌خوام به سیستم چیزی اضافه کنم اروری شبیه به این می‌گیرم: 
An unhandled exception of type 'System.InvalidOperationException' occurred in EntityFramework.dll

Additional information: Multiplicity constraint violated. The role 'Center_Persons_Source' of the relationship 'Yarigaran.DataLayer.Center_Persons' has multiplicity 1 or 0..1.
مطالب
معرفی Lex.Db
Lex.Db یک بانک اطلاعاتی درون پروسه‌ای (مدفون شده یا embedded) بسیار سریع نوشته شده با سی‌شارپ است. این بانک اطلاعاتی کم حجم، سورس باز بوده و مجوز استفاده از آن LGPL است. به این معنا که استفاده از اسمبلی‌های آن در هر نوع پروژه‌ای آزاد است.
نکته مهم آن سازگاری با برنامه‌های دات نت 4 به بعد، همچنین برنامه‌های ویندوز 8، سیلورلایت 5، ویندوز فون 8 و همچنین اندروید (از طریق Mono) است. به علاوه چون با دات نت تهیه شده است، دیگر نیازی نیست دو نگارش 32 بیتی و 64 بیتی آن توزیع شوند و به این ترتیب مشکلات توزیع بانک‌های اطلاعاتی native مانند SQLite را ندارد ( و مطابق ادعای نویسنده آلمانی آن، از SQLite سریعتر است).
API این بانک اطلاعاتی، هر دو نوع متدهای synchronous  و  asynchronous را شامل می‌شود؛ به همین جهت با برنامه‌های ویندوز 8 و سیلورلایت نیز سازگاری دارد.
Lex.Db از برنامه‌های چندریسمانی و همچنین استفاده از یک بانک اطلاعاتی آن توسط چندین پروسه همزمان نیز پشتیبانی می‌کند.
در ادامه مروری خواهیم داشت بر نحوه استفاده از آن در حالت طراحی رابطه‌ای؛ از این جهت که فعلا به ظاهر این بانک اطلاعاتی روابط را پشتیبانی نمی‌کند، اما در عمل پیاده سازی آن مشکل نیست.

دریافت Lex.Db

برای دریافت Lex.Db، دستور ذیل را در خط فرمان پاورشل نیوگت وارد نمائید:
 PM> Install-Package Lex.Db
بسته به نوع پروژه شما (دات نت یا WinRT یا ...)، اسمبلی متناسبی به پروژه اضافه خواهد شد.


مدل‌های برنامه

    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
    }

    public class Order
    {
        public int Id { get; set; }
        public int? CustomerFK { get; set; }
        public int[] ProductsFK { get; set; }
    }
مدل‌های برنامه آزمایشی مطلب جاری را در اینجا ملاحظه می‌کنید. برای طراحی روابط یک به صفر یا یک و همچنین یک به چند، تنها کافی است کلیدهای اصلی یا آرایه‌ای از کلیدهای اصلی مرتبط را در اینجا ذخیره کنیم، که نمونه‌ای از آن‌را در کلاس Order ملاحظه می‌کنید.


آغاز بانک اطلاعاتی

    public static class Database
    {
        public static DbInstance Instance { get; private set; }

        public static DbTable<Product> Products { get; private set; }
        public static DbTable<Order> Orders { get; private set; }
        public static DbTable<Customer> Customers { get; private set; }

        /// <summary>
        /// سازنده استاتیکی که در طول عمر برنامه فقط یکبار اجرا می‌شود
        /// </summary>
        static Database()
        {
            createDb();
            getTables();
        }

        private static void getTables()
        {
            Products = Instance.Table<Product>();
            Customers = Instance.Table<Customer>();
            Orders = Instance.Table<Order>();
        }

        private static void createDb()
        {
            Instance = new DbInstance(Path.Combine(Environment.CurrentDirectory, "LexDbTests"));

            Instance.Map<Product>()
                    .WithIndex("NameIdx", x => x.Name)
                    .Automap(i => i.Id, true);

            Instance.Map<Order>()
                    .Automap(i => i.Id, true);

            Instance.Map<Customer>()
                    .WithIndex("NameIdx", x => x.Name)
                    .WithIndex("CityIdx", x => x.City)
                    .Automap(i => i.Id, true);

            Instance.Initialize();
        }
    }
کلاس دیتابیس و سازنده آن، استاتیک تعریف شده‌اند؛ تا در طول عمر برنامه تنها یکبار وهله سازی شوند. new DbInstance یک وهله جدید از بانک اطلاعاتی را آغاز می‌کند. سازنده آن، مسیر پوشه‌ای که فایل‌های این بانک اطلاعاتی در آن ذخیره خواهند شد را دریافت می‌کند. Lex.Db به ازای هر کلاس مدلی که به آن معرفی شود، دو فایل data و index را ایجاد می‌کند.
سپس توسط وهله‌ای از بانک اطلاعاتی که ایجاد کردیم، کار معرفی خواص مدل‌های برنامه توسط متد Map و Automap انجام می‌شود. متد Automap خاصیت primary key کلاس را دریافت کرده و همچنین پارامتر دوم آن مشخص می‌کند که آیا این کلید اصلی به صورت خودکار ایجاد شود یا خیر. به علاوه در همینجا می‌توان روی فیلدهای مختلف، ایندکس نیز ایجاد کرد. متد WithIndex یک نام دلخواه را دریافت کرده و سپس خاصیتی را که باید بر روی آن ایندکس ایجاد شود، دریافت می‌کند.
در نهایت متد Initialize باید فراخوانی گردد. البته اگر برنامه شما WinRT است، این متد Initialize Async خواهد بود.
جداول نیز بر اساس مدل‌های برنامه از طریق متد Instance.Table در دسترس قرار گرفته‌اند.

افزودن اطلاعات به بانک اطلاعاتی
        private static void addData()
        {
            var customer1 = new Customer { Name = "customer1", City = "City1" };
            var customer2 = new Customer { Name = "customer2", City = "City2" };
            Database.Instance.Save(customer1, customer2); // automatic Id assignment after Save

            var product1 = new Product { Name = "product1" };
            var product2 = new Product { Name = "product2" };
            Database.Instance.Save(product1, product2); // automatic Id assignment after Save

            var order1 = new Order { CustomerFK = customer1.Id, ProductsFK = new[] { product1.Id } };
            var order2 = new Order { CustomerFK = customer2.Id, ProductsFK = new[] { product1.Id, product2.Id } };
            Database.Instance.Save(order1, order2); // automatic Id assignment after Save
        }
اکنون که کار آغاز بانک اطلاعاتی صورت گرفت، برای افزودن اطلاعات از متد Database.Instance.Save می‌توان استفاده کرد (در برنامه‌های WinRT از  متد Save Async استفاده کنید).
در اینجا نیازی به ذکر Id نمونه‌های ساخته شده نیست؛ از این جهت که در حین عملیات Save، به صورت خودکار انتساب خواهند یافت.
همچنین نحوه مقدار دهی کلیدهای خارجی نیز با استفاده از همین کلیدهای اصلی آماده شده است.


واکشی تمام اطلاعات

        private static void loadAll()
        {
            var orders = Database.Orders.LoadAll();
            foreach (var order in orders)
            {
                // نحوه دریافت اطلاعات مشتری بر اساس کلید خارجی ثبت شده
                var orderCustomer = Database.Customers.LoadByKey(order.CustomerFK.Value);
                Console.WriteLine("Order Id: {0}, Customer: {1} ({2}) {3}", order.Id, orderCustomer.Name, orderCustomer.Id, orderCustomer.City);

                // نحوه بازیابی لیستی از اشیاء مرتبط از طریق آرایه‌ای از کلیدهای خارجی ثبت شده
                var orderProducts = Database.Products.LoadByKeys(order.ProductsFK);
                foreach (var product in orderProducts)
                {
                    Console.WriteLine("  Product Id: {0}, Name: {1}", product.Id, product.Name);
                }
            }
        }
بانک اطلاعاتی آغاز شد؛ تعدادی رکورد نیز در آن ثبت گردید. اکنون برای بازیابی اطلاعات می‌توان از متدهای در دسترس جداول کلاس Database استفاده کرد. برای مثال متد LoadAll تمام رکوردهای یک جدول را واکشی می‌کند (در برنامه‌های WinRT این متد LoadAll Async خواهد بود).
سپس با استفاده از متدهای LoadByKey و LoadByKeys، به سادگی می‌توان اشیاء مرتبط با هر سفارش را نیز واکشی کرد.


استفاده از ایندکس‌ها برای کوئری گرفتن

        private static void queryingByAnIndex()
        {
            var name = "customer1";
            var customersList = Database.Customers
                                        .IndexQueryByKey("NameIdx", name)
                                        .ToList();
            foreach (var person in customersList)
            {
                Console.WriteLine(person.Name);
            }
        }
در ابتدای بحث، توسط متد WithIndex، تعدادی ایندکس را نیز تعریف کردیم. اکنون توسط این ایندکس‌ها و متد IndexQueryByKey، می‌توان کوئری‌هایی بسیار سریع را تهیه کرد.
            // Using Take and Skip
            var list1 = Database.Orders.Query<int>() // primary idx
                                       .Take(1).Skip(2).ToList();

            // Querying Between Ranges 
            var list2 = Database.Customers
                                .IndexQuery<string>("NameIdx")
                                .GreaterThan("a", orEqual: true).LessThan("d").ToList();
همچنین در اینجا متدهایی مانند Take و Skip و یا جستجو در یک بازه توسط متدهای GreaterThan و LessThan نیز پشتیبانی می‌شوند.


حذف رکوردها
        private static void deletingRecords()
        {
            Database.Customers.DeleteByKey(key: 1);

            var customers = Database.Customers.LoadByKeys(new[] { 1, 2 });
            Database.Customers.Delete(customers);
        }
برای حذف رکوردها از متدهای DeleteByKey و یا Delete می‌توان استفاده کرد. متد Delete می‌تواند آرایه‌ای از اشیاء را نیز قبول کند.
و اگر خواستید کل بانک اطلاعاتی را خالی کنید، متد Database.Instance.Purge اینکار را انجام خواهد داد.


کدهای کامل این مثال را از اینجا نیز می‌توانید دریافت کنید:
Program-LexDb.cs
 
نظرات مطالب
متدهای کمکی مفید در پروژه های asp.net mvc
چه خطایی می‌ده؟ چون من الان استفاده کردم و خطایی نداد.
اگر یک چنین خطایی دریافت کردید:
The conversion of a varchar data type to a datetime data type resulted in an out-of-range value.
مربوط است به نال بودن یکی از مقادیر تاریخ ثبت شده در بانک اطلاعاتی. خاصیت را در سمت کدهای خود nullable تعریف کنید.