App Studio
Extending the Service Layer
Adding a new data operation to App Studio requires exactly three steps: add the method signature to IAppStudioService, implement it in AIExtension.Service.AppStudioDataService, and add the controller endpoint that calls the service. No other changes are needed.
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.