Skip to content

Save Request Handler

The SaveRequestHandler is the base class that handles Create (insert) and Update service requests. Sergen generates a small subclass for every entity; the base class contains all the default logic, so an empty subclass already produces a working save service.

Generated Handler

For a Language entity, Sergen generates:

using MyRow = MyProject.Administration.LanguageRow;

namespace MyProject.Administration;

public interface ILanguageSaveHandler : ISaveHandler<MyRow> { }

public class LanguageSaveHandler(IRequestContext context)
    : SaveRequestHandler<MyRow>(context), ILanguageSaveHandler
{
}

The interface (ISaveHandler<MyRow>) and the class derive from the generic SaveRequestHandler<TRow> and ISaveRequestHandler. Request handlers are auto-registered via the type source, so no explicit registration is needed.

In StartSharp, the handler interface does not have to be written by hand — it can be generated from the handler class with the [GenerateInterface] attribute. This works for all request handler types. See Generating Handler Interfaces. For how handlers are discovered and registered automatically, see Auto-Registration of Request Handlers.

The Service Endpoint

The endpoint exposes two actions that delegate to the handler:

[Route("Services/Administration/Language/[action]")]
[ConnectionKey(typeof(MyRow)), ServiceAuthorize(typeof(MyRow))]
public class LanguageEndpoint : ServiceEndpoint
{
    [HttpPost, AuthorizeCreate(typeof(MyRow))]
    public SaveResponse Create(IUnitOfWork uow, SaveRequest<MyRow> request,
        [FromServices] ILanguageSaveHandler handler)
    {
        return handler.Create(uow, request);
    }

    [HttpPost, AuthorizeUpdate(typeof(MyRow))]
    public SaveResponse Update(IUnitOfWork uow, SaveRequest<MyRow> request,
        [FromServices] ILanguageSaveHandler handler)
    {
        return handler.Update(uow, request);
    }
}
  • [AuthorizeCreate(typeof(MyRow))] / [AuthorizeUpdate(typeof(MyRow))] validate the create/update permission declared on the row.
  • The IUnitOfWork argument provides a transaction, so the save either commits or rolls back as a whole.
  • [FromServices] injects the (auto-registered) handler.

The Request and Response

The request is a SaveRequest<TEntity>:

public class SaveRequest<TEntity> : ServiceRequest, ISaveRequest
{
    public object EntityId { get; set; }
    public TEntity Entity { get; set; }
    public Dictionary<string, TEntity> Localizations { get; set; }
}
  • EntityId — set for Update (which record to update); ignored for Create.
  • Entity — the fields to insert/update. Because rows track assignments, only the fields actually sent from the client are included in the SQL statement.
  • Localizations — optional per-language entity values when localization is used.

The response is a SaveResponse:

public class SaveResponse : ServiceResponse
{
    public object EntityId { get; set; }
}

EntityId contains the ID of the created or updated record.

Create vs Update

Inside the handler, IsCreate is true for Create requests and IsUpdate for Update requests:

if (IsCreate)
    // new record
else
    // existing record

Row holds the entity being saved. For updates, Old holds the previously stored entity.

Handler Type Hierarchy

The generated handler derives from SaveRequestHandler<TRow>, which itself derives from the fully generic SaveRequestHandler<TRow, TSaveRequest, TSaveResponse>:

public class SaveRequestHandler<TRow>(IRequestContext context)
    : SaveRequestHandler<TRow, SaveRequest<TRow>, SaveResponse>(context), ISaveHandler<TRow>
    where TRow : class, IRow, IIdRow, new()
{
}

The three generic arguments let you customize the request and response types. If you need a save handler that works with a custom request/response (e.g. one with extra fields), derive from the three-argument version directly:

public class MySaveHandler(IRequestContext context)
    : SaveRequestHandler<MyRow, MySaveRequest, MySaveResponse>(context), IMySaveHandler
{
}

Create and Update interfaces

The handler interface generated by Sergen (ILanguageSaveHandler) derives from ISaveHandler<MyRow>, which combines the create and update contracts. The underlying generic interfaces are:

ISaveRequestProcessor and SaveRequestType

ISaveRequestProcessor is the abstraction used when a handler needs to process a save without knowing up front whether it's a create or an update. Its Process method takes a SaveRequestType:

public enum SaveRequestType
{
    Create,
    Update,
    Auto   // determine from request.EntityId
}

SaveRequestHandler<TRow> implements ISaveRequestProcessor, so Process(uow, request, SaveRequestType.Auto) is how the create/update decision is made internally.

Create/Update proxies

CreateHandlerProxy and UpdateHandlerProxy are internal classes registered by AddProxyRequestHandlers(). They let the DI container resolve ICreateHandler<TRow> / IUpdateHandler<TRow> on demand: each proxy resolves the concrete handler through IDefaultHandlerFactory and forwards the call. You normally don't use them directly — they're what makes the generic handler interfaces resolvable (see Auto-Registration of Request Handlers).

Lifecycle Methods

The main overridable methods are:

  • OnValidateRequest() — validate the request (permissions, required fields, etc.).
  • OnSetInternalFields() — set fields like InsertUserId, InsertDate, UpdateUserId, UpdateDate.
  • BeforeSave() — called right before the INSERT/UPDATE is executed.
  • AfterSave() — called right after the INSERT/UPDATE succeeds.
  • OnReturn() — called just before the response is returned.

The BeforeSave/AfterSave methods also invoke any registered SaveBehavior.OnBeforeSave / OnAfterSave, so a behavior runs in the same phase.

Example: Calculating a Value Before Save

A common use is computing a value in BeforeSave. For example, the Student Information System tutorial computes a grade average before the record is saved:

public class GradesSaveHandler : SaveRequestHandler<MyRow>, IGradesSaveHandler
{
    public GradesSaveHandler(IRequestContext context) : base(context)
    {
    }

    protected override void BeforeSave()
    {
        base.BeforeSave();

        Row.CalculateAverage();
    }
}

Behaviors

Behaviors implementing ISaveBehavior run for every save handler. They are discovered through the type source and are the recommended way to add cross-cutting save logic (audit logging, multi-tenancy, master–detail saving, etc.) without touching individual handlers.

See Also