Agent skill
backend-architecture
Use this skill when working on the ASP.NET Core backend — adding controllers, repositories, validators, authorization, WebSocket endpoints, or Aspire orchestration. Apply when modifying project layering (Core, Insulation, Web, Job), configuring services, returning ProblemDetails errors, or understanding how the backend is structured.
Install this agent skill to your Project
npx add-skill https://github.com/exceptionless/Exceptionless/tree/main/.agents/skills/backend-architecture
SKILL.md
Backend Architecture
Quick Start
Run Exceptionless.AppHost from your IDE. Aspire automatically starts all services (Elasticsearch, Redis) with proper ordering. The dashboard opens at the assigned localhost port.
dotnet run --project src/Exceptionless.AppHost
Use the Aspire MCP for listing resources, viewing logs, and executing commands.
Project Layering
Exceptionless.Core → Domain logic, services, repositories, validation
Exceptionless.Insulation → Infrastructure implementations (Redis, GeoIP, Mail, HealthChecks)
Exceptionless.Web → ASP.NET Core host, controllers, WebSocket hubs
Exceptionless.Job → Background job workers
Dependency Direction
Web → Core ← Insulation
Job → Core ← Insulation
Exceptionless.Core
Contains all domain logic, services, and repositories.
Services
Real services in the codebase (see src/Exceptionless.Core/Services/):
UsageService— Tracks event usage per organization/projectEventPostService— Handles event post storage and retrievalStackService— Stack management and status updatesOrganizationService— Organization lifecycle managementMessageService— WebSocket message coordinationSlackService— Slack integration
Repositories
Repositories extend Foundatio.Repositories.Elasticsearch and use validation:
// From src/Exceptionless.Core/Repositories/Base/RepositoryBase.cs
public abstract class RepositoryBase<T> : ElasticRepositoryBase<T> where T : class, IIdentity, new()
{
protected readonly IValidator<T>? _validator;
protected readonly AppOptions _options;
public RepositoryBase(IIndex index, IValidator<T>? validator, AppOptions options) : base(index)
{
_validator = validator;
_options = options;
NotificationsEnabled = options.EnableRepositoryNotifications;
}
protected override Task ValidateAndThrowAsync(T document)
{
if (_validator is null)
return Task.CompletedTask;
return _validator.ValidateAndThrowAsync(document);
}
}
Repositories use Foundatio Parsers for query parsing against Elasticsearch.
Validation
Two validation patterns are used (transitioning to MiniValidator for new code):
FluentValidation for Domain Models
Used by repositories (see src/Exceptionless.Core/Validation/):
// From src/Exceptionless.Core/Validation/OrganizationValidator.cs
public class OrganizationValidator : AbstractValidator<Organization>
{
public OrganizationValidator(BillingPlans plans)
{
RuleFor(o => o.Name).NotEmpty().WithMessage("Please specify a valid name.");
RuleFor(o => o.PlanId).NotEmpty().WithMessage("Please specify a valid plan id.");
RuleFor(o => o.SuspensionCode).NotEmpty().When(o => o.IsSuspended);
}
}
MiniValidator for API Request Models
Uses DataAnnotations with MiniValidator (preferred for new code — repositories are migrating to this):
// From src/Exceptionless.Web/Models/Login.cs
public record Login
{
[Required]
public required string Email { get; init; }
[Required, StringLength(100, MinimumLength = 6)]
public required string Password { get; init; }
}
MiniValidator integration (see src/Exceptionless.Core/Validation/MiniValidationValidator.cs):
public class MiniValidationValidator(IServiceProvider serviceProvider)
{
public async Task ValidateAndThrowAsync<T>(T instance)
{
(bool isValid, var errors) = await MiniValidator.TryValidateAsync(instance, serviceProvider, recurse: true);
if (!isValid)
throw new MiniValidatorException("Please correct the specified errors and try again", errors);
}
}
public class MiniValidatorException(string message, IDictionary<string, string[]> errors) : Exception(message)
{
public IDictionary<string, string[]> Errors { get; } = errors;
}
Auto-validation via AutoValidationActionFilter handles API model validation automatically.
Exceptionless.Insulation
Infrastructure implementations only — NOT services or repositories:
Configuration/— YAML configuration extensionsGeo/— MaxMind GeoIP serviceHealthChecks/— Elasticsearch, Cache, Queue, Storage health checksMail/— MailKit mail senderRedis/— Redis connection mapping
Authorization with Policy Constants
Use AuthorizationRoles constants (NOT string literals):
// From src/Exceptionless.Core/Authorization/AuthorizationRoles.cs
public static class AuthorizationRoles
{
public const string ClientPolicy = nameof(ClientPolicy);
public const string Client = "client";
public const string UserPolicy = nameof(UserPolicy);
public const string User = "user";
public const string GlobalAdminPolicy = nameof(GlobalAdminPolicy);
public const string GlobalAdmin = "global";
}
Apply to controllers:
// From src/Exceptionless.Web/Controllers/AuthController.cs
[Route(API_PREFIX + "/auth")]
[Authorize(Policy = AuthorizationRoles.UserPolicy)]
public class AuthController : ExceptionlessApiController
{
[AllowAnonymous]
[HttpPost("login")]
public async Task<ActionResult<TokenResult>> LoginAsync(Login model) { }
}
// From src/Exceptionless.Web/Controllers/AdminController.cs
[Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)]
public class AdminController : ExceptionlessApiController { }
Controller Patterns
CRUD via RepositoryApiController
Most controllers extend RepositoryApiController<TRepository, TModel, TViewModel, TNewModel, TUpdateModel>:
// From src/Exceptionless.Web/Controllers/OrganizationController.cs
[Route(API_PREFIX + "/organizations")]
[Authorize(Policy = AuthorizationRoles.UserPolicy)]
public class OrganizationController : RepositoryApiController<IOrganizationRepository, Organization, ViewOrganization, NewOrganization, NewOrganization>
{
[HttpGet]
public async Task<ActionResult<IReadOnlyCollection<ViewOrganization>>> GetAllAsync(string? mode = null)
{
var organizations = await GetModelsAsync(GetAssociatedOrganizationIds().ToArray());
var viewOrganizations = await MapCollectionAsync<ViewOrganization>(organizations, true);
return Ok(viewOrganizations);
}
}
Thin Controllers for Auth/Special Cases
// From src/Exceptionless.Web/Controllers/AuthController.cs
public class AuthController : ExceptionlessApiController
{
[AllowAnonymous]
[HttpPost("login")]
public async Task<ActionResult<TokenResult>> LoginAsync(Login model)
{
string email = model.Email.Trim().ToLowerInvariant();
using var _ = _logger.BeginScope(new ExceptionlessState()
.Tag("Login")
.Identity(email)
.SetHttpContext(HttpContext));
var user = await _userRepository.GetByEmailAddressAsync(email);
if (user is null || !user.IsActive)
return Unauthorized();
return Ok(new TokenResult { Token = await GetOrCreateAuthenticationTokenAsync(user) });
}
}
ProblemDetails and Error Handling
Return Helpers
// Success responses
return Ok(data);
return Created(uri, await MapAsync<TViewModel>(model, true));
return NoContent();
// Error responses from ExceptionlessApiController
return Unauthorized(); // 401
return Forbidden(); // 403 - custom helper
return NotFound(); // 404
return ValidationProblem(ModelState); // 422 with validation errors
Exception to ProblemDetails Mapping
Exceptions are automatically converted via ExceptionToProblemDetailsHandler:
// From src/Exceptionless.Web/Startup.cs
MiniValidatorException => StatusCodes.Status422UnprocessableEntity,
ValidationException => StatusCodes.Status422UnprocessableEntity,
// Other exceptions map to 500
WebSocket Hubs (NOT SignalR)
Uses custom WebSocket implementation with Foundatio message bus:
// From src/Exceptionless.Web/Hubs/MessageBusBroker.cs
public sealed class MessageBusBroker : IStartupAction
{
private readonly WebSocketConnectionManager _connectionManager;
private readonly IMessageSubscriber _subscriber;
public async Task RunAsync(CancellationToken shutdownToken = default)
{
await Task.WhenAll(
_subscriber.SubscribeAsync<EntityChanged>(OnEntityChangedAsync, shutdownToken),
_subscriber.SubscribeAsync<PlanChanged>(OnPlanChangedAsync, shutdownToken),
_subscriber.SubscribeAsync<UserMembershipChanged>(OnUserMembershipChangedAsync, shutdownToken)
);
}
}
Key files:
Hubs/MessageBusBroker.cs— Subscribes to message bus, broadcasts to WebSocket clientsHubs/WebSocketConnectionManager.cs— Manages WebSocket connections
Configuration Pattern
Uses YAML files with custom environment variable binding:
// From src/Exceptionless.Web/Program.cs
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddYamlFile("appsettings.yml", optional: true, reloadOnChange: true)
.AddYamlFile($"appsettings.{environment}.yml", optional: true, reloadOnChange: true)
.AddCustomEnvironmentVariables()
.AddCommandLine(args)
.Build();
AppOptions
All configuration binds to AppOptions class with nested options:
AppOptions.EmailOptionsAppOptions.AuthOptionsAppOptions.IntercomOptionsAppOptions.SlackOptionsAppOptions.StripeOptions
Access via direct injection (not IOptions<T>):
public class UsageService
{
public UsageService(AppOptions options, ILoggerFactory loggerFactory)
{
_options = options;
}
}
Service Discovery
Services reference each other by name in Aspire:
// AppHost topology
var elasticsearch = builder.AddElasticsearch("elasticsearch");
var api = builder.AddProject<Projects.Exceptionless_Web>("api")
.WithReference(elasticsearch);
// In service, get connection by resource name
var esConnection = builder.Configuration.GetConnectionString("elasticsearch");
Dependencies
- NuGet feeds configured in NuGet.Config
- Version alignment in
src/Directory.Build.props - Avoid deprecated APIs — check for alternatives before using legacy methods
Route Patterns
[Route(API_PREFIX + "/organizations")] // Collection
[HttpGet("{id}")] // Single resource
[Route("~/" + API_PREFIX + "/admin/organizations")] // Admin override
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
foundatio-repositories
releasenotes
Generate formatted changelogs from git history since the last release tag. Use when preparing release notes that categorize changes into breaking changes, features, fixes, and other sections.
e2e-testing
Use this skill when writing or running end-to-end browser tests with Playwright. Covers Page Object Model patterns, selector strategies (data-testid, getByRole, getByLabel), fixtures, and accessibility audits with axe-playwright. Apply when adding E2E test coverage, debugging flaky tests, or testing user flows through the browser.
tanstack-query
Use this skill when fetching data, managing server state, or handling API mutations in the Svelte frontend. Covers createQuery, createMutation, query keys, cache invalidation, optimistic updates, and WebSocket-driven refetching. Apply when adding API calls, managing loading/error states, or coordinating cache updates after mutations.
dogfood
Systematically explore and test a web application to find bugs, UX issues, and other problems. Use when asked to "dogfood", "QA", "exploratory test", "find issues", "bug hunt", "test this app/site/platform", or review the quality of a web application. Produces a structured report with full reproduction evidence -- step-by-step screenshots, repro videos, and detailed repro steps for every issue -- so findings can be handed directly to the responsible teams.
storybook
Use this skill when creating or updating Storybook stories for Svelte components. Covers Svelte CSF story format, defineMeta, argTypes, snippet-based customization, and autodocs. Apply when adding visual documentation for components, setting up story files, or running Storybook for development.
Didn't find tool you were looking for?