Skip to content

Undelete Request Handler

The UndeleteRequestHandlerAsync is the base class that handles Undelete service requests. It restores a record that was previously soft deleted.

From Serenity 10.5.0, Sergen generates asynchronous handlers (UndeleteRequestHandlerAsync) and endpoint actions by default. The synchronous UndeleteRequestHandler 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.

Sergen only generates an Undelete handler for rows that support soft delete, i.e. rows implementing IIsActiveDeletedRow or IIsDeletedRow. For a row without an IsActive/IsDeleted field (which is hard-deleted), there is nothing to undelete, so no handler is generated.

Generated Handler

For a row that supports soft delete, Sergen generates:

using MyRow = MyProject.Administration.UserRow;

namespace MyProject.Administration;

public interface IUserUndeleteHandler : IUndeleteHandlerAsync<MyRow> { }

public class UserUndeleteHandler(IRequestContext context)
    : UndeleteRequestHandlerAsync<MyRow>(context), IUserUndeleteHandler
{
}

The class derives from the generic UndeleteRequestHandlerAsync<TRow> and IUndeleteRequestHandler.

UndeleteRequestHandlerAsync<TRow> itself derives from the fully generic UndeleteRequestHandlerAsync<TRow, TUndeleteRequest, TUndeleteResponse> — the extra arguments let you customize the request and response types if you ever need an undelete handler with a custom request/response.

The Service Endpoint

The endpoint exposes a single Undelete action:

[Route("Services/Administration/User/[action]")]
[ConnectionKey(typeof(MyRow)), ServiceAuthorize(typeof(MyRow))]
public class UserEndpoint : ServiceEndpoint
{
    [HttpPost, AuthorizeUndelete(typeof(MyRow))]
    public Task<UndeleteResponse> Undelete(IUnitOfWork uow, UndeleteRequest request,
        [FromServices] IUserUndeleteHandler handler, CancellationToken cancellationToken = default)
    {
        return handler.UndeleteAsync(uow, request, cancellationToken);
    }
}

[AuthorizeUndelete(typeof(MyRow))] validates the undelete permission declared on the row.

Undelete Permission

Undeletion can be authorized separately from deletion. The handler (and [AuthorizeUndelete]) reads the permission from the row type, using the first one it finds:

  1. UndeletePermissionAttribute
  2. DeletePermissionAttribute
  3. ModifyPermissionAttribute
  4. ReadPermissionAttribute
[DeletePermission("Administration:Security")]
[UndeletePermission("Administration:Security:Undelete")]
public sealed class UserRow : Row<UserRow.RowFields>, IIdRow, IIsActiveDeletedRow
{
    // ...
}

When a row does not declare an explicit undelete permission, the fallback chain above applies, so most applications are unaffected. A user who is denied receives an EntityNotFound error, so a caller cannot distinguish a missing record from a forbidden one. The row metadata sent to the client also carries an undeletePermission, which the EntityGrid and EntityDialog use for their include-deleted toggle and Undelete button — see EntityGrid CRUD.

Undelete Permission

Undeletion can be authorized separately from deletion. The handler determines the required permission via GetUndeletePermission(), which uses the first of the following that the row type declares:

  1. UndeletePermissionAttribute
  2. DeletePermissionAttribute
  3. ModifyPermissionAttribute
  4. ReadPermissionAttribute
[DeletePermission("Administration:Security")]
[UndeletePermission("Administration:Security:Undelete")]
public sealed class UserRow : Row<UserRow.RowFields>, IIdRow, IIsActiveDeletedRow
{
    // ...
}

If the row declares no permission attribute at all, the undelete operation is denied (the fail-closed default — see Permission Checks Fail Closed).

A user who is denied access receives an EntityNotFound validation error (rather than AccessDenied), so a caller cannot distinguish a missing record from a forbidden one. The row metadata sent to the client carries the undeletePermission, which EntityGrid and EntityDialog use for their include-deleted toggle and Undelete button — see EntityGrid CRUD.

The Request and Response

The request is an UndeleteRequest:

public class UndeleteRequest : ServiceRequest
{
    public object? EntityId { get; set; }
}

It only contains the ID of the record to restore. The response is an UndeleteResponse:

public class UndeleteResponse : ServiceResponse
{
    public bool WasNotDeleted { get; set; }
}

WasNotDeleted is true if the record was not actually soft-deleted (e.g. it was already restored, or its IsActive was not -1).

What Undelete Does

Depending on the row type:

  • For IIsActiveDeletedRow rows, the handler issues UPDATE ... SET IsActive = 1 where IsActive = -1, restoring the record to active.
  • For IIsDeletedRow rows, it sets IsDeleted = false.

If the row type doesn't implement one of these interfaces, the handler raises an error, because there is no way to undelete a hard-deleted record.

Lifecycle Methods

The main overridable methods on UndeleteRequestHandlerAsync are:

  • OnBeforeUndeleteAsync() — called before the undelete is executed.
  • OnAfterUndeleteAsync() — called after the undelete 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.

OnBeforeUndeleteAsync/OnAfterUndeleteAsync also invoke any registered IUndeleteBehavior.OnBeforeUndeleteAsync / OnAfterUndeleteAsync.

See Also