Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/messagepack-serialization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# MessagePack serialization wire schema (draft)

OpenRiaServices supports MessagePack with MIME type `application/vnd.msgpack`.

## Request envelope (POST/QUERY/Submit)

Top-level object map:

- `parameters`: map<string, value?>
Parameter name to a MessagePack value serialized with the declared parameter type converter. `nil` means `null`.
- `queryOptions`: array of `ServiceQueryPart` (optional)
- `includeTotalCount`: bool (optional)

## Success response envelope

Top-level object map:

- `result`: value?
MessagePack value for the declared operation return type. `nil` means `null` / no value.

## Fault response envelope

Top-level object map:

- `fault`: `DomainServiceFault`

## Notes

- GET query behavior stays unchanged (URL-encoded query parameters).
- Envelopes are map-based for schema/version tolerance.
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

#nullable enable

namespace OpenRiaServices.Client.DomainClients.Http.MessagePack
{
abstract class MessagePackRequestEnvelopeBase
{
public MessagePackMethodParameters? Parameters { get; set; }
}

sealed class MessagePackQueryRequestEnvelope : MessagePackRequestEnvelopeBase
{
public List<ServiceQueryPart>? QueryOptions { get; set; }
public bool IncludeTotalCount { get; set; }
}

sealed class MessagePackInvokeRequestEnvelope : MessagePackRequestEnvelopeBase
{
}

sealed class MessagePackSubmitRequestEnvelope : MessagePackRequestEnvelopeBase
{
}

abstract class MessagePackResponseEnvelopeBase
{
public DomainServiceFault? Fault { get; set; }
public abstract object? GetResult();
}


sealed class MessagePackQueryResponseEnvelope<TResult> : MessagePackResponseEnvelopeBase
{
public TResult? Result { get; set; }
public override object? GetResult() => Result;
}

sealed class MessagePackInvokeResponseEnvelope<TResult> : MessagePackResponseEnvelopeBase
{
public TResult? Result { get; set; }
public override object? GetResult() => Result;
}

sealed class MessagePackSubmitResponseEnvelope<TResult> : MessagePackResponseEnvelopeBase
{
public TResult? Result { get; set; }
public override object? GetResult() => Result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;

namespace OpenRiaServices.Client.DomainClients.Http.MessagePack
{
sealed class MessagePackMethodParameters
{
public MessagePackMethodParameters(MethodParameters methodParameters, IDictionary<string, object> values)
{
MethodParameters = methodParameters ?? throw new ArgumentNullException(nameof(methodParameters));
Values = values ?? throw new ArgumentNullException(nameof(values));
}

public IDictionary<string, object> Values { get; }

public MethodParameters MethodParameters { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using Nerdbank.MessagePack;
using System;
using System.Threading.Tasks;

namespace OpenRiaServices.Client.DomainClients.Http.MessagePack;
sealed class MessagePackMethodParametersConverter : MessagePackConverter<MessagePackMethodParameters>
{
internal static readonly object MethodParametersKey = new();

public override bool PreferAsyncSerialization => true;

public override MessagePackMethodParameters Read(ref MessagePackReader reader, SerializationContext context)
{
throw new NotImplementedException();
}

public override void Write(ref MessagePackWriter writer, in MessagePackMethodParameters value, SerializationContext context)
{
if (value is null)
{
writer.WriteNil();
return;
}

context.DepthStep();
MethodParameters methodParameters = GetMethodParameters(context);
writer.WriteMapHeader(value.Values.Count);

foreach (var parameterValue in value.Values)
{
writer.Write(parameterValue.Key);
Type parameterType = methodParameters.GetTypeForMethodParameter(parameterValue.Key);
WriteValue(ref writer, parameterValue.Value, parameterType, context);
}
}

public override async ValueTask<MessagePackMethodParameters> ReadAsync(MessagePackAsyncReader reader, SerializationContext context)
{
throw new NotImplementedException();
}

public override async ValueTask WriteAsync(MessagePackAsyncWriter writer, MessagePackMethodParameters value, SerializationContext context)
{
if (value is null)
{
writer.WriteNil();
return;
}

context.DepthStep();
MethodParameters methodParameters = GetMethodParameters(context);
writer.WriteMapHeader(value.Values.Count);

foreach (var parameterValue in value.Values)
{
writer.Write(static (ref MessagePackWriter syncWriter, string key) => syncWriter.Write(key), parameterValue.Key);
Type parameterType = methodParameters.GetTypeForMethodParameter(parameterValue.Key);
await WriteValueAsync(writer, parameterValue.Value, parameterType, context).ConfigureAwait(false);
await writer.FlushIfAppropriateAsync(context).ConfigureAwait(false);
}
}

private static MethodParameters GetMethodParameters(SerializationContext context)
=> (MethodParameters)context[MethodParametersKey];
private static void WriteValue(ref MessagePackWriter writer, object value, Type parameterType, SerializationContext context)
{
if (value is null)
writer.WriteNil();
else
context.GetConverter(parameterType, context.TypeShapeProvider).WriteObject(ref writer, value, context);
}

private static ValueTask WriteValueAsync(MessagePackAsyncWriter writer, object value, Type parameterType, SerializationContext context)
=> value is null
? WriteNilAsync(writer)
: context.GetConverter(parameterType, context.TypeShapeProvider).WriteObjectAsync(writer, value, context);

private static ValueTask WriteNilAsync(MessagePackAsyncWriter writer)
{
writer.WriteNil();
return default;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
using Nerdbank.MessagePack;
using OpenRiaServices.Client.DomainClients.Http.MessagePack;
using PolyType;
using PolyType.ReflectionProvider;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.ServiceModel;
using System.Threading;
using System.Threading.Tasks;

namespace OpenRiaServices.Client.DomainClients.Http
{
/// <summary>
/// <see cref="DomainClient"/> implementation which uses MessagePack over HTTP.
/// </summary>
sealed class MessagePackHttpDomainClient : HttpDomainClient
{
internal const string MediaType = "application/vnd.msgpack";
private static readonly HttpMethod s_queryMethod = new("QUERY");
private readonly MessagePackSerializer _serializer;
private readonly ITypeShapeProvider _typeShapeProvider;

public MessagePackHttpDomainClient(HttpClient httpClient, Type serviceInterface, MessagePackHttpDomainClientFactory factory)
: base(httpClient, serviceInterface, factory)
{
_serializer = factory.Serializer;
_typeShapeProvider = factory.TypeShapeProver;
}

private protected override Task<HttpResponseMessage> PostAsync(string operationName, IDictionary<string, object> parameters, List<ServiceQueryPart> queryOptions, CancellationToken cancellationToken)
=> SendWithBodyAsync(HttpMethod.Post, operationName, parameters, queryOptions, cancellationToken);

private protected override Task<HttpResponseMessage> QueryAsync(string operationName, IDictionary<string, object> parameters, List<ServiceQueryPart> queryOptions, CancellationToken cancellationToken)
=> SendWithBodyAsync(s_queryMethod, operationName, parameters, queryOptions, cancellationToken);

private async Task<HttpResponseMessage> SendWithBodyAsync(HttpMethod method, string operationName, IDictionary<string, object> parameters, List<ServiceQueryPart> queryOptions, CancellationToken cancellationToken)
{
using var request = new HttpRequestMessage(method, operationName);
MethodParameters methodParameters = GetMethodParameters(operationName);
var envelope = CreateRequestEnvelope(method, operationName, methodParameters, parameters, queryOptions);
MessagePackSerializer operationSerializer = CreateOperationSerializer(_serializer, methodParameters);

using var stream = new MemoryStream();
// TODO: If possible, replace this buffering with a custom HttpContent override of SerializeToStream
// so the request payload can be serialized asynchronously directly to the outgoing request stream.
await operationSerializer.SerializeObjectAsync(stream, envelope, _typeShapeProvider.GetTypeShapeOrThrow(envelope.GetType()), cancellationToken).ConfigureAwait(false);

var bytes = stream.ToArray();
request.Content = new ByteArrayContent(bytes);
request.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(MediaType);

return await HttpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false);
}

private MessagePackRequestEnvelopeBase CreateRequestEnvelope(HttpMethod method, string operationName, MethodParameters methodParameters, IDictionary<string, object> parameters, List<ServiceQueryPart> queryOptions)
{
MessagePackMethodParameters requestParameters = (parameters is { Count: > 0 })
? new (methodParameters, parameters) : null;

if (queryOptions is not null && queryOptions.Count > 0)
{
var request = new MessagePackQueryRequestEnvelope()
{
QueryOptions = new List<ServiceQueryPart>(queryOptions.Count),
Parameters = requestParameters
};
foreach (var queryOption in queryOptions)
{
if (string.Equals(queryOption.QueryOperator, "includeTotalCount", StringComparison.OrdinalIgnoreCase)
&& bool.TryParse(queryOption.Expression, out bool includeTotalCount))
{
request.IncludeTotalCount = includeTotalCount;
}
else
{
request.QueryOptions.Add(queryOption);
}
}
return request;
}

return string.Equals(operationName, "SubmitChanges", StringComparison.Ordinal)
? new MessagePackSubmitRequestEnvelope() { Parameters = requestParameters }
: new MessagePackInvokeRequestEnvelope() { Parameters = requestParameters };
}

private protected override async Task<object> ReadResponseAsync(HttpResponseMessage response, string operationName, Type returnType)
{
using (response)
{
if (!response.IsSuccessStatusCode && response.Content.Headers.ContentType?.MediaType != MediaType)
{
var message = string.Format(CultureInfo.InvariantCulture, Resources.DomainClient_UnexpectedHttpStatusCode, (int)response.StatusCode, response.StatusCode);

if (response.StatusCode == HttpStatusCode.BadRequest)
throw new DomainOperationException(message, OperationErrorStatus.NotSupported, (int)response.StatusCode, null);
else if (response.StatusCode == HttpStatusCode.Unauthorized)
throw new DomainOperationException(message, OperationErrorStatus.Unauthorized, (int)response.StatusCode, null);
else if (response.StatusCode == HttpStatusCode.NotFound)
throw new DomainOperationException(message, OperationErrorStatus.NotFound, (int)response.StatusCode, null);
else
throw new DomainOperationException(message, OperationErrorStatus.ServerError, (int)response.StatusCode, null);
}

using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
Type envelopeType = GetResponseEnvelopeType(operationName, returnType);
var typeShape = _typeShapeProvider.GetTypeShapeOrThrow(envelopeType);

var envelope = (MessagePackResponseEnvelopeBase)await _serializer.DeserializeObjectAsync(stream, typeShape).ConfigureAwait(false)
?? (MessagePackResponseEnvelopeBase)Activator.CreateInstance(envelopeType);

if (envelope.Fault is not null)
{
throw new FaultException<DomainServiceFault>(
envelope.Fault,
new FaultReason(new[] { new FaultReasonText(envelope.Fault.ErrorMessage, CultureInfo.CurrentCulture.Name) }),
new FaultCode("Sender"),
operationName);
}

object result = envelope.GetResult();
if (returnType == typeof(void) || result is null)
return null;

return result;
}
}


private static MessagePackSerializer CreateOperationSerializer(MessagePackSerializer serializer, MethodParameters methodParameters)
{
SerializationContext context = serializer.StartingContext;
context[MessagePackMethodParametersConverter.MethodParametersKey] = methodParameters;
return serializer with { StartingContext = context };
}

private static Type GetResponseEnvelopeType(string operationName, Type returnType)
{
if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(QueryResult<>))
return typeof(MessagePackQueryResponseEnvelope<>).MakeGenericType(returnType);
if (string.Equals(operationName, "SubmitChanges", StringComparison.Ordinal))
return typeof(MessagePackSubmitResponseEnvelope<>).MakeGenericType(returnType);
return typeof(MessagePackInvokeResponseEnvelope<>).MakeGenericType(returnType);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace OpenRiaServices.Client.DomainClients.Http
/// <summary>
/// A dictionary of parameter name and types for a method
/// </summary>
internal class MethodParameters
internal sealed class MethodParameters
{
private readonly string _operationName;
private readonly Dictionary<string, Type> _parameterNameAndTypeDictionary;
Expand Down
Loading
Loading