Skip to content

Latest commit

 

History

History
137 lines (93 loc) · 9.02 KB

Layer-DataSvc.md

File metadata and controls

137 lines (93 loc) · 9.02 KB

DataSvc (Service Orchestration)

The DataSvc is primarily responsible for orchestrating the underlying data access; whilst often one-to-one there may be times that this class will be used to coordinate multiple data access components. This layer is responsible for ensuring that the related Entity is for the most part fully constructed/updated/etc. as per the desired operation.


Execution caching

To improve potential performance, and reduce chattiness, within an in-process execution context the DataSvc introduces a level of caching (short-lived); this can be turned off where not required. The cache is managed using the IRequestCache.

The purpose of this cache is to minimise this chattiness to the underlying data source, to reduce this cost, where the time between calls (measured in nanoseconds/milliseconds) is such that the data retrieved previously is considered sufficient/valid. This way within an execution context (request) lifetime a developer can invoke the XxxDataSvc multiple times with only a single data source cost reducing the need to cache (and pass around) themselves.

This logic of getting, setting and clearing the cache is included within the primary Get, Create, Update and Delete operations only. Other operations will need to be reviewed and added accordingly (manually).


Event-driven

To support the goals of an Event-driven architecture an event publish can be included.

An EventData publish is invoked where the eventing infrastructure has been included (configured) during code-generation. The IEventPublisher implementation is responsible for orchestraing the publishing and sending of the event message(s).

Note: This is always performed directly after the primary operation logic such that the event is only published where successful. This is may not be transactional (depends on implementation) so if the event publish fails there may be no automatic rollback capabilitity. The implementor will need to decide the corrective action for this type of failure; i.e. consider the transaction outbox pattern.


Usage

This layer is generally code-generated and provides options to provide a fully custom implementation, or 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}DataSvc. There is also a corresonding interface named I{Entity}DataSvc generated so the likes of test mocking etc. can be employed. For example, if the entity is named Person, there will be corresponding PersonDataSvc and IPersonDataSvc 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
DataSvcInvoker The logic is wrapped by a DataSvcInvoker. This enables the InvokerArgs options to be specified, including TransactionScopeOption and Exception handler. These values are generally specified in the code-generation configuration. This invocation will only be output where required, or alternatively explicitly specified.
Cache Trys the cache and returns result where found (as applicable).
Data The I{Entity}Data layer is invoked to perform the data processing.
EventPublish Constructs the EventData and invokes the Event.Publish.
Cache Performs a cache set or remove (as applicable).
OnAfter The OnAfter extension opportunity; where set this will be invoked. This enables logic to be invoked after the primary Operation is performed.

† Note: To minimize the generated code the extension opportunities are only generated where selected. This is performed by setting the dataSvcExtensions attribute to true within the Entity code-generation configuration.

The following demonstrates the generated code (a snippet from the sample RobotDataSvc) that does not include DataSvcExtensions:

// A Get operation.
public Task<Result<Robot?>> GetAsync(Guid id) => Result.Go().CacheGetOrAddAsync(_cache, id, () => _data.GetAsync(id));

// A Create operation.
public Task<Result<Robot>> CreateAsync(Robot value) => DataSvcInvoker.Current.InvokeAsync(this, _ =>
{
    return Result.GoAsync(_data.CreateAsync(value))
                 .Then(r => _events.PublishValueEvent(r, new Uri($"/robots/{r.Id}", UriKind.Relative), $"Demo.Robot", "Create"))
                 .Then(r => _cache.SetValue(r));
}, new InvokerArgs { EventPublisher = _events });

The non-Result based version would be similar to:

// A Get operation.
public Task<Robot?> GetAsync(Guid id) => _cache.GetOrAddAsync(id, () => _data.GetAsync(id));

// A Create operation.
public Task<Robot> CreateAsync(Robot value) => DataSvcInvoker.Current.InvokeAsync(this, async _ =>
{
    var __result = await _data.CreateAsync(value ?? throw new ArgumentNullException(nameof(value))).ConfigureAwait(false);
    _events.PublishValueEvent(__result, new Uri($"/robots/{__result.Id}", UriKind.Relative), $"Demo.Robot", "Create");
    return _cache.SetValue(__result);
}, new InvokerArgs { EventPublisher = _events });

The following demonstrates the generated code (a snippet from the sample PersonDataSvc) that includes DataSvcExtensions:

// A Get operation.
public async Task<Person?> GetExAsync(Guid id)
{
    if (_cache.TryGetValue(id, out Person? __val))
        return __val;

    var __result = await _data.GetExAsync(id).ConfigureAwait(false);
    await Invoker.InvokeAsync(_getExOnAfterAsync?.Invoke(__result, id)).ConfigureAwait(false);
    return _cache.SetValue(__result);
}

// A create operation.
public Task<Person> CreateAsync(Person value) => DataSvcInvoker.Current.InvokeAsync(this, async _ =>
{
    var __result = await _data.CreateAsync(value ?? throw new ArgumentNullException(nameof(value))).ConfigureAwait(false);
    await Invoker.InvokeAsync(_createOnAfterAsync?.Invoke(__result)).ConfigureAwait(false);
    _events.PublishValueEvent(__result, new Uri($"/person/{__result.Id}", UriKind.Relative), $"Demo.Person", "Create");
    return _cache.SetValue(__result);
}, new InvokerArgs { IncludeTransactionScope = true, EventPublisher = _events });

Custom

A custom (OnImplementation) processing pipeline generally consists of:

Step Description
DataSvcInvoker The logic is wrapped by a DataSvcInvoker. This enables the InvokerArgs options to be specified, including TransactionScopeOption and Exception handler. These values are generally specified in the code-generation configuration. This invocation will only be output where required, or alternatively explicitly specified.
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<int>> DataSvcCustomAsync() => DataSvcCustomOnImplementationAsync();

The non-Result based version would be similar to:

public Task<int> DataSvcCustomAsync() => DataSvcCustomOnImplementationAsync();