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 β Subscribers
Detailed 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/SoftDeleteProduct
Input:
SoftDeleteProductDto
from 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
SoftDeleteProductCommand
with 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
SoftDeleteProductCommand
from 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
ProductSoftDeletedEvent
from 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
SoftDeleteProductDto
public partial class SoftDeleteProductDto : DtoBridge
{
public string Id { get; set; }
public string Reason { get; set; }
public string Comments { get; set; }
}
Command: SoftDeleteProductCommand
SoftDeleteProductCommand
public 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