Portal Community

The Three-Step Pattern

1
Add the method to IAppStudioService — defines the contract
2
Implement it in AppStudioDataService — provides multi-tenancy, caching, and audit
3
Add the controller endpoint — exposes it over HTTP and calls the service

Step 1: Add to IAppStudioService

// AppStudio/Services/IAppStudioService.cs
public interface IAppStudioService
{
    // ... existing methods ...

    // NEW: Get app usage statistics
    Task GetAppUsageStatsAsync(string tenantId, string appId,
                                               DateRange dateRange);
}

Step 2: Implement in AppStudioDataService

// AIExtension/Services/AppStudioDataService.cs
public class AppStudioDataService : IAppStudioService
{
    // ... existing implementations ...

    public async Task GetAppUsageStatsAsync(
        string tenantId, string appId, DateRange dateRange)
    {
        // Always scope to tenantId — this is non-negotiable
        var stats = await _db.AppAuditLogs
            .Where(l => l.TenantId == tenantId
                     && l.AppId == appId
                     && l.Timestamp >= dateRange.Start
                     && l.Timestamp <= dateRange.End)
            .GroupBy(l => l.Action)
            .Select(g => new { Action = g.Key, Count = g.Count() })
            .ToListAsync();

        return new AppUsageStats
        {
            AppId = appId,
            TenantId = tenantId,
            Period = dateRange,
            ActionCounts = stats.ToDictionary(s => s.Action, s => s.Count)
        };
    }
}

Step 3: Add Controller Endpoint

// AppStudio/Controllers/AppStatsController.cs
[ApiController]
[Route("api/tenants/{tenantId}/apps/{appId}/stats")]
public class AppStatsController : ControllerBase
{
    private readonly IAppStudioService _service;

    public AppStatsController(IAppStudioService service) => _service = service;

    [HttpGet]
    public async Task GetUsageStats(
        string tenantId, string appId,
        [FromQuery] DateTime from, [FromQuery] DateTime to)
    {
        // TenantId from route is validated against JWT by middleware
        var stats = await _service.GetAppUsageStatsAsync(
            tenantId, appId, new DateRange(from, to));
        return Ok(stats);
    }
}

What NOT to Do

// ❌ WRONG — do not inject DbContext into App Studio controllers
public class AppStatsController : ControllerBase
{
    private readonly AIExtDbContext _db;  // ❌ Forbidden

    public async Task GetStats(string tenantId, string appId)
    {
        // This bypasses multi-tenancy enforcement, caching, and audit
        var data = await _db.AppAuditLogs
            .Where(l => l.AppId == appId)  // ❌ Missing tenantId scoping
            .ToListAsync();
    }
}

// ❌ WRONG — do not add repositories to the App Studio project
// ❌ WRONG — do not use HttpClient to call a separate App Studio service
// ✅ CORRECT — IAppStudioService is the only allowed data access point
Interface-first discipline Always define the method in IAppStudioService first — before writing the implementation. This keeps the contract explicit and allows mocking in unit tests without depending on the AIExtension.Service implementation.