Skip to content

sulmar/ef-tutorial

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 

Repository files navigation

Instalacja biblioteki

PM> Install-Package EntityFramework

Utworzenie kontekstu

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; }
}

Połączenie do bazy danych

 <connectionStrings>
    <add name="MyConnection" providerName="System.Data.SqlClient" connectionString="Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=MyDb;Integrated Security=True" />
  </connectionStrings>

Algorytm wyszukiwania bazy danych

  1. Jeśli w konstruktorze DbContext podano nazwę połączenia to szuka jej w pliku konfiguracynym app.config w sekcji connectionStrings

  2. Jeśli użyto domyślnego konstruktora DbContext to szuka pliku konfiguracynym app.config nazwy klasy DbContext w sekcji connectionStrings

  3. Szuka instancji SQL Express

  4. Szuka bazy danych LocalDb o adresie (localdb)\mssqllocaldb

Wyświetlenie parametrów połączenia

Console.WriteLine(context.Database.Connection.ConnectionString);
  • Azure Data Studio

https://docs.microsoft.com/en-us/sql/azure-data-studio/download?view=sql-server-2017

DbContext

Klasa DbContext jest główną częścią Entity Framework. Instacja DbContext reprezentuje sesję z bazą danych.

Zadania DbContext:

  1. Querying - Konwertuje Linq-To-Entities do zapytań SQL i wysyła je do bazy danych
  2. Change Tracking - śledzenie zmian
  3. Persisting Data - Zapisywanie zmian encji w bazie danych
  4. Caching - pamięć podręczna pierwszego poziomu
  5. Manage Relationship - zarządzanie relacjami
  6. Transactions - zarządzanie transakcjami
  7. 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")
    {
    }
}

Właściwości DbContext

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

Fabryka

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());
        }
    }
    

Konwencje

Odkrywanie typów (Type Discovery)

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.

Konwencja nazywania tabel (Table Name Convention)

Code First tworzy tabele o nazwach w liczbie mnogiej od nazwy encji.

Nazwy kolum

EF wyszukuje wszystkie publiczne właściwości, które posiadają get i set i tworzy kolumny o takich samych nazwach.

Typy danych i rozmiar kolumn

.NET SQL
string nvarchar(max)
decimal decimal(18, 2)
double float
int int
bool bit
DateTime datetime
byte[] varbinary(max)

Klucz podstawowy (Primary Key)

EF wyszukuje właściwość, której nazwa kończy się na ID. Wielkość liter nie ma znaczenia.

Klucze obce (Foreign Key)

Konwencja relacji Jeden-do-wielu

Konwencja 1

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.

Konwencja 2

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.

Konwencja 3

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 4

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; }
}

Konwencja relacji Jeden-do-jeden

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; }
}

Konwencja relacji wiele-do-wielu

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

Konfiguracja relacji Jeden-do-wielu z uzyciem 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);


        }

Konfiguracja kaskadowego usuwania z Fluent API

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

Konfiguracja jeden-do-jeden z Fluent API

 modelBuilder.Entity<Order>()
                .HasOne<Payment>()
                .WithOne(p=>p.Order)
                .HasForeignKey<Payment>(p=>p.PaymentId);

Konfiguracja wiele-do-wiele z Fluent API

Podstawowa konfiguracja

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

Własna nazwy tabeli

modelBuilder.Entity<User>()
                .HasMany(e => e.Roles)
                .WithMany(e => e.Users)
                .Map(m=>
                {
                    m.ToTable("UsersInRoles");
                });

Powstanie tabela pośrednicząca o nazwie UsersInRoles

Własne klucze obce

modelBuilder.Entity<User>()
                .HasMany(e => e.Roles)
                .WithMany(e => e.Users)
                .Map(m=>
                {
                    m.MapLeftKey("UserId");
                    m.MapRightKey("RoleId");
                });

Typ złożony (Complex Type)

Jeśli klasa nie ma klucza podstawowego.

Śledzenie (Tracking)

AutoDetectChanges

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();
            }
        }

Lokalne wyłączenie śledzenia

using (var context = new MyContext())
{
    var blogs = context.Customers
        .AsNoTracking()
        .ToList();
}

Pobranie informacji o encjach

 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()} ");
}

Praca z odłączonymi encjami

Attach()

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

Entry

context.Entry(order).State = EntityState.Modified

Wyrażenie przyłącza encję do kontekstu i ustawia stan na Modified. Ignoruje wszystkie pozostałe encje.

Add()

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

Update()

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

Delete()

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

Change Tracker

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}");
}

Surowy SQL

Uruchomienie zapytania SQL i pobranie wyników

public IEnumerable<Customer> Get(string lastname)
{
    string sql = $"select * from dbo.customers where LastName = '{lastname}'";
    return context.Database.SqlQuery<Customer>(sql);
}

Uruchomienie procedury składowanej

using (var context = new SampleContext())
{
    var books = context.Database
        .SqlQuery("GetAllCustomers")
        .ToList();
}

Uruchomienie procedury składowanej z parametrami

using (var context = new SampleContext())
{
    var city = new SqlParameter("@City", "Warsaw");
    var customers = context
        .SqlQuery("GetCustomersByCity @City" , city)
        .ToList();
}

Typy anonimowe i pobieranie wartości

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");

Indeksy

class EmployeeConfiguration : EntityTypeConfiguration<Employee>
{
    public EmployeeConfiguration()
    {

        HasIndex(p => p.Email)                
            .IsUnique();

    }
}

Migracje

Przydatne polecenia

  • Enable-Migrations - włączenie migracji
  • Add-Migration {migration} - utworzenie migracji
  • Add-Migration {migration} -force - ponowne utworzenie migracji
  • Update-Database - aktualizacja bazy danych do najnowszej wersji
  • Update-Database -script - wygenerowanie skryptu do aktualizacji bazy danych do najnowszej wersji
  • Update-Database -verbose - aktualizacja bazy danych do najnowszej wersji + wyświetlanie logu
  • Update-Database -TargetMigration: {migration} - aktualizacja bazy danych do wskazanej migracji
  • Update-Database -SourceMigration: {migrationA} -TargetMigration: {migrationB} - aktualizacja bazy danych pomiędzy migracjami
  • Update-Database -TargetMigration: $InitialDatabase - aktualizacja bazy danych do pustej bazy danych pomiędzy migracjami
  • Update-Database -SourceMigration: $InitialDatabase -script - wygenerowanie kompletnego skryptu od pustej bazy danych

Utworzenie triggera

  1. 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
  1. Ustaw Build Action na Embedded Resource

  2. 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");
        }
    }

Utworzenie procedury składowanej


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");
}

Inicjalizatory

Wbudowane inicjalizatory

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

Ustawienie inicjalizatora w kodzie

 public class MyContext : DbContext
 {
     public MyContext() : base("MyDbConnection")
     {
         Database.SetInitializer(new DropCreateDatabaseIfModelChanges<MyContext>());
     } 
 }

Ustawienie inicjalizatora w pliku konfiguracyjnym

<entityFramework>
        <contexts>
           <context type="MyApp.MyContext, MyContext">
           <databaseInitializer type="System.Data.Entity.DropCreateDatabaseAlways`1[[MyApp.MyContext, MyApp]], EntityFramework" />
           </context>
        </contexts>
    </entityFramework>
    

Własny inicjalizator

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");
        }
    }

Wyłączenie inicjalizatorów

public MyContext() : base("MyDbConnection")
{
    Database.SetInitializer<MyContext>(null);
 }

Rozszerzenie wbudowanego inicjalizatora

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);
        }

    }

Śledzenie zapytań SQL

Logowanie

public MyContext()
{
    this.Database.Log += Console.WriteLine;
}

Formatowanie logów

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)); 
    } 
}

Ładowanie powiązanych encji

Zachłanne ładowanie (Eager Loading)

Ładowanie powiązanej właściwości

using (var context = new RentContext())
{
    var rentals = context.Vehicle
        .Include(p => p.Owner)                          
        .ToList();
}

Ładowanie zagnieżdżonych encji

using (var context = new RentContext())
{
    var rentals = context.Vehicle
        .Include(p => p.Owner).Select(b => b.Rentee).Include(b => b.Address);                               
        .ToList();
}

Leniwe ładowanie (Lazy Loading)

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; }
}

Jawne ładowanie (Explicit Loading)

Ładowanie powiązanej encji

context.Entry(vehicle).Reference(p => p.Owner).Load();

Ładowanie powiązanej kolekcji

context.Entry(vehicle).Collection(p => p.Passangers).Load();

Filtrowanie ładowanej kolekcji

context.Entry(vehicle)
    .Collection(p => p.Passangers)
    .Query()
    .Where(p=>p.Gender = Gender.Female)
    .Load();

Transakcje

Transakcje bazy danych

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();
        }
    }
}

Rozproszone transakcje

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)

Współbieżność

Token

Konfiguracja za pomocą atrybutu

public class Employee
   {
       public int Id { get; set; }
 
       [ConcurrencyCheck]
       public string FirstName { get; set; }
       public string LastName { get; set; }
   }

Konfiguracja za pomocą FluentAPI

class EmployeeConfiguration : EntityTypeConfiguration<Employee>
    {
        public EmployeeConfiguration()
        {
            Property(p => p.FirstName)
                .IsConcurrencyToken();
        }
    }

Wykrywanie kolizji

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);
       }
   }
   

RowVersion

Konfiguracja za pomocą atrybutu

public class Employee
{
    [Timestamp]
    public byte[] RowVersion { get; set; }
}

Konfiguracja za pomocą FluentApi

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();
   }
}

Mapowanie operacji CRUD na procedury składowane

Konfiguracja FluentApi

class EmployeeConfiguration : EntityTypeConfiguration<Employee>
    {
        public EmployeeConfiguration()
        {
            MapToStoredProcedures();
        }
    }

Utworzone zostaną procedury składowane.

Modyfikacja nazw

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"));
             });
        }
    }

Odłączone encje

Zapis odłączonej encji z użyciem biblioteki GraphDiff

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();
    }
}

http://blog.brentmckendrick.com/introducing-graphdiff-for-entity-framework-code-first-allowing-automated-updates-of-a-graph-of-detached-entities/

About

Przewodnik po Entity Framework 6.x

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published