Kommentar
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Avlyssningsmaskiner för Entity Framework Core (EF Core) möjliggör avlyssning, ändring och/eller undertryckning av EF Core-åtgärder. Detta omfattar databasåtgärder på låg nivå, till exempel körning av ett kommando, samt åtgärder på högre nivå, till exempel anrop till SaveChanges.
Interceptorer skiljer sig från loggning och diagnostik eftersom de tillåter ändring eller undertryckning av åtgärden som fångas upp. Enkel loggning eller Microsoft.Extensions.Logging är bättre val för loggning.
Interceptorer registreras per DbContext-instans när kontexten har konfigurerats. Använd en diagnostiklyssnare för att få samma information men för alla DbContext-instanser i processen.
Tillgängliga interceptorer
I följande tabell visas tillgängliga interceptor-gränssnitt:
| Uppfångare | Operationer som har fångats | Singleton |
|---|---|---|
| IDbCommandInterceptor | Skapa kommandon Kör kommandon Kommandofel Frigöra kommandots DbDataReader |
Nej. |
| IDbConnectionInterceptor | Öppna och stänga anslutningar Skapa anslutningar Anslutningsfel |
Nej. |
| IDbTransactionInterceptor | Skapa transaktioner Använd befintliga transaktioner Begå transaktioner Återställa transaktioner Skapa och använda savepoints Transaktionsfel |
Nej. |
| ISaveChangesInterceptor | SparaÄndringar/SparadeÄndringar SparaÄndringarMisslyckades Optimistisk samtidighetshantering |
Nej. |
| IMaterializationInterceptor | Skapa, initiera och slutföra entitetsinstanser från frågeresultat | Ja |
| IQueryExpressionInterceptor | Ändra LINQ-uttrycksträdet innan en fråga kompileras | Ja |
| IIdentityResolutionInterceptor | Lösa identitetskonflikter vid spårning av entiteter | Ja |
Registrera interceptorer
Interceptorer registreras med hjälp av AddInterceptors när du konfigurerar en DbContext-instans. Detta görs ofta i en åsidosättning av DbContext.OnConfiguring. Till exempel:
public class ExampleContext : BlogsContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(new TaggedQueryCommandInterceptor());
}
AddInterceptors Alternativt kan anropas som en del av AddDbContext eller när du skapar en DbContextOptions instans som ska skickas till DbContext-konstruktorn.
Tips/Råd
OnConfiguring anropas fortfarande när AddDbContext används eller en DbContextOptions-instans skickas till DbContext-konstruktorn. Detta gör det till den perfekta platsen för att tillämpa kontextkonfiguration oavsett hur DbContext konstrueras.
Interceptorer är ofta tillståndslösa, vilket innebär att en enda interceptor-instans kan användas för alla DbContext-instanser. Till exempel:
public class TaggedQueryCommandInterceptorContext : BlogsContext
{
private static readonly TaggedQueryCommandInterceptor _interceptor
= new TaggedQueryCommandInterceptor();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(_interceptor);
}
Varje interceptor-instans måste implementera ett eller flera gränssnitt som härletts från IInterceptor. Varje instans bör bara registreras en gång även om den implementerar flera avlyssningsgränssnitt. EF Core dirigerar händelser för varje gränssnitt efter behov.
Singleton-interceptorer
Vissa interceptorer implementerar ISingletonInterceptor (se tabellen ovan); dessa interceptorer registreras som singleton-tjänster i EF Cores interna tjänstleverantör, vilket innebär att en enda instans delas över alla DbContext instanser som använder samma tjänstleverantör.
Eftersom singleton-interceptorer blir en del av EF Cores interna tjänstkonfiguration skapar varje distinkt interceptor-instans en ny intern tjänstleverantör. Om du skickar en ny instans av en singleton interceptor varje gång en DbContext konfigureras, till exempel i AddDbContext, så kommer det så småningom att utlösa en ManyServiceProvidersCreatedWarning och försämra prestandan.
Varning
Återanvänd alltid samma singleton interceptor-instans för alla DbContext instanser. Skapa inte en ny instans varje gång kontexten konfigureras.
Följande är till exempel felaktigt eftersom en ny interceptor-instans skapas för varje kontextkonfiguration:
// Don't do this! A new instance each time causes a new internal service provider to be built.
services.AddDbContext<CustomerContext>(
b => b.UseSqlServer(connectionString)
.AddInterceptors(new MyMaterializationInterceptor()));
Återanvänd i stället samma instans:
// Correct: reuse a single interceptor instance
var interceptor = new MyMaterializationInterceptor();
services.AddDbContext<CustomerContext>(
b => b.UseSqlServer(connectionString)
.AddInterceptors(interceptor));
Eller använd ett statiskt fält:
public class CustomerContext : DbContext
{
private static readonly MyMaterializationInterceptor _interceptor = new();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(_interceptor);
}
Eftersom dessa interceptorer är singletons måste de vara trådsäkra. De bör generellt sett inte hålla ett förändringsbart tillstånd. Om du behöver komma åt begränsade tjänster (till exempel den aktuella DbContext) använder du Context eller liknande egenskaper för händelsedata som skickas till varje interceptor-metod.
Databasavlyssning
Anmärkning
Databasavlyssning är endast tillgängligt för relationsdatabasprovidrar.
Databasavlyssning på låg nivå delas upp i de tre gränssnitten som visas i följande tabell.
| Uppfångare | Databasåtgärder har avlyssnats |
|---|---|
| IDbCommandInterceptor | Skapa kommandon Kör kommandon Kommandofel Frigöra kommandots DbDataReader |
| IDbConnectionInterceptor | Öppna och stänga anslutningar Skapa anslutningar Anslutningsfel |
| IDbTransactionInterceptor | Skapa transaktioner Använd befintliga transaktioner Begå transaktioner Återställa transaktioner Skapa och använda savepoints Transaktionsfel |
Basklasserna DbCommandInterceptor, DbConnectionInterceptoroch DbTransactionInterceptor innehåller no-op implementeringar för varje metod i motsvarande gränssnitt. Använd basklasserna för att undvika behovet av att implementera oanvända avlyssningsmetoder.
Metoderna för varje interceptortyp kommer i par, där den första anropas innan databasåtgärden startas och den andra efter att åtgärden har slutförts. Till exempel DbCommandInterceptor.ReaderExecuting anropas innan en fråga körs och DbCommandInterceptor.ReaderExecuted anropas efter att frågan har skickats till databasen.
Varje par med metoder har både synkroniserings- och asynkroniseringsvariationer. Detta gör det möjligt för asynkrona I/O, till exempel att begära en åtkomsttoken, att ske som en del av avlyssningen av en asynkron databasåtgärd.
Exempel: Kommandoavlyssning för att lägga till frågetips
Tips/Råd
Du kan ladda ned kommandoavlyssningsexemplet från GitHub.
En IDbCommandInterceptor kan användas för att ändra SQL innan den skickas till databasen. Det här exemplet visar hur du ändrar SQL så att det innehåller ett frågetips.
Ofta avgör den svåraste delen av avlyssningen när kommandot motsvarar frågan som behöver ändras. Att parsa SQL är ett alternativ, men tenderar att vara bräckligt. Ett annat alternativ är att använda EF Core-frågetaggar för att tagga varje fråga som ska ändras. Till exempel:
var blogs1 = await context.Blogs.TagWith("Use hint: robust plan").ToListAsync();
Den här taggen kan sedan identifieras i interceptorn eftersom den alltid inkluderas som en kommentar på den första raden i kommandotexten. Vid identifiering av taggen ändras frågan SQL för att lägga till rätt tips:
public class TaggedQueryCommandInterceptor : DbCommandInterceptor
{
public override InterceptionResult<DbDataReader> ReaderExecuting(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result)
{
ManipulateCommand(command);
return result;
}
public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result,
CancellationToken cancellationToken = default)
{
ManipulateCommand(command);
return new ValueTask<InterceptionResult<DbDataReader>>(result);
}
private static void ManipulateCommand(DbCommand command)
{
if (command.CommandText.StartsWith("-- Use hint: robust plan", StringComparison.Ordinal))
{
command.CommandText += " OPTION (ROBUST PLAN)";
}
}
}
Meddelanden:
- Interceptorn ärver från DbCommandInterceptor för att undvika att behöva implementera varje metod i interceptor-gränssnittet.
- Interceptorn implementerar metoder för både synkronisering och asynkronisering. Detta säkerställer att samma frågetips tillämpas på synkroniserings- och asynkrona frågor.
- Interceptorn implementerar de
Executingmetoder som anropas av EF Core med den genererade SQL-filen innan den skickas till databasen. Jämför detta med metodernaExecutedsom anropas när databasanropet har returnerats.
När du kör koden i det här exemplet genereras följande när en fråga taggas:
-- Use hint: robust plan
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b] OPTION (ROBUST PLAN)
Å andra sidan, när en fråga inte är taggad, skickas den till databasen oförändrad:
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
Exempel: Anslutningsavlyssning för SQL Azure-autentisering med hjälp av AAD
Tips/Råd
Du kan ladda ned anslutningsavlyssningsexemplet från GitHub.
En IDbConnectionInterceptor kan användas för att ändra DbConnection innan den används för att ansluta till databasen. Detta kan användas för att hämta en åtkomsttoken för Azure Active Directory (AAD). Till exempel:
public class AadAuthenticationInterceptor : DbConnectionInterceptor
{
public override InterceptionResult ConnectionOpening(
DbConnection connection,
ConnectionEventData eventData,
InterceptionResult result)
=> throw new InvalidOperationException("Open connections asynchronously when using AAD authentication.");
public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(
DbConnection connection,
ConnectionEventData eventData,
InterceptionResult result,
CancellationToken cancellationToken = default)
{
var sqlConnection = (SqlConnection)connection;
var provider = new AzureServiceTokenProvider();
// Note: in some situations the access token may not be cached automatically the Azure Token Provider.
// Depending on the kind of token requested, you may need to implement your own caching here.
sqlConnection.AccessToken = await provider.GetAccessTokenAsync("https://database.windows.net/", null, cancellationToken);
return result;
}
}
Tips/Råd
Microsoft.Data.SqlClient stöder nu AAD-autentisering via anslutningssträng. Mer information finns i SqlAuthenticationMethod.
Varning
Observera att interceptorn genererar om ett synkroniseringsanrop görs för att öppna anslutningen. Det beror på att det inte finns någon icke-asynkron metod för att hämta åtkomsttoken och det inte finns något universellt och enkelt sätt att anropa en asynkron metod från en icke-asynkron kontext utan att riskera dödläge.
Varning
I vissa fall kanske åtkomsttoken inte cachelagras automatiskt av Azure Token-providern. Beroende på vilken typ av token som begärs kan du behöva implementera din egen cachelagring här.
Exempel: Lat initiering av en anslutningssträng
Anslutningssträngar är ofta statiska tillgångar som läses från en konfigurationsfil. Dessa kan enkelt skickas till UseSqlServer eller liknande när du konfigurerar en DbContext. Ibland kan dock anslutningssträngen ändras för varje kontextinstans. Till exempel kan varje klientorganisation i ett system med flera klientorganisationer ha en annan anslutningssträng.
En IDbConnectionInterceptor kan användas för att hantera dynamiska anslutningar och anslutningssträngar. Detta börjar med möjligheten att konfigurera DbContext utan någon anslutningssträng. Till exempel:
services.AddDbContext<CustomerContext>(
b => b.UseSqlServer());
En av IDbConnectionInterceptor metoderna kan sedan implementeras för att konfigurera anslutningen innan den används.
ConnectionOpeningAsync är ett bra val eftersom det kan utföra en asynkron åtgärd för att hämta anslutningssträngen, hitta en åtkomsttoken och så vidare. Anta till exempel att en tjänst är avgränsad till den nuvarande begäran och förstår den nuvarande klientorganisationen.
services.AddScoped<ITenantConnectionStringFactory, TestTenantConnectionStringFactory>();
Varning
Det kan gå mycket långsamt att utföra en asynkron sökning efter en anslutningssträng, åtkomsttoken eller liknande varje gång den behövs. Överväg att cachelagra dessa saker och endast uppdatera den cachelagrade strängen eller token med jämna mellanrum. Åtkomsttoken kan till exempel ofta användas under en längre tid innan de behöver uppdateras.
Detta kan matas in i varje DbContext instans med konstruktorinmatning:
public class CustomerContext : DbContext
{
private readonly ITenantConnectionStringFactory _connectionStringFactory;
public CustomerContext(
DbContextOptions<CustomerContext> options,
ITenantConnectionStringFactory connectionStringFactory)
: base(options)
{
_connectionStringFactory = connectionStringFactory;
}
// ...
}
Den här tjänsten används sedan när du skapar interceptor-implementeringen för kontexten:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(
new ConnectionStringInitializationInterceptor(_connectionStringFactory));
Slutligen använder interceptorn den här tjänsten för att hämta anslutningssträngen asynkront och ange den första gången som anslutningen används:
public class ConnectionStringInitializationInterceptor : DbConnectionInterceptor
{
private readonly ITenantConnectionStringFactory _connectionStringFactory;
public ConnectionStringInitializationInterceptor(ITenantConnectionStringFactory connectionStringFactory)
{
_connectionStringFactory = connectionStringFactory;
}
public override InterceptionResult ConnectionOpening(
DbConnection connection,
ConnectionEventData eventData,
InterceptionResult result)
=> throw new NotSupportedException("Synchronous connections not supported.");
public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(
DbConnection connection, ConnectionEventData eventData, InterceptionResult result,
CancellationToken cancellationToken = new())
{
if (string.IsNullOrEmpty(connection.ConnectionString))
{
connection.ConnectionString = (await _connectionStringFactory.GetConnectionStringAsync(cancellationToken));
}
return result;
}
}
Anmärkning
Anslutningssträngen hämtas bara första gången som en anslutning används. Därefter används anslutningssträngen som lagras på den DbConnection utan att leta upp en ny anslutningssträng.
Tips/Råd
Den här interceptorn åsidosätter den icke-asynkrona ConnectionOpening metod som ska utlösas eftersom tjänsten för att hämta anslutningssträngen måste anropas från en asynkron kodsökväg.
Exempel: Avancerad kommandoavlyssning för cachelagring
Tips/Råd
Du kan ladda ned det avancerade kommandoavlyssningsexemplet från GitHub.
EF Core-interceptorer kan:
- Tala om för EF Core att undertrycka körningen av operationen som intercepteras
- Ändra det resultat av operationen som rapporteras tillbaka till EF Core
Det här exemplet visar en interceptor som använder dessa funktioner för att bete sig som en primitiv cache på andra nivån. Cachelagrade frågeresultat returneras för en specifik fråga, vilket undviker en databas tur och retur.
Varning
Var försiktig när du ändrar STANDARDbeteendet för EF Core på det här sättet. EF Core kan bete sig på oväntade sätt om det får ett onormalt resultat som inte kan bearbetas korrekt. Det här exemplet visar också interceptor-begrepp. det är inte avsett som en mall för en robust cacheimplementering på andra nivån.
I det här exemplet kör programmet ofta en fråga för att hämta det senaste "dagliga meddelandet":
async Task<string> GetDailyMessage(DailyMessageContext context)
=> (await context.DailyMessages.TagWith("Get_Daily_Message").OrderBy(e => e.Id).LastAsync()).Message;
Den här frågan är taggad så att den enkelt kan identifieras i interceptorn. Tanken är att bara fråga databasen efter ett nytt meddelande en gång om dagen. Vid andra tillfällen använder programmet ett cachelagrat resultat. (Exemplet använder en fördröjning på 10 sekunder i exemplet för att simulera en ny dag.)
Interceptorns läge
Den här interceptorn är tillståndskänslig: den lagrar ID:t och meddelandetexten för det senaste dagliga meddelandet som efterfrågas, plus tiden då frågan kördes. På grund av det här tillståndet behöver vi också ett lås eftersom cachelagringen kräver att samma interceptor måste användas av flera kontextinstanser.
private readonly object _lock = new object();
private int _id;
private string _message;
private DateTime _queriedAt;
Före körning
Executing I metoden (dvs. innan du gör ett databasanrop) identifierar interceptorn den taggade frågan och kontrollerar sedan om det finns ett cachelagrat resultat. Om ett sådant resultat hittas ignoreras frågan och cachelagrade resultat används i stället.
public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result,
CancellationToken cancellationToken = default)
{
if (command.CommandText.StartsWith("-- Get_Daily_Message", StringComparison.Ordinal))
{
lock (_lock)
{
if (_message != null
&& DateTime.UtcNow < _queriedAt + new TimeSpan(0, 0, 10))
{
command.CommandText = "-- Get_Daily_Message: Skipping DB call; using cache.";
result = InterceptionResult<DbDataReader>.SuppressWithResult(new CachedDailyMessageDataReader(_id, _message));
}
}
}
return new ValueTask<InterceptionResult<DbDataReader>>(result);
}
Observera hur koden anropar InterceptionResult<TResult>.SuppressWithResult och skickar en ersättning DbDataReader som innehåller cachelagrade data. Denna avlyssningsresultat returneras sedan, vilket orsakar undertryckning av frågeutförande. Ersättningsläsaren används istället av EF Core för att presentera frågeresultaten.
Den här interceptorn ändrar även kommandotexten. Den här manipulationen krävs inte, men förbättrar tydligheten i loggmeddelanden. Kommandotexten behöver inte vara giltig SQL eftersom frågan nu inte kommer att köras.
Efter exekvering
Om inget cachelagrat meddelande är tillgängligt, eller om det har upphört att gälla, undertrycker inte koden ovan resultatet. EF Core kör därför frågan som vanligt. Den återgår sedan till interceptorns metod-Executed efter utförandet. Om resultatet inte redan är en cachelagrad läsare, extraheras det nya meddelande-ID:t och strängen från den verkliga läsaren och cachelagras redo för nästa användning av den här förfrågan.
public override async ValueTask<DbDataReader> ReaderExecutedAsync(
DbCommand command,
CommandExecutedEventData eventData,
DbDataReader result,
CancellationToken cancellationToken = default)
{
if (command.CommandText.StartsWith("-- Get_Daily_Message", StringComparison.Ordinal)
&& !(result is CachedDailyMessageDataReader))
{
try
{
await result.ReadAsync(cancellationToken);
lock (_lock)
{
_id = result.GetInt32(0);
_message = result.GetString(1);
_queriedAt = DateTime.UtcNow;
return new CachedDailyMessageDataReader(_id, _message);
}
}
finally
{
await result.DisposeAsync();
}
}
return result;
}
Demonstration
Exemplet på caching-interceptor innehåller ett enkelt konsolprogram som hämtar dagliga meddelanden för att testa cachelagringen:
// 1. Initialize the database with some daily messages.
using (var context = new DailyMessageContext())
{
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();
context.AddRange(
new DailyMessage { Message = "Remember: All builds are GA; no builds are RTM." },
new DailyMessage { Message = "Keep calm and drink tea" });
await context.SaveChangesAsync();
}
// 2. Query for the most recent daily message. It will be cached for 10 seconds.
using (var context = new DailyMessageContext())
{
Console.WriteLine(await GetDailyMessage(context));
}
// 3. Insert a new daily message.
using (var context = new DailyMessageContext())
{
context.Add(new DailyMessage { Message = "Free beer for unicorns" });
await context.SaveChangesAsync();
}
// 4. Cached message is used until cache expires.
using (var context = new DailyMessageContext())
{
Console.WriteLine(await GetDailyMessage(context));
}
// 5. Pretend it's the next day.
Thread.Sleep(10000);
// 6. Cache is expired, so the last message will not be queried again.
using (var context = new DailyMessageContext())
{
Console.WriteLine(await GetDailyMessage(context));
}
async Task<string> GetDailyMessage(DailyMessageContext context)
=> (await context.DailyMessages.TagWith("Get_Daily_Message").OrderBy(e => e.Id).LastAsync()).Message;
Detta resulterar i följande utdata:
info: 10/15/2020 12:32:11.801 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
-- Get_Daily_Message
SELECT "d"."Id", "d"."Message"
FROM "DailyMessages" AS "d"
ORDER BY "d"."Id" DESC
LIMIT 1
Keep calm and drink tea
info: 10/15/2020 12:32:11.821 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[@p0='Free beer for unicorns' (Size = 22)], CommandType='Text', CommandTimeout='30']
INSERT INTO "DailyMessages" ("Message")
VALUES (@p0);
SELECT "Id"
FROM "DailyMessages"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
info: 10/15/2020 12:32:11.826 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
-- Get_Daily_Message: Skipping DB call; using cache.
Keep calm and drink tea
info: 10/15/2020 12:32:21.833 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
-- Get_Daily_Message
SELECT "d"."Id", "d"."Message"
FROM "DailyMessages" AS "d"
ORDER BY "d"."Id" DESC
LIMIT 1
Free beer for unicorns
Observera från loggutdata att programmet fortsätter att använda det cachelagrade meddelandet tills tidsgränsen upphör att gälla, då databasen efterfrågas igen för ett nytt meddelande.
Exempel: Loggning av SQL Server-frågestatistik
Det här exemplet visar två interceptorer som fungerar tillsammans för att skicka SQL Server-frågestatistik till programloggen. För att generera statistiken behöver vi en IDbCommandInterceptor för att göra två saker.
Först kommer interceptorn att prefixa kommandon med SET STATISTICS IO ON, vilket instruerar SQL Server att skicka statistik till klienten efter att ett resultatset har förbrukats.
public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result,
CancellationToken cancellationToken = default)
{
command.CommandText = "SET STATISTICS IO ON;" + Environment.NewLine + command.CommandText;
return new(result);
}
För det andra kommer interceptorn att implementera metoden DataReaderClosingAsync, som anropas efter att DbDataReader har konsumerat resultatet, men innan den har stängts. När SQL Server skickar statistik placerar den dem i ett andra resultat på läsaren, så just nu läser interceptorn det resultatet genom att anropa NextResultAsync som fyller statistik på anslutningen.
public override async ValueTask<InterceptionResult> DataReaderClosingAsync(
DbCommand command,
DataReaderClosingEventData eventData,
InterceptionResult result)
{
await eventData.DataReader.NextResultAsync();
return result;
}
Den andra interceptorn behövs för att hämta statistiken från anslutningen och skriva ut dem till programmets logger. För detta använder vi en IDbConnectionInterceptor, som implementerar ConnectionCreated -metoden.
ConnectionCreated anropas omedelbart efter att EF Core har skapat en anslutning och kan därför användas för att utföra ytterligare konfiguration av anslutningen. I det här fallet hämtar interceptorn en ILogger och ansluter sedan till händelsen SqlConnection.InfoMessage för att logga meddelandena.
public override DbConnection ConnectionCreated(ConnectionCreatedEventData eventData, DbConnection result)
{
var logger = eventData.Context!.GetService<ILoggerFactory>().CreateLogger("InfoMessageLogger");
((SqlConnection)eventData.Connection).InfoMessage += (_, args) =>
{
logger.LogInformation(1, args.Message);
};
return result;
}
Viktigt!
Metoderna ConnectionCreating och ConnectionCreated anropas bara när EF Core skapar en DbConnection. De anropas inte om programmet skapar DbConnection och skickar det till EF Core.
Filtrera efter kommandokälla
Den CommandEventData som tillhandahålls till diagnostikkällor och interceptorer innehåller en CommandSource egenskap som anger vilken del av EF som var ansvarig för att skapa kommandot. Detta kan användas som ett filter i interceptorn. Vi kanske till exempel vill ha en interceptor som endast gäller för kommandon som kommer från SaveChanges:
public class CommandSourceInterceptor : DbCommandInterceptor
{
public override InterceptionResult<DbDataReader> ReaderExecuting(
DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
if (eventData.CommandSource == CommandSource.SaveChanges)
{
Console.WriteLine($"Saving changes for {eventData.Context!.GetType().Name}:");
Console.WriteLine();
Console.WriteLine(command.CommandText);
}
return result;
}
}
avlyssning av SaveChanges
Tips/Råd
Du kan ladda ned SaveChanges-interceptorexemplet från GitHub.
SaveChanges och SaveChangesAsync avlyssningspunkter definieras av ISaveChangesInterceptor gränssnittet. När det gäller andra interceptorer tillhandahålls basklassen SaveChangesInterceptor med no-op metoder som en bekvämlighet.
Tips/Råd
Interceptorer är kraftfulla. I många fall kan det dock vara enklare att åsidosätta metoden SaveChanges eller använda .NET-händelserna för SaveChanges som exponeras i DbContext.
Exempel: SaveChanges-avlyssning för granskning
SaveChanges kan avlyssnas för att skapa ett oberoende granskningsregister över de ändringar som gjorts.
Anmärkning
Detta är inte avsett att vara en robust granskningslösning. Snarare är det ett förenklat exempel som används för att demonstrera funktionerna i avlyssning.
Programkontexten
Exemplet för granskning använder en enkel DbContext med bloggar och inlägg.
public class BlogsContext : DbContext
{
private readonly AuditingInterceptor _auditingInterceptor = new AuditingInterceptor("DataSource=audit.db");
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.AddInterceptors(_auditingInterceptor)
.UseSqlite("DataSource=blogs.db");
public DbSet<Blog> Blogs { get; set; }
}
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>();
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public Blog Blog { get; set; }
}
Observera att en ny instans av interceptorn har registrerats för varje DbContext-instans. Detta beror på att granskningsavlyssnaren innehåller ett tillstånd som är länkat till den aktuella kontextinstansen.
Granskningskontexten
Exemplet innehåller också en andra DbContext och modell som används för granskningsdatabasen.
public class AuditContext : DbContext
{
private readonly string _connectionString;
public AuditContext(string connectionString)
{
_connectionString = connectionString;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlite(_connectionString);
public DbSet<SaveChangesAudit> SaveChangesAudits { get; set; }
}
public class SaveChangesAudit
{
public int Id { get; set; }
public Guid AuditId { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public bool Succeeded { get; set; }
public string ErrorMessage { get; set; }
public ICollection<EntityAudit> Entities { get; } = new List<EntityAudit>();
}
public class EntityAudit
{
public int Id { get; set; }
public EntityState State { get; set; }
public string AuditMessage { get; set; }
public SaveChangesAudit SaveChangesAudit { get; set; }
}
Interceptorn
Den allmänna idén för granskning med interceptorn är:
- Ett granskningsmeddelande skapas i början av SaveChanges och skrivs till granskningsdatabasen
- SaveChanges tillåts fortsätta
- Om SaveChanges lyckas uppdateras granskningsmeddelandet för att indikera att det har lyckats
- Om SaveChanges misslyckas uppdateras granskningsmeddelandet för att indikera felet
Den första fasen hanteras innan ändringar skickas till databasen med hjälp av åsidosättningar av ISaveChangesInterceptor.SavingChanges och ISaveChangesInterceptor.SavingChangesAsync.
public async ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
_audit = CreateAudit(eventData.Context);
using var auditContext = new AuditContext(_connectionString);
auditContext.Add(_audit);
await auditContext.SaveChangesAsync();
return result;
}
public InterceptionResult<int> SavingChanges(
DbContextEventData eventData,
InterceptionResult<int> result)
{
_audit = CreateAudit(eventData.Context);
using var auditContext = new AuditContext(_connectionString);
auditContext.Add(_audit);
auditContext.SaveChanges();
return result;
}
Om du åsidosätter både synkroniserings- och asynkrona metoder ser du till att granskning sker oavsett om SaveChanges eller SaveChangesAsync anropas. Observera också att asynkron överlagring i sig kan utföra icke-blockerande asynkron I/O till granskningsdatabasen. Du kanske vill kasta från synkroniseringsmetoden SavingChanges för att säkerställa att all databas-I/O är asynkron. Detta kräver sedan att programmet alltid anropar SaveChangesAsync och aldrig SaveChanges.
Granskningsmeddelandet
Varje interceptor-metod har en eventData parameter som ger sammanhangsberoende information om händelsen som fångas upp. I det här fallet ingår det aktuella programmet DbContext i händelsedata, som sedan används för att skapa ett granskningsmeddelande.
private static SaveChangesAudit CreateAudit(DbContext context)
{
context.ChangeTracker.DetectChanges();
var audit = new SaveChangesAudit { AuditId = Guid.NewGuid(), StartTime = DateTime.UtcNow };
foreach (var entry in context.ChangeTracker.Entries())
{
var auditMessage = entry.State switch
{
EntityState.Deleted => CreateDeletedMessage(entry),
EntityState.Modified => CreateModifiedMessage(entry),
EntityState.Added => CreateAddedMessage(entry),
_ => null
};
if (auditMessage != null)
{
audit.Entities.Add(new EntityAudit { State = entry.State, AuditMessage = auditMessage });
}
}
return audit;
string CreateAddedMessage(EntityEntry entry)
=> entry.Properties.Aggregate(
$"Inserting {entry.Metadata.DisplayName()} with ",
(auditString, property) => auditString + $"{property.Metadata.Name}: '{property.CurrentValue}' ");
string CreateModifiedMessage(EntityEntry entry)
=> entry.Properties.Where(property => property.IsModified || property.Metadata.IsPrimaryKey()).Aggregate(
$"Updating {entry.Metadata.DisplayName()} with ",
(auditString, property) => auditString + $"{property.Metadata.Name}: '{property.CurrentValue}' ");
string CreateDeletedMessage(EntityEntry entry)
=> entry.Properties.Where(property => property.Metadata.IsPrimaryKey()).Aggregate(
$"Deleting {entry.Metadata.DisplayName()} with ",
(auditString, property) => auditString + $"{property.Metadata.Name}: '{property.CurrentValue}' ");
}
Resultatet är en entitet SaveChangesAudit med en samling EntityAudit entiteter, en för varje infogning, uppdatering eller borttagning. Interceptorn infogar sedan dessa entiteter i granskningsdatabasen.
Tips/Råd
I varje EF Core-händelsedataklass skrivs ToString över för att generera motsvarande loggmeddelande för händelsen. Anrop ContextInitializedEventData.ToString genererar till exempel "Entity Framework Core 5.0.0 initierade "BlogsContext" med providern "Microsoft.EntityFrameworkCore.Sqlite" med alternativ: Ingen".
Upptäcka framgång
Granskningsentiteten lagras på interceptorn så att den kan nås igen när SaveChanges antingen lyckas eller misslyckas. För att lyckas anropas ISaveChangesInterceptor.SavedChanges eller ISaveChangesInterceptor.SavedChangesAsync.
public int SavedChanges(SaveChangesCompletedEventData eventData, int result)
{
using var auditContext = new AuditContext(_connectionString);
auditContext.Attach(_audit);
_audit.Succeeded = true;
_audit.EndTime = DateTime.UtcNow;
auditContext.SaveChanges();
return result;
}
public async ValueTask<int> SavedChangesAsync(
SaveChangesCompletedEventData eventData,
int result,
CancellationToken cancellationToken = default)
{
using var auditContext = new AuditContext(_connectionString);
auditContext.Attach(_audit);
_audit.Succeeded = true;
_audit.EndTime = DateTime.UtcNow;
await auditContext.SaveChangesAsync(cancellationToken);
return result;
}
Granskningsentiteten är kopplad till granskningskontexten eftersom den redan finns i databasen och måste uppdateras. Sedan ställer vi in Succeeded och EndTime, vilket gör att dessa egenskaper markeras som ändrade så att SaveChanges kan skicka en uppdatering till granskningsdatabasen.
Upptäcka misslyckande
Fel hanteras på ungefär samma sätt som framgång, men i ISaveChangesInterceptor.SaveChangesFailed metoden eller ISaveChangesInterceptor.SaveChangesFailedAsync . Händelsedata innehåller undantaget som utlöstes.
public void SaveChangesFailed(DbContextErrorEventData eventData)
{
using var auditContext = new AuditContext(_connectionString);
auditContext.Attach(_audit);
_audit.Succeeded = false;
_audit.EndTime = DateTime.UtcNow;
_audit.ErrorMessage = eventData.Exception.Message;
auditContext.SaveChanges();
}
public async Task SaveChangesFailedAsync(
DbContextErrorEventData eventData,
CancellationToken cancellationToken = default)
{
using var auditContext = new AuditContext(_connectionString);
auditContext.Attach(_audit);
_audit.Succeeded = false;
_audit.EndTime = DateTime.UtcNow;
_audit.ErrorMessage = eventData.Exception.InnerException?.Message;
await auditContext.SaveChangesAsync(cancellationToken);
}
Demonstration
Granskningsexemplet innehåller ett enkelt konsolprogram som gör ändringar i bloggdatabasen och sedan visar den granskning som skapades.
// Insert, update, and delete some entities
using (var context = new BlogsContext())
{
context.Add(
new Blog { Name = "EF Blog", Posts = { new Post { Title = "EF Core 3.1!" }, new Post { Title = "EF Core 5.0!" } } });
await context.SaveChangesAsync();
}
using (var context = new BlogsContext())
{
var blog = await context.Blogs.Include(e => e.Posts).SingleAsync();
blog.Name = "EF Core Blog";
context.Remove(blog.Posts.First());
blog.Posts.Add(new Post { Title = "EF Core 6.0!" });
await context.SaveChangesAsync();
}
// Do an insert that will fail
using (var context = new BlogsContext())
{
try
{
context.Add(new Post { Id = 3, Title = "EF Core 3.1!" });
await context.SaveChangesAsync();
}
catch (DbUpdateException)
{
}
}
// Look at the audit trail
using (var context = new AuditContext("DataSource=audit.db"))
{
foreach (var audit in await context.SaveChangesAudits.Include(e => e.Entities).ToListAsync())
{
Console.WriteLine(
$"Audit {audit.AuditId} from {audit.StartTime} to {audit.EndTime} was{(audit.Succeeded ? "" : " not")} successful.");
foreach (var entity in audit.Entities)
{
Console.WriteLine($" {entity.AuditMessage}");
}
if (!audit.Succeeded)
{
Console.WriteLine($" Error: {audit.ErrorMessage}");
}
}
}
Resultatet visar innehållet i granskningsdatabasen:
Audit 52e94327-1767-4046-a3ca-4c6b1eecbca6 from 10/14/2020 9:10:17 PM to 10/14/2020 9:10:17 PM was successful.
Inserting Blog with Id: '-2147482647' Name: 'EF Blog'
Inserting Post with Id: '-2147482647' BlogId: '-2147482647' Title: 'EF Core 3.1!'
Inserting Post with Id: '-2147482646' BlogId: '-2147482647' Title: 'EF Core 5.0!'
Audit 8450f57a-5030-4211-a534-eb66b8da7040 from 10/14/2020 9:10:17 PM to 10/14/2020 9:10:17 PM was successful.
Inserting Post with Id: '-2147482645' BlogId: '1' Title: 'EF Core 6.0!'
Updating Blog with Id: '1' Name: 'EF Core Blog'
Deleting Post with Id: '1'
Audit 201fef4d-66a7-43ad-b9b6-b57e9d3f37b3 from 10/14/2020 9:10:17 PM to 10/14/2020 9:10:17 PM was not successful.
Inserting Post with Id: '3' BlogId: '' Title: 'EF Core 3.1!'
Error: SQLite Error 19: 'UNIQUE constraint failed: Post.Id'.
Exempel: Optimistisk samtidighetsavlyssning
EF Core stöder det optimistiska samtidighetsmönstret genom att kontrollera att antalet rader som faktiskt påverkas av en uppdatering eller borttagning är detsamma som antalet rader som förväntas påverkas. Detta är ofta kopplat till en samtidighetstoken. det vill: ett kolumnvärde som endast matchar det förväntade värdet om raden inte har uppdaterats sedan det förväntade värdet lästes.
EF signalerar ett brott mot optimistisk samtidighet genom att kasta en DbUpdateConcurrencyException.
ISaveChangesInterceptor har metoder ThrowingConcurrencyException och ThrowingConcurrencyExceptionAsync som anropas innan utlöses DbUpdateConcurrencyException . Dessa avlyssningspunkter tillåter att undantaget ignoreras, eventuellt tillsammans med asynkrona databasändringar för att lösa överträdelsen.
Om två begäranden till exempel försöker ta bort samma entitet på nästan samma gång kan den andra borttagningen misslyckas eftersom raden i databasen inte längre finns. Detta kan vara bra – slutresultatet är att entiteten ändå har tagits bort. Följande interceptor visar hur detta kan göras:
public class SuppressDeleteConcurrencyInterceptor : ISaveChangesInterceptor
{
public InterceptionResult ThrowingConcurrencyException(
ConcurrencyExceptionEventData eventData,
InterceptionResult result)
{
if (eventData.Entries.All(e => e.State == EntityState.Deleted))
{
Console.WriteLine("Suppressing Concurrency violation for command:");
Console.WriteLine(((RelationalConcurrencyExceptionEventData)eventData).Command.CommandText);
return InterceptionResult.Suppress();
}
return result;
}
public ValueTask<InterceptionResult> ThrowingConcurrencyExceptionAsync(
ConcurrencyExceptionEventData eventData,
InterceptionResult result,
CancellationToken cancellationToken = default)
=> new(ThrowingConcurrencyException(eventData, result));
}
Det finns flera saker som är värda att notera om den här interceptorn:
- Både synkrona och asynkrona avlyssningsmetoder implementeras. Detta är viktigt om programmet kan anropa antingen
SaveChangesellerSaveChangesAsync. Men om all programkod är asynkron behöver den baraThrowingConcurrencyExceptionAsyncimplementeras. På samma sätt, om programmet aldrig använder asynkrona databasmetoder, behöver det baraThrowingConcurrencyExceptionimplementeras. Detta gäller vanligtvis för alla interceptorer med synkroniserings- och asynkroniseringsmetoder. - Interceptorn har åtkomst till EntityEntry objekt för de entiteter som sparas. I det här fallet används detta för att kontrollera om samtidighetsöverträdelsen sker för en borttagningsåtgärd.
- Om programmet använder en relationsdatabasprovider kan objektet ConcurrencyExceptionEventData omvandlas till ett RelationalConcurrencyExceptionEventData objekt. Detta ger ytterligare, relationsspecifik information om databasåtgärden som utförs. I det här fallet skrivs relationskommandotexten ut till konsolen.
- Genom att återvända
InterceptionResult.Suppress()uppmanas EF Core att undertrycka den åtgärd den var på väg att vidta—i det här fallet att kastaDbUpdateConcurrencyException. Den här möjligheten att ändra EF Cores beteende, snarare än att bara observera vad EF Core gör, är en av de mest kraftfulla funktionerna i interceptorer.
Materialiseringsavlyssning
IMaterializationInterceptor stöder avlyssning före och efter att en entitetsinstans har skapats och före och efter att egenskaperna för den instansen initieras. Interceptorn kan ändra eller ersätta entitetsinstansen vid varje punkt. På så sätt kan du:
- Ange ommappade egenskaper eller anropa metoder som behövs för validering, beräknade värden eller flaggor.
- Använda en fabrik för att skapa instanser.
- Att skapa en annan entitetsinstans än vad EF normalt skulle skapa, till exempel en instans från en cache eller av en proxytyp.
- Injicera tjänster i en entitetsinstans.
Anmärkning
IMaterializationInterceptor är en singleton interceptor, vilket innebär att en enda instans delas mellan alla DbContext instanser.
Exempel: Enkla åtgärder vid skapande av entitet
Anta att vi vill hålla reda på den tid då en entitet hämtades från databasen, kanske så att den kan visas för en användare som redigerar data. För att åstadkomma detta definierar vi först ett gränssnitt:
public interface IHasRetrieved
{
DateTime Retrieved { get; set; }
}
Att använda ett gränssnitt är vanligt med interceptorer eftersom det gör att samma interceptor kan fungera med många olika entitetstyper. Till exempel:
public class Customer : IHasRetrieved
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public string? PhoneNumber { get; set; }
[NotMapped]
public DateTime Retrieved { get; set; }
}
Observera att [NotMapped] attributet används för att ange att den här egenskapen endast används när du arbetar med entiteten och inte bör sparas i databasen.
Interceptorn måste sedan implementera lämplig metod från IMaterializationInterceptor och ange den tid som hämtas:
public class SetRetrievedInterceptor : IMaterializationInterceptor
{
public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
{
if (instance is IHasRetrieved hasRetrieved)
{
hasRetrieved.Retrieved = DateTime.UtcNow;
}
return instance;
}
}
En instans av den här interceptorn registreras när du konfigurerar DbContext:
public class CustomerContext : DbContext
{
private static readonly SetRetrievedInterceptor _setRetrievedInterceptor = new();
public DbSet<Customer> Customers => Set<Customer>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.AddInterceptors(_setRetrievedInterceptor)
.UseSqlite("Data Source = customers.db");
}
Tips/Råd
Den här interceptorn är tillståndslös, vilket är vanligt, så en enda instans skapas och delas mellan alla DbContext instanser.
Nu, när en Customer frågas från databasen, kommer egenskapen Retrieved att ställas in automatiskt. Till exempel:
await using (var context = new CustomerContext())
{
var customer = await context.Customers.SingleAsync(e => e.Name == "Alice");
Console.WriteLine($"Customer '{customer.Name}' was retrieved at '{customer.Retrieved.ToLocalTime()}'");
}
Genererar utdata:
Customer 'Alice' was retrieved at '9/22/2022 5:25:54 PM'
Exempel: Mata in tjänster i entiteter
EF Core har redan inbyggt stöd för att mata in vissa särskilda tjänster i kontextinstanser. Se till exempel Lat inläsning utan proxyservrar, vilket fungerar genom att ILazyLoader mata in tjänsten.
En IMaterializationInterceptor kan användas för att generalisera detta till valfri tjänst. I följande exempel visas hur du injicerar ILogger i entiteter, så att de kan utföra sin egen loggning.
Anmärkning
När tjänster matas in i entiteter kopplas dessa entitetstyper till de inmatade tjänsterna, som vissa anser vara ett antimönster.
Precis som tidigare används ett gränssnitt för att definiera vad som kan göras.
public interface IHasLogger
{
ILogger? Logger { get; set; }
}
Och entitetstyper som loggar måste implementera det här gränssnittet. Till exempel:
public class Customer : IHasLogger
{
private string? _phoneNumber;
public int Id { get; set; }
public string Name { get; set; } = null!;
public string? PhoneNumber
{
get => _phoneNumber;
set
{
Logger?.LogInformation(1, $"Updating phone number for '{Name}' from '{_phoneNumber}' to '{value}'.");
_phoneNumber = value;
}
}
[NotMapped]
public ILogger? Logger { get; set; }
}
Den här gången måste interceptorn implementera IMaterializationInterceptor.InitializedInstance, som anropas efter att varje entitetsinstans har skapats och dess egenskapsvärden har initierats. Interceptorn hämtar en ILogger från kontexten och initierar IHasLogger.Logger med den:
public class LoggerInjectionInterceptor : IMaterializationInterceptor
{
private ILogger? _logger;
public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
{
if (instance is IHasLogger hasLogger)
{
_logger ??= materializationData.Context.GetService<ILoggerFactory>().CreateLogger("CustomersLogger");
hasLogger.Logger = _logger;
}
return instance;
}
}
Den här gången används en ny instans av interceptorn för varje DbContext instans, eftersom den erhållna ILogger kan ändras per DbContext instans och ILogger cachelagras på interceptorn:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(new LoggerInjectionInterceptor());
När ändringen Customer.PhoneNumber ändras loggas nu den här ändringen i programmets logg. Till exempel:
info: CustomersLogger[1]
Updating phone number for 'Alice' from '+1 515 555 0123' to '+1 515 555 0125'.
Avlyssning av frågeuttryck
IQueryExpressionInterceptor tillåter avlyssning av LINQ-uttrycksträdet för en fråga innan den kompileras. Detta kan användas för att dynamiskt ändra frågor på sätt som gäller i hela programmet.
Anmärkning
IQueryExpressionInterceptor är en singleton interceptor, vilket innebär att en enskild instans vanligtvis delas mellan alla DbContext instanser.
Varning
Interceptorer är kraftfulla, men det är lätt att få saker fel när du arbetar med uttrycksträd. Tänk alltid på om det finns ett enklare sätt att uppnå det du vill ha, till exempel att ändra frågan direkt.
Exempel: Mata in ordning i frågor för stabil sortering
Överväg en metod som returnerar en sida med kunder:
Task<List<Customer>> GetPageOfCustomers(string sortProperty, int page)
{
using var context = new CustomerContext();
return context.Customers
.OrderBy(e => EF.Property<object>(e, sortProperty))
.Skip(page * 20).Take(20).ToListAsync();
}
Tips/Råd
Den här frågan använder EF.Property metoden för att ange den egenskap som ska sorteras efter. På så sätt kan programmet dynamiskt skicka in egenskapsnamnet, vilket tillåter sortering efter valfri egenskap av entitetstyp. Tänk på att sortering efter icke-indexerade kolumner kan vara långsam.
Detta fungerar bra så länge egenskapen som används för sortering alltid returnerar en stabil beställning. Men så är det kanske inte alltid. Linq-frågan ovan genererar till exempel följande på SQLite när du beställer efter Customer.City:
SELECT "c"."Id", "c"."City", "c"."Name", "c"."PhoneNumber"
FROM "Customers" AS "c"
ORDER BY "c"."City"
LIMIT @__p_1 OFFSET @__p_0
Om det finns flera kunder med samma City är sorteringen av den här frågan inte stabil. Detta kan leda till förlorade eller duplicerade resultat när användaren bläddrar genom data.
Ett vanligt sätt att åtgärda det här problemet är att utföra en sekundär sortering efter primärnyckel. Men i stället för att lägga till detta manuellt i varje fråga kan en interceptor lägga till den sekundära ordningen dynamiskt. För att underlätta detta definierar vi ett gränssnitt för alla entiteter som har en heltalsnyckel:
public interface IHasIntKey
{
int Id { get; }
}
Det här gränssnittet implementeras av entitetstyperna av intresse:
public class Customer : IHasIntKey
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public string? City { get; set; }
public string? PhoneNumber { get; set; }
}
Sedan behöver vi en interceptor som implementerar IQueryExpressionInterceptor:
public class KeyOrderingExpressionInterceptor : IQueryExpressionInterceptor
{
public Expression QueryCompilationStarting(Expression queryExpression, QueryExpressionEventData eventData)
=> new KeyOrderingExpressionVisitor().Visit(queryExpression);
private class KeyOrderingExpressionVisitor : ExpressionVisitor
{
private static readonly MethodInfo ThenByMethod
= typeof(Queryable).GetMethods()
.Single(m => m.Name == nameof(Queryable.ThenBy) && m.GetParameters().Length == 2);
protected override Expression VisitMethodCall(MethodCallExpression? methodCallExpression)
{
var methodInfo = methodCallExpression!.Method;
if (methodInfo.DeclaringType == typeof(Queryable)
&& methodInfo.Name == nameof(Queryable.OrderBy)
&& methodInfo.GetParameters().Length == 2)
{
var sourceType = methodCallExpression.Type.GetGenericArguments()[0];
if (typeof(IHasIntKey).IsAssignableFrom(sourceType))
{
var lambdaExpression = (LambdaExpression)((UnaryExpression)methodCallExpression.Arguments[1]).Operand;
var entityParameterExpression = lambdaExpression.Parameters[0];
return Expression.Call(
ThenByMethod.MakeGenericMethod(
sourceType,
typeof(int)),
methodCallExpression,
Expression.Lambda(
typeof(Func<,>).MakeGenericType(entityParameterExpression.Type, typeof(int)),
Expression.Property(entityParameterExpression, nameof(IHasIntKey.Id)),
entityParameterExpression));
}
}
return base.VisitMethodCall(methodCallExpression);
}
}
}
Detta ser förmodligen ganska komplicerat- och det är det! Det är vanligtvis inte lätt att arbeta med uttrycksträd. Nu ska vi titta på vad som händer:
I grund och botten kapslar interceptorn in en ExpressionVisitor. Besökaren åsidosätter VisitMethodCall, som anropas när det finns ett anrop till en metod i frågeuttrycksträdet.
Besökaren kontrollerar om detta är ett anrop till den OrderBy metod som vi är intresserade av.
Om det är det kontrollerar besökaren ytterligare om det generiska metodanropet är för en typ som implementerar vårt
IHasIntKeygränssnitt.Nu vet vi att metodanropet är av formatet
OrderBy(e => ...). Vi extraherar lambda-uttrycket från det här anropet och hämtar parametern som används i uttrycket ,edet vill säga .Nu skapar vi en ny MethodCallExpression med hjälp av Expression.Call builder-metoden. I det här fallet är
ThenBy(e => e.Id)metoden som anropas . Vi bygger detta med hjälp av parametern som extraherats ovan och åtkomst till egenskapenIdpå gränssnittetIHasIntKey.Indata i det här anropet är det ursprungliga
OrderBy(e => ...), så slutresultatet är ett uttryck förOrderBy(e => ...).ThenBy(e => e.Id).Det här ändrade uttrycket returneras från besökaren, vilket innebär att LINQ-frågan nu har ändrats korrekt för att inkludera ett
ThenByanrop.EF Core fortsätter och kompilerar det här frågeuttrycket till lämplig SQL för den databas som används.
Genom att registrera den här interceptorn och köra GetPageOfCustomers genereras nu följande SQL:
SELECT "c"."Id", "c"."City", "c"."Name", "c"."PhoneNumber"
FROM "Customers" AS "c"
ORDER BY "c"."City", "c"."Id"
LIMIT @__p_1 OFFSET @__p_0
Detta skapar nu alltid en stabil beställning, även om det finns flera kunder med samma City.
I många fall kan samma sak uppnås mer enkelt genom att ändra frågan direkt. Till exempel:
Task<List<Customer>> GetPageOfCustomers2(string sortProperty, int page)
{
using var context = new CustomerContext();
return context.Customers
.OrderBy(e => EF.Property<object>(e, sortProperty))
.ThenBy(e => e.Id)
.Skip(page * 20).Take(20).ToListAsync();
}
I det här fallet ThenBy läggs helt enkelt till i frågan. Ja, det kan behöva göras separat till varje fråga, men det är enkelt, lätt att förstå och kommer alltid att fungera.
Identifieringsavlyssning
IIdentityResolutionInterceptor tillåter avlyssning av identitetsmatchningskonflikter när nya DbContext entitetsinstanser börjar spåras.
Anmärkning
Den här interceptorn anropas för närvarande endast när DbContext.Update, DbContext.Attachoch liknande metoder används för att spåra entiteter som redan spåras med samma nyckel. Det anropas inte för entiteter som returneras från sökfrågor. Detta kan ändras i en framtida version. se det här problemet.
En DbContext kan bara spåra en entitetsinstans med ett visst primärnyckelvärde. Det innebär att flera instanser av en entitet med samma nyckelvärde måste lösas till en enda instans. En interceptor av den här typen anropas med den befintliga spårade instansen och den nya instansen och måste tillämpa eventuella egenskapsvärden och relationsändringar från den nya instansen i den befintliga instansen. Den nya instansen ignoreras sedan.
EF Core tillhandahåller en inbyggd implementering, UpdatingIdentityResolutionInterceptor, som uppdaterar den befintliga spårade entiteten med värden från den nya instansen. Detta kan registreras när kontexten konfigureras:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.AddInterceptors(new UpdatingIdentityResolutionInterceptor());
Om du vill implementera logik för anpassad identitetsmatchning skapar du en klass som implementerar och åsidosätter IIdentityResolutionInterceptorUpdateTrackedInstance metoden:
public class CustomIdentityResolutionInterceptor : IIdentityResolutionInterceptor
{
public void UpdateTrackedInstance(
IdentityResolutionInterceptionData interceptionData,
EntityEntry existingEntry,
object newEntity)
{
// Custom logic to merge property values from newEntity into the existing tracked entity
existingEntry.CurrentValues.SetValues(newEntity);
}
}