مطالب
Image Annotations
می‌خواهیم با تغییر jQuery Image Annotation  این پلاگین و برای asp.net استفاده کنیم

ایجاد دیتابیس
ابتدا یک دیتابیس به نام Coordinates  ایجاد کنید و سپس جدول زیر رو ایجاد کنید
USE [Coordinates]
GO
CREATE TABLE [dbo].[Coords2](
[top] [int] NULL,
[left] [int] NULL,
[width] [int] NULL,
[height] [int] NULL,
[text] [nvarchar](50) NULL,
[id] [uniqueidentifier] NULL,
[editable] [bit] NULL
) ON [PRIMARY]
GO

ایجاد کلاس Coords برای خواندن و ذخیره اطلاعات
public class Coords
{
    public string top;
    public string left;
    public string width;
    public string height;
    public string text;
    public string id;
    public string editable;

    public Coords(string top, string left, string width, string height, string text, string id, string editable)
    {
        this.top = top;
        this.left = left;
        this.width = width;
        this.height = height;
        this.text = text;
        this.id = id;
        this.editable = editable;


    }
}
فرم اصلی برنامه شامل 3 وب سرویس به شرح زیر می‌باشد

1-GetDynamicContext 
این متد در زمان لود اطلاعات از دیتابیس استفاده می‌شود (وقتی که postback صورت می‌گیرد)
[WebMethod]
    public static List<Coords> GetDynamicContext(string entryId, string entryName)
    {
        List<Coords> CoordsList = new List<Coords>();

        string connect = "Connection String";
        using (SqlConnection conn = new SqlConnection(connect))
        {
            string query = "SELECT [top], [left], width, height, text, id, editable FROM  Coords2";
            using (SqlCommand cmd = new SqlCommand(query, conn))
            {
                conn.Open();
                using (SqlDataReader reader=cmd.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        CoordsList.Add(new Coords(reader["top"].ToString(), reader["left"].ToString(),
                                                  reader["width"].ToString(), reader["height"].ToString(),
                                                  reader["text"].ToString(), reader["id"].ToString(),
                                                  reader["editable"].ToString()));
                    }
                }
               
               conn.Close();
            }
        }

         return CoordsList;


    }

2,3 -SaveCoordsو DeleteCoords 
این دو متد هم واسه ذخیره و حذف می‌باشند که نکته خاصی ندارند و خودتون بهینه اش کنید(در فایل ضمیمه موجودند)

تغییر فایل jquery.annotate.js  جهت فراخوانی وب سرویس ها
فقط لازمه که سه قسمت زیر رو در فایل اصلی تغییر بدید
$.fn.annotateImage.ajaxLoad = function (image) {
        ///<summary>
        ///Loads the annotations from the "getUrl" property passed in on the
        ///     options object.
        ///</summary>
      
        $.ajax({
            type: "POST",
            contentType: "application/json; charset=utf-8",
            url: "Default.aspx/GetDynamicContext",
            data: "{'entryId': '" + 1 + "','entryName': '" + 2 + "'}",
            dataType: "json",
            success: function (msg) {
                image.notes = msg.d;
                $.fn.annotateImage.load(image);
            }
        });
};

 $.fn.SaveCoords = function (note) {
        $.ajax({
            type: "POST",
            contentType: "application/json; charset=utf-8",
            url: "Default.aspx/SaveCoords",
            data: "{'top': '" + note.top + "','left': '" + note.left + "','width': '" + note.width + "','height': '" + note.height + "','text': '" + note.text + "','id': '" + note.id + "','editable': '" + note.editable + "'}",
            dataType: "json",
            success: function (msg) {
                note.id = msg.d;
             }
        });
 };
$.fn.annotateView.prototype.edit = function () {
///<summary>
///Edits the annotation.
///</summary>
if (this.image.mode == 'view') {
this.image.mode = 'edit';
var annotation = this;
// Create/prepare the editable note elements
var editable = new $.fn.annotateEdit(this.image, this.note);
$.fn.annotateImage.createSaveButton(editable, this.image, annotation);
// Add the delete button
var del = $('<a>حذف</a>');
del.click(function () {
var form = $('#image-annotate-edit-form form');
$.fn.annotateImage.appendPosition(form, editable)
if (annotation.image.useAjax) {
$.ajax({
type: "POST",
contentType: "application/json; charset=utf-8",
url: "Default.aspx/DeleteCoords",
// url: annotation.image.deleteUrl,
// data: form.serialize(),
data: "{'id': '" + editable.note.id + "'}",
dataType: "json",
success: function (msg) {
// image.notes = msg.d;
// $.fn.annotateImage.load(image);
},
error: function (e) { alert("An error occured deleting that note.") }
});
}
annotation.image.mode = 'view';
editable.destroy();
annotation.destroy();
});
editable.form.append(del);
$.fn.annotateImage.createCancelButton(editable, this.image);
}
};
این پروژه شامل یه سری فایل css هم هست که می‌تونید کل پروژه رو از  اینجا دانلود کنید
نظرات مطالب
خودکار کردن تعاریف DbSetها در EF Code first
در EF Core یک چنین شکلی را پیدا می‌کند (البته «روش صحیح بارگذاری پویای اسمبلی‌ها در NET Core.» را هم مدنظر داشته باشید):
    public class BloggingContext : DbContext
    {        
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            loadEntities(modelBuilder,
            asmPath: @"D:\Prog\SomeAsm1\bin\Debug\netstandard2.0\SomeAsm1.dll",
            nameSpace: "SomeAsm1.Models");
            base.OnModelCreating(modelBuilder);
        }

        private static void loadEntities(ModelBuilder modelBuilder, string asmPath, string nameSpace)
        {
            var modelInAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(asmPath);
            // var modelInAssembly = Assembly.Load(new AssemblyName("ModuleApp"));
            var entityMethod = typeof(ModelBuilder).GetMethod("Entity", new Type[] { });
            foreach (var type in modelInAssembly.ExportedTypes)
            {
                if (type.BaseType is System.Object && !type.IsAbstract && type.Namespace == nameSpace)
                {
                    entityMethod.MakeGenericMethod(type).Invoke(modelBuilder, new object[] { });
                }
            }
        }
مطالب
ذخیره‌ی سوابق کامل تغییرات یک رکورد در یک فیلد توسط Entity framework Core
در این مقاله، نوشته‌ی ایمان محمدی، ذخیره‌ی اطلاعات نظارتی هر Entity توسط دو فیلد CreatedSources و ModifiedSources به صورت JSON انجام می‌شود که در هر کدام از این فیلدها، اطلاعات مختلفی مانند ip کاربر، شناسه دستگاه، HostName، ClientName و یک سری اطلاعات دیگر ذخیره می‌شوند. بیایید به این اطلاعات متادیتا بگوییم. در این حالت اگر رکورد، چندین بار تغییر کند، متادیتای آخرین تغییرات در فیلد ModifiedSources ذخیره می‌شود. حالا اگر ما بخواهیم اطلاعات متادیتای همه‌ی تغییرات را داشته باشیم چه؟ اگر بخواهیم علاوه بر اطلاعات بالا، اینکه چه کسی و در چه زمانی این تغییرات را انجام داده است، نیز داشته باشیم چطور؟ اگر بخواهیم حتی اطلاعات متادیتای حذف یک رکورد را داشته باشیم چطور (در حالت soft-delete که رکورد واقعا پاک نمی‌شود)؟ سوال جالبتر اینکه اگر بخواهیم تمام تاریخچه‌ی مقادیر مختلف یک رکورد را از ابتدای ایجاد شدن داشته باشیم چطور؟ در این مقاله قصد داریم به همه‌ی این موارد اضافی برسیم؛ آن هم فقط با یک ستون در Entityهایمان، به اسم Audit!

ابتدا کلاس پایه موجودیت‌هایمان را تعریف می‌کنیم؛ تا بر روی Entityهایمان بتوانیم فیلد نظارتی Audit را اعمال کنیم:
public class BaseEntity : IBaseEntity
{
   [JsonIgnore]
   int Id { get; set; } 

   [JsonIgnore] 
    string? Audit { get; set; }
}
ویژگی [JsonIgnore]  به این منظور استفاده شده است تا از serialize کردن این فیلدها هنگام ایجاد Audit، جلوگیری شود؛ تا در نهایت حجم جیسن Audit کاهش یابد. با مطالعه‌ی ادامه‌ی مقاله، متوجه این قضیه خواهید شد.

دقیقا مانند مقاله‌ی اشاره شده (که خواندن آن توصیه می‌شود)، کلاس AuditSourceValues را ایجاد می‌کنیم:
public class AuditSourceValues
{
    [JsonProperty("hn")]
    public string? HostName { get; set; }

    [JsonProperty("mn")]
    public string? MachineName { get; set; }

    [JsonProperty("rip")]
    public string? RemoteIpAddress { get; set; }

    [JsonProperty("lip")]
    public string? LocalIpAddress { get; set; }

    [JsonProperty("ua")]
    public string? UserAgent { get; set; }

    [JsonProperty("an")]
    public string? ApplicationName { get; set; }

    [JsonProperty("av")]
    public string? ApplicationVersion { get; set; }

    [JsonProperty("cn")]
    public string? ClientName { get; set; }

    [JsonProperty("cv")]
    public string? ClientVersion { get; set; }

    [JsonProperty("o")]
    public string? Other { get; set; }
}
با تعریف کردن نام برای فیلد‌های JSON و نادیده گرفتن مقادیر نال، سعی کردیم حجم خروجی JSON پایین باشد.

اکنون کلاس EntityAudit را ایجاد می‌کنیم که شامل تمامی اطلاعات مورد نیاز ما برای ثبت تاریخچه‌ی کامل هر موجودیت است:
public class EntityAudit<TEntity>
{
    [JsonProperty("type")]
    [JsonConverter(typeof(StringEnumConverter))]
    public EntityEventType EventType { get; set; }

    [JsonProperty("user", NullValueHandling = NullValueHandling.Include)]
    public int? ActorUserId { get; set; }

    [JsonProperty("at")]
    public DateTime ActDateTime { get; set; }

    [JsonProperty("sources")]
    public AuditSourceValues? AuditSourceValues { get; set; }

    [JsonProperty("newValues", NullValueHandling = NullValueHandling.Include)]
    public TEntity NewEntity { get; set; } = default!;

    public string? SerializeJson()
    {
        return JsonSerializer.Serialize(this, 
            options: new JsonSerializerOptions { WriteIndented = false, IgnoreNullValues = true }); 
    }
}

دقت کنید که این کلاس به صورت جنریک تعریف شده است تا اگر بعدا بخواهیم آن را Deserialize کنیم و مثلا از آن API بسازیم، یا استفاده‌ی خاصی را از آن داشته باشیم، به‌راحتی به Entity مد نظر تبدیل شود. در این مقاله فقط به ذخیره‌ی آن پرداخته می‌شود و استفاده از این فیلد که به راحتی و با کمک DbFunctionها در Entity Framework قابل انجام است به خواننده واگذار می‌شود. 

همچنین اینام EntityEventType که تعریف آن در زیر می‌آید دارای ویژگی [JsonConverter(typeof(StringEnumConverter))]  می‌باشد تا مقدار رشته‌ای آن را بجای مقدار عددی، در خروجی جیسن داشته باشیم. این اینام، شامل  تمامی عملیاتی است که بر روی یک رکورد قابل انجام است و به این صورت تعریف می‌شود:
public enum EntityEventType
{
    Create = 0,
    Update = 1,
    Delete = 2
}

تامین اطلاعات کلاس AuditSourceValues به همان صورت است که در مقاله‌ی اشاره شده آمده‌است؛ ابتدا تعریف اینترفیس IAuditSourcesProvider و سپس ایجاد کلاس AuditSourcesProvider:
public interface IAuditSourcesProvider
{
    AuditSourceValues GetAuditSourceValues();
}
public class AuditSourcesProvider : IAuditSourcesProvider
{
    protected readonly IHttpContextAccessor HttpContextAccessor;

    public AuditSourcesProvider(IHttpContextAccessor httpContextAccessor)
    {
        HttpContextAccessor = httpContextAccessor;
    }

    public virtual AuditSourceValues GetAuditSourceValues()
    {
        var httpContext = HttpContextAccessor.HttpContext;

        return new AuditSourceValues
        {
            HostName = GetHostName(httpContext),
            MachineName = GetComputerName(httpContext),
            LocalIpAddress = GetLocalIpAddress(httpContext),
            RemoteIpAddress = GetRemoteIpAddress(httpContext),
            UserAgent = GetUserAgent(httpContext),
            ApplicationName = GetApplicationName(httpContext),
            ClientName = GetClientName(httpContext),
            ClientVersion = GetClientVersion(httpContext),
            ApplicationVersion = GetApplicationVersion(httpContext),
            Other = GetOther(httpContext)
        };
    }

    protected virtual string? GetUserAgent(HttpContext httpContext)
    {
        return httpContext.Request?.Headers["User-Agent"].ToString();
    }

    protected virtual string? GetRemoteIpAddress(HttpContext httpContext)
    {
        return httpContext.Connection?.RemoteIpAddress?.ToString();
    }

    protected virtual string? GetLocalIpAddress(HttpContext httpContext)
    {
        return httpContext.Connection?.LocalIpAddress?.ToString();
    }

    protected virtual string GetHostName(HttpContext httpContext)
    {
        return httpContext.Request.Host.ToString();
    }

    protected virtual string GetComputerName(HttpContext httpContext)
    {
        return Environment.MachineName;
    }
    protected virtual string? GetApplicationName(HttpContext httpContext)
    {
        return Assembly.GetEntryAssembly()?.GetName().Name;
    }

    protected virtual string? GetApplicationVersion(HttpContext httpContext)
    {
        return Assembly.GetEntryAssembly()?.GetName().Version.ToString();
    }

    protected virtual string? GetClientVersion(HttpContext httpContext)
    {
        return httpContext.Request?.Headers["client-version"];
    }
    protected virtual string? GetClientName(HttpContext httpContext)
    {
        return httpContext.Request?.Headers["client-name"];
    }

    protected virtual string? GetOther(HttpContext httpContext)
    {
        return null;
    }
}

حالا برای تامین اطلاعات کلاس EntityAudit کار مشابهی می‌کنیم. ابتدا اینترفیس IEntityAuditProvider را به صورت زیر تعریف می‌کنیم: 
public interface IEntityAuditProvider
{
    string? GetAuditValues(EntityEventType eventType, object? entity, string? previousJsonAudit = null);
}

  و سپس کلاس EntityAuditProvider را ایجاد می‌کنیم:
public class EntityAuditProvider : IEntityAuditProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IAuditSourcesProvider _auditSourcesProvider;

    #region Constructor Injections

    public EntityAuditProvider(IHttpContextAccessor httpContextAccessor, IAuditSourcesProvider auditSourcesProvider)
    {
        _httpContextAccessor = httpContextAccessor;
        _auditSourcesProvider = auditSourcesProvider;
    }

    #endregion

    public virtual string? GetAuditValues(EntityEventType eventType, object? newEntity, string? previousJsonAudit = null)
    {
        var httpContext = _httpContextAccessor.HttpContext;
        int? userId;

        var user = httpContext.User;

        if (!user.Identity.IsAuthenticated)
            userId = null;
        else
            userId = user.Claims.Where(x => x.Type == "UserID").Select(x => x.Value).First().ToInt();

        var auditSourceValues = _auditSourcesProvider.GetAuditSourceValues();

        var auditJArray = new JArray();

        // Update & Delete
        if (eventType == EntityEventType.Update || eventType == EntityEventType.Delete)
        {
            auditJArray = JArray.Parse(previousJsonAudit!);
        }

        // Delete => No NewValues
        if (eventType == EntityEventType.Delete)
        {
            newEntity = null;
        }

        JObject newAuditJObject = JObject.FromObject(new EntityAudit<object?>
        {
            EventType = eventType,
            ActorUserId = userId,
            ActDateTime = DateTime.Now,
            AuditSourceValues = auditSourceValues,
            NewEntity = newEntity
        }, new JsonSerializer
        {
            NullValueHandling = NullValueHandling.Ignore,
            Formatting = Formatting.None
        });

        auditJArray.Add(newAuditJObject);

        return auditJArray.SerializeToJson(true);
    }
}
در این کلاس برای اینکه به جیسن قبلی Audit که تاریخچه‌ی قبلی رکورد می‌باشد یک آیتم را اضافه کنیم، از JArray و JObject پکیج Newtonsoft استفاده کرد‌ه‌ایم.

حالا همه چیز آماده است. مانند مقاله‌ی اشاره شده، از مفهوم Interceptor استفاده می‌کنیم. کلاس AuditSaveChangesInterceptor را که از کلاس SaveChangesInterceptor مشتق می‌شود، به صورت زیر ایجاد می‌کنیم: 
public class AuditSaveChangesInterceptor : SaveChangesInterceptor
{
    private readonly IEntityAuditProvider _entityAuditProvider;

    #region Constructor Injections

    public AuditSaveChangesInterceptor(IEntityAuditProvider entityAuditProvider)
    {
        _entityAuditProvider = entityAuditProvider;
    }

    #endregion

    public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
    {
        ApplyAudits(eventData.Context.ChangeTracker);
        return base.SavingChanges(eventData, result);
    }

    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result,
        CancellationToken cancellationToken = new CancellationToken())
    {
        ApplyAudits(eventData.Context.ChangeTracker);
        return base.SavingChangesAsync(eventData, result, cancellationToken);
    }

    private void ApplyAudits(ChangeTracker changeTracker)
    {
        ApplyCreateAudits(changeTracker);
        ApplyUpdateAudits(changeTracker);
        ApplyDeleteAudits(changeTracker);
    }

    private void ApplyCreateAudits(ChangeTracker changeTracker)
    {
        var addedEntries = changeTracker.Entries()
            .Where(x => x.State == EntityState.Added);

        foreach (var addedEntry in addedEntries)
        {
            if (addedEntry.Entity is IBaseEntity entity)
            {              
                entity.Audit = _entityAuditProvider.GetAuditValues(EntityEventType.Create, entity);
            }
        }
    }

    private void ApplyUpdateAudits(ChangeTracker changeTracker)
    {
        var modifiedEntries = changeTracker.Entries()
            .Where(x => x.State == EntityState.Modified);

        foreach (var modifiedEntry in modifiedEntries)
        {
            if (modifiedEntry.Entity is IBaseEntity entity)
            {
                var eventType = entity.IsArchived ? EntityEventType.Delete : EntityEventType.Update; // Maybe Soft Delete
                entity.Audit = _entityAuditProvider.GetAuditValues(eventType, entity, entity.Audit);
            }
        }
    }

    private void ApplyDeleteAudits(ChangeTracker changeTracker)
    {
        var deletedEntries = changeTracker.Entries()
            .Where(x => x.State == EntityState.Deleted);

        foreach (var modifiedEntry in deletedEntries)
        {
            if (modifiedEntry.Entity is IBaseEntity entity)
            {
                entity.Audit = _entityAuditProvider.GetAuditValues(EntityEventType.Delete, entity, entity.Audit);
            }
        }
    }

}


و سپس آن را به سیستم معرفی می‌کنیم:

services.AddDbContext<ATADbContext>((serviceProvider, options) =>
{
    options
        .UseSqlServer(...)

    // Interceptors
    var entityAuditProvider = serviceProvider.GetRequiredService<IEntityAuditProvider>();
    options.AddInterceptors(new AuditSaveChangesInterceptor(entityAuditProvider));

});

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

نمونه‌ی نتیجه‌ای را که از این روش بدست می‌آید، در این تصویر می‌بینید. اگر بخواهید به صورت نرم‌افزاری یا کدی از این دیتا استفاده کنید، باید آن را Deserialize کنید که همانطور که گفته شد با امکاناتی که SQL Server برای خواندن فیلدهای JSON دارد و معرفی آن به EF، قابل انجام است. در غیر اینصورت استفاده از این دیتا به صورت چشمی یا استفاده از Json Formatterها به‌راحتی امکان پذیر است. 

 
نمونه‌ی کامل فیلد Audit که در JsonFormatter قرار داده شده است، بعد از ایجاد شدن و یکبار آپدیت و سپس حذف نرم رکورد:
[
   {
      "type":"Create",
      "user":1,
      "at":"2020-11-24T23:05:54.2692711+03:30",
      "sources":{
         "hn":"localhost:44398",
         "mn":"DESKTOP-N1GAV2U",
         "rip":"::1",
         "lip":"::1",
         "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
         "an":"Server.Api",
         "av":"1.0.0.0"
      },
      "newValues":{               
         "Name":"Farshad"
      }
   },
   {
      "type":"Update",
      "user":1,
      "at":"2020-11-24T23:06:20.0838188+03:30",
      "sources":{
         "hn":"localhost:44398",
         "mn":"DESKTOP-N1GAV2U",
         "rip":"::1",
         "lip":"::1",
         "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
         "an":"Server.Api",
         "av":"1.0.0.0"
      },
      "newValues":{                 
         "Name":"Edited Farshad"
      }
   },
   {
      "type":"Delete",
      "user":null,
      "at":"2020-11-24T23:06:28.601837+03:30",
      "sources":{
         "hn":"localhost:44398",
         "mn":"DESKTOP-N1GAV2U",
         "rip":"::1",
         "lip":"::1",
         "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
         "an":"Server.Api",
         "av":"1.0.0.0"
      },
      "newValues":null
   }
]

یک روش مرسوم داشتن تاریخچه‌ی تغییرات رکورد که با جستجو در اینترنت نیز می‌توان به آن رسید، داشتن یک جدول جداگانه به اسم Audit است که با هر بار تغییر هر Entity، یک رکورد در آن ایجاد می‌شود. ساختار آن مانند تصاویر زیر است:


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

مطالب
تبدیل یک View به رشته و بازگشت آن به همراه نتایج JSON حاصل از یک عملیات Ajax ایی در ASP.NET MVC

ممکن است بخواهیم در پاسخ یک تقاضای Ajax ایی، اگر عملیات در سمت سرور با موفقیت انجام شد، خروجی یک Controller action را به کاربر نهایی نشان دهیم. در چنین سناریویی لازم است که بتوانیم خروجی یک action را بصورت رشته برگردانیم. در این مقاله به این مسئله خواهیم پرداخت .
فرض کنید در یک سیستم وبلاگ ساده قصد داریم امکان کامنت گذاشتن بصورت
Ajax را پیاده سازی کنیم. یک ایده عملی و کارآ این است: بعد از اینکه کاربر متن کامنت را وارد کرد و دکمه‌ی ارسال کامنت را زد، تقاضا به سمت سرور ارسال شود و اگر سرور پیغام موفقیت را صادر کرد، متن نوشته شده توسط کاربر را به کمک کدهای JavaScript و در همان سمت کلاینت بصورت یک کادر کامنت جدید به محتوای صفحه اضافه کنیم. بنده در اینجا برای اینکه بتوانم اصل موضوع مورد بحث را توضیح دهم، از یک سناریوی جایگزین استفاده می‌کنم؛ کاربر موقعیکه دکمه ارسال را زد، تقاضا به سرور ارسال میشود. سرور بعد از انجام عملیات، تحت یک شی  JSON هم نتیجه‌ی انجام عملیات و هم محتوای HTML نمایش کامنت جدید در صفحه را به سمت کلاینت ارسال خواهد کرد و کلاینت در صورت موفقیت آمیز بودن عملیات، آن محتوا را به صفحه اضافه می‌کند.

با توجه به توضیحات داده شده، ابتدا یک شیء نیاز داریم تا بتوانیم توسط آن نتیجه‌ی عملیات Ajax ایی را بصورت  JSON به سمت کلاینت ارسال کنیم:

public class MyJsonResult
{
  public bool success { set; get; }
  public bool HasWarning { set; get; }
  public string WarningMessage { set; get; }
  public int errorcode { set; get; }
public string message {set; get; }   public object data { set; get; }  }

سپس به متدی نیاز داریم که کار تبدیل نتیجه‌ی action را به رشته، انجام دهد:

public static string RenderViewToString(ControllerContext context,
    string viewPath,
    object model = null,
    bool partial = false) 
{
    ViewEngineResult viewEngineResult = null;
    if (partial) viewEngineResult = ViewEngines.Engines.FindPartialView(context, viewPath);
    else viewEngineResult = ViewEngines.Engines.FindView(context, viewPath, null);
    if (viewEngineResult == null) throw new FileNotFoundException("View cannot be found.");
    var view = viewEngineResult.View;
    context.Controller.ViewData.Model = model;
    string result = null;
    using(var sw = new StringWriter()) {
        var ctx = new ViewContext(context, view, context.Controller.ViewData, context.Controller.TempData, sw);
        view.Render(ctx, sw);
        result = sw.ToString();
    }
    return result;
}
در اینجا موتور View را بر اساس اطلاعات یک View، مدل و سایر اطلاعات Context جاری کنترلر، وادار به تولید معادل رشته‌ای آن می‌کنیم.

فرض کنیم در سمت Controller هم از کدی شبیه به این استفاده میکنیم:
public JsonResult AddComment(CommentViewModel model) {
    MyJsonResult result = new MyJsonResult() {
        success = false;
    };
    if (!ModelState.IsValid) {
        result.success = false;
        result.message = "لطفاً اطلاعات فرم را کامل وارد کنید";
        return Json(result);
    }
    try {
        Comment theComment = model.toCommentModel();
        //EF service factory
        Factory.CommentService.Create(theComment);
        Factory.SaveChanges();
        result.data = Tools.RenderViewToString(this.ControllerContext, "/views/posts/_AComment", model, true);
        result.success = true;
    } catch (Exception ex) {
        result.success = false;
        result.message = "اشکال زمان اجرا";
    }
    return Json(result);
}

و در سمت کلاینت برای ارسال Form به صورت Ajax ایی خواهیم داشت:

@using (Ajax.BeginForm("AddComment", "posts", 
new AjaxOptions()
{
   HttpMethod = "Post", 
   OnSuccess = "AddCommentSuccess", 
   LoadingElementId = "AddCommentLoading"
}, new { id = "frmAddComment", @class = "form-horizontal" }))
{ 
    @Html.HiddenFor(m => m.PostId)
    <label for="fname">@Texts.ContactName</label> 
    <input type="text" id="fname" name="FullName" class="form-control" placeholder="@Texts.ContactName ">
    <label for="email">@Texts.Email</label> 
    <input type="email" id="InputEmail" name="email" class="form-control" placeholder="@Texts.Email">
    <br><textarea name="C_Content" cols="60" rows="10" class="form-control"></textarea><br>
    <input type="submit" value="@Texts.SubmitComments" name="" class="btn btn-primary">
    <div class="loading-mask" style="display:none">@Texts.LoadingMessage</div>
}
در اینجا در صورت موفقیت آمیز بودن عملیات، متد جاوا اسکریپتی AddCommentSuccess فراخوانی خواهد شد.
باید توجه شود Texts در اینجا یک Resource هست که به منظور نگهداری کلمات استفاده شده در سایت، برای زبانهای مختلف استفاده می‌شود (رجوع شود به مفهوم بومی سازی در Asp.net) .

و در قسمت script ‌ها داریم:

<script type="text/javascript">
  function AddCommentSuccess(jsData) {
   if (jsData && jsData.message)
    alert(jsData.message);
   if (jsData && jsData.success) {
    document.getElementById("frmAddComment").reset();
      //افزودن کامنت جدید ساخته شده توسط کاربر به لیست کامنتهای صفحه
    $("#divAllComments").html(jsData.data + $("#divAllComments").html());    
   }
  }
</script>
متد AddCommentSuccess اطلاعات شیء JSON بازگشتی از کنترلر را دریافت و سپس پیام آن‌را در صورت موفقیت آمیز بودن عملیات، به DIV ایی با id مساوی divAllComments اضافه می‌کند.

پاسخ به بازخورد‌های پروژه‌ها
خطای property not found
ممنون از پاسخ شما.
این منبع داده گزارش :
dataSource.StronglyTypedList<WeekDayClassSessionSemiRow>(DataSource);
این هم مدل‌های من :
public class WeekDayClassSessionSemi
    {
        public int DayNumber { get; set; }
        public int WeekNumber { get; set; }
        public bool HasSession { get; set; }
        public int Percent { get; set; }
        public string Text { get; set; }
        public string Date { get; set; }
        public string SemiDate { get; set; }
        public bool IsHoliday { get; set; }
    }
    public class WeekDayClassSessionSemiRow
    {
        public string RowNumber { get; set; }
        public bool IsSelected { get; set; }
        public Class Class { get; set; }
        public string ClassName { get; set; }
        public string ClassFullName { get; set; }
        public int WeekNumber { get; set; }
        public string WeekTitle { get; set; }
        public WeekDayClassSessionSemi WD0 { get; set; }
        public WeekDayClassSessionSemi WD1 { get; set; }
        public WeekDayClassSessionSemi WD2 { get; set; }
        public WeekDayClassSessionSemi WD3 { get; set; }
        public WeekDayClassSessionSemi WD4 { get; set; }
        public WeekDayClassSessionSemi WD5 { get; set; }
        public WeekDayClassSessionSemi WD6 { get; set; }
    }
یک سری اطلاعات را ار بانک بصورت زیر لود میکنم:
public List<Session> GetClassesSessions(int centerId, int termId, List<int> classIds)
        {
            var query = _tEntities
                    .Include(p => p.Program)
                    .Include(p => p.Program.Lesson)
                    .Include(p => p.Program.Trend)
                    .Include(p => p.Program.Teacher)
                    .Include(p => p.Class)
                    .Include(p => p.Class.EducationalPlace)
                    .Include(p => p.Class.ClassType)
                .Where(p => p.Program.Center_Id == centerId)
                .Where(p => p.Program.Term_Id == termId)
                .Where(BuildOrExpressionExtension.BuildOrExpression<Session, int>(u => u.Class_Id, classIds));
            return query.ToList();
        }
_sessionService = ObjectFactory.GetInstance<ISessionService>();
var sessionList = _sessionService.GetClassesSessions(centerId, termId, classIds);
سپس منبع داده موردنیازم را که لیستی از WeekDayClassSessionSemiRow است ایجاد میکنم و به گزارش ارسال میکنم ولی باز خطای Percent property not found را دریافت می‌کنم.
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 18 - کار با ASP.NET Web API
ارتقاء به ASP.NET Core 2.1: بهبود اعتبارسنجی پارامترها

تا پیش از نگارش 2.1، برای اعمال اعتبارسنجی به اطلاعات دریافتی از کاربر باید به صورت زیر عمل کرد:
public class UserModel   
{
   [Required, EmailAddress]
   public string Email { get; set; }
 
   [Required, StringLength(1000)]
   public string Name { get; set; }
}
اطلاعات مدنظر به صورت یک کلاس مدل تعریف شده و سپس ویژگی‌های اعتبارسنجی به خواص این کلاس اضافه می‌شوند.
در این حالت در اکشن متد تعریفی با بررسی ModelState.IsValid می‌توان وضعیت اعتبارسنجی اطلاعات دریافتی از سمت کاربر را مشاهده کرد:
public IActionResult SaveUser(UserModel model)
{
     if(!ModelState.IsValid)
     {

 در نگارش 2.1 الزامی به تعریف این کلاس مدل نیست و ویژگی‌های اعتبارسنجی را به پارامترهای تعریف اکشن متد هم می‌توان اعمال کرد:
public IActionResult SaveUser(
     [Required, EmailAddress] string Email  
     [Required, StringLength(1000)] string Name)
{
    if(!ModelState.IsValid)

یک نکته‌ی تکمیلی: اعمال ویژگی Required به non-nullable value types تاثیری ندارد. به همین جهت ویژگی دیگری به نام BindRequired نیز در اینجا اضافه شده‌است تا برای نمونه در مثال زیر اطمینان حاصل شود که testId از مقادیر route و qty از مقادیر کوئری استرینگ مقدار دهی شده‌اند:
public IActionResult Get([BindRequired, FromRoute] Guid testId, [BindRequired, FromQuery] int qty)   
{
   if(!ModelState.IsValid)

- به این ترتیب می‌توان تعداد ViewModelهای مورد نیاز یک برنامه را به شدت کاهش داد. البته نکته‌ی «بررسی Bad code smell ها: تعداد زیاد پارامترهای ورودی» و «آشنایی با Refactoring - قسمت 7» را هم مدنظر داشته باشید و زیاده‌روی نکنید!
- همچنین اگر ویژگی [ApiController] را نیز به کنترلر جاری اعمال کنید، بررسی ModelState.IsValid نیز به صورت خودکار انجام خواهد شد و نیازی به کدنویسی اضافه‌تری نخواهد داشت.
مطالب دوره‌ها
کار با AutoMapper زمانیکه نوع منبع داده مورد استفاده مشخص نیست
در سناریوهای متداول نگاشت اشیاء، مشخص است که نوع ViewModel برنامه چیست و معادل Model آن کدام است. اما حالت‌هایی مانند کار با anonymous objects و یا data reader و data table و امثال آن نیز وجود دارند که در این حالت‌ها، نوع منبع داده‌ی مورد استفاده، شیء مشخصی نیست که بتوان آن‌را در قسمت CreateMap مشخص کرد. برای مدیریت یک چنین حالت‌هایی، متد DynamicMap طراحی شده‌است.

مثال اول: تبدیل یک DataTable به لیست جنریک معادل

فرض کنید یک DataTable را با ساختار و داده‌های ذیل در اختیار داریم:
var dataTable = new DataTable("SalaryList");
dataTable.Columns.Add("User", typeof (string));
dataTable.Columns.Add("Month", typeof (int));
dataTable.Columns.Add("Salary", typeof (decimal));
 
var rnd = new Random();
for (var i = 0; i < 200; i++)
  dataTable.Rows.Add("User " + i, rnd.Next(1, 12), rnd.Next(400, 2000));
نوع این DataTable کاملا پویا است و می‌تواند هربار در قسمت‌های مختلف برنامه تعریف متفاوتی داشته باشد.
در ادامه معادل کلاس ساختار ستون‌های این DataTable را به صورت ذیل تهیه می‌کنیم.
public class SalaryList
{
  public string User { set; get; }
  public int Month { set; get; }
  public decimal Salary { set; get; }
}
اکنون می‌خواهیم اطلاعات DataTable را به لیستی جنریک از SalaryList نگاشت کنیم. برای اینکار تنها کافی است از متد DaynamicMap استفاده نمائیم:
var salaryList = AutoMapper.Mapper.DynamicMap<IDataReader, List<SalaryList>>(dataTable.CreateDataReader());
منبع داده را از نوع IDataReader بر اساس متد CreateDataReader مشخص کرده‌ایم. به این ترتیب AutoMapper قادر خواهد بود تا اطلاعات این DataTable را به صورت خودکار پیمایش کند. سپس مقصد را نیز لیست جنریکی از کلاس SalaryList تعیین کرده‌ایم. مابقی کار را متد DynamicMap انجام می‌دهد.
کار با AutoMapper نسبت به راه حل‌های Reflection متداول بسیار سریعتر است. زیرا AutoMapper از مباحث Fast reflection به صورت توکار استفاده می‌کند.


مثال دوم: تبدیل لیستی از اشیاء anonymous به لیستی جنریک

در اینجا قصد داریم یک شیء anonymous را به شیء معادل SalaryList آن نگاشت کنیم. این‌کار را نیز می‌توان توسط متد DynamicMap انجام داد:
var anonymousObject = new
{
  User = "User 1",
  Month = 1,
  Salary = 100000
};
var salary = Mapper.DynamicMap<SalaryList>(anonymousObject);
و یا نمونه‌ی دیگر آن تبدیل یک لیست anonymous به معادل جنریک آن است که به نحو ذیل قابل انجام است:
var anonymousList = new[]
{
  new
  {
   User = "User 1",
   Month = 1,
   Salary = 100000
  },
  new
  {
   User = "User 2",
   Month = 1,
   Salary = 300000
  }
};
var salaryList = anonymousList.Select(item => Mapper.DynamicMap<SalaryList>(item)).ToList();
این نکته در مورد حاصل کوئری‌های LINQ یا IQueryable‌ها نیز صادق است.


مثال سوم: نگاشت پویا به یک اینترفیس

فرض کنید یک چنین اینترفیسی، در برنامه تعریف شده‌است و همچنین دارای هیچ نوع پیاده سازی هم در برنامه نیست:
public interface ICustomerService
{
  string Code { get; set; }
  string Name { get; set; }
}
اکنون قصد داریم یک شیء anonymous را به آن نگاشت کنیم:
var anonymousObject = new
{
  Code = "111",
  Name = "Test 1"
};
var result = Mapper.DynamicMap<ICustomerService>(anonymousObject);
در این حالت خاص، AutoMapper با استفاده از یک Dynamic Proxy به نام LinFu (که با اسمبلی آن Merge شده‌است)، پیاده سازی پویایی را از اینترفیس مشخص شده تهیه کرده و سپس کار نگاشت را انجام می‌دهد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: 
AM_Sample05.zip
مطالب
ایجاد یک ActionFilter جهت تمیز کردن اطلاعات ورودی در ASP.NET Core
در  ASP.NET Core کار جلوگیری از حملات  XSS بر عهده برنامه نویس گذاشته شده‌است و مانند نسخه‌های قبلی، Request Validation یا اعتبارسنجی درخواست‌ها به صورت توکار در آن وجود ندارد. برای اطلاعات بیشتر به این مقاله مراجعه کنید.

هرچند ASP.NET Core داده‌ها را هنگام نمایش، encode می‌کند و عملا بسیاری از حملات خنثی می‌شوند، اما در صورتیکه بخواهیم داده‌های غیر مطمئن، در بانک اطلاعاتی نیز ذخیره نشوند، باید آنها را ارزیابی کنیم. یکی از روش‌های مقابله‌ی با این حملات، تمیز کردن اطلاعات ورودی کاربر است. در ادامه به ایجاد یک ActionFilter می‌پردازیم تا این کار را برای ورودی‌های یک Action Method انجام دهد:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
    public class SanitizeInputAttribute : ActionFilterAttribute
    {
        var sanitizer = new Ganss.XSS.HtmlSanitizer(); 

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            if (filterContext.ActionArguments != null)
            {
                foreach (var parameter in filterContext.ActionArguments)
                {
                    var properties = parameter.Value.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public)
                        .Where(x => x.CanRead && x.CanWrite && x.PropertyType == typeof(string) && x.GetGetMethod(true).IsPublic && x.GetSetMethod(true).IsPublic);
                    foreach (var propertyInfo in properties)
                    {
                        if (propertyInfo.GetValue(parameter.Value) != null)
                            propertyInfo.SetValue(parameter.Value,
                                sanitizer.Sanitize(propertyInfo.GetValue(parameter.Value).ToString()));  
                    }
                }
            }
        }
    }
در این فیلتر، از کتابخانه
HtmlSanitizer برای تمیز کردن اطلاعات استفاده کرده‌ایم. ابتدا تمام ورودی‌های اکشن متد را خوانده و سپس ورودی‌هایی را که از نوع  string هستند، پیدا کرده و مقدار فعلی آنها را با مقدار sanitize شده، جایگزین می‌کند. بنابراین اگر در رشته‌ی ورودی، عبارت یا تگ خطرناک یا غیر مجازی باشد، حذف می‌گردد.

برای استفاده از این فیلتر کافی است به صورت زیر عمل کنیم:
[SanitizeInput]
public IActionResult Add(GroupDto dto)
مطالب
بررسی روش آپلود فایل‌ها در ASP.NET Core
مدیریت پردازش آپلود فایل‌ها در ASP.NET Core نسبت به ASP.NET MVC 5.x به طور کامل تغییر کرده‌است و اینبار بجای ذکر نوع System.Web.HttpPostedFileBase باید از اینترفیس جدید IFormFile واقع در فضای نام Microsoft.AspNetCore.Http کمک گرفت.


مراحل فعال سازی آپلود فایل‌ها در ASP.NET Core

مرحله‌ی اول فعال سازی آپلود فایل‌ها در ASP.NET Core، شامل افزودن ویژگی "enctype="multipart/form-data به یک فرم تعریف شده‌است:
<form method="post"
      asp-action="Index"
      asp-controller="TestFileUpload"
      enctype="multipart/form-data">
    <input type="file" name="files" multiple />
    <input type="submit" value="Upload" />
</form>
در اینجا همچنین ذکر ویژگی multiple در input از نوع file، امکان ارسال چندین فایل با هم را نیز میسر می‌کند.
در سمت سرور، امضای اکشن متد دریافت کننده‌ی این فایل‌ها به صورت ذیل خواهد بود:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Index(IList<IFormFile> files)
در اینجا نام پارامتر تعریف شده، باید دقیقا مساوی نام input از نوع file باشد. همچنین از آنجائیکه ویژگی multiple را نیز در سمت کلاینت قید کرده‌ایم، این پارامتر سمت سرور از نوع یک لیست، تعریف شده‌است. اگر ویژگی multiple را حذف کنیم می‌توان آن‌را به صورت ساده‌ی IFormFile files نیز تعریف کرد.


یافتن جایگزینی برای Server.MapPath در ASP.NET Core

زمانیکه فایل ارسالی، در سمت سرور دریافت شد، مرحله‌ی بعد، ذخیره سازی آن بر روی سرور است و از آنجائیکه ما دقیقا نمی‌دانیم ریشه‌ی سایت در کدام پوشه‌ی سرور واقع شده‌است، می‌شد از متد Server.MapPath برای یافتن دقیق آن کمک گرفت. با حذف این متد در ASP.NET Core، روش یافتن ریشه‌ی سایت یا همان پوشه‌ی wwwroot در اینجا شامل مراحل ذیل است:
public class TestFileUploadController : Controller
{
    private readonly IHostingEnvironment _environment;
    public TestFileUploadController(IHostingEnvironment environment)
    {
        _environment = environment;
    }
ابتدا اینترفیس توکار IHostingEnvironment را در سازنده‌ی کلاس تزریق می‌کنیم. سرویس HostingEnvironment جزو سرویس‌های از پیش تعریف شده‌ی ASP.NET Core است و نیازی به تنظیمات اضافه‌تری ندارد. همینقدر که ذکر شود، به صورت خودکار توسط ASP.NET Core مقدار دهی و تامین می‌گردد.
پس از آن خاصیت environment.WebRootPath_ به ریشه‌ی پوشه‌ی wwwroot برنامه، بر روی سرور اشاره می‌کند. به این ترتیب می‌توان مسیر دقیقی را جهت ذخیره سازی فایل‌های رسیده، مشخص کرد.


امکان ذخیره سازی async فایل‌ها در ASP.NET Core

عملیات کار با فایل‌ها، عملیاتی است که از مرزهای IO سیستم عبور می‌کند. به همین جهت یکی از بهترین مثال‌های پیاده سازی async، جهت رها سازی تردهای برنامه و بالا بردن میزان پاسخ‌دهی آن با بالا بردن تعداد تردهای آزاد بیشتر است. در ASP.NET Core، نوشتن async محتوای فایل رسیده در یک stream پشتیبانی می‌شود و این stream می‌تواند یک FileStream و یا MemoryStream باشد. در ذیل نحوه‌ی کار async با یک FileStream را مشاهده می‌کنید:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Index(IList<IFormFile> files)
{
    var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
    if (!Directory.Exists(uploadsRootFolder))
    {
        Directory.CreateDirectory(uploadsRootFolder);
    }
 
    foreach (var file in files)
    {
        if (file == null || file.Length == 0)
        {
            continue;
        }
 
        var filePath = Path.Combine(uploadsRootFolder, file.FileName);
        using (var fileStream = new FileStream(filePath, FileMode.Create))
        {
            await file.CopyToAsync(fileStream).ConfigureAwait(false);
        }
    }
    return View();
}
در اینجا کدهای کامل متد دریافت فایل‌ها را در سمت سرور مشاهده می‌کنید. ابتدا با استفاده از خاصیت environment.WebRootPath_، به مسیر ریشه‌ی wwwroot دسترسی و سپس پوشه‌ی uploads را در آن جهت ذخیره سازی فایل‌های دریافتی، تعیین کرده‌ایم.
چون برنامه‌های ASP.NET Core قابلیت اجرای بر روی لینوکس را نیز دارند، تا حد امکان باید از Path.Combine جهت جمع زدن اجزای مختلف یک میسر، استفاده کرد. از این جهت که در لینوکس، جداکننده‌ی اجزای مسیرها، / است بجای \ در ویندوز و متد Path.Combine به صورت خودکار این مسایل را لحاظ خواهد کرد.
در آخر با استفاده از متد file.CopyToAsync کار نوشتن غیرهمزمان محتوای فایل دریافتی در یک FileStream انجام می‌شود؛ به همین جهت در امضای متد فوق، <async Task<IActionResult را نیز ملاحظه می‌کنید.


پشتیبانی کامل از Model Binding آپلود فایل‌ها در ASP.NET Core

در ASP.NET MVC 5.x اگر ویژگی Required را بر روی یک خاصیت از نوع HttpPostedFileBase قرار دهید ... کار نمی‌کند و در سمت کلاینت تاثیری را به همراه نخواهد داشت؛ مگر اینکه تنظیمات سمت کلاینت آن‌را به صورت دستی انجام دهیم. این مشکلات در ASP.NET Core، کاملا برطرف شده‌اند:
public class UserViewModel
{
    [Required(ErrorMessage = "Please select a file.")]
    [DataType(DataType.Upload)]
    public IFormFile Photo { get; set; }
}
در اینجا یک خاصیت از نوع IFormFile، با دو ویژگی Required و DataType خاص آن در یک ViewModel تعریف شده‌اند. فرم معادل آن در ASP.NET Core به صورت ذیل خواهد بود:
@model UserViewModel
 
<form method="post"
      asp-action="UploadPhoto"
      asp-controller="TestFileUpload"
      enctype="multipart/form-data">
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>
 
    <input asp-for="Photo" />
    <span asp-validation-for="Photo" class="text-danger"></span>
    <input type="submit" value="Upload"/>
</form>
در اینجا ابتدا نوع مدل View تعیین شده‌است و سپس با استفاده از Tag Helpers، صرفا یک input را به خاصیت Photo مدل View جاری متصل کرده‌ایم. همین اتصال سبب فعال سازی مباحث اعتبارسنجی سمت سرور و کاربر نیز می‌شود.
اینبار جهت فعال سازی و استفاده‌ی از قابلیت‌های Model Binding می‌توان از ModelState نیز بهره گرفت:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhoto(UserViewModel userViewModel)
{
    if (ModelState.IsValid)
    {
        var formFile = userViewModel.Photo;
        if (formFile == null || formFile.Length == 0)
        {
            ModelState.AddModelError("", "Uploaded file is empty or null.");
            return View(viewName: "Index");
        }
 
        var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
        if (!Directory.Exists(uploadsRootFolder))
        {
            Directory.CreateDirectory(uploadsRootFolder);
        }
 
        var filePath = Path.Combine(uploadsRootFolder, formFile.FileName);
        using (var fileStream = new FileStream(filePath, FileMode.Create))
        {
            await formFile.CopyToAsync(fileStream).ConfigureAwait(false);
        }
 
        RedirectToAction("Index");
    }
    return View(viewName: "Index");
}
اگر ModelState معتبر باشد، کار ذخیره سازی تک فایل رسیده را انجام می‌دهیم. سایر نکات این متد، با اکشن متد Index که پیشتر بررسی شد، یکی هستند.


بررسی پسوند فایل‌های رسیده‌ی به سرور

ASP.NET Core دارای ویژگی است به نام FileExtensions که ... هیچ ارتباطی به خاصیت‌هایی از نوع IFormFile ندارد:
 [FileExtensions(Extensions = ".png,.jpg,.jpeg,.gif", ErrorMessage = "Please upload an image file.")]
ویژگی FileExtensions صرفا جهت درج بر روی خواصی از نوع string طراحی شده‌است. بنابراین قرار دادن این ویژگی بر روی خاصیت‌هایی از نوع IFormFile، سبب فعال سازی اعتبارسنجی سمت سرور پسوندهای فایل‌های رسیده، نخواهد شد.
در ادامه جهت بررسی پسوندهای فایل‌های رسیده، می‌توان یک ویژگی اعتبارسنجی سمت سرور جدید را طراحی کرد:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class UploadFileExtensionsAttribute : ValidationAttribute
{
    private readonly IList<string> _allowedExtensions;
    public UploadFileExtensionsAttribute(string fileExtensions)
    {
        _allowedExtensions = fileExtensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
    }
 
    public override bool IsValid(object value)
    {
        var file = value as IFormFile;
        if (file != null)
        {
            return isValidFile(file);
        }
 
        var files = value as IList<IFormFile>;
        if (files == null)
        {
            return false;
        }
 
        foreach (var postedFile in files)
        {
            if (!isValidFile(postedFile)) return false;
        }
 
        return true;
    }
 
    private bool isValidFile(IFormFile file)
    {
        if (file == null || file.Length == 0)
        {
            return false;
        }
 
        var fileExtension = Path.GetExtension(file.FileName);
        return !string.IsNullOrWhiteSpace(fileExtension) &&
               _allowedExtensions.Any(ext => fileExtension.Equals(ext, StringComparison.OrdinalIgnoreCase));
    }
}
در اینجا با ارث بری از کلاس پایه ValidationAttribute و بازنویسی متد IsValid آن، کار اعتبارسنجی پسوند فایل‌ها و یا فایل رسیده را انجام داده‌ایم. این ویژگی جدید اگر بر روی خاصیتی از نوع IFormFile قرار بگیرد، پارامتر object value متد IsValid آن حاوی اطلاعات فایل و یا فایل‌های رسیده، خواهد بود. بر این اساس می‌توان تصمیم گیری کرد که آیا پسوند این فایل، مجاز است یا خیر.
public class UserViewModel
{
    [Required(ErrorMessage = "Please select a file.")]
    //`FileExtensions` needs to be applied to a string property. It doesn't work on IFormFile properties, and definitely not on IEnumerable<IFormFile> properties.
    //[FileExtensions(Extensions = ".png,.jpg,.jpeg,.gif", ErrorMessage = "Please upload an image file.")]
    [UploadFileExtensions(".png,.jpg,.jpeg,.gif", ErrorMessage = "Please upload an image file.")]
    [DataType(DataType.Upload)]
    public IFormFile Photo { get; set; }
}
در اینجا روش استفاده‌ی از این ویژگی اعتبارسنجی جدید را نیز با تکمیل ViewModel کاربر، مشاهده می‌کنید. پس از آن تنها بررسی if (ModelState.IsValid) در یک اکشن متد، نتیجه‌ی دریافتی از اعتبارسنج جدید UploadFileExtensions را در اختیار ما قرار می‌دهد و بر این اساس می‌توان تصمیم‌گیری کرد که آیا باید فایل رسیده را ذخیره کرد یا خیر.
مطالب
آموزش LightInject IoC Container - قسمت 1
LightInject در حال حاضر یکی از قدرتمند‌ترین IoC Container‌‌ها است که از لحاظ سرعت و کارآیی در بالاترین جایگاه در میان IoC Container‌‌های موجود قرار دارد. جهت بررسی کارایی IoC Container‌ها می‌توانید به این لینک مراجعه کنید . LightInject یک IoC Container فوق العاده سبک وزن می‌باشد که تمامی قابلیت‌های متداولی که از یک Service Container انتظار می‌رود را شامل می‌شود. تنها شامل یک فایل .cs می‌باشد که تمامی کدهای آن در همین یک فایل نوشته شده‌اند. در پروژه‌های کوچک تا بزرگ بدون از دست دادن کارآیی، با بالاترین سرعت ممکن عمل تزریق وابستگی را انجام می‌دهد. در این مجموعه مقالات به بررسی کامل این IoC Container می‌پردازیم و تمامی قابلیت‌های آن را آموزش می‌دهیم.

نحوه نصب و راه اندازی LightInject
در پنجره Package Manager Console می‌توانید با نوشتن دستور ذیل، نسخه باینری آن را نصب کنید که به فایل .dll آن Reference میدهد.

PM> Install-Package LightInject
 همچنین می‌توانید توسط دستور ذیل فایل .cs آن را به پروژه اضافه نمایید. 

PM> Install-Package LightInject.Source

 آماده سازی پروژه نمونه 
قبل از شروع کار با LightInject، یک پروژه Windows Forms Application را با ساختار کلاس‌های ذیل ایجاد نمایید. (در مقالات بعدی و پس از آموزش کامل LightInject نحوه استفاده از آن را در ASP.NET MVC نیز آموزش می‌دهیم)
    public class PersonModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Family { get; set; }
        public DateTime Birth { get; set; }
    }

    public interface IRepository<T> where T:class
    {
        void Insert(T entity);
        IEnumerable<T> FindAll();
    }

    public interface IPersonRepository:IRepository<PersonModel>
    {
    }

    public class PersonRepository:IPersonRepository
    {
        public void Insert(PersonModel entity)
        {
            throw new NotImplementedException();
        }

        public IEnumerable<PersonModel> FindAll()
        {
            throw new NotImplementedException();
        }
    }

    public interface IPersonService
    {
        void Insert(PersonModel entity);
        IEnumerable<PersonModel> FindAll();
    }

    public class PersonService:IPersonService
    {
        private readonly IPersonRepository _personRepository;

        public PersonService(IPersonRepository personRepository)
        {
            _personRepository = personRepository;
        }

        public void Insert(PersonModel entity)
        {
            _personRepository.Insert(entity);
        }

        public IEnumerable<PersonModel> FindAll()
        {
            return _personRepository.FindAll();
        }
    }
توضیحات
PersonModel: ساختار داده ای جدول Person در سمت Application، که در لایه Domain Model ایجاد می‌گردد.
توجه: جهت سهولت تست و تسریع کدنویسی از لایه بندی و از کلاس‌های ViewModel استفاده نکردیم.
IRepository: یک Interface عمومی برای تمامی Interface‌های مربوط به Repository که عملیات مربوط به پایگاه داده مثل بروزرسانی و واکشی اطلاعات را انجام می‌دهند.
IPersonRepository: واسط بین لایه Service و لایه Repository می‌باشد.
PersonRepository: پیاده سازی واقعی عملیات مربوط به پایگاه داده برای PersonModel می‌باشد. به کلاسهایی که حاوی پیاده سازی واقعی کد می‌باشند Concrete Class می‌گویند.
IPersonService: واسط بین رابط کاربری و لایه سرویس می‌باشد. رابط کاربری به جای دسترسی مستقیم به PersonService از IPersonService استفاده می‌کند.
PersonService: دریافت درخواست‌های رابط کاربری و بررسی قوانین تجاری، سپس ارسال درخواست به لایه Repository در صورت صحت درخواست، و در نهایت ارسال پاسخ دریافتی به رابط کاربری. در واقع واسطی بین Repository و UI می‌باشد.
پس از ایجاد ساختار فوق کد مربوط به Form1 را بصورت زیر تغییر دهید.
public partial class Form1 : Form
    {
        private readonly IPersonService _personService;
        public Form1(IPersonService personService)
        {
            _personService = personService;
            InitializeComponent();
        }
    }
توضیحات
در کد فوق به منظور ارتباط با سرویس از IPersonService استفاده نمودیم که به عنوان پارامتر ورودی برای سازنده Form1 تعریف شده است. حتما با Dependency Inversion و انواع Dependency Injection آشنا هستید که به سراغ مطالعه این مقاله آمدید و علت این نوع کدنویسی را هم می‌دانید. بنابراین توضیح بیشتری در این مورد نمی‌دهم.
حال اگر برنامه را اجرا کنید در Program.cs با خطای عدم وجود سازنده بدون پارامتر برای Form1 مواجه می‌شوید که کد آن را باید به صورت زیر تغییر می‌دهیم.
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            var container = new ServiceContainer();
            container.Register<IPersonService, PersonService>();
            container.Register<IPersonRepository, PersonRepository>();
            Application.Run(new Form1(container.GetInstance<IPersonService>()));
        }
توضیحات
کلاس ServiceContainer وظیفه‌ی Register کردن یک کلاس را برای یک Interface دارد. زمانی که می‌خواهیم Form1 را نمونه سازی نماییم و Application را راه اندازی کنیم، باید نمونه ای را از جنس IPersonService ایجاد نموده و به سازنده‌ی Form1 ارسال نماییم. با رعایت اصل DIP، نمونه سازی واقعی یک کلاس لایه دیگر، نباید در داخل کلاس‌های لایه جاری انجام شود. برای این منظور از شیء container استفاده نمودیم و توسط متد GetInstance، نمونه‌ای از جنس IPersonService را ایجاد نموده و به Form1 پاس دادیم. حال container از کجا متوجه می‌شود که چه کلاسی را برای IPersonService نمونه سازی نماید؟
در خطوط قبلی توسط متد Register، کلاس PersonService را برای IPersonService ثبت نمودیم. container نیز برای نمونه سازی به کلاس هایی که برایش Register نمودیم مراجعه می‌نماید و نمونه سازی را انجام می‌دهد. جهت استفاده از PersonService به پارامتر ورودی IPersonRepository برای سازنده‌ی آن نیاز داریم که کلاس PersonRepository را برای IPersonRepository ثبت کردیم.
حال اگر برنامه را اجرا کنید، به درستی اجرا خواهد شد. برنامه را متوقف کنید و به کد موجود در Program.cs مراجعه نموده و دو خط مربوط به Register را Comment نمایید. سپس برنامه را اجرا کنید و خطای تولید شده را ببینید. این خطا بیان می‌کند که امکان نمونه سازی برای IPersonService را ندارد. چون قبلا هیچ کلاسی را برای آن Register نکرده ایم.
Named Services
در برخی مواقع، بیش از یک کلاس وجود دارند که ممکن است از یک Interface ارث بری نمایند. در این حالت و در زمان Register، باید به ServiceContainer بگوییم که کدام کلاس را باید نمونه سازی نماید. برای بررسی این موضوع، کلاسهای زیر را به ساختار پروژه اضافه نمایید.
    public class WorkerModel:PersonModel
    {
        public ManagerModel Manager { get; set; }
    }

    public class ManagerModel:PersonModel
    {
        public IEnumerable<WorkerModel> Workers { get; set; }
    }

    public class WorkerRepository:IPersonRepository
    {
        public void Insert(PersonModel entity)
        {
            throw new NotImplementedException();
        }

        public IEnumerable<PersonModel> FindAll()
        {
            throw new NotImplementedException();
        }
    }

    public class ManagerRepository:IPersonRepository
    {
        public void Insert(PersonModel entity)
        {
            throw new NotImplementedException();
        }

        public IEnumerable<PersonModel> FindAll()
        {
            throw new NotImplementedException();
        }
    }

    public class WorkerService:IPersonService
    {
        private readonly IPersonRepository _personRepository;

        public WorkerService(IPersonRepository personRepository)
        {
            _personRepository = personRepository;
        }

        public void Insert(PersonModel entity)
        {
            var worker = entity as WorkerModel;
            _personRepository.Insert(worker);
        }

        public IEnumerable<PersonModel> FindAll()
        {
            return _personRepository.FindAll();
        }
    }

    public class ManagerService:IPersonService
    {
        private readonly IPersonRepository _personRepository;

        public ManagerService(IPersonRepository personRepository)
        {
            _personRepository = personRepository;
        }

        public void Insert(PersonModel entity)
        {
            var manager = entity as ManagerModel;
            _personRepository.Insert(manager);
        }

        public IEnumerable<PersonModel> FindAll()
        {
            return _personRepository.FindAll();
        }
    }
توضیحات
دو کلاس Manager و Worker به همراه سرویس‌ها و Repository هایشان اضافه شده اند که از IPersonService و IPersonRepository مشتق شده اند.
حال کد کلاس Program را به صورت زیر تغییر می‌دهیم
...
 var container = new ServiceContainer();
            container.Register<IPersonService, PersonService>();
            container.Register<IPersonService, WorkerService>();
            container.Register<IPersonRepository, PersonRepository>();
            container.Register<IPersonRepository, WorkerRepository>();
            Application.Run(new Form1(container.GetInstance<IPersonService>()));
توضیحات
در کد فوق، چون WorkerService بعد از PersonService ثبت یا Register شده است، LightInject در زمان ارسال پارامتر به Form1، نمونه ای از کلاس WorkerService را ایجاد میکند. اما اگر بخواهیم از کلاس PersonService نمونه سازی نماید باید کد را به صورت زیر تغییر دهیم.
...
            container.Register<IPersonService, PersonService>("PersonService");
            container.Register<IPersonService, WorkerService>();
            container.Register<IPersonRepository, PersonRepository>();
            container.Register<IPersonRepository, WorkerRepository>();
            Application.Run(new Form1(container.GetInstance<IPersonService>("PersonService")));
همانطور که مشاهده می‌نمایید، در زمان Register نامی را به آن اختصاص دادیم که در زمان نمونه سازی از این نام استفاده شده است.
اگر در زمان ثبت، نامی را به نمونه‌ی مورد نظر اختصاص داده باشیم، و فقط یک Register برای آن Interface معرفی نموده باشیم، در زمان نمونه سازی، LightInject آن نمونه را به عنوان سرویس پیش فرض در نظر می‌گیرد.
  container.Register<IPersonService, PersonService>("PersonService");
  Application.Run(new Form1(container.GetInstance<IPersonService>()));
در کد فوق، چون برای IPersonService فقط یک کلاس برای نمونه سازی معرفی شده است، با فراخوانی متد GetInstance، حتی بدون ذکر نام، نمونه ای را از کلاس PersonService ایجاد می‌کند.
IEnumerable<T>
زمانی که چند کلاس را که از یک Interface مشتق شده اند، با هم Register می‌نمایید، LightInject این قابلیت را دارد که این کلاس‌های Register شده را در قالب یک لیست شمارشی برگردانید.
            container.Register<IPersonService, PersonService>();
            container.Register<IPersonService, WorkerService>("WorkerService");
            var personList = container.GetInstance<IEnumerable<IPersonService>>();
در کد فوق لیستی با دو آیتم ایجاد می‌شود که یک آیتم از نوع PersonService و دیگری از نوع WorkerService می‌باشد. همچنین از کد زیر نیز می‌توانید استفاده کنید:
            container.Register<IPersonService, PersonService>();
            container.Register<IPersonService, WorkerService>("WorkerService");
            var personList = container.GetAllInstances<IPersonService>();
به جای متد GetInstance از متد GetAllInstances استفاده شده است.
LightInject از Collection‌های زیر نیز پشتیبانی می‌نماید:
  • Array
  • ICollection<T>
  • IList<T>
  • IReadOnlyCollection<T>
  • IReadOnlyList<T>
Values
توسط LightInject می‌توانید مقادیر ثابت را نیز تعریف کنید
            container.RegisterInstance<string>("SomeValue");
            var value = container.GetInstance<string>();
متغیر value با رشته "SomeValue" مقداردهی می‌گردد. اگر چندین ثابت رشته ای داشته باشید می‌توانید نام جداگانه ای را به هر کدام اختصاص دهید و در زمان فراخوانی مقدار به آن نام اشاره کنید.
            container.RegisterInstance<string>("SomeValue","String1");
            container.RegisterInstance<string>("OtherValue","String2");
            var value = container.GetInstance<string>("String2");
متغیر value با رشته "OtherValue" مقداردهی می‌گردد.