Werken met gegevens in ASP.NET Core Apps
Tip
Deze inhoud is een fragment uit het eBook, Architect Modern Web Applications met ASP.NET Core en Azure, beschikbaar op .NET Docs of als een gratis downloadbare PDF die offline kan worden gelezen.
"Gegevens zijn kostbaar en zullen langer duren dan de systemen zelf."
Tim Berners-Lee
Gegevenstoegang is een belangrijk onderdeel van bijna elke softwaretoepassing. ASP.NET Core ondersteunt verschillende opties voor gegevenstoegang, waaronder Entity Framework Core (en Entity Framework 6), en kan werken met elk .NET-framework voor gegevenstoegang. Welke framework voor gegevenstoegang moet worden gebruikt, is afhankelijk van de behoeften van de toepassing. Het abstraheren van deze keuzes uit de ApplicationCore- en UI-projecten en het inkapselen van implementatiedetails in Infrastructuur helpt bij het produceren van losjes gekoppelde, testbare software.
Entity Framework Core (voor relationele databases)
Als u een nieuwe ASP.NET Core-toepassing schrijft die met relationele gegevens moet werken, is Entity Framework Core (EF Core) de aanbevolen manier voor uw toepassing om toegang te krijgen tot de gegevens. EF Core is een object-relationele mapper (O/RM) waarmee .NET-ontwikkelaars objecten van en naar een gegevensbron kunnen behouden. Het elimineert de noodzaak voor de meeste ontwikkelaars van gegevenstoegangscode die doorgaans moeten worden geschreven. Net als ASP.NET Core is EF Core helemaal opnieuw geschreven om modulaire platformoverschrijdende toepassingen te ondersteunen. U voegt deze toe aan uw toepassing als een NuGet-pakket, configureert deze tijdens het opstarten van de app en vraagt deze aan via afhankelijkheidsinjectie, waar u het ook nodig hebt.
Als u EF Core wilt gebruiken met een SQL Server-database, voert u de volgende dotnet CLI-opdracht uit:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
Ondersteuning voor een InMemory-gegevensbron toevoegen voor testen:
dotnet add package Microsoft.EntityFrameworkCore.InMemory
The DbContext
Als u met EF Core wilt werken, hebt u een subklasse van DbContextnodig. Deze klasse bevat eigenschappen die verzamelingen vertegenwoordigen van de entiteiten waarmee uw toepassing werkt. Het eShopOnWeb-voorbeeld bevat een CatalogContext
verzamelingen voor items, merken en typen:
public class CatalogContext : DbContext
{
public CatalogContext(DbContextOptions<CatalogContext> options) : base(options)
{
}
public DbSet<CatalogItem> CatalogItems { get; set; }
public DbSet<CatalogBrand> CatalogBrands { get; set; }
public DbSet<CatalogType> CatalogTypes { get; set; }
}
Uw DbContext moet een constructor hebben die dit argument accepteert DbContextOptions
en doorgeeft aan de basisconstructor DbContext
. Als u slechts één DbContext in uw toepassing hebt, kunt u een exemplaar doorgeven van DbContextOptions
, maar als u meer dan één exemplaar hebt, moet u het algemene DbContextOptions<T>
type gebruiken, waarbij uw DbContext-type wordt doorgegeven als de algemene parameter.
EF Core configureren
In uw ASP.NET Core-toepassing configureert u EF Core doorgaans in Program.cs met de andere afhankelijkheden van uw toepassing. EF Core maakt gebruik van een DbContextOptionsBuilder
, die verschillende nuttige uitbreidingsmethoden ondersteunt om de configuratie te stroomlijnen. Als u CatalogContext wilt configureren voor het gebruik van een SQL Server-database met een verbindingsreeks gedefinieerd in Configuration, voegt u de volgende code toe:
builder.Services.AddDbContext<CatalogContext>(
options => options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection")));
De in-memory database gebruiken:
builder.Services.AddDbContext<CatalogContext>(options =>
options.UseInMemoryDatabase());
Nadat u EF Core hebt geïnstalleerd, een onderliggend dbContext-type hebt gemaakt en het type hebt toegevoegd aan de services van de toepassing, kunt u EF Core gebruiken. U kunt een exemplaar van uw DbContext-type aanvragen in elke service die deze nodig heeft en aan de slag gaan met uw persistente entiteiten met behulp van LINQ alsof ze zich gewoon in een verzameling bevinden. EF Core vertaalt uw LINQ-expressies in SQL-query's om uw gegevens op te slaan en op te halen.
U kunt zien welke query's EF Core uitvoert door een logboekregistratie te configureren en ervoor te zorgen dat het niveau ervan is ingesteld op ten minste informatie, zoals wordt weergegeven in afbeelding 8-1.
Afbeelding 8-1. EF Core-query's vastleggen in de console
Gegevens ophalen en opslaan
Als u gegevens wilt ophalen uit EF Core, opent u de juiste eigenschap en gebruikt u LINQ om het resultaat te filteren. U kunt LINQ ook gebruiken om projectie uit te voeren, waardoor het resultaat van het ene type naar het andere wordt getransformeerd. In het volgende voorbeeld worden CatalogBrands opgehaald, gesorteerd op naam, gefilterd op de eigenschap Ingeschakeld en geprojecteerd op een SelectListItem
type:
var brandItems = await _context.CatalogBrands
.Where(b => b.Enabled)
.OrderBy(b => b.Name)
.Select(b => new SelectListItem {
Value = b.Id, Text = b.Name })
.ToListAsync();
Het is belangrijk in het bovenstaande voorbeeld om de aanroep toe te ToListAsync
voegen om de query onmiddellijk uit te voeren. Anders wijst de instructie een IQueryable<SelectListItem>
toe aan brandItems, die pas wordt uitgevoerd nadat deze is geïnventariseerd. Er zijn voor- en nadelen om resultaten van methoden te retourneren IQueryable
. Hiermee kan de query EF Core verder worden gewijzigd, maar kan dit ook leiden tot fouten die alleen tijdens runtime optreden, als bewerkingen worden toegevoegd aan de query die EF Core niet kan vertalen. Het is over het algemeen veiliger om filters door te geven aan de methode voor het uitvoeren van de gegevenstoegang en het retourneren van een in-memory verzameling (bijvoorbeeld List<T>
) als resultaat.
EF Core houdt wijzigingen bij op entiteiten die worden opgehaald uit persistentie. Als u wijzigingen in een bijgehouden entiteit wilt opslaan, roept u de SaveChangesAsync
methode op dbContext aan, waarbij u ervoor zorgt dat dit hetzelfde DbContext-exemplaar is dat is gebruikt om de entiteit op te halen. Het toevoegen en verwijderen van entiteiten wordt rechtstreeks uitgevoerd op de juiste DbSet-eigenschap, opnieuw met een aanroep om de databaseopdrachten uit te SaveChangesAsync
voeren. In het volgende voorbeeld ziet u hoe u entiteiten toevoegt, bijwerkt en verwijdert uit persistentie.
// create
var newBrand = new CatalogBrand() { Brand = "Acme" };
_context.Add(newBrand);
await _context.SaveChangesAsync();
// read and update
var existingBrand = _context.CatalogBrands.Find(1);
existingBrand.Brand = "Updated Brand";
await _context.SaveChangesAsync();
// read and delete (alternate Find syntax)
var brandToDelete = _context.Find<CatalogBrand>(2);
_context.CatalogBrands.Remove(brandToDelete);
await _context.SaveChangesAsync();
EF Core ondersteunt zowel synchrone als asynchrone methoden voor het ophalen en opslaan van bestanden. In webtoepassingen is het raadzaam om het asynchrone/wachtpatroon te gebruiken met de asynchrone methoden, zodat webserverthreads niet worden geblokkeerd terwijl wordt gewacht totdat de bewerkingen voor gegevenstoegang zijn voltooid.
Zie Buffering en streaming voor meer informatie.
Gerelateerde gegevens ophalen
Wanneer EF Core entiteiten ophaalt, worden alle eigenschappen ingevuld die rechtstreeks met die entiteit in de database worden opgeslagen. Navigatie-eigenschappen, zoals lijsten met gerelateerde entiteiten, worden niet ingevuld en zijn mogelijk ingesteld op null. Dit proces zorgt ervoor dat EF Core niet meer gegevens ophaalt dan nodig is, wat vooral belangrijk is voor webtoepassingen, die aanvragen snel moeten verwerken en antwoorden op een efficiënte manier moeten retourneren. Als u relaties met een entiteit wilt opnemen met behulp van gretig laden, geeft u de eigenschap op met behulp van de extensiemethode Opnemen in de query, zoals wordt weergegeven:
// .Include requires using Microsoft.EntityFrameworkCore
var brandsWithItems = await _context.CatalogBrands
.Include(b => b.Items)
.ToListAsync();
U kunt meerdere relaties opnemen en u kunt ook subrelaties opnemen met Behulp van ThenInclude. EF Core voert één query uit om de resulterende set entiteiten op te halen. U kunt ook navigatie-eigenschappen van navigatie-eigenschappen opnemen door een '.' door te geven. -gescheiden tekenreeks voor de .Include()
extensiemethode, zoals:
.Include("Items.Products")
Naast het inkapselen van filterlogica kan een specificatie de vorm opgeven van de gegevens die moeten worden geretourneerd, inclusief welke eigenschappen moeten worden ingevuld. Het eShopOnWeb-voorbeeld bevat verschillende specificaties die aantonen dat er gretig laadinformatie binnen de specificatie wordt ingekapseld. U kunt hier zien hoe de specificatie wordt gebruikt als onderdeel van een query:
// Includes all expression-based includes
query = specification.Includes.Aggregate(query,
(current, include) => current.Include(include));
// Include any string-based include statements
query = specification.IncludeStrings.Aggregate(query,
(current, include) => current.Include(include));
Een andere optie voor het laden van gerelateerde gegevens is het gebruik van expliciet laden. Met expliciet laden kunt u extra gegevens laden in een entiteit die al is opgehaald. Omdat deze benadering een afzonderlijke aanvraag voor de database omvat, wordt het niet aanbevolen voor webtoepassingen, waardoor het aantal retouren per database per aanvraag moet worden geminimaliseerd.
Luie laadfunctie is een functie waarmee automatisch gerelateerde gegevens worden geladen, omdat ernaar wordt verwezen door de toepassing. EF Core heeft ondersteuning toegevoegd voor luie laadtijden in versie 2.1. Luie laden is niet standaard ingeschakeld en vereist het installeren van de Microsoft.EntityFrameworkCore.Proxies
. Net als bij expliciet laden moet luie laden doorgaans worden uitgeschakeld voor webtoepassingen, omdat het gebruik ervan leidt tot extra databasequery's binnen elke webaanvraag. Helaas gaat de overhead die wordt gemaakt door luie belasting vaak onopgemerkt tijdens de ontwikkeling, wanneer de latentie klein is en vaak de gegevenssets die worden gebruikt voor het testen, klein zijn. In productie, met meer gebruikers, meer gegevens en meer latentie, kunnen de extra databaseaanvragen vaak leiden tot slechte prestaties voor webtoepassingen die intensief gebruikmaken van luie laadbewerkingen.
Vermijd luie laadentiteiten in webtoepassingen
Het is een goed idee om uw toepassing te testen tijdens het onderzoeken van de werkelijke databasequery's die worden gemaakt. Onder bepaalde omstandigheden kan EF Core veel meer query's of een duurdere query maken dan optimaal is voor de toepassing. Een dergelijk probleem staat bekend als een cartesische explosie. Het EF Core-team maakt de AsSplitQuery-methode beschikbaar als een van de verschillende manieren om runtimegedrag af te stemmen.
Gegevens inkapselen
EF Core ondersteunt verschillende functies waarmee uw model de status ervan correct kan inkapselen. Een veelvoorkomend probleem in domeinmodellen is dat ze eigenschappen voor verzamelingsnavigatie beschikbaar maken als openbaar toegankelijke lijsttypen. Met dit probleem kan elke samenwerker de inhoud van deze verzamelingstypen bewerken, waardoor belangrijke bedrijfsregels met betrekking tot de verzameling kunnen worden overgeslagen, waardoor het object mogelijk een ongeldige status heeft. De oplossing voor dit probleem is het beschikbaar maken van alleen-lezentoegang tot gerelateerde verzamelingen en expliciet methoden bieden voor het definiëren van manieren waarop clients deze kunnen bewerken, zoals in dit voorbeeld:
public class Basket : BaseEntity
{
public string BuyerId { get; set; }
private readonly List<BasketItem> _items = new List<BasketItem>();
public IReadOnlyCollection<BasketItem> Items => _items.AsReadOnly();
public void AddItem(int catalogItemId, decimal unitPrice, int quantity = 1)
{
var existingItem = Items.FirstOrDefault(i => i.CatalogItemId == catalogItemId);
if (existingItem == null)
{
_items.Add(new BasketItem()
{
CatalogItemId = catalogItemId,
Quantity = quantity,
UnitPrice = unitPrice
});
}
else existingItem.Quantity += quantity;
}
}
Met dit entiteitstype wordt geen openbare List
eigenschap of ICollection
eigenschap weergegeven, maar wordt in plaats daarvan een IReadOnlyCollection
type weergegeven dat het onderliggende lijsttype verpakt. Wanneer u dit patroon gebruikt, kunt u aan Entity Framework Core aangeven dat het backingveld als volgt moet worden gebruikt:
private void ConfigureBasket(EntityTypeBuilder<Basket> builder)
{
var navigation = builder.Metadata.FindNavigation(nameof(Basket.Items));
navigation.SetPropertyAccessMode(PropertyAccessMode.Field);
}
Een andere manier waarop u uw domeinmodel kunt verbeteren, is door waardeobjecten te gebruiken voor typen die geen identiteit hebben en alleen worden onderscheiden door hun eigenschappen. Het gebruik van dergelijke typen als eigenschappen van uw entiteiten kan helpen logica specifiek te houden voor het waardeobject waar het hoort en kan dubbele logica voorkomen tussen meerdere entiteiten die hetzelfde concept gebruiken. In Entity Framework Core kunt u waardeobjecten in dezelfde tabel behouden als hun eigen entiteit door het type als een entiteit in eigendom te configureren, zoals:
private void ConfigureOrder(EntityTypeBuilder<Order> builder)
{
builder.OwnsOne(o => o.ShipToAddress);
}
In dit voorbeeld is de eigenschap van het ShipToAddress
type Address
. Address
is een waardeobject met verschillende eigenschappen zoals Street
en City
. EF Core wijst het object toe aan de Order
tabel met één kolom per Address
eigenschap, waarbij elke kolomnaam wordt voorafgegaan door de naam van de eigenschap. In dit voorbeeld bevat de Order
tabel kolommen zoals ShipToAddress_Street
en ShipToAddress_City
. Het is ook mogelijk om desgewenst eigen typen op te slaan in afzonderlijke tabellen.
Meer informatie over ondersteuning voor entiteiten in eigendom in EF Core.
Tolerante verbindingen
Externe resources zoals SQL-databases zijn soms niet beschikbaar. In gevallen van tijdelijke onbeschikbaarheid kunnen toepassingen logica voor opnieuw proberen gebruiken om te voorkomen dat er een uitzondering wordt gegenereerd. Deze techniek wordt vaak aangeduid als verbindingstolerantie. U kunt uw eigen nieuwe poging implementeren met een exponentiële uitsteltechniek door te proberen opnieuw te proberen met een exponentieel toenemende wachttijd, totdat een maximumaantal nieuwe pogingen is bereikt. Deze techniek omvat het feit dat cloudresources af en toe niet beschikbaar zijn gedurende korte tijd, wat resulteert in het mislukken van sommige aanvragen.
Voor Azure SQL DB biedt Entity Framework Core al tolerantie voor interne databaseverbindingen en logica voor opnieuw proberen. Maar u moet de uitvoeringsstrategie van Entity Framework voor elke DbContext-verbinding inschakelen als u flexibele EF Core-verbindingen wilt hebben.
De volgende code op het niveau van de EF Core-verbinding maakt bijvoorbeeld tolerante SQL-verbindingen mogelijk die opnieuw worden geprobeerd als de verbinding mislukt.
builder.Services.AddDbContext<OrderingContext>(options =>
{
options.UseSqlServer(builder.Configuration["ConnectionString"],
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorNumbersToAdd: null);
}
);
});
Uitvoeringsstrategieën en expliciete transacties met Behulp van BeginTransaction en meerdere DbContexts
Wanneer nieuwe pogingen zijn ingeschakeld in EF Core-verbindingen, wordt elke bewerking die u uitvoert met EF Core een eigen bewerking die opnieuw kan worden geprobeerd. Elke query en elke aanroep worden SaveChangesAsync
opnieuw geprobeerd als een eenheid als er een tijdelijke fout optreedt.
Als uw code echter een transactie initieert met Behulp van BeginTransaction, definieert u uw eigen groep bewerkingen die als een eenheid moeten worden behandeld; alles binnen de transactie moet worden teruggedraaid als er een fout optreedt. U ziet een uitzondering zoals de volgende als u die transactie probeert uit te voeren wanneer u een EF-uitvoeringsstrategie (beleid voor opnieuw proberen) gebruikt en u er meerdere SaveChangesAsync
van meerdere DbContexts in opneemt.
System.InvalidOperationException: De geconfigureerde uitvoeringsstrategie SqlServerRetryingExecutionStrategy
biedt geen ondersteuning voor door de gebruiker geïnitieerde transacties. Gebruik de uitvoeringsstrategie die wordt geretourneerd door DbContext.Database.CreateExecutionStrategy()
alle bewerkingen in de transactie uit te voeren als een eenheid die opnieuw kan worden geprobeerd.
De oplossing is om de EF-uitvoeringsstrategie handmatig aan te roepen met een gemachtigde die alles vertegenwoordigt dat moet worden uitgevoerd. Als er een tijdelijke fout optreedt, roept de uitvoeringsstrategie de gemachtigde opnieuw aan. De volgende code laat zien hoe u deze aanpak implementeert:
// Use of an EF Core resiliency strategy when using multiple DbContexts
// within an explicit transaction
// See:
// https://learn.microsoft.com/ef/core/miscellaneous/connection-resiliency
var strategy = _catalogContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
// Achieving atomicity between original Catalog database operation and the
// IntegrationEventLog thanks to a local transaction
using (var transaction = _catalogContext.Database.BeginTransaction())
{
_catalogContext.CatalogItems.Update(catalogItem);
await _catalogContext.SaveChangesAsync();
// Save to EventLog only if product price changed
if (raiseProductPriceChangedEvent)
{
await _integrationEventLogService.SaveEventAsync(priceChangedEvent);
transaction.Commit();
}
}
});
De eerste DbContext is de _catalogContext
en de tweede DbContext bevindt zich in het _integrationEventLogService
object. Ten slotte wordt de doorvoeractie meerdere DbContexts uitgevoerd en wordt een EF-uitvoeringsstrategie gebruikt.
Verwijzingen – Entity Framework Core
- EF Core Docshttps://learn.microsoft.com/ef/
- EF Core: Gerelateerde gegevenshttps://learn.microsoft.com/ef/core/querying/related-data
- Luie laadentiteiten voorkomen in ASPNET-toepassingenhttps://ardalis.com/avoid-lazy-loading-entities-in-asp-net-applications
EF Core of micro-ORM?
HOEWEL EF Core een uitstekende keuze is voor het beheren van persistentie en voor het grootste deel databasegegevens van toepassingsontwikkelaars inkapselt, is dit niet de enige keuze. Een ander populair opensource alternatief is Dapper, een zogenaamde micro-ORM. Een micro-ORM is een lichtgewicht, minder volledig hulpmiddel voor het toewijzen van objecten aan gegevensstructuren. In het geval van Dapper richten de ontwerpdoelen zich op prestaties, in plaats van volledig de onderliggende query's in te kapselen die worden gebruikt om gegevens op te halen en bij te werken. Omdat sql niet abstract is van de ontwikkelaar, is Dapper 'dichter bij het metaal' en kunnen ontwikkelaars de exacte query's schrijven die ze willen gebruiken voor een bepaalde bewerking voor gegevenstoegang.
EF Core heeft twee belangrijke functies die het van Dapper scheidt, maar ook toevoegt aan de prestatieoverhead. De eerste is de vertaling van LINQ-expressies in SQL. Deze vertalingen worden in de cache opgeslagen, maar er is zelfs overhead bij het uitvoeren van deze vertalingen de eerste keer. De tweede is het bijhouden van wijzigingen voor entiteiten (zodat efficiënte update-instructies kunnen worden gegenereerd). Dit gedrag kan worden uitgeschakeld voor specifieke query's met behulp van de AsNoTracking extensie. EF Core genereert ook SQL-query's die meestal zeer efficiënt zijn en in elk geval perfect acceptabel zijn vanuit het oogpunt van prestaties, maar als u een nauwkeurige controle nodig hebt over de precieze query die moet worden uitgevoerd, kunt u ook aangepaste SQL doorgeven (of een opgeslagen procedure uitvoeren) met EF Core. In dit geval presteert Dapper nog steeds beter dan EF Core, maar slechts een beetje. Actuele prestatiebenchmarkgegevens voor verschillende methoden voor gegevenstoegang vindt u op de Dapper-site.
Als u wilt zien hoe de syntaxis voor Dapper verschilt van EF Core, kunt u deze twee versies van dezelfde methode overwegen voor het ophalen van een lijst met items:
// EF Core
private readonly CatalogContext _context;
public async Task<IEnumerable<CatalogType>> GetCatalogTypes()
{
return await _context.CatalogTypes.ToListAsync();
}
// Dapper
private readonly SqlConnection _conn;
public async Task<IEnumerable<CatalogType>> GetCatalogTypesWithDapper()
{
return await _conn.QueryAsync<CatalogType>("SELECT * FROM CatalogType");
}
Als u complexere objectgrafieken wilt bouwen met Dapper, moet u zelf de bijbehorende query's schrijven (in tegenstelling tot het toevoegen van een Include zoals u dat zou doen in EF Core). Deze functionaliteit wordt ondersteund via verschillende syntaxis, waaronder een functie met de naam MultiToewijzing waarmee u afzonderlijke rijen kunt toewijzen aan meerdere toegewezen objecten. Als u bijvoorbeeld een klassepost met een eigenschapseigenaar van het type Gebruiker hebt, retourneert de volgende SQL alle benodigde gegevens:
select * from #Posts p
left join #Users u on u.Id = p.OwnerId
Order by p.Id
Elke geretourneerde rij bevat zowel gebruikers- als postgegevens. Omdat de gebruikersgegevens via de eigenschap Eigenaar aan de Post-gegevens moeten worden gekoppeld, wordt de volgende functie gebruikt:
(post, user) => { post.Owner = user; return post; }
De volledige codevermelding voor het retourneren van een verzameling berichten met de eigenschap Eigenaar die is gevuld met de bijbehorende gebruikersgegevens, is:
var sql = @"select * from #Posts p
left join #Users u on u.Id = p.OwnerId
Order by p.Id";
var data = connection.Query<Post, User, Post>(sql,
(post, user) => { post.Owner = user; return post;});
Omdat het minder inkapseling biedt, moeten ontwikkelaars meer weten over hoe hun gegevens worden opgeslagen, hoe ze deze efficiënt kunnen opvragen en meer code schrijven om deze op te halen. Wanneer het model wordt gewijzigd, moet elke betrokken query worden bijgewerkt in plaats van alleen een nieuwe migratie te maken (een andere EF Core-functie) en/of toewijzingsgegevens bij te werken op één plaats in een DbContext. Deze query's hebben geen gecompileerd-tijdgaranties, zodat ze tijdens runtime kunnen worden onderbroken als reactie op wijzigingen in het model of de database, waardoor fouten moeilijker te detecteren zijn. In ruil voor deze compromissen biedt Dapper extreem snelle prestaties.
Voor de meeste toepassingen en de meeste onderdelen van bijna alle toepassingen biedt EF Core acceptabele prestaties. De productiviteitsvoordelen van de ontwikkelaar wegen dus waarschijnlijk op tegen de prestatieoverhead. Voor query's die kunnen profiteren van caching, kan de werkelijke query slechts een klein percentage van de tijd worden uitgevoerd, waardoor er relatief kleine verschillen in queryprestaties zijn.
SQL of NoSQL
Relationele databases zoals SQL Server hebben traditioneel de marketplace voor permanente gegevensopslag overheerst, maar zijn niet de enige oplossing die beschikbaar is. NoSQL-databases zoals MongoDB bieden een andere benadering voor het opslaan van objecten. In plaats van objecten toe te passen aan tabellen en rijen, is een andere optie om de hele objectgrafiek te serialiseren en het resultaat op te slaan. De voordelen van deze benadering zijn in eerste instantie eenvoud en prestaties. Het is eenvoudiger om één geserialiseerd object met een sleutel op te slaan dan het object op te splitsen in veel tabellen met relaties en rijen bij te werken die mogelijk zijn gewijzigd sinds het object voor het laatst is opgehaald uit de database. Op dezelfde manier is het ophalen en deserialiseren van één object uit een sleutelarchief doorgaans veel sneller en eenvoudiger dan complexe joins of meerdere databasequery's die nodig zijn om hetzelfde object volledig op te stellen vanuit een relationele database. Door het ontbreken van vergrendelingen of transacties of een vast schema kunnen NoSQL-databases ook worden geschaald op veel computers, waardoor zeer grote gegevenssets worden ondersteund.
Aan de andere kant hebben NoSQL-databases (zoals ze meestal worden genoemd) hun nadelen. Relationele databases maken gebruik van normalisatie om consistentie af te dwingen en duplicatie van gegevens te voorkomen. Deze aanpak vermindert de totale grootte van de database en zorgt ervoor dat updates voor gedeelde gegevens onmiddellijk beschikbaar zijn in de hele database. In een relationele database kan een adrestabel verwijzen naar een landtabel op id, zodat als de naam van een land/regio is gewijzigd, de adresrecords baat hebben bij de update zonder dat ze zelf moeten worden bijgewerkt. In een NoSQL-database, Adres en het bijbehorende land kan echter worden geserialiseerd als onderdeel van veel opgeslagen objecten. Voor een update van een land-/regionaam moeten al deze objecten worden bijgewerkt in plaats van één rij. Relationele databases kunnen ook relationele integriteit garanderen door regels zoals refererende sleutels af te dwingen. NoSQL-databases bieden doorgaans geen dergelijke beperkingen voor hun gegevens.
Een andere complexiteit waarmee NoSQL-databases te maken hebben, is versiebeheer. Wanneer de eigenschappen van een object worden gewijzigd, kan het mogelijk niet worden gedeserialiseerd vanuit eerdere versies die zijn opgeslagen. Daarom moeten alle bestaande objecten met een geserialiseerde (vorige) versie van het object worden bijgewerkt om te voldoen aan het nieuwe schema. Deze benadering verschilt niet conceptueel van een relationele database, waarbij schemawijzigingen soms updatescripts of toewijzingsupdates vereisen. Het aantal vermeldingen dat moet worden gewijzigd, is echter vaak veel groter in de NoSQL-benadering, omdat er meer duplicatie van gegevens is.
Het is mogelijk in NoSQL-databases om meerdere versies van objecten op te slaan, iets dat relationele schemadatabases doorgaans niet ondersteunen. In dit geval moet uw toepassingscode echter rekening houden met het bestaan van eerdere versies van objecten, wat extra complexiteit toevoegt.
NoSQL-databases dwingen doorgaans GEEN ACID af, wat betekent dat ze zowel prestatie- als schaalbaarheidsvoordelen hebben ten opzichte van relationele databases. Ze zijn zeer geschikt voor extreem grote gegevenssets en objecten die niet geschikt zijn voor opslag in genormaliseerde tabelstructuren. Er is geen reden waarom één toepassing niet kan profiteren van zowel relationele als NoSQL-databases, waarbij elke toepassing het meest geschikt is.
Azure Cosmos DB
Azure Cosmos DB is een volledig beheerde NoSQL-databaseservice die cloudgebaseerde schemavrije gegevensopslag biedt. Azure Cosmos DB is gebouwd voor snelle en voorspelbare prestaties, hoge beschikbaarheid, elastisch schalen en wereldwijde distributie. Ondanks dat het een NoSQL-database is, kunnen ontwikkelaars gebruikmaken van uitgebreide en vertrouwde SQL-querymogelijkheden op JSON-gegevens. Alle resources in Azure Cosmos DB worden opgeslagen als JSON-documenten. Resources worden beheerd als items, die documenten met metagegevens en feeds zijn, die verzamelingen items zijn. Afbeelding 8-2 toont de relatie tussen verschillende Azure Cosmos DB-resources.
Afbeelding 8-2. Azure Cosmos DB-resourceorganisatie.
De Azure Cosmos DB-querytaal is een eenvoudige maar krachtige interface voor het uitvoeren van query's op JSON-documenten. De taal ondersteunt een subset van de ANSI SQL-grammatica en zorgt voor een diepe integratie van JavaScript-object-, -matrix-, -objectconstructie- en functieaanroepen.
Verwijzingen - Azure Cosmos DB
- Inleiding tot Azure Cosmos DB https://learn.microsoft.com/azure/cosmos-db/introduction
Andere persistentieopties
Naast relationele en NoSQL-opslagopties kunnen ASP.NET Core-toepassingen Azure Storage gebruiken om verschillende gegevensindelingen en bestanden op een schaalbare manier in de cloud op te slaan. Azure Storage is zeer schaalbaar, dus u kunt beginnen met het opslaan van kleine hoeveelheden gegevens en omhoog schalen om honderden of terabytes op te slaan als uw toepassing dit vereist. Azure Storage ondersteunt vier soorten gegevens:
Blob Storage voor ongestructureerde tekst of binaire opslag, ook wel objectopslag genoemd.
Table Storage voor gestructureerde gegevenssets, toegankelijk via rijsleutels.
Queue Storage voor betrouwbare berichten op basis van wachtrijen.
File Storage voor gedeelde bestandstoegang tussen virtuele Azure-machines en on-premises toepassingen.
Verwijzingen – Azure Storage
- Inleiding tot Azure Storage https://learn.microsoft.com/azure/storage/common/storage-introduction
Caching
In webtoepassingen moet elke webaanvraag zo kort mogelijk worden voltooid. Een manier om deze functionaliteit te bereiken, is door het aantal externe aanroepen te beperken dat de server moet uitvoeren om de aanvraag te voltooien. Caching omvat het opslaan van een kopie van gegevens op de server (of een ander gegevensarchief dat gemakkelijker wordt opgevraagd dan de bron van de gegevens). Webtoepassingen, en met name niet-beveiligd-WACHTWOORDVERIFICATIE traditionele webtoepassingen, moeten de volledige gebruikersinterface bouwen met elke aanvraag. Deze benadering omvat vaak het herhaaldelijk maken van veel van dezelfde databasequery's van de ene gebruikersaanvraag naar de volgende. In de meeste gevallen worden deze gegevens zelden gewijzigd, dus er is weinig reden om deze voortdurend aan te vragen vanuit de database. ASP.NET Core ondersteunt het opslaan van antwoorden in cache, voor het opslaan van volledige pagina's en het opslaan van gegevens in cache, wat meer gedetailleerd cachinggedrag ondersteunt.
Bij het implementeren van caching is het belangrijk dat u rekening houdt met de scheiding van problemen. Vermijd het implementeren van cachinglogica in uw gegevenstoegangslogica of in uw gebruikersinterface. In plaats daarvan kapselt u caching in in zijn eigen klassen en gebruikt u de configuratie om het gedrag ervan te beheren. Deze benadering volgt de principes Open/Closed en Single Responsibility en maakt het voor u eenvoudiger om te beheren hoe u caching gebruikt in uw toepassing naarmate deze groeit.
ASP.NET Core-antwoord opslaan in cache
ASP.NET Core ondersteunt twee niveaus van reactiecaching. Het eerste niveau slaat niets op de server in de cache op, maar voegt HTTP-headers toe waarmee clients en proxyservers antwoorden in de cache kunnen opslaan. Deze functionaliteit wordt geïmplementeerd door het kenmerk ResponseCache toe te voegen aan afzonderlijke controllers of acties:
[ResponseCache(Duration = 60)]
public IActionResult Contact()
{
ViewData["Message"] = "Your contact page.";
return View();
}
In het vorige voorbeeld wordt de volgende header toegevoegd aan het antwoord, waardoor clients het resultaat maximaal 60 seconden in de cache moeten opslaan.
Cache-Control: public,max-age=60
Als u cacheopslag aan de serverzijde aan de toepassing wilt toevoegen, moet u verwijzen naar het Microsoft.AspNetCore.ResponseCaching
NuGet-pakket en vervolgens de middleware voor antwoordcaching toevoegen. Deze middleware is geconfigureerd met services en middleware tijdens het opstarten van de app:
builder.Services.AddResponseCaching();
// other code omitted, including building the app
app.UseResponseCaching();
De Middleware voor het opslaan van antwoorden in cache slaat automatisch antwoorden op basis van een set voorwaarden in de cache op, die u kunt aanpassen. Standaard worden slechts 200 (OK)-antwoorden die via GET- of HEAD-methoden zijn aangevraagd, in de cache opgeslagen. Bovendien moeten aanvragen een antwoord hebben met een cachebesturingselement: openbare header en kunnen geen headers voor autorisatie of set-cookie bevatten. Bekijk een volledige lijst met de cachevoorwaarden die worden gebruikt door de middleware voor het opslaan van antwoorden in cache.
Gegevens in de cache
In plaats van (of naast) volledige webreacties in de cache te plaatsen, kunt u de resultaten van afzonderlijke gegevensquery's in de cache opslaan. Voor deze functionaliteit kunt u geheugencache gebruiken op de webserver of een gedistribueerde cache gebruiken. In deze sectie wordt gedemonstreert hoe u implementeert in cachegeheugen.
Voeg ondersteuning toe voor geheugen (of gedistribueerde) caching met de volgende code:
builder.Services.AddMemoryCache();
builder.Services.AddMvc();
Zorg ervoor dat u ook het Microsoft.Extensions.Caching.Memory
NuGet-pakket toevoegt.
Zodra u de service hebt toegevoegd, vraagt IMemoryCache
u via afhankelijkheidsinjectie aan waar u toegang nodig hebt tot de cache. In dit voorbeeld wordt het CachedCatalogService
ontwerppatroon Proxy (of Decorator) gebruikt door een alternatieve implementatie van die implementatie te ICatalogService
bieden waarmee de toegang wordt beheerd (of gedrag toevoegt aan) de onderliggende CatalogService
implementatie.
public class CachedCatalogService : ICatalogService
{
private readonly IMemoryCache _cache;
private readonly CatalogService _catalogService;
private static readonly string _brandsKey = "brands";
private static readonly string _typesKey = "types";
private static readonly TimeSpan _defaultCacheDuration = TimeSpan.FromSeconds(30);
public CachedCatalogService(
IMemoryCache cache,
CatalogService catalogService)
{
_cache = cache;
_catalogService = catalogService;
}
public async Task<IEnumerable<SelectListItem>> GetBrands()
{
return await _cache.GetOrCreateAsync(_brandsKey, async entry =>
{
entry.SlidingExpiration = _defaultCacheDuration;
return await _catalogService.GetBrands();
});
}
public async Task<Catalog> GetCatalogItems(int pageIndex, int itemsPage, int? brandID, int? typeId)
{
string cacheKey = $"items-{pageIndex}-{itemsPage}-{brandID}-{typeId}";
return await _cache.GetOrCreateAsync(cacheKey, async entry =>
{
entry.SlidingExpiration = _defaultCacheDuration;
return await _catalogService.GetCatalogItems(pageIndex, itemsPage, brandID, typeId);
});
}
public async Task<IEnumerable<SelectListItem>> GetTypes()
{
return await _cache.GetOrCreateAsync(_typesKey, async entry =>
{
entry.SlidingExpiration = _defaultCacheDuration;
return await _catalogService.GetTypes();
});
}
}
Als u de toepassing wilt configureren voor het gebruik van de versie in de cache van de service, maar de service nog steeds het exemplaar van CatalogService wilt laten ophalen dat nodig is in de constructor, voegt u de volgende regels toe in Program.cs:
builder.Services.AddMemoryCache();
builder.Services.AddScoped<ICatalogService, CachedCatalogService>();
builder.Services.AddScoped<CatalogService>();
Als deze code is ingesteld, worden de databaseoproepen voor het ophalen van de catalogusgegevens slechts één keer per minuut uitgevoerd in plaats van op elke aanvraag. Afhankelijk van het verkeer naar de site kan dit een aanzienlijke invloed hebben op het aantal query's dat in de database is gemaakt en de gemiddelde laadtijd van de pagina voor de startpagina die momenteel afhankelijk is van alle drie de query's die door deze service worden weergegeven.
Een probleem dat zich voordoet wanneer caching wordt geïmplementeerd, zijn verouderde gegevens , dat wil gezegd, gegevens die zijn gewijzigd bij de bron, maar een verouderde versie in de cache blijft. Een eenvoudige manier om dit probleem te verhelpen is het gebruik van kleine cacheduur, omdat voor een drukke toepassing een beperkt extra voordeel is om de lengtegegevens uit te breiden in de cache. Denk bijvoorbeeld aan een pagina die één databasequery maakt en 10 keer per seconde wordt aangevraagd. Als deze pagina één minuut in de cache wordt opgeslagen, resulteert dit in het aantal databasequery's dat per minuut is gemaakt om van 600 naar 1 te dalen, een vermindering van 99,8%. Als in plaats daarvan de cacheduur één uur is gemaakt, zou de algehele vermindering 99,997% zijn, maar nu worden de waarschijnlijkheid en potentiële leeftijd van verouderde gegevens beide aanzienlijk verhoogd.
Een andere benadering is om cachevermeldingen proactief te verwijderen wanneer de gegevens die ze bevatten, worden bijgewerkt. Elke afzonderlijke vermelding kan worden verwijderd als de sleutel bekend is:
_cache.Remove(cacheKey);
Als uw toepassing functionaliteit beschikbaar maakt voor het bijwerken van vermeldingen die in de cache worden opgeslagen, kunt u de bijbehorende cachevermeldingen in uw code verwijderen waarmee de updates worden uitgevoerd. Soms kunnen er veel verschillende vermeldingen zijn die afhankelijk zijn van een bepaalde set gegevens. In dat geval kan het handig zijn om afhankelijkheden tussen cachevermeldingen te maken met behulp van een CancellationChangeToken. Met een CancellationChangeToken kunt u meerdere cachevermeldingen tegelijk laten verlopen door het token te annuleren.
// configure CancellationToken and add entry to cache
var cts = new CancellationTokenSource();
_cache.Set("cts", cts);
_cache.Set(cacheKey, itemToCache, new CancellationChangeToken(cts.Token));
// elsewhere, expire the cache by cancelling the token\
_cache.Get<CancellationTokenSource>("cts").Cancel();
Caching kan de prestaties van webpagina's die herhaaldelijk dezelfde waarden van de database aanvragen aanzienlijk verbeteren. Zorg ervoor dat u de gegevenstoegang en paginaprestaties meet voordat u caching toepast en pas alleen caching toe wanneer u ziet dat er verbetering nodig is. Caching verbruikt geheugenbronnen van webservers en verhoogt de complexiteit van de toepassing, dus het is belangrijk dat u niet voortijdig optimaliseert met behulp van deze techniek.
Gegevens ophalen naar BlazorWebAssembly apps
Als u apps bouwt die gebruikmaken van Blazor Server, kunt u Entity Framework en andere technologieën voor directe gegevenstoegang gebruiken, zoals deze tot nu toe in dit hoofdstuk zijn besproken. Bij het bouwen van BlazorWebAssembly apps, zoals andere beveiligd-WACHTWOORDVERIFICATIE-frameworks, hebt u echter een andere strategie nodig voor gegevenstoegang. Deze toepassingen hebben doorgaans toegang tot gegevens en communiceren met de server via web-API-eindpunten.
Als de gegevens of bewerkingen die worden uitgevoerd gevoelig zijn, controleert u de sectie over beveiliging in het vorige hoofdstuk en beveiligt u uw API's tegen onbevoegde toegang.
U vindt een voorbeeld van een BlazorWebAssembly app in de eShopOnWeb-referentietoepassing in het BlazorAdmin-project. Dit project wordt gehost in het webproject eShopOnWeb en stelt gebruikers in de groep Administrators in staat om de items in de store te beheren. U ziet een schermopname van de toepassing in afbeelding 8-3.
Afbeelding 8-3. Schermopname van eShopOnWeb Catalog Admin.
Wanneer u gegevens ophaalt uit web-API's binnen een BlazorWebAssembly app, gebruikt u gewoon een exemplaar van HttpClient
zoals u dat zou doen in een .NET-toepassing. De basisstappen zijn het maken van de aanvraag die moet worden verzonden (indien nodig, meestal voor POST- of PUT-aanvragen), wachten op de aanvraag zelf, de statuscode verifiëren en het antwoord deserialiseren. Als u veel aanvragen voor een bepaalde set API's wilt indienen, is het een goed idee om uw API's in te kapselen en het HttpClient
basisadres centraal te configureren. Op deze manier kunt u de wijzigingen op slechts één plek aanbrengen als u een van deze instellingen tussen omgevingen wilt aanpassen. U moet ondersteuning voor deze service toevoegen in uw Program.Main
:
builder.Services.AddScoped(sp => new HttpClient
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});
Als u veilig toegang tot services nodig hebt, moet u toegang krijgen tot een beveiligd token en het HttpClient
token configureren om dit token als verificatieheader door te geven bij elke aanvraag:
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
Deze activiteit kan worden uitgevoerd vanuit elk onderdeel dat erin HttpClient
is geïnjecteerd, mits deze HttpClient
niet is toegevoegd aan de services van de toepassing met een Transient
levensduur. Elke verwijzing naar HttpClient
in de toepassing verwijst naar hetzelfde exemplaar, dus wijzigingen in het exemplaar in één onderdeel stromen door de hele toepassing. Een goede plek om deze verificatiecontrole uit te voeren (gevolgd door het opgeven van het token) bevindt zich in een gedeeld onderdeel, zoals de hoofdnavigatie voor de site. Meer informatie over deze benadering vindt u in het BlazorAdmin
project in de referentietoepassing eShopOnWeb.
Een voordeel van BlazorWebAssembly traditionele JavaScript-SPA's is dat u geen kopieën van uw gegevensoverdrachtobjecten (DTU's) hoeft te synchroniseren. Uw BlazorWebAssembly project en uw web-API-project kunnen beide dezelfde DTU's delen in een gemeenschappelijk gedeeld project. Deze aanpak elimineert enige wrijving die gepaard gaat met het ontwikkelen van SPA's.
Als u snel gegevens wilt ophalen uit een API-eindpunt, kunt u de ingebouwde helpermethode gebruiken. GetFromJsonAsync
Er zijn vergelijkbare methoden voor POST, PUT, enzovoort. Hieronder ziet u hoe u een CatalogItem op te halen uit een API-eindpunt met behulp van een geconfigureerde HttpClient
app BlazorWebAssembly :
var item = await _httpClient.GetFromJsonAsync<CatalogItem>($"catalog-items/{id}");
Zodra u de gegevens hebt die u nodig hebt, worden wijzigingen doorgaans lokaal bijgehouden. Wanneer u updates wilt aanbrengen in het back-endgegevensarchief, roept u hiervoor aanvullende web-API's aan.
Verwijzingen – Blazor Gegevens
- Een web-API aanroepen vanuit ASP.NET Core Blazorhttps://learn.microsoft.com/aspnet/core/blazor/call-web-api