ICollection یک IEnumerable است که قابلیت Add و Remove به آن اضافه شده.
IList یک ICollection است که به اعضای آن از طریق ایندکسها میتوان دسترسی اتفاقی داشت.
در تمام مثالهای EF Code first این سایت از ICollection برای معرفی خواص راهبری استفاده شده چون EF برای انجام اعمال داخلی خودش به مجموعههایی که قابلیت افزودن یا حذف عناصر را داشته باشند، نیاز دارد. به علاوه اگر به سورس EF هم مراجعه کنید برای تشخیص روابط بین کلاسها به دنبال ICollection میگردد.
همچنین رسم است حین انتخاب اینترفیسهایی از این دست که از هم مشتق میشوند، روال انتخاب «the least specific abstraction» رعایت شود. یعنی انتخاب کوچکترین پیاده سازی با حداقل نیازهایی که کاربرد مورد نظر را برآورده میکند.
public class Mailer { public static bool SendEmail() { Console.WriteLine("Sending Mail ..."); // simulate error Random rnd = new Random(); var rndNumber = rnd.Next(1, 10); if (rndNumber != 3) // * throw new SmtpFailedRecipientException(); Console.WriteLine("Mail Sent successfully"); return true; } }
public static class Retry { public static void Do(Action action,TimeSpan retryInterval,int maxAttemptCount = 3) { Do<object>(() => { action(); return null; }, retryInterval, maxAttemptCount); } public static T Do<T>(Func<T> action,TimeSpan retryInterval,int maxAttemptCount = 3) { var exceptions = new List<Exception>(); for (int attempted = 0; attempted < maxAttemptCount; attempted++) { try { if (attempted > 0) { Thread.Sleep(retryInterval); } return action(); } catch (Exception ex) { exceptions.Add(ex); } } throw new AggregateException(exceptions); } }
retryInterval= retryInterval.Add(TimeSpan.FromSeconds(10));
در سادهترین حالت، استفاده از Polly همانند زیر است:
var policy = Policy.Handle<SmtpFailedRecipientException>().Retry(); policy.Execute(Mailer.SendEmail);
متد Retry، دارای Overloadهای مختلفی است که یکی از آنها مقدار تعداد دفعات تلاش را دریافت میکند؛ همانند:
var policy = Policy.Handle<SmtpFailedRecipientException>().Retry(5);
لازم به ذکر است که باید دقیقا Exception مورد نظر را در بخش Config به کار ببرید. برای نمونه اگر کد فوق را همانند زیر به کار ببرید، در صورتیکه متد ارسال ایمیل با خطایی مواجه شود، هیچ تلاشی برای اجرای مجدد نخواهد کرد:
var policy = Policy.Handle<SqlException>().Retry(5);
برای نمونه میتوان از متد ForEver آن استفاده کرد تا زمانیکه متد مورد نظر Success نشده باشد، سعی در اجرای آن کند:
Policy .Handle<DivideByZeroException>() .RetryForever()
در این مطلب قصد داریم علاوه بر طراحی زیرساختی برای راه اندازی هرچه سریعتر 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 با هم بررسی خواهیم کرد.
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید.
yield و نحوهٔ پیادهسازی آن
public class ArrayEnumerable<T> : IEnumerable<T> { T[] _array; public ArrayEnumerable(T[] array) { _array = array; } public IEnumerator<T> GetEnumerator() { int index = 0; while (index < _array.Length) { yield return _array[index]; index++; } yield break; } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } }
همانطور که میبینیم کد قبلی ما به مقدار بسیاری سادهتر و خواناتر شد و برای فهم آن کافی است که مفهوم yield را بدانیم.
yield به معنای برآوردن یا ارائهکردن کلید واژهای است که میتوان آن را اینگونه تصور کرد که با هر با صدا زدهشدن کد را متوقف میکند و نتیجهای را برمیگرداند و با درخواست ما برای ادامهٔ کار (با MoveNext) کار خود را از همان جای متوقف شده ادامه میدهد.
حالا اگر کمی دقیقتر باشیم سوالی که باید برای ما پیش بیاید این است که آیا CLR خود yield را پشیبانی میکند؟
public class ArrayEnumerable<T> : IEnumerable<T>, IEnumerable { // Fields private T[] _array; // Methods public ArrayEnumerable(T[] array) { this._array = array; } public IEnumerator<T> GetEnumerator() { return new <GetEnumerator>d__0(0); } IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } // Nested Types [CompilerGenerated] private sealed class <GetEnumerator>d__0 : IEnumerator<T>, IEnumerator, IDisposable { // Fields private int <>1__state; private T <>2__current; public ArrayEnumerable<T> <>4__this; public int <index>5__1; // Methods [DebuggerHidden] public <GetEnumerator>d__0(int <>1__state) { this.<>1__state = <>1__state; } private bool MoveNext() { switch (this.<>1__state) { case 0: this.<>1__state = -1; this.<index>5__1 = 0; while (this.<index>5__1 < ArrayEnumerable<T>._array.Length) { this.<>2__current = ArrayEnumerable<T>._array[this.<index>5__1]; this.<>1__state = 1; return true; Label_0050: this.<>1__state = -1; this.<index>5__1++; } break; case 1: goto Label_0050; } return false; } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } void IDisposable.Dispose() { } // Properties T IEnumerator<T>.Current { [DebuggerHidden] get { return this.<>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return this.<>2__current; } } } }
در این کد کامپایلر وضعیتهای مختلفی که برای توقف و ادامهٔ کار MoveNext که مهمترین بخش کد هست را با کمک ترکیبی از switch case و goto پیادهسازی کردهاست که با کمی دقت میتوانید متوجه منطق آن شوید :)
ممکن است به نظرتان برسد که این قطعه کد از نظر (حداقل نامگذاری) در سیشارپ صحیح نیست. اینگونه نامگذاریها که از نظر CLR (و زبان IL) درست ولی از نظر زبان سطح بالا نادرست هستند باعث میشوند که از هرگونه برخورد نامی احتمالی با نامهای معتبر تعریف شده توسط کاربر جلوگیری شود.
احتمالاً اگر پیشزمینه نسبت به این مطلب داشته باشید با خود خواهید گفت که «این که واضح بود، اصلاً وظیفهٔ ماشین در سطح پایین نیست که چنین عملی را پشتیبانی کند». واضحبودن این موضوع برای شما شاید به این دلیل باشد که پیادهسازی yield را قبلاً جای دیگری ندیدهاید. برای درک این مطلب در اینجا نحوهٔ پیادهسازی yield را در پایتون بررسی میکنیم.
def array_iterator(array): length = len(array) index = 0 while index < length: yield array[index] index = index + 1
>>> import dis >>> dis.dis(array_iterator) 2 0 LOAD_GLOBAL 0 (len) 3 LOAD_FAST 0 (array) 6 CALL_FUNCTION 1 9 STORE_FAST 1 (length) 3 12 LOAD_CONST 1 (0) 15 STORE_FAST 2 (index) 4 18 SETUP_LOOP 35 (to 56) >> 21 LOAD_FAST 2 (index) 24 LOAD_FAST 1 (length) 27 COMPARE_OP 0 (<) 30 POP_JUMP_IF_FALSE 55 5 33 LOAD_FAST 0 (array) 36 LOAD_FAST 2 (index) 39 BINARY_SUBSCR 40 YIELD_VALUE 41 POP_TOP 6 42 LOAD_FAST 2 (index) 45 LOAD_CONST 2 (1) 48 BINARY_ADD 49 STORE_FAST 2 (index) 52 JUMP_ABSOLUTE 21 >> 55 POP_BLOCK >> 56 LOAD_CONST 0 (None) 59 RETURN_VALUE
public class SimpleStateMachine : IEnumerable<bool> { public IEnumerator<bool> GetEnumerator() { while (true) { yield return true; yield return false; } } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } }
new SimpleStateMachine().Take(20).ToList().ForEach(x => Console.WriteLine(x));
HttpHandler
public class MyHttpHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { } public bool IsReusable { get { return false; } } }
public class MyHttpHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { HttpResponse response = context.Response; HttpRequest request = context.Request; response.Write("Every Page has a some text like this"); } public bool IsReusable { get { return false; } } }
<system.web> <httpHandlers> <add verb="*" path="*.aspx" type="MyHttpHandler"/> </httpHandlers> </system.web>
<configuration> <system.web> <httpHandlers> <add name="myhttphandler" verb="*" path="*.aspx" type="MyHttpHandler"/> </httpHandlers> </system.web> </configuration>
گزینه Type که نام کلاس میباشد و اگر کلاس داخل یک فضای نام قرار گرفته باشد، باید اینطور نوشت : namespace.ClassName |
گزینه verb شامل مقادیری چون Get,Post,Head,Putو Delete میباشد و httphandler را فقط برای این نوع درخواستها اجرا میکند و در صورتیکه بخواهید چندتا از آنها را استفاده کنید، با , از هم جدا میشوند. مثلا Get,post و درصورتیکه همهی گزینهها را بخواهید علامت * را میتوان استفاده کرد. |
گزینهی path این امکان را به شما میدهد که مسیر و نوع فایلهایی را که قصد دارید روی آنها فقط اجرا شود، مشخص کنید و ما در قطعه کد بالا گفتهایم که تنها روی فایلهایی با پسوند aspx اجرا شود و چون مسیری هم ذکر نکردیم برای همهی مسیرها قابل اجراست. یکی از مزیتهای دادن پسوند این است که میتوانید پسوندهای اختصاصی داشته باشید. مثلا پسوند RSS برای فیدهای وب سایتتان. بسیاری از برنامه نویسان به جای استفاده از صفحات aspx از ashx استفاده میکنند که به مراتب سبکتر از aspx هست و شامل بخش ui نمیشود و نتیجه خروجی آن بر اساس کدی که مینویسید مشخص میشود که میتواند صفحه متنی یا عکس یا xml یا ... باشد. در اینجا در مورد ساخت صفحات ashx توضیح داده شده است. |
public class MyHttpHandler1 :IHttpHandler { public void ProcessRequest(HttpContext context) { HttpResponse response = context.Response; response.Write("this is httphandler1"); } public bool IsReusable { get { return false; } } } public class MyHttpHandler2 : IHttpHandler { public void ProcessRequest(HttpContext context) { HttpResponse response = context.Response; response.Write("this is httphandler2"); } public bool IsReusable { get { return false; } } }
public class MyFactory : IHttpHandlerFactory { public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTrasnlated) { } public void ReleaseHandler(IHttpHandler handler) { } }
Context | یک شی از کلاس httpcontext که دسترسی ما را برای اشیاء سروری چون response,request,session و... فراهم میکند. |
RequestType | مشخص میکند که درخواست صفحه به چه صورتی است. این گزینه برای مواردی است که verb بیش از یک مورد را حمایت میکند. برای مثال دوست دارید یک هندلر را برای درخواستهای Get ارسال کنید و هندلر دیگر را برای درخواستهای نوع Post |
URL | مسیر مجازی virtual Path صفحه صدا زده شده |
PathTranslated | مسیر فیزیکی صفحه درخواست کننده را ارسال میکند. |
public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTrasnlated) { string handlername = "MyHttpHandler1"; if(url.Substring(url.LastIndexOf("/")+1).StartsWith("t")) { handlername = "MyHttpHandler2"; } try { return (IHttpHandler) Activator.CreateInstance(Type.GetType(handlername)); } catch (Exception e) { throw new HttpException("Error: " + handlername, e); } } public void ReleaseHandler(IHttpHandler handler) { } }
موقعی که اینترفیس IHttpAsyncHandler را ارث بری کنید (این اینترفیس نیز از IHttpHandler ارث بری کرده است و دو متد اضافهتر دارد)، باید دو متد دیگر را نیز پیاده سازی کنید:در اعمالی که disk I/O و یا network I/O دارند، پردازش موازی و اعمال async به شدت مقیاس پذیری سیستم را بالا میبرند. به این ترتیب worker thread جاری (که تعداد آنها محدود است)، سریعتر آزاد شده و به worker pool بازگشت داده میشود تا بتواند به یک درخواست دیگر رسیده سرویس دهد. در این حالت میتوان با منابع کمتری، درخواستهای بیشتری را پردازش کرد.
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback callback, object obj) { } public void EndProcessRequest(IAsyncResult result) { }
public class AsynchOperation : IAsyncResult { private bool _completed; private Object _state; private AsyncCallback _callback; private HttpContext _context; bool IAsyncResult.IsCompleted { get { return _completed; } } WaitHandle IAsyncResult.AsyncWaitHandle { get { return null; } } Object IAsyncResult.AsyncState { get { return _state; } } bool IAsyncResult.CompletedSynchronously { get { return false; } } }
public AsynchOperation(AsyncCallback callback, HttpContext context, Object state) { _callback = callback; _context = context; _state = state; _completed = false; }
public void StartAsyncWork() { ThreadPool.QueueUserWorkItem(new WaitCallback(StartAsyncTask),null); }
private void StartAsyncTask(Object workItemState) { _context.Response.Write("<p>Completion IsThreadPoolThread is " + Thread.CurrentThread.IsThreadPoolThread + "</p>\r\n"); _context.Response.Write("Hello World from Async Handler!"); _completed = true; _callback(this); }
context.Response.Write("<p>Begin IsThreadPoolThread is " + Thread.CurrentThread.IsThreadPoolThread + "</p>\r\n"); AsynchOperation asynch = new AsynchOperation(callback, context, obj); asynch.StartAsyncWork(); return asynch;
public class MyHttpHandler : IHttpAsyncHandler { public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback callback, object obj) { context.Response.Write("<p>Begin IsThreadPoolThread is " + Thread.CurrentThread.IsThreadPoolThread + "</p>\r\n"); AsynchOperation asynch = new AsynchOperation(callback, context, obj); asynch.StartAsyncWork(); return asynch; } public void EndProcessRequest(IAsyncResult result) { } public void ProcessRequest(HttpContext context) { throw new InvalidOperationException(); } public bool IsReusable { get { return false; } } } public class AsynchOperation : IAsyncResult { private bool _completed; private Object _state; private AsyncCallback _callback; private HttpContext _context; bool IAsyncResult.IsCompleted { get { return _completed; } } WaitHandle IAsyncResult.AsyncWaitHandle { get { return null; } } Object IAsyncResult.AsyncState { get { return _state; } } bool IAsyncResult.CompletedSynchronously { get { return false; } } public AsynchOperation(AsyncCallback callback, HttpContext context, Object state) { _callback = callback; _context = context; _state = state; _completed = false; } public void StartAsyncWork() { ThreadPool.QueueUserWorkItem(new WaitCallback(StartAsyncTask),null); } private void StartAsyncTask(Object workItemState) { _context.Response.Write("<p>Completion IsThreadPoolThread is " + Thread.CurrentThread.IsThreadPoolThread + "</p>\r\n"); _context.Response.Write("Hello World from Async Handler!"); _completed = true; _callback(this); }
مایکروسافت در MSDN نوشته است که httphandlerها در واقع فرآیندهایی هستند (به این فرایندها بیشتر End Point میگویند) که در پاسخ به درخواستهای رسیده شده توسط asp.net application اجرا میشوند و بیشترین درخواست هایی هم که میرسد از نوع صفحات Aspx میباشد و موقعی که کاربری درخواست صفحهی aspx میکند هندلرهای مربوط به page اجرا میشوند.
ModelBinder سفارشی در ASP.NET MVC
- توضیحات تکمیلی آن در اینجا «ASP.NET Wire Format for Model Binding to Arrays, Lists, Collections, Dictionaries »
EF Code First #1
در ادامه بحث ASP.NET MVC میشود به ابزاری به نام MVC Scaffolding اشاره کرد. کار این ابزار که توسط یکی از اعضای تیم ASP.NET MVC به نام استیو اندرسون تهیه شده، تولید کدهای اولیه یک برنامه کامل ASP.NET MVC از روی مدلهای شما میباشد. حجم بالایی از کدهای تکراری آغازین برنامه را میشود توسط این ابزار تولید و بعد سفارشی کرد. MVC Scaffolding حتی قابلیت تولید کد بر اساس الگوی Repository و یا نوشتن Unit tests مرتبط را نیز دارد. بدیهی است این ابزار جای یک برنامه نویس را نمیتواند پر کند اما کدهای آغازین یک سری کارهای متداول و تکراری را به خوبی میتواند پیاده سازی و ارائه دهد. زیر ساخت این ابزار، علاوه بر ASP.NET MVC، آشنایی با Entity framework code first است.
در طی سری ASP.NET MVC که در این سایت تا به اینجا مطالعه کردید من به شدت سعی کردم از ابزارگرایی پرهیز کنم. چون شخصی که نمیداند مسیریابی چیست، اطلاعات چگونه به یک کنترلر منتقل یا به یک View ارسال میشوند، قراردادهای پیش فرض فریم ورک چیست یا زیر ساخت امنیتی یا فیلترهای ASP.NET MVC کدامند، چطور میتواند از ابزار پیشرفته Code generator ایی استفاده کند، یا حتی در ادامه کدهای تولیدی آنرا سفارشی سازی کند؟ بنابراین برای استفاده از این ابزار و درک کدهای تولیدی آن، نیاز به یک پیشنیاز دیگر هم وجود دارد: «Entity framework code first»
امسال دو کتاب خوب در این زمینه منتشر شدهاند به نامهای:
Programming Entity Framework: DbContext, ISBN: 978-1-449-31296-1
Programming Entity Framework: Code First, ISBN: 978-1-449-31294-7
که هر دو به صورت اختصاصی به مقوله EF Code first پرداختهاند.
در طی روزهای بعدی EF Code first را با هم مرور خواهیم کرد و البته این مرور مستقل است از نوع فناوری میزبان آن؛ میخواهد یک برنامه کنسول باشد یا WPF یا یک سرویس ویندوز NT و یا ... یک برنامه وب.
البته از دیدگاه مایکروسافت، M در MVC به معنای EF Code first است. به همین جهت MVC3 به صورت پیش فرض ارجاعی را به اسمبلیهای آن دارد و یا حتی به روز رسانی که برای آن ارائه داده نیز در جهت تکمیل همین بحث است.
مروری سریع بر تاریخچه Entity framework code first
ویژوال استودیو 2010 و دات نت 4، به همراه EF 4.0 ارائه شدند. با این نگارش امکان استفاده از حالتهای طراحی database first و model first مهیا است. پس از آن، به روز رسانیهای EF خارج از نوبت و به صورت منظم، هر از چندگاهی ارائه میشوند و در زمان نگارش این مطلب، آخرین نگارش پایدار در دسترس آن 4.3.1 میباشد. از زمان EF 4.1 به بعد، نوع جدیدی از مدل سازی به نام Code first به این فریم ورک اضافه شد و در نگارشهای بعدی آن، مباحث DB migration جهت ساده سازی تطابق اطلاعات مدلها با بانک اطلاعاتی، اضافه گردیدند. در روش Code first، کار با طراحی کلاسها که در اینجا مدل دادهها نامیده میشوند، شروع گردیده و سپس بر اساس این اطلاعات، تولید یک بانک اطلاعاتی جدید و یا استفاده از نمونهای موجود میسر میگردد.
پیشتر در روش database first ابتدا یک بانک اطلاعاتی موجود، مهندسی معکوس میشد و از روی آن فایل XML ایی با پسوند EDMX تولید میگشت. سپس به کمک entity data model designer ویژوال استودیو، این فایل نمایش داده شده و یا امکان اعمال تغییرات بر روی آن میسر میشد. همچنین در روش دیگری به نام model first نیز کار از entity data model designer جهت طراحی موجودیتها آغاز میگشت.
اما با روش Code first دیگر در ابتدای امر مدل فیزیکی و یک بانک اطلاعاتی وجود خارجی ندارد. در اینجا EF تعاریف کلاسهای شما را بررسی کرده و بر اساس آن، اطلاعات نگاشتهای خواص کلاسها به جداول و فیلدهای بانک اطلاعاتی را تشکیل میدهد. البته عموما تعاریف ساده کلاسها بر این منظور کافی نیستند. به همین جهت از یک سری متادیتا به نام ویژگیها یا اصطلاحا data annotations مهیا در فضای نام System.ComponentModel.DataAnnotations برای افزودن اطلاعات لازم مانند نام فیلدها، جداول و یا تعاریف روابط ویژه نیز استفاده میگردد. به علاوه در روش Code first یک API جدید به نام Fluent API نیز جهت تعاریف این ویژگیها و روابط، با کدنویسی مستقیم نیز درنظر گرفته شده است. نهایتا از این اطلاعات جهت نگاشت کلاسها به بانک اطلاعاتی و یا برای تولید ساختار یک بانک اطلاعاتی خالی جدید نیز میتوان کمک گرفت.
مزایای EF Code first
- مطلوب برنامه نویسها! : برنامه نویسهایی که مدتی تجربه کار با ابزارهای طراح را داشته باشند به خوبی میدانند این نوع ابزارها عموما demo-ware هستند. چندجا کلیک میکنید، دوبار Next، سه بار OK و ... به نظر میرسد کار تمام شده. اما واقعیت این است که عمری را باید صرف نگهداری و یا پیاده سازی جزئیاتی کرد که انجام آنها با کدنویسی مستقیم بسیار سریعتر، سادهتر و با کنترل بیشتری قابل انجام است.
- سرعت: برای کار با EF Code first نیازی نیست در ابتدای کار بانک اطلاعاتی خاصی وجود داشته باشد. کلاسهای خود را طراحی و شروع به کدنویسی کنید.
- سادگی: در اینجا دیگر از فایلهای EDMX خبری نیست و نیازی نیست مرتبا آنها را به روز کرده یا نگهداری کرد. تمام کارها را با کدنویسی و کنترل بیشتری میتوان انجام داد. به علاوه کنترل کاملی بر روی کد نهایی تهیه شده نیز وجود دارد و توسط ابزارهای تولید کد، ایجاد نمیشوند.
- طراحی بهتر بانک اطلاعاتی نهایی: اگر طرح دقیقی از مدلهای برنامه داشته باشیم، میتوان آنها را به المانهای کوچک و مشخصی، تقسیم و refactor کرد. همین مساله در نهایت مباحث database normalization را به نحوی مطلوب و با سرعت بیشتری میسر میکند.
- امکان استفاده مجدد از طراحی کلاسهای انجام شده در سایر ORMهای دیگر. چون طراحی مدلهای برنامه به بانک اطلاعاتی خاصی گره نمیخورند و همچنین الزاما هم قرار نیست جزئیات کاری EF در آنها لحاظ شود، این کلاسها در صورت نیاز در سایر پروژهها نیز به سادگی قابل استفاده هستند.
- ردیابی سادهتر تغییرات: روش اصولی کار با پروژههای نرم افزاری همواره شامل استفاده از یک ابزار سورس کنترل مانند SVN، Git، مرکوریال و امثال آن است. به این ترتیب ردیابی تغییرات انجام شده به سادگی توسط این ابزارها میسر میشوند.
- سادهتر شدن طراحیهای پیچیدهتر: برای مثال پیاده سازی ارث بری، ایجاد کلاسهای خود ارجاع دهنده و امثال آن با کدنویسی سادهتر است.
دریافت آخرین نگارش EF
برای دریافت و نصب آخرین نگارش EF نیاز است از NuGet استفاده شود و این مزایا را به همراه دارد:
به کمک NuGet امکان با خبر شدن از به روز رسانی جدید صورت گرفته به صورت خودکار درنظر گرفته شده است و همچنین کار دریافت بستههای مرتبط و به روز رسانی ارجاعات نیز در این حالت خودکار است. به علاوه توسط NuGet امکان دسترسی به کتابخانههایی که مثلا در گوگلکد قرار دارند و به صورت معمول امکان دریافت آنها برای ما میسر نیست، نیز بدون مشکل فراهم است (برای نمونه ELMAH، که اصل آن از گوگلکد قابل دریافت است؛ اما بسته نیوگت آن نیز در دسترس میباشد).
پس از نصب NuGet، تنها کافی است بر روی گره References در Solution explorer ویژوال استودیو، کلیک راست کرده و به کمک NuGet آخرین نگارش EF را نصب کرد. در گالری آنلاین آن، عموما EF اولین گزینه است (به علت تعداد بالای دریافت آن).
حین استفاده از NuGet جهت نصب Ef، ابتدا ارجاعاتی به اسمبلیهای زیر به برنامه اضافه خواهند شد:
System.ComponentModel.DataAnnotations.dll
System.Data.Entity.dll
EntityFramework.dll
بدیهی است بدون استفاده از NuGet، تمام این کارها را باید دستی انجام داد.
سپس در پوشهای به نام packages، فایلهای مرتبط با EF قرار خواهند گرفت که شامل اسمبلی آن به همراه ابزارهای DB Migration است. همچنین فایل packages.config که شامل تعاریف اسمبلیهای نصب شده است به پروژه اضافه میشود. NuGet به کمک این فایل و شماره نگارش درج شده در آن، شما را از به روز رسانیهای بعدی مطلع خواهد ساخت:
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="EntityFramework" version="4.3.1" />
</packages>
همچنین اگر به فایل app.config یا web.config برنامه نیز مراجعه کنید، یک سری تنظیمات ابتدایی اتصال به بانک اطلاعاتی در آن ذکر شده است:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=4.3.1.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</configSections>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework">
<parameters>
<parameter value="Data Source=(localdb)\v11.0; Integrated Security=True; MultipleActiveResultSets=True" />
</parameters>
</defaultConnectionFactory>
</entityFramework>
</configuration>
همانطور که ملاحظه میکنید بانک اطلاعاتی پیش فرضی که در اینجا ذکر شده است، LocalDB میباشد. این بانک اطلاعاتی را از این آدرس نیز میتوانید دریافت کنید.
البته ضرورتی هم به استفاده از آن نیست و از سایر نگارشهای SQL Server نیز میتوان استفاده کرد ولی خوب ... مزیت استفاده از آن برای کاربر نهایی این است که «نیازی به یک مهندس برای نصب، راه اندازی و نگهداری ندارد». تنها مشکل آن این است که از ویندوز XP پشتیبانی نمیکند. البته SQL Server CE 4.0 این محدودیت را ندارد.
ضمن اینکه باید درنظر داشت EF به فناوری میزبان خاصی گره نخورده است و مثالهایی که در اینجا بررسی میشوند صرفا تعدادی برنامه کنسول معمولی هستند و نکات عنوان شده در آنها در تمام فناوریهای میزبان موجود به یک نحو کاربرد دارند.
قراردادهای پیش فرض EF Code first
عنوان شد که اطلاعات کلاسهای ساده تشکیل دهنده مدلهای برنامه، برای تعریف جداول و فیلدهای یک بانک اطلاعات و همچنین مشخص سازی روابط بین آنها کافی نیستند و مرسوم است برای پر کردن این خلاء از یک سری متادیتا و یا Fluent API مهیا نیز استفاده گردد. اما در EF Code first یک سری قرار داد توکار نیز وجود دارند که مطلع بودن از آنها سبب خواهد شد تا حجم کدنویسی و تنظیمات جانبی این فریم ورک به حداقل برسند. برای نمونه مدلهای معروف بلاگ و مطالب آنرا درنظر بگیرید:
namespace EF_Sample01.Models
{
public class Post
{
public int Id { set; get; }
public string Title { set; get; }
public string Content { set; get; }
public virtual Blog Blog { set; get; }
}
}
using System.Collections.Generic;
namespace EF_Sample01.Models
{
public class Blog
{
public int Id { set; get; }
public string Title { set; get; }
public string AuthorName { set; get; }
public IList<Post> Posts { set; get; }
}
}
یکی از قراردادهای EF Code first این است که کلاسهای مدل شما را جهت یافتن خاصیتی به نام Id یا ClassId مانند BlogId، جستجو میکند و از آن به عنوان primary key و فیلد identity جدول بانک اطلاعاتی استفاده خواهد کرد.
همچنین در کلاس Blog، خاصیت لیستی از Posts و در کلاس Post خاصیت virtual ایی به نام Blog وجود دارند. به این ترتیب روابط بین دو کلاس و ایجاد کلید خارجی متناظر با آنرا به صورت خودکار انجام خواهد داد.
نهایتا از این اطلاعات جهت تشکیل database schema یا ساختار بانک اطلاعاتی استفاده میگردد.
اگر به فضاهای نام دو کلاس فوق دقت کرده باشید، به کلمه Models ختم شدهاند. به این معنا که در پوشهای به همین نام در پروژه جاری قرار دارند. یا مرسوم است کلاسهای مدل را در یک پروژه class library مجزا به نام DomainClasses نیز قرار دهند. این پروژه نیازی به ارجاعات اسمبلیهای EF ندارد و تنها به اسمبلی System.ComponentModel.DataAnnotations.dll نیاز خواهد داشت.
EF Code first چگونه کلاسهای مورد نظر را انتخاب میکند؟
ممکن است دهها و صدها کلاس در یک پروژه وجود داشته باشند. EF Code first چگونه از بین این کلاسها تشخیص خواهد داد که باید از کدامیک استفاده کند؟ اینجا است که مفهوم جدیدی به نام DbContext معرفی شده است. برای تعریف آن یک کلاس دیگر را به پروژه برای مثال به نام Context اضافه کنید. همچنین مرسوم است که این کلاس را در پروژه class library دیگری به نام DataLayer اضافه میکنند. این پروژه نیاز به ارجاعی به اسمبلیهای EF خواهد داشت. در ادامه کلاس جدید اضافه شده باید از کلاس DbContext مشتق شود:
using System.Data.Entity;
using EF_Sample01.Models;
namespace EF_Sample01
{
public class Context : DbContext
{
public DbSet<Blog> Blogs { set; get; }
public DbSet<Post> Posts { set; get; }
}
}
سپس در اینجا به کمک نوع جنریکی به نام DbSet، کلاسهای دومین برنامه را معرفی میکنیم. به این ترتیب، EF Code first ابتدا به دنبال کلاسی مشتق شده از DbContext خواهد گشت. پس از یافتن آن، خواصی از نوع DbSet را بررسی کرده و نوعهای متناظر با آنرا به عنوان کلاسهای دومین درنظر میگیرد و از سایر کلاسهای برنامه صرفنظر خواهد کرد. این کل کاری است که باید انجام شود.
اگر دقت کرده باشید، نام کلاسهای موجودیتها، مفرد هستند و نام خواص تعریف شده به کمک DbSet، جمع میباشند که نهایتا متناظر خواهند بود با نام جداول بانک اطلاعاتی تشکیل شده.
تشکیل خودکار بانک اطلاعاتی و افزودن اطلاعات به جداول
تا اینجا بدون تهیه یک بانک اطلاعاتی نیز میتوان از کلاس Context تهیه شده استفاده کرد و کار کدنویسی را آغاز نمود. بدیهی است جهت اجرای نهایی کدها، نیاز به یک بانک اطلاعاتی خواهد بود. اگر تنظیمات پیش فرض فایل کانفیگ برنامه را تغییر ندهیم، از همان defaultConnectionFactory یاده شده استفاده خواهد کرد. در این حالت نام بانک اطلاعاتی به صورت خودکار تنظیم شده و مساوی «EF_Sample01.Context» خواهد بود.
برای سفارشی سازی آن نیاز است فایل app.config یا web.config برنامه را اندکی ویرایش نمود:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
...
</configSections>
<connectionStrings>
<clear/>
<add name="Context"
connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true"
providerName="System.Data.SqlClient"
/>
</connectionStrings>
...
</configuration>
در اینجا به بانک اطلاعاتی testdb2012 در وهله پیش فرض SQL Server نصب شده، اشاره شده است. فقط باید دقت داشت که تگ configSections باید در ابتدای فایل قرار گیرد و مابقی تنظیمات پس از آن.
یا اگر علاقمند باشید که از SQL Server CE استفاده کنید، تنظیمات رشته اتصالی را به نحو زیر مقدار دهی نمائید:
<connectionStrings>
<add name="MyContextName"
connectionString="Data Source=|DataDirectory|\Store.sdf"
providerName="System.Data.SqlServerCe.4.0" />
</connectionStrings>
در هر دو حالت، name باید به نام کلاس مشتق شده از DbContext اشاره کند که در مثال جاری همان Context است.
یا اگر علاقمند بودید که این قرارداد توکار را تغییر داده و نام رشته اتصالی را با کدنویسی تعیین کنید، میتوان به نحو زیر عمل کرد:
public class Context : DbContext
{
public Context()
: base("ConnectionStringName")
{
}
البته ضرورتی ندارد این بانک اطلاعاتی از پیش موجود باشد. در اولین بار اجرای کدهای زیر، به صورت خودکار بانک اطلاعاتی و جداول Blogs و Posts و روابط بین آنها تشکیل میگردد:
using EF_Sample01.Models;
namespace EF_Sample01
{
class Program
{
static void Main(string[] args)
{
using (var db = new Context())
{
db.Blogs.Add(new Blog { AuthorName = "Vahid", Title = ".NET Tips" });
db.SaveChanges();
}
}
}
}
در این تصویر چند نکته حائز اهمیت هستند:
الف) نام پیش فرض بانک اطلاعاتی که به آن اشاره شد (اگر تنظیمات رشته اتصالی قید نگردد).
ب) تشکیل خودکار primary key از روی خواصی به نام Id
ج) تشکیل خودکار روابط بین جداول و ایجاد کلید خارجی (به کمک خاصیت virtual تعریف شده)
د) تشکیل جدول سیستمی به نام dbo.__MigrationHistory که از آن برای نگهداری سابقه به روز رسانیهای ساختار جداول کمک گرفته خواهد شد.
ه) نوع و طول فیلدهای متنی، nvarchar از نوع max است.
تمام اینها بر اساس پیش فرضها و قراردادهای توکار EF Code first انجام شده است.
در کدهای تعریف شده نیز، ابتدا یک وهله از شیء Context ایجاد شده و سپس به کمک آن میتوان به جدول Blogs اطلاعاتی را افزود و در آخر ذخیره نمود. استفاده از using هم دراینجا نباید فراموش شود، زیرا اگر استثنایی در این بین رخ دهد، کار پاکسازی منابع و بستن اتصال گشوده شده به بانک اطلاعاتی به صورت خودکار انجام خواهد شد.
در ادامه اگر بخواهیم مطلبی را به Blog ثبت شده اضافه کنیم، خواهیم داشت:
using EF_Sample01.Models;
namespace EF_Sample01
{
class Program
{
static void Main(string[] args)
{
//addBlog();
addPost();
}
private static void addPost()
{
using (var db = new Context())
{
var blog = db.Blogs.Find(1);
db.Posts.Add(new Post
{
Blog = blog,
Content = "data",
Title = "EF"
});
db.SaveChanges();
}
}
private static void addBlog()
{
using (var db = new Context())
{
db.Blogs.Add(new Blog { AuthorName = "Vahid", Title = ".NET Tips" });
db.SaveChanges();
}
}
}
}
متد db.Blogs.Find، بر اساس primary key بلاگ ثبت شده، یک وهله از آنرا یافته و سپس از آن جهت تشکیل شیء Post و افزودن آن به جدول Posts استفاده میشود. متد Find ابتدا Contxet جاری را جهت یافتن شیءایی با id مساوی یک جستجو میکند (اصطلاحا به آن first level cache هم گفته میشود). اگر موفق به یافتن آن شد، بدون صدور کوئری اضافهای به بانک اطلاعاتی از اطلاعات همان شیء استفاده خواهد کرد. در غیراینصورت نیاز خواهد داشت تا ابتدا کوئری لازم را به بانک اطلاعاتی ارسال کرده و اطلاعات شیء Blog متناظر با id=1 را دریافت کند. همچنین اگر نیاز داشتیم تا تنها با سطح اول کش کار کنیم، در EF Code first میتوان از خاصیتی به نام Local نیز استفاده کرد. برای مثال خاصیت db.Blogs.Local بیانگر اطلاعات موجود در سطح اول کش میباشد.
نهایتا کوئری Insert تولید شده توسط آن به شکل زیر خواهد بود (لاگ شده توسط برنامه SQL Server Profiler):
exec sp_executesql N'insert [dbo].[Posts]([Title], [Content], [Blog_Id])
values (@0, @1, @2)
select [Id]
from [dbo].[Posts]
where @@ROWCOUNT > 0 and [Id] = scope_identity()',
N'@0 nvarchar(max) ,@1 nvarchar(max) ,@2 int',
@0=N'EF',
@1=N'data',
@2=1
این نوع کوئرهای پارامتری چندین مزیت مهم را به همراه دارند:
الف) به صورت خودکار تشکیل میشوند. تمام کوئریهای پشت صحنه EF پارامتری هستند و نیازی نیست مرتبا مزایای این امر را گوشزد کرد و باز هم عدهای با جمع زدن رشتهها نسبت به نوشتن کوئریهای نا امن SQL اقدام کنند.
ب) کوئرهای پارامتری در مقابل حملات تزریق اس کیوال مقاوم هستند.
ج) SQL Server با کوئریهای پارامتری همانند رویههای ذخیره شده رفتار میکند. یعنی query execution plan محاسبه شده آنها را کش خواهد کرد. همین امر سبب بالا رفتن کارآیی برنامه در فراخوانیهای بعدی میگردد. الگوی کلی مشخص است. فقط پارامترهای آن تغییر میکنند.
د) مصرف حافظه SQL Server کاهش مییابد. چون SQL Server مجبور نیست به ازای هر کوئری اصطلاحا Ad Hoc رسیده یکبار execution plan متفاوت آنها را محاسبه و سپس کش کند. این مورد مشکل مهم تمام برنامههایی است که از کوئریهای پارامتری استفاده نمیکنند؛ تا حدی که گاهی تصور میکنند شاید SQL Server دچار نشتی حافظه شده، اما مشکل جای دیگری است.
مشکل! ساختار بانک اطلاعاتی تشکیل شده مطلوب کار ما نیست.
تا همینجا با حداقل کدنویسی و تنظیمات مرتبط با آن، پیشرفت خوبی داشتهایم؛ اما نتیجه حاصل آنچنان مطلوب نیست و نیاز به سفارشی سازی دارد. برای مثال طول فیلدها را نیاز داریم به مقدار دیگری تنظیم کنیم، تعدادی از فیلدها باید به صورت not null تعریف شوند یا نام پیش فرض بانک اطلاعاتی باید مشخص گردد و مواردی از این دست. با این موارد در قسمتهای بعدی بیشتر آشنا خواهیم شد.
کتابخانه CSharpFunctionalExtensions
This library helps write code in more functional way. To get to know more about the principles behind it, check out the Applying Functional Principles in C# Pluralsight course.