From 0f06340580b7d6de099bffaf811e8d6565d8da75 Mon Sep 17 00:00:00 2001 From: Mario Di Vece Date: Tue, 12 Apr 2016 12:19:08 -0500 Subject: [PATCH] minor filename change --- .../RegExRoutingTest.cs | 69 ++ .../Unosquare.Labs.EmbedIO.Tests.csproj | 2 +- .../Modules/WebApiModule.cs | 686 +++++++++--------- 3 files changed, 413 insertions(+), 344 deletions(-) create mode 100644 Unosquare.Labs.EmbedIO.Tests/RegExRoutingTest.cs diff --git a/Unosquare.Labs.EmbedIO.Tests/RegExRoutingTest.cs b/Unosquare.Labs.EmbedIO.Tests/RegExRoutingTest.cs new file mode 100644 index 000000000..3e075c735 --- /dev/null +++ b/Unosquare.Labs.EmbedIO.Tests/RegExRoutingTest.cs @@ -0,0 +1,69 @@ +using System; +using System.Linq; +using System.Threading; +using NUnit.Framework; +using Unosquare.Labs.EmbedIO.Modules; +using Unosquare.Labs.EmbedIO.Tests.Properties; +using Unosquare.Labs.EmbedIO.Tests.TestObjects; + +namespace Unosquare.Labs.EmbedIO.Tests +{ + [TestFixture] + public class RegExRoutingTest + { + protected WebServer WebServer; + protected TestConsoleLog Logger = new TestConsoleLog(); + + [SetUp] + public void Init() + { + WebServer = + new WebServer(Resources.ServerAddress, Logger, RoutingStrategy.RegEx) + .WithWebApiController(); + WebServer.RunAsync(); + } + + [Test] + public void TestWebApi() + { + Assert.IsNotNull(WebServer.Module(), "WebServer has WebApiModule"); + + Assert.AreEqual(WebServer.Module().ControllersCount, 1, "WebApiModule has one controller"); + } + + [Test] + public void GetJsonDataWithRegexId() + { + TestHelper.ValidatePerson(Resources.ServerAddress + TestRegexController.RelativePath + "regex/1"); + } + + [Test] + public void GetJsonDatAsyncaWithRegexId() + { + TestHelper.ValidatePerson(Resources.ServerAddress + TestRegexController.RelativePath + "regexasync/1"); + } + + [Test] + public void GetJsonDataWithRegexDate() + { + var person = PeopleRepository.Database.First(); + TestHelper.ValidatePerson(Resources.ServerAddress + TestRegexController.RelativePath + "regexdate/" + + person.DoB.ToString("yyyy-MM-dd")); + } + + [Test] + public void GetJsonDataWithRegexWithTwoParams() + { + var person = PeopleRepository.Database.First(); + TestHelper.ValidatePerson(Resources.ServerAddress + TestRegexController.RelativePath + "regextwo/" + + person.MainSkill + "/" + person.Age); + } + + [TearDown] + public void Kill() + { + Thread.Sleep(TimeSpan.FromSeconds(1)); + WebServer.Dispose(); + } + } +} \ No newline at end of file diff --git a/Unosquare.Labs.EmbedIO.Tests/Unosquare.Labs.EmbedIO.Tests.csproj b/Unosquare.Labs.EmbedIO.Tests/Unosquare.Labs.EmbedIO.Tests.csproj index 8a136034e..e21399ec0 100644 --- a/Unosquare.Labs.EmbedIO.Tests/Unosquare.Labs.EmbedIO.Tests.csproj +++ b/Unosquare.Labs.EmbedIO.Tests/Unosquare.Labs.EmbedIO.Tests.csproj @@ -56,7 +56,7 @@ True Resources.resx - + diff --git a/Unosquare.Labs.EmbedIO/Modules/WebApiModule.cs b/Unosquare.Labs.EmbedIO/Modules/WebApiModule.cs index 0d64604d0..5df8048cc 100644 --- a/Unosquare.Labs.EmbedIO/Modules/WebApiModule.cs +++ b/Unosquare.Labs.EmbedIO/Modules/WebApiModule.cs @@ -1,131 +1,131 @@ -namespace Unosquare.Labs.EmbedIO.Modules -{ - using System.Net; - using System.Text.RegularExpressions; - using EmbedIO; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - using System.Threading.Tasks; - - /// - /// A very simple module to register class methods as handlers. - /// Public instance methods that match the WebServerModule.ResponseHandler signature, and have the WebApi handler attribute - /// will be used to respond to web server requests - /// - public class WebApiModule : WebModuleBase +namespace Unosquare.Labs.EmbedIO.Modules +{ + using System.Net; + using System.Text.RegularExpressions; + using EmbedIO; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using System.Threading.Tasks; + + /// + /// A very simple module to register class methods as handlers. + /// Public instance methods that match the WebServerModule.ResponseHandler signature, and have the WebApi handler attribute + /// will be used to respond to web server requests + /// + public class WebApiModule : WebModuleBase { - #region Inmutable Declarations - - private readonly List ControllerTypes = new List(); - - private readonly Dictionary, MethodInfo>>> DelegateMap + #region Inmutable Declarations + + private readonly List ControllerTypes = new List(); + + private readonly Dictionary, MethodInfo>>> DelegateMap = new Dictionary, MethodInfo>>>(StringComparer.InvariantCultureIgnoreCase); private static readonly Regex RouteParamRegEx = new Regex(@"\{[^\/]*\}", RegexOptions.IgnoreCase | RegexOptions.Compiled); private const string RegExRouteReplace = "(.*)"; - #endregion - - /// - /// Initializes a new instance of the class. - /// - public WebApiModule() - : base() - { - this.AddHandler(ModuleMap.AnyPath, HttpVerbs.Any, (server, context) => - { - var verb = context.RequestVerb(); - var regExRouteParams = new Dictionary(); - var path = server.RoutingStrategy == RoutingStrategy.Wildcard - ? NormalizeWildcardPath(verb, context) - : NormalizeRegExPath(verb, context, regExRouteParams); - - // return a non-math if no handler hold the route - if (path == null) return false; - - var methodPair = DelegateMap[path][verb]; - var controller = methodPair.Item1(); - - // ensure module does not retun cached responses + #endregion + + /// + /// Initializes a new instance of the class. + /// + public WebApiModule() + : base() + { + this.AddHandler(ModuleMap.AnyPath, HttpVerbs.Any, (server, context) => + { + var verb = context.RequestVerb(); + var regExRouteParams = new Dictionary(); + var path = server.RoutingStrategy == RoutingStrategy.Wildcard + ? NormalizeWildcardPath(verb, context) + : NormalizeRegExPath(verb, context, regExRouteParams); + + // return a non-math if no handler hold the route + if (path == null) return false; + + var methodPair = DelegateMap[path][verb]; + var controller = methodPair.Item1(); + + // ensure module does not retun cached responses context.NoCache(); // Log the handler to be used - server.Log.DebugFormat("Handler: {0}.{1}", methodPair.Item2.DeclaringType.FullName, methodPair.Item2.Name); - - // Select the routing strategy - if (server.RoutingStrategy == RoutingStrategy.RegEx) - { - // Initially, only the server and context objects will be available - var args = new List() { server, context }; - - // Parse the arguments to their intended type skipping the first two. - foreach (var arg in methodPair.Item2.GetParameters().Skip(2)) - { - if (regExRouteParams.ContainsKey(arg.Name) == false) continue; - // get a reference to the parse method - var parseMethod = arg.ParameterType.GetMethod(nameof(int.Parse), new[] { typeof(string) }); - - // add the parsed argument to the argument list if available - args.Add(parseMethod != null ? - parseMethod.Invoke(null, new[] { regExRouteParams[arg.Name] }) : - regExRouteParams[arg.Name]); - } - - // Now, check if the call is handled asynchronously. - if (methodPair.Item2.ReturnType == typeof(Task)) - { - // Run the method asynchronously - var returnValue = Task.Run(async () => - { - var task = await (Task)methodPair.Item2.Invoke(controller, args.ToArray()); - return task; - }); - - return returnValue.Result; - } - else - { - // If the handler is not asynchronous, simply call the method. - var returnValue = (bool)methodPair.Item2.Invoke(controller, args.ToArray()); - return returnValue; - } - } - else if (server.RoutingStrategy == RoutingStrategy.Wildcard) - { - if (methodPair.Item2.ReturnType == typeof(Task)) - { - // Asynchronous handling of wildcard matching strategy - var method = Delegate.CreateDelegate(typeof(AsyncResponseHandler), controller, methodPair.Item2); - var returnValue = Task.Run(async () => - { - var task = await (Task)method.DynamicInvoke(server, context); - return task; - }); - - return returnValue.Result; - } - else - { - // Regular handling of wildcard matching strategy - var method = Delegate.CreateDelegate(typeof(ResponseHandler), controller, methodPair.Item2); - var returnValue = (bool)method.DynamicInvoke(server, context); - return returnValue; - } - } + server.Log.DebugFormat("Handler: {0}.{1}", methodPair.Item2.DeclaringType.FullName, methodPair.Item2.Name); + + // Select the routing strategy + if (server.RoutingStrategy == RoutingStrategy.RegEx) + { + // Initially, only the server and context objects will be available + var args = new List() { server, context }; + + // Parse the arguments to their intended type skipping the first two. + foreach (var arg in methodPair.Item2.GetParameters().Skip(2)) + { + if (regExRouteParams.ContainsKey(arg.Name) == false) continue; + // get a reference to the parse method + var parseMethod = arg.ParameterType.GetMethod(nameof(int.Parse), new[] { typeof(string) }); + + // add the parsed argument to the argument list if available + args.Add(parseMethod != null ? + parseMethod.Invoke(null, new[] { regExRouteParams[arg.Name] }) : + regExRouteParams[arg.Name]); + } + + // Now, check if the call is handled asynchronously. + if (methodPair.Item2.ReturnType == typeof(Task)) + { + // Run the method asynchronously + var returnValue = Task.Run(async () => + { + var task = await (Task)methodPair.Item2.Invoke(controller, args.ToArray()); + return task; + }); + + return returnValue.Result; + } + else + { + // If the handler is not asynchronous, simply call the method. + var returnValue = (bool)methodPair.Item2.Invoke(controller, args.ToArray()); + return returnValue; + } + } + else if (server.RoutingStrategy == RoutingStrategy.Wildcard) + { + if (methodPair.Item2.ReturnType == typeof(Task)) + { + // Asynchronous handling of wildcard matching strategy + var method = Delegate.CreateDelegate(typeof(AsyncResponseHandler), controller, methodPair.Item2); + var returnValue = Task.Run(async () => + { + var task = await (Task)method.DynamicInvoke(server, context); + return task; + }); + + return returnValue.Result; + } + else + { + // Regular handling of wildcard matching strategy + var method = Delegate.CreateDelegate(typeof(ResponseHandler), controller, methodPair.Item2); + var returnValue = (bool)method.DynamicInvoke(server, context); + return returnValue; + } + } else { // Log the handler to be used server.Log.WarnFormat($"Routing strategy '{server.RoutingStrategy}' is not supported by this module."); return false; - } - }); - } - - - + } + }); + } + + + /// /// Normalizes a path meant for RegEx matching, extracts the route parameters, and returns the registered /// path in the internal delegate map. @@ -134,32 +134,32 @@ public WebApiModule() /// The context. /// The route parameters. /// - private string NormalizeRegExPath(HttpVerbs verb, HttpListenerContext context, - Dictionary routeParams) - { - var path = context.RequestPath(); - - foreach (var route in DelegateMap.Keys) - { - var regex = new Regex(RouteParamRegEx.Replace(route, RegExRouteReplace)); - var match = regex.Match(path); - - if (!match.Success || !DelegateMap[route].Keys.Contains(verb)) continue; - - var pathParts = route.Split('/'); - var i = 1; // match group index - - foreach (var pathPart in pathParts.Where(x => x.StartsWith("{"))) - { - routeParams.Add(pathPart.Replace("{", "").Replace("}", ""), match.Groups[i++].Value); - } - - return route; - } - - return null; - } - + private string NormalizeRegExPath(HttpVerbs verb, HttpListenerContext context, + Dictionary routeParams) + { + var path = context.RequestPath(); + + foreach (var route in DelegateMap.Keys) + { + var regex = new Regex(RouteParamRegEx.Replace(route, RegExRouteReplace)); + var match = regex.Match(path); + + if (!match.Success || !DelegateMap[route].Keys.Contains(verb)) continue; + + var pathParts = route.Split('/'); + var i = 1; // match group index + + foreach (var pathPart in pathParts.Where(x => x.StartsWith("{"))) + { + routeParams.Add(pathPart.Replace("{", "").Replace("}", ""), match.Groups[i++].Value); + } + + return route; + } + + return null; + } + /// /// Normalizes a URL request path meant for Wildcard matching and returns the registered /// path in the internal delegate map. @@ -167,210 +167,210 @@ private string NormalizeRegExPath(HttpVerbs verb, HttpListenerContext context, /// The verb. /// The context. /// - private string NormalizeWildcardPath(HttpVerbs verb, HttpListenerContext context) - { - var path = context.RequestPath(); - - var wildcardPaths = DelegateMap.Keys - .Where(k => k.Contains("/" + ModuleMap.AnyPath)) - .Select(s => s.ToLowerInvariant()) - .ToArray(); - - var wildcardMatch = wildcardPaths.FirstOrDefault(p => // wildcard at the end + private string NormalizeWildcardPath(HttpVerbs verb, HttpListenerContext context) + { + var path = context.RequestPath(); + + var wildcardPaths = DelegateMap.Keys + .Where(k => k.Contains("/" + ModuleMap.AnyPath)) + .Select(s => s.ToLowerInvariant()) + .ToArray(); + + var wildcardMatch = wildcardPaths.FirstOrDefault(p => // wildcard at the end path.StartsWith(p.Substring(0, p.Length - ModuleMap.AnyPath.Length)) // wildcard in the middle so check both start/end - || (path.StartsWith(p.Substring(0, p.IndexOf(ModuleMap.AnyPath, StringComparison.Ordinal))) - && path.EndsWith(p.Substring(p.IndexOf(ModuleMap.AnyPath, StringComparison.Ordinal) + 1))) - ); - - if (string.IsNullOrWhiteSpace(wildcardMatch) == false) - path = wildcardMatch; - - if (DelegateMap.ContainsKey(path) == false) - return null; - - if (DelegateMap[path].ContainsKey(verb) == false) // TODO: Fix Any Verb - { - var originalPath = context.RequestPath(); - if (DelegateMap.ContainsKey(originalPath) && - DelegateMap[originalPath].ContainsKey(verb)) - { - path = originalPath; - } - else - return null; - } - - return path; - } - - /// - /// Gets the name of this module. - /// - /// - /// The name. - /// - public override string Name => "Web API Module"; - - /// - /// Gets the number of controller objects registered in this API - /// - public int ControllersCount => ControllerTypes.Count; - - /// - /// Registers the controller. - /// - /// - /// Controller types must be unique within the module - public void RegisterController() - where T : WebApiController, new() - { - if (ControllerTypes.Contains(typeof(T))) - throw new ArgumentException("Controller types must be unique within the module"); - - RegisterController(typeof(T)); - } - - /// - /// Registers the controller. - /// - /// - /// - /// Controller types must be unique within the module - public void RegisterController(Func controllerFactory) - where T : WebApiController - { - if (ControllerTypes.Contains(typeof(T))) - throw new ArgumentException("Controller types must be unique within the module"); - - RegisterController(typeof(T), controllerFactory); - } - - /// - /// Registers the controller. - /// - /// Type of the controller. - public void RegisterController(Type controllerType) - { - Func controllerFactory = () => Activator.CreateInstance(controllerType); - this.RegisterController(controllerType, controllerFactory); - } - - /// - /// Registers the controller. - /// - /// Type of the controller. - /// The controller factory method. - public void RegisterController(Type controllerType, Func controllerFactory) - { - var protoDelegate = new ResponseHandler((server, context) => true); - var protoAsyncDelegate = new AsyncResponseHandler((server, context) => Task.FromResult(true)); - - var methods = controllerType - .GetMethods(BindingFlags.Instance | BindingFlags.Public) - .Where( - m => (m.ReturnType == protoDelegate.Method.ReturnType - || m.ReturnType == protoAsyncDelegate.Method.ReturnType) - && m.GetParameters() - .Select(pi => pi.ParameterType) - .Take(2) - .SequenceEqual(protoDelegate.Method.GetParameters() - .Select(pi => pi.ParameterType))); - - foreach (var method in methods) - { - var attribute = - method.GetCustomAttributes(typeof(WebApiHandlerAttribute), true).FirstOrDefault() as - WebApiHandlerAttribute; - if (attribute == null) continue; - - foreach (var path in attribute.Paths) - { - var delegatePath = new Dictionary, MethodInfo>>(); - - if (DelegateMap.ContainsKey(path)) - delegatePath = DelegateMap[path]; // update - else - DelegateMap.Add(path, delegatePath); // add - - var delegatePair = new Tuple, MethodInfo>(controllerFactory, method); - if (DelegateMap[path].ContainsKey(attribute.Verb)) - DelegateMap[path][attribute.Verb] = delegatePair; // update - else - DelegateMap[path].Add(attribute.Verb, delegatePair); // add - } - } - - ControllerTypes.Add(controllerType); - } - } - - /// - /// Decorate methods within controllers with this attribute in order to make them callable from the Web API Module - /// Method Must match the WebServerModule. - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class WebApiHandlerAttribute : Attribute - { - /// - /// Initializes a new instance of the class. - /// - /// The verb. - /// The paths. - /// The argument 'paths' must be specified. - public WebApiHandlerAttribute(HttpVerbs verb, string[] paths) - { - if (paths == null || paths.Length == 0) - throw new ArgumentException("The argument 'paths' must be specified."); - - this.Verb = verb; - this.Paths = paths; - } - - /// - /// Initializes a new instance of the class. - /// - /// The verb. - /// The path. - /// The argument 'path' must be specified. - public WebApiHandlerAttribute(HttpVerbs verb, string path) - { - if (string.IsNullOrWhiteSpace(path)) - throw new ArgumentException("The argument 'path' must be specified."); - - this.Verb = verb; - this.Paths = new string[] { path }; - } - - /// - /// Gets or sets the verb. - /// - /// - /// The verb. - /// - public HttpVerbs Verb { get; protected set; } - - /// - /// Gets or sets the paths. - /// - /// - /// The paths. - /// - public string[] Paths { get; protected set; } - } - - /// - /// Inherit from this class and define your own Web API methods - /// You must RegisterController in the Web API Module to make it active - /// - public abstract class WebApiController - { - /// - /// Initializes a new instance of the class. - /// - public WebApiController() - { - // placeholder - } - } + || (path.StartsWith(p.Substring(0, p.IndexOf(ModuleMap.AnyPath, StringComparison.Ordinal))) + && path.EndsWith(p.Substring(p.IndexOf(ModuleMap.AnyPath, StringComparison.Ordinal) + 1))) + ); + + if (string.IsNullOrWhiteSpace(wildcardMatch) == false) + path = wildcardMatch; + + if (DelegateMap.ContainsKey(path) == false) + return null; + + if (DelegateMap[path].ContainsKey(verb) == false) // TODO: Fix Any Verb + { + var originalPath = context.RequestPath(); + if (DelegateMap.ContainsKey(originalPath) && + DelegateMap[originalPath].ContainsKey(verb)) + { + path = originalPath; + } + else + return null; + } + + return path; + } + + /// + /// Gets the name of this module. + /// + /// + /// The name. + /// + public override string Name => "Web API Module"; + + /// + /// Gets the number of controller objects registered in this API + /// + public int ControllersCount => ControllerTypes.Count; + + /// + /// Registers the controller. + /// + /// + /// Controller types must be unique within the module + public void RegisterController() + where T : WebApiController, new() + { + if (ControllerTypes.Contains(typeof(T))) + throw new ArgumentException("Controller types must be unique within the module"); + + RegisterController(typeof(T)); + } + + /// + /// Registers the controller. + /// + /// + /// + /// Controller types must be unique within the module + public void RegisterController(Func controllerFactory) + where T : WebApiController + { + if (ControllerTypes.Contains(typeof(T))) + throw new ArgumentException("Controller types must be unique within the module"); + + RegisterController(typeof(T), controllerFactory); + } + + /// + /// Registers the controller. + /// + /// Type of the controller. + public void RegisterController(Type controllerType) + { + Func controllerFactory = () => Activator.CreateInstance(controllerType); + this.RegisterController(controllerType, controllerFactory); + } + + /// + /// Registers the controller. + /// + /// Type of the controller. + /// The controller factory method. + public void RegisterController(Type controllerType, Func controllerFactory) + { + var protoDelegate = new ResponseHandler((server, context) => true); + var protoAsyncDelegate = new AsyncResponseHandler((server, context) => Task.FromResult(true)); + + var methods = controllerType + .GetMethods(BindingFlags.Instance | BindingFlags.Public) + .Where( + m => (m.ReturnType == protoDelegate.Method.ReturnType + || m.ReturnType == protoAsyncDelegate.Method.ReturnType) + && m.GetParameters() + .Select(pi => pi.ParameterType) + .Take(2) + .SequenceEqual(protoDelegate.Method.GetParameters() + .Select(pi => pi.ParameterType))); + + foreach (var method in methods) + { + var attribute = + method.GetCustomAttributes(typeof(WebApiHandlerAttribute), true).FirstOrDefault() as + WebApiHandlerAttribute; + if (attribute == null) continue; + + foreach (var path in attribute.Paths) + { + var delegatePath = new Dictionary, MethodInfo>>(); + + if (DelegateMap.ContainsKey(path)) + delegatePath = DelegateMap[path]; // update + else + DelegateMap.Add(path, delegatePath); // add + + var delegatePair = new Tuple, MethodInfo>(controllerFactory, method); + if (DelegateMap[path].ContainsKey(attribute.Verb)) + DelegateMap[path][attribute.Verb] = delegatePair; // update + else + DelegateMap[path].Add(attribute.Verb, delegatePair); // add + } + } + + ControllerTypes.Add(controllerType); + } + } + + /// + /// Decorate methods within controllers with this attribute in order to make them callable from the Web API Module + /// Method Must match the WebServerModule. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class WebApiHandlerAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The verb. + /// The paths. + /// The argument 'paths' must be specified. + public WebApiHandlerAttribute(HttpVerbs verb, string[] paths) + { + if (paths == null || paths.Length == 0) + throw new ArgumentException("The argument 'paths' must be specified."); + + this.Verb = verb; + this.Paths = paths; + } + + /// + /// Initializes a new instance of the class. + /// + /// The verb. + /// The path. + /// The argument 'path' must be specified. + public WebApiHandlerAttribute(HttpVerbs verb, string path) + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException("The argument 'path' must be specified."); + + this.Verb = verb; + this.Paths = new string[] { path }; + } + + /// + /// Gets or sets the verb. + /// + /// + /// The verb. + /// + public HttpVerbs Verb { get; protected set; } + + /// + /// Gets or sets the paths. + /// + /// + /// The paths. + /// + public string[] Paths { get; protected set; } + } + + /// + /// Inherit from this class and define your own Web API methods + /// You must RegisterController in the Web API Module to make it active + /// + public abstract class WebApiController + { + /// + /// Initializes a new instance of the class. + /// + public WebApiController() + { + // placeholder + } + } } \ No newline at end of file