The Manager layer is primarily responsible for hosting the key business and/or workflow logic. This is also where the primary validation is performed to ensure the consistency of the request before any further processing is performed.
This layer is generally code-generated and provides options to provide a fully custom implementation, and has extension opportunities to inject additional logic into the processing pipeline.
The Operation
element within the entity.beef-5.yaml
configuration primarily drives the output
There is a generated class per Entity
named {Entity}Manager
. There is also a corresonding interface named I{Entity}Manager
generated so the likes of test mocking etc. can be employed. For example, if the entity is named Person
, there will be corresponding PersonManager
and IPersonManager
classes.
CoreEx version 3.0.0
introduced monadic error-handling, often referred to as Railway-oriented programming. This is enabled via the key types of Result
and Result<T>
; please review the corresponding documentation for more detail on purpose and usage.
The Result
and Result<T>
have been integrated into the code-generated output and is leveraged within the underlying validation. This is intended to simplify success and failure tracking, avoiding the need, and performance cost, in throwing resulting exceptions.
This is implemented by default; however, can be disabled by setting the useResult
attribute to false
within the code-generation configuration.
An end-to-end code-generated processing pipeline generally consists of:
Step | Description |
---|---|
ManagerInvoker |
The logic is wrapped by a ManagerInvoker . This enables the InvokerArgs options to be specified, including TransactionScopeOption and Exception handler. These values are generally specified in the code-generation configuration. |
OperationType |
The ExecutionContext.OperationType is set via the InvokerArgs to specify the type of operation being performed (Create , Read , Update or Delete ) so other functions down the call stack can infer operation intent where required. |
GenerateIdentifier |
Where the identifier for an entity needs to be generated for a Create operation this is specified in the code-generation configuration; or alternatively it will be generated by the underlying data source. |
CleanUp |
Entity CleanUp is the process of reviewing and updating the entity properties to make sure it is in a logical / consistent state. This is only performed where ManagerCleanUp configuration property is specified. |
PreValidate † |
The PreValidate extension opportunity; where set this will be invoked. This enables logic to be invoked before the validation performed. |
Validation |
The specified validator is invoked to validate all input. The MultiValidator is used where extensions† have been selected as it provides an additional OnValidate extension opportunity. |
OnBefore † |
The OnBefore extension opportunity; where set this will be invoked. This enables logic to be invoked before the primary Operation is performed (and after the successful validation). |
DataSvc |
The I{Entity}DataSvc layer is invoked to orchestrate the data processing. |
OnAfter † |
The OnAfter extension opportunity; where set this will be invoked. This enables logic to be invoked after the primary Operation is performed. |
CleanUp |
An Entity CleanUp of the response before returning. This is only performed where ManagerCleanUp configuration property is specified. |
† Note: To minimize the generated code the extension opportunities are only generated where selected. This is performed by setting the managerExtensions
attribute to true
within the Entity
code-generation configuration.
The following demonstrates the generated code (a snippet from the sample RobotManager
) that does not include ManagerExtensions
:
public Task<Result<Robot>> CreateAsync(Robot value) => ManagerInvoker.Current.InvokeAsync(this, ct =>
{
return Result.Go(value).Required()
.ThenAsync(async v => v.Id = await _identifierGenerator.GenerateIdentifierAsync<Guid, Robot>().ConfigureAwait(false))
.Then(v => Cleaner.CleanUp(v))
.ValidateAsync(v => v.Interop(() => FluentValidator.Create<RobotValidator>().Wrap()), cancellationToken: ct)
.ThenAsAsync(v => _dataService.CreateAsync(value));
}, InvokerArgs.Create);
The non-Result
based version would be similar to:
public Task<Robot> CreateAsync(Robot value) => ManagerInvoker.Current.InvokeAsync(this, async _ =>
{
value.Required().Id = await _identifierGenerator.GenerateIdentifierAsync<Guid, Robot>().ConfigureAwait(false);
Cleaner.CleanUp(value);
await value.Validate().Entity().With<RobotValidator>().ValidateAsync(true).ConfigureAwait(false);
return Cleaner.Clean(await _dataService.CreateAsync(value).ConfigureAwait(false));
}, InvokerArgs.Create);
The following demonstrates the generated code (a snippet from the sample PersonManager
) that includes ManagerExtensions
:
public Task<Person> CreateAsync(Person value) => ManagerInvoker.Current.InvokeAsync(this, async _ =>
{
value.Required().Id = await _identifierGenerator.GenerateIdentifierAsync<Guid, Person>().ConfigureAwait(false);
Cleaner.CleanUp(value);
await Invoker.InvokeAsync(_createOnPreValidateAsync?.Invoke(value)).ConfigureAwait(false);
await MultiValidator.Create()
.Add(value.Validate(nameof(value)).Entity().With<PersonValidator>())
.Additional((__mv) => _createOnValidate?.Invoke(__mv, value))
.ValidateAsync(true).ConfigureAwait(false);
await Invoker.InvokeAsync(_createOnBeforeAsync?.Invoke(value)).ConfigureAwait(false);
var __result = await _dataService.CreateAsync(value).ConfigureAwait(false);
await Invoker.InvokeAsync(_createOnAfterAsync?.Invoke(__result)).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, InvokerArgs.Create);
A custom (OnImplementation
) processing pipeline generally consists of:
Step | Description |
---|---|
ManagerInvoker |
The logic is wrapped by a ManagerInvoker . This enables the InvokerArgs options to be specified, including TransactionScopeOption and Exception handler. These values are generally specified in the code-generation configuration. |
OperationType |
The ExecutionContext.OperationType is set via the InvokerArgs to specify the type of operation being performed (Create , Read , Update or Delete ) so other functions down the call stack can infer operation intent where required. |
OnImplementation |
Invocation of a named XxxOnImplementaionAsync method that must be implemented in a non-generated partial class. |
The following demonstrates the generated code:
public Task<Result> RaisePowerSourceChangeAsync(Guid id, RefDataNamespace.PowerSource? powerSource) => ManagerInvoker.Current.InvokeAsync(this, ct =>
{
return Result.Go()
.ThenAsync(() => RaisePowerSourceChangeOnImplementationAsync(id, powerSource));
}, InvokerArgs.Unspecified);
The non-Result
based version would be similar to:
public Task AddAsync(Person person) => ManagerInvoker.Current.InvokeAsync(this, async _ =>
{
await AddOnImplementationAsync(person).ConfigureAwait(false);
}, InvokerArgs.Unspecified);