amantinband Clean Architecture Template

repository·main·Indexed 23 days ago

https://github.com/amantinband/clean-architecture

A production-ready Clean Architecture template for .NET applications. It demonstrates the implementation of complex role, permission, and policy-based authorization using IAuthorizeableRequest<T>, domain events for eventual consistency, and background services for email notifications. The project includes a multi-layered testing strategy comprising domain unit tests, application subcutaneous tests, and presentation integration tests.

Tokens
3.9K
Snippets
11
Records
18
Agent score
83%

What's inside amantinband-clean-architecture

  1. How Domain Events and Eventual Consistency work

    main

    The system uses Domain Events to manage side effects without bloating single transactions. This allows for high performance and reactive behavior.

    The Workflow:

    1. Atomic Update: A use case updates a single domain object within a single transaction (e.g., marking a subscription as Canceled).
    2. Event Dispatch: A IDomainEvent is added to the domain object during the update.
    3. Queueing: Upon persisting changes, the AppDbContext extracts these events and adds them to a queue in the HttpContext.Items.
    4. Asynchronous Processing: After the HTTP response is sent, the EventualConsistencyMiddleware dequeues and publishes the events for offline processing (e.g., deleting related reminders).
    // Example of adding an event within a domain method
    public ErrorOr<Success> CancelSubscription(Guid subscriptionId)
    {
        // ... validation ...
        Subscription = Subscription.Canceled;
        _domainEvents.Add(new SubscriptionCanceledEvent(this, subscriptionId));
        return Result.Success;
    }
  2. Understand the testing strategy

    main

    The project uses a multi-layered testing approach to ensure reliability across different architectural boundaries:

    • Domain Layer Unit Tests: Verifies domain entities and their invariants.
    • Application Layer Unit Tests: Tests standalone components like ValidationBehavior and AuthorizationBehavior.
    • Application Layer Subcutaneous Tests: Operates just below the presentation layer. These test the core logic (Application and Domain layers) based on actual expected usage. These are highly recommended as they provide high value with relatively low effort.
    • Presentation Layer Integration Tests: Covers the entire system, including the API layer, database, and external dependencies, to ensure component integration works correctly.
  3. Generate a test token

    main

    Since the project is designed to work with external identity providers, it includes a simple token generator endpoint for testing purposes. You can generate a token by sending a POST request to the /tokens/generate endpoint.

    Navigate to requests/Tokens/GenerateToken.http in the repository to use the provided HTTP file. You can customize the Id, FirstName, LastName, Email, Permissions, and Roles in the JSON body to simulate different user scenarios.

    POST {{host}}/tokens/generate
    Content-Type: application/json
    
    {
        "Id": "bae93bf5-9e3c-47b3-aace-3034653b6bb2",
        "FirstName": "Amichai",
        "LastName": "Mantinband",
        "Email": "amichai@mantinband.com",
        "Permissions": [
            "set:reminder",
            "get:reminder",
            "dismiss:reminder",
            "delete:reminder",
            "create:subscription",
            "delete:subscription",
            "get:subscription"
        ],
        "Roles": [
            "Admin"
        ]
    }
  4. Install the Clean Architecture template

    main

    You can install the project as a .NET template or clone the repository directly to explore the implementation.

    To install the template and create a new project named CleanArchitecture:

    dotnet new install Amantinband.CleanArchitecture.Template
    
    dotnet new clean-arch -o CleanArchitecture

    Alternatively, clone the repository:

    git clone https://github.com/amantinband/clean-architecture
  5. Configure Email Settings

    main

    The ReminderEmailBackgroundService sends email notifications. You must configure the EmailSettings in your configuration files to enable this service.

    Using appsettings.json

    Update appsettings.json or appsettings.Development.json with your SMTP details:

    {
      "EmailSettings": {
        "EnableEmailNotifications": true,
        "DefaultFromEmail": "your-email@gmail.com",
        "SmtpSettings": {
          "Server": "smtp.gmail.com",
          "Port": 587,
          "Username": "your-email@gmail.com",
          "Password": "your-password"
        }
      }
    }

    Using dotnet user-secrets

    For local development, it is recommended to use user secrets to avoid committing credentials to source control:

    dotnet user-secrets --project src/CleanArchitecture.Api set EmailSettings:EnableEmailNotifications true
    dotnet user-secrets --project src/CleanArchitecture.Api set EmailSettings:DefaultFromEmail amantinband@gmail.com
    dotnet user-secrets --project src/CleanArchitecture.Api set EmailSettings:SmtpSettings:Server smtp-relay.brevo.com
    dotnet user-secrets --project src/CleanArchitecture.Api set EmailSettings:SmtpSettings:Port 587
    dotnet user-secrets --project src/CleanArchitecture.Api set EmailSettings:SmtpSettings:Username amantinband@gmail.com
    dotnet user-secrets --project src/CleanArchitecture.Api set EmailSettings:SmtpSettings:Password your-password
  6. Run the API service using Docker Compose

    main

    The project provides a docker-compose.yml file to orchestrate the API service. When running via Docker Compose, the service is named api and maps port 5001 on the host to port 5001 in the container. The environment is set to Development by default.

    To persist data, the SQLite database file located at ./src/CleanArchitecture.Api/CleanArchitecture.sqlite on the host is mounted to /app/CleanArchitecture.sqlite inside the container.

    services:
      api:
        container_name: clean-architecture-api
        build:
          context: .
          dockerfile: Dockerfile
        ports:
          - "5001:5001"
        environment:
          - ASPNETCORE_ENVIRONMENT=Development
        restart: on-failure
        volumes:
          - ./src/CleanArchitecture.Api/CleanArchitecture.sqlite:/app/CleanArchitecture.sqlite
  7. Create a subscription

    main

    To create a subscription for a user, send a POST request to the /users/{userId}/subscriptions endpoint. You must include a Bearer token in the Authorization header.

    Request Body:

    {
        "SubscriptionType": "Basic"
    }
    POST {{host}}/users/{{userId}}/subscriptions
    Content-Type: application/json
    Authorization: Bearer {{token}}
    
    {
        "SubscriptionType": "Basic"
    }
  8. Create a reminder

    main

    To create a reminder, send a POST request to the /users/{userId}/subscriptions/{subscriptionId}/reminders endpoint. You must include a Bearer token in the Authorization header.

    Request Body:

    {
        "text": "let's do it",
        "dateTime": "2025-2-26"
    }
    POST {{host}}/users/{{userId}}/subscriptions/{{subscriptionId}}/reminders
    Content-Type: application/json
    Authorization: Bearer {{token}}
    
    {
        "text": "let's do it",
        "dateTime": "2025-2-26"
    }
  9. Implement Role-Based Authorization

    main

    To apply role-based authorization, use the [Authorize] attribute with the Roles parameter on your command or query. The request must implement the IAuthorizeableRequest<T> interface.

    Example:

    [Authorize(Roles = "Admin")]
    public record CancelSubscriptionCommand(Guid UserId, Guid SubscriptionId) : IAuthorizeableRequest<ErrorOr<Success>>;
  10. Implement Permission-Based Authorization

    main

    To apply permission-based authorization, use the [Authorize] attribute with the Permissions parameter. The request must implement the IAuthorizeableRequest<T> interface.

    Example:

    [Authorize(Permissions = "get:reminder")]
    public record GetReminderQuery(Guid UserId, Guid SubscriptionId, Guid ReminderId) : IAuthorizeableRequest<ErrorOr<Reminder>>;
  11. Mix and match authorization types

    main

    You can implement complex authorization scenarios by combining Permissions, Policies, and Roles on your request records.

    There are two ways to apply these:

    1. Single Attribute: Pass all requirements into a single [Authorize] attribute.
    2. Multiple Attributes: Apply multiple [Authorize] attributes to the same record, where each attribute represents an additional requirement that must be met.

    Requests must implement IAuthorizeableRequest<T> to use these attributes.

    // Option 1: Single attribute
    [Authorize(Permissions = "get:reminder,list:reminder", Policies = "SelfOrAdmin", Roles = "ReminderManager")]
    public record ListRemindersQuery(Guid UserId, Guid SubscriptionId, Guid ReminderId) : IAuthorizeableRequest<ErrorOr<Reminder>>;
    
    // Option 2: Multiple attributes
    [Authorize(Permissions = "get:reminder")]
    [Authorize(Permissions = "list:reminder")]
    [Authorize(Policies = "SelfOrAdmin")]
    [Authorize(Roles = "ReminderManager")]
    public record ListRemindersQuery(Guid UserId, Guid SubscriptionId, Guid ReminderId) : IAuthorizeableRequest<ErrorOr<Reminder>>;