@ -0,0 +1,257 @@ |
|||
# Integration Testing Best Practices for Building a Robust Application Layer |
|||
|
|||
## 1. Introduction to Integration Testing |
|||
|
|||
For software development purposes, it is not sufficient to validate individual components individually in a vacuum. In practical usage, programs consist of holistic systems comprising a myriad of components such as **databases, services, APIs, and existing tools**, which **must work together**. An application would fail testing parameters for an individual component sufficiently but fail as a whole if such components fail to interact appropriately. |
|||
|
|||
Whereas unit tests verify individual parts one by one, **integration tests confirm software reliability with integrated parts**. Integration testing guarantees that the overall system performs as per design parameters whenever parts of the system are interfederated. |
|||
|
|||
**Why Integration Testing Is Important:** |
|||
|
|||
* **Reliability:** Checks the system works as it should, even with network problems, service stops, or incorrect data. |
|||
* **Easy to Update:** Makes sure adding or changing parts doesn't break what already works. |
|||
* **Good Quality:** Finds problems before users do. |
|||
* **Strong Application Layer:** Checks that database actions, service handling, and API communications all work together correctly. |
|||
|
|||
**Example: Detailed Integration Test with Dependency Injection** |
|||
|
|||
```csharp |
|||
[Fact] |
|||
public async Task OrderCreation_WithValidInput_ShouldPersistAndReturnSuccess() |
|||
{ |
|||
// Set up: Use a complete Test Server |
|||
var factory = new WebApplicationFactory<Startup>(); |
|||
var client = factory.CreateClient(); |
|||
|
|||
// Do: |
|||
var request = new CreateOrderRequest { ProductId = 1, Quantity = 2 }; |
|||
var response = await client.PostAsJsonAsync("/api/orders", request); |
|||
|
|||
// Check |
|||
response.EnsureSuccessStatusCode(); // Stops if the code isn't successful |
|||
var order = await response.Content.ReadFromJsonAsync<Order>(); |
|||
Assert.NotNull(order); |
|||
Assert.Equal(2, order.Quantity); |
|||
} |
|||
``` |
|||
|
|||
This example uses a memory `TestServer` to act like the full HTTP process, from the controller to the database, for a more real test. |
|||
|
|||
## 2. Setting Up an Isolated Integration Test Environment |
|||
|
|||
Integration tests should happen in their own area to prevent: |
|||
|
|||
* **Slow tests** because of network issues or big data amounts |
|||
* **Unreliable results** from live data changes |
|||
* **Tests messing with each other** |
|||
|
|||
**Tips for Keeping Tests Separate:** |
|||
|
|||
* **Memory databases:** Fast and easy to reset for simple data tasks. |
|||
* **Container-based areas (Docker/TestContainers):** Copy real areas safely for complex setups (like PostgreSQL, Redis). |
|||
* **Database Actions:** Undo changes after each test to keep things separate and fast. |
|||
|
|||
**Example: Using Actions to Keep Things Separate** |
|||
|
|||
```csharp |
|||
[Fact] |
|||
public async Task OrderCreation_RollsBack_AfterTest() |
|||
{ |
|||
// Set up: Start an action that will be undone later |
|||
await using var transaction = await _dbContext.Database.BeginTransactionAsync(); |
|||
|
|||
var service = new OrderService(_dbContext); |
|||
var result = service.CreateOrder(1, 2); |
|||
Assert.True(result.IsSuccess); |
|||
|
|||
// Do & Check |
|||
var orderCount = await _dbContext.Orders.CountAsync(); |
|||
Assert.Equal(1, orderCount); |
|||
|
|||
// Clean up: Undo the action to reset all changes |
|||
await transaction.RollbackAsync(); |
|||
} |
|||
``` |
|||
|
|||
Using actions makes sure tests stay separate and can run at the same time safely. |
|||
|
|||
## 3. Seeding Test Data for Consistent Integration Results |
|||
|
|||
Start data gives a steady base for tests. Include complex links for real situations. |
|||
|
|||
```csharp |
|||
// Use a special way to get the data started |
|||
private static async Task SeedData(AppDbContext dbContext) |
|||
{ |
|||
await dbContext.Products.AddRangeAsync( |
|||
new Product { Id = 1, Name = "Coffee", Price = 5, Stock = 50 }, |
|||
new Product { Id = 2, Name = "Tea", Price = 3, Stock = 20 } |
|||
); |
|||
await dbContext.Users.AddAsync(new User { Id = 1, Name = "Alice", IsActive = true }); |
|||
await dbContext.Orders.AddAsync(new Order { Id = 1, UserId = 1, ProductId = 1, Quantity = 2 }); |
|||
|
|||
await dbContext.SaveChangesAsync(); |
|||
} |
|||
``` |
|||
|
|||
**Tip:** Use the Builder style or a special `TestSeeder` class to make detailed, reusable data setups. |
|||
|
|||
## 4. Validating API and Service Layer Interactions |
|||
|
|||
Integration tests should copy what real users do to check the whole request-response process. |
|||
|
|||
```csharp |
|||
// Detailed API Test with CancellationToken and Custom Headers |
|||
var request = new CreateOrderRequest { ProductId = 1, Quantity = 2 }; |
|||
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/api/orders") |
|||
{ |
|||
Content = JsonContent.Create(request) |
|||
}; |
|||
requestMessage.Headers.Add("X-Request-Id", Guid.NewGuid().ToString()); |
|||
|
|||
// Act like a timeout is happening |
|||
var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50)); |
|||
await Assert.ThrowsAsync<TaskCanceledException>(async () => |
|||
{ |
|||
await _client.SendAsync(requestMessage, cts.Token); |
|||
}); |
|||
``` |
|||
|
|||
Test both normal uses and unusual ones like timeouts, bad requests, and too many users at once for a truly reliable API. |
|||
|
|||
## 5. Mocking External Dependencies in Integration Tests |
|||
|
|||
Use copies or fake versions for services like payment systems, emails, or other APIs to keep your application logic separate. |
|||
|
|||
```csharp |
|||
// Set up: Copy an outside payment service |
|||
var paymentServiceMock = new Mock<IPaymentService>(); |
|||
paymentServiceMock.Setup(x => x.ProcessPayment(It.IsAny<decimal>(), It.IsAny<string>())) |
|||
.ThrowsAsync(new TimeoutException("Payment gateway timeout")); |
|||
|
|||
// Do: Put the copy into the test area |
|||
services.AddScoped(_ => paymentServiceMock.Object); |
|||
|
|||
// Check: See how the system handles the timeout |
|||
var response = await _client.PostAsJsonAsync("/api/payments", new { Amount = 100 }); |
|||
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); |
|||
Assert.Contains("Payment service unavailable", await response.Content.ReadAsStringAsync()); |
|||
``` |
|||
|
|||
Mimicking lets you act like things are failing (like APIs timing out or services being down) and check that your application handles these issues well without using real network calls. |
|||
|
|||
## 6. Covering Success and Failure Scenarios in Tests |
|||
|
|||
A good test set has full coverage of both successful actions and error handling. |
|||
|
|||
```csharp |
|||
// Normal use: A successful order creation |
|||
var validOrder = new CreateOrderRequest { ProductId = 1, Quantity = 1 }; |
|||
var successResponse = await _client.PostAsJsonAsync("/api/orders", validOrder); |
|||
Assert.Equal(HttpStatusCode.Created, successResponse.StatusCode); |
|||
|
|||
// Bad request: Wrong product ID |
|||
var invalidProductOrder = new CreateOrderRequest { ProductId = 999, Quantity = 1 }; |
|||
var badProductResponse = await _client.PostAsJsonAsync("/api/orders", invalidProductOrder); |
|||
Assert.Equal(HttpStatusCode.NotFound, badProductResponse.StatusCode); |
|||
|
|||
// Conflict: Not enough stock |
|||
var largeOrder = new CreateOrderRequest { ProductId = 1, Quantity = 1000 }; |
|||
var stockResponse = await _client.PostAsJsonAsync("/api/orders", largeOrder); |
|||
Assert.Equal(HttpStatusCode.Conflict, stockResponse.StatusCode); |
|||
``` |
|||
|
|||
Testing everything makes sure your application layer gives helpful and correct error messages to users. |
|||
|
|||
## 7. Ensuring Cleanup and Test Isolation |
|||
|
|||
Keep test results steady by making sure each test is totally separate from others. |
|||
|
|||
```csharp |
|||
// Use a special cleanup method or a test setup |
|||
public class OrderTests : IDisposable |
|||
{ |
|||
private readonly AppDbContext _dbContext; |
|||
|
|||
public OrderTests() |
|||
{ |
|||
_dbContext = new AppDbContext(GetDbContextOptions()); |
|||
// Setup logic here |
|||
} |
|||
|
|||
// Cleanup logic |
|||
public void Dispose() |
|||
{ |
|||
_dbContext.Database.EnsureDeleted(); |
|||
_dbContext.Dispose(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Clean up automatically for tests that run at the same time using `IDisposable` or by putting each test in a database action. |
|||
|
|||
## 8. Automating Integration Tests with CI/CD Pipelines |
|||
|
|||
Automate tests in CI/CD lines for regular, consistent checking. |
|||
|
|||
```yaml |
|||
jobs: |
|||
test-and-build: |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- uses: actions/checkout@v3 |
|||
- name: Setup .NET |
|||
uses: actions/setup-dotnet@v3 |
|||
with: |
|||
dotnet-version: 7.0.x |
|||
- name: Build |
|||
run: dotnet build --configuration Release |
|||
- name: Run Integration Tests |
|||
run: dotnet test --filter Category=Integration --logger trx;LogFileName=testresults.trx |
|||
``` |
|||
|
|||
Regular testing finds integration problems early, saving effort over time. |
|||
|
|||
## 9. Complete Integration Test Example with a Basic Comparison |
|||
|
|||
```csharp |
|||
[Fact] |
|||
public async Task CompleteOrderWorkflow_ShouldPersistAllSteps() |
|||
{ |
|||
// Set up: Start complex data for a full action |
|||
await SeedData(_dbContext); |
|||
|
|||
// Do: Act like a real user request |
|||
var request = new CreateOrderRequest { ProductId = 1, Quantity = 2, UserId = 1 }; |
|||
var response = await _client.PostAsJsonAsync("/api/orders", request); |
|||
Assert.Equal(HttpStatusCode.Created, response.StatusCode); |
|||
|
|||
// Check 1: See the database state |
|||
var order = await _dbContext.Orders.FirstOrDefaultAsync(o => o.UserId == 1 && o.ProductId == 1); |
|||
Assert.NotNull(order); |
|||
Assert.Equal(2, order.Quantity); |
|||
|
|||
// Check 2: See side effects (like stock going down) |
|||
var product = await _dbContext.Products.FirstOrDefaultAsync(p => p.Id == 1); |
|||
Assert.Equal(48, product.Stock); |
|||
} |
|||
``` |
|||
|
|||
🍪 **Cookie Comparison** |
|||
|
|||
* **Starting products** are like the dough you have ready. |
|||
* **The new order** is like the new stuff you mix in. |
|||
* **The oven (application)** mixes both, baking (business logic). |
|||
* **The cookie** is the right outcome, using the old dough and new stuff well. |
|||
|
|||
This shows how a good application layer uses new and old data well together. |
|||
|
|||
--- |
|||
|
|||
### 10. Summary: |
|||
|
|||
* **Separate Area:** Ensures tests run on their own. |
|||
* **Start Data:** Gives results that are consistent. |
|||
* **Mimicking Outside Parts:** Keeps tests stable. |
|||
* **Testing Both Good and Bad:** Makes the situation solid. |
|||
* **CI/CD Automation:** Keeps the system stable. |
|||
|
After Width: | Height: | Size: 471 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 236 KiB |
@ -0,0 +1,820 @@ |
|||
# Unit of Work Pattern with Generic Repository in ASP.NET Core |
|||
|
|||
Picture this: You're building an e-commerce system and a customer places an order. You need to create the order record, update inventory, charge payment, and send a confirmation email. What happens if the payment succeeds but the inventory update fails? You're left with inconsistent data and an angry customer. |
|||
|
|||
This is exactly where the Unit of Work pattern becomes invaluable. Instead of managing transactions manually across multiple repositories, this pattern coordinates all your data changes as a single, atomic operation. |
|||
|
|||
## Understanding the Unit of Work Pattern |
|||
|
|||
The Unit of Work pattern maintains a list of objects affected by a business transaction and coordinates writing out changes while resolving concurrency problems. Think of it as your transaction coordinator that ensures all-or-nothing operations. |
|||
|
|||
### The Problem: Scattered Transaction Management |
|||
|
|||
Without proper coordination, you often end up with code like this: |
|||
|
|||
```csharp |
|||
// Each repository manages its own context - risky! |
|||
await _productRepository.CreateAsync(product); |
|||
await _inventoryRepository.UpdateStockAsync(productId, -quantity); |
|||
await _orderRepository.CreateAsync(order); |
|||
``` |
|||
|
|||
This approach has several problems: |
|||
- Each operation might use a different database context |
|||
- No automatic rollback if one operation fails |
|||
- Manual transaction management becomes complex |
|||
- Data consistency isn't guaranteed |
|||
|
|||
### The Solution: Coordinated Operations |
|||
|
|||
With Unit of Work, the same scenario becomes much cleaner: |
|||
|
|||
```csharp |
|||
await _unitOfWork.BeginTransactionAsync(); |
|||
|
|||
try |
|||
{ |
|||
var productRepo = _unitOfWork.Repository<Product>(); |
|||
var inventoryRepo = _unitOfWork.Repository<Inventory>(); |
|||
var orderRepo = _unitOfWork.Repository<Order>(); |
|||
|
|||
await productRepo.AddAsync(product); |
|||
await inventoryRepo.UpdateStockAsync(productId, -quantity); |
|||
await orderRepo.AddAsync(order); |
|||
|
|||
await _unitOfWork.SaveChangesAsync(); |
|||
await _unitOfWork.CommitTransactionAsync(); |
|||
} |
|||
catch |
|||
{ |
|||
await _unitOfWork.RollbackTransactionAsync(); |
|||
throw; |
|||
} |
|||
``` |
|||
|
|||
Now all operations either succeed together or fail together, guaranteeing data consistency. |
|||
|
|||
## Sample Implementation |
|||
|
|||
> To keep the example short, I will only show the Product entity along with the implementation of the Generic Repository and Unit of Work. In this example, I will use Blazor and .NET 9. |
|||
|
|||
You can access the sample project here https://github.com/m-aliozkaya/UnitOfWorkDemo. |
|||
|
|||
### 1. Generic Repository Implementation |
|||
|
|||
The repository interface defines the contract for data operations. Here's what we're working with: |
|||
|
|||
_IRepository.cs_ at `~/Data/Repositories` |
|||
```csharp |
|||
using System.Linq.Expressions; |
|||
|
|||
namespace UOWDemo.Repositories; |
|||
|
|||
public interface IRepository<T> where T : class |
|||
{ |
|||
Task<T?> GetByIdAsync(int id); |
|||
Task<IEnumerable<T>> GetAllAsync(); |
|||
Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate); |
|||
Task<T?> SingleOrDefaultAsync(Expression<Func<T, bool>> predicate); |
|||
|
|||
Task AddAsync(T entity); |
|||
void Update(T entity); |
|||
void Remove(T entity); |
|||
void RemoveRange(IEnumerable<T> entities); |
|||
|
|||
Task<bool> ExistsAsync(int id); |
|||
Task<int> CountAsync(); |
|||
Task<int> CountAsync(Expression<Func<T, bool>> predicate); |
|||
} |
|||
``` |
|||
|
|||
_Repository.cs_ at `~/Data/Repositories` |
|||
```csharp |
|||
using Microsoft.EntityFrameworkCore; |
|||
using System.Linq.Expressions; |
|||
using UOWDemo.Data; |
|||
|
|||
namespace UOWDemo.Repositories; |
|||
|
|||
public class Repository<T> : IRepository<T> where T : class |
|||
{ |
|||
protected readonly ApplicationDbContext _context; |
|||
protected readonly DbSet<T> _dbSet; |
|||
|
|||
public Repository(ApplicationDbContext context) |
|||
{ |
|||
_context = context; |
|||
_dbSet = context.Set<T>(); |
|||
} |
|||
|
|||
public virtual async Task<T?> GetByIdAsync(int id) |
|||
{ |
|||
return await _dbSet.FindAsync(id); |
|||
} |
|||
|
|||
public virtual async Task<IEnumerable<T>> GetAllAsync() |
|||
{ |
|||
return await _dbSet.ToListAsync(); |
|||
} |
|||
|
|||
public virtual async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate) |
|||
{ |
|||
return await _dbSet.Where(predicate).ToListAsync(); |
|||
} |
|||
|
|||
public virtual async Task<T?> SingleOrDefaultAsync(Expression<Func<T, bool>> predicate) |
|||
{ |
|||
return await _dbSet.SingleOrDefaultAsync(predicate); |
|||
} |
|||
|
|||
public virtual async Task AddAsync(T entity) |
|||
{ |
|||
await _dbSet.AddAsync(entity); |
|||
} |
|||
|
|||
public virtual void Update(T entity) |
|||
{ |
|||
_dbSet.Update(entity); |
|||
} |
|||
|
|||
public virtual void Remove(T entity) |
|||
{ |
|||
_dbSet.Remove(entity); |
|||
} |
|||
|
|||
public virtual void RemoveRange(IEnumerable<T> entities) |
|||
{ |
|||
_dbSet.RemoveRange(entities); |
|||
} |
|||
|
|||
public virtual async Task<bool> ExistsAsync(int id) |
|||
{ |
|||
var entity = await _dbSet.FindAsync(id); |
|||
return entity != null; |
|||
} |
|||
|
|||
public virtual async Task<int> CountAsync() |
|||
{ |
|||
return await _dbSet.CountAsync(); |
|||
} |
|||
|
|||
public virtual async Task<int> CountAsync(Expression<Func<T, bool>> predicate) |
|||
{ |
|||
return await _dbSet.CountAsync(predicate); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 2. Unit of Work Implementation |
|||
|
|||
_IUnitOfWork.cs_ at `~/Data/UnitOfWork` |
|||
```csharp |
|||
public interface IUnitOfWork : IDisposable, IAsyncDisposable |
|||
{ |
|||
IRepository<T> Repository<T>() where T : class; |
|||
Task<int> SaveChangesAsync(); |
|||
Task BeginTransactionAsync(); |
|||
Task CommitTransactionAsync(); |
|||
Task RollbackTransactionAsync(); |
|||
} |
|||
``` |
|||
|
|||
_UnitOfWork.cs_ at `~/Data/UnitOfWork` |
|||
```csharp |
|||
public class UnitOfWork : IUnitOfWork |
|||
{ |
|||
private readonly ApplicationDbContext _context; |
|||
private bool _disposed; |
|||
private IDbContextTransaction? _currentTransaction; |
|||
|
|||
// ConcurrentDictionary for thread-safe repository caching |
|||
private readonly ConcurrentDictionary<Type, object> _repositories = new(); |
|||
|
|||
public UnitOfWork(ApplicationDbContext context) |
|||
{ |
|||
_context = context; |
|||
} |
|||
|
|||
public IRepository<T> Repository<T>() where T : class |
|||
{ |
|||
return (IRepository<T>)_repositories.GetOrAdd( |
|||
typeof(T), |
|||
_ => new Repository<T>(_context) |
|||
); |
|||
} |
|||
|
|||
public int SaveChanges() => context.SaveChanges(); |
|||
public Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) => |
|||
context.SaveChangesAsync(cancellationToken); |
|||
|
|||
public async Task BeginTransactionAsync() |
|||
{ |
|||
if (_currentTransaction != null) |
|||
return; |
|||
|
|||
_currentTransaction = await _context.Database.BeginTransactionAsync(); |
|||
} |
|||
|
|||
public async Task CommitTransactionAsync() |
|||
{ |
|||
if (_currentTransaction == null) |
|||
return; |
|||
|
|||
await _currentTransaction.CommitAsync(); |
|||
await _currentTransaction.DisposeAsync(); |
|||
_currentTransaction = null; |
|||
} |
|||
|
|||
public async Task RollbackTransactionAsync() |
|||
{ |
|||
if (_currentTransaction == null) |
|||
return; |
|||
|
|||
await _currentTransaction.RollbackAsync(); |
|||
await _currentTransaction.DisposeAsync(); |
|||
_currentTransaction = null; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (!_disposed) |
|||
{ |
|||
_context.Dispose(); |
|||
_currentTransaction?.Dispose(); |
|||
_disposed = true; |
|||
} |
|||
GC.SuppressFinalize(this); |
|||
} |
|||
|
|||
public async ValueTask DisposeAsync() |
|||
{ |
|||
if (!_disposed) |
|||
{ |
|||
await _context.DisposeAsync(); |
|||
if (_currentTransaction != null) |
|||
{ |
|||
await _currentTransaction.DisposeAsync(); |
|||
} |
|||
_disposed = true; |
|||
} |
|||
GC.SuppressFinalize(this); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 3. Configure Dependency Injection |
|||
|
|||
Register the services in your `Program.cs`: |
|||
|
|||
```csharp |
|||
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>(); |
|||
``` |
|||
|
|||
|
|||
### 4. Defining and Registering Entity to DbContext |
|||
|
|||
First, let's define a simple `Product` entity: |
|||
|
|||
_Product.cs_ at `~/Data/Entities` |
|||
```csharp |
|||
using System.ComponentModel.DataAnnotations; |
|||
|
|||
namespace UOWDemo.Models; |
|||
|
|||
public class Product |
|||
{ |
|||
public int Id { get; set; } |
|||
|
|||
[Required] |
|||
[MaxLength(100)] |
|||
public string Name { get; set; } = string.Empty; |
|||
|
|||
[MaxLength(500)] |
|||
public string? Description { get; set; } |
|||
|
|||
[Required] |
|||
[Range(0.01, double.MaxValue)] |
|||
public decimal Price { get; set; } |
|||
|
|||
[Required] |
|||
[Range(0, int.MaxValue)] |
|||
public int Stock { get; set; } |
|||
|
|||
public DateTime CreatedDate { get; set; } = DateTime.UtcNow; |
|||
} |
|||
``` |
|||
|
|||
* Go to your `DbContext` and implement following code. |
|||
|
|||
```csharp |
|||
public DbSet<Product> Products { get; set; } |
|||
|
|||
protected override void OnModelCreating(ModelBuilder modelBuilder) |
|||
{ |
|||
base.OnModelCreating(modelBuilder); |
|||
|
|||
modelBuilder.Entity<Product>(entity => |
|||
{ |
|||
entity.HasKey(e => e.Id); |
|||
entity.Property(e => e.Price).HasPrecision(18, 2); |
|||
entity.Property(e => e.CreatedDate).HasDefaultValueSql("GETUTCDATE()"); |
|||
}); |
|||
|
|||
modelBuilder.Entity<Product>().HasData( |
|||
new Product { Id = 1, Name = "Laptop", Description = "High-performance laptop", Price = 1299.99m, Stock = 15, CreatedDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc) }, |
|||
new Product { Id = 2, Name = "Mouse", Description = "Wireless gaming mouse", Price = 79.99m, Stock = 50, CreatedDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc) }, |
|||
new Product { Id = 3, Name = "Keyboard", Description = "Mechanical keyboard", Price = 149.99m, Stock = 25, CreatedDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc) } |
|||
); |
|||
} |
|||
``` |
|||
|
|||
### 5. Implement the UI |
|||
|
|||
_ProductService.cs_ at `~/Services` |
|||
```csharp |
|||
using System.Text.Json; |
|||
using UOWDemo.Models; |
|||
|
|||
namespace UOWDemo.Services; |
|||
|
|||
public class ProductService |
|||
{ |
|||
private readonly HttpClient _httpClient; |
|||
|
|||
public ProductService(HttpClient httpClient) |
|||
{ |
|||
_httpClient = httpClient; |
|||
} |
|||
|
|||
public async Task<List<Product>> GetAllProductsAsync() |
|||
{ |
|||
var response = await _httpClient.GetAsync("/api/products"); |
|||
response.EnsureSuccessStatusCode(); |
|||
var json = await response.Content.ReadAsStringAsync(); |
|||
return JsonSerializer.Deserialize<List<Product>>(json, new JsonSerializerOptions |
|||
{ |
|||
PropertyNameCaseInsensitive = true |
|||
}) ?? new List<Product>(); |
|||
} |
|||
|
|||
public async Task CreateTwoProductsWithUowAsync(Product product1, Product product2) |
|||
{ |
|||
var request = new TwoProductsRequest { Product1 = product1, Product2 = product2 }; |
|||
var json = JsonSerializer.Serialize(request); |
|||
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); |
|||
|
|||
var response = await _httpClient.PostAsync("/api/products/two-products-with-uow", content); |
|||
response.EnsureSuccessStatusCode(); |
|||
} |
|||
|
|||
public async Task CreateTwoProductsWithoutUowAsync(Product product1, Product product2) |
|||
{ |
|||
var request = new TwoProductsRequest { Product1 = product1, Product2 = product2 }; |
|||
var json = JsonSerializer.Serialize(request); |
|||
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); |
|||
|
|||
var response = await _httpClient.PostAsync("/api/products/two-products-without-uow", content); |
|||
response.EnsureSuccessStatusCode(); |
|||
} |
|||
|
|||
public async Task DeleteProductAsync(int id) |
|||
{ |
|||
var response = await _httpClient.DeleteAsync($"/api/products/{id}"); |
|||
response.EnsureSuccessStatusCode(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
_Products.razor_ at `~/Components/Pages` |
|||
```csharp |
|||
@page "/products" |
|||
@using UOWDemo.Models |
|||
@using UOWDemo.Services |
|||
@inject ProductService ProductService |
|||
@inject IJSRuntime JSRuntime |
|||
@rendermode InteractiveServer |
|||
|
|||
<PageTitle>Products</PageTitle> |
|||
|
|||
<div class="container"> |
|||
<div class="row"> |
|||
<div class="col-12"> |
|||
<h1>Product Management</h1> |
|||
<p>This page demonstrates Unit of Work transaction patterns with bulk operations.</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="row mb-3"> |
|||
<div class="col-12"> |
|||
<button class="btn btn-success me-2" @onclick="ShowTwoProductsForm"> |
|||
<i class="bi bi-plus-circle"></i> Add Two Products Demo |
|||
</button> |
|||
</div> |
|||
</div> |
|||
|
|||
@if (isLoading) |
|||
{ |
|||
<div class="text-center"> |
|||
<div class="spinner-border" role="status"> |
|||
<span class="visually-hidden">Loading...</span> |
|||
</div> |
|||
</div> |
|||
} |
|||
else |
|||
{ |
|||
<div class="row"> |
|||
<div class="col-12"> |
|||
<div class="table-responsive"> |
|||
<table class="table table-striped table-hover"> |
|||
<thead class="table-dark"> |
|||
<tr> |
|||
<th>ID</th> |
|||
<th>Name</th> |
|||
<th>Description</th> |
|||
<th>Price</th> |
|||
<th>Stock</th> |
|||
<th>Actions</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
@foreach (var product in products) |
|||
{ |
|||
<tr> |
|||
<td>@product.Id</td> |
|||
<td>@product.Name</td> |
|||
<td>@product.Description</td> |
|||
<td>$@product.Price.ToString("F2")</td> |
|||
<td>@product.Stock</td> |
|||
<td> |
|||
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteProduct(product.Id)"> |
|||
<i class="bi bi-trash"></i> Delete |
|||
</button> |
|||
</td> |
|||
</tr> |
|||
} |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
} |
|||
</div> |
|||
|
|||
|
|||
@if (showTwoProductsModal) |
|||
{ |
|||
<div class="modal fade show d-block" tabindex="-1" style="background-color: rgba(0,0,0,0.5);"> |
|||
<div class="modal-dialog modal-lg"> |
|||
<div class="modal-content"> |
|||
<div class="modal-header"> |
|||
<h5 class="modal-title">Two Products Demo - Unit of Work vs No Unit of Work</h5> |
|||
<button type="button" class="btn-close" @onclick="HideTwoProductsModal"></button> |
|||
</div> |
|||
<div class="modal-body"> |
|||
<div class="row"> |
|||
<div class="col-md-6"> |
|||
<div class="card"> |
|||
<div class="card-header"> |
|||
<h6 class="card-title">Product 1</h6> |
|||
</div> |
|||
<div class="card-body"> |
|||
<div class="mb-3"> |
|||
<label class="form-label">Name</label> |
|||
<input type="text" class="form-control" @bind="product1.Name" /> |
|||
</div> |
|||
<div class="mb-3"> |
|||
<label class="form-label">Description</label> |
|||
<textarea class="form-control" @bind="product1.Description"></textarea> |
|||
</div> |
|||
<div class="mb-3"> |
|||
<label class="form-label">Price</label> |
|||
<input type="number" step="0.01" class="form-control" @bind="product1.Price" /> |
|||
</div> |
|||
<div class="mb-3"> |
|||
<label class="form-label">Stock</label> |
|||
<input type="number" class="form-control" @bind="product1.Stock" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6"> |
|||
<div class="card"> |
|||
<div class="card-header"> |
|||
<h6 class="card-title">Product 2</h6> |
|||
</div> |
|||
<div class="card-body"> |
|||
<div class="mb-3"> |
|||
<label class="form-label">Name</label> |
|||
<input type="text" class="form-control" @bind="product2.Name" /> |
|||
</div> |
|||
<div class="mb-3"> |
|||
<label class="form-label">Description</label> |
|||
<textarea class="form-control" @bind="product2.Description"></textarea> |
|||
</div> |
|||
<div class="mb-3"> |
|||
<label class="form-label">Price</label> |
|||
<input type="number" step="0.01" class="form-control" @bind="product2.Price" /> |
|||
</div> |
|||
<div class="mb-3"> |
|||
<label class="form-label">Stock</label> |
|||
<input type="number" class="form-control" @bind="product2.Stock" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="alert alert-info mt-3"> |
|||
<strong>Demo Purpose:</strong> Check the console logs to see the difference in SQL transactions. |
|||
<ul> |
|||
<li><strong>With Unit of Work:</strong> Both products saved in a single transaction</li> |
|||
<li><strong>Without Unit of Work:</strong> Each product saved in separate transactions</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
<div class="modal-footer d-flex justify-content-between"> |
|||
<button type="button" class="btn btn-success flex-fill me-2" @onclick="SaveTwoProductsWithUow"> |
|||
Save with Unit of Work (Single Transaction) |
|||
</button> |
|||
<button type="button" class="btn btn-warning flex-fill ms-2" @onclick="SaveTwoProductsWithoutUow"> |
|||
Save without Unit of Work (Separate Transactions) |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
} |
|||
|
|||
@code { |
|||
private List<Product> products = new(); |
|||
private bool isLoading = true; |
|||
|
|||
private bool showTwoProductsModal = false; |
|||
private Product product1 = new(); |
|||
private Product product2 = new(); |
|||
|
|||
protected override async Task OnInitializedAsync() |
|||
{ |
|||
await LoadProducts(); |
|||
} |
|||
|
|||
private async Task LoadProducts() |
|||
{ |
|||
try |
|||
{ |
|||
isLoading = true; |
|||
products = await ProductService.GetAllProductsAsync(); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
await JSRuntime.InvokeVoidAsync("alert", $"Error loading products: {ex.Message}"); |
|||
} |
|||
finally |
|||
{ |
|||
isLoading = false; |
|||
} |
|||
} |
|||
|
|||
|
|||
private void ShowTwoProductsForm() |
|||
{ |
|||
product1 = new Product { Name = "Product 1", Description = "First product description", Price = 10.00m, Stock = 100 }; |
|||
product2 = new Product { Name = "Product 2", Description = "Second product description", Price = 20.00m, Stock = 50 }; |
|||
showTwoProductsModal = true; |
|||
} |
|||
|
|||
private void HideTwoProductsModal() |
|||
{ |
|||
showTwoProductsModal = false; |
|||
product1 = new Product(); |
|||
product2 = new Product(); |
|||
} |
|||
|
|||
private async Task SaveTwoProductsWithUow() |
|||
{ |
|||
try |
|||
{ |
|||
await ProductService.CreateTwoProductsWithUowAsync(product1, product2); |
|||
await JSRuntime.InvokeVoidAsync("alert", "Two products saved with Unit of Work! Check console logs to see single transaction."); |
|||
await LoadProducts(); |
|||
HideTwoProductsModal(); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
await JSRuntime.InvokeVoidAsync("alert", $"Error saving products with UoW: {ex.Message}"); |
|||
} |
|||
} |
|||
|
|||
private async Task SaveTwoProductsWithoutUow() |
|||
{ |
|||
try |
|||
{ |
|||
await ProductService.CreateTwoProductsWithoutUowAsync(product1, product2); |
|||
await JSRuntime.InvokeVoidAsync("alert", "Two products saved without Unit of Work! Check console logs to see separate transactions."); |
|||
await LoadProducts(); |
|||
HideTwoProductsModal(); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
await JSRuntime.InvokeVoidAsync("alert", $"Error saving products without UoW: {ex.Message}"); |
|||
} |
|||
} |
|||
|
|||
private async Task DeleteProduct(int id) |
|||
{ |
|||
if (await JSRuntime.InvokeAsync<bool>("confirm", "Are you sure you want to delete this product?")) |
|||
{ |
|||
try |
|||
{ |
|||
await ProductService.DeleteProductAsync(id); |
|||
await LoadProducts(); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
await JSRuntime.InvokeVoidAsync("alert", $"Error deleting product: {ex.Message}"); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
UI Preview |
|||
|
|||
 |
|||
|
|||
_ProductsController.cs_ at `~/Controllers` |
|||
```csharp |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using UOWDemo.Data; |
|||
using UOWDemo.Models; |
|||
using UOWDemo.UnitOfWork; |
|||
|
|||
namespace UOWDemo.Controllers; |
|||
|
|||
[ApiController] |
|||
[Route("api/[controller]")] |
|||
public class ProductsController : ControllerBase |
|||
{ |
|||
private readonly IUnitOfWork _unitOfWork; |
|||
private readonly ApplicationDbContext _context; |
|||
|
|||
public ProductsController(IUnitOfWork unitOfWork, ApplicationDbContext context) |
|||
{ |
|||
_unitOfWork = unitOfWork; |
|||
_context = context; |
|||
} |
|||
|
|||
[HttpGet] |
|||
public async Task<IActionResult> GetProducts() |
|||
{ |
|||
var repository = _unitOfWork.Repository<Product>(); |
|||
var products = await repository.GetAllAsync(); |
|||
return Ok(products); |
|||
} |
|||
|
|||
|
|||
[HttpPost("two-products-with-uow")] |
|||
public async Task<IActionResult> CreateTwoProductsWithUow(TwoProductsRequest request) |
|||
{ |
|||
if (!ModelState.IsValid) |
|||
return BadRequest(ModelState); |
|||
|
|||
await _unitOfWork.BeginTransactionAsync(); |
|||
|
|||
try |
|||
{ |
|||
var repository = _unitOfWork.Repository<Product>(); |
|||
await repository.AddAsync(request.Product1); |
|||
await repository.AddAsync(request.Product2); |
|||
await _unitOfWork.SaveChangesAsync(); |
|||
await _unitOfWork.CommitTransactionAsync(); |
|||
|
|||
return Ok(new { message = "Two products created with Unit of Work (single transaction)", |
|||
products = new[] { request.Product1, request.Product2 } }); |
|||
} |
|||
catch |
|||
{ |
|||
await _unitOfWork.RollbackTransactionAsync(); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
[HttpPost("two-products-without-uow")] |
|||
public async Task<IActionResult> CreateTwoProductsWithoutUow(TwoProductsRequest request) |
|||
{ |
|||
if (!ModelState.IsValid) |
|||
return BadRequest(ModelState); |
|||
|
|||
// First product - separate transaction |
|||
await _context.Products.AddAsync(request.Product1); |
|||
await _context.SaveChangesAsync(); |
|||
|
|||
// Second product - separate transaction |
|||
await _context.Products.AddAsync(request.Product2); |
|||
await _context.SaveChangesAsync(); |
|||
|
|||
return Ok(new { message = "Two products created without Unit of Work (separate transactions)", |
|||
products = new[] { request.Product1, request.Product2 } }); |
|||
} |
|||
|
|||
[HttpDelete("{id}")] |
|||
public async Task<IActionResult> DeleteProduct(int id) |
|||
{ |
|||
var repository = _unitOfWork.Repository<Product>(); |
|||
var product = await repository.GetByIdAsync(id); |
|||
|
|||
if (product == null) |
|||
return NotFound(); |
|||
|
|||
repository.Remove(product); |
|||
await _unitOfWork.SaveChangesAsync(); |
|||
|
|||
return NoContent(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
The code base should be shown like this. |
|||
|
|||
 |
|||
|
|||
### 6. Seeing the Difference: Transaction Logging |
|||
|
|||
To see the transaction behavior in action, configure Entity Framework logging in `appsettings.Development.json`: |
|||
|
|||
```json |
|||
{ |
|||
"Logging": { |
|||
"LogLevel": { |
|||
"Default": "Information", |
|||
"Microsoft.AspNetCore": "Warning", |
|||
"Microsoft.EntityFrameworkCore.Database.Command": "Information", |
|||
"Microsoft.EntityFrameworkCore.Database.Transaction": "Information" |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Now when you run the demo, the console will show: |
|||
|
|||
**With Unit of Work - One Transaction:** |
|||
``` |
|||
BEGIN TRANSACTION |
|||
INSERT INTO [Products] ([Name], [Description], [Price], [Stock]) VALUES ('Product 1', ...) |
|||
INSERT INTO [Products] ([Name], [Description], [Price], [Stock]) VALUES ('Product 2', ...) |
|||
COMMIT TRANSACTION |
|||
``` |
|||
|
|||
**Without Unit of Work - Two Separate Transactions:** |
|||
``` |
|||
BEGIN TRANSACTION |
|||
INSERT INTO [Products] ([Name], [Description], [Price], [Stock]) VALUES ('Product 1', ...) |
|||
COMMIT TRANSACTION |
|||
|
|||
BEGIN TRANSACTION |
|||
INSERT INTO [Products] ([Name], [Description], [Price], [Stock]) VALUES ('Product 2', ...) |
|||
COMMIT TRANSACTION |
|||
``` |
|||
|
|||
## What If? |
|||
|
|||
 |
|||
|
|||
What if we were using the ABP Framework instead of manually implementing the Unit of Work and Generic Repository? Well, most of the heavy lifting we did in this example would be handled automatically. ABP provides built-in support for Unit of Work, transactions, and repository patterns, allowing you to focus on business logic rather than plumbing code. |
|||
|
|||
### Key Advantages with ABP |
|||
|
|||
Automatic Transaction Management: Every application service method runs within a transaction by default. If an exception occurs, changes are automatically rolled back. |
|||
|
|||
* [UnitOfWork] Attribute: You can simply annotate a method with [UnitOfWork] to ensure all repository operations within it run in a single transaction. |
|||
|
|||
* Automatic SaveChanges: You don’t need to call SaveChanges() manually; ABP takes care of persisting changes at the end of a Unit of Work. |
|||
|
|||
* Configurable Transaction Options: Transaction isolation levels and timeouts can be easily configured, helping with performance and data consistency. |
|||
|
|||
* Event-Based Completion: After a successful transaction, related domain events can be triggered automatically—for example, sending a confirmation email when an entity is created. |
|||
|
|||
And many of them. If you interested in check this document. https://abp.io/docs/latest/framework/architecture/domain-driven-design/unit-of-work |
|||
|
|||
📌 Example: |
|||
|
|||
```csharp |
|||
[UnitOfWork] |
|||
public void CreatePerson(CreatePersonInput input) |
|||
{ |
|||
var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress }; |
|||
_personRepository.Insert(person); |
|||
_statisticsRepository.IncrementPeopleCount(); |
|||
} |
|||
``` |
|||
|
|||
In this example, both repository operations execute within the same transaction. ABP handles the commit automatically, so either all changes succeed or none are applied. |
|||
|
|||
> Takeaway: With ABP, developers don’t need to manually implement Unit of Work or manage transactions. This reduces boilerplate code, ensures consistency, and lets you focus on the domain logic. |
|||
|
|||
## Conclusion |
|||
|
|||
The Unit of Work pattern shines in scenarios where multiple operations must succeed or fail together. By centralizing transaction management and repository access, it reduces complexity and ensures consistency. |
|||
|
|||
This demo kept things simple with a single Product entity, but the same approach scales to more complex domains. Whether you’re building an e-commerce app, a financial system, or any data-heavy application, adopting Unit of Work with a Generic Repository can make your codebase cleaner, safer, and easier to maintain. |
|||
|
|||
Feel free to clone the sample project, experiment with it, and adapt the pattern to your own needs. 🚀 |
|||
@ -0,0 +1,608 @@ |
|||
# 10 Modern HTML & CSS Techniques Every Designer Should Know in 2025 |
|||
|
|||
HTML and CSS are developing with new features every year. In 2025, things are much more excited than before; because now only classic labels and simple styles are not enough. There are many modern techniques that strengthen the user experience, make the design more flexible and make our business serious easier. In this article, I have compiled 10 of each designer for you. |
|||
|
|||
Let's start ... |
|||
|
|||
## Modern HTML Techniques |
|||
|
|||
|
|||
### 1. `<details>` and `<summary>` tag |
|||
|
|||
This structure offers a structure where users can open and turn off according to their wishes. |
|||
If you need to examine in detail; |
|||
|
|||
```` |
|||
<details> |
|||
<p>Proin magna felis, vestibulum non felis quis, consequat commodo ligula. </p> |
|||
<p>Sed at purus magna. Sed auctor nisl velit.</p> |
|||
</details> |
|||
```` |
|||
This is the general use. If we do not specify a special title, the **Details** title comes by default. To change this, we should use `<summary>`. |
|||
|
|||
```` |
|||
<details> |
|||
<summary>Lorem Ipsum</summary> |
|||
<p>Proin magna felis, vestibulum non felis quis, consequat commodo ligula. </p> |
|||
<p>Sed at purus magna. Sed auctor nisl velit.</p> |
|||
</details> |
|||
```` |
|||
|
|||
#### Styling |
|||
|
|||
The `<summary>` element can be styled with CSS; its color, font, background, and other properties can be customized. |
|||
|
|||
Additionally, the default triangle marker of `<summary>` can also be styled using `::marker`. |
|||
The marker is a small sign indicating that the structure is open or closed. |
|||
We can use the `:: marker` pseudo-element to style it. But we should use it as `::marker` which belongs to the `<summary>`. |
|||
|
|||
**👉** *HTML Demo* : https://codepen.io/halimekarayay/pen/OPyKBZM |
|||
|
|||
|
|||
#### Attributes |
|||
|
|||
##### Open Attribute |
|||
By default, the structure is **closed**. We can change the default open/closed state using `open`. |
|||
|
|||
<details open> |
|||
<summary>This is a title</summary> |
|||
<p>Lorem Ipsum is simply dummy text of the printing and typesetting industry</p> |
|||
<p>Lorem Ipsum is simply dummy text of the printing and typesetting industry</p> |
|||
</details> |
|||
|
|||
|
|||
##### Name |
|||
|
|||
This feature allows multiple `<details>` to move into a group by connecting to each other. Only one of them is open at the same time. This feature allows developers to easily create user interface features such as accordion without writing a script. |
|||
|
|||
|
|||
<details name="requirements"> |
|||
<summary>Graduation Requirements</summary> |
|||
<p> |
|||
Sed eu ipsum magna. Ut ultricies arcu nec lectus interdum, sit amet elementum diam elementum. |
|||
</p> |
|||
</details> |
|||
<details name="requirements"> |
|||
<summary>System Requirements</summary> |
|||
<p> |
|||
Curabitur porta quis mi id gravida. Ut convallis, ligula quis blandit sagittis. |
|||
</p> |
|||
</details> |
|||
<details name="requirements"> |
|||
<summary>Job Requirements</summary> |
|||
<p> |
|||
Suspendisse malesuada arcu eget condimentum pretium. |
|||
</p> |
|||
</details> |
|||
|
|||
**👉** *HTML Demo* : [https://codepen.io/halimekarayay/pen/MYaNzmP](https://codepen.io/halimekarayay/pen/MYaNzmP) |
|||
|
|||
<img src="browser-support.png"> |
|||
|
|||
##### *More Information* |
|||
- [https://css-tricks.com/using-styling-the-details-element/](https://css-tricks.com/using-styling-the-details-element/) |
|||
- [https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/details](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/details) |
|||
--- |
|||
|
|||
|
|||
|
|||
### 2. `<dialog>` Tag |
|||
`<dialog>` tag is a modern label used to create native modal and popup in HTML. |
|||
As of 2025, it is now supported in many browser and can be easily controlled with Javascript. |
|||
|
|||
// HTML |
|||
<dialog id="myDialog"> |
|||
<p>This is a modal window.</p> |
|||
<button id="closeBtn">Close</button> |
|||
</dialog> |
|||
<button id="openBtn">Open Modal</button> |
|||
|
|||
|
|||
// JS |
|||
<script> |
|||
const dialog = document.getElementById('myDialog'); |
|||
const openBtn = document.getElementById('openBtn'); |
|||
const closeBtn = document.getElementById('closeBtn'); |
|||
|
|||
openBtn.addEventListener('click', () => { |
|||
dialog.showModal(); |
|||
}); |
|||
|
|||
closeBtn.addEventListener('click', () => { |
|||
dialog.close(); |
|||
}); |
|||
</script> |
|||
|
|||
- `showModal()`: It opens the `<dialog>` modal. |
|||
- `show()`: It opens the `<dialog>` like a normal popup. |
|||
- `close():` Closes `<dialog>` |
|||
|
|||
**👉** *HTML Demo* :[https://codepen.io/halimekarayay/pen/XJmvojX](https://codepen.io/halimekarayay/pen/XJmvojX) |
|||
|
|||
#### Use in Form |
|||
|
|||
|
|||
// HTML |
|||
<button id="openBtn">Login</button> |
|||
<dialog id="loginDialog"> |
|||
<form method="dialog"> |
|||
<label>Username <input name="username" required></label><br><br> |
|||
<label>Password: <input type="password" name="password" required></label> |
|||
<menu> |
|||
<button value="cancel">Cancel</button> |
|||
<button value="ok">Login</button> |
|||
</menu> |
|||
</form> |
|||
</dialog> |
|||
|
|||
|
|||
// JS |
|||
<script> |
|||
const dialog = document.getElementById('loginDialog'); |
|||
const openBtn = document.getElementById('openBtn'); |
|||
|
|||
openBtn.addEventListener('click', () => { |
|||
dialog.showModal(); |
|||
}); |
|||
|
|||
dialog.addEventListener('close', () => { |
|||
console.log('Dialog closed, returnValue:', dialog.returnValue); |
|||
}); |
|||
</script> |
|||
|
|||
- `method = "dialog"` form is switched off automatically. |
|||
- `button value="..."` with the user's selection value can be obtained. |
|||
|
|||
**👉** *HTML Demo* : [https://codepen.io/halimekarayay/pen/VYvoqPr](https://codepen.io/halimekarayay/pen/VYvoqPr) |
|||
|
|||
|
|||
#### Style and Design |
|||
|
|||
// CSS |
|||
dialog { |
|||
border: none; |
|||
border-radius: 10px; |
|||
padding: 20px; |
|||
width: 400px; |
|||
box-shadow: 0 5px 15px rgba(0,0,0,0.3); |
|||
} |
|||
dialog::backdrop { |
|||
background: rgba(0, 0, 0, 0.5); |
|||
} |
|||
|
|||
|
|||
|
|||
- `dialog::backdrop` when the modal is opened, the background darkens. |
|||
- Size, color and shade can be fully customized with CSS. |
|||
|
|||
**👉** *HTML Demo* : [https://codepen.io/halimekarayay/pen/VYvoqPr](https://codepen.io/halimekarayay/pen/VYvoqPr) |
|||
|
|||
<img src="browser-support.png"> |
|||
|
|||
##### *More Information* |
|||
|
|||
- [https://developer.mozilla.org/enUS/docs/Web/HTML/Reference/Elements/dialog](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog) |
|||
|
|||
--- |
|||
|
|||
### 3. Inert Attribute |
|||
|
|||
`inert attr()` temporarily makes an element interactive. |
|||
So the user cannot focus on that area with the Tab key, Screen Reader does not see, clickable links do not work. |
|||
|
|||
<div inert> |
|||
<button>This button cannot be clicked</button> |
|||
</div> |
|||
|
|||
|
|||
|
|||
#### Specifically, `inert` does the following: |
|||
- Prevents the click event from being fired when the user clicks on the element. |
|||
- Prevents the focus event from being raised by preventing the element from gaining focus. |
|||
- Prevents any contents of the element from being found/matched during any use of the browser's find-in-page feature. |
|||
- Prevents users from selecting text within the element — akin to using the CSS property user-select to disable text selection. |
|||
- Prevents users from editing any contents of the element that are otherwise editable. |
|||
- Hides the element and its content from assistive technologies by excluding them from the accessibility tree. |
|||
|
|||
````` |
|||
<div> |
|||
<label for="button1">Button 1</label> |
|||
<button id="button1">I am not inert</button> |
|||
</div> |
|||
|
|||
<div inert> |
|||
<label for="button2">Button 2</label> |
|||
<button id="button2">I am inert</button> |
|||
</div> |
|||
````` |
|||
|
|||
|
|||
|
|||
**👉** *HTML Demo* : [https://codepen.io/halimekarayay/pen/WbQVLBb](https://codepen.io/halimekarayay/pen/WbQVLBb) |
|||
|
|||
<img src="browser-support.png"> |
|||
|
|||
##### *More Information*: |
|||
- [https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/inert](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/inert) |
|||
|
|||
--- |
|||
### 4. Popover Attribute |
|||
|
|||
In the past, we had to write javascript to create opened windows such as tooltip, modal or dropdown. No need for this anymore: Thanks to HTML5's new butt attribute, we can manage the pop-up windows completely without writing a single line of Javascript. This feature supports accessibility, provides automatic focus management and eliminates the additional library load. |
|||
|
|||
<button popovertarget="info">Show information</button> |
|||
<div id="info" popover> |
|||
<p>This content opens when clicking.</p> |
|||
</div> |
|||
|
|||
- `popover attribute` makes `<div>` into a pop-up window |
|||
- `popovertarget="info"` button triggers the related popover. |
|||
|
|||
|
|||
#### Styling |
|||
Popovers are actually normal DOM elements, only the browser adds the logic of **opening/closing**. So you can style it as you wish: |
|||
|
|||
[popover] { |
|||
padding: 1rem; |
|||
border-radius: 8px; |
|||
background: white; |
|||
box-shadow: 0 4px 16px rgba(0,0,0,.2); |
|||
} |
|||
|
|||
*In summary:* A brand new feature that allows the `popover attr()` HTML to produce opensable windows on its own. |
|||
Less JavaScript means more accessibility and easier care. |
|||
|
|||
**👉** *HTML Demo* : [https://codepen.io/halimekarayay/pen/ByoXMwo](https://codepen.io/halimekarayay/pen/ByoXMwo) |
|||
|
|||
<img src="browser-support.png"> |
|||
|
|||
##### *More Information*: |
|||
https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/popover |
|||
|
|||
--- |
|||
### 5. Fetchpriority Attribute |
|||
|
|||
Performance on web pages has always been one of the most critical issues. As of 2025, we can clearly state which resource should be loaded first thanks to the `fetchpriority attribute` supported by browsers. This seriously improves the user experience, especially for visuals and important files. |
|||
|
|||
Scanners normally load resources according to their algorithms. But for example, if there is a large hero image at the top of the page, it is very important that it comes quickly. That's where `fetchpriority` comes into play. |
|||
|
|||
`fetchpriority` can take three values: |
|||
- `high` → priority. |
|||
- `low` → then load it. |
|||
- `auto` → default. |
|||
|
|||
|
|||
Improves the LCP (Largest Contentful Paint) metric, the “visible” of the page is accelerated. |
|||
It provides a more fluent first experience to the user. |
|||
It makes a big difference, especially in multi -ly illustrated pages or heavy -based projects. |
|||
|
|||
<img src="browser-support.png"> |
|||
|
|||
|
|||
##### *More Information*: |
|||
- [https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority) |
|||
|
|||
--- |
|||
|
|||
## Modern CSS Techniques |
|||
|
|||
### 1. CSS Nesting |
|||
|
|||
CSS Nesting provides the ability to write a style rule into another style rule. In the past, this feature was possible only in prepromessors (eg Sass, Less), but now most of the browsers began to gain native support. With this feature, you can make style files more read, modular and easier to manage. In addition, the repetitive selector spelling is more comfortable to maintain the code. |
|||
|
|||
Let's think of a simple **HTML** scenario: |
|||
|
|||
<div class="card"> |
|||
<h2>Lorem Ipsum </h2> |
|||
<p>Morbi maximus elit leo, in molestie mi dapibus vel.</p> |
|||
<a href="#">Continue</a> |
|||
</div> |
|||
|
|||
|
|||
|
|||
If the common classic css is to be used, it is as follows: |
|||
|
|||
.card { |
|||
padding: 1rem; |
|||
border: 1px solid #ccc; |
|||
} |
|||
.card h2 { |
|||
font-size: 1.5rem; |
|||
} |
|||
.card p { |
|||
font-size: 1rem; |
|||
} |
|||
.card a { |
|||
color: blue; |
|||
text-decoration: none; |
|||
} |
|||
.card a:hover { |
|||
text-decoration: underline; |
|||
} |
|||
|
|||
|
|||
|
|||
You can write the same style as CSS nesting as follows: |
|||
|
|||
.card { |
|||
padding: 1rem; |
|||
border: 1px solid #ccc; |
|||
h2 { |
|||
font-size: 1.5rem; |
|||
} |
|||
p { |
|||
font-size: 1rem; |
|||
} |
|||
a { |
|||
color: blue; |
|||
text-decoration: none; |
|||
&:hover { |
|||
text-decoration: underline; |
|||
} |
|||
} |
|||
} |
|||
This structure increases readability by collecting style rules in a single block for the items in `.card`. |
|||
|
|||
#### Recommendation |
|||
|
|||
- You can use CSS Nesting directly in your project, but I suggest you pay attention to the following points: |
|||
- If the users of the target audience use old browsers (eg old Android browser, old iOS safari), think of Fallback Style or Polyfill. |
|||
- If the code is compiled (such as PostCSS), use the correct versions of Plugin who control nesting support. |
|||
- Deep Nesting can cause style complexity; Limit 2–3 levels for readability. |
|||
|
|||
|
|||
##### *More Information*: |
|||
- https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting/Using_CSS_nesting |
|||
- https://www.w3.org/TR/css-nesting-1 |
|||
|
|||
--- |
|||
|
|||
### 2. @container Queries (Container Query) |
|||
|
|||
|
|||
We have used **media query** for **responsive design** for years: we have written different styles according to the screen width. But sometimes a component (eg a card) should behave differently in a different container. Here `@Container Queries` comes into play. |
|||
|
|||
**Media query** looks at the width of the entire page. But sometimes it looks small when a card is lined up side by side, it should look big when it is alone. In this case, instead of looking at the page width, it would make more sense to look at the inclusive width of the card. |
|||
|
|||
- The browser checks the condition in `@container` for each parent (parent) element. |
|||
- If the parent element is **marked as a container,** (`container-type` is given), its size is examined. |
|||
- So `@container` automatically connects to the nearest **“ container ”** upper element. |
|||
|
|||
|
|||
.card-list { |
|||
container-type: inline-size; |
|||
} |
|||
|
|||
@container (min-width: 400px) { |
|||
.card { |
|||
flex-direction: row; |
|||
} |
|||
} |
|||
|
|||
- `.card-list` → container. |
|||
- `.card` → child. |
|||
- When we say `@container (min-width: 400px),` the browser measures the width of the `.card` `.card-list`. |
|||
- If the `.card-list` width is greater than 400px, the style is working. |
|||
|
|||
|
|||
#### If there is more than one container |
|||
|
|||
If there is more than one container in the same hierarchy, the browser is based on the closest. |
|||
|
|||
|
|||
// HTML |
|||
<section class="wrapper"> |
|||
<div class="card-list"> |
|||
<div class="card">...</div> |
|||
</div> |
|||
</section> |
|||
|
|||
// CSS |
|||
.wrapper { |
|||
container-type: inline-size; |
|||
} |
|||
|
|||
.card-list { |
|||
container-type: inline-size; |
|||
} |
|||
|
|||
@container (min-width: 600px) { |
|||
.card { |
|||
background: lightblue; |
|||
} |
|||
} |
|||
|
|||
|
|||
#### container-name |
|||
|
|||
If you want to say, **"which container should be looked at",** you should add the `container-name`: |
|||
|
|||
.card-list { |
|||
container-type: inline-size; |
|||
container-name: cards; |
|||
} |
|||
|
|||
@container cards (min-width: 600px) { |
|||
.card { |
|||
background: lightblue; |
|||
} |
|||
} |
|||
|
|||
The browser is directly targeted by `.card-list`, and does not look at another container. |
|||
|
|||
##### *More Information*: |
|||
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries |
|||
|
|||
|
|||
--- |
|||
### 3. :has() Selector |
|||
|
|||
CSS has always been able to give style for years. But it was not possible to “choose the parent according to his child”. Here `:has()` filled this gap and created the **parent selector revolution** in the world of CSS. |
|||
|
|||
- It is possible to replace the upper element according to user interactions. |
|||
- We can only solve many conditions with JavaScript with CSS. |
|||
- Form validation, card structures, Dropdown menus are very practical. |
|||
|
|||
For example, marking the form with incorrect input with red edges: |
|||
|
|||
form:has(input:invalid) { |
|||
border: 2px solid red; |
|||
} |
|||
|
|||
In the dropdown menu, emphasizing the menu, which has an element of hover: |
|||
|
|||
.menu:has(li:hover) { |
|||
background: #f0f0f0; |
|||
} |
|||
|
|||
In the card structure, giving different styles to the cards that contain pictures: |
|||
|
|||
.card:has(img) { |
|||
border: 1px solid #ccc; |
|||
padding: 1rem; |
|||
} |
|||
|
|||
- It allows you to write more **readable css**. |
|||
- Reduces the need for JavaScript in many places. |
|||
- It provides great convenience especially for **forms, navigation menus and card grids**. |
|||
|
|||
##### *More Information*: |
|||
- http://developer.mozilla.org/en-US/docs/Web/CSS/:has |
|||
- https://www.w3.org/TR/selectors-4/#has-pseudo |
|||
|
|||
--- |
|||
|
|||
### 4. Subgrid |
|||
CSS grid revolutionized the world of layout, but there was a missing: |
|||
The child grid elements in a grid could not directly use the line and column alignment of the parent grid. Here `subgrid` solves this problem. |
|||
|
|||
- Provides consistent alignment. |
|||
- Nested saves grid from repetitive definitions in their structures. |
|||
- It produces cleaner, flexible and sustainable layouts. |
|||
|
|||
|
|||
|
|||
Main grid: |
|||
|
|||
.grid { |
|||
display: grid; |
|||
grid-template-columns: 200px 1fr; |
|||
grid-template-rows: auto auto; |
|||
gap: 1rem; |
|||
} |
|||
|
|||
|
|||
|
|||
Child grid (subgrid): |
|||
|
|||
.article { |
|||
display: grid; |
|||
grid-template-columns: subgrid; |
|||
grid-column: 1 / -1; |
|||
} |
|||
|
|||
|
|||
|
|||
HTML example: |
|||
|
|||
<div class="grid"> |
|||
<header>Title</header> |
|||
<div class="article"> |
|||
<h2>Article Title</h2> |
|||
<p><Article Content</p> |
|||
</div> |
|||
<footer>Footer content</footer> |
|||
</div> |
|||
|
|||
In this structure, `.article` forms its own grid, but it takes over its columns ** from parent grid **. |
|||
Thus, both the title, the article content and the foother appear to the same. |
|||
|
|||
|
|||
- Layout is more regular with **less CSS code**. |
|||
- Especially in **complex page designs** (blog, dashboard, magazine designs) great convenience |
|||
|
|||
##### *More Information*: |
|||
- https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout/Subgrid |
|||
- https://web.dev/articles/css-subgrid |
|||
|
|||
--- |
|||
|
|||
### 5. Scroll-driven Animations (scroll-timeline, view-timeline) |
|||
|
|||
In the past, we had to use a **javascript** to move a item with scroll or to start animation. |
|||
But now thanks to the new feature of CSS, we can do this **directly with CSS**. This provides both performance and less coded solution. |
|||
|
|||
- `@scroll-Timeline:` Allows us to use a scroll field as “timeline .. So the animation progresses as the page shifts down. |
|||
- `animation-Timeline:` It ensures that animation is synchronized with this scroll movement. |
|||
|
|||
Let's make a box move to the right as a box shifts down: |
|||
|
|||
// HTML |
|||
<div class="scroller"> |
|||
<div class="box"></div> |
|||
</div> |
|||
|
|||
// CSS |
|||
.scroller { |
|||
height: 200vh; |
|||
background: linear-gradient(white, lightblue); |
|||
} |
|||
|
|||
.box { |
|||
width: 100px; |
|||
height: 100px; |
|||
background: tomato; |
|||
|
|||
animation: moveRight 1s linear; |
|||
animation-timeline: scroll(); |
|||
} |
|||
|
|||
@keyframes moveRight { |
|||
from { transform: translateX(0); } |
|||
to { transform: translateX(300px); } |
|||
} |
|||
|
|||
📌 Here, `animation-timeline: scroll ();` when we say, the box is moving as scroll progresses. In other words, the percentage of progression of scroll is → animation progress. |
|||
|
|||
#### Usage with view-timeline |
|||
|
|||
In some cases, we may want the animation to **work while a particular item appears**. |
|||
That's where the `view-timeline` comes into play. |
|||
|
|||
// HTML |
|||
<div class="container"> |
|||
<div class="card">I am animated</div> |
|||
</div> |
|||
|
|||
// CSS |
|||
.container { |
|||
height: 150vh; |
|||
background: lightgray; |
|||
} |
|||
|
|||
.card { |
|||
margin: 100px auto; |
|||
width: 200px; |
|||
height: 100px; |
|||
background: pink; |
|||
|
|||
animation: fadeIn 1s linear; |
|||
animation-timeline: view(); |
|||
} |
|||
|
|||
@keyframes fadeIn { |
|||
from { opacity: 0; transform: translateY(50px); } |
|||
to { opacity: 1; transform: translateY(0); } |
|||
} |
|||
|
|||
📌 Here `.card` when it begins to be visible, the animation comes into play. |
|||
|
|||
**👉** *HTML Demo* : https://codepen.io/halimekarayay/pen/myVbmZy |
|||
|
|||
|
|||
##### *More Information*: |
|||
https://developer.chrome.com/docs/css-ui/scroll-driven-animations |
|||
|
|||
----- |
|||
|
|||
**In conclusion,** HTML and CSS are evolving every year, giving us the opportunity to do more with less code. The 10 techniques we covered in this article are among the must-know tools for modern web projects in 2025. Of course, technology keeps moving forward; but once you start adding these features to your projects, you’ll not only improve the user experience but also speed up your development process. Don’t be afraid to experiment—because the future of the web is being shaped by these new standards. 🚀 |
|||
|
After Width: | Height: | Size: 29 KiB |
@ -0,0 +1,308 @@ |
|||
# How to Build an In Memory Background Job Queue in ASP.NET Core from Scratch |
|||
|
|||
In web applications, providing a fast and responsive API is crucial for the user experience. However, some operations, such as sending emails, generating reports, or adding users in bulk, can take significant time. Performing these tasks during a request can block threads, resulting in slow response times. |
|||
|
|||
To significantly alleviate this issue, it would be more effective to run long running tasks in a background process. Instead of waiting for the request to complete, we can queue the task, provide an immediate response to the user, and perform the work in a separate background process. |
|||
|
|||
While libraries like Hangfire or message brokers like RabbitMQ exist for this purpose, you don't need any external dependencies. In this blog post, we'll build an in memory background job queue from scratch in ASP.NET Core using only built in .NET features. |
|||
|
|||
 |
|||
|
|||
## Core Components of Background Job System |
|||
|
|||
Before implementing the code, it is important to first understand the basic parts that make up the background job system and how they work to process queued tasks. |
|||
|
|||
### What are IHostedService and BackgroundService? |
|||
|
|||
`IHostedService` is an interface used in ASP.NET Core to implement long running background tasks managed by the application's lifecycle. When your application starts, it starts all registered `IHostedService` applications. When it shuts down, it instructs them to stop. |
|||
|
|||
`BackgroundService` is an abstract base class that implements `IHostedService`. It gives you a more efficient way to create a timed service by giving you a single overridable method `ExecuteAsync(CancellationToken stoppingToken)`. We'll use this as the basis for our job processor. |
|||
|
|||
### The Producer/Consumer Structure |
|||
|
|||
* **Producer:** This is the part of your application that creates jobs and adds them to the queue. In our example, the producer will be an API Controller. |
|||
* **Consumer:** This is a background service that constantly monitors the queue, pulls jobs from the queue, and executes them. Our `BackgroundService` application will be the consumer. |
|||
* **Queue:** This is the data structure between the producer and consumer that holds jobs waiting to be processed. |
|||
|
|||
|
|||
 |
|||
|
|||
### Why System.Threading.Channels? |
|||
|
|||
Introduced in .NET Core 3.0, `System.Threading.Channels` provides a synchronization data structure designed for asynchronous producer, consumer scenarios. |
|||
|
|||
* **Thread Safety:** Handles all complex locking and synchronization operations internally, making it usable as a whole from multiple threads (for example, when multiple API requests are adding work simultaneously). |
|||
* **Async Native:** Designed for use with `async`/`await`, preventing thread blocking. You can asynchronously wait for an item in the queue to become available. |
|||
* **Performance:** Optimized for speed and low memory allocation. Generally superior to legacy constructs like `ConcurrentQueue` for asynchronous workflows because it eliminates the need for polling. |
|||
|
|||
## Implementing the Background Job Queue |
|||
|
|||
Now that we understand the architecture, we can implement the background job queue. |
|||
|
|||
### Create the Project |
|||
|
|||
First, create a new ASP.NET Core Web API project using the .NET CLI. |
|||
|
|||
```bash |
|||
dotnet new sln -n BackgroundJobQueue |
|||
|
|||
dotnet new webapi -n BackgroundJobQueue.Api |
|||
|
|||
dotnet sln BackgroundJobQueue.sln add BackgroundJobQueue.Api/BackgroundJobQueue.Api.csproj |
|||
``` |
|||
|
|||
### Define the Job Queue Interface |
|||
|
|||
For dependency injection and testability, we'll start by defining an interface for our queue. This interface will define methods for adding a job (`EnqueueAsync`) and removing a job (`DequeueAsync`). |
|||
|
|||
Create a new file named `IBackgroundTaskQueue.cs`. |
|||
|
|||
```csharp |
|||
namespace BackgroundJobQueue; |
|||
|
|||
public interface IBackgroundTaskQueue |
|||
{ |
|||
// Adds a work item to the queue |
|||
ValueTask EnqueueAsync(Func<CancellationToken, ValueTask> workItem); |
|||
|
|||
// Removes and returns a work item from the queue |
|||
ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(CancellationToken cancellationToken); |
|||
} |
|||
``` |
|||
|
|||
We'll use `Func<CancellationToken, ValueTask>` to represent a work item. This allows us to queue any asynchronous method that accepts a `CancellationToken`. |
|||
|
|||
### Implement the Queue Service with Channels |
|||
|
|||
Now, let's implement the `IBackgroundTaskQueue` interface using the `System.Threading.Channels` class. This class will manage the in memory channel. |
|||
|
|||
Create a new file named `BackgroundTaskQueue.cs`. |
|||
|
|||
```csharp |
|||
using System.Threading.Channels; |
|||
|
|||
namespace BackgroundJobQueue; |
|||
|
|||
public class BackgroundTaskQueue : IBackgroundTaskQueue |
|||
{ |
|||
private readonly Channel<Func<CancellationToken, ValueTask>> _queue; |
|||
|
|||
public BackgroundTaskQueue(int capacity) |
|||
{ |
|||
// BoundedChannelOptions specifies the behavior of the channel. |
|||
var options = new BoundedChannelOptions(capacity) |
|||
{ |
|||
// FullMode.Wait tells the writer to wait asynchronously if the queue is full. |
|||
FullMode = BoundedChannelFullMode.Wait |
|||
}; |
|||
_queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options); |
|||
} |
|||
|
|||
public async ValueTask EnqueueAsync(Func<CancellationToken, ValueTask> workItem) |
|||
{ |
|||
if (workItem is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(workItem)); |
|||
} |
|||
|
|||
// Writes an item to the channel. If the channel is full, |
|||
await _queue.Writer.WriteAsync(workItem); |
|||
} |
|||
|
|||
public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(CancellationToken cancellationToken) |
|||
{ |
|||
// Reads an item from the channel. If the channel is empty, |
|||
var workItem = await _queue.Reader.ReadAsync(cancellationToken); |
|||
|
|||
return workItem; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### Create the Consumer Service (QueuedHostedService) |
|||
|
|||
This is our consumer. It's a `BackgroundService` that continuously dequeues and executes jobs. |
|||
|
|||
Create a new file named `QueuedHostedService.cs`. |
|||
|
|||
```csharp |
|||
namespace BackgroundJobQueue; |
|||
|
|||
public class QueuedHostedService : BackgroundService |
|||
{ |
|||
private readonly IBackgroundTaskQueue _taskQueue; |
|||
private readonly ILogger<QueuedHostedService> _logger; |
|||
|
|||
public QueuedHostedService(IBackgroundTaskQueue taskQueue, ILogger<QueuedHostedService> logger) |
|||
{ |
|||
_taskQueue = taskQueue; |
|||
_logger = logger; |
|||
} |
|||
|
|||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) |
|||
{ |
|||
_logger.LogInformation("Queued Hosted Service is running."); |
|||
|
|||
while (!stoppingToken.IsCancellationRequested) |
|||
{ |
|||
// Dequeue a work item |
|||
var workItem = await _taskQueue.DequeueAsync(stoppingToken); |
|||
|
|||
try |
|||
{ |
|||
// Execute the work item |
|||
await workItem(stoppingToken); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "Error occurred executing {WorkItem}.", nameof(workItem)); |
|||
} |
|||
} |
|||
|
|||
_logger.LogInformation("Queued Hosted Service is stopping."); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
The `try-catch` block here is critical. It ensures that if one job fails with an exception, it won't crash the entire background service. |
|||
|
|||
### Register the Services in Program.cs |
|||
|
|||
Next we need to register the queue and hosted service in the dependency injection container inside `Program.cs`. |
|||
|
|||
```csharp |
|||
using BackgroundJobQueue; |
|||
|
|||
var builder = WebApplication.CreateBuilder(args); |
|||
|
|||
//... |
|||
|
|||
// Register the background task queue as a Singleton |
|||
builder.Services.AddSingleton<IBackgroundTaskQueue>(_ => |
|||
{ |
|||
// You can configure the capacity of the queue here |
|||
if (!int.TryParse(builder.Configuration["QueueCapacity"], out var queueCapacity)) |
|||
{ |
|||
queueCapacity = 100; |
|||
} |
|||
return new BackgroundTaskQueue(queueCapacity); |
|||
}); |
|||
|
|||
// Register the hosted service |
|||
builder.Services.AddHostedService<QueuedHostedService>(); |
|||
|
|||
|
|||
var app = builder.Build(); |
|||
|
|||
//... |
|||
|
|||
app.Run(); |
|||
``` |
|||
|
|||
We register `IBackgroundTaskQueue` as a **Singleton** because we need a single, shared queue instance for the entire application. We register `QueuedHostedService` using `AddHostedService()` to ensure the .NET runtime manages its lifecycle. |
|||
|
|||
### Create a Producer (API Controller) |
|||
|
|||
Finally, let's create a producer. This will be a simple API controller with an endpoint that enqueues a new background job. |
|||
|
|||
Create a new controller named `JobsController.cs`. |
|||
|
|||
```csharp |
|||
using Microsoft.AspNetCore.Mvc; |
|||
|
|||
namespace BackgroundJobQueue.Controllers; |
|||
|
|||
[ApiController] |
|||
[Route("[controller]")] |
|||
public class JobsController : ControllerBase |
|||
{ |
|||
private readonly IBackgroundTaskQueue _queue; |
|||
private readonly ILogger<JobsController> _logger; |
|||
|
|||
public JobsController(IBackgroundTaskQueue queue, ILogger<JobsController> logger) |
|||
{ |
|||
_queue = queue; |
|||
_logger = logger; |
|||
} |
|||
|
|||
[HttpPost] |
|||
public async Task<IActionResult> EnqueueJob() |
|||
{ |
|||
// Enqueue a job that simulates a long running task |
|||
await _queue.EnqueueAsync(async token => |
|||
{ |
|||
// Simulate a 5 second task |
|||
var guid = Guid.NewGuid(); |
|||
_logger.LogInformation("Job {Guid} started.", guid); |
|||
await Task.Delay(TimeSpan.FromSeconds(5), token); |
|||
_logger.LogInformation("Job {Guid} finished.", guid); |
|||
}); |
|||
|
|||
return Ok("Job has been enqueued."); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Now, when you run your application and send a POST request to `/jobs`, the API will respond instantly with "Job has been enqueued." while the 5 second task runs in the background. You'll see the log messages appear in your console after the delay. |
|||
|
|||
## Important Considerations and Advanced Topics |
|||
|
|||
### Shutdown |
|||
|
|||
The `CancellationToken` passed to ExecuteAsync is important. The host triggers this token when you stop your application. Our while loop condition `(!stoppingToken.IsCancellationRequested)` and passing the token to `DequeueAsync` cause the service to stop listening for new items and exit. |
|||
|
|||
### Error Handling |
|||
|
|||
Our `try-catch` block prevents a single failed job from crashing the consumer. For production scenarios, you can consider a more advanced error handling strategy, such as a retry mechanism. Libraries like **Polly** can be integrated here to automatically retry failed jobs with policies like fallback. |
|||
|
|||
### Limits of the In Memory Queue |
|||
|
|||
The biggest drawback of this approach is that the queue is **in memory**. If your application process restarts or crashes for any reason, any work waiting in the queue will be lost. This solution is best for idempotent or non critical tasks. |
|||
|
|||
### When Should You Use External Libraries? |
|||
|
|||
Built from the ground up, this solution is powerful, but it's important to know when to turn to more advanced tools. |
|||
|
|||
* **Hangfire:** Use Hangfire when you need job continuity (saves jobs to a database), automatic retries, a management dashboard, scheduled jobs, and delayed execution. Background tasks are an ideal solution while processing. |
|||
|
|||
* **RabbitMQ** When building a distributed system (such as a microservices architecture), you can use a custom message broker. These brokers provide scalable queues that isolate your services and guarantee message delivery. |
|||
|
|||
## Frequently Asked Questions |
|||
|
|||
**Q1: What happens to the jobs in the queue if my application restarts or crashes?** |
|||
|
|||
**A:** Because this is an **in-memory** queue, any unprocessed work is **lost** when the application closes or crashes. This solution is best suited for non-critical or idempotent tasks (i.e., they can be safely rerun without causing problems). For guaranteed business continuity, you should use a solution that stores jobs in a database or message broker, such as **Hangfire** or **RabbitMQ**. |
|||
|
|||
**Q2: How can I implement retry logic for jobs that fail due to an exception?** |
|||
|
|||
**A:** The `try-catch` block in `QueuedHostedService` prevents the entire background service from crashing, but it doesn't automatically retry the failed job. You can integrate a library like **Polly** to add robust retry capabilities. You can wrap the `await workItem(stoppingToken);` call in a Polly retry policy to automatically rerun the job a configured number of times with delays. |
|||
|
|||
[Polly Official Documentation](https://github.com/App-vNext/Polly) |
|||
|
|||
**Q3: Will this work in a load balanced environment with multiple server instances?** |
|||
|
|||
**A:** No, this implementation is instance-specific. The queue exists only in the memory of the server instance that receives the API request. If you deploy your application across multiple servers, a job queued on Server A will be processed only by Server A. For a shared queue that can be processed by any server in a web environment, you should use a centralized, distributed message broker such as **RabbitMQ**, **Azure Service Bus**. |
|||
|
|||
**Q4: What happens if jobs are added to the queue faster than the background service can process them?** |
|||
|
|||
**A:** This application uses a BoundedChannel with a fixed capacity (for example, 100 in our example). When the queue is full, setting `FullMode = BoundedChannelFullMode.Wait` causes the producer (API controller) to asynchronously wait until space becomes available in the queue. This creates "backpressure" that prevents your application from running out of memory. However, this also means that if the background service is constantly overloaded, your API endpoint will become slower to respond as it waits to queue new jobs. |
|||
|
|||
**Q5: How can I monitor the number of jobs currently in the queue?** |
|||
|
|||
**A:** For basic monitoring, you can inject `IBackgroundTaskQueue` into a service and periodically record the queue count. A more advanced approach is to create a custom metrics endpoint (for example, for Prometheus). You can create a custom controller that accesses the `Count` property of the `ChannelReader` to report the current queue depth and set alerts if the queue grows too long. |
|||
|
|||
|
|||
## Conclusion |
|||
|
|||
We've built a performant, in memory background job queue in ASP.NET Core using only built in framework features. This is a way to increase your API's responsiveness and improve the user experience by disabling long running tasks. |
|||
|
|||
Using `IHostedService` and `System.Threading.Channels`, we've created an efficient implementation of the producer, consumer model. While this in memory approach has some limitations, it's a powerful tool for many common scenarios. As your needs grow, you can consider more feature rich tools like Hangfire or RabbitMQ, knowing you've mastered the fundamentals. |
|||
|
|||
## References |
|||
|
|||
* [Background tasks with hosted services in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services) |
|||
* [An Introduction to System.Threading.Channels](https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/) |
|||
* [ABP Framework - Background Jobs](https://docs.abp.io/en/abp/latest/Background-Jobs) |
|||
* [Creating a queued background task service with IHostedService by Andrew Lock](https://andrewlock.net/controlling-ihostedservice-execution-order-in-aspnetcore-3/) |
|||
* [Hangfire Documentation](https://docs.hangfire.io/en/latest/getting-started/aspnet-core-applications.html) |
|||
* [RabbitMQ Tutorials](https://www.rabbitmq.com/getstarted.html) |
|||
|
After Width: | Height: | Size: 305 KiB |
|
After Width: | Height: | Size: 746 KiB |
@ -0,0 +1,360 @@ |
|||
# The Most Popular & Best Distributed Event Buses with .NET Clients |
|||
|
|||
## Why Event Buses Matter |
|||
|
|||
In distributed systems, the messaging layer often determines whether projects succeed or fail. When microservices struggle to communicate effectively, it's typically due to messaging architecture problems. |
|||
|
|||
Event buses solve this communication challenge. Instead of services calling each other directly (which becomes complex quickly), they publish events when something happens. Other services subscribe to the events they care about. While the concept is simple, implementation details matter significantly - especially in the .NET ecosystem. |
|||
|
|||
This article examines the major event bus technologies available today, covering their strengths, weaknesses, and practical implementation considerations. |
|||
|
|||
## The Main Contenders |
|||
|
|||
The following analysis covers the major event bus technologies, examining their practical strengths and limitations based on real-world usage patterns. |
|||
|
|||
### RabbitMQ - The Old Reliable |
|||
|
|||
RabbitMQ is known for its reliability and consistent performance. Built on AMQP (Advanced Message Queuing Protocol), it provides a solid foundation for enterprise messaging scenarios. |
|||
|
|||
RabbitMQ's key advantage lies in its routing flexibility, allowing sophisticated message flow patterns throughout distributed systems. |
|||
|
|||
**Key Strengths:** |
|||
- Message persistence and guaranteed delivery |
|||
- Flexible routing patterns (direct, topic, fanout, headers) |
|||
- Management UI and monitoring tools |
|||
- Mature .NET client library |
|||
|
|||
**.NET Integration Example:** |
|||
```csharp |
|||
using RabbitMQ.Client; |
|||
using RabbitMQ.Client.Events; |
|||
|
|||
// Publisher |
|||
var factory = new ConnectionFactory() { HostName = "localhost" }; |
|||
using var connection = factory.CreateConnection(); |
|||
using var channel = connection.CreateModel(); |
|||
|
|||
channel.QueueDeclare(queue: "order_events", durable: true, exclusive: false, autoDelete: false); |
|||
|
|||
var message = JsonSerializer.Serialize(new OrderCreated { OrderId = Guid.NewGuid() }); |
|||
var body = Encoding.UTF8.GetBytes(message); |
|||
|
|||
channel.BasicPublish(exchange: "", routingKey: "order_events", basicProperties: null, body: body); |
|||
``` |
|||
|
|||
**Works best when:** You need complex routing, can't afford to lose messages, or you're dealing with traditional enterprise patterns. |
|||
|
|||
### Apache Kafka - The Heavy Hitter |
|||
|
|||
Kafka represents a different approach to messaging. Rather than being a traditional message broker, it functions as a distributed log system that excels at messaging workloads. |
|||
|
|||
While Kafka's concepts (partitions, offsets, consumer groups) can seem complex initially, understanding them reveals why the platform has gained widespread adoption. The throughput capabilities are exceptional. |
|||
|
|||
**Key Strengths:** |
|||
- Exceptional throughput (millions of messages/second) |
|||
- Built-in partitioning and replication |
|||
- Message replay capabilities |
|||
- Strong ordering guarantees within partitions |
|||
|
|||
**.NET Integration Example:** |
|||
```csharp |
|||
using Confluent.Kafka; |
|||
|
|||
// Producer |
|||
var config = new ProducerConfig { BootstrapServers = "localhost:9092" }; |
|||
using var producer = new ProducerBuilder<string, string>(config).Build(); |
|||
|
|||
var result = await producer.ProduceAsync("order-events", |
|||
new Message<string, string> |
|||
{ |
|||
Key = orderId.ToString(), |
|||
Value = JsonSerializer.Serialize(orderEvent) |
|||
}); |
|||
|
|||
// Consumer |
|||
var consumerConfig = new ConsumerConfig |
|||
{ |
|||
GroupId = "order-processor", |
|||
BootstrapServers = "localhost:9092", |
|||
AutoOffsetReset = AutoOffsetReset.Earliest |
|||
}; |
|||
|
|||
using var consumer = new ConsumerBuilder<string, string>(consumerConfig).Build(); |
|||
consumer.Subscribe("order-events"); |
|||
|
|||
while (true) |
|||
{ |
|||
var consumeResult = consumer.Consume(cancellationToken); |
|||
// Process message |
|||
consumer.Commit(consumeResult); |
|||
} |
|||
``` |
|||
|
|||
**Perfect for:** High-volume streaming, event sourcing, or when you need to replay messages later. |
|||
|
|||
### Azure Service Bus - The Microsoft Way |
|||
|
|||
For organizations using the Microsoft ecosystem, Azure Service Bus provides a natural fit. It offers enterprise-grade messaging without infrastructure management overhead. |
|||
|
|||
The integration with other Azure services is seamless, and features like dead letter queues provide robust error handling capabilities. |
|||
|
|||
**Key Strengths:** |
|||
- Dead letter queues and message sessions |
|||
- Duplicate detection and scheduled messages |
|||
- Integration with Azure ecosystem |
|||
- Auto-scaling capabilities |
|||
|
|||
**.NET Integration Example:** |
|||
```csharp |
|||
using Azure.Messaging.ServiceBus; |
|||
|
|||
await using var client = new ServiceBusClient(connectionString); |
|||
var sender = client.CreateSender("order-queue"); |
|||
|
|||
var message = new ServiceBusMessage(JsonSerializer.Serialize(orderEvent)) |
|||
{ |
|||
MessageId = Guid.NewGuid().ToString(), |
|||
ContentType = "application/json" |
|||
}; |
|||
|
|||
await sender.SendMessageAsync(message); |
|||
|
|||
// Processor |
|||
var processor = client.CreateProcessor("order-queue"); |
|||
processor.ProcessMessageAsync += async args => |
|||
{ |
|||
var order = JsonSerializer.Deserialize<OrderEvent>(args.Message.Body); |
|||
// Process order |
|||
await args.CompleteMessageAsync(args.Message); |
|||
}; |
|||
await processor.StartProcessingAsync(); |
|||
``` |
|||
|
|||
**Great choice when:** You're on Azure, need enterprise features, or want someone else to handle the operations. |
|||
|
|||
### Amazon SQS - Keep It Simple |
|||
|
|||
Amazon SQS prioritizes simplicity and reliability over extensive features. While not the most feature-rich option, this approach often aligns well with practical requirements. |
|||
|
|||
SQS works particularly well in serverless architectures where reliable queuing is needed without operational complexity. |
|||
|
|||
**Key Strengths:** |
|||
- Virtually unlimited scalability |
|||
- Server-side encryption |
|||
- Dead letter queue support |
|||
- Pay-per-use pricing model |
|||
|
|||
**.NET Integration Example:** |
|||
```csharp |
|||
using Amazon.SQS; |
|||
using Amazon.SQS.Model; |
|||
|
|||
var sqsClient = new AmazonSQSClient(); |
|||
var queueUrl = await sqsClient.GetQueueUrlAsync("order-events"); |
|||
|
|||
// Send message |
|||
await sqsClient.SendMessageAsync(new SendMessageRequest |
|||
{ |
|||
QueueUrl = queueUrl.QueueUrl, |
|||
MessageBody = JsonSerializer.Serialize(orderEvent) |
|||
}); |
|||
|
|||
// Receive messages |
|||
var response = await sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest |
|||
{ |
|||
QueueUrl = queueUrl.QueueUrl, |
|||
MaxNumberOfMessages = 10, |
|||
WaitTimeSeconds = 20 |
|||
}); |
|||
|
|||
foreach (var message in response.Messages) |
|||
{ |
|||
// Process message |
|||
await sqsClient.DeleteMessageAsync(queueUrl.QueueUrl, message.ReceiptHandle); |
|||
} |
|||
``` |
|||
|
|||
**Use it when:** You're on AWS, building serverless, or just want something that works without complexity. |
|||
|
|||
### Apache ActiveMQ - The Veteran |
|||
|
|||
ActiveMQ has been serving enterprise messaging needs for many years, predating the current microservices trend. |
|||
|
|||
While not the most modern option, it supports an extensive range of messaging protocols and continues to operate reliably in legacy enterprise environments. |
|||
|
|||
**Key Strengths:** |
|||
- Multiple protocol support (AMQP, STOMP, MQTT) |
|||
- Clustering and high availability |
|||
- JMS compliance |
|||
- Web-based administration |
|||
|
|||
**.NET Integration Example:** |
|||
```csharp |
|||
using Apache.NMS; |
|||
using Apache.NMS.ActiveMQ; |
|||
|
|||
var factory = new ConnectionFactory("tcp://localhost:61616"); |
|||
using var connection = factory.CreateConnection(); |
|||
using var session = connection.CreateSession(); |
|||
|
|||
var destination = session.GetQueue("order.events"); |
|||
var producer = session.CreateProducer(destination); |
|||
|
|||
var message = session.CreateTextMessage(JsonSerializer.Serialize(orderEvent)); |
|||
producer.Send(message); |
|||
``` |
|||
|
|||
**Consider it for:** Legacy environments, multi-protocol needs, or when you're stuck with JMS requirements. |
|||
|
|||
### Redpanda - Kafka Without the Pain |
|||
|
|||
Redpanda is a newer entrant that addresses Kafka's operational complexity. The project maintains Kafka API compatibility while eliminating JVM overhead and Zookeeper dependencies. |
|||
|
|||
This approach significantly reduces operational burden while preserving the familiar Kafka programming model. |
|||
|
|||
**Key Strengths:** |
|||
- Kafka API compatibility |
|||
- No dependency on JVM or Zookeeper |
|||
- Lower resource consumption |
|||
- Built-in schema registry |
|||
|
|||
**.NET Integration:** |
|||
Uses the same Confluent.Kafka client library, making migration seamless: |
|||
|
|||
```csharp |
|||
var config = new ProducerConfig { BootstrapServers = "localhost:9092" }; |
|||
// Same code as Kafka - drop-in replacement |
|||
``` |
|||
|
|||
**Try it if:** You want Kafka's power but hate managing Kafka clusters. |
|||
|
|||
### Amazon Kinesis - The Analytics Focused One |
|||
|
|||
Amazon Kinesis is AWS's streaming platform, designed primarily for analytics and machine learning workloads rather than general messaging. |
|||
|
|||
While Kinesis excels in real-time analytics pipelines, other AWS services like SQS may be more suitable for general event-driven architecture patterns. |
|||
|
|||
**Key Strengths:** |
|||
- Real-time data processing |
|||
- Integration with AWS analytics services |
|||
- Automatic scaling |
|||
- Built-in data transformation |
|||
|
|||
**.NET Integration Example:** |
|||
```csharp |
|||
using Amazon.Kinesis; |
|||
using Amazon.Kinesis.Model; |
|||
|
|||
var kinesisClient = new AmazonKinesisClient(); |
|||
|
|||
await kinesisClient.PutRecordAsync(new PutRecordRequest |
|||
{ |
|||
StreamName = "order-stream", |
|||
Data = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(orderEvent))), |
|||
PartitionKey = orderId.ToString() |
|||
}); |
|||
``` |
|||
|
|||
**Good for:** Real-time analytics, ML pipelines, or when you're deep in the AWS ecosystem. |
|||
|
|||
### Apache Pulsar - The Ambitious One |
|||
|
|||
Apache Pulsar (originally developed by Yahoo) aims to provide comprehensive messaging capabilities with advanced features like multi-tenancy support. |
|||
|
|||
While Pulsar offers sophisticated functionality, its complexity may exceed requirements for many use cases. However, organizations needing its specific features may find the additional complexity justified. |
|||
|
|||
**Key Strengths:** |
|||
- Multi-tenancy support |
|||
- Geo-replication |
|||
- Flexible consumption models |
|||
- Built-in schema registry |
|||
|
|||
**.NET Integration Example:** |
|||
```csharp |
|||
using DotPulsar; |
|||
|
|||
await using var client = PulsarClient.Builder() |
|||
.ServiceUrl(new Uri("pulsar://localhost:6650")) |
|||
.Build(); |
|||
|
|||
var producer = client.NewProducer(Schema.String) |
|||
.Topic("order-events") |
|||
.Create(); |
|||
|
|||
await producer.Send("Hello World"); |
|||
``` |
|||
|
|||
**Consider it for:** Multi-tenant SaaS platforms or when you need geo-replication out of the box. |
|||
|
|||
## Performance Comparison |
|||
|
|||
The following comparison presents performance characteristics based on industry benchmarks and real-world implementations. Actual results will vary depending on specific use cases and configurations: |
|||
|
|||
| Feature | RabbitMQ | Kafka | Azure Service Bus | Amazon SQS | ActiveMQ | Redpanda | Kinesis | Pulsar | |
|||
|---------|----------|-------|------------------|------------|----------|----------|---------|---------| |
|||
| **Throughput** | 10K-100K msg/sec | 1M+ msg/sec | 100K+ msg/sec | Unlimited | 50K msg/sec | 1M+ msg/sec | 1M+ records/sec | 1M+ msg/sec | |
|||
| **Latency** | <10ms | <10ms | <100ms | 200-1000ms | <50ms | <10ms | <100ms | <10ms | |
|||
| **Data Retention** | Until consumed | Days to weeks | 14 days max | 14 days max | Until consumed | Days to weeks | 24hrs-365 days | Configurable | |
|||
| **Ordering Guarantees** | Queue-level | Partition-level | Session-level | FIFO queues | Queue-level | Partition-level | Shard-level | Partition-level | |
|||
| **Operational Complexity** | Medium | High | Low (managed) | Low (managed) | Medium | Low | Low (managed) | Medium | |
|||
| **Multi-tenancy** | Basic | Manual setup | Native | IAM-based | Basic | Native | IAM-based | Native | |
|||
| **.NET Client Maturity** | Excellent | Excellent | Excellent | Good | Good | Excellent (Kafka-compatible) | Good | Fair | |
|||
|
|||
## Real-World Use Cases |
|||
|
|||
The following scenarios demonstrate how different technologies perform in production environments: |
|||
|
|||
**E-commerce Order Processing** |
|||
- RabbitMQ: Effective for complex order workflows until reaching approximately 50K orders/day |
|||
- Kafka: Enables analytics teams to replay months of historical order events for analysis |
|||
- Azure Service Bus: Provides reliable order processing with minimal operational overhead for .NET-focused organizations |
|||
|
|||
**Financial Trading** |
|||
- Market data processing: Migration from traditional MQ to Kafka reduced latency from 50ms to 5ms |
|||
- Multi-tenant platforms: Pulsar's advanced features prove valuable despite requiring significant learning investment |
|||
|
|||
**IoT Projects** |
|||
- Sensor data ingestion: Kafka handles high-volume sensor data effectively but requires substantial computational resources |
|||
- Smart city implementations: Kinesis performs well for real-time analytics but creates vendor lock-in considerations |
|||
|
|||
## Selection Guidelines |
|||
|
|||
**RabbitMQ is suitable for:** |
|||
- Traditional enterprise messaging scenarios |
|||
- Teams familiar with established messaging patterns |
|||
- Applications requiring guaranteed delivery and message persistence |
|||
- Systems not requiring massive scale initially |
|||
|
|||
**Kafka works best when:** |
|||
- Analytics or machine learning workloads are involved |
|||
- High throughput is a genuine requirement |
|||
- Event replay capabilities are needed |
|||
- Teams have Kafka expertise available |
|||
|
|||
**Azure Service Bus fits well for:** |
|||
- Organizations already using Azure infrastructure |
|||
- Requirements for enterprise features with minimal operational overhead |
|||
- .NET-focused development teams |
|||
|
|||
**Amazon SQS is appropriate when:** |
|||
- AWS ecosystem integration is preferred |
|||
- Serverless architectures are being implemented |
|||
- Simple, reliable queuing is the primary requirement |
|||
|
|||
**Alternative considerations:** |
|||
- **Redpanda**: Kafka compatibility with reduced operational complexity |
|||
- **Pulsar**: Multi-tenancy requirements justify additional complexity |
|||
- **Kinesis**: Real-time analytics within the AWS ecosystem |
|||
- **ActiveMQ**: Legacy system integration requirements |
|||
|
|||
## Conclusion |
|||
|
|||
No event bus technology is universally perfect. Each option involves trade-offs, and the optimal choice varies significantly between organizations and use cases. |
|||
|
|||
**Starting Simple**: Beginning with straightforward solutions like RabbitMQ or managed services such as Azure Service Bus addresses most initial requirements effectively. Migration to more specialized platforms remains possible as needs evolve. |
|||
|
|||
**Complex Requirements**: Organizations with immediate high-scale or analytics requirements may justify Kafka's complexity from the start. However, adequate team expertise is essential for successful implementation. |
|||
|
|||
**Operational Considerations**: Technology selection should align with team capabilities. The most advanced event bus provides no value if the team cannot effectively operate and troubleshoot it during critical situations. |
|||
|
|||
**Monitoring and Reliability**: Regardless of the chosen platform, understanding failure modes and implementing comprehensive monitoring is crucial. Event buses often serve as system backbones - their failure typically cascades throughout the entire architecture. |
|||
@ -0,0 +1,192 @@ |
|||
# How Can We Apply the DRY Principle in a Better Way |
|||
|
|||
## Introduction |
|||
|
|||
In modern software development, the **DRY principle** - short for *Don't Repeat Yourself* - is one of the most important rules for writing clean and easy-to-maintain code. First introduced by Andrew Hunt and David Thomas in *The Pragmatic Programmer*, DRY focuses on removing repetition in code, documentation, and processes. The main idea is: *"Every piece of knowledge should exist in only one place in the system."* Even if this idea looks simple, using it in real projects - especially big ones - needs discipline, good planning, and the right tools. |
|||
|
|||
This article explains what the DRY principle is, why it matters, how we can use it in a better way, and how big projects can make it work. |
|||
|
|||
--- |
|||
|
|||
## What is the DRY Principle? |
|||
|
|||
The DRY principle is about reducing repetition. Repetition can happen in many forms: |
|||
|
|||
- **Code repetition:** Writing the same logic in different functions, classes, or modules. |
|||
|
|||
- **Business logic repetition:** Having the same business rules in different parts of the system. |
|||
|
|||
- **Configuration repetition:** Keeping multiple versions of the same settings or constants. |
|||
|
|||
- **Documentation repetition:** Copying the same explanations across files, which later become inconsistent. |
|||
|
|||
When teams follow DRY, each piece of knowledge exists only once, making the system easier to update and lowering the chance of mistakes. |
|||
|
|||
--- |
|||
|
|||
## Why is DRY Important? |
|||
|
|||
1. **Easy Maintenance** |
|||
If some logic changes, we only need to update it in one place instead of many places. |
|||
|
|||
2. **Consistency** |
|||
DRY helps avoid bugs caused by different versions of the same logic. |
|||
|
|||
3. **Better Growth** |
|||
DRY codebases are smaller and more modular, so they grow more easily. |
|||
|
|||
4. **Teamwork** |
|||
In large teams, DRY reduces confusion and makes it easier for everyone to understand the system. |
|||
|
|||
--- |
|||
|
|||
## Common Mistakes with DRY |
|||
|
|||
Sometimes developers use DRY in the wrong way. Trying too hard to remove repetition can create very complex code that is difficult to read. For example: |
|||
|
|||
- **Abstracting too early:** Generalizing code before real patterns appear can cause confusion. |
|||
|
|||
- **Over-engineering:** Building tools or frameworks for problems that only happen once. |
|||
|
|||
- **Too much connection:** Forcing different modules to share code when they should stay independent. |
|||
|
|||
The goal of DRY is not to remove *all* repetition, but to remove *meaningless repetition* that makes code harder to maintain. |
|||
|
|||
--- |
|||
|
|||
## How Can We Use DRY Better? |
|||
|
|||
### 1. Find Repetition Early |
|||
|
|||
Tools like **linters, static analyzers, and code reviews** help find repeated logic. Some tools can even automatically detect duplicate code. |
|||
|
|||
### 2. Create Reusable Components |
|||
|
|||
Instead of copying code, move shared logic into: |
|||
|
|||
- Functions or methods |
|||
|
|||
- Utility classes |
|||
|
|||
- Shared modules or libraries |
|||
|
|||
- Configuration files (for constants) |
|||
|
|||
**Example:** |
|||
|
|||
```python |
|||
# Not DRY |
|||
send_email_to_admin(subject, body) |
|||
send_email_to_user(subject, body) |
|||
|
|||
# DRY |
|||
send_email(recipient_type, subject, body) |
|||
``` |
|||
|
|||
### 3. Keep Documentation in One Place |
|||
|
|||
Do not copy the same documentation to many files. Use wikis, shared docs, or API tools that pull from one single source. |
|||
|
|||
### 4. Use Frameworks and Standards |
|||
|
|||
Frameworks like **Spring Boot, ASP.NET, or Django** give built-in ways to follow DRY, so developers don’t have to repeat the same patterns. |
|||
|
|||
### 5. Automate Tasks with High Repetition |
|||
|
|||
Instead of writing the same boilerplate code again and again, use: |
|||
|
|||
- Code generators |
|||
|
|||
- CI/CD pipelines |
|||
|
|||
- Infrastructure as Code (IaC) |
|||
|
|||
--- |
|||
|
|||
## Practical Steps to Apply DRY in Daily Work |
|||
|
|||
Applying DRY is not only about big design decisions; it is also about small habits in daily coding. Some practical steps include: |
|||
|
|||
- **Review before copy-paste:** When you feel the need to copy code, stop and ask if it can be moved to a shared method or module. |
|||
|
|||
- **Write helper functions:** If you see similar logic more than twice, create a helper function instead of repeating it. |
|||
|
|||
- **Use clear naming:** Good names for functions and variables make shared code easier to understand and reuse. |
|||
|
|||
- **Communicate with the team:** Before creating a new utility or feature, check if something similar already exists in the project. |
|||
|
|||
- **Refactor regularly:** Small refactors during development help keep code clean and reduce hidden repetition. |
|||
|
|||
These small actions in daily work make it easier to follow DRY naturally, without waiting for big rewrites. |
|||
|
|||
--- |
|||
|
|||
## DRY in Large Projects |
|||
|
|||
In big systems, DRY is harder but also more important. Large codebases often involve many developers working at the same time, which increases the chance of duplication. |
|||
|
|||
### How Big Teams Use DRY: |
|||
|
|||
1. **Modular Architectures** |
|||
Breaking applications into services, libraries, or packages helps to centralize shared functionality. |
|||
|
|||
2. **Shared Libraries and APIs** |
|||
Instead of rewriting logic, teams create internal libraries that act as the single source of truth. |
|||
|
|||
3. **Code Reviews and Pair Programming** |
|||
Developers check each other’s work to find repetition early. |
|||
|
|||
4. **Automated Tools** |
|||
Tools like **SonarQube, PMD, and ESLint** help detect duplicate code. |
|||
|
|||
5. **Clear Ownership** |
|||
Assigning owners for modules helps keep consistency and avoids duplication. |
|||
|
|||
--- |
|||
|
|||
## Example: DRY in a Large Project |
|||
|
|||
Imagine a large e-commerce platform with multiple teams: |
|||
|
|||
- **Team A** handles user authentication. |
|||
|
|||
- **Team B** handles orders. |
|||
|
|||
- **Team C** handles payments. |
|||
|
|||
Without DRY, both Team B and Team C might create their own email notification system. With DRY, they share a **Notification Service**: |
|||
|
|||
```csharp |
|||
// Shared Notification Service |
|||
public class NotificationService { |
|||
public void Send(string recipient, string subject, string message) { |
|||
// Unified logic for sending notifications |
|||
} |
|||
} |
|||
|
|||
// Usage in Orders Module |
|||
notificationService.Send(order.CustomerEmail, "Order Confirmed", "Your order has been placed."); |
|||
|
|||
// Usage in Payments Module |
|||
notificationService.Send(payment.CustomerEmail, "Payment Received", "Your payment was successful."); |
|||
``` |
|||
|
|||
This way, if the email format changes, only the **Notification Service** needs updating. |
|||
|
|||
--- |
|||
|
|||
## Conclusion |
|||
|
|||
The DRY principle is still one of the most important rules in modern software development. While the rule sounds simple - *don’t repeat yourself* - using it in the right way requires careful thinking. Wrong use of DRY can make things more complex, but correct use of DRY improves maintainability, growth, and teamwork. |
|||
|
|||
To use DRY better: |
|||
|
|||
- Focus on meaningful repetition. |
|||
|
|||
- Create reusable components. |
|||
|
|||
- Use frameworks, automation, and shared libraries. |
|||
|
|||
- Encourage teamwork and code reviews. |
|||
|
|||
In large projects, DRY works best with strong architecture, helpful tools, and discipline. If teams apply these practices, they can build cleaner, more maintainable, and scalable systems. |
|||
@ -0,0 +1,41 @@ |
|||
@using Volo.Docs.TableOfContents |
|||
@model List<TocItem> |
|||
@{ |
|||
if (Model == null || Model.Count == 0) |
|||
{ |
|||
return; |
|||
} |
|||
} |
|||
|
|||
<ul class="nav nav-pills flex-column"> |
|||
@foreach (var item in Model) |
|||
{ |
|||
<li class="nav-item @(item.Children.Any() ? "toc-item-has-children" : "")"> |
|||
<a class="nav-link" href="#@item.Heading.Id">@item.Heading.Text</a> |
|||
|
|||
@if (item.Children.Any()) |
|||
{ |
|||
RenderChildrenRecursive(item.Children, 1); |
|||
} |
|||
</li> |
|||
} |
|||
</ul> |
|||
|
|||
@{ |
|||
void RenderChildrenRecursive(List<TocItem> children, int depth) |
|||
{ |
|||
<ul class="nav nav-pills flex-column toc-depth-@depth"> |
|||
@foreach (var child in children) |
|||
{ |
|||
<li class="nav-item @(child.Children.Any() ? "toc-item-has-children" : "")"> |
|||
<a class="nav-link" href="#@child.Heading.Id">@child.Heading.Text</a> |
|||
|
|||
@if (child.Children.Any()) |
|||
{ |
|||
RenderChildrenRecursive(child.Children, depth + 1); |
|||
} |
|||
</li> |
|||
} |
|||
</ul> |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace Volo.Docs.TableOfContents; |
|||
|
|||
public interface ITocGeneratorService : IApplicationService |
|||
{ |
|||
List<TocHeading> GenerateTocHeadings(string markdownContent); |
|||
|
|||
List<TocItem> GenerateTocItems(List<TocHeading> tocHeadings, int topLevel, int levelCount); |
|||
|
|||
int GetTopLevel(List<TocHeading> tocHeadings); |
|||
|
|||
List<TocItem> GenerateTocItems(string markdownContent, int levelCount, int? topLevel = null); |
|||
} |
|||
@ -0,0 +1,209 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using Markdig; |
|||
using Markdig.Extensions.AutoIdentifiers; |
|||
using Markdig.Renderers.Html; |
|||
using Markdig.Syntax; |
|||
using Markdig.Syntax.Inlines; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Docs.TableOfContents; |
|||
|
|||
public class TocGeneratorService : ITocGeneratorService, ITransientDependency |
|||
{ |
|||
private const int MinHeadingLevel = 1; |
|||
private const int MaxHeadingLevel = 6; |
|||
|
|||
public virtual List<TocHeading> GenerateTocHeadings(string markdownContent) |
|||
{ |
|||
if (markdownContent.IsNullOrWhiteSpace()) |
|||
{ |
|||
return new List<TocHeading>(); |
|||
} |
|||
|
|||
var markdownPipeline = CreateMarkdownPipeline(); |
|||
var document = Markdig.Markdown.Parse(markdownContent, markdownPipeline); |
|||
var headingBlocks = document.Descendants<HeadingBlock>(); |
|||
|
|||
return headingBlocks |
|||
.Select(CreateTocHeading) |
|||
.ToList(); |
|||
} |
|||
|
|||
public virtual List<TocItem> GenerateTocItems(List<TocHeading> tocHeadings, int topLevel, int levelCount) |
|||
{ |
|||
var maxLevel = GetMaxLevel(tocHeadings, topLevel, levelCount); |
|||
|
|||
var filteredHeadings = tocHeadings |
|||
.Where(heading => heading.Level >= topLevel && heading.Level <= maxLevel) |
|||
.ToList(); |
|||
|
|||
return BuildHierarchicalStructure(filteredHeadings, topLevel); |
|||
} |
|||
|
|||
public virtual int GetTopLevel(List<TocHeading> headings) |
|||
{ |
|||
return Enumerable.Range(MinHeadingLevel, MaxHeadingLevel) |
|||
.FirstOrDefault(level => headings.Count(h => h.Level == level) > 1, MinHeadingLevel); |
|||
} |
|||
|
|||
public virtual List<TocItem> GenerateTocItems(string markdownContent, int levelCount, int? topLevel = null) |
|||
{ |
|||
var headings = GenerateTocHeadings(markdownContent); |
|||
if (headings.Count == 0) |
|||
{ |
|||
return new List<TocItem>(); |
|||
} |
|||
|
|||
var resolvedTopLevel = topLevel ?? GetTopLevel(headings); |
|||
return GenerateTocItems(headings, resolvedTopLevel, levelCount); |
|||
} |
|||
|
|||
protected virtual MarkdownPipeline CreateMarkdownPipeline() |
|||
{ |
|||
return new MarkdownPipelineBuilder() |
|||
.UseAutoIdentifiers(AutoIdentifierOptions.GitHub) |
|||
.UseAdvancedExtensions() |
|||
.Build(); |
|||
} |
|||
|
|||
protected virtual TocHeading CreateTocHeading(HeadingBlock headingBlock) |
|||
{ |
|||
var plainText = GetPlainText(headingBlock.Inline); |
|||
var id = headingBlock.GetAttributes().Id; |
|||
return new TocHeading(headingBlock.Level, plainText, id); |
|||
} |
|||
|
|||
protected virtual List<TocItem> BuildHierarchicalStructure(List<TocHeading> headings, int topLevel) |
|||
{ |
|||
var result = new List<TocItem>(); |
|||
|
|||
for (var i = 0; i < headings.Count; i++) |
|||
{ |
|||
var currentHeading = headings[i]; |
|||
|
|||
if (currentHeading.Level != topLevel) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var children = GetDirectChildren(headings, i, currentHeading.Level); |
|||
result.Add(new TocItem(currentHeading, children)); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
protected virtual List<TocItem> GetDirectChildren(List<TocHeading> allHeadings, int parentIndex, int parentLevel) |
|||
{ |
|||
var targetChildLevel = parentLevel + 1; |
|||
var children = new List<TocItem>(); |
|||
|
|||
for (var i = parentIndex + 1; i < allHeadings.Count; i++) |
|||
{ |
|||
var heading = allHeadings[i]; |
|||
|
|||
// Stop if we encounter a heading at the same level or higher than parent
|
|||
if (heading.Level <= parentLevel) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
// Only process direct children (not grandchildren)
|
|||
if (heading.Level != targetChildLevel) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var grandChildren = GetDirectChildren(allHeadings, i, heading.Level); |
|||
children.Add(new TocItem(heading, grandChildren)); |
|||
} |
|||
|
|||
return children; |
|||
} |
|||
|
|||
protected virtual string GetPlainText(ContainerInline container) |
|||
{ |
|||
if (container == null) |
|||
{ |
|||
return string.Empty; |
|||
} |
|||
|
|||
// Optimization for simple case with single literal inline
|
|||
if (HasExactCount(container, 1) && container.First() is LiteralInline singleLiteral) |
|||
{ |
|||
return singleLiteral.Content.ToString(); |
|||
} |
|||
|
|||
return ProcessInlineContent(container); |
|||
} |
|||
|
|||
protected virtual string ProcessInlineContent(ContainerInline container) |
|||
{ |
|||
var textBuilder = new StringBuilder(); |
|||
var processingQueue = new Queue<Inline>(); |
|||
|
|||
// Enqueue all initial inlines
|
|||
foreach (var inline in container) |
|||
{ |
|||
processingQueue.Enqueue(inline); |
|||
} |
|||
|
|||
// Process each inline in the queue
|
|||
while (processingQueue.Count > 0) |
|||
{ |
|||
var currentInline = processingQueue.Dequeue(); |
|||
AppendInlineText(currentInline, textBuilder, processingQueue); |
|||
} |
|||
|
|||
return textBuilder.ToString(); |
|||
} |
|||
|
|||
protected virtual void AppendInlineText(Inline inline, StringBuilder builder, Queue<Inline> processingQueue) |
|||
{ |
|||
switch (inline) |
|||
{ |
|||
case LiteralInline literal: |
|||
builder.Append(literal.Content); |
|||
break; |
|||
|
|||
case CodeInline code: |
|||
builder.Append(code.Content); |
|||
break; |
|||
|
|||
case ContainerInline containerInline: |
|||
foreach (var childInline in containerInline) |
|||
{ |
|||
processingQueue.Enqueue(childInline); |
|||
} |
|||
break; |
|||
} |
|||
} |
|||
|
|||
protected virtual int GetMaxLevel(List<TocHeading> tocHeadings, int topLevel, int levelCount) |
|||
{ |
|||
return tocHeadings.Where(h => h.Level >= topLevel) |
|||
.Select(h => h.Level) |
|||
.Distinct() |
|||
.OrderBy(level => level) |
|||
.Skip(levelCount - 1) |
|||
.FirstOrDefault(topLevel); |
|||
} |
|||
|
|||
protected virtual bool HasExactCount<T>(IEnumerable<T> enumerable, int count) |
|||
{ |
|||
var itemCount = 0; |
|||
foreach (var _ in enumerable) |
|||
{ |
|||
itemCount++; |
|||
if (itemCount > count) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
return itemCount == count; |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.Docs.TableOfContents; |
|||
|
|||
public record TocHeading(int Level, string Text, string Id); |
|||
|
|||
public record TocItem(TocHeading Heading, List<TocItem> Children); |
|||