Table of Contents

Class ChatService

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 ChatService : BaseService<ChatEntity>, IChatService
Inheritance
ChatService
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

ChatService(INpgsqlDbContextFactory)

Initializes a new instance of ChatService.

public ChatService(INpgsqlDbContextFactory dbContextFactory)

Parameters

dbContextFactory INpgsqlDbContextFactory

DbContext factory for creating contexts.

Exceptions

ArgumentNullException

Thrown when dbContextFactory is null.

Fields

MaxAuditAccountLength

Maximum allowed length for audit account name. This length should never be close to reached. Maximum account name is 32 characters.

public const int MaxAuditAccountLength = 256

Field Value

int

MaxAuditNameLength

Maximum allowed length for audit character name. This length should never be close to reached. Maximum character name is 32 characters.

public const int MaxAuditNameLength = 256

Field Value

int

MaxMessageLength

Maximum allowed length for chat messages. This length should never be close to reached. Maximum server message should be 256 characters.

public const int MaxMessageLength = 4000

Field Value

int

Methods

FetchAsync(DateTime, long, int, long, CancellationToken)

Fetches paginated chat messages excluding local messages for the specified scene server.

public Task<DatabaseResult<List<ChatData>>> FetchAsync(DateTime lastFetch, long lastPosition, int amount, long sceneServerId, CancellationToken cancellationToken = default)

Parameters

lastFetch DateTime

Timestamp to compare messages against.

lastPosition long

Last message ID fetched (for pagination).

amount int

Maximum number of messages to fetch.

sceneServerId long

Scene server ID to filter out local messages.

cancellationToken CancellationToken

Cancellation token.

Returns

Task<DatabaseResult<List<ChatData>>>

A DatabaseResult<T> containing the list of chat message data on success, or a DatabaseException on failure.

Remarks

This method uses LINQ with AsNoTracking for optimal read performance and automatically benefits from the retry policy configured on the DbContext without requiring explicit execution strategy wrapping. Filters out local channel messages (Tell, Guild, Party, World, Trade) from the specified scene server. Returns empty list for invalid amount.

PersistAsync(long, string, string, long, long, ChatChannel, string, DateTime, CancellationToken)

Persists a chat message with denormalized audit fields.

public Task<DatabaseResult> PersistAsync(long characterId, string characterName, string accountName, long worldServerId, long sceneServerId, ChatChannel channel, string message, DateTime serverReceivedTime, CancellationToken cancellationToken = default)

Parameters

characterId long

Character ID sending the message.

characterName string

Character name (denormalized for audit retention).

accountName string

Account name (denormalized for audit retention).

worldServerId long

World server ID.

sceneServerId long

Scene server ID.

channel ChatChannel

Chat channel.

message string

Message content.

serverReceivedTime DateTime

Timestamp when server received the message (for legal audit trail).

cancellationToken CancellationToken

Cancellation token.

Returns

Task<DatabaseResult>

A DatabaseResult indicating success or containing a DatabaseException on failure.

Remarks

Chat audit fields are denormalized so logs can survive character deletion. Passing the names avoids a race where the character row is deleted between lookup and insert. Uses BaseService.ExecuteWriteAsync for:

  • Automatic transient failure retry
  • Centralized exception handling and mapping
  • Consistent DatabaseResult pattern

PersistBatchAsync(List<(long characterId, string characterName, string accountName, long worldServerId, long sceneServerId, ChatChannel channel, string message, DateTime serverReceivedTime)>, int, CancellationToken)

Persists multiple chat messages in batches.

public Task<DatabaseResult> PersistBatchAsync(List<(long characterId, string characterName, string accountName, long worldServerId, long sceneServerId, ChatChannel channel, string message, DateTime serverReceivedTime)> messages, int maxBatchSize = 1000, CancellationToken cancellationToken = default)

Parameters

messages List<(long characterId, string characterName, string accountName, long worldServerId, long sceneServerId, ChatChannel channel, string message, DateTime serverReceivedTime)>

List of chat messages to persist. Each tuple contains: (characterId, characterName, accountName, worldServerId, sceneServerId, channel, message, serverReceivedTime).

maxBatchSize int

Maximum number of messages per database round-trip (500–2500).

cancellationToken CancellationToken

Cancellation token.

Returns

Task<DatabaseResult>

A DatabaseResult indicating success or failure.