Table of Contents

Class GuildLogService

Namespace
FishMMO.Database.Npgsql.Services
Assembly
FishMMO-DB.dll

Base class for database services that execute EF Core operations with consistent execution behavior (transactional and read-only), retry behavior for transient failures, and standardized error mapping into DatabaseResult.

public sealed class GuildLogService : BaseService<GuildLogEntity>, IGuildLogService
Inheritance
GuildLogService
Implements
Inherited Members

Remarks

This base type provides three primary execution paths:

  • ExecuteTransactionAsync(Func<Task>, CancellationToken) and ExecuteTransactionAsync<TResult>(Func<Task<TResult>>,string,CancellationToken) create a fresh NpgsqlDbContext, begin an explicit transaction, execute the delegate, then call SaveChangesAsync(CancellationToken) and commit.
  • ExecuteWriteAsync(Func<Task>,string,CancellationToken) and ExecuteWriteAsync<TResult>(Func<Task<TResult>>,string,CancellationToken) create a fresh NpgsqlDbContext, execute the delegate, then call SaveChangesAsync(CancellationToken) without starting an explicit transaction.
  • ExecuteReadAsync(Func<Task>,string,CancellationToken) and ExecuteReadAsync<TResult>(Func<Task<TResult>>,string,CancellationToken) create a fresh NpgsqlDbContext but do not start an explicit transaction and do not call SaveChanges.

Standalone Execution (no ambient scope): A new context is created per attempt to avoid EF change-tracker state leaking across retries. Transient database failures are retried with exponential backoff. Optimistic concurrency conflicts (Version-based authority) and StaleStateException are never retried; they are returned as non-transient failures so the caller can re-read and decide how to proceed.

Ambient Scope Execution (inside an existing Unit of Work): When an operation detects an active FishMMO.Database.Npgsql.Services.DatabaseExecutionScope, it reuses the ambient NpgsqlDbContext and does NOT retry on transient failures. This is by design for the following reasons:

  1. Transaction State Corruption: PostgreSQL aborts the entire transaction on most transient failures. The connection and transaction become unusable, making retry with the same context impossible.
  2. Context State Pollution: The DbContext's change tracker accumulates state. Retrying with a polluted change tracker can cause duplicate key violations or incorrect updates.
  3. Semantic Correctness: If operation A succeeded and operation B failed transiently, retrying B alone without re-evaluating A's preconditions could violate business invariants.

When a transient failure occurs inside an ambient scope, the failure is returned immediately so the caller can restart the entire unit of work with fresh state. Savepoints are used for nested atomicity but cannot recover from connection-level failures.

Constructors

GuildLogService(INpgsqlDbContextFactory)

Initializes a new instance of GuildLogService.

public GuildLogService(INpgsqlDbContextFactory dbContextFactory)

Parameters

dbContextFactory INpgsqlDbContextFactory

DbContext factory for creating contexts.

Methods

AppendAsync(GuildLogData, CancellationToken)

Appends one row to a guild's activity log.

public Task<DatabaseResult> AppendAsync(GuildLogData entry, CancellationToken cancellationToken = default)

Parameters

entry GuildLogData

The event to record. ID is ignored.

cancellationToken CancellationToken

Token to cancel the operation.

Returns

Task<DatabaseResult>

A DatabaseResult indicating success or failure.

FetchRecentAsync(long, int, CancellationToken)

Fetches a guild's most recent log rows, newest first.

public Task<DatabaseResult<IReadOnlyList<GuildLogData>>> FetchRecentAsync(long guildId, int limit, CancellationToken cancellationToken = default)

Parameters

guildId long

Guild ID.

limit int

Maximum rows to return.

cancellationToken CancellationToken

Token to cancel the operation.

Returns

Task<DatabaseResult<IReadOnlyList<GuildLogData>>>

The most recent rows, newest first.

PruneAsync(long, int, CancellationToken)

Deletes log rows beyond the most recent keep for one guild.

public Task<DatabaseResult<int>> PruneAsync(long guildId, int keep, CancellationToken cancellationToken = default)

Parameters

guildId long

Guild ID.

keep int

Number of newest rows to retain.

cancellationToken CancellationToken

Token to cancel the operation.

Returns

Task<DatabaseResult<int>>

The number of rows removed.

Remarks

An unbounded append-only table attached to a long-lived guild grows without limit and nothing else in the schema would ever remove from it. Trimming to a fixed depth is what makes the feature safe to leave running for a year.