دوره آموزشی Entity FrameWork Core - قسمت پانزدهم

دوره آموزشی Entity FrameWork Core - قسمت چهاردهممبدل های مقدار(Value Conversions) : مبدل های مقدار اجازه می‌دهند که مقادیر property هنگام خواندن یا نوشتن "از/به" پایگاه داده، تبدیل شوند.این تبدیل می تواند از یک مقدار به مقدار دیگر از همان نوع باشد (مثلاً رشته های رمزگذاری شده) یا از مقداری از یک نوع به مقداری از نوع دیگر (به عنوان مثال، تبدیل مقادیر enum "به/از" رشته ها در پایگاه داده).به عنوان مثال، برای ذخیره enum ها به عنوان string در پایگاه داده، نوع مدل نوع enum و نوع ارائه دهنده آن String است. این دو نوع می توانند یکسان باشند.به زبان ساده Value Conversions : هنگامی که با مدل‌سازی داده‌های EF Core سروکار داریم، ممکن است داده‌ها را به گونه‌ای متفاوت از نحوه مدیریت آن در پایگاه داده ذخیره کنیم.پیکر بندی value converter :آنها در DbContext.OnModelCreating پیکربندی می‌شوند. به عنوان مثال، یک نوع enum و موجودیت تعریف شده را در نظر بگیرید:public class Rider { public int Id { get; set; } public EquineBeast Mount { get; set; } } public enum EquineBeast { Donkey, Mule, Horse, Unicorn }تبدیل ها را می توان در OnModelCreating پیکربندی کرد تا مقادیر enum را به عنوان رشته هایی مانند "Donkey"، "Mule" و غیره در پایگاه داده ذخیره کنند. protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion( v => v.ToString(), v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v)); }توجه داشته باشید یک مقدار تهی هرگز به یک value converter منتقل نمی شود. یک null در یک ستون پایگاه داده همیشه یک null در نمونه موجودیت است و بالعکس. معمول است که یک value converter برای هر property که از نوع CLR مربوطه استفاده می کند پیکربندی شود. به جای اینکه این کار را به صورت دستی برای هر property انجام دهید، می توانید از پیکربندی مدل قبل از قرارداد استفاده کنید تا این کار را یک بار برای کل مدل خود انجام دهید. برای انجام این کار، value converter خود را به عنوان یک کلاس تعریف کنید:public class CurrencyConverter : ValueConverter<Currency, decimal> { public CurrencyConverter() : base( v => v.Amount, v => new Currency(v)) { } }سپس، ConfigureConventions را در Context Type خود override کنید و مبدل را به صورت زیر پیکربندی کنید:protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { configurationBuilder .Properties<Currency>() .HaveConversion<CurrencyConverter>(); }مبدل های مقدار از پیش تعریف شده(Pre-defined conversions) :خوب EF Core شامل بسیاری از value converterهای از پیش تعریف شده است که از نوشتن دستی توابع تبدیل اجتناب می کند. در عوض، EF Core بر اساس نوع property در مدل و نوع ارائه‌دهنده پایگاه داده درخواستی، تبدیل را برای استفاده انتخاب می‌کند.به عنوان مثال، تبدیل enum به string به عنوان مثال بالا استفاده می شود، اما EF Core در واقع این کار را به طور خودکار انجام می دهد زمانی که نوع ارائه دهنده به عنوان string با استفاده از نوع عمومی HasConversion پیکربندی شود:protected override void OnModelCreating(ModelBuilder modelBuilder){ modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion<string>(); }همین امر را می توان با مشخص کردن صریح نوع ستون پایگاه داده به دست آورد. برای مثال، اگر نوع موجودیت به این صورت تعریف شود:با استفاده از Data Annotations :public class Rider2 { public int Id { get; set; } [Column(TypeName = &quotnvarchar(24)&quot)] public EquineBeast Mount { get; set; } }با استفاده از Fluent API :modelBuilder .Entity<Rider2>() .Property(e => e.Mount) .HasColumnType(&quotnvarchar(24)&quot);سپس مقادیر enum به عنوان string در پایگاه داده بدون هیچ گونه پیکربندی بیشتر در OnModelCreating ذخیره می شوند. زمانی که که چندین property از یک converter استفاده می کنند راهکار زیر می تواند مفید باشد:protected override void OnModelCreating(ModelBuilder modelBuilder){ var converter = new ValueConverter<EquineBeast, string>( v => v.ToString(), v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v)); modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion(converter); }همانطور که در بالا ذکر شد، EF Core با مجموعه ای از value converterهای از پیش تعریف شده ارائه می شود. در بسیاری از موارد EF مبدل داخلی مناسب را بر اساس نوع property در مدل و نوع درخواست شده در پایگاه داده انتخاب می کند، همانطور که در بالا برای enumها نشان داده شده است. برای مثال، استفاده از:protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder .Entity<User>() .Property(e => e.IsActive) .HasConversion<int>(); }این از نظر عملکردی مشابه ایجاد نمونه زیر است:protected override void OnModelCreating(ModelBuilder modelBuilder) { var converter = new BoolToZeroOneConverter<int>(); modelBuilder .Entity<User>() .Property(e => e.IsActive) .HasConversion(converter); }برای مشاهده لیست value convertorهای از پیش تعریف شده اینجا را کلیک نمایید.برخی از انواع پایگاه داده دارای جنبه هایی هستند که نحوه ذخیره داده ها را تغییر می دهند. این شامل:دقت و مقیاس(Precision and scale) برای ستون های decimal و ستون های date/timeاندازه/طول ستون های binary و stringیونیکد برای ستون های stringاین جنبه‌ها را می‌توان پیکربندی کرد. به عنوان مثال، هنگام تبدیل از یک enum به string، می‌توانیم تعیین کنیم که ستون پایگاه داده غیریونیکد باشد و حداکثر 20 کاراکتر را ذخیره کند:protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion<string>() .HasMaxLength(20) .IsUnicode(false); }و یا :protected override void OnModelCreating(ModelBuilder modelBuilder){ var converter = new ValueConverter<EquineBeast, string>( v => v.ToString(), v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v)); modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion(converter) .HasMaxLength(20) .IsUnicode(false); }با این حال، اگر به طور پیش فرض تمام ستون های کلاس EquineBeast باید varchar(20) باشند، این اطلاعات را می توان به عنوان ConverterMappingHints به مبدل مقدار داد. مثلا:protected override void OnModelCreating(ModelBuilder modelBuilder) { var converter = new ValueConverter<EquineBeast, string>( v => v.ToString(), v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v), new ConverterMappingHints(size: 20, unicode: false)); modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion(converter); }اکنون هر زمانی که از این مبدل استفاده می شود، ستون پایگاه داده بدون یونیکد با حداکثر طول 20 خواهد بود. با این حال، اینها فقط hints هستند زیرا توسط هر جنبه ای که به صراحت در property نگاشت شده تنظیم شده است، override می شوند.مثال ها :اعمال value convertor برای یک value object ساده :بیشتر بخوانید : Entities, Value Objects, Aggregates and Rootspublic class Order { public int Id { get; set; } public Dollars Price { get; set; } }این مثال از یک نوع ساده(simple type) برای بسته بندی یک نوع اولیه(primitive type) استفاده می کند. این می تواند زمانی مفید باشد که بخواهید نوع مدل شما خاص تر (و در نتیجه از نظر type ایمن تر) نسبت به نوع اولیه باشد. در این مثال، آن نوع Dollars است که نوع اولیه decimal را درون خود بسته بندی می کند و به صورت یک entity type می توان از آن استفاده کرد:معروف ترین انواع داده های اولیه(primitive type) عبارتند از: int، object، short، char، float، double، char، bool. آنها اولیه نامیده می شوند زیرا انواع داخلی اصلی هستند و می توانند برای ساخت انواع داده های دیگر استفاده شوند.public readonly struct Dollars { public Dollars(decimal amount) => Amount = amount; public decimal Amount { get; } public override string ToString() => $&quot${Amount}&quot }و هنگامی که در پایگاه داده ذخیره می شود به decimal تبدیل می شود:modelBuilder.Entity<Order>() .Property(e => e.Price) .HasConversion( v => v.Amount, v => new Dollars(v));اعمال value convertor برای Composite value objects :در مثال قبلی، نوع value object فقط یک ویژگی داشت. معمولاً برای یک نوع value object، چندین ویژگی ترکیب می‌شود که با هم یک مفهوم دامنه(domain concept) را تشکیل می‌دهند. به عنوان مثال، یک نوع پول عمومی که شامل مقدار و واحد پول است:public readonly struct Money { [JsonConstructor] public Money(decimal amount, Currency currency) { Amount = amount; Currency = currency; } public override string ToString() => (Currency == Currency.UsDollars ? &quot$&quot : &quot£&quot) + Amount; public decimal Amount { get; } public Currency Currency { get; } } public enum Currency { UsDollars, PoundsSterling }این value object را می توان در یک نوع موجودیت مانند قبل استفاده کرد:public class Order { public int Id { get; set; } public Money Price { get; set; } }مبدل های مقدار(value convertor)در حال حاضر فقط می توانند مقادیر را "به و از" یک ستون پایگاه داده واحد تبدیل کنند. این محدودیت به این معنی است که تمام مقادیر ویژگی از شی باید در یک مقدار ستون واحد کدگذاری شوند. این معمولاً با سریال‌سازی(serializing) شیء در هنگام ورود به پایگاه داده و سپس جداسازی(deserializing) مجدد آن در هنگام خروج انجام می‌شود. به عنوان مثال، با استفاده از System.Text.Json:modelBuilder.Entity<Order>() .Property(e => e.Price) .HasConversion( v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null), v => JsonSerializer.Deserialize<Money>(v, (JsonSerializerOptions)null));اعمال value convertorها برای مجموعه ای از انواع اولیه :سریال سازی(serializing) همچنین می تواند برای ذخیره مجموعه ای از مقادیر اولیه استفاده شود. مثلا:public class Post { public int Id { get; set; } public string Title { get; set; } public string Contents { get; set; } public ICollection<string> Tags { get; set; } }با استفاده از System.Text.Json:modelBuilder.Entity<Post>() .Property(e => e.Tags) .HasConversion( v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null), v => JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions)null), new ValueComparer<ICollection<string>>( (c1, c2) => c1.SequenceEqual(c2), c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), c => (ICollection<string>)c.ToList()));اعمال value convertorها برای مجموعه ای از value objectها:با ترکیب دو مثال قبلی با هم می توانیم مجموعه ای از value objectها ایجاد کنیم. به عنوان مثال، یک نوع AnnualFinance را در نظر بگیرید که بودجه یک blog را برای یک سال مدل می کند:public readonly struct AnnualFinance { [JsonConstructor] public AnnualFinance(int year, Money income, Money expenses) { Year = year; Income = income; Expenses = expenses; } public int Year { get; } public Money Income { get; } public Money Expenses { get; } public Money Revenue => new Money(Income.Amount - Expenses.Amount, Income.Currency); }این نوع چندین نوع پول را که قبلا ایجاد کرده بودیم تشکیل می دهد:public readonly struct Money { [JsonConstructor] public Money(decimal amount, Currency currency) { Amount = amount; Currency = currency; } public override string ToString() => (Currency == Currency.UsDollars ? &quot$&quot : &quot£&quot) + Amount; public decimal Amount { get; } public Currency Currency { get; } } public enum Currency { UsDollars, PoundsSterling }سپس می‌توانیم مجموعه‌ای از AnnualFinance را به entity type خود اضافه کنیم:public class Blog{ public int Id { get; set; } public string Name { get; set; } public IList<AnnualFinance> Finances { get; set; } }و دوباره از سریال سازی برای ذخیره این استفاده کنید:modelBuilder.Entity<Blog>() .Property(e => e.Finances) .HasConversion( v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null), v => JsonSerializer.Deserialize<List<AnnualFinance>>(v, (JsonSerializerOptions)null), new ValueComparer<IList<AnnualFinance>>( (c1, c2) => c1.SequenceEqual(c2), c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), c => (IList<AnnualFinance>)c.ToList()));رمزنگاری مقادیر پراپرتی (Encrypt property values) :مبدل های مقدار را می توان برای رمزگذاری مقادیر پراپرتی قبل از ارسال آنها به پایگاه داده و سپس رمزگشایی آنها در هنگام خروج استفاده کرد. به عنوان مثال، استفاده از معکوس سازی رشته به عنوان جایگزینی برای یک الگوریتم رمزگذاری واقعی:modelBuilder.Entity<User>().Property(e => e.Password).HasConversion( v => new string(v.Reverse().ToArray()), v => new string(v.Reverse().ToArray()));البته این یک مثال ساده از رمزنگاری است و در یک برنامه واقعی پیشنهاد نمی شود اگر رمزگذاری خود را برای محافظت از داده های حساس اعمال می کنید، مطمئن شوید که همه مفاهیم را درک کرده اید. به جای آن از مکانیزم های رمزگذاری از پیش ساخته شده استفاده کنید، مانند Always Encrypted در SQL Server.ما می توانیم از value conertor ها برای دستیبابی به اهداف زیر در برنامه خود استفاده کنیم:بهینه سازی ذخیره سازی پایگاه دادهبهینه سازی عملکرد نوشتن بهینه سازی تأخیر خواندناما ببینیم که چگونه از یک value conertor برای بهینه‌سازی اطلاعات(که در غیر این صورت اطلاعات تکراری در پایگاه داده ما هستند) استفاده می‌کنیم.بیایید با یک مدل پایگاه داده فرضی Movie شروع کنیم که حاوی یک ویژگی رکورد StreamingService است. اگرچه ممکن است به نظر برسد که هر چند وقت یکبار ممکن است یک سرویس پخش جدید راه اندازی شود، اما می توانیم وانمود کنیم که این مجموعه داده محدود است.public class Movie { public int Id { get; set; } public string Name { get; set; } public StreamingService StreamingService { get; set; } } public record StreamingService(string Id, string Description) { public static StreamingService Netflix { get; } = new(&quotnetflix&quot, &quotNetflix streaming service&quot); public static StreamingService Hulu { get; } = new(&quothulu&quot, &quotHulu&quot); public static StreamingService HBOMax { get; } = new(&quothbo-max&quot, &quotHBO Max&quot); public static StreamingService DisneyPlus { get; } = new(&quotdisney-plus&quot, &quotDisney+&quot); public static StreamingService AppleTvPlus { get; } = new(&quotapple-tv-plus&quot, &quotApple TV+&quot); public static IReadOnlyList<StreamingService> All = new[] { Netflix, Hulu, HBOMax, DisneyPlus, AppleTvPlus }; }انتقال بیش از Id به پایگاه داده در شبکه و ذخیره سازی بیهوده خواهد بود. ما می توانیم بقیه اطلاعات را در نمونه های record سی شارپ خود ذخیره کنیم. ناگفته نماند که کپی کردن این موجودیت های فقط خواندنی به میزان استفاده از حافظه برنامه ما می افزاید.حالا ببینیم چگونه به EF Core بگوییم که record instances ما را به مقادیر string ترجمه کند که بتوانیم در پایگاه داده خود ذخیره کنیم.public class Database : DbContext { public DbSet<Movie> Movies { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder .EnableSensitiveDataLogging() .LogTo(Console.WriteLine) .UseSqlite(&quotData Source=movies.db&quot); base.OnConfiguring(optionsBuilder); } protected override void OnModelCreating(ModelBuilder modelBuilder) { var converter = new ValueConverter<StreamingService, string>( from => from.Id, to => StreamingService.All.FirstOrDefault(s => s.Id == to) ); modelBuilder .Entity<Movie>() .Property(m => m.StreamingService) .HasConversion(converter); base.OnModelCreating(modelBuilder); } }نوشتن پرس و جو با مبدل های مقدار(Writing Queries With Value Converters)مبدل مقدار ما از ما انتظار دارد که از نمونه های StreamingService در پرس و جو های LINQ خود استفاده کنیم.// ? wrong var results = await catalog .Movies .Where(m => m.StreamingService.Id == StreamingService.Netflix.Id) .ToListAsync(); // ? correct var results = await catalog .Movies .Where(m => m.StreamingService == StreamingService.Netflix) .ToListAsync();لازم به یادآوری است که EF Core فقط در مورد top-level entity ما می داند. نمی داند که StreamingService دارای یک ویژگی شناسه یا توضیحات است. تلاش برای استفاده از هر یک از ویژگی های StreamingService در یک پرس و جو به یک استثنا ختم می شود.using System; using System.Linq; using EFCoreValueConverters; using Microsoft.EntityFrameworkCore; // save new movie await using var catalog = new Database(); catalog.Movies.Add(new Movie { Name = &quotBirdbox&quot, StreamingService = StreamingService.Netflix }); await catalog.SaveChangesAsync(); // retrieve await using var browser = new Database(); var results = await catalog .Movies .Where(m => m.StreamingService == StreamingService.Netflix) .ToListAsync(); foreach (var result in results) { Console.WriteLine($&quot{result.Name} now streaming on {result.StreamingService.Description}!&quot); }ابتدا، بیایید ببینیم چگونه movie خود را ذخیره کردیم:Executing DbCommand [Parameters=[@p0='Birdbox' (Size = 7), @p1='netflix' (Size = 7)], CommandType='Text', CommandTimeout='30']INSERT INTO "Movies" ("Name", "StreamingService")VALUES (@p0, @p1);SELECT "Id"FROM "Movies"WHERE changes() = 1 AND "rowid" = last_insert_rowid();می بینیم که netflix به عنوان یک پارامتر استفاده شده است، به این معنی که EF Core رکورد StreamingService ما را به درستی نگاشت کرده است. بعد، بیایید پرس و جوی خوانده شده خود را ببینیم.Executed DbCommand (1ms) [Parameters=[@__Netflix_0='netflix' (Size = 7)], CommandType='Text', CommandTimeout='30']SELECT "m"."Id", "m"."Name", "m"."StreamingService"FROM "Movies" AS "m"WHERE "m"."StreamingService" = @__Netflix_0نتیجه نهایی :Birdbox now streaming on Netflix streaming service!مبدل‌های مقدار به ما امکان می‌دهند انواع نسبتاً پیچیده‌ای را که ممکن است توسط موتورهای پایگاه داده پشتیبانی نشود را نقشه‌برداری کنیم. در مثال بالا، ما توانستیم اطلاعات مهم کاربر را در application layer خود ذخیره کنیم و در عین حال حداقل مقدار لازم برای بازسازی مدل های خود را ذخیره کنیم. استفاده مجدد از read-only recordها همچنین باعث صرفه جویی در حافظه ما در موقعیت های حیاتی می شود. بیشتر بخوانید : دوره آموزشی Entity FrameWork Core - قسمت شانزدهمبیشتر بخوانید : دوره آموزشی Entity FrameWork Coreبیشتر بخوانید : نقشه راه توسعه دهندگان Asp.NET Corehttps://zarinp.al/farshidazizi