├── .gitignore ├── LICENSE ├── README.md ├── Strathweb.TypedRouting.AspNetCore.sln ├── sample └── Demo │ ├── AnnotationFilter.cs │ ├── Controllers │ ├── ItemsController.cs │ └── OtherController.cs │ ├── Demo.csproj │ ├── Item.cs │ ├── MandatoryHeaderConstraint.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Startup.cs │ ├── TimerFilter.cs │ └── web.config ├── src └── Strathweb.TypedRouting.AspNetCore │ ├── MvcBuilderExtensions.cs │ ├── MvcCoreBuilderExtensions.cs │ ├── Param.cs │ ├── Strathweb.TypedRouting.AspNetCore.csproj │ ├── TypedRoute.cs │ ├── TypedRoutingApplicationModelConvention.cs │ ├── TypedRoutingOptions.cs │ └── TypedRoutingOptionsSetup.cs ├── strathweb.png └── tests └── Strathweb.TypedRouting.AspNetCore.Tests ├── IntegrationTests.cs └── Strathweb.TypedRouting.AspNetCore.Tests.csproj /.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 | 24 | # Visual Studio 2015 cache/options directory 25 | .vs/ 26 | # Uncomment if you have tasks that create the project's static files in wwwroot 27 | #wwwroot/ 28 | 29 | # MSTest test Results 30 | [Tt]est[Rr]esult*/ 31 | [Bb]uild[Ll]og.* 32 | 33 | # NUNIT 34 | *.VisualState.xml 35 | TestResult.xml 36 | 37 | # Build Results of an ATL Project 38 | [Dd]ebugPS/ 39 | [Rr]eleasePS/ 40 | dlldata.c 41 | 42 | # DNX 43 | project.lock.json 44 | artifacts/ 45 | 46 | *_i.c 47 | *_p.c 48 | *_i.h 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.tmp_proj 63 | *.log 64 | *.vspscc 65 | *.vssscc 66 | .builds 67 | *.pidb 68 | *.svclog 69 | *.scc 70 | 71 | # Chutzpah Test files 72 | _Chutzpah* 73 | 74 | # Visual C++ cache files 75 | ipch/ 76 | *.aps 77 | *.ncb 78 | *.opendb 79 | *.opensdf 80 | *.sdf 81 | *.cachefile 82 | 83 | # Visual Studio profiler 84 | *.psess 85 | *.vsp 86 | *.vspx 87 | *.sap 88 | 89 | # TFS 2012 Local Workspace 90 | $tf/ 91 | 92 | # Guidance Automation Toolkit 93 | *.gpState 94 | 95 | # ReSharper is a .NET coding add-in 96 | _ReSharper*/ 97 | *.[Rr]e[Ss]harper 98 | *.DotSettings.user 99 | 100 | # JustCode is a .NET coding add-in 101 | .JustCode 102 | 103 | # TeamCity is a build add-in 104 | _TeamCity* 105 | 106 | # DotCover is a Code Coverage Tool 107 | *.dotCover 108 | 109 | # NCrunch 110 | _NCrunch_* 111 | .*crunch*.local.xml 112 | nCrunchTemp_* 113 | 114 | # MightyMoose 115 | *.mm.* 116 | AutoTest.Net/ 117 | 118 | # Web workbench (sass) 119 | .sass-cache/ 120 | 121 | # Installshield output folder 122 | [Ee]xpress/ 123 | 124 | # DocProject is a documentation generator add-in 125 | DocProject/buildhelp/ 126 | DocProject/Help/*.HxT 127 | DocProject/Help/*.HxC 128 | DocProject/Help/*.hhc 129 | DocProject/Help/*.hhk 130 | DocProject/Help/*.hhp 131 | DocProject/Help/Html2 132 | DocProject/Help/html 133 | 134 | # Click-Once directory 135 | publish/ 136 | 137 | # Publish Web Output 138 | *.[Pp]ublish.xml 139 | *.azurePubxml 140 | # TODO: Comment the next line if you want to checkin your web deploy settings 141 | # but database connection strings (with potential passwords) will be unencrypted 142 | *.pubxml 143 | *.publishproj 144 | 145 | # NuGet Packages 146 | *.nupkg 147 | # The packages folder can be ignored because of Package Restore 148 | **/packages/* 149 | # except build/, which is used as an MSBuild target. 150 | !**/packages/build/ 151 | # Uncomment if necessary however generally it will be regenerated when needed 152 | #!**/packages/repositories.config 153 | # NuGet v3's project.json files produces more ignoreable files 154 | *.nuget.props 155 | *.nuget.targets 156 | 157 | # Microsoft Azure Build Output 158 | csx/ 159 | *.build.csdef 160 | 161 | # Microsoft Azure Emulator 162 | ecf/ 163 | rcf/ 164 | 165 | # Microsoft Azure ApplicationInsights config file 166 | ApplicationInsights.config 167 | 168 | # Windows Store app package directory 169 | AppPackages/ 170 | BundleArtifacts/ 171 | 172 | # Visual Studio cache files 173 | # files ending in .cache can be ignored 174 | *.[Cc]ache 175 | # but keep track of directories ending in .cache 176 | !*.[Cc]ache/ 177 | 178 | # Others 179 | ClientBin/ 180 | ~$* 181 | *~ 182 | *.dbmdl 183 | *.dbproj.schemaview 184 | *.pfx 185 | *.publishsettings 186 | node_modules/ 187 | orleans.codegen.cs 188 | 189 | # RIA/Silverlight projects 190 | Generated_Code/ 191 | 192 | # Backup & report files from converting an old project file 193 | # to a newer Visual Studio version. Backup files are not needed, 194 | # because we have git ;-) 195 | _UpgradeReport_Files/ 196 | Backup*/ 197 | UpgradeLog*.XML 198 | UpgradeLog*.htm 199 | 200 | # SQL Server files 201 | *.mdf 202 | *.ldf 203 | 204 | # Business Intelligence projects 205 | *.rdl.data 206 | *.bim.layout 207 | *.bim_*.settings 208 | 209 | # Microsoft Fakes 210 | FakesAssemblies/ 211 | 212 | # GhostDoc plugin setting file 213 | *.GhostDoc.xml 214 | 215 | # Node.js Tools for Visual Studio 216 | .ntvs_analysis.dat 217 | 218 | # Visual Studio 6 build log 219 | *.plg 220 | 221 | # Visual Studio 6 workspace options file 222 | *.opt 223 | 224 | # Visual Studio LightSwitch build output 225 | **/*.HTMLClient/GeneratedArtifacts 226 | **/*.DesktopClient/GeneratedArtifacts 227 | **/*.DesktopClient/ModelManifest.xml 228 | **/*.Server/GeneratedArtifacts 229 | **/*.Server/ModelManifest.xml 230 | _Pvt_Extensions 231 | 232 | # Paket dependency manager 233 | .paket/paket.exe 234 | 235 | # FAKE - F# Make 236 | .fake/ 237 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 Filip W 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Strathweb.TypedRouting.AspNetCore 2 | 3 | A library enabling strongly typed routing in ASP.NET Core MVC projects. 4 | 5 | ## Installation 6 | 7 | Everything is on [Nuget](https://www.nuget.org/packages/Strathweb.TypedRouting.AspNetCore). [![Nuget](http://img.shields.io/nuget/v/Strathweb.TypedRouting.AspNetCore.svg?maxAge=10800)](https://www.nuget.org/packages/Strathweb.TypedRouting.AspNetCore) 8 | 9 | ``` 10 | nuget install Strathweb.TypedRouting.AspNetCore 11 | ``` 12 | or via the .NET Core CLI: 13 | 14 | ``` 15 | dotnet add package Strathweb.TypedRouting.AspNetCore 16 | ``` 17 | 18 | ## Setup 19 | 20 | In your `Startup` class, after adding MVC, call `AddTypedRouting();` and then configure your routes: 21 | 22 | ```csharp 23 | services.AddMvc().AddTypedRouting(opt => 24 | { 25 | opt.Get("homepage", c => c.Action(x => x.Index())); 26 | opt.Get("aboutpage/{name}", c => c.Action(x => x.About(Param.Any))); 27 | opt.Post("sendcontact", c => c.Action(x => x.Contact())); 28 | }); 29 | ``` 30 | 31 | This creates: 32 | * a GET route to `/homepage` 33 | * a GET route to `/aboutpage/{name}` 34 | * a POST route to `/sendcontact` 35 | 36 | All of which will route to the relevant methods on our `HomeController`. 37 | 38 | ## Link generation 39 | 40 | Since the API is fluent, you can also give the routes names so that you can use them with i.e. link generation. 41 | 42 | ```csharp 43 | opt.Get("api/values/{id}", c => c.Action(x => x.Get(Param.Any))).WithName("GetValueById"); 44 | ``` 45 | 46 | Now you can use it with `IUrlHelper` (it's a `Url` property on every controller): 47 | 48 | ```csharp 49 | var link = Url.Link("GetValueById", new { id = 1 }); 50 | ``` 51 | 52 | `IUrlHelper` can also be obtained from `HttpContext`, anywhere in the pipeline (i.e. in a filter): 53 | 54 | ```csharp 55 | var services = context.HttpContext.RequestServices; 56 | var urlHelper = services.GetRequiredService().GetUrlHelper(context); 57 | var link = urlHelper.Link("GetValueById", new { id = 1 }); 58 | ``` 59 | 60 | Finally, you can also use this link generation technique with the built-in action results, such as for example `CreatedAtRouteResult`: 61 | 62 | ```csharp 63 | public IActionResult Post([FromBody]string value) 64 | { 65 | var result = CreatedAtRoute("GetValueById", new { id = 1 }, "foo"); 66 | return result; 67 | } 68 | ``` 69 | 70 | ## Filters 71 | 72 | The route definitions can also be done along with filters that should be executed for a given route. This is equivalent to defining a controller action, and annotating it with a relevant attribute such as action filter or authorization filter. 73 | 74 | ```csharp 75 | services.AddMvc().AddTypedRouting(opt => 76 | { 77 | opt.Get("api/items", c => c.Action(x => x.Get())).WithFilters(new AnnotationFilter()); 78 | }); 79 | ``` 80 | 81 | Filters can also be resolved from ASP.NET Core DI system - as long as they are registered there before. 82 | 83 | ```csharp 84 | services.AddSingleton(); 85 | 86 | services.AddMvc().AddTypedRouting(opt => 87 | { 88 | opt.Get("api/items", c => c.Action(x => x.Get())).WithFilter(); 89 | }); 90 | ``` 91 | 92 | ## Authorization Policies 93 | 94 | The route definitions can also have ASP.NET Core authorization policies attached to them. 95 | 96 | You can pass in a policy instance: 97 | 98 | ```csharp 99 | services.AddMvc().AddTypedRouting(opt => 100 | { 101 | opt.Get("api/secure", c => c.Action(x => x.Foo()). 102 | WithAuthorizationPolicy(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build())); 103 | }); 104 | ``` 105 | 106 | You can also define a policy as string - then a corresponding policy must be previously registerd in ASP.NET Core DI system. 107 | 108 | ```csharp 109 | services.AddAuthorization(o => 110 | { 111 | o.AddPolicy("MyPolicy", b => b.RequireAuthenticatedUser()); 112 | }); 113 | 114 | services.AddMvc().AddTypedRouting(opt => 115 | { 116 | opt.Get("api/secure", c => c.Action(x => x.Foo()). 117 | WithAuthorizationPolicy("MyPolicy")); 118 | }); 119 | ``` 120 | 121 | ## Action constraints 122 | 123 | The library supports two ways of specifying MVC action constraints: 124 | 125 | - inline in the template 126 | - via fluent API 127 | 128 | The inline constraints are the same as you can use with attribute routing. For example: 129 | 130 | ```csharp 131 | opt.Get("api/other/{id:int}", c => c.Action(x => x.Action2(Param.Any))); 132 | ``` 133 | 134 | You can also specify constraints via the fluent API, by chaining `IActionConstraintMetadata` implementations. Consider the following sample constraint class: 135 | 136 | ```csharp 137 | public class MandatoryHeaderConstraint : IActionConstraint, IActionConstraintMetadata 138 | { 139 | private string _header; 140 | 141 | public MandatoryHeaderConstraint(string header) 142 | { 143 | _header = header; 144 | } 145 | 146 | public int Order 147 | { 148 | get 149 | { 150 | return 0; 151 | } 152 | } 153 | 154 | public bool Accept(ActionConstraintContext context) 155 | { 156 | // only allow route to be hit if the predefined header is present 157 | if (context.RouteContext.HttpContext.Request.Headers.ContainsKey(_header)) 158 | { 159 | return true; 160 | } 161 | 162 | return false; 163 | } 164 | } 165 | ``` 166 | 167 | You can now use this class in the route declaration: 168 | 169 | ```csharp 170 | opt.Get("api/other", c => c.Action(x => x.Action1())).WithConstraints(new MandatoryHeaderConstraint("CustomHeader")); 171 | ``` 172 | 173 | ## License 174 | 175 | [MIT](https://github.com/filipw/Strathweb.TypedRouting.AspNetCore/blob/master/LICENSE) 176 | -------------------------------------------------------------------------------- /Strathweb.TypedRouting.AspNetCore.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 15 3 | VisualStudioVersion = 15.0.26430.12 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{AE85EB00-0646-4796-A7C7-93EBF06FA66C}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strathweb.TypedRouting.AspNetCore", "src\Strathweb.TypedRouting.AspNetCore\Strathweb.TypedRouting.AspNetCore.csproj", "{D2234F89-1547-4038-AABD-1BFDC5E784F8}" 8 | EndProject 9 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo", "sample\Demo\Demo.csproj", "{6AEBA69E-BFC3-46F2-91C6-50C718FE70D4}" 10 | EndProject 11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strathweb.TypedRouting.AspNetCore.Tests", "tests\Strathweb.TypedRouting.AspNetCore.Tests\Strathweb.TypedRouting.AspNetCore.Tests.csproj", "{7AD4A5F8-7B61-4FBF-833F-E904C36A97F6}" 12 | EndProject 13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9CBBEAD5-DA4B-4244-9478-A3CBC0407610}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {D2234F89-1547-4038-AABD-1BFDC5E784F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {D2234F89-1547-4038-AABD-1BFDC5E784F8}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {D2234F89-1547-4038-AABD-1BFDC5E784F8}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {D2234F89-1547-4038-AABD-1BFDC5E784F8}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {6AEBA69E-BFC3-46F2-91C6-50C718FE70D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {6AEBA69E-BFC3-46F2-91C6-50C718FE70D4}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {6AEBA69E-BFC3-46F2-91C6-50C718FE70D4}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {6AEBA69E-BFC3-46F2-91C6-50C718FE70D4}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {7AD4A5F8-7B61-4FBF-833F-E904C36A97F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {7AD4A5F8-7B61-4FBF-833F-E904C36A97F6}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {7AD4A5F8-7B61-4FBF-833F-E904C36A97F6}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {7AD4A5F8-7B61-4FBF-833F-E904C36A97F6}.Release|Any CPU.Build.0 = Release|Any CPU 33 | EndGlobalSection 34 | GlobalSection(SolutionProperties) = preSolution 35 | HideSolutionNode = FALSE 36 | EndGlobalSection 37 | GlobalSection(NestedProjects) = preSolution 38 | {D2234F89-1547-4038-AABD-1BFDC5E784F8} = {AE85EB00-0646-4796-A7C7-93EBF06FA66C} 39 | {6AEBA69E-BFC3-46F2-91C6-50C718FE70D4} = {AE85EB00-0646-4796-A7C7-93EBF06FA66C} 40 | {7AD4A5F8-7B61-4FBF-833F-E904C36A97F6} = {9CBBEAD5-DA4B-4244-9478-A3CBC0407610} 41 | EndGlobalSection 42 | EndGlobal 43 | -------------------------------------------------------------------------------- /sample/Demo/AnnotationFilter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Filters; 2 | 3 | namespace Demo 4 | { 5 | public class AnnotationFilter : ActionFilterAttribute 6 | { 7 | public override void OnActionExecuting(ActionExecutingContext context) 8 | { 9 | context.HttpContext.Response.Headers.Add("FilterBefore",typeof(AnnotationFilter).ToString()); 10 | 11 | } 12 | 13 | public override void OnActionExecuted(ActionExecutedContext context) 14 | { 15 | context.HttpContext.Response.Headers.Add("FilterAfter", typeof(AnnotationFilter).ToString()); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /sample/Demo/Controllers/ItemsController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace Demo.Controllers 8 | { 9 | public class ItemsController : Controller 10 | { 11 | public IEnumerable Get() 12 | { 13 | return new Item[] { new Item { Text = "value1" }, new Item { Text = "value2" } }; 14 | } 15 | 16 | public Item Get(int id) 17 | { 18 | return new Item { Text = "value" }; 19 | } 20 | 21 | public IActionResult Post([FromBody]Item item) 22 | { 23 | // one way to generate a link 24 | var link = Url.Link("GetItemById", new { id = 1 }); 25 | 26 | // another way to geenrate a link, using the built in action results 27 | var result = CreatedAtRoute("GetItemById", new { id = 1 }, item); 28 | return result; 29 | } 30 | 31 | public Item Put(int id, [FromBody]Item item) 32 | { 33 | return item; 34 | } 35 | 36 | public int Delete(int id) 37 | { 38 | return id; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sample/Demo/Controllers/OtherController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Demo.Controllers 8 | { 9 | public class OtherController : Controller 10 | { 11 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Await.Warning", "CS4014:Await.Warning")] 12 | public async Task Action1() 13 | { 14 | await Task.Delay(100); 15 | return "bar"; 16 | } 17 | 18 | public int Action2(int id) 19 | { 20 | return id; 21 | } 22 | 23 | public void Unreachable() { } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sample/Demo/Demo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | true 6 | Demo 7 | Exe 8 | Demo 9 | 10 | 11 | 12 | 13 | PreserveNewest 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /sample/Demo/Item.cs: -------------------------------------------------------------------------------- 1 | namespace Demo 2 | { 3 | public class Item 4 | { 5 | public string Text { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /sample/Demo/MandatoryHeaderConstraint.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.ActionConstraints; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Demo 8 | { 9 | public class MandatoryHeaderConstraint : IActionConstraint, IActionConstraintMetadata 10 | { 11 | private string _header; 12 | 13 | public MandatoryHeaderConstraint(string header) 14 | { 15 | _header = header; 16 | } 17 | 18 | public int Order => 0; 19 | 20 | public bool Accept(ActionConstraintContext context) 21 | { 22 | if (context.RouteContext.HttpContext.Request.Headers.ContainsKey(_header)) 23 | { 24 | return true; 25 | } 26 | 27 | return false; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sample/Demo/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Builder; 8 | 9 | namespace Demo 10 | { 11 | public class Program 12 | { 13 | public static void Main(string[] args) 14 | { 15 | var host = new WebHostBuilder() 16 | .UseKestrel() 17 | .UseContentRoot(Directory.GetCurrentDirectory()) 18 | .UseIISIntegration() 19 | .UseStartup() 20 | .Build(); 21 | 22 | host.Run(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sample/Demo/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:5563/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "launchUrl": "api/values", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "Demo": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "launchUrl": "http://localhost:5000/api/values", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /sample/Demo/Startup.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Logging; 9 | using Strathweb.TypedRouting.AspNetCore; 10 | using Demo.Controllers; 11 | using Microsoft.AspNetCore.Authorization; 12 | using Microsoft.AspNetCore.Authentication.JwtBearer; 13 | 14 | namespace Demo 15 | { 16 | public class Startup 17 | { 18 | public Startup(IHostingEnvironment env) 19 | {} 20 | 21 | public void ConfigureServices(IServiceCollection services) 22 | { 23 | services.AddSingleton(); 24 | services.AddSingleton(); 25 | 26 | services.AddAuthentication(o => 27 | { 28 | o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 29 | o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 30 | }).AddJwtBearer(); 31 | 32 | services.AddAuthorization(o => 33 | { 34 | o.AddPolicy("MyPolicy", b => b.RequireAuthenticatedUser()); 35 | }); 36 | 37 | services.AddMvc().AddTypedRouting(opt => 38 | { 39 | opt.Get("api/items", c => c.Action(x => x.Get())). 40 | WithFilters(new AnnotationFilter()); 41 | 42 | opt.Get("api/items/{id}", c => c.Action(x => x.Get(Param.Any))). 43 | WithName("GetItemById"). 44 | WithFilter(); 45 | 46 | opt.Post("api/items", c => c.Action(x => x.Post(Param.Any))); 47 | opt.Put("api/items/{id}", c => c.Action(x => x.Put(Param.Any, Param.Any))); 48 | opt.Delete("api/items/{id}", c => c.Action(x => x.Delete(Param.Any))); 49 | 50 | opt.Get("api/other", c => c.Action(x => x.Action1())). 51 | WithConstraints(new MandatoryHeaderConstraint("CustomHeader")); 52 | 53 | opt.Get("api/other/{id:int}", c => c.Action(x => x.Action2(Param.Any))); 54 | 55 | opt.Get("api/secure_string", c => c.Action(x => x.Unreachable()). 56 | WithAuthorizationPolicy("MyPolicy")); 57 | 58 | opt.Get("api/secure_instance", c => c.Action(x => x.Unreachable()). 59 | WithAuthorizationPolicy(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build())); 60 | }); 61 | } 62 | 63 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 64 | { 65 | loggerFactory.AddDebug(); 66 | 67 | // this is needed to make authz policies work 68 | // at least a single authn middleware must be present 69 | app.UseAuthentication(); 70 | app.UseMvc(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /sample/Demo/TimerFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Mvc.Filters; 3 | 4 | namespace Demo 5 | { 6 | public class TimerFilter : ActionFilterAttribute 7 | { 8 | public override void OnActionExecuting(ActionExecutingContext context) 9 | { 10 | var start = DateTimeOffset.UtcNow; 11 | context.HttpContext.Items.Add("start", start); 12 | } 13 | 14 | public override void OnActionExecuted(ActionExecutedContext context) 15 | { 16 | if (context.HttpContext.Items.TryGetValue("start", out var start)) 17 | { 18 | var end = DateTimeOffset.UtcNow; 19 | context.HttpContext.Response.Headers.Add("ActionDuration", (end - (DateTimeOffset)start).ToString()); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sample/Demo/web.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Strathweb.TypedRouting.AspNetCore/MvcBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Options; 8 | using Microsoft.Extensions.DependencyInjection.Extensions; 9 | 10 | namespace Strathweb.TypedRouting.AspNetCore 11 | { 12 | public static class MvcBuilderExtensions 13 | { 14 | public static IMvcBuilder AddTypedRouting(this IMvcBuilder builder, Action typedRoutingOptionsConfiguration) 15 | { 16 | var typedRoutingOptions = new TypedRoutingOptions(); 17 | typedRoutingOptionsConfiguration(typedRoutingOptions); 18 | 19 | builder.Services.AddSingleton, TypedRoutingOptionsSetup>(); 20 | builder.Services.AddSingleton(typedRoutingOptions); 21 | 22 | return builder; 23 | } 24 | 25 | public static IMvcBuilder AddTypedRouting(this IMvcBuilder builder, TypedRoutingOptions typedRoutingOptions) 26 | { 27 | builder.Services.AddSingleton, TypedRoutingOptionsSetup>(); 28 | builder.Services.AddSingleton(typedRoutingOptions); 29 | return builder; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Strathweb.TypedRouting.AspNetCore/MvcCoreBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Options; 8 | using Microsoft.Extensions.DependencyInjection.Extensions; 9 | 10 | namespace Strathweb.TypedRouting.AspNetCore 11 | { 12 | public static class MvcCoreBuilderExtensions 13 | { 14 | public static IMvcCoreBuilder AddTypedRouting(this IMvcCoreBuilder builder, Action typedRoutingOptionsConfiguration) 15 | { 16 | var typedRoutingOptions = new TypedRoutingOptions(); 17 | typedRoutingOptionsConfiguration(typedRoutingOptions); 18 | 19 | builder.Services.AddSingleton, TypedRoutingOptionsSetup>(); 20 | builder.Services.AddSingleton(typedRoutingOptions); 21 | 22 | return builder; 23 | } 24 | 25 | public static IMvcCoreBuilder AddTypedRouting(this IMvcCoreBuilder builder, TypedRoutingOptions typedRoutingOptions) 26 | { 27 | builder.Services.AddSingleton, TypedRoutingOptionsSetup>(); 28 | builder.Services.AddSingleton(typedRoutingOptions); 29 | return builder; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Strathweb.TypedRouting.AspNetCore/Param.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Strathweb.TypedRouting.AspNetCore 7 | { 8 | public static class Param 9 | { 10 | public static TValue Any => default(TValue); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Strathweb.TypedRouting.AspNetCore/Strathweb.TypedRouting.AspNetCore.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 2.0.1 5 | A library enabling typed routing in ASP.NET MVC Core projects. Next generation of the Strathweb.TypedRouting Web API 2 library. 6 | filipw 7 | netstandard2.0 8 | Strathweb.TypedRouting.AspNetCore 9 | Strathweb.TypedRouting.AspNetCore 10 | ASP.NET Core;routing;typed;asp net;aspnetcore 11 | https://raw.githubusercontent.com/filipw/Strathweb.TypedRouting.AspNetCore/master/strathweb.png 12 | https://github.com/filipw/Strathweb.TypedRouting.AspNetCore 13 | MIT 14 | git 15 | https://github.com/filipw/Strathweb.TypedRouting.AspNetCore.git 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Strathweb.TypedRouting.AspNetCore/TypedRoute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Mvc.ApplicationModels; 8 | using Microsoft.AspNetCore.Mvc.ActionConstraints; 9 | using Microsoft.AspNetCore.Mvc.Internal; 10 | using Microsoft.AspNetCore.Mvc.Filters; 11 | using Microsoft.AspNetCore.Authorization; 12 | using Microsoft.AspNetCore.Mvc.Authorization; 13 | 14 | namespace Strathweb.TypedRouting.AspNetCore 15 | { 16 | public class TypedRoute : AttributeRouteModel 17 | { 18 | public TypedRoute(string template) 19 | { 20 | Template = template; 21 | Constraints = new List(); 22 | Filters = new FilterCollection(); 23 | FilterTypes = new List(); 24 | } 25 | 26 | public TypeInfo ControllerType { get; private set; } 27 | 28 | public MethodInfo ActionMember { get; private set; } 29 | 30 | public List Constraints { get; private set; } 31 | 32 | public FilterCollection Filters { get; } 33 | 34 | public List FilterTypes { get; } 35 | 36 | public TypedRoute Controller() 37 | { 38 | ControllerType = typeof(TController).GetTypeInfo(); 39 | return this; 40 | } 41 | 42 | public TypedRoute Action(Expression> expression) 43 | { 44 | ActionMember = GetMethodInfoInternal(expression); 45 | ControllerType = ActionMember.DeclaringType.GetTypeInfo(); 46 | return this; 47 | } 48 | 49 | public TypedRoute Action(Expression> expression) 50 | { 51 | ActionMember = GetMethodInfoInternal(expression); 52 | ControllerType = ActionMember.DeclaringType.GetTypeInfo(); 53 | return this; 54 | } 55 | 56 | private static MethodInfo GetMethodInfoInternal(dynamic expression) 57 | { 58 | var method = expression.Body as MethodCallExpression; 59 | if (method != null) 60 | return method.Method; 61 | 62 | throw new ArgumentException("Expression is incorrect!"); 63 | } 64 | 65 | public TypedRoute WithName(string name) 66 | { 67 | Name = name; 68 | return this; 69 | } 70 | 71 | public TypedRoute ForHttpMethods(params string[] methods) 72 | { 73 | Constraints.Add(new HttpMethodActionConstraint(methods)); 74 | return this; 75 | } 76 | 77 | public TypedRoute WithConstraints(params IActionConstraintMetadata[] constraints) 78 | { 79 | Constraints.AddRange(constraints); 80 | return this; 81 | } 82 | 83 | public TypedRoute WithFilters(params IFilterMetadata[] filters) 84 | { 85 | foreach (var filter in filters) 86 | { 87 | Filters.Add(filter); 88 | } 89 | 90 | return this; 91 | } 92 | 93 | public TypedRoute WithFilter() where T : IFilterMetadata 94 | { 95 | FilterTypes.Add(typeof(T)); 96 | return this; 97 | } 98 | 99 | public TypedRoute WithAuthorizationPolicy(string authorizationPolicyName) 100 | { 101 | Filters.Add(new AuthorizeFilter(authorizationPolicyName)); 102 | return this; 103 | } 104 | 105 | public TypedRoute WithAuthorizationPolicy(AuthorizationPolicy policy) 106 | { 107 | Filters.Add(new AuthorizeFilter(policy)); 108 | 109 | return this; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Strathweb.TypedRouting.AspNetCore/TypedRoutingApplicationModelConvention.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc.ApplicationModels; 7 | using Microsoft.AspNetCore.Mvc.Internal; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.AspNetCore.Mvc.Filters; 10 | 11 | namespace Strathweb.TypedRouting.AspNetCore 12 | { 13 | public class TypedRoutingApplicationModelConvention : IApplicationModelConvention 14 | { 15 | private readonly IServiceProvider _serviceProvider; 16 | private readonly TypedRoutingOptions _typedRoutingOptions; 17 | 18 | public TypedRoutingApplicationModelConvention(IServiceProvider serviceProvider, TypedRoutingOptions typedRoutingOptions) 19 | { 20 | _serviceProvider = serviceProvider; 21 | _typedRoutingOptions = typedRoutingOptions; 22 | } 23 | 24 | public void Apply(ApplicationModel application) 25 | { 26 | foreach (var controller in application.Controllers) 27 | { 28 | if (_typedRoutingOptions.Routes.ContainsKey(controller.ControllerType)) 29 | { 30 | var typedRoutes = _typedRoutingOptions.Routes[controller.ControllerType]; 31 | foreach (var route in typedRoutes) 32 | { 33 | var action = controller.Actions.FirstOrDefault(x => x.ActionMethod == route.ActionMember); 34 | if (action == null) continue; 35 | 36 | var selectorModel = new SelectorModel 37 | { 38 | AttributeRouteModel = route, 39 | }; 40 | 41 | foreach(var constraint in route.Constraints) 42 | { 43 | selectorModel.ActionConstraints.Add(constraint); 44 | } 45 | 46 | foreach (var filter in route.Filters) 47 | { 48 | action.Filters.Add(filter); 49 | } 50 | 51 | // resolution from DI only supported when ServiceProvider is there 52 | if (_serviceProvider != null) 53 | { 54 | foreach (var filter in route.FilterTypes) 55 | { 56 | var filterMetadata = _serviceProvider.GetService(filter) as IFilterMetadata; 57 | if (filterMetadata != null) 58 | { 59 | action.Filters.Add(filterMetadata); 60 | } 61 | } 62 | } 63 | 64 | var nonAttributeSelectors = action.Selectors.Where(x => x.AttributeRouteModel == null).ToArray(); 65 | foreach (var nonAttributeSelector in nonAttributeSelectors) 66 | { 67 | action.Selectors.Remove(nonAttributeSelector); 68 | } 69 | 70 | action.Selectors.Insert(0, selectorModel); 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Strathweb.TypedRouting.AspNetCore/TypedRoutingOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.DependencyInjection.Extensions; 5 | using System.Reflection; 6 | 7 | namespace Strathweb.TypedRouting.AspNetCore 8 | { 9 | public class TypedRoutingOptions 10 | { 11 | internal Dictionary> Routes = new Dictionary>(); 12 | 13 | public TypedRoute Get(string template, Action configSetup) 14 | { 15 | return AddRoute(template, configSetup).ForHttpMethods("GET"); 16 | } 17 | 18 | public TypedRoute Post(string template, Action configSetup) 19 | { 20 | return AddRoute(template, configSetup).ForHttpMethods("POST"); 21 | } 22 | 23 | public TypedRoute Put(string template, Action configSetup) 24 | { 25 | return AddRoute(template, configSetup).ForHttpMethods("PUT"); 26 | } 27 | 28 | public TypedRoute Delete(string template, Action configSetup) 29 | { 30 | return AddRoute(template, configSetup).ForHttpMethods("DELETE"); 31 | } 32 | 33 | public TypedRoute Route(string template, Action configSetup) 34 | { 35 | return AddRoute(template, configSetup); 36 | } 37 | 38 | private TypedRoute AddRoute(string template, Action configSetup) 39 | { 40 | var route = new TypedRoute(template); 41 | configSetup(route); 42 | 43 | if (Routes.ContainsKey(route.ControllerType)) 44 | { 45 | var controllerActions = Routes[route.ControllerType]; 46 | controllerActions.Add(route); 47 | } 48 | else 49 | { 50 | var controllerActions = new List { route }; 51 | Routes.Add(route.ControllerType, controllerActions); 52 | } 53 | 54 | return route; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Strathweb.TypedRouting.AspNetCore/TypedRoutingOptionsSetup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.Options; 6 | 7 | namespace Strathweb.TypedRouting.AspNetCore 8 | { 9 | public class TypedRoutingOptionsSetup : IConfigureOptions 10 | { 11 | private readonly IServiceProvider _serviceProvider; 12 | private readonly TypedRoutingOptions _options; 13 | 14 | public TypedRoutingOptionsSetup(IServiceProvider serviceProvider, TypedRoutingOptions options) 15 | { 16 | _serviceProvider = serviceProvider; 17 | _options = options; 18 | } 19 | 20 | public void Configure(MvcOptions options) 21 | { 22 | options.Conventions.Add(new TypedRoutingApplicationModelConvention(_serviceProvider, _options)); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /strathweb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipw/Strathweb.TypedRouting.AspNetCore/d6312ad8269a46230fa28b04b6e5c90c30bd336d/strathweb.png -------------------------------------------------------------------------------- /tests/Strathweb.TypedRouting.AspNetCore.Tests/IntegrationTests.cs: -------------------------------------------------------------------------------- 1 | using Demo; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.TestHost; 4 | using Newtonsoft.Json; 5 | using System; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Net; 9 | using System.Net.Http; 10 | using System.Net.Http.Headers; 11 | using System.Threading.Tasks; 12 | using Xunit; 13 | 14 | namespace Strathweb.TypedRouting.AspNetCore.Tests 15 | { 16 | public class IntegrationTests 17 | { 18 | private TestServer _server; 19 | 20 | public IntegrationTests() 21 | { 22 | //TypedRoutingApplicationModelConvention.Routes.Clear(); 23 | _server = new TestServer(new WebHostBuilder() 24 | .UseContentRoot(Directory.GetCurrentDirectory()) 25 | .UseStartup()); 26 | } 27 | 28 | [Fact] 29 | public async Task Get_List() 30 | { 31 | var client = _server.CreateClient(); 32 | 33 | var request = new HttpRequestMessage(HttpMethod.Get, "api/items"); 34 | var result = await client.SendAsync(request); 35 | var items = JsonConvert.DeserializeObject(await result.Content.ReadAsStringAsync()); 36 | 37 | Assert.Equal(2, items.Length); 38 | Assert.Equal("value1", items[0].Text); 39 | Assert.Equal("value2", items[1].Text); 40 | } 41 | 42 | [Fact] 43 | public async Task Filter_From_Instance() 44 | { 45 | var client = _server.CreateClient(); 46 | var request = new HttpRequestMessage(HttpMethod.Get, "api/items"); 47 | var result = await client.SendAsync(request); 48 | 49 | Assert.Equal("Demo.AnnotationFilter", result.Headers.GetValues("FilterBefore").FirstOrDefault()); 50 | Assert.Equal("Demo.AnnotationFilter", result.Headers.GetValues("FilterAfter").FirstOrDefault()); 51 | } 52 | 53 | [Fact] 54 | public async Task Get_ById() 55 | { 56 | var client = _server.CreateClient(); 57 | 58 | var request = new HttpRequestMessage(HttpMethod.Get, "api/items/7"); 59 | var result = await client.SendAsync(request); 60 | var item = JsonConvert.DeserializeObject(await result.Content.ReadAsStringAsync()); 61 | 62 | Assert.NotNull(item); 63 | Assert.Equal("value", item.Text); 64 | } 65 | 66 | [Fact] 67 | public async Task Filter_From_DI() 68 | { 69 | var client = _server.CreateClient(); 70 | 71 | var request = new HttpRequestMessage(HttpMethod.Get, "api/items/7"); 72 | var result = await client.SendAsync(request); 73 | 74 | Assert.Equal("Demo.AnnotationFilter", result.Headers.GetValues("FilterBefore").FirstOrDefault()); 75 | Assert.Equal("Demo.AnnotationFilter", result.Headers.GetValues("FilterAfter").FirstOrDefault()); 76 | } 77 | 78 | [Fact] 79 | public async Task AuthorizationPolicy_DefineViaString() 80 | { 81 | var client = _server.CreateClient(); 82 | 83 | var request = new HttpRequestMessage(HttpMethod.Get, "api/secure_string"); 84 | var result = await client.SendAsync(request); 85 | 86 | Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode); 87 | } 88 | 89 | [Fact] 90 | public async Task AuthorizationPolicy_DefineViaPolicyInstance() 91 | { 92 | var client = _server.CreateClient(); 93 | 94 | var request = new HttpRequestMessage(HttpMethod.Get, "api/secure_instance"); 95 | var result = await client.SendAsync(request); 96 | 97 | Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode); 98 | } 99 | 100 | [Fact] 101 | public async Task Post() 102 | { 103 | var client = _server.CreateClient(); 104 | 105 | var item = new Item { Text = "foo" }; 106 | var request = new HttpRequestMessage(HttpMethod.Post, "api/items"); 107 | request.Content = new StringContent(JsonConvert.SerializeObject(item)); 108 | request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); 109 | 110 | var result = await client.SendAsync(request); 111 | var echoItem = JsonConvert.DeserializeObject(await result.Content.ReadAsStringAsync()); 112 | 113 | Assert.NotNull(echoItem); 114 | Assert.Equal(item.Text, echoItem.Text); 115 | } 116 | 117 | [Fact] 118 | public async Task Put() 119 | { 120 | var client = _server.CreateClient(); 121 | 122 | var item = new Item { Text = "foo" }; 123 | var request = new HttpRequestMessage(HttpMethod.Put, "api/items/10"); 124 | request.Content = new StringContent(JsonConvert.SerializeObject(item)); 125 | request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); 126 | 127 | var result = await client.SendAsync(request); 128 | var echoItem = JsonConvert.DeserializeObject(await result.Content.ReadAsStringAsync()); 129 | 130 | Assert.NotNull(echoItem); 131 | Assert.Equal(item.Text, echoItem.Text); 132 | } 133 | 134 | [Fact] 135 | public async Task Delete() 136 | { 137 | var client = _server.CreateClient(); 138 | 139 | var request = new HttpRequestMessage(HttpMethod.Delete, "api/items/10"); 140 | 141 | var result = await client.SendAsync(request); 142 | var response = await result.Content.ReadAsStringAsync(); 143 | 144 | Assert.NotNull(response); 145 | Assert.Equal("10", response); 146 | } 147 | 148 | [Fact] 149 | public async Task ApiOther_WithHeader() 150 | { 151 | var client = _server.CreateClient(); 152 | 153 | var request = new HttpRequestMessage(HttpMethod.Get, "api/other"); 154 | request.Headers.Add("CustomHeader", "abc"); 155 | 156 | var result = await client.SendAsync(request); 157 | var response = await result.Content.ReadAsStringAsync(); 158 | 159 | Assert.NotNull(response); 160 | Assert.Equal("bar", response); 161 | } 162 | 163 | [Fact] 164 | public async Task ApiOther_WithoutHeader() 165 | { 166 | var client = _server.CreateClient(); 167 | 168 | var request = new HttpRequestMessage(HttpMethod.Get, "api/other"); 169 | var result = await client.SendAsync(request); 170 | 171 | Assert.Equal(HttpStatusCode.NotFound, result.StatusCode); 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /tests/Strathweb.TypedRouting.AspNetCore.Tests/Strathweb.TypedRouting.AspNetCore.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | --------------------------------------------------------------------------------