Як я мігрував з EF 6 до EF Core 5
Tags: dotnet, tutorial, ukrainian, efcore
Published: 2021-10-26 11:57:37

  Часом буває так, що доводиться працювати з проектом, який використовує старі технології, а нам, як розробникам, хочеться використовувати щось нове, модне, що всі зараз обговорюють. Натомість в клієнта свої цілі - додавання нового функціоналу, який допоможе розвивати бізнес. В цій статті я опишу яким чином мені вдалось задовольнити цілі розробників і клієнта та мігрувати з EF 6 до EF Core 5. В аплікації використовується підхід `code-first`.
 
## Використання EF 6 та EF Core 5 паралельно

  З точки зору даних, як EF 6 так і EF Core 5 є мапером, який конвертує представлення даних з бази до коду або навпаки. Виходячи з цього, ми можемо реалізувати ще один мапер, використовуючи EF Core 6.
  Цей гібридний підхід можна реалізувати досить швидко. Для цього необхідно зробити наступні кроки.

### Виділення моделей даних в окремий проект

 Так як наші моделі даних, класи, залишаються незмінними при обох підходах, їх можна виділити в окремий проект. Тепер ми можемо їх використовувати в обох підходах. Щоб перевірити чи ми випадково нічого не забули/змінили можна спробувати додати міграцію в EF 6. Якщо вона пуста, то ми нічого не змінили.
```
EntityFramework6\add-migration <my_test_migration>
```

### Створення EF 5 Core контексту бази даних

 Створюємо новий контекст бази даних використовуючи EF Core 5, додаємо існуючі моделі, які ми в попередньому кроці виділили в окремий проект і залишилось тільки додати конфігурацію таблиць. Конфігурація в нас описана за допомогою `Data Annotation` та `Fluent API`. `Data Annotation` частина перевикористовуєтсья, оскільки знахотиься в проекті з моделями даних. `Fluent API` частина конфігурації в EF Core 5 відрізняється від EF 6. Тому ми реалізовуємо її для контексту.
> З поточним налаштуванням ми вже можемо новий API реалізовувати на EF Core 5 context чи разом з EF 6 context але поки **тільки для запитів на читання**.

### А як бути з операціями модифікації?

З операція модифікації ~~трохи складніше~~ - необхідно забезпечити транзакційність, якщо ми використовуємо одразу два контексти (хочемо перевикористати існуючий, добре протестований код). Оскільки EF 6 та EF Core 5 використовують одні примітиви для транзакцій - `DbTransaction`, то ми можемо в EF 6 реалізувати метод, який повертає транзакцію, а в EF Core 5 метод який використовує її. Ось ми і забезпечили транзакційність в двох контекстах.
```csharp
public DbTransaction BeginTransaction() // EF 6
public UseTransaction(DbTransaction transaction) //EF Core 5
```
  У випадку з одним контекстом наша робота не зміниться, просто викликаємо метод `SaveChangesAsync()`

### А що з міграціями?

В нас джерело даних одне, отже і за міграції повинен відповідати один підхід. Наша ціль використовувати EF Core 5, отже, міграції будемо запускати використовуючи новий підхід. Для реалізації необхідно зробити наступні кроки:
* Додати початкову міграцію в EF Core 5 - яка повторить схему існуючої бази даних `Add-Migration Init-Schema`
* Додати міграцію до EF 6 - Яка створить таблицю `__EFMigrationsHistory` і додасть `Init-Schema` рекорд в цю таблицю. Це нам необхідно, щоб була можливість створити нову бази даних з міграцій.
```csharp
public override void Up()
{
    Sql(
        @"CREATE TABLE [dbo].[__EFMigrationsHistory](
	    [MigrationId] [nvarchar](150) NOT NULL,
	    [ProductVersion] [nvarchar](32) NOT NULL
        )
        go
        INSERT INTO __EFMigrationsHistory
        VALUES('20210121161711_Init-Schema', '5.0.3')"
    );
}
```
* І тепер достатньо запустити оновлення бази дани `EntityFramework6\update-database`
* EF Core 5 міграції готові до використання. Можемо видалити старі міграції з коду та бази даних.

## API тести

  Тепер ми можемо малими кроками замінювати окремі запити до бази з EF 6 до EF Core 5. Підхід який я застосовув `Red-Green-Refactor`. Це працює так - ми пишемо API тест використовуючи Asp Net Core TestHost, далі замінюємо запит(ти) з EF 6 на EF Core 5 і знову запускаємо тест.

## Виправлення помилок
  Впродовж міграції я наткнувся на кілька проблем.
1. В проекті використовується `LinqKit` для EF 6, але є відповідник також для EF Core 5 - `LinqKit.Microsoft.EntityFrameworkCore`
* Метод `DefaultIfEmpty()` не працює в FE Core. Реалізація змінилась з
```csharp
var currentHours = await ctx.SomeEntity
  .Select(x => x.Hours)
  .DefaultIfEmpty()
  .SumAsync();
```
на наступне
```csharp
var currentHours = await ctx.SomeEntity
  .SumAsync(x => (double?)x) ?? default;
```
* Метод `Concat()` - використовується в проекті при побудові проекцій і після міграції необхідно додавати `.AsQurable()` до **Navigation properties**
```csharp
entity.InstancesTo
  .AsQueryable()
  .Where(x => x.Name != name)
  .Concat(entity.InstancesFrom.AsQueryable())
```

## Посилання
1. https://www.oreilly.com/library/view/modern-c-programming/9781941222423/f_0054.html
2. https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-5.0
3. https://github.com/scottksmith95/LINQKit
4. https://stackoverflow.com/questions/59555696/defaultifempty-exception-bug-or-limitation-with-ef-core