Уявіть ситуацію: ваш додаток вже у продакшені, обробляє замовлення, і раптом бухгалтер помічає, що сума замовлення 1234.5678 зберігається у базі як 1234.57. Два центи втрачено через округлення. Хто винен? Не PostgreSQL, не SQL Server — вони зберегли рівно те, що їм сказали. Винний маппінг типу decimal без явного HasPrecision. EF Core за замовчуванням обрав decimal(18, 2), і цього було недостатньо для фінансових розрахунків.
Або інший сценарій: ваші enum-значення зберігаються у базі як цілі числа 0, 1, 2. Хтось вручну виправляє дані у базі, вводить 5 — а такого статусу не існує. Або ви перейменовуєте enum-член зі Shipped на InTransit і переставляєте порядок елементів. Тепер всі дані у БД означають не те, що ви думаєте.
Або третій: ваш UserId — це просто int, і компілятор не заперечує, коли ви передаєте OrderId там, де очікується UserId. Strongly Typed IDs вирішують цю проблему, але вимагають конвертера.
Всі ці сценарії — про конфігурацію властивостей у EF Core: як C#-типи маппляться на SQL-типи, і як ми можемо перехопити цей маппінг для вирішення реальних задач. Ця стаття розкриває весь арсенал інструментів, що є у вашому розпорядженні.
EF Core виконує type mapping — автоматичне перетворення між типами C# і SQL-типами конкретної СУБД. Кожен провайдер (SQL Server, PostgreSQL, SQLite тощо) має власну таблицю відповідностей. Розуміння цих відповідностей рятує від несподіванок.
| C# тип | SQL Server тип за замовчуванням | Примітка |
|---|---|---|
int | int | 4 байти, -2B..+2B |
long | bigint | 8 байт |
short | smallint | 2 байти |
byte | tinyint | 0..255 |
bool | bit | 0 або 1 |
string | nvarchar(max) | Unicode, без ліміту |
char | nvarchar(1) | один символ |
decimal | decimal(18, 2) | ⚠️ Округлення! |
double | float | IEEE 754 |
float | real | IEEE 754, менша точність |
DateTime | datetime2 | до 100 нс точності |
DateTimeOffset | datetimeoffset | з часовим поясом |
DateOnly | date | EF Core 6+ |
TimeOnly | time | EF Core 6+ |
Guid | uniqueidentifier | 16 байт |
byte[] | varbinary(max) | бінарні дані |
enum | int | числове значення |
| C# тип | PostgreSQL тип | Примітка |
|---|---|---|
int | integer | |
long | bigint | |
string | text | немає максимуму за замовчуванням |
decimal | numeric | ⚠️ Без precision за замовчуванням |
bool | boolean | |
DateTime | timestamp with time zone | Npgsql конвертує у UTC |
Guid | uuid | native тип |
byte[] | bytea |
DateTime на timestamp with time zone і вимагає, щоб всі DateTime значення були в UTC. Якщо передати DateTime.Now (local time) — отримаєте виключення. Завжди використовуйте DateTime.UtcNow або DateTimeOffset.string без обмеження — це проблемаКоли ви маєте public string Name { get; set; } без конфігурації, EF Core для SQL Server генерує nvarchar(max). Це проблема з точки зору:
nvarchar(max) не можуть бути повністю охоплені індексом (SQL Server обмежує розмір ключа індексу). Пошук по Name без індексу — повне сканування таблиці.Name мало б бути максимум 100 символів.nvarchar(max) семантично означає «рядок довільної довжини», а не «ім'я людини».HasColumnType() — найпряміший інструмент: ви вказуєте рядково точний SQL-тип, що відображатиметься у DDL.
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public decimal Weight { get; set; }
public byte[] Thumbnail { get; set; } = Array.Empty<byte>();
public string Tags { get; set; } = string.Empty; // JSON-рядок
}
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.HasKey(p => p.Id);
builder.Property(p => p.Name)
.HasMaxLength(200)
.IsRequired();
builder.Property(p => p.Description)
.HasColumnType("nvarchar(4000)");
// Явний тип для фінансового значення
builder.Property(p => p.Price)
.HasColumnType("decimal(10, 4)");
// Вага: менша точність достатня
builder.Property(p => p.Weight)
.HasColumnType("decimal(8, 3)");
// Фіксований розмір для мініатюри
builder.Property(p => p.Thumbnail)
.HasColumnType("varbinary(8000)");
// PostgreSQL: явно jsonb замість text
// builder.Property(p => p.Tags).HasColumnType("jsonb");
}
}
Декомпозиція конфігурації:
HasColumnType("decimal(10, 4)") — генерує DECIMAL(10, 4) у DDL. Число 10 — загальна кількість цифр (precision), 4 — після коми (scale). Це означає максимальне значення 999999.9999.HasColumnType("nvarchar(4000)") — обмежує рядок 4000 символами Unicode. На відміну від HasMaxLength(4000), який лише додає валідацію на рівні анотацій, HasColumnType задає точний SQL-тип.HasColumnType("varbinary(8000)") — бінарний стовпець фіксованого максимального розміру. varbinary(max) зберігає дані поза основним рядком (LOB), varbinary(8000) — у рядку таблиці, що швидше читається.HasColumnType() та HasMaxLength() / HasPrecision() — не взаємовиключні, але якщо вказати HasColumnType, він перевизначає будь-яке HasMaxLength. Використовуйте HasColumnType коли вам потрібна рядкова специфікація, HasPrecision — коли хочете залишити вибір SQL-типу провайдеру.HasPrecision(precision, scale) — більш семантичний спосіб керування decimal-типами. Замість рядкового "decimal(18, 6)" ви передаєте два числа, а провайдер сам генерує правильний SQL-тип.
public class FinancialTransaction
{
public int Id { get; set; }
public decimal Amount { get; set; } // Сума транзакції
public decimal ExchangeRate { get; set; } // Курс обміну
public decimal Fee { get; set; } // Комісія
public decimal TaxAmount { get; set; } // Податок
}
public class FinancialTransactionConfiguration : IEntityTypeConfiguration<FinancialTransaction>
{
public void Configure(EntityTypeBuilder<FinancialTransaction> builder)
{
// Сума: 12 цифр всього, 4 після коми — підходить для сум до 99,999,999.9999
builder.Property(t => t.Amount)
.HasPrecision(14, 4)
.IsRequired();
// Курс обміну: потрібна висока точність після коми
builder.Property(t => t.ExchangeRate)
.HasPrecision(18, 8);
// Комісія: менші суми, менша точність
builder.Property(t => t.Fee)
.HasPrecision(10, 2);
// Податок: аналогічно до суми
builder.Property(t => t.TaxAmount)
.HasPrecision(14, 4);
}
}
Генерований DDL (SQL Server):
CREATE TABLE [FinancialTransactions] (
[Id] INT NOT NULL IDENTITY,
[Amount] DECIMAL(14, 4) NOT NULL,
[ExchangeRate] DECIMAL(18, 8) NOT NULL,
[Fee] DECIMAL(10, 2) NOT NULL,
[TaxAmount] DECIMAL(14, 4) NOT NULL,
CONSTRAINT [PK_FinancialTransactions] PRIMARY KEY ([Id])
);
Розуміння precision і scale критично для коректного зберігання фінансових даних:
Тобто decimal(14, 4) може зберегти максимум 9999999999.9999 — десять цифр до коми і чотири після. Якщо спробувати зберегти 99999999999.9999 (одинадцять цифр до коми) — база поверне помилку переповнення.
decimal(19, 4) або навіть decimal(19, 6) — це відповідає стандарту ISO 4217 і покриває більшість реальних фінансових сценаріїв. Ніколи не використовуйте double або float для грошей — вони мають помилки округлення через бінарне представлення.Value Converter (конвертер значень) — це пара функцій: одна перетворює C#-значення у значення для збереження у БД, друга — значення з БД назад у C#. EF Core викликає їх автоматично при читанні та запису.
Це потужніший інструмент, ніж HasColumnType: конвертер не просто каже «який SQL-тип використати», він трансформує саме значення.
// Повна форма: явний ValueConverter<TModel, TProvider>
// TModel — тип у C# (ваш клас або enum)
// TProvider — тип у БД (string, int, byte[], тощо)
var converter = new ValueConverter<OrderStatus, string>(
// convertToProviderExpression: C# → БД
v => v.ToString(),
// convertFromProviderExpression: БД → C#
v => Enum.Parse<OrderStatus>(v)
);
Ця пара лямбд — серце будь-якого конвертера. Перша викликається при SaveChanges(), друга — при матеріалізації запиту.
Fluent API надає HasConversion<T>() і HasConversion(converter) — зручний спосіб налаштувати конвертер без явного створення об'єкту ValueConverter.
Зберігання enum як рядка — одне з найпоширеніших застосувань конвертерів. Порівняйте:
-- Без конвертера: числа незрозумілі
SELECT * FROM Orders WHERE Status = 2; -- 2 — це що?
-- З конвертером на string:
SELECT * FROM Orders WHERE Status = 'Shipped'; -- зрозуміло!
public enum OrderStatus
{
Pending,
Processing,
Shipped,
Delivered,
Cancelled,
Refunded
}
public class Order
{
public int Id { get; set; }
public OrderStatus Status { get; set; }
public string CustomerName { get; set; } = string.Empty;
public decimal TotalAmount { get; set; }
}
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.HasKey(o => o.Id);
// HasConversion<string>: enum → рядок, рядок → enum
builder.Property(o => o.Status)
.HasConversion<string>()
.HasMaxLength(50);
builder.Property(o => o.CustomerName)
.IsRequired()
.HasMaxLength(200);
builder.Property(o => o.TotalAmount)
.HasPrecision(14, 2);
}
}
Генерований SQL для INSERT:
INSERT INTO [Orders] ([CustomerName], [Status], [TotalAmount])
VALUES ('John Doe', 'Shipped', 150.00);
Тепер при перейменуванні члену enum (наприклад, Shipped → InTransit) — потрібна міграція даних, але принаймні дані залишаються людиночитаємими і не ламаються від зміни порядку членів. Числові enum-значення ламаються при будь-яких структурних змінах enum.
Якщо вам потрібен контроль над тим, яке значення зберігається (наприклад, скорочений рядок "pnd" замість "Pending"):
public class OrderStatusConverter : ValueConverter<OrderStatus, string>
{
private static readonly Dictionary<OrderStatus, string> _toDb = new()
{
[OrderStatus.Pending] = "pnd",
[OrderStatus.Processing] = "prc",
[OrderStatus.Shipped] = "shp",
[OrderStatus.Delivered] = "dlv",
[OrderStatus.Cancelled] = "cnl",
[OrderStatus.Refunded] = "rfd"
};
private static readonly Dictionary<string, OrderStatus> _fromDb =
_toDb.ToDictionary(kvp => kvp.Value, kvp => kvp.Key);
public OrderStatusConverter() : base(
v => _toDb[v],
v => _fromDb[v])
{ }
}
// Використання:
builder.Property(o => o.Status)
.HasConversion(new OrderStatusConverter())
.HasMaxLength(3)
.IsUnicode(false); // ASCII достатньо для кодів
Цей підхід дає стабільні значення у БД: перейменування enum-члену не змінює збережене значення "shp". Ціна — потрібно підтримувати словники синхронізованими.
Strongly Typed ID — патерн, що замінює примітивний ключ (int, Guid) на окремий тип-обгортку. Це дозволяє компілятору відловлювати помилки на кшталт «передав OrderId там, де очікувався CustomerId».
// Без Strongly Typed IDs: компілятор не помітить помилку
public async Task<Order?> GetOrdersByCustomer(int orderId, int customerId)
{
// ...
}
// Виклик з переставленими аргументами — компілятор мовчить!
var order = await GetOrdersByCustomer(customerId, orderId);
// Strongly Typed IDs через readonly struct
public readonly struct CustomerId : IEquatable<CustomerId>
{
public int Value { get; }
public CustomerId(int value)
{
if (value <= 0) throw new ArgumentException("CustomerId must be positive", nameof(value));
Value = value;
}
public bool Equals(CustomerId other) => Value == other.Value;
public override bool Equals(object? obj) => obj is CustomerId other && Equals(other);
public override int GetHashCode() => Value.GetHashCode();
public override string ToString() => $"Customer#{Value}";
public static implicit operator int(CustomerId id) => id.Value;
public static explicit operator CustomerId(int value) => new(value);
}
public readonly struct OrderId : IEquatable<OrderId>
{
public int Value { get; }
public OrderId(int value) => Value = value;
public bool Equals(OrderId other) => Value == other.Value;
public override bool Equals(object? obj) => obj is OrderId other && Equals(other);
public override int GetHashCode() => Value.GetHashCode();
public static implicit operator int(OrderId id) => id.Value;
public static explicit operator OrderId(int value) => new(value);
}
public class Customer
{
public CustomerId Id { get; set; }
public string Name { get; set; } = string.Empty;
public ICollection<Order> Orders { get; set; } = new List<Order>();
}
public class Order
{
public OrderId Id { get; set; }
public CustomerId CustomerId { get; set; } // FK через Strongly Typed ID
public Customer Customer { get; set; } = null!;
public decimal TotalAmount { get; set; }
}
Конфігурація конвертерів:
public class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{
public void Configure(EntityTypeBuilder<Customer> builder)
{
// Конвертер: CustomerId ↔ int
builder.HasKey(c => c.Id);
builder.Property(c => c.Id)
.HasConversion(
id => id.Value, // CustomerId → int (у БД)
value => new CustomerId(value) // int → CustomerId (з БД)
);
builder.Property(c => c.Name)
.IsRequired()
.HasMaxLength(200);
}
}
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.HasKey(o => o.Id);
builder.Property(o => o.Id)
.HasConversion(
id => id.Value,
value => new OrderId(value)
);
// FK теж потребує конвертера
builder.Property(o => o.CustomerId)
.HasConversion(
id => id.Value,
value => new CustomerId(value)
);
builder.HasOne(o => o.Customer)
.WithMany(c => c.Orders)
.HasForeignKey(o => o.CustomerId);
builder.Property(o => o.TotalAmount)
.HasPrecision(14, 2);
}
}
Тепер компілятор захищає від переплутування ID:
// Без Strongly Typed IDs:
void Process(int orderId, int customerId) { ... }
Process(customerId, orderId); // ← мовчить! Помилка лише в рантаймі
// Зі Strongly Typed IDs:
void Process(OrderId orderId, CustomerId customerId) { ... }
Process(customerId, orderId); // ← помилка компіляції! CustomerId ≠ OrderId
До появи нативних JSON Columns (EF Core 7+) стандартним підходом було зберігання складних об'єктів як JSON-рядків через конвертер. Навіть зараз цей підхід корисний для simple values і конфігурацій.
public class UserPreferences
{
public string Theme { get; set; } = "light";
public string Language { get; set; } = "uk";
public bool EmailNotifications { get; set; } = true;
public List<string> FavoriteCategories { get; set; } = new();
public Dictionary<string, int> CustomSettings { get; set; } = new();
}
public class User
{
public int Id { get; set; }
public string Email { get; set; } = string.Empty;
// Складний об'єкт → зберігається як JSON у single column
public UserPreferences Preferences { get; set; } = new();
}
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.HasKey(u => u.Id);
builder.Property(u => u.Email)
.IsRequired()
.HasMaxLength(320);
// Конвертер: UserPreferences ↔ JSON string
builder.Property(u => u.Preferences)
.HasConversion(
prefs => JsonSerializer.Serialize(prefs, JsonOptions),
json => JsonSerializer.Deserialize<UserPreferences>(json, JsonOptions)
?? new UserPreferences()
)
.HasColumnType("nvarchar(max)");
}
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
}
UserPreferences, якщо він зберігається як JSON-рядок. Якщо ви зміните user.Preferences.Theme = "dark" — EF Core не побачить цієї зміни. Потрібен Value Comparer (розглядаємо у другій частині) або явне позначення стану через context.Entry(user).Property(u => u.Preferences).IsModified = true. Або використовуйте нативні JSON Columns (стаття 11).Конвертери чудово підходять для прозорого шифрування та дешифрування чутливих полів — персональних даних, токенів, PII (Personally Identifiable Information).
public class EncryptedStringConverter : ValueConverter<string, string>
{
public EncryptedStringConverter(string encryptionKey) : base(
v => Encrypt(v, encryptionKey),
v => Decrypt(v, encryptionKey))
{ }
private static string Encrypt(string plainText, string key)
{
using var aes = Aes.Create();
aes.Key = Convert.FromHexString(key);
aes.GenerateIV();
using var encryptor = aes.CreateEncryptor();
var plainBytes = Encoding.UTF8.GetBytes(plainText);
var cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
// Зберігаємо IV разом із шифротекстом: IV (16 bytes) + ciphertext
var result = new byte[aes.IV.Length + cipherBytes.Length];
aes.IV.CopyTo(result, 0);
cipherBytes.CopyTo(result, aes.IV.Length);
return Convert.ToBase64String(result);
}
private static string Decrypt(string cipherBase64, string key)
{
var fullCipher = Convert.FromBase64String(cipherBase64);
using var aes = Aes.Create();
aes.Key = Convert.FromHexString(key);
// Перші 16 байт — IV
var iv = fullCipher[..16];
var cipherBytes = fullCipher[16..];
aes.IV = iv;
using var decryptor = aes.CreateDecryptor();
var plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
return Encoding.UTF8.GetString(plainBytes);
}
}
public class PatientRecord
{
public int Id { get; set; }
public string PatientName { get; set; } = string.Empty; // Шифруємо
public string SocialSecurityNumber { get; set; } = string.Empty; // Шифруємо
public string Diagnosis { get; set; } = string.Empty; // Шифруємо
public DateTime BirthDate { get; set; }
}
public class PatientRecordConfiguration : IEntityTypeConfiguration<PatientRecord>
{
private readonly string _encryptionKey;
public PatientRecordConfiguration(string encryptionKey)
{
_encryptionKey = encryptionKey;
}
public void Configure(EntityTypeBuilder<PatientRecord> builder)
{
var converter = new EncryptedStringConverter(_encryptionKey);
builder.Property(p => p.PatientName)
.HasConversion(converter)
.HasMaxLength(500); // Шифрований текст довший за початковий
builder.Property(p => p.SocialSecurityNumber)
.HasConversion(converter)
.HasMaxLength(500);
builder.Property(p => p.Diagnosis)
.HasConversion(converter)
.HasColumnType("nvarchar(max)");
}
}
EF Core постачає набір вбудованих конвертерів у просторі імен Microsoft.EntityFrameworkCore.Storage.ValueConversion. Їх не потрібно писати самостійно — просто вказати:
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
// EnumToStringConverter<TEnum>: enum → string і назад
builder.Property(e => e.Status)
.HasConversion(new EnumToStringConverter<OrderStatus>());
// BoolToZeroOneConverter<TProvider>: bool → 0/1 як числовий тип
builder.Property(e => e.IsActive)
.HasConversion(new BoolToZeroOneConverter<int>());
// BoolToStringConverter: bool → кастомні рядки ("Y"/"N", "true"/"false")
builder.Property(e => e.IsVerified)
.HasConversion(new BoolToStringConverter("N", "Y"))
.HasMaxLength(1)
.IsUnicode(false);
// DateTimeKindValueConverter: додає Kind до DateTime при читанні
// Корисно для провайдерів, що не зберігають Kind
builder.Property(e => e.CreatedAt)
.HasConversion(new DateTimeKindValueConverter(DateTimeKind.Utc));
// TimeSpanToTicksConverter: TimeSpan → long (ticks)
builder.Property(e => e.Duration)
.HasConversion(new TimeSpanToTicksConverter());
// TimeSpanToStringConverter: TimeSpan → "hh:mm:ss" рядок
builder.Property(e => e.WorkingHours)
.HasConversion(new TimeSpanToStringConverter());
EnumToStringConverter<T>
HasMaxLength.EnumToNumberConverter<TEnum, TNumber>
BoolToZeroOneConverter<T>
bool → 0/1 у вказаному числовому типі T. Зворотній: 0 → false, інше → true.BoolToStringConverter
bool → кастомні рядки. Конструктор приймає falseValue та trueValue.DateTimeKindValueConverter
DateTimeKind при читанні з БД. Корисно для SQLite, що не зберігає Kind.TimeSpanToTicksConverter
TimeSpan → long (число тіків). Компактне, точне. Не людиночитаємо у БД.TimeSpanToStringConverter
TimeSpan → рядок формату "hh:mm:ss.fffffff". Людиночитаємо, але займає більше місця.NumberToBytesConverter<T>
Якщо є тип (наприклад, enum або Strongly Typed ID), що зустрічається у багатьох сутностях, реєструвати конвертер у кожній конфігурації — це повторення. EF Core підтримує глобальні конвертери через HasConversion у OnModelCreating разом з modelBuilder.Properties<T>():
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Застосовуємо конвертер до ВСІХ властивостей типу OrderStatus у ВСІХ сутностях
modelBuilder.Properties<OrderStatus>()
.HaveConversion<string>()
.HaveMaxLength(50);
// Застосовуємо до всіх DateTime: додаємо Kind = Utc
modelBuilder.Properties<DateTime>()
.HaveConversion(new DateTimeKindValueConverter(DateTimeKind.Utc));
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
modelBuilder.Properties<CustomerId>().HaveConversion(...).Завдання 1.1: Виправлення типів
Є клас BlogPost з такими властивостями:
public class BlogPost
{
public int Id { get; set; }
public string Title { get; set; } = "";
public string Content { get; set; } = "";
public decimal ViewRating { get; set; }
public DateTime PublishedAt { get; set; }
public PostStatus Status { get; set; }
}
public enum PostStatus { Draft, Published, Archived }
За допомогою Fluent API налаштуйте:
Title — максимум 300 символів, обов'язковеContent — nvarchar(max), необов'язковеViewRating — decimal(5, 2) (від 0.00 до 999.99)Status — зберігати як рядокЗавдання 1.2: Конвертер bool
Є legacy-база з таблицею Users, де IsActive зберігається як CHAR(1): 'Y' або 'N'. Напишіть конфігурацію для entity User, що правильно маппить bool IsActive на цей формат.
Завдання 1.3: TimeSpan у базі
Маєте клас VideoLesson з public TimeSpan Duration. Налаштуйте зберігання у вигляді цілого числа секунд (long). Підказка: TimeSpan ↔ long через TotalSeconds.
Завдання 2.1: Strongly Typed ID
Для інтернет-магазину реалізуйте Strongly Typed IDs для ProductId і CategoryId (обидва int-базовані). Переконайтесь, що:
CategoryId до ProductIdint у БДProduct.CategoryId коректно маппитьсяЗавдання 2.2: Кастомний enum-конвертер
Є PaymentMethod enum: CreditCard, DebitCard, BankTransfer, Cryptocurrency. У базі дані зберігаються як скорочені коди: "CC", "DC", "BT", "CR". Реалізуйте конвертер, що підтримує такий маппінг. Що станеться, якщо в БД зустрінеться невідомий код?
Завдання 2.3: Глобальний конвертер для DateTime
У проєкті є 5 різних entity, кожне з полями CreatedAt та UpdatedAt типу DateTime. Налаштуйте глобальний конвертер, що при читанні з БД гарантує DateTimeKind.Utc для всіх DateTime-властивостей.
Завдання 3.1: Захист PII
Реалізуйте повну систему захисту персональних даних для PatientRecord:
FullName, PhoneNumber, Email мають зберігатися у зашифрованому виглядіApplyConfigurationsFromAssembly — придумайте, як передати ключ у конфігураціюЗавдання 3.2: JSON-конфігурація
Розробіть клас ApplicationSettings з полями: NotificationEmails (список рядків), FeatureFlags (словник bool), RetryPolicy (об'єкт з MaxRetries та DelayMs). Збережіть весь об'єкт як JSON у один стовпець nvarchar(max). Обговоріть: коли цей підхід кращий за нативні JSON Columns (стаття 11)?
У цій частині ми розглянули фундаментальні інструменти конфігурації властивостей:
decimal без HasPrecision — це завжди ризик.HasColumnType — точний рядковий SQL-тип. Перевизначає HasMaxLength.HasPrecision — семантичне керування decimal(p, s). Провайдер сам генерує правильний DDL.modelBuilder.Properties<T>() для застосування конвертера до всіх властивостей одного типу.У другій частині ми розглянемо Value Comparers (чому вони критичні для change tracking), Value Generators, Default Values, Computed Columns, Shadow Properties, Backing Fields та Temporal Tables.
Зв'язки Advanced — Many-to-Many та Складні Сценарії
Глибокий розбір Many-to-Many зв'язків в EF Core — implicit і explicit join entity, skip navigations, alternate keys, composite FK, shadow FK, backing fields, polymorphic associations як антипатерн та правильні альтернативи, складні графи об'єктів.
Властивості — Value Comparers, Generators, Shadow Properties (Частина 2)
Продовження розбору конфігурації властивостей EF Core — Value Comparers для правильного Change Tracking, Value Generators, Default Values, Computed Columns, Shadow Properties, Backing Fields та Temporal Tables.