Skip to content

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 synchronous DeleteRequestHandler is 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/IsDeleted field), the record is physically removed with a DELETE statement.
  • Soft delete — if the row implements IIsActiveDeletedRow (i.e. it has an IsActive field where -1 means deleted), the handler issues an UPDATE ... SET IsActive = -1 instead 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 IsDeleted field), the handler sets IsDeleted = 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.

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 MasterDetailRelation behavior 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.

See Also