9 پیش بینی برای آینده برنامه نویسی
آیندهی Entity Framework
The first version of ASP.NET was released 17 years ago and during these years, it is fascinating to see how the ASP.NET team constructively reacted through these years to the major shifts happening on the web. Initially a platform that was closed and tried to hide and abstract the web; ASP.NET has metamorphized into an open source and cross platform - one that fully embraces the nature of the web. This is the first part of a series of 3 articles that will cover the history of ASP.NET from its launch to the latest ASP.NET Core releases.
معرفی C# Source Generators
مخلص کلام اینکه : اگه با Fody یا PostSharp و همچنین Code Analyzerها آشنایی دارین. این قابلیت یه چیزی تو مایههای ترکیب ایناس و بهتون اجازه میده موقع Compile شدن کد پروژه رو Analyze کنین و یه کد جدیدی بهش اضافه کنین. (مثلا پیاده سازی اینترفیس INotifyPropertyChanged به صورت خودکار به هنگام کامپایل)
We’re pleased to introduce the first preview of Source Generators, a new C# compiler feature that lets C# developers inspect user code and generate new C# source files that can be added to a compilation. This is done via a new kind of component that we’re calling a Source Generator.
To get started with Source Generators, you’ll need to install the latest .NET 5 preview and the latest Visual Studio preview.
نمونه ای از پیاده سازی INotifyPropertyChanged with C# 9.0 Source Generators
در این مطلب قصد داریم علاوه بر طراحی زیرساختی برای راه اندازی هرچه سریعتر ServiceLayer، طراحی ای برای مکانیزم Validation به عنوان یک Cross Cutting Concern، نیز ارائه داده و آن را پیاده سازی کنیم.
پیش نیازها:
- قبلا در سایت در مورد لایه بندی نرم افزار و ServiceLayer مطلب منتشر شده است؛ لذا مطالعه این سری مقالات برگرفته از کتاب Professional ASP.NET Design Patterns جزء پیش نیازهای این مطلب میباشد.
- دوره Aspect oriented programming
- مطالب مربوط به کتابخانه FluentValidation
- دوره بررسی مفاهیم معکوس سازی وابستگیها و ابزارهای مرتبط با آن
- دوره AutoMapper
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 با هم بررسی خواهیم کرد.
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید.
Get-Process | Where-Object { $_.Name -eq 'Dropbox' }
PS /Users/sirwanafifi/Desktop> $block = { >> $newVar = 10 >> Write-Host $newVar >> }
PS /Users/sirwanafifi/Desktop> & $block
PS /Users/sirwanafifi/Desktop> & "Get-Process"
PS /Users/sirwanafifi/Desktop> & "1 + 1" or PS /Users/sirwanafifi/Desktop> & "Get-Process -Name Slack"
Function Add-Something { Write-Host "$_ World" } "Hello" | Add-Something
Function Add-Something { [CmdletBinding()] Param( [Parameter(ValueFromPipeline = $true)] [string]$Name ) Write-Host "$Name World" } "Hello" | Add-Something
$API_KEY = "...." Function Read-WeatherData { [CmdletBinding()] Param( [Parameter(ValueFromPipeline = $true)] [string]$CityName ) $Url = "https://api.openweathermap.org/data/2.5/forecast?q=$CityName&cnt=40&appid=$API_KEY&units=metric" Try { Write-Verbose "Reading weather data for $CityName" $Response = Invoke-RestMethod -Uri $Url $Response.list | ForEach-Object { Write-Verbose "Processing $($_.dt_txt)" [PSCustomObject]@{ City = $Response.city.name DateTime = [DateTime]::Parse($_.dt_txt) Temperature = $_.main.temp Humidity = $_.main.humidity Pressure = $_.main.pressure WindSpeed = $_.wind.speed WindDirection = $_.wind.deg Cloudiness = $_.clouds.all Weather = $_.weather.main WeatherDescription = $_.weather.description } } | Where-Object { $_.DateTime.Date -eq (Get-Date).Date } Write-Verbose "Done processing $CityName" } Catch { Write-Error $_.Exception.Message } }
Read-WeatherData -CityName "London" -Verbose
PS /> [Parameter]::new() ExperimentName : ExperimentAction : None Position : -2147483648 ParameterSetName : __AllParameterSets Mandatory : False ValueFromPipeline : False ValueFromPipelineByPropertyName : False ValueFromRemainingArguments : False HelpMessage : HelpMessageBaseName : HelpMessageResourceId : DontShow : False TypeId : System.Management.Automation.ParameterAttribute
Function Ping-Website { [CmdletBinding()] Param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [ValidatePattern('^www\..*')] [string[]]$Websites, [ValidateRange(1, 3)] [int]$Count = 3 ) $Results = @() $Websites | ForEach-Object { $Website = $_ $Result = Test-Connection -ComputerName $Website -Count $Count -Quiet $ResultText = $Result ? 'Success' : 'Failed' $Results += @{ Website = $Website Result = $ResultText } Write-Verbose "The result of pinging $Website is $ResultText" } $Results | ForEach-Object { $_ | Select-Object @{ Name = "Website"; Expression = { $_.Website }; }, @{ Name = "Result"; Expression = { $_.Result }; }, @{ Name = "Number Of Attempts"; Expression = { $Count }; } } }
Function Ping-Website { [CmdletBinding()] Param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [ValidateScript({ If (-Not ($_ | Test-Path) ) { Throw "File or folder does not exist" } If (-Not ($_ | Test-Path -PathType Leaf) ) { Throw "The Path argument must be a file. Folder paths are not allowed." } If ($_ -NotMatch "(\.json)$") { throw "The file specified in the path argument must be either of type json" } Return $true })] [Alias("src", "source", "file")] [System.IO.FileInfo]$Path, [int]$Count = 1 ) $Results = [System.Collections.ArrayList]@() $Urls = Get-Content -Path $Path | ConvertFrom-Json $Urls | ForEach-Object -Parallel { $Website = $_.url $Result = Test-Connection -ComputerName $Website -Count $using:Count -Quiet $ResultText = $Result ? 'Success' : 'Failed' $Item = @{ Website = $Website Result = $ResultText } $null = ($using:Results).Add($Item) } $Results | ForEach-Object -Parallel { $_ | Select-Object @{ Name = "Website"; Expression = { $_.Website }; }, @{ Name = "Result"; Expression = { $_.Result }; }, @{ Name = "Number Of Attempts"; Expression = { $using:Count }; } } }
Function Ping-Website { [CmdletBinding()] Param( # As before ) # As before $Urls | ForEach-Object -Parallel { $DebugPreference = $using:DebugPreference $VerbosePreference = $using:VerbosePreference $InformationPreference = $using:InformationPreference # As before } # As before }
$scriptBlock = { $logOutput = { param($message) Write-Host $message } [int]$someVariable = 10 $doSomeWork = { & $logOutput -message "Some variable value: $someVariable" } $someVariable = 20 & $doSomeWork }
$scriptBlock = { $logOutput = { param($message) Write-Host $message } [int]$someVariable = 10 $doSomeWork = { & $logOutput -message "Some variable value: $someVariable" }.GetNewClosure() $someVariable = 20 & $doSomeWork }
begin process end dynamicparam
function Show-Pipeline { begin { Write-Host "Pipeline start" } process { Write-Host "Pipeline process $_" } end { Write-Host "Pipeline end $_" } }
PS /> 1..2 | Show-Pipeline Pipeline start Pipeline process 1 Pipeline process 2 Pipeline end 2
PS /> "www.google.com", "www.yahoo.com" | Ping-Website Website Result Number Of Attempts ------- ------ ------------------ www.yahoo.com Success 3
Function Ping-Website { [CmdletBinding()] Param( # As before ) process { # As before } }
PS /> "www.google.com", "www.yahoo.com" | Ping-Website Website Result Number Of Attempts ------- ------ ------------------ www.google.com Success 3 www.yahoo.com Success 3
PS /> Read-Csv ./users.csv -Columns name
using namespace System.Management.Automation Function Read-Csv { Param ( [Parameter(Mandatory = $true, Position = 0)] [string]$Path ) DynamicParam { $firstLine = Get-Content $Path | Select-Object -First 1 [String[]]$headers = $firstLine -split ', ' $parameters = [RuntimeDefinedParameterDictionary]::new() $parameter = [RuntimeDefinedParameter]::new( 'Columns', [String[]], [Attribute[]]@( [Parameter]@{ Mandatory = $false; Position = 1 } [ValidateSet]::new($headers) ) ) $parameters.Add($parameter.Name, $parameter) Return $parameters } Begin { $csvContent = Import-Csv $Path If ($PSBoundParameters.ContainsKey('Columns')) { $columns = $PSBoundParameters['Columns'] $csvContent | Select-Object -Property $columns } Else { $csvContent } } }