mirror of https://github.com/abpframework/abp.git
268 changed files with 5070 additions and 1975 deletions
|
After Width: | Height: | Size: 769 KiB |
@ -0,0 +1,191 @@ |
|||
# **Truly Layering a .NET Application Based on DDD Principles** |
|||
|
|||
Okay, so we ALL been there, right? You start new project thinking "this time will be different" - clean code, perfect architecture, everything organized. Fast forward 3 months and your codebase look like someone throw grenade into bowl of spaghetti. Business logic everywhere, your controllers doing database work, and every new feature feel like defusing bomb. |
|||
|
|||
I been there too many times, and honestly, it suck. But here thing - there actually way to build .NET apps that not turn into maintenance nightmare. It called **Layered Architecture** + **Domain-Driven Design (DDD)**, and once you get it, it game changer. |
|||
|
|||
Let me walk you through this step by step, no fluff, just practical stuff that actually work. |
|||
|
|||
### **Layered Architecture 101 (The Foundation)** |
|||
|
|||
So layered architecture basically about keeping your code organized. Instead of having everything mixed together like bad smoothie, you separate concerns into different layers. Think like organizing your room - clothes go in closet, books on shelf, etc. |
|||
|
|||
Here how it typically break down: |
|||
|
|||
* **Presentation Layer (UI):** This what users actually see and click on - your ASP.NET Core MVC stuff, Razor Pages, Blazor, whatever float your boat. |
|||
* **Application Layer:** The conductor of orchestra. It not do heavy lifting itself, but tell everyone else what to do. It like middle manager of your code. |
|||
* **Domain Layer:** The VIP section. This where all your business rules live - entities, value objects, whole nine yards. This layer pure and not give damn about databases or UI. |
|||
* **Infrastructure Layer:** The "how-to" guy. Database stuff, email sending, API calls - basically all technical plumbing that make everything work. |
|||
|
|||
The golden rule? **Dependency Rule**: Layers can only talk to layers below them (or more central). UI talk to Application, Application talk to Domain, but Domain? Domain not talk to anyone. It the cool kid that everyone want to hang out with. |
|||
|
|||
### **DDD: Where Magic Happen** |
|||
|
|||
Alright, so DDD not some fancy framework you install from NuGet. It more like mindset - basically saying "hey, let make our code actually reflect business we building for." Instead of having bunch of random classes, we organize everything around actual business domain. |
|||
|
|||
Think like this: if you building e-commerce app, your code should scream "I'M E-COMMERCE APP" not "I'M BUNCH OF RANDOM CLASSES." |
|||
|
|||
Here toolkit DDD give you (all living in your Domain Layer): |
|||
|
|||
* **Entity:** This something that have identity. Like `Customer` - two customers with same name still different people because they have different IDs. It like having two friends named John - they not same person. |
|||
* **Value Object:** Opposite of entity. It defined by what it contain, not who it is. `Address` perfect for this - if two addresses have same street, city, and zip code, they same address. Usually immutable too. |
|||
* **Aggregate & Aggregate Root:** This where it get interesting. Aggregate like family of related objects that stick together. **Aggregate Root** head of family - only one you talk to when you want change something. Like `Order` that contain `OrderItem`s. You not mess with `OrderItem` directly, you tell `Order` to handle it. |
|||
* **Repository (Interface):** Think like your data access contract. It say "here how you can get and save stuff" without caring about whether it SQL Server, MongoDB, or file on your desktop. Interface live in Domain, implementation go in Infrastructure. |
|||
* **Domain Service:** When business logic too complex for single entity or value object, this your go-to. It like utility class but for business rules. |
|||
|
|||
### **Putting It All Together: Real C# Code** |
|||
|
|||
Alright, enough theory. Let see what this actually look like in real .NET solution. You typically have projects like: |
|||
|
|||
* `MyProject.Domain` (or `.Core`) - The VIP section |
|||
* `MyProject.Application` - The middle manager |
|||
* `MyProject.Infrastructure` - The technical guy |
|||
* `MyProject.Web` (or whatever UI you using) - The pretty face |
|||
|
|||
**1. The Domain Layer (`MyProject.Domain`) - The Heart** |
|||
|
|||
This where magic happen. Zero dependencies on other projects (maybe some basic utility libraries, but that it). Pure business logic, no database nonsense, no UI concerns. |
|||
|
|||
```csharp |
|||
// In MyProject.Domain/Orders/Order.cs |
|||
public class Order : AggregateRoot<Guid> |
|||
{ |
|||
public Address ShippingAddress { get; private set; } |
|||
private readonly List<OrderItem> _orderItems = new(); |
|||
public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly(); |
|||
|
|||
// Private constructor for ORM |
|||
private Order() { } |
|||
|
|||
public Order(Guid id, Address shippingAddress) : base(id) |
|||
{ |
|||
ShippingAddress = shippingAddress; |
|||
} |
|||
|
|||
public void AddOrderItem(Guid productId, int quantity, decimal price) |
|||
{ |
|||
if (quantity <= 0) |
|||
{ |
|||
throw new BusinessException("Quantity must be greater than zero."); |
|||
} |
|||
// More business rules... |
|||
_orderItems.Add(new OrderItem(productId, quantity, price)); |
|||
} |
|||
} |
|||
|
|||
// In MyProject.Domain/Orders/IOrderRepository.cs |
|||
public interface IOrderRepository |
|||
{ |
|||
Task<Order> GetAsync(Guid id); |
|||
Task AddAsync(Order order); |
|||
Task UpdateAsync(Order order); |
|||
} |
|||
``` |
|||
|
|||
See what I mean? The `Order` class all about business rules (`AddOrderItem` with validation and all that jazz). It not give damn about databases or how it get saved. That someone else problem. |
|||
|
|||
**2. The Application Layer (`MyProject.Application`) - The Conductor** |
|||
|
|||
This where we orchestrate everything. It talk to domain objects and use repositories to get/save data. Think like middle manager that coordinate work but not do heavy lifting. |
|||
|
|||
```csharp |
|||
// In MyProject.Application/Orders/OrderAppService.cs |
|||
public class OrderAppService |
|||
{ |
|||
private readonly IOrderRepository _orderRepository; |
|||
|
|||
public OrderAppService(IOrderRepository orderRepository) |
|||
{ |
|||
_orderRepository = orderRepository; |
|||
} |
|||
|
|||
public async Task CreateOrderAsync(CreateOrderDto input) |
|||
{ |
|||
var shippingAddress = new Address(input.Street, input.City, input.ZipCode); |
|||
var order = new Order(Guid.NewGuid(), shippingAddress); |
|||
|
|||
foreach (var item in input.Items) |
|||
{ |
|||
order.AddOrderItem(item.ProductId, item.Quantity, item.Price); |
|||
} |
|||
|
|||
await _orderRepository.AddAsync(order); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
The application service coordinate everything but let domain objects handle actual business rules. Clean separation! |
|||
|
|||
**3. The Infrastructure Layer (`MyProject.Infrastructure`) - The Technical Guy** |
|||
|
|||
This where we implement all interfaces we defined in domain. Entity Framework Core, email services, API clients - all technical plumbing live here. |
|||
|
|||
```csharp |
|||
// In MyProject.Infrastructure/Orders/EfCoreOrderRepository.cs |
|||
public class EfCoreOrderRepository : IOrderRepository |
|||
{ |
|||
private readonly MyDbContext _dbContext; |
|||
|
|||
public EfCoreOrderRepository(MyDbContext dbContext) |
|||
{ |
|||
_dbContext = dbContext; |
|||
} |
|||
|
|||
public async Task<Order> GetAsync(Guid id) |
|||
{ |
|||
// EF Core logic to get the order |
|||
return await _dbContext.Orders.FindAsync(id); |
|||
} |
|||
|
|||
public async Task AddAsync(Order order) |
|||
{ |
|||
await _dbContext.Orders.AddAsync(order); |
|||
} |
|||
|
|||
// ... other implementations |
|||
} |
|||
``` |
|||
|
|||
### **ABP Framework: The Shortcut (Because We Lazy)** |
|||
|
|||
Look, setting all this up from scratch pain. That where **ABP Framework** come in clutch. It basically DDD and layered architecture on steroids, and it do all boring setup work for you. |
|||
|
|||
ABP not just talk talk - it walk walk. When you create new ABP solution, boom! Perfect project structure, all layered and DDD-compliant, ready to go. |
|||
|
|||
Here what you get out of box: |
|||
|
|||
* **Base Classes:** `AggregateRoot`, `Entity`, `ValueObject` - all with good stuff like optimistic concurrency and domain events. No more writing boilerplate. |
|||
* **Generic Repositories:** No more writing `IRepository` interfaces for every single entity. ABP give you `IRepository<TEntity, TKey>` with all standard CRUD methods. Just inject it and go. |
|||
* **Application Services:** Inherit from `ApplicationService` and boom - you done. It handle validation, authorization, exception handling, all that cross-cutting concern stuff without cluttering your actual business logic. |
|||
|
|||
With ABP, our `OrderAppService` become way cleaner: |
|||
|
|||
```csharp |
|||
// In ABP project, this much cleaner |
|||
public class OrderAppService : ApplicationService, IOrderAppService |
|||
{ |
|||
private readonly IRepository<Order, Guid> _orderRepository; |
|||
|
|||
public OrderAppService(IRepository<Order, Guid> orderRepository) |
|||
{ |
|||
_orderRepository = orderRepository; |
|||
} |
|||
|
|||
public async Task CreateAsync(CreateOrderDto input) |
|||
{ |
|||
// ... same logic as before, but using ABP generic repository |
|||
var order = new Order(...); |
|||
await _orderRepository.InsertAsync(order); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### **Wrapping Up** |
|||
|
|||
Look, I get it - this stuff take discipline and it not always fastest way to get features out door. But here thing: when you actually layer your app properly and put solid Domain Model at center, you end up with software that not suck to maintain. |
|||
|
|||
Your code start speaking language of business instead of some random technical jargon. That whole point of DDD - make your code reflect what you actually building for. |
|||
|
|||
Yeah, it take work upfront, but payoff huge. And frameworks like ABP make journey way less painful. Trust me, your future self will thank you when you not debugging spaghetti code at 2 AM. |
|||
|
|||
What you think? You try this approach before, or you still stuck in spaghetti code phase? Let me know in comments! |
|||
@ -0,0 +1,333 @@ |
|||
# Best Practices Guide for REST API Design |
|||
|
|||
This guide compiles best practices for building robust, scalable, and sustainable RESTful APIs, based on information gathered from various sources. |
|||
|
|||
## 1. Fundamentals of REST Architecture |
|||
|
|||
REST is based on specific constraints and principles that support features like simplicity, scalability, and statelessness. The six core principles of RESTful architecture are: |
|||
|
|||
- **Uniform Interface**: This is about consistency. You use standard HTTP methods (GET, POST, PUT, DELETE) and URIs to interact with resources. The client knows how to talk to the server without needing some custom instruction manual. |
|||
|
|||
- **Client-Server**: The client (e.g., a frontend app) and the server are separate. The server handles data and logic, the client handles the user interface. They can evolve independently as long as the API contract doesn't change. |
|||
|
|||
- **Stateless**: This is a big one. The server doesn't remember anything about the client between requests. Every single request must contain all the info needed to process it (like an auth token). This is key for scalability. |
|||
|
|||
- **Cacheable**: Responses should declare whether they can be cached or not. Good caching can massively improve performance and reduce server load. |
|||
|
|||
- **Layered System**: You can have things like proxies or load balancers between the client and the server without the client knowing. It just talks to one endpoint, and the layers in between handle the rest. |
|||
|
|||
- **Code on Demand (Optional)**: This is the only optional one. It means the server can send back executable code (like JavaScript) to the client. Less common in the world of modern SPAs, but it's part of the spec. |
|||
|
|||
## 2. URI Design and Naming Conventions |
|||
|
|||
The URI structure is critical for making your API understandable and intuitive. |
|||
|
|||
### Use Nouns Instead of Verbs |
|||
|
|||
Your URIs should represent things (resources), not actions. The HTTP method already tells you what the action is. |
|||
|
|||
- **Good:** `/api/users` |
|||
|
|||
- **Bad:** `/api/getUsers` |
|||
|
|||
### Use Plural Nouns for Resource Names |
|||
|
|||
Stick with plural nouns for collections. It keeps things consistent, even when you're accessing a single item from that collection. |
|||
|
|||
- **Get all users:** `GET /api/users` |
|||
|
|||
- **Get a single user:** `GET /api/users/{id}` |
|||
|
|||
### Use Nested Routes to Show Relationships |
|||
|
|||
If a resource only exists in the context of another (like a user's orders), reflect that in the URL. |
|||
|
|||
- **Good:** `/api/users/{userId}/orders` (All orders for a user) |
|||
|
|||
- **Bad:** `/api/orders?userId={userId}` |
|||
|
|||
- **Good:** `/api/users/{userId}/orders/{orderId}` (A specific order for a user) |
|||
|
|||
**Note:** Use this structure only if the child resource is tightly coupled to the parent. Avoid nesting deeper than two or three levels, as this can complicate the URIs. |
|||
|
|||
### Path Parameters vs. Query Parameters |
|||
|
|||
Use the correct parameter type based on its function. |
|||
|
|||
- **Path Parameters (`/users/{id}`):** Use these to identify a specific resource or a collection. They are mandatory for the endpoint to resolve. |
|||
|
|||
- *Example:* `GET /api/users/123` uniquely identifies user 123. |
|||
|
|||
- **Query Parameters (`?key=value`):** Use these for optional actions like filtering, sorting, or pagination on a collection. |
|||
|
|||
- *Example:* `GET /api/users?role=admin&sort=lastName` filters the user collection. |
|||
|
|||
### Keep the URL Structure Consistent |
|||
|
|||
- **Use lowercase letters:** Since some systems are case-sensitive, always use lowercase in URIs for consistency. |
|||
|
|||
- *Example:* Use `/api/product-offers` instead of `/api/Product-Offers`. |
|||
|
|||
- **Use special characters correctly:** Use characters like `/`, `?`, and `#` only for their defined purposes. |
|||
|
|||
- *Example:* To get comments for a specific post, use the path `/posts/123/comments`. To filter those comments, use a query parameter: `/posts/123/comments?authorId=45`. |
|||
|
|||
## 3. Correct Usage of HTTP Methods |
|||
|
|||
Each HTTP method has a specific purpose. Sticking to these standards makes your API predictable. |
|||
|
|||
| **HTTP Method** | **Description** | **Idempotent*** | **Safe**** | |
|||
| --------------- | --------------------------------------------------------------------------- | --------------- | ---------- | |
|||
| **GET** | Retrieves a resource or a collection of resources. | Yes | Yes | |
|||
| **POST** | Creates a new resource. | No | No | |
|||
| **PUT** | Updates an existing resource completely or creates it if it does not exist. | Yes | No | |
|||
| **PATCH** | Partially updates an existing resource. | No | No | |
|||
| **DELETE** | Deletes a resource. | Yes | No | |
|||
|
|||
- **Idempotent:** Doing it once has the same effect as doing it 100 times. Deleting a user is idempotent; once it's gone, it's gone. |
|||
|
|||
- **Safe:** The request doesn't change anything on the server. GET is safe. |
|||
|
|||
**Example in practice:** |
|||
|
|||
Let's consider a resource endpoint for a collection of articles: `/api/articles`. |
|||
|
|||
- **`GET /api/articles`**: Retrieves a list of all articles. |
|||
|
|||
- **`GET /api/articles/123`**: Retrieves the specific article with ID 123. |
|||
|
|||
- **`POST /api/articles`**: Creates a new article. The data for the new article is sent in the request body. |
|||
|
|||
- **`PUT /api/articles/123`**: Replaces the entire article with ID 123 using the new data sent in the request body. |
|||
|
|||
- **`PATCH /api/articles/123`**: Partially updates the article with ID 123. For example, you could send only the `{"title": "New Title"}` in the request body to update just the title. |
|||
|
|||
- **`DELETE /api/articles/123`**: Deletes the article with ID 123. |
|||
|
|||
## 4. Data Exchange and Responses |
|||
|
|||
### Prefer the JSON Format |
|||
|
|||
It's the standard. It's lightweight, human-readable, and every language can parse it easily. Send and receive your data as JSON. |
|||
|
|||
- *Example Request Body:* |
|||
|
|||
``` |
|||
{ |
|||
"title": "Best Practices for APIs", |
|||
"authorId": 5, |
|||
"content": "An article about designing great APIs..." |
|||
} |
|||
``` |
|||
|
|||
### Use Appropriate HTTP Status Codes |
|||
|
|||
Use standard HTTP status codes to provide clear information to the client about the outcome of their request. |
|||
|
|||
- **2xx (Success):** |
|||
|
|||
- `200 OK`: The request was successful. (For GET, PUT, PATCH) |
|||
|
|||
- `201 Created`: The resource was successfully created. (For POST) The response should include a `Location` header with the URI of the new resource. |
|||
|
|||
- *Example:* `POST /api/articles` responds with `201 Created` and the header `Location: /api/articles/124`. |
|||
|
|||
- `204 No Content`: The request was successful, but there is no response body. (For DELETE) |
|||
|
|||
- **4xx (Client Error):** |
|||
|
|||
- `400 Bad Request`: Invalid request (e.g., missing or incorrect data). |
|||
|
|||
- `401 Unauthorized`: Authentication is required. |
|||
|
|||
- `403 Forbidden`: No permission. |
|||
|
|||
- `404 Not Found`: The requested resource could not be found. |
|||
|
|||
- **5xx (Server Error):** |
|||
|
|||
- `500 Internal Server Error`: An unexpected error occurred on the server. |
|||
|
|||
### Provide Clear and Consistent Error Responses |
|||
|
|||
When something goes wrong, give back a useful JSON error message. Your future self and any developer using your API will thank you. |
|||
|
|||
- *Example of a detailed error response:* |
|||
|
|||
``` |
|||
{ |
|||
"type": "[https://---.com/probs/validation-error](https://example.com/probs/validation-error)", |
|||
"title": "Your request parameters didn't validate.", |
|||
"status": 400, |
|||
"detail": "The 'email' field must be a valid email address.", |
|||
"instance": "/api/users" |
|||
} |
|||
``` |
|||
|
|||
## 5. Performance Optimization |
|||
|
|||
Optimizing API performance is crucial for providing a good user experience and ensuring the scalability of your service. Key strategies include caching, efficient data retrieval, and controlling traffic. |
|||
|
|||
### Caching |
|||
|
|||
Caching is one of the most effective ways to improve performance. By storing and reusing frequently accessed data, you can significantly reduce latency and server load. |
|||
|
|||
- **How it works:** Caching can be implemented at various levels (client-side, CDN, server-side). REST APIs can facilitate this by using standard HTTP caching headers. |
|||
|
|||
- **Key Headers:** |
|||
|
|||
- `Cache-Control`: Tells the client how long to cache something (e.g., `public, max-age=600`). |
|||
|
|||
- `ETag`: A unique version identifier for a resource. The client can send this back in an `If-None-Match` header. If the data hasn't changed, you can just return `304 Not Modified` with an empty body, saving bandwidth. |
|||
|
|||
- `Last-Modified`: Indicates when the resource was last changed. Similar to `ETag`, it can be used for conditional requests with the `If-Modified-Since` header. |
|||
|
|||
- *Example Response Header for Caching:* |
|||
|
|||
``` |
|||
Cache-Control: public, max-age=600 |
|||
ETag: "x234dff" |
|||
``` |
|||
|
|||
### Filtering, Sorting, and Pagination |
|||
|
|||
For endpoints that return lists of resources, it's inefficient to return the entire dataset at once, especially if it's large. Implementing these features gives clients more control over the data they receive. |
|||
|
|||
- **Filtering:** Allows clients to narrow down the result set based on specific criteria. This reduces the amount of data transferred and makes it easier for the client to find what it needs. |
|||
|
|||
- *Example:* `GET /api/orders?status=shipped&customer_id=123` |
|||
|
|||
- **Sorting:** Enables clients to request the data in a specific order. A common convention is to specify the field to sort by and the direction (ascending or descending). |
|||
|
|||
- *Example:* `GET /api/users?sort=lastName_asc` or `GET /api/products?sort=-price` (the `-` indicates descending order). |
|||
|
|||
- **Pagination:** Breaks down a large result set into smaller, manageable chunks called "pages". This prevents overloading the server and client with massive amounts of data in a single response. |
|||
|
|||
- *Example:* `GET /api/articles?page=2&pageSize=20` (retrieves the second page, with 20 articles per page). |
|||
|
|||
### Rate Limiting |
|||
|
|||
Protect your API from abuse by limiting how many requests a client can make in a given time. If they exceed the limit, return a `429 Too Many Requests`. |
|||
It's also super helpful to return these headers so the client knows what's going on: |
|||
|
|||
- `X-RateLimit-Limit`: Total requests allowed. |
|||
|
|||
- `X-RateLimit-Remaining`: How many requests they have left. |
|||
|
|||
- `Retry-After`: How many seconds they should wait before trying again. |
|||
|
|||
## 6. Security |
|||
|
|||
Security is not an optional feature; it must be a core part of your API design. |
|||
|
|||
- **Always Use HTTPS (TLS):** Encrypt all traffic to prevent man-in-the-middle attacks. There are no exceptions to this rule for production APIs. |
|||
|
|||
- **Authentication & Authorization:** |
|||
|
|||
- **Authentication** (Who are you?): Use a standard like OAuth 2.0 or JWT Bearer Tokens. |
|||
|
|||
- **Authorization** (What are you allowed to do?): Check permissions for every request. Just because a user is logged in doesn't mean they can delete another user's data. |
|||
|
|||
- **Input Validation**: Always validate and sanitize data coming from the client to prevent injection attacks. If the data is bad, reject it with a `400 Bad Request`. |
|||
|
|||
- **Use Security Headers**: Add headers like `Strict-Transport-Security` and `Content-Security-Policy` to add extra layers of browser-level protection. |
|||
|
|||
## 7. API Lifecycle Management |
|||
|
|||
### Versioning |
|||
|
|||
Your API will change. Versioning lets you make breaking changes without messing up existing clients. The most common way is in the URI. |
|||
|
|||
- **URI Versioning (Most Common):** `https://api.example.com/v1/users` |
|||
|
|||
- **Pros:** Simple, explicit, and easy to explore in a browser. |
|||
|
|||
- **Header Versioning:** The client requests a version via a custom HTTP header. |
|||
|
|||
- *Example:* `Accept-Version: v1` |
|||
|
|||
- **Pros:** Keeps the URI clean. |
|||
|
|||
- **Media Type Versioning (Content Negotiation):** The version is included in the `Accept` header. |
|||
|
|||
- *Example:* `Accept: application/vnd.example.v1+json` |
|||
|
|||
- **Pros:** Technically the "purest" REST approach. |
|||
|
|||
### Backward Compatibility & Deprecation |
|||
|
|||
When you release v2, don't just kill v1. Keep it running for a while and communicate a clear shutdown schedule to your users. |
|||
|
|||
### Documentation |
|||
|
|||
An API is only as good as its documentation. Use tools like the **OpenAPI Specification (formerly Swagger)** to generate interactive, machine-readable documentation. Good docs should include: |
|||
|
|||
- Authentication instructions. |
|||
|
|||
- Clear explanations of each endpoint. |
|||
|
|||
- Request/response examples. |
|||
|
|||
- Error code definitions. |
|||
|
|||
## 8. Monitoring and Testing |
|||
|
|||
### Monitoring and Logging |
|||
|
|||
To ensure your API is reliable, you must monitor its health and log important events. |
|||
|
|||
- **Structured Logging:** Log in a machine-readable format like JSON. Include a `correlationId` to track a single request across multiple services. |
|||
|
|||
- **Monitoring:** Track key metrics like latency (response time), error rate, and requests per second. Use tools like Prometheus, Grafana, or Datadog to visualize these metrics and set up alerts. |
|||
|
|||
### API Testing |
|||
|
|||
Thorough testing is essential to prevent bugs and regressions. |
|||
|
|||
- **Unit Tests:** Test individual components and business logic in isolation. |
|||
|
|||
- **Integration Tests:** Test the interaction between different parts of your API, including the database. |
|||
|
|||
- **Contract Tests:** Verify that your API adheres to its documented contract (e.g., the OpenAPI spec). |
|||
|
|||
## 9. Advanced Level: HATEOAS |
|||
|
|||
**HATEOAS (Hypermedia as the Engine of Application State)** is a REST principle that allows your API to be self-documenting and more discoverable. It involves including hyperlinks in responses for actions that can be performed on the relevant resource. |
|||
|
|||
For example, a response for a user resource might look like this: |
|||
|
|||
``` |
|||
{ |
|||
"id": 1, |
|||
"name": "Deo Steel", |
|||
"links": [ |
|||
{ "rel": "self", "href": "/users/1", "method": "GET" }, |
|||
{ "rel": "update", "href": "/users/1", "method": "PUT" }, |
|||
{ "rel": "delete", "href": "/users/1", "method": "DELETE" } |
|||
] |
|||
} |
|||
``` |
|||
|
|||
This way, the client can follow the links in the response to take the next step, rather than manually constructing the URIs. |
|||
|
|||
## 10. A Practical Shortcut: Leveraging Frameworks like ABP.IO |
|||
|
|||
Okay, that was a lot. While it's crucial to understand all these principles, you don't have to build everything from scratch. Modern frameworks can handle a ton of this for you. I work a lot in the .NET space, and **ABP Framework** is a great example of this. |
|||
|
|||
Here’s how it automates many of the things we just talked about: |
|||
|
|||
- **Automatic API Controllers**: You write your business logic in an "Application Service," and ABP automatically creates the REST API endpoints for you, following all the correct naming and HTTP method conventions. (Covers sections 2 & 3). |
|||
|
|||
- **Built-in Best Practices**: |
|||
|
|||
- **Standardized Error Responses**: It has a built-in exception handling system that automatically generates clean, consistent JSON error responses. (Covers section 4). |
|||
|
|||
- **Input Validation**: It has automatic validation for your DTOs. If a request is invalid, it returns a detailed `400 Bad Request` without you writing a single line of code for it. (Covers section 6). |
|||
|
|||
- **Paging, Sorting, Filtering**: You get these out of the box by just using their predefined interfaces. (Covers section 5). |
|||
|
|||
- **Integrated Security**: It comes with a full auth system. You just add an `[Authorize]` attribute to a method, and it handles the rest. It also automatically manages database transactions per API request (Unit of Work) to ensure data consistency. (Covers section 6). |
|||
|
|||
- **Automatic Documentation**: It automatically generates an OpenAPI/Swagger UI for your API, which is a massive help for anyone who needs to use it. (Covers section 7). |
|||
|
|||
Using a framework like this lets you focus on your core business logic, confident that the foundation is built on solid, established best practices. |
|||
@ -0,0 +1,803 @@ |
|||
# Step-by-Step AWS Secrets Manager Integration in ABP Framework Projects |
|||
|
|||
## Introduction |
|||
In this article, we are going to discuss how to secure sensitive data in ABP Framework projects using AWS Secrets Manager and explain various aspects and concepts of _secret_ data management. We will explain step-by-step AWS Secrets Manager integration. |
|||
|
|||
|
|||
## What is the Problem? |
|||
Modern applications must store sensitive data such as API keys, database connection strings, OAuth client credentials, and other similar sensitive data. These are at the center of functionality but if stored in the wrong place can be massive security issues. |
|||
|
|||
At build time, the first place that comes to mind is usually **appsettings.json**. This is a configuration file; it is not a secure place to store secret information, especially in production. |
|||
|
|||
### Common Security Risks: |
|||
- **Plain text storage**: Plain text storage of passwords |
|||
- **Exposure to version control**: Secrets are rendered encrypted in Git repositories |
|||
- **No access control**: Anyone who has file access can see the secrets |
|||
- **No rotation**: We must change them manually |
|||
- **No audit trail**: Who accessed which secret when is not known |
|||
|
|||
## .NET User Secrets Tool vs AWS Secrets Manager |
|||
|
|||
**User Secrets (.NET Secret Manager Tools)** is a dev environment only, local file-based solution that keeps sensitive information out of the repository. |
|||
|
|||
**AWS Secrets Manager** is production. It's a centralized, encrypted, and audited secret management service. |
|||
|
|||
| Feature | User Secrets (Dev) | AWS Secrets Manager (Prod) | |
|||
| ---------------------- | ---------------------------- | ------------------------------ | |
|||
| Scope | Local developer machine | All environments (dev/stage/prod) | |
|||
| Storage | JSON in user profile | Managed service (centralized) | |
|||
| Encryption | None (plain text file) | Encrypted with KMS | |
|||
| Access Control | OS file permissions | IAM policies | |
|||
| Rotation | None | Yes (automatic) | |
|||
| Audit / Traceability | None | Yes (CloudTrail) | |
|||
| Typical Usage | Quick dev outside repo | Production secret management | |
|||
|
|||
--- |
|||
|
|||
## AWS Secrets Manager |
|||
Especially designed to securely store and handle sensitive and confidential data for our applications. It even supports features such as secret rotation, replication, and many more. |
|||
|
|||
AWS Secrets Manager offers a trial of 30 days. After that, there is a $0.40 USD/month charge per stored secret. There is also a $0.05 USD fee per 10,000 API requests. |
|||
|
|||
### Key Features: |
|||
- **Automatic encryption**: KMS automatic encryption |
|||
- **Automatic rotation**: Scheduled secret rotation |
|||
- **Fine-grained access control**: IAM fine-grained access control |
|||
- **Audit logging**: Full audit logging with CloudTrail |
|||
- **Cross-region replication**: Cross-region replication |
|||
- **API integration**: Programmatic access support |
|||
|
|||
--- |
|||
|
|||
## Step 1: AWS Secrets Manager Setup |
|||
|
|||
### 1.1 Creating a Secret in AWS Console |
|||
First, search for the Secrets Manager service in the AWS Management Console. |
|||
|
|||
1. **AWS Console** → **Secrets Manager** → **Store a new secret** |
|||
2. Select **Secret type**: |
|||
- **Other type of secret** (For custom key-value pairs) |
|||
- **Credentials for RDS database** (For databases) |
|||
- **Credentials for DocumentDB database** |
|||
- **Credentials for Redshift cluster** |
|||
|
|||
|
|||
|
|||
3. Enter **Secret value**: |
|||
```json |
|||
{ |
|||
"ConnectionString": "Server=myserver;Database=mydb;User Id=myuser;Password=mypassword;" |
|||
} |
|||
``` |
|||
|
|||
4. Set **Secret name**: `prod/ABPAWSTest/ConnectionString` |
|||
5. Add **Description**: "ABP Framework connection string for production" |
|||
6. Choose **Encryption key** (default KMS key is sufficient) |
|||
7. Configure **Automatic rotation** settings (optional) |
|||
|
|||
### 1.2 IAM Permissions |
|||
Create an IAM policy for secret access: |
|||
|
|||
```json |
|||
{ |
|||
"Version": "2012-10-17", |
|||
"Statement": [ |
|||
{ |
|||
"Effect": "Allow", |
|||
"Action": [ |
|||
"secretsmanager:GetSecretValue", |
|||
"secretsmanager:DescribeSecret" |
|||
], |
|||
"Resource": "arn:aws:secretsmanager:eu-north-1:588118819172:secret:prod/ABPAWSTest/ConnectionString-*" |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Step 2: ABP Framework Project Setup |
|||
|
|||
### 2.1 NuGet Packages |
|||
Add the required AWS packages to your project: |
|||
|
|||
```bash |
|||
dotnet add package AWSSDK.SecretsManager |
|||
dotnet add package AWSSDK.Extensions.NETCore.Setup |
|||
``` |
|||
|
|||
### 2.2 Configuration Files |
|||
|
|||
**appsettings.json** (Development): |
|||
```json |
|||
{ |
|||
"AWS": { |
|||
"Profile": "default", |
|||
"Region": "eu-north-1", |
|||
"AccessKey": "YOUR_ACCESS_KEY", |
|||
"SecretKey": "YOUR_SECRET_KEY" |
|||
}, |
|||
"SecretsManager": { |
|||
"SecretName": "prod/ABPAWSTest/ConnectionString", |
|||
"SecretArn": "arn:aws:secretsmanager:eu-north-1:588118819172:secret:prod/ABPAWSTest/ConnectionString-xtYQxv" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
**appsettings.Production.json** (Production): |
|||
```json |
|||
{ |
|||
"AWS": { |
|||
"Region": "eu-north-1" |
|||
// Use environment variables or IAM roles in production |
|||
}, |
|||
"SecretsManager": { |
|||
"SecretName": "prod/ABPAWSTest/ConnectionString" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 2.3 Environment Variables (Production) |
|||
```bash |
|||
export AWS_ACCESS_KEY_ID=your_access_key |
|||
export AWS_SECRET_ACCESS_KEY=your_secret_key |
|||
export AWS_DEFAULT_REGION=eu-north-1 |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Step 3: AWS Integration Implementation |
|||
|
|||
### 3.1 Program.cs Configuration |
|||
|
|||
```csharp |
|||
using Amazon; |
|||
using Amazon.SecretsManager; |
|||
|
|||
public class Program |
|||
{ |
|||
public async static Task<int> Main(string[] args) |
|||
{ |
|||
var builder = WebApplication.CreateBuilder(args); |
|||
|
|||
// AWS Secrets Manager configuration |
|||
var awsOptions = builder.Configuration.GetAWSOptions(); |
|||
|
|||
// Read AWS credentials from appsettings |
|||
var accessKey = builder.Configuration["AWS:AccessKey"]; |
|||
var secretKey = builder.Configuration["AWS:SecretKey"]; |
|||
var region = builder.Configuration["AWS:Region"]; |
|||
|
|||
if (!string.IsNullOrEmpty(accessKey) && !string.IsNullOrEmpty(secretKey)) |
|||
{ |
|||
awsOptions.Credentials = new Amazon.Runtime.BasicAWSCredentials(accessKey, secretKey); |
|||
} |
|||
|
|||
if (!string.IsNullOrEmpty(region)) |
|||
{ |
|||
awsOptions.Region = RegionEndpoint.GetBySystemName(region); |
|||
} |
|||
|
|||
builder.Services.AddDefaultAWSOptions(awsOptions); |
|||
builder.Services.AddAWSService<IAmazonSecretsManager>(); |
|||
|
|||
// ... ABP configuration |
|||
await builder.AddApplicationAsync<YourAppModule>(); |
|||
var app = builder.Build(); |
|||
|
|||
await app.InitializeApplicationAsync(); |
|||
await app.RunAsync(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 3.2 Secrets Manager Service |
|||
|
|||
**Interface:** |
|||
```csharp |
|||
public interface ISecretsManagerService |
|||
{ |
|||
Task<string> GetSecretAsync(string secretName); |
|||
Task<T> GetSecretAsync<T>(string secretName) where T : class; |
|||
Task<string> GetConnectionStringAsync(); |
|||
} |
|||
``` |
|||
|
|||
**Implementation:** |
|||
```csharp |
|||
using Amazon.SecretsManager; |
|||
using Amazon.SecretsManager.Model; |
|||
using Volo.Abp.DependencyInjection; |
|||
using System.Text.Json; |
|||
|
|||
public class SecretsManagerService : ISecretsManagerService, IScopedDependency |
|||
{ |
|||
private readonly IAmazonSecretsManager _secretsManager; |
|||
private readonly IConfiguration _configuration; |
|||
private readonly ILogger<SecretsManagerService> _logger; |
|||
|
|||
public SecretsManagerService( |
|||
IAmazonSecretsManager secretsManager, |
|||
IConfiguration configuration, |
|||
ILogger<SecretsManagerService> logger) |
|||
{ |
|||
_secretsManager = secretsManager; |
|||
_configuration = configuration; |
|||
_logger = logger; |
|||
} |
|||
|
|||
public async Task<string> GetSecretAsync(string secretName) |
|||
{ |
|||
try |
|||
{ |
|||
var request = new GetSecretValueRequest |
|||
{ |
|||
SecretId = secretName |
|||
}; |
|||
|
|||
var response = await _secretsManager.GetSecretValueAsync(request); |
|||
|
|||
_logger.LogInformation("Successfully retrieved secret: {SecretName}", secretName); |
|||
|
|||
return response.SecretString; |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "Failed to retrieve secret: {SecretName}", secretName); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
public async Task<T> GetSecretAsync<T>(string secretName) where T : class |
|||
{ |
|||
var secretValue = await GetSecretAsync(secretName); |
|||
|
|||
try |
|||
{ |
|||
return JsonSerializer.Deserialize<T>(secretValue) |
|||
?? throw new InvalidOperationException($"Failed to deserialize secret {secretName}"); |
|||
} |
|||
catch (JsonException ex) |
|||
{ |
|||
_logger.LogError(ex, "Failed to deserialize secret {SecretName}", secretName); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
public async Task<string> GetConnectionStringAsync() |
|||
{ |
|||
var secretName = _configuration["SecretsManager:SecretName"] |
|||
?? throw new InvalidOperationException("SecretsManager:SecretName configuration is missing"); |
|||
|
|||
return await GetSecretAsync(secretName); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Step 4: Usage Examples |
|||
|
|||
### 4.1 Using in Application Service |
|||
|
|||
```csharp |
|||
[RemoteService(false)] |
|||
public class DatabaseService : ApplicationService |
|||
{ |
|||
private readonly ISecretsManagerService _secretsManager; |
|||
|
|||
public DatabaseService(ISecretsManagerService secretsManager) |
|||
{ |
|||
_secretsManager = secretsManager; |
|||
} |
|||
|
|||
public async Task<string> GetDatabaseConnectionAsync() |
|||
{ |
|||
// Get connection string from AWS Secrets Manager |
|||
var connectionString = await _secretsManager.GetConnectionStringAsync(); |
|||
|
|||
// Use the connection string |
|||
return connectionString; |
|||
} |
|||
|
|||
public async Task<ApiConfiguration> GetApiConfigAsync() |
|||
{ |
|||
// Deserialize JSON secret |
|||
var config = await _secretsManager.GetSecretAsync<ApiConfiguration>("prod/MyApp/ApiConfig"); |
|||
|
|||
return config; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 4.2 DbContext Configuration |
|||
|
|||
```csharp |
|||
public class YourDbContextConfigurer |
|||
{ |
|||
public static void Configure(DbContextOptionsBuilder<YourDbContext> builder, string connectionString) |
|||
{ |
|||
builder.UseSqlServer(connectionString); |
|||
} |
|||
|
|||
public static void Configure(DbContextOptionsBuilder<YourDbContext> builder, DbConnection connection) |
|||
{ |
|||
builder.UseSqlServer(connection); |
|||
} |
|||
} |
|||
|
|||
// Usage in Module |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
var configuration = context.Services.GetConfiguration(); |
|||
var secretsManager = context.Services.GetRequiredService<ISecretsManagerService>(); |
|||
|
|||
// Get secret at startup and pass to DbContext |
|||
var connectionString = await secretsManager.GetConnectionStringAsync(); |
|||
|
|||
context.Services.AddAbpDbContext<YourDbContext>(options => |
|||
{ |
|||
options.AddDefaultRepositories(includeAllEntities: true); |
|||
options.DbContextOptions.UseSqlServer(connectionString); |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Step 5: Best Practices & Security |
|||
|
|||
### 5.1 Security Best Practices |
|||
|
|||
1. **Environment-based Configuration:** |
|||
- Development: appsettings.json |
|||
- Production: Environment variables or IAM roles |
|||
|
|||
2. **Principle of Least Privilege:** |
|||
```json |
|||
{ |
|||
"Effect": "Allow", |
|||
"Action": "secretsmanager:GetSecretValue", |
|||
"Resource": "arn:aws:secretsmanager:region:account:secret:specific-secret-*" |
|||
} |
|||
``` |
|||
|
|||
3. **Secret Rotation:** |
|||
- Set up automatic rotation |
|||
- Custom rotation logic with Lambda functions |
|||
|
|||
4. **Caching Strategy:** |
|||
```csharp |
|||
public class CachedSecretsManagerService : ISecretsManagerService |
|||
{ |
|||
private readonly IMemoryCache _cache; |
|||
private readonly SecretsManagerService _secretsManager; |
|||
|
|||
public async Task<string> GetSecretAsync(string secretName) |
|||
{ |
|||
var cacheKey = $"secret:{secretName}"; |
|||
|
|||
if (_cache.TryGetValue(cacheKey, out string cachedValue)) |
|||
{ |
|||
return cachedValue; |
|||
} |
|||
|
|||
var value = await _secretsManager.GetSecretAsync(secretName); |
|||
|
|||
_cache.Set(cacheKey, value, TimeSpan.FromMinutes(30)); |
|||
|
|||
return value; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 5.2 Error Handling |
|||
|
|||
```csharp |
|||
public async Task<string> GetSecretWithRetryAsync(string secretName) |
|||
{ |
|||
const int maxRetries = 3; |
|||
var delay = TimeSpan.FromSeconds(1); |
|||
|
|||
for (int i = 0; i < maxRetries; i++) |
|||
{ |
|||
try |
|||
{ |
|||
return await GetSecretAsync(secretName); |
|||
} |
|||
catch (AmazonSecretsManagerException ex) when (i < maxRetries - 1) |
|||
{ |
|||
_logger.LogWarning(ex, "Retry {Attempt} for secret {SecretName}", i + 1, secretName); |
|||
await Task.Delay(delay); |
|||
delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2); // Exponential backoff |
|||
} |
|||
} |
|||
|
|||
throw new InvalidOperationException($"Failed to retrieve secret {secretName} after {maxRetries} attempts"); |
|||
} |
|||
``` |
|||
|
|||
### 5.3 Performance Optimization |
|||
|
|||
```csharp |
|||
public class PerformantSecretsManagerService : ISecretsManagerService |
|||
{ |
|||
private readonly IAmazonSecretsManager _secretsManager; |
|||
private readonly IMemoryCache _cache; |
|||
private readonly ILogger<PerformantSecretsManagerService> _logger; |
|||
private readonly SemaphoreSlim _semaphore = new(1, 1); |
|||
|
|||
public async Task<string> GetSecretAsync(string secretName) |
|||
{ |
|||
var cacheKey = $"secret:{secretName}"; |
|||
|
|||
// Try to get from cache first |
|||
if (_cache.TryGetValue(cacheKey, out string cachedValue)) |
|||
{ |
|||
return cachedValue; |
|||
} |
|||
|
|||
// Use semaphore to prevent multiple concurrent requests for the same secret |
|||
await _semaphore.WaitAsync(); |
|||
try |
|||
{ |
|||
// Double-check pattern |
|||
if (_cache.TryGetValue(cacheKey, out cachedValue)) |
|||
{ |
|||
return cachedValue; |
|||
} |
|||
|
|||
// Fetch from AWS |
|||
var value = await GetSecretFromAwsAsync(secretName); |
|||
|
|||
// Cache for 30 minutes |
|||
_cache.Set(cacheKey, value, TimeSpan.FromMinutes(30)); |
|||
|
|||
return value; |
|||
} |
|||
finally |
|||
{ |
|||
_semaphore.Release(); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Step 6: Testing & Debugging |
|||
|
|||
### 6.1 Unit Testing |
|||
|
|||
```csharp |
|||
public class SecretsManagerServiceTests : AbpIntegratedTest<TestModule> |
|||
{ |
|||
private readonly ISecretsManagerService _secretsManager; |
|||
|
|||
public SecretsManagerServiceTests() |
|||
{ |
|||
_secretsManager = GetRequiredService<ISecretsManagerService>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Get_Connection_String() |
|||
{ |
|||
// Act |
|||
var connectionString = await _secretsManager.GetConnectionStringAsync(); |
|||
|
|||
// Assert |
|||
connectionString.ShouldNotBeNullOrEmpty(); |
|||
connectionString.ShouldContain("Server="); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Deserialize_Json_Secret() |
|||
{ |
|||
// Arrange |
|||
var secretName = "test/json/config"; |
|||
|
|||
// Act |
|||
var config = await _secretsManager.GetSecretAsync<TestConfig>(secretName); |
|||
|
|||
// Assert |
|||
config.ShouldNotBeNull(); |
|||
config.ApiKey.ShouldNotBeNullOrEmpty(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 6.2 Mock Implementation for Testing |
|||
|
|||
```csharp |
|||
public class MockSecretsManagerService : ISecretsManagerService, ISingletonDependency |
|||
{ |
|||
private readonly Dictionary<string, string> _secrets = new() |
|||
{ |
|||
["prod/ABPAWSTest/ConnectionString"] = "Server=localhost;Database=TestDb;Trusted_Connection=true;", |
|||
["prod/MyApp/ApiKey"] = "test-api-key", |
|||
["prod/MyApp/Config"] = """{"ApiUrl": "https://api.test.com", "Timeout": 30}""" |
|||
}; |
|||
|
|||
public Task<string> GetSecretAsync(string secretName) |
|||
{ |
|||
if (_secrets.TryGetValue(secretName, out var secret)) |
|||
{ |
|||
return Task.FromResult(secret); |
|||
} |
|||
|
|||
throw new ArgumentException($"Unknown secret: {secretName}"); |
|||
} |
|||
|
|||
public async Task<T> GetSecretAsync<T>(string secretName) where T : class |
|||
{ |
|||
var json = await GetSecretAsync(secretName); |
|||
return JsonSerializer.Deserialize<T>(json) |
|||
?? throw new InvalidOperationException($"Failed to deserialize {secretName}"); |
|||
} |
|||
|
|||
public Task<string> GetConnectionStringAsync() |
|||
{ |
|||
return GetSecretAsync("prod/ABPAWSTest/ConnectionString"); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 6.3 Integration Testing |
|||
|
|||
```csharp |
|||
public class SecretsManagerIntegrationTests : IClassFixture<WebApplicationFactory<Program>> |
|||
{ |
|||
private readonly WebApplicationFactory<Program> _factory; |
|||
private readonly HttpClient _client; |
|||
|
|||
public SecretsManagerIntegrationTests(WebApplicationFactory<Program> factory) |
|||
{ |
|||
_factory = factory; |
|||
_client = _factory.CreateClient(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Connect_To_Database_With_Secret() |
|||
{ |
|||
// Arrange & Act |
|||
var response = await _client.GetAsync("/api/health"); |
|||
|
|||
// Assert |
|||
response.EnsureSuccessStatusCode(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Step 7: Monitoring & Observability |
|||
|
|||
### 7.1 CloudWatch Metrics |
|||
|
|||
```csharp |
|||
public class MonitoredSecretsManagerService : ISecretsManagerService |
|||
{ |
|||
private readonly ISecretsManagerService _inner; |
|||
private readonly IMetrics _metrics; |
|||
private readonly ILogger<MonitoredSecretsManagerService> _logger; |
|||
|
|||
public async Task<string> GetSecretAsync(string secretName) |
|||
{ |
|||
using var activity = Activity.StartActivity("SecretsManager.GetSecret"); |
|||
activity?.SetTag("secret.name", secretName); |
|||
|
|||
var stopwatch = Stopwatch.StartNew(); |
|||
|
|||
try |
|||
{ |
|||
var result = await _inner.GetSecretAsync(secretName); |
|||
|
|||
_metrics.Counter("secrets_manager.requests") |
|||
.WithTag("secret_name", secretName) |
|||
.WithTag("status", "success") |
|||
.Increment(); |
|||
|
|||
_metrics.Timer("secrets_manager.duration") |
|||
.WithTag("secret_name", secretName) |
|||
.Record(stopwatch.ElapsedMilliseconds); |
|||
|
|||
return result; |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_metrics.Counter("secrets_manager.requests") |
|||
.WithTag("secret_name", secretName) |
|||
.WithTag("status", "error") |
|||
.WithTag("error_type", ex.GetType().Name) |
|||
.Increment(); |
|||
|
|||
_logger.LogError(ex, "Failed to retrieve secret {SecretName}", secretName); |
|||
throw; |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 7.2 Health Checks |
|||
|
|||
```csharp |
|||
public class SecretsManagerHealthCheck : IHealthCheck |
|||
{ |
|||
private readonly IAmazonSecretsManager _secretsManager; |
|||
private readonly ILogger<SecretsManagerHealthCheck> _logger; |
|||
|
|||
public SecretsManagerHealthCheck( |
|||
IAmazonSecretsManager secretsManager, |
|||
ILogger<SecretsManagerHealthCheck> logger) |
|||
{ |
|||
_secretsManager = secretsManager; |
|||
_logger = logger; |
|||
} |
|||
|
|||
public async Task<HealthCheckResult> CheckHealthAsync( |
|||
HealthCheckContext context, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
try |
|||
{ |
|||
// Try to list secrets to verify connection |
|||
var request = new ListSecretsRequest { MaxResults = 1 }; |
|||
await _secretsManager.ListSecretsAsync(request, cancellationToken); |
|||
|
|||
return HealthCheckResult.Healthy("AWS Secrets Manager is accessible"); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "AWS Secrets Manager health check failed"); |
|||
return HealthCheckResult.Unhealthy("AWS Secrets Manager is not accessible", ex); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Register in Program.cs |
|||
builder.Services.AddHealthChecks() |
|||
.AddCheck<SecretsManagerHealthCheck>("secrets-manager"); |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Step 8: Advanced Scenarios |
|||
|
|||
### 8.1 Dynamic Configuration Reload |
|||
|
|||
```csharp |
|||
public class DynamicSecretsConfigurationProvider : ConfigurationProvider, IDisposable |
|||
{ |
|||
private readonly ISecretsManagerService _secretsManager; |
|||
private readonly Timer _reloadTimer; |
|||
private readonly string _secretName; |
|||
|
|||
public DynamicSecretsConfigurationProvider( |
|||
ISecretsManagerService secretsManager, |
|||
string secretName) |
|||
{ |
|||
_secretsManager = secretsManager; |
|||
_secretName = secretName; |
|||
|
|||
// Reload every 5 minutes |
|||
_reloadTimer = new Timer(ReloadSecrets, null, TimeSpan.Zero, TimeSpan.FromMinutes(5)); |
|||
} |
|||
|
|||
private async void ReloadSecrets(object state) |
|||
{ |
|||
try |
|||
{ |
|||
var secretValue = await _secretsManager.GetSecretAsync(_secretName); |
|||
var config = JsonSerializer.Deserialize<Dictionary<string, string>>(secretValue); |
|||
|
|||
Data.Clear(); |
|||
foreach (var kvp in config) |
|||
{ |
|||
Data[kvp.Key] = kvp.Value; |
|||
} |
|||
|
|||
OnReload(); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
// Log error but don't throw to avoid crashing the timer |
|||
Console.WriteLine($"Failed to reload secrets: {ex.Message}"); |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_reloadTimer?.Dispose(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 8.2 Multi-Region Failover |
|||
|
|||
```csharp |
|||
public class MultiRegionSecretsManagerService : ISecretsManagerService |
|||
{ |
|||
private readonly List<IAmazonSecretsManager> _clients; |
|||
private readonly ILogger<MultiRegionSecretsManagerService> _logger; |
|||
|
|||
public MultiRegionSecretsManagerService( |
|||
IConfiguration configuration, |
|||
ILogger<MultiRegionSecretsManagerService> logger) |
|||
{ |
|||
_logger = logger; |
|||
_clients = new List<IAmazonSecretsManager>(); |
|||
|
|||
// Create clients for multiple regions |
|||
var regions = new[] { "us-east-1", "us-west-2", "eu-west-1" }; |
|||
foreach (var region in regions) |
|||
{ |
|||
var config = new AmazonSecretsManagerConfig |
|||
{ |
|||
RegionEndpoint = RegionEndpoint.GetBySystemName(region) |
|||
}; |
|||
_clients.Add(new AmazonSecretsManagerClient(config)); |
|||
} |
|||
} |
|||
|
|||
public async Task<string> GetSecretAsync(string secretName) |
|||
{ |
|||
Exception lastException = null; |
|||
|
|||
foreach (var client in _clients) |
|||
{ |
|||
try |
|||
{ |
|||
var request = new GetSecretValueRequest { SecretId = secretName }; |
|||
var response = await client.GetSecretValueAsync(request); |
|||
|
|||
_logger.LogInformation("Retrieved secret from region {Region}", |
|||
client.Config.RegionEndpoint.SystemName); |
|||
|
|||
return response.SecretString; |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
lastException = ex; |
|||
_logger.LogWarning(ex, "Failed to retrieve secret from region {Region}", |
|||
client.Config.RegionEndpoint.SystemName); |
|||
} |
|||
} |
|||
|
|||
throw new InvalidOperationException( |
|||
"Failed to retrieve secret from all regions", lastException); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Conclusion |
|||
|
|||
AWS Secrets Manager integration with ABP Framework significantly enhances the security of your applications. With this integration: |
|||
|
|||
**Centralized Secret Management**: All secrets are managed centrally |
|||
**Better Security**: Encryption through KMS and access control through IAM |
|||
**Audit Trail**: Complete recording of who accessed which secret when |
|||
**Automatic Rotation**: Secrets can be rotated automatically |
|||
**High Availability**: AWS high availability guarantee |
|||
**Easy Integration**: Native integration with ABP Framework |
|||
**Cost Effective**: Pay only for what you use |
|||
**Scalable**: Scales with your application needs |
|||
|
|||
With this post, you can securely utilize AWS Secrets Manager in your ABP Framework applications and bid farewell to secret management concerns in production. |
|||
|
|||
### Key Benefits: |
|||
- **Developer Productivity**: No hardcoded secrets in config files |
|||
- **Operational Excellence**: Automation of rotation and monitoring |
|||
- **Security Compliance**: Meet enterprise security requirements |
|||
- **Peace of Mind**: Professional-grade secret management |
|||
|
|||
--- |
|||
|
|||
## Additional Resources |
|||
|
|||
- [AWS Secrets Manager Documentation](https://docs.aws.amazon.com/secretsmanager/) |
|||
- [ABP Framework Documentation](https://docs.abp.io/) |
|||
- [AWS SDK for .NET](https://docs.aws.amazon.com/sdk-for-net/) |
|||
- [AWS Security Best Practices](https://aws.amazon.com/architecture/security-identity-compliance/) |
|||
- [Sample Project Repository](https://github.com/fahrigedik/AWSIntegrationABP) |
|||
|
After Width: | Height: | Size: 396 KiB |
@ -0,0 +1,316 @@ |
|||
|
|||
# Demystified Aggregates in DDD & .NET: From Theory to Practice |
|||
|
|||
## Introduction |
|||
|
|||
Domain-Driven Design (DDD) is one of the key foundations of modern software architecture and has taken a strong place in the .NET world. At the center of DDD are Aggregates, which protect the consistency of business rules. While they are one of DDD’s biggest strengths, they’re also one of the most commonly misunderstood ideas. Trying to follow “pure” DDD rules to the letter often clashes with the complexity and performance needs of real-world projects, leaving developers in tough situations. The goal of this article is to take a fresh, practical look at Aggregates and show how they can be applied in a way that works in real life. |
|||
|
|||
---------- |
|||
|
|||
### **Chapter 1: Laying the Groundwork: What Is a Classic Aggregate?** |
|||
|
|||
Before jumping into pragmatic shortcuts, let’s make sure we’re all on the same page. To do that, we’ll start with the classic “by the book” definition of an Aggregate and the rules that make it tick. |
|||
|
|||
#### **What Exactly Is an Aggregate?** |
|||
|
|||
At its simplest, an **Aggregate** is a group of related objects (Entities and Value Objects) that are treated as **one unit of change**. And this group has a leader: the **Aggregate Root**. |
|||
|
|||
- **Aggregate Root** → Think of it as the gatekeeper. All outside commands (like “add a product to the order”) must go through the root. You can’t just poke around and change stuff inside. |
|||
|
|||
- **Entity** → Objects within the Aggregate that have their own identity (ID). Example: an `OrderLine` inside an `Order`. |
|||
|
|||
- **Value Object** → Objects without an identity. They’re defined entirely by their values, like an `Address` or `Money`. |
|||
|
|||
|
|||
The Aggregate’s main purpose isn’t just grouping things together—it’s about **protecting business rules (invariants).** For example: _“an order’s total amount can never be negative.”_ The Aggregate Root makes sure rules like this are never broken. |
|||
|
|||
|
|||
#### **The Role of Aggregates: Transaction Boundaries** |
|||
|
|||
The most important job of an Aggregate is defining the **transactional consistency boundary**. In other words: |
|||
|
|||
👉 Any change you make inside an Aggregate either **fully succeeds** or **fully fails**. There’s no half-done state. |
|||
|
|||
From a database perspective, when you call `SaveChanges()` or `Commit()`, everything within one Aggregate gets saved in a single transaction. If you add a product and update the total price, those two actions are atomic—they succeed together. Thanks to Aggregates, you’ll never end up in weird states like _“product was added but total wasn’t updated.”_ |
|||
|
|||
|
|||
#### **The Golden Rules of Aggregates** |
|||
|
|||
Classic DDD lays out three golden rules for working with Aggregates: |
|||
|
|||
1. **Talk Only to the Root** |
|||
You can’t directly update something like an `OrderLine`. You must go through the root: `Order.AddOrderLine(...)` or `Order.RemoveOrderLine(...)`. That way, the root always enforces the rules. |
|||
|
|||
2. **Reference Other Aggregates by ID Only** |
|||
An `Order` shouldn’t hold a `Customer` object directly. Instead, it should just store `CustomerId`. This keeps Aggregates independent and avoids loading massive object graphs. |
|||
|
|||
3. **Change Only One Aggregate per Transaction** |
|||
Need to create an order _and_ update loyalty points? Classic DDD says: do it in two steps. First, save the `Order`. Then publish a **domain event** to update the `Customer`. This enables scalability but introduces **eventual consistency**. |
|||
|
|||
|
|||
|
|||
#### **A Classic Example: The Order Aggregate in .NET** |
|||
|
|||
Here’s a simple example showing an `Order` Aggregate that enforces a business rule: |
|||
|
|||
```csharp |
|||
// Aggregate Root: The entry point and rule enforcer |
|||
public class Order |
|||
{ |
|||
public Guid Id { get; private set; } |
|||
public Guid CustomerId { get; private set; } |
|||
|
|||
private readonly List<OrderLine> _orderLines = new(); |
|||
public IReadOnlyCollection<OrderLine> OrderLines => _orderLines.AsReadOnly(); |
|||
|
|||
public decimal TotalPrice { get; private set; } |
|||
|
|||
public Order(Guid id, Guid customerId) |
|||
{ |
|||
Id = id; |
|||
CustomerId = customerId; |
|||
} |
|||
|
|||
public void AddOrderLine(Guid productId, int quantity, decimal price) |
|||
{ |
|||
// Rule 1: Max 10 order lines |
|||
if (_orderLines.Count >= 10) |
|||
throw new InvalidOperationException("An order can contain at most 10 products."); |
|||
|
|||
// Rule 2: No duplicate products |
|||
var existingLine = _orderLines.FirstOrDefault(ol => ol.ProductId == productId); |
|||
if (existingLine != null) |
|||
throw new InvalidOperationException("This product is already in the order."); |
|||
|
|||
var orderLine = new OrderLine(productId, quantity, price); |
|||
_orderLines.Add(orderLine); |
|||
|
|||
RecalculateTotalPrice(); |
|||
} |
|||
|
|||
private void RecalculateTotalPrice() |
|||
{ |
|||
TotalPrice = _orderLines.Sum(ol => ol.TotalPrice); |
|||
} |
|||
} |
|||
|
|||
public class OrderLine |
|||
{ |
|||
public Guid Id { get; private set; } |
|||
public Guid ProductId { get; private set; } |
|||
public int Quantity { get; private set; } |
|||
public decimal UnitPrice { get; private set; } |
|||
public decimal TotalPrice => Quantity * UnitPrice; |
|||
|
|||
public OrderLine(Guid productId, int quantity, decimal unitPrice) |
|||
{ |
|||
Id = Guid.NewGuid(); |
|||
ProductId = productId; |
|||
Quantity = quantity; |
|||
UnitPrice = unitPrice; |
|||
} |
|||
} |
|||
|
|||
``` |
|||
|
|||
Here, the `Order` enforces the rule _“an order can have at most 10 items”_ inside its `AddOrderLine` method. Nobody outside the class can bypass this, because `_orderLines` is private. |
|||
|
|||
👉 That’s the real strength of a classic Aggregate: **business rules are always protected at the boundary.** |
|||
|
|||
---------- |
|||
|
|||
### **Chapter 2: Theory in Books vs. Reality in Code — Why Classic Aggregates Struggle** |
|||
|
|||
In Chapter 1, we painted the “ideal” world of DDD. Aggregates were like fortresses guarding our business rules… |
|||
But what happens when we try to build that fortress in a real project with tools like Entity Framework Core? That’s when the gap between theory and practice starts to show up. |
|||
|
|||
#### **1. That `.Include()` Chain — Do We Really Need It? The Performance Trap** |
|||
|
|||
DDD books tell us: _“To validate a business rule, you must load the entire aggregate into memory.”_ |
|||
Sounds reasonable if consistency is the goal. |
|||
|
|||
But let’s picture a scenario: we have an `Order` aggregate with **500 order lines** inside it. And all we want to do is change its status to `Confirmed`. |
|||
|
|||
```csharp |
|||
// Just to update a single field... |
|||
var order = await _context.Orders |
|||
.Include(o => o.OrderLines) // <-- 500 rows pulled in! |
|||
.SingleOrDefaultAsync(o => o.Id == orderId); |
|||
|
|||
order.Confirm(); // Just sets order.Status = "Confirmed"; |
|||
|
|||
await _context.SaveChangesAsync(); |
|||
|
|||
``` |
|||
|
|||
This query pulls **all 500 order lines into memory** just so we can flip a single `Status` field. Even in small projects, this is a silent performance killer. As the system grows, it will drag your app down. |
|||
|
|||
|
|||
#### **2. The Abandoned Fortress — Sliding into Anemic Domain Models** |
|||
|
|||
Now, what’s a developer’s natural reaction to this? Something like: |
|||
|
|||
_“Pulling this much data is expensive. Maybe I should strip down the aggregate into a plain POCO with properties only, and move the logic into an `OrderService` class.”_ |
|||
|
|||
This is how we slip straight into the **Anemic Domain Model** trap. Our classes lose their behavior, becoming nothing more than data bags. |
|||
The whole DDD principle of _“keep behavior close to data”_ evaporates. Business logic leaks out of the aggregate and spreads across services. We think we’re doing DDD, but in reality, we’ve fallen back into classic transaction-script style coding. |
|||
|
|||
|
|||
#### **3. One Model Doesn’t Fit All — The Clash of Command and Query** |
|||
|
|||
Aggregates are designed for **commands** — write operations where business rules must be enforced. |
|||
|
|||
But what about **queries**? Imagine a dashboard where we just want to list the last 10 orders. All we need is `OrderId`, `CustomerName`, and `TotalAmount`. |
|||
|
|||
Loading 10 fully-hydrated `Order` aggregates (with all their order lines) just for that list? That’s like using a cannon to hunt a sparrow. Wasteful, slow, and clumsy. |
|||
Aggregates simply aren’t built for reporting or read-heavy scenarios. |
|||
|
|||
|
|||
And there you have it — the three usual suspects that make developers doubt DDD in real life: |
|||
|
|||
- Performance headaches |
|||
|
|||
- The risk of falling into an Anemic Model |
|||
|
|||
- Aggregates being too heavy for read operations |
|||
|
|||
|
|||
So, should we give up on DDD? Absolutely not! |
|||
The key is to stop following the rules blindly and instead focus on their **real intent**. In the next chapter, we’ll explore the pragmatic approach — **Demystified Aggregates** — and how they can actually help us solve these problems. |
|||
|
|||
---------- |
|||
|
|||
### **Chapter 3: Enter the Solution — What Exactly Is a "Demystified Aggregate"?** |
|||
|
|||
The issues we listed in the last chapter don’t mean DDD is bad. They just show that blindly applying textbook rules without considering the realities of your project creates friction. |
|||
|
|||
A **Demystified Aggregate** isn’t a library or a framework. It’s a **way of thinking**. Its philosophy is simple: focus on the Aggregate’s real job, and make sure it does that job **as efficiently as possible.** |
|||
|
|||
|
|||
#### **1. Philosophy: Focus on Purpose, Not Rules** |
|||
|
|||
What’s the Aggregate’s most sacred duty? |
|||
**To protect business rules (invariants) during a data change (command).** |
|||
|
|||
Here’s the key: an Aggregate’s job isn’t to always hold all data in memory. Its job is to **ensure consistency while performing an operation**. |
|||
|
|||
Think of it like a security guard at a bank vault. Their job is to make sure transfers are done correctly. They don’t need to memorize the serial number of every single banknote. They just need the critical info for the current operation: the balance and the transfer amount. |
|||
|
|||
The Demystified Aggregate says the same thing: when running a method, you **only load the data that method actually needs**, not the entire Aggregate. |
|||
|
|||
|
|||
#### **2. The Core Idea: What “State” Does a Behavior Actually Need?** |
|||
|
|||
To apply this idea in code, ask yourself: |
|||
_“What data does the `Confirm()` method on my `Order` Aggregate actually need?”_ |
|||
|
|||
- Maybe just the order’s current `Status`. (`"Pending"` can become `"Confirmed"`, `"Cancelled"` throws an error.) |
|||
|
|||
- What about `AddItem(product, quantity)`? |
|||
|
|||
- It needs the `Status` (can’t add items to a cancelled order). |
|||
|
|||
- And maybe the existing `OrderLines` (to increase quantity if the item already exists). |
|||
|
|||
|
|||
See the pattern? Each behavior needs different data. So why load everything every single time? |
|||
|
|||
|
|||
#### **3. How Do We Do This in .NET & EF Core? Practical Solutions** |
|||
|
|||
Putting this philosophy into code is easier than you might think. |
|||
|
|||
**The Approach: Purpose-Built Repository Methods** |
|||
|
|||
Instead of a generic `GetByIdAsync()`, create methods tailored to the operation at hand. Let’s revisit our classic **Order Confirmation** scenario in a “Before & After” style. |
|||
|
|||
**BEFORE (Classic & Inefficient Approach)** |
|||
|
|||
```csharp |
|||
// Repository Layer |
|||
public async Task<Order> GetByIdAsync(Guid id) |
|||
{ |
|||
// LOAD EVERYTHING! |
|||
return await _context.Orders |
|||
.Include(o => o.OrderLines) |
|||
.SingleOrDefaultAsync(o => o.Id == id); |
|||
} |
|||
|
|||
// Application Service Layer |
|||
public async Task ConfirmOrderAsync(Guid orderId) |
|||
{ |
|||
var order = await _orderRepository.GetByIdAsync(orderId); |
|||
order.Confirm(); // This method might not even care about OrderLines! |
|||
await _unitOfWork.SaveChangesAsync(); |
|||
} |
|||
|
|||
``` |
|||
|
|||
**AFTER (Demystified & Focused Approach)** |
|||
|
|||
```csharp |
|||
// Repository Layer |
|||
public async Task<Order> GetForConfirmationAsync(Guid id) |
|||
{ |
|||
// LOAD ONLY WHAT WE NEED! (No OrderLines needed) |
|||
return await _context.Orders |
|||
.SingleOrDefaultAsync(o => o.Id == id); |
|||
} |
|||
|
|||
// Application Service Layer |
|||
public async Task ConfirmOrderAsync(Guid orderId) |
|||
{ |
|||
// Intent is crystal clear in the code! |
|||
var order = await _orderRepository.GetForConfirmationAsync(orderId); |
|||
|
|||
// Aggregate still protects the business rule. |
|||
// Confirm() checks status, etc. |
|||
order.Confirm(); |
|||
|
|||
await _unitOfWork.SaveChangesAsync(); |
|||
} |
|||
|
|||
``` |
|||
|
|||
**What Do We Gain?** |
|||
|
|||
1. **Awesome Performance:** We avoid unnecessary JOINs and data transfer. |
|||
|
|||
2. **Clear Intent:** Anyone reading `GetForConfirmationAsync` immediately knows this operation only cares about the order itself, not its items. Code documents itself. |
|||
|
|||
3. **No Compromise:** Our Aggregate still enforces the business rules via `Confirm()`. DDD’s spirit remains intact. |
|||
|
|||
|
|||
For **read/query operations**, the answer is even simpler: skip Aggregates altogether! Use optimized queries that return DTOs via `Select` projections, or even raw SQL with Dapper. |
|||
|
|||
That’s the essence of a Demystified Aggregate: **using the right tool for the right job.** |
|||
|
|||
In the next chapter, we’ll wrap everything up and tie all the concepts together. |
|||
|
|||
---------- |
|||
|
|||
### **Conclusion: Pragmatism Beats Dogmatism in DDD** |
|||
|
|||
We’ve reached the finish line. We started with the “pure” textbook definition of Aggregates in the ideal world of Domain-Driven Design. Then we hit the real-world walls of performance and complexity. Finally, we learned how to break through those walls. |
|||
|
|||
The biggest lesson from the **Demystified Aggregates** approach is simple: |
|||
|
|||
**DDD isn’t a rigid rulebook — it’s a way of thinking.** |
|||
|
|||
Our goal isn’t to implement the “most pure DDD ever written in a book.” It’s to make our domain logic clean, solid, understandable, and performant. In this journey, patterns and rules should serve us, not the other way around. |
|||
|
|||
|
|||
### **Key Takeaways** |
|||
|
|||
1. **Focus on the Core Purpose:** |
|||
The primary reason an Aggregate exists is to enforce business rules (invariants) and ensure consistency while handling a command. Every design decision should revolve around this purpose. |
|||
|
|||
2. **Load Only What You Need:** |
|||
You don’t have to load the entire Aggregate to execute a behavior. Use purpose-built repository methods (`GetForX()`) to fetch just the data needed for the operation. This can drastically improve both performance and readability. |
|||
|
|||
3. **Separate Writing from Reading:** |
|||
Use rich, protected Aggregates for commands (write operations). For queries (read operations), don’t burden your Aggregates. Instead, rely on projections, DTOs, or optimized queries. This is one of the simplest, most practical ways to embrace CQRS principles. |
|||
|
|||
Don’t be afraid to shape your Aggregates based on your project and the realities of your tools (like Entity Framework Core). The power of DDD lies in its **flexibility and pragmatism**. |
|||
|
|||
|
|||
|
After Width: | Height: | Size: 547 KiB |
|
After Width: | Height: | Size: 558 KiB |
|
After Width: | Height: | Size: 357 KiB |
@ -0,0 +1,138 @@ |
|||
# Color Psychology in Web Design: How to Choose the Perfect Palette in 2025 |
|||
|
|||
## The Importance of Color Psychology in Web Design |
|||
|
|||
First impressions online happen in under 50 milliseconds, and studies show that up to 90% of a user’s initial perception is based on color. In 2025, as digital competition intensifies, brands cannot rely on functionality alone—users expect emotionally engaging and visually strategic designs. |
|||
|
|||
Whether you’re designing for e-commerce, SaaS, lifestyle blogs, or creative portfolios, the right color palette is more than decoration. It directly influences: |
|||
|
|||
- Trust and credibility (blue = reliability, green = balance) |
|||
- Conversions and urgency (red for CTAs, orange for enthusiasm) |
|||
- Brand recall (consistent use of signature colors builds recognition) |
|||
- User comfort and inclusivity (high contrast, accessibility standards) |
|||
|
|||
 |
|||
|
|||
👉 In short, color psychology is not a design afterthought—it’s a core driver of user engagement, retention, and revenue growth. |
|||
|
|||
## The Psychology of Color: How Hues Shape Human Behavior |
|||
|
|||
Color psychology explains how different hues influence emotion, perception, and decision-making. Let’s explore the most common colors in web design: |
|||
|
|||
- 🔴 **Red** – Sparks urgency and excitement. Common in sales promotions, e-commerce banners, and notifications. |
|||
Example: YouTube’s red play button signals attention and action. |
|||
|
|||
- 🟠 **Orange** – Represents energy, warmth, and enthusiasm. |
|||
Example: Amazon’s orange “Add to Cart” button guides users toward purchases. |
|||
|
|||
- 🟡 **Yellow** – Radiates optimism, creativity, and energy. |
|||
Example: Snapchat’s bright yellow interface reflects playfulness and innovation. |
|||
|
|||
- 🟢 **Green** – Suggests growth, sustainability, and wellness. |
|||
Example: Whole Foods and health apps use green to symbolize balance and eco-conscious values. |
|||
|
|||
- 🔵 **Blue** – Signals trust, calm, and stability. |
|||
Example: PayPal and LinkedIn use blue to reinforce security and professionalism. |
|||
|
|||
- 🟣 **Purple** – Symbolizes luxury, creativity, and imagination. |
|||
Example: Beauty brands like Urban Decay use purple to highlight creativity and exclusivity. |
|||
|
|||
- ⚫ **Black** – Conveys sophistication and exclusivity. |
|||
Example: Apple and Chanel use black to emphasize premium value. |
|||
|
|||
- ⚪ **White** – Symbolizes purity, simplicity, and clarity. |
|||
Example: Apple’s predominantly white interfaces highlight elegance and minimalism. |
|||
|
|||
👉 Pro Insight: Combine psychology with analytics. Tools like Hotjar and Google Optimize reveal how color placement (e.g., green vs. red CTA buttons) impacts conversion rates. |
|||
|
|||
## Beyond Psychology: Cultural Differences in Color Meaning |
|||
|
|||
In global web design, one palette does not fit all. Colors have different cultural associations that can influence user perception: |
|||
|
|||
- **White** – Purity and minimalism in Western cultures, but associated with mourning in parts of East Asia. |
|||
- **Red** – Danger in the West, but luck and prosperity in China. |
|||
- **Green** – Islamically significant in the Middle East, while in the West it often symbolizes money. |
|||
- **Black** – Luxury in the West, but mourning in many cultures. |
|||
|
|||
 |
|||
|
|||
👉 For international brands, it’s crucial to localize website color palettes to avoid misinterpretations. |
|||
|
|||
## Web Design Color Trends for 2025 |
|||
|
|||
The digital landscape evolves rapidly, and so do color preferences. The biggest color palette trends in 2025 include: |
|||
|
|||
**Neo-Minimalist Neutrals** ✨ |
|||
Soft beiges, warm grays, and creamy whites offer calm, distraction-free interfaces—ideal for B2B websites and productivity apps. |
|||
|
|||
**Vibrant Gradients & Neon Accents** 🌈 |
|||
Tech and e-commerce brands embrace multi-tone gradients and neon highlights to reflect energy and innovation. |
|||
|
|||
**Dark Mode with High-Contrast Highlights** 🌑 |
|||
Now a default in many apps, dark mode improves eye comfort while allowing accent colors to stand out for easier navigation. |
|||
|
|||
**Nature-Inspired Palettes** 🍃 |
|||
Eco-conscious businesses are adopting earthy greens, ocean blues, and muted browns to connect with sustainability-minded users. |
|||
|
|||
**Soft Pastels for Inclusivity** 💕 |
|||
Gentle tones—lavenders, light pinks, and mints—are trending in lifestyle, wellness, and community-driven platforms thanks to their approachable feel. |
|||
|
|||
**Dynamic AI-Driven Palettes** 🤖 |
|||
AI enables adaptive color systems that change in real-time based on time of day, user mood, or demographics. |
|||
|
|||
👉 Case Study: Spotify uses dark backgrounds but personalizes accents with vibrant colors that align with playlists and campaigns. |
|||
|
|||
## Best Practices for Choosing the Perfect Website Color Palette |
|||
|
|||
A successful palette balances brand identity, psychology, accessibility, and testing. Follow these steps: |
|||
|
|||
**Anchor in Brand Identity** |
|||
Select a primary color that embodies your company’s values. |
|||
Example: Tiffany & Co.’s teal blue has become globally iconic. |
|||
**Apply the 60-30-10 Rule** |
|||
|
|||
- 60% background |
|||
- 30% secondary tone |
|||
- 10% accent (CTAs, highlights) |
|||
This ratio maintains visual balance. |
|||
|
|||
**Leverage Color Theory** |
|||
Use complementary or triadic harmonies for better contrast and consistency. |
|||
|
|||
**Prioritize Accessibility (WCAG Standards)** |
|||
Ensure a minimum 4.5:1 contrast ratio for text and use color-blind–friendly palettes. |
|||
|
|||
**Account for Cultural Meanings** |
|||
Research local associations before targeting global markets. |
|||
|
|||
**Test Across Devices & Modes** |
|||
Preview palettes in both light and dark modes, and across desktop, mobile, and tablet. |
|||
|
|||
**A/B Test for Conversions** |
|||
Experiment with button colors, background shades, and highlights to find what performs best. |
|||
|
|||
## The Role of Color in Branding and SEO |
|||
|
|||
Color impacts brand authority and indirectly influences SEO by shaping user behavior: |
|||
|
|||
- **Bounce Rate** – Engaging palettes keep users onsite longer. |
|||
- **Dwell Time** – Comfortable color schemes encourage scrolling. |
|||
- **Conversion Rates** – Optimized CTAs improve click-throughs. |
|||
- **Social Sharing** – Bold, appealing colors increase shareability |
|||
|
|||
 |
|||
|
|||
## Future Outlook: Where Color Psychology Is Headed |
|||
|
|||
- **Mood-Adaptive Websites** |
|||
Platforms will adjust palettes based on detected user mood or behavior. |
|||
- **AR & VR Color Experiences** |
|||
Immersive 3D design will require palettes that adapt across realities. |
|||
- **Inclusive Color Systems** |
|||
Accessibility-first palettes will become the standard, ensuring usability for all. |
|||
|
|||
## Final Thoughts |
|||
|
|||
In 2025, the perfect color palette blends psychological insights, cultural awareness, and data-driven testing. Colors are no longer just visual accents—they are storytelling tools that build trust, guide behavior, and boost conversions. |
|||
|
|||
👉 Brands that embrace color psychology in web design today will not only stand out visually but also gain lasting competitive and business advantages. |
|||
@ -0,0 +1,10 @@ |
|||
namespace Volo.Abp.EventBus.Distributed; |
|||
|
|||
public enum IncomingEventStatus |
|||
{ |
|||
Pending = 0, |
|||
|
|||
Discarded = 1, |
|||
|
|||
Processed = 2 |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
namespace Volo.Abp.EventBus.Distributed; |
|||
|
|||
public enum InboxProcessorFailurePolicy |
|||
{ |
|||
/// <summary>
|
|||
/// Default behavior, retry the following event in next period time.
|
|||
/// </summary>
|
|||
Retry, |
|||
|
|||
/// <summary>
|
|||
/// Skip the failed event and retry it after a delay.
|
|||
/// The delay doubles with each retry, starting from the configured InboxProcessorRetryBackoffFactor
|
|||
/// (e.g., 10, 20, 40, 80 seconds, etc.).
|
|||
/// The event is discarded if it still fails after reaching the maximum retry count.
|
|||
/// </summary>
|
|||
RetryLater, |
|||
|
|||
/// <summary>
|
|||
/// Skip the event and do not retry it.
|
|||
/// </summary>
|
|||
Discard, |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
import { |
|||
mergeApplicationConfig, |
|||
ApplicationConfig, |
|||
provideAppInitializer, |
|||
inject, |
|||
PLATFORM_ID, |
|||
TransferState |
|||
} from '@angular/core'; |
|||
import { isPlatformServer } from '@angular/common'; |
|||
import { provideServerRendering, withRoutes } from '@angular/ssr'; |
|||
|
|||
import { appConfig } from './app.config'; |
|||
import { appServerRoutes } from './app.routes.server'; |
|||
import { SSR_FLAG } from '@abp/ng.core'; |
|||
|
|||
const serverConfig: ApplicationConfig = { |
|||
providers: [ |
|||
provideAppInitializer(() => { |
|||
const platformId = inject(PLATFORM_ID); |
|||
const transferState = inject<TransferState>(TransferState); |
|||
if (isPlatformServer(platformId)) { |
|||
transferState.set(SSR_FLAG, true); |
|||
} |
|||
}), |
|||
provideServerRendering(withRoutes(appServerRoutes)), |
|||
], |
|||
}; |
|||
|
|||
export const config = mergeApplicationConfig(appConfig, serverConfig); |
|||
@ -0,0 +1,8 @@ |
|||
import { RenderMode, ServerRoute } from '@angular/ssr'; |
|||
|
|||
export const appServerRoutes: ServerRoute[] = [ |
|||
{ |
|||
path: '**', |
|||
renderMode: RenderMode.Server, |
|||
} |
|||
]; |
|||
@ -0,0 +1,7 @@ |
|||
import { bootstrapApplication } from '@angular/platform-browser'; |
|||
import { AppComponent } from './app/app.component'; |
|||
import { config } from './app/app.config.server'; |
|||
|
|||
const bootstrap = () => bootstrapApplication(AppComponent, config); |
|||
|
|||
export default bootstrap; |
|||
@ -0,0 +1,187 @@ |
|||
import { |
|||
AngularNodeAppEngine, |
|||
createNodeRequestHandler, |
|||
isMainModule, |
|||
writeResponseToNodeResponse, |
|||
} from '@angular/ssr/node'; |
|||
import express from 'express'; |
|||
import { dirname, resolve } from 'node:path'; |
|||
import { fileURLToPath } from 'node:url'; |
|||
import {environment} from './environments/environment'; |
|||
import * as oidc from 'openid-client'; |
|||
import { ServerCookieParser } from '@abp/ng.core'; |
|||
|
|||
if (environment.production === false) { |
|||
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0"; |
|||
} |
|||
|
|||
const serverDistFolder = dirname(fileURLToPath(import.meta.url)); |
|||
const browserDistFolder = resolve(serverDistFolder, '../browser'); |
|||
|
|||
const app = express(); |
|||
const angularApp = new AngularNodeAppEngine(); |
|||
|
|||
const ISSUER = new URL(environment.oAuthConfig.issuer); |
|||
const CLIENT_ID = environment.oAuthConfig.clientId; |
|||
const REDIRECT_URI = environment.oAuthConfig.redirectUri; |
|||
const SCOPE = environment.oAuthConfig.scope; |
|||
|
|||
const config = await oidc.discovery(ISSUER, CLIENT_ID, /* client_secret */ undefined); |
|||
const secureCookie = { httpOnly: true, sameSite: 'lax' as const, secure: environment.production, path: '/' }; |
|||
const tokenCookie = { ...secureCookie, httpOnly: false }; |
|||
|
|||
app.use(ServerCookieParser.middleware()); |
|||
|
|||
const sessions = new Map<string, { pkce?: string; state?: string; refresh?: string; at?: string, returnUrl?: string }>(); |
|||
|
|||
app.get('/authorize', async (_req, res) => { |
|||
const code_verifier = oidc.randomPKCECodeVerifier(); |
|||
const code_challenge = await oidc.calculatePKCECodeChallenge(code_verifier); |
|||
const state = oidc.randomState(); |
|||
|
|||
if (_req.query.returnUrl) { |
|||
const returnUrl = String(_req.query.returnUrl || null); |
|||
res.cookie('returnUrl', returnUrl, { ...secureCookie, maxAge: 5 * 60 * 1000 }); |
|||
} |
|||
|
|||
const sid = crypto.randomUUID(); |
|||
sessions.set(sid, { pkce: code_verifier, state }); |
|||
res.cookie('sid', sid, secureCookie); |
|||
|
|||
const url = oidc.buildAuthorizationUrl(config, { |
|||
redirect_uri: REDIRECT_URI, |
|||
scope: SCOPE, |
|||
code_challenge, |
|||
code_challenge_method: 'S256', |
|||
state, |
|||
}); |
|||
res.redirect(url.toString()); |
|||
}); |
|||
|
|||
app.get('/logout', async (req, res) => { |
|||
try { |
|||
const sid = req.cookies.sid; |
|||
|
|||
if (sid && sessions.has(sid)) { |
|||
sessions.delete(sid); |
|||
} |
|||
|
|||
res.clearCookie('sid', secureCookie); |
|||
res.clearCookie('access_token', tokenCookie); |
|||
res.clearCookie('refresh_token', secureCookie); |
|||
res.clearCookie('expires_at', tokenCookie); |
|||
res.clearCookie('returnUrl', secureCookie); |
|||
|
|||
const endSessionEndpoint = config.serverMetadata().end_session_endpoint; |
|||
if (endSessionEndpoint) { |
|||
const logoutUrl = new URL(endSessionEndpoint); |
|||
logoutUrl.searchParams.set('post_logout_redirect_uri', REDIRECT_URI); |
|||
logoutUrl.searchParams.set('client_id', CLIENT_ID); |
|||
|
|||
return res.redirect(logoutUrl.toString()); |
|||
} |
|||
res.redirect('/'); |
|||
|
|||
} catch (error) { |
|||
console.error('Logout error:', error); |
|||
res.status(500).send('Logout error'); |
|||
} |
|||
}); |
|||
|
|||
app.get('/', async (req, res, next) => { |
|||
try { |
|||
const { code, state } = req.query as any; |
|||
if (!code || !state) return next(); |
|||
|
|||
const sid = req.cookies.sid; |
|||
const sess = sid && sessions.get(sid); |
|||
if (!sess || state !== sess.state) return res.status(400).send('invalid state'); |
|||
|
|||
const tokenEndpoint = config.serverMetadata().token_endpoint!; |
|||
const body = new URLSearchParams({ |
|||
grant_type: 'authorization_code', |
|||
code: String(code), |
|||
redirect_uri: environment.oAuthConfig.redirectUri, |
|||
code_verifier: sess.pkce!, |
|||
client_id: CLIENT_ID |
|||
}); |
|||
|
|||
const resp = await fetch(tokenEndpoint, { |
|||
method: 'POST', |
|||
headers: { 'content-type': 'application/x-www-form-urlencoded' }, |
|||
body, |
|||
}); |
|||
|
|||
if (!resp.ok) { |
|||
const errTxt = await resp.text(); |
|||
console.error('token error:', resp.status, errTxt); |
|||
return res.status(500).send('token error'); |
|||
} |
|||
|
|||
const tokens = await resp.json(); |
|||
|
|||
const expiresInSec = |
|||
Number(tokens.expires_in ?? tokens.expiresIn ?? 3600); |
|||
const skewSec = 60; |
|||
const accessExpiresAt = new Date( |
|||
Date.now() + Math.max(0, expiresInSec - skewSec) * 1000 |
|||
); |
|||
|
|||
sessions.set(sid, { ...sess, at: tokens.access_token, refresh: tokens.refresh_token }); |
|||
res.cookie('access_token', tokens.access_token, {...tokenCookie, maxAge: accessExpiresAt.getTime()}); |
|||
res.cookie('refresh_token', tokens.refresh_token, secureCookie); |
|||
res.cookie('expires_at', String(accessExpiresAt.getTime()), tokenCookie); |
|||
|
|||
const returnUrl = req.cookies?.returnUrl ?? '/'; |
|||
res.clearCookie('returnUrl', secureCookie); |
|||
|
|||
return res.redirect(returnUrl); |
|||
} catch (e) { |
|||
console.error('OIDC error:', e); |
|||
return res.status(500).send('oidc error'); |
|||
} |
|||
}); |
|||
|
|||
/** |
|||
* Serve static files from /browser |
|||
*/ |
|||
app.use( |
|||
express.static(browserDistFolder, { |
|||
maxAge: '1y', |
|||
index: false, |
|||
redirect: false, |
|||
}), |
|||
); |
|||
|
|||
/** |
|||
* Handle all other requests by rendering the Angular application. |
|||
*/ |
|||
app.use((req, res, next) => { |
|||
angularApp |
|||
.handle(req) |
|||
.then(response => { |
|||
if (response) { |
|||
res.cookie('ssr-init', 'true', {...secureCookie, httpOnly: false}); |
|||
return writeResponseToNodeResponse(response, res); |
|||
} else { |
|||
return next() |
|||
} |
|||
}) |
|||
.catch(next); |
|||
}); |
|||
|
|||
/** |
|||
* Start the server if this module is the main entry point. |
|||
* The server listens on the port defined by the `PORT` environment variable, or defaults to 4000. |
|||
*/ |
|||
if (isMainModule(import.meta.url)) { |
|||
const port = process.env['PORT'] || 4200; |
|||
app.listen(port, () => { |
|||
console.log(`Node Express server listening on http://localhost:${port}`); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions. |
|||
*/ |
|||
export const reqHandler = createNodeRequestHandler(app); |
|||
@ -1,2 +1,3 @@ |
|||
export * from './api.interceptor'; |
|||
export * from './timezone.interceptor'; |
|||
export * from './transfer-state.interceptor'; |
|||
|
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue