Soft Delete Example
Overview
This document demonstrates the complete Soft Delete flow using a SoftDeleteProduct feature. The flow is similar to an Update operation (PUT) but uses SetSoftDelete() instead of SetModified() or SetDeleted(). This pattern allows for reversible deletions while maintaining data integrity.
Complete Flow Architecture
PUT Request → Controller → Service → PreBus Plugins → Command Handler → Domain Model → Database → Event Publishing → SubscribersDetailed Flow Breakdown
1. PUT Request
↓
2. Controller (API Entry Point)
↓
3. Service Layer (Business Orchestration)
↓
4. PreBus Processing (Validation Pipeline)
├── SoftDeleteProductSequence (Plugin Registration)
├── SoftDeleteProductDataPacket (Validation Context)
└── IsValidForSoftDelete Plugin (Business Rules)
↓
5. Command Handler (Data Processing)
↓
6. Domain Model (Business Logic)
↓
7. Database (Data Persistence)
↓
8. Event Publishing (Asynchronous Processing)
↓
9. Subscribers (Side Effects)Step-by-Step Implementation
1. API Controller - The Entry Point
File: ProductsController_SoftDeleteProduct.cs
[HttpPut]
[Route("SoftDeleteProduct")]
[ProducesResponseType(typeof(BadRequestResult), 400)]
[ProducesResponseType(typeof(string), 200)]
public async Task<IActionResult> SoftDeleteProduct([FromBody]SoftDeleteProductDto dto)
{
return await RunService(200, dto, _processProductsService.SoftDeleteProduct);
}What Happens:
HTTP Method:
PUT /api/Products/SoftDeleteProductInput:
SoftDeleteProductDtofrom request bodyAction: Calls the service layer to process the soft delete
Response: HTTP 200 OK with success status
2. Service Layer - Business Orchestration
File: ProcessProductsService_SoftDeleteProduct.cs
public async Task<CommandResult> SoftDeleteProduct(SoftDeleteProductDto dto)
{
var packet = await ProcessBusinessRuleSequence<SoftDeleteProductDataPacket, SoftDeleteProductSequence, SoftDeleteProductDto, FlexAppContextBridge>(dto);
if (packet.HasError)
{
return new CommandResult(Status.Failed, packet.Errors());
}
else
{
dto.SetGeneratedId(_pkGenerator.GenerateKey());
SoftDeleteProductCommand cmd = new SoftDeleteProductCommand
{
Dto = dto,
};
await ProcessCommand(cmd);
CommandResult cmdResult = new CommandResult(Status.Success);
SoftDeleteProductResultModel outputResult = new SoftDeleteProductResultModel();
outputResult.Id = dto.Id;
outputResult.IsSoftDeleted = true;
cmdResult.result = outputResult;
return cmdResult;
}
}What Happens:
PreBus Processing: Executes business rule sequences (plugins)
Validation: Processes business rule sequences
ID Generation: Generates unique key for tracking
Command Creation: Creates
SoftDeleteProductCommandwith DTOCommand Processing: Calls the command handler
Result: Returns success with soft delete status
2.1. PreBus Business Rule Sequence - Validation Pipeline
File: SoftDeleteProductSequence.cs
public class SoftDeleteProductSequence : FlexiBusinessRuleSequenceBase<SoftDeleteProductDataPacket>
{
public SoftDeleteProductSequence()
{
this.Add<IsValidForSoftDelete>();
this.Add<CheckProductDependencies>();
}
}What Happens:
Plugin Registration: Registers validation plugins in execution order
Sequential Processing: Executes plugins one by one
Error Collection: Collects validation errors from all plugins
Early Exit: Stops processing if any plugin fails
2.2. PreBus Data Packet - Validation Context
File: SoftDeleteProductDataPacket.cs
public partial class SoftDeleteProductDataPacket : FlexiFlowDataPacketWithDtoBridge<SoftDeleteProductDto, FlexAppContextBridge>
{
protected readonly ILogger<SoftDeleteProductDataPacket> _logger;
public SoftDeleteProductDataPacket(ILogger<SoftDeleteProductDataPacket> logger)
{
_logger = logger;
}
#region "Properties
//Models and other properties goes here
public Product Product { get; set; }
public bool HasActiveOrders { get; set; }
public bool HasActiveInventory { get; set; }
#endregion
}What Happens:
Context Container: Holds DTO and application context
Error Collection: Collects validation errors from plugins
Data Sharing: Allows plugins to share data during validation
Logging: Provides logging capabilities for plugins
2.3. PreBus Validation Plugin - Business Rules
File: IsValidForSoftDelete.cs
public partial class IsValidForSoftDelete : FlexiBusinessRuleBase, IFlexiBusinessRule<SoftDeleteProductDataPacket>
{
public override string Id { get; set; } = "3a1cd5b1fa98b4c63378de9607706083";
public override string FriendlyName { get; set; } = "IsValidForSoftDelete";
protected readonly ILogger<IsValidForSoftDelete> _logger;
protected readonly RepoFactory _repoFactory;
public IsValidForSoftDelete(ILogger<IsValidForSoftDelete> logger, RepoFactory repoFactory)
{
_logger = logger;
_repoFactory = repoFactory;
}
public virtual async Task Validate(SoftDeleteProductDataPacket packet)
{
_repoFactory.Init(packet.Dto);
// Check if product exists
var product = _repoFactory.GetRepo().FindAll<Product>()
.Where(p => p.Id == packet.Dto.Id && !p.IsSoftDeleted)
.FirstOrDefault();
if (product == null)
{
packet.AddError("ProductNotFound", "Product not found or already soft deleted");
return;
}
packet.Product = product;
// Check if product has active orders
var hasActiveOrders = _repoFactory.GetRepo().FindAll<OrderItem>()
.Any(oi => oi.ProductId == packet.Dto.Id && !oi.Order.IsSoftDeleted);
if (hasActiveOrders)
{
packet.AddError("HasActiveOrders", "Cannot soft delete product with active orders");
}
packet.HasActiveOrders = hasActiveOrders;
await Task.CompletedTask;
}
}What Happens:
Business Rule Validation: Implements specific validation logic
Database Access: Validates product exists and can be soft deleted
Dependency Checking: Ensures no active orders reference the product
Error Reporting: Adds errors to the data packet if validation fails
Data Sharing: Populates packet with validation results
3. Command Handler - Data Processing
File: SoftDeleteProductHandler.cs
public virtual async Task Execute(SoftDeleteProductCommand cmd, IFlexServiceBusContext serviceBusContext)
{
_flexAppContext = cmd.Dto.GetAppContext();
_repoFactory.Init(cmd.Dto);
_model = _repoFactory.GetRepo().FindAll<Product>().Where(m=>m.Id == cmd.Dto.Id).FirstOrDefault();
if (_model != null)
{
_model.SoftDeleteProduct(cmd);
_repoFactory.GetRepo().InsertOrUpdate(_model);
int records = await _repoFactory.GetRepo().SaveAsync();
if (records > 0)
{
_logger.LogDebug("{} with {} soft deleted in Database: ", typeof(Product).Name, _model.Id);
}
else
{
_logger.LogWarning("No records soft deleted for {} with {}", typeof(Product).Name, _model.Id);
}
EventCondition = CONDITION_ONSUCCESS;
}
else
{
_logger.LogWarning("Product with ID {} not found for soft delete", cmd.Dto.Id);
EventCondition = CONDITION_ONFAILED;
}
await this.Fire(EventCondition, serviceBusContext);
}What Happens:
Context Setup: Initializes application context and repository
Entity Lookup: Finds existing product by ID
Null Check: Ensures product exists before soft deleting
Domain Logic: Calls domain model to process business rules
Database Save: Updates the product in database (soft delete)
Logging: Logs success/failure of database operation
Event Publishing: Fires events for subscribers
4. Domain Model - Business Logic
File: Product/SoftDeleteProduct.cs
public virtual Product SoftDeleteProduct(SoftDeleteProductCommand cmd)
{
Guard.AgainstNull("Product model cannot be empty", cmd);
this.Convert(cmd.Dto);
this.LastModifiedBy = cmd.Dto.GetAppContext()?.UserId;
this.SoftDeletedBy = cmd.Dto.GetAppContext()?.UserId;
this.SoftDeletedAt = DateTime.UtcNow;
//Map any other field not handled by Automapper config
this.SetSoftDelete();
//Set your appropriate SetSoftDelete for the inner object here
this.OrderItems?.SetSoftDelete();
return this;
}What Happens:
Validation: Guards against null commands
Data Mapping: Converts DTO to domain model
Audit Fields: Sets last modified by and soft deleted by user
Timestamp: Records when the soft delete occurred
State Management: Uses
SetSoftDelete()instead ofSetModified()orSetDeleted()Child Objects: Processes child object soft deletions
5. NServiceBus Handler - Message Processing
File: SoftDeleteProductNsbHandler.cs
public class SoftDeleteProductNsbHandler : NsbCommandHandler<SoftDeleteProductCommand>
{
readonly ILogger<SoftDeleteProductNsbHandler> _logger;
readonly IFlexHost _flexHost;
readonly ISoftDeleteProductHandler _handler;
public SoftDeleteProductNsbHandler(ILogger<SoftDeleteProductNsbHandler> logger, IFlexHost flexHost, ISoftDeleteProductHandler handler)
{
_logger = logger;
_flexHost = flexHost;
_handler = handler;
}
public override async Task Handle(SoftDeleteProductCommand message, IMessageHandlerContext context)
{
_logger.LogTrace($"Executing {nameof(SoftDeleteProductNsbHandler)}");
await _handler.Execute(message, new NsbHandlerContextBridge(context));
}
}What Happens:
Message Reception: Receives
SoftDeleteProductCommandfrom message busLogging: Logs handler execution
Delegation: Calls the actual command handler
Context Bridge: Converts NServiceBus context to FlexBase context
6. Event Publishing - Asynchronous Processing
Event: ProductSoftDeletedEvent
public class ProductSoftDeletedEvent : FlexEventBridge<FlexAppContextBridge>
{
// Event data is automatically populated by FlexBase
}What Happens:
Event Creation: FlexBase creates event with product data
Message Bus: Event is published to message bus
Subscriber Notification: All subscribers are notified
7. Event Subscribers - Side Effects
File: NotifyInventoryOnProductSoftDeleted.cs
public partial class NotifyInventoryOnProductSoftDeleted : INotifyInventoryOnProductSoftDeleted
{
protected readonly ILogger<NotifyInventoryOnProductSoftDeleted> _logger;
protected string EventCondition = "";
public NotifyInventoryOnProductSoftDeleted(ILogger<NotifyInventoryOnProductSoftDeleted> logger)
{
_logger = logger;
}
public virtual async Task Execute(ProductSoftDeletedEvent @event, IFlexServiceBusContext serviceBusContext)
{
_flexAppContext = @event.AppContext;
//TODO: Write your business logic here:
// - Update inventory status
// - Notify suppliers about product unavailability
// - Update analytics and reporting
// - Archive related data
// - Send notifications to stakeholders
await this.Fire<NotifyInventoryOnProductSoftDeleted>(EventCondition, serviceBusContext);
}
}What Happens:
Event Reception: Receives
ProductSoftDeletedEventfrom message busSide Effects: Executes business logic (inventory updates, notifications, etc.)
Additional Events: Can fire more events if needed
Data Transfer Objects (DTOs)
Input DTO: SoftDeleteProductDto
SoftDeleteProductDtopublic partial class SoftDeleteProductDto : DtoBridge
{
public string Id { get; set; }
public string Reason { get; set; }
public string Comments { get; set; }
}Command: SoftDeleteProductCommand
SoftDeleteProductCommandpublic class SoftDeleteProductCommand : FlexCommandBridge<SoftDeleteProductDto, FlexAppContextBridge>
{
// Command data is automatically populated by FlexBase
}Key Differences from Regular Delete/Update
Soft Delete-Specific Characteristics
Reversible Operation: Can be undone unlike hard delete
State Management: Uses
SetSoftDelete()instead ofSetDeleted()orSetModified()Audit Trail: Tracks who soft deleted and when
Business Logic: Includes reason and comments for soft delete
Data Preservation: Data remains in database but marked as soft deleted
Query Filtering: Soft-deleted entities are filtered out of normal queries
PreBus Validation Focus
IsValidForSoftDelete: Validates product exists and can be soft deleted
CheckProductDependencies: Ensures no active references to the product
Business Rules: Validates soft delete is allowed based on business constraints
Data Integrity: Ensures soft delete doesn't violate business rules
Soft Delete vs Hard Delete vs Update
Method
SetSoftDelete()
SetDeleted()
SetModified()
Reversible
✅ Yes
❌ No
✅ Yes
Data Preserved
✅ Yes
❌ No
✅ Yes
Audit Trail
✅ Full
✅ Limited
✅ Full
Query Filtering
✅ Filtered
❌ Removed
✅ Normal
Use Case
Temporary removal
Permanent removal
Data modification
Flow Summary
Synchronous Flow (Immediate Response)
PUT Request → Controller receives request
Service Processing → Business orchestration and PreBus validation
PreBus Plugins → Sequential validation of business rules
Command Handler → Entity lookup and data processing
Domain Logic → Business rules and soft delete state management
Response → HTTP 200 OK with soft delete status
Asynchronous Flow (Event Processing)
Event Publishing → ProductSoftDeletedEvent published to message bus
Subscriber Processing → NotifyInventoryOnProductSoftDeleted executes
Side Effects → Inventory updates, supplier notifications, analytics
Key Benefits
Data Safety: Preserves data for potential recovery
Business Continuity: Maintains referential integrity
Audit Compliance: Complete audit trail of soft deletions
Reversible Operations: Can be undone if needed
Business Rules: Validates soft delete is appropriate
Event-Driven: Notifies other systems of soft deletions
Testable: Each component can be tested independently
Maintainable: Clear separation of concerns
This SoftDeleteProduct example demonstrates how FlexBase enables clean, maintainable, and scalable soft delete operations with proper validation, reversible state management, and event-driven architecture! 🚀
Last updated