次の方法で共有


インターセプター

Entity Framework Core (EF Core) インターセプターを使用すると、EF Core 操作のインターセプト、変更、抑制が可能になります。 これには、コマンドの実行などの低レベルのデータベース操作や、SaveChanges の呼び出しなどの上位レベルの操作が含まれます。

インターセプターは、インターセプトされる操作の変更または抑制を許可するという点で、ログ記録や診断とは異なります。 ログ記録には、単純なログ記録 または Microsoft.Extensions.Logging の方が適しています。

インターセプターは、コンテキストの構成時に DbContext インスタンスごとに登録されます。 診断リスナーを使用して、プロセス内のすべての DbContext インスタンスに対して同じ情報を取得します。

インターセプターの登録

インターセプターは、AddInterceptorsするときにを使用して登録されます。 これは一般的に、 DbContext.OnConfiguringのオーバーライドで行われます。 例えば次が挙げられます。

public class ExampleContext : BlogsContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.AddInterceptors(new TaggedQueryCommandInterceptor());
}

または、 AddInterceptors は、 AddDbContext の一部として、または dbContext コンストラクターに渡す DbContextOptions インスタンスを作成するときに呼び出すことができます。

ヒント

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から派生した 1 つ以上のインターフェイスを実装する必要があります。 各インスタンスは、複数のインターセプト インターフェイスを実装している場合でも、1 回だけ登録する必要があります。EF Core は、必要に応じて各インターフェイスのイベントをルーティングします。

データベースの傍受

データベースのインターセプトは、リレーショナル データベース プロバイダーでのみ使用できます。

低レベルのデータベース インターセプトは、次の表に示す 3 つのインターフェイスに分割されます。

インターセプター インターセプトされたデータベース操作
IDbCommandInterceptor コマンドの作成
コマンドの実行
コマンドの失敗
コマンドの DbDataReader の指定
IDbConnectionInterceptor 接続の開始と終了
接続エラー
IDbTransactionInterceptor トランザクションの作成
既存のトランザクションの使用
トランザクションのコミット
トランザクションのロールバック
セーブポイントの作成と使用
トランザクションの失敗

基底クラス DbCommandInterceptorDbConnectionInterceptor、および DbTransactionInterceptor には、対応するインターフェイス内の各メソッドの no-op 実装が含まれています。 使用されていないインターセプト メソッドを実装する必要を回避するには、基底クラスを使用します。

各インターセプター型のメソッドはペアになっています。最初のメソッドはデータベース操作が開始される前に呼び出され、2 つ目は操作が完了した後に呼び出されます。 たとえば、 DbCommandInterceptor.ReaderExecuting はクエリが実行される前に呼び出され、クエリがデータベースに送信された後に DbCommandInterceptor.ReaderExecuted が呼び出されます。

メソッドの各ペアには、同期と非同期の両方のバリエーションがあります。 これにより、非同期データベース操作のインターセプトの一環として、アクセス トークンの要求などの非同期 I/O を実行できます。

例: クエリ ヒントを追加するコマンド インターセプト

ヒント

コマンド インターセプターのサンプルは GitHub からダウンロードできます。

IDbCommandInterceptorは、データベースに送信される前に SQL を変更するために使用できます。 この例では、クエリ ヒントを含むように SQL を変更する方法を示します。

多くの場合、インターセプトの最も複雑な部分は、コマンドが変更する必要があるクエリに対応するタイミングを決定することです。 SQL の解析は 1 つのオプションですが、脆弱になる傾向があります。 もう 1 つのオプションは、 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 から継承します。
  • インターセプターは、同期メソッドと非同期メソッドの両方を実装します。 これにより、同期クエリと非同期クエリに同じクエリ ヒントが適用されます。
  • インターセプターは、生成された SQL を使用して EF Core によって呼び出された Executing メソッドを、データベースに送信する に実装します。 これは、データベース呼び出しが返された後に呼び出される 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 を参照してください。

Warnung

接続を開くために同期呼び出しが行われた場合、インターセプターがスローされます。 これは、アクセス トークンを取得する非同期以外のメソッド がなく、デッドロックを危険にさらすことなく非非同期コンテキストから非同期メソッドを呼び出すユニバーサルで簡単な方法がないためです。

Warnung

場合によっては、アクセス トークンが Azure トークン プロバイダーに自動的にキャッシュされない場合があります。 要求されたトークンの種類によっては、ここで独自のキャッシュを実装することが必要になる場合があります。

例: キャッシュ用の高度なコマンド インターセプト

EF Core インターセプターは次のことができます。

  • インターセプトされる操作の実行を抑制するように EF Core に指示する
  • EF Core に報告された操作の結果を変更する

この例では、これらの機能を使用してプリミティブ第 2 レベルのキャッシュのように動作するインターセプターを示します。 キャッシュされたクエリ結果は、特定のクエリに対して返され、データベースラウンドトリップを回避します。

Warnung

この方法で EF Core の既定の動作を変更するときは注意してください。 EF Core は、正常に処理できない異常な結果が発生した場合、予期しない方法で動作する可能性があります。 また、この例ではインターセプターの概念を示します。これは、堅牢な第 2 レベルのキャッシュ実装のテンプレートとして意図されていません。

この例では、アプリケーションはクエリを頻繁に実行して、最新の "毎日のメッセージ" を取得します。

async Task<string> GetDailyMessage(DailyMessageContext context)
    => (await context.DailyMessages.TagWith("Get_Daily_Message").OrderBy(e => e.Id).LastAsync()).Message;

このクエリは、インターセプターで簡単に検出できるように タグ付けされます 。 データベースに対してクエリを実行して、毎日 1 回だけ新しいメッセージを検索するという考え方です。 それ以外の場合、アプリケーションはキャッシュされた結果を使用します。 (このサンプルでは、サンプルで 10 秒の遅延を使用して新しい日をシミュレートします)。

インターセプターの状態

このインターセプターはステートフルです。クエリが実行された最新の毎日のメッセージの ID とメッセージ テキストと、そのクエリが実行された時刻が格納されます。 この状態のため、キャッシュでは複数のコンテキスト インスタンスで同じインターセプターを使用する必要があるため、 ロック も必要です。

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 メソッドに戻ります。 この時点で、結果がまだキャッシュリーダーでない場合は、新しいメッセージ ID と文字列が実際のリーダーから抽出され、このクエリを次に使用する準備が整います。

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 インターセプト

ヒント

SaveChanges インターセプター サンプルは GitHub からダウンロードできます。

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 インスタンスごとに登録されていることに注意してください。 これは、監査インターセプターに現在のコンテキスト インスタンスにリンクされた状態が含まれているためです。

監査コンテキスト

このサンプルには、監査データベースに使用される 2 つ目の 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.SavingChangesISaveChangesInterceptor.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}' ");
}

結果は、挿入、更新、または削除ごとに 1 つずつ、SaveChangesAudit エンティティのコレクションを持つEntityAudit エンティティです。 その後、インターセプターは、これらのエンティティを監査データベースに挿入します。

ヒント

ToString は、イベントの同等のログ メッセージを生成するために、すべての EF Core イベント データ クラスでオーバーライドされます。 たとえば、ContextInitializedEventData.ToString を呼び出すと、"Entity Framework Core 5.0.0 が 'BlogsContext' を初期化し、プロバイダー 'Microsoft.EntityFrameworkCore.Sqlite' を使用し、オプションは 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;
}

監査エンティティは、データベースに既に存在し、更新する必要があるため、監査コンテキストにアタッチされます。 次に、 SucceededEndTimeを設定します。これにより、これらのプロパティが変更済みとしてマークされ、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'.