{"id":13597034,"url":"https://github.com/amantinband/clean-architecture","last_synced_at":"2025-05-14T18:03:58.887Z","repository":{"id":212439476,"uuid":"699266565","full_name":"amantinband/clean-architecture","owner":"amantinband","description":"The ultimate clean architecture template for .NET applications 💪","archived":false,"fork":false,"pushed_at":"2024-07-14T06:24:24.000Z","size":15628,"stargazers_count":1739,"open_issues_count":15,"forks_count":293,"subscribers_count":31,"default_branch":"main","last_synced_at":"2025-04-13T06:20:00.677Z","etag":null,"topics":["asp-net-core","clean-architecture","dotnet","dotnet-core","software-architecture","web-application"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/amantinband.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["amantinband"],"patreon":"amantinband","open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"lfx_crowdfunding":null,"custom":null}},"created_at":"2023-10-02T09:36:16.000Z","updated_at":"2025-04-11T14:55:52.000Z","dependencies_parsed_at":"2024-01-17T19:37:03.639Z","dependency_job_id":"f2fa28ee-77b7-435e-a757-cd511990500c","html_url":"https://github.com/amantinband/clean-architecture","commit_stats":null,"previous_names":["amantinband/clean-architecture"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/amantinband%2Fclean-architecture","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/amantinband%2Fclean-architecture/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/amantinband%2Fclean-architecture/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/amantinband%2Fclean-architecture/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/amantinband","download_url":"https://codeload.github.com/amantinband/clean-architecture/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254198452,"owners_count":22030964,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asp-net-core","clean-architecture","dotnet","dotnet-core","software-architecture","web-application"],"created_at":"2024-08-01T17:00:18.397Z","updated_at":"2025-05-14T18:03:53.873Z","avatar_url":"https://github.com/amantinband.png","language":"C#","funding_links":["https://github.com/sponsors/amantinband","https://patreon.com/amantinband"],"categories":["Templates","C# #"],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n[![Build](https://github.com/amantinband/clean-architecture/actions/workflows/build.yml/badge.svg)](https://github.com/amantinband/clean-architecture/actions/workflows/build.yml) [![Publish template to NuGet](https://github.com/amantinband/clean-architecture/actions/workflows/publish.yml/badge.svg)](https://github.com/amantinband/clean-architecture/actions/workflows/publish.yml)\n\n[![GitHub contributors](https://img.shields.io/github/contributors/amantinband/clean-architecture)](https://GitHub.com/amantinband/clean-architecture/graphs/contributors/) [![GitHub Stars](https://img.shields.io/github/stars/amantinband/clean-architecture.svg)](https://github.com/amantinband/clean-architecture/stargazers) [![GitHub license](https://img.shields.io/github/license/amantinband/clean-architecture)](https://github.com/amantinband/clean-architecture/blob/main/LICENSE)\n[![codecov](https://codecov.io/gh/amantinband/clean-architecture/branch/main/graph/badge.svg?token=DR2EBIWK7B)](https://codecov.io/gh/amantinband/clean-architecture)\n\n---\n\n![Clean Architecture Template Title](assets/Clean%20Architecture%20Template%20Title.png)\n\n---\n\n\u003c/div\u003e\n\n```shell\ndotnet new install Amantinband.CleanArchitecture.Template\n\ndotnet new clean-arch -o CleanArchitecture\n```\n\n- [️Important notice ⚠️](#️important-notice-️)\n- [Give it a star ⭐](#give-it-a-star-)\n- [Domain Overview 🌍](#domain-overview-)\n  - [Basic Subscription](#basic-subscription)\n  - [Pro Subscription](#pro-subscription)\n- [Use Cases / Features 🤓](#use-cases--features-)\n  - [Subscriptions](#subscriptions)\n  - [Reminders](#reminders)\n- [Getting Started 🏃](#getting-started-)\n  - [YouTube Tutorial](#youtube-tutorial)\n  - [Install the template or clone the project](#install-the-template-or-clone-the-project)\n  - [Run the service using Docker or the .NET CLI](#run-the-service-using-docker-or-the-net-cli)\n  - [Generate a token](#generate-a-token)\n  - [Create a subscription](#create-a-subscription)\n  - [Create a reminder](#create-a-reminder)\n- [Folder Structure 📁](#folder-structure-)\n- [Authorization 🔐](#authorization-)\n  - [Authorization Types](#authorization-types)\n    - [Role-Based Authorization](#role-based-authorization)\n    - [Permission-Based Authorization](#permission-based-authorization)\n    - [Policy-Based Authorization](#policy-based-authorization)\n  - [Mixing Authorization Types](#mixing-authorization-types)\n- [Testing 📝](#testing-)\n  - [Test Types](#test-types)\n    - [Domain Layer Unit Tests](#domain-layer-unit-tests)\n    - [Application Layer Unit Tests](#application-layer-unit-tests)\n    - [Application Layer Subcutaneous Tests](#application-layer-subcutaneous-tests)\n    - [Presentation Layer Integration Tests](#presentation-layer-integration-tests)\n- [Fun features 💃🕺](#fun-features-)\n  - [Domain Events \\\u0026 Eventual Consistency](#domain-events--eventual-consistency)\n    - [Eventual Consistency Mechanism](#eventual-consistency-mechanism)\n  - [Background service for sending email reminders](#background-service-for-sending-email-reminders)\n    - [Configure Email Settings](#configure-email-settings)\n    - [Configure Email Settings Manually](#configure-email-settings-manually)\n    - [Configure Email Settings via User Secrets](#configure-email-settings-via-user-secrets)\n- [Contribution 🤲](#contribution-)\n- [Credits 🙏](#credits-)\n- [License 🪪](#license-)\n\n# ️Important notice ⚠️\n\nThis template is still under construction 👷.\n\nCheck out my comprehensive [course](https://dometrain.com/bundle/from-zero-to-hero-clean-architecture/?coupon_code=GITHUB) on Dometrain where I cover everything you need to know when building production applications structured following clean architecture. Use the exclusive coupon code `GITHUB` to get 5% off (btw this is the only promo code for a discount on the bundle, which is already 20% off).\n\n\u003cimg src=\"assets/Clean%20Architecture%20Template%20Promo%20Code.png\" height=30px \u003e\n\n# Give it a star ⭐\n\nLoving it? Show your support by giving this project a star!\n\n# Domain Overview 🌍\n\nThis is a simple reminder application. It allows users to create and manage their reminders.\n\nTo create reminders, a user must have an active subscription.\n\n## Basic Subscription\n\nUsers with a basic subscription can create up to 3 daily reminders.\n\n## Pro Subscription\n\nUsers with a pro subscription do not have a daily limit on the number of reminders.\n\n# Use Cases / Features 🤓\n\n## Subscriptions\n\n1. Create Subscription\n1. Get Subscription\n1. Cancel Subscription\n\n## Reminders\n\n1. Set Reminder\n1. Get Reminder\n1. Delete Reminder\n1. Dismiss Reminder\n1. List Reminders\n\n# Getting Started 🏃\n\n## YouTube Tutorial\n\n[![Clean Architecture Tutorial](https://img.youtube.com/vi/DTmlhHu8tHA/0.jpg)](https://www.youtube.com/watch?v=DTmlhHu8tHA)\n\n## Install the template or clone the project\n\n```shell\ndotnet new install Amantinband.CleanArchitecture.Template\n\ndotnet new clean-arch -o CleanArchitecture\n```\n\nor\n\n```shell\ngit clone https://github.com/amantinband/clean-architecture\n```\n\n## Run the service using Docker or the .NET CLI\n\n```shell\ndocker compose up\n```\n\nor\n\n```shell\ndotnet run --project src/CleanArchitecture.Api\n```\n\n## Generate a token\n\nNavigate to `requests/Tokens/GenerateToken.http` and generate a token.\n\n\u003e Note: Since most systems use an external identity provider, this project uses a simple token generator endpoint that generates a token based on the details you provide. This is a simple way to generate a token for testing purposes and is closer to how your system will likely be designed when using an external identity provider.\n\n```yaml\nPOST {{host}}/tokens/generate\nContent-Type: application/json\n```\n\n```http\n{\n    \"Id\": \"bae93bf5-9e3c-47b3-aace-3034653b6bb2\",\n    \"FirstName\": \"Amichai\",\n    \"LastName\": \"Mantinband\",\n    \"Email\": \"amichai@mantinband.com\",\n    \"Permissions\": [\n        \"set:reminder\",\n        \"get:reminder\",\n        \"dismiss:reminder\",\n        \"delete:reminder\",\n        \"create:subscription\",\n        \"delete:subscription\",\n        \"get:subscription\"\n    ],\n    \"Roles\": [\n        \"Admin\"\n    ]\n}\n```\n\n\u003e ### NOTE: Replacing http file variables (`{{variableName}}`)\n\u003e\n\u003e #### Option 1 (recommended) - Using the [REST Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension for VS Code\n\u003e\n\u003e Use the [REST Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension for VS Code + update the values under [.vscode/settings.json](.vscode/settings.json). This will update the value for all http files.\n\u003e\n\u003e ```json\n\u003e {\n\u003e    \"rest-client.environmentVariables\": {\n\u003e        \"$shared\": { // these will be shared across all http files, regardless of the environment\n\u003e            \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiTGlvciIsImZhbWlseV9uYW1lIjoiRGFnYW4iLCJlbWFpbCI6Imxpb3JAZGFnYW4uY29tIiwiaWQiOiJhYWU5M2JmNS05ZTNjLTQ3YjMtYWFjZS0zMDM0NjUzYjZiYjIiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJBZG1pbiIsInBlcm1pc3Npb25zIjpbInNldDpyZW1pbmRlciIsImdldDpyZW1pbmRlciIsImRpc21pc3M6cmVtaW5kZXIiLCJkZWxldGU6cmVtaW5kZXIiLCJjcmVhdGU6c3Vic2NyaXB0aW9uIiwiZGVsZXRlOnN1YnNjcmlwdGlvbiIsImdldDpzdWJzY3JpcHRpb24iXSwiZXhwIjoxNzA0MTM0MTIzLCJpc3MiOiJSZW1pbmRlclNlcnZpY2UiLCJhdWQiOiJSZW1pbmRlclNlcnZpY2UifQ.wyvn9cq3ohp-JPTmbBd3G1cAU1A6COpiQd3C_e_Ng5s\",\n\u003e            \"userId\": \"aae93bf5-9e3c-47b3-aace-3034653b6bb2\",\n\u003e            \"subscriptionId\": \"c8ee11f0-d4bb-4b43-a448-d511924b520e\",\n\u003e            \"reminderId\": \"08233bb1-ce29-49e2-b346-5f8b7cf61593\"\n\u003e        },\n\u003e        \"dev\": { // when the environment is set to dev, these values will be used\n\u003e            \"host\": \"http://localhost:5001\",\n\u003e        },\n\u003e        \"prod\": { // when the environment is set to prod, these values will be used\n\u003e            \"host\": \"http://your-prod-endpoint.com\",\n\u003e        }\n\u003e    }\n\u003e}\n\u003e ```\n\u003e\n\u003e #### Options 2 - Defining the variables in the http file itself\n\u003e\n\u003e Define the variables in the http file itself. This will only update the value for the current http file.\n\u003e\n\u003e ```yaml\n\u003e @host = http://localhost:5001\n\u003e\n\u003e POST {{host}}/tokens/generate\n\u003e ```\n\u003e\n\u003e #### Option 3 - Manually\n\u003e\n\u003e Replace the variables manually.\n\u003e\n\u003e ```yaml\n\u003e POST {{host}}/tokens/generate\n\u003e ```\n\u003e\n\u003e 👇\n\u003e\n\u003e ```yaml\n\u003e POST http://localhost:5001/tokens/generate\n\u003e ```\n\u003e\n\n## Create a subscription\n\n```yaml\nPOST {{host}}/users/{{userId}}/subscriptions\nContent-Type: application/json\nAuthorization: Bearer {{token}}\n```\n\n```http\n{\n    \"SubscriptionType\": \"Basic\"\n}\n```\n\n## Create a reminder\n\n```yaml\nPOST {{host}}/users/{{userId}}/subscriptions/{{subscriptionId}}/reminders\nContent-Type: application/json\nAuthorization: Bearer {{token}}\n```\n\n```http\n{\n    \"text\": \"let's do it\",\n    \"dateTime\": \"2025-2-26\"\n}\n```\n\n# Folder Structure 📁\n\n![Folder structure](assets/Clean%20Architecture%20Template.png)\n\nYou can use the [this](https://www.figma.com/community/file/1334042945934670571/clean-architecture-project-file-system) figma community file to explore or create your own folder structure respresentation.\n\n# Authorization 🔐\n\nThis project puts an emphasis on complex authorization scenarios and supports *role-based*, *permission-based* and *policy-based* authorization.\n\n## Authorization Types\n\n### Role-Based Authorization\n\nTo apply role based authorization, use the `Authorize` attribute with the `Roles` parameter and implement the `IAuthorizeableRequest` interface.\n\nFor example:\n\n```csharp\n[Authorize(Roles = \"Admin\")]\npublic record CancelSubscriptionCommand(Guid UserId, Guid SubscriptionId) : IAuthorizeableRequest\u003cErrorOr\u003cSuccess\u003e\u003e;\n```\n\nWill only allow users with the `Admin` role to cancel subscriptions.\n\n### Permission-Based Authorization\n\nTo apply permission based authorization, use the `Authorize` attribute with the `Permissions` parameter and implement the `IAuthorizeableRequest` interface.\n\nFor example:\n\n```csharp\n[Authorize(Permissions = \"get:reminder\")]\npublic record GetReminderQuery(Guid UserId, Guid SubscriptionId, Guid ReminderId) : IAuthorizeableRequest\u003cErrorOr\u003cReminder\u003e\u003e;\n```\n\nWill only allow users with the `get:reminder` permission to get a subscription.\n\n### Policy-Based Authorization\n\nTo apply policy based authorization, use the `Authorize` attribute with the `Policy` parameter and implement the `IAuthorizeableRequest` interface.\n\nFor example:\n\n```csharp\n[Authorize(Policies = \"SelfOrAdmin\")]\npublic record GetReminderQuery(Guid UserId, Guid SubscriptionId, Guid ReminderId) : IAuthorizeableRequest\u003cErrorOr\u003cReminder\u003e\u003e;\n```\n\nWill only allow users who pass the `SelfOrAdmin` policy to get a subscription.\n\nEach policy is implemented as a simple method in the `PolicyEnforcer` class.\n\nThe policy \"SelfOrAdmin\" for example, can be implemented as follows:\n\n```csharp\npublic class PolicyEnforcer : IPolicyEnforcer\n{\n    public ErrorOr\u003cSuccess\u003e Authorize\u003cT\u003e(\n        IAuthorizeableRequest\u003cT\u003e request,\n        CurrentUser currentUser,\n        string policy)\n    {\n        return policy switch\n        {\n            \"SelfOrAdmin\" =\u003e SelfOrAdminPolicy(request, currentUser),\n            _ =\u003e Error.Unexpected(description: \"Unknown policy name\"),\n        };\n    }\n\n    private static ErrorOr\u003cSuccess\u003e SelfOrAdminPolicy\u003cT\u003e(IAuthorizeableRequest\u003cT\u003e request, CurrentUser currentUser) =\u003e\n        request.UserId == currentUser.Id || currentUser.Roles.Contains(Role.Admin)\n            ? Result.Success\n            : Error.Unauthorized(description: \"Requesting user failed policy requirement\");\n}\n```\n\n## Mixing Authorization Types\n\nYou can mix and match authorization types to create complex authorization scenarios.\n\nFor example:\n\n```csharp\n[Authorize(Permissions = \"get:reminder,list:reminder\", Policies = \"SelfOrAdmin\", Roles = \"ReminderManager\")]\npublic record ListRemindersQuery(Guid UserId, Guid SubscriptionId, Guid ReminderId) : IAuthorizeableRequest\u003cErrorOr\u003cReminder\u003e\u003e;\n```\n\nWill only allow users with the `get:reminder` and  `list:reminder` permission, and who pass the `SelfOrAdmin` policy, and who have the `ReminderManager` role to list reminders.\n\nAnother option, is specifying the `Authorize` attribute multiple times:\n\n```csharp\n[Authorize(Permissions = \"get:reminder\")]\n[Authorize(Permissions = \"list:reminder\")]\n[Authorize(Policies = \"SelfOrAdmin\")]\n[Authorize(Roles = \"ReminderManager\")]\npublic record ListRemindersQuery(Guid UserId, Guid SubscriptionId, Guid ReminderId) : IAuthorizeableRequest\u003cErrorOr\u003cReminder\u003e\u003e;\n```\n\n# Testing 📝\n\nThis project puts an emphasis on testability and comes with a comprehensive test suite.\n\n![](assets/Clean%20Architecture%20Template%20Testing%20Suite.png)\n\n## Test Types\n\n### Domain Layer Unit Tests\n\nThe domain layer is tested using unit tests.\nBy the bare minimum, each domain entity should have a test that verifies its invariants.\n\n![Domain Layer unit tests](assets/Clean%20Architecture%20Template%20Domain%20Layer%20Unit%20Tests.png)\n\n### Application Layer Unit Tests\n\nThe application layer is tested using both unit tests and subcutaneous tests.\n\nSince each one of the application layer use cases has its corresponding subcutaneous tests, the unit tests are used to test the application layer standalone components, such as the `ValidationBehavior` and the `AuthorizationBehavior`.\n\n![Application Layer unit tests](assets/Clean%20Architecture%20Template%20Application%20Layer%20Unit%20Tests.png)\n\n### Application Layer Subcutaneous Tests\n\nSubcutaneous tests are tests that operate right under the presentation layer.\nThese tests are responsible for testing the core logic of our application, which is the application layer and the domain layer.\n\nThe reason there are so many of these tests, is because each one of the application layer use cases has its corresponding subcutaneous tests.\n\nThis allows us to test the application layer and the domain layer based on the actual expected usage, giving us the confidence that our application works as expected and that the system cannot be manipulated in a way we don't allow.\n\nI recommend spending more effort on these tests than the other tests, since they aren't too expensive to write, and the value they provide is huge.\n\n![](assets/Clean%20Architecture%20Template%20Subcutaneous%20Tests.png)\n\n### Presentation Layer Integration Tests\n\nThe api layer is tested using integration tests. This is where we want to cover the entire system, including the database, external dependencies and the presentation layer.\n\nUnlike the subcutaneous tests, the focus of these tests is to ensure the integration between the various components of our system and other systems.\n\n![Integration Tests](assets/Clean%20Architecture%20Template%20Integration%20Tests.png)\n\n# Fun features 💃🕺\n\n## Domain Events \u0026 Eventual Consistency\n\n\u003e Note: Eventual consistency and the domain events pattern add a layer of complexity. If you don't need it, don't use it. If you need it, make sure your system is designed properly and that you have the right tools to manage failures.\n\nThe domain is designed so each use case which manipulates data, updates a single domain object in a single transaction.\n\nFor example, when a user cancels a subscription, the only change that happens atomically is the subscription is marked as canceled:\n\n```csharp\npublic ErrorOr\u003cSuccess\u003e CancelSubscription(Guid subscriptionId)\n{\n    if (subscriptionId != Subscription.Id)\n    {\n        return Error.NotFound(\"Subscription not found\");\n    }\n\n    Subscription = Subscription.Canceled;\n\n    _domainEvents.Add(new SubscriptionCanceledEvent(this, subscriptionId));\n\n    return Result.Success;\n}\n```\n\nThen, in an eventual consistency manner, the system will update all the relevant data. Which includes:\n\n1. Deleting the subscription from the database and marking all reminders as deleted ([Subscriptions/Events/SubscriptionDeletedEventHandler.cs](src/CleanArchitecture.Application/Subscriptions/Events/SubscriptionCanceledEventHandler.cs)])\n1. Deleting all the reminders marked as deleted from the database ([Reminders/Events/ReminderDeletedEventHandler.cs](src/CleanArchitecture.Application/Reminders/Events/ReminderDeletedEventHandler.cs)]\n\n\u003e Note: Alongside the performance benefits, this allows to reuse reactive behavior. For example, the `ReminderDeletedEventHandler` is invoked both when a subscription is deleted and when a reminder is deleted.\n\n### Eventual Consistency Mechanism\n\n1. Each invariant is encapsulated in a single domain object. This allows performing changes by updating a single domain object in a single transaction.\n1. If `domain object B` needs to react to changes in `domain object A`, a [Domain Event](src/CleanArchitecture.Domain/Common/IDomainEvent.cs) is added to `domain object A` alongside the changes.\n1. Upon persisting `domain object A` changes to the database, the domain events are [extracted and added to a queue](src/CleanArchitecture.Infrastructure/Common/Persistence/AppDbContext.cs) for offline processing:\n    ```csharp\n    private void AddDomainEventsToOfflineProcessingQueue(List\u003cIDomainEvent\u003e domainEvents)\n    {\n        Queue\u003cIDomainEvent\u003e domainEventsQueue = new();\n        domainEvents.ForEach(domainEventsQueue.Enqueue);\n\n        _httpContextAccessor.HttpContext.Items[\"DomainEvents\"] = domainEventsQueue;\n    }\n    ```\n1. After the user receives a response, the [EventualConsistencyMiddleware](src/CleanArchitecture.Infrastructure/Common/Middleware/EventualConsistencyMiddleware.cs) is invoked and processes the domain events:\n    ```csharp\n    public async Task InvokeAsync(HttpContext context, IEventualConsistencyProcessor eventualConsistencyProcessor)\n    {\n        context.Response.OnCompleted(async () =\u003e\n        {\n            if (context.Items.TryGetValue(\"DomainEvents\", out var value) ||\n                value is not Queue\u003cIDomainEvent\u003e domainEvents)\n            {\n                return;\n            }\n\n            while (domainEvents.TryDequeue(out var nextEvent))\n            {\n                await publisher.Publish(nextEvent);\n            }\n        });\n    }\n    ```\n\n\u003e Note: the code snippets above are a simplified version of the actual implementation.\n\n## Background service for sending email reminders\n\nThere is a simple background service that runs every minute and sends email reminders for all reminders that are due ([ReminderEmailBackgroundService](src/CleanArchitecture.Infrastructure/Reminders/BackgroundServices/ReminderEmailBackgroundService.cs)):\n\n```csharp\nprivate async void SendEmailNotifications(object? state)\n{\n    await _fluentEmail\n        .To(user.Email)\n        .Subject($\"{dueReminders.Count} reminders due!\")\n        .Body($\"\"\"\n              Dear {user.FirstName} {user.LastName} from the present.\n\n              I hope this email finds you well.\n\n              I'm writing you this email to remind you about the following reminders:\n              {string.Join('\\n', dueReminders.Select((reminder, i) =\u003e $\"{i + 1}. {reminder.Text}\"))}\n\n              Best,\n              {user.FirstName} from the past.\n              \"\"\")\n        .SendAsync();\n}\n```\n\n### Configure Email Settings\n\nTo configure the service to send emails, make sure to update the email settings under the `appsettings.json`/`appsettings.Development.json` file:\n\nYou can use your own SMTP server or use a service like [Brevo](https://brevo.com/).\n\n### Configure Email Settings Manually\n\n```json\n{\n  \"EmailSettings\": {\n    \"EnableEmailNotifications\": false,\n    \"DefaultFromEmail\": \"your-email@gmail.com (also, change EnableEmailNotifications to true 👆)\",\n    \"SmtpSettings\": {\n      \"Server\": \"smtp.gmail.com\",\n      \"Port\": 587,\n      \"Username\": \"your-email@gmail.com\",\n      \"Password\": \"your-password\"\n    }\n  }\n}\n```\n\n\u003e note: you may need to allow less secure apps to access your email account.\n\n### Configure Email Settings via User Secrets\n\n```shell\ndotnet user-secrets --project src/CleanArchitecture.Api set EmailSettings:EnableEmailNotifications true\ndotnet user-secrets --project src/CleanArchitecture.Api set EmailSettings:DefaultFromEmail amantinband@gmail.com\ndotnet user-secrets --project src/CleanArchitecture.Api set EmailSettings:SmtpSettings:Server smtp-relay.brevo.com\ndotnet user-secrets --project src/CleanArchitecture.Api set EmailSettings:SmtpSettings:Port 587\ndotnet user-secrets --project src/CleanArchitecture.Api set EmailSettings:SmtpSettings:Username amantinband@gmail.com\ndotnet user-secrets --project src/CleanArchitecture.Api set EmailSettings:SmtpSettings:Password your-password\n```\n\n# Contribution 🤲\n\nIf you have any questions, comments, or suggestions, please open an issue or create a pull request 🙂\n\n# Credits 🙏\n\n- [CleanArchitecture](https://github.com/jasontaylordev/CleanArchitecture) - An awesome clean architecture solution template by Jason Taylor\n\n# License 🪪\n\nThis project is licensed under the terms of the [MIT](https://github.com/mantinband/clean-architecture/blob/main/LICENSE) license.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Famantinband%2Fclean-architecture","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Famantinband%2Fclean-architecture","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Famantinband%2Fclean-architecture/lists"}