namespace App.User;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public record User(string Id, string Email, string Name, IReadOnlyList<string> Tags);
public class UserNotFoundException : Exception
{
public string UserId { get; }
public UserNotFoundException(string id) : base($"user {id} not found")
{
UserId = id;
}
}
public class ConflictException : Exception
{
public string Field { get; }
public string Value { get; }
public ConflictException(string field, string value) : base($"conflict on {field}={value}")
{
Field = field;
Value = value;
}
}
public interface IUserRepository
{
Task<User?> FindByIdAsync(string id);
Task<User?> FindByEmailAsync(string email);
Task<User> InsertAsync(User user);
IAsyncEnumerable<User> ScanAsync();
}
public class InMemoryRepository : IUserRepository
{
private readonly ConcurrentDictionary<string, User> _byId = new();
public Task<User?> FindByIdAsync(string id)
{
_byId.TryGetValue(id, out var user);
return Task.FromResult<User?>(user);
}
public Task<User?> FindByEmailAsync(string email)
{
var match = _byId.Values.FirstOrDefault(u => u.Email == email);
return Task.FromResult<User?>(match);
}
public async Task<User> InsertAsync(User user)
{
var existing = await FindByEmailAsync(user.Email);
if (existing is not null)
{
throw new ConflictException("email", user.Email);
}
_byId[user.Id] = user;
return user;
}
public async IAsyncEnumerable<User> ScanAsync()
{
foreach (var u in _byId.Values)
{
yield return u;
await Task.Yield();
}
}
}
public class UserService
{
private readonly IUserRepository _repo;
public UserService(IUserRepository repo)
{
_repo = repo;
}
public async Task<User> GetAsync(string id)
{
var user = await _repo.FindByIdAsync(id);
if (user is null)
{
throw new UserNotFoundException(id);
}
return user;
}
public async Task<User> CreateAsync(string email, string name, IReadOnlyList<string> tags)
{
var existing = await _repo.FindByEmailAsync(email);
if (existing is not null)
{
throw new ConflictException("email", email);
}
var user = new User(MakeId(email), email, name, tags);
return await _repo.InsertAsync(user);
}
public async IAsyncEnumerable<User> WithTagAsync(string tag)
{
await foreach (var u in _repo.ScanAsync())
{
if (u.Tags.Contains(tag))
{
yield return u;
}
}
}
private static string MakeId(string email)
{
var at = email.IndexOf('@');
return at > 0 ? email[..at].ToLowerInvariant() : email.ToLowerInvariant();
}
}