Introduction
A handler is an object that participates in a query. It can initialize, decorate, and finalize results. Handler gets an IQuery argument, which carries request and result objects, context dictionary, and service stack for futher queries.
IHandler<Request, Response> handles a specific Request type, and returns a specific Response type.
public class Handler : IHandler<TypeRequest, Type>
{
public void Handle(IQuery<TypeRequest, Type> query)
{
// Assign result
query.Response.SetValue(typeof(string));
}
}
IHandler<object, object> is forwarded with all requests classes and structs. This, however, causes value-typed requests to be boxed, which adds a bit of heap garbage.
public class Handler5 : IHandler<object, object>
{
public void Handle(IQuery<object, object> query)
{
// Already handled
if (query.Handled()) return;
// Assign result
query.Response.SetValue(typeof(string));
}
}
IHandlerAsync<Request, Response> and IHandlerAsync process queries asynchronously.
public class DownloadUrl : IHandlerAsync<string, string>
{
public async Task HandleAsync(IQuery<string, string> query)
{
// Already handled
if (query.Handled()) return;
// Create client
WebClient client = new WebClient();
// Get url
string url = query.Request;
// Download content
string content = await client.DownloadStringTaskAsync(url);
// Write result
query.Response.SetValue(content);
}
}
IHandlerBase interface allows generic method type parameters Handle<T>(IQuery<,>).
public class Handler13 : IHandlerBase
{
[Order(1000)]
public void Handle<T>(IQuery<ScopedServiceRequest<T>, object> query)
{
}
}
And same for async method Task HandleAsync<T>(IQuery<,>).
public class Handler14 : IHandlerBase
{
[Order(1000)]
public Task HandleAsync<T>(IQuery<ScopedServiceRequest<T>, object> query)
{
return Task.Delay(1);
}
}
where constraint can be used with generics Handle method.
public class Handler15 : IHandlerBase
{
public void Handle<T>(IQuery<ScopedServiceRequest<T>, object> query) where T : IList<T> { }
}
MethodArgumentConstraints-property can be used to list generics method argument constraints if where is not expressive enough.
public class Handler16 : IHandlerBase
{
/// <summary>Additional type constraints (will be picked in <see cref="IHandlerInfo"/>)</summary>
public (string, Type)[]? MethodArgumentConstraints =>
new (string, Type)[]
{
("T", typeof(IList<>))
};
public void Handle<T>(IQuery<ScopedServiceRequest<T>, object> query) { }
}
IHandler handles all request and response types.
public class ConsoleLogger : IHandler
{
public void Handle<Request, Response>(IQuery<Request, Response> query) where Request : notnull
=> Console.WriteLine(query.Response);
}
public bool HandlesRequest(Type requestType) method can be used to evaluate which request types are to be handled.
public class Handler17 : IHandlerBase
{
/// <summary>Evaluate which request types to evaluate</summary>
public bool HandlesRequest(Type requestType)
=> requestType.IsGenericType &&
requestType.GetGenericTypeDefinition().Equals(typeof(ScopedServiceRequest<>));
public void Handle<T>(IQuery<ScopedServiceRequest<T>, object> query) { }
}
If handler implements multiple IHandlerT interfaces, HandlesRequest can be assigned to specific Handle() method by introducing it in an interface.
public class Handler18 : IMyHandler
{
/// <summary>Evaluate which request types to evaluate</summary>
bool IMyHandler.HandlesRequest(Type requestType)
=> requestType.IsGenericType &&
requestType.GetGenericTypeDefinition().Equals(typeof(ScopedServiceRequest<>));
void IMyHandler.Handle<T>(IQuery<ScopedServiceRequest<T>, object> query) { }
}
public interface IMyHandler : IHandlerBase
{
/// <summary>Evaluate which request types to evaluate</summary>
bool HandlesRequest(Type requestType);
/// <summary>Handle</summary>
void Handle<T>(IQuery<ScopedServiceRequest<T>, object> query);
}
Custom Handler interface
Custom handler interfaces can be defined.
public interface IServiceHandler<T> : IHandlerBaseSync
{
void Handle(IQuery<ScopedServiceRequest<T>, object> query);
}
public interface IServiceHandler : IHandlerBaseSync
{
public void Handle<T>(IQuery<ScopedServiceRequest<T>, object> query);
}
If custom handler interface has "Order" property, then the Handle() method will be associated with it in the implementation.
public interface IServiceHandlerWithOrder<T> : IHandlerBaseSync
{
long? Order { get; }
void Handle(IQuery<ScopedServiceRequest<T>, object> query);
}
Query
A IQuery<Request, Response> is an object that is passed through every applicable handler. Each handler can choose whether it participates in building the result value. It can add a result or replace or modify a previously assigned result.
public class Handler2 : IHandler<TypeRequest, Type>
{
public void Handle(IQuery<TypeRequest, Type> query)
{
// Assign result (overwrite result from earlier handler)
query.Response.SetValue(typeof(string));
}
}
If handler intends to assign an initial result, then it should check that previous handler has not already assigned a result by checking query.Handled().
public class Handler2B : IHandler<TypeRequest, Type>
{
public void Handle(IQuery<TypeRequest, Type> query)
{
// Already handled by another handler
if (query.Handled()) return;
// Assign result
query.Response.SetValue(typeof(string));
}
}
IQuery
/// <summary>Contains query information such as request and response.</summary>
/// <remarks>The resolver implementation that constructs query may add initial UserData instance that contains session related information.</remarks>
public interface IQuery : IServiceContainer
{
/// <summary>Constraint to request type</summary>
Type RequestType { get; }
/// <summary>Constraint to result type</summary>
Type ResponseType { get; }
/// <summary>Request</summary>
object Request { get; }
/// <summary>Response</summary>
IEntry Response { get; }
/// <summary>Signal whether to break query. If assigned true, then resolver should not pass query to any further handler. The last remaining entry state is to be marked completed.</summary>
bool BreakQuery { get; set; }
/// <summary>Signal that indicates that service's scope or call has been aborted.</summary>
/// <remarks>
/// Most <see cref="IHandlerBase"/> implementations don't need to check cancel request as the calling resolver checks them.
/// However, if <see cref="IHandlerBase"/> does long, async or blocking operations, then periodical checking is suggested.
/// </remarks>
CancellationToken CancellationToken { get; }
/// <summary>Is <see cref="Context"/> initialized.</summary>
bool HasContextData { get; }
/// <summary>Lazy initialized context data.</summary>
IDictionary<string, object>? Context { get; set; }
}
/// <summary>Query interface with strongly typed request.</summary>
/// <typeparam name="_Request"></typeparam>
public interface IQuery<out _Request> : IQuery where _Request : notnull
{
/// <summary>Request</summary>
new _Request Request { get; }
}
/// <summary>Query interface with strongly typed request and result.</summary>
/// <typeparam name="_Request"></typeparam>
/// <typeparam name="_Response"></typeparam>
public interface IQuery<out _Request, in _Response> : IQuery<_Request> where _Request : notnull
{
/// <summary>Result container</summary>
new IEntryWritable<_Response> Response { get; }
}
Entry
A IEntry<Response> is the object where result is written to. If cache is used, entry remains in cache and carries result value.
IEntry interface.
/// <summary>
/// Cache and entry entry. Can contain a resolve status, resolve error or entry value.
///
/// Sub-interfaces:
/// <see cref="IEntry{T}"/>
/// <see cref="IEntryWritable"/>
/// <see cref="IEntryWritable{T}"/>
/// <see cref="IEntryReadable"/>
/// <see cref="IEntryReadable{T}"/>
/// <see cref="IEntryObservable"/>
/// <see cref="IEntryVisitable"/>
/// <see cref="IEntryAwaitable"/>
/// <see cref="IEntryDecoration"/>
/// <see cref="IEntryCast"/>
/// </summary>
public interface IEntry
{
/// <summary>Current status</summary>
EntryStatus Status { get; }
/// <summary>Policy flags</summary>
EntryPolicy Policy { get; set; }
/// <summary>Processing thread id. 0=unassigned.</summary>
int ThreadId { get; set; }
}
/// <summary>Indicates that entry has a designated type.</summary>
public interface IEntryT : IEntry { }
/// <summary>Indicates entry is desginated for value type <typeparamref name="T"/>.</summary>
public interface IEntry<T> : IEntryT { }
/// <summary>Indiciates that entry is readable.</summary>
public interface IEntryReadable : IEntry
{
/// <summary>Error</summary>
Exception? Error { get; }
/// <summary>Snapshot of state</summary>
EntryState State { get; }
/// <summary>Entry value as object.</summary>
object? Value { get; }
}
/// <summary>Indiciates that entry is readable for specific type.</summary>
public interface IEntryReadableT : IEntryReadable, IEntryT { }
/// <summary>Readable entry of <typeparamref name="T"/> value.</summary>
public interface IEntryReadable<out T> : IEntryReadableT
{
/// <summary>Entry value as <typeparamref name="T"/>.</summary>
new T Value { get; }
}
/// <summary>Indiciates that entry is writable.</summary>
public interface IEntryWritable : IEntry
{
/// <summary>Try to mark entry into processing state. Only one thread may reserve the processing state. The thread that gets the state must complete the entry into <see cref="EntryStatus.Completed"/> state, and other threads must wait for it.</summary>
/// <returns>True if state changed from <see cref="EntryStatus.Unassigned"/> to <see cref="EntryStatus.Processing"/></returns>
/// <exception cref="Exception">Unexpected error (from an observer)</exception>
bool TrySetProcessing();
/// <summary>Try dispose associated value.</summary>
/// <exception cref="Exception">Unexpected error (from an observer)</exception>
bool TrySetCanceled();
/// <summary>Try to assign <see cref="EntryStatus.NoValue"/> state.</summary>
/// <exception cref="Exception">Unexpected error (from an observer)</exception>
bool TrySetNoValue();
/// <summary>Try to assign <paramref name="value"/>.</summary>
/// <exception cref="Exception">Unexpected error (from an observer)</exception>
bool TrySetValue(object? value);
/// <summary>Try to assign default value, e.g. null</summary>
/// <exception cref="Exception">Unexpected error (from an observer)</exception>
bool TrySetValueDefault();
/// <summary>Try to assign <paramref name="errorValue"/> and set state to <see cref="EntryStatus.Error"/>.</summary>
/// <exception cref="Exception">Unexpected error (from an observer)</exception>
bool TrySetErrorValue(Exception errorValue);
/// <summary>Assign <paramref name="error"/> and set state to <see cref="EntryStatus.QueryError"/>.</summary>
/// <exception cref="Exception">Unexpected error (from an observer)</exception>
bool TrySetQueryError(Exception error);
/// <summary>Assign flags.</summary>
/// <exception cref="Exception">Unexpected error (from an observer)</exception>
bool TrySetFlags(EntryStatus flagsToInclude, EntryStatus flagsToExclude);
/// <summary>
/// Try to dispose associated value.
/// Entry must be in <see cref="EntryStatus.Value"/> state.
/// If value implements <see cref="IDisposable"/> it's called, if not then function succeeds.
/// If dispose fails, then exception is let to fly, and state is not changed.
///
/// If dispose was successful or value did not implement <see cref="IDisposable"/> then state is changed to <see cref="EntryStatus.ValueDisposed"/>.
/// </summary>
/// <exception cref="AggregateException">Any exception thrown from <see cref="IDisposable.Dispose"/> or observers.</exception>
bool TryDisposeValue();
/// <summary>
/// Try to the reference to the associated value.
/// Entry must be in <see cref="EntryStatus.Value"/>, <see cref="EntryStatus.Error"/>, <see cref="EntryStatus.QueryError"/> or <see cref="EntryStatus.ValueDisposed"/> state.
/// If value has not been dispose, this method does not dispose it.
///
/// If value is not nullable then default value is set.
/// State of entry is set to <see cref="EntryStatus.ValueNulled"/>.
/// </summary>
/// <exception cref="Exception">Any exception thrown from observers.</exception>
bool TryNullValue();
}
/// <summary>Indiciates that entry is writable for specific type.</summary>
public interface IEntryWritableT : IEntryWritable, IEntryT { }
/// <summary>Writable entry for <typeparamref name="T"/> values.</summary>
public interface IEntryWritable<in T> : IEntryWritableT
{
/// <summary>Assign <paramref name="value"/>.</summary>
bool TrySetValue(T value);
}
EntryStatus
EntryStatus represents internal status of IEntry.
/// <summary>Status of <see cref="IEntry"/>.</summary>
[Flags]
public enum EntryStatus : ulong
{
////// Below here are states. Only one state may apply. //////
////// However, they are listed as flags so that multiple states may be indicated. //////
/// <summary>Unassigned.</summary>
Unassigned = 0,
/// <summary>Query started.</summary>
Processing = 1UL << 10,
/// <summary>Query was cancelled.</summary>
Cancelled = 1UL << 12,
/// <summary>Request was processed, but no handler was able to produce a value.</summary>
NoValue = 1UL << 14,
/// <summary>Query resolved to a successful value.</summary>
Value = 1UL << 16,
/// <summary>Query resolved successfully but result is an error value.</summary>
Error = 1UL << 18,
/// <summary><see cref="IService"/> or <see cref="IHandlerBase"/> threw an unexpected exception.</summary>
QueryError = 1UL << 20,
/// <summary>Value is disposed.</summary>
ValueDisposed = 1UL << 22,
/// <summary>Value reference is nulled or defaulted.</summary>
ValueNulled = 1UL << 23,
/// <summary>Entry container is disposed.</summary>
Disposed = 1UL << 24,
/// <summary>Any value assigned. Used with <see cref="IService.TryGetInitialized"/></summary>
AssignedStates = Cancelled | NoValue | Value | Error | QueryError | ValueDisposed | ValueNulled | Disposed,
////// Below here are status flags //////
/// <summary>Query completed (or broken). This flag that indicates that resolver has applied query on every <see cref="IHandlerBase"/>.</summary>
Completed = 1UL << 30,
/// <summary>Entry is being managed by a cache.</summary>
Cached = 1UL << 31,
/// <summary>Reserved for future use for the library</summary>
Reserved2 = 1UL << 32,
/// <summary>Reserved for future use for the library</summary>
Reserved3 = 1UL << 33,
/// <summary>Reserved for future use for the library</summary>
Reserved4 = 1UL << 34,
/// <summary>Reserved for future use for the library</summary>
Reserved5 = 1UL << 35,
/// <summary>Reserved for future use for the library</summary>
Reserved6 = 1UL << 36,
/// <summary>Reserved for future use for the library</summary>
Reserved7 = 1UL << 37,
/// <summary>Reserved for future use for the library</summary>
Reserved8 = 1UL << 38,
/// <summary>Reserved for future use for the library</summary>
Reserved9 = 1UL << 39,
/// <summary>Flag allocated for any 3rd party domain specific use</summary>
User0 = 1UL << 40,
/// <summary>Flag allocated for any 3rd party domain specific use</summary>
User1 = 1UL << 41,
/// <summary>Flag allocated for any 3rd party domain specific use</summary>
User2 = 1UL << 42,
/// <summary>Flag allocated for any 3rd party domain specific use</summary>
User3 = 1UL << 43,
/// <summary>Flag allocated for any 3rd party domain specific use</summary>
User4 = 1UL << 44,
/// <summary>Flag allocated for any 3rd party domain specific use</summary>
User5 = 1UL << 45,
/// <summary>Flag allocated for any 3rd party domain specific use</summary>
User6 = 1UL << 46,
/// <summary>Flag allocated for any 3rd party domain specific use</summary>
User7 = 1UL << 47,
/// <summary>Mask for flags</summary>
Flags = Completed | Cached | Reserved2 | Reserved3 | Reserved4 | Reserved5 | Reserved6 | Reserved7 | Reserved8 | Reserved9 | User0 | User1 | User3 | User4 | User5 | User6 | User7,
}
- Unassigned is initial state. Query processing has not yet started.
- Processing state is set when handlers start processing the request.
- Cancelled is set when request is cancelled.
- Value is set when a result is assigned by a handler.
- NoValue is set if no handler produced a result.
- Error if handler assigns an error status.
- QueryError is set if unexpected exception is captured from handler.
- ValueDisposed is set when assigned value is disposed along with service or cache.
- ValueNulled is set reference to value is removed from entry.
- Disposed is set entry object has been disposed.
Evaluation Order
Handlers are evaluated in order of assigned order values. The higher the number the later the handler will be processed. [Order] attribute can be placed on method and on class.
public class Handler3 : IHandler<TypeRequest, Type>
{
[Order(1_000_000_000)]
public void Handle(IQuery<TypeRequest, Type> query)
{
// Already handled
if (query.Handled()) return;
// Assign result
query.Response.SetValue(typeof(string));
}
}
Multiple handlers can be implemented in the same class with different [Order] values.
public class Handler8 : IHandler<TypeRequest, Type>, IHandler<ScopedServiceRequest<IList>, IList>
{
[Order(1000)]
public void Handle(IQuery<TypeRequest, Type> query)
{
}
[Order(2000)]
public void Handle(IQuery<ScopedServiceRequest<IList>, IList> query)
{
}
}
Another way to provide evaluation order is to implement IHandlerWithOrder and IHandlerAsyncWithOrder.
public class Handler4 : IHandlerWithOrder<TypeRequest, Type>
{
/// <summary>Evaluation order</summary>
long? IHandlerWithOrder<TypeRequest, Type>.Order => 1_000_000_000;
public void Handle(IQuery<TypeRequest, Type> query)
{
// Already handled
if (query.Handled()) return;
// Assign result
query.Response.SetValue(typeof(string));
}
}
Break Query
.BreakQuery is a property to indicate that query is not to be processed by any further handlers. This does not fail the already assigned result, stops further evaluation and accepts the result.
public class Handler20 : IHandler<TypeRequest, Type>
{
public void Handle(IQuery<TypeRequest, Type> query)
{
// Assign result
query.Response.SetValue(typeof(string));
// Break query process
query.BreakQuery = true;
}
}
Full Example
Full example
#pragma warning disable SYSLIB0014
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Avalanche.Service;
public class handler_index
{
// <1>
public class Handler : IHandler<TypeRequest, Type>
{
public void Handle(IQuery<TypeRequest, Type> query)
{
// Assign result
query.Response.SetValue(typeof(string));
}
}
// </1>
// <2>
public class Handler2 : IHandler<TypeRequest, Type>
{
public void Handle(IQuery<TypeRequest, Type> query)
{
// Assign result (overwrite result from earlier handler)
query.Response.SetValue(typeof(string));
}
}
// </2>
// <2B>
public class Handler2B : IHandler<TypeRequest, Type>
{
public void Handle(IQuery<TypeRequest, Type> query)
{
// Already handled by another handler
if (query.Handled()) return;
// Assign result
query.Response.SetValue(typeof(string));
}
}
// </2B>
// <3>
public class Handler3 : IHandler<TypeRequest, Type>
{
[Order(1_000_000_000)]
public void Handle(IQuery<TypeRequest, Type> query)
{
// Already handled
if (query.Handled()) return;
// Assign result
query.Response.SetValue(typeof(string));
}
}
// </3>
// <4>
public class Handler4 : IHandlerWithOrder<TypeRequest, Type>
{
/// <summary>Evaluation order</summary>
long? IHandlerWithOrder<TypeRequest, Type>.Order => 1_000_000_000;
public void Handle(IQuery<TypeRequest, Type> query)
{
// Already handled
if (query.Handled()) return;
// Assign result
query.Response.SetValue(typeof(string));
}
}
// </4>
// <5>
public class Handler5 : IHandler<object, object>
{
public void Handle(IQuery<object, object> query)
{
// Already handled
if (query.Handled()) return;
// Assign result
query.Response.SetValue(typeof(string));
}
}
// </5>
// <6>
public class ConsoleLogger : IHandler
{
public void Handle<Request, Response>(IQuery<Request, Response> query) where Request : notnull
=> Console.WriteLine(query.Response);
}
// </6>
// <7>
public class DownloadUrl : IHandlerAsync<string, string>
{
public async Task HandleAsync(IQuery<string, string> query)
{
// Already handled
if (query.Handled()) return;
// Create client
WebClient client = new WebClient();
// Get url
string url = query.Request;
// Download content
string content = await client.DownloadStringTaskAsync(url);
// Write result
query.Response.SetValue(content);
}
}
// </7>
// <8>
public class Handler8 : IHandler<TypeRequest, Type>, IHandler<ScopedServiceRequest<IList>, IList>
{
[Order(1000)]
public void Handle(IQuery<TypeRequest, Type> query)
{
}
[Order(2000)]
public void Handle(IQuery<ScopedServiceRequest<IList>, IList> query)
{
}
}
// </8>
// <10>
public interface IServiceHandler<T> : IHandlerBaseSync
{
void Handle(IQuery<ScopedServiceRequest<T>, object> query);
}
// </10>
// <11>
public interface IServiceHandlerWithOrder<T> : IHandlerBaseSync
{
long? Order { get; }
void Handle(IQuery<ScopedServiceRequest<T>, object> query);
}
// </11>
// <12>
public interface IServiceHandler : IHandlerBaseSync
{
public void Handle<T>(IQuery<ScopedServiceRequest<T>, object> query);
}
// </12>
// <13>
public class Handler13 : IHandlerBase
{
[Order(1000)]
public void Handle<T>(IQuery<ScopedServiceRequest<T>, object> query)
{
}
}
// </13>
// <14>
public class Handler14 : IHandlerBase
{
[Order(1000)]
public Task HandleAsync<T>(IQuery<ScopedServiceRequest<T>, object> query)
{
return Task.Delay(1);
}
}
// </14>
// <15>
public class Handler15 : IHandlerBase
{
public void Handle<T>(IQuery<ScopedServiceRequest<T>, object> query) where T : IList<T> { }
}
// </15>
// <16>
public class Handler16 : IHandlerBase
{
/// <summary>Additional type constraints (will be picked in <see cref="IHandlerInfo"/>)</summary>
public (string, Type)[]? MethodArgumentConstraints =>
new (string, Type)[]
{
("T", typeof(IList<>))
};
public void Handle<T>(IQuery<ScopedServiceRequest<T>, object> query) { }
}
// </16>
// <17>
public class Handler17 : IHandlerBase
{
/// <summary>Evaluate which request types to evaluate</summary>
public bool HandlesRequest(Type requestType)
=> requestType.IsGenericType &&
requestType.GetGenericTypeDefinition().Equals(typeof(ScopedServiceRequest<>));
public void Handle<T>(IQuery<ScopedServiceRequest<T>, object> query) { }
}
// </17>
// <18>
public class Handler18 : IMyHandler
{
/// <summary>Evaluate which request types to evaluate</summary>
bool IMyHandler.HandlesRequest(Type requestType)
=> requestType.IsGenericType &&
requestType.GetGenericTypeDefinition().Equals(typeof(ScopedServiceRequest<>));
void IMyHandler.Handle<T>(IQuery<ScopedServiceRequest<T>, object> query) { }
}
public interface IMyHandler : IHandlerBase
{
/// <summary>Evaluate which request types to evaluate</summary>
bool HandlesRequest(Type requestType);
/// <summary>Handle</summary>
void Handle<T>(IQuery<ScopedServiceRequest<T>, object> query);
}
// </18>
// <20>
public class Handler20 : IHandler<TypeRequest, Type>
{
public void Handle(IQuery<TypeRequest, Type> query)
{
// Assign result
query.Response.SetValue(typeof(string));
// Break query process
query.BreakQuery = true;
}
}
// </20>
}
#pragma warning restore SYSLIB0014