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
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! 🚀
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;
}
}
public class SoftDeleteProductSequence : FlexiBusinessRuleSequenceBase<SoftDeleteProductDataPacket>
{
public SoftDeleteProductSequence()
{
this.Add<IsValidForSoftDelete>();
this.Add<CheckProductDependencies>();
}
}
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
}
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;
}
}
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);
}
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;
}
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));
}
}
public class ProductSoftDeletedEvent : FlexEventBridge<FlexAppContextBridge>
{
// Event data is automatically populated by FlexBase
}
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);
}
}
public partial class SoftDeleteProductDto : DtoBridge
{
public string Id { get; set; }
public string Reason { get; set; }
public string Comments { get; set; }
}
public class SoftDeleteProductCommand : FlexCommandBridge<SoftDeleteProductDto, FlexAppContextBridge>
{
// Command data is automatically populated by FlexBase
}