Service Logic

Service Logic executes on create, read, update or delete of an entity. Service Logic classes are automatically discovered and executed by the framework.

Logic Stage

Service Logic can execute at the listed stages below.

  • PreValidation: Before security validation and before the save to the database. This occurs before validation attributes such as UIRequired, UIReadOnly, UICreateOnly, and UICharacterLimit are checked. If setting a required field in Service Logic, use PreValidation.
  • PreOperation: After security validation and before the save to the database
  • PostOperation: After the save to the database

Create Service Logic

To create Service Logic, create a new file in the project in the Services folder.

Project / Services / MyEntityService.cs

// The ServiceLogic attribute specifies what table this will execute on, what operation(s), and when (before or after the transaction)
[ServiceLogic(nameof(MyEntity), DataOperation.Create | DataOperation.Update, LogicStage.PreOperation, 100)]
public class MyEntityService : IServiceLogic
{
public async Task<Response<object?>> Execute(ServiceContext context)
{
if (context.DataOperation is DataOperation.Create)
{
// Do something on Create
}
if (context.DataOperation is DataOperation.Update)
{
// Do something on Update
}
return ServiceResult.Success();
}
}

The ServiceLogic attribute takes the following parameters.

  • TableName: The name of the entity the service logic will execute on. '*' for all entities.
  • DataOperations: Flags that determine what data operations this should execute on, create, read, update, and delete.
  • LogicStages: Flags that determine which stages the service logic should exeucte on, PreOperation, or PostOperation.
  • Order: Service Logic classes executing on the same table, data operation, and logic stage, will execute following the order.

ServiceContext

ServiceContext has a number of properties that can be used during execution.

Project / Services / MyEntityService.cs

[ServiceLogic(nameof(MyEntity), DataOperation.Create | DataOperation.Update | DataOperation.Read, LogicStage.PreOperation | LogicStage.PostOperation, 100)]
public class MyEntityService : IServiceLogic
{
public async Task<Response<object?>> Execute(ServiceContext context)
{
// Get the db context
DataContext db = context.GetDbContext<DataContext>();
// Do something on Create or Update
if (context.DataOperation is DataOperation.Create or DataOperation.Update)
{
// If this is an update, PreEntity is populated with the entity before the update
MyEntity preEntity = context.GetPreEntity<MyEntity>();
// The Entity property will have the latest values to be saved to the database
MyEntity entity = context.GetEntity<MyEntity>()
// Do something before the SaveChanges call
if (context.LogicStage is LogicStage.PreOperation)
{
// If the Name field is being updated (always true on Create)
if (context.ValueChanged(nameof(MyEntity.Name)))
{
}
}
// Do something after the SaveChanges call
// On create if we need the id of the record being saved
if (context.LogicStage is LogicStage.PostOperation)
{
}
}
// Do something on Read
if (context.DataOperation is DataOperation.Read)
{
// Get the query that was sent to the api
ReadInput readInput = context.ReadInput;
}
return ServiceResult.Success();
}
}

Modify Records

Use the Create, Update, and Delete methods of the ServiceContext to modify records. While it is possible to modify records directly using the Entity Framework DbContext, using the ServiceContext ensures that the service logic is executed.

Project / Logic / MyEntityService.cs

[ServiceLogic(nameof(MyEntity), DataOperation.Create | DataOperation.Update, LogicStage.PreOperation, 100)]
public class MyEntityService : IServiceLogic
{
public async Task<Response<object?>> Execute(ServiceContext context)
{
// Get the db context
DataContext db = context.GetDbContext<DataContext>();
List<Widget> widgets = await db.Widgets.ToListAsync();
// Update the price of all Widgets
foreach (Widget widget in widgets)
{
widget.Price = 9.99M;
// Update the Widget and trigger any service logic
await context.Update<Widget>(widget);
}
return ServiceResult.Success();
}
}

Dependency Injection

Service Logic classes support constructor-based dependency injection. The framework uses ActivatorUtilities.CreateInstance() to instantiate service logic classes, allowing automatic injection of registered services:

Project / Services / WidgetService.cs

[ServiceLogic(nameof(Widget), DataOperation.Create, LogicStage.PreOperation)]
public class WidgetService : IServiceLogic
{
private readonly IEmailService _emailService;
private readonly ICacheService _cacheService;
public WidgetService(IEmailService emailService, ICacheService cacheService)
{
_emailService = emailService;
_cacheService = cacheService;
}
public async Task<Response<object?>> Execute(ServiceContext context)
{
// Use injected services
await _emailService.SendNotification("Widget created");
await _cacheService.InvalidateCache("widgets");
// Use ServiceContext for framework services
context.Logger.LogInformation("Widget processing completed");
return ServiceResult.Success();
}
}

Performance

For optimal performance, prioritize using PreOperation logic whenever possible. This allows the database save to be delayed until all records are processed, or until an entity with PostOperation logic is triggered.

Best Practices

ValueChanged

  • Always use context.ValueChanged("FieldName") to check if a field has changed instead of comparing values directly between the PreEntity and Entity. This will always return true on create operations.

Use context.Create/Update/Delete

Always use context.Create/Update/Delete() - Direct db.Add() bypasses service logic and validations.

// ❌ WRONG: db.Add(entity); await db.SaveChangesAsync();
// ✅ CORRECT:
await context.Create<MyEntity>(entity);

Code Organization

  • Isolate logic into separate methods and use defensive coding to ensure it executes only when expected.

Project / Services / UserService.cs

[ServiceLogic(nameof(User), DataOperation.Create, LogicStage.PreOperation)]
public class UserService : IServiceLogic
{
public async Task<Response<object?>> Execute(ServiceContext context)
{
// Pre-Operation
await SetUserEmailAddress(context);
// Post-Operation
await SendNotificationEmail(context);
return ServiceResult.Success();
}
private async Task SetUserEmailAddress(ServiceContext context)
{
if (context.DataOperation is not DataOperation.Create)
{
return;
}
if (context.LogicStage is not LogicStage.PreOperation)
{
return;
}
// Set User Email Logic
...
}
private async Task SendNotificationEmail(ServiceContext context)
{
if (context.DataOperation is not DataOperation.Create)
{
return;
}
if (context.LogicStage is not LogicStage.PostOperation)
{
return;
}
// Send Notification Email Logic
...
}
}

Error Handling

Do not throw exceptions from service logic. Instead return a failed ServiceResult with an appropriate error message.

Project / Services / ForumCategoryService.cs

[ServiceLogic(nameof(ForumCategory), DataOperation.Create | DataOperation.Update, LogicStage.PreOperation)]
public class ForumCategoryService : IServiceLogic
{
public async Task<Response<object?>> Execute(ServiceContext context)
{
var validateDisplayOrderResponse = await ValidateDisplayOrder(context);
if (!validateDisplayOrderResponse.Succeeded) return validateDisplayOrderResponse;
return ServiceResult.Success();
}
private async Task<Response<object?>> ValidateDisplayOrder(ServiceContext context)
{
if (context.DataOperation is not (DataOperation.Create or DataOperation.Update))
{
return ServiceResult.Success();
}
if (context.LogicStage is not LogicStage.PreOperation)
{
return ServiceResult.Success();
}
var category = context.GetEntity<ForumCategory>();
var db = context.GetDbContext<DataContext>();
// Check if display order is already taken by another category
var existingCategory = await db.ForumCategories
.Where(c => c.DisplayOrder == category.DisplayOrder && c.CategoryId != category.CategoryId)
.FirstOrDefaultAsync();
if (existingCategory != null)
{
return ServiceResult.Error(
$"Display order {category.DisplayOrder} is already in use by category '{existingCategory.Name}'");
}
return ServiceResult.Success();
}
}

Was this page helpful?