مطالب
طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها

در این مطلب قصد داریم علاوه بر طراحی زیرساختی برای راه اندازی هرچه سریعتر ServiceLayer، طراحی ای برای مکانیزم Validation به عنوان یک Cross Cutting Concern، نیز ارائه داده و آن را پیاده سازی کنیم.

پیش نیازها:

 ServiceLayer در معماری لایه‌ای، در برگیرنده ApplicationService هایی می‌باشد که به عنوان مدخل ورودی (Entry Point) برنامه، در معرض دید لایه Presentation قرار گرفته و داده را به فرمت مورد نیاز Presentation در اختیارش قرار خواهند داد.
 این سرویس‌ها DTO‌ها را به عنوان پارامتر دریافت کرده و DTO هایی را به عنوان خروجی برگشت خواهند داد. مباحثی مانند Logging، Caching، Business Validation Authorization و مدیریت تراکنش‌ها را می‌توان در این لایه در نظر گرفت.

در ادامه اگر واژه «سرویس» به کار گرفته می‌شود منظور ما ApplicationServiceها می‌باشند.

کار را با ارائه یکسری واسط و کلاس پایه برای عملیات CRUD در سرویس‌ها به صورت زیر پیش می‌بریم.

قرار است به صورت قراردادی، تمام سرویس‌های ما واسط زیر را پیاده سازی کرده باشند. این مورد در مباحث تعریف Policy‌های مربوط به StructureMap مفید خواهد بود.

namespace MvcFramework.Framework.Application.Services
{
    public interface IApplicationService : ITransientDependency
    {
    }
}

دو واسط دیگر برای اعمال طول عمر اشیاء به صورت قراردادی در StructureMap به شکل زیر در نظر گرفته شده‌اند.

namespace MvcFramework.Framework.Dependency
{
    public interface ISingletonDependency
    {
    }
}
namespace MvcFramework.Framework.Dependency
{
    public interface ITransientDependency
    {
    }
}

و با پیاده سازی یک LifeCyclePolicy از دو واسط بالا به شکل زیر استفاده خواهیم کرد.

namespace MvcFramework.Framework.Dependency
{
    public class LifeCyclePolicy : IInstancePolicy
    {
        public void Apply(Type pluginType, Instance instance)
        {
            if (typeof(ISingletonDependency).IsAssignableFrom(instance.ReturnedType))
                instance.SetLifecycleTo<SingletonLifecycle>();
            else if (typeof(ITransientDependency).IsAssignableFrom(instance.ReturnedType))
                instance.SetLifecycleTo<TransientLifecycle>();
        }
    }
}

به این صورت تنظیم طول عمر اشیاء ساخته شده توسط StructureMap این بار به صورت قرادادی بوده و لازم به ذکر تک تک این موارد در تنظیمات اولیه مربوط به Container آن نیست.

کلاس پایه‌ای را که پیاده ساز واسط IApplicationService می‌باشد، برای مقابله با عدم نگارش پذیری واسط‌ها، به شکل زیر در نظر میگیریم. 

namespace MvcFramework.Framework.Application.Services
{
    public abstract class ApplicationService : IApplicationService
    {
    }
}

بسته به نیاز پروژه خودتان می‌توانید اعضای مشترک بین سرویس‌ها را در دل این کلاس قرار دهید.

در ادامه واسط ICrudApplicationSevie را به شکل زیر طراحی خواهیم کرد.

namespace MvcFramework.Framework.Application.Services
{
    public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel> :
        ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest,
            PagedListResponse<TModel, PagedListRequest>, DynamicListRequest>
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
    {
    }

    public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, in TDynamicListRequest> :
        ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest,
            PagedListResponse<TModel, PagedListRequest>, TDynamicListRequest>
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
        where TDynamicListRequest : DynamicListRequest
    {
    }

    public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, in TPagedListRequest,
        TPagedListResponse> :
        ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, TPagedListRequest, TPagedListResponse,
            DynamicListRequest>
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
        where TPagedListRequest : PagedListRequest, new()
        where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest>
    {
    }

    public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, in TPagedListRequest,
        TPagedListResponse,
        in TDynamicListRequest> : IApplicationService
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
        where TPagedListRequest : PagedListRequest, new()
        where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest>
        where TDynamicListRequest : DynamicListRequest
    {
        void Create(TCreateModel model);
        void Create(IList<TCreateModel> models);
        Task CreateAsync(TCreateModel model);
        Task CreateAsync(IList<TCreateModel> models);

        IList<TModel> GetList();
        DynamicListResponse GetDynamicList(TDynamicListRequest request);
        TPagedListResponse GetPagedList(TPagedListRequest request);
        IList<LookupItem> GetLookup();
        TModel GetById(long id);
        TEditModel GetForEdit(long id);
        bool Exists(long id);
        Task<IList<TModel>> GetListAsync();
        Task<DynamicListResponse> GetDynamicListAsync(TDynamicListRequest request);
        Task<TPagedListResponse> GetPagedListAsync(TPagedListRequest request);
        Task<IList<LookupItem>> GetLookupAsync();
        Task<TModel> GetByIdAsync(long id);
        Task<TEditModel> GetForEditAsync(long id);
        Task<bool> ExistsAsync(long id);

        void Edit(TEditModel model);
        void Edit(IList<TEditModel> models);
        Task EditAsync(TEditModel model);
        Task EditAsync(IList<TEditModel> models);
        
        void Delete(TDeleteModel model);
        void Delete(IList<TDeleteModel> models);
        Task DeleteAsync(TDeleteModel model);
        Task DeleteAsync(IList<TDeleteModel> models);
    }
}

سرویسی که نیاز دارد از عملیات CRUD نیز پشتیبانی داشته باشد، بهتر است واسط آن از یک چنین واسطی که در بالا معرفی شد، ارث بری کند. 

مدل‌ها و واسط‌های پیش فرضی را که در واسط بالا از آنها استفاده شده است، در زیر مشاهده می‌کنید:

 واسط IModel

namespace MvcFramework.Framework.Application.Models
{
    public interface IModel
    {
        long Id { get; set; }
    }
}

واسط IEditModel

namespace MvcFramework.Framework.Application.Models
{
    public interface IEditModel : IModel
    {
        byte[] RowVersion { get; set; }
    }
}

واسط IDeleteModel

namespace MvcFramework.Framework.Application.Models
{
    public interface IDeleteModel : IModel
    {
        byte[] RowVersion { get; set; }
    }
}

کلاس LookupItem

namespace MvcFramework.Framework.Application.Models
{
    public class LookupItem
    {
        public string Value { get; set; }
        public string Text { get; set; }
        public bool Selected { get; set; }
    }
}

کلاس PagedListRequest

namespace MvcFramework.Framework.Application.Models
{
    public class PagedListRequest : IShouldNormalize
    {
        public long TotalCount { get; set; }
        public int PageNumber { get; set; }
        public int PageSize { get; set; }

        /// <summary>
        ///     Sorting information.
        ///     Should include sorting field and optionally a direction (ASC or DESC)
        ///     Can contain more than one field separated by comma (,).
        /// </summary>
        /// <example>
        ///     Examples:
        ///     "Name"
        ///     "Name DESC"
        ///     "Name ASC, Age DESC"
        /// </example>
        public string SortBy { get; set; }

        public void Normalize()
        {
            if (PageNumber < 1)
                PageNumber = 1;

            if (PageSize < 0)
                PageSize = 10;

            if (SortBy.IsEmpty())
                SortBy = "Id DESC";
        }
    }
}

در این طراحی دو شکل از GetPagedList در نظر گرفته شده است؛ یکی با ورودی و خروجی داینامیک مثلا جهت استفاده برای نمایش اطلاعات در کندو گرید که در ادامه با آن بیشتر آشنا خواهید شد و دیگری هم برای زمانیکه نیاز دارید اطلاعات صفحه بندی شده‌ای را در اختیار داشته باشید. کلاس بالا برای پیاده سازی شکل دومی که صحبت شد، استفاده میشود. پیاده سازی واسط IShouldNormalize باعث خواهد شد که قبل از اجرای خود متد، این نوع پارامترها با استفاده از یک Interceptor شناسایی شده و متد Normalize آنها اجرا شود.


کلاس PagedListResponse

namespace MvcFramework.Framework.Application.Models
{
    public class PagedListResponse<TModel, TPagedListRequest>
        where TPagedListRequest : PagedListRequest, new()
        where TModel : IModel
    {
        public PagedListResponse()
        {
            Result = new List<TModel>();
            Request = new TPagedListRequest();
        }
        public IList<TModel> Result { get; set; }
        public TPagedListRequest Request { get; set; }
    }
}

کلاس بالا به عنوان نوع خروجی متد GetPagedList مورد استفاده قرار میگرد. وجود خصوصیتی از نوع PagedListRequest هم برای مواردی مانند صفحه بندی نیز می‌تواند مفید باشد.


کلاس‌های DynamicListRequest و DynamicListResponse برگرفته از کتابخانه Kendo.DynamicLinq می باشند.


کلاس Entity

namespace MvcFramework.Framework.Domain.Entities
{
    public abstract class Entity
    {
        #region Properties

        public long Id { get; set; }
        public byte[] RowVersion { get; set; }
        public EntityChangeState State { get; set; }

        #endregion

        #region Public Methods

        [SuppressMessage("ReSharper", "BaseObjectGetHashCodeCallInGetHashCode")]
        [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
        public override int GetHashCode()
        {
            if (IsTransient())
                return base.GetHashCode();

            unchecked
            {
                var hash = this.GetRealType().GetHashCode();
                return (hash * 31) ^ Id.GetHashCode();
            }
        }

        public virtual bool IsTransient()
        {
            return Id == 0;
        }

        public override bool Equals(object obj)
        {
            var other = obj as Entity;
            if (ReferenceEquals(other, null)) return false;

            if (ReferenceEquals(this, other)) return true;

            var typeOfThis = this.GetRealType();
            var typeOfOther = other.GetRealType();

            if (typeOfThis != typeOfOther) return false;

            if (IsTransient() || other.IsTransient()) return false;

            return Id.Equals(other.Id);
        }

        public override string ToString()
        {
            return $"[{this.GetRealType().Name} : {Id}]";
        }

        #endregion

        #region Operators

        public static bool operator ==(Entity left, Entity right)
        {
            return Equals(left, right);
        }

        public static bool operator !=(Entity left, Entity right)
        {
            return !(left == right);
        }

        #endregion
    }
}

در این کلاس یکسری خصوصیات پایه ای مانند Id و متدهای مشترک بین Entityها قرار گرفته شده است. این کلاس پایه تمام Entity‌های سیستم می‌باشد.

پیاده سازی پیش فرض از واسط ICrudApplicationService به شکل زیر می‌باشد.

namespace MvcFramework.Framework.Application.Services
{
    public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel> :
        CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest,
            PagedListResponse<TModel, PagedListRequest>, DynamicListRequest>
        where TEntity : Entity
        where TCreateModel : class
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
    {
        protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper)
        {
        }
    }

    public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel,
        TDynamicListRequest> :
        CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest,
            PagedListResponse<TModel, PagedListRequest>, TDynamicListRequest>
        where TEntity : Entity
        where TCreateModel : class
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
        where TDynamicListRequest : DynamicListRequest
    {
        protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper)
        {
        }
    }

    public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel,
        TPagedListRequest,
        TPagedListResponse> :
        CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, TPagedListRequest,
            TPagedListResponse,
            DynamicListRequest>
        where TEntity : Entity
        where TCreateModel : class
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
        where TPagedListRequest : PagedListRequest, new()
        where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest>, new()
    {
        protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper)
        {
        }
    }

    public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel,
        TPagedListRequest,
        TPagedListResponse, TDynamicListRequest> : ApplicationService,
        ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, TPagedListRequest, TPagedListResponse,
            TDynamicListRequest>
        where TEntity : Entity
        where TCreateModel : class
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
        where TPagedListRequest : PagedListRequest, new()
        where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest>, new()
        where TDynamicListRequest : DynamicListRequest

    {
        #region Constructor

        protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper)
        {
            Guard.ArgumentNotNull(unitOfWork, nameof(unitOfWork));
            Guard.ArgumentNotNull(mapper, nameof(mapper));

            UnitOfWork = unitOfWork;
            Mapper = mapper;
            EntitySet = UnitOfWork.Set<TEntity>();
        }

        #endregion

        #region Properties

        protected IQueryable<TEntity> UnTrackedEntitySet => EntitySet.AsNoTracking();
        protected IUnitOfWork UnitOfWork { get; }
        protected IMapper Mapper { get; }
        protected IDbSet<TEntity> EntitySet { get; }

        #endregion

        #region ICrudApplicationService Members

        #region Methods

        [Transactional]
        public virtual void Create(TCreateModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var entity = Mapper.Map<TEntity>(model);

            EntitySet.Add(entity);
            UnitOfWork.SaveChanges();
        }

        [Transactional]
        public virtual void Create(IList<TCreateModel> models)
        {
            Guard.ArgumentNotEmpty(models, nameof(models));

            var entities = Mapper.Map<IList<TEntity>>(models);

            UnitOfWork.AddRange(entities);
            UnitOfWork.SaveChanges();
        }

        [Transactional]
        public virtual Task CreateAsync(TCreateModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var entity = Mapper.Map<TEntity>(model);

            EntitySet.Add(entity);
            return UnitOfWork.SaveChangesAsync();
        }

        [Transactional]
        public virtual Task CreateAsync(IList<TCreateModel> models)
        {
            Guard.ArgumentNotEmpty(models, nameof(models));

            var entities = Mapper.Map<IList<TEntity>>(models);

            UnitOfWork.AddRange(entities);
            return UnitOfWork.SaveChangesAsync();
        }


        [Transactional]
        public virtual void Edit(TEditModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var entity = Mapper.Map<TEntity>(model);

            UnitOfWork.MarkAsChanged(entity);
            UnitOfWork.SaveChanges();
        }

        [Transactional]
        public virtual void Edit(IList<TEditModel> models)
        {
            Guard.ArgumentNotNull(models, nameof(models));
            Guard.ArgumentNotEmpty(models, nameof(models));

            var entities = Mapper.Map<IList<TEntity>>(models);

            UnitOfWork.UpdateRange(entities);
            UnitOfWork.SaveChanges();
        }

        [Transactional]
        public virtual Task EditAsync(TEditModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var entity = Mapper.Map<TEntity>(model);

            UnitOfWork.MarkAsChanged(entity);
            return UnitOfWork.SaveChangesAsync();
        }

        [Transactional]
        public virtual Task EditAsync(IList<TEditModel> models)
        {
            Guard.ArgumentNotNull(models, nameof(models));
            Guard.ArgumentNotEmpty(models, nameof(models));

            var entities = Mapper.Map<IList<TEntity>>(models);

            UnitOfWork.UpdateRange(entities);
            return UnitOfWork.SaveChangesAsync();
        }


        public virtual IList<TModel> GetList()
        {
            return EntitySet.ProjectToList<TModel>(Mapper.ConfigurationProvider);
        }

        public virtual DynamicListResponse GetDynamicList(TDynamicListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));

            var query = ApplyFiltering(request);

            return query.ProjectTo<TModel>().ToListResponse(request);
        }

        public virtual TPagedListResponse GetPagedList(TPagedListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));

            var query = ApplyFiltering(request);

            request.TotalCount = query.LongCount();

            query = ApplySorting(query, request);
            query = ApplyPaging(query, request);

            var result = query.ProjectToList<TModel>(Mapper.ConfigurationProvider);

            return new TPagedListResponse
            {
                Result = result,
                Request = request
            };
        }

        public virtual IList<LookupItem> GetLookup()
        {
            return EntitySet.ProjectToList<LookupItem>(Mapper.ConfigurationProvider);
        }

        public virtual TModel GetById(long id)
        {
            Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));

            var entity =
                EntitySet.Where(a => a.Id == id).ProjectToFirstOrDefault<TModel>(Mapper.ConfigurationProvider);

            if (entity == null)
                throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetById");

            return entity;
        }

        public virtual TEditModel GetForEdit(long id)
        {
            Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));

            var entity =
                EntitySet.Where(a => a.Id == id).ProjectToFirstOrDefault<TEditModel>(Mapper.ConfigurationProvider);

            if (entity == null)
                throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetForEdit");

            return entity;
        }

        public virtual bool Exists(long id)
        {
            Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));

            return EntitySet.Any(a => a.Id == id);
        }

        public virtual async Task<IList<TModel>> GetListAsync()
        {
            return await EntitySet.ProjectToListAsync<TModel>(Mapper.ConfigurationProvider);
        }

        public virtual Task<DynamicListResponse> GetDynamicListAsync(TDynamicListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));

            var query = ApplyFiltering(request);

            return query.ProjectTo<TModel>().ToListResponseAsync(request);
        }

        public virtual async Task<TPagedListResponse> GetPagedListAsync(TPagedListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));

            var query = ApplyFiltering(request);

            request.TotalCount = await query.LongCountAsync().ConfigureAwait(false);

            query = ApplySorting(query, request);
            query = ApplyPaging(query, request);

            var result = await query.ProjectToListAsync<TModel>(Mapper.ConfigurationProvider).ConfigureAwait(false);

            return new TPagedListResponse
            {
                Result = result,
                Request = request
            };
        }

        public virtual async Task<IList<LookupItem>> GetLookupAsync()
        {
            return await EntitySet.ProjectToListAsync<LookupItem>(Mapper.ConfigurationProvider);
        }

        public virtual async Task<TModel> GetByIdAsync(long id)
        {
            Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));

            var entity = await UnTrackedEntitySet.Where(a => a.Id == id)
                .ProjectToFirstOrDefaultAsync<TModel>(Mapper.ConfigurationProvider);

            if (entity == null)
                throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetByIdAsync");

            return entity;
        }

        public virtual async Task<TEditModel> GetForEditAsync(long id)
        {
            Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));

            var entity = await UnTrackedEntitySet.Where(a => a.Id == id)
                .ProjectToFirstOrDefaultAsync<TEditModel>(Mapper.ConfigurationProvider);

            if (entity == null)
                throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetForEditAsync");

            return entity;
        }

        public virtual Task<bool> ExistsAsync(long id)
        {
            Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));

            return EntitySet.AnyAsync(a => a.Id == id);
        }


        [Transactional]
        public virtual void Delete(TDeleteModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var entity = Mapper.Map<TEntity>(model);

            UnitOfWork.MarkAsDeleted(entity);
            UnitOfWork.SaveChanges();
        }

        [Transactional]
        public virtual void Delete(IList<TDeleteModel> models)
        {
            Guard.ArgumentNotEmpty(models, nameof(models));
            Guard.ArgumentNotEmpty(models, nameof(models));

            var entities = Mapper.Map<IList<TEntity>>(models);

            UnitOfWork.RemoveRange(entities);
            UnitOfWork.SaveChanges();
        }

        [Transactional]
        public virtual Task DeleteAsync(TDeleteModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var entity = Mapper.Map<TEntity>(model);

            UnitOfWork.MarkAsDeleted(entity);
            return UnitOfWork.SaveChangesAsync();
        }

        [Transactional]
        public virtual Task DeleteAsync(IList<TDeleteModel> models)
        {
            Guard.ArgumentNotEmpty(models, nameof(models));
            Guard.ArgumentNotEmpty(models, nameof(models));

            var entities = Mapper.Map<IList<TEntity>>(models);

            UnitOfWork.RemoveRange(entities);
            return UnitOfWork.SaveChangesAsync();
        }

        #endregion

        #endregion

        #region Protected Methods

        /// <summary>
        ///     Apply Filtering To GetDynamicList
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        protected virtual IQueryable<TEntity> ApplyFiltering(TDynamicListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));

            return UnTrackedEntitySet;
        }

        /// <summary>
        ///     Apply Filtering To GetPagedList and GetPagedListAsync
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        protected virtual IQueryable<TEntity> ApplyFiltering(TPagedListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));

            return UnTrackedEntitySet;
        }

        /// <summary>
        ///     Apply Sorting To GetPagedList and GetPagedListAsync
        /// </summary>
        /// <param name="query">query</param>
        /// <param name="request">PagedListRequest</param>
        /// <returns></returns>
        protected virtual IQueryable<TEntity> ApplySorting(IQueryable<TEntity> query, TPagedListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));
            Guard.ArgumentNotNull(query, nameof(query));

            return !request.SortBy.IsEmpty() ? query.OrderBy(request.SortBy) : query.OrderByDescending(e => e.Id);
        }

        /// <summary>
        ///     Apply Paging To GetPagedList and GetPagedListAsync
        /// </summary>
        /// <param name="request">PagedListRequest</param>
        /// <param name="query">query</param>
        /// <returns></returns>
        protected virtual IQueryable<TEntity> ApplyPaging(IQueryable<TEntity> query, TPagedListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));
            Guard.ArgumentNotNull(query, nameof(query));

            return request != null
                ? query.Page((request.PageNumber - 1) * request.PageSize, request.PageSize)
                : query;
        }

        #endregion
    }
}

همه متد‌های این کلاس پایه، قابلیت override شدن را دارند. به عنوان مثال یکسری متد با دسترسی protected مثلا ApplyFiltering هم برای بازنویسی نحوه فیلتر کردن خروجی GetPagedList می‌توانند در SubClassها مورد استفاده قرار گیرند. برای مباحث مرتب سازی هم از کتابخانه System.Linq.Dynamic استفاده شده است. 

برای مکانیزم Validation خودکار هم از کتابخانه FluentValidatoin کمک گرفته شده است و با استفاده از Interceptor زیر در صورت یافتن Validator مربوط به Model ورودی، عملیات اعتبارسنجی انجام میگرد و در صورت معتبر نبودن، استثنایی صادر خواهد شد که حاوی اطلاعات مربوط به جزئیات خطاها نیز می‌باشد.

ValidatorInterceptor

namespace MvcFramework.Framework.Aspects.Validation
{
    public class ValidatorInterceptor : ISyncInterceptionBehavior
    {
        private readonly IValidatorFactory _validatorFactory;

        public ValidatorInterceptor(IValidatorFactory validatorFactory)
        {
            _validatorFactory = validatorFactory;
        }

        public IMethodInvocationResult Intercept(ISyncMethodInvocation methodInvocation)
        {
            var argumentValues = methodInvocation.Arguments.Select(a => a.Value).ToArray();

            var validator = new MethodInvocationValidator(_validatorFactory, methodInvocation.MethodInfo,
                argumentValues);

            validator.Validate();

            return methodInvocation.InvokeNext();
        }
    }
}

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

کلاس MethodInvocationValidator

namespace MvcFramework.Framework.Aspects.Validation
{
    internal class MethodInvocationValidator
    {
        #region Constructor

        public MethodInvocationValidator(IValidatorFactory validatorFactory, MethodInfo method,
            object[] parameterValues)
        {
            Guard.ArgumentNotNull(method, nameof(method));
            Guard.ArgumentNotNull(parameterValues, nameof(parameterValues));
            Guard.ArgumentNotNull(validatorFactory, nameof(validatorFactory));

            _method = method;
            _parameterValues = parameterValues;
            _validatorFactory = validatorFactory;
            _parameters = method.GetParameters();

            _parametersToBeNormalized = new List<IShouldNormalize>();
        }

        #endregion

        #region Public Methods

        public void Validate()
        {
            if (!CheckShouldBeValidate()) return;

            foreach (var parameterValue in _parameterValues)
                ValidateMethodParameter(parameterValue);

            foreach (var parameterToBeNormalized in _parametersToBeNormalized)
                parameterToBeNormalized.Normalize();
        }

        #endregion

        #region Fields

        private readonly MethodInfo _method;
        private readonly object[] _parameterValues;
        private readonly ParameterInfo[] _parameters;
        private readonly IValidatorFactory _validatorFactory;
        private readonly List<IShouldNormalize> _parametersToBeNormalized;

        #endregion

        #region Private Methods

        private bool CheckShouldBeValidate()
        {
            if (!_method.IsPublic)
                return false;

            if (IsValidationDisabled())
                return false;

            if (_parameters.IsNullOrEmpty())
                return false;

            if (_parameters.Length != _parameterValues.Length)
                throw new Exception("Method parameter count does not match with argument count!");

            return true;
        }

        private bool IsValidationDisabled()
        {
            if (_method.IsDefined(typeof(EnableValidationAttribute), true))
                return false;

            return ReflectionHelper
                       .GetSingleAttributeOfMemberOrDeclaringTypeOrDefault<DisableValidationAttribute>(_method) != null;
        }

        private void ValidateMethodParameter(object parameterValue)
        {
            if (parameterValue == null) return;

            var parameterValueList = parameterValue as IEnumerable<object>;
            if (parameterValueList != null)
            {
                var valueList = parameterValueList.ToList();

                ValidateMethodParameterValues(valueList);
            }
            else
            {
                ValidateMethodParameterValues(new List<object> { parameterValue });
            }

            if (parameterValue is IShouldNormalize)
                _parametersToBeNormalized.Add(parameterValue as IShouldNormalize);
        }

        private void ValidateMethodParameterValues(List<object> valueList)
        {
            var ruleSet = GetRuleSet(_method);

            var validator = _validatorFactory.GetValidator(valueList.First().GetType());
            if (validator == null) return;

            foreach (var item in valueList)
                ValidateWithReflection(validator, item, ruleSet);
        }

        private static string GetRuleSet(MemberInfo method)
        {
            const string @default = "default";

            var attribute = method.GetCustomAttribute<ValidateWithRuleAttribute>();

            if (attribute == null)
                return @default;

            var rules = new List<string> { @default };

            rules.AddRange(attribute.RuleSetNames);

            return string.Join(",", rules).TrimEnd(',');
        }

        private static void ValidateAndThrow<T>(IValidator<T> validator, T argument, string ruleSet)
        {
            validator.ValidateAndThrow(argument, ruleSet);
        }

        private void ValidateWithReflection(IValidator validator, object argument, string ruleSet)
        {
            GetType().GetMethod(nameof(ValidateAndThrow), BindingFlags.Static | BindingFlags.NonPublic)
                .MakeGenericMethod(argument.GetType())
                .Invoke(null, new[] { validator, argument, ruleSet });
        }

        #endregion
    }
}

در متد Validate آن ابتدا چک می‌شود که آیا اعتبارسنجی می‌بایستی انجام شود یا خیر. سپس تک تک آرگومان‌های ارسالی را با استفاده از متد ValidateMethodParameter وارد مکانیزم اعتبارسنجی می‌کند. در داخل این متد ابتدا نوع آرگومان تشخیص داده شده و این مقادیر به متد ValidateMethodParameterValues ارسال شده و داخل آن ابتدا Validator مرتبط را یافته و آن را به متد ValidateWithReflection ارسال می‌کند. در این بین متد GetRuleSets وظیفه واکشی اسامی RuleSet هایی که بر روی متد مورد نظر تنظیم شده اند را دارد؛ برای مواقعی که از یک ویومدل برای ویرایش، درج و حذف استفاده کنید، در این صورت با توجه به اینکه برای یک ویومدل یک Validator خواهید داشت، امکانات RuleSet مربوط به FluentValidation کارساز خواهند بود. به این صورت که برای هر کدام از عملیات حذف، ویرایش و درج، RuleSet مناسب را تعریف کرده و با استفاده از ValidateWithRuleAttribute برروی متدهای مورد نظر، این ruleها در سیستم اعتبارسنجی ارائه شده اعمال خواهند شد.

با توجه به اینکه متد ValidateAndThrow در واسط IValidator‎<T>‎ تعریف شده‌است و از آنجاییکه ما نوع داده مدل مورد نظر را هم نداریم لازم است با استفاده از MakeGenericMethod به صورت داینامیک نوع داده T را مشخص کنیم و فراخوانی متد استاتیک ValidatorWithThrow‎<T>‎ را با Reflection انجام دهیم.

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

namespace MvcFramework.Framework
{
    public class FrameworkRegistry : Registry
    {
        public FrameworkRegistry()
        {
            For<IValidatorFactory>().Singleton().Use<StructureMapValidatorFactory>();

            Scan(scan =>
            {
                scan.TheCallingAssembly();
                scan.WithDefaultConventions();
                scan.LookForRegistries();
            });

            Policies.Interceptors(new DynamicProxyInterceptorPolicy(f => typeof(IApplicationService).IsAssignableFrom(f), typeof(ValidatorInterceptor),typeof(TransactionInterceptor)));
        }
    }
}

در کد بالا با استفاده از DynamicProxyInterceptorPolicy، یک Policy را برای Intercept کردن متدهای مربوط به کلاس هایی که پیاده ساز IApplicationService می‌باشند، معرفی کرده‌ایم.

کار اعتبارسنجی هم به پایان رسید؛ در زیر استفاده از سرویس پایه معرفی شده را می‌توانید مشاهده کنید.

namespace MyApp.ServiceLayer.Roles
{
    public interface IRoleApplicationService :
        ICrudApplicationService<RoleViewModel, RoleCreateViewModel, RoleEditViewModel, RoleDeleteViewModel, RolePagedListRequest, RoleListViewModel>
    {
    }
}

namespace MyApp.ServiceLayer.Roles
{
    public class RoleApplicationService :
        CrudApplicationService<Role, RoleViewModel, RoleCreateViewModel, RoleEditViewModel, RoleDeleteViewModel, RolePagedListRequest, RoleListViewModel>,
        IRoleApplicationService
    {
        #region Constructor

        public RoleApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper)
        {
        }

        #endregion
    }
}


نکته: در این لایه بندی نکات مربوط به مطلب «پیاده سازی ماژولار Autofac» نیز با استفاده از StructureMap اعمال شده است. بدین ترتیب در هر لایه یک Registry مربوط به StructureMap ایجاد شده است. به شکل زیر:

FrameworkRegistry

namespace MyApp.Framework
{
    public class FrameworkRegistry : Registry
    {
        public FrameworkRegistry()
        {
            For<IValidatorFactory>().Singleton().Use<StructureMapValidatorFactory>();

            Scan(scan =>
            {
                scan.TheCallingAssembly();
                scan.WithDefaultConventions();
                scan.AssembliesFromApplicationBaseDirectory();
                scan.AddAllTypesOf<IRunOnEndTask>();
                scan.AddAllTypesOf<IRunOnOwinStartupTask>();
                scan.AddAllTypesOf<IRunOnStartTask>();
                scan.AddAllTypesOf<IRunOnBeginRequestTask>();
                scan.AddAllTypesOf<IRunOnErrorTask>();
                scan.AddAllTypesOf<IRunOnEndRequestTask>();

                scan.LookForRegistries();
            });

            Policies.Interceptors(new DynamicProxyInterceptorPolicy(f => typeof(IApplicationService).IsAssignableFrom(f), typeof(ValidatorInterceptor)/*, typeof(TransactionInterceptor)*/));
        }
    }
}


DataLayerRegistry

namespace MyApp.DataLayer
{
    public class DataLayerRegistry : Registry
    {
        public DataLayerRegistry()
        {
            Scan(scan =>
            {
                scan.TheCallingAssembly();
                scan.WithDefaultConventions();
                scan.AssembliesFromApplicationBaseDirectory();
                scan.AddAllTypesOf<IRunOnStartTask>();
            });

            //todo:use container per request (Nested Containers) instead of HttpContextLifeCycle
            For<IUnitOfWork>().Use<ApplicationDbContext>();
        }
    }
}


ServiceLayerRegistry

namespace MyApp.ServiceLayer
{
    public class ServiceLayerRegistry : Registry
    {
        #region Constructor

        public ServiceLayerRegistry()
        {
            Scan(scan =>
            {
                scan.TheCallingAssembly();
                scan.WithDefaultConventions();
                scan.AssembliesFromApplicationBaseDirectory();
                scan.AddAllTypesOf<IRunOnEndTask>();
                scan.AddAllTypesOf<IRunOnOwinStartupTask>();
                scan.AddAllTypesOf<IRunOnStartTask>();
                scan.AddAllTypesOf<IRunOnBeginRequestTask>();
                scan.AddAllTypesOf<IRunOnErrorTask>();
                scan.AddAllTypesOf<IRunOnEndRequestTask>();

                scan.Assembly(typeof(DataLayerRegistry).Assembly);
                scan.LookForRegistries();

                scan.AddAllTypesOf<Profile>().NameBy(item => item.FullName);
                scan.AddAllTypesOf<IHaveCustomMappings>().NameBy(item => item.FullName);
            });

            FluentValidationConfig();
            AutoMapperConfig();
        }

        #endregion

        #region Private Methods

        private void AutoMapperConfig()
        {
            For<MapperConfiguration>().Singleton().Use("MapperConfig", ctx =>
            {
                var config = new MapperConfiguration(cfg =>
                {
                    cfg.CreateMissingTypeMaps = true;
                    AddProfiles(ctx, cfg);
                    AddIHaveCustomMappings(ctx, cfg);
                    AddMapFrom(cfg);
                });

                config.AssertConfigurationIsValid();

                return config;
            });

            For<IMapper>().Singleton().Use(ctx => ctx.GetInstance<MapperConfiguration>().CreateMapper(ctx.GetInstance));
        }

        private void FluentValidationConfig()
        {
            AssemblyScanner.FindValidatorsInAssembly(Assembly.GetExecutingAssembly())
                .ForEach(result =>
                {
                    For(result.InterfaceType)
                        .Singleton()
                        .Use(result.ValidatorType);
                });
        }

        private static void AddMapFrom(IProfileExpression cfg)
        {
            var types = typeof(RoleViewModel).Assembly.GetExportedTypes();
            var maps = (from t in types
                        from i in t.GetInterfaces()
                        where i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IMapFrom<>) && !t.IsAbstract &&
                              !t.IsInterface
                        select new
                        {
                            Source = i.GetGenericArguments()[0],
                            Destination = t
                        }).ToArray();

            foreach (var map in maps)
                cfg.CreateMap(map.Source, map.Destination);
        }

        private static void AddProfiles(IContext ctx, IMapperConfigurationExpression cfg)
        {
            var profiles = ctx.GetAllInstances<Profile>().ToList();
            foreach (var profile in profiles)
                cfg.AddProfile(profile);
        }

        private static void AddIHaveCustomMappings(IContext ctx, IMapperConfigurationExpression cfg)
        {
            var mappings = ctx.GetAllInstances<IHaveCustomMappings>().ToList();
            foreach (var mapping in mappings)
                mapping.CreateMappings(cfg);
        }

        #endregion
    }
}


WebRegistry

namespace MyApp.Web
{
    public class WebRegistry : Registry
    {
        public WebRegistry()
        {
            Scan(scan =>
            {
                scan.TheCallingAssembly();
                scan.WithDefaultConventions();
                scan.AssembliesFromApplicationBaseDirectory();
                
                scan.AddAllTypesOf<IRunOnEndTask>();
                scan.AddAllTypesOf<IRunOnOwinStartupTask>();
                scan.AddAllTypesOf<IRunOnStartTask>();
                scan.AddAllTypesOf<IRunOnBeginRequestTask>();
                scan.AddAllTypesOf<IRunOnErrorTask>();
                scan.AddAllTypesOf<IRunOnEndRequestTask>();

                scan.Assembly(typeof(ServiceLayerRegistry).Assembly);
                scan.LookForRegistries();
            });
        }
    }
}

در این طراحی، لایه Web یا همان Presentation به DataLayer و DomainClasses هیچ ارجاعی ندارد.


در قسمت بعد استفاده از این سرویس را در یک برنامه ASP.NET MVC با هم بررسی خواهیم کرد. 

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

اشتراک‌ها
چگونه قبل از build کل پروژه، تنها بخشی از کد C# را اجرا کنیم!

can’t be the only person who hates waiting for the code to compile and run… Especially when I want to test only a small part of it! That’s a total waste of time! But good news everyone, I found a way to run a part of my C# code without building the whole project! 

Ctrl + E, E – the magical shortcut! 

چگونه قبل از build کل پروژه، تنها بخشی از کد C# را اجرا کنیم!
نظرات مطالب
C# 7 - Tuple return types and deconstruction
مزیت خروجی‌های از نوع Tuple برای طراحان کتابخانه‌ها 
تبدیل خروجی‌های dynamic قبلی
// query dapper via "dynamic"
int id = ...
var row = conn.QuerySingle("select Name, LastUpdated from Records where Id=@id", new {id});
string name = row.Name; // "dynamic"
به tuple جدید
// query dapper via C# 7 tuples
int id = ...
var row = conn.QuerySingle<(string name, DateTime lastUpdated)>(
     "select Name, LastUpdated from Records where Id=@id", new {id});
// use row.name and row.lastUpdated
با مزیت نداشتن هیچ نوع سربار اضافه‌ای (کد dynamic ابتدایی جهت پردازش، DLR را وارد صحنه می‌کند؛ اما کد فوق خیر. به علاوه چون value type است، حتی سربار heap allocation را هم ندارد).
مطالب
Blazor 5x - قسمت 29 - برنامه‌ی Blazor WASM - یک تمرین: رزرو کردن یک اتاق انتخابی


در قسمت قبل که لیست اتاق‌های دریافتی از Web API را نمایش دادیم، هرکدام از آن‌ها، به همراه یک دکمه‌ی Book هم هستند (تصویر فوق) که هدف از آن، فراهم آوردن امکان رزرو کردن آن اتاق، توسط کاربران سایت است. این قسمت را می‌توان به عنوان تمرینی جهت یادآوری مراحل مختلف تهیه‌ی یک Web API و قسمت‌های سمت کلاینت آن، تکمیل کرد.



تهیه موجودیت و مدل متناظر با صفحه‌ی ثبت رزرو یک اتاق

تا اینجا در برنامه‌ی سمت کلاینت، زمانیکه بر روی دکمه‌ی Go صفحه‌ی اول کلیک می‌کنیم، تاریخ شروع رزرو و تعداد روز مدنظر، به صفحه‌ی مشاهده‌ی لیست اتاق‌ها ارسال می‌شود. اکنون می‌خواهیم در این لیست اتاق‌های نمایش داده شده، اگر بر روی لینک Book اتاقی کلیک شد، به صفحه‌ی اختصاصی رزرو آن اتاق هدایت شویم (مانند تصویر فوق). به همین جهت نیاز است موجودیت متناظر با اطلاعاتی را که قرار است از کاربر دریافت کنیم، به صورت زیر به پروژه‌ی BlazorServer.Entities اضافه کنیم:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BlazorServer.Entities
{
    public class RoomOrderDetail
    {
        public int Id { get; set; }

        [Required]
        public string UserId { get; set; }

        [Required]
        public string StripeSessionId { get; set; }

        public DateTime CheckInDate { get; set; }

        public DateTime CheckOutDate { get; set; }

        public DateTime ActualCheckInDate { get; set; }

        public DateTime ActualCheckOutDate { get; set; }

        public long TotalCost { get; set; }

        public int RoomId { get; set; }

        public bool IsPaymentSuccessful { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public string Email { get; set; }

        public string Phone { get; set; }

        [ForeignKey("RoomId")]
        public HotelRoom HotelRoom { get; set; }

        public string Status { get; set; }
    }
}
در اینجا مشخصات شروع و پایان رزرو یک اتاق مشخص و مشخصات کاربری که قرار است این فرم را پر کند، مشاهده می‌کنید که Status یا وضعیت آن، در پروژه‌ی مشترک BlazorServer.Common به صورت زیر تعریف می‌شود:
namespace BlazorServer.Common
{
    public static class BookingStatus
    {
        public const string Pending = "Pending";
        public const string Booked = "Booked";
        public const string CheckedIn = "CheckedIn";
        public const string CheckedOutCompleted = "CheckedOut";
        public const string NoShow = "NoShow";
        public const string Cancelled = "Cancelled";
    }
}
پس از این تعاریف، DbSet آن‌را نیز به ApplicationDbContext اضافه می‌کنیم:
namespace BlazorServer.DataAccess
{
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public DbSet<RoomOrderDetail> RoomOrderDetails { get; set; }

        // ...
    }
}
بنابراین مرحله‌ی بعدی، ایجاد و اجرای Migrations متناظر با این جدول جدید است. برای این منظور با استفاده از خط فرمان به پوشه‌ی BlazorServer.DataAccess وارد شده و دستورات زیر را اجرا می‌کنیم:
dotnet tool update --global dotnet-ef --version 5.0.4
dotnet build
dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddRoomOrderDetails --context ApplicationDbContext
dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext
این دستورات به پروژه‌ی آغازین WebApi اشاره می‌کنند که قرار است از طریق سرویسی، با بانک اطلاعاتی ارتباط برقرار کند.

پس از تعریف یک موجودیت، یک DTO متناظر با آن‌را که جهت مدلسازی UI از آن استفاده خواهیم کرد، در پروژه‌ی BlazorServer.Models ایجاد می‌کنیم:
using System;
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Models
{
    public class RoomOrderDetailsDTO
    {
        public int Id { get; set; }

        [Required]
        public string UserId { get; set; }

        [Required]
        public string StripeSessionId { get; set; }

        [Required]
        public DateTime CheckInDate { get; set; }

        [Required]
        public DateTime CheckOutDate { get; set; }

        public DateTime ActualCheckInDate { get; set; }

        public DateTime ActualCheckOutDate { get; set; }

        [Required]
        public long TotalCost { get; set; }

        [Required]
        public int RoomId { get; set; }

        public bool IsPaymentSuccessful { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public string Email { get; set; }

        public string Phone { get; set; }

        public HotelRoomDTO HotelRoomDTO { get; set; }

        public string Status { get; set; }
    }
}
و همچنین در پروژه‌ی BlazorServer.Models.Mappings، نگاشت دوطرفه‌ی AutoMapper آن‌را نیز برقرار می‌کنیم؛ تا در حین تبدیل اطلاعات بین این دو، نیازی به تکرار سطرهای مقدار دهی اطلاعات خواص، نباشد:
namespace BlazorServer.Models.Mappings
{
    public class MappingProfile : Profile
    {
        public MappingProfile()
        {
            // ... 
            CreateMap<RoomOrderDetail, RoomOrderDetailsDTO>().ReverseMap(); // two-way mapping
        }
    }
}


ایجاد سرویسی برای کار با جدول RoomOrderDetails

در برنامه‌ی سمت کلاینت برای کار با بانک اطلاعاتی، دیگر نمی‌توان از سرویس‌های سمت سرور به صورت مستقیم استفاده کرد. به همین جهت آن‌ها را از طریق یک Web API endpoint، در معرض دید استفاده کننده قرار می‌دهیم. اما پیش از اینکار، سرویس سمت سرور Web API باید بتواند با سرویس دسترسی به اطلاعات جدول RoomOrderDetails، کار کند. بنابراین در ادامه این سرویس را تهیه می‌کنیم:
namespace BlazorServer.Services
{
    public interface IRoomOrderDetailsService
    {
        Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details);

        Task<List<RoomOrderDetailsDTO>> GetAllRoomOrderDetailsAsync();

        Task<RoomOrderDetailsDTO> GetRoomOrderDetailAsync(int roomOrderId);

        Task<bool> IsRoomBookedAsync(int RoomId, DateTime checkInDate, DateTime checkOutDate);

        Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(int id);

        Task<bool> UpdateOrderStatusAsync(int RoomOrderId, string status);
    }
}
که به صورت زیر پیاده سازی می‌شود:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        private readonly ApplicationDbContext _dbContext;
        private readonly IMapper _mapper;
        private readonly IConfigurationProvider _mapperConfiguration;

        public RoomOrderDetailsService(ApplicationDbContext dbContext, IMapper mapper)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
            _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
            _mapperConfiguration = mapper.ConfigurationProvider;
        }

        public async Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details)
        {
            var roomOrder = _mapper.Map<RoomOrderDetail>(details);
            roomOrder.Status = BookingStatus.Pending;
            var result = await _dbContext.RoomOrderDetails.AddAsync(roomOrder);
            await _dbContext.SaveChangesAsync();
            return _mapper.Map<RoomOrderDetailsDTO>(result.Entity);
        }

        public Task<List<RoomOrderDetailsDTO>> GetAllRoomOrderDetailsAsync()
        {
            return _dbContext.RoomOrderDetails
                            .Include(roomOrderDetail => roomOrderDetail.HotelRoom)
                            .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                            .ToListAsync();
        }

        public async Task<RoomOrderDetailsDTO> GetRoomOrderDetailAsync(int roomOrderId)
        {
            var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails
                                            .Include(u => u.HotelRoom)
                                                .ThenInclude(x => x.HotelRoomImages)
                                            .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                                            .FirstOrDefaultAsync(u => u.Id == roomOrderId);

            roomOrderDetailsDTO.HotelRoomDTO.TotalDays =
                roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days;
            return roomOrderDetailsDTO;
        }

        public Task<bool> IsRoomBookedAsync(int RoomId, DateTime checkInDate, DateTime checkOutDate)
        {
            return _dbContext.RoomOrderDetails
                .AnyAsync(
                    roomOrderDetail =>
                        roomOrderDetail.RoomId == RoomId &&
                        roomOrderDetail.IsPaymentSuccessful &&
                        (
                            (checkInDate < roomOrderDetail.CheckOutDate && checkInDate > roomOrderDetail.CheckInDate) ||
                            (checkOutDate > roomOrderDetail.CheckInDate && checkInDate < roomOrderDetail.CheckInDate)
                        )
                );
        }

        public Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(int id)
        {
            throw new NotImplementedException();
        }

        public Task<bool> UpdateOrderStatusAsync(int RoomOrderId, string status)
        {
            throw new NotImplementedException();
        }
    }
}
توضیحات:
- از متد CreateAsync برای تبدیل مدل فرم ثبت اطلاعات، به یک رکورد جدول RoomOrderDetails، استفاده می‌کنیم.
- متد GetAllRoomOrderDetailsAsync، لیست تمام سفارش‌های ثبت شده را بازگشت می‌دهد.
- متد GetRoomOrderDetailAsync بر اساس شماره اتاقی که دریافت می‌کند، لیست سفارشات آن اتاق خاص را بازگشت می‌دهد. این لیست به علت استفاده از Include‌های تعریف شده، به همراه مشخصات اتاق و همچنین تصاویر مرتبط با آن اتاق نیز هست.
- متد IsRoomBookedAsync بر اساس شماره اتاق و بازه‌ی زمانی درخواستی توسط یک کاربر مشخص می‌کند که آیا اتاق خالی شده‌است یا خیر؟

پس از تعریف این سرویس، به کلاس آغازین پروژه‌ی Web API مراجعه کرده و آن‌را به سیستم تزریق وابستگی‌ها، معرفی می‌کنیم:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IRoomOrderDetailsService, RoomOrderDetailsService>();
            // ...
 
 
تشکیل سرویس ابتدایی کار با RoomOrderDetails در پروژه‌ی WASM

در ادامه، تعاریف خالی سرویس سمت کلاینت کار با RoomOrderDetails  را به پروژه‌ی WASM اضافه می‌کنیم. تکمیل این سرویس را به قسمت بعدی واگذار خواهیم کرد:
namespace BlazorWasm.Client.Services
{
    public interface IClientRoomOrderDetailsService
    {
        Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(RoomOrderDetailsDTO details);
        Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details);
    }
}
با این پیاده سازی ابتدایی:
namespace BlazorWasm.Client.Services
{
    public class ClientRoomOrderDetailsService : IClientRoomOrderDetailsService
    {
        private readonly HttpClient _httpClient;

        public ClientRoomOrderDetailsService(HttpClient httpClient)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        }

        public Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(RoomOrderDetailsDTO details)
        {
            throw new NotImplementedException();
        }

        public Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details)
        {
            throw new NotImplementedException();
        }
    }
}
که این مورد نیز باید به نحو زیر به سیستم تزریق وابستگی‌های برنامه‌ی سمت کلاینت در فایل Program.cs آن اضافه شود:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...
            builder.Services.AddScoped<IClientRoomOrderDetailsService, ClientRoomOrderDetailsService>();
            // ...
        }
    }
}


تعریف مدل فرم ثبت اطلاعات سفارش

پس از تدارک مقدمات فوق، اکنون می‌توانیم کار تکمیل فرم ثبت اطلاعات سفارش را شروع کنیم. به همین جهت مدل مخصوص آن‌را در برنامه‌ی سمت کلاینت به صورت زیر تشکیل می‌دهیم:
using BlazorServer.Models;

namespace BlazorWasm.Client.Models.ViewModels
{
    public class HotelRoomBookingVM
    {
        public RoomOrderDetailsDTO OrderDetails { get; set; }
    }
}


تعریف کامپوننت جدید RoomDetails و مقدار دهی اولیه‌ی مدل آن

در ادامه فایل جدید BlazorWasm.Client\Pages\HotelRooms\RoomDetails.razor را ایجاد کرده و به صورت زیر مقدار دهی اولیه می‌کنیم:
@page "/hotel/room-details/{Id:int}"

@inject IJSRuntime JsRuntime
@inject ILocalStorageService LocalStorage
@inject IClientHotelRoomService HotelRoomService

@if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null)
{
    <div class="spinner"></div>
}
else
{

}

@code {
    [Parameter]
    public int? Id { get; set; }

    HotelRoomBookingVM HotelBooking  = new HotelRoomBookingVM();
    int NoOfNights = 1;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            HotelBooking.OrderDetails = new RoomOrderDetailsDTO();
            if (Id != null)
            {
                if (await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking) != null)
                {
                    var roomInitialInfo = await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking);
                    HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync(
                        Id.Value, roomInitialInfo.StartDate, roomInitialInfo.EndDate);
                    NoOfNights = roomInitialInfo.NoOfNights;
                    HotelBooking.OrderDetails.CheckInDate = roomInitialInfo.StartDate;
                    HotelBooking.OrderDetails.CheckOutDate = roomInitialInfo.EndDate;
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = roomInitialInfo.NoOfNights;
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount =
                        roomInitialInfo.NoOfNights * HotelBooking.OrderDetails.HotelRoomDTO.RegularRate;
                }
                else
                {
                    HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync(
                        Id.Value, DateTime.Now, DateTime.Now.AddDays(1));
                    NoOfNights = 1;
                    HotelBooking.OrderDetails.CheckInDate = DateTime.Now;
                    HotelBooking.OrderDetails.CheckOutDate = DateTime.Now.AddDays(1);
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = 1;
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount =
                        HotelBooking.OrderDetails.HotelRoomDTO.RegularRate;
                }
            }
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }
}
- در ابتدا مسیریابی کامپوننت جدید RoomDetails را مشخص کرد‌ه‌ایم که یک Id را می‌پذیرد که همان Id اتاق انتخاب شده‌ی توسط کاربر است. به همین جهت پارامتر عمومی متناظری با آن هم در قسمت کدهای کامپوننت تعریف شده‌است.
- سپس سرویس توکار IJSRuntime به کامپوننت تزریق شده‌است تا توسط آن و Toastr، بتوان خطاهایی را به کاربر نمایش داد.
- از سرویس ILocalStorageService برای دسترسی به اطلاعات شروع به رزرو شخص و تعداد روز مدنظر او استفاده می‌کنیم که در قسمت قبل آن‌را مقدار دهی کردیم.
- همچنین از سرویس IClientHotelRoomService که آن‌را نیز در قسمت قبل افزودیم، برای فراخوانی متد GetHotelRoomDetailsAsync آن استفاده کرده‌ایم.

در روال آغازین OnInitializedAsync، اگر Id تنظیم شده بود، یعنی کاربر به درستی وارد این صفحه شده‌است. سپس بررسی می‌کنیم که آیا اطلاعاتی از درخواست ابتدایی او در Local Storage مرورگر وجود دارد یا خیر؟ اگر این اطلاعات وجود داشته باشد، بر اساس آن، بازه‌ی تاریخی دقیقی را می‌توان تشکیل داد و اگر خیر، این بازه را از امروز، به مدت 1 روز درنظر می‌گیریم.
پس از پایان کار متد OnInitializedAsync، چون اجزای HotelBooking مقدار دهی کامل شده‌اند، نمایش loading ابتدای کامپوننت، متوقف شده و قسمت else شرط نوشته شده اجرا می‌شود؛ یعنی اصل UI فرم نمایان خواهد شد.

در قسمت قبل، متد GetHotelRoomDetailsAsync را تکمیل نکردیم؛ چون به آن نیازی نداشتیم و فقط قصد داشتیم تا لیست تمام اتاق‌ها را نمایش دهیم. اما در اینجا برای تکمیل کدهای آغازین کامپوننت RoomDetails، متد دریافت اطلاعات یک اتاق را نیز تکمیل می‌کنیم تا توسط آن بتوان در این کامپوننت نیز جزئیات اتاق انتخابی را نمایش داد:
namespace BlazorWasm.Client.Services
{
    public class ClientHotelRoomService : IClientHotelRoomService
    {
        private readonly HttpClient _httpClient;

        public ClientHotelRoomService(HttpClient httpClient)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        }

        public Task<HotelRoomDTO> GetHotelRoomDetailsAsync(int roomId, DateTime checkInDate, DateTime checkOutDate)
        {
            // How to url-encode query-string parameters properly
            var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, $"/api/hotelroom/{roomId}"))
                            .AddParameter("checkInDate", $"{checkInDate:yyyy'-'MM'-'dd}")
                            .AddParameter("checkOutDate", $"{checkOutDate:yyyy'-'MM'-'dd}")
                            .Uri;
            return _httpClient.GetFromJsonAsync<HotelRoomDTO>(uri);
        }

        public Task<IEnumerable<HotelRoomDTO>> GetHotelRoomsAsync(DateTime checkInDate, DateTime checkOutDate)
        {
           // ...
        }
    }
}

اتصال مدل کامپوننت RoomDetails به فرم ثبت سفارش آن

تا اینجا مدل فرم را مقدار دهی اولیه کردیم. اکنون می‌توانیم قسمت else شرط نوشته شده را تکمیل کرده و در قسمتی از آن، مشخصات اتاق جاری را نمایش دهیم و در قسمتی دیگر، فرم ثبت سفارش را تکمیل کنیم.
الف) نمایش مشخصات اتاق جاری
در کامپوننت جاری با استفاده از خواص مقدار دهی اولیه شده‌ی شیء HotelBooking.OrderDetails.HotelRoomDTO، می‌توان جزئیات اتاق انتخابی را نمایش داد که نمونه‌ای از آن‌را در قسمت قبل هم مشاهده کردید:
@if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null)
{
    <div class="spinner"></div>
}
else
{
    <div class="mt-4 mx-4 px-0 px-md-5 mx-md-5">
        <div class="row p-2 my-3 " style="border-radius:20px; ">
            <div class="col-12 col-lg-7 p-4" style="border: 1px solid gray">
                <div class="row px-2 text-success border-bottom">
                    <div class="col-8 py-1"><p style="font-size:x-large;margin:0px;">Selected Room</p></div>
                    <div class="col-4 p-0"><a href="hotel/rooms" class="btn btn-secondary btn-block">Back to Room's</a></div>
                </div>
                <div class="row">
                    <div class="col-6">
                        <div id="" class="carousel slide mb-4 m-md-3 m-0 pt-3 pt-md-0" data-ride="carousel">
                            <div id="carouselExampleIndicators" class="carousel slide" data-ride="carousel">
                                <ol class="carousel-indicators">
                                    <li data-target="#carouselExampleIndicators" data-slide-to="0" class="active"></li>
                                    <li data-target="#carouselExampleIndicators" data-slide-to="1"></li>
                                </ol>
                                <div class="carousel-inner">
                                    <div class="carousel-item active">
                                        <img class="d-block w-100" src="images/slide1.jpg" alt="First slide">
                                    </div>
                                </div>
                                <a class="carousel-control-prev" href="#carouselExampleIndicators" role="button" data-slide="prev">
                                    <span class="carousel-control-prev-icon" aria-hidden="true"></span>
                                    <span class="sr-only">Previous</span>
                                </a>
                                <a class="carousel-control-next" href="#carouselExampleIndicators" role="button" data-slide="next">
                                    <span class="carousel-control-next-icon" aria-hidden="true"></span>
                                    <span class="sr-only">Next</span>
                                </a>
                            </div>
                        </div>
                    </div>
                    <div class="col-6">
                        <span class="float-right pt-4">
                            <span class="float-right">Occupancy : @HotelBooking.OrderDetails.HotelRoomDTO.Occupancy adults </span><br />
                            <span class="float-right pt-1">Size : @HotelBooking.OrderDetails.HotelRoomDTO.SqFt sqft</span><br />
                            <h4 class="text-warning font-weight-bold pt-5">
                                <span style="border-bottom:1px solid #ff6a00">
                                    @HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount.ToString("#,#.00#;(#,#.00#)")
                                </span>
                            </h4>
                            <span class="float-right">Cost for @HotelBooking.OrderDetails.HotelRoomDTO.TotalDays nights</span>
                        </span>
                    </div>
                </div>
                <div class="row p-2">
                    <div class="col-12">
                        <p class="card-title text-warning" style="font-size:xx-large">@HotelBooking.OrderDetails.HotelRoomDTO.Name</p>
                        <p class="card-text" style="font-size:large">
                            @((MarkupString)@HotelBooking.OrderDetails.HotelRoomDTO.Details)
                        </p>
                    </div>

                </div>
            </div>
}
ب) نمایش فرم متصل به مدل کامپوننت
قسمت دوم UI کامپوننت جاری، نمایش فرم زیر است که اجزای مختلف آن به فیلد HotelBooking متصل شده‌اند:
@if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null)
{
    <div class="spinner"></div>
}
else
{
  // ...
             
            <div class="col-12 col-lg-5 p-4 2 mt-4 mt-md-0" style="border: 1px solid gray;">
                <EditForm Model="HotelBooking" class="container" OnValidSubmit="HandleCheckout">
                    <div class="row px-2 text-success border-bottom"><div class="col-7 py-1"><p style="font-size:x-large;margin:0px;">Enter Details</p></div></div>

                    <div class="form-group pt-2">
                        <label class="text-warning">Name</label>
                        <InputText @bind-Value="HotelBooking.OrderDetails.Name" type="text" class="form-control" />
                    </div>
                    <div class="form-group pt-2">
                        <label class="text-warning">Phone</label>
                        <InputText @bind-Value="HotelBooking.OrderDetails.Phone" type="text" class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">Email</label>
                        <InputText @bind-Value="HotelBooking.OrderDetails.Email" type="text" class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">Check in Date</label>
                        <InputDate @bind-Value="HotelBooking.OrderDetails.CheckInDate" type="date" disabled class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">Check Out Date</label>
                        <InputDate @bind-Value="HotelBooking.OrderDetails.CheckOutDate" type="date" disabled class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">No. of nights</label>
                        <select class="form-control" value="@NoOfNights" @onchange="HandleNoOfNightsChange">
                            @for (var i = 1; i <= 10; i++)
                            {
                                if (i == NoOfNights)
                                {
                                    <option value="@i" selected="selected">@i</option>
                                }
                                else
                                {
                                    <option value="@i">@i</option>
                                }
                            }
                        </select>
                    </div>
                    <div class="form-group">
                        <button type="submit" class="btn btn-success form-control">Checkout Now</button>
                    </div>
                </EditForm>
            </div>
        </div>
    </div>
}
در این فرم دو روال رویدادگردان زیر نیز مورد استفاده هستند:
@code {
    // ...

    private async Task HandleNoOfNightsChange(ChangeEventArgs e)
    {
        NoOfNights = Convert.ToInt32(e.Value.ToString());
        HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync(
            Id.Value,
            HotelBooking.OrderDetails.CheckInDate,
            HotelBooking.OrderDetails.CheckInDate.AddDays(NoOfNights));

        HotelBooking.OrderDetails.CheckOutDate = HotelBooking.OrderDetails.CheckInDate.AddDays(NoOfNights);
        HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = NoOfNights;
        HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount =
                NoOfNights * HotelBooking.OrderDetails.HotelRoomDTO.RegularRate;
    }

    private async Task HandleCheckout()
    {
        if (!await HandleValidation())
        {
            return;
        }
    }

    private async Task<bool> HandleValidation()
    {
        if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Name))
        {
            await JsRuntime.ToastrError("Name cannot be empty");
            return false;
        }

        if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Phone))
        {
            await JsRuntime.ToastrError("Phone cannot be empty");
            return false;
        }

        if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Email))
        {
            await JsRuntime.ToastrError("Email cannot be empty");
            return false;
        }
        return true;
    }
}
- کاربر اگر تعداد شب‌های اقامت را از طریق دارپ‌داون فرم تغییر داد، در روال رویدادگردان HandleNoOfNightsChange، محاسبات جدیدی را بر این اساس انجام می‌دهیم؛ چون هزینه و سایر مشخصات جزئیات اتاق نمایش داده شده، باید تغییر کنند.
- همچنین کدهای ابتدایی HandleCheckout را که برای ثبت نهایی اطلاعات فرم است، تهیه کرده‌ایم. البته در این قسمت این مورد را فقط محدود به اعتبارسنجی دستی و سفارشی که در متد HandleValidation مشاهده می‌کنید، کرده‌ایم. این روش دستی را نیز می‌توان برای تعریف منطق اعتبارسنجی یک فرم بکار برد و آن‌را توسط کدهای #C تکمیل کرد. البته باید درنظر داشت که data annotation validator توکار، هنوز از اعتبارسنجی خواص تو در تو، پشتیبانی نمی‌کند. به همین جهت است که در اینجا خودمان این اعتبارسنجی را به صورت دستی تعریف کرده‌ایم.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-29.zip
نظرات مطالب
PersianDateTime جایگزینی برای System.DateTime
using (var db = new DBTestEntities())
            {
                dataGridView1.DataSource =(from t in db.test
                                              select  new
                                              { 
                                                  Id= t.Id,
                                                  time = new PersianDateTime( DateTime.Parse(t.ResponseDate.ToString())).ToString(PersianDateTimeFormat.DateShortTime)
                                              }).ToList();                
            }

برای ذخیره اطلاعات در بانک این کلاس خوب کار میکند و مشکلی نیست . ولی برای نمایش در برنامه ویندوزی کد بالا عمل نمیکند و هیچ چیزی نشان نمی‌دهد . یک جدول ساده دارم که شامل دو فیلد  Id  و datetime  است .
مطالب
نحوه استفاده از Text template ها در دات نت - قسمت دوم
بعد از ایجاد فایل Text template که در جلسه قبل با آن آشنا شدید، برای شروع قواعد زیر را در نظر بگیرید :
- تنظیمات مربوط به فایل Text template و نحوه تولید خروجی در ابتدای فایل و بین علامت <#@ و  #> قرار میگیرد.
- هر متنی که بصورت معمول در فایل tt نوشته شود، به همان صورت در فایل خروجی قرار می‌گیرد.
- هر دستوری که در بین علامت‌های  <#=  و #> قرار گیرد هنگام کامپایل اجرا شده و معادل آن در همان مکان متن قرار میگیرد.
- هر دستوری که بین علامت‌های  <#  و #>  قرار گیرد، هنگام کامپایل اجرا می‌شود. در این صورت دستورات نوشته شده در این قسمت فقط اجرا می‌گردد و معمولا برای استفاده در قسمتهای دیگر، داخل بلوک <#= #> نوشته می‌شود . 
- برای تعریف کلاس یا متد جدید جهت استفاده در فایل tt می‌توانیم کلاس را در بین علامت <#+ و  #> قرار دهیم. در این صورت کلاس و متد‌های نوشته شده در قسمتهای دیگر، داخل بلوک  <#= #> و یا  <# #> مورد استفاده قرار میگیرند.

اجازه دهید با یک مثال ساده قواعد اولیه را بررسی کنیم :  
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".txt" #>

<# var T = DateTime.Now; #>

The Time is : <#= T #>

The Time is : <#=  DateTime.Now #>
در این مثال، T در واقع متغیری است که در  بلوک <# #> تعریف گردیده و در بلوک <#= #> مقدار آن استفاده میشود. خروجی فایل چیزی شبیه به دو خط زیر خواهد بود:

The Time is : 02/16/2014 14:17:39
The Time is : 02/16/2014 14:17:39
به عنوان یک مثال دیگر که قواعد توضیح داده شده را پوشش دهد به مثال زیر توجه کنید :
<#@ template debug="true" hostspecific="false" language="C#" #>
<#@ output extension=".cs" #>

using System;
using System.Text;

<# string ClassName = "DotnetTips"; #>
public class <#= ClassName + "_" + new MyTestClass().Str #>
{
}

<#+
public class MyTestClass
{
public string  Str { get{return new DateTime().DayOfWeek.ToString() ;} }
}
#>
خروجی  Text template   بالا فایل Cs    با  محتوی شبیه کد زیر خواهد بود:  (روز نگارش مطلب البته دوشنبه است) 
using System;
using System.Text;

public class DotnetTips_Monday
{
}

به عنوان یک مثال ساده دیگر برای فهم بیشتر به کد زیر جهت تولید Table  در Html  توجه کنید:  

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".html" #>

<html><body>
<table>
    <# for (int i = 1; i <= 10; i++)
       { #>
         <tr>
 <td>Test name <#= i #> </td>
             <td>Test value <#= i * i #> </td> 
 </tr>
    <# } #>
 </table>
</body></html>
فکر می‌کنم این 3 مثال ساده، تا حد زیادی قواعد اولیه T4 Text Template  را برای شما روشن کرده باشد. در قسمت بعدی برخی قواعد تکمیلی را در این مورد خدمتتون ارائه میدم.
مطالب
تشخیص اصالت ردیف‌های یک بانک اطلاعاتی در EF Core
همیشه فرض بر این است که مدیر سیستم، فردی است امین و درستکار. این شخص/اشخاص کارهای شبکه، پشتیبان‌گیری، نگهداری و امثال آن‌را انجام داده و از سیستم‌ها محافظت می‌کنند. اکنون این سناریوهای واقعی را درنظر بگیرید:
- پس از خداحافظی با شرکتی که در آن کار می‌کردی، شخصی با پوزخند به شما می‌گوید که «می‌دونستی در برنامه‌ی حق و دستمزد شما، بچه‌های ادمین شبکه، دیتابیس برنامه رو مستقیما دستکاری می‌کردند و تعداد ساعات کاری بیشتری رو وارد می‌کردند»؟!
- مسئول فروشی/مسئول پذیرشی که یاد گرفته چطور به صورت مستقیم به بانک اطلاعاتی دسترسی پیدا کند و آمار فروش/پذیرش روز خودش را در بانک اطلاعاتی، با دستکاری مستقیم و خارج از برنامه، کمتر از مقدار واقعی نمایش دهد.
- باز هم مدیر سیستمی/شبکه‌ای که دسترسی مستقیم به بانک اطلاعاتی دارد، در ساعاتی مشخص، کلمه‌ی عبور هش شده‌ی خودش را مستقیما، بجای کلمه‌ی عبور ادمین برنامه در بانک اطلاعاتی وارد کرده و پس از آن ...

این موارد متاسفانه واقعی هستند! اکنون سؤال اینجا است که آیا برنامه‌ی شما قادر است تشخیص دهد رکوردهایی که هم اکنون در بانک اطلاعاتی ثبت شده‌اند، واقعا توسط برنامه و تمام سطوح دسترسی که برای آن طراحی کرده‌اید، به این شکل درآمده‌اند، یا اینکه توسط اشخاصی به صورت مستقیم و با دور زدن کامل برنامه، از طریق management studioهای مختلف، در سیستم وارد و دستکاری شده‌اند؟! در ادامه راه حلی را برای بررسی این مشکل مهم، مرور خواهیم کرد.


چگونه تغییرات رکوردها را در بانک‌های اطلاعاتی ردیابی کنیم؟

روش متداولی که برای بررسی تغییرات رکوردها مورد استفاده قرار می‌گیرد، هش کردن تمام اطلاعات یک ردیف از جدول است و سپس مقایسه‌ی این هش‌ها با هم. علت استفاده‌ی از الگوریتم‌های هش نیز، حداقل به دو علت است:
- با تغییر حتی یک بیت از اطلاعات، مقدار هش تولید شده تغییر می‌کند.
- طول نهایی مقدار هش شده‌ی اطلاعاتی حجیم، بسیار کم است و به راحتی توسط بانک‌های اطلاعاتی، قابل مدیریت و جستجو است.

اگر از SQL Server استفاده می‌کنید، یک چنین قابلیتی را به صورت توکار به همراه دارد:
SELECT
    [Id], 
    (SELECT top 1  * FROM  [AppUsers] FOR XML auto),
    HASHBYTES ('SHA2_256', (SELECT top 1  * FROM  [AppUsers] FOR XML auto)) AS [hash] -- varbinary(n), since 2012
FROM
    [AppUsers]
با این خروجی


کاری که این کوئری انجام می‌دهد شامل دو مرحله است:
الف) کوئری "SELECT top 1 * FROM [AppUsers] FOR XML auto" کاری شبیه به serialization را انجام می‌دهد. همانطور که مشاهده می‌کنید، نام و مقادیر تمام فیلدهای یک ردیف را به صورت یک خروجی XML در می‌آورد. بنابراین دیگر نیازی نیست تا کار تبدیل مقادیر تمام ستون‌های یک ردیف را به عبارتی قابل هش، به صورت دستی انجام دهیم؛ رشته‌ی XML ای آن هم اکنون آماده‌است.
ب) متد HASHBYTES، این خروجی serialized را با الگوریتم SHA2_256، هش می‌کند. الگوریتم‌های SHA2_256 و همچنین SHA2_512، از سال 2012 به بعد به SQL Server اضافه شده‌اند.

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


چگونه تغییرات رکوردها را در بانک‌های اطلاعاتی، توسط EF Core ردیابی کنیم؟

مزیت روش فوق، توکار بودن آن است که کارآیی فوق العاده‌ای را نیز به همراه دارد. اما چون در ادامه قصد داریم از یک ORM استفاده کنیم و ORMها نیز قرار است توانایی کار کردن با انواع و اقسام بانک‌های اطلاعاتی را داشته باشند، دو مرحله‌ی serialization و هش کردن را در کدهای برنامه و با مدیریت EF Core، مستقل از بانک اطلاعاتی خاصی، انجام خواهیم داد.


معرفی موجودیت‌های برنامه

در مثالی که بررسی خواهیم کرد، دو موجودیت Blog و Post تعریف شده‌اند:
using System.Collections.Generic;

namespace EFCoreRowIntegrity
{
    public interface IAuditableEntity
    {
        string Hash { set; get; }
    }

    public static class AuditableShadowProperties
    {
        public static readonly string CreatedDateTime = nameof(CreatedDateTime);
        public static readonly string ModifiedDateTime = nameof(ModifiedDateTime);
    }

    public class Blog : IAuditableEntity
    {
        public int BlogId { get; set; }
        public string Url { get; set; }

        public List<Post> Posts { get; set; }

        public string Hash { get; set; }
    }

    public class Post : IAuditableEntity
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        public int BlogId { get; set; }
        public Blog Blog { get; set; }

        public string Hash { get; set; }
    }
}
- در اینجا اینترفیس IAuditableEntity را نیز مشاهده می‌کنید که دارای یک خاصیت Hash است. تمام موجودیت‌هایی که قرار است دارای فیلد هش باشند، نیاز است این اینترفیس را پیاده سازی کنند؛ مانند دو موجودیت Blog و Post. در ادامه مقدار خاصیت هش را به صورت خودکار توسط سیستم Tracking، محاسبه و به روز رسانی می‌کنیم.
- به علاوه جهت تکمیل بحث، دو خاصیت سایه‌ای نیز تعریف شده‌اند تا بررسی کنیم که آیا هش این‌ها نیز درست محاسبه می‌شود یا خیر.
- علت اینکه خاصیت Hash، سایه‌ای تعریف نشد، سهولت دسترسی و بالا بردن کارآیی آن بود.



معرفی ظرفی برای نگهداری نام خواص و مقادیر متناظر با یک موجودیت

در ادامه دو کلاس AuditEntry و AuditProperty را مشاهده می‌کنید:
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace EFCoreRowIntegrity
{
    public class AuditEntry
    {
        public EntityEntry EntityEntry { set; get; }
        public IList<AuditProperty> AuditProperties { set; get; } = new List<AuditProperty>();

        public AuditEntry() { }

        public AuditEntry(EntityEntry entry)
        {
            EntityEntry = entry;
        }
    }

    public class AuditProperty
    {
        public string Name { set; get; }
        public object Value { set; get; }

        public bool IsTemporary { set; get; }
        public PropertyEntry PropertyEntry { set; get; }

        public AuditProperty() { }

        public AuditProperty(string name, object value, bool isTemporary, PropertyEntry property)
        {
            Name = name;
            Value = value;
            IsTemporary = isTemporary;
            PropertyEntry = property;
        }
    }
}
زمانیکه توسط سیستم Tracking، موجودیت‌های اضافه شده و یا ویرایش شده را استخراج می‌کنیم، AuditEntry همان موجودیت در حال بررسی است که دارای تعدادی خاصیت یا AuditProperty می‌باشد. این‌ها را توسط دو کلاس فوق برای عملیات بعدی، ذخیره و نگهداری می‌کنیم.


معرفی روشی برای هش کردن مقادیر یک شیء

زمانیکه توسط سیستم Tracking، در حال کاربر بر روی موجودیت‌های اضافه شده و یا ویرایش شده هستیم، می‌خواهیم فیلد هش آن‌ها را نیز به صورت خودکار ویرایش و مقدار دهی کنیم. کلاس زیر، منطق ارائه دهنده‌ی این مقدار هش را بیان می‌کند:
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Newtonsoft.Json;

namespace EFCoreRowIntegrity
{
    public static class HashingExtensions
    {
        public static string GenerateObjectHash(this object @object)
        {
            if (@object == null)
            {
                return string.Empty;
            }

            var jsonData = JsonConvert.SerializeObject(@object, Formatting.Indented);
            using (var hashAlgorithm = new SHA256CryptoServiceProvider())
            {
                var byteValue = Encoding.UTF8.GetBytes(jsonData);
                var byteHash = hashAlgorithm.ComputeHash(byteValue);
                return Convert.ToBase64String(byteHash);
            }
        }

        public static string GenerateEntityEntryHash(this EntityEntry entry, string propertyToIgnore)
        {
            var auditEntry = new Dictionary<string, object>();
            foreach (var property in entry.Properties)
            {
                var propertyName = property.Metadata.Name;
                if (propertyName == propertyToIgnore)
                {
                    continue;
                }
                auditEntry[propertyName] = property.CurrentValue;
            }
            return auditEntry.GenerateObjectHash();
        }

        public static string GenerateEntityHash<TEntity>(this DbContext context, TEntity entity, string propertyToIgnore)
        {
            return context.Entry(entity).GenerateEntityEntryHash(propertyToIgnore);
        }
    }
}
- در اینجا توسط متد JsonConvert.SerializeObject کتابخانه‌ی Newtonsoft.Json، شیء موجودیت را تبدیل به یک رشته‌ی JSON کرده و توسط الگوریتم SHA256 هش می‌کنیم. در آخر هم این مقدار را به صورت Base64 ارائه می‌دهیم.
- نکته‌ی مهم: ما نمی‌خواهیم تمام خواص یک موجودیت را هش کنیم. برای مثال اگر موجودیتی دارای چندین رابطه با جداول دیگری بود، ما مقادیر این‌ها را هش نمی‌کنیم (چون رکوردهای متناظر با آن‌ها در جداول خودشان می‌توانند دارای فیلد هش مخصوصی باشند). بنابراین یک Dictionary را از خواص و مقادیر متناظر با آن‌ها تشکیل داده و این Dictionary را تبدیل به JSON می‌کنیم.
- همچنین در این بین، مقدار خود فیلد Hash یک شیء نیز نباید در هش محاسبه شده، حضور داشته باشد. به همین جهت پارامتر propertyToIgnore را مشاهده می‌کنید.


معرفی Context برنامه که کار هش کردن خودکار موجودیت‌ها را انجام می‌دهد

اکنون نوبت استفاده از تنظیمات انجام شده‌ی تا این مرحله‌است:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.Extensions.Logging;

namespace EFCoreRowIntegrity
{
    public class BloggingContext : DbContext
    {
        public BloggingContext()
        { }

        public BloggingContext(DbContextOptions options)
            : base(options)
        { }

        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
                optionsBuilder.EnableSensitiveDataLogging();
                var path = Path.Combine(Directory.GetCurrentDirectory(), "app_data", "EFCore.RowIntegrity.mdf");
                optionsBuilder.UseSqlServer($"Server=(localdb)\\mssqllocaldb;Database=EFCore.RowIntegrity;AttachDbFilename={path};Trusted_Connection=True;");
                optionsBuilder.UseLoggerFactory(new LoggerFactory().AddConsole((message, logLevel) =>
                logLevel == LogLevel.Debug &&
                           message.StartsWith("Microsoft.EntityFrameworkCore.Database.Command")));
            }
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            foreach (var entityType in modelBuilder.Model
                                                   .GetEntityTypes()
                                                   .Where(e => typeof(IAuditableEntity)
                                                   .IsAssignableFrom(e.ClrType)))
            {
                modelBuilder.Entity(entityType.ClrType)
                            .Property<DateTimeOffset?>(AuditableShadowProperties.CreatedDateTime);
                modelBuilder.Entity(entityType.ClrType)
                            .Property<DateTimeOffset?>(AuditableShadowProperties.ModifiedDateTime);
            }
        }

        public override int SaveChanges()
        {
            var auditEntries = OnBeforeSaveChanges();
            var result = base.SaveChanges();
            OnAfterSaveChanges(auditEntries);
            return result;
        }

        private IList<AuditEntry> OnBeforeSaveChanges()
        {
            var auditEntries = new List<AuditEntry>();

            foreach (var entry in ChangeTracker.Entries<IAuditableEntity>())
            {
                if (entry.State == EntityState.Detached || entry.State == EntityState.Unchanged)
                {
                    continue;
                }

                var auditEntry = new AuditEntry(entry);
                auditEntries.Add(auditEntry);

                var now = DateTimeOffset.UtcNow;

                foreach (var property in entry.Properties)
                {
                    var propertyName = property.Metadata.Name;
                    if (propertyName == nameof(IAuditableEntity.Hash))
                    {
                        continue;
                    }

                    if (property.IsTemporary)
                    {
                        // It's an auto-generated value and should be retrieved from the DB after calling the base.SaveChanges().
                        auditEntry.AuditProperties.Add(new AuditProperty(propertyName, null, true, property));
                        continue;
                    }

                    switch (entry.State)
                    {
                        case EntityState.Added:
                            entry.Property(AuditableShadowProperties.CreatedDateTime).CurrentValue = now;
                            auditEntry.AuditProperties.Add(new AuditProperty(propertyName, property.CurrentValue, false, property));
                            break;
                        case EntityState.Modified:
                            auditEntry.AuditProperties.Add(new AuditProperty(propertyName, property.CurrentValue, false, property));
                            entry.Property(AuditableShadowProperties.ModifiedDateTime).CurrentValue = now;
                            break;
                    }
                }
            }

            return auditEntries;
        }

        private void OnAfterSaveChanges(IList<AuditEntry> auditEntries)
        {
            foreach (var auditEntry in auditEntries)
            {
                foreach (var auditProperty in auditEntry.AuditProperties.Where(x => x.IsTemporary))
                {
                    // Now we have the auto-generated value from the DB.
                    auditProperty.Value = auditProperty.PropertyEntry.CurrentValue;
                    auditProperty.IsTemporary = false;
                }
                auditEntry.EntityEntry.Property(nameof(IAuditableEntity.Hash)).CurrentValue =
                    auditEntry.AuditProperties.ToDictionary(x => x.Name, x => x.Value).GenerateObjectHash();
            }
            base.SaveChanges();
        }
    }
}
در اینجا اصل کار، در متد بازنویسی شده‌ی SaveChanges انجام می‌شود:
public override int SaveChanges()
{
    var auditEntries = OnBeforeSaveChanges();
    var result = base.SaveChanges();
    OnAfterSaveChanges(auditEntries);
    return result;
}
در متد OnBeforeSaveChanges، تمام موجودیت‌های تغییر کرده‌ی از نوع IAuditableEntity را که دارای فیلد هش هستند، یافته و نام خاصیت و مقدار متناظر با آن‌ها را در ظرف‌های AuditEntry که پیشتر معرفی شدند، ذخیره می‌کنیم. هنوز در این مرحله کار هش کردن را انجام نخواهیم داد. علت را می‌توانید در بررسی خواص موقتی مشاهده کنید:
if (property.IsTemporary)
{
   // It's an auto-generated value and should be retrieved from the DB after calling the base.SaveChanges().
   auditEntry.AuditProperties.Add(new AuditProperty(propertyName, null, true, property));
   continue;
}
خواص موقتی، عموما تولید شده‌ی توسط دیتابیس هستند. برای مثال زمانیکه یک Id عددی خود افزاینده را به عنوان کلید اصلی جدول معرفی می‌کنید، مقدار آن پس از فراخوانی متد base.SaveChanges، از بانک اطلاعاتی دریافت شده و در اختیار برنامه قرار می‌گیرد. به همین جهت است که نیاز داریم لیست این خواص و مقادیر را یکبار پیش از base.SaveChanges ذخیره کنیم و پس از آن، خواص موقتی را که اکنون دارای مقدار هستند، مقدار دهی کرده و سپس هش نهایی شیء را محاسبه کنیم. اگر پیش از base.SaveChanges این هش را محاسبه کنیم، برای مثال حاوی مقدار Id شیء، نخواهد بود.

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


روش بررسی اصالت یک موجودیت

در متد زیر، روش محاسبه‌ی هش واقعی یک موجودیت دریافت شده‌ی از بانک اطلاعاتی را توسط متد الحاقی GenerateEntityHash مشاهده می‌کنید. اگر این هش واقعی (بر اساس مقادیر فعلی این ردیف که حتی ممکن است به صورت دستی و خارج از برنامه تغییر کرده باشد)، با مقدار Hash ثبت شده‌ی پیشین در آن ردیف یکی بود، اصالت این ردیف تائید خواهد شد:
private static void CheckRow1IsAuthentic()
{
    using (var context = new BloggingContext())
    {
        var blog1 = context.Blogs.Single(x => x.BlogId == 1);
        var entityHash = context.GenerateEntityHash(blog1, propertyToIgnore: nameof(IAuditableEntity.Hash));
        var dbRowHash = blog1.Hash;
        Console.WriteLine($"entityHash: {entityHash}\ndbRowHash:  {dbRowHash}");
        if (entityHash == dbRowHash)
        {
            Console.WriteLine("This row is authentic!");
        }
        else
        {
            Console.WriteLine("This row is tampered outside of the application!");
        }
    }
}
یک نمونه خروجی آن به صورت زیر است:
entityHash: P110cYquWpoaZuTpCWaqBn6HPSGdoQdmaAN05s1zYqo=
dbRowHash: P110cYquWpoaZuTpCWaqBn6HPSGdoQdmaAN05s1zYqo=
This row is authentic!

اکنون بانک اطلاعاتی را خارج از برنامه، مستقیما دستکاری می‌کنیم و برای مثال Url اولین ردیف را تغییر می‌دهیم:


در ادامه یکبار دیگر برنامه را اجرا خواهیم کرد:
entityHash: tdiZhKMJRnROGLLam1WpldA0fy/CbjJaR2Y2jNU9izk=
dbRowHash: P110cYquWpoaZuTpCWaqBn6HPSGdoQdmaAN05s1zYqo=
This row is tampered outside of the application!
همانطور که مشاهده می‌کنید، هش واقعی جدید، با هش ثبت شده‌ی در ردیف، یکی نیست؛ که بیانگر ویرایش مستقیم این ردیف می‌باشد.
به علاوه باید درنظر داشت، محاسبه‌ی این هش بدون خود برنامه، کار ساده‌ای نیست. به همین جهت به روز رسانی دستی آن تقریبا غیرممکن است؛ خصوصا اگر متد GenerateObjectHash، کمی با پیچ و تاب بیشتری نیز تهیه شود.


چگونه وضعیت اصالت تعدادی ردیف را بررسی کنیم؟

مثال قبل، در مورد روش بررسی اصالت یک تک ردیف بود. کوئری زیر روش محاسبه‌ی فیلد جدید IsAuthentic را در بین لیستی از ردیف‌ها نمایش می‌دهد:
var blogs = (from blog in context.Blogs.ToList() // Note: this `ToList()` is necessary here for having Shadow properties values, otherwise they will considered `null`.
             let computedHash = context.GenerateEntityHash(blog, nameof(IAuditableEntity.Hash))
             select new
             {
               blog.BlogId,
               blog.Url,
               RowHash = blog.Hash,
               ComputedHash = computedHash,
               IsAuthentic = blog.Hash == computedHash
             }).ToList();


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: EFCoreRowIntegrity.zip
مطالب
اتصال Node.js به SQL Server با استفاده از Edge.js
اگر خواسته باشید که با استفاده از Node.js به SQL Server متصل شوید، احتمالا متوجه شده‌اید ماژولی که مایکروسافت منتشر کرده است، ناقص بوده و به صورت پیش نمایش است که بسیاری از ویژگی‌ها و مسائل مهم، در آن در نظر گرفته نشده است.

یکی دیگر از ماژول‌هایی که امکان اتصال Node.js را به SQL Server ممکن می‌کند، Edge.js است. Edge.js یک ماژول Node.js است که امکان اجرای کدهای دات نت را در همان پروسه توسط Node.js فراهم می‌کند. این مسئله، توسعه دهندگان Node.js را قادر می‌سازد تا از فناوری‌هایی که به صورت سنتی استفاده‌ی از آنها سخت یا غیر ممکن بوده است را به راحتی استفاده کنند. برای نمونه:
  • SQL Server
  • Active Directory
  • Nuget packages
  • استفاده از سخت افزار کامپیوتر (مانند وب کم، میکروفن و چاپگر)


نصب Node.js

اگر Node.js را بر روی سیستم خود نصب ندارید، می‌توانید از اینجا آن را دانلود کنید. بعد از نصب برای اطمینان از کارکرد آن، command prompt را باز کرده و دستور زیر را تایپ کنید:

node -v
شما باید نسخه‌ی نصب شده‌ی Node.js را مشاهده کنید.

ایجاد پوشه پروژه

سپس پوشه‌ای را برای پروژه Node.js خود ایجاد کنید. مثلا با استفاده از command prompt و دستور زیر:

md \projects\node-edge-test1
cd \projects\node-edge-test1

نصب Edge.js

Node با استفاده از package manager خود دانلود و نصب ماژول‌ها را خیلی آسان کرده است. برای نصب، در command prompt عبارت زیر را تایپ کنید:

npm install edge
npm install edge-sql
فرمان اول باعث نصب Edge.js و دومین فرمان سبب نصب پشتیبانی از SQL Server می‌شود.

Hello World

ایجاد یک فایل متنی با نام server.js و نوشتن کد زیر در آن:
var edge = require('edge');

// The text in edge.func() is C# code
var helloWorld = edge.func('async (input) => { return input.ToString(); }');

helloWorld('Hello World!', function (error, result) {
    if (error) throw error;
    console.log(result);
});
حالا برای اجرای این Node.js application از طریق command prompt کافی است به صورت زیر عمل کنید:
node server.js
همانطور که مشاهده می‌کنید "!Hello World" در خروجی چاپ شد.

ایجاد پایگاه داده تست

در مثال‌های بعدی، نیاز به یک پایگاه داده داریم تا query‌ها را اجرا کنیم. در صورتی که SQL Server بر روی سیستم شما نصب نیست، می‌توانید نسخه‌ی رایگان آن را از اینجا دانلود و نصب کنید. همچنین SQL Management Studio Express را نیز نصب کنید.

  1. در SQL Management Studio، یک پایگاه داده را با نام node-test با تنظیمات پیش فرض ایجاد کنید.
  2. بر روی پایگاه داده node-test راست کلیک کرده و New Query را انتخاب کنید.
  3. اسکریپت زیر را copy کرده و در آنجا paste کنید، سپس بر روی Execute کلیک کنید.
IF EXISTS(SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('SampleUsers')) BEGIN; DROP TABLE SampleUsers; END; GO

CREATE TABLE SampleUsers ( Id INTEGER NOT NULL IDENTITY(1, 1), FirstName VARCHAR(255) NOT NULL, LastName VARCHAR(255) NOT NULL, Email VARCHAR(255) NOT NULL, CreateDate DATETIME NOT NULL DEFAULT(getdate()), PRIMARY KEY (Id) ); GO

INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Orla','Sweeney','nunc@convallisincursus.ca','Apr 13, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Zia','Pickett','porttitor.tellus.non@Duis.com','Aug 31, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Justina','Ayala','neque.tellus.imperdiet@temporestac.com','Jul 28, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Levi','Parrish','adipiscing.elit@velarcueu.com','Jun 21, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Pearl','Warren','In@dignissimpharetra.org','Mar 3, 2014');
نتیجه‌ی اجرای کد بالا، ایجاد جدولی با نام SampleUsers و درج 5 رکورد در آن می‌شود.

تنظیمات ConnectionString

قبل از استفاده از Edge.js با SQL Server، باید متغیر محیطی (environment variable) با نام EDGE_SQL_CONNECTION_STRING را تعریف کنید.

set EDGE_SQL_CONNECTION_STRING=Data Source=localhost;Initial Catalog=node-test;Integrated Security=True
این متغیر تنها برای command prompt جاری تعریف شده است و با بستن آن از دست می‌رود. در صورتیکه از Node.js Tools for Visual Studio استفاده می‌کنید، نیاز به ایجاد یک متغیر محیطی دائمی و راه اندازی مجدد VS دارید. همچنین در صورتیکه بخواهید متغیر محیطی دائمی ایجاد کنید، فرمان زیر را اجرا کنید:
SETX EDGE_SQL_CONNECTION_STRING "Data Source=localhost;Initial Catalog=node-test;Integrated Security=True"


روش اول: اجرای مستقیم SQL Server Query در Edge.js

فایلی با نام server-sql-query.js را ایجاد کرده و کد زیر را در آن وارد کنید:

var http = require('http');
var edge = require('edge');
var port = process.env.PORT || 8080;

var getTopUsers = edge.func('sql', function () {/*
    SELECT TOP 3 * FROM SampleUsers ORDER BY CreateDate DESC
*/});

function logError(err, res) {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.write("Error: " + err);
    res.end("");
}    

http.createServer(function (req, res) {
    res.writeHead(200, { 'Content-Type': 'text/html' });

    getTopUsers(null, function (error, result) {
        if (error) { logError(error, res); return; }
        if (result) {
            res.write("<ul>");
            result.forEach(function(user) {
                res.write("<li>" + user.FirstName + " " + user.LastName + ": " + user.Email + "</li>");
            });
            res.end("</ul>");
        }
        else {
        }
    });
}).listen(port);
console.log("Node server listening on port " + port);
سپس با استفاده از command prompt، فرمان زیر را اجرا کنید:
node server-sql-query.js
حال مرورگر خود را باز و سپس آدرس http://localhost:8080 را باز کنید. در صورتی که همه چیز به درستی انجام گرفته باشد لیستی از 3 کاربر را خواهید دید.

روش دوم: اجرای کد دات نت برای SQL Server Query

Edge.js تنها از دستورات Update، Insert، Select و Delete پشتیبانی می‌کند. در حال حاضر از store procedures و مجموعه‌ای از کد SQL پشتیبانی نمی‌کند. بنابراین، اگر چیزی بیشتر از عملیات CRUD می‌خواهید انجام دهید، باید از دات نت برای این کار استفاده کنید.

یادتان باشد، همیشه async

مدل اجرایی Node.js به صورت یک حلقه‌ی رویداد تک نخی است. بنابراین این بسیار مهم است که کد دات نت شما به صورت async باشد. در غیر اینصورت یک فراخوانی به دات نت سبب مسدود شدن و ایجاد خرابی در Node.js می‌شود.

ایجاد یک Class Library

اولین قدم، ایجاد یک پروژه Class Library در Visual Studio که خروجی آن یک فایل DLL است و استفاده از آن در Edge.js است. پروژه Class Library با عنوان EdgeSampleLibrary ایجاد کرده و فایل کلاسی با نام Sample1 را به آن اضافه کنید و سپس کد زیر را در آن وارد کنید:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Threading.Tasks;

namespace EdgeSampleLibrary
{
     public class Sample1
    {
        public async Task<object> Invoke(object input)
        {
            // Edge marshalls data to .NET using an IDictionary<string, object>
            var payload = (IDictionary<string, object>) input;
            var pageNumber = (int) payload["pageNumber"];
            var pageSize = (int) payload["pageSize"];
            return await QueryUsers(pageNumber, pageSize);
        }

        public async Task<List<SampleUser>> QueryUsers(int pageNumber, int pageSize)
        {
            // Use the same connection string env variable
            var connectionString = Environment.GetEnvironmentVariable("EDGE_SQL_CONNECTION_STRING");
            if (connectionString == null)
                throw new ArgumentException("You must set the EDGE_SQL_CONNECTION_STRING environment variable.");

            // Paging the result set using a common table expression (CTE).
            // You may rather do this in a stored procedure or use an 
            // ORM that supports async.
            var sql = @"
DECLARE @RowStart int, @RowEnd int;
SET @RowStart = (@PageNumber - 1) * @PageSize + 1;
SET @RowEnd = @PageNumber * @PageSize;

WITH Paging AS
(
    SELECT  ROW_NUMBER() OVER (ORDER BY CreateDate DESC) AS RowNum,
            Id, FirstName, LastName, Email, CreateDate
    FROM    SampleUsers
)
SELECT  Id, FirstName, LastName, Email, CreateDate
FROM    Paging
WHERE   RowNum BETWEEN @RowStart AND @RowEnd
ORDER BY RowNum;
";
            var users = new List<SampleUser>();

            using (var cnx = new SqlConnection(connectionString))
            {
                using (var cmd = new SqlCommand(sql, cnx))
                {
                    await cnx.OpenAsync();

                    cmd.Parameters.Add(new SqlParameter("@PageNumber", SqlDbType.Int) { Value = pageNumber });
                    cmd.Parameters.Add(new SqlParameter("@PageSize", SqlDbType.Int) { Value = pageSize });

                    using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection))
                    {
                        while (await reader.ReadAsync())
                        {
                            var user = new SampleUser
                            {
                                Id = reader.GetInt32(0), 
                                FirstName = reader.GetString(1), 
                                LastName = reader.GetString(2), 
                                Email = reader.GetString(3), 
                                CreateDate = reader.GetDateTime(4)
                            };
                           users.Add(user);
                        }
                    }
                }
            }
            return users;
        } 
    }

    public class SampleUser
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public DateTime CreateDate { get; set; }
    }
}
سپس ذخیره و کامپایل کنید. فایل DLL خروجی که در مسیر
[project]/bin/Debug/EdgeSampleLibrary.dll
قرار دارد را در پوشه‌ی پروژه Node کپی کنید. فایل جدیدی را با نام server-dotnet-query.js در پروژه Node ایجاد کنید و کد زیر را در آن وارد کنید:
var http = require('http');
var edge = require('edge');
var port = process.env.PORT || 8080;

// Set up the assembly to call from Node.js 
var querySample = edge.func({ assemblyFile: 'EdgeSampleLibrary.dll', typeName: 'EdgeSampleLibrary.Sample1', methodName: 'Invoke' });

function logError(err, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write("Got error: " + err); res.end(""); }

http.createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'text/html' });

    // This is the data we will pass to .NET
    var data = { pageNumber: 1, pageSize: 3 };

    // Invoke the .NET function
    querySample(data, function (error, result) {
        if (error) { logError(error, res); return; }
        if (result) {
            res.write("<ul>");
            result.forEach(function(user) {
                res.write("<li>" + user.FirstName + " " + user.LastName + ": " + user.Email + "</li>");
            });
            res.end("</ul>");
        }
        else {
            res.end("No results");
        }
    });

}).listen(port);

console.log("Node server listening on port " + port);
سپس از طریق command prompt آن را اجرا کنید:
node server-dotnet-query.js
حال مرورگر خود را باز کرده و به آدرس http://localhost:8080 بروید. در صورتیکه همه چیز به درستی انجام گرفته باشد، لیستی از 3 کاربر را خواهید دید. مقادیر pageNumber و pageSize را در فایل جاوااسکریپت تغییر دهید و تاثیر آن را بر روی خروجی مشاهده کنید.
 
نکته: برای ایجاد pageNumber و pageSize داینامیک با استفاده از ارسال مقادیر توسط QueryString، می‌توانید از ماژول connect استفاده کنید.
بازخوردهای پروژه‌ها
مشکل با نوشتن تابع تجمعی سفارشی(از طریق پیاده سازی IAggregateFunction)
با سلام؛ ضمن تشکر از اینکه تجربیاتتون رو رایگان در اختیار بقیه قرار می‌دید، به شخصه خیلی استفاده کردم.
سوالی داشتم در رابطه با پیاده سازی اینترفیس IAggregateFunction  
من میخوام یه گزارش بنویسم که تو اون ستون آخرش میخواد مانده تجمعی را حساب کنه.
بنابراین میخواستم با پیاده سازی این اینترفیس و همچنین بازنویسی متد ProcessingBoundary آخرین مقدار رو به عنوان خروجی تابع تجمعی ارسال کنم.
public object ProcessingBoundary(IList<SummaryCellData> columnCellsSummaryData)
        {
            if (columnCellsSummaryData == null || !columnCellsSummaryData.Any()) return 0;

            var list = columnCellsSummaryData;
            var lastItem = list.Last();

            return lastItem.CellData.PropertyValue;

        }
در پروژه‌ی دیگه ای این اینترفیس رو پیاده سازی کردم و مشکلی نبود ولی در پروژه جاری
که پروژه ایست با مشخصات:
نوع پروژه : WPF with MVVM
از Prism و Unity هم برای ماژولار شدن استفاده کردم.
خطای زیر رو میده : 
Method 'set_DisplayFormatFormula' in type 'Hezareh.Modules.Accounting.Reporting.ViewModels.MySampleAggregateFunction' from assembly 'Hezareh.Modules.Accounting, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' does not have an implementation.
در صورتی که اینترفیس IAggregateFunction به صورت کامل توسط کلاس  MySampleAggregateFunction پیاده سازی شده است و این هم کد کامل کلاس که همون کد مثال Sum خودتونه، که فقط تابع  ProcessingBoundary رو تغییر دادم. این هم کد کاملش :
 public class MySampleAggregateFunction : IAggregateFunction
    {
        public MySampleAggregateFunction()
        {

        }

        /// <summary>
        /// Fires before rendering of this cell.
        /// Now you have time to manipulate the received object and apply your custom formatting function.
        /// It can be null.
        /// </summary>
        public Func<object, string> DisplayFormatFormula { set; get; }

        #region Fields (6)

        double _groupAvg;
        long _groupRowNumber;
        double _groupSum;
        double _overallAvg;
        long _overallRowNumber;
        double _overallSum;

        #endregion Fields

        #region Properties (2)

        /// <summary>
        /// Returns current groups' aggregate value.
        /// </summary>
        public object GroupValue
        {
            get { return _groupAvg; }
        }

        /// <summary>
        /// Returns current row's aggregate value without considering the presence of the groups.
        /// </summary>
        public object OverallValue
        {
            get { return _overallAvg; }
        }

        #endregion Properties

        #region Methods (4)

        // Public Methods (1) 

        /// <summary>
        /// Fires after adding a cell to the main table.
        /// </summary>
        /// <param name="cellDataValue">Current cell's data</param>
        /// <param name="isNewGroupStarted">Indicated starting a new group</param>
        public void CellAdded(object cellDataValue, bool isNewGroupStarted)
        {
            checkNewGroupStarted(isNewGroupStarted);

            _overallRowNumber++;
            _groupRowNumber++;

            double cellValue;
            if (double.TryParse(cellDataValue.ToSafeString(), NumberStyles.AllowThousands | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out cellValue))
            {
                groupAvg(cellValue);
                overallAvg(cellValue);
            }
        }
        // Private Methods (3) 

        private void checkNewGroupStarted(bool newGroupStarted)
        {
            if (newGroupStarted)
            {
                _groupRowNumber = 0;
                _groupAvg = 0;
                _groupSum = 0;
            }
        }

        private void groupAvg(double cellValue)
        {
            _groupSum += cellValue;
            _groupAvg = _groupSum / _groupRowNumber;
        }

        private void overallAvg(double cellValue)
        {
            _overallSum += cellValue;
            _overallAvg = _overallSum / _overallRowNumber;
        }

        /// <summary>
        /// A general method which takes a list of data and calculates its corresponding aggregate value.
        /// It will be used to calculate the aggregate value of each pages individually, with considering the previous pages data.
        /// </summary>
        /// <param name="columnCellsSummaryData">List of data</param>
        /// <returns>Aggregate value</returns>
        public object ProcessingBoundary(IList<SummaryCellData> columnCellsSummaryData)
        {
            if (columnCellsSummaryData == null || !columnCellsSummaryData.Any()) return 0;

            var list = columnCellsSummaryData;
            var lastItem = list.Last();

            return lastItem.CellData.PropertyValue;

        }
        #endregion Methods

    }
و همچنین این هم تنظیمات ستونی که از این تابع تجمعی میخوام استفاده کنم.
columns.AddColumn(column =>
                {
                    column.PropertyName<VoucherRowPrintViewModel>(x => x.CaclulatedRemains);
                    column.CellsHorizontalAlignment(PdfRpt.Core.Contracts.HorizontalAlignment.Right);
                    column.IsVisible(true);
                    column.Order(5);
                    column.Width(1.5f);
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                    column.AggregateFunction(aggregateFunction =>
                    {
                        aggregateFunction.CustomAggregateFunction(new MySampleAggregateFunction());
                        aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                    column.HeaderCell("مانده");
                });
ممنون میشم در صورت امکان کمکم کنید.