├── swaggerexample.png ├── src ├── Controllerless.WebApi.Description │ ├── packages.config │ ├── Internal │ │ ├── Requires.cs │ │ ├── ControllerlessControllerDescriptor.cs │ │ ├── CollectionExtensions.cs │ │ └── ControllerlessParameterDescriptor.cs │ ├── ITypeDescriptionProvider.cs │ ├── CompositeApiExplorer.cs │ ├── CompositeTypeDescriptionProvider.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── ControllerlessActionDescriptor.cs │ ├── XmlDocumentationTypeDescriptionProvider.cs │ ├── Controllerless.WebApi.Description.csproj │ └── ControllerlessApiExplorer.cs ├── Controllerless.WebApi │ ├── Properties │ │ └── AssemblyInfo.cs │ └── Controllerless.WebApi.csproj └── Controllerless.WebApi.Description.sln ├── LICENSE ├── .gitignore └── README.md /swaggerexample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnetjunkie/controllerless/HEAD/swaggerexample.png -------------------------------------------------------------------------------- /src/Controllerless.WebApi.Description/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/Controllerless.WebApi.Description/Internal/Requires.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SolidServices.Controllerless.WebApi.Description 4 | { 5 | internal static class Requires 6 | { 7 | public static void IsNotNull(object instance, string paramName) 8 | { 9 | if (object.ReferenceEquals(instance, null)) 10 | { 11 | throw new ArgumentNullException(paramName); 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Controllerless.WebApi.Description/Internal/ControllerlessControllerDescriptor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.Web.Http.Controllers; 3 | 4 | namespace SolidServices.Controllerless.WebApi.Description 5 | { 6 | internal sealed class ControllerlessControllerDescriptor : HttpControllerDescriptor 7 | { 8 | // note you might provide some asp.net attributes here 9 | public override Collection GetCustomAttributes() => new Collection(); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Controllerless.WebApi.Description/Internal/CollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SolidServices.Controllerless.WebApi.Description 8 | { 9 | internal static class CollectionExtensions 10 | { 11 | public static void AddRange(this ICollection collection, IEnumerable range) 12 | { 13 | foreach (var item in range) collection.Add(item); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Controllerless.WebApi.Description/ITypeDescriptionProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SolidServices.Controllerless.WebApi.Description 4 | { 5 | /// 6 | /// Provides descriptions for requested types. 7 | /// 8 | public interface ITypeDescriptionProvider 9 | { 10 | /// Gets the type's description or null when there is no description for the given type. 11 | /// The type. 12 | /// The description of the requested type or null. 13 | string GetDescription(Type type); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Controllerless.WebApi.Description/Internal/ControllerlessParameterDescriptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Web.Http.Controllers; 3 | using System.Web.Http.Description; 4 | 5 | namespace SolidServices.Controllerless.WebApi.Description 6 | { 7 | internal sealed class ControllerlessParameterDescriptor : HttpParameterDescriptor 8 | { 9 | private readonly string parameterName; 10 | private readonly Type parameterType; 11 | 12 | // HttpActionDescriptor actionDescriptor, 13 | internal ControllerlessParameterDescriptor(string parameterName, Type parameterType) 14 | { 15 | this.parameterName = ToCamelCase(parameterName); 16 | this.parameterType = parameterType; 17 | } 18 | 19 | public override string ParameterName => this.parameterName; 20 | public override Type ParameterType => this.parameterType; 21 | 22 | private static string ToCamelCase(string name) => name.Substring(0, 1).ToLower() + name.Substring(1); 23 | } 24 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Steven 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Controllerless.WebApi.Description/CompositeApiExplorer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using System.Linq; 4 | using System.Web.Http.Description; 5 | 6 | namespace SolidServices.Controllerless.WebApi.Description 7 | { 8 | /// 9 | /// Wraps multiple instances and combines them into one collection of api descriptions. 10 | /// The results will be cached. 11 | /// 12 | public sealed class CompositeApiExplorer : IApiExplorer 13 | { 14 | private readonly Lazy> descriptions; 15 | 16 | /// Constructs a new instance of . 17 | /// 18 | public CompositeApiExplorer(params IApiExplorer[] apiExplorers) 19 | { 20 | Requires.IsNotNull(apiExplorers, nameof(apiExplorers)); 21 | 22 | this.descriptions = new Lazy>(() => 23 | new Collection(apiExplorers.SelectMany(x => x.ApiDescriptions).ToList())); 24 | } 25 | 26 | /// Gets the API descriptions. 27 | public Collection ApiDescriptions => this.descriptions.Value; 28 | } 29 | } -------------------------------------------------------------------------------- /src/Controllerless.WebApi/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Controllerless.WebApi")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Controllerless.WebApi")] 13 | [assembly: AssemblyCopyright("Copyright © 2016")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("8af38c39-4bb8-4d0d-9b3f-b6f1c19a8d08")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/Controllerless.WebApi.Description/CompositeTypeDescriptionProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SolidServices.Controllerless.WebApi.Description 4 | { 5 | /// 6 | /// Wraps a set of instances and the first found description of a requested type. 7 | /// 8 | public sealed class CompositeTypeDescriptionProvider : ITypeDescriptionProvider 9 | { 10 | private readonly ITypeDescriptionProvider[] providers; 11 | 12 | /// 13 | /// Constructs a new instance of . 14 | /// 15 | /// 16 | public CompositeTypeDescriptionProvider(params ITypeDescriptionProvider[] providers) 17 | { 18 | Requires.IsNotNull(providers, nameof(providers)); 19 | this.providers = providers; 20 | } 21 | 22 | /// Gets the type's description or null when there is no description for the given type. 23 | /// The type. 24 | /// The description of the requested type or null. 25 | public string GetDescription(Type type) 26 | { 27 | foreach (var provider in this.providers) 28 | { 29 | string description = provider.GetDescription(type); 30 | 31 | if (!string.IsNullOrEmpty(description)) 32 | { 33 | return description; 34 | } 35 | } 36 | 37 | return null; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/Controllerless.WebApi.Description.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Controllerless.WebApi.Description", "Controllerless.WebApi.Description\Controllerless.WebApi.Description.csproj", "{7325A177-DD50-4AAC-A899-59C281E532C9}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Controllerless.WebApi", "Controllerless.WebApi\Controllerless.WebApi.csproj", "{8AF38C39-4BB8-4D0D-9B3F-B6F1C19A8D08}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {7325A177-DD50-4AAC-A899-59C281E532C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {7325A177-DD50-4AAC-A899-59C281E532C9}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {7325A177-DD50-4AAC-A899-59C281E532C9}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {7325A177-DD50-4AAC-A899-59C281E532C9}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {8AF38C39-4BB8-4D0D-9B3F-B6F1C19A8D08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {8AF38C39-4BB8-4D0D-9B3F-B6F1C19A8D08}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {8AF38C39-4BB8-4D0D-9B3F-B6F1C19A8D08}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {8AF38C39-4BB8-4D0D-9B3F-B6F1C19A8D08}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /src/Controllerless.WebApi.Description/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Resources; 2 | using System.Reflection; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | 6 | // General Information about an assembly is controlled through the following 7 | // set of attributes. Change these attribute values to modify the information 8 | // associated with an assembly. 9 | [assembly: AssemblyTitle("SolidServices.Controllerless.WebApi.Description")] 10 | [assembly: AssemblyDescription("Custom ApiExplorer for genrating API documentation for message based apis.")] 11 | [assembly: AssemblyConfiguration("")] 12 | [assembly: AssemblyCompany("dotnetjunkie")] 13 | [assembly: AssemblyProduct("Controllerless.WebApi.Description")] 14 | [assembly: AssemblyCopyright("Copyright © 2016 dotnetjunkie")] 15 | [assembly: AssemblyTrademark("")] 16 | [assembly: AssemblyCulture("")] 17 | 18 | // Setting ComVisible to false makes the types in this assembly not visible 19 | // to COM components. If you need to access a type in this assembly from 20 | // COM, set the ComVisible attribute to true on that type. 21 | [assembly: ComVisible(false)] 22 | 23 | // The following GUID is for the ID of the typelib if this project is exposed to COM 24 | [assembly: Guid("7325a177-dd50-4aac-a899-59c281e532c9")] 25 | 26 | // Version information for an assembly consists of the following four values: 27 | // 28 | // Major Version 29 | // Minor Version 30 | // Build Number 31 | // Revision 32 | // 33 | // You can specify all the values or you can default the Build and Revision Numbers 34 | // by using the '*' as shown below: 35 | // [assembly: AssemblyVersion("1.0.*")] 36 | [assembly: AssemblyVersion("0.1.2.0")] 37 | [assembly: AssemblyFileVersion("0.1.2.0")] 38 | [assembly: NeutralResourcesLanguage("en-US")] 39 | 40 | -------------------------------------------------------------------------------- /src/Controllerless.WebApi/Controllerless.WebApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {8AF38C39-4BB8-4D0D-9B3F-B6F1C19A8D08} 8 | Library 9 | Properties 10 | Controllerless.WebApi 11 | Controllerless.WebApi 12 | v4.5 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 53 | -------------------------------------------------------------------------------- /src/Controllerless.WebApi.Description/ControllerlessActionDescriptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using System.Web.Http.Controllers; 8 | using System.Web.Http.Description; 9 | 10 | namespace SolidServices.Controllerless.WebApi.Description 11 | { 12 | /// 13 | /// An used by Controllerless for constructing APIs for Actions. 14 | /// 15 | public sealed class ControllerlessActionDescriptor : HttpActionDescriptor 16 | { 17 | private readonly Collection parameters; 18 | 19 | internal ControllerlessActionDescriptor(ApiDescription description, Type messageType, string actionName, 20 | Type returnType, IEnumerable parameters) 21 | { 22 | this.ApiDescription = description; 23 | this.MessageType = messageType; 24 | this.ActionName = actionName; 25 | this.ReturnType = returnType; 26 | this.parameters = new Collection(parameters.ToList()); 27 | this.parameters.ToList().ForEach(p => p.ActionDescriptor = this); 28 | } 29 | 30 | /// Gets the parent . 31 | public ApiDescription ApiDescription { get; } 32 | 33 | /// The type of the message that this action processes. 34 | public Type MessageType { get; } 35 | 36 | /// The name of the action. 37 | public override string ActionName { get; } 38 | 39 | /// Gets the return type of the descriptor.. 40 | public override Type ReturnType { get; } 41 | 42 | /// Retrieves the parameters for the action descriptor. 43 | /// The parameters for the action descriptor. 44 | public override Collection GetParameters() => this.parameters; 45 | 46 | /// 47 | /// Gets the custom attributes for the . 48 | /// 49 | /// The type of attribute to search for. 50 | /// true to search this action's inheritance chain to find the attributes; otherwise, false. 51 | /// The collection of custom attributes applied to this action. 52 | public override Collection GetCustomAttributes(bool inherit) => 53 | new Collection(this.MessageType.GetCustomAttributes(typeof(T), inherit).OfType().ToArray()); 54 | 55 | /// Throws an exception. 56 | /// 57 | /// 58 | /// 59 | /// 60 | public override Task ExecuteAsync(HttpControllerContext controllerContext, 61 | IDictionary arguments, CancellationToken cancellationToken) 62 | { 63 | // Never called. 64 | throw new NotImplementedException(); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/Controllerless.WebApi.Description/XmlDocumentationTypeDescriptionProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Linq; 4 | using System.Xml.XPath; 5 | 6 | namespace SolidServices.Controllerless.WebApi.Description 7 | { 8 | // NOTE: The code in this file is copy-pasted from the default Web API Visual Studio 2013 template. 9 | /// 10 | /// Allows getting type descriptions based on .NET XML documentation files that are generated by the 11 | /// C# or VB compiler. 12 | /// 13 | public class XmlDocumentationTypeDescriptionProvider : ITypeDescriptionProvider 14 | { 15 | private const string TypeExpression = "/doc/members/member[@name='T:{0}']"; 16 | 17 | private XPathNavigator _documentNavigator; 18 | 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | /// The physical path to XML document. 23 | public XmlDocumentationTypeDescriptionProvider(string documentPath) 24 | { 25 | Requires.IsNotNull(documentPath, nameof(documentPath)); 26 | 27 | _documentNavigator = new XPathDocument(documentPath).CreateNavigator(); 28 | } 29 | 30 | /// Gets the type's description or null when there is no description for the given type. 31 | /// The type. 32 | /// The description of the requested type or null. 33 | public string GetDescription(Type type) 34 | { 35 | XPathNavigator typeNode = GetTypeNode(type); 36 | return GetTagValue(typeNode, "summary"); 37 | } 38 | 39 | private XPathNavigator GetTypeNode(Type type) 40 | { 41 | string controllerTypeName = GetTypeName(type); 42 | string selectExpression = String.Format(CultureInfo.InvariantCulture, TypeExpression, controllerTypeName); 43 | return _documentNavigator.SelectSingleNode(selectExpression); 44 | } 45 | 46 | private static string GetTagValue(XPathNavigator parentNode, string tagName) 47 | { 48 | if (parentNode != null) 49 | { 50 | XPathNavigator node = parentNode.SelectSingleNode(tagName); 51 | if (node != null) 52 | { 53 | return node.Value.Trim(); 54 | } 55 | } 56 | 57 | return null; 58 | } 59 | 60 | private static string GetTypeName(Type type) 61 | { 62 | string name = type.FullName; 63 | if (type.IsGenericType) 64 | { 65 | // Format the generic type name to something like: Generic{System.Int32,System.String} 66 | Type genericType = type.GetGenericTypeDefinition(); 67 | Type[] genericArguments = type.GetGenericArguments(); 68 | string genericTypeName = genericType.FullName; 69 | 70 | // Trim the generic parameter counts from the name 71 | genericTypeName = genericTypeName.Substring(0, genericTypeName.IndexOf('`')); 72 | string[] argumentTypeNames = genericArguments.Select(t => GetTypeName(t)).ToArray(); 73 | name = String.Format(CultureInfo.InvariantCulture, "{0}{{{1}}}", genericTypeName, String.Join(",", argumentTypeNames)); 74 | } 75 | if (type.IsNested) 76 | { 77 | // Changing the nested type name from OuterType+InnerType to OuterType.InnerType to match the XML documentation syntax. 78 | name = name.Replace("+", "."); 79 | } 80 | 81 | return name; 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /src/Controllerless.WebApi.Description/Controllerless.WebApi.Description.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {7325A177-DD50-4AAC-A899-59C281E532C9} 8 | Library 9 | Properties 10 | SolidServices.Controllerless.WebApi.Description 11 | SolidServices.Controllerless.WebApi.Description 12 | v4.5 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | bin\Debug\SolidServices.Controllerless.WebApi.Description.xml 24 | true 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | bin\Release\SolidServices.Controllerless.WebApi.Description.xml 34 | true 35 | 36 | 37 | 38 | packages\Newtonsoft.Json.4.5.11\lib\net40\Newtonsoft.Json.dll 39 | True 40 | 41 | 42 | 43 | 44 | 45 | 46 | packages\Microsoft.AspNet.WebApi.Client.5.0.0\lib\net45\System.Net.Http.Formatting.dll 47 | True 48 | 49 | 50 | packages\Microsoft.AspNet.WebApi.Core.5.0.0\lib\net45\System.Web.Http.dll 51 | True 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Controllerless 2 | ### SOLID designed applications don't need controllers. Remove the cruft; become controllerless. 3 | 4 | 5 | [![NuGet](https://img.shields.io/nuget/v/SolidServices.Controllerless.WebApi.Description.svg)](https://www.nuget.org/packages/SolidServices.Controllerless.WebApi.Description/) 6 | 7 | 8 | If you're writing SOLID message-based applications, you came to the right place. If not, please [go here](https://github.com/dotnetjunkie/solidservices) to understand what such application design can bring you and your team. 9 | 10 | **Controllerless** is a sample project that shows how to build applications based on message-driven architectures that don't require the cruft of defining the service or presentation-layer classes like Web API controllers or WCF services. The projects and samples in this repository are expected to grow over time. For now, don't expect this code to be hardened production-ready code. 11 | 12 | Currently the only supported project is the Controllerless Web API documentation generation project: 13 | 14 | ## Controllerless Web API documentation generation 15 | 16 | *Controllerless Web API documentation generation* contains an alternative Web API `ApiExplorer` implementation that makes it possible to build documentation based on the messages you defined in your application, instead of controllers and actions, since we promote a happy world without the cruft that controllers bring. 17 | 18 | On top of *Controllerless Web API documentation generation*, Swagger documentation can be easily generated using [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle). 19 | 20 | The main type in the *Controllerless Web API documentation generation* is the `ControllerlessApiExplorer`. The `ControllerlessApiExplorer` generates a list of `ApiDescriptions` that can be used to generate documentation (for instance using Swagger). 21 | 22 | ### Short example 23 | The following example shows the creation of a `ControllerlessApiExplorer` with a default configuration: 24 | 25 | ``` c# 26 | // Create a ControllerlessApiExplorer 27 | var messageApiExplorer = new ControllerlessApiExplorer( 28 | // List of 'action' messages that this explorer uses. 29 | messageTypes: new[] { typeof(GetOrderById), typeof(ShipOrder) }, 30 | // Delegate that produces the return type for a given message type. 31 | responseTypeSelector: messageType => GetResponseType(messageType)); 32 | 33 | // Replace the original explorer with our new one 34 | config.Services.Replace(typeof(IApiExplorer), messageApiExplorer); 35 | ``` 36 | 37 | This `ControllerlessApiExplorer` returns documentation for the two supplied request messages and produces descriptions for the following relative paths: 38 | 39 | - [POST] api/messages/GetOrderById 40 | - [POST] api/messages/ShipOrder 41 | 42 | ### Full example 43 | 44 | `ControllerlessApiExplorer` is fully configurable and can be adapter to your needs. The following example shows an `ControllerlessApiExplorer` that is used for generation of the command API of an application, where commands don't return anything (although the infrastructure should obviously still communicate HTTP error codes back to the client): 45 | 46 | ``` c# 47 | 48 | var commandApiExplorer = new ControllerlessApiExplorer( 49 | messageTypes: new[] { typeof(CreateOrder), typeof(ShipOrder) }, 50 | responseTypeSelector: messageType => typeof(void)) 51 | { 52 | // Prefix of your API. Defaults to "api/" 53 | ApiPrefix = "api/", 54 | // Name of the controller. Typically "commands" or "queries". 55 | ControllerName = "commands", 56 | // Parameter name for the action's parameter. The default value is 'message'. 57 | ParameterName = "command", 58 | // Delegate that returns the name of the action. The default returns 'messageType.Name'. 59 | ActionNameSelector = messageType => messageType.Name, 60 | // Delete that returns the HttpMethod for a messageType. The default returns HttpMethod.Post. 61 | HttpMethodSelector = messageType => HttpMethod.Post, 62 | // Delegate that defines the parameter source for a given message type. Typically 63 | // FromBody for commands and FromUri or FromBody for queries. Default is FromBody. 64 | ParameterSourceSelector = messageType => ApiParameterSource.FromBody, 65 | // Builds the relative path based on the action name. 66 | // By default it returns ApiPrefix + controllerName + "/" + actionName. 67 | RelativePathSelector = actionName => "api/commands/" + actionName, 68 | // The list of supported request body formatters. By default it contains only the json formatter. 69 | SupportedRequestBodyFormatters = new Collection { new JsonMediaTypeFormatter() }, 70 | }; 71 | 72 | // Combine the original explorer, plus your custom explorers in one composite explorer. 73 | config.Services.Replace(typeof(IApiExplorer), 74 | new CompositeApiExplorer( 75 | config.Services.GetApiExplorer(), 76 | commandApiExplorer, 77 | queryApiExplorer)); 78 | ``` 79 | 80 | #### Integrate with Swashbuckle 81 | 82 | The previous example works as is. After you plugin [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle) to generate Swagger documentation you're almost done. To allow Swashbuckle to generate descriptions for parameters and message and return types, you need to point Swashbuckle to the compiler generated .XML documentation file(s) of your message types: 83 | 84 | ``` c# 85 | // Part of your Swashbuckle config 86 | string xmlCommentsPath = HostingEnvironment.MapPath("~/App_Data/Contract.xml"); 87 | 88 | swaggerDocsConfig.IncludeXmlComments(xmlCommentsPath); 89 | ``` 90 | 91 | This allows you to use XML documentation on your message such as follows: 92 | 93 | ``` c# 94 | /// Commands an order to be shipped. 95 | public class ShipOrderCommand 96 | { 97 | /// The id of the order. 98 | public Guid OrderId { get; set; } 99 | } 100 | ``` 101 | 102 | On top of that you need to implement a custom `IOperationFilter` to allow your actions to have a description: 103 | 104 | ``` c# 105 | 106 | sealed class ControllerlessActionOperationFilter : IOperationFilter 107 | { 108 | private readonly ITypeDescriptionProvider provider; 109 | 110 | public ControllerlessActionOperationFilter(string xmlCommentsPath) 111 | { 112 | this.provider = new XmlDocumentationTypeDescriptionProvider(xmlCommentsPath); 113 | } 114 | 115 | public void Apply(Operation operation, SchemaRegistry sr, ApiDescription desc) 116 | { 117 | var descriptor = desc.ActionDescriptor as ControllerlessActionDescriptor; 118 | operation.summary = descriptor != null 119 | ? this.provider.GetDescription(descriptor.MessageType) 120 | : operation.summary; 121 | } 122 | } 123 | ``` 124 | 125 | This operation filter makes use of **Controllerless** to retrieve the description of a message type from the created `XmlDocumentationTypeDescriptionProvider`. `XmlDocumentationTypeDescriptionProvider` is a type provided by **Controllerless** and allows parsing an XML comment file. You can configure Swashbuckle to use this class as follows: 126 | 127 | ``` c# 128 | var filter = new ControllerlessActionOperationFilter(xmlCommentsPath); 129 | c.OperationFilter(() => filter); 130 | ``` 131 | 132 | The above results in the following Swagger documentation: 133 | 134 | ![Alt text](swaggerexample.png?raw=true "Swagger example") 135 | -------------------------------------------------------------------------------- /src/Controllerless.WebApi.Description/ControllerlessApiExplorer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | using System.Net.Http; 7 | using System.Net.Http.Formatting; 8 | using System.Reflection; 9 | using System.Web.Http.Controllers; 10 | using System.Web.Http.Description; 11 | 12 | namespace SolidServices.Controllerless.WebApi.Description 13 | { 14 | /// 15 | /// Api Explorer for creating a documentation for a set of messages under one specific part of your application. 16 | /// 17 | public sealed class ControllerlessApiExplorer : IApiExplorer 18 | { 19 | private readonly IEnumerable messageTypes; 20 | private readonly Lazy> descriptions; 21 | private readonly Func responseTypeSelector; 22 | 23 | /// Constructs a new . 24 | /// The list of messages that this explorer documents. 25 | /// 26 | public ControllerlessApiExplorer( 27 | IEnumerable messageTypes, 28 | Func responseTypeSelector) 29 | { 30 | Requires.IsNotNull(messageTypes, nameof(messageTypes)); 31 | Requires.IsNotNull(responseTypeSelector, nameof(responseTypeSelector)); 32 | 33 | this.messageTypes = messageTypes.ToArray(); 34 | this.responseTypeSelector = responseTypeSelector; 35 | this.descriptions = new Lazy>(this.GetDescriptions); 36 | this.ControllerDescriptor = new ControllerlessControllerDescriptor { ControllerName = "messages" }; 37 | this.RelativePathSelector = actionName => this.ApiPrefix + this.ControllerDescriptor.ControllerName + "/" + actionName; 38 | } 39 | 40 | /// Gets or sets the prefix for the API. Default value is "api/". 41 | public string ApiPrefix { get; set; } = "api/"; 42 | 43 | /// 44 | /// Gets or sets the name of the controller in the description. The default is "messages". 45 | /// 46 | public string ControllerName 47 | { 48 | get { return this.ControllerDescriptor.ControllerName; } 49 | set { this.ControllerDescriptor.ControllerName = value; } 50 | } 51 | 52 | /// Gets or sets the action name selector. The default returns the the short name of the supplied message type. 53 | public Func ActionNameSelector { get; set; } = type => type.Name; 54 | 55 | /// Gets or sets the http method selector. The default return HttpMethod.Post for every supplied message type. 56 | public Func HttpMethodSelector { get; set; } = type => HttpMethod.Post; 57 | 58 | /// 59 | /// Gets or sets the relative path selector. Builds a relative path based on the action name as returned from 60 | /// the . The default returns the action name prefixed by the 61 | /// and supplied controller name. 62 | public Func RelativePathSelector { get; set; } 63 | 64 | /// Gets or sets the parameter source selector. The default always returns FromBody, which means 65 | public Func ParameterSourceSelector { get; set; } = type => ApiParameterSource.FromBody; 66 | 67 | /// 68 | /// Gets or sets the parameter name that is used when the parameter source is set to FromBody for a 69 | /// given message type. The default value is 'message'. 70 | public string ParameterName { get; set; } = "message"; 71 | 72 | /// 73 | /// Gets or sets the that is added to the list of 74 | /// elements that is produced by this Api Explorer. 75 | /// 76 | public HttpControllerDescriptor ControllerDescriptor { get; set; } 77 | 78 | /// 79 | /// Gets or sets the type descriptor selector. Returns a description for the given message type or return type. 80 | /// Return an empty string by default. 81 | /// 82 | private Func TypeDescriptionSelector { get; set; } = type => 83 | type.GetCustomAttribute()?.Description; 84 | 85 | /// 86 | /// Gets or sets the collection of supported s for decoding request bodies. 87 | /// The default only contains the . 88 | /// 89 | public Collection SupportedRequestBodyFormatters { get; set; } = new Collection 90 | { 91 | new JsonMediaTypeFormatter(), 92 | }; 93 | 94 | /// 95 | /// Gets the list of instances for the given set of message types. 96 | /// The collection is cached. 97 | /// 98 | public Collection ApiDescriptions => descriptions.Value; 99 | 100 | private Collection GetDescriptions() => 101 | new Collection(this.messageTypes.Select(CreateApiDescription).ToList()); 102 | 103 | private static readonly PropertyInfo ResponseDescriptionProperty = typeof(ApiDescription).GetProperty("ResponseDescription"); 104 | 105 | private ApiDescription CreateApiDescription(Type messageType) 106 | { 107 | string actionName = this.ActionNameSelector(messageType); 108 | var responseType = this.responseTypeSelector(messageType); 109 | 110 | var desc = new ApiDescription 111 | { 112 | HttpMethod = this.HttpMethodSelector(messageType), 113 | RelativePath = this.RelativePathSelector(actionName), 114 | Documentation = this.TypeDescriptionSelector(messageType), 115 | }; 116 | 117 | var parameterSource = this.ParameterSourceSelector(messageType); 118 | 119 | if (desc.HttpMethod == HttpMethod.Get && parameterSource == ApiParameterSource.FromBody) 120 | { 121 | throw new InvalidOperationException("For the given message type " + messageType.FullName + 122 | ", the provided HttpMethodSelector returned GET, while the ParameterSourceProvider " + 123 | "returned FromBody. This is an invalid combination, because GET requests don't have " + 124 | "a body."); 125 | } 126 | 127 | desc.ActionDescriptor = new ControllerlessActionDescriptor(desc, messageType, actionName, 128 | responseType, BuildParameters(desc, messageType, parameterSource)) 129 | { 130 | ControllerDescriptor = this.ControllerDescriptor 131 | }; 132 | 133 | ResponseDescriptionProperty.SetValue(desc, new ResponseDescription 134 | { 135 | DeclaredType = responseType, 136 | ResponseType = responseType, 137 | Documentation = this.TypeDescriptionSelector(responseType), 138 | }); 139 | 140 | desc.SupportedRequestBodyFormatters.AddRange(this.SupportedRequestBodyFormatters); 141 | 142 | desc.ParameterDescriptions.AddRange( 143 | from parameter in desc.ActionDescriptor.GetParameters() 144 | select ToApiParameterDescription(parameter, parameterSource)); 145 | 146 | return desc; 147 | } 148 | 149 | private IEnumerable BuildParameters(ApiDescription description, 150 | Type messageType, ApiParameterSource parameterSource) 151 | { 152 | if (parameterSource == ApiParameterSource.FromUri) 153 | { 154 | return 155 | from prop in messageType.GetProperties(BindingFlags.Instance | BindingFlags.Public) 156 | select new ControllerlessParameterDescriptor(prop.Name, prop.PropertyType); 157 | } 158 | else 159 | { 160 | return new[] { new ControllerlessParameterDescriptor(this.ParameterName, messageType) }; 161 | } 162 | } 163 | 164 | private ApiParameterDescription ToApiParameterDescription(HttpParameterDescriptor descriptor, 165 | ApiParameterSource parameterSource) 166 | { 167 | return new ApiParameterDescription 168 | { 169 | Documentation = this.TypeDescriptionSelector(descriptor.ParameterType), 170 | Name = descriptor.ParameterName, 171 | ParameterDescriptor = descriptor, 172 | Source = parameterSource 173 | }; 174 | } 175 | } 176 | } --------------------------------------------------------------------------------