Havit.Data.EntityFrameworkCore.Patterns
2.10.2-pre01
Prefix Reserved
dotnet add package Havit.Data.EntityFrameworkCore.Patterns --version 2.10.2-pre01
NuGet\Install-Package Havit.Data.EntityFrameworkCore.Patterns -Version 2.10.2-pre01
<PackageReference Include="Havit.Data.EntityFrameworkCore.Patterns" Version="2.10.2-pre01" />
<PackageVersion Include="Havit.Data.EntityFrameworkCore.Patterns" Version="2.10.2-pre01" />
<PackageReference Include="Havit.Data.EntityFrameworkCore.Patterns" />
paket add Havit.Data.EntityFrameworkCore.Patterns --version 2.10.2-pre01
#r "nuget: Havit.Data.EntityFrameworkCore.Patterns, 2.10.2-pre01"
#:package Havit.Data.EntityFrameworkCore.Patterns@2.10.2-pre01
#addin nuget:?package=Havit.Data.EntityFrameworkCore.Patterns&version=2.10.2-pre01&prerelease
#tool nuget:?package=Havit.Data.EntityFrameworkCore.Patterns&version=2.10.2-pre01&prerelease
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)
- aggregate root (
- 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
LanguageimplementujícíHavit.Model.Localizations.ILanguage - interface
ILocalized<TLocalizationEntity>dědící zHavit.Model.Localizations.ILocalized<TLocalizationEntity, Language>pro označení tříd, které jsou lokalizovány - interface
ILocalization<TLocalizedEntity>dědící zHavit.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í
DbContextOptionspro 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.DataTypeAttributeConventionPokud je vlastnost třídy modelu označena atributem DataTypeAttribute s hodnotou DataType.Date pak se použije v databázi datový typ Date.CascadeDeleteToRestictConventionVšem cizím klíčům s nastaví DeleteBehavior na Restrict, čímž zamezí kaskádnímu delete.CacheAttributeToAnnotationConventionHodnoty zadané v atributu[Cache]předá do anotations.
Volitelné konvence
StringPropertiesDefaultValueConventionPro všechny stringové vlastnosti, pokud nemají výchozí hodnotu, se použije výchozí hodnotaString.Empty. Nastavuje vlastnosti výchozí hodnotu aValueGeneratednaNeverdle Entity Framework Core - 10 - Know-How - Výchozí hodnoty v databázi vs. uložené hodnoty.
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.Toolnení "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 .
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).MetadataNamespacenamespace, 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
V 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
V 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řídaFakeEntityDataSource, např.FakeLaguageDataSource), avšak nejsou napojeny na databázi. - Třídy jsou dekorovány atributem
[Fake]a jsou vnořeny do namespaceFakes. - 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
DataaDataIncludingDeleted- 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í cokolivExpressionrozšiřujeFunco to, že se lambda výraz přeloží jako expression treeIEnumerableří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.PropertyBlze použít jen tehdy, pokudPropertyAnení kolekcí objektů).- použijte override
LoadReferences+LoadReferencesAsync
- použijte override
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
Idzá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:
- Nejprve získáme všechny klíče entit dle vyhledávané vlastnosti
- Poté všechny entity načteme pomocí repository.GetObjects(…), čímž dostaneme objekty do paměti (identity mapy, DbContext).
- 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.Zemespustí 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é vlastnostiLoadAll, `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í) aXyzIncludingDeleted(obvykle persistentní), pak je použití kolekce Xyz automaticky nahrazeno načtením kolekceXyzIncludingDeleted. - 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 kolekciXyzIncludingDeleted, 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.Deletedvyvolá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 cacheXyDbRespository.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 cacheXyDbRepository.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 cacheDbUnitOfWork.Commit[Async]- invaliduje položky v cacheXyEntries.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 nikolivIEntityCacheKeyGenerator- definuje, pod jakým klíčem bude entita uložena do cacheIEntityCacheOptionsGenerator- 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.BlackDataLoader.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.AdministratorDataLoader.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ýmId(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
GetAlltypu 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á
Personvlastnost pro domácí adresuHomeAddress(owned) typuAddress, pak při změně (např.) uliceChangeTrackervidí změnu v owned entitěAddress, nikoliv vPerson. 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í
Persondo 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]ForInsertAdd[Range]ForInsertAsync- určeno pro použití HiLo strategie generování IdAdd[Range]ForUpdateAdd[Range]ForDeleteCommit[Async]RegisterAfterCommitActionClear- 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 | Versions 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. |
-
net10.0
- Havit.Core (>= 2.0.35)
- Havit.Data.EntityFrameworkCore (>= 2.10.0)
- Havit.Data.EntityFrameworkCore.Patterns.Analyzers (>= 2.10.1)
- Havit.Data.Patterns (>= 2.1.33)
- MockQueryable.EntityFrameworkCore (>= 10.0.1)
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