طراحی پایگاه داده چند زبانه
EF Code First #15
برای نمونه پروایدر رسمی Oracle ODP.Net با EF Code first کار میکند و یا یک نمونه دیگر در اینجا (^)
در قسمت 14 سری EF code first این سایت به این مطلب پرداخته شده.
چک لیست مسائل امنیتی در mvc در محیط های کاری و واقعی
- مطالعهی مطالب گروه AutoMapper در سایت، دید خوبی را برای شروع به کار با آن فراهم میکنند و در اینجا قصد تکرار این مباحث پایهای را نخواهیم داشت. هدف بیشتر بررسی یک سری نکات پیشرفتهتر و عمیقتر است از کار با AutoMapper.
- آشنایی با Lazy loading و Eager loading در حین کار با EF
ساختار و پیشنیازهای برنامهی مطلب جاری
جهت سهولت پیگیری مطلب و تمرکز بیشتر بر روی مفاهیم اصلی مورد بحث، یک برنامهی کنسول را آغاز کرده و سپس بستههای نیوگت ذیل را به آن اضافه کنید:
PM> install-package AutoMapper PM> install-package EntityFramework
آشنایی با ساختار مدلهای برنامه
در اینجا ساختار جداول مطالب یک بلاگ را به همراه نویسندگان آنها، مشاهده میکنید:
public class BlogPost { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } [ForeignKey("UserId")] public virtual User User { get; set; } public int UserId { get; set; } } public class User { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } public virtual ICollection<BlogPost> BlogPosts { get; set; } }
هدف از این مثال
فرض کنید اطلاعاتی که قرار است به کاربر نمایش داده شوند، توسط ViewModel ذیل تهیه میشود:
public class UserViewModel { public int Id { set; get; } public string Name { set; get; } public ICollection<BlogPost> BlogPosts { get; set; } }
تهیه نگاشتهای AutoMapper
برای مدیریت بهتر نگاشتهای AutoMapper توصیه شدهاست که کلاسهای Profile ایی را به شکل ذیل تهیه کنیم:
public class TestProfile : Profile { protected override void Configure() { this.CreateMap<User, UserViewModel>(); } public override string ProfileName { get { return this.GetType().Name; } } }
ثبت و معرفی پروفایلهای AutoMapper
پس از تهیهی پروفایل مورد نیاز، در ابتدای برنامه با استفاده از متد Mapper.Initialize، کار ثبت این تنظیمات صورت خواهد گرفت:
Mapper.Initialize(cfg => // In Application_Start() { cfg.AddProfile<TestProfile>(); });
روش متداول کار با AutoMapper جهت نگاشت اطلاعات User به ViewModel آن
در ادامه به نحو متداولی، ابتدا اولین کاربر ثبت شده را یافته و سپس با استفاده از متد Mapper.Map اطلاعات این شیء user به ViewModel آن نگاشت میشود:
using (var context = new MyContext()) { var user1 = context.Users.FirstOrDefault(); if (user1 != null) { var uiUser = new UserViewModel(); Mapper.Map(source: user1, destination: uiUser); Console.WriteLine(uiUser.Name); foreach (var post in uiUser.BlogPosts) { Console.WriteLine(post.Title); } } }
الف) یافتن اولین کاربر
ب) واکشی لیست مطالب او در یک کوئری دیگر
کاهش تعداد رفت و برگشتها به سرور با استفاده از متدهای ویژهی AutoMapper
در حالت متداول کار با EF، با استفاده از متد Include میتوان این Lazy loading را لغو کرد و در همان اولین کوئری، مطالب کاربر یافت شده را نیز دریافت نمود:
var user1 = context.Users.Include(user => user.BlogPosts).FirstOrDefault();
در این حالت، AutoMapper برای ساده سازی این مراحل، متدهای Project To را معرفی کردهاست:
var uiUser = context.Users.Project().To<UserViewModel>().FirstOrDefault();
بنابراین با استفاده از متدهای Project To میتوان از ذکر Includeهای EF صرفنظر کرد و همچنین دیگر نیازی به نوشتن متد Select جهت نگاشت دستی خواص مورد نظر به خواص ViewModel نیست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
AM_Sample01.zip
البته بهتره که هر زمان در عملیات Bulk Insert تعداد رکوردهای مورد نظر خیلی زیاد بود به ازای یک تعداد مشخص از Entity ها(برای مثال 1000) یک بار DbContext رو SaveChanged کرده و اونو Dispose کنید و دوباره یک Instance جدید از DbContext بسازید و ادامه کار(دلیل دوباره ساختن DbContext هم اینه که DbContext ، بعد از دستور SaveChanged دیتای مورد نظر رو در دیتابیس ذخیره میکنهه ولی فقط State هر Entity رو به Unchaged تغییر میده و خود Entity رو Detach نمیکنه که این خود باعث افزایش ObjectGraph موجود در DbContext میشود و در نتیجه کاهش کارایی).
در ضمن میتونید با فراخوانی دستور DetectChanged مستقیما DbContext رو مجبور به بررسی وضعیت خواص CurrentValue و OriginalValue هر Entity بکنید.
در ورژنهای قبلی ویژوال استودیو، در زمان بارگذاری پروژه، احتیاجی به اجرای نرم افزارهای تحریم گذر نبود؛ همانند ورژن 15.6. ولی در این ورژن که من نصب کردم بدلیل نصب خودکار کتابخانههای متریال دیزاین، باید از این گونه نرم افزارها نیز استفاده کرد.
درقسمت بعدی گزینه BlankApp را انتخاب و در قسمت Minimum Android Version که با انتخاب آن میتوانیم ورژن گوشیهای اندروید برای استفاده از این اپلیکیشن را انتخاب نماییم. به عنوان مثال با انتخاب اندروید 4.4 برنامه ما صرفا برای گوشیهای اندورید 4.4 به بالا جواب میدهد. بعد از تایید، پروژه باز شده که با این solution روبرو میشویم.
- قسمت Properties را اگر بازکنیم، با دو گزینه روبرو میشویم که یکی فایل android manifest هست و اگر روی properties آن کلیک و ویژگیهایی را انتخاب کنیم، بطور خودکار بر روی manifest تاثیر میگذارند. در قسمتهای بعد در این رابطه جداگانه بحث خواهیم کرد.
- در قسمت Asset که به معنای منابع اندروید میباشد، به عنوان مثال صفحات Razor، فونت و یا صفحات HTML و یا عکس و یا ... را میتوانیم قرار دهیم.
- در قسمت Resource که پوشههای آن layout ،mipmap ،values و resource.designer میباشند، در پوشه layout میتوانیم صفحات استاندارد اندروید را شروع به طراحی کنیم. درقسمت mipmap عکسها و یا فایلهای xml ایی را که قرار است استفاده کنیم، در پروژه قرار میدهیم. در قسمت value که بیشتر برای انتخاب و تغییر تم یا استفاده از Resourceها (همانند Asp.mvc که استفاده میکردیم) است که البته با ساختاری متفاوت در اندروید از آنها استفاده میکنیم، قرار میگیرند.
- در قسمت Resource.Designer که در مطالب بعد با آن آشنا خواهید شد، تمامی آیتمهای انتخابی از جمله Layout ها و عکسها و... با ذخیره کردن در این قسمت دخیره میشوند که بعد با رفرنس دادن از طریق resource پروژه میتوانیم از عکسها و لیآوتها در کد نویسی استفاده کنیم.
- در انتها جهت معرفی به mainactivity میرسیم که یک صفحه است شامل المنتها و اجزای مختلف و کاربر میتواند با آن ارتباط برقرار کند.
[Activity(Label = "@string/app_name", Theme = "@style/AppTheme", MainLauncher = true)] public class MainActivity : AppCompatActivity { protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); // Set our view from the "main" layout resource SetContentView(Resource.Layout.activity_main); } }
- در گزینه بعدی Mainluncher را میبینیم که تعیین کنندهی نقطه شروع اکتیویتی ما در بین اکتیویتیهای دیگر میباشد.
بدیهی است درایورهای مربوطه به گوشی اندروید را باید تهیه کرد که در سایت مربوط به سازنده و یا در سایتهای دیگر میتوانید دانلود کنید. اولین برنامه را مینویسیم که هدف از آن، اجرای 10 دکمه بصورت داینامیک هست و اینکه با کلیک بر روی هر کدام از دکمهها، رنگ آن آبی شود.
protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); LinearLayout ln; Button btn; // Set our view from the "main" layout resource SetContentView(Resource.Layout.activity_main); for (int i = 0; i < 5; i++) { btn = new Button(this); btn.Text = i.ToString(); ln= FindViewById<LinearLayout>(Resource.Id.linearLayout1); btn.Click += Btn_Click; ln.AddView(btn); } } private void Btn_Click(object sender, System.EventArgs e) { Button btntest = sender as Button; btntest.SetBackgroundColor(Android.Graphics.Color.Blue); } }
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:minWidth="25px" android:minHeight="25px"> <LinearLayout android:orientation="vertical" android:minWidth="25px" android:minHeight="25px" android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/linearLayout1" /> </RelativeLayout>
public class BaseEntity : IBaseEntity { [JsonIgnore] int Id { get; set; } [JsonIgnore] string? Audit { get; set; } }
public class AuditSourceValues { [JsonProperty("hn")] public string? HostName { get; set; } [JsonProperty("mn")] public string? MachineName { get; set; } [JsonProperty("rip")] public string? RemoteIpAddress { get; set; } [JsonProperty("lip")] public string? LocalIpAddress { get; set; } [JsonProperty("ua")] public string? UserAgent { get; set; } [JsonProperty("an")] public string? ApplicationName { get; set; } [JsonProperty("av")] public string? ApplicationVersion { get; set; } [JsonProperty("cn")] public string? ClientName { get; set; } [JsonProperty("cv")] public string? ClientVersion { get; set; } [JsonProperty("o")] public string? Other { get; set; } }
public class EntityAudit<TEntity> { [JsonProperty("type")] [JsonConverter(typeof(StringEnumConverter))] public EntityEventType EventType { get; set; } [JsonProperty("user", NullValueHandling = NullValueHandling.Include)] public int? ActorUserId { get; set; } [JsonProperty("at")] public DateTime ActDateTime { get; set; } [JsonProperty("sources")] public AuditSourceValues? AuditSourceValues { get; set; } [JsonProperty("newValues", NullValueHandling = NullValueHandling.Include)] public TEntity NewEntity { get; set; } = default!; public string? SerializeJson() { return JsonSerializer.Serialize(this, options: new JsonSerializerOptions { WriteIndented = false, IgnoreNullValues = true }); } }
دقت کنید که این کلاس به صورت جنریک تعریف شده است تا اگر بعدا بخواهیم آن را Deserialize کنیم و مثلا از آن API بسازیم، یا استفادهی خاصی را از آن داشته باشیم، بهراحتی به Entity مد نظر تبدیل شود. در این مقاله فقط به ذخیرهی آن پرداخته میشود و استفاده از این فیلد که به راحتی و با کمک DbFunctionها در Entity Framework قابل انجام است به خواننده واگذار میشود.
public enum EntityEventType { Create = 0, Update = 1, Delete = 2 }
public interface IAuditSourcesProvider { AuditSourceValues GetAuditSourceValues(); }
public class AuditSourcesProvider : IAuditSourcesProvider { protected readonly IHttpContextAccessor HttpContextAccessor; public AuditSourcesProvider(IHttpContextAccessor httpContextAccessor) { HttpContextAccessor = httpContextAccessor; } public virtual AuditSourceValues GetAuditSourceValues() { var httpContext = HttpContextAccessor.HttpContext; return new AuditSourceValues { HostName = GetHostName(httpContext), MachineName = GetComputerName(httpContext), LocalIpAddress = GetLocalIpAddress(httpContext), RemoteIpAddress = GetRemoteIpAddress(httpContext), UserAgent = GetUserAgent(httpContext), ApplicationName = GetApplicationName(httpContext), ClientName = GetClientName(httpContext), ClientVersion = GetClientVersion(httpContext), ApplicationVersion = GetApplicationVersion(httpContext), Other = GetOther(httpContext) }; } protected virtual string? GetUserAgent(HttpContext httpContext) { return httpContext.Request?.Headers["User-Agent"].ToString(); } protected virtual string? GetRemoteIpAddress(HttpContext httpContext) { return httpContext.Connection?.RemoteIpAddress?.ToString(); } protected virtual string? GetLocalIpAddress(HttpContext httpContext) { return httpContext.Connection?.LocalIpAddress?.ToString(); } protected virtual string GetHostName(HttpContext httpContext) { return httpContext.Request.Host.ToString(); } protected virtual string GetComputerName(HttpContext httpContext) { return Environment.MachineName; } protected virtual string? GetApplicationName(HttpContext httpContext) { return Assembly.GetEntryAssembly()?.GetName().Name; } protected virtual string? GetApplicationVersion(HttpContext httpContext) { return Assembly.GetEntryAssembly()?.GetName().Version.ToString(); } protected virtual string? GetClientVersion(HttpContext httpContext) { return httpContext.Request?.Headers["client-version"]; } protected virtual string? GetClientName(HttpContext httpContext) { return httpContext.Request?.Headers["client-name"]; } protected virtual string? GetOther(HttpContext httpContext) { return null; } }
حالا برای تامین اطلاعات کلاس EntityAudit کار مشابهی میکنیم. ابتدا اینترفیس IEntityAuditProvider را به صورت زیر تعریف میکنیم:
public interface IEntityAuditProvider { string? GetAuditValues(EntityEventType eventType, object? entity, string? previousJsonAudit = null); }
و سپس کلاس EntityAuditProvider را ایجاد میکنیم:
public class EntityAuditProvider : IEntityAuditProvider { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IAuditSourcesProvider _auditSourcesProvider; #region Constructor Injections public EntityAuditProvider(IHttpContextAccessor httpContextAccessor, IAuditSourcesProvider auditSourcesProvider) { _httpContextAccessor = httpContextAccessor; _auditSourcesProvider = auditSourcesProvider; } #endregion public virtual string? GetAuditValues(EntityEventType eventType, object? newEntity, string? previousJsonAudit = null) { var httpContext = _httpContextAccessor.HttpContext; int? userId; var user = httpContext.User; if (!user.Identity.IsAuthenticated) userId = null; else userId = user.Claims.Where(x => x.Type == "UserID").Select(x => x.Value).First().ToInt(); var auditSourceValues = _auditSourcesProvider.GetAuditSourceValues(); var auditJArray = new JArray(); // Update & Delete if (eventType == EntityEventType.Update || eventType == EntityEventType.Delete) { auditJArray = JArray.Parse(previousJsonAudit!); } // Delete => No NewValues if (eventType == EntityEventType.Delete) { newEntity = null; } JObject newAuditJObject = JObject.FromObject(new EntityAudit<object?> { EventType = eventType, ActorUserId = userId, ActDateTime = DateTime.Now, AuditSourceValues = auditSourceValues, NewEntity = newEntity }, new JsonSerializer { NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.None }); auditJArray.Add(newAuditJObject); return auditJArray.SerializeToJson(true); } }
public class AuditSaveChangesInterceptor : SaveChangesInterceptor { private readonly IEntityAuditProvider _entityAuditProvider; #region Constructor Injections public AuditSaveChangesInterceptor(IEntityAuditProvider entityAuditProvider) { _entityAuditProvider = entityAuditProvider; } #endregion public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result) { ApplyAudits(eventData.Context.ChangeTracker); return base.SavingChanges(eventData, result); } public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = new CancellationToken()) { ApplyAudits(eventData.Context.ChangeTracker); return base.SavingChangesAsync(eventData, result, cancellationToken); } private void ApplyAudits(ChangeTracker changeTracker) { ApplyCreateAudits(changeTracker); ApplyUpdateAudits(changeTracker); ApplyDeleteAudits(changeTracker); } private void ApplyCreateAudits(ChangeTracker changeTracker) { var addedEntries = changeTracker.Entries() .Where(x => x.State == EntityState.Added); foreach (var addedEntry in addedEntries) { if (addedEntry.Entity is IBaseEntity entity) { entity.Audit = _entityAuditProvider.GetAuditValues(EntityEventType.Create, entity); } } } private void ApplyUpdateAudits(ChangeTracker changeTracker) { var modifiedEntries = changeTracker.Entries() .Where(x => x.State == EntityState.Modified); foreach (var modifiedEntry in modifiedEntries) { if (modifiedEntry.Entity is IBaseEntity entity) { var eventType = entity.IsArchived ? EntityEventType.Delete : EntityEventType.Update; // Maybe Soft Delete entity.Audit = _entityAuditProvider.GetAuditValues(eventType, entity, entity.Audit); } } } private void ApplyDeleteAudits(ChangeTracker changeTracker) { var deletedEntries = changeTracker.Entries() .Where(x => x.State == EntityState.Deleted); foreach (var modifiedEntry in deletedEntries) { if (modifiedEntry.Entity is IBaseEntity entity) { entity.Audit = _entityAuditProvider.GetAuditValues(EntityEventType.Delete, entity, entity.Audit); } } } }
و سپس آن را به سیستم معرفی میکنیم:
services.AddDbContext<ATADbContext>((serviceProvider, options) => { options .UseSqlServer(...) // Interceptors var entityAuditProvider = serviceProvider.GetRequiredService<IEntityAuditProvider>(); options.AddInterceptors(new AuditSaveChangesInterceptor(entityAuditProvider)); });
نمونهی کامل فیلد Audit که در JsonFormatter قرار داده شده است، بعد از ایجاد شدن و یکبار آپدیت و سپس حذف نرم رکورد:
[ { "type":"Create", "user":1, "at":"2020-11-24T23:05:54.2692711+03:30", "sources":{ "hn":"localhost:44398", "mn":"DESKTOP-N1GAV2U", "rip":"::1", "lip":"::1", "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36", "an":"Server.Api", "av":"1.0.0.0" }, "newValues":{ "Name":"Farshad" } }, { "type":"Update", "user":1, "at":"2020-11-24T23:06:20.0838188+03:30", "sources":{ "hn":"localhost:44398", "mn":"DESKTOP-N1GAV2U", "rip":"::1", "lip":"::1", "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36", "an":"Server.Api", "av":"1.0.0.0" }, "newValues":{ "Name":"Edited Farshad" } }, { "type":"Delete", "user":null, "at":"2020-11-24T23:06:28.601837+03:30", "sources":{ "hn":"localhost:44398", "mn":"DESKTOP-N1GAV2U", "rip":"::1", "lip":"::1", "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36", "an":"Server.Api", "av":"1.0.0.0" }, "newValues":null } ]
ولی روش گفته شده در این مقاله، همین عملیات را به صورت کاملتری و فقط بر روی یک ستون همان جدول انجام میدهد که باعث ذخیرهی دیتای کمتر، یکپارچگی بهتر و دسترسیپذیری و راحتی استفاده از آن میشود.