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 createdMessageEditedEvent: Existing message modifiedMessageDeletedEvent: Message removed
Channel Events
ChannelCreatedEvent: New channel establishedChannelMemberAddedEvent: User added to channelChannelMemberRemovedEvent: User removed from channel
Presence Events
UserWentOnlineEvent: User status changed to onlineUserWentOfflineEvent: User status changed to offline
Guardian Events
GuardianApprovalRequestedEvent: Approval workflow initiatedGuardianApprovalGrantedEvent: Guardian approved requestGuardianApprovalDeniedEvent: Guardian rejected request
Benefits of Event-Driven Architecture
- Decoupled Modules: Services no longer have direct dependencies on notification or other cross-cutting services
- Single Responsibility: Each service focuses on its core domain without handling notifications
- Extensibility: New event handlers can be added without modifying existing services
- Testability: Services and event handlers can be tested independently
- 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
- Permission Definitions: Static permission constants
- Role-Based Permissions: Default permissions assigned to roles
- User-Specific Overrides: Individual permission grants/denials
- 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
- Create module project following naming convention
- Implement module interfaces and services
- Define domain events for cross-module communication
- Register services in module registration class
- Add module to main application startup
Adding New Domain Events
- Create event class inheriting from
DomainEventBase - Include all necessary data for handlers
- Create corresponding event handlers
- 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
- Development Guide: Set up your development environment
- API Reference: Explore specific endpoints
- Real-time Features: Understand SignalR integration