Table of Contents

Repositories

BSGameLib doesn't store anything. It defines the interfaces that the rest of your server asks for entities through, and you write the classes behind them, on whatever storage the game uses: a database through Entity Framework or a SQL client, files, or memory.

IRepository

IRepository<TKey, TEntity> is the shape for a store of one kind of entity. Its key type is a struct that implements IEquatable<TKey>, such as long or Guid, and its entity type is a class that implements IIdentifiable<TKey> for that key type.

Member Does
Create(entity) Adds a new entity.
GetAll() Returns every entity.
GetById(id) Returns the entity with that key, or null.
Update(entity) Saves changes to an existing entity.
Delete(id) Removes the entity with that key.
Delete(entity) Removes an entity.

IIdentifiable<TKey> has one property, Id, which the repository uses as the key.

IRepositoryService

IRepositoryService<TEntity> has the same job for an entity whose key is a long, with members for listing, checking and saving:

Member Does
All Returns every entity.
Exists(id) Tells whether an entity with that ID exists.
GetById(id) Returns the entity with that ID, or null.
Save(entity) Saves an entity.

It derives from IInjectedDependency, so a class implementing it is registered as a shared service whichever way you ask for it.

A repository

Write one class for each kind of entity. The host registers it as a shared singleton, so it is used by every handler at once, and it has to be safe to use from several threads. See Packet handlers and the loop.

using System.Collections.Concurrent;
using BSGameLib.Core;
using BSGameLib.Server;

public class Account : IIdentifiable<long>
{
  public long Id { get; set; }
  public string Name { get; set; } = "";
}

public class AccountRepository : IRepository<long, Account>
{
  private readonly ConcurrentDictionary<long, Account> _accounts = new();

  public void Create(Account entity) => _accounts[entity.Id] = entity;

  public IEnumerable<Account> GetAll() => _accounts.Values.ToList();

  public Account GetById(long entityId) => _accounts.TryGetValue(entityId, out var account) ? account : null;

  public void Update(Account entity) => _accounts[entity.Id] = entity;

  public void Delete(long entityId) => _accounts.TryRemove(entityId, out _);

  public void Delete(Account entity) => Delete(entity.Id);
}

The host registers AccountRepository under IRepository<long, Account>. Anything that needs accounts asks for that interface, as this handler does:

using BSGameLib.Networking;
using BSGameLib.Server;

public class LookupPacket : IDataPacket
{
  public long AccountId { get; set; }
}

public class AccountLookupHandler(IRepository<long, Account> accounts) : IDataPacketHandler<LookupPacket>
{
  public void Handle(INetConnection connection, LookupPacket packet)
  {
    var account = accounts.GetById(packet.AccountId);
    Console.WriteLine(account is null ? "No such account" : account.Name);
  }
}