WithRequiredDependent مربوط به EF 6.x است و از EF Core حذف شدهاست (در اینجا HasOne و WithOne مطابق مقالهی فوق ارائه شدهاند که واضحتر هستند). نسخهی سازگار با EF 6.x آن در اینجا پیشتر مطرح شدهاست.
پاسخ به پرسشها
چطور باید برای سایت dntips.ir پیشنهاد و نظر ارسال کرد؟
ممنون از پاسختان
خواستم نظری در خصوص tagها بدم. اگر امکان داره Unique باشند. نمونه های زیر در بخش Tag توسط منوی باز شونده پیشنهاد شده:
- Codefirst, EF Code First, EF CodeFirst
- efcore, Ef core
تشکر
اشتراکها
NET Core 3.0 Preview 7. منتشر شد
اشتراکها
Rider 2019.2.3 منتشر شد
اشتراکها
مسیر راه Blazor Hybrid در دات نت 7
اشتراکها
فرآیند رندر شدن در Angular2
So how exactly is Angular2 built such that it allows for custom renderers to be created? The first thing to understand is that the internal bits of Angular2 are split into two areas: the worker (core) area and the UI area.
The worker (core) area is responsible for building out the components, directives, filters, services and bootstrap code; The UI area is responible for rendering out the application in the DOM.
یکی از مزایای کار با ORMها، امکان تعویض نوع بانک اطلاعاتی برنامه، بدون نیازی به تغییری در کدهای برنامه است. برای مثال فرض کنید میخواهید با تغییر رشتهی اتصالی برنامه، یکبار از بانک اطلاعاتی SQL Server و بار دیگر از بانک اطلاعاتی کاملا متفاوتی مانند SQLite استفاده کنید. در این مطلب نکات استفادهی از چندین نوع بانک اطلاعاتی متفاوت را در برنامههای مبتنی بر EF Core بررسی خواهیم کرد.
هر بانک اطلاعاتی باید Migration و Context خاص خودش را داشته باشد
تامین کنندهی بانکهای اطلاعاتی مختلف، عموما تنظیمات خاص خودشان را داشته و همچنین دستورات SQL متفاوتی را نیز تولید میکنند. به همین جهت نمیتوان از یک تک Context، هم برای SQLite و هم SQL Server استفاده کرد. به علاوه قصد داریم اطلاعات Migrations هر کدام را نیز در یک اسمبلی جداگانه قرار دهیم. در یک چنین حالتی EF نمیپذیرد که Context تولید کنندهی Migration، در اسمبلی دیگری قرار داشته باشد و باید حتما در همان اسمبلی Migration قرار گیرد. بنابراین ساختار پوشه بندی مثال جاری به صورت زیر خواهد بود:
- در پوشهی EFCoreMultipleDb.DataLayer فقط اینترفیس IUnitOfWork را قرار میدهیم. از این جهت که وقتی قرار شد در برنامه چندین Context تعریف شوند، لایهی سرویس برنامه قرار نیست بداند در حال حاضر با کدام Context کار میکند. به همین جهت است که تغییر بانک اطلاعاتی برنامه، تغییری را در کدهای اصلی آن ایجاد نخواهد کرد.
- در پوشهی EFCoreMultipleDb.DataLayer.SQLite کدهای Context و همچنین IDesignTimeDbContextFactory مخصوص SQLite را قرار میدهیم.
- در پوشهی EFCoreMultipleDb.DataLayer.SQLServer کدهای Context و همچنین IDesignTimeDbContextFactory مخصوص SQL Server را قرار میدهیم.
برای نمونه ابتدای Context مخصوص SQLite چنین شکلی را دارد:
و IDesignTimeDbContextFactory مخصوص آن که برای Migrations از آن استفاده میشود، به صورت زیر تهیه خواهد شد:
هدف از این فایل، ساده سازی کار تولید اطلاعات Migrations برای EF Core است. به این صورت ساخت new SQLiteDbContext توسط ما صورت خواهد گرفت و دیگر EF Core درگیر جزئیات وهله سازی آن نمیشود.
تنظیمات رشتههای اتصالی بانکهای اطلاعاتی مختلف
در اینجا محتویات فایل appsettings.json را که در آن تنظیمات رشتههای اتصالی دو بانک SQL Server LocalDB و همچنین SQLite در آن ذکر شدهاند، مشاهده میکنید:
همین رشتهی اتصالی است که در SQLiteDbContextFactory مورد استفاده قرار میگیرد.
یک کلید InUseKey را هم در اینجا تعریف کردهایم تا مشخص باشد در ابتدای کار برنامه، کلید کدام رشتهی اتصالی مورد استفاده قرار گیرد. برای مثال در اینجا کلید رشتهی اتصالی SQLite تنظیم شدهاست.
در این تنظیمات یک DataDirectory را نیز مشاهده میکنید. مقدار آن در فایل Startup.cs برنامه به صورت زیر بر اساس پوشهی جاری تعیین میشود و در نهایت به wwwroot\app_data اشاره خواهد کرد:
دستورات تولید Migrations و به روز رسانی بانک اطلاعاتی
چون تعداد Contextهای برنامه بیش از یک مورد شدهاست، دستورات متداولی را که تاکنون برای تولید Migrations و یا به روز رسانی ساختار بانک اطلاعاتی اجرا میکردید، با پیام خطایی که این مساله را گوشزد میکند، متوقف خواهند شد. راه حل آن ذکر صریح Context مدنظر است:
برای تولید Migrations، از طریق خط فرمان، به پوشهی اسمبلی مدنظر وارد شده و دستور زیر را اجرا کنید:
در اینجا ذکر startup-project و همچنین context برای پروژههایی که context آنها خارج از startup-project است و همچنین بیش از یک context دارند، ضروریاست. بدیهی است این دستورات را باید یکبار در پوشهی EFCoreMultipleDb.DataLayer.SQLite و یکبار در پوشهی EFCoreMultipleDb.DataLayer.SQLServer اجرا کنید.
دو سطر اول آن، زمان اجرای دستورات را به عنوان نام فایلها تولید میکنند.
پس از تولید Migrations، اکنون نوبت به تولید بانک اطلاعاتی و یا به روز رسانی بانک اطلاعاتی موجود است:
در این مورد نیز ذکر startup-project و همچنین context مدنظر ضروری است.
بدیهی است این رویه را پس از هربار تغییراتی در موجودیتهای برنامه و یا تنظیمات آنها در Contextهای متناظر، نیاز است مجددا اجرا کنید. البته اجرای اولین دستور اجباری است؛ اما میتوان دومین دستور را به صورت زیر نیز اجرا کرد:
متد applyPendingMigrations، کار وهله سازی IUnitOfWork را انجام میدهد. سپس متد Migrate آنرا اجرا میکند، تا تمام Migrations تولید شده، اما اعمال نشدهی به بانک اطلاعاتی، به صورت خودکار به آن اعمال شوند. متد Migrate نیز به صورت زیر تعریف میشود:
مرحلهی آخر: انتخاب بانک اطلاعاتی در برنامهی آغازین
پس از این تنظیمات، قسمتی که کار تعریف IUnitOfWork و همچنین DbContext جاری برنامه را انجام میدهد، به صورت زیر پیاده سازی میشود:
در اینجا ابتدا مقدار InUseKey از فایل تنظیمات برنامه دریافت میشود. بر اساس مقدار آن، رشتهی اتصالی مدنظر دریافت شده و سپس یکی از دو حالت SQLite و یا SQLServer انتخاب میشوند. برای مثال اگر Sqlite انتخاب شده باشد، IUnitOfWork به SQLiteDbContext تنظیم میشود. به این ترتیب لایهی سرویس برنامه که با IUnitOfWork کار میکند، به صورت خودکار وهلهای از SQLiteDbContext را دریافت خواهد کرد.
آزمایش برنامه
ابتدا کدهای کامل این مطلب را از اینجا دریافت کنید: EFCoreMultipleDb.zip
سپس آنرا اجرا نمائید. چنین تصویری را مشاهده خواهید کرد:
اکنون برنامه را بسته و سپس فایل appsettings.json را جهت تغییر مقدار InUseKey به کلید SqlServerConnection ویرایش کنید:
اینبار اگر مجددا برنامه را اجرا کنید، چنین خروجی قابل مشاهدهاست:
مقدار username، در contextهای هر کدام از این بانکهای اطلاعاتی، با مقدار متفاوتی به عنوان اطلاعات اولیهی آن ثبت شدهاست. سرویسی هم که اطلاعات آنرا تامین میکند، به صورت زیر تعریف شدهاست:
همانطور که مشاهده میکنید، با تغییر context برنامه، هیچ نیازی به تغییر کدهای UsersService نیست؛ چون اساسا این سرویس نمیداند که IUnitOfWork چگونه تامین میشود.
هر بانک اطلاعاتی باید Migration و Context خاص خودش را داشته باشد
تامین کنندهی بانکهای اطلاعاتی مختلف، عموما تنظیمات خاص خودشان را داشته و همچنین دستورات SQL متفاوتی را نیز تولید میکنند. به همین جهت نمیتوان از یک تک Context، هم برای SQLite و هم SQL Server استفاده کرد. به علاوه قصد داریم اطلاعات Migrations هر کدام را نیز در یک اسمبلی جداگانه قرار دهیم. در یک چنین حالتی EF نمیپذیرد که Context تولید کنندهی Migration، در اسمبلی دیگری قرار داشته باشد و باید حتما در همان اسمبلی Migration قرار گیرد. بنابراین ساختار پوشه بندی مثال جاری به صورت زیر خواهد بود:
- در پوشهی EFCoreMultipleDb.DataLayer فقط اینترفیس IUnitOfWork را قرار میدهیم. از این جهت که وقتی قرار شد در برنامه چندین Context تعریف شوند، لایهی سرویس برنامه قرار نیست بداند در حال حاضر با کدام Context کار میکند. به همین جهت است که تغییر بانک اطلاعاتی برنامه، تغییری را در کدهای اصلی آن ایجاد نخواهد کرد.
- در پوشهی EFCoreMultipleDb.DataLayer.SQLite کدهای Context و همچنین IDesignTimeDbContextFactory مخصوص SQLite را قرار میدهیم.
- در پوشهی EFCoreMultipleDb.DataLayer.SQLServer کدهای Context و همچنین IDesignTimeDbContextFactory مخصوص SQL Server را قرار میدهیم.
برای نمونه ابتدای Context مخصوص SQLite چنین شکلی را دارد:
public class SQLiteDbContext : DbContext, IUnitOfWork { public SQLiteDbContext(DbContextOptions options) : base(options) { } public virtual DbSet<User> Users { set; get; }
namespace EFCoreMultipleDb.DataLayer.SQLite.Context { public class SQLiteDbContextFactory : IDesignTimeDbContextFactory<SQLiteDbContext> { public SQLiteDbContext CreateDbContext(string[] args) { var basePath = Directory.GetCurrentDirectory(); Console.WriteLine($"Using `{basePath}` as the BasePath"); var configuration = new ConfigurationBuilder() .SetBasePath(basePath) .AddJsonFile("appsettings.json") .Build(); var builder = new DbContextOptionsBuilder<SQLiteDbContext>(); var connectionString = configuration.GetConnectionString("SqliteConnection") .Replace("|DataDirectory|", Path.Combine(basePath, "wwwroot", "app_data")); builder.UseSqlite(connectionString); return new SQLiteDbContext(builder.Options); } } }
تنظیمات رشتههای اتصالی بانکهای اطلاعاتی مختلف
در اینجا محتویات فایل appsettings.json را که در آن تنظیمات رشتههای اتصالی دو بانک SQL Server LocalDB و همچنین SQLite در آن ذکر شدهاند، مشاهده میکنید:
{ "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "SqlServerConnection": "Data Source=(LocalDB)\\MSSQLLocalDB;Initial Catalog=ASPNETCoreSqlDB;AttachDbFilename=|DataDirectory|\\ASPNETCoreSqlDB.mdf;Integrated Security=True;MultipleActiveResultSets=True;", "SqliteConnection": "Data Source=|DataDirectory|\\ASPNETCoreSqliteDB.sqlite", "InUseKey": "SqliteConnection" } }
یک کلید InUseKey را هم در اینجا تعریف کردهایم تا مشخص باشد در ابتدای کار برنامه، کلید کدام رشتهی اتصالی مورد استفاده قرار گیرد. برای مثال در اینجا کلید رشتهی اتصالی SQLite تنظیم شدهاست.
در این تنظیمات یک DataDirectory را نیز مشاهده میکنید. مقدار آن در فایل Startup.cs برنامه به صورت زیر بر اساس پوشهی جاری تعیین میشود و در نهایت به wwwroot\app_data اشاره خواهد کرد:
var connectionStringKey = Configuration.GetConnectionString("InUseKey"); var connectionString = Configuration.GetConnectionString(connectionStringKey) .Replace("|DataDirectory|", Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "app_data"));
دستورات تولید Migrations و به روز رسانی بانک اطلاعاتی
چون تعداد Contextهای برنامه بیش از یک مورد شدهاست، دستورات متداولی را که تاکنون برای تولید Migrations و یا به روز رسانی ساختار بانک اطلاعاتی اجرا میکردید، با پیام خطایی که این مساله را گوشزد میکند، متوقف خواهند شد. راه حل آن ذکر صریح Context مدنظر است:
برای تولید Migrations، از طریق خط فرمان، به پوشهی اسمبلی مدنظر وارد شده و دستور زیر را اجرا کنید:
For /f "tokens=2-4 delims=/ " %%a in ('date /t') do (set mydate=%%c_%%a_%%b) For /f "tokens=1-2 delims=/:" %%a in ("%TIME: =0%") do (set mytime=%%a%%b) dotnet build dotnet ef migrations --startup-project ../EFCoreMultipleDb.Web/ add V%mydate%_%mytime% --context SQLiteDbContext
دو سطر اول آن، زمان اجرای دستورات را به عنوان نام فایلها تولید میکنند.
پس از تولید Migrations، اکنون نوبت به تولید بانک اطلاعاتی و یا به روز رسانی بانک اطلاعاتی موجود است:
dotnet build dotnet ef --startup-project ../EFCoreMultipleDb.Web/ database update --context SQLServerDbContext
بدیهی است این رویه را پس از هربار تغییراتی در موجودیتهای برنامه و یا تنظیمات آنها در Contextهای متناظر، نیاز است مجددا اجرا کنید. البته اجرای اولین دستور اجباری است؛ اما میتوان دومین دستور را به صورت زیر نیز اجرا کرد:
namespace EFCoreMultipleDb.Web { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { applyPendingMigrations(app); // ... } private static void applyPendingMigrations(IApplicationBuilder app) { var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>(); using (var scope = scopeFactory.CreateScope()) { var uow = scope.ServiceProvider.GetService<IUnitOfWork>(); uow.Migrate(); } } } }
namespace EFCoreMultipleDb.DataLayer.SQLite.Context { public class SQLiteDbContext : DbContext, IUnitOfWork { // ... public void Migrate() { this.Database.Migrate(); } } }
مرحلهی آخر: انتخاب بانک اطلاعاتی در برنامهی آغازین
پس از این تنظیمات، قسمتی که کار تعریف IUnitOfWork و همچنین DbContext جاری برنامه را انجام میدهد، به صورت زیر پیاده سازی میشود:
namespace EFCoreMultipleDb.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddScoped<IUsersService, UsersService>(); var connectionStringKey = Configuration.GetConnectionString("InUseKey"); var connectionString = Configuration.GetConnectionString(connectionStringKey) .Replace("|DataDirectory|", Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "app_data")); switch (connectionStringKey) { case "SqlServerConnection": services.AddScoped<IUnitOfWork, SQLServerDbContext>(); services.AddDbContext<SQLServerDbContext>(options => { options.UseSqlServer( connectionString, dbOptions => { var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds; dbOptions.CommandTimeout(minutes); dbOptions.EnableRetryOnFailure(); }); }); break; case "SqliteConnection": services.AddScoped<IUnitOfWork, SQLiteDbContext>(); services.AddDbContext<SQLiteDbContext>(options => { options.UseSqlite( connectionString, dbOptions => { var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds; dbOptions.CommandTimeout(minutes); }); }); break; default: throw new NotImplementedException($"`{connectionStringKey}` is not defined in `appsettings.json` file."); } services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); }
آزمایش برنامه
ابتدا کدهای کامل این مطلب را از اینجا دریافت کنید: EFCoreMultipleDb.zip
سپس آنرا اجرا نمائید. چنین تصویری را مشاهده خواهید کرد:
اکنون برنامه را بسته و سپس فایل appsettings.json را جهت تغییر مقدار InUseKey به کلید SqlServerConnection ویرایش کنید:
{ "ConnectionStrings": { // … "InUseKey": "SqlServerConnection" } }
مقدار username، در contextهای هر کدام از این بانکهای اطلاعاتی، با مقدار متفاوتی به عنوان اطلاعات اولیهی آن ثبت شدهاست. سرویسی هم که اطلاعات آنرا تامین میکند، به صورت زیر تعریف شدهاست:
namespace EFCoreMultipleDb.Services { public interface IUsersService { Task<User> FindUserAsync(int userId); } public class UsersService : IUsersService { private readonly IUnitOfWork _uow; private readonly DbSet<User> _users; public UsersService(IUnitOfWork uow) { _uow = uow; _users = _uow.Set<User>(); } public Task<User> FindUserAsync(int userId) { return _users.FindAsync(userId); } } }
از EF Core 2.1 به بعد، قابلیت جدیدی تحت عنوان «تبدیلگرهای مقدار»، به آن اضافه شدهاست. برای مثال در EF Core، زمانیکه اطلاعات Enums، در بانک اطلاعاتی ذخیره میشوند، معادل عددی آنها درج خواهند شد. اگر علاقمند باشید تا بجای این مقادیر عددی دقیقا همان رشتهی تعریف کنندهی Enum درج شود، میتوان یک «تبدیلگر مقدار» را برای آن نوشت. برای مثال در موجودیت Rider زیر، خاصیت Mount از نوع یک enum است.
برای اینکه در حین درج رکوردهای Rider در بانک اطلاعاتی دقیقا از مقادیر رشتهای EquineBeast استفاده شود، میتوان به صورت زیر عمل کرد:
در اینجا در حین تعریف جزئیات نگاشت یک مدل میتوان متد جدید HasConversion را نیز فراخوانی کرد. پارامتر اول آن، روش تبدیل مقدار enum را به یک رشته، جهت درج در بانک اطلاعاتی و پارامتر دوم آن، روش تبدیل مقدار رشتهای خوانده شدهی از بانک اطلاعاتی را جهت وهله سازی یک Rider داری خاصیت enum، مشخص میکند.
نکته 1: مقادیر نال، هیچگاه به تبدیلگرهای مقدار، ارسال نمیشوند. اینکار پیاده سازی آنها را سادهتر میکند و همچنین میتوان آنها را بین خواص نالپذیر و نالنپذیر، به اشتراک گذاشت. بنابراین برای مقادیر نال نمیتوان تبدیلگر نوشت.
نکته 2: کاری که در متد HasConversion فوق انجام شدهاست، در حقیقت وهله سازی ضمنی یک ValueConverter و استفاده از آن است. میتوان اینکار را به صورت صریح نیز انجام داد:
مزیت اینکار این است که اگر قرار شد برای چندین خاصیت از تبدیلگر مقدار مشابهی استفاده کنیم، میتوان از یک converter تعریف شده بجای تکرار کدهای آن استفاده کرد.
تبدیلگرهای مقدار توکار EF Core
برای بسیاری از اعمال متداول، در فضای نام Microsoft.EntityFrameworkCore.Storage.ValueConversion، تعدادی تبدیلگر مقدار تدارک دیده شدهاند که به این شرح میباشند:
BoolToZeroOneConverter: تبدیلگر bool به صفر و یک
BoolToStringConverter: تبدیلگر bool به Y و یا N
BoolToTwoValuesConverter: تبدیلگر bool به دو مقداری دلخواه
BytesToStringConverter: تبدیلگر آرایهای از بایتها به یک رشتهی Base64-encoded
CastingConverter: تبدیلگر یک نوع به نوعی دیگر
CharToStringConverter: تبدیلگر char به string
DateTimeOffsetToBinaryConverter: تبدیلگر DateTimeOffset به یک مقدار 64 بیتی باینری
DateTimeOffsetToBytesConverter: تبدیلگر DateTimeOffset به آرایهای از بایتها
DateTimeOffsetToStringConverter: تبدیلگر DateTimeOffset به رشته
DateTimeToBinaryConverter: تبدیلگر DateTime به یک مقدار 64 بیتی با درج DateTimeKind
DateTimeToStringConverter: تبدیلگر DateTime به یک رشته
DateTimeToTicksConverter: تبدیلگر DateTime به ticks آن
EnumToNumberConverter: تبدیلگر Enum به عدد متناظر با آن
EnumToStringConverter: تبدیلگر Enum به رشته
GuidToBytesConverter: تبدیلگر Guid به آرایهای از بایتها
GuidToStringConverter: تبدیلگر Guid به رشته
NumberToBytesConverter: تبدیلگر اعداد به آرایهای از بایتها
NumberToStringConverter: تبدیلگر اعداد به رشته
StringToBytesConverter: تبدیلگر رشته به آرایهای از بایتهای UTF8 معادل آن
TimeSpanToStringConverter: تبدیلگر TimeSpan به رشته
TimeSpanToTicksConverter: تبدیلگر TimeSpan به ticks آن
برای نمونه در این لیست، EnumToStringConverter نیز وجود دارد. بنابراین نیازی به تعریف دستی آن مانند مثال ابتدای بحث نیست و میتوان به صورت زیر از آن استفاده کرد:
نکته: تمام تبدیل کنندههای مقدار توکار EF Core، بدون حالت هستند. بنابراین میتوان یک تک وهلهی از آنها را بین چندین خاصیت به اشتراک گذاشت.
تعیین نوع تبدیلگر مقدار، جهت ساده سازی تعاریف
برای حالاتی که تبدیلگر مقدار توکاری تعریف شدهاست، صرفا تعریف نوع تبدیل، کفایت میکند:
برای نمونه در اینجا با ذکر نوع رشته، تبدیل enum به string به صورت خودکار انجام خواهد شد. معادل اینکار، تعریف نوع سمت بانک اطلاعاتی این خاصیت است:
در این حالت حتی نیازی به تعریف HasConversion هم نیست.
نوشتن تبدیلگر خودکار مقادیر خواص، به نمونهای رمزنگاری شده
پس از آشنایی با مفهوم «تبدیلگرهای مقدار» در +EF Core 2.1، اکنون میتوانیم یک نمونهی سفارشی از آنرا نیز طراحی کنیم:
در اینجا معکوس کردن رشتهها به عنوان الگوریتم سادهی رمزنگاری اطلاعات انتخاب شدهاست. نحوهی اعمال این ValueConverter جدید را نیز ملاحظه میکنید.
میتوان قسمت HasConversion را به صورت زیر خودکار کرد:
ابتدا یک Attribute جدید را به نام Encrypted به برنامه اضافه میکنیم:
هدف از این Attribute خالی، صرفا نشانه گذاری خاصیتهایی است که قرار است به صورت رمزنگاری شده در بانک اطلاعاتی ذخیره شوند؛ مانند خاصیت Value زیر:
پس از آن، متد OnModelCreating را به صورت زیر اصلاح میکنیم تا به کمک Reflection و اطلاعات موجودیتهای ثبت شدهی در سیستم، متد SetValueConverter را بر روی خواصی که دارای EncryptedAttribute هستند، به صورت خودکار فراخوانی کند:
تاثیر ValueConverterها بر روی اعمال متداول کار با بانک اطلاعاتی
از دیدگاه برنامه، ValueConverterهای تعریف شده، هیچگونه تاثیری را بر روی کوئری نوشتن و یا ثبت و ویرایش اطلاعات ندارند و عملکرد آنها کاملا از دیدگاه سایر قسمتهای برنامه مخفی است. برای مثال در برنامه، فرمان به روز رسانی خاصیت Value را با مقدار .A new value to test صادر کردهایم (مقدار دهی متداول)، اما همانطور که ملاحظه میکنید، نمونهی رمزنگاری شدهی آن به صورت خودکار در بانک اطلاعاتی درج شدهاست (پارامتر p0):
و یا کوئری زیر
به این نحو ترجمه خواهد شد:
یعنی نیازی نیست تا مقداری را که در حال جستجوی آن هستیم، خودمان به صورت دستی رمزنگاری کرده و سپس در کوئری قرار دهیم. اینکار به صورت خودکار انجام میشود.
public class Rider { public int Id { get; set; } public EquineBeast Mount { get; set; } } public enum EquineBeast { Donkey, Mule, Horse, Unicorn }
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion( v => v.ToString(), v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v)); }
نکته 1: مقادیر نال، هیچگاه به تبدیلگرهای مقدار، ارسال نمیشوند. اینکار پیاده سازی آنها را سادهتر میکند و همچنین میتوان آنها را بین خواص نالپذیر و نالنپذیر، به اشتراک گذاشت. بنابراین برای مقادیر نال نمیتوان تبدیلگر نوشت.
نکته 2: کاری که در متد HasConversion فوق انجام شدهاست، در حقیقت وهله سازی ضمنی یک ValueConverter و استفاده از آن است. میتوان اینکار را به صورت صریح نیز انجام داد:
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
برای بسیاری از اعمال متداول، در فضای نام Microsoft.EntityFrameworkCore.Storage.ValueConversion، تعدادی تبدیلگر مقدار تدارک دیده شدهاند که به این شرح میباشند:
BoolToZeroOneConverter: تبدیلگر bool به صفر و یک
BoolToStringConverter: تبدیلگر bool به Y و یا N
BoolToTwoValuesConverter: تبدیلگر bool به دو مقداری دلخواه
BytesToStringConverter: تبدیلگر آرایهای از بایتها به یک رشتهی Base64-encoded
CastingConverter: تبدیلگر یک نوع به نوعی دیگر
CharToStringConverter: تبدیلگر char به string
DateTimeOffsetToBinaryConverter: تبدیلگر DateTimeOffset به یک مقدار 64 بیتی باینری
DateTimeOffsetToBytesConverter: تبدیلگر DateTimeOffset به آرایهای از بایتها
DateTimeOffsetToStringConverter: تبدیلگر DateTimeOffset به رشته
DateTimeToBinaryConverter: تبدیلگر DateTime به یک مقدار 64 بیتی با درج DateTimeKind
DateTimeToStringConverter: تبدیلگر DateTime به یک رشته
DateTimeToTicksConverter: تبدیلگر DateTime به ticks آن
EnumToNumberConverter: تبدیلگر Enum به عدد متناظر با آن
EnumToStringConverter: تبدیلگر Enum به رشته
GuidToBytesConverter: تبدیلگر Guid به آرایهای از بایتها
GuidToStringConverter: تبدیلگر Guid به رشته
NumberToBytesConverter: تبدیلگر اعداد به آرایهای از بایتها
NumberToStringConverter: تبدیلگر اعداد به رشته
StringToBytesConverter: تبدیلگر رشته به آرایهای از بایتهای UTF8 معادل آن
TimeSpanToStringConverter: تبدیلگر TimeSpan به رشته
TimeSpanToTicksConverter: تبدیلگر TimeSpan به ticks آن
برای نمونه در این لیست، EnumToStringConverter نیز وجود دارد. بنابراین نیازی به تعریف دستی آن مانند مثال ابتدای بحث نیست و میتوان به صورت زیر از آن استفاده کرد:
var converter = new EnumToStringConverter<EquineBeast>(); modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion(converter);
تعیین نوع تبدیلگر مقدار، جهت ساده سازی تعاریف
برای حالاتی که تبدیلگر مقدار توکاری تعریف شدهاست، صرفا تعریف نوع تبدیل، کفایت میکند:
modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion<string>();
public class Rider { public int Id { get; set; } [Column(TypeName = "nvarchar(24)")] public EquineBeast Mount { get; set; } }
نوشتن تبدیلگر خودکار مقادیر خواص، به نمونهای رمزنگاری شده
پس از آشنایی با مفهوم «تبدیلگرهای مقدار» در +EF Core 2.1، اکنون میتوانیم یک نمونهی سفارشی از آنرا نیز طراحی کنیم:
namespace DbConfig.Web.DataLayer.Context { public class MyAppContext : DbContext { // … protected override void OnModelCreating(ModelBuilder builder) { var encryptedConverter = new ValueConverter<string, string>( convertToProviderExpression: v => new string(v.Reverse().ToArray()), // encrypt convertFromProviderExpression: v => new string(v.Reverse().ToArray()) // decrypt ); // Custom application mappings builder.Entity<ConfigurationValue>(entity => { entity.Property(e => e.Value).IsRequired().HasConversion(encryptedConverter); }); } } }
میتوان قسمت HasConversion را به صورت زیر خودکار کرد:
ابتدا یک Attribute جدید را به نام Encrypted به برنامه اضافه میکنیم:
using System; namespace Test { [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] public sealed class EncryptedAttribute : Attribute { } }
namespace DbConfig.Web.DomainClasses { public class ConfigurationValue { public int Id { get; set; } public string Key { get; set; } [Encrypted] public string Value { get; set; } } }
namespace DbConfig.Web.DataLayer.Context { public class MyAppContext : DbContext { protected override void OnModelCreating(ModelBuilder builder) { var encryptedConverter = new ValueConverter<string, string>( convertToProviderExpression: v => new string(v.Reverse().ToArray()), // encrypt convertFromProviderExpression: v => new string(v.Reverse().ToArray()) // decrypt ); foreach (var entityType in builder.Model.GetEntityTypes()) { foreach (var property in entityType.GetProperties()) { var attributes = property.PropertyInfo.GetCustomAttributes(typeof(EncryptedAttribute), false); if (attributes.Any()) { property.SetValueConverter(encryptedConverter); } } } }
از دیدگاه برنامه، ValueConverterهای تعریف شده، هیچگونه تاثیری را بر روی کوئری نوشتن و یا ثبت و ویرایش اطلاعات ندارند و عملکرد آنها کاملا از دیدگاه سایر قسمتهای برنامه مخفی است. برای مثال در برنامه، فرمان به روز رسانی خاصیت Value را با مقدار .A new value to test صادر کردهایم (مقدار دهی متداول)، اما همانطور که ملاحظه میکنید، نمونهی رمزنگاری شدهی آن به صورت خودکار در بانک اطلاعاتی درج شدهاست (پارامتر p0):
Executed DbCommand (22ms) [Parameters=[@p1='1', @p0='.tset ot eulav wen A' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='180'] SET NOCOUNT ON; UPDATE [Configurations] SET [Value] = @p0 WHERE [Id] = @p1; SELECT @@ROWCOUNT;
و یا کوئری زیر
db.Set<ConfigurationValue>().Where(x => x.Value.EndsWith("world!"))
SELECT [x].[Id], [x].[Key], [x].[Value] FROM [Configurations] AS [x] WHERE RIGHT([x].[Value], LEN(N'world!')) = N'!dlrow'
اشتراکها