├── src └── LightweightSiteSample │ ├── Views │ ├── _ViewImports.cshtml │ ├── Index.cshtml │ ├── _Layout.cshtml │ ├── WebApiContrib.cshtml │ └── FormatFilter.cshtml │ ├── Startup.cs │ ├── web.config │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── project.json │ ├── LightweightSiteSample.xproj │ └── wwwroot │ └── css │ └── splendor.min.css ├── global.json ├── README.md ├── LICENSE ├── LightweightSiteSample.sln └── .gitignore /src/LightweightSiteSample/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @addTagHelper "*, WebApiContrib.Core.TagHelpers.Markdown" -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ "src", "test" ], 3 | "sdk": { 4 | "version": "1.0.0-preview2-003121" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lightweight ASP.NET Core Sample Site 2 | 3 | Blog post: http://www.strathweb.com/2016/08/building-a-lightweight-controller-less-markdown-only-website-in-asp-net-core/ 4 | -------------------------------------------------------------------------------- /src/LightweightSiteSample/Views/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | ViewBag.Title = "Markdown ASP.NET Core site"; 4 | ViewBag.HideBackLink = true; 5 | } 6 | 7 | 8 | # List of blog posts 9 | 10 | - [Announcing WebApiContrib](/WebApiContrib) 11 | - [Customizing FormatFilter behavior](/FormatFilter) 12 | -------------------------------------------------------------------------------- /src/LightweightSiteSample/Views/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @ViewBag.Title 5 | 6 | 7 | 8 | @RenderBody() 9 | @if (!ViewBag.HideBackLink) 10 | { 11 |

12 | Back to Home 13 |

14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/LightweightSiteSample/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using WebApiContrib.Core.WebPages; 4 | 5 | namespace LightweightSiteSample 6 | { 7 | public class Startup 8 | { 9 | public void ConfigureServices(IServiceCollection services) 10 | { 11 | services.AddWebPages(new WebPagesOptions { RootViewName = "Index" }); 12 | } 13 | 14 | public void Configure(IApplicationBuilder app) 15 | { 16 | app.UseStaticFiles(); 17 | app.UseWebPages(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/LightweightSiteSample/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/LightweightSiteSample/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 | 8 | namespace LightweightSiteSample 9 | { 10 | public class Program 11 | { 12 | public static void Main(string[] args) 13 | { 14 | var host = new WebHostBuilder() 15 | .UseKestrel() 16 | .UseContentRoot(Directory.GetCurrentDirectory()) 17 | .UseIISIntegration() 18 | .UseStartup() 19 | .Build(); 20 | 21 | host.Run(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/LightweightSiteSample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:31614/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "LightweightSiteSample": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "launchUrl": "http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 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 | -------------------------------------------------------------------------------- /src/LightweightSiteSample/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "Microsoft.NETCore.App": { 4 | "version": "1.0.0", 5 | "type": "platform" 6 | }, 7 | "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", 8 | "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", 9 | "Microsoft.AspNetCore.StaticFiles": "1.0.0", 10 | "WebApiContrib.Core.WebPages": "1.0.1", 11 | "WebApiContrib.Core.TagHelpers.Markdown": "1.0.0" 12 | }, 13 | 14 | "tools": { 15 | "Microsoft.AspNetCore.Server.IISIntegration.Tools": { 16 | "version": "1.0.0-preview2-final", 17 | "imports": "portable-net45+win8+dnxcore50" 18 | } 19 | }, 20 | 21 | "frameworks": { 22 | "netcoreapp1.0": { 23 | "imports": [ 24 | "dotnet5.6", 25 | "dnxcore50", 26 | "portable-net45+win8" 27 | ] 28 | } 29 | }, 30 | 31 | "buildOptions": { 32 | "emitEntryPoint": true, 33 | "preserveCompilationContext": true 34 | }, 35 | 36 | "runtimeOptions": { 37 | "gcServer": true 38 | }, 39 | 40 | "publishOptions": { 41 | "include": [ 42 | "wwwroot", 43 | "web.config", 44 | "Views" 45 | ] 46 | }, 47 | 48 | "scripts": { 49 | "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/LightweightSiteSample/LightweightSiteSample.xproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 10 | f6c384b7-2be3-4750-a337-56a2554eecc3 11 | LightweightSiteSample 12 | .\obj 13 | .\bin\ 14 | v4.5.2 15 | 16 | 17 | 18 | 2.0 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LightweightSiteSample.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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0B9E05C5-8A38-440C-A628-A797FACF4483}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{86E2E3BF-E7BB-4448-94E7-BBC323DACE30}" 9 | ProjectSection(SolutionItems) = preProject 10 | global.json = global.json 11 | EndProjectSection 12 | EndProject 13 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "LightweightSiteSample", "src\LightweightSiteSample\LightweightSiteSample.xproj", "{F6C384B7-2BE3-4750-A337-56A2554EECC3}" 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 | {F6C384B7-2BE3-4750-A337-56A2554EECC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {F6C384B7-2BE3-4750-A337-56A2554EECC3}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {F6C384B7-2BE3-4750-A337-56A2554EECC3}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {F6C384B7-2BE3-4750-A337-56A2554EECC3}.Release|Any CPU.Build.0 = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(SolutionProperties) = preSolution 27 | HideSolutionNode = FALSE 28 | EndGlobalSection 29 | GlobalSection(NestedProjects) = preSolution 30 | {F6C384B7-2BE3-4750-A337-56A2554EECC3} = {0B9E05C5-8A38-440C-A628-A797FACF4483} 31 | EndGlobalSection 32 | EndGlobal 33 | -------------------------------------------------------------------------------- /src/LightweightSiteSample/wwwroot/css/splendor.min.css: -------------------------------------------------------------------------------- 1 | @media print{*,:after,:before{background:0 0!important;color:#000!important;box-shadow:none!important;text-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}}@media screen and (min-width:32rem) and (max-width:48rem){html{font-size:15px}}@media screen and (min-width:48rem){html{font-size:16px}}body{line-height:1.85}.splendor-p,p{font-size:1rem;margin-bottom:1.3rem}.splendor-h1,.splendor-h2,.splendor-h3,.splendor-h4,h1,h2,h3,h4{margin:1.414rem 0 .5rem;font-weight:inherit;line-height:1.42}.splendor-h1,h1{margin-top:0;font-size:3.998rem}.splendor-h2,h2{font-size:2.827rem}.splendor-h3,h3{font-size:1.999rem}.splendor-h4,h4{font-size:1.414rem}.splendor-h5,h5{font-size:1.121rem}.splendor-h6,h6{font-size:.88rem}.splendor-small,small{font-size:.707em}canvas,iframe,img,select,svg,textarea,video{max-width:100%}@import url(http://fonts.googleapis.com/css?family=Merriweather:300italic,300);html{font-size:18px;max-width:100%}body{color:#444;font-family:Merriweather,Georgia,serif;margin:0;max-width:100%}:not(div):not(img):not(body):not(html):not(li):not(blockquote):not(p),p{margin:1rem auto;max-width:36rem;padding:.25rem}div,div img{width:100%}blockquote p{font-size:1.5rem;font-style:italic;margin:1rem auto;max-width:48rem}li{margin-left:2rem}h1{padding:4rem 0!important}p{color:#555;height:auto;line-height:1.45}code,pre{font-family:Menlo,Monaco,"Courier New",monospace}pre{background-color:#fafafa;font-size:.8rem;overflow-x:scroll;padding:1.125em}a,a:visited{color:#3498db}a:active,a:focus,a:hover{color:#2980b9} 2 | -------------------------------------------------------------------------------- /src/LightweightSiteSample/Views/WebApiContrib.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | var title = $"Announcing WebApiContrib for ASP.NET Core!"; 4 | 5 | ViewBag.Title = title; 6 | ViewBag.HideBackLink = false; 7 | } 8 | 9 | # @title 10 | 11 | In the past, a [bunch of us](https://github.com/orgs/WebApiContrib/people) from the ASP.NET Web API community worked together on a WebApiContrib project (or really, *projects*, cause there were many of them!). 12 | 13 | The idea was to provide an easy to use platform, a one stop place for community contributions for ASP.NET Web API - both larger add ons, such as HTML/Razor support for Web API, as well as smaller things like i.e. reusable filters or even helper methods. This worked extremely well - [WebApiContrib packages](https://www.nuget.org/packages?q=Tags%3A"WebApiContrib") were downloaded over 500k times on Nuget, and a nice community has emerged around the project on [Github](https://github.com/WebApiContrib). 14 | 15 | Recently, we decided to restart the project, this time focusing on ASP.NET Core. Since the "brand" has caught on in the community and is fairly recognizable, we just called it [WebApiContrib.Core](https://github.com/WebApiContrib/WebAPIContrib.Core). 16 | 17 | 18 | There is already a bunch of things there: 19 | 20 | - Main package containing i.e. *GlobalRoutePrefixConvention*, *FromBodyApplicationModelConvention* or *OverridableFilterProvider* 21 | - BSON formatter 22 | - CSV formatter 23 | - JSONP formatter 24 | - PlainText formatter 25 | - Markdown tag helper 26 | - WebPages functionality (Razor pages without controllers!) 27 | - Confitional requests support based on RFC-7232 28 | 29 | And it's growing quickly. There are several packages pushed to Nuget already too. 30 | 31 | It would be wonderful if you would like to particpiate in this community effort - I am sure alomst everyone working on ASP.NET Core would have something interesting to contribute. 32 | 33 | If you would like to join the conversation, you can use [this link](https://webapicontrib.azurewebsites.net) to join the slack channel. The repo is [located here on Github](https://github.com/WebApiContrib/WebAPIContrib.Core). 34 | 35 | Thanks! 36 | -------------------------------------------------------------------------------- /.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 | /src/LightweightSiteSample/Properties/PublishProfiles 254 | -------------------------------------------------------------------------------- /src/LightweightSiteSample/Views/FormatFilter.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | var title = "Customizing FormatFilter behavior in ASP.NET Core MVC 1.0"; 4 | 5 | ViewBag.Title = title; 6 | ViewBag.HideBackLink = false; 7 | } 8 | 9 | # @title 10 | 11 | When building HTTP APIs with ASP.NET Core MVC , the framework allows you to use *FormatFilter* to let the calling client override any content negotiation that might have happened on the server side. 12 | 13 | This way, the client can for example force the return data to be JSON or CSV or any other format suitable (as long as the server supports it, of course). 14 | 15 | The built in mechanism (out of the box version of *FormatFilter*) is a little limited, so let's have a look at how you can extend and customize its behavior. 16 | 17 | ### A little FormatFilter background 18 | 19 | I [already blogged about FormatFilter](http://www.strathweb.com/2016/02/formatfilter-and-mediatypemappings-in-asp-net-core-1-0-mvc/) a few months ago. 20 | 21 | As a reminder, *FormatFilter* allows you to use querystring value or a route value to override content negotiation. 22 | 23 | In the example from the old blog post: 24 | 25 | ``` 26 | [FormatFilter] 27 | public class BooksController : Controller 28 | { 29 | [Route("[controller]/{id}.{format?}")] 30 | public Book GetProduct(int id) 31 | { 32 | return new Book { Id = id, Title = "foo"}; 33 | } 34 | } 35 | ``` 36 | 37 | In this case, format filter would allow us to request this resource the following way: 38 | 39 | ``` 40 | GET http://my.api.com/books/1 41 | GET http://my.api.com/books/1?format=xml 42 | GET http://my.api.com/books/1.xml 43 | ``` 44 | 45 | In the first example, the content negotiation process would run normally, so the server would consider the `Accept` header of the request and determine the response media type that way. 46 | 47 | In the latter two examples, the presence of *FormatFilter* on the controller (it could also be applied on the action), would force the response to pick up a formatter from the *FormatterMappings*, using the *xml* key. 48 | 49 | At this point you may ask what are formatter mappings - again this was covered [in the old post](http://www.strathweb.com/2016/02/formatfilter-and-mediatypemappings-in-asp-net-core-1-0-mvc/), but in short, formatter mappings let you bind a specific media type to a specific string value. For example: 50 | 51 | public void ConfigureServices(IServiceCollection services) 52 | { 53 | var mvcBuilder = services.AddMvc(opt => 54 | { 55 | opt.FormatterMappings.SetMediaTypeMappingForFormat("xml", new MediaTypeHeaderValue("application/xml")); 56 | }); 57 | } 58 | 59 | This sample configuration ensures that if the client passes *xml* as the *format* key - in the route data or in querystring, the MVC will always respond with *application/xml* media type. Remember it's necessary to decorate a controller/action with *[FormatFilter]* - setting the formatter mappings will not have any effect without that. 60 | 61 | Out of the box, *FormatFilter* has got both the *format* key, and the fact that it looks at querystring and route data, hardcoded - meaning you cannot use a different word and you cannot obtain the format in any other way. 62 | 63 | This is where the customization process kicks in. 64 | 65 | ### Customizing FormatFilter behavior 66 | 67 | As a interesting (useless?) piece of trivia, I may mention, that the fact that we can customize the *FormatFilter* at all is thanks to this monumental 8-character [pull request](https://github.com/aspnet/Mvc/pull/4483) I sent to MVC a while ago. 68 | 69 | With this change, we can now subclass *FormatFilter* and introduce our custom logic. 70 | 71 | Let's imagine we want to use *dataType* key instead of *format* and we would like to allow the client to also pass this information in the header - instead of just route data and query string. 72 | 73 | This is shown in the next. First let's define some extension methods to extra route data, query string and header values from *ActionContext*. 74 | 75 | public static class HttpRequestExtensions 76 | { 77 | public static string GetValueFromRouteData(this ActionContext context, string key) 78 | { 79 | object value; 80 | if (context.RouteData.Values.TryGetValue(key, out value)) 81 | { 82 | var routeValue = value?.ToString(); 83 | return string.IsNullOrEmpty(routeValue) ? null : routeValue; 84 | } 85 | 86 | return null; 87 | } 88 | 89 | public static string GetValueFromQueryString(this ActionContext context, string key) 90 | { 91 | StringValues queryValue; 92 | if (context.HttpContext.Request.Query.TryGetValue(key, out queryValue)) 93 | { 94 | return queryValue.ToString(); 95 | } 96 | 97 | return null; 98 | } 99 | 100 | public static string GetValueFromHeader(this ActionContext context, string key) 101 | { 102 | StringValues headerValue; 103 | if (context.HttpContext.Request.Headers.TryGetValue(key, out headerValue)) 104 | { 105 | return headerValue.ToString(); 106 | } 107 | 108 | return null; 109 | } 110 | } 111 | 112 | Now we can implement our custom filter. 113 | 114 | public class MyFormatFilter : FormatFilter 115 | { 116 | const string key = "DataType"; 117 | 118 | public MyFormatFilter(IOptions options) : base(options) 119 | { 120 | } 121 | 122 | public override string GetFormat(ActionContext context) 123 | { 124 | var format = context.GetValueFromHeader(key) ?? context.GetValueFromRouteData(key) ?? context.GetValueFromQueryString(key); 125 | return format; 126 | } 127 | } 128 | 129 | Which can then be registered in the ASP.NET services in place of the default implementation. 130 | 131 | public void ConfigureServices(IServiceCollection services) 132 | { 133 | services.AddMvc(opt => 134 | { 135 | opt.FormatterMappings.SetMediaTypeMappingForFormat("xml", new MediaTypeHeaderValue("application/xml")); 136 | }); 137 | services.Replace(ServiceDescriptor.Singleton()); 138 | } 139 | 140 | If we now look back at the original sample controller we had, we can access the book resource in XML in the following ways: 141 | 142 | 143 | ``` 144 | GET http://my.api.com/books/1?DataType=xml 145 | GET http://my.api.com/books/1.xml 146 | 147 | GET http://my.api.com/books/1 148 | DataType: xml /* this is a header */ 149 | ``` 150 | 151 | Of course you can customize it even further. For example, perhaps you would like to override media type per user? This is entirely possible: 152 | 153 | public class MyFormatFilter : FormatFilter 154 | { 155 | const string key = "DataType"; 156 | 157 | public MyFormatFilter(IOptions options) : base(options) 158 | { 159 | } 160 | 161 | public override string GetFormat(ActionContext context) 162 | { 163 | ClaimsPrincipal user = context.HttpContext.User; 164 | 165 | // set up format based on user identity here 166 | // i.e. based on your user's profile 167 | return format; 168 | } 169 | } 170 | 171 | There are many other customization scenarios here. Another good example is that you could now register *FormatFilter* globally - and use the custom *FormatFilter* to mute its behavior in the controllers/actions you do not wish it to be applied. 172 | --------------------------------------------------------------------------------