|
|
|
|
using System.Reflection;
|
|
|
|
|
using System.Text.Json.Nodes;
|
|
|
|
|
using Connected.Annotations;
|
|
|
|
|
using Connected.Interop;
|
|
|
|
|
using Connected.Interop.Annotations;
|
|
|
|
|
using Connected.ServiceModel;
|
|
|
|
|
using Connected.ServiceModel.Transactions;
|
|
|
|
|
using Microsoft.AspNetCore.Http;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
|
|
|
|
|
|
namespace Connected.Rest;
|
|
|
|
|
|
|
|
|
|
internal sealed class ApiServiceRequestDelegate : IDisposable
|
|
|
|
|
{
|
|
|
|
|
private ApiFormatter _formatter = null;
|
|
|
|
|
private IContext _context;
|
|
|
|
|
|
|
|
|
|
public ApiServiceRequestDelegate(HttpContext httpContext)
|
|
|
|
|
{
|
|
|
|
|
HttpContext = httpContext;
|
|
|
|
|
_context = httpContext.RequestServices.GetService<IContextProvider>().Create();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool IsDisposed { get; set; }
|
|
|
|
|
private HttpContext HttpContext { get; }
|
|
|
|
|
private IContext Context => _context;
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// This method invokes the Api Service method with the request parameters.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>Result ot the Api method or nothing is the methods return type is void.</returns>
|
|
|
|
|
public async Task InvokeAsync()
|
|
|
|
|
{
|
|
|
|
|
/*
|
|
|
|
|
* First, try to get appropriate method (target) from the resolution service.
|
|
|
|
|
* Methods must be defined with interface which have ApiServie attribute
|
|
|
|
|
*/
|
|
|
|
|
if (Context.GetService<IApiResolutionService>()?.ResolveMethod(HttpContext) is not ApiInvokeDescriptor descriptor)
|
|
|
|
|
{
|
|
|
|
|
await RenderError(StatusCodes.Status404NotFound);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
/*
|
|
|
|
|
* Event if the method is found we must validate if it is defined for the current Http method.
|
|
|
|
|
*/
|
|
|
|
|
if (!await ValidateVerb(descriptor))
|
|
|
|
|
return;
|
|
|
|
|
/*
|
|
|
|
|
* Now map request arguments with the method's one.
|
|
|
|
|
*/
|
|
|
|
|
var arguments = await MapArgumentsAsync(descriptor.Method);
|
|
|
|
|
/*
|
|
|
|
|
* And instantiate the Scoped service from the DI.
|
|
|
|
|
*/
|
|
|
|
|
var service = Context.GetService(descriptor.Service);
|
|
|
|
|
/*
|
|
|
|
|
* Invoking the method with parsed arguments and rendering results with the formatter
|
|
|
|
|
* specified in the request content type (probably Json).
|
|
|
|
|
*/
|
|
|
|
|
var result = await Methods.InvokeAsync(descriptor.Method, service, arguments?.ToArray());
|
|
|
|
|
/*
|
|
|
|
|
* Now, commit changes made in the context.
|
|
|
|
|
*/
|
|
|
|
|
if (Context.GetService<ITransactionContext>() is ITransactionContext transaction)
|
|
|
|
|
await transaction.Commit();
|
|
|
|
|
/*
|
|
|
|
|
* Send result to the client.
|
|
|
|
|
*/
|
|
|
|
|
await RenderResult(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task RenderError(int statusCode)
|
|
|
|
|
{
|
|
|
|
|
await Formatter.RenderError(statusCode, null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task RenderResult(object content)
|
|
|
|
|
{
|
|
|
|
|
await Formatter.RenderResult(content);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<bool> ValidateVerb(ApiInvokeDescriptor descriptor)
|
|
|
|
|
{
|
|
|
|
|
if (string.Equals(HttpContext.Request.Method, HttpMethods.Get, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
if ((descriptor.Verbs & ServiceMethodVerbs.Get) != ServiceMethodVerbs.Get)
|
|
|
|
|
{
|
|
|
|
|
await RenderError(StatusCodes.Status405MethodNotAllowed);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (string.Equals(HttpContext.Request.Method, HttpMethods.Post, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
if ((descriptor.Verbs & ServiceMethodVerbs.Post) != ServiceMethodVerbs.Post)
|
|
|
|
|
{
|
|
|
|
|
await RenderError(StatusCodes.Status405MethodNotAllowed);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (string.Equals(HttpContext.Request.Method, HttpMethods.Put, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
if ((descriptor.Verbs & ServiceMethodVerbs.Put) != ServiceMethodVerbs.Put)
|
|
|
|
|
{
|
|
|
|
|
await RenderError(StatusCodes.Status405MethodNotAllowed);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (string.Equals(HttpContext.Request.Method, HttpMethods.Delete, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
if ((descriptor.Verbs & ServiceMethodVerbs.Delete) != ServiceMethodVerbs.Delete)
|
|
|
|
|
{
|
|
|
|
|
await RenderError(StatusCodes.Status405MethodNotAllowed);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (string.Equals(HttpContext.Request.Method, HttpMethods.Patch, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
if ((descriptor.Verbs & ServiceMethodVerbs.Patch) != ServiceMethodVerbs.Patch)
|
|
|
|
|
{
|
|
|
|
|
await RenderError(StatusCodes.Status405MethodNotAllowed);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (string.Equals(HttpContext.Request.Method, HttpMethods.Options, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
if ((descriptor.Verbs & ServiceMethodVerbs.Options) != ServiceMethodVerbs.Options)
|
|
|
|
|
{
|
|
|
|
|
await RenderError(StatusCodes.Status405MethodNotAllowed);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// This method maps request arguments to method arguments.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="method">The <see cref="MethodInfo"/> to which arguments will be mapped.</param>
|
|
|
|
|
/// <returns>List of method's arguments needed to successfully invoke a method.</returns>
|
|
|
|
|
/// <exception cref="SysException">Thrown if a method argument is interface but no <see cref="ArgsBindingAttribute{T}"/> is present.</exception>
|
|
|
|
|
private async Task<List<object>> MapArgumentsAsync(MethodInfo method)
|
|
|
|
|
{
|
|
|
|
|
var arguments = new List<object>();
|
|
|
|
|
var requestParams = await ParseArgumentsAsync();
|
|
|
|
|
/*
|
|
|
|
|
* Look for all method parameters. Note that this is already an implementation method not the interface one.
|
|
|
|
|
*/
|
|
|
|
|
foreach (var parameter in method.GetParameters())
|
|
|
|
|
{
|
|
|
|
|
/*
|
|
|
|
|
* Most Api methods will have only one parameter which inherits from IDto.
|
|
|
|
|
*/
|
|
|
|
|
if (parameter.ParameterType.GetInterface(typeof(IDto).FullName) is not null && parameter.ParameterType.IsInterface)
|
|
|
|
|
{
|
|
|
|
|
/*
|
|
|
|
|
* If it's an interface type parameter it must be one of the following:
|
|
|
|
|
* - if it has an ArgsBindingAttribute<> we will create instance from its definition
|
|
|
|
|
* - we'll look into ApiDiscovery and try to match the implementation class automatically
|
|
|
|
|
*/
|
|
|
|
|
var attribute = parameter.GetCustomAttribute(typeof(ArgsBindingAttribute<>));
|
|
|
|
|
|
|
|
|
|
if (attribute is not null)
|
|
|
|
|
{
|
|
|
|
|
/*
|
|
|
|
|
* There is a type defined in an attribute which we need.
|
|
|
|
|
*/
|
|
|
|
|
var genericArguments = attribute.GetType().GetGenericArguments();
|
|
|
|
|
var argument = Context.GetService(genericArguments[0]);
|
|
|
|
|
/*
|
|
|
|
|
* Merge request properties into argument instance.
|
|
|
|
|
*/
|
|
|
|
|
Serializer.Merge(requestParams, argument);
|
|
|
|
|
|
|
|
|
|
arguments.Add(argument);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (Context.GetService<IApiResolutionService>()?.ResolveArgument(parameter) is Type resolvedType)
|
|
|
|
|
{
|
|
|
|
|
var argument = Context.GetService(resolvedType);
|
|
|
|
|
/*
|
|
|
|
|
* Merge request properties into argument instance.
|
|
|
|
|
*/
|
|
|
|
|
Serializer.Merge(argument, requestParams);
|
|
|
|
|
|
|
|
|
|
arguments.Add(argument);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
throw new SysException(this, $"{SR.ErrBindingAttributeMissing} ({method.DeclaringType.FullName}.{method.Name})");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
/*
|
|
|
|
|
* It's not an IDto, we are currently supporting only types from DI.
|
|
|
|
|
* We are going to support binding to
|
|
|
|
|
*/
|
|
|
|
|
if (parameter.ParameterType.IsTypePrimitive())
|
|
|
|
|
arguments.Add(ResolvePrimitiveArgument(parameter, requestParams));
|
|
|
|
|
else if (Context.GetService(parameter.ParameterType) is object argument)
|
|
|
|
|
{
|
|
|
|
|
Serializer.Merge(argument, requestParams);
|
|
|
|
|
arguments.Add(argument);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
await RenderError(StatusCodes.Status400BadRequest);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return arguments;
|
|
|
|
|
}
|
|
|
|
|
private static object? ResolvePrimitiveArgument(ParameterInfo parameter, JsonNode? requestParams)
|
|
|
|
|
{
|
|
|
|
|
if (requestParams is JsonObject jobject)
|
|
|
|
|
{
|
|
|
|
|
if (jobject.ContainsKey(parameter.Name))
|
|
|
|
|
{
|
|
|
|
|
var value = (object)jobject[parameter.Name].AsValue();
|
|
|
|
|
|
|
|
|
|
if (value is not null && TypeConversion.TryConvert(value, out object result, parameter.ParameterType))
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (requestParams is JsonValue value)
|
|
|
|
|
{
|
|
|
|
|
var val = (object)value[parameter.Name].AsValue();
|
|
|
|
|
|
|
|
|
|
if (val is not null && TypeConversion.TryConvert(val, out object result, parameter.ParameterType))
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// This method parses Request arguments into JsonNode.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>A JsonNode representing request parameters.</returns>
|
|
|
|
|
private async Task<JsonNode?> ParseArgumentsAsync()
|
|
|
|
|
{
|
|
|
|
|
var method = HttpContext.Request.Method;
|
|
|
|
|
/*
|
|
|
|
|
* Post, Delete, Put and Patch methods have parameters in the request body, let formatter do the work.
|
|
|
|
|
*/
|
|
|
|
|
if (method.Equals(HttpMethods.Post, StringComparison.OrdinalIgnoreCase) || method.Equals(HttpMethods.Delete, StringComparison.Ordinal)
|
|
|
|
|
|| method.Equals(HttpMethods.Put, StringComparison.OrdinalIgnoreCase) || method.Equals(HttpMethods.Patch, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
return await Formatter.ParseArguments();
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
/*
|
|
|
|
|
* For Get, Options and Trace use query string
|
|
|
|
|
*/
|
|
|
|
|
var r = new JsonObject();
|
|
|
|
|
|
|
|
|
|
foreach (var i in HttpContext.Request.Query.Keys)
|
|
|
|
|
r.Add(i, HttpContext.Request.Query[i].ToString());
|
|
|
|
|
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private ApiFormatter Formatter
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
if (_formatter is null)
|
|
|
|
|
{
|
|
|
|
|
var contentType = HttpContext.Request.ContentType;
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(contentType))
|
|
|
|
|
_formatter = new JsonFormatter();
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (contentType.Contains(';'))
|
|
|
|
|
contentType = contentType.Split(';')[0].Trim();
|
|
|
|
|
|
|
|
|
|
if (string.Compare(contentType, JsonFormatter.ContentType, true) == 0)
|
|
|
|
|
_formatter = new JsonFormatter();
|
|
|
|
|
else if (string.Compare(contentType, FormFormatter.ContentType, true) == 0)
|
|
|
|
|
_formatter = new FormFormatter();
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
|
|
|
|
|
|
|
|
|
|
throw new SysException(this, $"{SR.ErrContentTypeNotSupported} ({contentType})");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_formatter.Context = HttpContext;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return _formatter;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Dispose(bool disposing)
|
|
|
|
|
{
|
|
|
|
|
if (!IsDisposed)
|
|
|
|
|
{
|
|
|
|
|
if (disposing)
|
|
|
|
|
{
|
|
|
|
|
if (_context is not null)
|
|
|
|
|
{
|
|
|
|
|
_context.Dispose();
|
|
|
|
|
_context = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
IsDisposed = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
Dispose(true);
|
|
|
|
|
GC.SuppressFinalize(this);
|
|
|
|
|
}
|
|
|
|
|
}
|