搜索

PythonReactSEOPDF
GitHub Skill

backend-architecture

Backend architecture for Exceptionless. Project layering, repositories, validation, controllers, authorization, WebSockets, configuration, and Aspire orchestration. Keywords: Core, Insulation, repositories, FluentValidation, MiniValidator, controllers, AuthorizationRoles, ProblemDetails, Aspire, WebSockets, AppOptions

exceptionless☆ 2,449 仓库 Stars513 仓库 Forks

安装与使用

请根据来源仓库和你的 AI 工具说明安装此技能。先阅读技能文件和依赖要求,再添加到工作流。

查看来源 ↗使用指南

技能说明

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/project
  • EventPostService — Handles event post storage and retrieval
  • StackService — Stack management and status updates
  • OrganizationService — Organization lifecycle management
  • MessageService — WebSocket message coordination
  • SlackService — 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 extensions
  • Geo/ — MaxMind GeoIP service
  • HealthChecks/ — Elasticsearch, Cache, Queue, Storage health checks
  • Mail/ — MailKit mail sender
  • Redis/ — 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 clients
  • Hubs/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.EmailOptions
  • AppOptions.AuthOptions
  • AppOptions.IntercomOptions
  • AppOptions.SlackOptions
  • AppOptions.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

使用示例

查看来源仓库中的使用示例与配置说明。

查看来源 ↗

来源历史

收录时间
2026-01-20
索引更新时间
2026-01-26
Branch
main

收录与同步日期属于本站记录。源码修改历史请查看来源仓库。

GitHub ↗

chat Comments (0)

chat_bubble_outline

No comments yet. Be the first to share your thoughts!