Table of Contents

Class GuildApplicationService

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 GuildApplicationService : BaseService<GuildApplicationEntity>, IGuildApplicationService
Inheritance
GuildApplicationService
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

GuildApplicationService(INpgsqlDbContextFactory)

Initializes a new instance of GuildApplicationService.

public GuildApplicationService(INpgsqlDbContextFactory dbContextFactory)

Parameters

dbContextFactory INpgsqlDbContextFactory

DbContext factory for creating contexts.

Methods

ApplyAsync(long, long, string, int, int, CancellationToken)

Submits an application, refusing duplicates, non-recruiting guilds and full guilds.

public Task<DatabaseResult> ApplyAsync(long guildId, long characterId, string message, int maxCapacity, int maxPendingPerCharacter, CancellationToken cancellationToken = default)

Parameters

guildId long

Guild applied to.

characterId long

Applying character.

message string

Applicant message.

maxCapacity int

Guild member cap.

maxPendingPerCharacter int

Most outstanding applications one character may hold.

cancellationToken CancellationToken

Cancellation token.

Returns

Task<DatabaseResult>

A result indicating success or the reason for refusal.

Remarks

Every one of those conditions is tested inside the INSERT. Checking them in application code and inserting afterwards is a time-of-check-to-time-of-use gap the applicant controls the timing of: apply to a guild with one seat left, twice, from two clients.

DeleteAsync(long, long, CancellationToken)

Deletes one application by ID.

public Task<DatabaseResult<bool>> DeleteAsync(long applicationId, long guildId, CancellationToken cancellationToken = default)

Parameters

applicationId long

Application ID.

guildId long

The guild the caller believes the application belongs to.

cancellationToken CancellationToken

Cancellation token.

Returns

Task<DatabaseResult<bool>>

True when a row was actually removed.

Remarks

The guild is part of the WHERE clause, not merely validated beforehand: it is the last line that stops an officer of one guild resolving another guild's application by ID. The boolean result is what the accept path uses to claim the application exactly once — two officers pressing Accept simultaneously, only one of whom gets true.

DeleteManyByCharacterAsync(long, CancellationToken)

Deletes every application a character has outstanding.

public Task<DatabaseResult<int>> DeleteManyByCharacterAsync(long characterId, CancellationToken cancellationToken = default)

Parameters

characterId long

Character ID.

cancellationToken CancellationToken

Cancellation token.

Returns

Task<DatabaseResult<int>>

The number of rows removed.

Remarks

Run when a character joins any guild. An application that outlives the applicant's guildless state is an accept waiting to fail.

FetchAsync(long, CancellationToken)

Fetches one application by ID.

public Task<DatabaseResult<GuildApplicationData?>> FetchAsync(long applicationId, CancellationToken cancellationToken = default)

Parameters

applicationId long

Application ID.

cancellationToken CancellationToken

Cancellation token.

Returns

Task<DatabaseResult<GuildApplicationData?>>

The application, or null.

FetchManyAsync(long, int, CancellationToken)

Fetches the pending applications for one guild, oldest first.

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

Parameters

guildId long

Guild ID.

limit int

Maximum rows to return.

cancellationToken CancellationToken

Cancellation token.

Returns

Task<DatabaseResult<IReadOnlyList<GuildApplicationData>>>

The pending queue.

SearchDirectoryAsync(string, int, CancellationToken)

Searches the recruitment directory.

public Task<DatabaseResult<IReadOnlyList<GuildDirectoryEntryData>>> SearchDirectoryAsync(string searchTerm, int limit, CancellationToken cancellationToken = default)

Parameters

searchTerm string

Optional case-insensitive term matched against name, blurb and tags.

limit int

Maximum rows to return.

cancellationToken CancellationToken

Cancellation token.

Returns

Task<DatabaseResult<IReadOnlyList<GuildDirectoryEntryData>>>

Matching recruiting guilds with their current member counts.