fleshout backend

This commit is contained in:
KS Jannette
2026-08-03 04:23:32 -04:00
parent a00282b1e6
commit 092ef17439
19 changed files with 500 additions and 52 deletions

View File

@@ -1,5 +1,22 @@
namespace Backend.Data;
/// <summary>
/// Storage-agnostic access to <see cref="User"/>. Implementations are selected by the
/// <c>Persistence:Provider</c> setting; see <see cref="PersistenceServiceCollectionExtensions"/>.
/// </summary>
public interface IUserRepository
{
Task<IReadOnlyList<User>> GetAllAsync(CancellationToken cancellationToken = default);
Task<User?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<User?> GetByEmailAsync(string email, CancellationToken cancellationToken = default);
Task<User> AddAsync(User user, CancellationToken cancellationToken = default);
/// <returns>The updated user, or <c>null</c> when no user has the given id.</returns>
Task<User?> UpdateAsync(User user, CancellationToken cancellationToken = default);
/// <returns><c>true</c> when a user was removed.</returns>
Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,58 @@
using System.Collections.Concurrent;
namespace Backend.Data.InMemory;
/// <summary>
/// Default implementation so the skeleton runs with no database. State is per-process
/// and lost on restart.
/// </summary>
public sealed class InMemoryUserRepository : IUserRepository
{
private readonly ConcurrentDictionary<Guid, User> _users = new();
public Task<IReadOnlyList<User>> GetAllAsync(CancellationToken cancellationToken = default)
{
IReadOnlyList<User> users = _users.Values
.OrderBy(user => user.CreatedAt)
.ToList();
return Task.FromResult(users);
}
public Task<User?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
_users.TryGetValue(id, out var user);
return Task.FromResult(user);
}
public Task<User?> GetByEmailAsync(string email, CancellationToken cancellationToken = default)
{
var user = _users.Values
.FirstOrDefault(candidate => string.Equals(candidate.Email, email, StringComparison.OrdinalIgnoreCase));
return Task.FromResult(user);
}
public Task<User> AddAsync(User user, CancellationToken cancellationToken = default)
{
_users[user.Id] = user;
return Task.FromResult(user);
}
public Task<User?> UpdateAsync(User user, CancellationToken cancellationToken = default)
{
if (!_users.ContainsKey(user.Id))
{
return Task.FromResult<User?>(null);
}
_users[user.Id] = user;
return Task.FromResult<User?>(user);
}
public Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken = default)
=> Task.FromResult(_users.TryRemove(id, out _));
}

View File

@@ -0,0 +1,13 @@
namespace Backend.Data;
public sealed class PersistenceOptions
{
public const string SectionName = "Persistence";
public string Provider { get; set; } = PersistenceProviders.InMemory;
}
public static class PersistenceProviders
{
public const string InMemory = "InMemory";
}

View File

@@ -0,0 +1,37 @@
using Backend.Data.InMemory;
namespace Backend.Data;
/// <summary>
/// Single place where a storage backend is chosen. To add a real database, implement
/// <see cref="IUserRepository"/> and register it under a new provider name here;
/// controllers stay untouched.
/// </summary>
public static class PersistenceServiceCollectionExtensions
{
public static IServiceCollection AddPersistence(
this IServiceCollection services,
IConfiguration configuration)
{
services.Configure<PersistenceOptions>(configuration.GetSection(PersistenceOptions.SectionName));
var provider = configuration.GetSection(PersistenceOptions.SectionName).Get<PersistenceOptions>()?.Provider
?? PersistenceProviders.InMemory;
switch (provider)
{
case PersistenceProviders.InMemory:
// Singleton because the store itself holds the data.
services.AddSingleton<IUserRepository, InMemoryUserRepository>();
break;
default:
throw new InvalidOperationException(
$"Unknown persistence provider '{provider}'. Supported providers: " +
$"'{PersistenceProviders.InMemory}'. Register additional providers in " +
$"{nameof(PersistenceServiceCollectionExtensions)}.");
}
return services;
}
}

15
backend/Data/User.cs Normal file
View File

@@ -0,0 +1,15 @@
namespace Backend.Data;
/// <summary>
/// Placeholder domain entity. Replace with the real model once the project has one.
/// </summary>
public sealed class User
{
public Guid Id { get; init; } = Guid.NewGuid();
public required string Email { get; set; }
public required string DisplayName { get; set; }
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
}