پاسخ به بازخورد‌های پروژه‌ها
تگ a در گزارش
نیازی نیست برای صرفا تبدیل HTML به PDF از کتابخانه PDFReport استفاده کنید. کتابخانه PdfReport برای قسمت‌های تبدیل HTML به PDF خودش از HTMLWorker کتابخانه iTextSharp استفاده می‌کند.
اطلاعات بیشتر

ضمنا این کتابخانه مشکلی با لینک‌ها هم ندارد. یک مثال:

            var html =  @"<a color='blue' href='https://www.dntips.ir'>سایت دات نت</a>";

            using (var pdfDoc = new Document(PageSize.A4))
            {
                PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
                pdfDoc.Open();

                
                FontFactory.Register("c:\\windows\\fonts\\tahoma.ttf");

                StyleSheet styles = new StyleSheet();
                styles.LoadTagStyle(HtmlTags.BODY, HtmlTags.FONTFAMILY, "tahoma");
                styles.LoadTagStyle(HtmlTags.BODY, HtmlTags.ENCODING, "Identity-H");
                styles.LoadTagStyle(HtmlTags.BODY, HtmlTags.ALIGN, HtmlTags.ALIGN_LEFT);

                var parsedHtmlElements = HTMLWorker.ParseToList(new StringReader(html), styles);

                PdfPCell pdfCell = new PdfPCell { Border = 0 };
                pdfCell.RunDirection = PdfWriter.RUN_DIRECTION_RTL;

                foreach (var htmlElement in parsedHtmlElements)
                {
                    pdfCell.AddElement(htmlElement);
                }

                var table1 = new PdfPTable(1);
                table1.WidthPercentage = 100;
                table1.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                table1.AddCell(pdfCell);
                pdfDoc.Add(table1);
            }

پ.ن.
در هر برنامه‌ای یک گزارش خطا زمان قابل رسیدگی خواهد بود که قابلیت تکرار مجدد داشته باشد به همراه ارائه کامل stack trace خطای دریافتی.
مطالب
شروع به کار با DNTFrameworkCore - قسمت 3 - پیاده‌سازی سرویس‌های موجودیت‌ها
در قسمت قبل سناریوهای مختلف مرتبط با طراحی موجودیت‌های سیستم را بررسی کردیم. در این قسمت به طراحی DTO‌های متناظر با موجودیت‌ها به همراه اعتبارسنج‌های مرتبط و در نهایت به پیاده سازی سرویس‌های CRUD آنها خواهیم پرداخت. 
قراردادها، مفاهیم و نکات اولیه
  1. برخلاف بسیاری از طراحی‌های موجود، بر فراز هر موجودیت اصلی (منظور AggregateRoot) باید یک DTO که از این پس با عنوان Model از آنها یاد خواهیم کرد، تعریف شود. 
  2. هیچ تراکنشی برای موجودیت‌های فرعی یا همان Detailها نخواهیم داشت. این موجودیت‌ها در تراکنش موجودیت اصلی مرتبط به آن مدیریت خواهند شد.
  3. هر Commandای که قرار است مرتبط با یک موجودیت اصلی در سیستم انجام پذیرد، باید از منطق تجاری آن موجودیت عبور کند و نباید با دور زدن منطق تجاری، از طرق مختلف تغییراتی بر آن موجودیت اعمال شود. (موضوع مهمی که در ادامه مطلب جاری تشریح خواهد شد)
  4. ویوهای مختلفی از یک موجودیت می‌توان انتظار داشت که ویو پیش‌فرض آن در CrudService تدارک دیده شده است. برای سایر موارد نیاز است در سرویس مرتبط، متدهای Read مختلفی را پیاده‌سازی کنید.
  5. با اعمال اصل CQS، متدهای ثبت و ویرایش در کلاس سرویس پایه CRUD، بعد از انجام عملیات مربوطه، Id و RowVersion مدل ورودی و هچنین Id و TrackingState موجودیت‌های فرعی وابسته، مقداردهی خواهند شد و نیاز به انجام یک Query دیگر و بازگشت آن به عنوان خروجی متدها نبوده است. به همین دلیل خروجی این متدها صرفا Result ای می‌باشد که نشان از امکان Failure بودن انجام آنها می‌باشد که با اصل مذکور در تضاد نمی‌باشد.
  6. ورودی متدهای Read شما که در اکثر موارد نیاز به مهیا کردن خروجی صفحه‌بندی شده دارند، باید از نوع PagedQueryModel و یا اگر همچنین نیاز به جستجوی پویا براساس فیلدهایی موجود در ReadModel مرتبط دارید، باید از نوع FilteredPagedQueryModel باشد. متدهای الحاقی برای اعمال خودکار این صفحه‌‌بندی و جستجوی پویا در نظر گرفته شده است. همچنین خروجی آنها در اکثر موارد از نوع IPagedQueryResult خواهد بود. اگر نیاز است تا جستجوی خاصی داشته باشید که خصوصیتی متناظر با آن فیلد در مدل Read وجود ندارد، لازم است تا از این QueryModel‌های مطرح شده، ارث‌بری کرده و خصوصیت اضافی مدنظر خود را تعریف کنید. بدیهی است که اعمال جستجوی این موارد خاص به عهده توسعه دهنده می‌باشد.
  7. عملیات ثبت، ویرایش و حذف، برای کار بر روی لیستی از وهله‌های Model، طراحی شده‌اند. این موضوع در بسیاری از دومین‌ها قابلیت مورد توجهی می‌باشد. 
  8. رخداد متناظر با عملیات CUD مرتبط با هر موجودیت اصلی، به عنوان یکسری نقاط قابل گسترش (Extensibility Point) در اختیار سایر بخش‌های سیستم می‌باشد. این رخدادها درون تراکنش جاری Raise خواهند شد؛ از این جهت امکان اعمال یکسری Rule جدید از سمت سایر موءلفه‌های سیستم موجود می‌باشد.
  9. برخلاف بسیاری از طراحی‌های موجود، قصد ایجاد لایه انتزاعی برفراز EF Core  به منظور رسیدن به Persistence Ignorance را ندارم. بنابراین امروز بسته DNTFrameworkCore.EntityFramework آن آماده می‌باشد. اگر توسعه دهنده‌ای قصد یکپارچه کردن این زیرساخت را با سایر ORMها یا Micro ORMها داشته باشد، می‌تواند Pull Request خود را ارسال کند.
  10. خبر خوب اینکه هیچ وابستگی به AutoMapper به منظور نگاشت مابین موجودیت‌ها و مدل‌های متناظر آنها، در این زیرساخت وجود ندارد. با پیاده سازی متدهای MapToModel و MapToEntity می‌توانید از کتابخانه Mapper مورد نظر خودتان استفاده کنید؛ یا به صورت دستی این کار را انجام دهید. بعد از چند سال استفاده از AutoMapper، این روزها خیلی اعتقادی به استفاده از آن ندارم.
  11. هیچ وابستگی به FluentValidation به منظور اعتبارسنجی ورودی متدها یا پیاده‌سازی قواعد تجاری، در این زیرساخت وجود ندارد. شما امکان استفاده از Attributeهای اعتبارسنجی توکار، پیاده سازی IValidatableObject توسط مدل یا در موارد خاص به منظور پیاده سازی قواعد تجاری پیچیده، پیاده سازی IModelValidator را دارید. با این حال برای یکپارچگی با این کتابخانه محبوب، می‌توانید بسته نیوگت DNTFrameworkCore.FluentValidation را نصب کرده و استفاده کنید.
  12. با اعمال الگوی Template Method در پیاده سازی سرویس CRUD پایه، از طریق تعدادی متد با پیشوندهای Before و After متناظر با عملیات CUD می‌توانید در فرآیند انجام آنها نیز دخالت داشته باشید؛ به عنوان مثال: BeforeEditAsync یا AfterCreateAsync
  13. باتوجه به اینکه در فرآیند انجام متدهای CUD، یکسری Event هم Raise خواهند شد و همچنین در خیلی از موراد شاید نیاز به فراخوانی SaveChange مرتبط با UnitOfWork جاری باشد، لذا مطمئن‌ترین راه حل برای این قضیه و حفظ ثبات سیستم، همان استفاده از تراکنش محیطی می‌باشد. از این جهت متدهای مذکور با TransactionAttribute نیز تزئین شده‌اند که برای فعال سازی این مکانیزم نیاز است تا TransactionInterceptor مربوطه را به سیستم معرفی کنید.
  14. ValidationInterceptor موجود در زیرساخت، در صورتیکه خروجی متد از نوع Result باشد، خطاهای ممکن را در قالب یک شی Result بازگشت خواهد داد؛ در غیر این صورت یک استثنای ValidationException پرتاب می‌شود که این مورد هم توسط GlobalExceptionFilter مدیریت خواهند شد و در قالب یک BadRequest به کلاینت ارسال خواهد شد.
  15. در سناریوهای Master-Detail، قرارداد این است که Detailها به همراه Master متناظر واکشی خواهند شد و در زمان ثبت و یا ویرایش هم همه آنها به همراه Master متناظر خود به سرور ارسال خواهند شد. 
نکته مهم:  همانطور که اشاره شد، در سناریوهای Master-Detail باید تمامی Detailها به سمت سرور ارسال شوند. در این صورت سناریویی را در نظر بگیرید که قرار است کاربر در front-office سیستم امکان حذف یک قلم از اقلام فاکتور را داشته باشد؛ این درحالی است که در back-office و در منطق تجاری اصلی، ما جایی برای حذف یک تک قلم ندیده‌ایم و کلا منطق و قواعد تجاری حاکم بر فاکتور را زیر سوال می‌برد. چرا که ممکن است یکسری قواعد تجاری متناسب با دومین، بر روی لیست اقلام یک فاکتور در زمان ذخیره سازی وجود داشته باشند که با حذف یک تک قلم از یک مسیر فرعی، کل فاکتور را در حالت نامعتبری برای ذخیره سازی‌های بعدی قرار دهد. در این موارد باید API شما یک DTO سفارشی را دریافت کند که شامل شناسه قلم فاکتور و شناسه فاکتور می‌باشد. سپس با استفاده از شناسه فاکتور و سرویس متناظر، آن را واکشی کرده و از لیست قلم‌های InvoiceModel، آن قلم را با TrackingState.Deleted علامت‌گذاری کنید. همچنین باید توجه داشته باشید که برروی فیلدهای موجود در جداول مرتبط با موجودیت‌های Detail، محدودیت‌های دیتابیسی از جمله Unique Constraint و ... را اعمال نکنید؛ مگر اینکه میدانید و دقیقا مطمئن باشید عملیات حذف اقلام، قبل از عملیات ثبت اقلام جدید رخ می‎دهد (این موضوع نیاز به توضیح و شبیه سازی شرایط خاص آن را دارد که در صورت نیاز می‌توان در مطلب جدایی به آن پرداخت).
‌پیاده سازی و بررسی تعدادی سرویس فرضی
برای شروع لازم است بسته‌های نیوگت زیر را نصب کنید:
PM> Install-Package DNTFrameworkCore -Version 1.0.0
PM> Install-Package DNTFrameworkCore.EntityFramework -Version 1.0.0

مثال اول: پیاده‌سازی سرویس یک موجودیت ساده بدون نیاز به ReadModel 
گام اول: طراحی Model متناظر
[LocalizationResource(Name = "SharedResource", Location = "DNTFrameworkCore.TestAPI")]
public class BlogModel : MasterModel<int>, IValidatableObject
{
    public string Title { get; set; }

    [MaxLength(50, ErrorMessage = "Maximum length is 50")]
    public string Url { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Title == "BlogTitle")
        {
            yield return new ValidationResult("IValidatableObject Message", new[] {nameof(Title)});
        }
    }
}
مدل متناظر با موجودیت‌های اصلی باید از کلاس جنریک MasterModel ارث‌بری کرده باشد. همانطور که ملاحظه می‌کنید، برای نشان دادن مکانیزم اعتبارسنجی، از DataAnnotationها و IValidatableObject استفاده شده‌است. LocalizationResource برای مشخص کردن نام و محل فایل Resource متناظر برای خواندن پیغام‌های اعتبارسنجی استفاده می‌شود. این مورد برای سناریوهای ماژولار و کامپوننت محور بیشتر می‌تواند مدنظر باشد. 
گام دوم: پیاده‌سازی اعتبارسنج مستقل
در صورت نیاز به اعتبارسنجی پیچیده برای مدل متناظر، می‌توانید با استفاده از دو روش زیر به این هدف برسید:
1- استفاده از کتابخانه DNTFrameworkCore.FluentValidation
public class BlogValidator : FluentModelValidator<BlogModel>
{
    public BlogValidator(IMessageLocalizer localizer)
    {
        RuleFor(b => b.Title).NotEmpty()
            .WithMessage(localizer["Blog.Fields.Title.Required"]);
    }
}
2- پیاده‌سازی IModelValidator یا ارث‌بری از کلاس ModelValidator پایه
public class BlogValidator : ModelValidator<BlogkModel>
{
    public override IEnumerable<ModelValidationResult> Validate(BlogModel model)
    {
        yield return new ModelValidationResult(nameof(BlogkModel.Title), "Validation from IModelValidator");
    }
}

گام سوم: پیاده‌سازی سرویس متناظر
public interface IBlogService : ICrudService<int, BlogModel>
{
}
پیاده سازی واسط بالا
public class BlogService : CrudService<Blog, int, BlogModel>, IBlogService
{
    public BlogService(CrudServiceDependency dependency) : base(dependency)
    {
    }

    protected override IQueryable<BlogModel> BuildReadQuery(FilteredPagedQueryModel model)
    {
        return EntitySet.AsNoTracking().Select(b => new BlogModel
            {Id = b.Id, RowVersion = b.RowVersion, Url = b.Url, Title = b.Title});
    }

    protected override Blog MapToEntity(BlogModel model)
    {
        return new Blog
        {
            Id = model.Id,
            RowVersion = model.RowVersion,
            Url = model.Url,
            Title = model.Title,
            NormalizedTitle = model.Title.ToUpperInvariant() //todo: normalize based on your requirement 
        };
    }

    protected override BlogModel MapToModel(Blog entity)
    {
        return new BlogModel
        {
            Id = entity.Id,
            RowVersion = entity.RowVersion,
            Url = entity.Url,
            Title = entity.Title
        };
    }
}
برای این چنین موجودیت‌هایی، بازنویسی همین 3 متد کفایت می‌کند؛ دو متد MapToModel و MapToEntity برای نگاشت مابین مدل و موجودیت مورد نظر و متد BuildReadQuery نیز برای تعیین نحوه ساخت کوئری ReadPagedListAsync پیش‌فرض موجود در CrudService به عنوان متد Read پیش‌فرض این موجودیت. باکمترین مقدار کدنویسی و با کیفیت قابل قبول، عملیات CRUD یک موجودیت ساده، تکمیل شد. 
مثال دوم: پیاده سازی سرویس یک موجودیت ساده با ReadModel و  FilteredPagedQueryModel متمایز
گام اول: طراحی Model متناظر
[LocalizationResource(Name = "SharedResource", Location = "DNTFrameworkCore.TestAPI")]
public class TaskModel : MasterModel<int>, IValidatableObject
{
    public string Title { get; set; }

    [MaxLength(50, ErrorMessage = "Validation from DataAnnotations")]
    public string Number { get; set; }

    public string Description { get; set; }
    public TaskState State { get; set; } = TaskState.Todo;

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Title == "IValidatableObject")
        {
            yield return new ValidationResult("Validation from IValidatableObject");
        }
    }
}
public class TaskReadModel : MasterModel<int>
{
    public string Title { get; set; }
    public string Number { get; set; }
    public TaskState State { get; set; } = TaskState.Todo;
    public DateTimeOffset CreationDateTime { get; set; }
    public string CreatorUserDisplayName { get; set; }
}
به عنوان مثال خصوصیاتی برای نمایش داریم که در زمان ثبت و ویرایش، انتظار دریافت آنها را از کاربر نیز نداریم. 
گام دوم: پیاده‌سازی اعتبارسنج  مستقل 
public class TaskValidator : ModelValidator<TaskModel>
{
    public override IEnumerable<ModelValidationResult> Validate(TaskModel model)
    {
        if (!Enum.IsDefined(typeof(TaskState), model.State))
        {
            yield return new ModelValidationResult(nameof(TaskModel.State), "Validation from IModelValidator");
        }
    }
}
 گام سوم: پیاده‌سازی سرویس متناظر
public interface ITaskService : ICrudService<int, TaskReadModel, TaskModel, TaskFilteredPagedQueryModel>
{
}
همانطور که ملاحظه می‌کنید، از ICrudService استفاده شده است که امکان تعیین نوع پارامتر جنریک TReadModel و TFilteredPagedQueryModel را هم دارد.
مدل جستجو و صفحه‌بندی سفارشی 
public class TaskFilteredPagedQueryModel : FilteredPagedQueryModel
{
    public TaskState? State { get; set; }
}


پیاده سازی واسط ITaskService با استفاده از AutoMapper

public class TaskService : CrudService<Task, int, TaskReadModel, TaskModel, TaskFilteredPagedQueryModel>,
  ITaskService
{
    private readonly IMapper _mapper;

    public TaskService(CrudServiceDependency dependency, IMapper mapper) : base(dependency)
    {
        _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
    }

    protected override IQueryable<TaskReadModel> BuildReadQuery(TaskFilteredPagedQueryModel model)
    {
        return EntitySet.AsNoTracking()
                    .WhereIf(model.State.HasValue, t => t.State == model.State)
                    .ProjectTo<TaskReadModel>(_mapper.ConfigurationProvider);
    }

    protected override Task MapToEntity(TaskModel model)
    {
        return _mapper.Map<Task>(model);
    }

    protected override TaskModel MapToModel(Task entity)
    {
        return _mapper.Map<TaskModel>(entity);
    }
}

به عنوان مثال در کلاس بالا برای نگاشت مابین مدل و موجودیت، از واسط IMapper کتابخانه AutoMapper استفاده شده‌است و همچنین عملیات جستجوی سفارشی در همان متد BuildReadQuery برای تولید کوئری متد Read پیش‌فرض، قابل ملاحظه می‌باشد.

مثال سوم: پیاده‌سازی سرویس یک موجودیت اصلی به همراه تعدادی موجودیت فرعی وابسته (سناریوهای Master-Detail) 

گام اول: طراحی Modelهای متناظر

    public class UserModel : MasterModel
    {
        public string UserName { get; set; }
        public string DisplayName { get; set; }
        public string Password { get; set; }
        public bool IsActive { get; set; }
        public ICollection<UserRoleModel> Roles { get; set; } = new HashSet<UserRoleModel>();
        public ICollection<PermissionModel> Permissions { get; set; } = new HashSet<PermissionModel>();
        public ICollection<PermissionModel> IgnoredPermissions { get; set; } = new HashSet<PermissionModel>();
    }

مدل بالا متناظر است با موجودیت کاربر سیستم، که به یکسری گروه کاربری متصل می‌باشد و همچنین دارای یکسری دسترسی مستقیم بوده و یا یکسری دسترسی از او گرفته شده‌است. مدل‌های Detail نیز از قرارداد خاصی پیروی خواهند کرد که در ادامه مشاهده خواهیم کرد.

public class PermissionModel : DetailModel<int>
{
    public string Name { get; set; }
}

به عنوان مثال PermissionModel بالا از DetailModel جنریک‌ای ارث‌بری کرده است که دارای Id و TrackingState نیز می‌باشد. 

public class UserRoleModel : DetailModel<int>
{
    public long RoleId { get; set; }
}

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

گام دوم: پیاده سازی اعتبارسنج مستقل

public class UserValidator : FluentModelValidator<UserModel>
{
    private readonly IUnitOfWork _uow;

    public UserValidator(IUnitOfWork uow, IMessageLocalizer localizer)
    {
        _uow = uow ?? throw new ArgumentNullException(nameof(uow));

        RuleFor(m => m.DisplayName).NotEmpty()
            .WithMessage(localizer["User.Fields.DisplayName.Required"])
            .MinimumLength(3)
            .WithMessage(localizer["User.Fields.DisplayName.MinimumLength"])
            .MaximumLength(User.MaxDisplayNameLength)
            .WithMessage(localizer["User.Fields.DisplayName.MaximumLength"])
            .Matches(@"^[\u0600-\u06FF,\u0590-\u05FF,0-9\s]*$")
            .WithMessage(localizer["User.Fields.DisplayName.RegularExpression"])
            .DependentRules(() =>
            {
                RuleFor(m => m).Must(model =>
                     !CheckDuplicateDisplayName(model.DisplayName, model.Id))
                    .WithMessage(localizer["User.Fields.DisplayName.Unique"])
                    .OverridePropertyName(nameof(UserModel.DisplayName));
            });

        RuleFor(m => m.UserName).NotEmpty()
            .WithMessage(localizer["User.Fields.UserName.Required"])
            .MinimumLength(3)
            .WithMessage(localizer["User.Fields.UserName.MinimumLength"])
            .MaximumLength(User.MaxUserNameLength)
            .WithMessage(localizer["User.Fields.UserName.MaximumLength"])
            .Matches("^[a-zA-Z0-9_]*$")
            .WithMessage(localizer["User.Fields.UserName.RegularExpression"])
            .DependentRules(() =>
            {
                RuleFor(m => m).Must(model =>
                     !CheckDuplicateUserName(model.UserName, model.Id))
                    .WithMessage(localizer["User.Fields.UserName.Unique"])
                    .OverridePropertyName(nameof(UserModel.UserName));
            });

        RuleFor(m => m.Password).NotEmpty()
            .WithMessage(localizer["User.Fields.Password.Required"])
            .When(m => m.IsNew, ApplyConditionTo.CurrentValidator)
            .MinimumLength(6)
            .WithMessage(localizer["User.Fields.Password.MinimumLength"])
            .MaximumLength(User.MaxPasswordLength)
            .WithMessage(localizer["User.Fields.Password.MaximumLength"]);

        RuleFor(m => m).Must(model => !CheckDuplicateRoles(model))
            .WithMessage(localizer["User.Fields.Roles.Unique"])
            .When(m => m.Roles != null && m.Roles.Any(r => !r.IsDeleted));
    }

    private bool CheckDuplicateUserName(string userName, long id)
    {
        var normalizedUserName = userName.ToUpperInvariant();
        return _uow.Set<User>().Any(u => u.NormalizedUserName == normalizedUserName && u.Id != id);
    }

    private bool CheckDuplicateDisplayName(string displayName, long id)
    {
        var normalizedDisplayName = displayName.NormalizePersianTitle();
        return _uow.Set<User>().Any(u => u.NormalizedDisplayName == normalizedDisplayName && u.Id != id);
    }

    private bool CheckDuplicateRoles(UserModel model)
    {
        var roles = model.Roles.Where(a => !a.IsDeleted);
        return roles.GroupBy(r => r.RoleId).Any(r => r.Count() > 1);
    }
}

به عنوان مثال در این اعتبارسنج بالا، قواعدی از جمله بررسی تکراری بودن نام‌کاربری و از این دست اعتبارسنجی‌ها نیز انجام شده است. نکته حائز اهمیت آن متد CheckDuplicateRoles می‌باشد:

private bool CheckDuplicateRoles(UserModel model)
{
    var roles = model.Roles.Where(a => !a.IsDeleted);
    return roles.GroupBy(r => r.RoleId).Any(r => r.Count() > 1);
}

با توجه به «نکته مهم» ابتدای بحث، model.Roles، شامل تمام گروه‌های کاربری متصل شده به کاربر می‌باشند که در این لیست برخی از آنها با TrackingState.Deleted، برخی دیگر با TrackingState.Added و ... علامت‌گذاری شده‌اند. لذا برای بررسی یکتایی و عدم تکرار در این سناریوها نیاز به اجری پرس‌و‌جویی بر روی دیتابیس نمی‌باشد. بدین منظور، با اعمال یک شرط، گروه‌های حذف شده را از بررسی خارج کرده‌ایم؛ چرا که آنها بعد از عبور از منطق تجاری، حذف خواهند شد. 


گام سوم: پیاده‌سازی سرویس متناظر

public interface IUserService : ICrudService<long, UserReadModel, UserModel>
{
}
public class UserService : CrudService<User, long, UserReadModel, UserModel>, IUserService
{
    private readonly IUserManager _manager;

    public UserService(CrudServiceDependency dependency, IUserManager manager) : base(dependency)
    {
        _manager = manager ?? throw new ArgumentNullException(nameof(manager));
    }

    protected override IQueryable<User> BuildFindQuery()
    {
        return base.BuildFindQuery()
            .Include(u => u.Roles)
            .Include(u => u.Permissions);
    }

    protected override IQueryable<UserReadModel> BuildReadQuery(FilteredPagedQueryModel model)
    {
        return EntitySet.AsNoTracking().Select(u => new UserReadModel
        {
            Id = u.Id,
            RowVersion = u.RowVersion,
            IsActive = u.IsActive,
            UserName = u.UserName,
            DisplayName = u.DisplayName,
            LastLoggedInDateTime = u.LastLoggedInDateTime
        });
    }

    protected override User MapToEntity(UserModel model)
    {
        return new User
        {
            Id = model.Id,
            RowVersion = model.RowVersion,
            IsActive = model.IsActive,
            DisplayName = model.DisplayName,
            UserName = model.UserName,
            NormalizedUserName = model.UserName.ToUpperInvariant(),
            NormalizedDisplayName = model.DisplayName.NormalizePersianTitle(),
            Roles = model.Roles.Select(r => new UserRole
                {Id = r.Id, RoleId = r.RoleId, TrackingState = r.TrackingState}).ToList(),
            Permissions = model.Permissions.Select(p => new UserPermission
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                IsGranted = true,
                Name = p.Name
            }).Union(model.IgnoredPermissions.Select(p => new UserPermission
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                IsGranted = false,
                Name = p.Name
            })).ToList()
        };
    }

    protected override UserModel MapToModel(User entity)
    {
        return new UserModel
        {
            Id = entity.Id,
            RowVersion = entity.RowVersion,
            IsActive = entity.IsActive,
            DisplayName = entity.DisplayName,
            UserName = entity.UserName,
            Roles = entity.Roles.Select(r => new UserRoleModel
                {Id = r.Id, RoleId = r.RoleId, TrackingState = r.TrackingState}).ToList(),
            Permissions = entity.Permissions.Where(p => p.IsGranted).Select(p => new PermissionModel
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                Name = p.Name
            }).ToList(),
            IgnoredPermissions = entity.Permissions.Where(p => !p.IsGranted).Select(p => new PermissionModel
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                Name = p.Name
            }).ToList()
        };
    }

    protected override Task BeforeSaveAsync(IReadOnlyList<User> entities, List<UserModel> models)
    {
        ApplyPasswordHash(entities, models);
        ApplySerialNumber(entities, models);
        return base.BeforeSaveAsync(entities, models);
    }

    private void ApplySerialNumber(IEnumerable<User> entities, IReadOnlyList<UserModel> models)
    {
        var i = 0;
        foreach (var entity in entities)
        {
            var model = models[i++];

            if (model.IsNew || !model.IsActive || !model.Password.IsEmpty() ||
                model.Roles.Any(a => a.IsNew || a.IsDeleted) ||
                model.IgnoredPermissions.Any(p => p.IsDeleted || p.IsNew) ||
                model.Permissions.Any(p => p.IsDeleted || p.IsNew))
            {
                entity.SerialNumber = _manager.NewSerialNumber();
            }
            else
            {
                //prevent include SerialNumber in update query
                UnitOfWork.Entry(entity).Property(a => a.SerialNumber).IsModified = false;
            }
        }
    }

    private void ApplyPasswordHash(IEnumerable<User> entities, IReadOnlyList<UserModel> models)
    {
        var i = 0;
        foreach (var entity in entities)
        {
            var model = models[i++];
            if (model.IsNew || !model.Password.IsEmpty())
            {
                entity.PasswordHash = _manager.HashPassword(model.Password);
            }
            else
            {
                //prevent include PasswordHash in update query
                UnitOfWork.Entry(entity).Property(a => a.PasswordHash).IsModified = false;
            }
        }
    }
}

در سناریوهای Master-Detail نیاز است متد دیگری تحت عنوان BuildFindQuery را نیز بازنویسی کنید. این متد برای بقیه حالات نیاز به بازنویسی نداشت؛ چرا که یک تک موجودیت واکشی می‌شد و خبری از موجودیت‌های Detail نبود. در اینجا لازم است تا روش تولید کوئری FindAsyn رو بازنویسی کنیم تا جزئیات دیگری را نیز واکشی کنیم. به عنوان مثال در اینجا Roles و Permissions کاربر نیز Include شده‌اند.

نکته: بازنویسی BuildFindQuery را شاید بتوان با روش‌های دیگری هم مانند تزئین موجودیت‌های وابسته با یک DetailOfAttribute و مشخص کردن نوع موجودیت اصلی، نیز جایگزین کرد.

متدهای MapToModel و MapToEntity هم به مانند قبل پیاده‌سازی شده‌اند. موضوع دیگری که در برخی از سناریوها پیش خواهد آمد، مربوط است به خصوصیتی که در زمان ثبت ضروری می‌باشد، ولی در زمان ویرایش اگر مقدار داشت باید با اطلاعات موجود در دیتابیس جایگزین شود؛ مانند Password و SerialNumber در موجودیت کاربر. برای این حالت می‌توان از متد BeforeSaveAsync بهره برد؛ به عنوان مثال برای SerialNumber:

private void ApplySerialNumber(IEnumerable<User> entities, IReadOnlyList<UserModel> models)
{
    var i = 0;
    foreach (var entity in entities)
    {
        var model = models[i++];

        if (model.IsNew || !model.IsActive || !model.Password.IsEmpty() ||
            model.Roles.Any(a => a.IsNew || a.IsDeleted) ||
            model.IgnoredPermissions.Any(p => p.IsDeleted || p.IsNew) ||
            model.Permissions.Any(p => p.IsDeleted || p.IsNew))
        {
            entity.SerialNumber = _manager.NewSerialNumber();
        }
        else
        {
            //prevent include SerialNumber in update query
            UnitOfWork.Entry(entity).Property(a => a.SerialNumber).IsModified = false;
        }
    }
}

در اینجا ابتدا بررسی شده‌است که اگر کاربر، جدید می‌باشد، غیرفعال شده است، کلمه عبور او تغییر داده شده است و یا تغییراتی در دسترسی‌ها و گروه‌های کاربری او وجود دارد، یک SerialNumber جدید ایجاد کند. در غیر این صورت با توجه به اینکه برای عملیات ویرایش، به صورت منفصل عمل می‌کنیم، نیاز است تا به شکل بالا، از قید این فیلد در کوئری ویرایش، جلوگیری کنیم. 

نکته: متد BeforeSaveAsync دقیقا بعد از ردیابی شدن وهله‌های موجودیت توسط Context برنامه و دقیقا قبل از UnitOfWork.SaveChange فراخوانی خواهد شد.


برای بررسی بیشتر، پیشنهاد می‌کنم پروژه DNTFrameworkCore.TestAPI موجود در مخزن این زیرساخت را بازبینی کنید.
مطالب
روش بهینه نمایش عکس در Xamarin Forms
پیشاپیش از شما دعوت می‌کنم اگر از سایر روش‌های توسعه برنامه‌های موبایل چون Flutter ،React native و ... استفاده می‌کنید نیز این مقاله را مطالعه کنید؛ چرا که ایده کلی و نکات مهم آن می‌تواند برای شما نیز مفید باشد.

زمانیکه شما از طراح، کاری را می‌گیرید و فرضا می‌خواهید در صفحه‌ای از برنامه موبایل، یک لیوان را نمایش دهید، به شما یک فایل png یا svg تحویل داده می‌شود. اگر چه عموما روش‌هایی وجود دارند تا همان فایل را به صورت مستقیم در صفحه برنامه خود استفاده کنید، ولی این کار به چند دلیل توصیه نمی‌شود:
۱- دستگاه‌های مختلف، Resolution‌های متفاوتی دارند و سخت‌افزار بعضا ضعیف موبایل، باید عکس را برای نمایش بهینه، Scale کند که عملی است هدر دهنده پردازنده و حافظه.
۲- حتی وقتی عمل Scale انجام شد، وقتی صفحه مربوطه بسته شود و به صفحه دیگری برویم، در باز کردن دوباره صفحه مربوطه، این عمل هزینه بر مجددا رخ می‌دهد.

برای حل این مشکل، می‌توان دو قدم برداشت:
قدم اول اینکه عکس svg و یا png آن لیوان را در Scaleهای مختلف، از پیش ذخیره کرد. این کار خود دو اشکال دارد. یک اینکه اگر این عمل به صورت دستی انجام شود، احتمال اشتباه بالا می‌رود و انجام این کار برای Android/iOS/Windows و برای Scaleهای مختلف، برای هر عکس، نیاز به ذخیره لااقل یک دو جین عکس دارد. دوم اینکه با ذخیره‌سازی چند باره یک عکس در Scaleهای مختلف، حجم فایل نهایی پروژه بالا می‌رود.

برای خودکاری سازی این فرآیند دستی، در Xamarin Forms، React native و ... ابزارهایی تعبیه شده است که ResizetizerNT نمونه Xamarin Forms آن است که یک عکس svg یا png را از شما گرفته و در Scaleهای مختلف، برای Android/iOS/Windows ذخیره می‌کند و طبیعتا احتمال اشتباه کم می‌شود و کار بسیار ساده می‌شود.

برای حل مشکل سایز خروجی نهایی، در Android و Google Play Store فرمت جدیدی جایگزین apk شده است به نام aab یا Android App Bundle که با توجه به Resolution دستگاهی که در حال دانلود برنامه است، فایلی را برای وی ساخته و می‌فرستد که فقط عکس‌های با Scale مناسب در آن است؛ پس می‌توانیم لااقل در Android نگران حجم نباشیم.
البته تاثیر این‌کار در Performance آن‌چنان بالاست که بهتر است برای Storeهای متفرقه که از aab پشتیبانی نمی‌کنند و کماکان فقط از apk پشتیبانی می‌کنند نیز کماکان عکس در Scaleهای مختلف ذخیره شود.

در iOS نیز ابزار pngcrunch وجود دارد که عکس‌های png را بهینه و compress می‌کند و در کاهش حجم فایل نهایی مؤثر است. در Xamarin.iOS در قسمت تنظیمات پروژه، در قسمت iOS Build، زدن تیک Checkbox مربوطه با نام Optimize PNG images به معنای درخواست استفاده از این ابزار مفید است.

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

در Android ابزاری معرفی شده‌است که Glide نام دارد و این ابزار، مرحله تبدیل png به bitmap قابل نمایش را cache می‌کند و دیگر باز و بسته کردن صفحات، یا استفاده از یک عکس در List view، اشکالی را ایجاد نمی‌کند. در Xamarin Forms با استفاده از GlideX.Forms، می‌توانید کاری کنید که در Android این بهینه سازی بسیار مؤثر صورت پذیرد و به Scroll نرم در List view و باز شدن سریع صفحات برسید. در iOS معادل همین کار را کتابخانه مطرح Nuke انجام می‌دهد که برای استفاده از آن در Xamarin Forms میتوانید از این Package استفاده کنید.
مزیت دیگر استفاده از GlideX.Forms و Nuke در این است که اگر جایی از برنامه، عکس‌هایی را از سرور دریافت و نمایش دهید (مثلا عکس مشتری‌ها در لیست مشتریان)، این دو ابزار عکس مربوطه را پس از Scale شدن و تبدیل شدن به Bitmap برای استفاده‌های بعدی Cache می‌کنند و سری بعد که نیاز به نمایش همان عکس می‌شود، به جای Scale شدن مجدد و تبدیل شدن به Bitmap دوباره، فقط از Cache محتوای آماده به نمایش به کاربر نمایش داده می‌شود.

در نهایت شما با چند خط تنظیمات ساده و نصب کردن چند package می‌توانید با داشتن یک svg یا png، آن‌را به بهینه‌ترین شکل ممکن در Android/iOS/Windows نمایش دهید.
این پروژه در Github تمامی این موارد را در کنار هم گردآوری کرده‌است که می‌توانید از سورس آن نیز برای درک بهتر موارد استفاده کنید.
مطالب
تزریق وابستگی‌های رایج ASP.NET MVC به برنامه
در پروژه خود می‌توانیم StructureMap را به گونه‌ایی تنظیم کنیم که کار تزریق لایه‌های انتزاعی ASP.NET را نیز انجام دهد؛ مثلاً CurrentHttpContext و یا داده‌های مربوط به مسیریابی و...
به عنوان مثال در برنامه شما ممکن است کدهای زیر چندین و چند بار تکرار شده باشند:
var userId= User.Identity.GetUserId();
var user = _context.Users.Find(userId);

var user = int.Parse(User.Identity.GetUserId());
کدهای فوق به این معنی است که پروژه‌ی شما به صورت کامل به سیستم ASP.NET Identity گره خورده است. خوب، این حالت زمانی پیچیده‌تر خواهد شد که در آینده بخواهید به یک سیستم Identity جدیدتر مهاجرت کنید.
در ادامه نحوه‌ی تزریق وابستگی‌های رایج ASP.NET را بررسی خواهیم کرد. ابتدا یک کلاس رجستری را به صورت زیر ایجاد خواهیم کرد:
public class CommonASPNETRegistry : StructureMap.Configuration.DSL.Registry
{
        public CommonASPNETRegistry()
        {
            For<IIdentity>().Use(() => HttpContext.Current.User.Identity);
            // Other dependencies
        }
}
در کد فوق همانطور که مشخص است، یک کلاس ریجستری ایجاد کرده‌ایم (Registry در واقع یکی از مفاهیم مربوط به استراکچرمپ می‌باشد که امکان ماژولار کردن تنظیمات را درون کلاس‌هایی مجزا، در اختیارمان قرار می‌دهد). درون سازنده‌ی این کلاس گفته‌ایم: زمانیکه درخواستی برای اینترفیس IIdentity داده شد، یک وهله از HttpContext.Current.User.Identity را در اختیار درخواست کننده قرار بده.
لازم به ذکر است می‌توانستیم از وابستگی‌های عنوان شده نیز بدون تزریق کردن آنها درون کنترلرها نیز استفاده کنیم. اما ریجستر کردن آنها این امکان را در اختیارمان قرار می‌دهد تا در هر جایی از برنامه‌مان بتوانیم به آنها دسترسی پیدا کنیم. در ادامه خواهید دید که دسترسی آسان به آنها می‌تواند خیلی مفید واقع شود؛ همچنین امکان تست کردن نیز آسانتر خواهد شد.
قدم بعدی افزودن Registry ایجاد شده به تنظیمات IoC Containerمان است:
public static class SmObjectFactory
{
        private static readonly Lazy<Container> _containerBuilder =
            new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication);

        public static IContainer Container
        {
            get { return _containerBuilder.Value; }
        }

        private static Container defaultContainer()
        {
            return new Container(ioc =>
            {
                // Other settings
                ioc.AddRegistry(new CommonASPNETRegistry());
                
            });
        }
}
اکنون به سادگی می‌توانیم از وابستگی‌های عنوان شده در برنامه‌مان استفاده کنیم. برای استفاده‌ی از آن، مثال اول را در نظر بگیرید "یافتن کاربر فعلی". همانطور که عنوان شد، استفاده از کدهایی شبیه به حالت زیر جهت یافتن کاربر جاری در برنامه ممکن است چندین بار تکرار شده باشد:
var user = int.Parse(User.Identity.GetUserId());
خوب، برای حل این مشکل اینترفیس زیر را اضافه می‌کنیم:
public interface ICurrentUser
{
        ApplicationUser User { get; }
}
پیاده‌سازی آن نیز به این صورت خواهد بود:
public class CurrentUser : ICurrentUser
{
        private readonly IIdentity _identity;
        private readonly IApplicationUserManager _userManager;
        private ApplicationUser _user;
        public CurrentUser(IIdentity identity, IApplicationUserManager userManager)
        {
            _identity = identity;
            _userManager = userManager;
        }
        public ApplicationUser User
        {
            get { return _user ?? (_user = _userManager.FindById(int.Parse(_identity.GetUserId()))); }
        }
}
درون کلاس فوق به اینترفیس IIdentity جهت ارائه آی‌دی کاربر جاری و اینترفیس IApplicationUserManager جهت یافتن اطلاعات کاربر نیاز خواهیم داشت. همانطور که مشاهده می‌کنید فیلد user_ در صورتیکه از قبل موجود باشد، برگردانده خواهد شد؛ در غیر اینصورت آن را از کانتکست مربوطه واکشی خواهد کرد.
اکنون با استفاده از روش فوق نه تنها درون کنترلرهایمان بلکه در هر جایی از برنامه‌مان می‌توانیم به کاربر جاری دسترسی داشته باشیم. همچنین در آینده نیز به راحتی می‌توانیم از سیستم ASP.NET Identity به هر سیستم دیگری سوئیچ کنیم.
برای استفاده از اینترفیس فوق نیز به این صورت عمل خواهیم کرد:
public class HomeController : BaseController
{
    private readonly ICurrentUser _currentUser;
    public HomeController(ICurrentUser user)
    {
        _user = user;
    }
    public ActionResult Index()
    {
        // user
        var user = _currentUser.User;
        // user id
        var userId = _currentUser.User.Id;
    }
}

مطالب
درباره Iterator methodها و yield return در #C
هنگامیکه می‌خواهید در متدهای خود مقداری (از هر نوع datatype دلخواه) را return نمایید، در حالت عادی قادر خواهید بود که فقط از یک return در بدنه متد خود استفاده نمایید:
 public int Sum(int a, int b)
{
     return a + b;
}
اما چنانچه از متدهای تکرار شونده استفاده نمایید، چطور؟

متدهای تکرار شونده یا Iterator method‌ها ، در داخل یک collection به صورت دلخواه iterate کرده یا به اصلاح پیمایش می‌کنند. این متدها از کلمه کلیدی Yield در هنگام return کردن مقادیر استفاده می‌کنند. (در C# از Yield return و در VB از Yield استفاده می‌شود)  به عبارت دیگر یک متد با خروجی از نوع قابل پیمایش (مانند IEnumerable)، با استفاده از چند yield return، دارای قابلیت پیمایش و بازگرداندن چندین مقدار به جای یک مقدار واحد می‌گردد.

برای درک بهتر مسئله از مثالی برای ادامه توضیحات استفاده می‌کنم. متد پیمایش شونده (Iterate method) زیر را در نظر بگیرید که خروجی IEnumerable دارد:
 public static IEnumerable SomeNumbers()
{
     yield return 3;
     yield return 5;
     yield return 8;
}
برای استفاده از مقادیر بازگشتی متد بالا از حلقه foreach زیر استفاده می‌نماییم:
static void Main()
{
    foreach (int number in SomeNumbers())
    {
        Console.Write(number.ToString() + " ");
    }
    // Output: 3 5 8
    Console.ReadKey();
}
حلقه foreach فوق ، در پایان اولین پیمایش، عدد 3 را باز گردانده و مکان این return را حفظ می‌کند. در چرخه بعدی عدد 5 را باز می‌گرداند و این نقطه را نیز نگه می‌دارد و در چرخه پایانی عدد 8 را برگردانده و سپس حلقه با رسیدن به نقطه پایانی متد، خاتمه می‌یابد.

برای خاتمه پیمایش در Iterator method‌ها ، میتوانید از foreach استفاده کنید و یا اینکه عبارت yield break  را بعد از تمامی yield return‌ها به کار گیرید:
 public static IEnumerable SomeNumbers()
{
   yield return 3;
   yield return 5;
   yield return 8;
   yeild break;
}
نکات:

  - در هنگام ایجاد Iterator method ها، نوع مقادیر خروجی متد ، باید یکی از انواع IEnumerable, IEnumerable<T>, IEnumerator,  و یا IEnumerator<T>. باشد.
 - در هنگام declare کردن ، نمی‌توانید از پارامترهای  ref و out استفاده نمایید.
 - در Anonymous method‌ها (متدهای بی نام) و Unsafe block‌ها نمی‌توانید از yield return (yield در VB ) استفاده نمایید.
 - نمی‌توانید از Yield return در بلوکهای try-catch استفاده کندی. اما می‌توانید در قسمت try بلوک try-finally استفاده نمایید.
 - از yield break  می‌توانید در بلوک try  و یا بلوک catch استفاده نمایید ، اما در بلوک finally خیر.
 - هنگام بروز خطا در foreach هایی که خارج از iterator method‌ها استفاده می‌شوند، بلوک finally داخل این متدها اجرا می‌گردد.

مثالی دیگر با استفاده Iterator method‌ها و yield return جهت بازگرداندن روزهای هفته:
static void Main()
{
  DaysOfTheWeek days = new DaysOfTheWeek();
  foreach (string day in days)
    {
        Console.Write(day + " ");
    }
    // Output: Sun Mon Tue Wed Thu Fri Sat
    Console.ReadKey();
}

public class DaysOfTheWeek : IEnumerable
{
  private string[] days = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
  public IEnumerator GetEnumerator()
    {
        for (int index = 0; index < days.Length; index++)
        {
            // Yield each day of the week. 
            yield return days[index];
        }
    }
}
منابع:
 yield ، Iterators
مطالب
ساخت ActionResult سفارشی
پیشتر با انواع ActionResult آشنا شدید. حال فرض کنید می‌خواهید نوعی رو برگردونید که براش ActionResult موجود نباشه مثلا RSS و یا فایل از نوع Excel و...
خوب، فرض کنید می‌خواهید اکشن متدی رو بنویسید که قراره نام یک فایل متنی رو بگیره و انو تو مروگر به کاربر نمایش بده.
برای اینکار از کلاس ActionResult، کلاس دیگه‌ی رو بنام TextResult به ارث می‌بریم و از این ActionResult سفارشی شده، در اکشن متد مربوطه استفاده می‌کنیم:
public class TextResult : ActionResult
{
    public string FileName { get; set; }
    public override void ExecuteResult(ControllerContext context)
    {
        var filePath = Path.Combine(context.HttpContext.Server.MapPath(@"~/Files/"), FileName);
        var data = File.ReadAllText(filePath);
        context.HttpContext.Response.Write(data);
    }
}
نحوه استفاده
    public ActionResult DownloadTextFile(string fileName)
    {
        return new TextResult { FileName = fileName };
    }
در واقع متد اصلی اینجا ExecuteResult هست که نتیجه‌ی کار یک اکشن رو می‌تونیم پردازش کنیم.
خوب، سوالی که اینجا پیش میاد اینه که چرا این همه کار اضافی، چرا از Return File  استفاده نمی‌کنی؟
    public ActionResult DownloadTextFile(string fileName)
    {
        var filePath = Path.Combine(HttpContext.Server.MapPath(@"~/Files/"), fileName);
        return File(filePath, "text");
    }
 یا کلا دلیل استفاده از ActionResult سفارشی چیه؟

  • جلوگیری از پیچیدگی و  تکرار کد
همیشه کار مثل مورد بالا راحت و کم کد! نیست.
به مثال زیر توجه کنید که قراره خروجی CSV  بهمون بده.
public class CsvActionResult : ActionResult
{
    public IEnumerable ModelListing { get; set; }

    public CsvActionResult(IEnumerable modelListing)
    {
        ModelListing = modelListing;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        byte[] data = new CsvFileCreator().AsBytes(ModelListing);
        var fileResult = new FileContentResult(data, "text/csv")
        {
            FileDownloadName = "CsvFile.csv"
        };
        fileResult.ExecuteResult(context);
    }
}
و نحوه استفاده:
    public ActionResult ExportUsers()
    {
        IEnumerable<User> model = UserRepository.GetUsers();
        return new CsvActionResult(model);
    }
حال فرض کنید بخواهیم همه این کدها رو داخل اکشن متد داشته باشیم، یکم پیچیده میشه و یا فرض کنید کنترلر دیگه‌‌ای نیاز به خروجی CSV  داشته باشه، تکرار کد زیاد میشه.

  • راحت کردن گرفتن تست واحد از اکشن‌ها متدها
کاربرد ActionResult سفارشی تو تست واحد اینه که وابستگی‌های یک اکشن رو که Mock کردنش سخته می‌بریم داخل ActionResult و هنگام نوشتن تست واحد درگیر کار با اون وابستگی نمی‌شیم.
به مثال زیر توجه کنید که قراره برای اکشن Logout  تست واحد بنویسیم
ابتدا بردن وابستگی‌ها به خارج از اکشن به کمک ActionResult سفارشی
public class LogoutActionResult : ActionResult
{
    public RedirectToRouteResult ActionAfterLogout { get; set; }
    public LogoutActionResult(RedirectToRouteResult actionAfterLogout)
    {
        ActionAfterLogout = actionAfterLogout;
    }
    public override void ExecuteResult(ControllerContext context)
    {
        FormsAuthentication.SignOut();
        ActionAfterLogout.ExecuteResult(context);
    }
}
نحوه استفاده از ActionResult سفارشی
    public ActionResult Logout()
    {
        var redirect = RedirectToAction("Index", "Home");
        return new LogoutActionResult(redirect);
    }
و سپس نحوه تست واحد نوشتن
    [TestMethod]
    public void The_Logout_Action_Returns_LogoutActionResult()
    {
        //arrange
        var account = new AccountController();

        //act
        var result = account.Logout() as LogoutActionResult;

        //assert
        Assert.AreEqual(result.ActionAfterLogout.RouteValues["Controller"], "Home");
    }
خوب به راحتی ما میایم فراخوانی متد SignOut رو از داخل اکشن می‌کشیم بیرون و این کار از اجرای متد SignOut  از داخل اکشن متد جلوگیری می‌کنه و همچنین با این کار هنگام تست واحد نوشتن نیاز نیست با Mock  کردن کلاس FormsAuthentication سروکار داشته باشیم و فقط کافیه چک کنیم خروجی از نوع LogoutActionResult هست یا خیر و یا می‌تونیم ActionAfterLogout رو چک کنیم.

منابع و مراجع: + و +
 
مطالب
آموزش مهندسی نرم افزار و UML - جلسه سوم

جلسه سوم :

در جلسه قبل به بررسی مشکلات تولید و توسعه سیستم‌های اطلاعاتی یا همان بسته‌های نرم افزاری پرداختیم در این جلسه به راهکاری که IT  برای فایق آمدن بر این مشکلات پیش روی ما قرار داده یا همان متدولوژی می‌پردازیم.

متدولوژی چیست ؟

          متدولوژی در واقع مجموعه ای از روش‌ها ، اصول و قواعدی است که برای قانونمند کردن تولید و توسعه نرم افزار ارائه می‌شود؛می توان گفت متدولوژی فرمولی جهت ساخت نرم افزار می‌باشد یا به عبارت دیگر متدولوژی چرخه حیات نرم افزار را مشخص می‌کند.

-چرخه حیات تولید وتوسعه نرم افزار یا SDLC(System Development Life Cycle)

مراحلی را که در طی تولید و توسعه نرم افزار سپری می‌شوند را SDLC می‌گویند.

انواع SDLC

1.     چرخه حیات سیستم‌های قدیمی یا TLC

2.     چرخه حیات سیستم‌های شی گرا یا OODLC

TLC(Traditional Life Cycle)-

در گذشته به دلیل اینکه اکثر برنامه‌ها بصورت فرآیندگرا یا Process Oriented نوشته می‌شدند از روش TLC  استفاده می‌شد . در روش‌های فرآیند گرا تمرکز اصلی بر روی فعالیت‌های سیستم بود در این روش بیشتر از نمودارهای ERD و DFD استفاده می‌شد .

البته اینا اضافه کنم که هنوز هم در بعضی از شرکت‌ها از این روش استفاده می‌شه هر چند که خودشونم نمی‌دونند یکی از دلایل اصلی هم فقر سواد شرکت‌های کار فرما می‌باشد . در ادامه به بررسی یکی از مدل‌های معروف TLC  یعنی مدل آبشاری می‌پردازیم.

-مدل آبشاری یا Water Fall  :

مدل آبشاری هر چند مدلی قدیمی می‌باشد اما مبنای اساسی مدل‌های شی گرا می‌باشد.

فازهای مختلف مدل آبشاری :

1.     مهندسی سیستم یا System Engineering

معرفی نیازمندی‌های کلی و مشخص نمودن کلیات سیستم به صورت سخت افزاری و نرم افزاری و تعاریف اصلی سیستم به طور مثال در پروژه وب سایت از Asp  استفاده کنیم یا Php 

2.     آنالیز نیازمندی‌ها یا Requirement Analysis

در این فاز به نیازمندی‌های کاربران می‌پردازیم یعنی در این فاز ما با چه یا What می‌پردازیم 

3.     طراحی یا Design

در این فاز ما به دنبال چگونه یا Who می‌رویم یعنی اینکه سیستم چگونه در جهت بر آوردن نیازمندی‌ها گام بردارد. 

4.     ساخت یا Construction

در این فاز آنچه را که در فاز طراحی مطرح کردیم به کد تبدیل می‌کنیم 

5.     تست Testing

در این مرحله سیستم از لحاظ کمی و کیفی تست میشوند. 

6.     نصب یا Installation

7.     نگهداری یا Maintenance

این فاز طولانی‌ترین و پرهزینه‌ترین قسمت چرخه عمر یک نرم افزار

معایب مدل آبشاری :

1.     مدل آبشاری تکرار بین فاز‌ها را در نظر نمی‌گیرد و خروجی هر فاز را قطعی در نظر می‌گیرد که اگر مثلا در فاز طراحی باشیم و یک نیازمندی در نظر گرفته نشده باشد این مدل هیچ راهکاری را برای در ج این نیازمندی در سیستم ارائه نمی‌دهد بعد‌ها برای رفع این مشکل مدل آبشاری با تکرار (Water Fall with Iteration)  معرفی گردید.

2.     دراین روش هر فاز هنگامی آغاز می‌شود که فاز قبل از خودش به پایان رسیده باشد که این امر مانع از Overlap  یا به اشتراک گذاری بین فازها می‌شد.

3.     سیستم‌های محاوره ای نیستند و در برابر تغییرات مقاوم نمی‌باشند.

مزایای مدل‌های آبشاری

1.     واگذاری هر فاز به یک تیم مشخص

2.     پیشرفت پروژه بیشتر به چشم می‌خورد.

 در جلسه آینده به بررسی مدل‌های شی گرا خواهیم پرداخت.

 

نظرات مطالب
بالا بردن سرعت بارگذاری اولیه EF Code first با تعداد مدل‌های زیاد
یک نکته‌ی تکمیلی: اضافه شدن قابلیت مشابهی جهت «بالا بردن سرعت بارگذاری اولیه EF-Core 6x با تعداد مدل‌های زیاد»

اگر تعداد مدل‌های زیادی را دارید و بارگذاری اولیه‌ی برنامه‌ی مبتنی بر EF-Core 6x شما کند است، می‌توانید با استفاده از قابلیت جدید Compiled models آن، این سرعت را حداقل 8 برابر افزایش دهید. روش انجام آن نیز ساده‌است:
public class ExampleContext : DbContext
{
    public DbSet<Person> People { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
    { 
        options.UseModel(CompiledModelsExample.ExampleContextModel.Instance);
        options.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=Test;Trusted_Connection=True;"); 
    }
}
به همراه اجرای یکی از دو دستور زیر:
CLI:
dotnet ef dbcontext optimize -c ExampleContext -o CompliledModels -n CompiledModelsExample
Package Manager Console:
Optimize-DbContext -Context ExampleContext -OutputDir CompiledModels -Namespace CompiledModelsExample

بنابراین به Context و متد OnConfiguring آن چنین سطری باید اضافه شود:
options.UseModel(CompiledModelsExample.ExampleContextModel.Instance);
که CompiledModelsExample آن همان نام فضای نام دلخواهی است که به دستور dotnet ef ارسال کرده‌ایم. مابقی آن نام Context به همراه Model است. البته اگر این سطر را هم اضافه نکنید، دستور dotnet ef فوق، آن‌را گوشزد خواهد کرد.

محدودیت‌های این روش در نگارش 6x:
- از فیلترهای سراسری (global query filters) پشتیبانی نمی‌کند.
- از تشکیل Lazy loading proxies پشتیبانی نمی‌کند. 
- از تشکیل Change tracking proxies پشتیبانی نمی‌کند. 
همچنین هربار که تغییری را در مدل‌ها ایجاد کردید، دستورات فوق را باید به صورت دستی مجددا اجرا کنید.
نظرات مطالب
آزمایش Web APIs توسط Postman - قسمت چهارم - نوشتن آزمون‌ها و اسکریپت‌ها
یک نکته‌ی تکمیلی: بهتر است برای چه مواردی در Postman آزمایش بنویسیم؟

یک response از سه قسمت هدر، status code و بدنه‌ی آن تشکیل می‌شود. بنابراین هر سه قسمت را باید آزمایش کرد تا بتوان توسط آن عملکرد یک Web API را بررسی نمود.
برای مثال فرض کنید که می‌خواهید برای متد Put، آزمایش بنویسید. این آزمایش باید شامل موارد زیر باشد:

بررسی موفقیت آمیز بودن عملیات
- آیا هدرهای درستی در response درج شده‌اند؟
- آیا status code دریافتی از سرور برای مثال 200 یا 204 است؟
- آیا عملیات به روز رسانی، موجودیت مشخص و مورد انتظاری را به روز رسانی کرده‌است یا خیر؟
- آیا تمام فیلدها به درستی به روز رسانی شده‌اند؟
- آیا فیلدهایی که در درخواست ارسالی قید نشده‌اند، با مقدار پیش‌فرض خود مقدار دهی شده‌اند؟

بررسی شکست عملیات
- آیا به روز رسانی موجودیتی که وجود ندارد، خروجی 404 را به همراه دارد یا خیر؟
- آیا اگر هدر content-type ذکر نشود، شاهد status code=415 unsupported media type خواهیم بود؟
- آیا اگر هدر content-type نامربوطی ذکر شود، شاهد status code=415 unsupported media type خواهیم بود؟
- آیا اگر بدنه‌ی درخواست خالی را ارسال کنیم، خروجی 400 bad request صادر می‌شود؟
- آیا اگر فیلدها را طوری تنظیم کنیم که سبب مشکلات اعتبارسنجی شوند، خروجی 422 unprocessable entity صادر می‌شود؟

بنابراین در حالت کلی:
- هر دو وضعیت موفقیت آمیز بودن و شکست عملیات باید بررسی شوند.
- مشکلات اعتبارسنجی ورودی‌ها باید بررسی شوند.
- ورودی‌های مورد انتظار را کم و زیاد کنید. اگر درخواستی قرار است کوئری استرینگی را بپذیرد، آن‌را قید نکنید یا برعکس.
نظرات مطالب
ارتقاء به 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 نیز به صورت خودکار انجام خواهد شد و نیازی به کدنویسی اضافه‌تری نخواهد داشت.