public class StoreContext : DbContext{ // Base -> parameter ctor that take DbContextOptions public StoreContext():base() { } protected override void OnConfiguring(DbContextOptionsBuilder options) { options.UseSqlServer("ConntectionString"); // منفعتش معايا }}
دي بكل بساطة هيورث الـ Ctor بتاع الـ Base بس هي بتحتاج Options فانت بتضر تعمل Overloading للـ OnConfiguring عشان تديلها الـ Options
والطريقة اللي فوق دي مشكلتها انها مبتستخدمش الـ DNAPI Dependency Injection
عندنا طريقة تانية بنستخدم بيها الـ DI
ApplicationDbContext.cs:
public class ApplicationDbContext : DbContext{ public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<Product> Products { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Product>().ToTable("Product"); }}
لازم يورث من كلاس اسمه DbContext
أول حاجة لازم نعمل override لميثود اسمها OnModelCreating ودي بكل بساطة بقوله إن معايا Entities وبعرفه ToTable انه هيروح يعدل هناك في الجدول دا
لو هو هو نفس اسم الـ Entity مش لازم تديله اسم انما لو مختلف لازم تكتب اسم الجدول في الـ DB بالظبط
لو الـ Primary Key اسمه ID أو اسمه اسم الـ Entity وبعده ID زي ProuctID هو تلقائي بيتعرف عليها انما لو مختلف لازم استخدم HasKey("Id") واكتب الـ Primary key
متنساش تعمل empty constructor عشان ميعمليش ايرور ويبقا بياخد DbContextOptions
الـ ctor يقوم بتمرير DbContextOptions الذي يحتوي على إعدادات الاتصال (مثل سلسلة الاتصال Connection String) وخيارات أخرى، مثل مقدم الخدمة (Provider) مثل SQL Server، SQLite، أو PostgreSQL.
الـDbSet<Product> Products { get; set; } هو التمثيل البرمجي لجدول Products في قاعدة البيانات.
هيبقا عندي 3 من Connection Strings عشان هيبقا عندي 3 Databases في المشروع
DbSets
دي زي ما قولنا بتمثل الجداول في المشروع بتاعنا
public class StoreContext : DbContext{ public StoreContext(DbContextOptions<StoreContext> options) :base(options) { } public DbSet<Product> Products {get; set;} public DbSet<ProductBrand> ProductBrands {get; set;} }
Fluent API
محتاجين نكتب التعديلات اللي عايزين نعملها على الـ Modules بتاعنا
هنعمل Override لـ OnModelCreating
public class StoreContext : DbContext{ public StoreContext(DbContextOptions<StoreContext> options) :base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { } public DbSet<Product> Products {get; set;} public DbSet<ProductBrand> ProductBrands {get; set;} }
مش محتاج أنادي على الـ OnModelCreating بتاع الـ Base اللي هو DbContext عشان مفيهاش DbSet فمش هيبقا ليها لازمة
Product Configuration
هنعمل Folder اسمه Config جوا Folder الـ Data اللي موجودة جوا الـ Repository عشان أحط فيها كل الـ Configurations بتاعنا
بتاخد من الـ Interface اللي اسمه IEntityTypeConfiguration
هنبدأ نعمل حاجتين عندنا:
الـ Properties كلها بتاعها (تاخد بالك انك تحل مشاكل مثلًا ان عناصر متبقاش NULL)
الـ Relationships بينها وبين باقي العناصر (بتحدد نوعها وبعدين بتحدد الـ Foreign key)
internal class ProductConfigurations : IEntityTypeConfiguration<Product>{ public void Configure(EntityTypeBuilder<Product> builder) { builder.Property(P => P.Name) .IsRequired() .HasMaxLength(100); builder.Property(P => P.Description) .IsRequired(); builder.Property(P => P.PictureUrl) .IsRequired(); builder.Property(P => P.Price) .HasColumnType("decimal(18, 2)"); // Foreign key relationship with Brand builder.HasOne(P => P.Brand) .WithMany() .HasForeignKey(P => P.BrandId); //.OnDelete(DeleteBehavior.SetNull); // Uncomment if you want to set null on delete // Foreign key relationship with Category builder.HasOne(P => P.Category) .WithMany() .HasForeignKey(P => P.CategoryId); }}
ونعمل واحد للـ Brand و Category
دلوقتي هنزود الـ Configuration في الـ OnModelCreating
عندنا طريقة قديمة:
ببياخدها من الـ Assembly اللي شغال وبياخد كل اللي وارث من الـIEntityTypeConfiguration وبيستخدم الـ Reflection
Migrations
اتكلمنا عنها في الـ EF Add First Migration
هنحتاج نعمل الـ Migration في البروجكت بتاع الـ API اللي فيه الـ Connection stringهننزل الـ Package اللي اسمه EntityFrameCore.Tools
لما نيجي نفتح الـ Package Manager هنحدد البروجكت بتاع الـ Repository
add-migration "ProductModule" -O Data/Migrations
وبعدين نعمل Update-Database
Update-Database
دي بتنفذ سطرين كود كدا انها بتعمل اوبجيكت من الـ DbContext ويمسك الـ Object يقوله object.Database.Migrate
Update Database
قولنا طريقة من شوية
بس دي مش أحسن طريقة تتنفذ لأني عايز لما أعمل run للبرنامج يعمل Apply Migration لوحده من غير ما أقوله لو مش معمولها Update
هعمل دي عن طريق اني هروح في أول الـ Main وأحط السطرين اللي قولنا عليهم
StoreContext dbContext = new StoreContext();await dbContext.Database.MigrateAsync();
بس محتاج هنا أديله Options وأنا لو عملت كدا يدوي هبوظ الـ Dependency Injection
فأنا محتاج أطلب من الـ CLR يعملي الحوار دا
ممكن واحد يقولي أعمل Ctor للـ Program نفسه يعملي الـ Object بس دا هيعملي مشاكل كتير
فأنا محتاج أعمل كدا بشكل Explicitly
فأنا هطلب بعد ما أعمل Configure بعد الـ DbContext وبعد ما أعمل Build لل App
محتاج أعمل Create Scope
var scope = app.Services.CreateScope();// استخدمت يوزينج عشان يديزبوز يوقف الريكويستvar services = scope.ServiceProvider;var _dbContext = services.GetRequiredService<StoreContext>();// Ask CLR for Creating Object from DbContext Explicitlyawait _dbContext.Database.MigrateAsync();
ولازم بعد ما أخلص شغلي معاه أقفل الـ Scope
وعشان أعمل كدا هستخدم الـ Finally لأنها هتتنفذ لما Try تخلص شغلها
try { var scope = app.Services.CreateScope(); var services = scope.ServiceProvider; var _dbContext = services.GetRequiredService<StoreContext>(); await _dbContext.Database.MigrateAsync(); } finally { scope.Dispose(); }
او بدل دي هستخدم Using
using var scope = app.Services.CreateScope();var services = scope.ServiceProvider;var _dbContext = services.GetRequiredService<StoreContext>();// Ask CLR for Creating Object from DbContext Explicitlyawait _dbContext.Database.MigrateAsync();
حاجة كمان ان لو حصل مشكلة في الـ Update كدا مش هيعمل Run فهحتاج أعمل الكود في Try … Catch
مش هستخدم Console عشان أعرض الـ Error فهستخدم الـ Logger Factory
الكود النهائي
using var scope = app.Services.CreateScope();var services = scope.ServiceProvider;var _dbContext = services.GetRequiredService<StoreContext>();var loggerFactory = services.GerRequiredService<ILoggerFactory>();try{ // Update-Database await _dbContext.Database.MigrateAsync(); }catch (Exception ex){ var logger = loggerFactory.CreateLogger<Program>(); logger.LogError(ex, "an error has been occurred during apply the migration");}
خد بالك انت كمان محتاج تعمل ال Program يبقا async Task
الطريقة دي بنستخدمها عشان نعمل Update بشكل مستمر
كل مرة أشغل البرنامج يعمل Apply لأي Migration عندك
والموضوع دا مهم لما تيجي تعمل Deploy على Server Production
وعشان الـ Data Seeding
Data Seeding
Seed ⇒ دا البداية اني بعمل داتا مبدأية
العملية دي بتحصل مرة واحدة في الحياة
ومرة واحدة للسيرفر
اني بحط الداتا موجودة قبل كدا
الخطوات
هنعمل Folder جديد في الفولدر بتاع Data اللي موجود في الـ Repository وأسميه DataSeeding
نحط فيه ملفات الـ JSON بتاعنا
جوا فولدر الـ Data هنعمل Helper نسميه StoreContextSeed وتبقى public static
نعمل Method اسمها SeedAsync
ولازم نمررلها الـ Database Context بتاعنا عشان يحطها فيها
هنبدأ بالـ Category أو الـ Brand وننتهي بالـ Product في الآخر خالص
هنستخدم الـ ReadAllText اللي هي بتفتح الفايل وتاخد الداتا وتقفله
وبنديلها Absolute path لمكان الفايل
هنستخدم بعد كدا الـ JSON واتكلمنا قبل كدا عن الـ Deserialize
فالأخر نضيف دي تحت الـ Migrate في البروجرام
public static class StoreContextSeed{ public async static Task SeedAsync(StoreContext _dbContext) { // File => String (JSON) var brandsData = File.ReadAllText("../Shary.Repository/Data/DataSeed/brands.json"); // String(JSON) => in-memory objects var brands = JsonSerializer.Deserialize<List<ProductBrand>>(brandsData); if(brands?.Count() > 0) { foreach(var brand in brands) { _dbContext.Set<ProductBrand>().Add(brand); } await _dbContext.SaveChangesAsync(); } }}// MAINtry{ await _dbContext.Database.MigrateAsync(); await StoreContextSeed.SeedAsync(_dbContext)}
لازم نتأكد ان الداتا مش ب null في الجدول ككل
Identify (ID)
خد بالك انك مينفعش تبعت الـ ID لو المفروض انه بيزيد لوحده أو انه مينفعش تدخله
ممكن اننا نعدل الـ Function اننا نشيل الـ ID
if(brands?.Count() > 0){ brands = brands.Select(b => new ProductBrand() { Name = b.Name, }).ToList(); // Removed ID from all list foreach(var brand in brands) { // Update-Database _dbContext.Set<ProductBrand>().Add(brand); } await _dbContext.SaveChangesAsync();}
حل تاني اننا نمسح كل الـ ID من الـ JSON فايل نفسه
بس كدا هنعمل Seeding على طول وانا محتاج اعمل الحوار دا مرة واحدة بس في الحياة زي ما قولنا
هنحط كل الكود في If
if(!_dbContext.ProductBrands.Any())
ودا مهم عشان لما نعمل Deploy وغيرها
وممكن نخلي الشرط:
if (_dbContext.ProductBrands.Count() == 0)
public async static Task SeedAsync(StoreContext _dbContext) { if(!_dbContext.ProductBrands.Any()) //if brands not any in table, do this code { var brandsData = File.ReadAllText("../Shary.Repository/Data/DataSeed/brands.json"); // String(JSON) => in-memory objects var brands = JsonSerializer.Deserialize<List<ProductBrand>>(brandsData); if(brands?.Count() > 0) { foreach(var brand in brands) { _dbContext.Set<ProductBrand>().Add(brand); } await _dbContext.SaveChangesAsync(); } } }}
هنعمل Seeding لكل الـ Entities اللي عندي في نفس الفانكشن