Skip to content

Property Management System - AI Development Context

This file provides comprehensive guidance for AI-assisted development of the Property Management System. Use this context to understand project patterns, coding standards, and common implementation approaches.

Essential Commands

.NET Backend Development

# Restore dependencies and build
dotnet restore && dotnet build

# Run the complete application with Aspire orchestration
dotnet run --project src/AppHost/PropertyManagement.AppHost
# Expected: Aspire dashboard at https://localhost:15888

# Run specific services for isolated development
dotnet run --project src/Services/Identity/PropertyManagement.Identity.WebApi
dotnet run --project src/Services/Property/PropertyManagement.Property.WebApi
dotnet run --project src/Services/Operations/PropertyManagement.Operations.WebApi

# Database migrations for specific service
dotnet ef migrations add InitialCreate --project src/Services/Property/PropertyManagement.Property.Infrastructure
dotnet ef database update --project src/Services/Property/PropertyManagement.Property.Infrastructure

# Format code and run tests
dotnet format && dotnet test

Frontend Development

cd frontend/web

# Install dependencies with exact versions
npm ci

# Development server with hot reload
npm run dev
# Expected: Next.js server at http://localhost:3000

# Type checking and linting
npm run type-check && npm run lint

# Build optimized production bundle
npm run build && npm run start

Testing and Quality

# Run all tests with coverage
dotnet test --collect:"XPlat Code Coverage" --logger "console;verbosity=detailed"

# Run specific service tests
dotnet test tests/Unit/Services/Property/PropertyManagement.Property.UnitTests.csproj
dotnet test tests/Integration/Services/Property/PropertyManagement.Property.IntegrationTests.csproj

# End-to-end API testing
dotnet test tests/E2E/PropertyManagement.E2E.Tests.csproj --settings tests.runsettings

Documentation

# Serve documentation locally with hot reload
cd documentations && mkdocs serve --dev-addr 127.0.0.1:8000

# Build and deploy documentation
mkdocs build --clean && mkdocs gh-deploy

Code Style Guidelines (for AI context)

C# / .NET Patterns

  • Language Version: Use C# 12 features including primary constructors, collection expressions, and required properties
  • Nullable Reference Types: Enabled project-wide, use proper null annotations
  • Record Types: Prefer for DTOs, value objects, and immutable data structures
  • Async/Await: Use throughout, avoid blocking synchronous calls
  • Result Pattern: Use custom Result type instead of throwing exceptions for business logic failures
// Preferred patterns for AI to follow:

// Primary constructor with validation
public sealed record CreatePropertyRequest(
    string Name,
    string Address,
    PropertyType Type,
    int Units)
{
    public static implicit operator CreatePropertyCommand(CreatePropertyRequest request) =>
        new(request.Name, request.Address, request.Type, request.Units);
}

// Rich domain model with business logic
public class Property : Entity<PropertyId>
{
    public string Name { get; private set; } = string.Empty;
    public Address Address { get; private set; } = null!;
    public PropertyType Type { get; private set; }

    // Business logic methods that return Result<T>
    public Result<Unit> AddUnit(Unit unit)
    {
        if (Units.Count >= GetMaxUnitsForType())
            return Result.Failure<Unit>("Maximum units exceeded for property type");

        Units.Add(unit);
        RaiseDomainEvent(new UnitAddedEvent(Id, unit.Id));
        return Result.Success(unit);
    }
}

// Application service with proper error handling
public class PropertyApplicationService
{
    public async Task<Result<PropertyResponse>> CreatePropertyAsync(
        CreatePropertyCommand command,
        CancellationToken cancellationToken = default)
    {
        // Input validation
        var validation = await _validator.ValidateAsync(command, cancellationToken);
        if (!validation.IsValid)
            return Result.Failure<PropertyResponse>(validation.Errors.First().ErrorMessage);

        // Domain logic
        var propertyResult = Property.Create(command.Name, command.Address, command.Type);
        if (propertyResult.IsFailure)
            return Result.Failure<PropertyResponse>(propertyResult.Error);

        // Persistence with transaction
        using var transaction = await _unitOfWork.BeginTransactionAsync(cancellationToken);

        await _propertyRepository.AddAsync(propertyResult.Value, cancellationToken);
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        await transaction.CommitAsync(cancellationToken);

        return Result.Success(propertyResult.Value.ToResponse());
    }
}

TypeScript / React Patterns

  • Strict TypeScript: Use strict mode with no any types
  • Functional Components: Use hooks, avoid class components
  • Server Components: Leverage Next.js 15 React Server Components
  • Type Safety: Define interfaces for all props and API responses
// Preferred patterns for frontend AI development:

// Strict interface definitions
interface PropertyDetailsProps {
  propertyId: string;
  isEditable?: boolean;
  onUpdate?: (property: Property) => void;
}

// Server component with async data fetching
export default async function PropertyPage({ 
  params 
}: { 
  params: Promise<{ id: string }> 
}) {
  const { id } = await params;
  const property = await getPropertyDetails(id);

  if (!property) {
    notFound();
  }

  return (
    <div className="space-y-6">
      <PropertyHeader property={property} />
      <PropertyDetails property={property} />
      <PropertyUnits propertyId={property.id} />
    </div>
  );
}

// Custom hook with proper error handling
export function usePropertyMutation() {
  return useMutation({
    mutationFn: async (data: CreatePropertyRequest): Promise<Property> => {
      const response = await fetch('/api/properties', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      });

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.message || 'Failed to create property');
      }

      return response.json();
    },
    onSuccess: (property) => {
      toast.success(`Property "${property.name}" created successfully`);
      queryClient.invalidateQueries({ queryKey: ['properties'] });
    },
    onError: (error) => {
      toast.error(error.message);
    },
  });
}

Architecture Patterns

Clean Architecture Implementation

The system follows Domain-Driven Design with Clean Architecture:

┌─────────────────────────────────────┐
│            Web API Layer            │  ← Controllers, Middleware, Configuration
├─────────────────────────────────────┤
│         Application Layer           │  ← Use Cases, DTOs, Command/Query Handlers
├─────────────────────────────────────┤
│           Domain Layer              │  ← Entities, Value Objects, Domain Services
├─────────────────────────────────────┤
│        Infrastructure Layer         │  ← Repositories, External Services, EF Context
└─────────────────────────────────────┘

Service Communication Patterns

  • Synchronous: Direct HTTP calls for immediate consistency requirements
  • Asynchronous: Domain events via message bus for eventual consistency
  • API Gateway: Centralized entry point with authentication and routing
  • BFF Pattern: Separate backend aggregations for web and mobile clients

Data Access Patterns

  • Repository Pattern: Abstraction over data access with interfaces in Domain layer
  • Unit of Work: Transaction management across multiple repositories
  • CQRS: Separate read/write models for complex scenarios
  • Dapper for Queries: Raw SQL for complex read operations
  • EF Core for Commands: Rich ORM for write operations and simple queries

Testing Patterns

Unit Testing Approach

// Domain logic testing with rich assertions
[Test]
public void AddUnit_WhenUnitsExceedLimit_ShouldReturnFailure()
{
    // Arrange
    var property = Property.Create("Test Property", validAddress, PropertyType.SingleFamily).Value;
    var existingUnits = Enumerable.Range(1, 10).Select(i => CreateValidUnit($"Unit-{i}"));

    foreach (var unit in existingUnits)
        property.AddUnit(unit);

    var newUnit = CreateValidUnit("Unit-11");

    // Act
    var result = property.AddUnit(newUnit);

    // Assert
    result.IsFailure.Should().BeTrue();
    result.Error.Should().Contain("Maximum units exceeded");
    property.Units.Should().HaveCount(10);
}

// Application service testing with mocks
[Test]
public async Task CreateProperty_WhenValidCommand_ShouldReturnSuccess()
{
    // Arrange
    var command = new CreatePropertyCommand("Valid Property", validAddress, PropertyType.Apartment, 25);

    _validator.Setup(v => v.ValidateAsync(command, It.IsAny<CancellationToken>()))
           .ReturnsAsync(new ValidationResult());

    // Act
    var result = await _service.CreatePropertyAsync(command);

    // Assert
    result.IsSuccess.Should().BeTrue();
    result.Value.Name.Should().Be("Valid Property");

    _propertyRepository.Verify(r => r.AddAsync(It.IsAny<Property>(), It.IsAny<CancellationToken>()), Times.Once);
    _unitOfWork.Verify(u => u.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
}

Integration Testing with TestContainers

[TestFixture]
public class PropertyIntegrationTests : IntegrationTestBase
{
    [Test]
    public async Task CreateProperty_EndToEnd_ShouldPersistAndReturnCorrectData()
    {
        // Arrange - Use test containers for database
        var request = new CreatePropertyRequest("Integration Test Property", validAddress, PropertyType.Apartment, 50);

        // Act - Call actual API endpoint
        var response = await _httpClient.PostAsJsonAsync("/api/properties", request);

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.Created);

        var property = await response.Content.ReadFromJsonAsync<PropertyResponse>();
        property.Should().NotBeNull();
        property!.Name.Should().Be("Integration Test Property");

        // Verify persistence
        var persisted = await _dbContext.Properties.FindAsync(Guid.Parse(property.Id));
        persisted.Should().NotBeNull();
    }
}

Common Business Patterns

Domain Events for Service Communication

// Raising domain events in entities
public class Property : Entity<PropertyId>
{
    public void CompleteCreation()
    {
        Status = PropertyStatus.Active;
        RaiseDomainEvent(new PropertyCreatedEvent(Id, Name, Address, Type));
    }
}

// Handling events in other services
public class PropertyCreatedEventHandler : INotificationHandler<PropertyCreatedEvent>
{
    public async Task Handle(PropertyCreatedEvent domainEvent, CancellationToken cancellationToken)
    {
        // Create default maintenance schedule
        await _operationsService.CreateDefaultMaintenanceScheduleAsync(domainEvent.PropertyId);

        // Initialize community features
        await _communityService.InitializePropertyCommunityAsync(domainEvent.PropertyId);

        // Send notifications to stakeholders
        await _notificationService.NotifyPropertyCreatedAsync(domainEvent);
    }
}

Error Handling and Validation

// Custom Result type for operation outcomes
public class Result<T>
{
    public bool IsSuccess { get; init; }
    public bool IsFailure => !IsSuccess;
    public T Value { get; init; } = default!;
    public string Error { get; init; } = string.Empty;

    public static Result<T> Success(T value) => new() { IsSuccess = true, Value = value };
    public static Result<T> Failure(string error) => new() { IsSuccess = false, Error = error };
}

// Global exception handling middleware
public class GlobalExceptionMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        try
        {
            await next(context);
        }
        catch (ValidationException ex)
        {
            await HandleValidationExceptionAsync(context, ex);
        }
        catch (DomainException ex)
        {
            await HandleDomainExceptionAsync(context, ex);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unhandled exception occurred");
            await HandleGenericExceptionAsync(context, ex);
        }
    }
}

Database Patterns

Entity Configuration

// EF Core entity configuration following DDD principles
public class PropertyConfiguration : IEntityTypeConfiguration<Property>
{
    public void Configure(EntityTypeBuilder<Property> builder)
    {
        builder.ToTable("Properties");

        // Value object mapping
        builder.OwnsOne(p => p.Address, address =>
        {
            address.Property(a => a.Street).HasColumnName("Address_Street").HasMaxLength(200);
            address.Property(a => a.City).HasColumnName("Address_City").HasMaxLength(100);
            address.Property(a => a.State).HasColumnName("Address_State").HasMaxLength(50);
            address.Property(a => a.ZipCode).HasColumnName("Address_ZipCode").HasMaxLength(10);
        });

        // Navigation properties with proper cascade behavior
        builder.HasMany(p => p.Units)
               .WithOne(u => u.Property)
               .HasForeignKey("PropertyId")
               .OnDelete(DeleteBehavior.Cascade);

        // Indexes for performance
        builder.HasIndex(p => p.Name);
        builder.HasIndex("Address_City", "Address_State");
    }
}

Repository Implementation

public class PropertyRepository : IPropertyRepository
{
    public async Task<Property?> GetByIdAsync(PropertyId id, CancellationToken cancellationToken = default)
    {
        return await _context.Properties
            .Include(p => p.Units)
            .FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
    }

    public async Task<IReadOnlyList<Property>> GetByOwnerIdAsync(UserId ownerId, CancellationToken cancellationToken = default)
    {
        // Use Dapper for complex queries when needed
        const string sql = """
            SELECT p.*, u.* 
            FROM Properties p
            LEFT JOIN Units u ON p.Id = u.PropertyId
            WHERE p.OwnerId = @OwnerId
            ORDER BY p.Name, u.Number
        """;

        var propertyDict = new Dictionary<PropertyId, Property>();

        await _connection.QueryAsync<Property, Unit, Property>(
            sql,
            (property, unit) =>
            {
                if (!propertyDict.TryGetValue(property.Id, out var existingProperty))
                {
                    existingProperty = property;
                    propertyDict.Add(property.Id, existingProperty);
                }

                if (unit != null)
                    existingProperty.AddUnit(unit);

                return existingProperty;
            },
            new { OwnerId = ownerId },
            splitOn: "Id");

        return propertyDict.Values.ToList();
    }
}

Performance and Security Considerations

Caching Strategies

  • Use Redis for frequently accessed read-only data (property listings, user preferences)
  • Implement cache-aside pattern with proper invalidation on updates
  • Cache authentication tokens and user sessions
  • Use distributed cache for multi-instance deployments

Security Patterns

  • JWT tokens with short expiration and refresh token rotation
  • Role-based and resource-based authorization
  • Input validation at multiple layers (client, API, domain)
  • SQL injection prevention through parameterized queries
  • Rate limiting and request throttling

Database Optimization

  • Use appropriate indexes for query patterns
  • Implement read replicas for reporting queries
  • Use connection pooling and proper timeout configurations
  • Monitor query performance and optimize slow queries
  • Implement database health checks and circuit breakers

Development Workflow

Branch Strategy

  • main: Production-ready code with automated deployments
  • develop: Integration branch for feature development
  • feature/*: Individual feature branches from develop
  • hotfix/*: Critical production fixes from main

Code Quality Gates

  1. All code must pass static analysis and formatting checks
  2. Unit test coverage must be > 80% for new code
  3. All public APIs require XML documentation
  4. Security-sensitive changes require additional review
  5. Performance-critical paths require benchmarking

CI/CD Pipeline

# Pipeline stages executed on every commit
dotnet restore && dotnet build --no-restore
dotnet format --verify-no-changes
dotnet test --collect:"XPlat Code Coverage" --no-build
dotnet pack --no-build --configuration Release

# Additional checks for PR validation
dotnet run --project tests/E2E/PropertyManagement.E2E.Tests
docker build -t property-management:pr-${{ github.event.number }} .
./scripts/security-scan.sh
./scripts/performance-test.sh

This comprehensive context should enable AI tools to understand the project structure, coding patterns, and implementation approaches used throughout the Property Management System.