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
可以作為AddDbContext的一部分呼叫,或在創建一個DbContextOptions實例時,用於傳遞至DbContext構造函數。
小提示
使用 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 = await context.Blogs.TagWith("Use hint: robust plan").ToListAsync();
接著,您可以在攔截器中偵測到此標籤,因為它一律會包含在命令文字的第一行中做為批註。 在偵測標記時,會修改查詢 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
方法,這些方法是在 SQL 產生並在傳送至資料庫 之前 由 EF Core 呼叫。 這與方法形成對比,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]
範例:SQL Azure 使用 AAD 驗證時的連線攔截
小提示
您可以從 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 介面所定義。 至於其他攔截器,提供了具有 no-op 方法的 SaveChangesInterceptor 基類,方便使用。
小提示
攔截器很強大。 不過,在許多情況下,覆寫 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.SavingChanges和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;
}
覆寫同步和非同步方法可確保無論是呼叫 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.SaveChangesFailed 或 ISaveChangesInterceptor.SaveChangesFailedAsync 方法中。 事件數據包含拋出的例外狀況。
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 = 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}");
}
}
}
結果會顯示稽核資料庫的內容:
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'.