Havit.Data.EntityFrameworkCore.Patterns 2.10.2-pre01

Prefix Reserved
This is a prerelease version of Havit.Data.EntityFrameworkCore.Patterns.
dotnet add package Havit.Data.EntityFrameworkCore.Patterns --version 2.10.2-pre01
                    
NuGet\Install-Package Havit.Data.EntityFrameworkCore.Patterns -Version 2.10.2-pre01
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Havit.Data.EntityFrameworkCore.Patterns" Version="2.10.2-pre01" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Havit.Data.EntityFrameworkCore.Patterns" Version="2.10.2-pre01" />
                    
Directory.Packages.props
<PackageReference Include="Havit.Data.EntityFrameworkCore.Patterns" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Havit.Data.EntityFrameworkCore.Patterns --version 2.10.2-pre01
                    
#r "nuget: Havit.Data.EntityFrameworkCore.Patterns, 2.10.2-pre01"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Havit.Data.EntityFrameworkCore.Patterns@2.10.2-pre01
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Havit.Data.EntityFrameworkCore.Patterns&version=2.10.2-pre01&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Havit.Data.EntityFrameworkCore.Patterns&version=2.10.2-pre01&prerelease
                    
Install as a Cake Tool

Obsah

Model

Úvod

Konvence datového modelu a výchozí chování jsou velmi dobře popsány v oficiální dokumentaci EF Core, proto zde není smysluplné dokumentaci opakovat. Viz https://docs.microsoft.com/en-us/ef/core/modeling/

Pojmenování tříd a modelu je v angličtině ev. v primárním jazyce projektu.

Primární klíč

Používáme primární klíč typu int pojmenovaný Id. Primátní klíč může být i jiného typu (celočíselný SByte, Int16, Int64, Byte, UInt16, UInt32, UInt64, dále string nebo Guid), podpora těchto typů zatím není kompletní (chybí minimálně podpora IDataLoader).

public int Id { get; set; }

Přítomnost a pojmenování primárního klíče je kontrolována unit testem.

public int Id { get; set; }

Délky stringů

U všech vlastností typu string je nutno uvést jejich maximální délku pomocí attributu [MaxLength]. Pokud nemá být délka omezená, atributu nezadáváme hodnotu nebo použijeme hodnotu Int32.MaxValue.

Ze zadaných hodnot jsou vygenerována metadata, např. pro snadné omezení maximální délky textu v UI.

[MaxLength(128)]
public string PasswordHash { get; set; }

[MaxLength(8)]
public string PasswordSalt { get; set; }

...

[MaxLength] // pro maximální možnou délku
public string Note { get; set; }

Výchozí hodnoty vlastností

Výchozí hodnoty vlastností definujeme přímo v kódu:

public bool IsActive { get; set; } = true;

Reference / cizí klíče

Není-li jiná potřeba, definujeme v páru cizí klíč (vlastnost typu int nesoucí hodnotu cizího klíče) a navigation property (obvykle reference na cílový objekt). Pro pojmenování konvenci EntityId a Entity.

Důvodem jsou možnosti pro dotazování či možnosti podpory seedování dat.

public Pohlavi Parent { get; set; }
public int ParentId { get; set; }

public Language Language { get; set; }
public int LanguageId { get; set; }

Unit test kontroluje, že jsou vlastnosti v páru, tedy že každá navigation property má i foreign key property. Dále kontroluje pojmenování vlastností končících na Id a nikoliv ID.

Kolekce One-To-Many (1:N)

  • Obvykle používáme List<T>, ale stristriktně předepsáno to není.
  • Kolekce mají smysl např. pro:
    • aggregate root (Order + OrderLines)
    • lokalizace (Country + CountryLocalizations)
    • členství uživatelů v rolích (User + Memberships + Role)
  • Kolekce zásadně nepoužíváme tam, kde jsou v kolekcích velké objemy příznakem smazaných dat. Důvodem je nemožnost rozumně načíst jen nesmazané záznamy.
  • Kolekce definujeme jako readonly a inicializujeme je v pomocí auto-property initializeru (nebo v konstruktoru).
public List<CountryLocalization> Localizations { get; } = new List<CountryLocalization>();

Kolekce Many-To-Many (M:N)

⚠️ Entity Framework Core 5.x přináší podporu pro vazby typu M:N (viz dokumentace), avšak HFW pro práci s kolekcemi nemá podporu.

Vazby M:N doporučujeme dekomponovat na dvě vazby 1:N (postup známý z EF Core 2.x a 3.x). Ve výchozím chování EF Core je třeba této entitě nakonfigurovat složený primární klíč (pomocí data anotations nelze definovat složený primární klíč), nám se klíč nastaví sám (pokud není ručně nastaven) konvencí. Pokud je to třeba, nastavíme pouze název databázové tabulky, do které je entita mapována.

Příklad

Pokud má mít User kolekci Roles, musíme zavést entity Membership se dvěma vlastnostmi. User pak bude mít kolekci nikoliv rolí, ale těchto Membershipů.

public class User
{
    public int Id { get; set; }

    public List<Membership> Roles { get; } = new List<Membership>();

    ...
}
public class Role
{
    public int Id { get; set; }
    ...
}
public class Membership
{
    public User User { get; set; }
    public int UserId { get; set; }

    public Role Role { get; set; }
    public int RoleId { get; set; }
}

Kolekce s filtrováním smazaných záznamů

Viz Entity Framework Core – Kolekce s filtrováním smazaných záznamů

Mazání příznakem (Soft Delete)

Podpora mazání příznakem je na objektech, které obsahují vlastnost Deleted typu Nullable<DateTime>. Podpora není implementovatelná na dočítání kolekcí modelových objektů, tj. při načítání kolekcí objektů jsou načítány i smazané objekty.

public DateTime? Deleted { get; set; }

Lokalizace

V aplikaci je třeba definovat:

  • Třídu Language implementující Havit.Model.Localizations.ILanguage
  • interface ILocalized<TLocalizationEntity> dědící z Havit.Model.Localizations.ILocalized<TLocalizationEntity, Language> pro označení tříd, které jsou lokalizovány
  • interface ILocalization<TLocalizedEntity> dědící z Havit.Model.Localizations.ILocalization<TLocalizedEntity, Language> pro označení tříd lokalizujících základní třídu (předchozí bod)

Datové třídy pak definujeme s těmito interfaces.

public class Country : ILocalized<CountryLocalization>
{
	public int Id { get; set; }

	...

	public List<CountryLocalization> Localizations { get; } = new List<CountryLocalization>();
}

public class CountryLocalization : ILocalization<Country>
{
	public int Id { get; set; }

	public Country Parent { get; set; }
	public int ParentId { get; set; }

	public Language Language { get; set; }
	public int LanguageId { get; set; }

	[MaxLength]
	public string Name { get; set; }
}

Entries / systémové záznamy (EnumClass)

Pokud má třída sloužit jako systémový číselník se známými hodnotami, použijeme vnořený veřejný enum Entry s hodnotami. Pokud mají mít záznamy v databázi stejné Id, což je obvyklé, je třeba uvést položkám hodnotu.

Na základě tohoto enumu pak generátor zakládá DataEntries.

Příklad
public class Role
{
    ...

    public enum Entry
    {
        Administrator = -1,
        CustomerAdministrator = -2,
        BookReader = -3,
        PublisherAdministrator = -4
    }
}

Kolekce s filtrováním smazaných záznamů

Bohužel není možné načíst jen nesmazané záznamy. Můžeme však načíst do paměti všechny záznamy a používat jen ty nesmazané, například vytvořením dvou kolekcí - persistentní (se všemi objekty) a nepersistentní (počítaná, filtruje jen nesmazané záznamy s persistentní kolekce.

V následujících ukázkách budeme pracovat s třídou Child, kterou lze příznakem označit za smazanou a s třídou Master mající kolekci Children objektů Child.

Pro implementaci potřebujeme zajistit:

  • Použití kolekcí v modelu
  • Mapování vlastností (kolekcí) v EF
  • Kolekce filtrující smazané záznamy

Filtrované kolekce není možné používat v queries (Where, OrderBy) ani v Include. V DataLoaderu je možné filtrované kolekce použít pro načtení záznamů.

Použití kolekcí v modelu

public class Child
{
	public int Id { get; set; }
	public int MasterId { get; set; }
	public Master Master { get; set; }
	public DateTime? Deleted { get; set; }
}
 
public class Master
{
	public int Id { get; set; }
	public ICollection<Child> Children { get; } // nepersistentní
	public IList<Child> ChildrenIncludingDeleted { get; } = new List<Child>(); // persistentní
	public Master()
	{
		// kolekce children je počítanou kolekcí
		Children = new FilteringCollection<Child>(ChildrenIncludingDeleted, child => child.Deleted == null);
	}
}

Mapování vlastností (kolekcí) v EF

public class MasterConfiguration : IEntityTypeConfiguration<Master>
{
    public void Configure(EntityTypeBuilder<Master> builder)
    {
        builder.Ignore(c => c.Children);
        builder.HasMany(c => c.ChildrenIncludingDeleted);
    }
}

Kolekce filtrující smazané záznamy

Viz Havit.Model.Collections.Generic.FilteringCollection<T> - zdrojáky. Kolekce je v nuget balíčku Havit.Model.

public class FilteringCollection<T> : ICollection<T>
{
	private readonly ICollection<T> source;
	private readonly Func<T, bool> filter;
	public FilteringCollection(ICollection<T> source, Func<T, bool> filter)
	{
		this.source = source;
		this.filter = filter;
	}
	public IEnumerator<T> GetEnumerator()
	{
		return source.Where(filter).GetEnumerator();
	}

    ...
}

Entity

Definuje datový kontext, jeho vlastnosti a migrace.

DbContext

Nevyžadujeme vytvářet vlastnosti typu DbSet pro každou evidovanou entitu.

Obvyklá (a doporučená) struktura třídy DbContext

ProjectNameDbContext je připraven v NewProjectTemplate, nicméně kdyby bylo potřeba ručně:

Důležité je dědit z Havit.Data.EntityFrameworkCore.DbContext.

Obvykle se používají dva konstruktory:

  • Konstruktor přijímající DbContextOptions pro běžné použití (produkční běh aplikace).
  • Bezparametrický kontruktor pro použití v unit testech, proto je internal.
public class NewProjectTemplateDbContext : Havit.Data.EntityFrameworkCore.DbContext
{
	internal NewProjectTemplateDbContext()
	{
		// NOOP
	}
	
	public NewProjectTemplateDbContext(DbContextOptions options) : base(options)
	{
		// NOOP
	}

	protected override void CustomizeModelCreating(ModelBuilder modelBuilder)
	{
		base.CustomizeModelCreating(modelBuilder);

		modelBuilder.RegisterModelFromAssembly(typeof(Havit.NewProjectTemplate.Model.Localizations.Language).Assembly);
		modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
	}
}

ConnectionString

Není žádné výchozí nastavení, jaký connection string bude použit. Vše je řešeno až při použití DbContextu, např. v konfiguraci AddDbContext(...). Není doporučeno použít OnConfiguring, neboť brání použití DbContext poolingu.

Exception handling metod Save[Async]

V případě selhání uložení objektů je vyhozena výjimka DbUpdateException, ta je však "ošklivě formátovaná" a vyžaduje dohledávání, co se vlastně stalo v InnerException.
Proto v případě výskytu DbUpdateException tuto zachytáváme a vyhazujeme novou instanci DbUpdateException s trochu lépe formátovanou zprávou (Message). Původní výjimku DbUpdateException použijeme jako InnerException námi vyhozené výjimky.

DesignTimeDbContextFactory

Viz dokumentace Design-time DbContext Creation.

Využívá jej tooling migrací a code generátor. Pro účely toolingu migrací musí db context používat SqlServer (nebo jinou relační databázi, nelze použít in-memory provider).

Registrace modelu a konfigurací

Abychom nemuseli registrovat entity ručně, je k dispozici extension metoda RegisterModelFromAssembly. Zaregistruje všechny třídy z dané assembly, které nemají žádný z atributů: [NotMapped], [ComplexType], [Owned].

Pro registraci konfigurací je k dispozici extension metoda ApplyConfigurationsFromAssembly.

Conventions

Výchozí konvence

  • ManyToManyEntityKeyDiscoveryConvention
    Konvence nastaví tabulkám, které reprezentují vazbu Many-To-Many složený primární klíč, pokud jej nemají nastaven. Index primárního klíče má sloupce v pořadí, v jakém byly definovány v kódu.
  • DataTypeAttributeConvention Pokud je vlastnost třídy modelu označena atributem DataTypeAttribute s hodnotou DataType.Date pak se použije v databázi datový typ Date.
  • CascadeDeleteToRestictConvention Všem cizím klíčům s nastaví DeleteBehavior na Restrict, čímž zamezí kaskádnímu delete.
  • CacheAttributeToAnnotationConvention Hodnoty zadané v atributu [Cache] předá do anotations.

Volitelné konvence

Selektivní potlačení konvence

Pokud některá naše konvence nevyhovuje na určitém místě, lze ji potlačit. Dříve (EF Core 2.x) bylo možné je potlačit v konfiguraci entity, to již (EF Core 3.x) možné není, neboť v té době jsou již konvence aplikovány. Jediná šance je znečistit model informací, že se konvence nemá aplikovat.

Potlačení konvence lze vyjádřit umístěním [SuppressConvention] s uvedením konvence, kterou potlačujeme. Identifikátory konvencí jsou ve třídě ConventionIdentifiers. Atribut i třída s identifikátory jsou v nuget balíčku Havit.Data.EntityFrameworkCore.Abstractions.

Potlačit lze tyto konvence:

  • StringPropertiesDefaultValueConvention (na modelové třídě pro všechny vlastnosti třídy, nebo jen na vlastnosti)
  • ManyToManyEntityKeyDiscoveryConvention (na modelové třídě)
[SuppressConvention(ConventionIdentifiers.ManyToManyEntityKeyDiscoveryConvention)]
public class SomeClass
{
...
}

public class OtherClass
{
...
	[SuppressConvention(ConventionIdentifiers.StringPropertiesDefaultValueConvention)]
	public string SomeString { get; set; }
}

Konfigurace

Viz dobře napsaná dokumentace EF Core.

Vztah M:N

Entity Framework Core 5.x přináší podporu pro vazby typu M:N (viz dokumentace), avšak HFW pro práci s kolekcemi nemá plnou podporu. Kolekce typu M:N je možné omezeně použít, nebude je umět dočíst DbDataLoader a nemají (a nejspíš mít později ani nebudou) řešenou podporu v entity validátorech a before commit processorech.

Příklad řešení v modelu a konfigurace je uveden v sekci Entity Framework Core - 02 - Model.

Migrations

Viz dokumentace migrations.

Spouštění Migrations

Spuštění migrations a seedů (viz další kapitoly) provádíme typicky při spuštění aplikace. Samotné spuštění při startu aplikace zajišťuje hosted service MigrationHostedService, která migrace a seedy spustí prostřednictvím MigrationService.UpgradeDatabaseSchemaAndDataAsync.

DataLayer

Generátor kódu

Implementace tříd popsaných v následujících kapitolách je automaticky generována s umožněním vlastního rozšíření vygenerovaného kódu.

Kód je generován pomocí dotnet tool Havit.Data.EntityFrameworkCore.CodeGenerator.Tool (musí být nainstalován), jenž spouští code generátor z NuGet balíčku Havit.Data.EntityFrameworkCore.CodeGenerator, který je zamýšlen pro použití v projektu Entity.

Aktualizace dotnet toolu Havit.Data.EntityFrameworkCore.CodeGenerator.Tool

Aktualizace Havit.Data.EntityFrameworkCore.CodeGenerator.Tool se očekávají příležitostně, např. když se změní podporovaná verze .NET, atp. Změny (aktualizace) Havit.Data.EntityFrameworkCore.CodeGenerator jsou podle změn, které potřebujeme udělat do code generátoru.

ℹ️ Tím, že Havit.Data.EntityFrameworkCore.CodeGenerator.Tool není "běžným nuget" balíčkem v projektu, ale jde o dotnet tool, nezobrazují se jeho aktualizace v Package Manageru. Pro aktualizaci je třeba z příkazové řádky spustit (aktuální složka ve složce se solution):

dotnet tool update Havit.Data.EntityFrameworkCore.CodeGenerator.Tool

Spuštění generátoru

Generátor lze spustit powershell skriptem Run-CodeGenerator.ps1 v rootu projektu DataLayer.

Pro spuštění přímo z Visual Studia si musíme otevřít jakoukoliv konzoli (Terminal, Developer Powershell, Nuget Package Console), přepnout se do složky DataLayer a spustit .

Otevření terminálu ve Visual Studiu

Běh generátoru je relativně rychlý, generátor je obvykle hotov během pár sekund.

Princip generátoru (aneb kde bere generátor data)

Generátor získává data z modelu DbContextu (DbContext.Model). DbContext se hledá v assembly projektu Entity, získává se přes DbContextActivator, čímž získáme instanci přes IDesignTimeDbContextFactory, pokud existuje. Viz Entity Framework Core - 03 - Entity.

Assembly pro Entity se hledá ve složce Entity/bin (a všech podsložkách), bere se poslední v čase, tj. nejaktuálnější. Tím řešíme případnou existenci více verzí assembly v případě existence lokálního buildu v Debug i v Release konfiguraci.

Konfigurace generátoru kódu

V rootu projektu s generátorem kódu nebo ve složce se solution je možné mít soubor efcore.codegenerator.json s nastavením:

{
	"ModelProjectPath": "...",
	"MetadataProjectPath": "...",
	"MetadataNamespace": "..."
}
  • ModelProjectPath - název složky (musí obsahovat *.csproj) nebo cesta k csproj, kam budou generována metadata z modelu, cesta je relativní vůči složce se solution. Výchozí hodnotou je Model\Model.csproj.
  • MetadataProjectPath - název složky (musí obsahovat *.csproj) nebo cesta k csproj, kam budou generována metadata z modelu, cesta je relativní vůči složce se solution. Výchozí hodnotou je Model\Model.csproj (by default stejné ako ModelProjectPath).
  • MetadataNamespace namespace, do kterého se metadata generují, je možno použít strukturovaný namespace, např. My.Customized.Metadata (na disku budou metadata vygenerována do složky _generated\My\Customized\Metadata).

Co generuje

Viz dále uvedené:

  • DbDataSource, FakeDataSource
  • DbRepository
  • DataEntries
  • Metadata

Generované soubory

DataLayeru jsou generovány soubory:Namespace

  • _generated\DataEntries\Namespace\IEntityEntries.cs
  • _generated\DataEntries\Namespace\EntityEntries.cs
  • _generated\DataSources\Namespace\IEntityDataSource.cs
  • _generated\DataSources\Namespace\EntityDbDataSource.cs
  • _generated\DataSources\Namespace\Fakes\FakeEntityDataSource.cs
  • _generated\Repositories\Namespace\IEntityRepository.cs
  • _generated\Repositories\Namespace\EntityDbRepository.cs
  • _generated\Repositories\Namespace\EntityDbRepositoryBase.cs
  • _generated\Repositories\Namespace\EntityDbRepositoryQueryProvider.cs
  • _generated\DataLayerServiceExtensions.cs

a dále jsou jednorázově vytvořeny soubory (tj. při opakovaném spuštění generátoru se nepřepisují, neaktualizují):

  • Repositories\Namespace\IEntityRepository.cs
  • Repositories\Namespace\EntityDbRepository.cs

Modelu (resp. dle nastavení MetadataProjectPath) jsou generovány soubory:

  • _generated\Metadata\Namespace\EntityMetadata.cs

Poznámka ke složce _generated

V každém projektu (Model, Entity) je jen jedna (rootová) složka _generated. To umožňuje přehledné zobrazení pending changes (všechno generované lze snadno sbalit a přeskočit) nebo třeba vynechání generovaných souborů z navigace ReSharperu.

Poznámka k entitám pro vztah M:N

Pro entity reprezentující vztah M:N (entity mající jen složený primární klíč ze dvou sloupců a nic víc) se žádný kód negeneruje.

DataSources

Zprostředkovává přístup k datům jako IQueryable. Umožňuje snadné podstrčení dat v testech.

IEntityDataSource, IDataSource<Entity>

Poskytuje dvě vlastnosti: Data a DataIncludingDeleted. Pokud obsahuje třída příznak smazání (soft delete), pak vlastnost Data automaticky odfiltruje přínakem smazané záznamy.

Pro každou entitu vzniká jeden interface pojmenovaný IEntityDataSource (např. ILanguageDataSource).

EntityDbDataSource

  • Generované třídy implementující IEntityDataSource.
  • Pro každou entitu vzniká jedna třída, třídy jsou pojmenované EntityDbDataSource (např. LanguageDbDataSource).
  • K dotazům automaticky přidává query tag IEntityDataSource.Data[IncludingDeleted].

Data jsou získávána z databáze (resp. z IDbContextu a jeho DbSetu).

FakeEntityDataSource

  • Jedná se rovněž o generované třídy implementující IEntityDataSource (rovněž je pro každou entity jedna třída FakeEntityDataSource, např. FakeLaguageDataSource), avšak nejsou napojeny na databázi.
  • Třídy jsou dekorovány atributem [Fake] a jsou vnořeny do namespace Fakes.
  • Data jsou čerpána z kolekce předané v konstruktoru. Určeno pro podstrčení dat v unit testech tam, kde je použita závislost IEntityDataSource (ev. službám ve frameworku se závislostí IDataSource<Entity>).
  • Implementace využívá MockQueryable.EntityFrameworkCore, čímž zajistíme fungování i asynchronních operací (což nad prostým IQueryable<Entity> nefunguje).
Příklad použití FakeEntityDataSource v unit testu
// Arrange

// připravíme data source obsahující zadané záznamy
FakeUserDataSource fakeUserDataSource = new FakeUserDataSource(
	new User { Id = 1, Username = "...", ... },
	new User { Id = 2, Username = "...", ... },
	new User { Id = 3, Username = "...", ... });

// použijeme data source jako závislost v testované třídě
ITestedService service = new TestedService(..., fakeUserDataSource, ...);

// Act
...

Repositories

Repositories jsou třídy s jednoduchými a opakovaně použitelnými metodami pro přístup k datům.

Repositories (navzdory 95% implementací nalezitelných na internetu) neobsahují metody pro CRUD operace.

IEntityRepository, IRepository<Entity>

Poskytuje metody:

  • GetObject[Async]
  • GetObjects[Async]
  • GetAll[Async]

Pro každou entitu vzniká jeden interface pojmenovaný IEntityRepository (např. ILanguageRepository), který implementuje IRepository<Entity>.

EntityDbRepository

Generované třídy implementují IEntityRepository.

Poskytuje veřejné metody (implementace IRepository<Entity>)

  • GetAll[Async] - Vrací příznakem nesmazané záznamy, pokud je metoda nad jednou instancí volána opakovaně, nedochází k opakovaným dotazům do databáze.
  • GetObject[Async] - Vrací objekt dle Id, pokud neexistuje záznam s takovým Id, je vyhozena výjimka.
  • GetObjects[Async] - Vrací objekty dle kolekce Id, pokud neexistuje záznam pro alespoň jedno Id, je vyhozena výjimka. Při opakovaném volání metody jsou objekt vrácen z identity mapy (I)DbContextu.

a protected vlastnosti

  • Data a DataIncludingDeleted - viz Data Sources, implementačně používají hodnoty ze závislosti IDataSource<TEntity>, čímž je lze snadno napsat test s mockem dat pro tyto vlatnosti.

Implementační instrukce

Není zvykem, aby se repository navzájem používaly jako závislosti v implementacích, protože by to mohlo vést až k nepřehlednému a neřešitelnému zauzlování repositories navzájem.

Pokud potřebuje jedna repository to samé, co jiná, což je samo o sobě nezvyklé, je doporučeno vyextrahovat kód do samostatné služby, např. jako Query.

Načítání závislých objektů

Pokud chceme načíst referované objekty či kolekce, disponuje EF třemi možnostmi načtení referovaných objektů. My máme navíc implementovaný DbDataLoader.

Repository disponuje možnostmi načíst závislé objekty.

GetLoadReferences

Metoda je určena k override a definuje, jaké závislosti mají být s objektem načteny. Syntaxe viz DbDataLoader.

Příklad:

protected  override  IEnumerable<Expression<Func<EmailTemplate,  object>>>  GetLoadReferences()
{
	yield  return x => x.Localizations;
}

Návratového typu IEnumerable<Expression<Func<Entity*, object>>> se není třeba bát 🙂):

  • Func<Entity, object> říká, že použijeme lambda výraz, kterým určíme z Entity, nějakou vlastnost vracející cokoliv
  • Expression rozšiřuje Func o to, že se lambda výraz přeloží jako expression tree
  • IEnumerable říká, že můžeme vrátit více takových výrazů.
  • Viz ukázka, je to jednoduché.
  • Aktuálně není možné touto metodou zajistit načtení objektů z kolekce (tedy x => x.PropertyA.PropertyB lze použít jen tehdy, pokud PropertyA není kolekcí objektů).
    • použijte override LoadReferences + LoadReferencesAsync
LoadReferences[Async]
  • Načte závislosti definované v GetLoadReferences.
  • Automaticky použito v metodách GetAll, GetObject(Async) a GetObjects(Async).
  • Pokud repository obsahuje vlastní metody vracející entity, je potřeba před navrácením dat provést dočtení závislostí touto metodou!
  • Načítání závislostí je provedeno pomocí DbDataLoaderu, nikoliv pomocí Include (byť by to mohlo být někdy výhodnější). Možno overridovat (rozšířit) o další dočítání věcí, co nejsou přímo podporované skrze GetLoadReferences (např. prvky kolekcí).

Příklad:

public EmailTemplate GetByXy(string xy) // vymyšleno pro ukázku
{
	EmailTemplate template = Data.FirstOrDefault(item => item.XY == xy);
	LoadReferences(template);
	return template;
}

DataEntries

DataEntries zpřístupňují systémové záznamy v databázi dle Entries v modelu.

Příklad vygenerovaného interface pro DataEntries

(viz Entries v Modelu)

public interface IRoleEntries : IDataEntries
{
	Role Administrator { get; }			
	Role BookReader { get; }			
	Role CustomerAdministrator { get; }			
	Role PublisherAdministrator { get; }			
}

Implementace vyzvedává objekty z příslušné repository (IRepository<Entity>) pomocí metody GetObject.

Příklady použití
IRoleEntries entries;
...
// máme strong-type k dispozici objekt, který reprezentuje konkrétní záznam v databázi
bool userIsAdmin = userRoles.Contains(entries.Administrator);
INastaveniEntries nastaveni;
...
// máme strong-type k dispozici objekt, který reprezentuje nastavení aplikace
string url = nastaveni.Current.ApplicationUrl

Párování záznamů v databázi

  • Pokud primární klíč cílové tabulky není autoincrement, páruje se Id záznamu s hodnotou enumu (Role.Id == (int)Role.Entry.Administrator).
  • Pokud je primární klíč cílové tabulky autoincrement, páruje se pomocí stringového sloupce Symbol, který je v takovém případě povinný (Role.Symbol == Role.Entry.Administrator.ToString()). Párování (Id, Symbol) se pro každou tabulku načítá jen jednou a drží se v paměti.

LookupServices

Jde o třídy, které mají zajistit možnost rychlého vyhledávání entity podle klíče. Na rozdíl od ostatních (Repository, DataSources) nejsou generované - píšeme je ručně, pro jejich napsání je však připravena silná podpora.

Třída je určena k použití u neměnných či občasně měněných entit a u entit které se mění hromadně (naráz). Není garantována stoprocentní spolehlivost u entit, které se mění často (myšleno zejména paralelně) v různých transakcích - invalidace a aktualizace může proběhnout v jiném pořadí, než v jakém doběhly commity.

Rovněž z principu “out-of-the-box” nefunguje korektně invalidace při použití více instancí aplikace k aktualizaci dat aplikace (farma, web+webjoby, atp.), pro distribuovanou invalidaci je udělána příprava.

Implementace

Je potřeba dědit z třídy, viz tato ukázka kódu minimálního kódu.

public class UzivatelLookupService : LookupServiceBase<string, Uzivatel>, IUzivatelLookupService
{
	public UzivatelLookupService(IEntityLookupDataStorage lookupStorage, IRepository<Uzivatel> repository, IDataSource<Uzivatel> dataSource, IEntityKeyAccessor entityKeyAccessor, ISoftDeleteManager softDeleteManager) : base(lookupStorage, repository, dataSource, entityKeyAccessor, softDeleteManager)
	{
	}

	public Uzivatel GetUzivatelByEmail(string email) => GetEntityByLookupKey(email);

	protected override Expression<Func<Uzivatel, string>> LookupKeyExpression => uzivatel => uzivatel.Email;

	protected override LookupServiceOptimizationHints OptimizationHints => LookupServiceOptimizationHints.None;
}

Implementována je zejména vlastnost - LookupKeyExpression, jejíž návratovou hodnototu je expression pro získání párovacího klíče. Zde tedy říkáme, že párujeme uživatele dle emailu. Druhou implementovanou vlastností je OptimizationHints, vysvětlení viz níže.

Metoda GetUzivatelByEmail je pak službou třídy samotné, kterou mohou její konzumenti používat. Pod pokličkou jen volá metody GetEntityByLookupKey.

IncludeDeleted

By default nejsou uvažovány (a vraceny) příznakem smazané záznamy. Pokud mají být použity, je třeba provést override vlastosti IncludeDeleted a vrátit true.

Filter

Pokud nás zajímají jen nějaké instance třídy (neprázdný párovací klíč, objekty v určitém stavu, atp.), lze volitelně provést override vlastnosti Filter a vrátit podmínku, kterou musí objekty splňovat.

ThrowExceptionWhenNotFound

Pokud# není podle klíče objekt nalezen, je vyhozena výjimka ObjectNotFoundException. Pokud nemá být vyhozena výjimka a má být vrácena hodnota null, lze provést override této vlastnosti, aby vracela false.

OptimizationHints

Pro efektivnější fungování invalidací (viz níže) je možné zadat určité hinty, např., pokud je entita readonly a tedy nemůže být za běhu aplikace změněna, nemusí k žádné invalidaci docházet.

Dependency Injection

Třídy je nutno do DI containaru instalovat nejen pod sebe sama, ale ještě pod servisní interface, který zajistí možnost invalidace dat při uložení nějaké entity (viz dále).

Není tak možné pro lookup service použít automatickou registraci pomocí attributu [Service].

	services.WithEntityPatternsInstaller()
		...
		.AddLookupService<IUserLookupService, UserLookupService>();

Invalidace

Pokud dojde k uložení entity, je potřeba lookup data nějakým způsobem invalidovat. S objektem se může stát spousta věcí - změna vyhledávacího klíče, smazání příznakem, změna jiných vlastností tak, aby objekt již neodpovídal filtru, atp. Je třeba zajistit, aby lookup data držená službou, byla aktuální.

Zvolené řešení je efektivnější než prostá invalidace, data jsou rovnou aktualizována na nové hodnoty.

To lze omezit tam, kde jsou entity např. readonly, viz OptimizationHints.

ClearLookupData

Pokud chceme ručně vynutit odstranění dat z paměti, je k dispozici metoda ClearLookupData.

Užitečné to může být pro situace:

  • Jednorázově jsme použili lookup service a víme, že ji dlouho nebudeme potřebovat - pak zavolání metody uvolní paměť alokovanou pro lookup data.
  • Došlo k úpravě dat mimo UnitOfWork (třeba stored procedurou) a potřebujeme dát lookup službě vědět, že lookup data již nejsou aktuální.

Použití repository

Objekty jsou po nalezení klíče v lookup datech vyzvednuty z repository. Přínos tohoto chování je takový, že získaný objekt je trackovaný a mohl být získán z cache, bez dotazu do databáze.

Pozor na scénáře, kde se ptáme do lookup služby opakovaně pro necachované objekty (nebo cachované, které ještě v cache nejsou), každé volání pak může udělat dotaz do databáze právě pro získání instance z repository.

Pro tuto situaci je k dispozici metoda, která nevrátí instanci entity, ale jen její klíč - GetEntityKeyByLookupKey. Je pak možno implementačně získat klíče všech objektů, které můžeme ve vlastním kódu přehodit metodě GetObjects repository. Pokud máme problém poté objekty roztřídit znovu dle klíčů, můžeme uvažovat takto:

  1. Nejprve získáme všechny klíče entit dle vyhledávané vlastnosti
  2. Poté všechny entity načteme pomocí repository.GetObjects(…), čímž dostaneme objekty do paměti (identity mapy, DbContext).
  3. Nyní se můžeme do lookup služby (a metody GetEntityByLookupKey) ptát jeden objekt po druhým, vracení objektů z repository již nebude dělat dotazy do databáze, neboť jsou již načteny.

Použití non-Int32 primárního klíče

K dispozici je bázová třída LookupServiceBase<TLookupKey, TEntity, TEntityKey>, kde jako TEntityKey je třeba zvolit skutečný typ primárního klíče.

Použití složeného klíče

Pro vyhledávání je možno použít složený klíč, klíč musí mít vlastní třídu, která musí zajistit fungování porovnání v Dictionary, tedy předefinovat porovnání. S úspěchem lze použít v implementaci anonymní třídu, byť se trochu zhorší kvalita kódu tím, že jako typ klíče musíme uvést typ object.

Použití více entit pod jedním klíčem

Není podporováno, je vyhozena výjimka.

Časová složitost

Snažíme se, aby složitost vyhledání byla O(1).

Konstantní složitost samozřejmě neplatí pro první volání, které sestavuje vyhledávací slovník.

Implementační detail

Sestavení lookup dat provede jediný dotaz do databáze pro všechny objekty (s ohledem na IncludeDeleted a Filter). Nenačítají se celé instance entit, ale jen jejich projekce, tj. vrací se netrackované objekty, tj. nenaplní se identity mapa (DbContext) instancemi entit, ve kterých je vyhledáváno.

Lokalizace

Pokud máme model s lokalizacemi (viz Entity Framework Core - 02 - Model, kapitola Lokalizace), pak službou ILocalizationService získáváme pro entitu z kolekce Localizations hodnotu pro zvolený jazyk. Hodnotu můžeme získat pro “aktuální” nebo zvolený jazyk.

ContryLocalization countryLocalization = localizationService.GetCurrentLocalization(country);
ContryLocalization countryLocalization = localizationService.GetLocalization(country, czechLanguage);

ℹ️Služba nezajišťuje načtení kolekce Localizations z databáze, zajišťuje jen výběr požadované hodnoty z této kolekce.

Logika hledání pro daný jazyk je postavena takto:

  • Pokud existuje položka pro zadaný jazyk, je použita tato.
  • Není-li nalezena, zkouší se hledat pro jazyk dle “(neutrálnější culture)[https://learn.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo?view=net-5.0]” jazyka.
  • Není-li nalezena, zkouší se hledat pro jazyk dle invariantní culture (prázdný UICulture).

Například pro uživatele pracující v češtině se hledá položka pro jazyky dle UICulture postupně pro “cs-cz”, “cs”, ““.

Registrace DI

Použití služby je podmíněno registrací do IoC containeru, což můžeme udělat extension metodou AddLocalizationServices.

services
  ...
  .AddLocalizationServices<Language>()
  ...;

DataLoader

Explicitní loader - dočítá objekty, které dosud nebyly načteny.

Činnost

  • Explicitní loader - dočítá objekty, které dosud nebyly načteny.
  • Objekty jedné vlastnosti jsou dočteny jedním dotazem.
  • Při dotazování se nepoužívá join přes všechno načítané, vždy jde o dotaz do tabulky, ze které se načítá daná vlastnost (žádné joiny).
  • Např. načtení faktura => faktura.Dodavatel.Adresa.Zeme spustí do databáze 3 dotazy - načtení dodavatelů, načtení adres a načtení zemí (pro všechny načítané faktury naráz). Nevadí, pokud je některá z vlastností po cestě null.
  • Spoléhá se na EF Change Tracker, objekty, ke kterým jsou dočítány "závislosti" musí být trackované. Tato podmínka je testována a v případě nesplnění je vyhozena výjimka.
  • Objekty musí mít klíč Id typu Int32.
  • Není vyžadována dualita cizího klíče a navigační property (není tedy vyžadována existence obou sloupců auto.Barva a auto.BarvaId, stačí samotné auto.Barva).
  • Neprovádí dotazy do databáze pro nově založené objekty (příklad: nově zakládanému a ještě neuloženému uživateli nemůžeme z databáze načítat role, když uživatel v databázi ještě není)
  • Instance kolekcí inicializuje na prázdné (pro IList<> a List<>), pokud jsou null.

Chování ohledně foreign keys

Při načítání referenci spoléhá na hodnoty cizích klíčů, potažmo jako shadow properties.

Mějme tedy příklad:

Auto auto = autoRepository.GetObject(1); // načte auto s Id 1, Barva bude null, BarvaId řekněme např. 2.
auto.BarvaId = 5; // změníme BarvaId na jinou hodnotu
dataLoader.Load(auto, a => a.Barva); // pokusíme se dočíst vlastnost Barva

Pod pokličkou se provede:

dbContext.Set<Barva>().Where(barva => barva.Id == 5).ToList();

Čímž se načte barva podle hodnoty cizího klíče objektu v paměti, nikoliv podle databáze (tam může být aktuálně třeba BarvaId == 2). Jinými slovy, po načtení bude mít auto přiřazenu do vlastnosti Barvainstanci sId` 5.

ℹ️ Tímto chováním se DataLoader v EF Core liší od implementace DataLoader v EF 6.

Metody

  • Load, LoadAsync - přijímá jeden objekt, ke kterému jsou dočteny požadované nenačtené vlastnosti
  • LoadAll, `LoadAllAsync - přijímá kolekci objektů, kterým jsou dočteny požadované nenačtené vlastnosti
  • Podporuje fluent API pro načítání dalších objektů (dataLoader.Load(...).ThenLoad(...).ThenLoad(...)), viz příklady.

Příklady

dataLoader.Load(jednoAuto, auto => auto.Vyrobce).ThenLoad(vyrobce => vyrobce.Kategorie);
dataLoader.Load(jednoAuto, auto => auto.Vyrobce.Kategorie); // funguje také se zřetězením
dataLoader.LoadAll(mnohoAut, auto => auto.Vyrobce.Kategorie); // mnohoAut = kolekce, pole, ... (IEnumerable<Auto>)
dataLoader.LoadAll(mnohoAut, auto => auto.NahradniDily).ThenLoad(nahradniDil => nahradniDil.Dodavatel); // načítání objektů v kolekci

Kolekce M:N

⚠️ Entity Framework Core 5.x přináší podporu pro vazby typu M:N (viz dokumentace), avšak HFW pro práci s kolekcemi nemá podporu. DataLoader při pokusu o načtení M:N kolekce neřízeně spadne.

Podpora kolekcí s filtrováním smazaných záznamů

  • Kolekce s filtrováním smazaných záznamů jsou dataloaderem podporovány.
  • Pokud model obsahuje kolekci Xyz (obvykle nepersistentní) a XyzIncludingDeleted (obvykle persistentní), pak je použití kolekce Xyz automaticky nahrazeno načtením kolekce XyzIncludingDeleted.
  • Konvence je dána pojmenováním kolekcí (přípona IncludingDeleted), žádné další testy vůči konfiguraci EF nejsou prováděny.
  • Pokud je dále použito ThenLoad(...), načtou se hodnoty jen nesmazaným záznamům, mají-li se načíst hodnoty i ke smazaným záznamům, je třeba použít kolekci XyzIncludingDeleted, lepší pochopení dá následující příklad.
  • Je požadováno, aby filtrovaná kolekce dokázala během práce vrátit nesmazané objekty, např. toto není podporováno, neboť získání ChildGroup.Deleted vyvolá NullReferenceException.
Příklad
public class Master
{
    public int Id { get; set; }
    public ICollection<Child> Children { get; } // nepersistentní
    public IList<Child> ChildrenIncludingDeleted { get; } = new List<Child>();// persistentní
    ...
}
 
dataLoader.Load(master, m => m.Children); // pod pokličkou je transformováno na načtení ChildrenIncludingDeleted, načteny jsou proto všechny Child (vč. příznakem smazaných) k danému masteru
dataLoader.Load(master, m => m.Children).ThenLoad(c => c.Boss); // načteny jsou všechny Children daného masteru, z nich se vyberou jen nesmazané a k těm se načte vlastnost Boss
dataLoader.Load(master, m => m.ChildrenIncludingDeleted).ThenLoad(c => c.Boss); // vlastnost Boss je načtena i smazaným Childům

DataLoader jako závislost v unit testech

K dispozici je FakeDataLoader, který nic nedělá. Lze tak použít v unit testech, které pracují s daty v paměti a nemají co dočítat.

// Arrange

// připravíme data source obsahující zadané záznamy
FakeUserDataSource userDataSource = new FakeUserDataSource(...); 

// připravíme fake data loaderu
FakeDataLoader dataLoader = new FakeDataLoader();

// použijeme data loader jako závislost v testované třídě
ITestedService service = new TestedService(..., fakeUserDataSource, fakeDataLoader, ...);

// Act
...

Cachování

Jak to funguje

Implementace cachování je realizována na úrovni Repositories, DbDataLoader a UnitOfWork:

  • XyDbRespository.GetObject[Async] - pokud nemá objekt v identity mapě, pokusí se ho najít v cache, pokud není ani v cache, načítá jej z databáze, poté jej uloží do cache
  • XyDbRespository.GetObjects[Async]- objekty, které nemá v identity mapě se pokusí najít v cache, objekty, které nejsou ani v cache, načítá z databáze a uloží je do cache
  • XyDbRepository.GetAll[Async]() - hledá v cache identifikátory objektů
  • DbDataLooader.Load[Async], DbDataLooader.LoadAll[Async] - při načítání referencí i kolekcí se pokusí najít objekty v cache, objekty, které nejsou v cache, načítá z databáze a uloží je do cache
  • DbUnitOfWork.Commit[Async] - invaliduje položky v cache
  • XyEntries.Item - pod pokličkou volá XyDbRepository.GetObject()

Implementaci cachování zajišťuje zejména IEntityCacheManager a jeho implementace.

Ve výchozí konfiguraci (viz dále) je použit EntityCacheManager, který realizuje cachování se závislostmi:

  • IEntityCacheSupportDecision - rozhoduje, zda je daná entita cachovaná či nikoliv
  • IEntityCacheKeyGenerator - definuje, pod jakým klíčem bude entita uložena do cache
  • IEntityCacheOptionsGenerator - určuje další parametry položky v cache (priorita, sliding expirace)
  • IEntityCacheDependencyManager - poskytuje klíč pro cache dependencies

⚠️ Cachování kolekcí

Cachování kolekcí funguje spolehlivě pro objekty, které nepřecházejí mezi různými parenty. Tj. cachování funguje v obvyklých typických scénářích - objekt s lokalizacemi, faktura s řádky faktur, atp.

Cachování kolekcí však nefunguje tam, kde mohou prvky kolekce přecházet mezi různými parenty, např. pokud budu přepínat zaměstnanci jeho nadřízeného zaměstnance a tento nadřízený zaměstnanec má kolekci svých podřízených, pak cachování této kolekce bude vykazovat chyby. Pokud má být v tomto scénáři nadřízený zaměstnanec cachován, nesmíme mu zapnout cachování kolekcí. Nesmíme ani použít "cachování všech entit se sliding expirací", jak je uvedeno níže.

(Důvod: Invalidace cache se provádí po uložení změn. Po uložení změn vidíme jen nový, aktuální stav objektů. Nejsme schopni tedy invalidovat cache pro původního nadřízeného zaměstnance, neboť nevíme, kdo to byl.)

Konfigurace

Výchozí konfigurace

Ve výchozí konfiguraci jsou:

  • cachovány entity, které označeny atributem Havit.Data.EntityFrameworkCore.Abstractions.Attributes.CacheAttribute (zjednodušeně),
  • v atributu lze nastavit prioritu položek v cache, sliding a absolute expiraci,
  • v atributu lze zakázat cachování klíčů GetAll.
  • kolekce jsou cachovány, pokud je cílový typ cachován (umožňuje cachovat entities)

ℹ️ Je vyžadována registrace závislosti ICacheService, kterou knihovny k EFCore neřeší, je třeba ji zaregistrovat do DI containeru samostatně.

Ukázková situace: Reference

public class Auto
{
	public int Id { get; set; }
	public Barva Barva { get; set; }
	// ...
}

[Cache]
public class Barva
{
	public int Id { get; set; }		
	// ...
}

Barva je cachovaná, Auto nikoliv.

Z cache se proto mohou odbavovat např.:

  • BarvaRepository.GetObject(...)
  • BarvaRepository.GetAll()
  • BarvaEntries.Black
  • DataLoader.Load(auto, a => a.Barva)

Ukázková situace: Kolekce 1:N

[Cache]
public class Stav : ILocalized<StavLocalization>
{
	public int Id { get; set; }
	public List<StavLocalization> Localizations { get; } = new List<StavLocalization>();
}

[Cache]
public class StavLocalization : ILocalization<Stav>
{
	public int Id { get; set; }
	public Stav Parent { get; set; }
	public int ParentId { get; set; }
	public Language Language { get; set; }
	public int LanguageId { get; set; }
	// ...
}

Číselník stavů je cachovaný vč. svých lokalizací.

Attribut [Cache] je třeba uvést na obou třídách, žádný předpoklad, "když X je cachované, tak XLocalization také" není uplatňován.

Z cache se proto mohou odbavovat např.:

  • StavRepository.GetObject(...)
  • StavRepository.GetAll()
  • StavEntries.Aktivni
  • (obdobně StavLocalizationRepository, StavLocalizationEntries, avšak nemá valného významu takto použít)
  • DataLoader.LoadAll(stavy, s => s.Localizations)
Ukázková situace: Dekomponovaný vztah M:N do asociační třídy s kolekcí 1:N
public class LoginAccount
{
	public int Id { get; set; }
	public List<Membership> Memberships { get; } = new List<Membership>();
	// ...
}

[Cache]
public class Membership
{
	public LoginAccount LoginAccount { get; set; }
	public int LoginAccountId { get; set; }
	public Role Role { get; set; }
	public int RoleId { get; set; }
}

[Cache]
public class Role
{
	[DatabaseGenerated(DatabaseGeneratedOption.None)]
	public int Id { get; set; }
	// ...
}

LoginAccount není cachován, třídy Membership a Role jsou cachovány.

Z cache se proto mohou odbavovat např.:

  • RoleRepository.GetObject(...)
  • RoleRepository.GetAll()
  • RoleEntries.Administrator
  • DataLoader.Load(loginAccount, la => la.Membership).ThenLoad(m => m.Role)

Cachování Membership je pro daný scénář nutné, ale nemá jiného významu, neboť nemáme pro třídy reprezentující M:N vazbu nepoužíváme repository.

Pokud nebude Membership označen jako cachovaný, nebude se LoginAccountu cachovat kolekce Memberships.

Cachování vypnuto

Pokud je nutné cachování vypnout (např. jednorázově běžící konzolovky, které jen sežerou paměť, ale data v cache nevyužijí), je možné toto řešit extension metodou:

services
    ...
    .AddDataLayerServices(new ComponentRegistrationOptions().ConfigureNoCaching())
    ...;

Není pak potřeba ani registrovat závislost ICacheService.

Cachování všech entit s použitím sliding expirace [Experimental]

Myšlenka: Na chvíli si do cache umístíme cokoliv, s čím pracujeme. Až s tím nebudeme pracovat, vypadne to z cache. Cachujeme tedy vše, ale zároveň všemu omezujeme dobu expirace.

Na rozdíl od výchozí konfigurace:

  • se neohlíží na [Cache], cachováno je vše,
  • v atributu nastavená priorita položek v cache, sliding a absolute expirace se respektuje (použije), pokud není uvedena sliding expirace, použije se výchozí.
services
    ...
    .AddDataLayerServices(new ComponentRegistrationOptions().ConfigureCacheAllEntitiesWithDefaultSlidingExpirationCaching(timeSpan))
    ...;

Cache dependencies

Pokud potřebujeme do cache uložit objekt, který bychom chtěli invalidovat v případě změněny nějakého konkrétního objektu v databázi, případně změny jakéhokoliv objektu v databázi, můžeme objekt do ICacheService registrovat se závislostmi.

Klíče závislostí lze získat ze služby IEntityCacheDependencyManager:

  • GetSaveCacheDependencyKey - závislosti jsou vyhozeny při uložení entity daného typu s daným Id (např. pro invalidaci nějaké vlastnosti subjektu, pokud se subjekt změní).
  • GetAllSaveCacheDependencyKey - závislosti jsou vyhozeny při uložení (a založení a smazání) jakékoliv entity daného typu (např. pro invalidaci součtu částek všech faktur).

Předpokládáme úpravu tohoto interface na základě dalších požadavků.

Invalidace

Invalidace provádí výhradně DbUnitOfWork v metodě Commit[Async].

Invaliduje se při uložení entity:

  • entita samotná
  • klíče pro GetAll typu dle entity
  • kolekce, jichž je entita členem
  • po invalidaci entity se uložená entita opět uloží do cache (tj. omezí se nutné načtení entity po její změně)
  • je myšleno na distribuovanou invalidaci v lokálních caches

Nepodporované scénáře

Uložení/vyzvednutí z cache:

  • Owned Types (cachování entity s owned types není a nebude, při použití Owned Entity Types je třeba úplně vypnout cachování - viz níže.)
  • Vazba 1:1 (v případě potřeby prověříme možnost doimplementování)

Veškeré obejití UnitOfWork:

  • např. Cascade Deletes

Modely s Owned Entity Types

Problémy, které způsobuje použití Owned Entity Types:

  • ChangeTracker sleduje změny na owned types samostatně, v pokud má Person vlastnost pro domácí adresu HomeAddress (owned) typu Address, pak při změně (např.) ulice ChangeTracker vidí změnu v owned entitě Address, nikoliv v Person. To ztěžuje invalidace. (Pozn: Ale ukládá to efektivně, takže musí jít nějak rozumně pospojovat entitu a jí použité owned typy).
  • Současná implementace uložení Person do cache neukládá owned entity types, tj. při odbavení položky z cache nebudou hodnoty pro vlastnosti dobře nastaveny.

Generovaná metadata

Na základě modelu jsou pro všechny stringové vlastnosti generována metadata s definicí jejich maximálních délek dle attributu [MaxLength] (viz Entity Framework Core - 02 - Model). Pro vlastnosti označované jako "maximální možná délka" se použije hodnota Int32.MaxValue, byť to není správně (nejde uložit tolik znaků, ale tolik byte). Jiná metadata negenerujeme.

Metadata jsou generována přímo do modelu a jsou určena pro definici maximálních délek např. ve view modelu. Změnou délky textu v modelu, se po přegenerování kódu změní vygenerované konstanty, které změní maximální velikosti viewmodelu...

Příklad
public static class LanguageMetadata
{
    public const int CultureMaxLength = 10;
    public const int NameMaxLength = 200;
    public const int SymbolMaxLength = 50;
    public const int UiCultureMaxLength = 10;
}

SoftDeleteManager

Implementeace ISoftDeleteManager rozhodují o tom, zda daná entita podporuje soft delete a pokud ano, poskytuje metody pro nastavení příznaku smazání (a odebrání příznaku smazání).

Výchozí implementace SoftDeleteManager říká, že soft-delete jsou ty entity, které mají vlastnost Deleted typu Nullable<DateTime>.

UnitOfWork

IUnitOfWork poskytuje metody:

  • Add[Range]ForInsert
  • Add[Range]ForInsertAsync - určeno pro použití HiLo strategie generování Id
  • Add[Range]ForUpdate
  • Add[Range]ForDelete
  • Commit[Async]
  • RegisterAfterCommitAction
  • Clear - Umožňuje vyčistit ChangeTracker podkladového DbContextu.
Add[Range]ForDelete

Entity, které podporují soft delete jsou metodou Add[Range]ForDelete označeny jako smazané příznakem, nedojde k jejich fyzickému smazání, ale k aktualizaci (UPDATE).

Fyzické smazání entity podporující soft delete není aktuálně možné (kdo bude potřebovat, nechť se ozve, doplníme metodu Add[Range]ForDestroy).

RegisterAfterCommitAction

Umožňuje přidat zvenku nějakou akci k provedení po commitu (odeslání emailu, smazání cache, atp. Umožnuje přidat jak synchronní akci tak asynchronní akci. Asynchronní akce funguje pouze v asynchronním commitu, v případě registrace asynchronní akce a spuštění synchronního commitu dojde k vyhození výjimky).

Příklad
private void ProcessPayment(Payment payment)
{
    ...
    // vůbec nevíme, kde je unitOfWork.Commit(), ale víme, že po jeho spuštění dojde k odeslání notifikace
	unitOfWork.RegisterAfterCommitAction(() => SendNotification(payment));
    ...	
}

Koncept BeforeCommitProcessorů

DbUnitOfWork obsahuje koncept, který umožní při volání commitu spustit služby pro každou změněnou entitu ještě před uložením objektu. Je možné tak "na poslední chvíli" provést v entitách nějaké změny.

Pro implementaci nějakého vlastního BeforeCommitProcessoru je vhodné dědit z BeforeCommitProcessor<TEntity>, což pomůže vypořádat se s dvojicí metod Run a RunAsync v interface IBeforeCommitProcessor<TEntity>.

Službu je potřeba si zaregistrovat službu do DI containeru pod interface IBeforeCommitProcessor<TEntity>.

Metoda vrací hodnotu výčtu ChangeTrackerImpact a má pomoci UnitOfWorku s výkonovou optimalizací. Hodnota říká, zda změna provedená before commit processorem může ovlivnit changetracker tak, že je nutné jej spustit znovu (což je potřeba typicky jen při přidání nové entity).

Příklad

Viz např. implementace SetCreatedToInsertingEntitiesBeforeCommitProcessor.

public class MyEntityBeforeCommitProcessor : BeforeCommitProcessor<MyEntity>
{
    public ChangeTrackerImpact Run(ChangeType changeType, MyEntity changingEntity)
    {
		if (changeType == ChangeType.Insert)
		{
			// do something
		}
		return ChangeTrackerImpact.NoImpact;
    }
}
SetCreatedToInsertingEntitiesBeforeCommitProcessor

Pro nově založené objekty, které mají vlastnost Created typu DateTime a v této vlastnosti je hodnota default(DateTime) nastaví aktuální čas (z ITimeService). Tj. automaticky nastavuje hodnotu Created entitám, které ji nastavenou nemají.

Je použit automaticky (díky registraci do DI containeru).

Koncept EntityValidatorů

Před uložením objektů (a po spuštění BeforeCommitProcessorů) se spustí validátory entit, které umožňují kontrolovat jejich stav. Pokud je zjištěna nějaká validační chyba, je vyhozena výjimka typu ValidationFailedException.

Pro implementaci nějakého vlastního EntityValidatoru je třeba implementovat interface IEntityValidator<TEntity>. K implementaci je jediná metoda Validate, jež má na výstupu kolekci IEnumerable stringů - zjištěných chyb při validaci.

Dále je třeba službu zaregistrovat do DI containeru.

Příklad
public class MyEntityEntityValidator : IEntityValidator<MyEntity>
{
	IEnumerable<string> Validate(ChangeType changeType, MyEntity changingEntity)
	{
		if (changingEntity.StartDate >= changingEntity.EndDate)
		{
			yield return "Počáteční datum musí předcházet koncovému datu.";
		}
	}
}

IValidatableObject.Validate()

Jednou ze specifických možností implementace EntityValidatoru je IValidatableObject.Validate() přímo entitě.

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
	if ((this.Parent == null) && (this.Id != (int)Project.Entry.Root))
	{
		yield return new ValidationResult($"Property {nameof(Parent)} is allowed to be null only for Root project.");
	}
	if ((this.Depth == 0) && (this.Id != (int)Project.Entry.Root))
	{
		yield return new ValidationResult($"Value 0 of {nameof(Depth)} property is allowed only for Root project.");
	}
}

Tyto validace lze pak do commit-sekvence zapojit zaregistrováním služby ValidatableObjectEntityValidator do dependency-injection containeru:

services.AddSingleton<IEntityValidator<object>, ValidatableObjectEntityValidator>();

ℹ️ ValidatableObjectEntityValidator nezajišťuje validace dle DataAnnotations atributů, jako jsou např. [Required], [MaxLength] apod.

Pořadí akcí v commitu

Během commitu dochází postupně k těmto akcím:

  • zavolání metody BeforeCommit
  • spuštění BeforeCommitProcessorů
  • spuštění EntityValidátorů
  • uložení změn do databáze (DbContext.SaveChanges[Async]).
  • zavolání metody AfterCommit (zajišťuje volání akcí registrovaných metodou RegisterAfterCommitAction)

Seedování dat

Seedování dat je automatické založení dat v databázi.

Definice dat k seedování

Seedování dat provádí třídy implementujíící interface IDataSeed. S jednoduchostí lze vytvořit třídu dědící ze třídy DataSeed<>, která tento interface poskytuje, je třeba jen implementovat template metody SeedData a SeedDataAsync.

Vytvořením instancí dat, metodou For a provedené konfigurace nad jejím výsledkem, se připraví data, která mají být v databázi. Připravená data se předhodí metodě Seed nebo SeedAsync. (Poznámka: Metoda For vychází z otevřenosti pro další rozšíření, kdy se mohou data získávat z jiných zdrojů, např. ForCsv, ForExcel, ForResource. To však není implementováno a budeme řešit, až bude potřeba.)

Párování pomocí sloupce Id (>99% případů)

Pokud jde o systémový číselník, do kterého nejsou vkládány hodnoty uživatelsky, pak můžeme na sloupci vypnout autoincrement a tím použít vlastní hodnoty pro Id.

Pokud jde o číselník, do kterého jsou vkládány hodnoty uživatelsky, můžeme namísto autoincrementu použít sekvenci, což nám umožní, abychom stále mohli použít vlastní hodnoty pro Id.

Seedovaná data s daty v databázi jsou pak párována pomocí Id.

Příklad bez autoincrementu
public class Role
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)] // nepoužijeme autoincrement, čímž umožníme vkládat vlastní hodnoty do sloupce Id
    public int Id { get; set; }
 
    ...
 
    public enum Entry
    {
        Writer = -3,
        Reader = -2,
        Administrator = -1
    }
}


public class RoleSeed : DataSeed<CoreProfile>
{
    public override void SeedData()
    {
        Role[] roles = new[]
        {
            new Role
            {
                Id = (int)Role.Entry.Administrator, // nastavíme hodnotu pro sloupec Id
                Name = "Administrátor"
            },
            ... // Weader, Writer
        };
        Seed(For(roles).PairBy(item => item.Id)); // řekneme, že se má párovat dle sloupce Id
    }
}
Příklad se sekvencí
public class User
{
    public int Id { get; set; }
    ...
 
    public enum Entry
    {
        SystemUser = -1
    }
}
public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.Property(user => user.Id).HasDefaultValueSql("NEXT VALUE FOR UserSequence");
    }
}
 
// dále je třeba na modelu (v DbContextu) zajistit existenci sekvence
// modelBuilder.HasSequence<int>("UserSequence");
public class UserSeed : DataSeed<CoreProfile>
{
    public override void SeedData()
    {
        User[] users = new[]
        {
            new User
            {
                Id = (int)User.Entry.SystemUser, // nastavíme hodnotu pro sloupec Id
                Name = "(Systémový uživatel)"
            }
        };
        Seed(For(users).PairBy(item => item.Id)); // řekneme, že se má párovat dle sloupce Id
    }
}
Párování pomocí sloupce Symbol (<1% případů)

ℹ️ Toto řešení jsme implementovali a používali jako první verzi v Entity Framework 6. V Entity Framework Core nemá valného použití díky možnosti využití sekvence a řešení dle předchozího odstavce.

Pokud z nějakého důvodu potřebujeme autoincrement na číselníku, do nějž potřebujeme seedovat data, např. pokud nemůžeme použít sekvenci, pak musíme párovat data v databázi s párovanými daty podle jiného sloupce než podle Id. V takovém případě používáme párování dle sloupce Symbol, který je (bohužel díky zpětné kompatibilitě) výchozím párováním, pokud sloupec existuje.

public class LanguageSeed : DataSeed<DefaultDataSeedProfile>
{
    public override void SeedData()
    {
        Language czechLanguage = new Language
        {
            Name = "Česky",
            Culture = "cs-CZ",
            UiCulture = "",
            Symbol = Language.Entry.Czech.ToString()
        };
 
        Seed(For(czechLanguage)); // PairBy(item => item.Symbol) je default, který není třeba uvádět
    }
}

Konfigurace

Poskytuje fluidní API, následující metody lze řetězit.

Párování seedovaných dat s daty v databázi

Metodami PairBy a AndBy lze určit sloupce, pomocí kterých budou seedovaná data párována s daty v databázi. Pro reference je nutno použít cizí klíče, nikoliv navigation property (jinými slovy: je nutno použít LanguageId, nikoliv Language).

Seed(For(...).PairBy(item => item.LanguageId).AndBy(item => item.ParentId));
Seed(For(...).PairBy(item => item.LanguageId, item => item.ParentId));
Aktualizace záznamů

Neexistující záznamy jsou standardně založeny. Existující záznamy aktualizovány.

Aktualizace existujících záznamů lze potlačit metodou WithoutUpdate:

Seed(For(...).WithoutUpdate());

Potlačení aktualizace pouze vybraných vlastností (sloupců) lze metodou ExcludeUpdate:

Seed(For(...).ExcludeUpdate(item => item.UserRank)); // sloupec UserRank nebude aktualizován
Závislé záznamy / Kolekce

Po uložení "parent" záznamů je možné zajistit uložení i jejich referencí či kolekcí (například lokalizace číselníků). Jaké hodnoty budou ukládány je určeno metodami AndFor nebo AndForAll. Tyto metody dále umožňuje provést nastavení seedování těchto záznamů.

Pro řešení "jak získat Id aktuálně uloženého záznamu" lze použít metodu AfterSave.

Dobrou ukázkou je níže ukázka výchozí konfigurace pro lokalizované tabulky.

Výchozí konfigurace

Symbol

Pokud třída obsahuje sloupec Symbol, je podle něj automaticky párováno (není-li určeno jinak; zpětná kompatibilita, sorry):

Seed(For(...).PairBy(item => item.Symbol));
Lokalizované tabulky

Lokalizovaným třídám zajistí uložení lokalizací, lokalizacím zajistí párování dle ParentId a LanguageId (aktuálně hardcodováno v HFW). Že jde o lokalizované a lokalizační třídy se poznává podle implementace ILocalization<,> a ILocalized<,>.

// pseudokód popisující implementaci
Seed(For(...)
     // po uložení každého lokalizovaného záznamu nastavíme jeho lokalizacím ParentId
    .AfterSave(item => item.SeedEntity.Localization.ForEach(localization => localization.ParentId = item.PersistedEntity.Id))
     // po seedu lokalizovaných dat budeme seedovat lokalizace, které budeme párovat pomocí ParentId a LanguageId
    .AndForAll(item => item.Localization, configuration =>
         {
            configuration.PairBy(item => item.ParentId, item => item.LanguageId);
         }));

Ve skutečnosti je tato výchozí hodnota implementována mnohem komplikovaněji, efekt je takovýto.

Závislost na jiných seedovaných datech

Pokud chceme seedovat data, potřebujeme závislosti (například pro lokalizovaná data potřebujeme mít provedeno seedování jazyků).

Metodou GetPrerequisiteDataSeeds lze říct, na jakých seedech je tento závislý. V ukázce musí nejprve proběhnout EmailTemplateSeed a LanguageSeed než je spuštěn EmailTemplateLocalizationSeed (HFW řeší i detekci cyklů, atp.).

Návratovou hodnotou metody je IEnumerable typů, nikoliv instancí, implementace je tak náchylná na chybu - např. použití typeof(Language) namísto typeof(LanguageSeed). Taková chyba je však v runtime detekována a je vyhozena výjimka.

public class EmailTemplateLocalizationSeed : DataSeed<DefaultDataSeedProfile>
{
    public override void SeedData()
    {
        ...
    }
 
    public override IEnumerable<Type> GetPrerequisiteDataSeeds()
    {
        yield return typeof(EmailTemplateSeed);
        yield return typeof(LanguageSeed);
    }
}

Profily

Data je možné seedovat pro různé účely - produkční data, testovací data pro testování funkcionality A, testovací data pro testování funkcionality B, atp. Pro tento scénář máme k dispozici profily pro seedování dat, které určují, které seedy se mají v jakém profilu spustit. Profil je třída implementující IDataSeedProfile, např. děděním z abstraktní třídy DataSeedProfile.

Jaká data patří do jakého profilu je určeno generickým parametrem u třídy DataSeed. Profily mohou mít závislosti na jiných profilech, jsou definovány pomocí metody GetPrerequisiteProfiles().

public class TestDataProfile : DataSeedProfile
{
    public override IEnumerable<Type> GetPrerequisiteProfiles()
    {
        yield return typeof(DefaultDataSeedProfile);
    }
}

Jak se spustí jednotlivé seedování dat jednotlivých profilů je uvedeno dále.

Spuštění seedování

Spuštění seedování zajišťuje třída DataSeedRunner. Ta dostává závislosti: Kolekci data seedů, které se mají provést, persister seedovaných dat a strategii rozhodující, zda je třeba seedování pustit. Profil, který se má spustit je určen generickým parametrem v metodě SeedData, viz ukázka.

Obvyklé spuštění je při startu aplikace (global.asax.cs, apod.):

Typicky je řešeno v projektu ve třídě MigrationService.

var dataSeedRunner = serviceScope.ServiceProvider.GetService<IDataSeedRunner>();
await dataSeedRunner.SeedDataAsync<CoreProfile>(false, cancellationToken);

Izolace jednotlivých seedů

Počet objektů sledovaných ChangeTrackerem postupně při volání jednotlivých seedů nenarůstá, což při seedování většího objemu dat znamená dopad na výkon. Pro izolaci jednotlich seedů se na začátku a konci metody Seed[Async] zajistí vyčištění changetrackeru. (Při ladění jednoho z projektů se dostáváme na pětinásobné zrychlení). Přes tuto izolaci jednotlivé seedy sdílejí databázovou transakci.

Omezení spuštění seedování

Aby se nespouštělo seedování dat při každém startu aplikace, pamatují si seedy v databázové tabulce __SeedData (zjednodušeně) verzi dataseedů, která byla spuštěna. V tabulce jsou záznamy pro jednotlivé profily, název profilu je primárním klíčem.

Pokud zjistíme, že daná verze již byla spuštěna, nebude se seedování spouštět.

Verze dataseedů se určí z názvu assembly, z file version a z data (datumu) posledního zápisu assembly. Díky datu poslední zápisu assembly nám funguje seedování i při vývoji, kde se nám jinak název a verze assembly nemění a bez data posledního zápisu assembly bychom při vývoji spustili seedování jen jedenkrát.

Toto je implementováno v OncePerVersionDataSeedRunDecision, která je zaregistrována do DI containeru pod IDataSeedRunDecision.

K dispozici je ještě strategie AlwaysRunDecision, která nic nekontroluje ale zajistí spuštění seedování vždy.

Dependency Injection

Registrace do DI containeru je podporována pro IServiceCollection. Pro registrace služeb se generuje extension metoda DataLayerServiceExtensions.AddDataLayerServices.

services
			.AddDbContext<IDbContext, GoranG3DbContext>(optionsBuilder =>
			{
				if (configuration.UseInMemoryDb)
				{
					optionsBuilder.UseInMemoryDatabase(nameof(GoranG3DbContext));
				}
				else
				{
					optionsBuilder.UseSqlServer(configuration.DatabaseConnectionString, c =>
					{
						c.MaxBatchSize(30);
						c.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
					});
				}
				optionsBuilder.UseDefaultHavitConventions();
			})
			.AddLocalizationServices<Language>() // volitelné
			.AddDataLayerServices()
			.AddDataSeeds(typeof(CoreProfile).Assembly)
			.AddLookupService<ICountryByIsoCodeLookupService, CountryByIsoCodeLookupService>();

		services.AddSingleton<IEntityValidator<object>, ValidatableObjectEntityValidator>(); // pokud je požadována validace entit pomocí IValidatableObject

		services.AddSingleton<ITimeService, ApplicationTimeService>();
		services.AddSingleton<TimeProvider, ApplicationTimeProvider>();
		services.AddSingleton<ICacheService, MemoryCacheService>();
		services.AddSingleton(new MemoryCacheServiceOptions { UseCacheDependenciesSupport = false });

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Havit.Data.EntityFrameworkCore.Patterns:

Package Downloads
Havit.Data.EntityFrameworkCore.Patterns.Windsor

HAVIT .NET Framework Extensions - Entity Framework Core Extensions - Installers for Castle Windsor

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.10.2-pre01 37 1/13/2026
2.10.1 618 12/1/2025
2.10.1-pre02 166 10/31/2025
2.10.1-pre01 201 6/4/2025
2.10.0 199 11/27/2025
2.9.34 243 10/16/2025
2.9.33 926 7/21/2025
2.9.32 780 5/13/2025
2.9.32-pre02 230 4/22/2025
2.9.32-pre01 239 4/17/2025
2.9.31 280 4/2/2025
2.9.30 319 2/25/2025
2.9.6-pre03 167 2/6/2025
2.9.6-pre02 155 1/30/2025
2.9.5 921 12/19/2024
2.9.4 276 12/9/2024
2.9.3 209 12/4/2024
2.9.2 309 11/20/2024
2.9.1 179 11/19/2024
2.9.0 187 11/13/2024
2.9.0-pre08 137 11/13/2024
2.9.0-pre07 144 11/11/2024
2.9.0-pre06 134 11/8/2024
2.9.0-pre05 140 11/6/2024
2.9.0-pre04 149 11/5/2024
2.9.0-pre03 135 10/16/2024
2.9.0-pre02 143 10/14/2024
2.9.0-pre01 146 10/8/2024
2.8.7 176 11/19/2024
2.8.6 219 11/7/2024
2.8.5 1,144 4/15/2024
2.8.3 311 3/1/2024
2.8.2 234 2/14/2024
2.8.1 237 2/1/2024
2.8.0 607 11/29/2023
2.8.0-pre01 191 11/27/2023
2.7.14 345 10/12/2023
2.7.13 1,406 10/10/2023
2.7.12 229 10/9/2023
2.7.12-pre01 182 10/9/2023
2.7.11 327 8/25/2023
2.7.10 263 8/23/2023
2.7.10-pre03 249 8/2/2023
2.7.10-pre02 217 8/1/2023
2.7.9 407 7/13/2023
2.7.8 259 7/10/2023
2.7.7 282 6/26/2023
2.7.6 295 6/13/2023
2.7.6-pre05 208 6/13/2023
2.7.5 303 6/7/2023
2.7.4 395 6/1/2023
2.7.3 915 3/21/2023
2.7.3-pre02 253 3/21/2023
2.7.3-pre01 258 3/21/2023
2.7.2 648 2/15/2023
2.7.1 464 1/31/2023
2.7.1-pre03 292 1/31/2023
2.7.1-pre02 263 1/19/2023
2.7.1-pre01 259 1/17/2023
2.6.6 803 1/9/2023
2.6.5 1,348 7/26/2022
2.6.4 1,034 4/25/2022
2.6.3 1,123 3/14/2022
2.6.3-preview01 328 3/3/2022
2.6.2 730 3/1/2022
2.6.2-preview01 314 3/1/2022
2.6.1 634 2/24/2022
2.6.0 643 2/21/2022
2.3.11 778 2/24/2022
2.3.10 912 1/24/2022
2.3.9 707 12/6/2021
2.3.8 939 10/19/2021
2.3.7 798 10/4/2021
2.3.6 906 9/29/2021
2.3.5 886 6/25/2021
2.3.4 583 6/25/2021
2.3.3 526 6/24/2021
2.3.2 696 5/21/2021
2.3.1 1,198 3/13/2021
2.3.0 888 3/4/2021
2.1.15 798 12/16/2020
2.1.14 1,030 12/15/2020
2.1.13 1,015 7/24/2020
2.1.12 750 6/17/2020
2.1.11 2,351 6/11/2020
2.1.10 2,774 6/4/2020
2.1.9 691 6/1/2020
2.1.8 746 5/22/2020
2.1.7 698 5/6/2020
2.1.6 698 5/4/2020
2.1.5 1,042 4/17/2020
2.1.4 722 4/12/2020
2.1.3 985 2/25/2020
2.1.2 709 2/20/2020
2.1.1 1,109 1/21/2020
2.1.0 1,065 1/9/2020
2.1.0-preview01 653 11/11/2019
2.0.11 761 11/21/2019
2.0.10 735 11/6/2019
2.0.9 719 11/1/2019
2.0.8 754 9/3/2019
2.0.7 1,676 5/7/2019
2.0.6 809 4/29/2019
2.0.5 1,175 4/16/2019
2.0.4 1,153 4/10/2019
2.0.2 1,209 3/29/2019
2.0.1 1,128 3/21/2019

v2.10.1 (1.12.2025)
• Zapojení Havit.Data.EntityFrameworkCore.Patterns.Analyzers jako závislosti
• Aktualizace MockQueryable.EntityFrameworkCore
v2.10.0 (27.11.2025)
• Aktualizace na EF Core 10
v2.9.34 (16.10.2025)
• Oprava FakeDataSource - pokud přijme ISoftDeleteManager, předá jej bázové třídě
v2.9.33 (21.7.2025)
• Podpora pro HiLo (IUnitOfWork přidává metody AddForInsertAsync a AddRangeForInsertAsync)
v2.9.32 (13.5.2025)
• SoftDeleteManager může být (opět) registrován s lifetime Scoped.
• Opravy pádů při použití navigace Many-To-Many.
v2.9.30 (25.2.2025)
• Podpora non-int primárních klíčů (podporovány jsou SByte, Int16, Int32, Int64 a unsigned varianty, Guid, string)
• Náhrada IRepository<TEntity> ve prospěch IRepository<TEntity, TKey> a související úpravy
v2.9.5 (19.12.2024)
• Vypnutí cachování nyní funguje
v2.9.4 (9.12.2024)
• DbUnitOfWork - Podpora registrace asynchronních after commit akcí (použitelné jen v CommmitAsync)
• ComponentRegistrationOptions - oprava (ne)možnosti vytvoření instance
v2.9.3 (4.12.2024)
• Odstranění výchozí implementace v IBeforeCommitProcessor<>, doplnění bázové třídy BeforeCommitProcessor.
v2.9.2 (20.11.2024)
• Uvolnění příliš omezující podmínky v QueryBase
v2.9.0 (12.11.2024)
• Aktualizace na EF Core 9
Novinky, úpravy:
• IUnitOfWork.Clear() pro vyčištění change trackeru (a samotného unit of worku)
• DbUnitOfWork.Commit[Async] - omezeno snížení volání detekce změn ze 3 na 1 (občas na 2)
• DbUnitOfWork PerformAddForInsert/Update/Delete - přibylo přetížení pro 1 entitu a stávající pole změněno na IEnumeable<>, breaking changes pro ty, kteří mají vlastní DbUnitOfWork s přetíženými metodami [BREAKING CHANGE]
• BeforeCommitProcessory nyní vrací informaci o tom, zda provedl úpravu s dopadem na change tracker [BREAKING CHANGE]
• BeforeCommitProcessory - podpora pro async variantu (lze použít jen v asynchronním commitu!)
• Seedování dat nyní používá UnitOfWork, neobchází tak (čištění) cache, používá before commit processory, atp.
• Seedování dat nyní izoluje jednotlivé seedy pomocí IUnitOfWork.Clear()
• Seedování má možnost nově použít SeedAsync (nutné pro fungování asynchronních before commit procesorů)
• DbDataLoader, DbDataSeedPersister - lepší identifikace zdroje SQL dotazu pomocí TagWith
• Změna registrace do DI containeru (viz níže) [BREAKING CHANGE]
• Odstranění obsolete memberů [BREAKING CHANGE]
• Spousta optimalizací alokací paměti a výkonu
Dependency Injection [BREAKING CHANGE]
• Odstranění IEntityPatternInstaller, WithEntityPatternsInstaller, použito jen IServiceCollection
• Registrace DbContextu pomocí prostředků samotného EF Core
• AddEntityPatterns - zrušen
• AddDataLayer - metoda nahrazena generovanou metodou AddDayaLayerServices
• AddDataLayer/AddDayaLayerServices nově neregistruje data seedy a potřebuje explicitní registraci
• ComponentRegistrationOptions již nemá DbUnitOfWorkType (vlastní unit of work lze zaregistrovat samostatně)
Optimalizace:
• DbUnitOfWork.Commit[Async] - optimalizace alokací paměti a využití reflexe v BeforeCommitProcessorech+EntityValidators
• Dictionary nahrazen FrozenDictionary, sestavení při startu aplikace, atp.
• DbDataLoader - optimalizace iterací polí, alokací paměti, předávání dat do následujícího ThenLoad