Konfiguracja Entity Framework 7 / Code-First -

Konfiguracja Entity Framework 7 / Code-First

Obiecałem, że nowy wpis o Aurorze pojawi się do niedzieli i słowa dotrzymałem 😀 W zasadzie wpis planowałem na wczoraj, ALE… konfiguracja Entity Frameworka 7 przysporzyła mi duuużo więcej problem niż zakładałem. Dodam jeszcze, że ogólnie kod Aurory będzie wyprzedzał znacznie wpisy, dlatego jeżeli ktoś jest zainteresowany jak się sprawy mają to zapraszam na githuba. A teraz do rzeczy.

Cały proces rozpoczynamy od zainstalowania EF7 w projektach/warstwach gdzie jest to konieczne. Proces odbywa się poprzez dodanie zależności w pliku project.jsonPrzykładowo w warstwie DataAccess wygląda on tak:

{
  "version": "1.0.0-*",
  "description": "Aurora.DataAccess Class Library",
  "authors": [ "dpawl" ],
  "tags": [ "" ],
  "projectUrl": "",
  "licenseUrl": "",
 
  "dependencies": {
    "Aurora.Infrastructure": "1.0.0-*",
    "Autofac.Extensions.DependencyInjection": "4.0.0-rc1-177",
    "EntityFramework.Commands": "7.0.0-rc1-final",
    "EntityFramework.MicrosoftSqlServer": "7.0.0-rc1-final",
    "EntityFramework.Core": "7.0.0-rc1-final",
    "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-rc1-final"
  },
 
  "commands": {
    "ef": "EntityFramework.Commands"
  },
 
  "frameworks": {
    "dnx451": { },
    "dnxcore50": {
      "dependencies": {
        "Microsoft.CSharp": "4.0.1-beta-23409",
        "System.Collections": "4.0.11-beta-23409",
        "System.Linq": "4.0.1-beta-23409",
        "System.Runtime": "4.0.21-beta-23409",
        "System.Threading": "4.0.11-beta-23409"
      }
    }
  }
}

 

Po zapisaniu pliku referencje powinny zostać dodane do naszego projektu. Btw to był chyba pierwszy problem jaki napotkałem, ponieważ domyślnie w tym pliku wszystkie frameworki są rejestrowane jako dotnet54 , a nie dnxcore50, co owocowało pięknymi errorami. Następnie zabieramy się za tworzenie tabel. Moje będą znajdować się w warstwie Infrastructure (wspomniałem o niej w pierwszym poście o Aurorze). Po dopisaniu referencji utworzyłem folder Entities, który zawiera podkatalog Interfaces. Teraz przedstawię Wam moją koncepcję dotyczącą tabel posługując się pseudo diagramem klas:

 

AuroraDbScheme

Jak widać klasą bazową każdej encji będzie klasa InternalEntity. Implementuje on dwa interfejsy: ISoftDeletable oraz IInternalEntity<int>. Pierwszy z nich dostarczy pole IsActive oraz metodę  Delete()które posłużą nam do implementacji soft delete (miękkiego usuwania?). Idea jest prosta, zamiast usuwać rekord z bazy danych, oznaczamy jego usunięcie poprzez zmianę flagi (w tym przypadku IsActive). Przy tworzeniu zapytania dołączamy programowy warunek, który zapewnia nam pobranie jedynie nieusuniętych rekordów. Dzięki temu zachowujemy ciągłość danych (przydatne do robienia np. raportów) jednocześnie zapewniając użytkownikom aplikacji wrażenie usunięcia danych. Spotkałem się z opinią, że nie jest to bezpieczne rozwiązanie, ale to już temat na inny wpis. Drugi interfejs dostarcza pole Id dla naszych tabel, którego typ oznacza TKey. Pytanie po co mi interfejs skoro i tak wszystkie tabele będą posiadały klucz typu Integer? Wszystko się zaraz wyjaśni 😉 Póki co, implementacja klasy InternalEnity:

 

public abstract class InternalEntity : IInternalEntity<int>, ISoftDeletable
    {
        [Key]
        public int Id { get; set; }
 
        public bool IsActive { get; private set; }
 
        protected InternalEntity()
        {
            this.IsActive = true;
        }
 
        void ISoftDeletable.Delete()
        {
            this.IsActive = false;
        }
    }

Tu jeszcze małe dopowiedzenie. Metoda Delete jest zaimplementowana explicit. Oznacza to, że nie będzie ona widoczna dopóki nasz obiekt nie zostanie zrzutowany na ISoftDeletable. W taki sposób można “ukrywać” implementacje metod, które znajdują się w interfejsie, choć głównie jest to stosowane w przypadku gdy implementujemy kilka interfejsów posiadających metody o takich samych nazwach. Po drugie sama klasa InternalEntity jako abstrakcyjna nie zostanie utworzona finalnie w bazie danych.

Przejdźmy dalej, mianowicie do tabeli użytkowników aplikacji. Wygląda ona następująco:

[Table("Users",Schema = "usr")]
    public class UserEntity : IdentityUser, IInternalEntity<string>, ISoftDeletable, ILockable
    {
        public bool IsActive { get; private set; }
        
        public bool IsLocked { get; private set; }

        public UserEntity()
        {
            this.IsActive = true;
            this.IsLocked = false;
        }
 
        void ILockable.Lock()
        {
            this.IsLocked = true;
        }
 
        void ILockable.Unlock()
        {
            this.IsLocked = false;
        }
 
        void ISoftDeletable.Delete()
        {
            this.IsActive = false;
        }
 
    }

 

Zaraz, zaraz – przecież wszystkie tabele miały dziedziczyć z klasy InternalEntity. WTF ? No własnie, będzie parę wyjątków. Nie będzie się to tyczyć tabel, które zostaną nam dostarczone przez ASP.NET Identity:

  • AspNetRoleClaims
  • AspNetRoles
  • AspNetUserClaims
  • AspNetUserLogins
  • AspNetUsers

Są to tabele zorientowane na zarządzanie użytkownikami. Dostajemy więc gotową tabele dla użytkowników, ról oraz tabele przydatne przy implementowaniu logowania np. przez portale społecznościowe. Nie oznacza to jednak, że nie możemy  tych tabel modyfikować/rozszerzać. Powyższy przykład własnie to robi dodając do naszej tabeli to, czego nie mogliśmy dostarczyć z klasy InternalEntity (w C# nie ma wielokrotnego dziedziczenia). Dodatkowo nasza klasa userów implementuje jeszcze jeden interfejs – ILockable, który dostarczy nam pole oraz metodę, dzięki której będziemy mogli oznaczać użytkowników jako zablokowanych. Aha, jeszcze jedno. Tabele userów implementuje także wcześniej wspomniany interfejs IInternalEntity, jednak tutaj jako typ podany został string. Dlaczego? Ano dlatego, że tabele Identity domyślnie posiadają klucze główne o typie string (w bazie jako nvarchar(450) ). Dzięki temu jednak, że implementujemy IInternalEntity posiadamy bardzo ważną informację – jeśli klasa implementuje IInternalEntity to jest ona tabelą. Przyda nam się to za chwilę…

Przejdźmy teraz do implementacji kontekstu, bez zbędnego pitu pitu:

public class AuroraContext : IdentityDbContext<UserEntity>
    {
        protected override void OnConfiguring(DbContextOptionsBuilder options)
        {
            options.UseSqlServer(@"Data Source=DESKTOP-JQOI1KG;database=Aurora;Integrated Security=True");
        }
 
        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.Entity<InternalEntity>().Property<DateTime>("CreatedDate");
            builder.Entity<InternalEntity>().Property<DateTime>("UpdatedDate");
            base.OnModelCreating(builder);
        }
 
        public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
        {
            ChangeTracker.DetectChanges();
 
            var modifiedEntities = ChangeTracker.Entries<InternalEntity>().Where(e => e.State == EntityState.Added || e.State == EntityState.Modified).ToList();
 
            foreach (var entity in modifiedEntities)
            {
                if (entity.State == EntityState.Added)
                    entity.Property("CreatedDate").CurrentValue = DateTime.UtcNow;
 
                entity.Property("UpdatedDate").CurrentValue = DateTime.UtcNow;
            }
 
            return await base.SaveChangesAsync(cancellationToken);
        }
    }

Po pierwsze – widać, że nasz kontekst dziedziczy z IdenetityDbContext i właśnie dzięki temu dodane zostaną wcześniej opisane tabele. Gdyby ktoś ich nie chciał, może dziedziczyć z DbContext. Dalej znajduje się przesłonięta metoda OnConfiguring. W niej znajdują się definicje connection stringa. Jest to nowość, gdyż wcześniej znajdowała się w pliku Web.config. Następnie, przesłonięta zostaje metoda OnModelCreating, a w niej kolejna nowość – shadow properties. Są to kolumny, które dołączamy do naszych tabel bez ich jawnej implementacji w samych klasach. Jak widać w tym przypadku, tabela InternalEntity zyska dwa pola, które będą informowały o dacie dodania rekordu oraz o dacie jego modyfikacji. Natomiast ich ustawienie odbywa się w ostatniej, przesłoniętej metodzie tj. SaveChagesAsync. Zostanie ona wywołana za każdym razem kiedy będziemy zapisywać zmiany. “Wychwycone” zostaną wszystkie rekordy, które zostały dodane lub zmodyfikowane, a następnie uzupełnione zostaną kolumny powstałe z shadow properties.

 


EDIT: Okazało się jednak, że takie ustawienie shadow properties nie skutkuje dodaniem dwóch pól do każdej tabeli, która dziedziczy po InternalEntity. Zamiast tego EF tworzył sobie tabelę w bazie danych z tymi polami, natomiast wszystkie “dzieci” ich nie posiadały. Wobec tego na razie cała metoda OnModelCreating została zakomentowana, a pola zostały umieszczone w interfejsie IAuditable, który klasa InternalEntity implementuje. Reszta pozostała bez zmian.


 

Na zakończenie omówmy jeszcze migracje. W tym celu musimy otworzyć konsolę w folderze z naszym projektem DataAccess, a następnie wpisać:

dnu restore

Kiedy operacja się zakończy wpisujemy:

dnx ef

Naszym oczom powinien pojawić się przekozacki rysunek jednorożca 😀

dnxef

 

W dostępnych komendach mamy m.in. migrations. Aby wyświetlić wszystkie opcje jakie posiadamy, wpisujemy:

dnx ef migrations –help

Wygląda to tak:

migrationsdnx

Nas interesuje póki co dodanie naszej pierwszej migracji. Wobec tego wpisujemy:

dnx ef migrations add Initial

Po chwili w naszym projekcie DataAcces powinien pojawić się katalog Migrations zawierający migrację oraz snapshota:

migrations

Teraz ważna sprawa ! 

W EF7 w przeciwieństwie do EF6 nie ma dostępnej metody Seed (z klasy np. DropCreateDatabaseIfNotExist). Dla osób, które nie wiedzą o co kaman była to metoda, w której można było naszą bazę uzupełnić przykładowymi danymi podczas procesu jej tworzenia. W nowej wersji odbywa się to w klasie Startup,  co jak dla mnie jest dziwne, no ale pewnie był ku temu powód. Generalnie rodzi to pewną komplikację. Kiedy uruchomiłem projekt dostawałem ciągle error o tym, że nie można podłączyć się do bazy danych. Jest to związane najprawdopodobniej z faktem, że EF7 w takiej postaci jak przedstawiłem nie generuje automatycznie bazy :/ Temat raczej pojawi się w sekcji “Problemy”, ponieważ jest to problem 😀 Postaram się zrobić takiego Seeda, który uruchomi się przy starcie aplikacji i może dzięki temu zacznie to działać tak jak powinno. Póki co hotfix jak sobie z tym poradzić. Użyjemy do tego celu migracji. Jak widać na wcześniejszym obrazku z konsoli, mamy dostępną opcję generowania skryptu SQL z migracji. Po wpisaniu:

dnx ef migrations script

Naszym oczom pojawi się skrypt SQL :

sql_migrations

Teraz wystarczy tylko podłączyć się do SQL Servera, dodać nową bazę danych, po czym wykonać skrypt. Dzięki temu aplikacja podłączy się do bazy, a my jesteśmy hepi 🙂

Na dziś to tyle. Wkrótce kolejne wpisy dotyczące architektury warstwy DataAccess. Zaimplementujemy sobie repozytoria i dowiemy się czym jest wzorzec Unit Of Work. A teraz idę degustować portera z browaru Kraftwerk 😀

 

You may also like...