Skip to content

Latest commit

 

History

History
121 lines (89 loc) · 9.04 KB

Layer-Manager.md

File metadata and controls

121 lines (89 loc) · 9.04 KB

Manager (Domain Logic)

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.


Usage

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.


Railway-oriented programming

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.


Code-generated

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);

Custom

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);