Architecture Guide

ChatMinder is built using a modular monolith architecture with domain-driven design principles, providing clean separation of concerns and excellent maintainability.

Project Structure

Core Projects

  • ChatMinder.Server: Main ASP.NET Core web application with API controllers and Razor Pages
  • ChatMinder.Server.Core: Shared constants, permissions, domain events, and base models
  • ChatMinder.Server.Data: Entity Framework DbContext and database migrations
  • ChatMinder.Server.Domain: Domain entities and data models
  • ChatMinder.Server.BackgroundServices: Background workers and scheduled tasks

Service Layer

  • ChatMinder.Server.Services.Identity: Authentication and user management
  • ChatMinder.Server.Services.Infrastructure: Core infrastructure services
  • ChatMinder.Server.Services.OAuth.Google: Google OAuth integration
  • ChatMinder.Server.Protection: User protection and guardian services

Modular Architecture

ChatMinder uses a modular monolith approach where features are organized into self-contained modules:

Module Structure Example (Modules/ChatMinder.Server.Modules.Messaging/)

Interfaces/
  IMessageService.cs
  IGuardianMessageApprovalService.cs
Models/
  MessageDto.cs
  PendingMessageDto.cs
Data/
  MessageRepository.cs
  PendingMessageRepository.cs
Services/
  MessageService.cs
  GuardianMessageApprovalService.cs
Events/
  MessageCreatedEvent.cs
  MessageApprovedEvent.cs
MessagingModuleRegistrations.cs

Available Modules

  • Channels: Channel creation, membership, and management
  • Contacts: Personal address book functionality
  • Guardians: Guardian management and delegation workflows
  • Messaging: Message sending, approval workflows
  • Moderation: Content moderation and approval systems
  • Notifications: Real-time notifications and push messaging
  • Presence: User online/offline status tracking
  • ProtectedUsers: Guardian-protected user management
  • SignalR: Real-time communication hub

Benefits of Modular Design

  • Clear Boundaries: Each module owns its domain logic and data access
  • Testability: Modules can be tested in isolation
  • Event-Driven Communication: Modules communicate via domain events, not direct references
  • Future Scalability: Modules can potentially be extracted to microservices later

Domain-Driven Design

Aggregates and Entities

Core Identity Framework Extensions

public class ApplicationUser : IdentityUser
{
    public bool IsProtectedUser { get; set; }
    public string Name { get; set; }
    public DateTime? DateOfBirth { get; set; }
    public string? Notes { get; set; }
    public ProtectionLevel ProtectionLevel { get; set; }
}

Relationship Management

public class UserProtectedUserEntity
{
    public string UserId { get; set; }           // Guardian user ID
    public string ProtectedUserId { get; set; }  // Protected user ID
    public bool IsOwner { get; set; }            // Primary ownership flag
    public DateTime CreatedAt { get; set; }
}

Communication Entities

public class Channel
{
    public Guid Id { get; set; }
    public ChannelType Type { get; set; }  // Direct, Group
    public string? Name { get; set; }
    public DateTime CreatedAt { get; set; }
    public string CreatedByUserId { get; set; }
}

public class Message
{
    public Guid Id { get; set; }
    public Guid ChannelId { get; set; }
    public string SenderId { get; set; }
    public string Content { get; set; }
    public MessageType Type { get; set; }
    public DateTime CreatedAt { get; set; }
    public Guid? ReplyToMessageId { get; set; }
}

Approval Workflow Entities

Message Approval System

public class PendingMessageEntity
{
    public Guid Id { get; set; }
    public Guid ChannelId { get; set; }
    public string SenderId { get; set; }
    public string Content { get; set; }
    public MessageType Type { get; set; }
    public DateTime CreatedAt { get; set; }
    public Guid? ReplyToMessageId { get; set; }
}

public class PendingMessageIncomingApprovalEntity
{
    public Guid Id { get; set; }
    public Guid PendingMessageId { get; set; }
    public string RecipientUserId { get; set; }
    public string GuardianUserId { get; set; }
    public bool IsApproved { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? ApprovedAt { get; set; }
}

Contact Management

public class Contact
{
    public Guid Id { get; set; }
    public string User1Id { get; set; }     // Lexicographically smaller user ID
    public string User2Id { get; set; }     // Lexicographically larger user ID
    public string? User1ContactName { get; set; }  // User1's name for User2
    public string? User2ContactName { get; set; }  // User2's name for User1
    public ContactStatus Status { get; set; }
    public DateTime CreatedAt { get; set; }
}

Domain Events Architecture

Event System Components

Core Event Infrastructure

// Base event class
public abstract record DomainEventBase(Guid EventId, DateTime OccurredAt) : IDomainEvent;

// Event publisher interface
public interface IDomainEventPublisher
{
    Task PublishAsync<T>(T domainEvent) where T : IDomainEvent;
}

// Event handler interface
public interface IDomainEventHandler<in T> where T : IDomainEvent
{
    Task HandleAsync(T domainEvent);
}

Event Publishing Pattern

// Example from MessageService
public async Task<CreateMessageResult> CreateMessageAsync(CreateMessageRequest request)
{
    // Create and save message
    var message = await CreateMessage(request);
    
    // Publish domain event
    var messageCreatedEvent = new MessageCreatedEvent(
        messageId: message.Id,
        channelId: message.ChannelId,
        senderId: message.SenderId,
        content: message.Content,
        messageType: message.Type,
        createdAt: message.CreatedAt,
        replyToMessageId: message.ReplyToMessageId);

    await _domainEventPublisher.PublishAsync(messageCreatedEvent);
    
    return result;
}

Available Domain Events

Message Events

  • MessageCreatedEvent: New message created
  • MessageEditedEvent: Existing message modified
  • MessageDeletedEvent: Message removed

Channel Events

  • ChannelCreatedEvent: New channel established
  • ChannelMemberAddedEvent: User added to channel
  • ChannelMemberRemovedEvent: User removed from channel

Presence Events

  • UserWentOnlineEvent: User status changed to online
  • UserWentOfflineEvent: User status changed to offline

Guardian Events

  • GuardianApprovalRequestedEvent: Approval workflow initiated
  • GuardianApprovalGrantedEvent: Guardian approved request
  • GuardianApprovalDeniedEvent: Guardian rejected request

Benefits of Event-Driven Architecture

  1. Decoupled Modules: Services no longer have direct dependencies on notification or other cross-cutting services
  2. Single Responsibility: Each service focuses on its core domain without handling notifications
  3. Extensibility: New event handlers can be added without modifying existing services
  4. Testability: Services and event handlers can be tested independently
  5. Maintainability: Clear separation between business logic and side effects

Permission System

Hierarchical Authorization

Permission Definitions

public static class Permissions
{
    // User management
    public const string ViewUsers = "users.view";
    public const string CreateUsers = "users.create";
    public const string EditUsers = "users.edit";
    public const string DeleteUsers = "users.delete";
    
    // Protected user management
    public const string ViewProtectedUsers = "protected-users.view";
    public const string CreateProtectedUsers = "protected-users.create";
    public const string ManageProtectedUsers = "protected-users.manage";
    
    // Messaging
    public const string SendMessages = "messages.send";
    public const string ViewMessages = "messages.view";
    public const string ApproveMessages = "messages.approve";
}

Authorization Flow

  1. Permission Definitions: Static permission constants
  2. Role-Based Permissions: Default permissions assigned to roles
  3. User-Specific Overrides: Individual permission grants/denials
  4. Admin Fallback: Administrators have full access
// Permission evaluation logic
public async Task<bool> HasPermissionAsync(string userId, string permission)
{
    // 1. Check if user is admin (bypass all checks)
    if (await IsAdminAsync(userId))
        return true;
        
    // 2. Check user-specific permission overrides
    var userPermission = await GetUserPermissionAsync(userId, permission);
    if (userPermission != null)
        return userPermission.IsGranted;
        
    // 3. Check role-based permissions
    var roles = await GetUserRolesAsync(userId);
    return await HasRolePermissionAsync(roles, permission);
}

Architectural Constraints

Dependency Rules

  • Controllers: Should not directly depend on repositories (use services)
  • Services: Can depend on repositories and domain models
  • Domain: Should only depend on Core, not other application layers
  • Modules: Communicate via domain events, not direct service references

Architecture Tests

[Fact]
public void Controllers_Should_NotDirectlyDependOnRepositories()
{
    // Validates that controllers use services, not repositories directly
}

[Fact]
public void Services_Should_UseInterfaces()
{
    // Ensures services implement business interfaces for testability
}

[Fact]
public void Domain_Models_Should_NotHaveDependencies()
{
    // Validates domain purity - no external dependencies
}

Data Access Patterns

Repository Pattern

public interface IMessageRepository
{
    Task<Message> GetByIdAsync(Guid messageId);
    Task<IEnumerable<Message>> GetChannelMessagesAsync(Guid channelId, int pageSize = 50);
    Task<Message> CreateAsync(Message message);
    Task UpdateAsync(Message message);
    Task DeleteAsync(Guid messageId);
}

Entity Framework Integration

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public DbSet<Channel> Channels { get; set; }
    public DbSet<Message> Messages { get; set; }
    public DbSet<Contact> Contacts { get; set; }
    
    protected override void OnModelCreating(ModelBuilder builder)
    {
        // Entity configurations
        builder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
    }
}

Scalability Considerations

Background Services

  • Message Processing: Async message approval workflows
  • Notification Delivery: Push notification distribution
  • Data Cleanup: Automated cleanup of expired data

Caching Strategy

  • In-Memory Caching: Frequently accessed user permissions
  • Distributed Caching: Session data and temporary state
  • Database Optimization: Proper indexing and query optimization

Real-time Performance

  • Connection Management: Efficient SignalR connection pooling
  • Event Broadcasting: Targeted event delivery to subscribed clients
  • Message Queuing: Reliable message delivery with retry mechanisms

Extension Points

Adding New Modules

  1. Create module project following naming convention
  2. Implement module interfaces and services
  3. Define domain events for cross-module communication
  4. Register services in module registration class
  5. Add module to main application startup

Adding New Domain Events

  1. Create event class inheriting from DomainEventBase
  2. Include all necessary data for handlers
  3. Create corresponding event handlers
  4. Register handlers in dependency injection container

Custom Authorization Policies

// Add custom policy
services.AddAuthorization(options =>
{
    options.AddPolicy("CustomPolicy", policy =>
        policy.RequireAuthenticatedUser()
              .RequireClaim("custom-claim"));
});

Next Steps