攔截器
Entity Framework Core (EF Core) 攔截器可讓您攔截、修改及/或隱藏 EF Core 作業。 這包括低階資料庫作業 (例如執行命令),以及較高層級的作業 (例如對 SaveChanges 的呼叫)。
攔截器與記錄和診斷不同,因為攔截器允許修改或隱藏正在攔截的作業。 簡單的記錄或 Microsoft.Extensions.Logging 是用於記錄的更好選擇。
設定上下文時,會針對每個 DbCoNtext 執行個體註冊攔截器。 使用診斷接聽程式來取得相同的資訊,但是是針對處理序中的所有 DbCoNtext 執行個體。
註冊攔截器
設定 DbCoNtext 實例 時 ,會使用 AddInterceptors 註冊攔截器。 這通常是在 的覆寫中完成。 DbContext.OnConfiguring 例如:
public class ExampleContext : BlogsContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(new TaggedQueryCommandInterceptor());
}
或者, AddInterceptors
可以在 建立實例以傳遞至 DbCoNtext 建構函式時呼叫 為 或 的一 DbContextOptions 部分 AddDbContext 。
提示
使用 AddDbCoNtext 或 DbCoNtextOptions 實例傳遞至 DbCoNtext 建構函式時,仍會呼叫 OnConfiguring。 不論 DbCoNtext 的建構方式為何,這都適合套用內容組態。
攔截器通常是無狀態的,這表示單一攔截器實例可用於所有 DbCoNtext 實例。 例如:
public class TaggedQueryCommandInterceptorContext : BlogsContext
{
private static readonly TaggedQueryCommandInterceptor _interceptor
= new TaggedQueryCommandInterceptor();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(_interceptor);
}
每個攔截器實例都必須實作衍生自 IInterceptor 的一或多個介面。 即使每個實例實作多個攔截介面,也應該註冊一次;EF Core 會視需要路由傳送每個介面的事件。
資料庫攔截
注意
資料庫攔截僅適用于關係資料庫提供者。
低階資料庫攔截會分割成下表所示的三個介面。
攔截 器 | 攔截的資料庫作業 |
---|---|
IDbCommandInterceptor | 建立命令 執行命令 命令 失敗 處置命令的 DbDataReader |
IDbConnectionInterceptor | 開啟和關閉連線 連線失敗 |
IDbTransactionInterceptor | 建立交易 使用現有的交易 認可交易 回復交易 建立和使用儲存點 交易失敗 |
基類 DbCommandInterceptor 、 DbConnectionInterceptor 和 DbTransactionInterceptor 包含對應介面中每個方法的 no-op 實作。 使用基類來避免需要實作未使用的攔截方法。
每個攔截器類型上的方法都會成對,第一個是在資料庫作業啟動之前呼叫,第二個是在作業完成之後呼叫。 例如, DbCommandInterceptor.ReaderExecuting 在執行查詢之前呼叫 ,並在 DbCommandInterceptor.ReaderExecuted 查詢傳送至資料庫之後呼叫。
每對方法都有同步處理和非同步變化。 這可讓非同步 I/O,例如要求存取權杖,在攔截非同步資料庫作業時發生。
範例:新增查詢提示的命令攔截
提示
您可以從 GitHub 下載命令攔截器範例 。
IDbCommandInterceptor可用來在 SQL 傳送至資料庫之前修改 SQL。 此範例示範如何修改 SQL 以包含查詢提示。
攔截最棘手的部分通常是判斷命令何時對應至需要修改的查詢。 剖析 SQL 是一個選項,但通常會很脆弱。 另一個選項是使用 EF Core 查詢標籤 來標記應該修改的每個查詢。 例如:
var blogs1 = context.Blogs.TagWith("Use hint: robust plan").ToList();
接著,您可以在攔截器中偵測到此標籤,因為它一律會包含在命令文字的第一行中做為批註。 在偵測標記時,會修改查詢 SQL 以新增適當的提示:
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)";
}
}
}
注意:
- 攔截器繼承自 DbCommandInterceptor ,以避免必須在攔截器介面中實作每個方法。
- 攔截器會同時實作同步處理和非同步方法。 這可確保相同的查詢提示會套用至同步和非同步查詢。
- 攔截器會
Executing
實作 EF Core 在傳送至資料庫之前 ,使用產生的 SQL 呼叫的方法。 這與Executed
傳回資料庫呼叫之後呼叫的方法形成對比。
執行此範例中的程式碼會在標記查詢時產生下列專案:
-- Use hint: robust plan
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b] OPTION (ROBUST PLAN)
另一方面,未標記查詢時,會將其傳送至未修改的資料庫:
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
範例:使用 AAD 對 SQL Azure 驗證進行連線攔截
提示
您可以從 GitHub 下載連線攔截器範例 。
IDbConnectionInterceptor可用來操作 DbConnection ,然後才用來連線到資料庫。 這可用來取得 Azure Active Directory (AAD) 存取權杖。 例如:
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;
}
}
提示
Microsoft.Data.SqlClient 現在透過 連接字串 支援 AAD 驗證。 如需相關資訊,請參閱 SqlAuthenticationMethod 。
警告
請注意,如果進行同步處理呼叫以開啟連接,攔截器就會擲回。 這是因為沒有非非同步方法可取得存取權杖 ,而且沒有通用且簡單的方法可從非非同步內容呼叫非同步方法,而不會造成死結 的風險。
警告
在某些情況下,存取權杖可能不會自動快取 Azure 權杖提供者。 視所要求的權杖類型而定,您可能需要在這裡實作自己的快取。
範例:用於快取的進階命令攔截
提示
您可以從 GitHub 下載進階命令攔截器範例 。
EF Core 攔截器可以:
- 告知 EF Core 隱藏正在攔截的作業
- 將回報的作業結果變更回 EF Core
此範例顯示使用這些功能的攔截器行為與基本第二層快取類似。 系統會針對特定查詢傳回快取查詢結果,以避免資料庫往返。
警告
以這種方式變更 EF Core 預設行為時,請小心。 如果 EF Core 取得無法正確處理的異常結果,EF Core 可能會以非預期的方式運作。 此外,此範例示範攔截器概念;它不是作為強固第二層快取實作的範本。
在此範例中,應用程式經常執行查詢以取得最新的「每日訊息」:
async Task<string> GetDailyMessage(DailyMessageContext context)
=> (await context.DailyMessages.TagWith("Get_Daily_Message").OrderBy(e => e.Id).LastAsync()).Message;
系統會標記 此查詢 ,以便輕鬆地在攔截器中偵測到它。 其概念是每天只查詢資料庫一次新訊息。 在其他時候,應用程式會使用快取的結果。 (此範例會使用範例中的延遲 10 秒來模擬新的一天。
攔截器狀態
此攔截器具狀態:它會儲存最近查詢每日訊息的識別碼和郵件內文,以及執行該查詢的時間。 由於這種狀態,我們也需要 鎖定 ,因為快取需要多個內容實例必須使用相同的攔截器。
private readonly object _lock = new object();
private int _id;
private string _message;
private DateTime _queriedAt;
執行前
在 Executing
方法中(也就是在進行資料庫呼叫之前),攔截器會偵測標記的查詢,然後檢查是否有快取的結果。 如果找到這類結果,則會隱藏查詢,並改用快取的結果。
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);
}
請注意程式碼如何呼叫 InterceptionResult<TResult>.SuppressWithResult 並傳遞包含快取資料的取代 DbDataReader 專案。 接著會傳回此 InterceptionResult,導致查詢執行歸併。 EF Core 會改用取代讀取器做為查詢的結果。
這個攔截器也會操作命令文字。 不需要此操作,但可改善記錄訊息的清晰性。 命令文字不需要是有效的 SQL,因為查詢現在不會執行。
執行之後
如果沒有可用的快取訊息,或已過期,則上述程式碼不會隱藏結果。 因此,EF Core 會正常執行查詢。 接著,它會在執行之後返回攔截器 Executed
的方法。 此時,如果結果尚未快取讀取器,則會從實際讀取器擷取新的訊息識別碼和字串,並快取以供下一次使用此查詢。
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;
}
示範
快 取攔截器範例 包含簡單的主控台應用程式,可查詢每日訊息以測試快取:
// 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;
這會產生下列輸出:
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
請注意,從記錄輸出中,應用程式會繼續使用快取的訊息,直到逾時到期為止,此時會重新查詢資料庫以取得任何新訊息。
SaveChanges 攔截
提示
您可以從 GitHub 下載 SaveChanges 攔截器範例 。
SaveChanges 和 SaveChangesAsync 攔截點是由 ISaveChangesInterceptor 介面所定義。 至於其他攔截器, SaveChangesInterceptor 具有 no-op 方法的基類會以方便的方式提供。
提示
攔截器很強大。 不過,在許多情況下,覆寫 SaveChanges 方法 或使用 DbCoNtext 上公開之 SaveChanges 的 .NET 事件可能比較容易。
範例:SaveChanges 攔截以進行稽核
您可以攔截 SaveChanges 來建立所做變更的獨立稽核記錄。
注意
這不是一個強大的稽核解決方案。 相反地,這是一個簡單的例子,用來示範攔截的特性。
應用程式內容
用於 稽核 的範例會使用具有部落格和文章的簡單 DbCoNtext。
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; }
}
請注意,每個 DbCoNtext 實例都會註冊攔截器的新實例。 這是因為稽核攔截器包含連結至目前內容實例的狀態。
稽核內容
此範例也包含用於稽核資料庫的第二個 DbCoNtext 和模型。
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; }
}
攔截器
使用攔截器進行稽核的一般概念是:
- 稽核訊息會在 SaveChanges 開頭建立,並寫入稽核資料庫
- SaveChanges 可以繼續
- 如果 SaveChanges 成功,則會更新稽核訊息以指出成功
- 如果 SaveChanges 失敗,則會更新稽核訊息以指出失敗
第一個階段會在使用 和 ISaveChangesInterceptor.SavingChangesAsync 的 ISaveChangesInterceptor.SavingChanges 覆寫傳送至資料庫之前處理。
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;
}
覆寫同步處理和非同步方法可確保無論呼叫 SaveChanges
還是 SaveChangesAsync
呼叫,都會進行稽核。 另請注意,非同步多載本身能夠對稽核資料庫執行非封鎖的非同步 I/O。 您可能想要從同步 SavingChanges
方法擲回,以確保所有資料庫 I/O 都是非同步。 如此一來,應用程式一律會呼叫 SaveChangesAsync
,且永遠不會 SaveChanges
呼叫 。
稽核訊息
每個攔截器方法都有一個 eventData
參數,提供所攔截事件的相關內容資訊。 在此情況下,事件資料會包含目前的應用程式 DbCoNtext,然後用來建立稽核訊息。
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}' ");
}
結果是 SaveChangesAudit
具有實體集合的 EntityAudit
實體,每個插入、更新或刪除各一個。 攔截器接著會將這些實體插入稽核資料庫中。
提示
ToString 會在每個 EF Core 事件資料類別中覆寫,以產生事件的對等記錄訊息。 例如,呼叫 ContextInitializedEventData.ToString
會產生 「Entity Framework Core 5.0.0 使用提供者 'Microsoft.EntityFrameworkCore.Sqlite' 來初始化 'BlogsCoNtext',且選項為:None」。
偵測成功
稽核實體會儲存在攔截器上,以便在 SaveChanges 成功或失敗之後再次存取它。 為成功, ISaveChangesInterceptor.SavedChanges 或 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;
}
稽核實體會附加至稽核內容,因為它已存在於資料庫中,而且需要更新。 接著,我們會設定 Succeeded
和 EndTime
,將這些屬性標示為已修改,因此 SaveChanges 會將更新傳送至稽核資料庫。
偵測失敗
失敗的處理方式與成功的方式大致相同,但在 或 ISaveChangesInterceptor.SaveChangesFailedAsync 方法中 ISaveChangesInterceptor.SaveChangesFailed 。 事件資料包含擲回的例外狀況。
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);
}
示範
稽 核範例 包含簡單的主控台應用程式,對部落格資料庫進行變更,然後顯示已建立的稽核。
// 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 = context.Blogs.Include(e => e.Posts).Single();
blog.Name = "EF Core Blog";
context.Remove(blog.Posts.First());
blog.Posts.Add(new Post { Title = "EF Core 6.0!" });
context.SaveChanges();
}
// 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 context.SaveChangesAudits.Include(e => e.Entities).ToList())
{
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}");
}
}
}
結果會顯示稽核資料庫的內容:
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'.