├── .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