Table of Contents

Class GuildRankService

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 GuildRankService : BaseService<GuildRankEntity>, IGuildRankService
Inheritance
GuildRankService
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

GuildRankService(INpgsqlDbContextFactory)

Initializes a new instance of GuildRankService.

public GuildRankService(INpgsqlDbContextFactory dbContextFactory)

Parameters

dbContextFactory INpgsqlDbContextFactory

DbContext factory for creating contexts.

Methods

CreateAsync(GuildRankData, int, CancellationToken)

Inserts a new rank row.

public Task<DatabaseResult> CreateAsync(GuildRankData rank, int maxRanks, CancellationToken cancellationToken = default)

Parameters

rank GuildRankData

The rank to insert.

maxRanks int

Maximum rank rows one guild may own.

cancellationToken CancellationToken

Cancellation token.

Returns

Task<DatabaseResult>

A result indicating success or failure.

DeleteAsync(long, byte, CancellationToken)

Deletes a rank row, refusing while any member still holds it.

public Task<DatabaseResult> DeleteAsync(long guildId, byte rankOrder, CancellationToken cancellationToken = default)

Parameters

guildId long

Guild ID.

rankOrder byte

The rank position to delete.

cancellationToken CancellationToken

Cancellation token.

Returns

Task<DatabaseResult>

A result indicating success or failure.

Remarks

The occupancy test and the delete are ONE statement. Split across two round trips, a member could be moved into the rank between them and end up holding a rank that no longer exists — which reads back as no permissions at all, quietly, forever.

EnsureDefaultsAsync(long, IReadOnlyList<GuildRankData>, CancellationToken)

Creates the default rank ladder for a guild if it does not already have one.

public Task<DatabaseResult<int>> EnsureDefaultsAsync(long guildId, IReadOnlyList<GuildRankData> defaults, CancellationToken cancellationToken = default)

Parameters

guildId long

Guild ID.

defaults IReadOnlyList<GuildRankData>

The rows to seed, in any order.

cancellationToken CancellationToken

Cancellation token.

Returns

Task<DatabaseResult<int>>

The number of rows actually inserted.

Remarks

IDEMPOTENT. Every row is inserted with ON CONFLICT (guild_id, rank_order) DO NOTHING, so running this against a guild that already has ranks changes nothing and returns zero. That is what makes it safe to call on every guild read, which is in turn what makes the migration of existing guilds require no migration step at all: a guild created before rank rows existed grows them the first time anybody looks at it, with the permissions its old enum ranks implied.

FetchManyAsync(long, CancellationToken)

Fetches every rank row for a guild, ordered by rank order ascending.

public Task<DatabaseResult<IReadOnlyList<GuildRankData>>> FetchManyAsync(long guildId, CancellationToken cancellationToken = default)

Parameters

guildId long

Guild ID.

cancellationToken CancellationToken

Cancellation token.

Returns

Task<DatabaseResult<IReadOnlyList<GuildRankData>>>

The guild's rank ladder.

UpdateAsync(long, byte, string, long, long, CancellationToken)

Updates one rank's name and permission mask.

public Task<DatabaseResult> UpdateAsync(long guildId, byte rankOrder, string name, long permissions, long incomingVersion, CancellationToken cancellationToken = default)

Parameters

guildId long

Guild ID.

rankOrder byte

The rank position to update.

name string

New display name.

permissions long

New permission bit mask.

incomingVersion long

The authoritative, monotonic version for this update.

cancellationToken CancellationToken

Cancellation token.

Returns

Task<DatabaseResult>

A result indicating success or failure.