Skip to content

Frequently Asked Questions (FAQ)

Common questions and answers for LBS Foundry developers.

Architecture Questions

Q: Why did we choose Event Sourcing?

A: Event sourcing provides several key benefits for LBS Foundry: - Complete audit trail - Essential for sports data integrity - Temporal queries - Analyze data at any point in time - Real-time capabilities - Stream events for live updates - Debugging - Full history of state changes - Compliance - Immutable record of all changes

See Event Sourcing Guide for technical details.

Q: What's the difference between Commands and Queries?

A: - Commands modify state and create events (write operations) - Queries read data from projections (read operations) - Commands require authentication and authorization - Queries can be public or secure depending on data sensitivity - Commands are processed through aggregates - Queries read from optimized read models

See Query/Command Alignment Design for more details.

Q: Why do we use strongly-typed IDs?

A: Strongly-typed IDs prevent common bugs:

// Wrong - easy to mix up parameters
UpdatePlayer(Guid playerId, Guid teamId, Guid seasonId)

// Right - compile-time safety
UpdatePlayer(PlayerId playerId, TeamId teamId, SeasonId seasonId)

They also make the code more self-documenting and enable better tooling support.

Q: When should I create a new aggregate vs extending an existing one?

A: Create a new aggregate when: - Different business invariants - Each aggregate enforces its own rules - Different lifecycle - Aggregates that are created/modified independently - Different access patterns - Different users/roles manage different aggregates - Bounded context boundaries - Different domains should have separate aggregates

Keep aggregates focused on a single business concept.

Security Questions

Q: How do I add authorization to my command?

A: Use the [RequiresRoles] attribute:

[DataContract(Name = "UpdatePlayerSalary")]
[RequiresRoles(RoleDefinition.UserManager, RoleDefinition.Admin)]
public sealed record UpdatePlayerSalaryCommand : DomainCommand<PlayerId>
{
    // Command properties
}

The SecurityCommandExecutor automatically enforces these roles.

Q: What's the difference between Member, UserManager, and Admin roles?

A: - Member - Basic authenticated user, can view most data - UserManager - Can manage users and perform user-related operations - Admin - Full system access, can perform any operation - System - Internal system operations, bypasses normal authorization

Role hierarchy is defined in RoleHierarchy.cs with inheritance rules.

Q: How do I make a query public vs secure?

A: Use different interfaces:

// Public query - no authentication required
public class PublicTeamStatsQuery : IPublicQuery, IRequiresUserContext
{
    public ClaimsPrincipal? User => UserContext.Current; // May be null
}

// Secure query - authentication required
[RequiresRoles(RoleDefinition.Member)]
public class PlayerDetailQuery : ISecureQuery, IRequiresUserContext
{
    public ClaimsPrincipal? User => UserContext.Current; // Never null
}

Q: How do I access user context in my code?

A: Use the UserContext class:

public void Execute(CreatePlayerCommand command)
{
    var userId = UserContext.CurrentUserId;
    var userEmail = UserContext.Current?.FindFirst(ClaimTypes.Email)?.Value;
    var isAdmin = UserContext.IsInRole(RoleDefinition.Admin);

    // Include in events for audit trail
    this.RaiseEvent(new PlayerCreatedEvent
    {
        // ... other properties
        CreatedBy = userId,
        CreatedAt = DateTime.UtcNow
    });
}

Development Questions

Q: How do I add a new command?

A: Follow these steps: 1. Create the command with proper attributes 2. Add to aggregate Execute method 3. Create corresponding event 4. Add Apply method to aggregate 5. Write tests for the functionality

See Common Tasks for detailed examples.

Q: How do I add a new query?

A: Follow these steps: 1. Create query class implementing ISearchQuery 2. Create query handler implementing IQueryHandler<TQuery> 3. Register handler in DI container 4. Add tests for query logic

See Common Tasks for detailed examples.

Q: How do I create read models/projections?

A: Use Marten projection builders:

public class PlayerContractBuilder : MultiStreamProjection<PlayerContract, PlayerId>
{
    public void Apply(PlayerCreatedEvent @event, PlayerContract contract)
    {
        contract.Id = @event.AggregateRootId.Value;
        contract.PlayerName = @event.PlayerName;
        // ... other properties
    }

    public void Apply(PlayerUpdatedEvent @event, PlayerContract contract)
    {
        contract.PlayerName = @event.PlayerName;
        contract.LastUpdated = @event.UpdatedAt;
    }
}

Register in your feature's MartenModule.cs.

Q: Should I use sync or async methods?

A: - Commands - Always async (database operations) - Queries - Always async (database operations)
- Pure functions - Sync is fine - I/O operations - Always async - CPU-bound work - Consider sync unless it's long-running

Follow the async/await pattern consistently.

Q: How do I handle validation in commands?

A: Validate in the aggregate's Execute method:

public void Execute(CreatePlayerCommand command)
{
    // Input validation
    if (string.IsNullOrWhiteSpace(command.PlayerName))
        throw new ArgumentException("Player name is required", nameof(command.PlayerName));

    // Business rule validation
    if (this.IsPlayerNameTaken(command.PlayerName))
        throw new InvalidOperationException($"Player name '{command.PlayerName}' is already taken");

    // Success - raise event
    this.RaiseEvent(new PlayerCreatedEvent { /* ... */ });
}

Q: How do I debug event sourcing issues?

A: Use these techniques: 1. Check event store - Query mt_events table directly 2. Add logging to Execute and Apply methods 3. Use debugger to step through aggregate loading 4. Check projection status - Query mt_event_progression table 5. Verify event serialization - Check JSON in database

See Debugging Guide for more details.

Testing Questions

Q: How do I test commands with authorization?

A: Use the authorization test helpers:

[Test]
public async Task CreatePlayer_WithMemberRole_ShouldSucceed()
{
    using (AuthorizationTestHelpers.SetTestUser(RoleDefinition.Member))
    {
        var command = new CreatePlayerCommand { /* ... */ };
        var events = await this.commandExecutor.ExecuteAsync(command, 0);
        events.Should().HaveCount(1);
    }
}

[Test] 
public async Task CreatePlayer_WithoutRole_ShouldThrow()
{
    using (AuthorizationTestHelpers.SetAnonymousUser())
    {
        var command = new CreatePlayerCommand { /* ... */ };
        var act = () => this.commandExecutor.ExecuteAsync(command, 0);
        await act.Should().ThrowAsync<UnauthorizedAccessException>();
    }
}

Q: How do I test queries?

A: Test both the logic and authorization:

[Test]
public async Task PlayerSearch_WithValidInput_ShouldReturnResults()
{
    // Arrange
    await this.SeedTestPlayers();
    using (AuthorizationTestHelpers.SetTestUser(RoleDefinition.Member))
    {
        var query = new PlayerSearchQuery { SearchTerm = "smith" };

        // Act
        var (results, count) = await this.queryService.ExecuteQueryAsync(
            query, typeof(PlayerContract), null, 0, 10, this.store);

        // Assert
        results.Should().NotBeEmpty();
        count.Should().BeGreaterThan(0);
    }
}

Q: How do I test with a clean database state?

A: Use proper test setup/teardown:

[SetUp]
public async Task SetUp()
{
    this.session = this.store.LightweightSession();

    // Clear existing data
    await this.session.DeleteWhere<PlayerContract>(x => true);
    await this.session.SaveChangesAsync();
}

[TearDown]
public void TearDown()
{
    this.session?.Dispose();
}

Deployment Questions

Q: How do I run the application locally?

A: Use the Aspire host for full-stack development:

# Start all services (API + Frontend + Dependencies)
dotnet run --project src/Aspire/LBS.AspireHost/LBS.AspireHost.csproj

# Or start services individually
dotnet run --project src/Apps/LBS.Api/LBS.Api.csproj  # API only
pnpm web:dev                                         # Frontend only (from repo root)

Q: How do I reset my development database?

A: There is no separate migration step. Marten applies the schema automatically on host startup via ApplyAllDatabaseChangesOnStartup() (AutoCreate.All), so the schema is recreated when a host such as the API starts:

# Complete reset, then start a host to re-apply the schema
dropdb lbs_foundry_dev
createdb lbs_foundry_dev
dotnet run --project src/Apps/LBS.Api/LBS.Api.csproj

Q: How do I check if services are running?

A: Use the Aspire dashboard: - URL: http://localhost:15000 - Monitor: Service health, logs, HTTP requests - Debug: Database connections, background jobs

Configuration Questions

Q: Where do I configure connection strings?

A: In appsettings.json files:

{
  "ConnectionStrings": {
    "Foundry": "Host=localhost;Database=lbs_foundry_dev;Username=lbs_dev;Password=dev123"
  }
}

Use environment-specific files (appsettings.Development.json) for local overrides.

Q: How do I add a new configuration setting?

A: 1. Add to appsettings.json 2. Create configuration class (if complex) 3. Register in DI using IOptions<T> 4. Inject where needed

// Configuration class
public class MySettings
{
    public string ApiKey { get; set; } = string.Empty;
    public int TimeoutSeconds { get; set; } = 30;
}

// Registration
services.Configure<MySettings>(configuration.GetSection("MySettings"));

// Usage
public class MyService
{
    private readonly MySettings settings;

    public MyService(IOptions<MySettings> options)
    {
        this.settings = options.Value;
    }
}

Q: How do I add authentication for a new external API?

A: Create a service account:

public class ExternalApiService
{
    public async Task CallExternalApiAsync()
    {
        using (UserContext.RunAsSystem())
        {
            // Create service account if needed
            var serviceAccountId = await this.serviceAccountManagement
                .CreateServiceAccountAsync(
                    "external-api@lbs.com", 
                    "secure-password",
                    "External", 
                    "API",
                    [RoleDefinition.DataIngestion]);

            // Use for API calls
        }
    }
}

Performance Questions

Q: How do I optimize slow queries?

A: 1. Add database indexes for frequently filtered fields 2. Use proper pagination with skip/take 3. Avoid N+1 queries by including related data 4. Use lightweight sessions for read operations 5. Profile with EXPLAIN ANALYZE in PostgreSQL

// Good - indexed query with pagination
var players = await session
    .Query<PlayerContract>()
    .Where(p => p.TeamId == teamId.Value)  // Indexed field
    .OrderBy(p => p.PlayerName)
    .Skip(skip)
    .Take(take)
    .ToListAsync();

Q: How do I handle large aggregates?

A: Use snapshots for aggregates with many events:

public class LargeAggregate : AggregateRoot<LargeAggregateId>, ISnapshot
{
    public void ApplySnapshot(object snapshot)
    {
        if (snapshot is LargeAggregateSnapshot s)
        {
            // Restore state from snapshot
            this.Property1 = s.Property1;
            // ... other properties
        }
    }

    public object CreateSnapshot()
    {
        return new LargeAggregateSnapshot
        {
            Property1 = this.Property1,
            // ... other properties
        };
    }
}

Q: How do I monitor application performance?

A: Use built-in observability: - Aspire Dashboard - Real-time metrics and logs - Structured logging - Performance measurements - Database monitoring - Query execution times - Custom metrics - Business-specific measurements

Conceptual Questions

Q: What's the difference between Domain Events and Integration Events?

A: - Domain Events - Internal to our bounded context, stored in event store - Integration Events - Cross-boundary communication, published to external systems - Domain Events drive our read models and business processes - Integration Events notify external systems of important changes

Q: Why don't we use traditional CRUD operations?

A: CRUD limitations: - Lost history - Can't see what changed over time - Concurrency issues - Last-write-wins problems - Audit difficulties - No natural audit trail - Temporal queries - Can't query historical state

Event sourcing solves these by storing the sequence of changes rather than just current state.

Q: When should I create a new read model vs extending existing ones?

A: Create new read models for: - Different use cases - Different query patterns need different optimizations - Different access patterns - Some data might be public while other is private - Performance reasons - Specialized indexes and structures - Different update frequencies - Some data changes often, some rarely


Have a question not covered here? Ask in the development channel or add it to this FAQ!