@@ -32,14 +32,14 @@ public class ImageSharpMiddleware
3232 /// <summary>
3333 /// The write worker used for limiting identical requests.
3434 /// </summary>
35- private static readonly ConcurrentDictionary < string , Lazy < Task > > WriteWorkers
36- = new ConcurrentDictionary < string , Lazy < Task > > ( StringComparer . OrdinalIgnoreCase ) ;
35+ private static readonly ConcurrentDictionary < string , Task < ImageWorkerResult > > WriteWorkers
36+ = new ConcurrentDictionary < string , Task < ImageWorkerResult > > ( StringComparer . OrdinalIgnoreCase ) ;
3737
3838 /// <summary>
3939 /// The read worker used for limiting identical requests.
4040 /// </summary>
41- private static readonly ConcurrentDictionary < string , Lazy < Task < ValueTuple < bool , ImageMetadata > > > > ReadWorkers
42- = new ConcurrentDictionary < string , Lazy < Task < ValueTuple < bool , ImageMetadata > > > > ( StringComparer . OrdinalIgnoreCase ) ;
41+ private static readonly ConcurrentDictionary < string , Task < ImageWorkerResult > > ReadWorkers
42+ = new ConcurrentDictionary < string , Task < ImageWorkerResult > > ( StringComparer . OrdinalIgnoreCase ) ;
4343
4444 /// <summary>
4545 /// Used to temporarily store source metadata reads to reduce the overhead of cache lookups.
@@ -251,30 +251,40 @@ private async Task ProcessRequestAsync(
251251
252252 // Check the cache, if present, not out of date and not requiring and update
253253 // we'll simply serve the file from there.
254- ( bool newOrUpdated , ImageMetadata sourceImageMetadata ) =
255- await this . IsNewOrUpdatedAsync ( sourceImageResolver , imageContext , key ) ;
254+ ImageWorkerResult readResult = default ;
255+ try
256+ {
257+ readResult = await this . IsNewOrUpdatedAsync ( sourceImageResolver , imageContext , key ) ;
258+ }
259+ finally
260+ {
261+ ReadWorkers . TryRemove ( key , out Task < ImageWorkerResult > _ ) ;
262+ }
256263
257- if ( ! newOrUpdated )
264+ if ( ! readResult . IsNewOrUpdated )
258265 {
266+ await this . SendResponseAsync ( imageContext , key , readResult . CacheImageMetadata , readResult . Resolver ) ;
259267 return ;
260268 }
261269
262- // Not cached? Let's get it from the image resolver.
263- RecyclableMemoryStream outStream = null ;
270+ // Not cached, or is updated ? Let's get it from the image resolver.
271+ var sourceImageMetadata = readResult . SourceImageMetadata ;
264272
265- // Enter a write lock which locks writing and any reads for the same request.
266- // This reduces the overheads of unnecessary processing plus avoids file locks.
267- await WriteWorkers . GetOrAdd (
268- key ,
269- _ => new Lazy < Task > (
270- async ( ) =>
273+ // Enter an asynchronous write worker which prevents multiple writes and delays any reads for the same request.
274+ // This reduces the overheads of unnecessary processing.
275+ try
276+ {
277+ ImageWorkerResult writeResult = await WriteWorkers . GetOrAddAsync (
278+ key ,
279+ async ( key ) =>
271280 {
281+ RecyclableMemoryStream outStream = null ;
272282 try
273283 {
274284 // Prevent a second request from starting a read during write execution.
275- if ( ReadWorkers . TryGetValue ( key , out Lazy < Task < ( bool , ImageMetadata ) > > readWork ) )
285+ if ( ReadWorkers . TryGetValue ( key , out Task < ImageWorkerResult > readWork ) )
276286 {
277- await readWork . Value ;
287+ await readWork ;
278288 }
279289
280290 ImageCacheMetadata cachedImageMetadata = default ;
@@ -334,11 +344,27 @@ await WriteWorkers.GetOrAdd(
334344 // Save the image to the cache and send the response to the caller.
335345 await this . cache . SetAsync ( key , outStream , cachedImageMetadata ) ;
336346
337- // Remove the resolver from the cache so we always resolve next request
347+ // Remove any resolver from the cache so we always resolve next request
338348 // for the same key.
339349 CacheResolverLru . TryRemove ( key ) ;
340350
341- await this . SendResponseAsync ( imageContext , key , cachedImageMetadata , outStream , null ) ;
351+ // Place the resolver in the lru cache.
352+ ( IImageCacheResolver ImageCacheResolver , ImageCacheMetadata ImageCacheMetadata ) cachedImage = await
353+ CacheResolverLru . GetOrAddAsync (
354+ key ,
355+ async k =>
356+ {
357+ IImageCacheResolver resolver = await this . cache . GetAsync ( k ) ;
358+ ImageCacheMetadata metadata = default ;
359+ if ( resolver != null )
360+ {
361+ metadata = await resolver . GetMetaDataAsync ( ) ;
362+ }
363+
364+ return ( resolver , metadata ) ;
365+ } ) ;
366+
367+ return new ImageWorkerResult ( cachedImage . ImageCacheMetadata , cachedImage . ImageCacheResolver ) ;
342368 }
343369 catch ( Exception ex )
344370 {
@@ -350,9 +376,17 @@ await WriteWorkers.GetOrAdd(
350376 finally
351377 {
352378 await this . StreamDisposeAsync ( outStream ) ;
353- WriteWorkers . TryRemove ( key , out Lazy < Task > _ ) ;
354379 }
355- } , LazyThreadSafetyMode . ExecutionAndPublication ) ) . Value ;
380+ } ) ;
381+
382+ await this . SendResponseAsync ( imageContext , key , writeResult . CacheImageMetadata , writeResult . Resolver ) ;
383+ }
384+ finally
385+ {
386+ // As soon as we have sent a response from a writer the result is available from a reader so we remove this task.
387+ // Any existing awaiters will continue to await.
388+ WriteWorkers . TryRemove ( key , out Task < ImageWorkerResult > _ ) ;
389+ }
356390 }
357391
358392 private ValueTask StreamDisposeAsync ( Stream stream )
@@ -377,85 +411,72 @@ private ValueTask StreamDisposeAsync(Stream stream)
377411#endif
378412 }
379413
380- private async Task < ValueTuple < bool , ImageMetadata > > IsNewOrUpdatedAsync (
414+ private async Task < ImageWorkerResult > IsNewOrUpdatedAsync (
381415 IImageResolver sourceImageResolver ,
382416 ImageContext imageContext ,
383417 string key )
384418 {
385- if ( WriteWorkers . TryGetValue ( key , out Lazy < Task > writeWork ) )
419+ // Pause until the write has been completed.
420+ if ( WriteWorkers . TryGetValue ( key , out Task < ImageWorkerResult > writeWorkResult ) )
386421 {
387- await writeWork . Value ;
422+ return await writeWorkResult ;
388423 }
389424
390- if ( ReadWorkers . TryGetValue ( key , out Lazy < Task < ( bool , ImageMetadata ) > > readWork ) )
391- {
392- return await readWork . Value ;
393- }
394-
395- return await ReadWorkers . GetOrAdd (
425+ return await ReadWorkers . GetOrAddAsync (
396426 key ,
397- _ => new Lazy < Task < ValueTuple < bool , ImageMetadata > > > (
398- async ( ) =>
427+ async ( key ) =>
399428 {
400- try
401- {
402- // Get the source metadata for processing, storing the result for future checks.
403- ImageMetadata sourceImageMetadata = await
404- SourceMetadataLru . GetOrAddAsync (
405- key ,
406- _ => sourceImageResolver . GetMetaDataAsync ( ) ) ;
407-
408- // Check to see if the cache contains this image.
409- // If not, we return early. No further checks necessary.
410- ( IImageCacheResolver ImageCacheResolver , ImageCacheMetadata ImageCacheMetadata ) cachedImage = await
411- CacheResolverLru . GetOrAddAsync (
412- key ,
413- async k =>
429+ // Get the source metadata for processing, storing the result for future checks.
430+ ImageMetadata sourceImageMetadata = await
431+ SourceMetadataLru . GetOrAddAsync (
432+ key ,
433+ _ => sourceImageResolver . GetMetaDataAsync ( ) ) ;
434+
435+ // Check to see if the cache contains this image.
436+ // If not, we return early. No further checks necessary.
437+ ( IImageCacheResolver ImageCacheResolver , ImageCacheMetadata ImageCacheMetadata ) cachedImage = await
438+ CacheResolverLru . GetOrAddAsync (
439+ key ,
440+ async k =>
441+ {
442+ IImageCacheResolver resolver = await this . cache . GetAsync ( k ) ;
443+ ImageCacheMetadata metadata = default ;
444+ if ( resolver != null )
414445 {
415- IImageCacheResolver resolver = await this . cache . GetAsync ( k ) ;
416- ImageCacheMetadata metadata = default ;
417- if ( resolver != null )
418- {
419- metadata = await resolver . GetMetaDataAsync ( ) ;
420- }
421-
422- return ( resolver , metadata ) ;
423- } ) ;
446+ metadata = await resolver . GetMetaDataAsync ( ) ;
447+ }
424448
425- if ( cachedImage . ImageCacheResolver is null )
426- {
427- // Remove the null resolver from the store.
428- CacheResolverLru . TryRemove ( key ) ;
429- return ( true , sourceImageMetadata ) ;
430- }
449+ return ( resolver , metadata ) ;
450+ } ) ;
431451
432- // Has the cached image expired?
433- // Or has the source image changed since the image was last cached?
434- if ( cachedImage . ImageCacheMetadata . ContentLength == 0 // Fix for old cache without length property
435- || cachedImage . ImageCacheMetadata . CacheLastWriteTimeUtc <= ( DateTimeOffset . UtcNow - this . options . CacheMaxAge )
436- || cachedImage . ImageCacheMetadata . SourceLastWriteTimeUtc != sourceImageMetadata . LastWriteTimeUtc )
437- {
438- // We want to remove the resolver from the store so that the next check gets the updated file.
439- CacheResolverLru . TryRemove ( key ) ;
440- return ( true , sourceImageMetadata ) ;
441- }
452+ if ( cachedImage . ImageCacheResolver is null )
453+ {
454+ // Remove the null resolver from the store.
455+ CacheResolverLru . TryRemove ( key ) ;
442456
443- // We're pulling the image from the cache.
444- await this . SendResponseAsync ( imageContext , key , cachedImage . ImageCacheMetadata , null , cachedImage . ImageCacheResolver ) ;
445- return ( false , sourceImageMetadata ) ;
457+ return new ImageWorkerResult ( sourceImageMetadata ) ;
446458 }
447- finally
459+
460+ // Has the cached image expired?
461+ // Or has the source image changed since the image was last cached?
462+ if ( cachedImage . ImageCacheMetadata . ContentLength == 0 // Fix for old cache without length property
463+ || cachedImage . ImageCacheMetadata . CacheLastWriteTimeUtc <= ( DateTimeOffset . UtcNow - this . options . CacheMaxAge )
464+ || cachedImage . ImageCacheMetadata . SourceLastWriteTimeUtc != sourceImageMetadata . LastWriteTimeUtc )
448465 {
449- ReadWorkers . TryRemove ( key , out Lazy < Task < ( bool , ImageMetadata ) > > _ ) ;
466+ // We want to remove the resolver from the store so that the next check gets the updated file.
467+ CacheResolverLru . TryRemove ( key ) ;
468+ return new ImageWorkerResult ( sourceImageMetadata ) ;
450469 }
451- } , LazyThreadSafetyMode . ExecutionAndPublication ) ) . Value ;
470+
471+ // The image is cached. Return the cached image so multiple callers can write a response.
472+ return new ImageWorkerResult ( sourceImageMetadata , cachedImage . ImageCacheMetadata , cachedImage . ImageCacheResolver ) ;
473+ } ) ;
452474 }
453475
454476 private async Task SendResponseAsync (
455477 ImageContext imageContext ,
456478 string key ,
457479 ImageCacheMetadata metadata ,
458- Stream stream ,
459480 IImageCacheResolver cacheResolver )
460481 {
461482 imageContext . ComprehendRequestHeaders ( metadata . CacheLastWriteTimeUtc , metadata . ContentLength ) ;
@@ -473,7 +494,11 @@ private async Task SendResponseAsync(
473494 this . logger . LogImageServed ( imageContext . GetDisplayUrl ( ) , key ) ;
474495
475496 // When stream is null we're sending from the cache.
476- await imageContext . SendAsync ( stream ?? await cacheResolver . OpenReadAsync ( ) , metadata ) ;
497+ using ( var stream = await cacheResolver . OpenReadAsync ( ) )
498+ {
499+ await imageContext . SendAsync ( stream , metadata ) ;
500+ }
501+
477502 return ;
478503
479504 case ImageContext . PreconditionState . NotModified :
0 commit comments