diff --git a/README.md b/README.md index 1e4551421..07e983d27 100644 --- a/README.md +++ b/README.md @@ -162,25 +162,25 @@ namespace Company.Project REST API Example: ----------------- -The WebApi module supports two routing strategies: Wildcard and RegEx. By default, and in order to maintain backwards compatibility, the WebApi module will use the **Wildcard Routing Strategy** and match routes using the asterisk `*` character in the route. **For example:** +The WebApi module supports two routing strategies: Wildcard and Regex. By default, and in order to maintain backwards compatibility, the WebApi module will use the **Wildcard Routing Strategy** and match routes using the asterisk `*` character in the route. **For example:** - The route `/api/people/*` will match any request with a URL starting with the two first URL segments `api` and `people` and ending with anything. The route `/api/people/hello` will be matched. - You can also use wildcards in the middle of the route. The route `/api/people/*/details` will match requests starting with the two first URL segments `api` and `people`, and ending with a `details` segment. The route `/api/people/hello/details` will be matched. -*Note that most REST services can be designed with this simpler Wildcard routing startegy. However, the RegEx matching strategy is the current recommended approach as we might be deprecating the Wildcard strategy altogether* +*Note that most REST services can be designed with this simpler Wildcard routing startegy. However, the Regex matching strategy is the current recommended approach as we might be deprecating the Wildcard strategy altogether* -On the other hand, the **RegEx Routing Strategy** will try to match and resolve the values from a route template, in a similar fashion to Microsoft's Web API 2. A method with the following route `/api/people/{id}` is going to match any request URL with three segments: the first two `api` and `people` and the last one is going to be parsed or converted to the type in the `id` argument of the handling method signature. Please read on if this was confusing as it is much simpler than it sounds. Additionally, you can put multiple values to match, for example `/api/people/{mainSkill}/{age}`, and receive the parsed values from the URL straight into the arguments of your handler method. +On the other hand, the **Regex Routing Strategy** will try to match and resolve the values from a route template, in a similar fashion to Microsoft's Web API 2. A method with the following route `/api/people/{id}` is going to match any request URL with three segments: the first two `api` and `people` and the last one is going to be parsed or converted to the type in the `id` argument of the handling method signature. Please read on if this was confusing as it is much simpler than it sounds. Additionally, you can put multiple values to match, for example `/api/people/{mainSkill}/{age}`, and receive the parsed values from the URL straight into the arguments of your handler method. *During server setup:* ```csharp -// The routing strategy is Wildcard by default, but you can change it to RegEx as follows: -var server = new WebServer("http://localhost:9696/", new NullLog(), RoutingStrategy.RegEx); +// The routing strategy is Wildcard by default, but you can change it to Regex as follows: +var server = new WebServer("http://localhost:9696/", new NullLog(), RoutingStrategy.Regex); server.RegisterModule(new WebApiModule()); server.Module().RegisterController(); ``` -*And our controller class (using RegEx Strategy) looks like:* +*And our controller class (using Regex Strategy) looks like:* ```csharp public class PeopleController : WebApiController diff --git a/Unosquare.Labs.EmbedIO.Samples/Unosquare.Labs.EmbedIO.Samples.csproj b/Unosquare.Labs.EmbedIO.Samples/Unosquare.Labs.EmbedIO.Samples.csproj index b42f35f71..c6a60654f 100644 --- a/Unosquare.Labs.EmbedIO.Samples/Unosquare.Labs.EmbedIO.Samples.csproj +++ b/Unosquare.Labs.EmbedIO.Samples/Unosquare.Labs.EmbedIO.Samples.csproj @@ -51,7 +51,7 @@ True - ..\packages\Tubular.ServerSide.0.9.42\lib\net45\Unosquare.Tubular.dll + ..\packages\Tubular.ServerSide.0.9.47\lib\net45\Unosquare.Tubular.dll True diff --git a/Unosquare.Labs.EmbedIO.Samples/packages.config b/Unosquare.Labs.EmbedIO.Samples/packages.config index 83a4bb056..b52e79590 100644 --- a/Unosquare.Labs.EmbedIO.Samples/packages.config +++ b/Unosquare.Labs.EmbedIO.Samples/packages.config @@ -4,5 +4,5 @@ - + \ No newline at end of file diff --git a/Unosquare.Labs.EmbedIO.Tests/RegExRoutingTest.cs b/Unosquare.Labs.EmbedIO.Tests/RegExRoutingTest.cs index 3e075c735..367339ef5 100644 --- a/Unosquare.Labs.EmbedIO.Tests/RegExRoutingTest.cs +++ b/Unosquare.Labs.EmbedIO.Tests/RegExRoutingTest.cs @@ -9,7 +9,7 @@ namespace Unosquare.Labs.EmbedIO.Tests { [TestFixture] - public class RegExRoutingTest + public class RegexRoutingTest { protected WebServer WebServer; protected TestConsoleLog Logger = new TestConsoleLog(); @@ -18,7 +18,7 @@ public class RegExRoutingTest public void Init() { WebServer = - new WebServer(Resources.ServerAddress, Logger, RoutingStrategy.RegEx) + new WebServer(Resources.ServerAddress, Logger, RoutingStrategy.Regex) .WithWebApiController(); WebServer.RunAsync(); } diff --git a/Unosquare.Labs.EmbedIO.Tests/Unosquare.Labs.EmbedIO.Tests.csproj b/Unosquare.Labs.EmbedIO.Tests/Unosquare.Labs.EmbedIO.Tests.csproj index e21399ec0..627565ff2 100644 --- a/Unosquare.Labs.EmbedIO.Tests/Unosquare.Labs.EmbedIO.Tests.csproj +++ b/Unosquare.Labs.EmbedIO.Tests/Unosquare.Labs.EmbedIO.Tests.csproj @@ -1,107 +1,107 @@ - - - - - Debug - AnyCPU - {2E100548-08CD-4EFD-AD08-AF584081B098} - Library - Properties - Unosquare.Labs.EmbedIO.Tests - Unosquare.Labs.EmbedIO.Tests - v4.5 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll - True - - - ..\packages\NUnit.2.6.4\lib\nunit.framework.dll - True - - - - - - - - - - - - - - - - True - True - Resources.resx - - - - - - - - - - - - - - - - - - - - - - - {7d7c29b4-9493-4ebd-8f20-6fac1e7161ee} - Unosquare.Labs.EmbedIO - - - - - Always - - - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - - - - + + + + + Debug + AnyCPU + {2E100548-08CD-4EFD-AD08-AF584081B098} + Library + Properties + Unosquare.Labs.EmbedIO.Tests + Unosquare.Labs.EmbedIO.Tests + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True + + + ..\packages\NUnit.2.6.4\lib\nunit.framework.dll + True + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + + + + + + + + + + + + + + + + + + {7d7c29b4-9493-4ebd-8f20-6fac1e7161ee} + Unosquare.Labs.EmbedIO + + + + + Always + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + + + + + --> \ No newline at end of file diff --git a/Unosquare.Labs.EmbedIO/Constants.cs b/Unosquare.Labs.EmbedIO/Constants.cs index 86979ab22..7dd87ba3c 100644 --- a/Unosquare.Labs.EmbedIO/Constants.cs +++ b/Unosquare.Labs.EmbedIO/Constants.cs @@ -736,6 +736,6 @@ public enum RoutingStrategy /// /// The Regex strategy /// - RegEx + Regex } } \ No newline at end of file diff --git a/Unosquare.Labs.EmbedIO/Modules/WebApiModule.cs b/Unosquare.Labs.EmbedIO/Modules/WebApiModule.cs index 5df8048cc..6d676ce93 100644 --- a/Unosquare.Labs.EmbedIO/Modules/WebApiModule.cs +++ b/Unosquare.Labs.EmbedIO/Modules/WebApiModule.cs @@ -1,376 +1,374 @@ -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 - = 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 - 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; - } - } - 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. - /// - /// The verb. - /// 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; - } - - /// - /// Normalizes a URL request path meant for Wildcard matching and returns the registered - /// path in the internal delegate map. - /// - /// 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 - 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 - } - } +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 + = 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 return 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; + } + } + 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. + /// + /// The verb. + /// 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; + } + + /// + /// Normalizes a URL request path meant for Wildcard matching and returns the registered + /// path in the internal delegate map. + /// + /// 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 + 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 + } + } } \ No newline at end of file diff --git a/Unosquare.Labs.EmbedIO/WebServer.cs b/Unosquare.Labs.EmbedIO/WebServer.cs index cf598a4a2..60a3d8282 100644 --- a/Unosquare.Labs.EmbedIO/WebServer.cs +++ b/Unosquare.Labs.EmbedIO/WebServer.cs @@ -66,7 +66,7 @@ public ReadOnlyCollection Modules /// /// Gets the URL RoutingStrategy used in this instance. - /// By default it is set to Wildcard, but RegEx is the the recommended value. + /// By default it is set to Wildcard, but Regex is the the recommended value. /// public RoutingStrategy RoutingStrategy { get; protected set; } @@ -533,16 +533,16 @@ public static WebServer Create(int port, ILog log = null) public static WebServer CreateWithConsole(string urlPrefix) { return new WebServer(urlPrefix, new SimpleConsoleLog()); - } - - /// - /// Static method to create a webserver instance using a simple console output. - /// This method is useful for fluent configuration. - /// - /// The URL prefix. - /// The routing strategy. - /// - /// The webserver instance. + } + + /// + /// Static method to create a webserver instance using a simple console output. + /// This method is useful for fluent configuration. + /// + /// The URL prefix. + /// The routing strategy. + /// + /// The webserver instance. /// public static WebServer CreateWithConsole(string urlPrefix, RoutingStrategy routingStrategy) {