├── .gitattributes ├── .gitignore ├── .nuget ├── NuGet.Config ├── NuGet.exe └── NuGet.targets ├── Build.bat ├── Build.proj ├── License.txt ├── README.md ├── RunDebugBuild.bat ├── RunReleaseBuild.bat ├── WebAPI.OutputCache.sln ├── sample └── WebApi.OutputCache.V2.Demo │ ├── IgnoreController.cs │ ├── Program.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── Team.cs │ ├── Teams2Controller.cs │ ├── TeamsController.cs │ ├── WebApi.OutputCache.V2.Demo.csproj │ ├── app.config │ └── packages.config ├── src ├── WebApi.OutputCache.Core │ ├── Cache │ │ ├── CacheExtensions.cs │ │ ├── IApiOutputCache.cs │ │ └── MemoryCacheDefault.cs │ ├── Constants.cs │ ├── IModelQuery.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Time │ │ ├── CacheTime.cs │ │ ├── ShortTime.cs │ │ ├── SpecificTime.cs │ │ ├── ThisDay.cs │ │ ├── ThisMonth.cs │ │ └── ThisYear.cs │ └── WebApi.OutputCache.Core.csproj └── WebApi.OutputCache.V2 │ ├── .gitignore │ ├── AutoInvalidateCacheOutputAttribute.cs │ ├── BaseCacheAttribute.cs │ ├── CacheOutputAttribute.cs │ ├── CacheOutputConfiguration.cs │ ├── DefaultCacheKeyGenerator.cs │ ├── HttpConfigurationExtensions.cs │ ├── ICacheKeyGenerator.cs │ ├── IgnoreCacheOutputAttribute.cs │ ├── InvalidateCacheOutputAttribute.cs │ ├── PerUserCacheKeyGenerator.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── TimeAttributes │ ├── CacheOutputUntilCacheAttribute.cs │ ├── CacheOutputUntilThisMonthAttribute.cs │ ├── CacheOutputUntilThisYearAttribute.cs │ └── CacheOutputUntilToday.cs │ ├── WebApi.OutputCache.V2.csproj │ └── packages.config └── test ├── WebApi.OutputCache.Core.Tests ├── MemoryCacheDefaultTests.cs ├── Properties │ └── AssemblyInfo.cs ├── WebApi.OutputCache.Core.Tests.csproj └── packages.config └── WebApi.OutputCache.V2.Tests ├── .gitignore ├── CacheKeyGenerationTestsBase.cs ├── CacheKeyGeneratorRegistrationTests.cs ├── CacheKeyGeneratorTests.cs ├── ClientSideTests.cs ├── ConfigurationTests.cs ├── ConnegTests.cs ├── CustomHeadersContent.cs ├── CustomHeadersTests.cs ├── DefaultCacheKeyGeneratorTests.cs ├── InlineInvalidateTests.cs ├── InvalidateTests.cs ├── MemoryCacheForTests.cs ├── PerUserCacheKeyGeneratorTests.cs ├── Properties └── AssemblyInfo.cs ├── ServerSideTests.cs ├── TestControllers ├── AutoInvalidateController.cs ├── AutoInvalidateWithTypeController.cs ├── CacheKeyController.cs ├── CacheKeyGenerationController.cs ├── CustomHeadersController.cs ├── IgnoreController.cs ├── InlineInvalidateController.cs └── SampleController.cs ├── WebApi.OutputCache.V2.Tests.csproj ├── app.config └── packages.config /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs) 2 | [Bb]in/ 3 | [Oo]bj/ 4 | 5 | # mstest test results 6 | TestResults 7 | 8 | ## Ignore Visual Studio temporary files, build results, and 9 | ## files generated by popular Visual Studio add-ons. 10 | 11 | # User-specific files 12 | *.suo 13 | *.user 14 | *.sln.docstates 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Rr]elease/ 19 | x64/ 20 | *_i.c 21 | *_p.c 22 | *.ilk 23 | *.meta 24 | *.obj 25 | *.pch 26 | *.pdb 27 | *.pgc 28 | *.pgd 29 | *.rsp 30 | *.sbr 31 | *.tlb 32 | *.tli 33 | *.tlh 34 | *.tmp 35 | *.log 36 | *.vspscc 37 | *.vssscc 38 | .builds 39 | 40 | # Visual C++ cache files 41 | ipch/ 42 | *.aps 43 | *.ncb 44 | *.opensdf 45 | *.sdf 46 | 47 | # Visual Studio profiler 48 | *.psess 49 | *.vsp 50 | *.vspx 51 | 52 | # Guidance Automation Toolkit 53 | *.gpState 54 | 55 | # ReSharper is a .NET coding add-in 56 | _ReSharper* 57 | 58 | # NCrunch 59 | *.ncrunch* 60 | .*crunch*.local.xml 61 | 62 | # Installshield output folder 63 | [Ee]xpress 64 | 65 | # DocProject is a documentation generator add-in 66 | DocProject/buildhelp/ 67 | DocProject/Help/*.HxT 68 | DocProject/Help/*.HxC 69 | DocProject/Help/*.hhc 70 | DocProject/Help/*.hhk 71 | DocProject/Help/*.hhp 72 | DocProject/Help/Html2 73 | DocProject/Help/html 74 | 75 | # Click-Once directory 76 | publish 77 | 78 | # Publish Web Output 79 | *.Publish.xml 80 | 81 | # NuGet Packages Directory 82 | packages 83 | 84 | # Windows Azure Build Output 85 | csx 86 | *.build.csdef 87 | 88 | # Windows Store app package directory 89 | AppPackages/ 90 | 91 | # Others 92 | [Bb]in 93 | [Oo]bj 94 | sql 95 | TestResults 96 | [Tt]est[Rr]esult* 97 | *.Cache 98 | ClientBin 99 | [Ss]tyle[Cc]op.* 100 | ~$* 101 | *.dbmdl 102 | Generated_Code #added for RIA/Silverlight projects 103 | 104 | # Backup & report files from converting an old project file to a newer 105 | # Visual Studio version. Backup files are not needed, because we have git ;-) 106 | _UpgradeReport_Files/ 107 | Backup*/ 108 | UpgradeLog*.XML 109 | -------------------------------------------------------------------------------- /.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.nuget/NuGet.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipw/Strathweb.CacheOutput/d95c8a71a20b9a0b11c5d92f3a49f9dd6fbde58b/.nuget/NuGet.exe -------------------------------------------------------------------------------- /.nuget/NuGet.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\..\ 5 | 6 | 7 | false 8 | 9 | 10 | false 11 | 12 | 13 | true 14 | 15 | 16 | false 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) 31 | $([System.IO.Path]::Combine($(ProjectDir), "packages.config")) 32 | $([System.IO.Path]::Combine($(SolutionDir), "packages")) 33 | 34 | 35 | 36 | 37 | $(SolutionDir).nuget 38 | packages.config 39 | $(SolutionDir)packages 40 | 41 | 42 | 43 | 44 | $(NuGetToolsPath)\nuget.exe 45 | @(PackageSource) 46 | 47 | "$(NuGetExePath)" 48 | mono --runtime=v4.0.30319 $(NuGetExePath) 49 | 50 | $(TargetDir.Trim('\\')) 51 | 52 | -RequireConsent 53 | 54 | $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(RequireConsentSwitch) -o "$(PackagesDir)" 55 | $(NuGetCommand) pack "$(ProjectPath)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols 56 | 57 | 58 | 59 | RestorePackages; 60 | $(ResolveReferencesDependsOn); 61 | 62 | 63 | 64 | 65 | $(BuildDependsOn); 66 | BuildPackage; 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 89 | 90 | 93 | 94 | 95 | 96 | 98 | 99 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /Build.bat: -------------------------------------------------------------------------------- 1 | "c:/Program Files (x86)/Microsoft Visual Studio/2017/Enterprise/MSBuild/15.0/Bin/msbuild.exe" /m build.proj /t:%* -------------------------------------------------------------------------------- /Build.proj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WebApi.OutputCache 5 | C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE\mstest.exe 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | Copyright 2013 Filip Wojcieszyn, Alexandre Brisebois 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📢 This repo is no longer maintained 📢 2 | 3 | First of all, I apologize it took so long to make an announcement, but I think it has been clear to everyone from the update frequency that this repo is no longer maintained. 4 | 5 | I would therefore like to officially (re)state that, indeed, this repo and the corresponding library is no longer maintained. I will archive it soon. 6 | 7 | It served its purpose back in the ASP.NET Web API days, and I hope it helped some people build their products. The ASP.NET landscape is obviously much different now, and, given many other involvements, I do not have the capacity or energy to keep this legacy thing alive. I am sorry if anyone feels let down by this. If you are using it you and still need it, you are of course free to fork and make any changes you wish. 8 | 9 | I would like to thank almost 1k people who starred this repo and everyone who contributed to over 3 million downloads on Nuget. Thank you all. 10 | 11 | ASP.NET Web API CacheOutput 12 | ======================== 13 | 14 | A small library bringing caching options, similar to MVC's "OutputCacheAttribute", to Web API actions. 15 | 16 | **CacheOutput** will take care of server side caching and set the appropriate client side (response) headers for you. 17 | 18 | You can specify the following properties: 19 | - *ClientTimeSpan* (corresponds to CacheControl MaxAge HTTP header) 20 | - *MustRevalidate* (corresponds to MustRevalidate HTTP header - indicates whether the origin server requires revalidation of 21 | a cache entry on any subsequent use when the cache entry becomes stale) 22 | - *ExcludeQueryStringFromCacheKey* (do not vary cache by querystring values) 23 | - *ServerTimeSpan* (time how long the response should be cached on the server side) 24 | - *AnonymousOnly* (cache enabled only for requests when Thread.CurrentPrincipal is not set) 25 | 26 | Additionally, the library is setting ETags for you, and keeping them unchanged for the duration of the caching period. 27 | Caching by default can only be applied to GET actions. 28 | 29 | Installation 30 | -------------------- 31 | You can build from the source here, or you can install the Nuget version: 32 | 33 | For Web API 2 (.NET 4.5) 34 | 35 | PM> Install-Package Strathweb.CacheOutput.WebApi2 36 | 37 | For Web API 1 (.NET 4.0) 38 | 39 | PM> Install-Package Strathweb.CacheOutput 40 | 41 | 42 | Usage 43 | -------------------- 44 | 45 | ```csharp 46 | //Cache for 100 seconds on the server, inform the client that response is valid for 100 seconds 47 | [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 100)] 48 | public IEnumerable Get() 49 | { 50 | return new string[] { "value1", "value2" }; 51 | } 52 | 53 | //Cache for 100 seconds on the server, inform the client that response is valid for 100 seconds. Cache for anonymous users only. 54 | [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 100, AnonymousOnly = true)] 55 | public IEnumerable Get() 56 | { 57 | return new string[] { "value1", "value2" }; 58 | } 59 | 60 | //Inform the client that response is valid for 50 seconds. Force client to revalidate. 61 | [CacheOutput(ClientTimeSpan = 50, MustRevalidate = true)] 62 | public string Get(int id) 63 | { 64 | return "value"; 65 | } 66 | 67 | //Cache for 50 seconds on the server. Ignore querystring parameters when serving cached content. 68 | [CacheOutput(ServerTimeSpan = 50, ExcludeQueryStringFromCacheKey = true)] 69 | public string Get(int id) 70 | { 71 | return "value"; 72 | } 73 | ``` 74 | 75 | 76 | Variations 77 | -------------------- 78 | *CacheOutputUntil* is used to cache data until a specific moment in time. This applies to both client and server. 79 | 80 | ```csharp 81 | //Cache until 01/25/2013 17:00 82 | [CacheOutputUntil(2013,01,25,17,00)] 83 | public string Get_until25012013_1700() 84 | { 85 | return "test"; 86 | } 87 | ``` 88 | 89 | *CacheOutputUntilToday* is used to cache data until a specific hour later on the same day. This applies to both client and server. 90 | 91 | ```csharp 92 | //Cache until 23:55:00 today 93 | [CacheOutputUntilToday(23,55)] 94 | public string Get_until2355_today() 95 | { 96 | return "value"; 97 | } 98 | ``` 99 | 100 | *CacheOutputUntilThisMonth* is used to cache data until a specific point later this month. This applies to both client and server. 101 | 102 | ```csharp 103 | //Cache until the 31st day of the current month 104 | [CacheOutputUntilThisMonth(31)] 105 | public string Get_until31_thismonth() 106 | { 107 | return "value"; 108 | } 109 | ``` 110 | 111 | *CacheOutputUntilThisYear* is used to cache data until a specific point later this year. This applies to both client and server. 112 | 113 | ```csharp 114 | //Cache until the 31st of July this year 115 | [CacheOutputUntilThisYear(7,31)] 116 | public string Get_until731_thisyear() 117 | { 118 | return "value"; 119 | } 120 | ``` 121 | 122 | Each of these can obviously be combined with the 5 general properties mentioned in the beginning. 123 | 124 | Caching convention 125 | -------------------- 126 | In order to determine the expected content type of the response, **CacheOutput** will run Web APIs internal *content negotiation process*, based on the incoming request & the return type of the action on which caching is applied. 127 | 128 | Each individual content type response is cached separately (so out of the box, you can expect the action to be cached as JSON and XML, if you introduce more formatters, those will be cached as well). 129 | 130 | **Important**: We use *action name* as part of the key. Therefore it is *necessary* that action names are unique inside the controller - that's the only way we can provide consistency. 131 | 132 | So you either should use unique method names inside a single controller, or (if you really want to keep them the same names when overloading) you need to use *ActionName* attribute to provide uniqeness for caching. Example: 133 | 134 | ```csharp 135 | [CacheOutput(ClientTimeSpan = 50, ServerTimeSpan = 50)] 136 | public IEnumerable Get() 137 | { 138 | return Teams; 139 | } 140 | 141 | [ActionName("GetById")] 142 | [CacheOutput(ClientTimeSpan = 50, ServerTimeSpan = 50)] 143 | public IEnumerable Get(int id) 144 | { 145 | return Teams; 146 | } 147 | ``` 148 | 149 | If you want to bypass the content negotiation process, you can do so by using the `MediaType` property: 150 | 151 | ```csharp 152 | [CacheOutput(ClientTimeSpan = 50, ServerTimeSpan = 50, MediaType = "image/jpeg")] 153 | public HttpResponseMessage Get(int id) 154 | { 155 | var response = new HttpResponseMessage(HttpStatusCode.OK); 156 | response.Content = GetImage(id); // e.g. StreamContent, ByteArrayContent,... 157 | response.Content.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg"); 158 | return response; 159 | } 160 | ``` 161 | 162 | This will always return a response with `image/jpeg` as value for the `Content-Type` header. 163 | 164 | Ignoring caching 165 | -------------------- 166 | You can set up caching globally (add the caching filter to `HttpConfiguration`) or on controller level (decorate the controller with the cahcing attribute). This means that caching settings will cascade down to all the actions in your entire application (in the first case) or in the controller (in the second case). 167 | 168 | You can still instruct a specific action to opt out from caching by using `[IgnoreCacheOutput]` attribute. 169 | 170 | ```csharp 171 | [CacheOutput(ClientTimeSpan = 50, ServerTimeSpan = 50)] 172 | public class IgnoreController : ApiController 173 | { 174 | [Route("cached")] 175 | public string GetCached() 176 | { 177 | return DateTime.Now.ToString(); 178 | } 179 | 180 | [IgnoreCacheOutput] 181 | [Route("uncached")] 182 | public string GetUnCached() 183 | { 184 | return DateTime.Now.ToString(); 185 | } 186 | } 187 | ``` 188 | 189 | Server side caching 190 | -------------------- 191 | By default **CacheOutput** will use *System.Runtime.Caching.MemoryCache* to cache on the server side. However, you are free to swap this with anything else 192 | (static Dictionary, Memcached, Redis, whatever..) as long as you implement the following *IApiOutputCache* interface (part of the distributed assembly). 193 | 194 | ```csharp 195 | public interface IApiOutputCache 196 | { 197 | T Get(string key) where T : class; 198 | object Get(string key); 199 | void Remove(string key); 200 | void RemoveStartsWith(string key); 201 | bool Contains(string key); 202 | void Add(string key, object o, DateTimeOffset expiration, string dependsOnKey = null); 203 | } 204 | ``` 205 | 206 | Suppose you have a custom implementation: 207 | 208 | ```csharp 209 | public class MyCache : IApiOutputCache 210 | { 211 | // omitted for brevity 212 | } 213 | ``` 214 | 215 | You can register your implementation using a handy *GlobalConfiguration* extension method: 216 | 217 | ```csharp 218 | //instance 219 | configuration.CacheOutputConfiguration().RegisterCacheOutputProvider(() => new MyCache()); 220 | 221 | // singleton 222 | var cache = new MyCache(); 223 | configuration.CacheOutputConfiguration().RegisterCacheOutputProvider(() => cache); 224 | ``` 225 | 226 | If you prefer **CacheOutput** to use resolve the cache implementation directly from your dependency injection provider, that's also possible. Simply register your *IApiOutputCache* implementation in your Web API DI and that's it. Whenever **CacheOutput** does not find an implementation in the *GlobalConiguration*, it will fall back to the DI resolver. Example (using Autofac for Web API): 227 | 228 | ```csharp 229 | cache = new MyCache(); 230 | var builder = new ContainerBuilder(); 231 | builder.RegisterInstance(cache); 232 | config.DependencyResolver = new AutofacWebApiDependencyResolver(builder.Build()); 233 | ``` 234 | 235 | If no implementation is available in neither *GlobalConfiguration* or *DependencyResolver*, we will default to *System.Runtime.Caching.MemoryCache*. 236 | 237 | Each method can be cached multiple times separately - based on the representation (JSON, XML and so on). Therefore, *CacheOutput* will pass *dependsOnKey* value (which happens to be a prefix of all variations of a given cached method) when adding items to cache - this gives us flexibility to easily remove all variations of the cached method when we want to clear the cache. When cache gets invalidated, we will call *RemoveStartsWith* and just pass that key. 238 | 239 | The default cache store, *System.Runtime.Caching.MemoryCache* supports dependencies between cache items, so it's enough to just remove the main one, and all sub-dependencies get flushed. However, if you change the defalt implementation, and your underlying store doesn't - it's not a problem. When we invalidate cache (and need to cascade through all dependencies), we call *RemoveStartsWith* - so your custom store will just have to iterate through the entire store in the implementation of this method and remove all items with the prefix passed. 240 | 241 | Cache invalidation 242 | -------------------- 243 | 244 | There are three ways to invalidate cache: 245 | 246 | - [AutoInvalidateCacheOutput] - on the controller level (through an attribute) 247 | - [InvalidateCacheOutput("ActionName")] - on the action level (through an attribute) 248 | - Manually - inside the action body 249 | 250 | Example: 251 | 252 | ```csharp 253 | [AutoInvalidateCacheOutput] 254 | public class Teams2Controller : ApiController 255 | { 256 | [CacheOutput(ClientTimeSpan = 50, ServerTimeSpan = 50)] 257 | public IEnumerable Get() 258 | { 259 | return Teams; 260 | } 261 | 262 | public void Post(Team value) 263 | { 264 | //do stuff 265 | } 266 | } 267 | ``` 268 | 269 | Decorating the controller with [AutoInvalidateCacheOutput] will automatically flush all cached *GET* data from this controller after a successfull *POST*/*PUT*/*DELETE* request. 270 | 271 | You can also use the [AutoInvalidateCacheOutput(TryMatchType = true)] variation. This will only invalidate such *GET* requests that return the same *Type* or *IEnumerable of Type* as the action peformed takes as input parameter. 272 | 273 | For example: 274 | 275 | ```csharp 276 | [AutoInvalidateCacheOutput(TryMatchType = true)] 277 | public class TeamsController : ApiController 278 | { 279 | [CacheOutput(ClientTimeSpan = 50, ServerTimeSpan = 50)] 280 | public IEnumerable Get() 281 | { 282 | return Teams; 283 | } 284 | 285 | [CacheOutput(ClientTimeSpan = 50, ServerTimeSpan = 50)] 286 | public IEnumerable GetTeamPlayers(int id) 287 | { 288 | //return something 289 | } 290 | 291 | public void Post(Team value) 292 | { 293 | //this will only invalidate Get, not GetTeamPlayers since TryMatchType is enabled 294 | } 295 | } 296 | ``` 297 | 298 | Invalidation on action level is similar - done through attributes. For example: 299 | 300 | ```csharp 301 | public class TeamsController : ApiController 302 | { 303 | [CacheOutput(ClientTimeSpan = 50, ServerTimeSpan = 50)] 304 | public IEnumerable Get() 305 | { 306 | return Teams; 307 | } 308 | 309 | [CacheOutput(ClientTimeSpan = 50, ServerTimeSpan = 50)] 310 | public IEnumerable GetTeamPlayers(int id) 311 | { 312 | //return something 313 | } 314 | 315 | [InvalidateCacheOutput("Get")] 316 | public void Post(Team value) 317 | { 318 | //this invalidates Get action cache 319 | } 320 | } 321 | ``` 322 | 323 | Obviously, multiple attributes are supported. You can also invalidate methods from separate controller: 324 | 325 | ```csharp 326 | [InvalidateCacheOutput("Get", typeof(OtherController))] //this will invalidate Get in a different controller 327 | [InvalidateCacheOutput("Get")] //this will invalidate Get in this controller 328 | public void Post(Team value) 329 | { 330 | //do stuff 331 | } 332 | ``` 333 | 334 | Finally, you can also invalidate manually. For example: 335 | 336 | ```csharp 337 | public void Put(int id, Team value) 338 | { 339 | // do stuff, update resource etc. 340 | 341 | // now get cache instance 342 | var cache = Configuration.CacheOutputConfiguration().GetCacheOutputProvider(Request); 343 | 344 | // and invalidate cache for method "Get" of "TeamsController" 345 | cache.RemoveStartsWith(Configuration.CacheOutputConfiguration().MakeBaseCachekey((TeamsController t) => t.Get())); 346 | } 347 | ``` 348 | 349 | As you see, you can we use expression try to allow you to point to the method in a strongly typed way (we can't unfortunately do that in the attributes, since C# doesn't support lambdas/expression trees in attributes). 350 | 351 | If your method takes in arguments, you can pass whatever - we only use the expression tree to get the name of the controller and the name of the action - and we invalidate all variations. 352 | 353 | You can also point to the method in a traditional way: 354 | 355 | ```csharp 356 | cache.RemoveStartsWith(Configuration.CacheOutputConfiguration().MakeBaseCachekey("TeamsController", "Get")); 357 | ``` 358 | 359 | Customizing the cache keys 360 | -------------------------- 361 | 362 | You can provide your own cache key generator. To do this, you need to implement the `ICacheKeyGenerator` interface. The default implementation should suffice in most situations. 363 | 364 | When implementing, it is easiest to inherit your custom generator from the `DefaultCacheKeyGenerator` class. 365 | 366 | To set your custom implementation as the default, you can do one of these things: 367 | 368 | // Method A: register directly 369 | Configuration.CacheOutputConfiguration().RegisterDefaultCacheKeyGeneratorProvider(() => new CustomCacheKeyGenerator()); 370 | 371 | // Method B: register for DI (AutoFac example, the key is to register it as the default ICacheKeyGenerator) 372 | builder.RegisterInstance(new CustomCacheKeyGenerator()).As(); // this will be default 373 | builder.RegisterType(); // this will be available, and constructed using dependency injection 374 | 375 | You can set a specific cache key generator for an action, using the `CacheKeyGenerator` property: 376 | 377 | [CacheOutput(CacheKeyGenerator=typeof(SuperNiceCacheKeyGenerator))] 378 | 379 | PS! If you need dependency injection in your custom cache key generator, register it with your DI *as itself*. 380 | 381 | This works for unregistered generators if they have a parameterless constructor, or with dependency injection if they are registered with your DI. 382 | 383 | Finding a matching cache key generator is done in this order: 384 | 385 | 1. Internal registration using `RegisterCacheKeyGeneratorProvider` or `RegisterDefaultCacheKeyGeneratorProvider`. 386 | 2. Dependency injection. 387 | 3. Parameterless constructor of unregistered classes. 388 | 4. `DefaultCacheKeyGenerator` 389 | 390 | 391 | JSONP 392 | -------------------- 393 | We automatically exclude *callback* parameter from cache key to allow for smooth JSONP support. 394 | 395 | So: 396 | 397 | /api/something?abc=1&callback=jQuery1213 398 | 399 | is cached as: 400 | 401 | /api/something?abc=1 402 | 403 | Position of the *callback* parameter does not matter. 404 | 405 | Etags 406 | -------------------- 407 | For client side caching, in addition to *MaxAge*, we will issue Etags. You can use the Etag value to make a request with *If-None-Match* header. If the resource is still valid, server will then response with a 304 status code. 408 | 409 | For example: 410 | 411 | GET /api/myresource 412 | Accept: application/json 413 | 414 | Status Code: 200 415 | Cache-Control: max-age=100 416 | Content-Length: 24 417 | Content-Type: application/json; charset=utf-8 418 | Date: Fri, 25 Jan 2013 03:37:11 GMT 419 | ETag: "5c479911-97b9-4b78-ae3e-d09db420d5ba" 420 | Server: Microsoft-HTTPAPI/2.0 421 | 422 | On the next request: 423 | 424 | GET /api/myresource 425 | Accept: application/json 426 | If-None-Match: "5c479911-97b9-4b78-ae3e-d09db420d5ba" 427 | 428 | Status Code: 304 429 | Cache-Control: max-age=100 430 | Content-Length: 0 431 | Date: Fri, 25 Jan 2013 03:37:13 GMT 432 | Server: Microsoft-HTTPAPI/2.0 433 | 434 | License 435 | -------------------- 436 | 437 | Licensed under Apache v2. License included. 438 | -------------------------------------------------------------------------------- /RunDebugBuild.bat: -------------------------------------------------------------------------------- 1 | build.bat DebugBuild & pause 2 | -------------------------------------------------------------------------------- /RunReleaseBuild.bat: -------------------------------------------------------------------------------- 1 | build.bat ReleaseBuild & pause 2 | -------------------------------------------------------------------------------- /WebAPI.OutputCache.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.22823.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{944DCBA5-777A-4BE8-A1A3-1EA0924D8B70}" 7 | ProjectSection(SolutionItems) = preProject 8 | .nuget\NuGet.Config = .nuget\NuGet.Config 9 | .nuget\NuGet.exe = .nuget\NuGet.exe 10 | .nuget\NuGet.targets = .nuget\NuGet.targets 11 | EndProjectSection 12 | EndProject 13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{CEA1F6A7-ACA0-4B5B-BEE0-23AEB7E30A7D}" 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{70946FFB-B575-4B2F-A174-3BDD93100097}" 16 | EndProject 17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "demo", "demo", "{1644F771-3A1F-40A4-80AA-CDB22B334F1F}" 18 | EndProject 19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.OutputCache.V2.Tests", "test\WebApi.OutputCache.V2.Tests\WebApi.OutputCache.V2.Tests.csproj", "{1267460B-C100-48AD-B2E2-9DDE2E40F052}" 20 | EndProject 21 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.OutputCache.V2", "src\WebApi.OutputCache.V2\WebApi.OutputCache.V2.csproj", "{7A6F57F6-38E1-4287-812E-AD7D1025BA5E}" 22 | EndProject 23 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.OutputCache.V2.Demo", "sample\WebApi.OutputCache.V2.Demo\WebApi.OutputCache.V2.Demo.csproj", "{FC585541-1F28-4D49-BCD9-1A08A90B2C66}" 24 | EndProject 25 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.OutputCache.Core", "src\WebApi.OutputCache.Core\WebApi.OutputCache.Core.csproj", "{3E45FA0B-C465-4DE9-9BC3-40A606B73E84}" 26 | EndProject 27 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.OutputCache.Core.Tests", "test\WebApi.OutputCache.Core.Tests\WebApi.OutputCache.Core.Tests.csproj", "{44F519D6-E825-442C-A112-B5C4404EAA44}" 28 | EndProject 29 | Global 30 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 31 | Debug|Any CPU = Debug|Any CPU 32 | Release|Any CPU = Release|Any CPU 33 | EndGlobalSection 34 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 35 | {1267460B-C100-48AD-B2E2-9DDE2E40F052}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {1267460B-C100-48AD-B2E2-9DDE2E40F052}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {1267460B-C100-48AD-B2E2-9DDE2E40F052}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {1267460B-C100-48AD-B2E2-9DDE2E40F052}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {7A6F57F6-38E1-4287-812E-AD7D1025BA5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {7A6F57F6-38E1-4287-812E-AD7D1025BA5E}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {7A6F57F6-38E1-4287-812E-AD7D1025BA5E}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {7A6F57F6-38E1-4287-812E-AD7D1025BA5E}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {FC585541-1F28-4D49-BCD9-1A08A90B2C66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {FC585541-1F28-4D49-BCD9-1A08A90B2C66}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {FC585541-1F28-4D49-BCD9-1A08A90B2C66}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {FC585541-1F28-4D49-BCD9-1A08A90B2C66}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {3E45FA0B-C465-4DE9-9BC3-40A606B73E84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {3E45FA0B-C465-4DE9-9BC3-40A606B73E84}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {3E45FA0B-C465-4DE9-9BC3-40A606B73E84}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {3E45FA0B-C465-4DE9-9BC3-40A606B73E84}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {44F519D6-E825-442C-A112-B5C4404EAA44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {44F519D6-E825-442C-A112-B5C4404EAA44}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {44F519D6-E825-442C-A112-B5C4404EAA44}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {44F519D6-E825-442C-A112-B5C4404EAA44}.Release|Any CPU.Build.0 = Release|Any CPU 55 | EndGlobalSection 56 | GlobalSection(SolutionProperties) = preSolution 57 | HideSolutionNode = FALSE 58 | EndGlobalSection 59 | GlobalSection(NestedProjects) = preSolution 60 | {1267460B-C100-48AD-B2E2-9DDE2E40F052} = {70946FFB-B575-4B2F-A174-3BDD93100097} 61 | {7A6F57F6-38E1-4287-812E-AD7D1025BA5E} = {CEA1F6A7-ACA0-4B5B-BEE0-23AEB7E30A7D} 62 | {FC585541-1F28-4D49-BCD9-1A08A90B2C66} = {1644F771-3A1F-40A4-80AA-CDB22B334F1F} 63 | {3E45FA0B-C465-4DE9-9BC3-40A606B73E84} = {CEA1F6A7-ACA0-4B5B-BEE0-23AEB7E30A7D} 64 | {44F519D6-E825-442C-A112-B5C4404EAA44} = {70946FFB-B575-4B2F-A174-3BDD93100097} 65 | EndGlobalSection 66 | EndGlobal 67 | -------------------------------------------------------------------------------- /sample/WebApi.OutputCache.V2.Demo/IgnoreController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Web.Http; 3 | 4 | namespace WebApi.OutputCache.V2.Demo 5 | { 6 | [CacheOutput(ClientTimeSpan = 50, ServerTimeSpan = 50)] 7 | [RoutePrefix("ignore")] 8 | public class IgnoreController : ApiController 9 | { 10 | [Route("cached")] 11 | public string GetCached() 12 | { 13 | return DateTime.Now.ToString(); 14 | } 15 | 16 | [IgnoreCacheOutput] 17 | [Route("uncached")] 18 | public string GetUnCached() 19 | { 20 | return DateTime.Now.ToString(); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /sample/WebApi.OutputCache.V2.Demo/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Web.Http; 3 | using System.Web.Http.SelfHost; 4 | using WebApi.OutputCache.Core.Cache; 5 | 6 | namespace WebApi.OutputCache.V2.Demo 7 | { 8 | class Program 9 | { 10 | static void Main(string[] args) 11 | { 12 | var config = new HttpSelfHostConfiguration("http://localhost:999"); 13 | config.MapHttpAttributeRoutes(); 14 | config.Routes.MapHttpRoute( 15 | name: "DefaultApi", 16 | routeTemplate: "api/{controller}/{id}", 17 | defaults: new { id = RouteParameter.Optional } 18 | ); 19 | var server = new HttpSelfHostServer(config); 20 | 21 | config.CacheOutputConfiguration().RegisterCacheOutputProvider(() => new MemoryCacheDefault()); 22 | 23 | server.OpenAsync().Wait(); 24 | 25 | Console.ReadKey(); 26 | 27 | server.CloseAsync().Wait(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sample/WebApi.OutputCache.V2.Demo/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("WebAPI.OutputCache.Demo")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("WebAPI.OutputCache.Demo")] 13 | [assembly: AssemblyCopyright("Copyright © 2013")] 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("b8e3c80f-067d-4fc7-a3ec-cf33384ae98d")] 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 | -------------------------------------------------------------------------------- /sample/WebApi.OutputCache.V2.Demo/Team.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.OutputCache.V2.Demo 2 | { 3 | public class Team 4 | { 5 | public int Id { get; set; } 6 | public string Name { get; set; } 7 | public string League { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /sample/WebApi.OutputCache.V2.Demo/Teams2Controller.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Web.Http; 6 | using WebApi.OutputCache.V2.TimeAttributes; 7 | 8 | namespace WebApi.OutputCache.V2.Demo 9 | { 10 | [AutoInvalidateCacheOutput] 11 | public class Teams2Controller : ApiController 12 | { 13 | private static readonly List Teams = new List 14 | { 15 | new Team {Id = 1, League = "NHL", Name = "Leafs"}, 16 | new Team {Id = 2, League = "NHL", Name = "Habs"}, 17 | }; 18 | 19 | [CacheOutput(ClientTimeSpan = 50, ServerTimeSpan = 50)] 20 | public IEnumerable Get() 21 | { 22 | return Teams; 23 | } 24 | 25 | [CacheOutputUntil(2014, 7, 20)] 26 | public Team GetById(int id) 27 | { 28 | var team = Teams.FirstOrDefault(i => i.Id == id); 29 | if (team == null) throw new HttpResponseException(HttpStatusCode.NotFound); 30 | 31 | return team; 32 | } 33 | 34 | public void Post(Team value) 35 | { 36 | if (!ModelState.IsValid) throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); 37 | Teams.Add(value); 38 | } 39 | 40 | public void Put(int id, Team value) 41 | { 42 | if (!ModelState.IsValid) throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); 43 | 44 | var team = Teams.FirstOrDefault(i => i.Id == id); 45 | if (team == null) throw new HttpResponseException(HttpStatusCode.NotFound); 46 | 47 | team.League = value.League; 48 | team.Name = value.Name; 49 | } 50 | 51 | public void Delete(int id) 52 | { 53 | var team = Teams.FirstOrDefault(i => i.Id == id); 54 | if (team == null) throw new HttpResponseException(HttpStatusCode.NotFound); 55 | 56 | Teams.Remove(team); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /sample/WebApi.OutputCache.V2.Demo/TeamsController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Web.Http; 6 | using WebApi.OutputCache.V2.TimeAttributes; 7 | 8 | namespace WebApi.OutputCache.V2.Demo 9 | { 10 | public class TeamsController : ApiController 11 | { 12 | private static readonly List Teams = new List 13 | { 14 | new Team {Id = 1, League = "NHL", Name = "Leafs"}, 15 | new Team {Id = 2, League = "NHL", Name = "Habs"}, 16 | }; 17 | 18 | [CacheOutput(ClientTimeSpan = 50, ServerTimeSpan = 50)] 19 | public IEnumerable Get() 20 | { 21 | return Teams; 22 | } 23 | 24 | [CacheOutputUntil(2016, 7, 20)] 25 | public Team GetById(int id) 26 | { 27 | var team = Teams.FirstOrDefault(i => i.Id == id); 28 | if (team == null) throw new HttpResponseException(HttpStatusCode.NotFound); 29 | 30 | return team; 31 | } 32 | 33 | [InvalidateCacheOutput("Get")] 34 | public void Post(Team value) 35 | { 36 | if (!ModelState.IsValid) throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); 37 | Teams.Add(value); 38 | } 39 | 40 | public void Put(int id, Team value) 41 | { 42 | if (!ModelState.IsValid) throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); 43 | 44 | var team = Teams.FirstOrDefault(i => i.Id == id); 45 | if (team == null) throw new HttpResponseException(HttpStatusCode.NotFound); 46 | 47 | team.League = value.League; 48 | team.Name = value.Name; 49 | 50 | var cache = Configuration.CacheOutputConfiguration().GetCacheOutputProvider(Request); 51 | cache.RemoveStartsWith(Configuration.CacheOutputConfiguration().MakeBaseCachekey((TeamsController t) => t.GetById(0))); 52 | } 53 | 54 | public void Delete(int id) 55 | { 56 | var team = Teams.FirstOrDefault(i => i.Id == id); 57 | if (team == null) throw new HttpResponseException(HttpStatusCode.NotFound); 58 | 59 | Teams.Remove(team); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /sample/WebApi.OutputCache.V2.Demo/WebApi.OutputCache.V2.Demo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {FC585541-1F28-4D49-BCD9-1A08A90B2C66} 8 | Exe 9 | Properties 10 | WebApi.OutputCache.V2.Demo 11 | WebApi.OutputCache.V2.Demo 12 | v4.5 13 | 512 14 | ..\ 15 | true 16 | 17 | 18 | 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | false 27 | 28 | 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | false 36 | 37 | 38 | 39 | 40 | 41 | 42 | ..\..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll 43 | 44 | 45 | 46 | 47 | ..\..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.Net.Http.dll 48 | 49 | 50 | ..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll 51 | True 52 | 53 | 54 | ..\..\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll 55 | True 56 | 57 | 58 | False 59 | ..\..\packages\Microsoft.AspNet.WebApi.SelfHost.5.2.2\lib\net45\System.Web.Http.SelfHost.dll 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | {3E45FA0B-C465-4DE9-9BC3-40A606B73E84} 82 | WebApi.OutputCache.Core 83 | 84 | 85 | {7a6f57f6-38e1-4287-812e-ad7d1025ba5e} 86 | WebApi2.OutputCache 87 | 88 | 89 | 90 | 91 | 98 | -------------------------------------------------------------------------------- /sample/WebApi.OutputCache.V2.Demo/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /sample/WebApi.OutputCache.V2.Demo/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/WebApi.OutputCache.Core/Cache/CacheExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebApi.OutputCache.Core.Cache 4 | { 5 | public static class CacheExtensions 6 | { 7 | public static T GetCachedResult(this IApiOutputCache cache, string key, DateTimeOffset expiry, Func resultGetter, bool bypassCache = true) where T : class 8 | { 9 | var result = cache.Get(key); 10 | 11 | if (result == null || bypassCache) 12 | { 13 | result = resultGetter(); 14 | if (result != null) cache.Add(key, result, expiry); 15 | } 16 | 17 | return result; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/WebApi.OutputCache.Core/Cache/IApiOutputCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace WebApi.OutputCache.Core.Cache 5 | { 6 | public interface IApiOutputCache 7 | { 8 | void RemoveStartsWith(string key); 9 | 10 | T Get(string key) where T : class; 11 | 12 | [Obsolete("Use Get instead")] 13 | object Get(string key); 14 | 15 | void Remove(string key); 16 | 17 | bool Contains(string key); 18 | 19 | void Add(string key, object o, DateTimeOffset expiration, string dependsOnKey = null); 20 | 21 | IEnumerable AllKeys { get; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/WebApi.OutputCache.Core/Cache/MemoryCacheDefault.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.Caching; 5 | 6 | namespace WebApi.OutputCache.Core.Cache 7 | { 8 | public class MemoryCacheDefault : IApiOutputCache 9 | { 10 | private static readonly MemoryCache Cache = MemoryCache.Default; 11 | 12 | public virtual void RemoveStartsWith(string key) 13 | { 14 | lock (Cache) 15 | { 16 | Cache.Remove(key); 17 | } 18 | } 19 | 20 | public virtual T Get(string key) where T : class 21 | { 22 | var o = Cache.Get(key) as T; 23 | return o; 24 | } 25 | 26 | [Obsolete("Use Get instead")] 27 | public virtual object Get(string key) 28 | { 29 | return Cache.Get(key); 30 | } 31 | 32 | public virtual void Remove(string key) 33 | { 34 | lock (Cache) 35 | { 36 | Cache.Remove(key); 37 | } 38 | } 39 | 40 | public virtual bool Contains(string key) 41 | { 42 | return Cache.Contains(key); 43 | } 44 | 45 | public virtual void Add(string key, object o, DateTimeOffset expiration, string dependsOnKey = null) 46 | { 47 | var cachePolicy = new CacheItemPolicy 48 | { 49 | AbsoluteExpiration = expiration 50 | }; 51 | 52 | if (!string.IsNullOrWhiteSpace(dependsOnKey)) 53 | { 54 | cachePolicy.ChangeMonitors.Add( 55 | Cache.CreateCacheEntryChangeMonitor(new[] { dependsOnKey }) 56 | ); 57 | } 58 | lock (Cache) 59 | { 60 | Cache.Add(key, o, cachePolicy); 61 | } 62 | } 63 | 64 | public virtual IEnumerable AllKeys 65 | { 66 | get 67 | { 68 | return Cache.Select(x => x.Key); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/WebApi.OutputCache.Core/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.OutputCache.Core 2 | { 3 | public sealed class Constants 4 | { 5 | public const string ContentTypeKey = ":response-ct"; 6 | public const string EtagKey = ":response-etag"; 7 | public const string GenerationTimestampKey = ":response-generationtimestamp"; 8 | public const string CustomHeaders = ":custom-headers"; 9 | public const string CustomContentHeaders = ":custom-content-headers"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/WebApi.OutputCache.Core/IModelQuery.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.OutputCache.Core 2 | { 3 | public interface IModelQuery 4 | { 5 | TResult Execute(TModel model); 6 | } 7 | } -------------------------------------------------------------------------------- /src/WebApi.OutputCache.Core/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("WebApi.OutputCache.Core")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("WebApi.OutputCache.Core")] 13 | [assembly: AssemblyCopyright("Copyright © 2013")] 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("b7a4d670-5938-4d45-93a2-79ea6e442333")] 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/WebApi.OutputCache.Core/Time/CacheTime.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebApi.OutputCache.Core.Time 4 | { 5 | public class CacheTime 6 | { 7 | // client cache length in seconds 8 | public TimeSpan ClientTimeSpan { get; set; } 9 | 10 | public TimeSpan? SharedTimeSpan { get; set; } 11 | 12 | public DateTimeOffset AbsoluteExpiration { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/WebApi.OutputCache.Core/Time/ShortTime.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebApi.OutputCache.Core.Time 4 | { 5 | public class ShortTime : IModelQuery 6 | { 7 | private readonly int serverTimeInSeconds; 8 | private readonly int clientTimeInSeconds; 9 | private readonly int? sharedTimeInSecounds; 10 | 11 | public ShortTime(int serverTimeInSeconds, int clientTimeInSeconds, int? sharedTimeInSecounds) 12 | { 13 | if (serverTimeInSeconds < 0) 14 | serverTimeInSeconds = 0; 15 | 16 | this.serverTimeInSeconds = serverTimeInSeconds; 17 | 18 | if (clientTimeInSeconds < 0) 19 | clientTimeInSeconds = 0; 20 | 21 | this.clientTimeInSeconds = clientTimeInSeconds; 22 | 23 | if (sharedTimeInSecounds.HasValue && sharedTimeInSecounds.Value < 0) 24 | sharedTimeInSecounds = 0; 25 | 26 | this.sharedTimeInSecounds = sharedTimeInSecounds; 27 | } 28 | 29 | public CacheTime Execute(DateTime model) 30 | { 31 | var cacheTime = new CacheTime 32 | { 33 | AbsoluteExpiration = model.AddSeconds(serverTimeInSeconds), 34 | ClientTimeSpan = TimeSpan.FromSeconds(clientTimeInSeconds), 35 | SharedTimeSpan = sharedTimeInSecounds.HasValue ? (TimeSpan?) TimeSpan.FromSeconds(sharedTimeInSecounds.Value) : null 36 | }; 37 | 38 | return cacheTime; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/WebApi.OutputCache.Core/Time/SpecificTime.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebApi.OutputCache.Core.Time 4 | { 5 | public class SpecificTime : IModelQuery 6 | { 7 | private readonly int year; 8 | private readonly int month; 9 | private readonly int day; 10 | private readonly int hour; 11 | private readonly int minute; 12 | private readonly int second; 13 | 14 | public SpecificTime(int year, int month, int day, int hour, int minute, int second) 15 | { 16 | this.year = year; 17 | this.month = month; 18 | this.day = day; 19 | this.hour = hour; 20 | this.minute = minute; 21 | this.second = second; 22 | } 23 | 24 | public CacheTime Execute(DateTime model) 25 | { 26 | var cacheTime = new CacheTime 27 | { 28 | AbsoluteExpiration = new DateTime(year, 29 | month, 30 | day, 31 | hour, 32 | minute, 33 | second), 34 | }; 35 | 36 | cacheTime.ClientTimeSpan = cacheTime.AbsoluteExpiration.Subtract(model); 37 | 38 | return cacheTime; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/WebApi.OutputCache.Core/Time/ThisDay.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebApi.OutputCache.Core.Time 4 | { 5 | public class ThisDay : IModelQuery 6 | { 7 | private readonly int hour; 8 | private readonly int minute; 9 | private readonly int second; 10 | 11 | public ThisDay(int hour, int minute, int second) 12 | { 13 | this.hour = hour; 14 | this.minute = minute; 15 | this.second = second; 16 | } 17 | 18 | public CacheTime Execute(DateTime model) 19 | { 20 | var cacheTime = new CacheTime 21 | { 22 | AbsoluteExpiration = new DateTime(model.Year, 23 | model.Month, 24 | model.Day, 25 | hour, 26 | minute, 27 | second), 28 | }; 29 | 30 | if (cacheTime.AbsoluteExpiration <= model) 31 | cacheTime.AbsoluteExpiration = cacheTime.AbsoluteExpiration.AddDays(1); 32 | 33 | cacheTime.ClientTimeSpan = cacheTime.AbsoluteExpiration.Subtract(model); 34 | 35 | return cacheTime; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/WebApi.OutputCache.Core/Time/ThisMonth.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebApi.OutputCache.Core.Time 4 | { 5 | public class ThisMonth : IModelQuery 6 | { 7 | private readonly int day; 8 | private readonly int hour; 9 | private readonly int minute; 10 | private readonly int second; 11 | 12 | public ThisMonth(int day, int hour, int minute, int second) 13 | { 14 | this.day = day; 15 | this.hour = hour; 16 | this.minute = minute; 17 | this.second = second; 18 | } 19 | 20 | public CacheTime Execute(DateTime model) 21 | { 22 | var cacheTime = new CacheTime 23 | { 24 | AbsoluteExpiration = new DateTime(model.Year, 25 | model.Month, 26 | day, 27 | hour, 28 | minute, 29 | second), 30 | }; 31 | 32 | if (cacheTime.AbsoluteExpiration <= model) 33 | cacheTime.AbsoluteExpiration = cacheTime.AbsoluteExpiration.AddMonths(1); 34 | 35 | cacheTime.ClientTimeSpan = cacheTime.AbsoluteExpiration.Subtract(model); 36 | 37 | return cacheTime; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/WebApi.OutputCache.Core/Time/ThisYear.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebApi.OutputCache.Core.Time 4 | { 5 | public class ThisYear : IModelQuery 6 | { 7 | private readonly int month; 8 | private readonly int day; 9 | private readonly int hour; 10 | private readonly int minute; 11 | private readonly int second; 12 | 13 | public ThisYear(int month, int day, int hour, int minute, int second) 14 | { 15 | this.month = month; 16 | this.day = day; 17 | this.hour = hour; 18 | this.minute = minute; 19 | this.second = second; 20 | } 21 | 22 | public CacheTime Execute(DateTime model) 23 | { 24 | var cacheTime = new CacheTime 25 | { 26 | AbsoluteExpiration = new DateTime(model.Year, 27 | month, 28 | day, 29 | hour, 30 | minute, 31 | second), 32 | }; 33 | 34 | if (cacheTime.AbsoluteExpiration <= model) 35 | cacheTime.AbsoluteExpiration = cacheTime.AbsoluteExpiration.AddYears(1); 36 | 37 | cacheTime.ClientTimeSpan = cacheTime.AbsoluteExpiration.Subtract(model); 38 | 39 | return cacheTime; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/WebApi.OutputCache.Core/WebApi.OutputCache.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {3E45FA0B-C465-4DE9-9BC3-40A606B73E84} 8 | Library 9 | Properties 10 | WebApi.OutputCache.Core 11 | WebApi.OutputCache.Core 12 | v4.0 13 | 512 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 65 | -------------------------------------------------------------------------------- /src/WebApi.OutputCache.V2/.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | [Dd]ebug/ 46 | [Rr]elease/ 47 | *_i.c 48 | *_p.c 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 | *.vspscc 63 | .builds 64 | *.dotCover 65 | 66 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 67 | packages/* 68 | !packages/repositories.config 69 | 70 | # Visual C++ cache files 71 | ipch/ 72 | *.aps 73 | *.ncb 74 | *.opensdf 75 | *.sdf 76 | 77 | # Visual Studio profiler 78 | *.psess 79 | *.vsp 80 | 81 | # ReSharper is a .NET coding add-in 82 | _ReSharper* 83 | 84 | # Installshield output folder 85 | [Ee]xpress 86 | 87 | # DocProject is a documentation generator add-in 88 | DocProject/buildhelp/ 89 | DocProject/Help/*.HxT 90 | DocProject/Help/*.HxC 91 | DocProject/Help/*.hhc 92 | DocProject/Help/*.hhk 93 | DocProject/Help/*.hhp 94 | DocProject/Help/Html2 95 | DocProject/Help/html 96 | 97 | # Click-Once directory 98 | publish 99 | 100 | # Others 101 | [Bb]in 102 | [Oo]bj 103 | sql 104 | TestResults 105 | *.Cache 106 | ClientBin 107 | stylecop.* 108 | ~$* 109 | *.dbmdl 110 | Generated_Code #added for RIA/Silverlight projects 111 | 112 | # Backup & report files from converting an old project file to a newer 113 | # Visual Studio version. Backup files are not needed, because we have git ;-) 114 | _UpgradeReport_Files/ 115 | Backup*/ 116 | UpgradeLog*.XML 117 | 118 | 119 | 120 | ############ 121 | ## Windows 122 | ############ 123 | 124 | # Windows image file caches 125 | Thumbs.db 126 | 127 | # Folder config file 128 | Desktop.ini 129 | 130 | 131 | ############# 132 | ## Python 133 | ############# 134 | 135 | *.py[co] 136 | 137 | # Packages 138 | *.egg 139 | *.egg-info 140 | dist 141 | build 142 | eggs 143 | parts 144 | bin 145 | var 146 | sdist 147 | develop-eggs 148 | .installed.cfg 149 | 150 | # Installer logs 151 | pip-log.txt 152 | 153 | # Unit test / coverage reports 154 | .coverage 155 | .tox 156 | 157 | #Translations 158 | *.mo 159 | 160 | #Mr Developer 161 | .mr.developer.cfg 162 | 163 | # Mac crap 164 | .DS_Store 165 | -------------------------------------------------------------------------------- /src/WebApi.OutputCache.V2/AutoInvalidateCacheOutputAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Reflection; 7 | using System.Web.Http; 8 | using System.Web.Http.Controllers; 9 | using System.Web.Http.Filters; 10 | 11 | namespace WebApi.OutputCache.V2 12 | { 13 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] 14 | public sealed class AutoInvalidateCacheOutputAttribute : BaseCacheAttribute 15 | { 16 | public bool TryMatchType { get; set; } 17 | 18 | public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) 19 | { 20 | if (actionExecutedContext.Response != null && !actionExecutedContext.Response.IsSuccessStatusCode) return; 21 | if (actionExecutedContext.ActionContext.Request.Method != HttpMethod.Post && 22 | actionExecutedContext.ActionContext.Request.Method != HttpMethod.Put && 23 | actionExecutedContext.ActionContext.Request.Method != HttpMethod.Delete && 24 | actionExecutedContext.ActionContext.Request.Method.Method.ToLower() != "patch" && 25 | actionExecutedContext.ActionContext.Request.Method.Method.ToLower() != "merge") return; 26 | 27 | var controller = actionExecutedContext.ActionContext.ControllerContext.ControllerDescriptor; 28 | var actions = FindAllGetMethods(controller.ControllerType, TryMatchType ? actionExecutedContext.ActionContext.ActionDescriptor.GetParameters() : null); 29 | 30 | var config = actionExecutedContext.ActionContext.Request.GetConfiguration(); 31 | EnsureCache(config, actionExecutedContext.ActionContext.Request); 32 | 33 | foreach (var action in actions) 34 | { 35 | var key = config.CacheOutputConfiguration().MakeBaseCachekey(controller.ControllerType.FullName, action); 36 | if (WebApiCache.Contains(key)) 37 | { 38 | WebApiCache.RemoveStartsWith(key); 39 | } 40 | } 41 | } 42 | 43 | private static IEnumerable FindAllGetMethods(Type controllerType, IEnumerable httpParameterDescriptors) 44 | { 45 | var actions = controllerType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly); 46 | var filteredActions = actions.Where(x => 47 | { 48 | if (x.Name.ToLower().StartsWith("get")) return true; 49 | if (x.GetCustomAttributes(typeof(HttpGetAttribute), true).Any()) return true; 50 | 51 | return false; 52 | }); 53 | 54 | if (httpParameterDescriptors != null) 55 | { 56 | var allowedTypes = httpParameterDescriptors.Select(x => x.ParameterType).ToList(); 57 | var filteredByType = filteredActions.ToList().Where(x => 58 | { 59 | if (allowedTypes.Any(s => s == x.ReturnType)) return true; 60 | if (allowedTypes.Any(s => typeof(IEnumerable).IsAssignableFrom(x.ReturnType) && x.ReturnType.GetGenericArguments().Any() && x.ReturnType.GetGenericArguments()[0] == s)) return true; 61 | if (allowedTypes.Any(s => typeof(IEnumerable).IsAssignableFrom(x.ReturnType) && x.ReturnType.GetElementType() == s)) return true; 62 | return false; 63 | }); 64 | 65 | filteredActions = filteredByType; 66 | } 67 | 68 | var projectedActions = filteredActions.Select(x => 69 | { 70 | var overridenNames = x.GetCustomAttributes(typeof(ActionNameAttribute), false); 71 | if (overridenNames.Any()) 72 | { 73 | var first = (ActionNameAttribute)overridenNames.FirstOrDefault(); 74 | if (first != null) return first.Name; 75 | } 76 | return x.Name; 77 | }); 78 | 79 | return projectedActions; 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /src/WebApi.OutputCache.V2/BaseCacheAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Web.Http; 3 | using System.Web.Http.Filters; 4 | using WebApi.OutputCache.Core.Cache; 5 | 6 | namespace WebApi.OutputCache.V2 7 | { 8 | public abstract class BaseCacheAttribute : ActionFilterAttribute 9 | { 10 | // cache repository 11 | protected IApiOutputCache WebApiCache; 12 | 13 | protected virtual void EnsureCache(HttpConfiguration config, HttpRequestMessage req) 14 | { 15 | WebApiCache = config.CacheOutputConfiguration().GetCacheOutputProvider(req); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/WebApi.OutputCache.V2/CacheOutputAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Net.Http.Formatting; 7 | using System.Net.Http.Headers; 8 | using System.Runtime.ExceptionServices; 9 | using System.Runtime.InteropServices; 10 | using System.Text; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | using System.Web.Http; 14 | using System.Web.Http.Controllers; 15 | using System.Web.Http.Filters; 16 | using WebApi.OutputCache.Core; 17 | using WebApi.OutputCache.Core.Cache; 18 | using WebApi.OutputCache.Core.Time; 19 | 20 | namespace WebApi.OutputCache.V2 21 | { 22 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] 23 | public class CacheOutputAttribute : ActionFilterAttribute 24 | { 25 | private const string CurrentRequestMediaType = "CacheOutput:CurrentRequestMediaType"; 26 | protected static MediaTypeHeaderValue DefaultMediaType = new MediaTypeHeaderValue("application/json") {CharSet = Encoding.UTF8.HeaderName}; 27 | 28 | /// 29 | /// Cache enabled only for requests when Thread.CurrentPrincipal is not set 30 | /// 31 | public bool AnonymousOnly { get; set; } 32 | 33 | /// 34 | /// Corresponds to MustRevalidate HTTP header - indicates whether the origin server requires revalidation of a cache entry on any subsequent use when the cache entry becomes stale 35 | /// 36 | public bool MustRevalidate { get; set; } 37 | 38 | /// 39 | /// Do not vary cache by querystring values 40 | /// 41 | public bool ExcludeQueryStringFromCacheKey { get; set; } 42 | 43 | /// 44 | /// How long response should be cached on the server side (in seconds) 45 | /// 46 | public int ServerTimeSpan { get; set; } 47 | 48 | /// 49 | /// Corresponds to CacheControl MaxAge HTTP header (in seconds) 50 | /// 51 | public int ClientTimeSpan { get; set; } 52 | 53 | 54 | private int? _sharedTimeSpan = null; 55 | 56 | /// 57 | /// Corresponds to CacheControl Shared MaxAge HTTP header (in seconds) 58 | /// 59 | public int SharedTimeSpan 60 | { 61 | get // required for property visibility 62 | { 63 | if (!_sharedTimeSpan.HasValue) 64 | throw new Exception("should not be called without value set"); 65 | return _sharedTimeSpan.Value; 66 | } 67 | set { _sharedTimeSpan = value; } 68 | } 69 | 70 | /// 71 | /// Corresponds to CacheControl NoCache HTTP header 72 | /// 73 | public bool NoCache { get; set; } 74 | 75 | /// 76 | /// Corresponds to CacheControl Private HTTP header. Response can be cached by browser but not by intermediary cache 77 | /// 78 | public bool Private { get; set; } 79 | 80 | /// 81 | /// Class used to generate caching keys 82 | /// 83 | public Type CacheKeyGenerator { get; set; } 84 | 85 | /// 86 | /// Comma seperated list of HTTP headers to cache 87 | /// 88 | public string IncludeCustomHeaders { get; set; } 89 | 90 | /// 91 | /// If set to something else than an empty string, this value will always be used for the Content-Type header, regardless of content negotiation. 92 | /// 93 | public string MediaType { get; set; } 94 | 95 | // cache repository 96 | private IApiOutputCache _webApiCache; 97 | 98 | protected virtual void EnsureCache(HttpConfiguration config, HttpRequestMessage req) 99 | { 100 | _webApiCache = config.CacheOutputConfiguration().GetCacheOutputProvider(req); 101 | } 102 | 103 | internal IModelQuery CacheTimeQuery; 104 | 105 | protected virtual bool IsCachingAllowed(HttpActionContext actionContext, bool anonymousOnly) 106 | { 107 | if (anonymousOnly) 108 | { 109 | if (Thread.CurrentPrincipal.Identity.IsAuthenticated) 110 | { 111 | return false; 112 | } 113 | } 114 | 115 | if (actionContext.ActionDescriptor.GetCustomAttributes().Any()) 116 | { 117 | return false; 118 | } 119 | 120 | return actionContext.Request.Method == HttpMethod.Get; 121 | } 122 | 123 | protected virtual void EnsureCacheTimeQuery() 124 | { 125 | if (CacheTimeQuery == null) ResetCacheTimeQuery(); 126 | } 127 | 128 | protected void ResetCacheTimeQuery() 129 | { 130 | CacheTimeQuery = new ShortTime( ServerTimeSpan, ClientTimeSpan, _sharedTimeSpan); 131 | } 132 | 133 | protected virtual MediaTypeHeaderValue GetExpectedMediaType(HttpConfiguration config, HttpActionContext actionContext) 134 | { 135 | if (!string.IsNullOrEmpty(MediaType)) 136 | { 137 | return new MediaTypeHeaderValue(MediaType); 138 | } 139 | 140 | MediaTypeHeaderValue responseMediaType = null; 141 | 142 | var negotiator = config.Services.GetService(typeof(IContentNegotiator)) as IContentNegotiator; 143 | var returnType = actionContext.ActionDescriptor.ReturnType; 144 | 145 | if (negotiator != null && returnType != typeof(HttpResponseMessage) && (returnType != typeof(IHttpActionResult) || typeof(IHttpActionResult).IsAssignableFrom(returnType))) 146 | { 147 | var negotiatedResult = negotiator.Negotiate(returnType, actionContext.Request, config.Formatters); 148 | 149 | if (negotiatedResult == null) 150 | { 151 | return DefaultMediaType; 152 | } 153 | 154 | responseMediaType = negotiatedResult.MediaType; 155 | if (string.IsNullOrWhiteSpace(responseMediaType.CharSet)) 156 | { 157 | responseMediaType.CharSet = Encoding.UTF8.HeaderName; 158 | } 159 | } 160 | else 161 | { 162 | if (actionContext.Request.Headers.Accept != null) 163 | { 164 | responseMediaType = actionContext.Request.Headers.Accept.FirstOrDefault(); 165 | if (responseMediaType == null || !config.Formatters.Any(x => x.SupportedMediaTypes.Any(value => value.MediaType == responseMediaType.MediaType))) 166 | { 167 | return DefaultMediaType; 168 | } 169 | } 170 | } 171 | 172 | return responseMediaType; 173 | } 174 | 175 | public override void OnActionExecuting(HttpActionContext actionContext) 176 | { 177 | if (actionContext == null) throw new ArgumentNullException("actionContext"); 178 | 179 | if (!IsCachingAllowed(actionContext, AnonymousOnly)) return; 180 | 181 | var config = actionContext.Request.GetConfiguration(); 182 | 183 | EnsureCacheTimeQuery(); 184 | EnsureCache(config, actionContext.Request); 185 | 186 | var cacheKeyGenerator = config.CacheOutputConfiguration().GetCacheKeyGenerator(actionContext.Request, CacheKeyGenerator); 187 | 188 | var responseMediaType = GetExpectedMediaType(config, actionContext); 189 | actionContext.Request.Properties[CurrentRequestMediaType] = responseMediaType; 190 | var cachekey = cacheKeyGenerator.MakeCacheKey(actionContext, responseMediaType, ExcludeQueryStringFromCacheKey); 191 | 192 | if (!_webApiCache.Contains(cachekey)) return; 193 | 194 | var responseHeaders = _webApiCache.Get>>(cachekey + Constants.CustomHeaders); 195 | var responseContentHeaders = _webApiCache.Get>>(cachekey + Constants.CustomContentHeaders); 196 | 197 | if (actionContext.Request.Headers.IfNoneMatch != null) 198 | { 199 | var etag = _webApiCache.Get(cachekey + Constants.EtagKey); 200 | if (etag != null) 201 | { 202 | if (actionContext.Request.Headers.IfNoneMatch.Any(x => x.Tag == etag)) 203 | { 204 | var time = CacheTimeQuery.Execute(DateTime.Now); 205 | var quickResponse = actionContext.Request.CreateResponse(HttpStatusCode.NotModified); 206 | if (responseHeaders != null) AddCustomCachedHeaders(quickResponse, responseHeaders, responseContentHeaders); 207 | 208 | SetEtag(quickResponse, etag); 209 | ApplyCacheHeaders(quickResponse, time); 210 | actionContext.Response = quickResponse; 211 | return; 212 | } 213 | } 214 | } 215 | 216 | var val = _webApiCache.Get(cachekey); 217 | if (val == null) return; 218 | 219 | var contenttype = _webApiCache.Get(cachekey + Constants.ContentTypeKey) ?? responseMediaType; 220 | var contentGeneration = _webApiCache.Get(cachekey + Constants.GenerationTimestampKey); 221 | 222 | DateTimeOffset? contentGenerationTimestamp = null; 223 | if (contentGeneration != null) 224 | { 225 | if (DateTimeOffset.TryParse(contentGeneration, out DateTimeOffset parsedContentGenerationTimestamp)) 226 | { 227 | contentGenerationTimestamp = parsedContentGenerationTimestamp; 228 | } 229 | }; 230 | 231 | actionContext.Response = actionContext.Request.CreateResponse(); 232 | actionContext.Response.Content = new ByteArrayContent(val); 233 | 234 | actionContext.Response.Content.Headers.ContentType = contenttype; 235 | var responseEtag = _webApiCache.Get(cachekey + Constants.EtagKey); 236 | if (responseEtag != null) SetEtag(actionContext.Response, responseEtag); 237 | 238 | if (responseHeaders != null) AddCustomCachedHeaders(actionContext.Response, responseHeaders, responseContentHeaders); 239 | 240 | var cacheTime = CacheTimeQuery.Execute(DateTime.Now); 241 | ApplyCacheHeaders(actionContext.Response, cacheTime, contentGenerationTimestamp); 242 | } 243 | 244 | public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) 245 | { 246 | if (actionExecutedContext.ActionContext.Response == null || !actionExecutedContext.ActionContext.Response.IsSuccessStatusCode) return; 247 | 248 | if (!IsCachingAllowed(actionExecutedContext.ActionContext, AnonymousOnly)) return; 249 | 250 | var actionExecutionTimestamp = DateTimeOffset.Now; 251 | var cacheTime = CacheTimeQuery.Execute(actionExecutionTimestamp.DateTime); 252 | if (cacheTime.AbsoluteExpiration > actionExecutionTimestamp) 253 | { 254 | var httpConfig = actionExecutedContext.Request.GetConfiguration(); 255 | var config = httpConfig.CacheOutputConfiguration(); 256 | var cacheKeyGenerator = config.GetCacheKeyGenerator(actionExecutedContext.Request, CacheKeyGenerator); 257 | 258 | var responseMediaType = actionExecutedContext.Request.Properties[CurrentRequestMediaType] as MediaTypeHeaderValue ?? GetExpectedMediaType(httpConfig, actionExecutedContext.ActionContext); 259 | var cachekey = cacheKeyGenerator.MakeCacheKey(actionExecutedContext.ActionContext, responseMediaType, ExcludeQueryStringFromCacheKey); 260 | 261 | if (!string.IsNullOrWhiteSpace(cachekey) && !(_webApiCache.Contains(cachekey))) 262 | { 263 | SetEtag(actionExecutedContext.Response, CreateEtag(actionExecutedContext, cachekey, cacheTime)); 264 | 265 | var responseContent = actionExecutedContext.Response.Content; 266 | 267 | if (responseContent != null) 268 | { 269 | var baseKey = config.MakeBaseCachekey(actionExecutedContext.ActionContext.ControllerContext.ControllerDescriptor.ControllerType.FullName, actionExecutedContext.ActionContext.ActionDescriptor.ActionName); 270 | var contentType = responseContent.Headers.ContentType; 271 | string etag = actionExecutedContext.Response.Headers.ETag.Tag; 272 | //ConfigureAwait false to avoid deadlocks 273 | var content = await responseContent.ReadAsByteArrayAsync().ConfigureAwait(false); 274 | 275 | responseContent.Headers.Remove("Content-Length"); 276 | 277 | _webApiCache.Add(baseKey, string.Empty, cacheTime.AbsoluteExpiration); 278 | _webApiCache.Add(cachekey, content, cacheTime.AbsoluteExpiration, baseKey); 279 | 280 | 281 | _webApiCache.Add(cachekey + Constants.ContentTypeKey, 282 | contentType, 283 | cacheTime.AbsoluteExpiration, baseKey); 284 | 285 | 286 | _webApiCache.Add(cachekey + Constants.EtagKey, 287 | etag, 288 | cacheTime.AbsoluteExpiration, baseKey); 289 | 290 | _webApiCache.Add(cachekey + Constants.GenerationTimestampKey, 291 | actionExecutionTimestamp.ToString(), 292 | cacheTime.AbsoluteExpiration, baseKey); 293 | 294 | if (!String.IsNullOrEmpty(IncludeCustomHeaders)) 295 | { 296 | // convert to dictionary of lists to ensure thread safety if implementation of IEnumerable is changed 297 | var headers = actionExecutedContext.Response.Headers.Where(h => IncludeCustomHeaders.Contains(h.Key)) 298 | .ToDictionary(x => x.Key, x => x.Value.ToList()); 299 | 300 | var contentHeaders = actionExecutedContext.Response.Content.Headers.Where(h => IncludeCustomHeaders.Contains(h.Key)) 301 | .ToDictionary(x => x.Key, x => x.Value.ToList()); 302 | 303 | _webApiCache.Add(cachekey + Constants.CustomHeaders, 304 | headers, 305 | cacheTime.AbsoluteExpiration, baseKey); 306 | 307 | _webApiCache.Add(cachekey + Constants.CustomContentHeaders, 308 | contentHeaders, 309 | cacheTime.AbsoluteExpiration, baseKey); 310 | } 311 | } 312 | } 313 | } 314 | 315 | ApplyCacheHeaders(actionExecutedContext.ActionContext.Response, cacheTime, actionExecutionTimestamp); 316 | } 317 | 318 | protected virtual void ApplyCacheHeaders(HttpResponseMessage response, CacheTime cacheTime, DateTimeOffset? contentGenerationTimestamp = null) 319 | { 320 | if (cacheTime.ClientTimeSpan > TimeSpan.Zero || MustRevalidate || Private) 321 | { 322 | var cachecontrol = new CacheControlHeaderValue 323 | { 324 | MaxAge = cacheTime.ClientTimeSpan, 325 | SharedMaxAge = cacheTime.SharedTimeSpan, 326 | MustRevalidate = MustRevalidate, 327 | Private = Private 328 | }; 329 | 330 | response.Headers.CacheControl = cachecontrol; 331 | } 332 | else if (NoCache) 333 | { 334 | response.Headers.CacheControl = new CacheControlHeaderValue { NoCache = true }; 335 | response.Headers.Add("Pragma", "no-cache"); 336 | } 337 | if ((response.Content != null) && contentGenerationTimestamp.HasValue) 338 | { 339 | response.Content.Headers.LastModified = contentGenerationTimestamp.Value; 340 | } 341 | } 342 | 343 | protected virtual void AddCustomCachedHeaders(HttpResponseMessage response, Dictionary> headers, Dictionary> contentHeaders) 344 | { 345 | foreach (var headerKey in headers.Keys) 346 | { 347 | foreach (var headerValue in headers[headerKey]) 348 | { 349 | response.Headers.Add(headerKey, headerValue); 350 | } 351 | } 352 | 353 | foreach (var headerKey in contentHeaders.Keys) 354 | { 355 | foreach (var headerValue in contentHeaders[headerKey]) 356 | { 357 | response.Content.Headers.Add(headerKey, headerValue); 358 | } 359 | } 360 | } 361 | 362 | protected virtual string CreateEtag(HttpActionExecutedContext actionExecutedContext, string cachekey, CacheTime cacheTime) 363 | { 364 | return Guid.NewGuid().ToString(); 365 | } 366 | 367 | private static void SetEtag(HttpResponseMessage message, string etag) 368 | { 369 | if (etag != null) 370 | { 371 | var eTag = new EntityTagHeaderValue(@"""" + etag.Replace("\"", string.Empty) + @""""); 372 | message.Headers.ETag = eTag; 373 | } 374 | } 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /src/WebApi.OutputCache.V2/CacheOutputConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | using System.Net.Http; 5 | using System.Reflection; 6 | using System.Web.Http; 7 | using WebApi.OutputCache.Core.Cache; 8 | 9 | namespace WebApi.OutputCache.V2 10 | { 11 | public class CacheOutputConfiguration 12 | { 13 | private readonly HttpConfiguration _configuration; 14 | 15 | public CacheOutputConfiguration(HttpConfiguration configuration) 16 | { 17 | _configuration = configuration; 18 | } 19 | 20 | public void RegisterCacheOutputProvider(Func provider) 21 | { 22 | _configuration.Properties.GetOrAdd(typeof(IApiOutputCache), x => provider); 23 | } 24 | 25 | public void RegisterCacheKeyGeneratorProvider(Func provider) 26 | where T: ICacheKeyGenerator 27 | { 28 | _configuration.Properties.GetOrAdd(typeof (T), x => provider); 29 | } 30 | 31 | public void RegisterDefaultCacheKeyGeneratorProvider(Func provider) 32 | { 33 | RegisterCacheKeyGeneratorProvider(provider); 34 | } 35 | 36 | public string MakeBaseCachekey(string controller, string action) 37 | { 38 | return string.Format("{0}-{1}", controller.ToLower(), action.ToLower()); 39 | } 40 | 41 | public string MakeBaseCachekey(Expression> expression) 42 | { 43 | var method = expression.Body as MethodCallExpression; 44 | if (method == null) throw new ArgumentException("Expression is wrong"); 45 | 46 | var methodName = method.Method.Name; 47 | var nameAttribs = method.Method.GetCustomAttributes(typeof(ActionNameAttribute), false); 48 | if (nameAttribs.Any()) 49 | { 50 | var actionNameAttrib = (ActionNameAttribute) nameAttribs.FirstOrDefault(); 51 | if (actionNameAttrib != null) 52 | { 53 | methodName = actionNameAttrib.Name; 54 | } 55 | } 56 | 57 | return string.Format("{0}-{1}", typeof(T).FullName.ToLower(), methodName.ToLower()); 58 | } 59 | 60 | private static ICacheKeyGenerator TryActivateCacheKeyGenerator(Type generatorType) 61 | { 62 | var hasEmptyOrDefaultConstructor = 63 | generatorType.GetConstructor(Type.EmptyTypes) != null || 64 | generatorType.GetConstructors(BindingFlags.Instance | BindingFlags.Public) 65 | .Any (x => x.GetParameters().All (p => p.IsOptional)); 66 | return hasEmptyOrDefaultConstructor 67 | ? Activator.CreateInstance(generatorType) as ICacheKeyGenerator 68 | : null; 69 | } 70 | 71 | public ICacheKeyGenerator GetCacheKeyGenerator(HttpRequestMessage request, Type generatorType) 72 | { 73 | generatorType = generatorType ?? typeof (ICacheKeyGenerator); 74 | object cache; 75 | _configuration.Properties.TryGetValue(generatorType, out cache); 76 | 77 | var cacheFunc = cache as Func; 78 | 79 | var generator = cacheFunc != null 80 | ? cacheFunc() 81 | : request.GetDependencyScope().GetService(generatorType) as ICacheKeyGenerator; 82 | 83 | return generator 84 | ?? TryActivateCacheKeyGenerator(generatorType) 85 | ?? new DefaultCacheKeyGenerator(); 86 | } 87 | 88 | public IApiOutputCache GetCacheOutputProvider(HttpRequestMessage request) 89 | { 90 | object cache; 91 | _configuration.Properties.TryGetValue(typeof(IApiOutputCache), out cache); 92 | 93 | var cacheFunc = cache as Func; 94 | 95 | var cacheOutputProvider = cacheFunc != null ? cacheFunc() : request.GetDependencyScope().GetService(typeof(IApiOutputCache)) as IApiOutputCache ?? new MemoryCacheDefault(); 96 | return cacheOutputProvider; 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /src/WebApi.OutputCache.V2/DefaultCacheKeyGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | using System.Web.Http.Controllers; 7 | 8 | namespace WebApi.OutputCache.V2 9 | { 10 | public class DefaultCacheKeyGenerator : ICacheKeyGenerator 11 | { 12 | public virtual string MakeCacheKey(HttpActionContext context, MediaTypeHeaderValue mediaType, bool excludeQueryString = false) 13 | { 14 | var key = MakeBaseKey(context); 15 | var parameters = FormatParameters(context, excludeQueryString); 16 | 17 | return string.Format("{0}{1}:{2}", key, parameters, mediaType); 18 | } 19 | 20 | protected virtual string MakeBaseKey(HttpActionContext context) 21 | { 22 | var controller = context.ControllerContext.ControllerDescriptor.ControllerType.FullName; 23 | var action = context.ActionDescriptor.ActionName; 24 | return context.Request.GetConfiguration().CacheOutputConfiguration().MakeBaseCachekey(controller, action); 25 | } 26 | 27 | protected virtual string FormatParameters(HttpActionContext context, bool excludeQueryString) 28 | { 29 | var actionParameters = context.ActionArguments.Where(x => x.Value != null).Select(x => x.Key + "=" + GetValue(x.Value)); 30 | 31 | string parameters; 32 | 33 | if (!excludeQueryString) 34 | { 35 | var queryStringParameters = 36 | context.Request.GetQueryNameValuePairs() 37 | .Where(x => x.Key.ToLower() != "callback") 38 | .Select(x => x.Key + "=" + x.Value); 39 | var parametersCollections = actionParameters.Union(queryStringParameters); 40 | parameters = "-" + string.Join("&", parametersCollections); 41 | 42 | var callbackValue = GetJsonpCallback(context.Request); 43 | if (!string.IsNullOrWhiteSpace(callbackValue)) 44 | { 45 | var callback = "callback=" + callbackValue; 46 | if (parameters.Contains("&" + callback)) parameters = parameters.Replace("&" + callback, string.Empty); 47 | if (parameters.Contains(callback + "&")) parameters = parameters.Replace(callback + "&", string.Empty); 48 | if (parameters.Contains("-" + callback)) parameters = parameters.Replace("-" + callback, string.Empty); 49 | if (parameters.EndsWith("&")) parameters = parameters.TrimEnd('&'); 50 | } 51 | } 52 | else 53 | { 54 | parameters = "-" + string.Join("&", actionParameters); 55 | } 56 | 57 | if (parameters == "-") parameters = string.Empty; 58 | return parameters; 59 | } 60 | 61 | private string GetJsonpCallback(HttpRequestMessage request) 62 | { 63 | var callback = string.Empty; 64 | if (request.Method == HttpMethod.Get) 65 | { 66 | var query = request.GetQueryNameValuePairs(); 67 | 68 | if (query != null) 69 | { 70 | var queryVal = query.FirstOrDefault(x => x.Key.ToLower() == "callback"); 71 | if (!queryVal.Equals(default(KeyValuePair))) callback = queryVal.Value; 72 | } 73 | } 74 | return callback; 75 | } 76 | 77 | private string GetValue(object val) 78 | { 79 | if (val is IEnumerable && !(val is string)) 80 | { 81 | var concatValue = string.Empty; 82 | var paramArray = val as IEnumerable; 83 | return paramArray.Cast().Aggregate(concatValue, (current, paramValue) => current + (paramValue + ";")); 84 | } 85 | return val.ToString(); 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /src/WebApi.OutputCache.V2/HttpConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Web.Http; 2 | 3 | namespace WebApi.OutputCache.V2 4 | { 5 | public static class HttpConfigurationExtensions 6 | { 7 | public static CacheOutputConfiguration CacheOutputConfiguration(this HttpConfiguration config) 8 | { 9 | return new CacheOutputConfiguration(config); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/WebApi.OutputCache.V2/ICacheKeyGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using System.Web.Http.Controllers; 3 | 4 | namespace WebApi.OutputCache.V2 5 | { 6 | public interface ICacheKeyGenerator 7 | { 8 | string MakeCacheKey(HttpActionContext context, MediaTypeHeaderValue mediaType, bool excludeQueryString = false); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/WebApi.OutputCache.V2/IgnoreCacheOutputAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebApi.OutputCache.V2 4 | { 5 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] 6 | public sealed class IgnoreCacheOutputAttribute : Attribute 7 | { 8 | } 9 | } -------------------------------------------------------------------------------- /src/WebApi.OutputCache.V2/InvalidateCacheOutputAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Web.Http.Filters; 4 | 5 | namespace WebApi.OutputCache.V2 6 | { 7 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)] 8 | public sealed class InvalidateCacheOutputAttribute : BaseCacheAttribute 9 | { 10 | private string _controller; 11 | private readonly string _methodName; 12 | 13 | public InvalidateCacheOutputAttribute(string methodName) 14 | : this(methodName, null) 15 | { 16 | } 17 | 18 | public InvalidateCacheOutputAttribute(string methodName, Type type = null) 19 | { 20 | _controller = type != null ? type.FullName : null; 21 | _methodName = methodName; 22 | } 23 | 24 | public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) 25 | { 26 | if (actionExecutedContext.Response != null && !actionExecutedContext.Response.IsSuccessStatusCode) return; 27 | _controller = _controller ?? actionExecutedContext.ActionContext.ControllerContext.ControllerDescriptor.ControllerType.FullName; 28 | 29 | var config = actionExecutedContext.Request.GetConfiguration(); 30 | EnsureCache(config, actionExecutedContext.Request); 31 | 32 | var key = actionExecutedContext.Request.GetConfiguration().CacheOutputConfiguration().MakeBaseCachekey(_controller, _methodName); 33 | if (WebApiCache.Contains(key)) 34 | { 35 | WebApiCache.RemoveStartsWith(key); 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/WebApi.OutputCache.V2/PerUserCacheKeyGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using System.Web.Http.Controllers; 3 | 4 | namespace WebApi.OutputCache.V2 5 | { 6 | public class PerUserCacheKeyGenerator : DefaultCacheKeyGenerator 7 | { 8 | public override string MakeCacheKey(HttpActionContext context, MediaTypeHeaderValue mediaType, bool excludeQueryString = false) 9 | { 10 | var baseKey = MakeBaseKey(context); 11 | var parameters = FormatParameters(context, excludeQueryString); 12 | var userIdentity = FormatUserIdentity(context); 13 | 14 | return string.Format("{0}{1}:{2}:{3}", baseKey, parameters, userIdentity, mediaType); 15 | } 16 | 17 | protected virtual string FormatUserIdentity(HttpActionContext context) 18 | { 19 | return context.RequestContext.Principal.Identity.Name.ToLower(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/WebApi.OutputCache.V2/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("WebAPI.OutputCache")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("WebAPI.OutputCache")] 13 | [assembly: AssemblyCopyright("Copyright © 2012")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | [assembly: InternalsVisibleTo("WebAPI.OutputCache.Tests")] 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("73559c35-3a0c-449c-b137-886cfc45e446")] 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("1.0.0.0")] 37 | [assembly: AssemblyFileVersion("1.0.0.0")] 38 | -------------------------------------------------------------------------------- /src/WebApi.OutputCache.V2/TimeAttributes/CacheOutputUntilCacheAttribute.cs: -------------------------------------------------------------------------------- 1 | using WebApi.OutputCache.Core.Time; 2 | 3 | namespace WebApi.OutputCache.V2.TimeAttributes 4 | { 5 | public sealed class CacheOutputUntilAttribute : CacheOutputAttribute 6 | { 7 | /// 8 | /// Cache item until absolute expiration 2012/01/01 @ 17h45 9 | /// 10 | /// 2012 11 | /// 1 12 | /// 1 13 | /// 17 14 | /// 45 15 | /// 0 16 | public CacheOutputUntilAttribute(int year, 17 | int month, 18 | int day, 19 | int hour = 0, 20 | int minute = 0, 21 | int second = 0) 22 | { 23 | CacheTimeQuery = new SpecificTime(year, month, day, hour, minute, second); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/WebApi.OutputCache.V2/TimeAttributes/CacheOutputUntilThisMonthAttribute.cs: -------------------------------------------------------------------------------- 1 | using WebApi.OutputCache.Core.Time; 2 | 3 | namespace WebApi.OutputCache.V2.TimeAttributes 4 | { 5 | public sealed class CacheOutputUntilThisMonthAttribute : CacheOutputAttribute 6 | { 7 | /// 8 | /// Cache item until absolute expiration THIS YEAR / THIS MONTH / 01 @ 17h45 9 | /// 10 | /// 1 11 | /// 17 12 | /// 45 13 | /// 0 14 | public CacheOutputUntilThisMonthAttribute(int day, 15 | int hour = 0, 16 | int minute = 0, 17 | int second = 0) 18 | { 19 | CacheTimeQuery = new ThisMonth(day, hour, minute, second); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/WebApi.OutputCache.V2/TimeAttributes/CacheOutputUntilThisYearAttribute.cs: -------------------------------------------------------------------------------- 1 | using WebApi.OutputCache.Core.Time; 2 | 3 | namespace WebApi.OutputCache.V2.TimeAttributes 4 | { 5 | public sealed class CacheOutputUntilThisYearAttribute : CacheOutputAttribute 6 | { 7 | /// 8 | /// Cache item until absolute expiration THIS YEAR / 01 / 01 @ 17h45 9 | /// 10 | /// 1 11 | /// 1 12 | /// 17 13 | /// 45 14 | /// 0 15 | public CacheOutputUntilThisYearAttribute(int month, 16 | int day, 17 | int hour = 0, 18 | int minute = 0, 19 | int second = 0) 20 | { 21 | CacheTimeQuery = new ThisYear(month, day, hour, minute, second); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/WebApi.OutputCache.V2/TimeAttributes/CacheOutputUntilToday.cs: -------------------------------------------------------------------------------- 1 | using WebApi.OutputCache.Core.Time; 2 | 3 | namespace WebApi.OutputCache.V2.TimeAttributes 4 | { 5 | public sealed class CacheOutputUntilToday : CacheOutputAttribute 6 | { 7 | /// 8 | /// Cache item until absolute expiration today @ 17h45 9 | /// 10 | /// 17 11 | /// 45 12 | /// 0 13 | public CacheOutputUntilToday(int hour = 23, 14 | int minute = 59, 15 | int second = 59) 16 | { 17 | CacheTimeQuery = new ThisDay(hour, minute, second); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/WebApi.OutputCache.V2/WebApi.OutputCache.V2.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {7A6F57F6-38E1-4287-812E-AD7D1025BA5E} 8 | Library 9 | Properties 10 | WebApi.OutputCache.V2 11 | WebApi.OutputCache.V2 12 | v4.5 13 | 512 14 | 15 | ..\ 16 | true 17 | 18 | 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | false 29 | 30 | 31 | pdbonly 32 | true 33 | bin\Release\ 34 | TRACE 35 | prompt 36 | 4 37 | bin\Release\WebApi.OutputCache.V2.xml 38 | false 39 | 40 | 41 | 42 | False 43 | ..\..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll 44 | 45 | 46 | 47 | 48 | 49 | ..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll 50 | True 51 | 52 | 53 | ..\..\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll 54 | True 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | {3e45fa0b-c465-4de9-9bc3-40a606b73e84} 80 | WebApi.OutputCache.Core 81 | 82 | 83 | 84 | 85 | 92 | -------------------------------------------------------------------------------- /src/WebApi.OutputCache.V2/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/WebApi.OutputCache.Core.Tests/MemoryCacheDefaultTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using WebApi.OutputCache.Core.Cache; 4 | 5 | namespace WebApi.OutputCache.Core.Tests 6 | { 7 | [TestFixture] 8 | public class MemoryCacheDefaultTests 9 | { 10 | [Test] 11 | public void returns_all_keys_in_cache() 12 | { 13 | IApiOutputCache cache = new MemoryCacheDefault(); 14 | cache.Add("base", "abc", DateTime.Now.AddSeconds(60)); 15 | cache.Add("key1", "abc", DateTime.Now.AddSeconds(60), "base"); 16 | cache.Add("key2", "abc", DateTime.Now.AddSeconds(60), "base"); 17 | cache.Add("key3", "abc", DateTime.Now.AddSeconds(60), "base"); 18 | 19 | var result = cache.AllKeys; 20 | 21 | CollectionAssert.AreEquivalent(new[] { "base", "key1", "key2", "key3" }, result); 22 | } 23 | 24 | [Test] 25 | public void remove_startswith_cascades_to_all_dependencies() 26 | { 27 | IApiOutputCache cache = new MemoryCacheDefault(); 28 | cache.Add("base", "abc", DateTime.Now.AddSeconds(60)); 29 | cache.Add("key1","abc", DateTime.Now.AddSeconds(60), "base"); 30 | cache.Add("key2", "abc", DateTime.Now.AddSeconds(60), "base"); 31 | cache.Add("key3", "abc", DateTime.Now.AddSeconds(60), "base"); 32 | Assert.IsNotNull(cache.Get("key1")); 33 | Assert.IsNotNull(cache.Get("key2")); 34 | Assert.IsNotNull(cache.Get("key3")); 35 | 36 | cache.RemoveStartsWith("base"); 37 | 38 | Assert.IsNull(cache.Get("base")); 39 | Assert.IsNull(cache.Get("key1")); 40 | Assert.IsNull(cache.Get("key2")); 41 | Assert.IsNull(cache.Get("key3")); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/WebApi.OutputCache.Core.Tests/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("WebApi.OutputCache.Core.Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("WebApi.OutputCache.Core.Tests")] 13 | [assembly: AssemblyCopyright("Copyright © 2013")] 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("dbcc1ae7-3eb9-4f4b-ab42-980571343293")] 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 | -------------------------------------------------------------------------------- /test/WebApi.OutputCache.Core.Tests/WebApi.OutputCache.Core.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {44F519D6-E825-442C-A112-B5C4404EAA44} 8 | Library 9 | Properties 10 | WebApi.OutputCache.Core.Tests 11 | WebApi.OutputCache.Core.Tests 12 | v4.5 13 | 512 14 | ..\..\ 15 | true 16 | 17 | 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | ..\..\packages\NUnit.2.6.3\lib\nunit.framework.dll 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | {3E45FA0B-C465-4DE9-9BC3-40A606B73E84} 56 | WebApi.OutputCache.Core 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 71 | -------------------------------------------------------------------------------- /test/WebApi.OutputCache.Core.Tests/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | [Dd]ebug/ 46 | [Rr]elease/ 47 | *_i.c 48 | *_p.c 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 | *.vspscc 63 | .builds 64 | *.dotCover 65 | 66 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 67 | packages/* 68 | !packages/repositories.config 69 | 70 | # Visual C++ cache files 71 | ipch/ 72 | *.aps 73 | *.ncb 74 | *.opensdf 75 | *.sdf 76 | 77 | # Visual Studio profiler 78 | *.psess 79 | *.vsp 80 | 81 | # ReSharper is a .NET coding add-in 82 | _ReSharper* 83 | 84 | # Installshield output folder 85 | [Ee]xpress 86 | 87 | # DocProject is a documentation generator add-in 88 | DocProject/buildhelp/ 89 | DocProject/Help/*.HxT 90 | DocProject/Help/*.HxC 91 | DocProject/Help/*.hhc 92 | DocProject/Help/*.hhk 93 | DocProject/Help/*.hhp 94 | DocProject/Help/Html2 95 | DocProject/Help/html 96 | 97 | # Click-Once directory 98 | publish 99 | 100 | # Others 101 | [Bb]in 102 | [Oo]bj 103 | sql 104 | TestResults 105 | *.Cache 106 | ClientBin 107 | stylecop.* 108 | ~$* 109 | *.dbmdl 110 | Generated_Code #added for RIA/Silverlight projects 111 | 112 | # Backup & report files from converting an old project file to a newer 113 | # Visual Studio version. Backup files are not needed, because we have git ;-) 114 | _UpgradeReport_Files/ 115 | Backup*/ 116 | UpgradeLog*.XML 117 | 118 | 119 | 120 | ############ 121 | ## Windows 122 | ############ 123 | 124 | # Windows image file caches 125 | Thumbs.db 126 | 127 | # Folder config file 128 | Desktop.ini 129 | 130 | 131 | ############# 132 | ## Python 133 | ############# 134 | 135 | *.py[co] 136 | 137 | # Packages 138 | *.egg 139 | *.egg-info 140 | dist 141 | build 142 | eggs 143 | parts 144 | bin 145 | var 146 | sdist 147 | develop-eggs 148 | .installed.cfg 149 | 150 | # Installer logs 151 | pip-log.txt 152 | 153 | # Unit test / coverage reports 154 | .coverage 155 | .tox 156 | 157 | #Translations 158 | *.mo 159 | 160 | #Mr Developer 161 | .mr.developer.cfg 162 | 163 | # Mac crap 164 | .DS_Store 165 | -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/CacheKeyGenerationTestsBase.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Net.Http.Headers; 4 | using System.Web.Http.Controllers; 5 | 6 | namespace WebApi.OutputCache.V2.Tests 7 | { 8 | /// 9 | /// Base class for implementing tests for the generation of cache keys (meaning: implementations of the 10 | /// 11 | public abstract class CacheKeyGenerationTestsBase where TCacheKeyGenerator : ICacheKeyGenerator 12 | { 13 | private const string ArgumentKey = "filterExpression"; 14 | private const string ArgumentValue = "val"; 15 | protected HttpActionContext context; 16 | protected MediaTypeHeaderValue mediaType; 17 | protected Uri requestUri; 18 | protected TCacheKeyGenerator cacheKeyGenerator; 19 | protected string BaseCacheKey; 20 | 21 | [SetUp] 22 | public virtual void Setup() 23 | { 24 | requestUri = new Uri("http://localhost:8080/cacheKeyGeneration?filter=val"); 25 | var controllerType = typeof(TestControllers.CacheKeyGenerationController); 26 | var actionMethodInfo = controllerType.GetMethod("Get"); 27 | var controllerDescriptor = new HttpControllerDescriptor() { ControllerType = controllerType }; 28 | var actionDescriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, actionMethodInfo); 29 | var request = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, requestUri.AbsoluteUri); 30 | 31 | context = new HttpActionContext( 32 | new HttpControllerContext() { ControllerDescriptor = controllerDescriptor, Request = request }, 33 | actionDescriptor 34 | ); 35 | mediaType = new MediaTypeHeaderValue("application/json"); 36 | 37 | BaseCacheKey = new CacheOutputConfiguration(null).MakeBaseCachekey((TestControllers.CacheKeyGenerationController c) => c.Get(String.Empty)); 38 | cacheKeyGenerator = BuildCacheKeyGenerator(); 39 | } 40 | 41 | protected abstract TCacheKeyGenerator BuildCacheKeyGenerator(); 42 | 43 | protected virtual void AssertCacheKeysBasicFormat(string cacheKey) 44 | { 45 | Assert.IsNotNull(cacheKey); 46 | StringAssert.StartsWith(BaseCacheKey, cacheKey, "Key does not start with BaseKey"); 47 | StringAssert.EndsWith(mediaType.ToString(), cacheKey, "Key does not end with MediaType"); 48 | } 49 | 50 | protected void AddActionArgumentsToContext() 51 | { 52 | context.ActionArguments.Add(ArgumentKey, ArgumentValue); 53 | } 54 | 55 | protected string FormatActionArgumentsForKeyAssertion() 56 | { 57 | return String.Format("{0}={1}", ArgumentKey, ArgumentValue); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/CacheKeyGeneratorRegistrationTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Http.Headers; 4 | using System.Threading; 5 | using System.Web.Http; 6 | using System.Web.Http.Controllers; 7 | using Autofac; 8 | using Autofac.Integration.WebApi; 9 | using Moq; 10 | using NUnit.Framework; 11 | using WebApi.OutputCache.Core.Cache; 12 | 13 | namespace WebApi.OutputCache.V2.Tests 14 | { 15 | [TestFixture] 16 | public class CacheKeyGeneratorRegistrationTests 17 | { 18 | private HttpServer _server; 19 | private string _url = "http://www.strathweb.com/api/"; 20 | private Mock _cache; 21 | private Mock _keyGenerator; 22 | 23 | [SetUp] 24 | public void init() 25 | { 26 | Thread.CurrentPrincipal = null; 27 | 28 | _cache = new Mock(); 29 | _keyGenerator = new Mock(); 30 | 31 | var conf = new HttpConfiguration(); 32 | 33 | var builder = new ContainerBuilder(); 34 | builder.RegisterInstance(_cache.Object); 35 | 36 | conf.DependencyResolver = new AutofacWebApiDependencyResolver(builder.Build()); 37 | conf.Routes.MapHttpRoute( 38 | name: "DefaultApi", 39 | routeTemplate: "api/{controller}/{action}/{id}", 40 | defaults: new { id = RouteParameter.Optional } 41 | ); 42 | 43 | _server = new HttpServer(conf); 44 | } 45 | 46 | [Test] 47 | public void registered_default_is_used() 48 | { 49 | _server.Configuration.CacheOutputConfiguration().RegisterDefaultCacheKeyGeneratorProvider(() => _keyGenerator.Object); 50 | 51 | var client = new HttpClient(_server); 52 | var result = client.GetAsync(_url + "sample/Get_c100_s100").Result; 53 | 54 | _keyGenerator.VerifyAll(); 55 | } 56 | 57 | [Test] 58 | public void last_registered_default_is_used() 59 | { 60 | _server.Configuration.CacheOutputConfiguration().RegisterDefaultCacheKeyGeneratorProvider(() => { 61 | Assert.Fail("First registration should have been overwritten"); 62 | return null; 63 | }); 64 | _server.Configuration.CacheOutputConfiguration().RegisterDefaultCacheKeyGeneratorProvider(() => _keyGenerator.Object); 65 | 66 | var client = new HttpClient(_server); 67 | var result = client.GetAsync(_url + "sample/Get_c100_s100").Result; 68 | 69 | _keyGenerator.VerifyAll(); 70 | } 71 | 72 | [Test] 73 | public void specific_registration_does_not_affect_default() 74 | { 75 | _server.Configuration.CacheOutputConfiguration().RegisterDefaultCacheKeyGeneratorProvider(() => _keyGenerator.Object); 76 | _server.Configuration.CacheOutputConfiguration().RegisterCacheKeyGeneratorProvider(() => new FailCacheKeyGenerator()); 77 | 78 | var client = new HttpClient(_server); 79 | var result = client.GetAsync(_url + "sample/Get_c100_s100").Result; 80 | 81 | _keyGenerator.VerifyAll(); 82 | } 83 | 84 | [Test] 85 | public void selected_generator_with_internal_registration_is_used() 86 | { 87 | _server.Configuration.CacheOutputConfiguration().RegisterCacheKeyGeneratorProvider(() => new InternalRegisteredCacheKeyGenerator("internal")); 88 | 89 | var client = new HttpClient(_server); 90 | var result = client.GetAsync(_url + "cachekey/get_internalregistered").Result; 91 | 92 | _cache.Verify(s => s.Add(It.Is(x => x == "internal"), It.IsAny(), It.Is(x => x <= DateTime.Now.AddSeconds(100)), It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.cachekeycontroller-get_internalregistered")), Times.Once()); 93 | } 94 | 95 | [Test] 96 | public void custom_unregistered_cache_key_generator_called() 97 | { 98 | var client = new HttpClient(_server); 99 | var result = client.GetAsync(_url + "cachekey/get_unregistered").Result; 100 | 101 | _cache.Verify(s => s.Contains(It.Is(x => x == "unregistered")), Times.Once()); 102 | } 103 | 104 | #region Helper classes 105 | private class FailCacheKeyGenerator : ICacheKeyGenerator 106 | { 107 | public string MakeCacheKey(HttpActionContext context, MediaTypeHeaderValue mediaType, bool excludeQueryString = false) 108 | { 109 | Assert.Fail("This cache key generator should never be invoked"); 110 | return "fail"; 111 | } 112 | } 113 | 114 | public class InternalRegisteredCacheKeyGenerator : ICacheKeyGenerator 115 | { 116 | private readonly string _key; 117 | 118 | public InternalRegisteredCacheKeyGenerator(string key) 119 | { 120 | _key = key; 121 | } 122 | 123 | public string MakeCacheKey(HttpActionContext context, MediaTypeHeaderValue mediaType, bool excludeQueryString = false) 124 | { 125 | return _key; 126 | } 127 | } 128 | #endregion 129 | } 130 | } -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/CacheKeyGeneratorTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Http.Headers; 4 | using System.Threading; 5 | using System.Web.Http; 6 | using System.Web.Http.Controllers; 7 | using Autofac; 8 | using Autofac.Integration.WebApi; 9 | using Moq; 10 | using NUnit.Framework; 11 | using WebApi.OutputCache.Core.Cache; 12 | 13 | namespace WebApi.OutputCache.V2.Tests 14 | { 15 | [TestFixture] 16 | class CacheKeyGeneratorTests 17 | { 18 | public class CustomCacheKeyGenerator : ICacheKeyGenerator 19 | { 20 | public string MakeCacheKey(HttpActionContext context, MediaTypeHeaderValue mediaType, bool excludeQueryString = false) 21 | { 22 | return "custom_key"; 23 | } 24 | } 25 | 26 | private HttpServer _server; 27 | private string _url = "http://www.strathweb.com/api/"; 28 | private Mock _cache; 29 | private Mock _keyGeneratorA; 30 | private CustomCacheKeyGenerator _keyGeneratorB; 31 | 32 | [SetUp] 33 | public void init() 34 | { 35 | Thread.CurrentPrincipal = null; 36 | 37 | _cache = new Mock(); 38 | _keyGeneratorA = new Mock(); 39 | _keyGeneratorB = new CustomCacheKeyGenerator(); 40 | 41 | var conf = new HttpConfiguration(); 42 | var builder = new ContainerBuilder(); 43 | builder.RegisterInstance(_cache.Object); 44 | // this should become the default cache key generator 45 | builder.RegisterInstance(_keyGeneratorA.Object).As(); 46 | builder.RegisterInstance(_keyGeneratorB); 47 | 48 | conf.DependencyResolver = new AutofacWebApiDependencyResolver(builder.Build()); 49 | conf.Routes.MapHttpRoute( 50 | name: "DefaultApi", 51 | routeTemplate: "api/{controller}/{action}/{id}", 52 | defaults: new { id = RouteParameter.Optional } 53 | ); 54 | 55 | _server = new HttpServer(conf); 56 | } 57 | 58 | [Test] 59 | public void custom_default_cache_key_generator_called_and_key_used() 60 | { 61 | var client = new HttpClient(_server); 62 | _keyGeneratorA.Setup(k => k.MakeCacheKey(It.IsAny(), It.IsAny(), It.IsAny())) 63 | .Returns("keykeykey") 64 | .Verifiable("Key generator was never called"); 65 | // use the samplecontroller to show that no changes are required to existing code 66 | var result = client.GetAsync(_url + "sample/Get_c100_s100").Result; 67 | 68 | _cache.Verify(s => s.Contains(It.Is(x => x == "keykeykey")), Times.Exactly(2)); 69 | _cache.Verify(s => s.Add(It.Is(x => x == "keykeykey"), It.IsAny(), It.Is(x => x <= DateTime.Now.AddSeconds(100)), It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100")), Times.Once()); 70 | _cache.Verify(s => s.Add(It.Is(x => x == "keykeykey:response-ct"), It.IsAny(), It.Is(x => x <= DateTime.Now.AddSeconds(100)), It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100")), Times.Once()); 71 | 72 | _keyGeneratorA.VerifyAll(); 73 | } 74 | 75 | [Test] 76 | public void custom_cache_key_generator_called() 77 | { 78 | var client = new HttpClient(_server); 79 | var result = client.GetAsync(_url + "cachekey/get_custom_key").Result; 80 | 81 | _cache.Verify(s => s.Contains(It.Is(x => x == "custom_key")), Times.Exactly(2)); 82 | _cache.Verify(s => s.Add(It.Is(x => x == "custom_key"), It.IsAny(), It.Is(x => x <= DateTime.Now.AddSeconds(100)), It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.cachekeycontroller-get_custom_key")), Times.Once()); 83 | _cache.Verify(s => s.Add(It.Is(x => x == "custom_key:response-ct"), It.IsAny(), It.Is(x => x <= DateTime.Now.AddSeconds(100)), It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.cachekeycontroller-get_custom_key")), Times.Once()); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/ClientSideTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net.Http; 4 | using System.Web.Http; 5 | using NUnit.Framework; 6 | using WebApi.OutputCache.Core.Time; 7 | 8 | namespace WebApi.OutputCache.V2.Tests 9 | { 10 | [TestFixture] 11 | public class ClientSideTests 12 | { 13 | private HttpServer _server; 14 | private string _url = "http://www.strathweb.com/api/sample/"; 15 | 16 | [TestFixtureSetUp] 17 | public void fixture_init() 18 | { 19 | var conf = new HttpConfiguration(); 20 | conf.Routes.MapHttpRoute( 21 | name: "DefaultApi", 22 | routeTemplate: "api/{controller}/{action}/{id}", 23 | defaults: new { id = RouteParameter.Optional } 24 | ); 25 | 26 | _server = new HttpServer(conf); 27 | } 28 | 29 | [Test] 30 | public void maxage_mustrevalidate_false_headers_correct() 31 | { 32 | var client = new HttpClient(_server); 33 | var result = client.GetAsync(_url + "Get_c100_s100").Result; 34 | 35 | Assert.AreEqual(TimeSpan.FromSeconds(100), result.Headers.CacheControl.MaxAge); 36 | Assert.IsFalse(result.Headers.CacheControl.MustRevalidate); 37 | } 38 | 39 | [Test] 40 | public void no_cachecontrol_when_clienttimeout_is_zero() 41 | { 42 | var client = new HttpClient(_server); 43 | var result = client.GetAsync(_url + "Get_c0_s100").Result; 44 | 45 | Assert.IsNull(result.Headers.CacheControl); 46 | } 47 | 48 | [Test] 49 | public void no_cachecontrol_when_request_not_succes() 50 | { 51 | var client = new HttpClient(_server); 52 | var result = client.GetAsync(_url + "Get_request_httpResponseException_noCache").Result; 53 | 54 | Assert.IsNull(result.Headers.CacheControl); 55 | } 56 | 57 | [Test] 58 | public void no_cachecontrol_when_request_exception() 59 | { 60 | var client = new HttpClient(_server); 61 | var result = client.GetAsync(_url + "Get_request_exception_noCache").Result; 62 | 63 | Assert.IsNull(result.Headers.CacheControl); 64 | } 65 | [Test] 66 | public void maxage_cachecontrol_when_no_content() 67 | { 68 | var client = new HttpClient(_server); 69 | var result = client.GetAsync(_url + "Get_request_noContent").Result; 70 | 71 | Assert.IsNotNull(result.Headers.CacheControl); 72 | Assert.AreEqual(TimeSpan.FromSeconds(50), result.Headers.CacheControl.MaxAge); 73 | } 74 | 75 | 76 | [Test] 77 | public void maxage_mustrevalidate_headers_correct_with_clienttimeout_zero_with_must_revalidate() 78 | { 79 | var client = new HttpClient(_server); 80 | var result = client.GetAsync(_url + "Get_c0_s100_mustR").Result; 81 | 82 | Assert.IsTrue(result.Headers.CacheControl.MustRevalidate); 83 | Assert.AreEqual(TimeSpan.Zero, result.Headers.CacheControl.MaxAge); 84 | } 85 | 86 | 87 | [Test] 88 | public void nocache_headers_correct() 89 | { 90 | var client = new HttpClient(_server); 91 | var result = client.GetAsync(_url + "Get_nocache").Result; 92 | 93 | Assert.IsTrue(result.Headers.CacheControl.NoCache, 94 | "NoCache in result headers was expected to be true when CacheOutput.NoCache=true."); 95 | Assert.IsTrue(result.Headers.Contains("Pragma"), 96 | "result headers does not contain expected Pragma."); 97 | Assert.IsTrue(result.Headers.GetValues("Pragma").Contains("no-cache"), 98 | "expected no-cache Pragma was not found"); 99 | } 100 | 101 | [Test] 102 | public void maxage_mustrevalidate_true_headers_correct() 103 | { 104 | var client = new HttpClient(_server); 105 | var result = client.GetAsync(_url + "Get_c50_mustR").Result; 106 | 107 | Assert.AreEqual(TimeSpan.FromSeconds(50), result.Headers.CacheControl.MaxAge); 108 | Assert.IsTrue(result.Headers.CacheControl.MustRevalidate); 109 | } 110 | 111 | [Test] 112 | public void maxage_private_true_headers_correct() 113 | { 114 | var client = new HttpClient(_server); 115 | var result = client.GetAsync(_url + "Get_c50_private").Result; 116 | 117 | Assert.AreEqual(TimeSpan.FromSeconds(50), result.Headers.CacheControl.MaxAge); 118 | Assert.IsTrue(result.Headers.CacheControl.Private); 119 | } 120 | 121 | [Test] 122 | public void maxage_mustrevalidate_headers_correct_with_cacheuntil() 123 | { 124 | var client = new HttpClient(_server); 125 | var result = client.GetAsync(_url + "Get_until25012100_1700").Result; 126 | var clientTimeSpanSeconds = new SpecificTime(2100, 01, 25, 17, 0, 0).Execute(DateTime.Now).ClientTimeSpan.TotalSeconds; 127 | var resultCacheControlSeconds = ((TimeSpan) result.Headers.CacheControl.MaxAge).TotalSeconds; 128 | Assert.IsTrue(Math.Round(clientTimeSpanSeconds - resultCacheControlSeconds) == 0); 129 | Assert.IsFalse(result.Headers.CacheControl.MustRevalidate); 130 | } 131 | 132 | [Test] 133 | public void maxage_mustrevalidate_headers_correct_with_cacheuntil_today() 134 | { 135 | var client = new HttpClient(_server); 136 | var result = client.GetAsync(_url + "Get_until2355_today").Result; 137 | 138 | Assert.IsTrue(Math.Round(new ThisDay(23,55,59).Execute(DateTime.Now).ClientTimeSpan.TotalSeconds - ((TimeSpan)result.Headers.CacheControl.MaxAge).TotalSeconds) == 0); 139 | Assert.IsFalse(result.Headers.CacheControl.MustRevalidate); 140 | } 141 | 142 | [Test] 143 | public void maxage_mustrevalidate_headers_correct_with_cacheuntil_this_month() 144 | { 145 | var client = new HttpClient(_server); 146 | var result = client.GetAsync(_url + "Get_until27_thismonth").Result; 147 | 148 | Assert.IsTrue(Math.Round(new ThisMonth(27,0,0,0).Execute(DateTime.Now).ClientTimeSpan.TotalSeconds - ((TimeSpan)result.Headers.CacheControl.MaxAge).TotalSeconds) == 0); 149 | Assert.IsFalse(result.Headers.CacheControl.MustRevalidate); 150 | } 151 | 152 | [Test] 153 | public void maxage_mustrevalidate_headers_correct_with_cacheuntil_this_year() 154 | { 155 | var client = new HttpClient(_server); 156 | var result = client.GetAsync(_url + "Get_until731_thisyear").Result; 157 | 158 | Assert.IsTrue(Math.Round(new ThisYear(7, 31, 0, 0, 0).Execute(DateTime.Now).ClientTimeSpan.TotalSeconds - ((TimeSpan)result.Headers.CacheControl.MaxAge).TotalSeconds) == 0); 159 | Assert.IsFalse(result.Headers.CacheControl.MustRevalidate); 160 | } 161 | 162 | [Test] 163 | public void maxage_mustrevalidate_headers_correct_with_cacheuntil_this_year_with_revalidate() 164 | { 165 | var client = new HttpClient(_server); 166 | var result = client.GetAsync(_url + "Get_until731_thisyear_mustrevalidate").Result; 167 | 168 | Assert.IsTrue(Math.Round(new ThisYear(7, 31, 0, 0, 0).Execute(DateTime.Now).ClientTimeSpan.TotalSeconds - ((TimeSpan)result.Headers.CacheControl.MaxAge).TotalSeconds) == 0); 169 | Assert.IsTrue(result.Headers.CacheControl.MustRevalidate); 170 | } 171 | 172 | [Test] 173 | public void private_true_headers_correct() 174 | { 175 | var client = new HttpClient(_server); 176 | var result = client.GetAsync(_url + "Get_private").Result; 177 | 178 | Assert.IsTrue(result.Headers.CacheControl.Private); 179 | } 180 | 181 | [Test] 182 | public void shared_max_age_header_correct() 183 | { 184 | var client = new HttpClient(_server); 185 | var result = client.GetAsync(_url + "Get_c100_s100_sm200").Result; 186 | Assert.AreEqual(result.Headers.CacheControl.SharedMaxAge,TimeSpan.FromSeconds(200)); 187 | } 188 | 189 | [Test] 190 | public void shared_max_age_header_not_present() 191 | { 192 | var client = new HttpClient(_server); 193 | var result = client.GetAsync(_url + "Get_c100_s100").Result; 194 | Assert.AreEqual(result.Headers.CacheControl.SharedMaxAge, null); 195 | } 196 | 197 | [TestFixtureTearDown] 198 | public void fixture_dispose() 199 | { 200 | if (_server != null) _server.Dispose(); 201 | } 202 | } 203 | } -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/ConfigurationTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Web.Http; 4 | using Moq; 5 | using NUnit.Framework; 6 | using WebApi.OutputCache.Core.Cache; 7 | 8 | namespace WebApi.OutputCache.V2.Tests 9 | { 10 | [TestFixture] 11 | public class ConfigurationTests 12 | { 13 | private HttpServer _server; 14 | private string _url = "http://www.strathweb.com/api/sample/"; 15 | private Mock _cache; 16 | 17 | [Test] 18 | public void cache_singleton_in_pipeline() 19 | { 20 | _cache = new Mock(); 21 | 22 | var conf = new HttpConfiguration(); 23 | conf.CacheOutputConfiguration().RegisterCacheOutputProvider(() => _cache.Object); 24 | 25 | conf.Routes.MapHttpRoute( 26 | name: "DefaultApi", 27 | routeTemplate: "api/{controller}/{action}/{id}", 28 | defaults: new { id = RouteParameter.Optional } 29 | ); 30 | 31 | _server = new HttpServer(conf); 32 | 33 | var client = new HttpClient(_server); 34 | var result = client.GetAsync(_url + "Get_c100_s100").Result; 35 | 36 | _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100:application/json; charset=utf-8")), Times.Exactly(2)); 37 | 38 | var result2 = client.GetAsync(_url + "Get_c100_s100").Result; 39 | _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100:application/json; charset=utf-8")), Times.Exactly(4)); 40 | 41 | _server.Dispose(); 42 | } 43 | 44 | [Test] 45 | public void cache_singleton() 46 | { 47 | var cache = new MemoryCacheDefault(); 48 | 49 | var conf = new HttpConfiguration(); 50 | conf.CacheOutputConfiguration().RegisterCacheOutputProvider(() => cache); 51 | 52 | object cache1; 53 | conf.Properties.TryGetValue(typeof(IApiOutputCache), out cache1); 54 | 55 | object cache2; 56 | conf.Properties.TryGetValue(typeof(IApiOutputCache), out cache2); 57 | 58 | Assert.AreSame(((Func)cache1)(), ((Func)cache2)()); 59 | } 60 | 61 | [Test] 62 | public void cache_instance() 63 | { 64 | var conf = new HttpConfiguration(); 65 | conf.CacheOutputConfiguration().RegisterCacheOutputProvider(() => new MemoryCacheDefault()); 66 | 67 | object cache1; 68 | conf.Properties.TryGetValue(typeof(IApiOutputCache), out cache1); 69 | 70 | object cache2; 71 | conf.Properties.TryGetValue(typeof(IApiOutputCache), out cache2); 72 | 73 | Assert.AreNotSame(((Func)cache1)(), ((Func)cache2)()); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/ConnegTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Http.Headers; 4 | using System.Web.Http; 5 | using Autofac; 6 | using Autofac.Integration.WebApi; 7 | using Moq; 8 | using NUnit.Framework; 9 | using WebApi.OutputCache.Core.Cache; 10 | 11 | namespace WebApi.OutputCache.V2.Tests 12 | { 13 | [TestFixture] 14 | public class ConnegTests 15 | { 16 | private HttpServer _server; 17 | private string _url = "http://www.strathweb.com/api/sample/"; 18 | private Mock _cache; 19 | 20 | [SetUp] 21 | public void init() 22 | { 23 | _cache = new Mock(); 24 | 25 | var conf = new HttpConfiguration(); 26 | var builder = new ContainerBuilder(); 27 | builder.RegisterInstance(_cache.Object); 28 | 29 | conf.DependencyResolver = new AutofacWebApiDependencyResolver(builder.Build()); 30 | conf.Routes.MapHttpRoute( 31 | name: "DefaultApi", 32 | routeTemplate: "api/{controller}/{action}/{id}", 33 | defaults: new { id = RouteParameter.Optional } 34 | ); 35 | 36 | _server = new HttpServer(conf); 37 | } 38 | 39 | [Test] 40 | public void subsequent_xml_request_is_not_cached() 41 | { 42 | var client = new HttpClient(_server); 43 | var result = client.GetAsync(_url + "Get_c100_s100").Result; 44 | 45 | _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100:application/json; charset=utf-8")), Times.Exactly(2)); 46 | _cache.Verify(s => s.Add(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100:application/json; charset=utf-8"), It.IsAny(), It.Is(x => x < DateTime.Now.AddSeconds(100)), It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100")), Times.Once()); 47 | 48 | var req = new HttpRequestMessage(HttpMethod.Get, _url + "Get_c100_s100"); 49 | req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/xml")); 50 | 51 | var result2 = client.SendAsync(req).Result; 52 | _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100:text/xml; charset=utf-8")), Times.Exactly(2)); 53 | _cache.Verify(s => s.Add(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100:text/xml; charset=utf-8"), It.IsAny(), It.Is(x => x < DateTime.Now.AddSeconds(100)), It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100")), Times.Once()); 54 | 55 | } 56 | 57 | [TearDown] 58 | public void fixture_dispose() 59 | { 60 | if (_server != null) _server.Dispose(); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/CustomHeadersContent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Net.Http.Formatting; 6 | using System.Net.Http.Headers; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using System.Web.Http; 11 | using System.Web.Http.Results; 12 | 13 | namespace WebApi.OutputCache.V2.Tests 14 | { 15 | public class CustomHeadersContent : OkNegotiatedContentResult 16 | { 17 | public string ContentDisposition { get; set; } 18 | 19 | public List ContentEncoding { get; set; } 20 | 21 | public string RequestHeader1 { get; set; } 22 | 23 | public List RequestHeader2 { get; set; } 24 | 25 | public CustomHeadersContent(T content, ApiController controller) 26 | : base(content, controller) { } 27 | 28 | public CustomHeadersContent(T content, IContentNegotiator contentNegotiator, HttpRequestMessage request, IEnumerable formatters) 29 | : base(content, contentNegotiator, request, formatters) { } 30 | 31 | public override async Task ExecuteAsync(CancellationToken cancellationToken) 32 | { 33 | HttpResponseMessage response = await base.ExecuteAsync(cancellationToken); 34 | 35 | if (!string.IsNullOrWhiteSpace(ContentDisposition)) 36 | { 37 | response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue(ContentDisposition); 38 | } 39 | if (ContentEncoding != null) 40 | { 41 | foreach (var contentEncoding in ContentEncoding) 42 | { 43 | response.Content.Headers.ContentEncoding.Add(contentEncoding); 44 | } 45 | } 46 | 47 | if (!string.IsNullOrWhiteSpace(RequestHeader1)) 48 | { 49 | response.Headers.Add("RequestHeader1", RequestHeader1); 50 | } 51 | if (RequestHeader2 != null) 52 | { 53 | foreach (var requestHeader2Value in RequestHeader2) 54 | { 55 | response.Headers.Add("RequestHeader2", requestHeader2Value); 56 | } 57 | } 58 | 59 | return response; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/CustomHeadersTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Net.Http.Headers; 5 | using System.Security.Principal; 6 | using System.Threading; 7 | using System.Web.Http; 8 | using Autofac; 9 | using Autofac.Integration.WebApi; 10 | using Moq; 11 | using NUnit.Framework; 12 | using WebApi.OutputCache.Core; 13 | using WebApi.OutputCache.Core.Cache; 14 | using System.Collections.Generic; 15 | using System.Linq; 16 | 17 | namespace WebApi.OutputCache.V2.Tests 18 | { 19 | [TestFixture] 20 | public class CustomHeadersTests 21 | { 22 | private HttpServer _server; 23 | private string _url = "http://www.strathweb.com/api/customheaders/"; 24 | private IApiOutputCache _cache; 25 | 26 | [SetUp] 27 | public void init() 28 | { 29 | Thread.CurrentPrincipal = null; 30 | 31 | _cache = new SimpleCacheForTests(); 32 | 33 | var conf = new HttpConfiguration(); 34 | var builder = new ContainerBuilder(); 35 | builder.RegisterInstance(_cache); 36 | 37 | conf.DependencyResolver = new AutofacWebApiDependencyResolver(builder.Build()); 38 | conf.Routes.MapHttpRoute( 39 | name: "DefaultApi", 40 | routeTemplate: "api/{controller}/{action}/{id}", 41 | defaults: new { id = RouteParameter.Optional } 42 | ); 43 | 44 | _server = new HttpServer(conf); 45 | } 46 | 47 | [Test] 48 | public void cache_custom_content_header() { 49 | var client = new HttpClient(_server); 50 | var req = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Custom_Content_Header"); 51 | var result = client.SendAsync(req).Result; 52 | 53 | var req2 = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Custom_Content_Header"); 54 | var result2 = client.SendAsync(req2).Result; 55 | 56 | Assert.That(result.Content.Headers.ContentDisposition.DispositionType, Is.EqualTo("attachment")); 57 | Assert.That(result2.Content.Headers.ContentDisposition.DispositionType, Is.EqualTo("attachment")); 58 | } 59 | 60 | [Test] 61 | public void cache_custom_content_header_with_multiply_values() 62 | { 63 | var client = new HttpClient(_server); 64 | var req = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Custom_Content_Header_Multiply_Values"); 65 | var result = client.SendAsync(req).Result; 66 | 67 | var req2 = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Custom_Content_Header_Multiply_Values"); 68 | var result2 = client.SendAsync(req2).Result; 69 | 70 | Assert.That(result.Content.Headers.ContentEncoding.Count, Is.EqualTo(2)); 71 | Assert.That(result.Content.Headers.ContentEncoding.First(), Is.EqualTo("deflate")); 72 | Assert.That(result.Content.Headers.ContentEncoding.Last(), Is.EqualTo("gzip")); 73 | 74 | Assert.That(result2.Content.Headers.ContentEncoding.Count, Is.EqualTo(2)); 75 | Assert.That(result2.Content.Headers.ContentEncoding.First(), Is.EqualTo("deflate")); 76 | Assert.That(result2.Content.Headers.ContentEncoding.Last(), Is.EqualTo("gzip")); 77 | } 78 | 79 | [Test] 80 | public void cache_custom_response_header() 81 | { 82 | var client = new HttpClient(_server); 83 | var req = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Custom_Response_Header"); 84 | var result = client.SendAsync(req).Result; 85 | 86 | var req2 = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Custom_Response_Header"); 87 | var result2 = client.SendAsync(req2).Result; 88 | 89 | Assert.That(result.Headers.GetValues("RequestHeader1").First(), Is.EqualTo("value1")); 90 | Assert.That(result2.Headers.GetValues("RequestHeader1").First(), Is.EqualTo("value1")); 91 | } 92 | 93 | [Test] 94 | public void cache_custom_response_header_with_multiply_values() 95 | { 96 | var client = new HttpClient(_server); 97 | var req = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Custom_Response_Header_Multiply_Values"); 98 | var result = client.SendAsync(req).Result; 99 | 100 | var req2 = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Custom_Response_Header_Multiply_Values"); 101 | var result2 = client.SendAsync(req2).Result; 102 | 103 | Assert.That(result.Headers.GetValues("RequestHeader2").Count(), Is.EqualTo(2)); 104 | Assert.That(result.Headers.GetValues("RequestHeader2").First(), Is.EqualTo("value2")); 105 | Assert.That(result.Headers.GetValues("RequestHeader2").Last(), Is.EqualTo("value3")); 106 | 107 | Assert.That(result2.Headers.GetValues("RequestHeader2").Count(), Is.EqualTo(2)); 108 | Assert.That(result2.Headers.GetValues("RequestHeader2").First(), Is.EqualTo("value2")); 109 | Assert.That(result2.Headers.GetValues("RequestHeader2").Last(), Is.EqualTo("value3")); 110 | } 111 | 112 | [Test] 113 | public void cache_multiply_custom_headers() 114 | { 115 | var client = new HttpClient(_server); 116 | var req = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Multiply_Custom_Headers"); 117 | var result = client.SendAsync(req).Result; 118 | 119 | var req2 = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Multiply_Custom_Headers"); 120 | var result2 = client.SendAsync(req2).Result; 121 | 122 | Assert.That(result.Content.Headers.ContentDisposition.DispositionType, Is.EqualTo("attachment")); 123 | Assert.That(result.Content.Headers.ContentEncoding.Count, Is.EqualTo(2)); 124 | Assert.That(result.Content.Headers.ContentEncoding.First(), Is.EqualTo("deflate")); 125 | Assert.That(result.Content.Headers.ContentEncoding.Last(), Is.EqualTo("gzip")); 126 | Assert.That(result.Headers.GetValues("RequestHeader1").First(), Is.EqualTo("value1")); 127 | Assert.That(result.Headers.GetValues("RequestHeader2").Count(), Is.EqualTo(2)); 128 | Assert.That(result.Headers.GetValues("RequestHeader2").First(), Is.EqualTo("value2")); 129 | Assert.That(result.Headers.GetValues("RequestHeader2").Last(), Is.EqualTo("value3")); 130 | 131 | Assert.That(result2.Content.Headers.ContentDisposition.DispositionType, Is.EqualTo("attachment")); 132 | Assert.That(result2.Content.Headers.ContentEncoding.Count, Is.EqualTo(2)); 133 | Assert.That(result2.Content.Headers.ContentEncoding.First(), Is.EqualTo("deflate")); 134 | Assert.That(result2.Content.Headers.ContentEncoding.Last(), Is.EqualTo("gzip")); 135 | Assert.That(result2.Headers.GetValues("RequestHeader1").First(), Is.EqualTo("value1")); 136 | Assert.That(result2.Headers.GetValues("RequestHeader2").Count(), Is.EqualTo(2)); 137 | Assert.That(result2.Headers.GetValues("RequestHeader2").First(), Is.EqualTo("value2")); 138 | Assert.That(result2.Headers.GetValues("RequestHeader2").Last(), Is.EqualTo("value3")); 139 | } 140 | 141 | [Test] 142 | public void cache_part_of_custom_headers() 143 | { 144 | var client = new HttpClient(_server); 145 | var req = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Part_Of_Custom_Headers"); 146 | var result = client.SendAsync(req).Result; 147 | 148 | var req2 = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Part_Of_Custom_Headers"); 149 | var result2 = client.SendAsync(req2).Result; 150 | 151 | Assert.That(result.Content.Headers.ContentDisposition.DispositionType, Is.EqualTo("attachment")); 152 | Assert.That(result.Content.Headers.ContentEncoding.Count, Is.EqualTo(2)); 153 | Assert.That(result.Content.Headers.ContentEncoding.First(), Is.EqualTo("deflate")); 154 | Assert.That(result.Content.Headers.ContentEncoding.Last(), Is.EqualTo("gzip")); 155 | Assert.That(result.Headers.GetValues("RequestHeader1").First(), Is.EqualTo("value1")); 156 | Assert.That(result.Headers.GetValues("RequestHeader2").Count(), Is.EqualTo(2)); 157 | Assert.That(result.Headers.GetValues("RequestHeader2").First(), Is.EqualTo("value2")); 158 | Assert.That(result.Headers.GetValues("RequestHeader2").Last(), Is.EqualTo("value3")); 159 | 160 | Assert.That(result2.Content.Headers.ContentDisposition, Is.Null); 161 | Assert.That(result2.Content.Headers.ContentEncoding.Count, Is.EqualTo(2)); 162 | Assert.That(result2.Content.Headers.ContentEncoding.First(), Is.EqualTo("deflate")); 163 | Assert.That(result2.Content.Headers.ContentEncoding.Last(), Is.EqualTo("gzip")); 164 | 165 | IEnumerable headerValue = null; 166 | Assert.That(result2.Headers.TryGetValues("RequestHeader1", out headerValue), Is.False); 167 | Assert.That(result2.Headers.TryGetValues("RequestHeader2", out headerValue), Is.False); 168 | } 169 | 170 | [TearDown] 171 | public void fixture_dispose() 172 | { 173 | if (_server != null) _server.Dispose(); 174 | } 175 | } 176 | } -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/DefaultCacheKeyGeneratorTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | 4 | namespace WebApi.OutputCache.V2.Tests 5 | { 6 | [TestFixture] 7 | public class DefaultCacheKeyGeneratorTests : CacheKeyGenerationTestsBase 8 | { 9 | protected override DefaultCacheKeyGenerator BuildCacheKeyGenerator() 10 | { 11 | return new DefaultCacheKeyGenerator(); 12 | } 13 | 14 | [Test] 15 | public void NoParametersIncludeQueryString_ShouldReturnBaseKeyAndQueryStringAndMediaTypeConcatenated() 16 | { 17 | var cacheKey = cacheKeyGenerator.MakeCacheKey(context, mediaType, false); 18 | 19 | AssertCacheKeysBasicFormat(cacheKey); 20 | Assert.AreEqual(String.Format("{0}-{1}:{2}", BaseCacheKey, requestUri.Query.Substring(1), mediaType), cacheKey, 21 | "Key does not match expected -:"); 22 | } 23 | 24 | [Test] 25 | public void NoParametersExcludeQueryString_ShouldReturnBaseKeyAndMediaTypeConcatenated() 26 | { 27 | var cacheKey = cacheKeyGenerator.MakeCacheKey(context, mediaType, true); 28 | 29 | AssertCacheKeysBasicFormat(cacheKey); 30 | Assert.AreEqual(String.Format("{0}:{1}", BaseCacheKey, mediaType), cacheKey, 31 | "Key does not match expected :"); 32 | } 33 | 34 | [Test] 35 | public void WithParametersIncludeQueryString_ShouldReturnBaseKeyAndArgumentsAndQueryStringAndMediaTypeConcatenated() 36 | { 37 | AddActionArgumentsToContext(); 38 | var cacheKey = cacheKeyGenerator.MakeCacheKey(context, mediaType, false); 39 | 40 | AssertCacheKeysBasicFormat(cacheKey); 41 | Assert.AreEqual(String.Format("{0}-{1}&{2}:{3}", BaseCacheKey, FormatActionArgumentsForKeyAssertion(), requestUri.Query.Substring(1), mediaType), cacheKey, 42 | "Key does not match expected -&:"); 43 | } 44 | 45 | [Test] 46 | public void WithParametersExcludeQueryString_ShouldReturnBaseKeyAndArgumentsAndMediaTypeConcatenated() 47 | { 48 | AddActionArgumentsToContext(); 49 | var cacheKey = cacheKeyGenerator.MakeCacheKey(context, mediaType, true); 50 | 51 | AssertCacheKeysBasicFormat(cacheKey); 52 | Assert.AreEqual(String.Format("{0}-{1}:{2}", BaseCacheKey, FormatActionArgumentsForKeyAssertion(), mediaType), cacheKey, 53 | "Key does not match expected -:"); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/InlineInvalidateTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading; 3 | using System.Web.Http; 4 | using Autofac; 5 | using Autofac.Integration.WebApi; 6 | using Moq; 7 | using NUnit.Framework; 8 | using WebApi.OutputCache.Core.Cache; 9 | 10 | namespace WebApi.OutputCache.V2.Tests 11 | { 12 | [TestFixture] 13 | public class InlineInvalidateTests 14 | { 15 | private HttpServer _server; 16 | private string _url = "http://www.strathweb.com/api/inlineinvalidate/"; 17 | private Mock _cache; 18 | 19 | [SetUp] 20 | public void init() 21 | { 22 | Thread.CurrentPrincipal = null; 23 | 24 | _cache = new Mock(); 25 | 26 | var conf = new HttpConfiguration(); 27 | var builder = new ContainerBuilder(); 28 | builder.RegisterInstance(_cache.Object); 29 | 30 | conf.DependencyResolver = new AutofacWebApiDependencyResolver(builder.Build()); 31 | conf.Routes.MapHttpRoute( 32 | name: "DefaultApi", 33 | routeTemplate: "api/{controller}/{action}/{id}", 34 | defaults: new { id = RouteParameter.Optional } 35 | ); 36 | 37 | _server = new HttpServer(conf); 38 | } 39 | 40 | [Test] 41 | public void inline_call_to_invalidate_is_correct() 42 | { 43 | var client = new HttpClient(_server); 44 | 45 | var result = client.PostAsync(_url + "Post", new StringContent(string.Empty)).Result; 46 | 47 | _cache.Verify(s => s.RemoveStartsWith(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.inlineinvalidatecontroller-get_c100_s100")), Times.Exactly(1)); 48 | } 49 | 50 | [Test] 51 | public void inline_call_to_invalidate_using_expression_tree_is_correct() 52 | { 53 | var client = new HttpClient(_server); 54 | var result = client.PutAsync(_url + "Put", new StringContent(string.Empty)).Result; 55 | 56 | _cache.Verify(s => s.RemoveStartsWith(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.inlineinvalidatecontroller-get_c100_s100")), Times.Exactly(1)); 57 | } 58 | 59 | [Test] 60 | public void inline_call_to_invalidate_using_expression_tree_with_param_is_correct() 61 | { 62 | var client = new HttpClient(_server); 63 | var result = client.DeleteAsync(_url + "Delete_parameterized").Result; 64 | 65 | _cache.Verify(s => s.RemoveStartsWith(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.inlineinvalidatecontroller-get_c100_s100_with_param")), Times.Exactly(1)); 66 | } 67 | 68 | [Test] 69 | public void inline_call_to_invalidate_using_expression_tree_with_custom_action_name_is_correct() 70 | { 71 | var client = new HttpClient(_server); 72 | var result = client.DeleteAsync(_url + "Delete_non_standard_name").Result; 73 | 74 | _cache.Verify(s => s.RemoveStartsWith(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.inlineinvalidatecontroller-getbyid")), Times.Exactly(1)); 75 | } 76 | 77 | [TearDown] 78 | public void fixture_dispose() 79 | { 80 | if (_server != null) _server.Dispose(); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/InvalidateTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Net.Http.Formatting; 3 | using System.Threading; 4 | using System.Web.Http; 5 | using Autofac; 6 | using Autofac.Integration.WebApi; 7 | using Moq; 8 | using NUnit.Framework; 9 | using WebApi.OutputCache.Core.Cache; 10 | 11 | namespace WebApi.OutputCache.V2.Tests 12 | { 13 | [TestFixture] 14 | public class InvalidateTests 15 | { 16 | private HttpServer _server; 17 | private string _url = "http://www.strathweb.com/api/sample/"; 18 | private Mock _cache; 19 | 20 | [SetUp] 21 | public void init() 22 | { 23 | Thread.CurrentPrincipal = null; 24 | 25 | _cache = new Mock(); 26 | 27 | var conf = new HttpConfiguration(); 28 | var builder = new ContainerBuilder(); 29 | builder.RegisterInstance(_cache.Object); 30 | 31 | conf.DependencyResolver = new AutofacWebApiDependencyResolver(builder.Build()); 32 | conf.Routes.MapHttpRoute( 33 | name: "DefaultApi", 34 | routeTemplate: "api/{controller}/{action}/{id}", 35 | defaults: new { id = RouteParameter.Optional } 36 | ); 37 | 38 | _server = new HttpServer(conf); 39 | } 40 | 41 | [Test] 42 | public void regular_invalidate_works_on_post() 43 | { 44 | SetupCacheForAutoInvalidate(); 45 | var client = new HttpClient(_server); 46 | 47 | var result2 = client.PostAsync(_url + "Post", new StringContent(string.Empty)).Result; 48 | 49 | _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100")), Times.Exactly(1)); 50 | _cache.Verify(s => s.RemoveStartsWith(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100")), Times.Exactly(1)); 51 | } 52 | 53 | [Test] 54 | public void regular_invalidate_on_two_methods_works_on_post() 55 | { 56 | SetupCacheForAutoInvalidate(); 57 | var client = new HttpClient(_server); 58 | 59 | var result2 = client.PostAsync(_url + "Post_2_invalidates", new StringContent(string.Empty)).Result; 60 | 61 | _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100")), Times.Exactly(1)); 62 | _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_s50_exclude_fakecallback")), Times.Exactly(1)); 63 | _cache.Verify(s => s.RemoveStartsWith(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100")), Times.Exactly(1)); 64 | _cache.Verify(s => s.RemoveStartsWith(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_s50_exclude_fakecallback")), Times.Exactly(1)); 65 | } 66 | 67 | [Test] 68 | public void controller_level_invalidate_on_three_methods_works_on_post() 69 | { 70 | SetupCacheForAutoInvalidate(); 71 | var client = new HttpClient(_server); 72 | 73 | var result2 = client.PostAsync("http://www.strathweb.com/api/autoinvalidate/Post", new StringContent(string.Empty)).Result; 74 | 75 | _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-get_c100_s100")), Times.Exactly(1)); 76 | _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-get_s50_exclude_fakecallback")), Times.Exactly(1)); 77 | _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-etag_match_304")), Times.Exactly(1)); 78 | _cache.Verify(s => s.RemoveStartsWith(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-get_c100_s100")), Times.Exactly(1)); 79 | _cache.Verify(s => s.RemoveStartsWith(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-get_s50_exclude_fakecallback")), Times.Exactly(1)); 80 | _cache.Verify(s => s.RemoveStartsWith(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-etag_match_304")), Times.Exactly(1)); 81 | } 82 | 83 | [Test] 84 | public void controller_level_invalidate_on_three_methods_works_on_put() 85 | { 86 | SetupCacheForAutoInvalidate(); 87 | var client = new HttpClient(_server); 88 | 89 | var result2 = client.PutAsync("http://www.strathweb.com/api/autoinvalidate/Put", new StringContent(string.Empty)).Result; 90 | 91 | _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-get_c100_s100")), Times.Exactly(1)); 92 | _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-get_s50_exclude_fakecallback")), Times.Exactly(1)); 93 | _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-etag_match_304")), Times.Exactly(1)); 94 | _cache.Verify(s => s.RemoveStartsWith(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-get_c100_s100")), Times.Exactly(1)); 95 | _cache.Verify(s => s.RemoveStartsWith(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-get_s50_exclude_fakecallback")), Times.Exactly(1)); 96 | _cache.Verify(s => s.RemoveStartsWith(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-etag_match_304")), Times.Exactly(1)); 97 | } 98 | 99 | [Test] 100 | public void controller_level_invalidate_on_three_methods_works_on_delete() 101 | { 102 | SetupCacheForAutoInvalidate(); 103 | var client = new HttpClient(_server); 104 | 105 | var result2 = client.DeleteAsync("http://www.strathweb.com/api/autoinvalidate/Delete").Result; 106 | 107 | _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-get_c100_s100")), Times.Exactly(1)); 108 | _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-get_s50_exclude_fakecallback")), Times.Exactly(1)); 109 | _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-etag_match_304")), Times.Exactly(1)); 110 | _cache.Verify(s => s.RemoveStartsWith(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-get_c100_s100")), Times.Exactly(1)); 111 | _cache.Verify(s => s.RemoveStartsWith(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-get_s50_exclude_fakecallback")), Times.Exactly(1)); 112 | _cache.Verify(s => s.RemoveStartsWith(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-etag_match_304")), Times.Exactly(1)); 113 | } 114 | 115 | [Test] 116 | public void controller_level_invalidate_with_type_check_does_not_invalidate_on_no_type_match() 117 | { 118 | SetupCacheForAutoInvalidate(); 119 | var client = new HttpClient(_server); 120 | 121 | var result2 = client.PostAsync("http://www.strathweb.com/api/autoinvalidatewithtype/Post", new StringContent(string.Empty)).Result; 122 | 123 | Assert.True(result2.IsSuccessStatusCode); 124 | _cache.Verify(s => s.Contains(It.IsAny()), Times.Never()); 125 | _cache.Verify(s => s.RemoveStartsWith(It.IsAny()), Times.Never()); 126 | } 127 | 128 | [Test] 129 | public void controller_level_invalidate_with_type_check_invalidates_only_methods_with_types_matched() 130 | { 131 | SetupCacheForAutoInvalidate(); 132 | var client = new HttpClient(_server); 133 | 134 | var result2 = client.PostAsync("http://www.strathweb.com/api/autoinvalidatewithtype/PostString", "hi", new JsonMediaTypeFormatter()).Result; 135 | 136 | _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatewithtypecontroller-get_c100_s100")), Times.Exactly(1)); 137 | _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatewithtypecontroller-get_c100_s100_array")), Times.Exactly(1)); 138 | _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatewithtypecontroller-get_s50_exclude_fakecallback")), Times.Never()); 139 | _cache.Verify(s => s.RemoveStartsWith(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatewithtypecontroller-get_c100_s100")), Times.Exactly(1)); 140 | _cache.Verify(s => s.RemoveStartsWith(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatewithtypecontroller-get_c100_s100_array")), Times.Exactly(1)); 141 | _cache.Verify(s => s.RemoveStartsWith(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatewithtypecontroller-get_s50_exclude_fakecallback")), Times.Never()); 142 | } 143 | 144 | private void SetupCacheForAutoInvalidate() 145 | { 146 | _cache.Setup(x => x.Contains(It.Is(s => s == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_s50_exclude_fakecallback"))).Returns(true); 147 | _cache.Setup(x => x.Contains(It.Is(s => s == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100"))).Returns(true); 148 | _cache.Setup(x => x.Contains(It.Is(s => s == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-get_c100_s100"))).Returns(true); 149 | _cache.Setup(x => x.Contains(It.Is(s => s == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-get_s50_exclude_fakecallback"))).Returns(true); 150 | _cache.Setup(x => x.Contains(It.Is(s => s == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatecontroller-etag_match_304"))).Returns(true); 151 | _cache.Setup(x => x.Contains(It.Is(s => s == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatewithtypecontroller-get_c100_s100"))).Returns(true); 152 | _cache.Setup(x => x.Contains(It.Is(s => s == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatewithtypecontroller-get_s50_exclude_fakecallback"))).Returns(true); 153 | _cache.Setup(x => x.Contains(It.Is(s => s == "webapi.outputcache.v2.tests.testcontrollers.autoinvalidatewithtypecontroller-get_c100_s100_array"))).Returns(true); 154 | } 155 | 156 | [TearDown] 157 | public void fixture_dispose() 158 | { 159 | if (_server != null) _server.Dispose(); 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/MemoryCacheForTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using WebApi.OutputCache.Core.Cache; 7 | 8 | namespace WebApi.OutputCache.V2.Tests 9 | { 10 | public class SimpleCacheForTests : IApiOutputCache 11 | { 12 | private Dictionary _cachedItems; 13 | 14 | public SimpleCacheForTests() { 15 | _cachedItems = new Dictionary(); 16 | } 17 | 18 | public virtual void RemoveStartsWith(string key) 19 | { 20 | throw new NotImplementedException(); 21 | } 22 | 23 | public virtual T Get(string key) where T : class 24 | { 25 | var o = _cachedItems[key] as T; 26 | return o; 27 | } 28 | 29 | [Obsolete("Use Get instead")] 30 | public virtual object Get(string key) 31 | { 32 | return _cachedItems[key]; 33 | } 34 | 35 | public virtual void Remove(string key) 36 | { 37 | _cachedItems.Remove(key); 38 | } 39 | 40 | public virtual bool Contains(string key) 41 | { 42 | return _cachedItems.ContainsKey(key); 43 | } 44 | 45 | public virtual void Add(string key, object o, DateTimeOffset expiration, string dependsOnKey = null) 46 | { 47 | _cachedItems.Add(key, o); 48 | } 49 | 50 | public virtual IEnumerable AllKeys 51 | { 52 | get 53 | { 54 | return _cachedItems.Keys; 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/PerUserCacheKeyGeneratorTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Security.Principal; 4 | 5 | namespace WebApi.OutputCache.V2.Tests 6 | { 7 | [TestFixture] 8 | public class PerUserCacheKeyGeneratorTests : CacheKeyGenerationTestsBase 9 | { 10 | private const string UserIdentityName = "SomeUserIDon'tMind"; 11 | 12 | [SetUp] 13 | public override void Setup() 14 | { 15 | base.Setup(); 16 | context.RequestContext.Principal = new GenericPrincipal(new GenericIdentity(UserIdentityName), new string[0]); 17 | } 18 | 19 | protected override PerUserCacheKeyGenerator BuildCacheKeyGenerator() 20 | { 21 | return new PerUserCacheKeyGenerator(); 22 | } 23 | 24 | private string FormatUserIdentityForAssertion() 25 | { 26 | return UserIdentityName.ToLower(); 27 | } 28 | 29 | [Test] 30 | public void NoParametersIncludeQueryString_ShouldReturnBaseKeyAndQueryStringAndUserIdentityAndMediaTypeConcatenated() 31 | { 32 | var cacheKey = cacheKeyGenerator.MakeCacheKey(context, mediaType, false); 33 | 34 | AssertCacheKeysBasicFormat(cacheKey); 35 | Assert.AreEqual(String.Format("{0}-{1}:{2}:{3}", BaseCacheKey, requestUri.Query.Substring(1), FormatUserIdentityForAssertion(), mediaType), cacheKey, 36 | "Key does not match expected -::"); 37 | } 38 | 39 | [Test] 40 | public void NoParametersExcludeQueryString_ShouldReturnBaseKeyAndUserIdentityAndMediaTypeConcatenated() 41 | { 42 | var cacheKey = cacheKeyGenerator.MakeCacheKey(context, mediaType, true); 43 | 44 | AssertCacheKeysBasicFormat(cacheKey); 45 | Assert.AreEqual(String.Format("{0}:{1}:{2}", BaseCacheKey, FormatUserIdentityForAssertion(), mediaType), cacheKey, 46 | "Key does not match expected ::"); 47 | } 48 | 49 | [Test] 50 | public void WithParametersIncludeQueryString_ShouldReturnBaseKeyAndArgumentsAndQueryStringAndUserIdentityAndMediaTypeConcatenated() 51 | { 52 | AddActionArgumentsToContext(); 53 | var cacheKey = cacheKeyGenerator.MakeCacheKey(context, mediaType, false); 54 | 55 | AssertCacheKeysBasicFormat(cacheKey); 56 | Assert.AreEqual(String.Format("{0}-{1}&{2}:{3}:{4}", BaseCacheKey, FormatActionArgumentsForKeyAssertion(), requestUri.Query.Substring(1), FormatUserIdentityForAssertion(), mediaType), cacheKey, 57 | "Key does not match expected -&::"); 58 | } 59 | 60 | [Test] 61 | public void WithParametersExcludeQueryString_ShouldReturnBaseKeyAndArgumentsAndUserIdentityAndMediaTypeConcatenated() 62 | { 63 | AddActionArgumentsToContext(); 64 | var cacheKey = cacheKeyGenerator.MakeCacheKey(context, mediaType, true); 65 | 66 | AssertCacheKeysBasicFormat(cacheKey); 67 | Assert.AreEqual(String.Format("{0}-{1}:{2}:{3}", BaseCacheKey, FormatActionArgumentsForKeyAssertion(), FormatUserIdentityForAssertion(), mediaType), cacheKey, 68 | "Key does not match expected -::"); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/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("WebAPI.OutputCache.Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("WebAPI.OutputCache.Tests")] 13 | [assembly: AssemblyCopyright("Copyright © 2013")] 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("a3aa26bd-d83e-42ef-bf3b-39875348ae0b")] 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 | -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/TestControllers/AutoInvalidateController.cs: -------------------------------------------------------------------------------- 1 | using System.Web.Http; 2 | 3 | namespace WebApi.OutputCache.V2.Tests.TestControllers 4 | { 5 | [AutoInvalidateCacheOutput] 6 | public class AutoInvalidateController : ApiController 7 | { 8 | [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 100)] 9 | public string Get_c100_s100() 10 | { 11 | return "test"; 12 | } 13 | 14 | [CacheOutput(ServerTimeSpan = 50)] 15 | public string Get_s50_exclude_fakecallback(int id = 0, string callback = null, string de = null) 16 | { 17 | return "test"; 18 | } 19 | 20 | [HttpGet] 21 | [CacheOutput(AnonymousOnly = true, ClientTimeSpan = 50, ServerTimeSpan = 50)] 22 | public string etag_match_304() 23 | { 24 | return "value"; 25 | } 26 | 27 | public void Post() 28 | { 29 | //do nothing 30 | } 31 | 32 | public void Put() 33 | { 34 | //do nothing 35 | } 36 | 37 | public void Delete() 38 | { 39 | //do nothing 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/TestControllers/AutoInvalidateWithTypeController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Web.Http; 3 | 4 | namespace WebApi.OutputCache.V2.Tests.TestControllers 5 | { 6 | [AutoInvalidateCacheOutput(TryMatchType = true)] 7 | public class AutoInvalidateWithTypeController : ApiController 8 | { 9 | [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 100)] 10 | public string Get_c100_s100() 11 | { 12 | return "test"; 13 | } 14 | 15 | [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 100)] 16 | public List Get_c100_s100_array() 17 | { 18 | return new List {"test"}; 19 | } 20 | 21 | [CacheOutput(ServerTimeSpan = 50)] 22 | public int Get_s50_exclude_fakecallback(int id = 0, string callback = null, string de = null) 23 | { 24 | return 7; 25 | } 26 | 27 | public void Post() 28 | { 29 | //this should not invalidate 30 | } 31 | 32 | public void PostString([FromBody]string x) 33 | { 34 | //this should invalidate string & ienumerable 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/TestControllers/CacheKeyController.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using System.Web.Http; 3 | using System.Web.Http.Controllers; 4 | 5 | namespace WebApi.OutputCache.V2.Tests.TestControllers 6 | { 7 | public class CacheKeyController : ApiController 8 | { 9 | private class UnregisteredCacheKeyGenerator : ICacheKeyGenerator 10 | { 11 | public string MakeCacheKey(HttpActionContext context, MediaTypeHeaderValue mediaType, bool excludeQueryString = false) 12 | { 13 | return "unregistered"; 14 | } 15 | } 16 | 17 | [CacheOutput(CacheKeyGenerator = typeof(CacheKeyGeneratorTests.CustomCacheKeyGenerator), ClientTimeSpan = 100, ServerTimeSpan = 100)] 18 | public string Get_custom_key() 19 | { 20 | return "test"; 21 | } 22 | 23 | [CacheOutput(CacheKeyGenerator = typeof(UnregisteredCacheKeyGenerator))] 24 | public string Get_unregistered() 25 | { 26 | return "test"; 27 | } 28 | 29 | [CacheOutput(CacheKeyGenerator = typeof(CacheKeyGeneratorRegistrationTests.InternalRegisteredCacheKeyGenerator), ServerTimeSpan = 100)] 30 | public string Get_internalregistered() 31 | { 32 | return "test"; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/TestControllers/CacheKeyGenerationController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web.Http; 5 | 6 | namespace WebApi.OutputCache.V2.Tests.TestControllers 7 | { 8 | /// 9 | /// Controller needed for generating the needed for testing the implementations 10 | /// 11 | [RoutePrefix("cacheKeyGeneration")] 12 | public class CacheKeyGenerationController : ApiController 13 | { 14 | private readonly string[] Values = new string[] { "first", "second", "third" }; 15 | 16 | [Route("")] 17 | public IEnumerable Get([FromUri(Name="filter")]string filterExpression) 18 | { 19 | return String.IsNullOrWhiteSpace(filterExpression) ? Values : Values.Where(x => x.Contains(filterExpression)); 20 | } 21 | 22 | [Route("{index}")] 23 | public string GetByIndex(int index) 24 | { 25 | return Values[index]; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/TestControllers/CustomHeadersController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Web.Http; 7 | 8 | namespace WebApi.OutputCache.V2.Tests.TestControllers 9 | { 10 | public class CustomHeadersController : ApiController 11 | { 12 | [HttpGet] 13 | [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 100, IncludeCustomHeaders = "Content-Disposition")] 14 | public IHttpActionResult Cache_Custom_Content_Header() 15 | { 16 | var result = new CustomHeadersContent("test", this) 17 | { 18 | ContentDisposition = "attachment" 19 | }; 20 | 21 | return result; 22 | } 23 | 24 | [HttpGet] 25 | [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 100, IncludeCustomHeaders = "Content-Encoding")] 26 | public IHttpActionResult Cache_Custom_Content_Header_Multiply_Values() 27 | { 28 | var result = new CustomHeadersContent("test", this) 29 | { 30 | ContentEncoding = new List { "deflate", "gzip" } 31 | }; 32 | 33 | return result; 34 | } 35 | 36 | [HttpGet] 37 | [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 100, IncludeCustomHeaders = "RequestHeader1")] 38 | public IHttpActionResult Cache_Custom_Response_Header() 39 | { 40 | var result = new CustomHeadersContent("test", this) 41 | { 42 | RequestHeader1 = "value1" 43 | }; 44 | 45 | return result; 46 | } 47 | 48 | [HttpGet] 49 | [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 100, IncludeCustomHeaders = "RequestHeader2")] 50 | public IHttpActionResult Cache_Custom_Response_Header_Multiply_Values() 51 | { 52 | var result = new CustomHeadersContent("test", this) 53 | { 54 | RequestHeader2 = new List { "value2", "value3" } 55 | }; 56 | 57 | return result; 58 | } 59 | 60 | [HttpGet] 61 | [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 100, IncludeCustomHeaders = "Content-Disposition,Content-Encoding,RequestHeader2,RequestHeader1")] 62 | public IHttpActionResult Cache_Multiply_Custom_Headers() 63 | { 64 | var result = new CustomHeadersContent("test", this) 65 | { 66 | ContentDisposition = "attachment", 67 | ContentEncoding = new List { "deflate", "gzip" }, 68 | RequestHeader1 = "value1", 69 | RequestHeader2 = new List { "value2", "value3" } 70 | }; 71 | 72 | return result; 73 | } 74 | 75 | [HttpGet] 76 | [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 100, IncludeCustomHeaders = "Content-Encoding,NotExistingHeader")] 77 | public IHttpActionResult Cache_Part_Of_Custom_Headers() 78 | { 79 | var result = new CustomHeadersContent("test", this) 80 | { 81 | ContentDisposition = "attachment", 82 | ContentEncoding = new List { "deflate", "gzip" }, 83 | RequestHeader1 = "value1", 84 | RequestHeader2 = new List { "value2", "value3" } 85 | }; 86 | 87 | return result; 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/TestControllers/IgnoreController.cs: -------------------------------------------------------------------------------- 1 | using System.Web.Http; 2 | 3 | namespace WebApi.OutputCache.V2.Tests.TestControllers 4 | { 5 | [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 100)] 6 | public class IgnoreController : ApiController 7 | { 8 | [HttpGet] 9 | public string Cached() 10 | { 11 | return "test"; 12 | } 13 | 14 | [HttpGet] 15 | [IgnoreCacheOutput] 16 | public string NotCached() 17 | { 18 | return "test"; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/TestControllers/InlineInvalidateController.cs: -------------------------------------------------------------------------------- 1 | using System.Web.Http; 2 | 3 | namespace WebApi.OutputCache.V2.Tests.TestControllers 4 | { 5 | public class InlineInvalidateController : ApiController 6 | { 7 | [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 100)] 8 | public string Get_c100_s100() 9 | { 10 | return "test"; 11 | } 12 | 13 | [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 100)] 14 | public string Get_c100_s100_with_param(int id) 15 | { 16 | return "test"; 17 | } 18 | 19 | [ActionName("getById")] 20 | [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 100)] 21 | public string Get_c100_s100(int id) 22 | { 23 | return "test"; 24 | } 25 | 26 | [CacheOutput(ServerTimeSpan = 50)] 27 | public string Get_s50_exclude_fakecallback(int id = 0, string callback = null, string de = null) 28 | { 29 | return "test"; 30 | } 31 | 32 | [HttpGet] 33 | [CacheOutput(AnonymousOnly = true, ClientTimeSpan = 50, ServerTimeSpan = 50)] 34 | public string etag_match_304() 35 | { 36 | return "value"; 37 | } 38 | 39 | public void Post() 40 | { 41 | var cache = Configuration.CacheOutputConfiguration().GetCacheOutputProvider(Request); 42 | cache.RemoveStartsWith(Configuration.CacheOutputConfiguration().MakeBaseCachekey(this.GetType().FullName, "Get_c100_s100")); 43 | 44 | //do nothing 45 | } 46 | 47 | public void Put() 48 | { 49 | var cache = Configuration.CacheOutputConfiguration().GetCacheOutputProvider(Request); 50 | cache.RemoveStartsWith(Configuration.CacheOutputConfiguration().MakeBaseCachekey((InlineInvalidateController x) => x.Get_c100_s100())); 51 | 52 | //do nothing 53 | } 54 | 55 | public void Delete_non_standard_name() 56 | { 57 | var cache = Configuration.CacheOutputConfiguration().GetCacheOutputProvider(Request); 58 | cache.RemoveStartsWith(Configuration.CacheOutputConfiguration().MakeBaseCachekey((InlineInvalidateController x) => x.Get_c100_s100(7))); 59 | } 60 | 61 | public void Delete_parameterized() 62 | { 63 | var cache = Configuration.CacheOutputConfiguration().GetCacheOutputProvider(Request); 64 | cache.RemoveStartsWith(Configuration.CacheOutputConfiguration().MakeBaseCachekey((InlineInvalidateController x) => x.Get_c100_s100_with_param(7))); 65 | 66 | //do nothing 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/TestControllers/SampleController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Net.Http.Headers; 5 | using System.Web.Http; 6 | using WebApi.OutputCache.V2.TimeAttributes; 7 | 8 | namespace WebApi.OutputCache.V2.Tests.TestControllers 9 | { 10 | public class SampleController : ApiController 11 | { 12 | [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 100)] 13 | public string Get_c100_s100() 14 | { 15 | return "test"; 16 | } 17 | 18 | [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 0)] 19 | public string Get_c100_s0() 20 | { 21 | return "test"; 22 | } 23 | 24 | [CacheOutput(ClientTimeSpan = 0, ServerTimeSpan = 100)] 25 | public string Get_c0_s100() 26 | { 27 | return "test"; 28 | } 29 | 30 | [CacheOutput(NoCache=true)] 31 | public string Get_nocache() 32 | { 33 | return "test"; 34 | } 35 | 36 | [CacheOutput(ClientTimeSpan = 0, ServerTimeSpan = 100, MustRevalidate = true)] 37 | public string Get_c0_s100_mustR() 38 | { 39 | return "test"; 40 | } 41 | 42 | [CacheOutput(ClientTimeSpan = 50, MustRevalidate = true)] 43 | public string Get_c50_mustR() 44 | { 45 | return "test"; 46 | } 47 | 48 | [CacheOutput(ClientTimeSpan = 50, Private = true)] 49 | public string Get_c50_private() 50 | { 51 | return "test"; 52 | } 53 | 54 | [CacheOutput(Private = true)] 55 | public string Get_private() 56 | { 57 | return "test"; 58 | } 59 | 60 | [CacheOutput(ServerTimeSpan = 50)] 61 | public string Get_s50_exclude_fakecallback(int? id = null, string callback = null, string de = null) 62 | { 63 | return "test"; 64 | } 65 | 66 | [CacheOutput(ServerTimeSpan = 50, ExcludeQueryStringFromCacheKey = false)] 67 | public string Get_s50_exclude_false(int id) 68 | { 69 | return "test"+id; 70 | } 71 | 72 | [CacheOutput(ServerTimeSpan = 50, ExcludeQueryStringFromCacheKey = true)] 73 | public string Get_s50_exclude_true(int id) 74 | { 75 | return "test" + id; 76 | } 77 | 78 | [CacheOutputUntil(2100, 01,25,17,00)] 79 | public string Get_until25012100_1700() 80 | { 81 | return "test"; 82 | } 83 | 84 | [CacheOutputUntilToday(23,55)] 85 | public string Get_until2355_today() 86 | { 87 | return "value"; 88 | } 89 | 90 | [CacheOutputUntilThisMonth(27)] 91 | public string Get_until27_thismonth() 92 | { 93 | return "value"; 94 | } 95 | 96 | [CacheOutputUntilThisYear(7,31)] 97 | public string Get_until731_thisyear() 98 | { 99 | return "value"; 100 | } 101 | 102 | [CacheOutputUntilThisYear(7, 31, MustRevalidate = true)] 103 | public string Get_until731_thisyear_mustrevalidate() 104 | { 105 | return "value"; 106 | } 107 | 108 | [CacheOutput(AnonymousOnly = true, ClientTimeSpan = 50, ServerTimeSpan = 50)] 109 | public string Get_s50_c50_anonymousonly() 110 | { 111 | return "value"; 112 | } 113 | 114 | [HttpGet] 115 | [CacheOutput(AnonymousOnly = true, ClientTimeSpan = 50, ServerTimeSpan = 50)] 116 | public string etag_match_304() 117 | { 118 | return "value"; 119 | } 120 | 121 | [CacheOutput(ClientTimeSpan = 50, ServerTimeSpan = 50)] 122 | public string Get_request_exception_noCache() 123 | { 124 | throw new System.Exception("Fault shouldn't cache"); 125 | } 126 | 127 | [CacheOutput(ClientTimeSpan = 50, ServerTimeSpan = 50)] 128 | public string Get_request_httpResponseException_noCache() 129 | { 130 | throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Conflict){ReasonPhrase = "Fault shouldn't cache"}); 131 | } 132 | 133 | [CacheOutput(ClientTimeSpan = 50, ServerTimeSpan = 50)] 134 | public HttpResponseMessage Get_request_noContent() 135 | { 136 | return Request.CreateResponse(HttpStatusCode.Accepted); 137 | } 138 | 139 | [CacheOutput(ClientTimeSpan = 50, ServerTimeSpan = 50, MediaType = "image/jpeg")] 140 | public HttpResponseMessage Get_c50_s50_image() 141 | { 142 | var response = new HttpResponseMessage(HttpStatusCode.OK) {Content = new ByteArrayContent(new byte[0])}; 143 | response.Content.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg"); 144 | return response; 145 | } 146 | 147 | [InvalidateCacheOutput("Get_c100_s100")] 148 | public void Post() 149 | { 150 | //do nothing 151 | } 152 | 153 | [InvalidateCacheOutput("Get_c100_s100")] 154 | [InvalidateCacheOutput("Get_s50_exclude_fakecallback")] 155 | public void Post_2_invalidates() 156 | { 157 | //do nothing 158 | } 159 | 160 | [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 100)] 161 | public IHttpActionResult Get_ihttpactionresult() 162 | { 163 | return Ok("value"); 164 | } 165 | 166 | [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 100, SharedTimeSpan = 200)] 167 | public string Get_c100_s100_sm200() 168 | { 169 | return "test"; 170 | } 171 | 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/WebApi.OutputCache.V2.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {1267460B-C100-48AD-B2E2-9DDE2E40F052} 8 | Library 9 | Properties 10 | WebApi.OutputCache.V2.Tests 11 | WebApi.OutputCache.V2.Tests 12 | v4.5 13 | 512 14 | ..\ 15 | true 16 | 17 | 18 | 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | false 27 | 28 | 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | false 36 | 37 | 38 | 39 | False 40 | ..\..\packages\Autofac.3.1.3\lib\net40\Autofac.dll 41 | 42 | 43 | False 44 | ..\..\packages\Autofac.WebApi2.3.0.0\lib\net45\Autofac.Integration.WebApi.dll 45 | 46 | 47 | ..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll 48 | 49 | 50 | ..\..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll 51 | True 52 | 53 | 54 | False 55 | ..\..\packages\NUnit.2.6.2\lib\nunit.framework.dll 56 | 57 | 58 | 59 | 60 | 61 | ..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll 62 | True 63 | 64 | 65 | ..\..\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll 66 | True 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | Code 80 | 81 | 82 | Code 83 | 84 | 85 | Code 86 | 87 | 88 | 89 | 90 | 91 | 92 | Code 93 | 94 | 95 | Code 96 | 97 | 98 | 99 | Code 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | Code 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | {3e45fa0b-c465-4de9-9bc3-40a606b73e84} 120 | WebApi.OutputCache.Core 121 | 122 | 123 | {7a6f57f6-38e1-4287-812e-ad7d1025ba5e} 124 | WebApi2.OutputCache 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 139 | -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/WebApi.OutputCache.V2.Tests/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | --------------------------------------------------------------------------------