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
IUnitOfWorkargument 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:
ICreateHandler<TRow, TSaveRequest, TSaveResponse>— declaresCreate(IUnitOfWork uow, TSaveRequest request).IUpdateHandler<TRow, TSaveRequest, TSaveResponse>— declaresUpdate(IUnitOfWork uow, TSaveRequest request).
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 likeInsertUserId,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.