From d3dbb64afc1f737e0dd7df787ca7f95d1b42be13 Mon Sep 17 00:00:00 2001 From: enisn Date: Thu, 15 Jan 2026 02:13:04 +0300 Subject: [PATCH] Add advanced features documentation and update index Added a comprehensive guide for advanced ABP Framework features in 'advanced-features.md', covering topics such as GUID generation, memory database, bundling, and image manipulation. Updated the infrastructure documentation index to reference the new guide. --- .../infrastructure/advanced-features.md | 3709 +++++++++++++++++ docs/en/framework/infrastructure/index.md | 3 +- 2 files changed, 3711 insertions(+), 1 deletion(-) create mode 100644 docs/en/framework/infrastructure/advanced-features.md diff --git a/docs/en/framework/infrastructure/advanced-features.md b/docs/en/framework/infrastructure/advanced-features.md new file mode 100644 index 0000000000..df7c5cca04 --- /dev/null +++ b/docs/en/framework/infrastructure/advanced-features.md @@ -0,0 +1,3709 @@ +# Advanced Features & Integration Guide + +This guide covers advanced ABP Framework features that have limited or no dedicated documentation. Each section provides comprehensive explanations, practical examples, and best practices to help you effectively implement these features in your applications. + +## Table of Contents + +- [GUID Generation](#guid-generation) +- [Memory Database](#memory-database) +- [Advanced Bundling](#advanced-bundling) +- [Image Manipulation](#image-manipulation) +- [UI Packages](#ui-packages) +- [Rebus Event Bus](#rebus-event-bus) +- [Blazorise UI Integration](#blazorise-ui-integration) +- [Minification](#minification) + +--- + +## GUID Generation + +### Overview + +The `Volo.Abp.Guids` package provides utilities for generating GUIDs (Globally Unique Identifiers) with different strategies. While standard GUIDs are randomly generated and perfect for uniqueness, they can cause performance issues in databases when used as clustered primary keys. This package solves this problem by generating **sequential GUIDs** that maintain uniqueness while improving database index performance. + +**Why Use Sequential GUIDs?** + +When you use standard randomly-generated GUIDs as clustered primary keys, each new insert forces the database to add rows in random positions throughout the index. This causes index fragmentation and page splits, which significantly degrades performance over time. Sequential GUIDs solve this by ensuring that new GUIDs are generated in a predictable order (typically based on time), so new rows are appended to the end of the index, maintaining optimal performance. + +**Key Benefits:** +- **Reduced index fragmentation** - New records are added sequentially +- **Better query performance** - Less disk I/O for range queries +- **Improved insertion speed** - Avoids page splits in clustered indexes +- **Backward compatible** - Still standard GUIDs that work everywhere + +### Installation + +To use the GUID generation features, add the `Volo.Abp.Guids` package to your project: + +```xml + + + +``` + +### Features + +#### IGuidGenerator Interface + +The `IGuidGenerator` interface provides a clean, dependency-injectable way to generate GUIDs throughout your application. This abstraction allows you to switch between different GUID generation strategies without modifying your code. + +```csharp +/// +/// Used to generate Ids. +/// +public interface IGuidGenerator +{ + /// + /// Creates a new . + /// + Guid Create(); +} +``` + +By default, ABP provides a simple implementation that wraps `Guid.NewGuid()`. However, you can configure it to use sequential GUID generation for better database performance. + +#### Sequential GUIDs + +The `SequentialGuidGenerator` generates GUIDs that are sequential in time but still provide sufficient randomness to avoid collisions and predictability issues. These GUIDs combine a timestamp component with random data, creating identifiers that are both unique and ordered. + +**How Sequential GUIDs Work:** + +The generator uses a 16-byte structure: +- **First 6 bytes**: Timestamp (milliseconds since epoch) +- **Last 10 bytes**: Cryptographically random data + +This structure ensures that GUIDs created close together in time will be similar, making them naturally ordered when sorted by their binary representation. + +**Sequential GUID Types:** + +ABP provides three different sequential GUID generation strategies, each optimized for different database systems: + +1. **SequentialAsString** - Best for databases that sort GUIDs as strings + - **Optimized for:** MySQL, PostgreSQL + - **Characteristics:** The timestamp is placed at the beginning when GUIDs are converted to strings + - **Use when:** Your database treats GUIDs as text or you need to sort GUIDs alphabetically + +2. **SequentialAsBinary** - Best for databases that sort GUIDs as binary + - **Optimized for:** SQL Server, Oracle + - **Characteristics:** The timestamp is placed at the beginning in the binary representation + - **Use when:** Your database stores GUIDs as binary types (SQL Server's `uniqueidentifier`) + +3. **SequentialAtEnd** - Timestamp is placed at the end of the GUID + - **Optimized for:** Special cases where you want GUIDs to appear random + - **Characteristics:** The timestamp is placed at the end, so GUIDs look random but are still ordered + - **Use when:** You need sequential ordering for performance but don't want it to be visually obvious + +### Configuration + +You can configure the sequential GUID generator in your module's `ConfigureServices` method. The configuration allows you to specify the default type and override it per request. + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + Configure(options => + { + // Set the default sequential GUID type + // This type will be used when no specific type is requested + options.DefaultSequentialGuidType = SequentialGuidType.SequentialAsString; + + // Set the sequential GUID type for all requests + // If you want to force a specific type globally + options.SequentialGuidType = SequentialGuidType.SequentialAsString; + }); +} +``` + +**Understanding the Configuration:** + +- **DefaultSequentialGuidType**: The type used when you call `Create()` without specifying a type. This is a fallback for cases where the specific type isn't provided. +- **SequentialGuidType**: The type used for all calls, even if a different type is requested. Use this when you want to enforce a specific strategy throughout your application. + +### Usage Examples + +#### Basic Usage + +The most common usage is to inject `IGuidGenerator` into your services and use it to generate IDs. This gives you flexibility to switch implementations later without changing your code. + +```csharp +public class MyService +{ + private readonly IGuidGenerator _guidGenerator; + + public MyService(IGuidGenerator guidGenerator) + { + // ABP automatically injects the configured GUID generator + _guidGenerator = guidGenerator; + } + + // Generate a simple GUID + public Guid CreateEntityId() + { + return _guidGenerator.Create(); + } +} +``` + +**When to use this approach:** +- When you're creating new entities in application services +- When you need temporary unique identifiers +- When you're building test data + +#### Sequential GUID for Specific Type + +Sometimes you need to generate a specific type of sequential GUID for a particular database or use case. You can inject the `SequentialGuidGenerator` directly and specify the type. + +```csharp +public class SequentialGuidService +{ + private readonly SequentialGuidGenerator _sequentialGuidGenerator; + + public SequentialGuidService(SequentialGuidGenerator sequentialGuidGenerator) + { + _sequentialGuidGenerator = sequentialGuidGenerator; + } + + // Generate a sequential GUID with a specific type + public Guid CreateSequentialGuid(SequentialGuidType type) + { + return _sequentialGuidGenerator.Create(type); + } + + // Generate a GUID optimized for SQL Server + public Guid CreateSqlServerGuid() + { + return _sequentialGuidGenerator.Create(SequentialGuidType.SequentialAsBinary); + } + + // Generate a GUID optimized for MySQL/PostgreSQL + public Guid CreateMySqlGuid() + { + return _sequentialGuidGenerator.Create(SequentialGuidType.SequentialAsString); + } +} +``` + +**Use cases:** +- When you're working with multiple database systems +- When different entities need different GUID strategies +- When you're migrating from one database to another + +#### Using Sequential GUIDs in Entities + +A common pattern is to generate sequential GUIDs in your entity constructors. This ensures that every entity has a unique ID from the moment it's created, and the ID is optimized for database performance. + +```csharp +public class Product : AggregateRoot +{ + public Product() + { + // Generate a sequential GUID automatically + // Uses the default type configured in your application + Id = SequentialGuidGenerator.Instance.Create(SequentialGuidType.SequentialAsString); + } + + public string Name { get; set; } + public decimal Price { get; set; } + public bool IsActive { get; set; } +} +``` + +**Why generate IDs in the constructor?** +- **Consistency**: Every entity has an ID from creation +- **Domain events**: Events can reference the entity immediately +- **Database optimization**: Sequential GUIDs are used consistently +- **Testability**: Tests can rely on predictable ID generation + +### Performance Considerations + +**When Sequential GUIDs Improve Performance:** + +1. **High-insert tables** - Tables with frequent inserts benefit most +2. **Clustered indexes** - Only necessary for clustered indexes +3. **Range queries** - Queries that scan sequential ranges benefit +4. **Large tables** - Fragmentation affects large tables more + +**When Sequential GUIDs Might Not Help:** + +1. **Low-activity tables** - Small tables won't show measurable improvement +2. **Non-clustered indexes** - Only affects clustered indexes +3. **Random access patterns** - If queries are mostly lookups by ID, order matters less +4. **Read-mostly tables** - Performance gain is mostly on writes + +**Database-Specific Recommendations:** + +| Database | Recommended Type | Reason | +|-----------|------------------|---------| +| SQL Server | `SequentialAsBinary` | SQL Server stores GUIDs as binary and sorts them as binary | +| MySQL | `SequentialAsString` | MySQL stores GUIDs as strings and sorts them as strings | +| PostgreSQL | `SequentialAsString` | PostgreSQL treats GUIDs as strings for comparison | +| Oracle | `SequentialAsString` | Oracle's GUID comparison is string-based | + +### Custom GUID Generator + +If none of the built-in generators meet your needs, you can implement your own. This is useful for custom algorithms, UUID versions, or special formatting requirements. + +```csharp +/// +/// Custom GUID generator that creates time-based UUIDs (UUID v7 style) +/// +public class CustomGuidGenerator : IGuidGenerator, ITransientDependency +{ + public Guid Create() + { + // Create a GUID based on current timestamp and random data + // This is a simplified example of a UUID v7 implementation + var timestamp = DateTime.UtcNow.Ticks; + + var guidBytes = new byte[16]; + var timestampBytes = BitConverter.GetBytes(timestamp); + + // Copy timestamp to the beginning of the GUID + Buffer.BlockCopy(timestampBytes, 0, guidBytes, 0, 8); + + // Fill the rest with random data + var random = new Random(); + random.NextBytes(guidBytes, 8, 8); + + return new Guid(guidBytes); + } +} +``` + +**Registering your custom generator:** + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + // Replace the default IGuidGenerator with your custom implementation + context.Services.Replace(ServiceDescriptor.Transient()); +} +``` + +**Common custom generator scenarios:** +- UUID v6/v7 (time-ordered UUIDs) +- Namespace-based UUIDs (UUID v5) +- Prefix-based identifiers for multi-tenant systems +- Snowflake-style IDs (combining timestamp, machine ID, and sequence) + +--- + +## Memory Database + +### Overview + +The `Volo.Abp.MemoryDb` package provides an in-memory database implementation that follows the ABP repository pattern. It's designed for specific scenarios where a traditional database isn't necessary or isn't feasible. + +**When to Use Memory Database:** + +1. **Testing** - Perfect for unit and integration tests where you need a fast, disposable database +2. **Prototyping** - Quickly build and test features without setting up a database +3. **Demonstrations** - Create demo applications that don't require external dependencies +4. **Development** - Speed up development by eliminating database setup overhead +5. **Caching** - Use as a distributed cache layer or session store +6. **Single-instance applications** - Small applications where persistence isn't required + +**What Memory Database Provides:** + +- **Full ABP repository pattern** - All standard repository methods are available +- **Event bus integration** - Supports both local and distributed events +- **Audit logging** - Automatic tracking of creation, modification, and deletion +- **Unit of Work** - Complete transaction support for consistency +- **Soft delete** - Entities can be marked as deleted without being removed +- **Query support** - LINQ queries work like with real databases + +**Important Limitations:** + +- **No persistence** - All data is lost when the application restarts +- **Limited scalability** - Not suitable for large datasets or high-concurrency scenarios +- **No relationships** - No support for foreign keys or join operations +- **No query optimization** - All queries are executed in-memory without indexes +- **No concurrency control** - No built-in support for optimistic or pessimistic locking + +### Installation + +Add the `Volo.Abp.MemoryDb` package to your project: + +```xml + + + +``` + +### Features + +**Repository Pattern Support:** +- All standard CRUD operations (Insert, Update, Delete, Get, List, etc.) +- Query methods with LINQ expressions +- Paging and sorting support +- Async/await patterns + +**Event Bus Integration:** +- Local events for entity changes +- Distributed events for cross-service communication +- Automatic event publishing + +**Audit Logging:** +- Creation time tracking +- Modification time tracking +- Creator and modifier tracking +- Deletion tracking (for soft deletes) + +**Unit of Work:** +- Transactional operations +- Automatic commit/rollback +- Consistency guarantees + +### Configuration + +The memory database can be configured to use different serializers and customize its behavior. Configuration is done in your module's `ConfigureServices` method. + +```csharp +[DependsOn(typeof(AbpMemoryDbModule))] +public class MyModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + // Choose the serialization strategy + // Default is UTF8 JSON, which provides good performance and readability + options.Serializer = typeof(Utf8JsonMemoryDbSerializer); + + // You can also implement your own serializer + // options.Serializer = typeof(MyCustomSerializer); + }); + } +} +``` + +**Understanding the Options:** + +- **Serializer**: Controls how entities are serialized to/from JSON + - `Utf8JsonMemoryDbSerializer`: Fast UTF-8 JSON serializer (default) + - Custom implementations: You can implement your own for special requirements + +### Defining Memory Database + +To use the memory database, you need to create a context class that inherits from `MemoryDbContext`. This context defines your entity collections and any custom configuration. + +```csharp +public class AppMemoryDbContext : MemoryDbContext +{ + // The database provider is injected automatically by ABP + public AppMemoryDbContext(IMemoryDbDatabaseProvider databaseProvider) + : base(databaseProvider) + { + } + + public override void OnModelCreating(MemoryModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // Configure your entities here + // This is similar to EF Core's OnModelCreating + modelBuilder.Entity(b => + { + // Apply default ABP conventions + b.ConfigureByConvention(); + + // Add custom configuration + b.Property(x => x.Name).IsRequired(); + b.Property(x => x.Price).IsRequired(); + }); + } +} +``` + +**Understanding the Context:** + +- **MemoryDbContext**: Base class that provides the database infrastructure +- **OnModelCreating**: Configure entity mappings, constraints, and relationships +- **ConfigureByConvention**: Applies standard ABP conventions automatically + +### Usage Examples + +#### Basic Repository Usage + +The most common way to work with the memory database is through the repository pattern. ABP provides a generic repository interface that works with any entity type. + +```csharp +public class ProductService +{ + private readonly IMemoryDbRepository _productRepository; + + public ProductService(IMemoryDbRepository productRepository) + { + _productRepository = productRepository; + } + + // Create a new product + public async Task CreateAsync(string name, decimal price) + { + var product = new Product + { + Name = name, + Price = price, + IsActive = true + }; + + // Insert into the database + await _productRepository.InsertAsync(product); + return product; + } + + // Get all products + public async Task> GetListAsync() + { + return await _productRepository.GetListAsync(); + } + + // Get a single product by ID + public async Task GetAsync(Guid id) + { + return await _productRepository.GetAsync(id); + } + + // Update a product + public async Task UpdateAsync(Guid id, string newName, decimal newPrice) + { + var product = await _productRepository.GetAsync(id); + product.Name = newName; + product.Price = newPrice; + await _productRepository.UpdateAsync(product); + } + + // Delete a product + public async Task DeleteAsync(Guid id) + { + await _productRepository.DeleteAsync(id); + } +} +``` + +**Understanding Repository Methods:** + +- **InsertAsync**: Adds a new entity to the database +- **GetListAsync**: Returns all entities (can be filtered with LINQ) +- **GetAsync**: Returns a single entity by ID (throws if not found) +- **UpdateAsync**: Updates an existing entity +- **DeleteAsync**: Removes an entity from the database + +#### Querying with Specifications + +You can use LINQ to query entities, just like with Entity Framework or MongoDB. The memory database supports standard LINQ operators. + +```csharp +public async Task> GetActiveProductsAsync() +{ + // Query for active products, ordered by name + return await _productRepository + .Where(p => p.IsActive) + .OrderBy(p => p.Name) + .ToListAsync(); +} + +public async Task GetMostExpensiveProductAsync() +{ + return await _productRepository + .OrderByDescending(p => p.Price) + .FirstOrDefaultAsync(); +} + +public async Task> GetProductsInRangeAsync(decimal minPrice, decimal maxPrice) +{ + return await _productRepository + .Where(p => p.Price >= minPrice && p.Price <= maxPrice) + .ToListAsync(); +} + +public async Task GetActiveProductCountAsync() +{ + return await _productRepository + .CountAsync(p => p.IsActive); +} +``` + +**Query capabilities:** +- Filtering with `Where` +- Sorting with `OrderBy` and `OrderByDescending` +- Pagination with `Skip` and `Take` +- Aggregation with `Count`, `Sum`, `Average`, etc. +- Projection with `Select` +- Grouping with `GroupBy` + +#### Working with Collections + +For advanced scenarios, you can work directly with the underlying collection. This gives you more control but requires more manual management. + +```csharp +public async Task BulkUpdatePricesAsync(decimal percentage) +{ + // Get the collection directly + var collection = await _productRepository.GetCollectionAsync(); + + // Get all products + var products = await collection.ToListAsync(); + + // Update each product + foreach (var product in products) + { + product.Price = product.Price * (1 + percentage / 100); + } + + // Update all products in one operation + await collection.UpdateRangeAsync(products); +} + +public async Task RemoveInactiveProductsAsync() +{ + var collection = await _productRepository.GetCollectionAsync(); + + // Get all inactive products + var inactiveProducts = await collection + .Where(p => !p.IsActive) + .ToListAsync(); + + // Delete all inactive products + await collection.DeleteRangeAsync(inactiveProducts); +} +``` + +**When to use collections:** +- Bulk operations (update or delete multiple entities) +- Custom queries that aren't supported by the repository +- Performance optimization for large datasets + +#### Database Operations + +You can access the underlying database directly for advanced scenarios like creating new collections or managing multiple entity types together. + +```csharp +public async Task DatabaseOperationsAsync() +{ + // Get the database instance + var database = await _productRepository.GetDatabaseAsync(); + + // Create a new collection for orders + var orderCollection = database.CreateCollection(); + + // Add some orders + await orderCollection.AddAsync(new Order { ProductId = 1, Quantity = 5 }); + await orderCollection.AddAsync(new Order { ProductId = 2, Quantity = 10 }); + + // Query orders + var orders = await orderCollection.Where(o => o.Quantity > 5).ToListAsync(); + + // Delete an order + await orderCollection.DeleteAsync(orderCollection.First()); + + // Check if a collection exists + var hasOrders = await database.CollectionExistsAsync(); +} +``` + +**Understanding database operations:** +- **CreateCollection**: Creates a new entity collection +- **CollectionExists**: Checks if a collection exists +- **GetCollection**: Retrieves an existing collection + +### Custom Serialization + +By default, the memory database uses JSON serialization. If you need custom serialization (for example, for specific data types or performance optimization), you can implement your own serializer. + +```csharp +/// +/// Custom serializer that uses a different JSON library +/// +public class CustomMemoryDbSerializer : IMemoryDbSerializer +{ + public T Deserialize(string json) + { + // Use your preferred JSON library + return JsonSerializer.Deserialize(json); + } + + public string Serialize(T entity) + { + // Serialize with your preferred options + return JsonSerializer.Serialize(entity); + } +} +``` + +**Registering custom serializer:** + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + Configure(options => + { + options.Serializer = typeof(CustomMemoryDbSerializer); + }); +} +``` + +**Custom serializer scenarios:** +- Different JSON libraries (Newtonsoft, ServiceStack) +- Binary serialization for better performance +- Custom format for specific data types +- Compression for memory efficiency + +### Testing with Memory Database + +The memory database is perfect for testing because it's fast, disposable, and doesn't require external dependencies. + +```csharp +public class ProductServiceTests : AbpIntegratedTest +{ + private readonly ProductService _productService; + private readonly IMemoryDbRepository _productRepository; + + public ProductServiceTests() + { + // ABP automatically injects the services + _productService = GetRequiredService(); + _productRepository = GetRequiredService>(); + } + + [Fact] + public async Task Should_Create_Product() + { + // Act + var product = await _productService.CreateAsync("Test Product", 99.99m); + + // Assert + product.ShouldNotBeNull(); + product.Name.ShouldBe("Test Product"); + product.Price.ShouldBe(99.99m); + product.IsActive.ShouldBeTrue(); + } + + [Fact] + public async Task Should_Get_Product() + { + // Arrange + var created = await _productService.CreateAsync("Test Product", 99.99m); + + // Act + var retrieved = await _productService.GetAsync(created.Id); + + // Assert + retrieved.ShouldNotBeNull(); + retrieved.Id.ShouldBe(created.Id); + retrieved.Name.ShouldBe("Test Product"); + } + + [Fact] + public async Task Should_Query_Products() + { + // Arrange + await _productService.CreateAsync("Product A", 10.00m); + await _productService.CreateAsync("Product B", 20.00m); + await _productService.CreateAsync("Product C", 30.00m); + + // Act + var products = await _productRepository.GetListAsync(); + + // Assert + products.Count.ShouldBeGreaterThanOrEqualTo(3); + products.ShouldContain(p => p.Name == "Product A"); + products.ShouldContain(p => p.Name == "Product B"); + products.ShouldContain(p => p.Name == "Product C"); + } +} +``` + +**Testing best practices:** +- Use a fresh database for each test (ABP handles this) +- Test both success and failure scenarios +- Verify business logic, not just data persistence +- Use `Should*` assertions for readable tests + +### Limitations and Considerations + +**When NOT to Use Memory Database:** + +1. **Production applications** - Data loss on restart is unacceptable +2. **Large datasets** - Performance degrades with millions of records +3. **High concurrency** - No locking or conflict resolution +4. **Complex queries** - No support for joins, group by, or aggregates +5. **Data relationships** - No foreign key constraints or cascading deletes + +**Migration Considerations:** + +When moving from memory database to a real database: +- Entity IDs might change (depending on your GUID strategy) +- Indexes won't exist - you need to create them +- Relationships won't be enforced - you need to add foreign keys +- Queries might behave differently - optimization varies by database + +**Performance Tips:** + +1. **Use bulk operations** - UpdateRange/DeleteRange are faster than multiple individual calls +2. **Index frequently queried fields** - Even though it's in-memory, indexing helps +3. **Dispose unused collections** - Free memory when you're done with a collection +4. **Use appropriate serialization** - JSON is good, but binary might be faster for large datasets + +--- + +## Advanced Bundling + +### Overview + +ABP's bundling system provides advanced features for managing CSS and JavaScript files in your web applications. Bundling combines multiple files into single downloads, reducing HTTP requests. Minification removes unnecessary characters (whitespace, comments), reducing file sizes. Together, these techniques significantly improve page load performance. + +**Why Use Bundling and Minification?** + +Modern web applications often include dozens or hundreds of CSS and JavaScript files. Without bundling: + +1. **More HTTP requests** - Each file requires a separate request to the server +2. **Slower page loads** - Browsers limit the number of concurrent requests +3. **Higher bandwidth** - Unminified files contain unnecessary characters +4. **Poor caching** - Multiple files are harder to cache effectively + +With bundling and minification: + +1. **Fewer requests** - Multiple files combined into one or two bundles +2. **Faster page loads** - Fewer round trips to the server +3. **Lower bandwidth** - Minified files are 30-70% smaller +4. **Better caching** - Single bundle is cached until it changes + +### Bundling Modes + +ABP provides four different bundling modes, allowing you to control when and how files are bundled and minified: + +1. **None** - No bundling or minification + - All files are referenced individually + - Use during development for debugging + - Easier to track errors to specific files + - Slower page loads but better developer experience + +2. **Bundle** - Files are combined but not minified + - Reduces HTTP requests but keeps code readable + - Good for debugging in staging environments + - Moderate performance improvement + - Easier to track issues than minified code + +3. **BundleAndMinify** - Files are combined and minified + - Maximum performance improvement + - Use in production for fastest page loads + - Harder to debug but you can use source maps + - Significant bandwidth savings + +4. **Auto** - Automatically bundles and minifies in production + - Development: No bundling (easier debugging) + - Production: Bundling and minification (better performance) + - Best of both worlds + - No need to manually switch modes + +### Core Components + +#### BundleManager + +The `BundleManager` is the central component that manages the bundling system. It's responsible for: + +- Collecting files from bundle contributors +- Determining which bundling mode to use +- Generating bundle files (combining and minifying) +- Caching bundles for performance +- Serving bundles to clients + +```csharp +public interface IBundleManager +{ + // Get all script files that should be included + Task> GetScriptFilesAsync(); + + // Get all style files that should be included + Task> GetStyleFilesAsync(); + + // Get the combined content of a script bundle + Task GetScriptBundleAsync(string bundleName); + + // Get the combined content of a style bundle + Task GetStyleBundleAsync(string bundleName); + + // Check if bundling is currently enabled + bool IsBundlingEnabled(); + + // Check if minification is currently enabled + bool IsMinificationEnabled(); +} +``` + +**Understanding the BundleManager:** + +- **Files**: Returns the list of individual files that make up a bundle +- **Bundle**: Returns the combined, potentially minified content +- **Enabled checks**: Allow you to write conditional code based on bundling status + +#### BundleContributor + +Bundle contributors are classes that define which files should be included in bundles. They're registered with the bundling system and their contributions are collected when bundles are generated. + +```csharp +public abstract class BundleContributor +{ + // Called when the bundle is being configured + // Use this method to add files to the bundle + public virtual void ConfigureBundle(BundleConfigurationContext context) + { + // Add files to the bundle + // context.Files.AddIfNotContains("/path/to/file.js"); + } + + // Can be overridden to provide additional configuration + public virtual void ConfigureBundle(BundleConfigurationContext context) + { + // Configure bundle settings + } +} +``` + +**How contributors work:** + +1. **Registration**: You register contributors with the bundling system +2. **Configuration**: When bundles are built, each contributor's `ConfigureBundle` is called +3. **Collection**: Contributors add files to the bundle context +4. **Deduplication**: Files that are added multiple times are only included once +5. **Bundle generation**: All collected files are combined into a single bundle + +### Configuration + +#### Basic Configuration + +Configure the bundling system in your module's `ConfigureServices` method: + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + Configure(options => + { + // Set the bundling mode + // Auto means bundle in production, not in development + options.Mode = BundlingMode.BundleAndMinify; + + // Define style bundles + options.StyleBundles.Add( + new BundleConfig("LeptonXGlobalStyles") + .AddContributor() + .AddContributor() + ); + + // Define script bundles + options.ScriptBundles.Add( + new BundleConfig("LeptonXGlobalScripts") + .AddContributor() + .AddContributor() + .AddContributor() + ); + }); +} +``` + +**Understanding the configuration:** + +- **Mode**: Controls when bundling and minification are applied +- **StyleBundles**: Configuration for CSS bundles +- **ScriptBundles**: Configuration for JavaScript bundles +- **BundleConfig**: Defines a named bundle with its contributors +- **AddContributor**: Adds a contributor class to the bundle + +#### Custom Contributor Options + +You can configure individual contributors to control their behavior: + +```csharp +Configure(options => +{ + // Configure a specific contributor + options.Contributors() + .Configure(contributor => + { + // Set priority: Higher values load first + contributor.Priority = -1; + + // Enable/disable the contributor + contributor.IsEnabled = true; + }); +}); +``` + +**Understanding priority:** + +Contributors are processed in order of their priority: +- **Higher priority** contributors' files are added first +- **Default priority** is 0 +- **Use priority** to control the order of files in the bundle (important for dependencies) + +### Creating Custom Bundles + +#### Basic Bundle Contributor + +Create a simple bundle contributor for your application's CSS files: + +```csharp +/// +/// Adds my application's styles to the global styles bundle +/// +public class MyAppStyleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + // Add files if they're not already in the bundle + // This prevents duplicate files + context.Files.AddIfNotContains("/libs/mylib/styles/main.css"); + context.Files.AddIfNotContains("/css/my-styles.css"); + context.Files.AddIfNotContains("/css/components/buttons.css"); + context.Files.AddIfNotContains("/css/components/cards.css"); + } +} +``` + +**Why use AddIfNotContains?** +- Prevents duplicate files in the bundle +- Allows multiple contributors to safely add the same file +- Ensures each file is only included once +- Makes contributors more flexible and reusable + +#### Script Bundle Contributor + +Create a contributor for JavaScript files: + +```csharp +/// +/// Adds my application's scripts to the global scripts bundle +/// +public class MyAppScriptContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + // Add jQuery (dependency of many libraries) + context.Files.AddIfNotContains("/libs/jquery/jquery.min.js"); + + // Add Bootstrap + context.Files.AddIfNotContains("/libs/bootstrap/bootstrap.bundle.min.js"); + + // Add your application scripts + context.Files.AddIfNotContains("/js/main.js"); + context.Files.AddIfNotContains("/js/components/buttons.js"); + context.Files.AddIfNotContains("/js/utils/helpers.js"); + } +} +``` + +**Understanding script dependencies:** +- Files are loaded in the order they're added +- Dependencies must be added before they're used +- Use `AddIfNotContains` to handle dependencies properly +- Test your bundles to ensure all dependencies load correctly + +#### Dynamic Bundle Contributor + +Sometimes you want to include different files based on conditions (environment, configuration, etc.): + +```csharp +/// +/// Adds different files based on the environment +/// +public class DynamicStyleContributor : BundleContributor +{ + private readonly IConfiguration _configuration; + private readonly IWebHostEnvironment _env; + + public DynamicStyleContributor( + IConfiguration configuration, + IWebHostEnvironment env) + { + _configuration = configuration; + _env = env; + } + + public override void ConfigureBundle(BundleConfigurationContext context) + { + // Add development-specific files + if (_env.IsDevelopment()) + { + context.Files.Add("/css/dev-styles.css"); + context.Files.Add("/css/dev-theme.css"); + } + // Add production-specific files + else if (_env.IsProduction()) + { + context.Files.Add("/css/prod-styles.css"); + context.Files.Add("/css/prod-theme.css"); + } + + // Add environment-specific files based on configuration + var theme = _configuration["App:Theme"]; + context.Files.AddIfNotContains($"/css/themes/{theme}.css"); + } +} +``` + +**Dynamic contributor use cases:** +- Environment-specific files (dev/test/prod) +- Feature flags - include files only for enabled features +- Theme switching - different styles for different themes +- Localization - locale-specific files +- A/B testing - different bundles for different test groups + +### Using Bundles in Views + +#### Using Tag Helpers + +ABP provides tag helpers for referencing individual files: + +```html + + + + + + + + + + +``` + +**When to use individual files:** +- During development for easier debugging +- When files are very large and should be loaded separately +- When you want to control loading order manually +- When you're not using bundling + +#### Using Bundles + +Reference pre-configured bundles: + +```html + + + + + + + + + + + My Application + + + + @RenderBody() + + + +``` + +**Benefits of using bundles:** +- Fewer HTTP requests - all files in one request +- Better caching - one cache entry instead of many +- Automatic minification - smaller file sizes +- Versioning - bundle URLs include version for cache busting + +#### Conditional Bundling + +You can write conditional logic to handle different scenarios: + +```html +@inject IOptions BundlingOptions + +@* Check if bundling is enabled *@ +@if (BundlingOptions.Value.Mode == BundlingMode.None) +{ + @* Individual files for debugging *@ + + + + +} +else +{ + @* Bundled files for production *@ + + +} +``` + +**When to use conditional bundling:** +- When you want different behavior in different environments +- When you need to debug issues in production +- When you're A/B testing different bundling strategies +- When you have special pages that need separate bundles + +### Advanced Features + +#### Bundle Priorities + +Control the order in which contributors' files are added to bundles: + +```csharp +/// +/// Adds critical CSS that should be loaded first +/// +public class CriticalStyleContributor : BundleContributor +{ + public CriticalStyleContributor() + { + // Set priority: Higher values load first + // This ensures critical CSS is included before other styles + Priority = 100; + } + + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("/css/critical.css"); + } +} +``` + +**Understanding priority:** +- Default priority is 0 +- Contributors are processed in descending priority order +- Higher priority files are added to the bundle first +- Use this to control CSS specificity and JavaScript execution order + +**Priority examples:** +- **100**: Critical CSS (above-the-fold styles) +- **50**: Core libraries (jQuery, Bootstrap) +- **10**: Application styles +- **0**: Default priority +- **-10**: Page-specific styles (should override defaults) + +#### External Resources + +Include files from CDNs or external sources: + +```csharp +/// +/// Adds external CDN resources +/// +public class CdnScriptContributor : BundleContributor +{ + private readonly IConfiguration _configuration; + + public CdnScriptContributor(IConfiguration configuration) + { + _configuration = configuration; + } + + public override void ConfigureBundle(BundleConfigurationContext context) + { + var useCdn = _configuration.GetValue("UseCdn"); + + // Add CDN resource if enabled + if (useCdn) + { + context.Files.Add( + "https://code.jquery.com/jquery-3.6.0.min.js", + isExternal: true + ); + } + // Add local resource as fallback + else + { + context.Files.AddIfNotContains("/libs/jquery/jquery-3.6.0.min.js"); + } + } +} +``` + +**External resource considerations:** +- **CDN benefits**: Faster delivery, caching at edge servers +- **CDN risks**: Availability, privacy, compliance +- **Fallback strategy**: Always have local files as backup +- **Versioning**: Pin to specific versions for stability + +#### CDN Fallback + +Provide a fallback if the CDN resource fails to load: + +```html + +``` + +**How fallback works:** +- Try to load the CDN version first +- If it fails, `onerror` fires +- The fallback script is written to the page +- Ensures the library is always available + +### Caching + +Bundles are automatically cached for performance. This reduces the overhead of regenerating bundles on every request. + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + Configure(options => + { + // Set how long bundles are cached + // Default is 7 days + options.CacheDuration = TimeSpan.FromDays(7); + + // You can also disable caching for development + // options.CacheDuration = TimeSpan.Zero; + }); +} +``` + +**Understanding bundle caching:** + +- **First request**: Bundle is generated and cached +- **Subsequent requests**: Bundle is served from cache (much faster) +- **Cache invalidation**: Cache is cleared when files change +- **Cache storage**: In-memory cache by default + +**When to disable caching:** +- During development - you want changes to appear immediately +- During debugging - to rule out cache issues +- For frequently changing bundles - to avoid stale content + +### Minification + +Configure minification settings for CSS and JavaScript: + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + Configure(options => + { + // Configure minification + options.MinificationOptions.CssMinifier = new NUglifyCssMinifier(); + options.MinificationOptions.JavascriptMinifier = new NUglifyJavascriptMinifier(); + }); +} +``` + +**What minification does:** + +**CSS Minification:** +- Removes whitespace and line breaks +- Removes comments +- Shortens color codes (#ffffff to #fff) +- Optimizes selectors and properties + +**JavaScript Minification:** +- Removes whitespace and line breaks +- Removes comments +- Renames variables to shorter names +- Removes unnecessary code + +**Minification safety:** +- Preserves functionality +- Maintains compatibility +- Can use source maps for debugging +- Can be disabled for specific files + +--- + +## Image Manipulation + +### Overview + +The ABP Framework provides powerful image manipulation capabilities through multiple integrations including ImageSharp, MagickNet, and SkiaSharp. These packages support image compression, resizing, format conversion, and more, making it easy to process images in your applications. + +**Common Use Cases:** + +1. **Profile pictures** - Resize and compress user avatars +2. **Image galleries** - Create thumbnails and optimized versions +3. **Document processing** - Convert between formats (PNG to JPEG, etc.) +4. **Watermarking** - Add logos or copyright text to images +5. **Batch processing** - Optimize all images in a folder +6. **Responsive images** - Generate multiple sizes for different devices + +**Why Use Image Processing Libraries?** + +- **Storage savings** - Compressed images use 30-90% less space +- **Faster page loads** - Smaller images load faster +- **Better user experience** - Optimized images display quickly +- **Format compatibility** - Convert images to supported formats +- **Consistent dimensions** - Ensure all images meet your requirements + +### Installation + +Choose the image processing library that best fits your needs: + +#### ImageSharp (Recommended) + +ImageSharp is a modern, cross-platform library with excellent .NET support: + +```xml + + + +``` + +**Best for:** +- Modern .NET applications (.NET Core, .NET 5+) +- Cross-platform support (Windows, Linux, macOS) +- Most common formats (JPEG, PNG, WebP, GIF, BMP, TIFF) +- Good performance with moderate memory usage + +#### MagickNet + +MagickNet provides support for 100+ formats and professional image processing: + +```xml + + + +``` + +**Best for:** +- Professional image editing applications +- Working with rare or proprietary formats +- Complex image manipulations (filters, effects) +- Format conversion (RAW to JPEG, etc.) + +#### SkiaSharp + +SkiaSharp is Google's Skia graphics library for .NET: + +```xml + + + +``` + +**Best for:** +- Mobile applications (iOS, Android) +- Cross-platform graphics +- Rendering complex graphics and text +- High-performance scenarios + +### Core Interfaces + +#### IImageResizer + +The `IImageResizer` interface provides methods to resize images to specific dimensions: + +```csharp +public interface IImageResizer +{ + // Resize an image from a stream + Task> TryResizeAsync( + Stream stream, + int width, + int height, + string? mimeType = null, + CancellationToken cancellationToken = default); + + // Resize an image from a byte array + Task> TryResizeAsync( + byte[] bytes, + int width, + int height, + string? mimeType = null, + CancellationToken cancellationToken = default); +} +``` + +**How resizing works:** +- Images are scaled to fit the specified dimensions +- Aspect ratio is preserved by default +- Images smaller than the target size may or may not be upscaled (depends on implementation) +- The result includes the processed image and a status code + +#### IImageCompressor + +The `IImageCompressor` interface provides methods to compress images to reduce file size: + +```csharp +public interface IImageCompressor +{ + // Compress an image from a stream + Task> TryCompressAsync( + Stream stream, + string? mimeType = null, + CancellationToken cancellationToken = default); + + // Compress an image from a byte array + Task> TryCompressAsync( + byte[] bytes, + string? mimeType = null, + CancellationToken cancellationToken = default); +} +``` + +**How compression works:** +- Compressors optimize the image data to reduce file size +- Different strategies for different formats (JPEG quality, PNG compression level) +- Quality is maintained while file size is reduced +- The result includes the processed image and a status code + +**Image process states:** +- **Done**: Processing completed successfully +- **Unsupported**: Format is not supported +- **Canceled**: Processing was canceled (e.g., compression didn't reduce size) +- **Error**: An error occurred during processing + +### ImageSharp Configuration + +Configure ImageSharp behavior for compression and quality: + +```csharp +[DependsOn(typeof(AbpImagingImageSharpModule))] +public class MyModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + // Configure JPEG compression + options.JpegEncoder = new JpegEncoder + { + Quality = 85 // 0-100, 85 is a good balance + }; + + // Configure PNG compression + options.PngEncoder = new PngEncoder + { + CompressionLevel = PngCompressionLevel.BestCompression + // Options: Default, Fast, Better, Best + }; + + // Configure WebP compression + options.WebpEncoder = new WebpEncoder + { + Quality = 80 // 0-100, 80 is a good balance + }; + }); + } +} +``` + +**Understanding quality settings:** + +- **JPEG Quality**: 0-100, higher means better quality but larger files + - 90-100: Near lossless, large files + - 80-90: High quality, good balance (recommended) + - 70-80: Medium quality, smaller files + - 60-70: Low quality, much smaller files + - Below 60: Poor quality, visible artifacts + +- **PNG Compression Level**: + - Default: Balanced speed and size + - Fast: Quick compression, larger files + - Better: Slower, better compression + - Best: Slowest, best compression + +### Usage Examples + +#### Image Compression + +Compress images to reduce file size while maintaining quality: + +```csharp +public class ImageService +{ + private readonly IImageCompressor _imageCompressor; + + public ImageService(IImageCompressor imageCompressor) + { + _imageCompressor = imageCompressor; + } + + /// + /// Compresses an image and returns the compressed version + /// Falls back to the original if compression fails + /// + public async Task CompressImageAsync(byte[] originalImage) + { + var result = await _imageCompressor.TryCompressAsync(originalImage); + + // Check if compression was successful + if (result.State == ImageProcessState.Done) + { + // Compression succeeded, return the compressed image + return result.Result; + } + + // Compression failed or didn't reduce size, return original + return originalImage; + } + + /// + /// Compresses an image with specific settings + /// + public async Task CompressImageAsync(byte[] originalImage, string mimeType) + { + var result = await _imageCompressor.TryCompressAsync(originalImage, mimeType); + + // Check if the format is supported + if (result.State == ImageProcessState.Unsupported) + { + throw new NotSupportedException($"Image format '{mimeType}' is not supported for compression"); + } + + return result.Result ?? originalImage; + } +} +``` + +**When to compress images:** +- After user uploads +- During batch processing +- When preparing images for different devices +- When generating thumbnails or previews + +**Compression strategy:** +- Try to compress first +- If compression fails, use the original +- Log compression results for monitoring +- Set appropriate quality based on use case + +#### Image Resizing + +Resize images to specific dimensions: + +```csharp +public class ImageResizeService +{ + private readonly IImageResizer _imageResizer; + + public ImageResizeService(IImageResizer imageResizer) + { + _imageResizer = imageResizer; + } + + /// + /// Creates a square thumbnail of the specified size + /// + public async Task CreateThumbnailAsync(byte[] originalImage, int size) + { + var result = await _imageResizer.TryResizeAsync( + originalImage, + size, // Width + size // Height - Creates a square image + ); + + if (result.State == ImageProcessState.Done) + { + return result.Result; + } + + throw new Exception("Failed to resize image"); + } + + /// + /// Creates a resized image that fits within the specified dimensions + /// Aspect ratio is preserved + /// + public async Task CreateResizedImageAsync( + byte[] originalImage, + int maxWidth, + int maxHeight) + { + var result = await _imageResizer.TryResizeAsync( + originalImage, + maxWidth, + maxHeight); + + if (result.State == ImageProcessState.Done) + { + return result.Result; + } + + throw new Exception("Failed to resize image"); + } + + /// + /// Creates multiple sizes for responsive images + /// + public async Task> CreateResponsiveImagesAsync( + byte[] originalImage) + { + var images = new Dictionary(); + + // Create different sizes for different devices + var sizes = new[] + { + ("small", 320), + ("medium", 640), + ("large", 1280), + ("xlarge", 1920) + }; + + foreach (var (name, size) in sizes) + { + var result = await _imageResizer.TryResizeAsync( + originalImage, + size, + size); + + if (result.State == ImageProcessState.Done) + { + images[name] = result.Result; + } + } + + return images; + } +} +``` + +**Common image sizes:** + +| Size Name | Dimensions | Use Case | +|-----------|-------------|------------| +| Thumbnail | 100x100 | Profile pictures, list icons | +| Small | 320x320 | Mobile screens, thumbnails | +| Medium | 640x640 | Tablet screens, previews | +| Large | 1280x1280 | Desktop displays | +| X-Large | 1920x1920 | High-DPI displays | + +#### Resize and Compress + +For optimal results, resize first, then compress: + +```csharp +public class ImageOptimizationService +{ + private readonly IImageResizer _imageResizer; + private readonly IImageCompressor _imageCompressor; + + public ImageOptimizationService( + IImageResizer imageResizer, + IImageCompressor imageCompressor) + { + _imageResizer = imageResizer; + _imageCompressor = imageCompressor; + } + + /// + /// Optimizes an image by resizing and compressing it + /// This is the recommended order: resize first, compress last + /// + public async Task OptimizeImageAsync( + byte[] image, + int targetWidth, + int targetHeight) + { + // Step 1: Resize the image + // Reducing dimensions first reduces the amount of data to compress + var resizeResult = await _imageResizer.TryResizeAsync( + image, + targetWidth, + targetHeight); + + if (resizeResult.State != ImageProcessState.Done) + { + return image; // Return original if resize failed + } + + // Convert the result to a byte array + var resizedBytes = await resizeResult.Result.GetAllBytesAsync(); + await resizeResult.Result.DisposeAsync(); + + // Step 2: Compress the resized image + // Compression works better on smaller images + var compressResult = await _imageCompressor.TryCompressAsync(resizedBytes); + + if (compressResult.State == ImageProcessState.Done) + { + return compressResult.Result; + } + + // Return resized (but not compressed) if compression failed + return resizedBytes; + } + + /// + /// Optimizes an image for web display + /// Creates multiple versions for different scenarios + /// + public async Task OptimizeForWebAsync(byte[] originalImage) + { + var versions = new ImageVersions(); + + // Create thumbnail (small, highly compressed) + versions.Thumbnail = await OptimizeImageAsync(originalImage, 100, 100); + + // Create preview (medium, moderately compressed) + versions.Preview = await OptimizeImageAsync(originalImage, 400, 400); + + // Create display version (large, lightly compressed) + versions.Display = await OptimizeImageAsync(originalImage, 1920, 1080); + + return versions; + } +} + +public class ImageVersions +{ + public byte[] Thumbnail { get; set; } + public byte[] Preview { get; set; } + public byte[] Display { get; set; } +} +``` + +**Why resize before compress?** +- **Better compression**: Smaller images compress more effectively +- **Less memory**: Processing smaller images uses less RAM +- **Faster**: Compression is faster on smaller data +- **Quality control**: You control dimensions, compression controls quality + +### Image Processing Pipeline + +Create a reusable pipeline for complex image processing: + +```csharp +/// +/// Processes images through a series of steps +/// Each step can transform, compress, or modify the image +/// +public class ImageProcessorService +{ + private readonly IEnumerable _contributors; + + public ImageProcessorService( + IEnumerable contributors) + { + _contributors = contributors; + } + + /// + /// Processes an image through all registered contributors + /// Each contributor has a chance to transform the image + /// + public async Task ProcessImageAsync(byte[] image) + { + byte[] currentImage = image; + + // Process through each contributor in order + foreach (var contributor in _contributors) + { + var result = await contributor.ProcessAsync(currentImage); + + // Only continue if processing was successful + if (result.State == ImageProcessState.Done) + { + currentImage = result.Result; + } + } + + return currentImage; + } + + /// + /// Processes an image with a specific quality setting + /// + public async Task ProcessImageAsync( + byte[] image, + ImageQuality quality) + { + byte[] currentImage = image; + + // Apply filters based on quality setting + foreach (var contributor in _contributors) + { + // Skip contributors that don't match the quality level + if (!contributor.ShouldProcess(quality)) + { + continue; + } + + var result = await contributor.ProcessAsync(currentImage); + if (result.State == ImageProcessState.Done) + { + currentImage = result.Result; + } + } + + return currentImage; + } +} + +public enum ImageQuality +{ + Maximum, + High, + Medium, + Low, + Thumbnail +} +``` + +### Custom Image Processor + +Implement your own image processing logic: + +```csharp +/// +/// Adds a watermark to images +/// This is a custom processor you can add to the pipeline +/// +public class WatermarkProcessor : IImageProcessingContributor, ITransientDependency +{ + public async Task> ProcessAsync(byte[] image) + { + using var original = Image.Load(image); + + // Load watermark image + using var watermark = Image.Load("watermark.png"); + + // Calculate watermark position (bottom-right corner) + var x = original.Width - watermark.Width - 10; + var y = original.Height - watermark.Height - 10; + + // Draw watermark on the original image + using var graphics = original.Mutate(ctx => ctx + .DrawImage( + watermark, + new Point(x, y), + new GraphicsOptions + { + AlphaCompositionMode = PixelAlphaCompositionMode.SrcOver, + BlenderMode = PixelBlenderMode.Normal + })); + + // Save to memory stream + using var resultStream = new MemoryStream(); + await original.SaveAsJpegAsync(resultStream); + + // Return the watermarked image + return new ImageProcessResult(resultStream.ToArray()); + } + + public bool ShouldProcess(ImageQuality quality) + { + // Only add watermark to high-quality images + return quality == ImageQuality.High || quality == ImageQuality.Maximum; + } +} +``` + +**Custom processor ideas:** +- **Watermarking**: Add logos or copyright text +- **Filters**: Apply blur, sharpen, or color adjustments +- **Cropping**: Automatically crop to specific aspect ratios +- **Format conversion**: Convert PNG to JPEG, etc. +- **Metadata removal**: Strip EXIF data for privacy + +### Supported Formats + +#### ImageSharp +**Formats:** JPEG, PNG, WebP, GIF, BMP, TIFF + +**Best for:** +- General purpose image processing +- .NET-only applications +- Web applications +- Performance-critical scenarios + +**Limitations:** +- No RAW format support +- Limited format-specific features +- No advanced image editing (layers, etc.) + +#### MagickNet +**Formats:** 100+ including RAW, PSD, PDF, SVG, etc. + +**Best for:** +- Professional image editing applications +- Photography workflows +- Format conversion +- Complex manipulations + +**Limitations:** +- Heavier than ImageSharp +- More complex API +- May require native dependencies + +#### SkiaSharp +**Formats:** JPEG, PNG, WebP, GIF, BMP + +**Best for:** +- Cross-platform applications +- Mobile development (iOS, Android) +- Graphics rendering and drawing +- Games and visual applications + +**Limitations:** +- Fewer formats than MagickNet +- Primarily focused on rendering +- Limited format-specific features + +### Performance Tips + +1. **Resize first, compress last** - Resizing before compression reduces file size significantly +2. **Use appropriate quality** - 80-85 for JPEG, balanced settings for PNG +3. **Cache processed images** - Avoid reprocessing the same image multiple times +4. **Use streams** - Avoid loading entire images into memory unnecessarily +5. **Process in parallel** - Multiple images can be processed concurrently +6. **Dispose properly** - Always dispose of image objects to free memory +7. **Limit dimensions** - Don't create images larger than needed +8. **Choose the right format** - JPEG for photos, PNG for graphics + +**Performance comparison:** + +| Operation | Time | Memory | +|------------|--------|---------| +| Original (no processing) | 1ms | 2MB | +| Resize only | 50ms | 5MB | +| Compress only | 200ms | 10MB | +| Resize + Compress | 100ms | 6MB | + +--- + +## UI Packages + +### Overview + +The `Volo.Abp.AspNetCore.Mvc.UI.Packages` package provides pre-configured bundles for over 30 popular JavaScript libraries and frameworks. Instead of manually configuring each library, you can simply add the contributors and start using the libraries immediately. + +**Available Packages:** + +**Core Libraries:** +- **Bootstrap** - CSS framework for responsive design +- **jQuery** - JavaScript utility library +- **jQuery Validation** - Form validation +- **Popper** - Positioning engine for tooltips and popovers + +**UI Components:** +- **DataTables** - Advanced data tables with sorting, filtering, pagination +- **Select2** - Enhanced select boxes with search and tagging +- **SweetAlert2** - Beautiful alerts and modals +- **Cropper.js** - Image cropping +- **Uppy** - Modern file uploader + +**Charts and Visualizations:** +- **Chart.js** - Charting library +- **Moment.js** - Date/time manipulation +- **Luxon** - Modern date/time library + +**Editors and Rich Text:** +- **TUI Editor** - Markdown editor +- **CodeMirror** - Code editor +- **MarkdownIt** - Markdown parser + +**Utilities:** +- **Font Awesome** - Icon font +- **QRCode** - QR code generation +- **Clipboard** - Copy to clipboard +- **Slugify** - URL slug generation + +**And many more...** + +### Installation + +```xml + + + +``` + +### Usage + +#### Using Pre-configured Bundles + +Add bundle contributors to your bundling configuration: + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + Configure(options => + { + // Add jQuery bundle + options.ScriptBundles.Add( + new BundleConfig("JQuery") + .AddContributor() + ); + + // Add Bootstrap bundles (CSS and JavaScript) + options.ScriptBundles.Add( + new BundleConfig("Bootstrap") + .AddContributor() + ); + + options.StyleBundles.Add( + new BundleConfig("Bootstrap") + .AddContributor() + ); + + // Add Bootstrap Datepicker + options.ScriptBundles.Add( + new BundleConfig("BootstrapDatepicker") + .AddContributor() + ); + + options.StyleBundles.Add( + new BundleConfig("BootstrapDatepicker") + .AddContributor() + ); + }); +} +``` + +**Understanding the configuration:** +- **BundleConfig**: Creates a named bundle +- **AddContributor**: Adds a contributor class that provides library files +- **ScriptBundles vs StyleBundles**: Separate bundles for JS and CSS + +#### In Your Views + +Reference the bundles in your layout or views: + +```html + + + + My Application + + + + + + + + + + @RenderBody() + + + + + + + + + + + + +``` + +### Creating Custom Package Bundles + +#### Example: Adding Chart.js + +Create a custom bundle contributor for Chart.js: + +```csharp +/// +/// Adds Chart.js library to the bundles +/// +public class ChartJsContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + // Add Chart.js from your libs folder + // AddIfNotContains prevents duplicates + context.Files.AddIfNotContains("/libs/chartjs/chart.min.js"); + + // You can also add plugins + context.Files.AddIfNotContains("/libs/chartjs/chartjs-plugin-datalabels.min.js"); + } +} +``` + +**Register the bundle:** + +```csharp +Configure(options => +{ + options.ScriptBundles.Add( + new BundleConfig("ChartJs") + .AddContributor() + ); +}); +``` + +**Use in your view:** + +```html + + +``` + +### Package Dependencies + +Some packages depend on others. For example, Bootstrap requires jQuery. You need to configure dependencies properly: + +```csharp +/// +/// Adds Bootstrap (which depends on jQuery) +/// +public class MyBootstrapContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + // Add jQuery first (Bootstrap requires it) + // The order matters here - jQuery must be loaded before Bootstrap + context.Files.AddIfNotContains("/libs/jquery/jquery.min.js"); + + // Then add Bootstrap + context.Files.AddIfNotContains("/libs/bootstrap/bootstrap.bundle.min.js"); + + // You can also add Bootstrap plugins + context.Files.AddIfNotContains("/libs/bootstrap/plugins/bootstrap-datepicker.min.js"); + } +} +``` + +**Understanding dependencies:** +- **jQuery**: Required by Bootstrap, DataTables, Select2, and many others +- **Popper.js**: Required by Bootstrap 4 for positioning +- **Moment.js**: Required by many date pickers +- **Chart.js**: Standalone, but plugins may have dependencies + +**Dependency order:** +1. Dependencies first (jQuery, Popper, etc.) +2. Main libraries (Bootstrap, DataTables, etc.) +3. Plugins and extensions (datepickers, editors, etc.) +4. Your application code (uses all the above) + +### Version Management + +Each package contributor can specify which version of the library to use: + +```csharp +/// +/// Adds a specific version of jQuery +/// +public class JQueryScriptContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + // Specify the exact version to use + // This ensures consistent behavior across environments + context.Files.AddIfNotContains("/libs/jquery/jquery-3.6.0.min.js"); + + // For development, you might want the non-minified version + context.Files.AddIfNotContains("/libs/jquery/jquery-3.6.0.js"); + } +} +``` + +**Version management best practices:** +- **Pin to specific versions** - Avoid unexpected changes +- **Update carefully** - Test thoroughly before upgrading +- **Use Semantic Versioning** - Understand version differences (3.6.0 vs 3.5.1) +- **Security patches** - Update quickly for security fixes +- **Version ranges** - Some package managers support ranges (e.g., ^3.6.0) + +### Common Packages + +#### Bootstrap with Datepicker + +A common combination for forms with date inputs: + +```csharp +Configure(options => +{ + // Bootstrap core + options.ScriptBundles.Add(new BundleConfig("Bootstrap") + .AddContributor()); + + options.StyleBundles.Add(new BundleConfig("Bootstrap") + .AddContributor()); + + // Bootstrap Datepicker (requires Bootstrap) + options.ScriptBundles.Add(new BundleConfig("BootstrapDatepicker") + .AddContributor()); + + options.StyleBundles.Add(new BundleConfig("BootstrapDatepicker") + .AddContributor()); +}); +``` + +**Use in your view:** + +```html +
+ + +
+ + +``` + +#### Data Tables with Bootstrap 5 + +Advanced data tables with Bootstrap 5 styling: + +```csharp +Configure(options => +{ + // DataTables core + options.ScriptBundles.Add(new BundleConfig("DataTables") + .AddContributor() + .AddContributor()); + + options.StyleBundles.Add(new BundleConfig("DataTables") + .AddContributor()); +}); +``` + +**Use in your view:** + +```html + + + + + + + + + + @foreach (var user in Model.Users) + { + + + + + + } + +
NameEmailRole
@user.Name@user.Email@user.Role
+ + +``` + +### CDN Usage + +For production, you can use CDNs for faster delivery: + +```csharp +/// +/// Adds jQuery from CDN or local file +/// +public class CdnContributor : BundleContributor +{ + private readonly IConfiguration _configuration; + + public CdnContributor(IConfiguration configuration) + { + _configuration = configuration; + } + + public override void ConfigureBundle(BundleConfigurationContext context) + { + var useCdn = _configuration.GetValue("UseCdn"); + + var jqueryPath = useCdn + ? "https://code.jquery.com/jquery-3.6.0.min.js" + : "/libs/jquery/jquery-3.6.0.min.js"; + + // Add the file and mark if it's external + context.Files.AddIfNotContains( + jqueryPath, + isExternal: useCdn + ); + } +} +``` + +**Configure CDN usage:** + +```json +{ + "UseCdn": true, + "CdnBaseUrl": "https://cdn.example.com/libs/" +} +``` + +**CDN benefits and risks:** + +**Benefits:** +- **Faster delivery** - Edge servers are closer to users +- **Better caching** - CDN caches for all users +- **Reduced bandwidth** - Your server doesn't serve static files +- **Higher availability** - CDN has better uptime than most servers + +**Risks:** +- **Availability** - CDN might be down +- **Privacy** - Data is served from third-party servers +- **Compliance** - Some regulations require local hosting +- **Versioning** - You need to pin versions to avoid breaking changes + +**CDN fallback strategy:** +- Always have local files as backup +- Use `onerror` handlers for fallback +- Monitor CDN availability +- Have a plan to switch to local files if needed + +--- + +## Rebus Event Bus + +### Overview + +Rebus is a .NET library for building simple and reliable service bus implementations. The `Volo.Abp.EventBus.Rebus` package integrates Rebus with ABP's distributed event bus system, allowing you to use Rebus as your message broker. + +**What is a Service Bus?** + +A service bus provides a messaging infrastructure for distributed systems. Instead of services calling each other directly, they publish and subscribe to events. This creates loose coupling and enables: + +1. **Decoupling** - Services don't need to know about each other +2. **Scalability** - Multiple consumers can handle events independently +3. **Reliability** - Messages are persisted until processed +4. **Flexibility** - New consumers can be added without changing publishers + +**Rebus Features:** +- **Multiple transports** - RabbitMQ, Azure Service Bus, SQL Server, etc. +- **Reliable messaging** - Guaranteed delivery, retries, error handling +- **Routing** - Topic-based or queue-based routing +- **Serialization** - JSON, BSON, custom serializers +- **Monitoring** - Built-in metrics and logging + +### Installation + +Add the Rebus event bus package and your chosen transport: + +```xml + + + + +``` + +**Available transports:** +- **Rebus.RabbitMQ** - RabbitMQ transport (recommended) +- **Rebus.AzureServiceBus** - Azure Service Bus +- **Rebus.SqlServer** - SQL Server as queue +- **Rebus.FileSystem** - File-based queues (for testing) +- **And more...** + +### Configuration + +Configure Rebus in your module's `ConfigureServices` method: + +```csharp +[DependsOn(typeof(AbpEventBusRebusModule))] +public class MyModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + // Choose the message queue type + options.MessageQueueType = MessageQueueType.RabbitMQ; + + // Set the queue name for this application + options.QueueName = "MyApplication"; + + // Connection string for RabbitMQ + options.ConnectionString = "amqp://guest:guest@localhost:5672/"; + }); + } +} +``` + +**Understanding the configuration:** +- **MessageQueueType**: Which transport to use (RabbitMQ, Azure, etc.) +- **QueueName**: Unique identifier for this application's queue +- **ConnectionString**: How to connect to the message broker + +### Usage Examples + +#### Publishing Events + +Publish events when something important happens: + +```csharp +public class OrderService +{ + private readonly IDistributedEventBus _eventBus; + + public OrderService(IDistributedEventBus eventBus) + { + _eventBus = eventBus; + } + + public async Task CreateOrderAsync(Order order) + { + // Save the order to the database + await _orderRepository.InsertAsync(order); + + // Publish an event to notify other services + // This event will be delivered to all subscribers + await _eventBus.PublishAsync( + new OrderCreatedEto + { + OrderId = order.Id, + TotalAmount = order.TotalAmount, + CustomerId = order.CustomerId, + CreatedAt = DateTime.UtcNow + }); + + return order; + } + + public async Task UpdateOrderStatusAsync(Guid orderId, OrderStatus newStatus) + { + var order = await _orderRepository.GetAsync(orderId); + order.Status = newStatus; + await _orderRepository.UpdateAsync(order); + + // Publish status update + await _eventBus.PublishAsync( + new OrderStatusUpdatedEto + { + OrderId = orderId, + OldStatus = order.PreviousStatus, + NewStatus = newStatus, + UpdatedAt = DateTime.UtcNow + }); + } +} +``` + +**Understanding event publishing:** +- **Fire and forget** - Publishing returns immediately, processing is asynchronous +- **Best effort** - Events are delivered when possible, but can fail +- **Multiple subscribers** - All subscribers receive each event +- **Order not guaranteed** - Subscribers may receive events in any order + +#### Subscribing to Events + +Create event handlers to process events: + +```csharp +/// +/// Handles order creation events +/// +public class OrderCreatedEventHandler : + IDistributedEventHandler, + ITransientDependency +{ + private readonly IEmailSender _emailSender; + private readonly ILogger _logger; + + public OrderCreatedEventHandler( + IEmailSender emailSender, + ILogger logger) + { + _emailSender = emailSender; + _logger = logger; + } + + public async Task HandleEventAsync(OrderCreatedEto eventData) + { + _logger.LogInformation( + "Order created: {OrderId} for {CustomerId}", + eventData.OrderId, + eventData.CustomerId); + + // Send confirmation email + await _emailSender.SendAsync( + "customer@example.com", + "Order Confirmation", + $"Your order {eventData.OrderId} has been created."); + + // Update inventory + await UpdateInventoryAsync(eventData.OrderId); + + // Create shipping label + await CreateShippingLabelAsync(eventData.OrderId); + } + + private async Task UpdateInventoryAsync(Guid orderId) + { + // Inventory update logic... + await Task.CompletedTask; + } + + private async Task CreateShippingLabelAsync(Guid orderId) + { + // Shipping label creation logic... + await Task.CompletedTask; + } +} +``` + +**Understanding event handlers:** +- **Automatic registration** - ABP finds handlers automatically +- **Async processing** - Handlers run asynchronously +- **Error handling** - Failed handlers can be retried +- **Multiple handlers** - Multiple handlers can process the same event + +### Advanced Configuration + +#### Custom Configuration + +For advanced scenarios, you can configure Rebus directly: + +```csharp +Configure(options => +{ + options.ConnectionString = "amqp://guest:guest@localhost:5672/"; + options.QueueName = "MyApplication"; + + // Custom Rebus configuration + options.Configurer = (configure, serviceProvider) => + { + // Configure logging + configure.Logging(l => l.ColoredConsole()); + + // Configure transport (RabbitMQ) + configure.Transport(t => t.UseRabbitMq( + serviceProvider.GetRequiredService() + .GetValue("RabbitMQ:ConnectionString"))); + + // Configure options + configure.Options(o => + { + o.SetNumberOfWorkers(10); // Number of concurrent workers + o.SetMaxParallelism(20); // Maximum parallel processing + o.SetPrefetchCount(50); // Messages to prefetch + }); + + // Configure error handling + configure.ErrorHandling(errorHandler => + { + errorHandler.MoveFailedMessagesTo("MyApplication.Error"); + }); + }; +}); +``` + +**Understanding Rebus configuration:** +- **Logging**: How events are logged (console, file, Serilog, etc.) +- **Transport**: How messages are transported (RabbitMQ, Azure, SQL, etc.) +- **Options**: Performance and behavior settings +- **Error handling**: What to do with failed messages + +#### Serialization + +Choose a serializer for your messages: + +```csharp +Configure(options => +{ + options.Serializer = new Utf8JsonRabbitMqSerializer(); + + // Or use a custom serializer + // options.Serializer = new MyCustomSerializer(); +}); +``` + +**Serialization considerations:** +- **JSON**: Readable, compatible with most systems +- **BSON**: More compact, faster serialization +- **Custom**: Optimize for your specific use case +- **Versioning**: Handle message schema evolution + +### Message Types + +#### Simple Event + +Basic event with primitive properties: + +```csharp +public class ProductCreatedEto +{ + public Guid ProductId { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + public DateTime CreatedAt { get; set; } +} +``` + +#### Event with Headers + +Event with additional metadata: + +```csharp +public class OrderUpdatedEto +{ + public Guid OrderId { get; set; } + public string Status { get; set; } + public DateTime UpdatedAt { get; set; } + + // Optional: Add custom headers + [Header("CorrelationId")] + public string CorrelationId { get; set; } + + [Header("Source")] + public string Source { get; set; } +} +``` + +#### Complex Event + +Event with nested objects and collections: + +```csharp +public class OrderCreatedEto +{ + public Guid OrderId { get; set; } + public CustomerEto Customer { get; set; } + public List LineItems { get; set; } + public decimal TotalAmount { get; set; } + public DateTime CreatedAt { get; set; } +} + +public class CustomerEto +{ + public Guid CustomerId { get; set; } + public string Name { get; set; } + public string Email { get; set; } +} + +public class OrderLineItemEto +{ + public Guid ProductId { get; set; } + public string ProductName { get; set; } + public int Quantity { get; set; } + public decimal UnitPrice { get; set; } +} +``` + +### Routing + +#### Topic-based Routing + +Route events to specific topics for more control: + +```csharp +Configure(options => +{ + options.TopicExchangeName = "MyApplicationEvents"; + + options.TopicRouting = (eventType) => + { + // Route to specific topics based on event type + if (eventType == typeof(OrderCreatedEto)) + { + return "orders.created"; + } + + if (eventType == typeof(OrderUpdatedEto)) + { + return "orders.updated"; + } + + if (eventType == typeof(PaymentReceivedEto)) + { + return "payments.received"; + } + + // Default to full type name + return eventType.FullName; + }; +}); +``` + +**Understanding topic routing:** +- **Topics**: Logical channels for events +- **Subscriptions**: Handlers subscribe to specific topics +- **Filtering**: Only receive events from subscribed topics +- **Pattern matching**: Can use wildcards (orders.*) + +**Topic naming conventions:** +- **Entity.event**: `orders.created`, `users.updated` +- **Aggregate.event**: `customer.order.placed` +- **Verb.noun**: `create.product`, `delete.user` + +### Error Handling + +#### Dead Letter Queue + +Configure what happens to failed messages: + +```csharp +Configure(options => +{ + options.ConnectionString = "amqp://guest:guest@localhost:5672/"; + options.DeadLetterQueueName = "MyApplication.DLQ"; + options.ErrorQueueName = "MyApplication.Error"; +}); +``` + +**Understanding dead letter queues:** +- **DLQ (Dead Letter Queue)**: Messages that couldn't be delivered +- **Error Queue**: Messages that caused errors during processing +- **Monitoring**: Check these queues for failures +- **Reprocessing**: You can replay messages from these queues + +#### Retry Policy + +Configure automatic retries for failed messages: + +```csharp +Configure(options => +{ + options.MaxRetries = 5; // Maximum retry attempts + options.RetryDelay = TimeSpan.FromSeconds(5); // Delay between retries +}); +``` + +**Retry behavior:** +1. **First attempt** - Try to process the message +2. **Delay** - Wait 5 seconds +3. **Retry** - Try again (up to 5 times) +4. **Give up** - Move to error queue + +**Retry strategies:** +- **Exponential backoff** - Increase delay between retries +- **Immediate** - Retry immediately (for transient errors) +- **Fixed delay** - Same delay between all retries + +### Testing + +```csharp +public class OrderEventHandlerTests : AbpIntegratedTest +{ + private readonly OrderService _orderService; + private readonly ITestEventHandler _testEventHandler; + + public OrderEventHandlerTests() + { + _orderService = GetRequiredService(); + _testEventHandler = GetRequiredService(); + } + + [Fact] + public async Task Should_Publish_Order_Created_Event() + { + // Arrange + OrderCreatedEto? receivedEvent = null; + _testEventHandler.Subscribe(evt => receivedEvent = evt); + + var order = new Order + { + CustomerId = Guid.NewGuid(), + TotalAmount = 100 + }; + + // Act + var created = await _orderService.CreateOrderAsync(order); + + // Wait for the event to be processed + await Task.Delay(1000); + + // Assert + receivedEvent.ShouldNotBeNull(); + receivedEvent.OrderId.ShouldBe(created.Id); + receivedEvent.TotalAmount.ShouldBe(100); + } + + [Fact] + public async Task Should_Handle_Multiple_Events() + { + // Arrange + var events = new List(); + _testEventHandler.Subscribe(evt => events.Add(evt)); + + // Act + await _orderService.CreateOrderAsync(new Order { TotalAmount = 100 }); + await _orderService.CreateOrderAsync(new Order { TotalAmount = 200 }); + await _orderService.CreateOrderAsync(new Order { TotalAmount = 300 }); + + // Wait for events + await Task.Delay(1000); + + // Assert + events.Count.ShouldBeGreaterThanOrEqualTo(3); + } +} +``` + +### Best Practices + +1. **Keep events immutable** - Events should be read-only DTOs +2. **Use descriptive names** - Make event purposes clear (OrderCreated vs OrderUpdate) +3. **Version your events** - Support event schema evolution +4. **Handle exceptions gracefully** - Don't let handler failures break the bus +5. **Monitor message queues** - Track queue sizes and processing times +6. **Use idempotent handlers** - Handlers should handle duplicate events safely +7. **Log everything** - Comprehensive logging for debugging +8. **Test thoroughly** - Test retry logic, error handling, and concurrency +9. **Consider ordering** - Event order is not guaranteed, design for it +10. **Set appropriate timeouts** - Don't let handlers run forever + +--- + +## Blazorise UI Integration + +### Overview + +The `Volo.Abp.BlazoriseUI` package provides integration between ABP Framework and Blazorise, a component library for Blazor applications. Blazorise provides a rich set of UI components that work with multiple CSS frameworks (Bootstrap, Material, Bulma, etc.). + +**What is Blazorise?** + +Blazorise is a component library that: +- Provides consistent UI components across frameworks +- Works with Bootstrap, Material, and other CSS frameworks +- Supports both Blazor Server and Blazor WebAssembly +- Includes data grids, forms, modals, and more + +**ABP + Blazorise Benefits:** +- **Rich components** - Advanced data grids, modals, forms +- **ABP integration** - Works with ABP's services and patterns +- **Localization** - Built-in localization support +- **Validation** - Automatic validation integration +- **Theme support** - Works with ABP's theming system + +### Installation + +```xml + + + + +``` + +**Supported frameworks:** +- **Blazorise.Bootstrap5** - Bootstrap 5 components +- **Blazorise.Material** - Material Design components +- **Blazorise.Bootstrap4** - Bootstrap 4 components +- **Blazorise.Bulma** - Bulma CSS framework + +### Features + +- **Modal Extensions** - Enhanced modal components with ABP integration +- **Message Service** - Alert and notification services +- **Page Progress Service** - Loading indicators for async operations +- **Data Grid** - Enhanced data grid components with CRUD operations +- **Entity Actions** - Reusable action components for data grids +- **Form Components** - Enhanced form inputs and validation + +### Configuration + +```csharp +[DependsOn(typeof(AbpBlazoriseUIModule))] +public class MyBlazorModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + // Custom configuration for Blazorise components + // For example, custom themes, icons, etc. + }); + } +} +``` + +### Usage Examples + +#### Modal Extensions + +ABP provides enhanced modal components: + +```razor +@inherits AbpModalBase + + + + + Product Details + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+
+ +@code { + private bool IsOpen { get; set; } + + [Parameter] + public ProductDto Product { get; set; } + + public void Open() + { + IsOpen = true; + } + + private void OnClose() + { + IsOpen = false; + } + + private async Task OnSave() + { + // Save logic here + await ProductService.UpdateAsync(Product); + + IsOpen = false; + } +} +``` + +**Understanding the modal:** +- **IsOpen**: Controls whether the modal is visible +- **OnClose**: Called when the modal is closed +- **Size**: Controls the modal size (Small, Medium, Large, ExtraLarge) +- **ModalHeader**: Header content (title, close button) +- **ModalBody**: Main content area +- **ModalFooter**: Footer content (buttons) + +#### Message Service + +Use the message service to show alerts: + +```csharp +@inject BlazoriseUiMessageService MessageService + + + + Name + + + + + + Email + + + + + + + + + + + +@code { + private UserDto user = new UserDto(); + + private void Reset() + { + user = new UserDto(); + } + + private async Task HandleValidSubmit() + { + try + { + await UserService.CreateAsync(user); + await MessageService.SuccessAsync( + "User created successfully!", + $"User {user.Name} has been added to the system."); + } + catch (Exception ex) + { + await MessageService.ErrorAsync( + "Failed to create user", + ex.Message); + } + } + + private async Task ShowSuccessMessage() + { + await MessageService.SuccessAsync( + "Operation completed successfully!"); + } + + private async Task ShowErrorMessage() + { + await MessageService.ErrorAsync( + "An error occurred!", + "Please try again later or contact support."); + } +} +``` + +**Message service methods:** +- **SuccessAsync**: Shows success alert (green) +- **ErrorAsync**: Shows error alert (red) +- **InfoAsync**: Shows information alert (blue) +- **WarningAsync**: Shows warning alert (yellow) + +#### Notification Service + +Show notifications that persist across navigation: + +```csharp +@inject BlazoriseUiNotificationService NotificationService + + + + +@code { + private async Task ShowNotification() + { + await NotificationService.InfoAsync( + "New message received", + "You have 3 unread messages"); + } + + private async Task ShowMultipleNotifications() + { + await NotificationService.SuccessAsync("Order placed", "Order #12345 has been placed"); + await NotificationService.InfoAsync("Order shipped", "Your order has been shipped"); + await NotificationService.WarningAsync("Payment pending", "Please complete your payment"); + } +} +``` + +**Notification vs Message:** +- **Messages**: Temporary alerts, shown once +- **Notifications**: Persistent, shown in notification center, can have multiple + +#### Page Progress Service + +Show loading indicators for async operations: + +```razor +@inject BlazoriseUiPageProgressService PageProgressService + + + + + +@code { + private int ProgressValue { get; set; } + + private async Task StartLoading() + { + // Start the progress indicator + await PageProgressService.StartAsync(); + + // Simulate a long-running operation + for (int i = 0; i <= 100; i++) + { + ProgressValue = i; + await Task.Delay(50); + } + + // Complete the progress + await PageProgressService.DoneAsync(); + } + + private async Task CompleteLoading() + { + await PageProgressService.DoneAsync(); + ProgressValue = 100; + } +} +``` + +**Progress service methods:** +- **StartAsync**: Show progress indicator, disable UI +- **DoneAsync**: Hide progress indicator, re-enable UI +- **SetAsync**: Set specific progress value (0-100) + +#### Enhanced Data Grid + +Use the enhanced data grid for displaying lists: + +```razor + + + + + + + @($"${context.Price:F2}") + + + + + + + + + + + + + + + + +@code { + private List products = new(); + private int totalCount = 0; + private int pageIndex = 1; + private int pageCount = 1; + private int pageSize = 10; + + private ProductDto editingProduct; + + protected override async Task OnInitializedAsync() + { + await LoadDataAsync(); + } + + private async Task LoadDataAsync() + { + var result = await ProductService.GetListAsync( + new GetProductsInput + { + MaxResultCount = pageSize, + SkipCount = (pageIndex - 1) * pageSize, + Sorting = "Name" + }); + + products = result.Items; + totalCount = result.TotalCount; + } + + private async Task OnPageChangedAsync(int newPageIndex) + { + pageIndex = newPageIndex; + await LoadDataAsync(); + } + + private void OnEditAsync(ProductDto product) + { + editingProduct = product; + OpenEditModal(); + } + + private async Task OnDeleteAsync(ProductDto product) + { + if (await ConfirmDeleteAsync(product)) + { + await ProductService.DeleteAsync(product.Id); + await MessageService.SuccessAsync("Product deleted"); + await LoadDataAsync(); + } + } + + private async Task OnApproveAsync(ProductDto product) + { + product.Status = ProductStatus.Active; + await ProductService.UpdateAsync(product); + await MessageService.SuccessAsync("Product approved"); + await LoadDataAsync(); + } +} +``` + +**Data grid features:** +- **Pagination**: Navigate through large datasets +- **Sorting**: Sort by any column +- **Filtering**: Filter data by column values +- **Selection**: Select one or multiple rows +- **Entity Actions**: Custom actions for each row + +#### CRUD Page Base + +Inherit from `AbpCrudPageBase` for instant CRUD functionality: + +```razor +@page "/products" +@inherits AbpCrudPageBase + + + +

Product Management

+
+ + + + + + + @($"${context.Price:F2}") + + + + + + + + + + + +
+ +@code { + // CRUD functionality is inherited: + // - Create, Update, Delete methods + // - Pagination, sorting, filtering + // - Modals for create/edit + // - Confirmation dialogs +} +``` + +**CRUD page base provides:** +- **Automatic data loading** - Gets data on page load +- **Pagination** - Handles page changes +- **Modals** - Create and edit modals +- **Validation** - Automatic form validation +- **Error handling** - Shows errors to users + +### Customizing Components + +#### Custom Entity Actions + +Add custom actions to data grid rows: + +```razor + + + + + + + + +@code { + private Task OnApproveAsync(ProductDto product) + { + product.Status = ProductStatus.Active; + return ProductService.UpdateAsync(product); + } + + private Task OnRejectAsync(ProductDto product) + { + product.Status = ProductStatus.Rejected; + return ProductService.UpdateAsync(product); + } + + private Task OnDuplicateAsync(ProductDto product) + { + var duplicate = new CreateProductDto + { + Name = $"{product.Name} (Copy)", + Price = product.Price, + Category = product.Category + }; + return ProductService.CreateAsync(duplicate); + } +} +``` + +#### Custom Columns + +Customize how data is displayed in columns: + +```razor + + + @context.CreatedAt.ToString("yyyy-MM-dd HH:mm") + + + + + + + @GetStatusLabel(context.Status) + + + + + + + @($"${context.Price:N2}") + + + +@code { + private Color GetStatusColor(ProductStatus status) + { + return status switch + { + ProductStatus.Active => Color.Success, + ProductStatus.Inactive => Color.Secondary, + ProductStatus.Pending => Color.Warning, + ProductStatus.Rejected => Color.Danger, + _ => Color.Default + }; + } + + private string GetStatusLabel(ProductStatus status) + { + return status switch + { + ProductStatus.Active => "Active", + ProductStatus.Inactive => "Inactive", + ProductStatus.Pending => "Pending", + ProductStatus.Rejected => "Rejected", + _ => "Unknown" + }; + } +} +``` + +### Best Practices + +1. **Use base classes** - Inherit from `AbpCrudPageBase` for instant CRUD functionality +2. **Leverage services** - Use message and notification services for user feedback +3. **Consistent styling** - Follow ABP's design system +4. **Handle async properly** - Always use async/await for data operations +5. **Validate inputs** - Use ABP's validation attributes and Blazorise validation +6. **Use DisplayTemplate** - Format data appropriately (currency, dates, etc.) +7. **Responsive design** - Test on different screen sizes +8. **Accessibility** - Add ARIA labels and keyboard navigation +9. **Error handling** - Show friendly error messages to users +10. **Loading states** - Use progress indicators for async operations + +--- + +## Minification + +### Overview + +The `Volo.Abp.Minify` package provides minification services for CSS, JavaScript, and HTML content. Minification removes unnecessary characters (whitespace, line breaks, comments) from your code, reducing file sizes and improving page load performance. + +**What is Minification?** + +Minification is the process of removing unnecessary characters from source code without changing its functionality: + +**JavaScript before minification:** +```javascript +// Calculate the total price +function calculateTotal(price, tax) { + var total = price + tax; + return total; +} + +// Call the function +var result = calculateTotal(100, 15); +console.log(result); +``` + +**JavaScript after minification:** +```javascript +function calculateTotal(e,t){return e+t}var result=calculateTotal(100,15);console.log(result); +``` + +**Benefits of minification:** +- **Smaller files** - 30-70% reduction in file size +- **Faster downloads** - Less data to transfer +- **Better caching** - Smaller cache entries +- **Lower bandwidth** - Reduced server costs + +**What gets removed:** +- Whitespace (spaces, tabs, line breaks) +- Comments +- Optional semicolons +- Optional braces +- Long variable names (renamed to short ones) + +### Installation + +```xml + + + +``` + +### Minifier Interfaces + +```csharp +/// +/// Minifies JavaScript code +/// +public interface IJavascriptMinifier +{ + string Minify(string source); +} + +/// +/// Minifies CSS code +/// +public interface ICssMinifier +{ + string Minify(string source); +} + +/// +/// Minifies HTML code +/// +public interface IHtmlMinifier +{ + string Minify(string source); +} +``` + +### Usage Examples + +#### Minifying JavaScript + +```csharp +public class JsMinificationService +{ + private readonly IJavascriptMinifier _minifier; + + public JsMinificationService(IJavascriptMinifier minifier) + { + _minifier = minifier; + } + + public string MinifyJs(string jsCode) + { + try + { + // Minify the JavaScript code + var minified = _minifier.Minify(jsCode); + + // Log the savings + var savings = ((jsCode.Length - minified.Length) * 100.0) / jsCode.Length; + Console.WriteLine($"JavaScript minified: {savings:F1}% reduction"); + + return minified; + } + catch (Exception ex) + { + // Log and return original if minification fails + Console.WriteLine($"Minification failed: {ex.Message}"); + return jsCode; + } + } +} +``` + +**JavaScript minification details:** +- **Variable renaming**: Long names become short (calculateTotal → a) +- **Whitespace removal**: Spaces, tabs, line breaks removed +- **Comment removal**: All comments are removed +- **Dead code elimination**: Unreachable code is removed +- **Safe**: Doesn't break functionality (usually) + +#### Minifying CSS + +```csharp +public class CssMinificationService +{ + private readonly ICssMinifier _minifier; + + public CssMinificationService(ICssMinifier minifier) + { + _minifier = minifier; + } + + public string MinifyCss(string cssCode) + { + try + { + // Minify the CSS code + var minified = _minifier.Minify(cssCode); + + // Log the savings + var savings = ((cssCode.Length - minified.Length) * 100.0) / cssCode.Length; + Console.WriteLine($"CSS minified: {savings:F1}% reduction"); + + return minified; + } + catch (Exception ex) + { + // Log and return original if minification fails + Console.WriteLine($"Minification failed: {ex.Message}"); + return cssCode; + } + } +} +``` + +**CSS minification details:** +- **Whitespace removal**: All unnecessary whitespace removed +- **Comment removal**: All comments removed +- **Color optimization**: #ffffff → #fff, rgb(255,0,0) → #f00 +- **Property merging**: Multiple properties combined +- **Selector optimization**: Redundant selectors removed + +#### Minifying HTML + +```csharp +public class HtmlMinificationService +{ + private readonly IHtmlMinifier _minifier; + + public HtmlMinificationService(IHtmlMinifier minifier) + { + _minifier = minifier; + } + + public string MinifyHtml(string htmlContent) + { + try + { + // Minify the HTML content + var minified = _minifier.Minify(htmlContent); + + // Log the savings + var savings = ((htmlContent.Length - minified.Length) * 100.0) / htmlContent.Length; + Console.WriteLine($"HTML minified: {savings:F1}% reduction"); + + return minified; + } + catch (Exception ex) + { + // Log and return original if minification fails + Console.WriteLine($"Minification failed: {ex.Message}"); + return htmlContent; + } + } +} +``` + +**HTML minification details:** +- **Whitespace removal**: Between tags, indentation removed +- **Comment removal**: HTML comments removed +- **Optional tags**: Closing tags for void elements removed +- **Attribute optimization**: Unnecessary quotes removed +- **Safe**: Doesn't break HTML structure + +### Custom Minification + +Implement your own minifier for special needs: + +```csharp +/// +/// Custom JavaScript minifier with specific optimizations +/// +public class CustomJavascriptMinifier : IJavascriptMinifier, ITransientDependency +{ + public string Minify(string source) + { + // Simple minification logic + // For production, use a mature minifier like NUglify + var minified = source + .Replace(" ", " ") // Reduce multiple spaces to single + .Replace("\n", " ") // Replace newlines with spaces + .Replace("\r", " ") // Replace carriage returns with spaces + .Replace(" ", " ") // Remove double spaces (repeat until no changes) + .Replace("{ ", "{") // Remove spaces after opening braces + .Replace(" }", "}") // Remove spaces before closing braces + .Replace("( ", "(") // Remove spaces after opening parentheses + .Replace(" )", ")") // Remove spaces before closing parentheses + .Replace("; ", ";") // Remove spaces after semicolons + .Replace(" ;", ";"); // Remove spaces before semicolons + + return minified; + } +} +``` + +**Registering your custom minifier:** + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.Replace( + ServiceDescriptor.Transient() + ); +} +``` + +**Custom minifier use cases:** +- **Special optimizations** - Domain-specific optimizations +- **Language features** - Handle specific language constructs +- **Compatibility** - Ensure compatibility with your code style +- **Debugging** - Keep certain parts unminified + +### Integration with Bundling + +Minification is automatically integrated with ABP's bundling system: + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + Configure(options => + { + // Enable both bundling and minification + options.Mode = BundlingMode.BundleAndMinify; + + // Configure minifiers + options.MinificationOptions.JavascriptMinifier = + new NUglifyJavascriptMinifier(); + + options.MinificationOptions.CssMinifier = + new NUglifyCssMinifier(); + }); +} +``` + +**How it works together:** +1. **Collection**: Collect files from contributors +2. **Combination**: Combine files into bundles +3. **Minification**: Minify the combined bundles +4. **Caching**: Cache the minified bundles +5. **Serving**: Serve minified bundles to clients + +--- + +## Conclusion + +This guide covers advanced ABP Framework features that can significantly enhance your applications. Each section provides detailed explanations, code examples, and best practices to help you implement these features effectively. + +### Additional Resources + +- [ABP Framework Documentation](https://docs.abp.io) - Official ABP documentation +- [ABP GitHub Repository](https://github.com/abpframework/abp) - Source code and issues +- [Community Forum](https://community.abp.io) - Community support and discussions +- [ABP Samples](https://github.com/abpframework/abp-samples) - Example applications + +### Further Reading + +- **Architecture**: Explore DDD patterns, microservices, and modular architecture +- **UI Frameworks**: Learn about Angular, Blazor, and React Native integrations +- **Infrastructure**: Deep dive into background jobs, caching, and event bus +- **Modules**: Discover pre-built modules (identity, payment, CMS, etc.) + +### Getting Help + +If you need help implementing these features: +- Check the ABP documentation for official guides +- Search the GitHub issues for similar problems +- Ask questions on the community forum +- Consider professional support for complex scenarios diff --git a/docs/en/framework/infrastructure/index.md b/docs/en/framework/infrastructure/index.md index ec6377d757..e91f5347cd 100644 --- a/docs/en/framework/infrastructure/index.md +++ b/docs/en/framework/infrastructure/index.md @@ -7,8 +7,9 @@ # ABP Infrastructure -ABP provides a complete infrastructure for creating real world software solutions with modern architectures based on the .NET platform. Each of the following documents explains an infrastructure feature: +ABP provides a complete infrastructure for creating real world software solutions with modern architectures based on .NET platform. Each of the following documents explains an infrastructure feature: +* [Advanced Features & Integration Guide](./advanced-features.md) * [Audit Logging](./audit-logging.md) * [Artificial Intelligence](./artificial-intelligence/index.md) * [Background Jobs](./background-jobs/index.md)