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 body

  • Action: 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 DTO

  • Command 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 of SetModified() or SetDeleted()

  • 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 bus

  • Logging: 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 bus

  • Side Effects: Executes business logic (inventory updates, notifications, etc.)

  • Additional Events: Can fire more events if needed

Data Transfer Objects (DTOs)

Input DTO: SoftDeleteProductDto

public partial class SoftDeleteProductDto : DtoBridge 
{
    public string Id { get; set; }
    public string Reason { get; set; }
    public string Comments { get; set; }
}

Command: SoftDeleteProductCommand

public class SoftDeleteProductCommand : FlexCommandBridge<SoftDeleteProductDto, FlexAppContextBridge>
{
    // Command data is automatically populated by FlexBase
}

Key Differences from Regular Delete/Update

Soft Delete-Specific Characteristics

  1. Reversible Operation: Can be undone unlike hard delete

  2. State Management: Uses SetSoftDelete() instead of SetDeleted() or SetModified()

  3. Audit Trail: Tracks who soft deleted and when

  4. Business Logic: Includes reason and comments for soft delete

  5. Data Preservation: Data remains in database but marked as soft deleted

  6. 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

Aspect
Soft Delete
Hard Delete
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)

  1. PUT Request β†’ Controller receives request

  2. Service Processing β†’ Business orchestration and PreBus validation

  3. PreBus Plugins β†’ Sequential validation of business rules

  4. Command Handler β†’ Entity lookup and data processing

  5. Domain Logic β†’ Business rules and soft delete state management

  6. Response β†’ HTTP 200 OK with soft delete status

Asynchronous Flow (Event Processing)

  1. Event Publishing β†’ ProductSoftDeletedEvent published to message bus

  2. Subscriber Processing β†’ NotifyInventoryOnProductSoftDeleted executes

  3. 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