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:
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 komentarze 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.