id: extensibility
Extensibility Patterns in SophiChain
Developer's Guide: How to Extend Modules Without Modifying Source Code
id: extensibility
📖 Overview
SophiChain modules are designed to be extended by consumers without modifying the original source code. This guide covers all extensibility mechanisms provided by ABP Framework and SophiChain-specific patterns.
Target Audience: Module consumers, plugin developers, customization specialists
Core Principle: All SophiChain modules MUST be extensible using these patterns.
id: extensibility
🎯 Why Extensibility Matters
Benefits:
- ✅ No Source Modification - Extend without touching original code
- ✅ Upgrade Safety - Your customizations survive module updates
- ✅ Maintainability - Clear separation between core and customizations
- ✅ Testability - Test customizations independently
- ✅ Composability - Multiple extensions can coexist
Use Cases:
- Add custom properties to entities
- Override business logic
- Add custom types (e.g., "Premium" customer type)
- Extend DTOs with additional fields
- React to domain events with custom logic
- Add custom API endpoints
id: extensibility
🏗️ Extension Mechanisms
1. Extension Properties (IHasExtraProperties)
Purpose: Add dynamic properties to entities without schema changes
When to Use:
- Adding tenant-specific fields
- Custom metadata storage
- Temporary features before schema migration
- Plugin-specific data
Pattern:
// Module ensures all entities implement IHasExtraProperties
public class Product : FullAuditedAggregateRoot<Guid>, IHasExtraProperties
{
public ExtraPropertyDictionary ExtraProperties { get; protected set; }
public Product()
{
ExtraProperties = new ExtraPropertyDictionary();
this.SetDefaultsForExtraProperties();
}
}
// Consumer extends entity
public class MyCustomizationModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
// No code needed - just use SetProperty()
}
}
// Usage in consumer code
public class MyProductService : ApplicationService
{
public async Task CustomizeProductAsync(Guid productId)
{
var product = await _productRepository.GetAsync(productId);
// Add custom properties
product.SetProperty("WarrantyMonths", 24);
product.SetProperty("LoyaltyPoints", 100);
product.SetProperty("CustomTag", "Premium");
await _productRepository.UpdateAsync(product);
}
public int GetWarrantyMonths(Product product)
{
return product.GetProperty<int>("WarrantyMonths");
}
}
Database Storage:
- MongoDB: Stored as nested document
- SQL: Stored as JSON column
Benefits:
- ✅ No schema changes required
- ✅ Type-safe access
- ✅ Automatic serialization
- ✅ Works with DTOs (see DTO Extensions)
id: extensibility
2. Service Override
Purpose: Replace or extend domain services and application services
When to Use:
- Modify business logic
- Add validation rules
- Inject custom behavior before/after operations
- Replace entire service implementation
Pattern A: Extend and Call Base
// Module provides virtual methods
public class ProductManager : DomainService
{
public virtual async Task<Product> CreateAsync(string name, decimal price)
{
await ValidateAsync(name, price);
var product = new Product(GuidGenerator.Create(), name, price);
return await _repository.InsertAsync(product);
}
protected virtual Task ValidateAsync(string name, decimal price)
{
// Default validation
return Task.CompletedTask;
}
}
// Consumer extends service
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(ProductManager))]
public class CustomProductManager : ProductManager
{
private readonly ICashbackService _cashbackService;
public CustomProductManager(ICashbackService cashbackService)
{
_cashbackService = cashbackService;
}
public override async Task<Product> CreateAsync(string name, decimal price)
{
// Before logic
await _warrantyService.ValidateWarrantyEligibilityAsync(price);
// Call base implementation
var product = await base.CreateAsync(name, price);
// After logic
product.SetProperty("WarrantyMonths", await _warrantyService.CalculateMonthsAsync(price));
return product;
}
protected override async Task ValidateAsync(string name, decimal price)
{
// Call base validation
await base.ValidateAsync(name, price);
// Add custom validation
if (price < 0)
throw new BusinessException("NegativePriceNotAllowed");
}
}
Pattern B: Complete Replacement
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IProductAppService))]
public class CustomProductAppService : IProductAppService
{
// Completely replace implementation
// Use when base implementation is not suitable
}
Guidelines:
- ✅ Module developers: Mark methods as
virtual - ✅ Consumers: Call
base.Method()unless intentionally replacing - ✅ Use
[Dependency(ReplaceServices = true)] - ✅ Use
[ExposeServices(typeof(OriginalType))]
id: extensibility
3. Domain Event Subscriptions
Purpose: Extend behavior by reacting to domain events
When to Use:
- Add side effects without modifying core logic
- Integrate with external systems
- Track custom analytics
- Trigger custom workflows
Pattern:
// Module publishes events
public class Product : AggregateRoot<Guid>
{
public void Activate()
{
IsActive = true;
AddLocalEvent(new ProductActivatedEvent { ProductId = Id });
}
}
// Consumer subscribes to events
public class CashbackProductActivatedHandler
: ILocalEventHandler<ProductActivatedEvent>,
ITransientDependency
{
private readonly ICashbackService _cashbackService;
public async Task HandleEventAsync(ProductActivatedEvent eventData)
{
// Add custom logic without modifying Product entity
await _cashbackService.EnableCashbackAsync(eventData.ProductId);
}
}
// Multiple handlers can subscribe to same event
public class AnalyticsProductActivatedHandler
: ILocalEventHandler<ProductActivatedEvent>,
ITransientDependency
{
public async Task HandleEventAsync(ProductActivatedEvent eventData)
{
// Track in analytics system
await _analyticsService.TrackActivationAsync(eventData.ProductId);
}
}
Benefits:
- ✅ No modification to original code
- ✅ Multiple handlers per event
- ✅ Loosely coupled
- ✅ Easy to test
id: extensibility
4. Provider Pattern
Purpose: Add implementations for pluggable interfaces
When to Use:
- Add external service providers
- Add data source providers
- Add notification channels
- Add storage providers
Pattern:
// Module defines interface
public interface IExternalServiceProvider
{
string ProviderCode { get; }
Task<ServiceResult> ProcessAsync(ServiceRequest request);
}
// Module auto-discovers implementations
public class ExternalService
{
private readonly IEnumerable<IExternalServiceProvider> _providers;
public ExternalService(IEnumerable<IExternalServiceProvider> providers)
{
_providers = providers; // All implementations injected
}
}
// Consumer creates implementation
public class CustomProvider : IExternalServiceProvider, ITransientDependency
{
public string ProviderCode => "MyCustomProvider";
public async Task<ServiceResult> ProcessAsync(ServiceRequest request)
{
// Custom implementation
}
}
// Consumer registers via module dependency
[DependsOn(typeof(CoreModule))]
public class MyAppModule : AbpModule
{
// CustomProvider auto-discovered via ITransientDependency
}
See: Pluggable Providers Guide for details
id: extensibility
5. DTO Extensions
Purpose: Add properties to DTOs without modifying contracts
When to Use:
- Add UI-specific fields
- Add calculated fields
- Add tenant-specific fields
- Extend third-party module DTOs
Pattern:
// Consumer configures DTO extensions
public class MyAppModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<IMvcBuilder>(mvcBuilder =>
{
mvcBuilder.AddApplicationPartIfNotExists(typeof(MyAppModule).Assembly);
});
// Add property to DTO
ObjectExtensionManager.Instance
.AddOrUpdateProperty<ProductDto, int>(
"WarrantyMonths",
options =>
{
options.DefaultValue = 12;
options.Attributes.Add(new RangeAttribute(0, 120));
});
ObjectExtensionManager.Instance
.AddOrUpdateProperty<ProductDto, string>(
"CustomCategory");
}
}
// Module DTOs automatically include extraProperties
public class ProductDto : EntityDto<Guid>
{
public string Name { get; set; }
public decimal Price { get; set; }
// extraProperties dictionary automatically added by ABP
}
// API response includes extensions
// {
// "id": "...",
// "name": "Product A",
// "price": 99.99,
// "extraProperties": {
// "WarrantyMonths": 24,
// "CustomCategory": "Premium"
// }
// }
// Usage in consumer code
public class MyProductAppService : ApplicationService
{
public async Task<ProductDto> GetWithExtensionsAsync(Guid id)
{
var product = await _productRepository.GetAsync(id);
var dto = ObjectMapper.Map<Product, ProductDto>(product);
// Extra properties automatically mapped from entity
// Or manually set:
dto.SetProperty("WarrantyMonths", product.GetProperty<int>("WarrantyMonths"));
return dto;
}
}
Benefits:
- ✅ API automatically includes
extraProperties - ✅ Swagger documentation updated
- ✅ Client proxies include extensions
- ✅ No contract modification needed
id: extensibility
6. Repository Customization
Purpose: Add custom query methods to repositories
When to Use:
- Add domain-specific queries
- Optimize frequently used queries
- Add complex filtering logic
Pattern:
// Module provides base repository
public interface IProductRepository : IRepository<Product, Guid>
{
Task<List<Product>> GetActiveAsync();
}
// Consumer creates custom repository interface
public interface ICustomProductRepository : IProductRepository
{
Task<List<Product>> GetProductsWithWarrantyAsync();
Task<List<Product>> GetPremiumProductsAsync();
Task<int> GetAverageWarrantyMonthsAsync();
}
// Consumer implements custom repository
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IProductRepository), typeof(ICustomProductRepository))]
public class CustomProductRepository
: MongoProductRepository,
ICustomProductRepository
{
public CustomProductRepository(
IMongoDbContextProvider<MyDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public virtual async Task<List<Product>> GetProductsWithWarrantyAsync()
{
return await (await GetMongoQueryableAsync())
.Where(p => p.GetProperty<int>("WarrantyMonths") > 0)
.ToListAsync();
}
public virtual async Task<List<Product>> GetPremiumProductsAsync()
{
return await (await GetMongoQueryableAsync())
.Where(p => p.GetProperty<string>("CustomTag") == "Premium")
.ToListAsync();
}
public virtual async Task<int> GetAverageWarrantyMonthsAsync()
{
var products = await GetListAsync();
return (int)products
.Select(p => p.GetProperty<int>("WarrantyMonths"))
.Average();
}
}
// Usage
public class MyProductService : ApplicationService
{
private readonly ICustomProductRepository _repository;
public MyProductService(ICustomProductRepository repository)
{
_repository = repository; // Custom repository injected
}
public async Task<List<ProductDto>> GetWarrantyProductsAsync()
{
var products = await _repository.GetProductsWithWarrantyAsync();
return ObjectMapper.Map<List<Product>, List<ProductDto>>(products);
}
}
id: extensibility
7. Type Provider Pattern
Purpose: Extend enumeration-like types with custom values
When to Use:
- Add custom resource types
- Add custom status types
- Add custom notification channels
- Extend any string-based type system
Pattern:
// Module defines type constants and provider
public static class ResourceType
{
public const string Standard = "Standard";
public const string Premium = "Premium";
public const string Enterprise = "Enterprise";
}
public interface IResourceTypeProvider
{
List<ResourceTypeDefinition> GetTypes();
}
public class DefaultResourceTypeProvider : IResourceTypeProvider
{
public List<ResourceTypeDefinition> GetTypes()
{
return new List<ResourceTypeDefinition>
{
new() { Code = ResourceType.Standard, Name = "Standard Resource" },
new() { Code = ResourceType.Premium, Name = "Premium Resource" },
new() { Code = ResourceType.Enterprise, Name = "Enterprise Resource" }
};
}
}
// Consumer creates custom provider
public class CustomResourceTypeProvider : IResourceTypeProvider, ITransientDependency
{
public List<ResourceTypeDefinition> GetTypes()
{
return new List<ResourceTypeDefinition>
{
new()
{
Code = "Trial",
Name = "Trial Resource",
Description = "Limited trial access"
},
new()
{
Code = "Educational",
Name = "Educational Resource"
}
};
}
}
// Module aggregates all providers
public class ResourceTypeRegistry : ISingletonDependency
{
private readonly IEnumerable<IResourceTypeProvider> _providers;
public ResourceTypeRegistry(IEnumerable<IResourceTypeProvider> providers)
{
_providers = providers;
}
public List<ResourceTypeDefinition> GetAllTypes()
{
return _providers
.SelectMany(p => p.GetTypes())
.DistinctBy(t => t.Code)
.ToList();
}
public bool IsValidType(string code)
{
return GetAllTypes().Any(t => t.Code == code);
}
}
Benefits:
- ✅ Extensible type system
- ✅ No enum modifications needed
- ✅ Multiple providers can coexist
- ✅ Type-safe string constants
id: extensibility
8. Controller Override
Purpose: Extend or replace API endpoints
When to Use:
- Add custom endpoints
- Modify request/response
- Add custom authorization
- Change routing
Pattern:
// Module provides controller
[Area("app")]
[RemoteService(Name = "Product")]
[Route("api/app/products")]
public class ProductController : AbpController, IProductAppService
{
private readonly IProductAppService _productAppService;
[HttpGet]
[Route("{id}")]
public virtual async Task<ProductDto> GetAsync(Guid id)
{
return await _productAppService.GetAsync(id);
}
}
// Consumer extends controller
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(ProductController))]
public class CustomProductController : ProductController
{
private readonly ICashbackService _cashbackService;
public CustomProductController(ICashbackService cashbackService)
{
_cashbackService = cashbackService;
}
// Override existing endpoint
public override async Task<ProductDto> GetAsync(Guid id)
{
var dto = await base.GetAsync(id);
// Add custom data
dto.SetProperty("CashbackRate",
await _cashbackService.GetCashbackRateAsync(id));
return dto;
}
// Add new endpoint
[HttpGet]
[Route("{id}/cashback-details")]
public async Task<CashbackDetailsDto> GetCashbackDetailsAsync(Guid id)
{
return await _cashbackService.GetDetailsAsync(id);
}
}
id: extensibility
📋 Extension Checklist for Module Developers
When creating a module, ensure:
Entities
- All entities implement
IHasExtraProperties - Entity constructors call
this.SetDefaultsForExtraProperties() - Properties use
private set(encapsulation)
Domain Services
- All public methods are
virtual - Complex methods have
protected virtualtemplate methods - Methods follow Template Method pattern
Application Services
- All methods are
virtual - Services use interface (
IMyAppService) - DTOs support extra properties
Events
- All state changes publish domain events
- Events contain minimal data (IDs + key fields)
- Event names use past tense
Providers
- Provider interfaces defined in Domain layer
- Registry services for auto-discovery
- Provider interfaces have health check methods
Repositories
- Repository methods are
virtual - Interfaces allow custom implementations
- Query methods support extension
id: extensibility
📋 Extension Checklist for Consumers
When extending a module:
Planning
- Identify extension points
- Review module documentation
- Check if extension mechanism exists
Implementation
- Use appropriate extension mechanism
- Call
basemethods when overriding (unless replacing) - Make your extensions
virtualtoo (chain extensibility) - Write tests for your extensions
Testing
- Test extension independently
- Test with module updates
- Test interaction with other extensions
id: extensibility
🎯 Real-World Examples
Example 1: Loyalty Program Extension
// Extend Product entity with rating properties
product.SetProperty("AverageRating", 4.5m);
product.SetProperty("ReviewCount", 127);
// Override product creation to initialize ratings
public class RatingProductManager : ProductManager
{
public override async Task<Product> CreateAsync(string name, decimal price)
{
var product = await base.CreateAsync(name, price);
// Initialize rating properties
product.SetProperty("AverageRating", 0m);
product.SetProperty("ReviewCount", 0);
return product;
}
}
// React to review events
public class ReviewSubmittedHandler
: ILocalEventHandler<ReviewSubmittedEvent>
{
public async Task HandleEventAsync(ReviewSubmittedEvent e)
{
await _ratingService.UpdateAverageRatingAsync(e.ProductId, e.Rating);
}
}
Example 2: Custom External Service Provider
// Implement external service provider
public class CustomNotificationProvider : INotificationProvider, ITransientDependency
{
public string ProviderCode => "CustomNotification";
public async Task<NotificationResult> SendAsync(NotificationRequest request)
{
// Custom notification sending logic
}
}
// Auto-discovered by the module, no registration needed
Example 3: Dynamic Pricing
// Extend DTO with dynamic pricing
ObjectExtensionManager.Instance
.AddOrUpdateProperty<ProductDto, decimal>("BasePrice")
.AddOrUpdateProperty<ProductDto, decimal>("SeasonalDiscount")
.AddOrUpdateProperty<ProductDto, decimal>("VolumeDiscount");
// Override pricing logic
public class DynamicPricingService : PricingService
{
public override async Task<decimal> CalculatePriceAsync(
Guid productId,
int quantity)
{
var product = await _repository.GetAsync(productId);
var basePrice = product.GetProperty<decimal>("BasePrice");
var seasonalDiscount = product.GetProperty<decimal>("SeasonalDiscount");
var volumeDiscount = quantity > 10 ? 0.1m : 0m;
var price = basePrice * (1 - seasonalDiscount) * (1 - volumeDiscount);
return price * quantity;
}
}
id: extensibility
✅ Best Practices
Do's
- ✅ Use extension properties for custom data
- ✅ Override services to extend behavior
- ✅ Subscribe to events for side effects
- ✅ Create providers for pluggable features
- ✅ Call
basemethods when overriding - ✅ Make your extensions extensible too (virtual methods)
- ✅ Document your extensions
- ✅ Test extensions independently
Don'ts
- ❌ Don't modify module source code
- ❌ Don't fork the module
- ❌ Don't break encapsulation
- ❌ Don't create tight coupling
- ❌ Don't skip base method calls (unless intentional)
- ❌ Don't forget to handle upgrades
id: extensibility
📚 References
id: extensibility
id: extensibility