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().Create(); } private bool IsDisposed { get; set; } private HttpContext HttpContext { get; } private IContext Context => _context; /// /// This method invokes the Api Service method with the request parameters. /// /// Result ot the Api method or nothing is the methods return type is void. 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()?.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() 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 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; } /// /// This method maps request arguments to method arguments. /// /// The to which arguments will be mapped. /// List of method's arguments needed to successfully invoke a method. /// Thrown if a method argument is interface but no is present. private async Task> MapArgumentsAsync(MethodInfo method) { var arguments = new List(); 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()?.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; } /// /// This method parses Request arguments into JsonNode. /// /// A JsonNode representing request parameters. private async Task 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); } }