PM> Install-Package EntityFramework
public class MyContext : DbContext
{
public MyContext(DbConnection connection, bool contextOwnsConnection)
:base(connection, contextOwnsConnection)
{ }
public DbSet<Customer> Customers { get; set; }
public DbSet<Order> Orders { get; set; }
}
<connectionStrings>
<add name="MyConnection" providerName="System.Data.SqlClient" connectionString="Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=MyDb;Integrated Security=True" />
</connectionStrings>
-
Jeśli w konstruktorze DbContext podano nazwę połączenia to szuka jej w pliku konfiguracynym app.config w sekcji connectionStrings
-
Jeśli użyto domyślnego konstruktora DbContext to szuka pliku konfiguracynym app.config nazwy klasy DbContext w sekcji connectionStrings
-
Szuka instancji SQL Express
-
Szuka bazy danych LocalDb o adresie (localdb)\mssqllocaldb
Console.WriteLine(context.Database.Connection.ConnectionString);
- Azure Data Studio
https://docs.microsoft.com/en-us/sql/azure-data-studio/download?view=sql-server-2017
Klasa DbContext jest główną częścią Entity Framework. Instacja DbContext reprezentuje sesję z bazą danych.
- Querying - Konwertuje Linq-To-Entities do zapytań SQL i wysyła je do bazy danych
- Change Tracking - śledzenie zmian
- Persisting Data - Zapisywanie zmian encji w bazie danych
- Caching - pamięć podręczna pierwszego poziomu
- Manage Relationship - zarządzanie relacjami
- Transactions - zarządzanie transakcjami
- Object Materialization - konwersja surowych danych do obiektów encji
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()
: base("MyDbConnection")
{
}
}
Metoda | Użycie |
---|---|
ChangeTracker | Dostarcza informacje i operacje do śledzenie obiektów |
Database | Dostarcza informacje o bazie danych i umożliwia operacje na bazie danych |
Configuration | Konfiguracja opcji |
Jeśli korzystamy z migracji, a konstuktor naszej klasy DbContext posiada parametr(y) nalezy utworzyć fabrykę:
public class MyContextFactory : IDbContextFactory<TransportContext>
{
public MyContext Create()
{
return new MyContext(new TransportDbInitializer());
}
}
EF wyszukuje typy, która wskazane są poprzez właściwość DbSet i tworzy odpowiadające im tabele. Uwględnia również referencyjne typy właściwości, które nie są wskazane przez DbSet oraz typy, które dziedziczą po klasie bazowej wskazanej przez DbSet.
Code First tworzy tabele o nazwach w liczbie mnogiej od nazwy encji.
EF wyszukuje wszystkie publiczne właściwości, które posiadają get i set i tworzy kolumny o takich samych nazwach.
.NET | SQL |
---|---|
string | nvarchar(max) |
decimal | decimal(18, 2) |
double | float |
int | int |
bool | bit |
DateTime | datetime |
byte[] | varbinary(max) |
EF wyszukuje właściwość, której nazwa kończy się na ID. Wielkość liter nie ma znaczenia.
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; }
}
public class User
{
public int Id { get; set; }
public string Login { get; set; }
public ICollection<Role> Roles { get; set; } // Navigation property
}
public class Role
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<User> Users { get; set; } // Navigation property
}
Zostanie automatycznie utworzona tabela pośrednia
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:
- Cascade - usuwa wszystkie encje wraz z encją nadrzędną
- ClientSetNull - klucze obce w encjach zaleznych będą ustawione na null
- Restrict - blokuje kaskadowe usuwanie
- SetNull - klucze obce w encjach zaleznych będą ustawione na null
modelBuilder.Entity<Order>()
.HasOne<Payment>()
.WithOne(p=>p.Order)
.HasForeignKey<Payment>(p=>p.PaymentId);
modelBuilder.Entity<User>()
.HasMany(e => e.Roles)
.WithMany(e => e.Users);
Powstanie tabela pośrednicząca o nazwie UserRoles
Jeśli zamienimy miejscami pola otrzymamy inną nazwę tabeli:
modelBuilder.Entity<Role>()
.HasMany(e => e.Users)
.WithMany(e => e.Roles);
Powstanie tabela pośrednicząca o nazwie RoleUsers
modelBuilder.Entity<User>()
.HasMany(e => e.Roles)
.WithMany(e => e.Users)
.Map(m=>
{
m.ToTable("UsersInRoles");
});
Powstanie tabela pośrednicząca o nazwie UsersInRoles
modelBuilder.Entity<User>()
.HasMany(e => e.Roles)
.WithMany(e => e.Users)
.Map(m=>
{
m.MapLeftKey("UserId");
m.MapRightKey("RoleId");
});
Jeśli klasa nie ma klucza podstawowego.
Domyślnie śledzenie zmian jest włączone. W celu zwiększenia wydajności, zwłaszcza przy dodawaniu dużej ilości encji warto wyłączyć automatyczne wykrywanie zmian:
using (var context = new MyContext())
{
context.Configuration.AutoDetectChangesEnabled = false;
}
Pamiętaj o wywołaniu metody DetectChanges() przed SaveChanges()
Przykład:
using (var context = new MyContext())
{
using (var context = new MyContext())
{
try
{
context.Configuration.AutoDetectChangesEnabled = false;
foreach (var product in context.Products)
{
product.UnitPrice = 1.0m;
}
}
finally
{
context.Configuration.AutoDetectChangesEnabled = true;
}
context.ChangeTracker.DetectChanges();
context.SaveChanged();
}
}
using (var context = new MyContext())
{
var blogs = context.Customers
.AsNoTracking()
.ToList();
}
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()} ");
}
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 |
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}");
}
public IEnumerable<Customer> Get(string lastname)
{
string sql = $"select * from dbo.customers where LastName = '{lastname}'";
return context.Database.SqlQuery<Customer>(sql);
}
using (var context = new SampleContext())
{
var books = context.Database
.SqlQuery("GetAllCustomers")
.ToList();
}
using (var context = new SampleContext())
{
var city = new SqlParameter("@City", "Warsaw");
var customers = context
.SqlQuery("GetCustomersByCity @City" , city)
.ToList();
}
var orderHeaders = db.Database.SqlQuery(
@"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");
class EmployeeConfiguration : EntityTypeConfiguration<Employee>
{
public EmployeeConfiguration()
{
HasIndex(p => p.Email)
.IsUnique();
}
}
Enable-Migrations
- włączenie migracjiAdd-Migration {migration}
- utworzenie migracjiAdd-Migration {migration} -force
- ponowne utworzenie migracjiUpdate-Database
- aktualizacja bazy danych do najnowszej wersjiUpdate-Database -script
- wygenerowanie skryptu do aktualizacji bazy danych do najnowszej wersjiUpdate-Database -verbose
- aktualizacja bazy danych do najnowszej wersji + wyświetlanie loguUpdate-Database -TargetMigration: {migration}
- aktualizacja bazy danych do wskazanej migracjiUpdate-Database -SourceMigration: {migrationA} -TargetMigration: {migrationB}
- aktualizacja bazy danych pomiędzy migracjamiUpdate-Database -TargetMigration: $InitialDatabase
- aktualizacja bazy danych do pustej bazy danych pomiędzy migracjamiUpdate-Database -SourceMigration: $InitialDatabase -script
- wygenerowanie kompletnego skryptu od pustej bazy danych
- Utwórz folder np. Scripts i plik OnDeleteOrderDetail.sql
CREATE TRIGGER OnDeleteOrderDetail
ON [dbo].[OrderDetails]
FOR DELETE
AS
UPDATE [dbo].[Orders] SET ModifiedAt = getdate() WHERE Id = deleted.OrderId
-
Ustaw Build Action na Embedded Resource
-
Utwórz klasę migracji
public partial class AddTriggerOnDeleteOrderDetails : DbMigration
{
public override void Up()
{
SqlResource("MyApp.Scripts.201609301218380_AddTriggerOnDeleteOrderDetail_Up.sql", suppressTransaction: true);
}
public override void Down()
{
Sql("IF OBJECT_ID ('[OnDeleteOrderDetail]', 'TR') IS NOT NULL DROP TRIGGER OnDeleteOrderDetail");
}
}
public override void Up()
{
CreateStoredProcedure(
"MyStoredProcedure",
p => new
{
id = p.Int()
},
@"SELECT some-data FROM my-table WHERE id = @id"
);
}
public override void Down()
{
DropStoredProcedure("MyStoredProcedure");
}
Typ | Opis |
---|---|
CreateDatabaseIfNotExists | utwórz bazę danych jeśli nie istnieje |
DropCreateDatabaseAlways | zawsze usuń i utwórz bazę danych |
DropCreateDatabaseIfModelChanges | usuń i utwórz bazę danych jeśli nastąpiły zmiany w modelu |
public class MyContext : DbContext
{
public MyContext() : base("MyDbConnection")
{
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<MyContext>());
}
}
<entityFramework>
<contexts>
<context type="MyApp.MyContext, MyContext">
<databaseInitializer type="System.Data.Entity.DropCreateDatabaseAlways`1[[MyApp.MyContext, MyApp]], EntityFramework" />
</context>
</contexts>
</entityFramework>
public class MyDbInitializer : IDatabaseInitializer<MyContext>
{
public void MyDbInitializer(MyContext context)
{
if (!context.Database.Exists() || !context.Database.CompatibleWithModel(true))
{
context.Database.Delete();
context.Database.Create();
}
// context.Database.ExecuteSqlCommand("Custom SQL Command here");
}
}
public MyContext() : base("MyDbConnection")
{
Database.SetInitializer<MyContext>(null);
}
Przykład wypełnienia danych
public class MyDbInitializer : CreateDatabaseIfNotExists<MyContext>
{
private readonly CustomerFaker customerFaker;
public MyDbInitializer(CustomerFaker customerFaker)
{
this.customerFaker = customerFaker;
}
protected override void Seed(TransportContext context)
{
context.Customers.AddRange(customerFaker.Generate(1000));
base.Seed(context);
}
}
public MyContext()
{
this.Database.Log += Console.WriteLine;
}
Utworzenie własnego formattera
public class OneLineFormatter : DatabaseLogFormatter
{
public OneLineFormatter(DbContext context, Action<string> writeAction)
: base(context, writeAction)
{
}
public override void LogCommand<TResult>(
DbCommand command, DbCommandInterceptionContext<TResult> interceptionContext)
{
Write(string.Format(
"Context '{0}' is executing command '{1}'{2}",
Context.GetType().Name,
command.CommandText.Replace(Environment.NewLine, ""),
Environment.NewLine));
}
public override void LogResult<TResult>(
DbCommand command, DbCommandInterceptionContext<TResult> interceptionContext)
{
}
}
Rejestracja:
public class MyDbConfiguration : DbConfiguration
{
public MyDbConfiguration()
{
SetDatabaseLogFormatter((context, writeAction) => new OneLineFormatter(context, writeAction));
}
}
using (var context = new RentContext())
{
var rentals = context.Vehicle
.Include(p => p.Owner)
.ToList();
}
using (var context = new RentContext())
{
var rentals = context.Vehicle
.Include(p => p.Owner).Select(b => b.Rentee).Include(b => b.Address);
.ToList();
}
MyContext.cs
public MyContext()
: base("MyDbConnection")
{
this.Configuration.LazyLoadingEnabled = true;
this.Configuration.ProxyCreationEnabled = true;
}
Właściwości muszą być oznaczone jako publiczne i wirtualne. W przeciwnym razie Lazy Loading nie będzie działać!
public class Vehicle : Base
{
public int Id { get; set; }
public virtual Employee Owner { get; set; }
public virtual ICollection<Employee> Passangers { get; set; }
}
context.Entry(vehicle).Reference(p => p.Owner).Load();
context.Entry(vehicle).Collection(p => p.Passangers).Load();
context.Entry(vehicle)
.Collection(p => p.Passangers)
.Query()
.Where(p=>p.Gender = Gender.Female)
.Load();
private void Save(Order order)
{
using (var context = new MyContext())
using (var transaction = context.Database.BeginTransaction())
{
try
{
context.Orders.Add(order);
context.SaveChanges();
context.Customers.Add(order.Customer);
context.SaveChanges();
transaction.Commit();
}
catch(Exception)
{
transaction.Rollback();
}
}
}
Dodaj referencję do System.Transactions
private static void Save(Order oder)
{
using (var scope = new TransactionScope())
{
using (var context1 = new OrdersContext())
{
context1.Orders.Add(order);
context1.SaveChanges();
}
using (var context2 = new CustomersContext())
{
context2.Customers.Add(order.Customer);
context2.SaveChanges();
}
scope.Complete();
}
}
uwaga: w przypadku wykorzystania transakcji w metodzie asynchronicznej otrzymamy błąd. Dlatego należy dodać parametr w konstruktorze:
var transactionScope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)
public class Employee
{
public int Id { get; set; }
[ConcurrencyCheck]
public string FirstName { get; set; }
public string LastName { get; set; }
}
class EmployeeConfiguration : EntityTypeConfiguration<Employee>
{
public EmployeeConfiguration()
{
Property(p => p.FirstName)
.IsConcurrencyToken();
}
}
private static void ConcurencyTest()
{
using (var context = new MyContext())
{
var employee = context.Empoyees.Find(1);
employee.FirstName = "John";
bool saveFailed;
do
{
saveFailed = false;
try
{
context.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
saveFailed = true;
ex.Entries.Single().Reload();
}
} while (saveFailed);
}
}
public class Employee
{
[Timestamp]
public byte[] RowVersion { get; set; }
}
public class Employee
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public byte[] RowVersion { get; set; }
}
class EmployeeConfiguration : EntityTypeConfiguration<Employee>
{
public EmployeeConfiguration()
{
Property(p => p.RowVersion)
.IsConcurrencyToken()
.IsRowVersion();
}
}
class EmployeeConfiguration : EntityTypeConfiguration<Employee>
{
public EmployeeConfiguration()
{
MapToStoredProcedures();
}
}
Utworzone zostaną procedury składowane.
Modyfikacja nazw procedur składowanych
class EmployeeConfiguration : EntityTypeConfiguration<Employee>
{
public EmployeeConfiguration()
{
MapToStoredProcedures(s =>
{
s.Update(u => u.HasName("modify_employee"));
s.Delete(d => d.HasName("delete_employee"));
s.Insert(i => i.HasName("insert_employee"));
});
}
}
Instalacja biblioteki
PM> Install-Package RefactorThis.GraphDiff
private static void GraphDiffTest()
{
Artist artist = new Artist { ArtistId = 1, FirstName = "The Artist Formerly Known as Prince" };
using (var context = new MusicStoreContext())
{
context.UpdateGraph<Artist>(artist, map => map
.OwnedCollection(p => p.Albums));
context.SaveChanges();
}
}