var ctx = new Entities(); var Fields = ctx.ENTITIES_FEILDS.ToList(); var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( name: new AssemblyName("Demo"), access: AssemblyBuilderAccess.Run); var moduleBuilder = assemblyBuilder.DefineDynamicModule(name: "Module"); var typeBuilder = moduleBuilder.DefineType(name: Fields.First(c => c.FEILD_ID == 1).ENTITIES.ENTITY_NAME, attr: TypeAttributes.Public); foreach (var item in Fields) { switch (item.FEILD_TYPE) { case 0://int { var intField = typeBuilder.DefineField(fieldName: string.Format("_{0}", item.FEILD_NAME), type: typeof(string), attributes: FieldAttributes.Private); var intProperty = typeBuilder.DefineProperty( name: item.FEILD_NAME, attributes: PropertyAttributes.HasDefault, returnType: typeof(string), parameterTypes: null); // خاصیت پارامتر ورودی ندارد //تعریف گت var intpropertyGetMethod = typeBuilder.DefineMethod( name: string.Format("get_{0}", item.FEILD_NAME), attributes: MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, returnType: typeof(string), parameterTypes: Type.EmptyTypes); // اتصال گت متد به خاصیت عددی intProperty.SetGetMethod(intpropertyGetMethod); //تعریف ست var propertySetMethod = typeBuilder.DefineMethod(name: string.Format("set_{0}", item.FEILD_NAME), attributes: MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, returnType: typeof(void), parameterTypes: new[] { typeof(string) }); //اتصال ست متد intProperty.SetSetMethod(propertySetMethod); // بدنه گت متد در اینجا تعریف خواهد شد var propertyGetMethodIL = intpropertyGetMethod.GetILGenerator(); propertyGetMethodIL.Emit(OpCodes.Ldarg_0); // بارگذاری اشارهگری به وهلهای از کلاس جاری در پشته propertyGetMethodIL.Emit(OpCodes.Ldfld, intField); // بارگذاری فیلد نام propertyGetMethodIL.Emit(OpCodes.Ret); //بدنه ست متد در اینجا تعریف شده است var propertySetIL = propertySetMethod.GetILGenerator(); propertySetIL.Emit(OpCodes.Ldarg_0); propertySetIL.Emit(OpCodes.Ldarg_1); propertySetIL.Emit(OpCodes.Stfld, intField); propertySetIL.Emit(OpCodes.Ret); } break; case 1://string { } break; } } var t = typeBuilder.CreateType(); var instance = Activator.CreateInstance(t); var type = instance.GetType(); //تغییر مقدار یک خاصیت var setNameMethod = type.GetMethod("set_CoOrder"); setNameMethod.Invoke(obj: instance, parameters: new[] {"1"}); // دسترسی به خاصیت نام var nProperty = t.GetProperty("CoOrder"); // و دریافت مقدار آن برای نمایش var result = nProperty.GetValue(instance, null); Console.WriteLine(result); Console.ReadKey();
مدل برنامه و نیازمندیهای اعتبارسنجی آن
namespace jqGrid08.Models { public class User { public int Id { set; get; } public string Name { set; get; } public string Email { set; get; } public string Password { set; get; } public string SiteUrl { set; get; } } }
- نام کاربر به صورت اجباری وارد شود و همچنین بین 3 تا 40 حرف باشد.
- همچنین نام کاربر نباید بر اساس اطلاعات موجود در بانک اطلاعاتی، تکراری وارد شود.
- ورود ایمیل شخص اجباری است؛ به علاوه فرمت آن نیز باید با یک ایمیل واقعی تطابق داشته باشد.
- ایمیل وارد شدهی یک کاربر جدید نیز نباید تکراری بوده و پیشتر توسط کاربر دیگری وارد شده باشد.
- ورود کلمهی عبور در حالت ثبت اطلاعات اجباری است؛ اما در حالت ویرایش اطلاعات خیر (از کلمهی عبور موجود در این حالت استفاده خواهد شد).
- ورود آدرس سایت کاربر اجباری بوده و همچنین فرمت آدرس وارد شده نیز باید معتبر باشد.
اعتبار سنجی سمت سرور و سمت کلاینت نام کاربر
colModel: [ { name: '@(StronglyTyped.PropertyName<User>(x => x.Name))', index: '@(StronglyTyped.PropertyName<User>(x => x.Name))', align: 'right', width: 150, editable: true, edittype: 'text', editoptions: { maxlength: 40 }, editrules: { required: true, custom: true, custom_func: function (value, colname) { if (!value) return [false, "لطفا نامی را وارد کنید"]; if (value.length < 3 || value.length > 40) return [false, colname + " باید بین 3 تا 40 حرف باشد"]; else return [true, ""]; } } }, ],
خروجی این متد یک آرایه دو عضوی است. اگر عضو اول آن true باشد، یعنی اعتبارسنجی موفقیت آمیز بودهاست؛ اگر خیر، عضو دوم آرایه، پیامی است که به کاربر نمایش داده خواهد شد.
تا اینجا کار اعتبارسنجی سمت کاربر به پایان میرسد. اما نیاز است در سمت سرور نیز بررسی شود که آیا نام وارد شده تکراری است یا خیر. برای این منظور تنها کافی است رویداد afterSubmit حالتهای Add و Edit را بررسی کنیم:
$('#list').jqGrid({ // ... }).navGrid( '#pager', //enabling buttons { add: true, del: true, edit: true, search: false }, //edit option { afterSubmit: showServerSideErrors }, //add options { afterSubmit: showServerSideErrors }, //delete options { }); }); function showServerSideErrors(response, postdata) { var result = $.parseJSON(response.responseText); if (result.success === false) { //نمایش خطای اعتبار سنجی سمت سرور پس از ویرایش یا افزودن //و همچنین جلوگیری از ثبت نهایی فرم return [false, result.message, result.id]; } return [true, "", result.id]; }
[HttpPost] public ActionResult AddUser(User postData) { //todo: Add user to repository if (postData == null) return Json(new { success = false, message = "اطلاعات دریافتی خالی است" }, JsonRequestBehavior.AllowGet); if (_usersInMemoryDataSource.Any( user => user.Name.Equals(postData.Name, StringComparison.InvariantCultureIgnoreCase))) { return Json(new { success = false, message = "نام کاربر تکراری است" }, JsonRequestBehavior.AllowGet); } if (_usersInMemoryDataSource.Any( user => user.Email.Equals(postData.Email, StringComparison.InvariantCultureIgnoreCase))) { return Json(new { success = false, message = "آدرس ایمیل کاربر تکراری است" }, JsonRequestBehavior.AllowGet); } postData.Id = _usersInMemoryDataSource.LastOrDefault() == null ? 1 : _usersInMemoryDataSource.Last().Id + 1; _usersInMemoryDataSource.Add(postData); return Json(new { id = postData.Id, success = true }, JsonRequestBehavior.AllowGet); }
خروجی روال رویدادگردان afterSubmit نیز بسیار شبیه است به حالت اعتبارسنجی سفارشی یک ستون. اگر عضو اول آرایه بازگشت داده شده توسط آن false باشد، یعنی اعتبارسنجی سمت سرور، با شکست مواجه شده و در این حالت از عضو دوم آرایه برای نمایش پیام خطای بازگشت داده شده از طرف سرور استفاده خواهد شد.
اعتبار سنجی ایمیل کاربر
colModel: [ { name: '@(StronglyTyped.PropertyName<User>(x => x.Email))', index: '@(StronglyTyped.PropertyName<User>(x => x.Email))', align: 'center', width: 150, editable: true, edittype: 'text', editoptions: { maxlength: 250, dir: 'ltr' }, editrules: { required: true, email: true }, formatter: 'email' }, ],
مطابق نیازمندیهای اعتبارسنجی پروژه، ایمیل وارد شده نیز نباید تکراری باشد. این مورد نیز توسط خروجی روال رویدادگردان afterSubmit که پیشتر توضیح داده شده، مدیریت میشود.
اعتبار سنجی کلمه عبور کاربر
colModel: [ { name: '@(StronglyTyped.PropertyName<User>(x => x.Password))', index: '@(StronglyTyped.PropertyName<User>(x => x.Password))', align: 'center', width: 70, editable: true, edittype: 'password', editoptions: { maxlength: 10, dir: 'ltr' }, editrules: { //required: true ---> در این حالت خاص قابل استفاده نیست //در حالت ویرایش رکورد، ورود کلمه عبور اختیاری است //در حالت افزودن رکورد، ورود کلمه عبور اجباری است } }, ],
برای این منظور تنها کافی است از روال رویدادگردان beforeSubmit استفاده کرد:
$('#list').jqGrid({ // ... }).navGrid( '#pager', //enabling buttons { add: true, del: true, edit: true, search: false }, //edit option { /*, beforeSubmit: function (posdata, obj) { //در حالت ویرایش رکورد، ورود کلمه عبور اختیاری است return [true, ""]; }*/ }, //add options { beforeSubmit: function (postdata, obj) { //در حالت افزودن رکورد، ورود کلمه عبور اجباری است if (postdata.Password == null || postdata.Password == "" || postdata.Password == undefined) return [false, "لطفا کلمه عبور را وارد کنید"]; return [true, ""]; } }, //delete options { }); });
اعتبار سنجی آدرس سایت کاربر
colModel: [ { name: '@(StronglyTyped.PropertyName<User>(x => x.SiteUrl))', index: '@(StronglyTyped.PropertyName<User>(x => x.SiteUrl))', align: 'center', width: 150, editable: true, edittype: 'text', editoptions: { maxlength: 1000, dir: 'ltr' }, editrules: { required: true, url: true }, formatter: function (cellvalue, options, rowObject) { return "<a href='" + cellvalue + "' >" + cellvalue + "</a>"; }, unformat: function (cellvalue, options, cell) { return $('a', cell).attr('href'); } }, ],
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
jqGrid08.zip
پیشنیازها
- آشنایی با مباحث Migrations در EF Code first
- آشنایی با مباحث الگوی واحد کار
- چگونه مدلهای EF را به صورت خودکار به Context اضافه کنیم؟
- چگونه تنظیمات مدلهای EF را به صورت خودکار به Context اضافه کنیم؟
کدهایی را که در این قسمت مشاهده خواهید کرد، در حقیقت همان برنامهی توسعه یافته «آشنایی با مباحث الگوی واحد کار» است و از ذکر قسمتهای تکراری آن جهت طولانی نشدن مبحث، صرفنظر خواهد شد. برای مثال Context و مدلهای محصولات و گروههای آنها به همراه کلاسهای لایه سرویس برنامهی اصلی، دقیقا همان کدهای مطلب «آشنایی با مباحث الگوی واحد کار» است.
تعریف domain classes مخصوص افزونهها
در ادامهی پروژهی افزونه پذیر فعلی، پروژهی class library جدیدی را به نام MvcPluginMasterApp.Plugin1.DomainClasses اضافه خواهیم کرد. از آن جهت تعریف کلاسهای مدل افزونهی یک استفاده میکنیم. برای مثال کلاس News را به همراه تنظیمات Fluent آن به این پروژهی جدید اضافه کنید:
using System.Data.Entity.ModelConfiguration; namespace MvcPluginMasterApp.Plugin1.DomainClasses { public class News { public int Id { set; get; } public string Title { set; get; } public string Body { set; get; } } public class NewsConfig : EntityTypeConfiguration<News> { public NewsConfig() { this.ToTable("Plugin1_News"); this.HasKey(news => news.Id); this.Property(news => news.Title).IsRequired().HasMaxLength(500); this.Property(news => news.Body).IsOptional().IsMaxLength(); } } }
PM> install-package EntityFramework
مشکل! برنامهی اصلی، همانند مطلب «آشنایی با مباحث الگوی واحد کار» دارای domain classes خاص خودش است به همراه تنظیمات Context ایی که صریحا در آن مدلهای متناظر با این پروژه در معرض دید EF قرار گرفتهاند:
public class MvcPluginMasterAppContext : DbContext, IUnitOfWork { public DbSet<Category> Categories { set; get; } public DbSet<Product> Products { set; get; }
تغییرات اینترفیس Unit of work جهت افزونه پذیری
در ادامه، اینترفیس بهبود یافتهی IUnitOfWork را جهت پذیرش DbSetهای پویا و همچنین EntityTypeConfigurationهای پویا، ملاحظه میکنید:
namespace MvcPluginMasterApp.PluginsBase { public interface IUnitOfWork : IDisposable { IDbSet<TEntity> Set<TEntity>() where TEntity : class; int SaveAllChanges(); void MarkAsChanged<TEntity>(TEntity entity) where TEntity : class; IList<T> GetRows<T>(string sql, params object[] parameters) where T : class; IEnumerable<TEntity> AddThisRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class; void SetDynamicEntities(Type[] dynamicTypes); void ForceDatabaseInitialize(); void SetConfigurationsAssemblies(Assembly[] assembly); } }
SetDynamicEntities : توسط این متد در ابتدای برنامه، نوعهای مدلهای جدید افزونهها به صورت خودکار به Context اضافه خواهند شد.
SetConfigurationsAssemblies : کار افزودن اسمبلیهای حاوی تعاریف EntityTypeConfigurationهای جدید و پویا را به عهده دارد.
ForceDatabaseInitialize: سبب خواهد شد تا مباحث migrations، پیش از شروع به کار برنامه، اعمال شوند.
در کلاس Context ذیل، نحوهی پیاده سازی این متدهای جدید را ملاحظه میکنید:
namespace MvcPluginMasterApp.DataLayer.Context { public class MvcPluginMasterAppContext : DbContext, IUnitOfWork { private readonly IList<Assembly> _configurationsAssemblies = new List<Assembly>(); private readonly IList<Type[]> _dynamicTypes = new List<Type[]>(); public void ForceDatabaseInitialize() { Database.Initialize(force: true); } public void SetConfigurationsAssemblies(Assembly[] assemblies) { if (assemblies == null) return; foreach (var assembly in assemblies) { _configurationsAssemblies.Add(assembly); } } public void SetDynamicEntities(Type[] dynamicTypes) { if (dynamicTypes == null) return; _dynamicTypes.Add(dynamicTypes); } protected override void OnModelCreating(DbModelBuilder modelBuilder) { addConfigurationsFromAssemblies(modelBuilder); addPluginsEntitiesDynamically(modelBuilder); base.OnModelCreating(modelBuilder); } private void addConfigurationsFromAssemblies(DbModelBuilder modelBuilder) { foreach (var assembly in _configurationsAssemblies) { modelBuilder.Configurations.AddFromAssembly(assembly); } } private void addPluginsEntitiesDynamically(DbModelBuilder modelBuilder) { foreach (var types in _dynamicTypes) { foreach (var type in types) { modelBuilder.RegisterEntityType(type); } } } } }
بهبود اینترفیس IPlugin جهت پذیرش نوعهای پویای EF
در قسمت اول، با اینترفیس IPlugin آشنا شدیم. هر افزونه باید دارای کلاسی باشد که این اینترفیس را پیاده سازی میکند. از آن جهت دریافت تنظیمات و یا ثبت تنظیمات مسیریابی و امثال آن استفاده میشود.
در اینجا متد GetEfBootstrapper آن کار دریافت تنظیمات EF هر افزونه را به عهد دارد.
namespace MvcPluginMasterApp.PluginsBase { public interface IPlugin { EfBootstrapper GetEfBootstrapper(); //...به همراه سایر متدهای مورد نیاز } public class EfBootstrapper { /// <summary> /// Assemblies containing EntityTypeConfiguration classes. /// </summary> public Assembly[] ConfigurationsAssemblies { get; set; } /// <summary> /// Domain classes. /// </summary> public Type[] DomainEntities { get; set; } /// <summary> /// Custom Seed method. /// </summary> public Action<IUnitOfWork> DatabaseSeeder { get; set; } } }
DomainEntities بیانگر لیست مدلها و موجودیتهای هر افزونه است.
DatabaseSeeder کار دریافت منطق متد Seed را بر عهده دارد. برای مثال اگر افزونهای نیاز است در آغاز کار تشکیل جداول آن، دیتای پیش فرض و خاصی را در بانک اطلاعاتی ثبت کند، میتوان از این متد استفاده کرد. اگر دقت کنید این Action یک وهله از IUnitOfWork را به افزونه ارسال میکند. بنابراین در این طراحی جدید، اینترفیس IUnitOfWork به پروژهی MvcPluginMasterApp.PluginsBase منتقل میشود. به این ترتیب دیگر نیازی نیست تا تک تک افزونهها ارجاع مستقیمی را به DataLayer پروژهی اصلی پیدا کنند.
تکمیل متد GetEfBootstrapper در افزونهها
اکنون جهت معرفی مدلها و تنظیمات EF آنها، تنها کافی است متد GetEfBootstrapper هر افزونه را تکمیل کنیم:
namespace MvcPluginMasterApp.Plugin1 { public class Plugin1 : IPlugin { public EfBootstrapper GetEfBootstrapper() { return new EfBootstrapper { DomainEntities = new[] { typeof(News) }, ConfigurationsAssemblies = new[] { typeof(NewsConfig).Assembly }, DatabaseSeeder = uow => { var news = uow.Set<News>(); if (news.Any()) { return; } news.Add(new News { Title = "News 1", Body = "news 1 news 1 news 1 ...." }); news.Add(new News { Title = "News 2", Body = "news 2 news 2 news 2 ...." }); } }; }
همچنین توسط delegate ایی به نام DatabaseSeeder، نحوهی دسترسی به متد Set واحد کار و سپس استفادهی از آن، برای تعریف متد Seed سفارشی نیز تکمیل شدهاست.
تدارک یک راه انداز EF، پیش از شروع به کار برنامه
در پوشهی App_Start پروژهی اصلی یا همان MvcPluginMasterApp، کلاس جدید EFBootstrapperStart را با کدهای ذیل اضافه کنید:
[assembly: PreApplicationStartMethod(typeof(EFBootstrapperStart), "Start")] namespace MvcPluginMasterApp { public static class EFBootstrapperStart { public static void Start() { var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList(); using (var uow = SmObjectFactory.Container.GetInstance<IUnitOfWork>()) { initDatabase(uow, plugins); runDatabaseSeeders(uow, plugins); } } private static void initDatabase(IUnitOfWork uow, IEnumerable<IPlugin> plugins) { foreach (var plugin in plugins) { var efBootstrapper = plugin.GetEfBootstrapper(); if (efBootstrapper == null) continue; uow.SetDynamicEntities(efBootstrapper.DomainEntities); uow.SetConfigurationsAssemblies(efBootstrapper.ConfigurationsAssemblies); } Database.SetInitializer(new MigrateDatabaseToLatestVersion<MvcPluginMasterAppContext, Configuration>()); uow.ForceDatabaseInitialize(); } private static void runDatabaseSeeders(IUnitOfWork uow, IEnumerable<IPlugin> plugins) { foreach (var plugin in plugins) { var efBootstrapper = plugin.GetEfBootstrapper(); if (efBootstrapper == null || efBootstrapper.DatabaseSeeder == null) continue; efBootstrapper.DatabaseSeeder(uow); uow.SaveAllChanges(); } } } }
همانطور که ملاحظه میکنید، ابتدا لیست تمام افزونههای موجود، به کمک StructureMap دریافت میشوند. سپس میتوان در متد initDatabase به متد GetEfBootstrapper هر افزونه دسترسی یافت و توسط آن تنظیمات مدلها را یافته و به Context اصلی برنامه اضافه کرد. سپس با فراخوانی ForceDatabaseInitialize تمام این موارد به صورت خودکار به بانک اطلاعاتی اعمال خواهند شد.
کار متد runDatabaseSeeders، یافتن DatabaseSeeder هر افزونه، اجرای آنها و سپس فراخوانی متد SaveAllChanges در آخر کار است.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید:
MvcPlugin
public partial class PersonallyEntities : DbContext { public PersonallyEntities() : base("name=PersonallyEntities") { } }
"name=PersonallyEntities"
metadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl;provider=System.Data.SqlClient;provider connection string="data source=.;initial catalog=Personally;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework"
public static string BuildEntityConnection(string connectionString) { var entityConnection = new EntityConnectionStringBuilder { Provider = "System.Data.SqlClient", ProviderConnectionString = connectionString, Metadata = "res://*" }; return entityConnection.ToString(); }
public partial class PersonallyEntities : DbContext { public PersonallyEntities(string connectionString) : base(connectionString) { } }
: base(connectionString)
var entityConnectionString = BuildeEntityConnection("Data Source=localhost;Initial Catalog=Personally; Integrated Security=True"); var PersonallyDb = new PersonallyEntities(entityConnectionString);
ObsoleteAttribute
ObsoleteAttribute بر روی تمامی عناصر یک برنامه بجز assemblies, modules، پارامترها و مقادیر بازگشتی قابل استفاده است. علامتگذاری یک عنصر به عنوان منسوخ شده، به کاربر استفاده کننده اطلاع میدهد که این عنصر در نسخههای آینده حذف خواهد شد.
با استفاده از پروپرتی Message آن پیامی را به کاربر استفاده کننده نشان خواهد داد و توصیه میشود در این پیام یک راه حل نیز ارائه شود.
پروپرتی IsError در صورتی که مقدار آن به true تعیین شده باشد و کامپایلر در صورتی که عنصری که این خصوصیت بر روی آن تعریف شده است، استفاده شده باشد، در پنجره Error List، پیام مربوط به Obsolete را نشان میدهد. برای مثال پس از استفاده از کلاس زیر، OrderDetailTotal به صورت warning و CalculateOrderDetailTotal به صورت Error در پنجره Error List نشان داده میشود.
public static class ObsoleteExample { // Mark OrderDetailTotal As Obsolete. [ObsoleteAttribute("This property (OrderDetailTotal) is obsolete. Use InvoiceTotal instead.", false)] public static decimal OrderDetailTotal { get { return 12m; } } public static decimal InvoiceTotal { get { return 25m; } } // Mark CalculateOrderDetailTotal As Obsolete. [ObsoleteAttribute("This method is obsolete. Call CalculateInvoiceTotal instead.", true)] public static decimal CalculateOrderDetailTotal() { return 0m; } public static decimal CalculateInvoiceTotal() { return 1m; } }
DefaultValueAttribute
DefaultValueAttribute جهت تعیین مقدار پیش فرض یک پروپرتی استفاده میشود. شما میتوانید یک DefaultValueAttribute را با هر مقداری ایجاد کنید. ایجاد مقدار پیش فرض برای یک پروپرتی باعث نمیشود که مقداردهی اولیهای به آن انجام گیرد؛ برای این کار نیاز به کدنویسی میباشد.
مثال زیر نحوه استفاده و مقداردهی اولیه پروپرتیها را نشان میدهد.
public class DefaultValueAttributeTest { public DefaultValueAttributeTest() { // Use the DefaultValue propety of each property to actually set it, via reflection. foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(this)) { var attr = prop.Attributes[typeof(DefaultValueAttribute)] as DefaultValueAttribute; if (attr != null) prop.SetValue(this, attr.Value); } } [DefaultValue(28)] public int Age { get; set; } [DefaultValue("Vahid")] public string FirstName { get; set; } [DefaultValue("Mohammad Taheri")] public string LastName { get; set; } public override string ToString() { return $"{this.FirstName} {this.LastName} is {this.Age}."; } }
DebuggerBrowsableAttribute
در صورت استفاده از DebuggerBrowsableAttribute ، شما میتوانید نحوه نمایش یک عضو را در پنجره متغیرها، در زمان دیباگ، تعیین کنید.public class DebuggerBrowsableTest { [DebuggerBrowsable(DebuggerBrowsableState.Never)] // عدم نمایش در زمان دیباگ در پنجره متغیرها public string FirstName { get; set; } [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] // مقدار پیش فرض public string LastName { get; set; } [DebuggerBrowsable( DebuggerBrowsableState.RootHidden )] // عدم نمایش در زمان دیباگ در پنجره متغیرها public string FullName => FirstName + " " + LastName; [DebuggerBrowsable( DebuggerBrowsableState.RootHidden )] // تنها در زمانی که یک آرایه یا لیست باشد نمایش داده میشود public string[] FullNameArray => new string[] { FirstName + " " + LastName }; }
اگر از کد مثال بالا استفاده کنید و با استفاده از کلید F11 به صورت خط به خط دستورات را اجرا کنید، مشاهده خواهید کرد متغیر FirstName و FullName در پنجره Autos نشان داده نخواهد شد.
Operator ??
عملگر ?? در صورتی که عملوند سمت چپ آن تهی (null) نباشد، مقدار آن را باز میگرداند و در غیر اینصورت مقدار عملوند سمت راست خود را باز میگرداند. نوعهای تهی پذیر (nullable) میتوانند دارای مقدار و یا به صورت تعریف نشده باشند. عملگر ?? وقتی که یک نوع تهی پذیر به یک نوع غیرتهی پذیر انتساب داده میشود، مقدار پیش فرض آن را باز میگرداند.
int? x = null; int y = x ?? -1; Console.WriteLine("y now equals -1 because x was null => {0}", y); int i = DefaultValueOperatorTest.GetNullableInt() ?? default(int); Console.WriteLine("i equals now 0 because GetNullableInt() returned null => {0}", i); string s = DefaultValueOperatorTest.GetStringValue(); Console.WriteLine("Returns 'Unspecified' because s is null => {0}", s ?? "Unspecified");
مطلبی را دیروز در وبلاگ آقای صحرایی دیدم در مورد بهبود کارآیی برنامهها و سایتهای ASP.Net ، که یکی از موارد آن "فاصله بین تگ ها را تا حد ممکن از بین ببرید" بود.
برای پیاده سازی آن به صورت زیر میتوان عمل کرد:
using System.Text.RegularExpressions;
//حذف فاصلههای خالی
private static readonly Regex REGEX_BETWEEN_TAGS = new Regex(@">\s+<", RegexOptions.Compiled);
private static readonly Regex REGEX_LINE_BREAKS = new Regex(@"\n\s+", RegexOptions.Compiled);
protected override void Render(HtmlTextWriter writer)
{
using (HtmlTextWriter htmlwriter = new HtmlTextWriter(new System.IO.StringWriter()))
{
base.Render(htmlwriter);
string html = htmlwriter.InnerWriter.ToString();
html = REGEX_BETWEEN_TAGS.Replace(html, "> <");
html = REGEX_LINE_BREAKS.Replace(html, string.Empty);
writer.Write(html.Trim());
}
}
یا روش دیگر اعمال آن سفارشی ساختن ASP.NET pipeline با کمک Response.Filter آن است. برای مشاهده پیاده سازی آن لطفا به مقاله زیر مراجعه بفرمائید:
برای بهینه سازی قسمت اعمال regular expressions آن میتوان به مقاله "چگونه Regex سریعتری داشته باشیم؟" مراجعه کرد.
<script src="//code.jquery.com/jquery-1.11.3.min.js"></script> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css"> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script> <link href="~/content/css/bootstrap-dialog.min.css" rel=stylesheet"></link> <script src="~/Scripts/bootstrap-dialog.min.js"></script>
var dialog=new BootstrapDialog(); dialog.Title="عنوان دیالوگ"; dialog.Message="متن پنجره"; //فعال سازی این خصوصیت باعث میشود یک دکمه بستن به //پنجره اضافه شده و همچنین توسط کلیک کاربر در خارج از صفحه //باعث بسته شدن پنجره شود یا استفاده از کلید //ESC dialog.Closable=false; //تغییر اندازه دیالوگ Dialog.Size=BootstrapDialogSize.SizeNormal; //رنگ بندی دیالوگ را تغییر میدهد.مقدار زیر باعث میشود //دیالوگ با رنگبندی قرمز نمایش داده شود تا برای نمایش خطاها مناسب باشد Dialog.Type=BootstrapDialogType.Danger; //برای اعمال کردن یک کلاس استابل دلخواه Dialog.CssClass=""; //آیکن برای دیالوگ-استفاده از نام کلاس آیکنهای بوت استراپ Dialog.SpinIcon=""; //یک توصیف است که فقط در کد صفحه نمایش داده میشود //استفاده خاصی ندارد Dialog.Description=""; //بعد از بستن دیالوگ ، کدهای آن در صفحه حذف خواهند شد //اگر میخواهید کد را بارها و بارها نمایش دهید //آن را با مقدار ناصحیح مقدار دهی کنید dialog.AutoDestory=false; //========== رویدادها ============= //این رویدا قبل از نمایش دیالوگ نمایش داده میشود dialog.OnShow="function(){alert('before Dialog');}"; //این رویداد بعد از نمایش دیالوگ اجرا میشود dialog.OnShown="function(){alert('after Dialog shown');}"; //موقع درخواست بستن دیالوگ قبل از بسته شدن اجرا میگردد dialog.OnHide="function(){alert('before Dialog close');}"; //بعد از بسته شدن دیالوگ اجرا میشود dialog.OnHidden="function(){alert('after Dialog close');}";
@{ var dialog=new BootstrapDialog(); dialog.... // ........ } @HTML.BootstrapDialog("example1",dialog)
var dialog=new BootstrapDialog(); var cancelButton=new BootstrapDialogButton("cancelButton"); //cancelButton.id="cancelButton"; cancelButton.label="Cancel"; cancelButton.Key=65; cancelButton.Action="function(){alert('You Clicked!');}"; dialog.AddButton(cancelButton);
dialog.RemoveButton("cancelButton");
داده ها
dialog.AddData("key","value");
dialog.RemoveData("key");
متدها
- دستی: که میتوانید اطلاعات متدها را در همان صفحه مثال و مستندات ببینید و از نامی که به دکمهها و پنجرهها میدهید آنها را اعمال کنید.
- با استفاده از کلاس: کلاس ما شامل دو متد دیگر برای کنترل متدها میباشد. حدود 13 متد در آن پشتیبانی میشود که باعث میشود در بسیاری از اوقات دیگری نیازی به دانستن نام متدها نداشته باشید. یکی از متدها برای استفاده در Helper طراحی شده است که خروجی آن از نوع MvcHtmlString است و متد دیگر خروجی string دارند که میتوانید در صورتیکه خواستید، در رویدادها و خارج از Html Helper از آن استفاده کنید.
$( "#btnshowpopup" ).click(function() { @HTML.RunBootstrapDialogMethod("example1",BootstrapDialogMethods.Open) });
$( "#btnshowpopup" ).click(function() { @HTML.RunBootstrapDialogMethod("example1",BootstrapDialogMethods.SetData,new{"key","value"}) });
cancelButton.Action="function(){{{0}}}"; cancelButton.Action=string.format(cancelButton.Action,RunBootstrapDialogMethod("example1",BootstrapDialogMethods.Close));
به عنوان یک مثال نهایی کد زیر را نوشته که نتیجه آن را در تصویر زیر میبینم:
@{ const string dialogName = "errorDialog"; var cancelButton = new BootstrapDialogButton(); cancelButton.Id = "btncancel"; cancelButton.Label = "بستن"; cancelButton.Action = "function(){{{0}}}"; cancelButton.Action = String.Format(cancelButton.Action, Dialogs.RunBootstrapDialogMethod(dialogName, BootstrapDialogMethods.Close)); var dialog = new BootstrapDialog(); dialog.AddButton(cancelButton); dialog.Title = "عنوان"; dialog.Message = "پیام هشدار"; dialog.DialogType=BootstrapDialogType.Warning; dialog.DialogSize=BootstrapDialogSize.SizeNormal; dialog.Closable = false; dialog.AddData("data1","5"); } @Html.BootstrapDialog(dialogName, dialog) @Html.RunBootstrapDialogMethod(dialogName,BootstrapDialogMethods.Open);
EF Code First #6
ادامه بررسی Fluent API جهت تعریف نگاشت کلاسها به بانک اطلاعاتی
در قسمتهای قبل با استفاده از متادیتا و data annotations جهت بررسی نحوه نگاشت اطلاعات کلاسها به جداول بانک اطلاعاتی آشنا شدیم. اما این موارد تنها قسمتی از تواناییهای Fluent API مهیا در EF Code first را ارائه میدهند. یکی از دلایل آن هم به محدود بودن تواناییهای ذاتی Attributes بر میگردد. برای مثال حین کار با Attributes امکان استفاده از متغیرها یا lambda expressions و امثال آن وجود ندارد. به علاوه شاید عدهای علاقمند نباشند تا کلاسهای خود را با data annotations شلوغ کنند.
در قسمت دوم این سری، مروری مقدماتی داشتیم بر Fluent API. در آنجا ذکر شد که امکان تعریف نگاشتها به کمک تواناییهای Fluent API به دو روش زیر میسر است:
الف) میتوان از متد protected override void OnModelCreating در کلاس مشتق شده از DbContext کار را شروع کرد.
ب) و یا اگر بخواهیم کلاس Context برنامه را شلوغ نکنیم بهتر است به ازای هر کلاس مدل برنامه، یک کلاس mapping مشتق شده از EntityTypeConfiguration را تعریف نمائیم. سپس میتوان این کلاسها را در متد OnModelCreating یاد شده، توسط متد modelBuilder.Configurations.Add جهت استفاده و اعمال، معرفی کرد.
کلاسهای مدلی را که در این قسمت بررسی خواهیم کرد، همان کلاسهای User و Project قسمت سوم هستند و هدف این قسمت بیشتر تطابق Fluent API با اطلاعات ارائه شده در قسمت سوم است؛ برای مثال در اینجا چگونه باید از خاصیتی صرفنظر کرد، مسایل همزمانی را اعمال نمود و امثال آن.
بنابراین یک پروژه جدید کنسول را آغاز نمائید. سپس با کمک NuGet ارجاعات لازم را به اسمبلیهای EF اضافه نمائید.
در پوشه Models این پروژه، سه کلاس تکمیل شده زیر، از قسمت سوم وجود دارند:
using System;
using System.Collections.Generic;
namespace EF_Sample03.Models
{
public class User
{
public int Id { set; get; }
public DateTime AddDate { set; get; }
public string Name { set; get; }
public string LastName { set; get; }
public string FullName
{
get { return Name + " " + LastName; }
}
public string Email { set; get; }
public string Description { set; get; }
public byte[] Photo { set; get; }
public IList<Project> Projects { set; get; }
public byte[] RowVersion { set; get; }
public InterestComponent Interests { set; get; }
public User()
{
Interests = new InterestComponent();
}
}
}
using System;
namespace EF_Sample03.Models
{
public class Project
{
public int Id { set; get; }
public DateTime AddDate { set; get; }
public string Title { set; get; }
public string Description { set; get; }
public virtual User User { set; get; }
public byte[] RowVesrion { set; get; }
}
}
namespace EF_Sample03.Models
{
public class InterestComponent
{
public string Interest1 { get; set; }
public string Interest2 { get; set; }
}
}
سپس یک پوشه جدید به نام Mappings را به پروژه اضافه نمائید. به ازای هر کلاس فوق، یک کلاس جدید را جهت تعاریف اطلاعات نگاشتها به کمک Fluent API اضافه خواهیم کرد:
using System.Data.Entity.ModelConfiguration;
using EF_Sample03.Models;
namespace EF_Sample03.Mappings
{
public class InterestComponentConfig : ComplexTypeConfiguration<InterestComponent>
{
public InterestComponentConfig()
{
this.Property(x => x.Interest1).HasMaxLength(450);
this.Property(x => x.Interest2).HasMaxLength(450);
}
}
}
using System.Data.Entity.ModelConfiguration;
using EF_Sample03.Models;
namespace EF_Sample03.Mappings
{
public class ProjectConfig : EntityTypeConfiguration<Project>
{
public ProjectConfig()
{
this.Property(x => x.Description).IsMaxLength();
this.Property(x => x.RowVesrion).IsRowVersion();
}
}
}
using System.Data.Entity.ModelConfiguration;
using EF_Sample03.Models;
using System.ComponentModel.DataAnnotations;
namespace EF_Sample03.Mappings
{
public class UserConfig : EntityTypeConfiguration<User>
{
public UserConfig()
{
this.HasKey(x => x.Id);
this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
this.ToTable("tblUser", schemaName: "guest");
this.Property(p => p.AddDate).HasColumnName("CreateDate").HasColumnType("date").IsRequired();
this.Property(x => x.Name).HasMaxLength(450);
this.Property(x => x.LastName).IsMaxLength().IsConcurrencyToken();
this.Property(x => x.Email).IsFixedLength().HasMaxLength(255); //nchar(128)
this.Property(x => x.Photo).IsOptional();
this.Property(x => x.RowVersion).IsRowVersion();
this.Ignore(x => x.FullName);
}
}
}
توضیحاتی در مورد کلاسهای تنظیمات نگاشتهای خواص به جداول و فیلدهای بانک اطلاعاتی
نظم بخشیدن به تعاریف نگاشتها
همانطور که ملاحظه میکنید، جهت نظم بیشتر پروژه و شلوغ نشدن متد OnModelCreating کلاس Context برنامه، که در ادامه کدهای آن معرفی خواهد شد، به ازای هر کلاس مدل، یک کلاس تنظیمات نگاشتها را اضافه کردهایم.
کلاسهای معمولی نگاشتها ازکلاس EntityTypeConfiguration مشتق خواهند شد و جهت تعریف کلاس InterestComponent به عنوان Complex Type، اینبار از کلاس ComplexTypeConfiguration ارث بری شده است.
تعیین طول فیلدها
در کلاس InterestComponentConfig، به کمک متد HasMaxLength، همان کار ویژگی MaxLength را میتوان شبیه سازی کرد که در نهایت، طول فیلد nvarchar تشکیل شده در بانک اطلاعاتی را مشخص میکند. اگر نیاز است این فیلد nvarchar از نوع max باشد، نیازی به تنظیم خاصی نداشته و حالت پیش فرض است یا اینکه میتوان صریحا از متد IsMaxLength نیز برای معرفی nvarchar max استفاده کرد.
تعیین مسایل همزمانی
در قسمت سوم با ویژگیهای ConcurrencyCheck و Timestamp آشنا شدیم. در اینجا اگر نوع خاصیت byte array بود و نیاز به تعریف آن به صورت timestamp وجود داشت، میتوان از متد IsRowVersion استفاده کرد. معادل ویژگی ConcurrencyCheck در اینجا، متد IsConcurrencyToken است.
تعیین کلید اصلی جدول
اگر پیش فرضهای EF Code first مانند وجود خاصیتی به نام Id یا ClassName+Id رعایت شود، نیازی به کار خاصی نخواهد بود. اما اگر این قراردادها رعایت نشوند، میتوان از متد HasKey (که نمونهای از آنرا در کلاس UserConfig فوق مشاهده میکنید)، استفاده کرد.
تعیین فیلدهای تولید شده توسط بانک اطلاعاتی
به کمک متد HasDatabaseGeneratedOption، میتوان مشخص کرد که آیا یک فیلد Identity است و یا یک فیلد محاسباتی ویژه و یا هیچکدام.
تعیین نام جدول و schema آن
اگر نیاز است از قراردادهای نامگذاری خاصی پیروی شود، میتوان از متد ToTable جهت تعریف نام جدول متناظر با کلاس جاری استفاده کرد. همچنین در اینجا امکان تعریف schema نیز وجود دارد.
تعیین نام و نوع سفارشی فیلدها
همچنین اگر نام فیلدها نیز باید از قراردادهای دیگری پیروی کنند، میتوان آنها را به صورت صریح توسط متد HasColumnName معرفی کرد. اگر نیاز است این خاصیت به نوع خاصی در بانک اطلاعاتی نگاشت شود، باید از متد HasColumnType کمک گرفت. برای مثال در اینجا بجای نوع datetime، از نوع ویژه date استفاده شده است.
معرفی فیلدها به صورت nchar بجای nvarchar
برای نمونه اگر قرار است هش کلمه عبور در بانک اطلاعاتی ذخیره شود، چون طول آن ثابت میباشد، توصیه شدهاست که بجای nvarchar از nchar برای تعریف آن استفاده شود. برای این منظور تنها کافی است از متد IsFixedLength استفاده شود. در این حالت طول پیش فرض 128 برای فیلد درنظر گرفته خواهد شد. بنابراین اگر نیاز است از طول دیگری استفاده شود، میتوان همانند سابق از متد HasMaxLength کمک گرفت.
ضمنا این فیلدها همگی یونیکد هستند و با n شروع شدهاند. اگر میخواهید از varchar یا char استفاده کنید، میتوان از متد IsUnicode با پارامتر false استفاده کرد.
معرفی یک فیلد به صورت null پذیر در سمت بانک اطلاعاتی
استفاده از متد IsOptional، فیلد را در سمت بانک اطلاعاتی به صورت فیلدی با امکان پذیرش مقادیر null معرفی میکند.
البته در اینجا به صورت پیش فرض byte arrayها به همین نحو معرفی میشوند و تنظیم فوق صرفا جهت ارائه توضیحات بیشتر در نظر گرفته شد.
صرفنظر کردن از خواص محاسباتی در تعاریف نگاشتها
با توجه به اینکه خاصیت FullName به صورت یک خاصیت محاسباتی فقط خواندنی، در کدهای برنامه تعریف شده است، با استفاده از متد Ignore، از نگاشت آن به بانک اطلاعاتی جلوگیری خواهیم کرد.
معرفی کلاسهای تعاریف نگاشتها به برنامه
استفاده از کلاسهای Config فوق خودکار نیست و نیاز است توسط متد modelBuilder.Configurations.Add معرفی شوند:
using System.Data.Entity;
using System.Data.Entity.Migrations;
using EF_Sample03.Mappings;
using EF_Sample03.Models;
namespace EF_Sample03.DataLayer
{
public class Sample03Context : DbContext
{
public DbSet<User> Users { set; get; }
public DbSet<Project> Projects { set; get; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new InterestComponentConfig());
modelBuilder.Configurations.Add(new ProjectConfig());
modelBuilder.Configurations.Add(new UserConfig());
//modelBuilder.ComplexType<InterestComponent>();
//modelBuilder.Ignore<InterestComponent>();
base.OnModelCreating(modelBuilder);
}
}
public class Configuration : DbMigrationsConfiguration<Sample03Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
protected override void Seed(Sample03Context context)
{
base.Seed(context);
}
}
}
در اینجا کلاس Context برنامه مثال جاری را ملاحظه میکنید؛ به همراه کلاس Configuration مهاجرت خودکار که در قسمتهای قبل بررسی شد.
در متد OnModelCreating نیز میتوان یک کلاس را از نوع Complex معرفی کرد تا برای آن در بانک اطلاعاتی جدول جداگانهای تعریف نشود. اما باید دقت داشت که اینکار را فقط یکبار میتوان انجام داد؛ یا توسط کلاس InterestComponentConfig و یا توسط متد modelBuilder.ComplexType. اگر هر دو با هم فراخوانی شوند، EF یک استثناء را صادر خواهد کرد.
و در نهایت، قسمت آغازین برنامه اینبار به شکل زیر خواهد بود که از آغاز کننده MigrateDatabaseToLatestVersion (قسمت چهارم این سری) نیز استفاده کرده است:
using System;
using System.Data.Entity;
using EF_Sample03.DataLayer;
namespace EF_Sample03
{
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample03Context, Configuration>());
using (var db = new Sample03Context())
{
var project1 = db.Projects.Find(1);
if (project1 != null)
{
Console.WriteLine(project1.Title);
}
}
}
}
}
ضمنا رشته اتصالی مورد استفاده تعریف شده در فایل کانفیک برنامه نیز به صورت زیر تعریف شده است:
<connectionStrings>
<clear/>
<add
name="Sample03Context"
connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true"
providerName="System.Data.SqlClient"
/>
/connectionStrings>
در قسمتهای بعد مباحث پیشرفتهتری از تنظیمات نگاشتها را به کمک Fluent API، بررسی خواهیم کرد. برای مثال روابط ارث بری، many-to-many و ... چگونه تعریف میشوند.
- تمام تعاریف بومی سازی مورد نیاز برنامه در یک تک فایل SharedResource.fa.resx قرار میگیرند. این فایل نیز در یک اسمبلی مستقل از برنامهی اصلی اضافه میشود.
- با استفاده از تزریق سرویس IStringLocalizer میتوان به کلیدهای فایل SharedResource.fa.resx در هر قسمتی از برنامهی Web API دسترسی یافت.
- در این بین اگر کلیدی یافت نشد، خطایی با ذکر دقیق جزئیات منبع جستجو شده، لاگ میشود.
- کلیدهای بومی سازی data annotations نیز قابل دریافت از فایل SharedResource.fa.resx میباشند.
در ادامه روش پیاده سازی یک چنین امکاناتی را بررسی میکنیم.
قرار دادن فایل منبع اشتراکی در اسمبلی ExternalResources
پس از ایجاد پروژهی ابتدایی Web API به نام Core3xSharedResource.WebApi، یک اسمبلی جدید را برای مثال به نام Core3xSharedResource.ExternalResources تعریف کرده و در داخل آن پوشهی جدید Resources را تعریف میکنیم. به این پوشه، فایل منبع جدیدی را به نام SharedResource.fa.resx اضافه میکنیم. در کنار آن باید یک کلاس خالی به نام SharedResource.cs نیز وجود داشته باشد.
کار با ین فایل (و یا فایلهای دیگری مانند SharedResource.en.resx) همانند تمام فایلهای منبع استاندارد است و نکتهی خاصی را به همراه ندارد.
معرفی فایل منبع اشتراکی به سرویسهای بومی سازی برنامه
پس از ایجاد و تکمیل فایل منبع اشتراکی، برای معرفی آن به برنامه، ابتدا کلاس جدید LocalizationConfig را تعریف کرده و در آن متد جدید AddCustomLocalization را به صورت زیر معرفی میکنیم:
public static class LocalizationConfig { public static IMvcBuilder AddCustomLocalization(this IMvcBuilder mvcBuilder, IServiceCollection services) { mvcBuilder.AddDataAnnotationsLocalization(options => { const string resourcesPath = "Resources"; string baseName = $"{resourcesPath}.{nameof(SharedResource)}"; var location = new AssemblyName(typeof(SharedResource).GetTypeInfo().Assembly.FullName).Name; options.DataAnnotationLocalizerProvider = (type, factory) => { // to use `SharedResource.fa.resx` file return factory.Create(baseName, location); }; }); services.AddLocalization(); services.AddScoped<IStringLocalizer>(provider => provider.GetRequiredService<IStringLocalizer<SharedResource>>()); services.AddScoped<ISharedResourceService, SharedResourceService>(); return mvcBuilder; } }
- سپس با استفاده از متد AddLocalization، سرویسهای پایهی بومی سازی ASP.NET Core به برنامه اضافه میشوند. برای مثال پس از این تعریف اگر در هر جائی از برنامه سرویس <IStringLocalizer<SharedResource را تزریق کنید، میتوان به مداخل فایل منبع اشتراکی، دسترسی یافت.
- در ادامه امکان تزریق سرویس غیرجنریک IStringLocalizer را نیز میسر کردهایم که تعاریف خودش را از همان سرویس توکار <IStringLocalizer<SharedResource دریافت میکند. مزیت اینکار، فراهم شدن امکانات بومی سازی، برای مثال در کتابخانههایی مانند Fluent Validation است که دقیقا از سرویس غیرجنریک IStringLocalizer برای دریافت منابع استفاده میکنند.
- در آخر تعریف یک سرویس سفارشی را نیز مشاهده میکنید که در ادامهی بحث تکمیل خواهد شد.
هدف از متد AddCustomLocalization فوق، خلوت کردن فایل startup برنامه است. این متد به صورت زیر مورد استفاده قرار میگیرد:
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddHttpContextAccessor(); services.AddControllers().AddCustomLocalization(services); }
پس از آن نیاز است میانافزار بومی سازی را نیز فعال کرد. متد UseCustomRequestLocalization زیر، اینکار را انجام میدهد:
public static class LocalizationConfig { public static IApplicationBuilder UseCustomRequestLocalization(this IApplicationBuilder app) { var requestLocalizationOptions = new RequestLocalizationOptions { DefaultRequestCulture = new RequestCulture(new CultureInfo("fa-IR")), SupportedCultures = new[] { new CultureInfo("en-US"), new CultureInfo("fa-IR") }, SupportedUICultures = new[] { new CultureInfo("en-US"), new CultureInfo("fa-IR") } }; app.UseRequestLocalization(requestLocalizationOptions); return app; } }
public class Startup { public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseCustomRequestLocalization(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } }
تعریف مدل برنامه به همراه ویژگیهای بومی سازی شده
در اینجا تعریف RegisterModel را مشاهده میکنید که ErrorMessageهای آن هرچند به ظاهر یک رشتهی معمولی هستند، اما در عمل از فایل منبع اشتراکی خوانده میشوند:
using System.ComponentModel.DataAnnotations; namespace Core3xSharedResource.Models.Account { public class RegisterModel { [Required(ErrorMessage = "Please enter an email address")] // -->> from the shared resources [EmailAddress(ErrorMessage = "Please enter a valid email address")] // -->> from the shared resources public string Email { get; set; } } }
فایل resx ما دارای یک چنین کلیدهایی است:
<?xml version="1.0" encoding="utf-8"?> <root> <data name="<b>Hello</b><i> {0}</i>" xml:space="preserve"> <value><b>سلام</b><i> {0}</i></value> </data> <data name="About Title" xml:space="preserve"> <value>درباره</value> </data> <data name="DNT" xml:space="preserve"> <value>.NET Tips</value> </data> <data name="SiteName" xml:space="preserve"> <value>DNT</value> </data> <data name="Please enter an email address" xml:space="preserve"> <value>لطفا ایمیلی را وارد کنید</value> </data> <data name="Please enter a valid email address" xml:space="preserve"> <value>لطفا ایمیل معتبری را وارد کنید</value> </data> </root>
آزمایش برنامه
اکنون برنامهی Web API، برای آزمایش آمادهاست. برای مثال در کنترلر زیر، سرویس عمومی IStringLocalizer به سازندهی کلاس تزریق شدهاست و سپس قصد بازگشت مقدار کلید «About Title» را دارد. همچنین خطاهای بومی شدهی مدل برنامه را نیز بررسی میکنیم:
using Core3xSharedResource.Models.Account; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Localization; namespace Core3xSharedResource.WebApi.Controllers { [ApiController] [Route("[controller]")] public class NormalIStringLocalizerController : ControllerBase { private readonly IStringLocalizer _localizer; public NormalIStringLocalizerController(IStringLocalizer localizer) { _localizer = localizer; } [HttpGet] public ActionResult<string> Get() { var localizedString = _localizer["About Title"]; if (localizedString.ResourceNotFound) { return NotFound($"The localization resource with ID:`{localizedString.Name}` not found. SearchedLocation: `{localizedString.SearchedLocation}`."); } return localizedString.Value; } [HttpPost] public ActionResult<RegisterModel> Post(RegisterModel model) { return model; } } }
حالت get را در تصویر فوق مشاهده میکنید. در Web API برای تنظیم زبان مورد استفاده میتوان از هدری به نام Accept-Language استفاده کرد که برای مثال در اینجا به fa تنظیم شدهاست و نتیجهی آن مراجعه به فایل SharedResource.fa.resx خواهد بود. اگر en-us وارد شود، نیاز خواهد بود تا فایل منبع اشتراکی دیگری را تعریف کنید. البته اگر این هدر تنظیم نشود، با توجه به تنظیمات متد UseCustomRequestLocalization، مقدار پیشفرض آن همان fa-IR خواهد بود.
حالت post را نیز در تصویر زیر میتوان مشاهده کرد:
در اینجا چون ایمیل وارد نشده، هر دو خطای تنظیم شدهی در مدل برنامه را دریافت کردهایم و این خطاها نیز فارسی هستند. به این معنا که بومی سازی data annotations نیز به درستی کار میکند.
تعریف یک سرویس عمومی برای محصور سازی قابلیتهای بومی سازی، در برنامههای Web API
در ادامه تعریف سرویس SharedResourceService را مشاهده میکنید که ثبت آنرا پیشتر انجام دادیم:
using System; using System.Collections.Generic; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.AspNetCore.Http; namespace Core3xSharedResource.Services { public interface ISharedResourceService { string this[string index] { get; } IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures); string GetString(string name, params object[] arguments); string GetString(string name); } public class SharedResourceService : ISharedResourceService { private readonly IStringLocalizer _sharedLocalizer; private readonly ILogger<SharedResourceService> _logger; private readonly IHttpContextAccessor _httpContextAccessor; public SharedResourceService( IStringLocalizer sharedHtmlLocalizer, IHttpContextAccessor httpContextAccessor, ILogger<SharedResourceService> logger ) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _sharedLocalizer = sharedHtmlLocalizer ?? throw new ArgumentNullException(nameof(sharedHtmlLocalizer)); _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) { return _sharedLocalizer.GetAllStrings(includeParentCultures); } public string this[string index] => GetString(index); public string GetString(string name, params object[] arguments) { var result = _sharedLocalizer.GetString(name, arguments); logError(name, result); return result; } private void logError(string name, LocalizedString result) { if (result.ResourceNotFound) { var acceptLanguage = _httpContextAccessor?.HttpContext?.Request?.Headers["Accept-Language"]; _logger.LogError($"The localization resource with Accept-Language:`{acceptLanguage}` & ID:`{name}` not found. SearchedLocation: `{result.SearchedLocation}`."); } } public string GetString(string name) { var result = _sharedLocalizer.GetString(name); logError(name, result); return result; } } }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Core3xSharedResource.zip
مطالب پیشین مرتبط با لوسین را در اینجا میتوانید پیگیری کنید. آخرین نگارش آن که تا این تاریخ، 4.8 بتا است، با داتنت(Core) سازگار است و روش برپایی آغازین آن ... تغییرات قابل توجهی داشتهاست که خلاصهی آنها را در این مطلب بررسی خواهیم کرد.
1) بستههای جدید مورد نیاز
برای کار با لوسین جدید، نیاز است حداقل سهبستهی زیر را نصب کنیم تا به امکانات پایهای و کوئری گیریهای پیشرفتهی آن دسترسی داشته باشیم:
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00016"/> <PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00016"/> <PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00016"/>
2) تهیه نگاشتهای لازم
فرض کنید شیء اصلی ما چنین ساختاری را دارد:
public class WhatsNewItemModel { public required int Id { set; get; } public required string OriginalTitle { set; get; } }
مرحلهی بعد کار با لوسین، تبدیل اشیاء سفارشی خود به شیء Document لوسین و برعکس است. به همین جهت به دو مپر برای این کارها نیاز است:
الف) نگاشتگر یک شیء سفارشی، به شیء Document
public static class LuceneDocumentMapper { public static Document MapToLuceneDocument(this WhatsNewItemModel post) { ArgumentNullException.ThrowIfNull(post); return [ new TextField(nameof(WhatsNewItemModel.OriginalTitle), post.OriginalTitle, Field.Store.YES), // Document StringField instances are sort of keywords, they are not analyzed, they indexed as is (in its original case). new StringField(nameof(WhatsNewItemModel.Id), post.Id.ToString(CultureInfo.InvariantCulture), Field.Store.YES), ]; } }
در اینجا یک متدالحاقی را تهیه کردهایم تا شیءای از نوع WhatsNewItemModel ما را به یک شیء Document لوسین، تبدیل کند.
چند نکته در اینجا حائز اهمیت هستند:
- در نگارش جدید لوسین، با اشیاء TextField و StringField جدید سروکار داریم و شیء قدیمی Field نگارشهای قبلی لوسین، منسوخ شده درنظر گرفته میشود.
- زمانی از شیء TextField استفاده میکنیم که قرار است توسط لوسین، تحلیل شده و در جستجوهای پیچیده استفاده شود.
- اگر فقط قرار است، مقداری را در این ایندکس ذخیره کنیم و قصد تحلیل آنها را نداریم و حداکثر یک کوئری سادهی یافتن اصل آنها، مدنظر ما است، باید از اشیاء StringField برای معرفی و نگاشت آنها استفاده کنیم (شبیه به کار با واژههای کلیدی).
- پرچم Field.Store.YES به این معنا است که اصل محتوای تحلیل شده نیز در ایندکس لوسین، درج شود. اگر این پرچم را به NO تنظیم کنیم، فقط تحلیل آن صورت گرفته و نتیجهی آن ذخیره میشود، که برای جستجوها مفید است؛ اما مقدار این فیلد دیگر قابل بازیابی نخواهد بود.
ب) نگاشتگر یک شیء Document لوسین، به یک شیء سفارشی
در زمان کوئری گرفتن از لوسین، خروجی نهایی یک شیء Document آن است که باید به شیء سفارشی مدنظر ما نگاشت شود:
public static class LuceneDocumentMapper { public static LuceneSearchResult MapToLuceneSearchResult(this Document document) { ArgumentNullException.ThrowIfNull(document); return new LuceneSearchResult { Id = document.Get(nameof(WhatsNewItemModel.Id), CultureInfo.InvariantCulture).ToInt(), OriginalTitle = document.Get(nameof(WhatsNewItemModel.OriginalTitle), CultureInfo.InvariantCulture) }; } }
نمونهای از این نگاشت را در متد الحاقی فوق مشاهده میکنید که توسط متد Get شیء Document قابل انجام است. بدیهی است خروجی این متد، یک رشتهاست و در صورت نیاز باید توسط ما کار تبدیلات ثانویه آنها انجام شود.
3) نیاز به یک تحلیلگر مناسب
لوسین برای تولید ایندکسهای جستجوی تمام متنی خود، از یک سری Analyzer استفاده میکنید که اگر سری پیشین مطالب مرتبط را مطالعه کنید، به نمونهی StandardAnalyzer آن خواهید رسید که هنوز هم معتبر و قابل استفادهاست و یا میتوان همانند سایت جاری، از یک LowerCaseHtmlStripAnalyzer استفاده کرد که این کارها را همزمان انجام میدهد:
الف) از یک لیست PersianStopwords.List برای حذف واژههای کم اهمیت زبان فارسی استفاده میکند. برای مثال ما نمیخواهیم که واژهی «ما» را با اهمیت شمرده و ایندکس کند و امثال آن.
ب) LowerCaseFilter را به متون دریافتی اعمال میکند. این کار در پشت صحنهی StandardAnalyzer توکار لوسین هم اعمال میشود. اگر با این موضوع آشنا نباشید، ممکن است در حین کوئری گرفتن، به نتیجهای نرسید! چون متن ارسالی به لوسین را ابتدا باید lower-case کنید و سپس آنرا کوئری بگیرید.
ج) HTMLStripCharFilter توکار لوسین هم به آن اعمال شدهاست. از این جهت که متن مقالات ما به همراه تگهای HTML ای هم هستند. این فیلتر کار حذف کردن آنها را در حین تحلیل، انجام میدهد و دیگر نیازی نیست تا ما خودمان متن ارسالی به لوسین را تمیز کنیم.
نکتهی مهم: این تحلیلگر ویژه، فقط باید به فیلدهایی از نوع TextField اعمال شود. اگر آنرا به StringField ها اعمال کنیم، دیگر قادر به کوئری گرفتن از آنها نخواهیم بود! چون تحلیلگر StringFieldها باید از نوع توکار KeywordAnalyzer ثبت و معرفی شود. این نوع فیلدها، حالت واژههای کلیدی را دارند (به همان صورتی که هست ثبت میشوند) و قرارنیست که توسط لوسین تحلیل ویژهای شوند. به همین جهت برای رسیدن به یک تحلیلگر ترکیبی که بتواند این دو نوع فیلد را با هم پوشش دهد و کار معرفی چندین نوع تحلیلگر را یکجا انجام دهد، نیاز به یک PerFieldAnalyzerWrapper جدید داریم:
_keywordAnalyzer = new KeywordAnalyzer(); _lowerCaseHtmlStripAnalyzer = new LowerCaseHtmlStripAnalyzer(LuceneVersion); _analyzer = new PerFieldAnalyzerWrapper(_lowerCaseHtmlStripAnalyzer, new Dictionary<string, Analyzer> { { nameof(WhatsNewItemModel.Id), _keywordAnalyzer } });
PerFieldAnalyzerWrapper در حقیقت برای تمام فیلدهایی که در قسمت دیکشنری فوق، ذکر نشدهاند، از LowerCaseHtmlStripAnalyzer استفاده میکند. برای مابقی موارد از KeywordAnalyzer کمک خواهد گرفت.
4) روش صحیح راه اندازی reader و writer های ایندکس لوسین جدید
کار با لوسین به حدی سریع است که از کیفیت آن شگفت زده خواهید شد! اما ... بهشرطی که بدانید دقیقا به چه صورتی باید نویسنده و خوانندهی ایندکسهای آنرا مدیریت کنید. اکثر مثالهایی را که بر روی اینترنت پیدا میکنید، به همراه متدهایی هستند که مدام در حال گشودن و dispose این نویسندهها و خوانندههای ایندکس هستند که ... این مثالها، روش کار صحیح با لوسین نیستند! و به شدت آنرا کند میکنند.
نکتهی مهمی که این مثالها به آن توجهی نکردهاند، «thread-safe» بودن نویسنده و خوانندهی ایندکس لوسین است. یعنی میتوان یک نمونه از اینها را در ابتدای کار برنامه ایجاد کرد و تا آخر کار برنامه، بدون نیاز به نمونه سازی مجدد و باز و بسته کردن آنها، بارها مورد استفادهی مجدد قرار داد و هیچ تداخلی هم ندارند و از قسمتهای مختلف برنامه هم قابل دسترسی هستند.
به همین جهت باید یک سرویس مرکزی را برای اینکار تدارک دید که طول عمر آن، حتما Singleton باشد تا بتواند نویسنده و خوانندهی ایندکس لوسین را فقط یکبار نمونه سازی و ایجاد کرده و تا پایان کار برنامه، زنده نگه دارد (کدهای کامل این کلاس را در اینجا میتوانید مطالعه کنید):
public class FullTextSearchService : IFullTextSearchService { private const LuceneVersion LuceneVersion = Lucene.Net.Util.LuceneVersion.LUCENE_48; private readonly Analyzer _analyzer; private readonly IAppFoldersService _appFoldersService; private readonly FSDirectory _fsDirectory; // IndexWriter instances are completely thread safe, meaning multiple threads can call any of its methods, concurrently. private readonly IndexWriter _indexWriter; private readonly KeywordAnalyzer _keywordAnalyzer; private readonly ILogger<FullTextSearchService> _logger; private readonly LowerCaseHtmlStripAnalyzer _lowerCaseHtmlStripAnalyzer; // Safely shares IndexSearcher instances across multiple threads, while periodically reopening. private readonly SearcherManager _searcherManager; private bool _isDisposed; public FullTextSearchService(IAppFoldersService appFoldersService, ILogger<FullTextSearchService> logger) { _appFoldersService = appFoldersService ?? throw new ArgumentNullException(nameof(appFoldersService)); _logger = logger; _keywordAnalyzer = new KeywordAnalyzer(); _lowerCaseHtmlStripAnalyzer = new LowerCaseHtmlStripAnalyzer(LuceneVersion); _analyzer = new PerFieldAnalyzerWrapper(_lowerCaseHtmlStripAnalyzer, new Dictionary<string, Analyzer> { // Document StringField instances are sort of keywords, they are not analyzed, they indexed as is (in its original case). // But StandardAnalyzer applies lower case filter to a query. // We can fix this by using KeywordAnalyzer with our query parser. { nameof(WhatsNewItemModel.Id), _keywordAnalyzer }, { nameof(WhatsNewItemModel.DocumentTypeIdHash), _keywordAnalyzer }, { nameof(WhatsNewItemModel.DocumentContentHash), _keywordAnalyzer } }); _fsDirectory = FSDirectory.Open(_appFoldersService.LuceneIndexFolderPath); _indexWriter = new IndexWriter(_fsDirectory, new IndexWriterConfig(LuceneVersion, _analyzer)); _searcherManager = new SearcherManager(_indexWriter, applyAllDeletes: true, searcherFactory: null); }
این سرویس، یک سرویس Singleton است که نحوهی آغاز و شروع به کار با اشیاء لوسین را در سازندهی آن مشاهده میکنید.
توضیحات:
الف) در اینجا، روش نمونه سازی PerFieldAnalyzerWrapper را که پیشتر در مورد آن بحث شد، مشاهده میکنید.
ب) سپس یک IndexWriter، نمونه سازی میشود که از تحلیلگر ترکیبی ما استفاده میکند.
ج) در ادامه یک SearcherManager جدید را مشاهده میکنید که با IndexWriter برنامه هماهنگ است و هر زمانیکه سندی به لوسین اضافه میشود، قادر به کوئری گرفتن از آن هم خواهیم بود.
نکتهی مهم: طول عمر تمام این موارد، با طول عمر کلاس سرویس جاری، یکی است. یعنی تنها یکبار در طول عمر برنامه نمونه سازی شده و تا پایان کار آن، زنده نگه داشته میشوند.
5) روش افزودن یک سند به ایندکس لوسین و سپس به روز رسانی آن
اکنون با استفاده از نگاشتگرهایی که در ابتدای بحث تهیه کردیم و همچنین شیء IndexWriter فوق، به صورت زیر میتوان یک شیء سفارشی خود را به ایندکس لوسین اضافه کنیم:
_indexWriter.AddDocument(post.MapToLuceneDocument()); _indexWriter.Flush(triggerMerge: true, applyAllDeletes: true); _indexWriter.Commit();
و یا اگر خواستیم سند موجودی را به روز کنیم، روش کار به شکل زیر است:
_indexWriter.UpdateDocument(new Term(nameof(WhatsNewItemModel.Id), post.Id.ToString()), post.MapToLuceneDocument());
new Term، در حقیقت یک کوئری جدید را سبب میشود که توسط آن سندی یافت شده، در پشت صحنه حذف میشود و سپس سند جدیدی بجای آن درج خواهد شد. در اینجا باید دقت داشت که چون Id ثبت شده از نوع StringField است، نباید حالت lower-case آنرا جستجو کرد و باید دقیقا به همان نحوی که ثبت شده، جستجو شود.
6) روش کار با searcherManager جدید لوسین
همانطور که عنوان شد، لوسین جدید به همراه یک searcherManager هم هست که کار آن، ارائهی thread-safe دسترسی به خوانندهی ایندکس لوسین است. نحوهی عمومی کار با آن را در ادامه مشاهده میکنید:
private TResult DoSearch<TResult>(Func<IndexSearcher, TResult> action, TResult defaultValue) { _searcherManager.MaybeRefreshBlocking(); var indexSearcher = _searcherManager.Acquire(); try { return action(indexSearcher); } catch (FileNotFoundException) { // It's not indexed yet. return defaultValue; } finally { _searcherManager.Release(indexSearcher); } }
با استفاده از searcherManager، در طول مدت زمان کوتاهی، بر روی ایندکس قفلگذاری شده و یک indexSearcher امن، در اختیار متدهای استفاده کنندهی از آن قرار میگیرند و در پایان کار، این قفل رها میشود.
برای مثال یک نمونه روش استفاده از این indexSearcher امن، به صورت زیر است:
public int GetNumberOfDocuments() => DoSearch(indexSearcher => indexSearcher.IndexReader.NumDocs, defaultValue: 0);
مابقی مثالهای آنرا میتوانید در کلاس FullTextSearchService مشاهده کنید که به همراه یافتن «مطالب مشابه»، جستجوهای صفحه بندی شده، جستجوهای مرتب شدهی بر اساس یک فیلد، امکان دسترسی به تمام اسناد ذخیره شدهی در ایندکس لوسین و امثال آن است که کلیات آن با قبل تفاوتی نکردهاست و مطالب و نکات آنرا پیشتر در مقالات سری لوسین بررسی کردهایم. تنها تفاوت مهمی که در اینجا وجود دارد، نحوهی برپایی و راه اندازی تحلیلگر، خواننده و نویسندهی ایندکس آن است که در این مطلب بررسی شدند؛ وگرنه کلیات جستجوی پیشرفتهی آن، مانند قبل است و تفاوت خاصی نکردهاست.