Delete Request Handler
The DeleteRequestHandlerAsync is the base class that handles Delete service requests. Like the other handlers, Sergen generates a small subclass for every entity; the base class contains all the default logic.
From Serenity 10.5.0, Sergen generates asynchronous handlers (
DeleteRequestHandlerAsync) and endpoint actions by default. The synchronousDeleteRequestHandleris kept for backward compatibility and is marked obsolete — it will be deprecated in a future version. New code — and code generated by Sergen — should use the async variants.
Generated Handler
For a Language entity, Sergen generates:
using MyRow = MyProject.Administration.LanguageRow;
namespace MyProject.Administration;
public interface ILanguageDeleteHandler : IDeleteHandlerAsync<MyRow> { }
public class LanguageDeleteHandler(IRequestContext context)
: DeleteRequestHandlerAsync<MyRow>(context), ILanguageDeleteHandler
{
}
The class derives from the generic DeleteRequestHandlerAsync<TRow> and IDeleteRequestHandler.
DeleteRequestHandlerAsync<TRow>itself derives from the fully generic DeleteRequestHandlerAsync<TRow, TDeleteRequest, TDeleteResponse> — the extra arguments let you customize the request and response types if you ever need a delete handler with a custom request/response.
The Service Endpoint
The endpoint exposes a single Delete action:
[Route("Services/Administration/Language/[action]")]
[ConnectionKey(typeof(MyRow)), ServiceAuthorize(typeof(MyRow))]
public class LanguageEndpoint : ServiceEndpoint
{
[HttpPost, AuthorizeDelete(typeof(MyRow))]
public Task<DeleteResponse> Delete(IUnitOfWork uow, DeleteRequest request,
[FromServices] ILanguageDeleteHandler handler, CancellationToken cancellationToken = default)
{
return handler.DeleteAsync(uow, request, cancellationToken);
}
}
[AuthorizeDelete(typeof(MyRow))] validates the delete permission declared on the row. The endpoint returns Task<DeleteResponse> and forwards the CancellationToken to the handler.
The Request and Response
The request is a DeleteRequest:
public class DeleteRequest : ServiceRequest
{
public object EntityId { get; set; }
}
It only contains the ID of the record to delete. The response is a DeleteResponse:
public class DeleteResponse : ServiceResponse
{
public bool WasAlreadyDeleted { get; set; }
}
WasAlreadyDeleted is true if the record was already deleted (soft deleted) before this call.
Hard Delete vs Soft Delete
The delete behavior depends on the row type:
- Hard delete — if the row is a plain table row (no
IsActive/IsDeletedfield), the record is physically removed with aDELETEstatement. - Soft delete — if the row implements IIsActiveDeletedRow (i.e. it has an
IsActivefield where-1means deleted), the handler issues anUPDATE ... SET IsActive = -1instead of deleting. The record stays in the table and can be restored later with the Undelete Request Handler. - IsDeleted soft delete — if the row implements IIsDeletedRow (an
IsDeletedfield), the handler setsIsDeleted = true.
IsActive is a common pattern in Serene/StartSharp templates: rows implementing IIsActiveRow use 1 for active and 0 for inactive, and -1 for deleted (the IIsActiveDeletedRow interface marks that -1 is the deleted value).
Lifecycle Methods
The main overridable methods on DeleteRequestHandlerAsync are:
OnBeforeDeleteAsync()— called before the delete/soft-delete is executed.OnAfterDeleteAsync()— called after the delete/soft-delete succeeds.OnReturnAsync()— called just before the response is returned.
Each accepts a CancellationToken cancellationToken = default and returns Task. Call the base implementation first when overriding.
OnBeforeDeleteAsync/OnAfterDeleteAsync also invoke any registered DeleteBehavior.OnBeforeDeleteAsync / OnAfterDeleteAsync.
Example: Deleting Related Records
The Movie tutorial manually deletes a movie's cast entries before deleting the movie, to avoid foreign-key errors:
public class MovieDeleteHandler(IRequestContext context,
IServiceResolver<IMovieCastDeleteHandler> movieCastDelete)
: DeleteRequestHandlerAsync<MyRow>(context), IMovieDeleteHandler
{
private readonly IServiceResolver<IMovieCastDeleteHandler> movieCastDelete = movieCastDelete
?? throw new ArgumentNullException(nameof(movieCastDelete));
protected override async Task OnBeforeDeleteAsync(CancellationToken cancellationToken = default)
{
await base.OnBeforeDeleteAsync(cancellationToken);
var mc = MovieCastRow.Fields;
var castIds = await Connection.QueryAsync<Int32>(
new SqlQuery().From(mc)
.Select(mc.MovieCastId)
.Where(mc.MovieId == Row.MovieId.Value), cancellationToken: cancellationToken);
foreach (var detailID in castIds)
{
await movieCastDelete.Resolve().DeleteAsync(UnitOfWork,
new() { EntityId = detailID }, cancellationToken);
}
}
}
Note that for master–detail relations you normally don't need to do this by hand — the
MasterDetailRelationbehavior handles related records automatically.
Behaviors
Behaviors implementing IDeleteBehaviorAsync (or the sync IDeleteBehaviorSync) run for every delete handler and are the recommended way to add cross-cutting delete logic.