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.
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).
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.
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.
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 |
---|---|
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 });
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();