Wprowadzenie

Tak jak wspomniałem w jednym z poprzednim wpisów – zaczniemy od podejścia database-first. Nauczymy się jak tworzyć klasy encji oraz klasy konsekstowe dla istniejącej bazy danych. W późniejszych wpisach będziemy się opierać na podejściu code-first.

Jak już doskonale wiecie EF Core nie zawiera w sobie kreatora z którego korzystaliśmy np. w Entity Framework 6. Musimy skorzystać z mechanizmu inżynierii wstecznej tworząc klasy encji i kontekstu na podstawie istniejącego schematu bazy danych.

Z pomocą przychodzi nam poniższe polecenie:

Scaffold-DbContext [-Connection] [-Provider] [-OutputDir] [-Context] [-Schemas>] [-Tables>] 
[-DataAnnotations] [-Force] [-Project] [-StartupProject] [<CommonParameters>]

Baza danych

Tym zagadnieniem nie będziemy zajmować się w tym wpisie. Każdy z Was dokładnie wie jak utworzyć nową bazę danych. Możecie również wykorzystać swoje istniejące projekty.

Przejdźmy do omówienia powyższego polecenia. Najważniejsze, z naszego punktu widzenia, jest połączenie się z odpowiednią bazą danych. W moim przypadku Scaffold-DbContext przyjmuje poniższą postać:

PM> Scaffold-DbContext "Server=xxx.webio.pl,port;database=nazwa_bazy_danych;Persist Security Info=True;User ID=user_id;password=user_password;" 
Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

Powyższe polecenie składa się z trzech części. Pierwsza z nich to lokalizacja serwera bazy danych wraz z informacją o nazwie i zabezpieczeniach. Tak jak wielokrotnie wspominałem w swoich projektach korzystam z Webio (link affiliacyjny). Informacje zawarte w connection-string są oczywiście poglądowe.

Druga część to informacja o dostawy usługi. Używam Microsoft SQL Server dlatego podałem nazwę odpowiedniej paczki: Microsoft.EntityFrameworkCore.SqlServer.

Ostatni parametr określa katalog do którego chcemy wygenerować wszystkie klasy. W naszym przypadku przyjmuje on nazwę Models.

Wykonanie powyższego polecenia skutkuje wygenerowaniem klasy kontekstowej oraz tworzy klasy dla każdej tabeli zdefiniowanej w naszej bazie danych: Klasy kontektowe oraz encji dla podejścia database-first Warto również zaznaczyć, że Fluent API wygenerował w utworzonej klasie kontekstowej specjalne konfiguracje w celu nadpisania domyślnych konwencji – szczegółowe omówienie w jednym z kolejnych wpisów.

Utworzony model bazuje na jednym z moich "projektów" w których używam ASP.NET Identity oraz dwóch prostych tabel celem gromadzenia danych. Spójrzcie jak wygląda wygenerowana klasa dla encji AspNetRole:

namespace EFCoreDbFirst.Models
{
    public partial class AspNetRole
    {
        public AspNetRole()
        {
            AspNetRoleClaims = new HashSet<AspNetRoleClaim>();
            AspNetUserRoles = new HashSet<AspNetUserRole>();
        }

        public string Id { get; set; }
        public string Name { get; set; }
        public string NormalizedName { get; set; }
        public string ConcurrencyStamp { get; set; }

        public virtual ICollection<AspNetRoleClaim> AspNetRoleClaims { get; set; }
        public virtual ICollection<AspNetUserRole> AspNetUserRoles { get; set; }
    }
}

Poniżej utworzona klasa kontekstowa (ApplicationDbContext), którą możemy wykorzystać do zapisywania oraz pobierania danych:

namespace EFCoreDbFirst.Models
{
    public partial class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext()
        {
        }

        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        public virtual DbSet<AspNetRole> AspNetRoles { get; set; }
        public virtual DbSet<AspNetRoleClaim> AspNetRoleClaims { get; set; }
        public virtual DbSet<AspNetUser> AspNetUsers { get; set; }
        public virtual DbSet<AspNetUserClaim> AspNetUserClaims { get; set; }
        public virtual DbSet<AspNetUserLogin> AspNetUserLogins { get; set; }
        public virtual DbSet<AspNetUserRole> AspNetUserRoles { get; set; }
        public virtual DbSet<AspNetUserToken> AspNetUserTokens { get; set; }
        public virtual DbSet<CompaniesDatum> CompaniesData { get; set; }
        public virtual DbSet<Company> Companies { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. 
#warning You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration
#warning see https://go.microsoft.com/fwlink/?linkid=2131148. 
#warning For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263.
// Tak jak widzicie EF Core ostrzega nas o potencjalnie niebezpiecznym użyciu connection string w naszym kodzie źródłowym
// W jednym z kolejnych wpisów utoworzymy aplikację w oparciu o podejście code-first. W tym wypadku zdefiniujemy
// nasze połączenie z bazą danych w pliku konfiguracyjnych w celu ochrony wrażliwych informacji.
                optionsBuilder.UseSqlServer("Server=xxx.webio.pl,port;database=nazwa_bazy_danych;Persist Security Info=True;
                    User ID=user_id;password=user_password");
            }
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.HasDefaultSchema("domyslna_nazwa_schematu");

            modelBuilder.Entity<AspNetRole>(entity =>
            {
                entity.HasIndex(e => e.NormalizedName, "RoleNameIndex")
                    .IsUnique()
                    .HasFilter("([NormalizedName] IS NOT NULL)");

                entity.Property(e => e.Name).HasMaxLength(256);

                entity.Property(e => e.NormalizedName).HasMaxLength(256);
            });

            modelBuilder.Entity<AspNetRoleClaim>(entity =>
            {
                entity.HasIndex(e => e.RoleId, "IX_AspNetRoleClaims_RoleId");

                entity.Property(e => e.RoleId).IsRequired();

                entity.HasOne(d => d.Role)
                    .WithMany(p => p.AspNetRoleClaims)
                    .HasForeignKey(d => d.RoleId);
            });

            modelBuilder.Entity<AspNetUser>(entity =>
            {
                entity.HasIndex(e => e.NormalizedEmail, "EmailIndex");

                entity.HasIndex(e => e.NormalizedUserName, "UserNameIndex")
                    .IsUnique()
                    .HasFilter("([NormalizedUserName] IS NOT NULL)");

                entity.Property(e => e.Email).HasMaxLength(256);

                entity.Property(e => e.NormalizedEmail).HasMaxLength(256);

                entity.Property(e => e.NormalizedUserName).HasMaxLength(256);

                entity.Property(e => e.UserName).HasMaxLength(256);
            });

            modelBuilder.Entity<AspNetUserClaim>(entity =>
            {
                entity.HasIndex(e => e.UserId, "IX_AspNetUserClaims_UserId");

                entity.Property(e => e.UserId).IsRequired();

                entity.HasOne(d => d.User)
                    .WithMany(p => p.AspNetUserClaims)
                    .HasForeignKey(d => d.UserId);
            });

            modelBuilder.Entity<AspNetUserLogin>(entity =>
            {
                entity.HasKey(e => new { e.LoginProvider, e.ProviderKey });

                entity.HasIndex(e => e.UserId, "IX_AspNetUserLogins_UserId");

                entity.Property(e => e.UserId).IsRequired();

                entity.HasOne(d => d.User)
                    .WithMany(p => p.AspNetUserLogins)
                    .HasForeignKey(d => d.UserId);
            });

            modelBuilder.Entity<AspNetUserRole>(entity =>
            {
                entity.HasKey(e => new { e.UserId, e.RoleId });

                entity.HasIndex(e => e.RoleId, "IX_AspNetUserRoles_RoleId");

                entity.HasOne(d => d.Role)
                    .WithMany(p => p.AspNetUserRoles)
                    .HasForeignKey(d => d.RoleId);

                entity.HasOne(d => d.User)
                    .WithMany(p => p.AspNetUserRoles)
                    .HasForeignKey(d => d.UserId);
            });

            modelBuilder.Entity<AspNetUserToken>(entity =>
            {
                entity.HasKey(e => new { e.UserId, e.LoginProvider, e.Name });

                entity.HasOne(d => d.User)
                    .WithMany(p => p.AspNetUserTokens)
                    .HasForeignKey(d => d.UserId);
            });

            modelBuilder.Entity<CompaniesDatum>(entity =>
            {
                entity.Property(e => e.TimeStamp).HasDefaultValueSql("('0001-01-01T00:00:00.0000000')");
            });

            OnModelCreatingPartial(modelBuilder);
        }

        partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
    }
}

Warto dodać, że przy wykorzystaniu polecenia Scaffold-DbContext nazwa klasy kontekstowej będzie zgodna z nazwą bazy danych podaną w poleceniu. W powyższym przykładzie (jak i w każdym swoim projekcie) dokonuje zawsze zmiany na ApplicationDbContext, która jest dla mnie czytelniejsza.

Kolejna istotna informacja dotyczy odtwarzania widoków przy pomocy inżynierii wstecznej. Taka funkcjonalność pojawiła się w EF Core 3.1 o czym możecie przeczytać tutaj: https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.x/#reverse-engineering-of-database-views

W przypadku procedur składowanych sytuacja jest nieco bardziej skomplikowana. W momencie przygotowywania wpisu społeczność ciągle czekała na tą funkcjonalność. Więcej możecie przeczytać tutaj: https://github.com/dotnet/efcore/issues/15105. Przeglądając jednak komenatrze możecie trafić na narzędzie SPToCore, którego celem jest osiągnięcie powyższej funkcjonalności – to jednak nie tematem tego wpisu.

DotNet CLI

W poprzednim wpisie wspomniałem również o poleceniu linii poleceń dotnet w celu wykonywania poleceń EF Core. Jeżeli preferujecie takie podejście możecie wykorzystać wiersz poleceń i wykonać poniższe polecenie:

dotnet ef dbcontext scaffold "Server=xxx.webio.pl,port;database=nazwa_bazy_danych;Persist Security Info=True;User ID=user_id;password=user_password;" 
Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

Od tego momentu wszelkie zmiany w modelu wymagają użycia poleceń migracji i aktualizacji schematu w celu zachowania zgodności kodu z postacią bazy danych. O tym będziemy szczegółowo rozmawiać w kolejnych wpisach.