Przewodnik EF Core
dotnet new {template}
- utworzenie nowego projektu na podstawie wybranego szablonudotnet new {template} -o {output}
- utworzenie nowego projektu w podanym katalogudotnet restore
- pobranie bibliotek nuget na podstawie pliku projektudotnet build
- kompilacja projektudotnet run
- uruchomienie projektudotnet run {app.dll}
- uruchomienie aplikacjidotnet test
- uruchomienie testów jednostkowychdotnet run watch
- uruchomienie projektu w trybie śledzenia zmiandotnet test
- uruchomienie testów jednostkowych w trybie śledzenia zmiandotnet add {project.csproj} referencje {library.csproj}
- dodanie odwołania do bibliotekidotnet remove {project.csproj} referencje {library.csproj}
- usunięcie odwołania do bibliotekidotnet new sln
- utworzenie nowego rozwiązaniadotnet sln {solution.sln} add {project.csproj}
- dodanie projektu do rozwiązaniadotnet sln {solution.sln} remove {project.csproj}
- usunięcie projektu z rozwiązaniadotnet publish -c Release -r {platform}
- publikacja aplikacjidotnet publish -c Release -r win10-x64
- publikacja aplikacji dla Windowsdotnet publish -c Release -r linux-x64
- publikacja aplikacji dla Linuxdotnet publish -c Release -r osx-x64
- publikacja aplikacji dla MacOS
docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=yourStrong(!)Password' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest
docker exec -it <container_id|container_name> /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P <your_password>
public class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options)
:base(options)
{ }
public DbSet<Customer> Customers { get; set; }
}
private static void CreateDbTest()
{
string connectionString = "Server=127.0.0.1,1433;Database=mydb;User Id=sa;Password=P@ssw0rd";
// string connectionString = Configuration.GetConnectionString("MyConnectionString");
var optionsBuilder = new DbContextOptionsBuilder<MyContext>();
optionsBuilder.UseSqlServer(connectionString);
using (var context = new MyContext(optionsBuilder.Options))
{
bool created = context.Database.EnsureCreated();
System.Console.WriteLine(created);
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<MyContext>(options => options.UseSqlite("Data Source=blog.db"));
}
https://docs.microsoft.com/en-us/sql/azure-data-studio/download?view=sql-server-2017
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
Database | NuGet Package |
---|---|
SQL Server | Microsoft.EntityFrameworkCore.SqlServer |
SQLite | Microsoft.EntityFrameworkCore.SQLite |
MySQL | MySql.Data.EntityFrameworkCore |
PostgreSQL | Npgsql.EntityFrameworkCore.PostgreSQL |
SQL Compact | EntityFrameworkCore.SqlServerCompact40 |
In-memory | Microsoft.EntityFrameworkCore.InMemory |
dotnet ef
- weryfikacja instalacjidotnet ef migrations add {migration}
- utworzenie migracjidotnet ef migrations remove
- usunięcie ostatniej migracjidotnet ef migrations list
- wyświetlenie listy wszystkich migracjidotnet ef migrations script
- wygenerowanie skryptu do aktualizacji bazy danych do najnowszej wersjidotnet ef database update
- aktualizacja bazy danych do najnowszej wersjidotnet ef database update -verbose
- aktualizacja bazy danych do najnowszej wersji + wyświetlanie logudotnet ef database update {migration}
- aktualizacja bazy danych do podanej migracjidotnet ef database drop
- usunięcie bazy danychdotnet ef dbcontext info
- wyświetlenie informacji o DbContext (provider, nazwa bazy danych, źródło)dotnet ef dbcontext list
- wyświetlenie listy DbContextówdotnet ef dbcontext scaffold {connectionstring} Microsoft.EntityFrameworkCore.SqlServer -o Models
- wygenerowanie modelu na podstawie bazy danychAdd-Migration {migration}
- utworzenie migracjiRemove-Migration
- usunięcie ostatniej migracjiUpdate-Database -script
- wygenerowanie skryptu do aktualizacji bazy danych do najnowszej wersjiUpdate-Database
- aktualizacja bazy danych do najnowszej wersjiUpdate-Database -verbose
- aktualizacja bazy danych do najnowszej wersji + wyświetlanie loguUpdate-Database {migration}
- aktualizacja bazy danych do podanej Scaffold-DbContext {connectionstring} Microsoft. Models
- wygenerowanie modelu na podstawie bazy danychKlasa DbContext jest główną częścią Entity Framework. Instacja DbContext reprezentuje sesję z bazą danych.
Models.cs
public class Order
{
public int OrderId { get; set; }
public string OrderNumber { get; set; }
public DateTime OrderDate { get; set; }
public DateTime DeliveryDate? { get; set; }
public Customer Customer { get; set; }
}
public class Customer
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public bool IsDeleted { get; set; }
}
MyContext.cs
public class MyContext : DbContext
{
public DbSet<Customer> Customers { get; set; }
public DbSet<Order> Orders { get; set; }
public CustomersContext(DbContextOptions options)
: base(options)
{
}
}
var optionsBuilder = new DbContextOptionsBuilder<MyContext>();
optionsBuilder.UseSqlite("Data Source=blog.db");
using (var context = new MyContext(optionsBuilder.Options))
{
// do stuff
}
DbContext umożliwia następujące zadania:
Metoda | Użycie |
---|---|
ChangeTracker | Dostarcza informacje i operacje do śledzenie obiektów |
Database | Dostarcza informacje i operacje bazy danych |
Model | Zwraca metadane o encjach, ich relacjach i w jaki sposób mapowane są do bazy danych |
dotnet add package Microsoft.EntityFrameworkCore.Design
W przypadku gdy DbContext posiada parametr i nie uzywamy DI nalezy utworzyc fabrykę:
public class MyContextFactory : IDesignTimeDbContextFactory<MyContext>
{
public MyContext CreateDbContext(string[] args)
{
string connectionString = "Server=127.0.0.1,1433;Database=mydb;User Id=sa;Password=P@ssw0rd";
var optionsBuilder = new DbContextOptionsBuilder<MyContext>();
optionsBuilder.UseSqlServer(connectionString);
return new MyContext(optionsBuilder.Options);
}
}
Encja zawiera navigation property.
public class Order
{
public int OrderId { get; set; }
public string OrderNumber { get; set; }
public Customer Customer { get; set; } // Navigation property
}
public class Customer
{
public int CustomerId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Zamówienie zawiera referencje do navigation property typu klient. EF utworzy shadow property CustomerId w modelu koncepcyjnym, które będzie mapowane do kolumny CustomerId w tabeli Orders.
Encja zawiera kolekcję.
public class Order
{
public int OrderId { get; set; }
public string OrderNumber { get; set; }
}
public class Customer
{
public int CustomerId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public List<Order> Orders { get; set; }
}
W bazie danych będzie taki sam rezultat jak w przypadku konwencji 1.
Relacja zawiera navigation property po obu stronach. W rezultacie otrzymujemy połączenie konwencji 1 i 2.
public class Order
{
public int OrderId { get; set; }
public string OrderNumber { get; set; }
public Customer Customer { get; set; } // Navigation property
}
public class Customer
{
public int CustomerId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public List<Order> Orders { get; set; }
}
Konwencja z uzyciem wlasciwosci foreign key
public class Order
{
public int OrderId { get; set; }
public string OrderNumber { get; set; }
public int CustomerId { get; set; } // Foreign key property
public Customer Customer { get; set; } // Navigation property
}
public class Customer
{
public int CustomerId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public List<Order> Orders { get; set; }
}
public class Order
{
public int OrderId { get; set; }
public string OrderNumber { get; set; }
public Payment Payment { get; set; } // Navigation property
}
public class Payment
{
public int PaymentId { get; set; }
public decimal Amount { get; set; }
public int OrderId { get; set; }
public Order Order { get; set; }
}
Obecnie w EF Core nie ma domyslnej konwencji, która konfiguruje relację wiele-do-wielu. Trzeba uzyc Fluent Api.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.HasOne<Customer>()
.WithMany(c=>c.Orders)
.HasForeignKey(p=>p.CustomerId);
Alternatywnie mozna wyjsc od drugiej strony
modelBuilder.Entity<Customer>()
.HasMany(c=>c.Orders)
.WithOne(o=>o.Customer)
.HasForeignKey(o=>o.CustomerId);
}
modelBuilder.Entity<Customer>()
.HasMany(c=>c.Orders)
.WithOne(o=>o.Customer)
.HasForeignKey(o=>o.CustomerId)
.OnDelete(DeleteBehavior.Cascade);
Rodzaje:
modelBuilder.Entity<Order>()
.HasOne<Payment>()
.WithOne(p=>p.Order)
.HasForeignKey<Payment>(p=>p.PaymentId);
PMC
Microsoft.EntityFrameworkCore.Tools
Domyślnie wszystkie pobierane obiekty poprzez context są śledzone i dzięki temu po przy wywołaniu metody SaveChanges zmiany utrwalane są w bazie danych.
Domyślnie właściwośc ChangeTracker.AutoDetectChanges jest ustawiona na true.
W celu zwiększenia wydajności, zwłaszcza przy dodawaniu wielu encji, ustaw na false.
Pamiętaj o wywołaniu metody DetectChanges() przed SaveChanges()
private static void DetectChangesTest()
{
var optionsBuilder = new DbContextOptionsBuilder<MyContext>();
optionsBuilder.UseSqlite("Data Source=blog.db")
.EnableSensitiveDataLogging();
var context = new MyContext(optionsBuilder.Options);
context.ChangeTracker.AutoDetectChangesEnabled = false;
Customer customer = new Customer { Id = 5, FirstName = "Abc" };
context.Customers.Attach(customer);
Console.WriteLine(context.Entry(customer).State);
customer.FirstName = "Xyz";
Console.WriteLine(context.Entry(customer).State);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.Entry(customer).State);
}
using (var context = new MyContext())
{
var blogs = context.Customers
.AsNoTracking()
.ToList();
}
using (var context = new MyContext())
{
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var blogs = context.Blogs.ToList();
}
EF Core umożliwia śledzenie zmian na podstawie interfejsu INotifyPropertyChanged
public abstract class BaseEntity : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propname = "")
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}
}
public class Customer : BaseEntity
{
public int Id { get; set; }
private string firstname;
public string FirstName
{
get => firstname; set
{
firstname = value;
OnPropertyChanged();
}
}
public string LastName { get; set; }
}
public class MyContext : DbContext
{
public MyContext(DbContextOptions options) : base(options)
{
ChangeTracker.AutoDetectChangesEnabled = false;
}
public DbSet<Customer> Customers { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangedNotifications);
base.OnModelCreating(modelBuilder);
}
}
Console.WriteLine(
$"Tracked Entities: {context.ChangeTracker.Entries().Count()}");
foreach (var entry in context.ChangeTracker.Entries())
{
Console.WriteLine($"Entity: {entry.Entity.GetType().Name},
State: {entry.State.ToString()} ");
}
using (var context = new MyContext())
{
var blog = context.Customers
.Select(c =>
new
{
Customer = c,
Orders = b.Orders.Count()
});
}
TrackGraph to nowa koncepcja wprowadzona wraz z EF Core. Umozliwia przejscie po grafie i ustawienie stanu encji.
context.ChangeTracker.TrackGraph(order, e => e.Entry.State = EntityState.Added);
context.ChangeTracker.TrackGraph(order, e => {
if (e.Entry.IsKeySet)
{
e.Entry.State = EntityState.Unchanged;
}
else
{
e.Entry.State = EntityState.Added;
}
});
Metoda Attach() przyłącza odłączony graf encji i zaczyna go śledzić.
Metoda Attach() ustawia główną encję na stan Added niezależnie od tego, czy posiada wartość klucza. Jeśli encje dzieci posiadają wartość klucza wówczas zaznaczane są jako Unchanged, a w przeciwnym razie jako Added.
context.Attach(entityGraph).State = state;
Attach() | Root entity with Key value | Root Entity with Empty or CLR default value | Child Entity with Key value | Child Entity with empty or CLR default value |
---|---|---|---|---|
EntityState.Added | Added | Added | Unchanged | Added |
EntityState.Modified | Modified | Exception | Unchanged | Added |
EntityState.Deleted | Deleted | Exception | Unchanged | Added |
context.Entry(order).State = EntityState.Modified
Wyrażenie przyłącza encję do kontekstu i ustawia stan na Modified. Ignoruje wszystkie pozostałe encje.
Metody DbContext.Add() i DbSet.Add() przyłączają graf encji do kontekstu i ustawiają stan encji na Added niezależnie od tego czy posiadają wartość klucza czy też nie.
Method | Root entity with/out Key value | Root entity with/out Key |
---|---|---|
DbContext.Add | Added | Added |
Metoda Update() przyłącza graf encji do kontekstu i ustawia stan poszczególnych encji zależnie od tego czy jest ustawiona wartość klucza.
Update() | Root entity with Key value | Root Entity with Empty or CLR default value | Child Entity with Key value | Child Entity with empty or CLR default value |
---|---|---|---|---|
DbContext.Update | Modified | Added | Modified | Added |
Metoda Delete() ustawia stan głównej encji na Deleted.
Delete() | Root entity with Key value | Root Entity with Empty or CLR default value | Child Entity with Key value | Child Entity with empty or CLR default value |
---|---|---|---|---|
DbContext.Delete | Deleted | Exception | Unchanged | Added |
internal class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{
public void Configure(EntityTypeBuilder<Customer> builder)
{
builder.HasQueryFilter(p => !p.IsDeleted);
}
}
using(var context = new MyContext())
{
var customers = context.Customers.IgnoreQueryFilters().ToList();
Display(customers);
}
Czasami sposób zapisu wartości jakiejś właściwości w bazie danych różni się od tej zdefiniowanej w klasie.
Na przykład w modelu posiadamy np. płeć jako enum a w bazie danych chcemy zapisać jako “M” lub “F”.
Albo bardziej złożony przykład - po stronie modelu posiadamy obiekt z adresem lub parametrami urządzenia, a w bazie danych chcemy zapisać go w jednej kolumnie jako xml lub json.
Niestety w poprzedniej wersji EF 6 nie było gotowego mechanizmu i trzeba było stosować obejścia.
Najczęściej obejście polegało na tym, że trzeba było utworzyć w klasie dodatkowe ukryte prywatne pole (tzw. backfield) odpowiadające typowi w bazie danych, a docelowa właściwość była oznaczona jako ignorowana przez EF. Następnie w metodach get i set była realizowana konwersja. Nie było to optymalne rozwiązanie i kod nie był przenośny.
W EF Core wprowadzono nową funkcję, tzw. konwertery (ValueConverters), które rozwiązują ten problem w bardzo elegancki sposób.
Nie trzeba już tworzyć dodatkowych pól, ale przede wszystkim można wielokrotnie używać tej samej konwersji.
Konwerter można użyć w konfiguracji oraz w konwencji.
builder.Property(p=>p.Gender)
.HasConversion(
v => v.ToString(),
v => (Gender)Enum.Parse(typeof(Gender), v)
);
var converter = new ValueConverter<Gender, string>(
v => v.ToString(),
v => (Gender)Enum.Parse(typeof(Gender), v));
var converter = new EnumToStringConverter<Gender>();
builder.Property(p=>p.Gender)
.HasConversion(converter);
Niektóre z konwerterów posiadają dodatkowe parametry:
builder.Property(p=>p.IsDeleted)
.HasConversion(new BoolToStringConverter("Y", "N"));
Lista wbudowanych konwerterów [https://docs.microsoft.com/en-us/ef/core/modeling/value-conversions]
W większości przypadków nie trzeba tworzyć konwerterów, bo wystarczy skorzystać z predefiniowanych konwersji:
builder.Property(p=>p.Gender)
.HasConversion<string>();
builder.Property(p => p.ShippingAddress).HasConversion(
v => JsonConvert.SerializeObject(v),
v => JsonConvert.DeserializeObject<Address>(v));
Utworzenie klasy własnego konwertera
public class JsonValueConverter<T> : ValueConverter<T, string>
{
public JsonValueConverter(ConverterMappingHints mappingHints = null)
: base(v => JsonConvert.SerializeObject(v),
v => JsonConvert.DeserializeObject<T>(v),
mappingHints)
{
}
}
Użycie własnego konwertera
builder.Property(p => p.ShippingAddress).HasConversion(new JsonValueConverter<Address>());
W celu ułatwienia korzystania z konwertera można utworzyć metodę rozszerzającą
public static class PropertyBuilderExtensions
{
public static PropertyBuilder<T> HasJsonValueConversion<T>(this PropertyBuilder<T> propertyBuilder) where T : class
{
propertyBuilder
.HasConversion(new JsonValueConverter<T>());
return propertyBuilder;
}
}
A następnie użyć jej podczas konfiguracji
builder.Property(p => p.ShippingAddress)
.HasJsonValueConversion();
Odczytanie stanu encji
Trace.WriteLine(context.Entry(customer).State);
foreach (var property in context.Entry(customer).Properties)
{
Trace.WriteLine($"{property.Metadata.Name} {property.IsModified} {property.OriginalValue} -> {property.CurrentValue}");
}
Uruchomienie zapytania SQL i pobranie wyników
public IEnumerable<Customer> Get(string lastname)
{
string sql = $"select * from dbo.customers where LastName = '{lastname}'";
return context.Customers.FromSql(sql);
}
Uruchomienie procedury składowanej
using (var context = new SampleContext())
{
var books = context.Customers
.FromSql("EXEC GetAllCustomers")
.ToList();
}
Uruchomienie sparametryzowanej procedury składowanej
Typ DbQuery został wprowadzony w .NET Core 2.1. Umozliwia mapowanie tabel i widoków.
Przypomina typ DbSet ale nie posiada operacji do zapisu, np. Add().
create view OrderHeaders as
select c.Name as CustomerName,
o.DateCreated,
sum(oi.Price) as TotalPrice,
count(oi.Price) as TotalItems
from OrderItems oi
inner join Orders o on oi.OrderId = o.OrderId
inner join Customers c on o.CustomerId = c.CustomerId
group by oi.OrderId, c.Name, o.DateCreated
Models.cs
public class OrderHeader
{
public string CustomerName { get; set; }
public DateTime DateCreated { get; set; }
public int TotalItems { get; set; }
public decimal TotalPrice { get; set; }
}
MyContext.cs
public class MyContext : DbContext
{
public DbSet<Order> Orders { get; set; }
public DbSet<Customer> Customers { get; set; }
public DbQuery<OrderHeader> OrderHeaders { get; set; }
...
}
Właściwości typu DbSet i DbQuery mozna rozdzielic na osobne pliki za pomocą klas częściowych
MyContext.cs
public partial class MyContext : DbContext
{
public DbSet<Order> Orders { get; set; }
public DbSet<Customer> Customers { get; set; }
...
}
MyContextDbQuery.cs
public partial class MyContext : DbContext
{
public DbQuery<OrderHeader> OrderHeaders { get; set; }
public DbQuery<OrderTotal> OrderTotals { get; set; }
...
}
Istnieje równiez mozliwośc pobierania danych z widoków bez uzycia DbQuery
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Query<OrderHeader>().ToView("OrderHeaders");
}
var orderHeaders = db.Query<OrderHeader>().ToList();
Jeśli nie posiadasz uprawnień do tworzenia widoków i tabel, ale chciałbyś skorzystac z typowanych klas, mozna uzyc metody FromSql()
var orderHeaders = db.OrderHeaders.FromSql(
@"select c.Name as CustomerName, o.DateCreated, sum(oi.Price) as TotalPrice,
count(oi.Price) as TotalItems
from OrderItems oi
inner join Orders o on oi.OrderId = o.OrderId
inner join Customers c on o.CustomerId = c.CustomerId
group by oi.OrderId, c.Name, o.DateCreated");
Shadow Properties są właściwościami, które nie są widoczne w klasach encji, ale są zawarte w modelu i są mapowane na kolumny w bazie danych.
Shadow Properties są przydatne w wielu scenariuszach:
public class MyContext : DbContext
{
public DbSet<Customer> Customers { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Customer>()
.Property<DateTime>("LastUpdated");
}
}
## Ustawianie wartości Shadow Properties
~~~ csharp
context.Add(customer);
context.Entry(customer).Property("LastUpdated").CurrentValue = DateTime.UtcNow();
context.SaveChanges();
public overrride int SaveChanges()
{
var addedEntities = ChangeTracker.Entries().Where(p=>p.State == EntityState.Added);
foreach(var entry in addedEntities)
{
entry.Property("CreatedDate").CurrentValue = DateTime.UtcNow();
}
return base.SaveChanges();
}
var customers = context.Customers.OrderBy(c=>EF.Property<DateTime>(c, "LastUpdated"));
lub od C# 6.0 z uzyciem using static
using static Microsoft.EntityFrameworkCore.EF;
var customers = context.Customers.OrderBy(c=>Property<DateTime>(c, "LastUpdated"));
x.Entity<Token>()
.HasIndex(d => new { d.ServiceKey, d.ExternalId })
.HasName("IX_ServiceKey_ExternalId")
.HasFilter(null)
.IsUnique(true);
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>()
.Property(b => b.CreatedDate)
.HasDefaultValueSql("CONVERT(date, GETDATE())");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Contact>()
.Property(p => p.DateCreated)
.ValueGeneratedOnAdd();
}
odpowiednik atrybutu: [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Contact>()
.Property(p => p.LastAccessed)
.ValueGeneratedOnAddOrUpdate();
}
odpowiednik atrybutu: [DatabaseGenerated(DatabaseGeneratedOption.Computed)]