You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Connected.Framework/Connected.Rest/Api/ApiServiceRequestDelegate.cs

320 lines
9.5 KiB

2 years ago
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);
}
}