---
description: Implement soft-delete pattern with grace period and restoration for entities (project)
---
# Add Soft-Delete Pattern Skill
Implement soft-delete semantics with grace period and restoration capabilities for NovaTune entities.
## Overview
Soft-delete provides:
- **Data recovery**: Users can restore deleted items within grace period
- **Audit trail**: Deletion timestamps preserved for compliance
- **Deferred cleanup**: Physical deletion happens asynchronously
- **Quota preservation**: Storage quota released only after physical deletion
## Steps
### 1. Add Soft-Delete Fields to Entity
Location: Extend existing entity model (e.g., `Track.cs`)
```csharp
public sealed class Track
{
// ... existing fields ...
// Soft-delete fields
///
/// Timestamp when the entity was soft-deleted.
/// Null if not deleted.
///
public DateTimeOffset? DeletedAt { get; set; }
///
/// Timestamp when physical deletion will occur.
/// Null if not deleted.
///
public DateTimeOffset? ScheduledDeletionAt { get; set; }
///
/// Status before deletion, used for restoration.
/// Null if not deleted.
///
public TrackStatus? StatusBeforeDeletion { get; set; }
///
/// Indicates if the entity is soft-deleted.
///
[JsonIgnore]
public bool IsDeleted => Status == TrackStatus.Deleted;
///
/// Indicates if the entity can be restored.
///
[JsonIgnore]
public bool CanRestore =>
IsDeleted &&
ScheduledDeletionAt.HasValue &&
ScheduledDeletionAt.Value > DateTimeOffset.UtcNow;
}
```
### 2. Add Status Enum Value
```csharp
public enum TrackStatus
{
Unknown = 0,
Processing = 1,
Ready = 2,
Failed = 3,
Deleted = 4 // Add if not present
}
```
### 3. Create Configuration Options
Location: `src/NovaTuneApp/NovaTuneApp.ApiService/Configuration/SoftDeleteOptions.cs`
```csharp
namespace NovaTuneApp.ApiService.Configuration;
public class SoftDeleteOptions
{
public const string SectionName = "SoftDelete";
///
/// Grace period before physical deletion.
/// Default: 30 days.
///
public TimeSpan GracePeriod { get; set; } = TimeSpan.FromDays(30);
///
/// Whether soft-delete is enabled (vs immediate delete).
/// Default: true.
///
public bool Enabled { get; set; } = true;
}
```
### 4. Create Custom Exceptions
Location: `src/NovaTuneApp/NovaTuneApp.ApiService/Infrastructure/Exceptions/`
```csharp
namespace NovaTuneApp.ApiService.Infrastructure.Exceptions;
///
/// Thrown when attempting to operate on a deleted entity.
///
public class EntityDeletedException : Exception
{
public string EntityId { get; }
public string EntityType { get; }
public DateTimeOffset DeletedAt { get; }
public EntityDeletedException(string entityType, string entityId, DateTimeOffset deletedAt)
: base($"{entityType} '{entityId}' has been deleted.")
{
EntityType = entityType;
EntityId = entityId;
DeletedAt = deletedAt;
}
}
///
/// Thrown when entity is already deleted.
///
public class AlreadyDeletedException : Exception
{
public string EntityId { get; }
public AlreadyDeletedException(string entityId)
: base($"Entity '{entityId}' is already deleted.")
{
EntityId = entityId;
}
}
///
/// Thrown when restoration grace period has expired.
///
public class RestorationExpiredException : Exception
{
public string EntityId { get; }
public DateTimeOffset DeletedAt { get; }
public DateTimeOffset ScheduledDeletionAt { get; }
public RestorationExpiredException(
string entityId,
DateTimeOffset deletedAt,
DateTimeOffset scheduledDeletionAt)
: base($"Entity '{entityId}' cannot be restored. Grace period expired at {scheduledDeletionAt}.")
{
EntityId = entityId;
DeletedAt = deletedAt;
ScheduledDeletionAt = scheduledDeletionAt;
}
}
///
/// Thrown when trying to restore non-deleted entity.
///
public class NotDeletedException : Exception
{
public string EntityId { get; }
public NotDeletedException(string entityId)
: base($"Entity '{entityId}' is not deleted and cannot be restored.")
{
EntityId = entityId;
}
}
```
### 5. Implement Soft-Delete in Service
```csharp
public class TrackManagementService : ITrackManagementService
{
private readonly IAsyncDocumentSession _session;
private readonly IOutboxService _outboxService;
private readonly IOptions _softDeleteOptions;
private readonly IStreamingService _streamingService;
private readonly ILogger _logger;
///
/// Soft-deletes a track.
///
public async Task DeleteTrackAsync(
string trackId,
string userId,
CancellationToken ct = default)
{
var track = await _session.LoadAsync