Browse Source

Merge branch 'dev' into net10.0

pull/23609/head
maliming 5 months ago
parent
commit
07155bc1f5
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 257
      docs/en/Community-Articles/2025-09-17-Integration-Testing-Best-Practices-for-Building-a-Robust-Application-Layer/article.md
  2. BIN
      docs/en/Community-Articles/2025-09-17-Integration-Testing-Best-Practices-for-Building-a-Robust-Application-Layer/cover.png
  3. BIN
      docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/img-1.png
  4. BIN
      docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/img-2.png
  5. BIN
      docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/img-3.png
  6. 820
      docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/post.md
  7. 608
      docs/en/Community-Articles/2025-09-18-10-Modern-HTML-CSS-Techniques-Every-Designer-Should-Know-in-2025/10-Modern-HTML-CSS-Techniques-Every-Designer-Should-Know-in-2025..md
  8. BIN
      docs/en/Community-Articles/2025-09-18-10-Modern-HTML-CSS-Techniques-Every-Designer-Should-Know-in-2025/browser-support.png
  9. 308
      docs/en/Community-Articles/2025-09-18-Building-a-Background-Job-Queue-in-ASP.NET-Core-Application/post.md
  10. BIN
      docs/en/Community-Articles/2025-09-18-Building-a-Background-Job-Queue-in-ASP.NET-Core-Application/producer-consumer.png
  11. BIN
      docs/en/Community-Articles/2025-09-18-Building-a-Background-Job-Queue-in-ASP.NET-Core-Application/structure.png
  12. 360
      docs/en/Community-Articles/2025-09-18-Distributed-Event-Buses-With-.NET-Clients/post.md
  13. 192
      docs/en/Community-Articles/2025-09-18-How-Can-We-Apply-the-DRY-Principle-in-a-Better-Way/post.md
  14. 5
      docs/en/framework/ui/angular/abp-window-service.md
  15. 70
      docs/en/framework/ui/angular/component-replacement.md
  16. 5
      docs/en/framework/ui/angular/config-state-service.md
  17. 12
      docs/en/framework/ui/angular/confirmation-service.md
  18. 3
      docs/en/framework/ui/angular/content-projection-service.md
  19. 13
      docs/en/framework/ui/angular/dom-insertion-service.md
  20. 3
      docs/en/framework/ui/angular/environment.md
  21. 3
      docs/en/framework/ui/angular/features.md
  22. 7
      docs/en/framework/ui/angular/global-features.md
  23. 38
      docs/en/framework/ui/angular/how-replaceable-components-work-with-extensions.md
  24. 13
      docs/en/framework/ui/angular/http-error-reporter-service.md
  25. 3
      docs/en/framework/ui/angular/http-requests.md
  26. 25
      docs/en/framework/ui/angular/lazy-load-service.md
  27. 11
      docs/en/framework/ui/angular/list-service.md
  28. 3
      docs/en/framework/ui/angular/localization.md
  29. 3
      docs/en/framework/ui/angular/modal.md
  30. 64
      docs/en/framework/ui/angular/modifying-the-menu.md
  31. 3
      docs/en/framework/ui/angular/page-alerts.md
  32. 2
      docs/en/framework/ui/angular/page-component.md
  33. 58
      docs/en/framework/ui/angular/permission-management-component-replacement.md
  34. 9
      docs/en/framework/ui/angular/permission-management.md
  35. 29
      docs/en/framework/ui/angular/router-events.md
  36. 5
      docs/en/framework/ui/angular/service-proxies.md
  37. 3
      docs/en/framework/ui/angular/settings.md
  38. 21
      docs/en/framework/ui/angular/subscription-service.md
  39. 45
      docs/en/framework/ui/angular/theming.md
  40. 35
      docs/en/framework/ui/angular/toaster-service.md
  41. 3
      docs/en/framework/ui/angular/track-by-service.md
  42. 18
      docs/en/modules/setting-management.md
  43. 5
      docs/en/tutorials/book-store/part-02.md
  44. 82
      docs/en/tutorials/book-store/part-03.md
  45. 12
      docs/en/tutorials/book-store/part-09.md
  46. 12
      docs/en/tutorials/book-store/part-10.md
  47. 7
      docs/en/tutorials/microservice/part-05.md
  48. 11
      docs/en/tutorials/todo/layered/index.md
  49. 11
      docs/en/tutorials/todo/single-layer/index.md
  50. 45
      docs/en/ui-themes/lepton-x-lite/angular.md
  51. 18
      docs/en/ui-themes/lepton-x/angular-customization.md
  52. 7
      framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/wwwroot/libs/bootstrap/css/bootstrap.min.css
  53. 9
      latest-versions.json
  54. 2
      modules/docs/src/Volo.Docs.Web/Markdown/MarkDigMarkdownConverter.cs
  55. 5
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml
  56. 20
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs
  57. 41
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/TableOfContents.cshtml
  58. 2
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/index.js
  59. 12
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/index.scss
  60. 43
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Scripts/vs.js
  61. 15
      modules/docs/src/Volo.Docs.Web/TableOfContents/ITocGeneratorService.cs
  62. 209
      modules/docs/src/Volo.Docs.Web/TableOfContents/TocGeneratorService.cs
  63. 7
      modules/docs/src/Volo.Docs.Web/TableOfContents/TocHeading.cs
  64. 12
      modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor
  65. 1
      npm/ng-packs/packages/core/src/lib/models/rest.ts
  66. 3
      npm/ng-packs/packages/core/src/lib/services/rest.service.ts

257
docs/en/Community-Articles/2025-09-17-Integration-Testing-Best-Practices-for-Building-a-Robust-Application-Layer/article.md

@ -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.

BIN
docs/en/Community-Articles/2025-09-17-Integration-Testing-Best-Practices-for-Building-a-Robust-Application-Layer/cover.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

BIN
docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/img-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/img-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/img-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

820
docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/post.md

@ -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
![UI Preview](img-1.png)
_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.
![Code base](img-2.png)
### 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](img-3.png)
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. 🚀

608
docs/en/Community-Articles/2025-09-18-10-Modern-HTML-CSS-Techniques-Every-Designer-Should-Know-in-2025/10-Modern-HTML-CSS-Techniques-Every-Designer-Should-Know-in-2025..md

@ -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. 🚀

BIN
docs/en/Community-Articles/2025-09-18-10-Modern-HTML-CSS-Techniques-Every-Designer-Should-Know-in-2025/browser-support.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

308
docs/en/Community-Articles/2025-09-18-Building-a-Background-Job-Queue-in-ASP.NET-Core-Application/post.md

@ -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.
![Background Job Queue Structure in ASP.NET Core](./structure.png)
## 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.
![Producer-Consumer Background Job Queue Structure](./producer-consumer.png)
### 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)

BIN
docs/en/Community-Articles/2025-09-18-Building-a-Background-Job-Queue-in-ASP.NET-Core-Application/producer-consumer.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

BIN
docs/en/Community-Articles/2025-09-18-Building-a-Background-Job-Queue-in-ASP.NET-Core-Application/structure.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 KiB

360
docs/en/Community-Articles/2025-09-18-Distributed-Event-Buses-With-.NET-Clients/post.md

@ -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.

192
docs/en/Community-Articles/2025-09-18-How-Can-We-Apply-the-DRY-Principle-in-a-Better-Way/post.md

@ -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.

5
docs/en/framework/ui/angular/abp-window-service.md

@ -13,10 +13,9 @@ Firstly, ensure that the service is injected into the component or any other Ang
```js
import { AbpWindowService } from '@abp/ng.core';
import { inject } from '@angular/core';
constructor(private abpWindowService: AbpWindowService) { }
// or
// private abpWindowService = inject(AbpWindowService)
private abpWindowService = inject(AbpWindowService)
```
### Downloading a Blob

70
docs/en/framework/ui/angular/component-replacement.md

@ -13,13 +13,14 @@ Then, open the `app.component.ts` and execute the `add` method of `ReplaceableCo
```js
import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum
import { Component, inject } from '@angular/core';
//...
@Component(/* component metadata */)
export class AppComponent {
constructor(
private replaceableComponents: ReplaceableComponentsService, // injected the service
) {
private replaceableComponents = inject(ReplaceableComponentsService);
constructor() {
this.replaceableComponents.add({
component: YourNewRoleComponent,
key: eIdentityComponents.Roles,
@ -56,12 +57,13 @@ Open `app.component.ts` in `src/app` folder and modify it as shown below:
import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents enum for component keys
import { MyApplicationLayoutComponent } from './my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent
import { Component, inject } from '@angular/core';
@Component(/* component metadata */)
export class AppComponent {
constructor(
private replaceableComponents: ReplaceableComponentsService, // injected the service
) {
private replaceableComponents = inject(ReplaceableComponentsService);
constructor() {
this.replaceableComponents.add({
component: MyApplicationLayoutComponent,
key: eThemeBasicComponents.ApplicationLayout,
@ -213,6 +215,23 @@ function configureRoutes() {
![LogoComponent](./images/logo-component.png)
Note
- If your goal is only to change the logo image or application name, you don't need to replace the component. Prefer providing the logo via `@abp/ng.theme.shared` so all themes/components consume it consistently:
```ts
// app.config.ts
import { provideLogo, withEnvironmentOptions } from '@abp/ng.theme.shared';
import { environment } from './environments/environment';
export const appConfig: ApplicationConfig = {
providers: [
provideLogo(withEnvironmentOptions(environment)),
],
};
```
If you still want to completely replace the logo component UI, follow the steps below:
Run the following command in `angular` folder to create a new component called `LogoComponent`.
```bash
@ -251,15 +270,15 @@ import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeB
@Component(/* component metadata */)
export class AppComponent implements OnInit {
constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
private replaceableComponents = inject(ReplaceableComponentsService);
ngOnInit() {
//...
this.replaceableComponents.add({
component: LogoComponent,
key: eThemeBasicComponents.Logo,
});
component: LogoComponent,
key: eThemeBasicComponents.Logo,
});
}
}
```
@ -437,15 +456,15 @@ import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeB
@Component(/* component metadata */)
export class AppComponent implements OnInit {
constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
private replaceableComponents = inject(ReplaceableComponentsService);
ngOnInit() {
//...
this.replaceableComponents.add({
component: RoutesComponent,
key: eThemeBasicComponents.Routes,
});
component: RoutesComponent,
key: eThemeBasicComponents.Routes,
});
}
}
```
@ -476,7 +495,7 @@ import {
SessionStateService,
LocalizationPipe
} from '@abp/ng.core';
import { Component, Inject } from '@angular/core';
import { Component, inject, Inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
@ -495,9 +514,13 @@ import snq from 'snq';
]
})
export class NavItemsComponent {
private configState = inject(ConfigStateService);
private authService = inject(AuthService);
private sessionState = inject(SessionStateService);
@Inject(NAVIGATE_TO_MANAGE_PROFILE) public navigateToManageProfile: any;
currentUser$: Observable<CurrentUserDto> = this.configState.getOne$('currentUser');
selectedTenant$ = this.sessionState.getTenant$();
languages$: Observable<LanguageInfo[]> = this.configState.getDeep$('localization.languages');
get smallScreen(): boolean {
@ -530,13 +553,6 @@ export class NavItemsComponent {
return this.sessionState.getLanguage();
}
constructor(
@Inject(NAVIGATE_TO_MANAGE_PROFILE) public navigateToManageProfile,
private configState: ConfigStateService,
private authService: AuthService,
private sessionState: SessionStateService
) {}
onChangeLang(cultureName: string) {
this.sessionState.setLanguage(cultureName);
}
@ -652,15 +668,15 @@ import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeB
@Component(/* component metadata */)
export class AppComponent implements OnInit {
constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
private replaceableComponents = inject(ReplaceableComponentsService);
ngOnInit() {
//...
this.replaceableComponents.add({
component: NavItemsComponent,
key: eThemeBasicComponents.NavItems,
});
component: NavItemsComponent,
key: eThemeBasicComponents.NavItems,
});
}
}
```

5
docs/en/framework/ui/angular/config-state-service.md

@ -8,12 +8,13 @@ In order to use the `ConfigStateService` you must inject it in your class as a d
```js
import { ConfigStateService } from '@abp/ng.core';
import { inject } from '@angular/core';
@Component({
/* class metadata here */
})
class DemoComponent {
private config = inject(ConfigStateService);
private config = inject(ConfigStateService);
}
```
@ -126,10 +127,12 @@ You can get the application configuration response and set the `ConfigStateServi
```js
import { AbpApplicationConfigurationService, ConfigStateService } from '@abp/ng.core';
import { inject } from '@angular/core';
private abpApplicationConfigurationService = inject(AbpApplicationConfigurationService);
private config = inject(ConfigStateService);
constructor() {
this.abpApplicationConfigurationService.get({ includeLocalizationResources: false }).subscribe(config => {
this.config.setState(config);

12
docs/en/framework/ui/angular/confirmation-service.md

@ -8,12 +8,13 @@ You do not have to provide the `ConfirmationService` at component level, because
```js
import { ConfirmationService } from '@abp/ng.theme.shared';
import { inject } from '@angular/core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private confirmation: ConfirmationService) {}
private confirmation = inject(ConfirmationService);
}
```
@ -36,8 +37,10 @@ You can subscribe to the confirmation closing event like below:
```js
import { Confirmation, ConfirmationService } from '@abp/ng.theme.shared';
import { inject } from '@angular/core';
constructor(private confirmation: ConfirmationService) {}
// inside the class:
private confirmation = inject(ConfirmationService);
this.confirmation
.warn('::WillBeDeleted', { key: '::AreYouSure', defaultValue: 'Are you sure?' })
@ -132,7 +135,10 @@ this.confirmation.clear();
You can change icons with the `withConfirmationIcon()` method inside `provideAbpThemeShared` function in the app.config.ts. The changes will affect all confirmation popup in the project.
```ts
import { provideAbpThemeShared, withConfirmationIcon } from '@abp/ng.theme.shared';
import {
provideAbpThemeShared,
withConfirmationIcon,
} from "@abp/ng.theme.shared";
export const appConfig: ApplicationConfig = {
providers: [

3
docs/en/framework/ui/angular/content-projection-service.md

@ -8,12 +8,13 @@ You do not have to provide the `ContentProjectionService` at module or component
```js
import { ContentProjectionService } from '@abp/ng.core';
import { inject } from '@angular/core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private contentProjectionService: ContentProjectionService) {}
private contentProjectionService = inject(ContentProjectionService);
}
```

13
docs/en/framework/ui/angular/dom-insertion-service.md

@ -8,12 +8,13 @@ You do not have to provide the `DomInsertionService` at module or component leve
```js
import { DomInsertionService } from '@abp/ng.core';
import { inject } from '@angular/core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private domInsertionService: DomInsertionService) {}
private domInsertionService = inject(DomInsertionService);
}
```
@ -27,12 +28,13 @@ The first parameter of `insertContent` method expects a `ContentStrategy`. If yo
```js
import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core';
import { inject } from '@angular/core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private domInsertionService: DomInsertionService) {}
private domInsertionService = inject(DomInsertionService);
ngOnInit() {
const scriptElement = this.domInsertionService.insertContent(
@ -54,12 +56,13 @@ If you pass a `StyleContentStrategy` instance as the first parameter of `insertC
```js
import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core';
import { inject } from '@angular/core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private domInsertionService: DomInsertionService) {}
private domInsertionService = inject(DomInsertionService);
ngOnInit() {
const styleElement = this.domInsertionService.insertContent(
@ -81,15 +84,15 @@ If you pass the inserted `HTMLScriptElement` or `HTMLStyleElement` element as th
```js
import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core';
import { inject } from '@angular/core';
@Component({
/* class metadata here */
})
class DemoComponent {
private domInsertionService = inject(DomInsertionService);
private styleElement: HTMLStyleElement;
constructor(private domInsertionService: DomInsertionService) {}
ngOnInit() {
this.styleElement = this.domInsertionService.insertContent(
CONTENT_STRATEGY.AppendStyleToHead('body {margin: 0;}')

3
docs/en/framework/ui/angular/environment.md

@ -132,7 +132,8 @@ export const appConfig: ApplicationConfig = {
In order to use the `EnvironmentService` you must inject it in your class as a dependency.
```js
import { EnvironmentService } from '@abp/ng.core';
import { EnvironmentService } from '@abp/ng.core';
import { inject } from '@angular/core';
@Component({
/* class metadata here */

3
docs/en/framework/ui/angular/features.md

@ -10,12 +10,13 @@ To use the `ConfigStateService`, you must inject it in your class as a dependenc
```js
import { ConfigStateService } from '@abp/ng.core';
import { inject } from '@angular/core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private config: ConfigStateService) {}
private config = inject(ConfigStateService);
}
```

7
docs/en/framework/ui/angular/global-features.md

@ -9,14 +9,14 @@ The `ConfigStateService.getGlobalFeatures` API allows you to get the enabled fea
````js
import { ConfigStateService } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core';
@Component({
/* class metadata here */
})
class DemoComponent implements OnInit {
constructor(private config: ConfigStateService) {}
private config = inject(ConfigStateService);
ngOnInit(): void {
// Gets all enabled global features.
const getGlobalFeatures = this.config.getGlobalFeatures();
@ -44,4 +44,3 @@ class DemoComponent implements OnInit {
}
}

38
docs/en/framework/ui/angular/how-replaceable-components-work-with-extensions.md

@ -17,6 +17,10 @@ yarn ng generate component my-roles/my-roles --flat --export
Open the generated `src/app/my-roles/my-roles.component.ts` file and replace its content with the following:
```js
import { Component, Injector, inject, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { finalize } from 'rxjs/operators';
import { ListService, PagedAndSortedResultRequestDto, PagedResultDto } from '@abp/ng.core';
import { eIdentityComponents, RolesComponent } from '@abp/ng.identity';
import { IdentityRoleDto, IdentityRoleService } from '@abp/ng.identity/proxy';
@ -27,9 +31,6 @@ import {
FormPropData,
generateFormFromProps
} from '@abp/ng.components/extensible';
import { Component, Injector, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { finalize } from 'rxjs/operators';
@Component({
selector: 'app-my-roles',
@ -40,13 +41,18 @@ import { finalize } from 'rxjs/operators';
provide: EXTENSIONS_IDENTIFIER,
useValue: eIdentityComponents.Roles,
},
{
provide: RolesComponent,
useExisting: MyRolesComponent
}
{
provide: RolesComponent,
useExisting: MyRolesComponent,
},
],
})
export class MyRolesComponent implements OnInit {
public readonly list = inject<ListService<PagedAndSortedResultRequestDto>>(ListService);
protected readonly confirmationService = inject(ConfirmationService);
protected readonly injector = inject(Injector);
protected readonly service = inject(IdentityRoleService);
data: PagedResultDto<IdentityRoleDto> = { items: [], totalCount: 0 };
form: FormGroup;
@ -63,17 +69,10 @@ export class MyRolesComponent implements OnInit {
permissionManagementKey = ePermissionManagementComponents.PermissionManagement;
onVisiblePermissionChange = event => {
onVisiblePermissionChange = (event) => {
this.visiblePermissions = event;
};
constructor(
public readonly list: ListService<PagedAndSortedResultRequestDto>,
protected confirmationService: ConfirmationService,
protected injector: Injector,
protected service: IdentityRoleService,
) {}
ngOnInit() {
this.hookToQuery();
}
@ -253,13 +252,18 @@ export class MyRolesModule {}
As the last step, it is needs to be replaced the `RolesComponent` with the `MyRolesComponent`. Open the `app.component.ts` and modify its content as shown below:
```js
import { Component, inject } from '@angular/core';
import { ReplaceableComponentsService } from '@abp/ng.core';
import { eIdentityComponents } from '@abp/ng.identity';
import { MyRolesComponent } from './my-roles/my-roles.component';
@Component(/* component metadata */)
@Component({
// component metadata
})
export class AppComponent {
constructor(private replaceableComponents: ReplaceableComponentsService) {
private replaceableComponents = inject(ReplaceableComponentsService);
constructor() {
this.replaceableComponents.add({ component: MyRolesComponent, key: eIdentityComponents.Roles });
}
}

13
docs/en/framework/ui/angular/http-error-reporter-service.md

@ -7,13 +7,14 @@ See the example below to learn how to report an error:
```ts
import { HttpErrorReporterService } from '@abp/ng.core';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Injectable, inject } from '@angular/core';
import { of } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class SomeService {
constructor(private http: HttpClient, private httpErrorReporter: HttpErrorReporterService) {}
private http = inject(HttpClient);
private httpErrorReporter = inject(HttpErrorReporterService);
getData() {
return this.http.get('http://example.com/get-data').pipe(
@ -31,17 +32,19 @@ See the following example to learn listening the reported errors:
```ts
import { HttpErrorReporterService } from '@abp/ng.core';
import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Injectable, inject } from '@angular/core';
@Injectable()
export class MyErrorHandler {
constructor(private httpErrorReporter: HttpErrorReporterService) {
private httpErrorReporter = inject(HttpErrorReporterService);
constructor() {
this.handleErrors();
}
handleErrors() {
this.httpErrorReporter.reporter$.subscribe((err: HttpErrorResponse) => {
// handle the errors here
// handle the errors here
});
}
}

3
docs/en/framework/ui/angular/http-requests.md

@ -31,12 +31,13 @@ In order to use the `RestService`, you must inject it in your class as a depende
```js
import { RestService } from '@abp/ng.core';
import { inject } from '@angular/core';
@Injectable({
/* class metadata here */
})
class DemoService {
constructor(private rest: RestService) {}
private rest = inject(RestService);
}
```

25
docs/en/framework/ui/angular/lazy-load-service.md

@ -11,12 +11,13 @@ You do not have to provide the `LazyLoadService` at module or component level, b
```js
import { LazyLoadService } from '@abp/ng.core';
import { inject } from '@angular/core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private lazyLoadService: LazyLoadService) {}
private lazyLoadService = inject(LazyLoadService);
}
```
@ -35,6 +36,7 @@ The first parameter of `load` method expects a `LoadingStrategy`. If you pass a
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
import { inject } from '@angular/core';
@Component({
template: `
@ -42,11 +44,11 @@ import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
`
})
class DemoComponent {
private lazyLoadService = inject(LazyLoadService);
libraryLoaded$ = this.lazyLoadService.load(
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/some-library.js'),
);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
@ -64,6 +66,7 @@ If you pass a `StyleLoadingStrategy` instance as the first parameter of `load` m
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
import { inject } from '@angular/core';
@Component({
template: `
@ -71,11 +74,11 @@ import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
`
})
class DemoComponent {
private lazyLoadService = inject(LazyLoadService);
stylesLoaded$ = this.lazyLoadService.load(
LOADING_STRATEGY.AppendAnonymousStyleToHead('/assets/some-styles.css'),
);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
@ -114,7 +117,8 @@ A common usecase is **loading multiple scripts and/or styles before using a feat
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
import { frokJoin } from 'rxjs';
import { forkJoin } from 'rxjs';
import { inject } from '@angular/core';
@Component({
template: `
@ -122,6 +126,8 @@ import { frokJoin } from 'rxjs';
`
})
class DemoComponent {
private lazyLoad = inject(LazyLoadService);
private stylesLoaded$ = forkJoin(
this.lazyLoad.load(
LOADING_STRATEGY.PrependAnonymousStyleToHead('/assets/library-dark-theme.css'),
@ -141,8 +147,6 @@ class DemoComponent {
);
scriptsAndStylesLoaded$ = forkJoin(this.scriptsLoaded$, this.stylesLoaded$);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
@ -156,6 +160,7 @@ Another frequent usecase is **loading dependent scripts in order**:
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
import { concat } from 'rxjs';
import { inject } from '@angular/core';
@Component({
template: `
@ -163,6 +168,8 @@ import { concat } from 'rxjs';
`
})
class DemoComponent {
private lazyLoad = inject(LazyLoadService);
scriptsLoaded$ = concat(
this.lazyLoad.load(
LOADING_STRATEGY.PrependAnonymousScriptToHead('/assets/library.js'),
@ -171,8 +178,6 @@ class DemoComponent {
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/script-that-requires-library.js'),
),
);
constructor(private lazyLoadService: LazyLoadService) {}
}
```

11
docs/en/framework/ui/angular/list-service.md

@ -12,6 +12,7 @@
import { ListService } from '@abp/ng.core';
import { BookDto } from '../models';
import { BookService } from '../services';
import { inject } from '@angular/core';
@Component({
/* class metadata here */
@ -32,10 +33,10 @@ class BookComponent {
items: BookDto[] = [];
count = 0;
constructor(
public readonly list: ListService,
private bookService: BookService,
) {
public readonly list = inject(ListService);
private bookService = inject(BookService);
constructor() {
// change ListService defaults here
this.list.maxResultCount = 20;
}
@ -78,7 +79,7 @@ You can extend the query parameter of the `ListService`'s `hookToQuery` method.
Firstly, you should pass your own type to `ListService` as shown below:
```typescript
constructor(public readonly list: ListService<BooksSearchParamsDto>) { }
public readonly list = inject(ListService<BooksSearchParamsDto>);
```
Then update the `bookStreamCreator` constant like following:

3
docs/en/framework/ui/angular/localization.md

@ -71,9 +71,10 @@ First of all you should import the `LocalizationService` from **@abp/ng.core**
```js
import { LocalizationService } from '@abp/ng.core';
import { inject } from '@angular/core';
class MyClass {
constructor(private localizationService: LocalizationService) {}
private localizationService = inject(LocalizationService);
}
```

3
docs/en/framework/ui/angular/modal.md

@ -132,12 +132,12 @@ See an example form inside a modal:
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { inject } from '@angular/core';
@Component(/* component metadata */)
export class BookComponent {
private fb = inject(FormBuilder);
private service = inject(BookService);
form = this.fb.group({
author: [null, [Validators.required]],
name: [null, [Validators.required]],
@ -147,7 +147,6 @@ export class BookComponent {
});
inProgress: boolean;
isModalOpen: boolean;
save() {

64
docs/en/framework/ui/angular/modifying-the-menu.md

@ -5,7 +5,7 @@ The menu is inside the `ApplicationLayoutComponent` in the @abp/ng.theme.basic p
## How to Add a Logo
The `logoUrl` property in the environment variables is the url of the logo.
The `logoUrl` property in the environment variables is the url of the logo.
You can add your logo to `src/assets` folder and set the `logoUrl` as shown below:
@ -20,6 +20,25 @@ export const environment = {
};
```
Then provide the logo at application startup using the Theme Shared provider. This makes the logo (and application name) available to all ABP/Theme components (including LeptonX brand component) via injection tokens.
```ts
// app.config.ts
import { provideLogo, withEnvironmentOptions } from '@abp/ng.theme.shared';
import { environment } from './environments/environment';
export const appConfig: ApplicationConfig = {
providers: [
// ... other providers
provideLogo(withEnvironmentOptions(environment)),
],
};
```
Notes
- This approach works across themes. If you are using LeptonX, the brand logo component reads these values automatically; you don't need any theme-specific code.
- You can still override visuals with CSS variables if desired. See the LeptonX section for CSS overrides.
## How to Add a Navigation Element
### Via `RoutesService`
@ -28,12 +47,14 @@ You can add routes to the menu by calling the `add` method of `RoutesService`. I
```js
import { RoutesService, eLayoutType } from '@abp/ng.core';
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
@Component(/* component metadata */)
export class AppComponent {
constructor(routes: RoutesService) {
routes.add([
private routes = inject(RoutesService);
constructor() {
this.routes.add([
{
path: '/your-path',
name: 'Your navigation',
@ -119,15 +140,16 @@ To get the route items as grouped we can use the `groupedVisible` (or Observable
```js
import { ABP, RoutesService, RouteGroup } from "@abp/ng.core";
import { Component } from "@angular/core";
import { Component, inject } from "@angular/core";
import { Observable } from "rxjs";
@Component(/* component metadata */)
export class AppComponent {
private routes = inject(RoutesService);
visible: RouteGroup<ABP.Route>[] | undefined = this.routes.groupedVisible;
//Or
visible$:Observable<RouteGroup<ABP.Route>[] | undefined> = this.routes.groupedVisible$;
constructor(private routes: RoutesService) {}
// Or
visible$: Observable<RouteGroup<ABP.Route>[] | undefined> = this.routes.groupedVisible$;
}
```
@ -158,12 +180,14 @@ export const appConfig: ApplicationConfig = {
```typescript
import { RoutesService } from '@abp/ng.core';
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
@Component(/* component metadata */)
export class AppComponent {
constructor(private routes: RoutesService) {
routes.setSingularizeStatus(false);
private routes = inject(RoutesService);
constructor() {
this.routes.setSingularizeStatus(false);
}
}
```
@ -292,7 +316,7 @@ You can add elements to the right part of the menu by calling the `addItems` met
```js
import { NavItemsService } from '@abp/ng.theme.shared';
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
@Component({
template: `
@ -304,8 +328,10 @@ export class MySearchInputComponent {}
@Component(/* component metadata */)
export class AppComponent {
constructor(private navItems: NavItemsService) {
navItems.addItems([
private navItems = inject(NavItemsService);
constructor() {
this.navItems.addItems([
{
id: 'MySearchInput',
order: 1,
@ -334,13 +360,15 @@ The `patchItem` method of `NavItemsService` finds an element by its `id` propert
```js
export class AppComponent {
constructor(private navItems: NavItemsService) {
navItems.patchItem(eThemeBasicComponents.Languages, {
private navItems = inject(NavItemsService);
constructor() {
this.navItems.patchItem(eThemeBasicComponents.Languages, {
requiredPolicy: 'new policy here',
order: 1,
});
navItems.removeItem(eThemeBasicComponents.CurrentUser);
this.navItems.removeItem(eThemeBasicComponents.CurrentUser);
}
}
```

3
docs/en/framework/ui/angular/page-alerts.md

@ -8,12 +8,13 @@ You can simply import `PageAlertService` from `@abp/ng.theme.shared` and utilize
```js
import { PageAlertService } from '@abp/ng.theme.shared';
import { inject } from '@angular/core';
@Component({
// ...
})
export class MyComponent {
constructor(private service: PageAlertService) {}
private service = inject(PageAlertService);
showWarning() {
this.service.show({

2
docs/en/framework/ui/angular/page-component.md

@ -175,7 +175,7 @@ export class MyPageRenderStrategy implements PageRenderStrategy {
* shouldRender can also return an Observable<boolean> which means
* an async service can be used within.
constructor(private service: SomeAsyncService) {}
service = inject(SomeAsyncService)
shouldRender(type: string) {
return this.service.checkTypeAsync(type).pipe(map(val => val.isTrue()));

58
docs/en/framework/ui/angular/permission-management-component-replacement.md

@ -19,7 +19,7 @@ import {
import { LocaleDirection } from '@abp/ng.theme.shared';
import {
Component,
EventEmitter, Inject, Input, Optional, Output, TrackByFunction
EventEmitter, Inject, inject, Input, Optional, Output, TrackByFunction
} from '@angular/core';
import { of } from 'rxjs';
import { finalize, switchMap, tap } from 'rxjs/operators';
@ -42,16 +42,18 @@ type PermissionWithStyle = PermissionGrantInfoDto & {
})
export class PermissionManagementComponent
implements
PermissionManagement.PermissionManagementComponentInputs,
PermissionManagement.PermissionManagementComponentOutputs {
PermissionManagement.PermissionManagementComponentInputs,
PermissionManagement.PermissionManagementComponentOutputs {
private readonly service = inject(PermissionsService);
private readonly configState = inject(ConfigStateService);
protected _providerName: string;
@Input()
get providerName(): string {
if (this.replaceableData) return this.replaceableData.inputs.providerName;
return this._providerName;
}
set providerName(value: string) {
this._providerName = value;
}
@ -60,10 +62,8 @@ export class PermissionManagementComponent
@Input()
get providerKey(): string {
if (this.replaceableData) return this.replaceableData.inputs.providerKey;
return this._providerKey;
}
set providerKey(value: string) {
this._providerKey = value;
}
@ -72,10 +72,8 @@ export class PermissionManagementComponent
@Input()
get hideBadges(): boolean {
if (this.replaceableData) return this.replaceableData.inputs.hideBadges;
return this._hideBadges;
}
set hideBadges(value: boolean) {
this._hideBadges = value;
}
@ -85,7 +83,6 @@ export class PermissionManagementComponent
get visible(): boolean {
return this._visible;
}
set visible(value: boolean) {
if (value === this._visible) return;
@ -106,15 +103,10 @@ export class PermissionManagementComponent
@Output() readonly visibleChange = new EventEmitter<boolean>();
data: GetPermissionListResultDto = { groups: [], entityDisplayName: null };
selectedGroup: PermissionGroupDto;
permissions: PermissionGrantInfoDto[] = [];
selectThisTab = false;
selectAllTab = false;
modalBusy = false;
trackByFn: TrackByFunction<PermissionGroupDto> = (_, item) => item.name;
@ -142,7 +134,6 @@ export class PermissionManagementComponent
get isVisible(): boolean {
if (!this.replaceableData) return this.visible;
return this.replaceableData.inputs.visible;
}
@ -152,9 +143,7 @@ export class PermissionManagementComponent
public replaceableData: ReplaceableComponents.ReplaceableTemplateData<
PermissionManagement.PermissionManagementComponentInputs,
PermissionManagement.PermissionManagementComponentOutputs
>,
private service: PermissionsService,
private configState: ConfigStateService
>
) {}
getChecked(name: string) {
@ -162,17 +151,11 @@ export class PermissionManagementComponent
}
isGrantedByOtherProviderName(grantedProviders: ProviderInfoDto[]): boolean {
if (grantedProviders.length) {
return grantedProviders.findIndex(p => p.providerName !== this.providerName) > -1;
}
return false;
return grantedProviders?.some(p => p.providerName !== this.providerName);
}
onClickCheckbox(clickedPermission: PermissionGrantInfoDto, value) {
if (
clickedPermission.isGranted &&
this.isGrantedByOtherProviderName(clickedPermission.grantedProviders)
)
if (clickedPermission.isGranted && this.isGrantedByOtherProviderName(clickedPermission.grantedProviders))
return;
setTimeout(() => {
@ -184,7 +167,6 @@ export class PermissionManagementComponent
} else if (clickedPermission.parentName === per.name && !clickedPermission.isGranted) {
return { ...per, isGranted: true };
}
return per;
});
@ -255,16 +237,11 @@ export class PermissionManagementComponent
this.setTabCheckboxState();
}
submit() {
const unchangedPermissions = getPermissions(this.data.groups);
const changedPermissions: UpdatePermissionDto[] = this.permissions
.filter(per =>
unchangedPermissions.find(unchanged => unchanged.name === per.name).isGranted ===
per.isGranted
? false
: true,
unchangedPermissions.find(unchanged => unchanged.name === per.name).isGranted !== per.isGranted
)
.map(({ name, isGranted }) => ({ name, isGranted }));
@ -321,26 +298,23 @@ export class PermissionManagementComponent
this.replaceableData.outputs.visibleChange(visible);
}
}
shouldFetchAppConfig() {
const currentUser = this.configState.getOne('currentUser') as CurrentUserDto;
if (this.providerName === 'R') return currentUser.roles.some(role => role === this.providerKey);
if (this.providerName === 'U') return currentUser.id === this.providerKey;
return false;
}
}
function findMargin(permissions: PermissionGrantInfoDto[], permission: PermissionGrantInfoDto) {
function findMargin(permissions: PermissionGrantInfoDto[], permission: PermissionGrantInfoDto): number {
const parentPermission = permissions.find(per => per.name === permission.parentName);
if (parentPermission && parentPermission.parentName) {
let margin = 20;
return (margin += findMargin(permissions, parentPermission));
return margin + findMargin(permissions, parentPermission);
}
return parentPermission ? 20 : 0;
}
@ -461,12 +435,12 @@ Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js
import { ReplaceableComponentsService } from '@abp/ng.core';
import { ePermissionManagementComponents } from '@abp/ng.permission-management';
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core';
import { PermissionManagementComponent } from './permission-management/permission-management.component';
//...
export class AppComponent implements OnInit {
constructor(private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
private readonly replaceableComponents = inject(ReplaceableComponentsService); // injected ReplaceableComponentsService
ngOnInit() {
this.replaceableComponents.add({

9
docs/en/framework/ui/angular/permission-management.md

@ -8,9 +8,10 @@ You can get permission as boolean value:
```js
import { PermissionService } from '@abp/ng.core';
import { inject } from '@angular/core';
export class YourComponent {
constructor(private permissionService: PermissionService) {}
private permissionService = inject(PermissionService);
ngOnInit(): void {
const canCreate = this.permissionService.getGrantedPolicy('AbpIdentity.Roles.Create');
@ -83,14 +84,14 @@ In some cases, a custom permission management may be needed. All you need to do
```js
import { ConfigStateService, PermissionService } from '@abp/ng.core';
import { Injectable } from '@angular/core';
import { Injectable, inject } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class CustomPermissionService extends PermissionService {
constructor(configStateService: ConfigStateService) {
super(configStateService);
constructor() {
super(inject(ConfigStateService));
}
// This is an example to show how to override the methods

29
docs/en/framework/ui/angular/router-events.md

@ -14,9 +14,12 @@ import {
Router,
} from '@angular/router';
import { filter } from 'rxjs/operators';
import { inject, Injectable } from '@angular/core';
@Injectable()
class SomeService {
private router = inject(Router);
navigationFinish$ = this.router.events.pipe(
filter(
event =>
@ -26,8 +29,6 @@ class SomeService {
),
);
/* Observable<Event> */
constructor(private router: Router) {}
}
```
@ -38,10 +39,10 @@ import { RouterEvents } from '@abp/ng.core';
@Injectable()
class SomeService {
private routerEvents = inject(RouterEvents);
navigationFinish$ = this.routerEvents.getNavigationEvents('End', 'Error', 'Cancel');
/* Observable<NavigationCancel | NavigationEnd | NavigationError> */
constructor(private routerEvents: RouterEvents) {}
}
```
@ -62,6 +63,8 @@ import { mapTo } from 'rxjs/operators';
@Injectable()
class SomeService {
private routerEvents = inject(RouterEvents);
navigationStart$ = this.routerEvents.getNavigationEvents('Start');
/* Observable<NavigationStart> */
@ -73,8 +76,6 @@ class SomeService {
this.navigationFinish$.pipe(mapTo(false)),
);
/* Observable<boolean> */
constructor(private routerEvents: RouterEvents) {}
}
```
@ -88,6 +89,8 @@ import { map } from 'rxjs/operators';
@Injectable()
class SomeService {
private routerEvents = inject(RouterEvents);
navigationEvent$ = this.routerEvents.getAllNavigationEvents();
/* Observable<NavigationCancel | NavigationEnd | NavigationError | NavigationStart> */
@ -95,8 +98,6 @@ class SomeService {
map(event => event instanceof NavigationStart),
);
/* Observable<boolean> */
constructor(private routerEvents: RouterEvents) {}
}
```
@ -127,7 +128,7 @@ class SomeService {
### How to Get Specific Router Events
You can use `getEvents` to get a stream of router events matching given event constructors.
You can use `getEvents` to get a stream of router events matching given event classes.
```js
import { RouterEvents } from '@abp/ng.core';
@ -135,16 +136,16 @@ import { ActivationEnd, ChildActivationEnd } from '@angular/router';
@Injectable()
class SomeService {
private routerEvents = inject(RouterEvents);
moduleActivation$ = this.routerEvents.getEvents(ActivationEnd, ChildActivationEnd);
/* Observable<ActivationEnd | ChildActivationEnd> */
constructor(private routerEvents: RouterEvents) {}
}
```
### How to Get All Router Events
You can use `getEvents` to get a stream of all router events without passing any event constructors. This is nothing different from accessing `events` property of `Router` and is added to the service just for convenience.
You can use `getEvents` to get a stream of all router events without passing any event classes. This is nothing different from accessing `events` property of `Router` and is added to the service just for convenience.
```js
import { RouterEvents } from '@abp/ng.core';
@ -152,9 +153,9 @@ import { ActivationEnd, ChildActivationEnd } from '@angular/router';
@Injectable()
class SomeService {
private routerEvents = inject(RouterEvents);
routerEvent$ = this.routerEvents.getAllEvents();
/* Observable<Event> */
constructor(private routerEvents: RouterEvents) {}
}
```

5
docs/en/framework/ui/angular/service-proxies.md

@ -89,14 +89,15 @@ The `generate-proxy` command generates one service per back-end controller and a
A variable named `apiName` (available as of v2.4) is defined in each service. `apiName` matches the module's `RemoteServiceName`. This variable passes to the `RestService` as a parameter at each request. If there is no microservice API defined in the environment, `RestService` uses the default. See [getting a specific API endpoint from application config](./http-requests#how-to-get-a-specific-api-endpoint-from-application-config)
The `providedIn` property of the services is defined as `'root'`. Therefore there is no need to provide them in a module. You can use them directly by injecting them into the constructor as shown below:
The `providedIn` property of the services is defined as `'root'`. Therefore there is no need to provide them in a module. You can use them directly by injecting as shown below:
```js
import { BookService } from '@proxy/books';
import { inject } from '@angular/core';
@Component(/* component metadata here */)
export class BookComponent implements OnInit {
constructor(private service: BookService) {}
private service = inject(BookService);
ngOnInit() {
this.service.get().subscribe(

3
docs/en/framework/ui/angular/settings.md

@ -10,12 +10,13 @@ To use the `ConfigStateService`, you must inject it in your class as a dependenc
```js
import { ConfigStateService } from '@abp/ng.core';
import { inject } from '@angular/core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private config: ConfigStateService) {}
private config = inject(ConfigStateService);
}
```

21
docs/en/framework/ui/angular/subscription-service.md

@ -8,6 +8,7 @@ You have to provide the `SubscriptionService` at component or directive level, b
```js
import { SubscriptionService } from '@abp/ng.core';
import { inject } from '@angular/core';
@Component({
/* class metadata here */
@ -16,7 +17,9 @@ import { SubscriptionService } from '@abp/ng.core';
class DemoComponent {
count$ = interval(1000);
constructor(private subscription: SubscriptionService) {
private subscription = inject(SubscriptionService);
constructor() {
this.subscription.addOne(this.count$, console.log);
}
}
@ -38,7 +41,7 @@ You can pass a `next` function and an `error` function.
providers: [SubscriptionService],
})
class DemoComponent implements OnInit {
constructor(private subscription: SubscriptionService) {}
private subscription = inject(SubscriptionService);
ngOnInit() {
const source$ = interval(1000);
@ -61,7 +64,7 @@ Or, you can pass an observer.
providers: [SubscriptionService],
})
class DemoComponent implements OnInit {
constructor(private subscription: SubscriptionService) {}
private subscription = inject(SubscriptionService);
ngOnInit() {
const source$ = interval(1000);
@ -87,7 +90,7 @@ There are two ways to do that. If you are not going to subscribe again, you may
providers: [SubscriptionService],
})
class DemoComponent implements OnInit {
constructor(private subscription: SubscriptionService) {}
private subscription = inject(SubscriptionService);
ngOnInit() {
this.subscription.addOne(interval(1000), console.log);
@ -107,7 +110,7 @@ This will clear all subscriptions, but you will not be able to subscribe again.
providers: [SubscriptionService],
})
class DemoComponent implements OnInit {
constructor(private subscription: SubscriptionService) {}
private subscription = inject(SubscriptionService);
ngOnInit() {
this.subscription.addOne(interval(1000), console.log);
@ -131,8 +134,7 @@ Sometimes, you may need to unsubscribe from a particular subscription but leave
})
class DemoComponent implements OnInit {
countSubscription: Subscription;
constructor(private subscription: SubscriptionService) {}
private subscription = inject(SubscriptionService);
ngOnInit() {
this.countSubscription = this.subscription.addOne(
@ -159,8 +161,7 @@ You may want to take control of a particular subscription. In such a case, you m
})
class DemoComponent implements OnInit {
countSubscription: Subscription;
constructor(private subscription: SubscriptionService) {}
private subscription = inject(SubscriptionService);
ngOnInit() {
this.countSubscription = this.subscription.addOne(
@ -186,7 +187,7 @@ Please use `isClosed` getter to check if `closeAll` was called before.
providers: [SubscriptionService],
})
class DemoComponent implements OnInit {
constructor(private subscription: SubscriptionService) {}
private subscription = inject(SubscriptionService);
ngOnInit() {
this.subscription.addOne(interval(1000), console.log);

45
docs/en/framework/ui/angular/theming.md

@ -105,12 +105,14 @@ The [Application Startup Template](../../../solution-templates/application-modul
```ts
import { RoutesService, eLayoutType } from '@abp/ng.core';
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
@Component(/* component metadata */)
export class AppComponent {
constructor(routes: RoutesService) {
routes.add([
private routes = inject(RoutesService);
constructor() {
this.routes.add([
{
path: '/your-path',
name: 'Your navigation',
@ -141,7 +143,7 @@ See the [Modifying the Menu](modifying-the-menu.md) document to learn more about
````ts
import { NavItemsService } from '@abp/ng.theme.shared';
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
@Component({
template: `
@ -153,8 +155,10 @@ export class MySearchInputComponent {}
@Component(/* component metadata */)
export class AppComponent {
constructor(private navItems: NavItemsService) {
navItems.addItems([
private navItems = inject(NavItemsService);
constructor() {
this.navItems.addItems([
{
id: 'MySearchInput',
order: 1,
@ -184,25 +188,25 @@ Language Selection toolbar item is generally a dropdown that is used to switch b
**Example: Get the currently selected language**
````ts
import {SessionStateService} from '@abp/ng.core';
import { SessionStateService } from '@abp/ng.core';
import { inject } from '@angular/core';
//...
constructor(private sessionState: SessionStateService) {
const lang = this.sessionState.getLanguage()
}
const sessionState = inject(SessionStateService);
const lang = sessionState.getLanguage();
````
**Example: Set the selected language**
````ts
import {SessionStateService} from '@abp/ng.core';
import { SessionStateService } from '@abp/ng.core';
import { inject } from '@angular/core';
//...
constructor(private sessionState: SessionStateService) {
const lang = this.sessionState.setLanguage('en')
}
const sessionState = inject(SessionStateService);
sessionState.setLanguage('en');
````
@ -219,7 +223,7 @@ All of the options are shown below. You can choose either of them.
````ts
import { eUserMenuItems } from '@abp/ng.theme.basic';
import { UserMenuService } from '@abp/ng.theme.shared';
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
// make sure that you import this component in a NgModule
@ -234,20 +238,23 @@ import { Router } from '@angular/router';
})
export class UserMenuItemComponent {
// you can inject the data through `INJECTOR_PIPE_DATA_TOKEN` token
constructor(@Inject(INJECTOR_PIPE_DATA_TOKEN) public data: UserMenu) {}
public data = inject<UserMenu>(INJECTOR_PIPE_DATA_TOKEN);
}
@Component({/* component metadata */})
export class AppComponent {
constructor(private userMenu: UserMenuService, private router: Router) {
private userMenu = inject(UserMenuService);
private router = inject(Router);
constructor() {
this.userMenu.addItems([
{
id: 'UserMenu.MyAccount',
order: 1,
// HTML example
html: `<a class="dropdown-item pointer">My account</a>`,
// text template example
textTemplate: {
text: 'AbpAccount::MyAccount',

35
docs/en/framework/ui/angular/toaster-service.md

@ -8,12 +8,13 @@ You do not have to provide the `ToasterService` at component level, because it i
```js
import { ToasterService } from '@abp/ng.theme.shared';
import { inject } from '@angular/core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private toaster: ToasterService) {}
private toaster = inject(ToasterService);
}
```
@ -36,22 +37,23 @@ Options can be passed as the third parameter to `success`, `warn`, `error`, and
```js
import { Toaster, ToasterService } from '@abp/ng.theme.shared';
//...
import { inject } from '@angular/core';
constructor(private toaster: ToasterService) {}
// inside the class
private toaster = inject(ToasterService);
//...
const options: Partial<Toaster.ToastOptions> = {
life: 10000,
sticky: false,
closable: true,
tapToDismiss: true,
messageLocalizationParams: ['Demo', '1'],
titleLocalizationParams: [],
iconClass: 'custom-icon-name';
};
this.toaster.error('AbpUi::EntityNotFoundErrorMessage', 'AbpUi::Error', options);
life: 10000,
sticky: false,
closable: true,
tapToDismiss: true,
messageLocalizationParams: ['Demo', '1'],
titleLocalizationParams: [],
iconClass: 'custom-icon-name'
};
this.toaster.error('AbpUi::EntityNotFoundErrorMessage', 'AbpUi::Error', options);
```
- `life` option is the closing time in milliseconds. Default value is `5000`.
@ -93,15 +95,16 @@ If you want the ABP to utilize 3rd party libraries for the toasters instead of t
```js
// your-custom-toaster.service.ts
import { Injectable } from '@angular/core';
import { Injectable, inject } from '@angular/core';
import { Config, LocalizationService } from '@abp/ng.core';
import { Toaster } from '@abp/ng.theme.shared';
import { ToastrService } from 'ngx-toastr';
@Injectable()
export class CustomToasterService implements Toaster.Service {
constructor(private toastr: ToastrService, private localizationService: LocalizationService) {}
private toastr = inject(ToastrService);
private localizationService = inject(LocalizationService)
error(
message: Config.LocalizationParam,
title?: Config.LocalizationParam,

3
docs/en/framework/ui/angular/track-by-service.md

@ -10,6 +10,7 @@ You do not have to provide the `TrackByService` at module or component level, be
```js
import { TrackByService } from '@abp/ng.core';
import { inject } from '@angular/core';
@Component({
/* class metadata here */
@ -17,7 +18,7 @@ import { TrackByService } from '@abp/ng.core';
class DemoComponent {
list: Item[];
constructor(public readonly track: TrackByService<Item>) {}
public readonly track = inject(TrackByService<Item>);
}
```

18
docs/en/modules/setting-management.md

@ -289,16 +289,18 @@ yarn ng generate component my-settings
Open the `app.component.ts` and modify the file as shown below:
```js
import { Component } from '@angular/core';
import { SettingTabsService } from '@abp/ng.setting-management/config'; // imported SettingTabsService
import { MySettingsComponent } from './my-settings/my-settings.component'; // imported MySettingsComponent
import { Component, inject } from '@angular/core';
import { SettingTabsService } from '@abp/ng.setting-management/config';
import { MySettingsComponent } from './my-settings/my-settings.component';
@Component(/* component metadata */)
@Component({
// component metadata
})
export class AppComponent {
constructor(private settingTabs: SettingTabsService) // injected MySettingsComponent
{
// added below
settingTabs.add([
private readonly settingTabs = inject(SettingTabsService);
constructor() {
this.settingTabs.add([
{
name: 'MySettings',
order: 1,

5
docs/en/tutorials/book-store/part-02.md

@ -446,7 +446,7 @@ Open the `/src/app/book/book.component.ts` file and replace the content as below
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core';
import { BookService, BookDto } from '@proxy/books';
@Component({
@ -458,7 +458,8 @@ import { BookService, BookDto } from '@proxy/books';
export class BookComponent implements OnInit {
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
constructor(public readonly list: ListService, private bookService: BookService) {}
public readonly list = inject(ListService);
private readonly bookService = inject(BookService);
ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getList(query);

82
docs/en/tutorials/book-store/part-03.md

@ -576,7 +576,7 @@ Open `/src/app/book/book.component.ts` and replace the content as below:
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core';
import { BookService, BookDto } from '@proxy/books';
@Component({
@ -588,9 +588,10 @@ import { BookService, BookDto } from '@proxy/books';
export class BookComponent implements OnInit {
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
isModalOpen = false; // add this line
isModalOpen = false;
constructor(public readonly list: ListService, private bookService: BookService) {}
public readonly list = inject(ListService);
private readonly bookService = inject(BookService);
ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getList(query);
@ -600,7 +601,7 @@ export class BookComponent implements OnInit {
});
}
// add new method
//add new method
createBook() {
this.isModalOpen = true;
}
@ -668,7 +669,7 @@ Open `/src/app/book/book.component.ts` and replace the content as below:
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core';
import { BookService, BookDto, bookTypeOptions } from '@proxy/books'; // add bookTypeOptions
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // add this
@ -688,11 +689,9 @@ export class BookComponent implements OnInit {
isModalOpen = false;
constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder // inject FormBuilder
) {}
public readonly list = inject(ListService);
private readonly bookService = inject(BookService);
private readonly fb = inject(FormBuilder); // inject FormBuilder
ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getList(query);
@ -702,6 +701,7 @@ export class BookComponent implements OnInit {
});
}
// add new method
createBook() {
this.buildForm(); // add this line
this.isModalOpen = true;
@ -735,7 +735,7 @@ export class BookComponent implements OnInit {
* Imported `FormGroup`, `FormBuilder` and `Validators` from `@angular/forms`.
* Added a `form: FormGroup` property.
* Added a `bookTypes` property as a list of `BookType` enum members. That will be used in form options.
* Injected `FormBuilder` into the constructor. [FormBuilder](https://angular.io/api/forms/FormBuilder) provides convenient methods for generating form controls. It reduces the amount of boilerplate needed to build complex forms.
* Injected with the `FormBuilder` inject function.. [FormBuilder](https://angular.io/api/forms/FormBuilder) provides convenient methods for generating form controls. It reduces the amount of boilerplate needed to build complex forms.
* Added a `buildForm` method to the end of the file and executed the `buildForm()` in the `createBook` method.
* Added a `save` method.
@ -823,7 +823,7 @@ Open `/src/app/book/book.component.ts` and replace the content as below:
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core';
import { BookService, BookDto, bookTypeOptions } from '@proxy/books';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
@ -848,11 +848,9 @@ export class BookComponent implements OnInit {
isModalOpen = false;
constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder
) {}
public readonly list = inject(ListService);
private readonly bookService = inject(BookService);
private readonly fb = inject(FormBuilder);
ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getList(query);
@ -903,7 +901,7 @@ Open `/src/app/book/book.component.ts` and replace the content as shown below:
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core';
import { BookService, BookDto, bookTypeOptions } from '@proxy/books';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
@ -925,11 +923,9 @@ export class BookComponent implements OnInit {
isModalOpen = false;
constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder
) {}
public readonly list = inject(ListService);
private readonly bookService = inject(BookService);
private readonly fb = inject(FormBuilder);
ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getList(query);
@ -1039,34 +1035,40 @@ This template will show the **Edit** text for edit record operation, **New Book*
Open the `/src/app/book/book.component.ts` file and inject the `ConfirmationService`.
Replace the constructor as below:
Replace the injected services as below:
```js
// ...
// add new imports
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared';
import { Component, OnInit, inject } from '@angular/core';
//change the constructor
constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder,
private confirmation: ConfirmationService // inject the ConfirmationService
) {}
// Add a delete method
delete(id: string) {
this.confirmation.warn('::AreYouSureToDelete', '::AreYouSure').subscribe((status) => {
if (status === Confirmation.Status.confirm) {
this.bookService.delete(id).subscribe(() => this.list.get());
}
});
// ...
export class BookComponent implements OnInit {
// ...
public readonly list = inject(ListService);
private readonly bookService = inject(BookService);
private readonly fb = inject(FormBuilder);
private readonly confirmation = inject(ConfirmationService); // inject the ConfirmationService
// ...
// Add a delete method
delete(id: string) {
this.confirmation.warn('::AreYouSureToDelete', '::AreYouSure').subscribe((status) => {
if (status === Confirmation.Status.confirm) {
this.bookService.delete(id).subscribe(() => this.list.get());
}
});
}
}
```
* We imported `ConfirmationService`.
* We injected `ConfirmationService` to the constructor.
* We injected `ConfirmationService` using the `inject()` function.
* Added a `delete` method.
> Check out the [Confirmation Popup documentation](../../framework/ui/angular/confirmation-service.md) for more about this service.

12
docs/en/tutorials/book-store/part-09.md

@ -601,7 +601,7 @@ This command generates the service proxy for the author service and the related
Open the `/src/app/author/author.component.ts` file and replace the content as below:
```js
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core';
import { ListService, PagedResultDto } from '@abp/ng.core';
import { AuthorService, AuthorDto } from '@proxy/authors';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
@ -623,12 +623,10 @@ export class AuthorComponent implements OnInit {
selectedAuthor = {} as AuthorDto;
constructor(
public readonly list: ListService,
private authorService: AuthorService,
private fb: FormBuilder,
private confirmation: ConfirmationService
) {}
public readonly list = inject(ListService);
private readonly authorService = inject(AuthorService);
private readonly fb = inject(FormBuilder);
private readonly confirmation = inject(ConfirmationService);
ngOnInit(): void {
const authorStreamCreator = (query) => this.authorService.getList(query);

12
docs/en/tutorials/book-store/part-10.md

@ -1009,12 +1009,12 @@ export class BookComponent implements OnInit {
isModalOpen = false;
constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder,
private confirmation: ConfirmationService
) {
public readonly list = inject(ListService);
private readonly bookService = inject(BookService);
private readonly fb = inject(FormBuilder);
private readonly confirmation = inject(ConfirmationService);
constructor() {
this.authors$ = bookService.getAuthorLookup().pipe(map((r) => r.items));
}

7
docs/en/tutorials/microservice/part-05.md

@ -643,7 +643,7 @@ export const ORDER_SERVICE_ROUTES: Routes = [
* Create `order.component.ts` file under the `projects/ordering-service/src/lib/order` folder as following code:
```typescript
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { OrderDto, OrderService } from './proxy/ordering-service/services';
@ -657,12 +657,13 @@ export class OrderComponent {
items: OrderDto[] = [];
constructor(private readonly proxy: OrderService) {
private readonly proxy = inject(OrderService);
constructor() {
this.proxy.getList().subscribe((res) => {
this.items = res;
});
}
}
```

11
docs/en/tutorials/todo/layered/index.md

@ -751,7 +751,7 @@ Open the `/angular/src/app/home/home.component.ts` file and replace its content
```js
import { ToasterService } from '@abp/ng.theme.shared';
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core';
import { TodoItemDto, TodoService } from '@proxy';
@Component({
@ -764,10 +764,8 @@ export class HomeComponent implements OnInit {
todoItems: TodoItemDto[];
newTodoText: string;
constructor(
private todoService: TodoService,
private toasterService: ToasterService)
{ }
private readonly todoService = inject(TodoService);
private readonly toasterService = inject(ToasterService);
ngOnInit(): void {
this.todoService.getList().subscribe(response => {
@ -775,7 +773,7 @@ export class HomeComponent implements OnInit {
});
}
create(): void{
create(): void {
this.todoService.create(this.newTodoText).subscribe((result) => {
this.todoItems = this.todoItems.concat(result);
this.newTodoText = null;
@ -789,7 +787,6 @@ export class HomeComponent implements OnInit {
});
}
}
```
We've used `todoService` to get the list of todo items and assigned the returning value to the `todoItems` array. We've also added `create` and `delete` methods. These methods will be used on the view side.

11
docs/en/tutorials/todo/single-layer/index.md

@ -718,7 +718,7 @@ Open the `/angular/src/app/home/home.component.ts` file and replace its content
```ts
import { ToasterService } from "@abp/ng.theme.shared";
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core';
import { TodoItemDto } from "@proxy/services/dtos";
import { TodoService } from "@proxy/services";
@ -727,16 +727,13 @@ import { TodoService } from "@proxy/services";
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
})
export class HomeComponent implements OnInit {
todoItems: TodoItemDto[];
newTodoText: string;
constructor(
private todoService: TodoService,
private toasterService: ToasterService)
{ }
private readonly todoService = inject(TodoService);
private readonly toasterService = inject(ToasterService);
ngOnInit(): void {
this.todoService.getList().subscribe(response => {
@ -744,7 +741,7 @@ export class HomeComponent implements OnInit {
});
}
create(): void{
create(): void {
this.todoService.create(this.newTodoText).subscribe((result) => {
this.todoItems = this.todoItems.concat(result);
this.newTodoText = null;

45
docs/en/ui-themes/lepton-x-lite/angular.md

@ -60,7 +60,39 @@ export const appConfig: ApplicationConfig = {
```
To change the logos and brand color of `LeptonX`, simply add the following CSS to the `styles.scss`
To change the logos and brand color of `LeptonX`, you have two options:
1) Provide logo and application name via the Theme Shared provider (recommended)
```ts
// app.config.ts
import { provideLogo, withEnvironmentOptions } from '@abp/ng.theme.shared';
import { environment } from './environments/environment';
export const appConfig: ApplicationConfig = {
providers: [
// ...
provideLogo(withEnvironmentOptions(environment)),
],
};
```
Ensure your environment contains the logo url and app name:
```ts
// environment.ts
export const environment = {
// ...
application: {
name: 'MyProjectName',
logoUrl: '/assets/images/logo.png',
},
};
```
The LeptonX brand component reads these values automatically from `@abp/ng.theme.shared`.
2) Or override via CSS variables in `styles.scss`
```css
:root {
@ -74,6 +106,8 @@ To change the logos and brand color of `LeptonX`, simply add the following CSS t
- `--lpx-logo-icon` is a square icon used when the menu is collapsed.
- `--lpx-brand` is a color used throughout the application, especially on active elements.
Tip: You can combine both approaches. For example, provide the main logo via `provideLogo(...)` and still fine-tune visuals (sizes, colors) with CSS.
### Server Side
In order to migrate to LeptonX on your server side projects (Host and/or AuthServer projects), please follow the [Server Side Migration](asp-net-core.md) document.
@ -97,15 +131,16 @@ The **Layout components** and all the replacable components are predefined in `e
```js
import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum
import { eThemeLeptonXComponents } from '@abp/ng.theme.lepton-x'; // imported eThemeLeptonXComponents enum
import { eThemeLeptonXComponents } from '@abp/ng.theme.lepton-x'; // imported eThemeLeptonXComponents enum
import { Component, inject } from '@angular/core';
//...
@Component(/* component metadata */)
export class AppComponent {
constructor(
private replaceableComponents: ReplaceableComponentsService, // injected the service
) {
private replaceableComponents = inject(ReplaceableComponentsService);
constructor() {
this.replaceableComponents.add({
component: YourNewApplicationLayoutComponent,
key: eThemeLeptonXComponents.ApplicationLayout,

18
docs/en/ui-themes/lepton-x/angular-customization.md

@ -18,14 +18,18 @@ The **Layout components** and all the replacable components are predefined in `e
### How to replace a component
```js
import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import {eThemeLeptonXComponents} from "@volosoft/abp.ng.theme.lepton-x"; // imported eThemeLeptonXComponents enum
//...
@Component(/* component metadata */)
import { Component, inject } from '@angular/core';
import { ReplaceableComponentsService } from '@abp/ng.core';
import { eThemeLeptonXComponents } from '@volosoft/abp.ng.theme.lepton-x';
import { YourNewApplicationLayoutComponent } from './your-new-application-layout.component'; // varsa
@Component({
// component metadata
})
export class AppComponent {
constructor(
private replaceableComponents: ReplaceableComponentsService, // injected the service
) {
private readonly replaceableComponents = inject(ReplaceableComponentsService);
constructor() {
this.replaceableComponents.add({
component: YourNewApplicationLayoutComponent,
key: eThemeLeptonXComponents.ApplicationLayout,

7
framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/wwwroot/libs/bootstrap/css/bootstrap.min.css

File diff suppressed because one or more lines are too long

9
latest-versions.json

@ -1,4 +1,13 @@
[
{
"version": "9.3.4",
"releaseDate": "",
"type": "stable",
"message": "",
"leptonx": {
"version": "4.3.4"
}
},
{
"version": "9.3.3",
"releaseDate": "",

2
modules/docs/src/Volo.Docs.Web/Markdown/MarkDigMarkdownConverter.cs

@ -1,5 +1,6 @@
using System.Text;
using Markdig;
using Markdig.Extensions.AutoIdentifiers;
using Volo.Abp.DependencyInjection;
using Volo.Docs.Markdown.Extensions;
@ -12,6 +13,7 @@ namespace Volo.Docs.Markdown
public MarkDigMarkdownConverter()
{
_markdownPipeline = new MarkdownPipelineBuilder()
.UseAutoIdentifiers(AutoIdentifierOptions.GitHub)
.UseAutoLinks()
.UseBootstrap()
.UseGridTables()

5
modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml

@ -39,7 +39,6 @@
<abp-style-bundle name="@typeof(IndexModel).FullName">
<abp-style type="@typeof(PrismjsStyleBundleContributor)"/>
<abp-style type="@typeof(MalihuCustomScrollbarPluginStyleBundleContributor)"/>
<abp-style src="/Pages/Documents/Project/bootstrap-toc.css"/>
<abp-style src="/Pages/Documents/Shared/Styles/vs.css"/>
<abp-style src="/Pages/Documents/Project/index.css"/>
@if (DocsUiOptions.Value.EnableEnlargeImage)
@ -72,7 +71,6 @@
<abp-script type="@typeof(PopperJsScriptBundleContributor)"/>
<abp-script src="/client-proxies/docs-proxy.js"/>
<abp-script src="/client-proxies/docs-common-proxy.js"/>
<abp-script src="/Pages/Documents/Project/bootstrap-toc.js"/>
<abp-script src="/Pages/Documents/Shared/Scripts/vs.js"/>
<abp-script src="/Pages/Documents/Project/index.js"/>
@if (DocsUiOptions.Value.EnableEnlargeImage)
@ -599,7 +597,8 @@
<h5 class="card-title">@L["InThisDocument"]</h5>
<div id="scroll-index" class="">
<nav id="docs-sticky-index" class="navbar index-scroll">
<nav id="toc" class="navbar index-scroll">
<partial name="/Pages/Documents/Project/TableOfContents.cshtml" model="Model.TocItems" />
</nav>
<div class="row">
<div class="col p-0">

20
modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs

@ -22,11 +22,12 @@ using Volo.Docs.Common.Documents;
using Volo.Docs.Common.Projects;
using Volo.Docs.Documents;
using Volo.Docs.Documents.Rendering;
using Volo.Docs.GitHub.Documents.Version;
using Volo.Docs.HtmlConverting;
using Volo.Docs.Localization;
using Volo.Docs.Models;
using Volo.Docs.Projects;
using Volo.Docs.GitHub.Documents.Version;
using Volo.Docs.Localization;
using Volo.Docs.TableOfContents;
using Volo.Docs.Utils;
namespace Volo.Docs.Pages.Documents.Project
@ -75,6 +76,8 @@ namespace Volo.Docs.Pages.Documents.Project
public string DocumentsUrlPrefix { get; set; }
public List<TocItem> TocItems { get; set; } = [];
public bool ShowProjectsCombobox { get; set; }
public bool ShowProjectsComboboxLabel { get; set; }
@ -98,6 +101,7 @@ namespace Volo.Docs.Pages.Documents.Project
public DocumentNavigationsDto DocumentNavigationsDto { get; private set; }
private const int MaxDescriptionMetaTagLength = 200;
private const int TocLevelCount = 2;
private readonly IDocumentAppService _documentAppService;
private readonly IDocumentToHtmlConverterFactory _documentToHtmlConverterFactory;
private readonly IProjectAppService _projectAppService;
@ -105,6 +109,7 @@ namespace Volo.Docs.Pages.Documents.Project
private readonly DocsUiOptions _uiOptions;
private readonly IPermissionChecker _permissionChecker;
private readonly IDocumentPdfAppService _documentPdfAppService;
private readonly ITocGeneratorService _tocGeneratorService;
protected IDocsLinkGenerator DocsLinkGenerator => LazyServiceProvider.LazyGetRequiredService<IDocsLinkGenerator>();
@ -117,7 +122,8 @@ namespace Volo.Docs.Pages.Documents.Project
IOptions<DocsUiOptions> options,
IWebDocumentSectionRenderer webDocumentSectionRenderer,
IPermissionChecker permissionChecker,
IDocumentPdfAppService documentPdfAppService)
IDocumentPdfAppService documentPdfAppService,
ITocGeneratorService tocGeneratorService)
{
ObjectMapperContext = typeof(DocsWebModule);
@ -128,6 +134,7 @@ namespace Volo.Docs.Pages.Documents.Project
_permissionChecker = permissionChecker;
_documentPdfAppService = documentPdfAppService;
_uiOptions = options.Value;
_tocGeneratorService = tocGeneratorService;
LocalizationResourceType = typeof(DocsResource);
}
@ -534,7 +541,14 @@ namespace Volo.Docs.Pages.Documents.Project
DocumentLanguageCode = language;
DocumentNameWithExtension = Document.Name;
SetDocumentPageTitle();
if (Document != null && !Document.Content.IsNullOrEmpty())
{
TocItems = _tocGeneratorService.GenerateTocItems(Document.Content, TocLevelCount);
}
await ConvertDocumentContentToHtmlAsync();
return true;
}
catch (DocumentNotFoundException e)

41
modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/TableOfContents.cshtml

@ -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>
}
}

2
modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/index.js

@ -42,8 +42,6 @@ var doc = doc || {};
$ul.append($li);
$lazyLiElement.append($ul)
window.Toc.helpers.initNavEvent();
},
loadAll : function(lazyLiElements){
if(doc.lazyExpandableNavigation.isAllLoaded){

12
modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/index.scss

@ -71,4 +71,16 @@ body {
background-color: transparent;
border-color: transparent;
font-size: 14px;
}
.toc-item-has-children > ul {
max-height: 0;
overflow: hidden;
opacity: 0;
transition: max-height 0.3s ease-in-out, opacity 0.3s ease-in-out;
}
.toc-item-has-children.open > ul {
max-height: 1000px;
opacity: 1;
}

43
modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Scripts/vs.js

@ -1,8 +1,6 @@
(function ($) {
$(function () {
window.Toc.helpers.initNavEvent();
var scrollTopBtn = $('.scroll-top-btn');
var enoughHeight = $('.docs-sidebar-wrapper > .docs-top').height();
var enoughHeightPlus = 500;
@ -64,10 +62,10 @@
handleCustomScrolls();
var $myNav = $('#docs-sticky-index');
Toc.init($myNav);
$('body').scrollspy({
target: $myNav,
offset:100
});
$('#docs-sticky-index a').on('click', function (event) {
@ -86,6 +84,23 @@
}
});
$("body").on('activate.bs.scrollspy', function (e) {
var $activeLink = $('.nav-link.active', $('#docs-sticky-index'));
var $activeLi = $activeLink.parent('li.nav-item');
$myNav.find('li.toc-item-has-children.open').each(function () {
if ($(this).has($activeLi).length === 0) {
$(this).removeClass('open');
}
});
var $parentToOpen = $activeLi.closest('li.toc-item-has-children');
if ($parentToOpen.length > 0) {
$parentToOpen.addClass('open');
}
});
$('.btn-toggle').on('click', function () {
$('.toggle-row').slideToggle(400);
$(this).toggleClass('less');
@ -99,6 +114,7 @@
$('.docs-tree-list').slideToggle();
});
initMenuToggle();
scrollToHashLink();
});
@ -125,26 +141,7 @@
});
}
window.Toc.helpers.createNavList = function () {
return $('<ul class="nav nav-pills flex-column"></ul>');
};
window.Toc.helpers.createChildNavList = function ($parent) {
var $childList = this.createNavList();
$parent.append($childList);
return $childList;
};
window.Toc.helpers.generateNavEl = function (anchor, text) {
var $a = $('<a class="nav-link"></a>');
$a.attr('href', '#' + anchor);
$a.text(text);
var $li = $('<li class="nav-item"></li>');
$li.append($a);
return $li;
};
window.Toc.helpers.initNavEvent = function () {
function initMenuToggle() {
$('li:not(.last-link) a.tree-toggle').off('click');
$('li:not(.last-link) span.plus-icon i.fa-chevron-right').off('click');

15
modules/docs/src/Volo.Docs.Web/TableOfContents/ITocGeneratorService.cs

@ -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);
}

209
modules/docs/src/Volo.Docs.Web/TableOfContents/TocGeneratorService.cs

@ -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;
}
}

7
modules/docs/src/Volo.Docs.Web/TableOfContents/TocHeading.cs

@ -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);

12
modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor

@ -30,10 +30,10 @@
</Field>
</Column>
</Row>
<fieldset class="border rounded-4 p-3">
<legend class="px-1 h5 mb-0">@L["PermissionGroup"]</legend>
@if (_groups != null && _groups.Any())
{
<div class="lpx-scroll-pills-container" >
@ -85,7 +85,7 @@
Disabled="@(IsDisabledPermission(permission))"
Cursor="Cursor.Pointer"
Checked="@permission.IsGranted"
CheckedChanged="@(b => PermissionChanged(b, group, permission))"
CheckedChanged="@(async (b) => await PermissionChanged(b, group, permission))"
TValue="bool">
@GetShownName(permission)
</Check>
@ -97,9 +97,9 @@
</Content>
</Tabs>
</div>
}
</fieldset>
</ModalBody>
<ModalFooter>
@ -107,4 +107,4 @@
<Button Color="Color.Primary" Clicked="SaveAsync">@L["Save"]</Button>
</ModalFooter>
</ModalContent>
</Modal>
</Modal>

1
npm/ng-packs/packages/core/src/lib/models/rest.ts

@ -7,6 +7,7 @@ export namespace Rest {
skipAddingHeader: boolean;
observe: Observe;
httpParamEncoder?: HttpParameterCodec;
responseType: ResponseType;
}>;
export const enum Observe {

3
npm/ng-packs/packages/core/src/lib/services/rest.service.ts

@ -38,13 +38,14 @@ export class RestService {
config = config || ({} as Rest.Config);
api = api || this.getApiFromStore(config.apiName);
const { method, params, ...options } = request;
const { observe = Rest.Observe.Body, skipHandleError } = config;
const { observe = Rest.Observe.Body, skipHandleError, responseType = Rest.ResponseType.JSON } = config;
const url = this.removeDuplicateSlashes(api + request.url);
const httpClient: HttpClient = this.getHttpClient(config.skipAddingHeader);
return httpClient
.request<R>(method, url, {
observe,
responseType: responseType as any,
...(params && {
params: this.getParams(params, config.httpParamEncoder),
}),

Loading…
Cancel
Save