mirror of
				https://github.com/nsnail/Ocelot.git
				synced 2025-11-04 16:10:50 +08:00 
			
		
		
		
	Fix/issue666 (#889)
* cache key now can generate from query string for request with Get Methods and request content for requests with post methods * MD5Helper Added. OutputCacheMiddleware now can generate cache key using method, url and content * unit test created for CacheKeyGenerator * CacheKeyGenerator Registered in OcelotBuilder as singletone
This commit is contained in:
		@@ -33,6 +33,10 @@
 | 
				
			|||||||
            builder.Services.RemoveAll(typeof(IOcelotCache<FileConfiguration>));
 | 
					            builder.Services.RemoveAll(typeof(IOcelotCache<FileConfiguration>));
 | 
				
			||||||
            builder.Services.AddSingleton<ICacheManager<FileConfiguration>>(fileConfigCacheManagerOutputCache);
 | 
					            builder.Services.AddSingleton<ICacheManager<FileConfiguration>>(fileConfigCacheManagerOutputCache);
 | 
				
			||||||
            builder.Services.AddSingleton<IOcelotCache<FileConfiguration>>(fileConfigCacheManager);
 | 
					            builder.Services.AddSingleton<IOcelotCache<FileConfiguration>>(fileConfigCacheManager);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            builder.Services.RemoveAll(typeof(ICacheKeyGenerator));
 | 
				
			||||||
 | 
					            builder.Services.AddSingleton<ICacheKeyGenerator, CacheKeyGenerator>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return builder;
 | 
					            return builder;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										19
									
								
								src/Ocelot/Cache/CacheKeyGenerator.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/Ocelot/Cache/CacheKeyGenerator.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					using System.Text;
 | 
				
			||||||
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
 | 
					using Ocelot.Middleware;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Ocelot.Cache {
 | 
				
			||||||
 | 
					    public class CacheKeyGenerator : ICacheKeyGenerator {
 | 
				
			||||||
 | 
					        public string GenerateRequestCacheKey(DownstreamContext context) {
 | 
				
			||||||
 | 
					            string hashedContent = null;
 | 
				
			||||||
 | 
					            StringBuilder downStreamUrlKeyBuilder = new StringBuilder($"{context.DownstreamRequest.Method}-{context.DownstreamRequest.OriginalString}");
 | 
				
			||||||
 | 
					            if (context.DownstreamRequest.Content != null) {
 | 
				
			||||||
 | 
					                string requestContentString = Task.Run(async () => await context.DownstreamRequest.Content.ReadAsStringAsync()).Result;
 | 
				
			||||||
 | 
					                downStreamUrlKeyBuilder.Append(requestContentString);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            hashedContent = MD5Helper.GenerateMd5(downStreamUrlKeyBuilder.ToString());
 | 
				
			||||||
 | 
					            return hashedContent;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										7
									
								
								src/Ocelot/Cache/ICacheKeyGenerator.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/Ocelot/Cache/ICacheKeyGenerator.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					using Ocelot.Middleware;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Ocelot.Cache {
 | 
				
			||||||
 | 
					    public interface ICacheKeyGenerator {
 | 
				
			||||||
 | 
					        string GenerateRequestCacheKey(DownstreamContext context);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										22
									
								
								src/Ocelot/Cache/MD5Helper.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/Ocelot/Cache/MD5Helper.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					using System.Security.Cryptography;
 | 
				
			||||||
 | 
					using System.Text;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Ocelot.Cache {
 | 
				
			||||||
 | 
					    public static class MD5Helper {
 | 
				
			||||||
 | 
					        public static string GenerateMd5(byte[] contentBytes) {
 | 
				
			||||||
 | 
					            MD5 md5 = MD5.Create();
 | 
				
			||||||
 | 
					            byte[] hash = md5.ComputeHash(contentBytes);
 | 
				
			||||||
 | 
					            StringBuilder sb = new StringBuilder();
 | 
				
			||||||
 | 
					            for (int i = 0; i < hash.Length; i++) {
 | 
				
			||||||
 | 
					                sb.Append(hash[i].ToString("X2"));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return sb.ToString();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public static string GenerateMd5(string contentString) {
 | 
				
			||||||
 | 
					            byte[] contentBytes = Encoding.Unicode.GetBytes(contentString);
 | 
				
			||||||
 | 
					            return GenerateMd5(contentBytes);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -7,19 +7,23 @@
 | 
				
			|||||||
    using Ocelot.Logging;
 | 
					    using Ocelot.Logging;
 | 
				
			||||||
    using Ocelot.Middleware;
 | 
					    using Ocelot.Middleware;
 | 
				
			||||||
    using System.IO;
 | 
					    using System.IO;
 | 
				
			||||||
 | 
					    using System.Text;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public class OutputCacheMiddleware : OcelotMiddleware
 | 
					    public class OutputCacheMiddleware : OcelotMiddleware
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private readonly OcelotRequestDelegate _next;
 | 
					        private readonly OcelotRequestDelegate _next;
 | 
				
			||||||
        private readonly IOcelotCache<CachedResponse> _outputCache;
 | 
					        private readonly IOcelotCache<CachedResponse> _outputCache;
 | 
				
			||||||
 | 
					        private readonly ICacheKeyGenerator _cacheGeneratot;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public OutputCacheMiddleware(OcelotRequestDelegate next,
 | 
					        public OutputCacheMiddleware(OcelotRequestDelegate next,
 | 
				
			||||||
            IOcelotLoggerFactory loggerFactory,
 | 
					            IOcelotLoggerFactory loggerFactory,
 | 
				
			||||||
            IOcelotCache<CachedResponse> outputCache)
 | 
					            IOcelotCache<CachedResponse> outputCache,
 | 
				
			||||||
 | 
					            ICacheKeyGenerator cacheGeneratot)
 | 
				
			||||||
                :base(loggerFactory.CreateLogger<OutputCacheMiddleware>())
 | 
					                :base(loggerFactory.CreateLogger<OutputCacheMiddleware>())
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _next = next;
 | 
					            _next = next;
 | 
				
			||||||
            _outputCache = outputCache;
 | 
					            _outputCache = outputCache;
 | 
				
			||||||
 | 
					            _cacheGeneratot = cacheGeneratot;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public async Task Invoke(DownstreamContext context)
 | 
					        public async Task Invoke(DownstreamContext context)
 | 
				
			||||||
@@ -31,10 +35,11 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var downstreamUrlKey = $"{context.DownstreamRequest.Method}-{context.DownstreamRequest.OriginalString}";
 | 
					            var downstreamUrlKey = $"{context.DownstreamRequest.Method}-{context.DownstreamRequest.OriginalString}";
 | 
				
			||||||
 | 
					            string downStreamRequestCacheKey = _cacheGeneratot.GenerateRequestCacheKey(context);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Logger.LogDebug($"Started checking cache for {downstreamUrlKey}");
 | 
					            Logger.LogDebug($"Started checking cache for {downstreamUrlKey}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var cached = _outputCache.Get(downstreamUrlKey, context.DownstreamReRoute.CacheOptions.Region);
 | 
					            var cached = _outputCache.Get(downStreamRequestCacheKey, context.DownstreamReRoute.CacheOptions.Region);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (cached != null)
 | 
					            if (cached != null)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -61,12 +66,13 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            cached = await CreateCachedResponse(context.DownstreamResponse);
 | 
					            cached = await CreateCachedResponse(context.DownstreamResponse);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            _outputCache.Add(downstreamUrlKey, cached, TimeSpan.FromSeconds(context.DownstreamReRoute.CacheOptions.TtlSeconds), context.DownstreamReRoute.CacheOptions.Region);
 | 
					            _outputCache.Add(downStreamRequestCacheKey, cached, TimeSpan.FromSeconds(context.DownstreamReRoute.CacheOptions.TtlSeconds), context.DownstreamReRoute.CacheOptions.Region);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Logger.LogDebug($"finished response added to cache for {downstreamUrlKey}");
 | 
					            Logger.LogDebug($"finished response added to cache for {downstreamUrlKey}");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void SetHttpResponseMessageThisRequest(DownstreamContext context, DownstreamResponse response)
 | 
					        private void SetHttpResponseMessageThisRequest(DownstreamContext context, 
 | 
				
			||||||
 | 
					                                                       DownstreamResponse response)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            context.DownstreamResponse = response;
 | 
					            context.DownstreamResponse = response;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -107,6 +107,7 @@ namespace Ocelot.DependencyInjection
 | 
				
			|||||||
            Services.TryAddSingleton<IHttpHandlerOptionsCreator, HttpHandlerOptionsCreator>();
 | 
					            Services.TryAddSingleton<IHttpHandlerOptionsCreator, HttpHandlerOptionsCreator>();
 | 
				
			||||||
            Services.TryAddSingleton<IDownstreamAddressesCreator, DownstreamAddressesCreator>();
 | 
					            Services.TryAddSingleton<IDownstreamAddressesCreator, DownstreamAddressesCreator>();
 | 
				
			||||||
            Services.TryAddSingleton<IDelegatingHandlerHandlerFactory, DelegatingHandlerHandlerFactory>();
 | 
					            Services.TryAddSingleton<IDelegatingHandlerHandlerFactory, DelegatingHandlerHandlerFactory>();
 | 
				
			||||||
 | 
					            Services.TryAddSingleton<ICacheKeyGenerator, CacheKeyGenerator>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc
 | 
					            // see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc
 | 
				
			||||||
            // could maybe use a scoped data repository
 | 
					            // could maybe use a scoped data repository
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,6 +19,7 @@ namespace Ocelot.Request.Middleware
 | 
				
			|||||||
            Headers = _request.Headers;
 | 
					            Headers = _request.Headers;
 | 
				
			||||||
            AbsolutePath = _request.RequestUri.AbsolutePath;
 | 
					            AbsolutePath = _request.RequestUri.AbsolutePath;
 | 
				
			||||||
            Query = _request.RequestUri.Query;
 | 
					            Query = _request.RequestUri.Query;
 | 
				
			||||||
 | 
					            Content = _request.Content;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public HttpRequestHeaders Headers { get; }
 | 
					        public HttpRequestHeaders Headers { get; }
 | 
				
			||||||
@@ -37,6 +38,8 @@ namespace Ocelot.Request.Middleware
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        public string Query { get; set; }
 | 
					        public string Query { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public HttpContent Content { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public HttpRequestMessage ToHttpRequestMessage()
 | 
					        public HttpRequestMessage ToHttpRequestMessage()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var uriBuilder = new UriBuilder
 | 
					            var uriBuilder = new UriBuilder
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										34
									
								
								test/Ocelot.UnitTests/Cache/CacheKeyGeneratorTests.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								test/Ocelot.UnitTests/Cache/CacheKeyGeneratorTests.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					using System.Net.Http;
 | 
				
			||||||
 | 
					using Microsoft.AspNetCore.Http;
 | 
				
			||||||
 | 
					using Ocelot.Cache;
 | 
				
			||||||
 | 
					using Ocelot.Middleware;
 | 
				
			||||||
 | 
					using Shouldly;
 | 
				
			||||||
 | 
					using TestStack.BDDfy;
 | 
				
			||||||
 | 
					using Xunit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Ocelot.UnitTests.Cache {
 | 
				
			||||||
 | 
					    public class CacheKeyGeneratorTests {
 | 
				
			||||||
 | 
					        private readonly ICacheKeyGenerator _cacheKeyGenerator;
 | 
				
			||||||
 | 
					        private readonly DownstreamContext _downstreamContext;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public CacheKeyGeneratorTests() {
 | 
				
			||||||
 | 
					            _cacheKeyGenerator = new CacheKeyGenerator();
 | 
				
			||||||
 | 
					            _cacheKeyGenerator = new CacheKeyGenerator();
 | 
				
			||||||
 | 
					            _downstreamContext = new DownstreamContext(new DefaultHttpContext()) {
 | 
				
			||||||
 | 
					                DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123"))
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [Fact]
 | 
				
			||||||
 | 
					        public void should_generate_cache_key_from_context() {
 | 
				
			||||||
 | 
					            this.Given(x => x.GivenCacheKeyFromContext(_downstreamContext))
 | 
				
			||||||
 | 
					                .BDDfy();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private void GivenCacheKeyFromContext(DownstreamContext context) {
 | 
				
			||||||
 | 
					            string generatedCacheKey = _cacheKeyGenerator.GenerateRequestCacheKey(context);
 | 
				
			||||||
 | 
					            string cachekey = MD5Helper.GenerateMd5("GET-https://some.url/blah?abcd=123");
 | 
				
			||||||
 | 
					            generatedCacheKey.ShouldBe(cachekey);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -27,14 +27,17 @@
 | 
				
			|||||||
        private OutputCacheMiddleware _middleware;
 | 
					        private OutputCacheMiddleware _middleware;
 | 
				
			||||||
        private readonly DownstreamContext _downstreamContext;
 | 
					        private readonly DownstreamContext _downstreamContext;
 | 
				
			||||||
        private readonly OcelotRequestDelegate _next;
 | 
					        private readonly OcelotRequestDelegate _next;
 | 
				
			||||||
 | 
					        private readonly ICacheKeyGenerator _cacheKeyGenerator;
 | 
				
			||||||
        private CachedResponse _response;
 | 
					        private CachedResponse _response;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public OutputCacheMiddlewareTests()
 | 
					        public OutputCacheMiddlewareTests()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _cache = new Mock<IOcelotCache<CachedResponse>>();
 | 
					            _cache = new Mock<IOcelotCache<CachedResponse>>();
 | 
				
			||||||
            _downstreamContext = new DownstreamContext(new DefaultHttpContext());
 | 
					            _downstreamContext = new DownstreamContext(new DefaultHttpContext());
 | 
				
			||||||
            _loggerFactory = new Mock<IOcelotLoggerFactory>();
 | 
					            _loggerFactory = new Mock<IOcelotLoggerFactory>();
 | 
				
			||||||
            _logger = new Mock<IOcelotLogger>();
 | 
					            _logger = new Mock<IOcelotLogger>();
 | 
				
			||||||
 | 
					            _cacheKeyGenerator = new CacheKeyGenerator();
 | 
				
			||||||
            _loggerFactory.Setup(x => x.CreateLogger<OutputCacheMiddleware>()).Returns(_logger.Object);
 | 
					            _loggerFactory.Setup(x => x.CreateLogger<OutputCacheMiddleware>()).Returns(_logger.Object);
 | 
				
			||||||
            _next = context => Task.CompletedTask;
 | 
					            _next = context => Task.CompletedTask;
 | 
				
			||||||
            _downstreamContext.DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123"));
 | 
					            _downstreamContext.DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123"));
 | 
				
			||||||
@@ -89,7 +92,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        private void WhenICallTheMiddleware()
 | 
					        private void WhenICallTheMiddleware()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cache.Object);
 | 
					            _middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cache.Object, _cacheKeyGenerator);
 | 
				
			||||||
            _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult();
 | 
					            _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,6 +23,7 @@
 | 
				
			|||||||
    public class OutputCacheMiddlewareRealCacheTests
 | 
					    public class OutputCacheMiddlewareRealCacheTests
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private readonly IOcelotCache<CachedResponse> _cacheManager;
 | 
					        private readonly IOcelotCache<CachedResponse> _cacheManager;
 | 
				
			||||||
 | 
					        private readonly ICacheKeyGenerator _cacheKeyGenerator;
 | 
				
			||||||
        private readonly OutputCacheMiddleware _middleware;
 | 
					        private readonly OutputCacheMiddleware _middleware;
 | 
				
			||||||
        private readonly DownstreamContext _downstreamContext;
 | 
					        private readonly DownstreamContext _downstreamContext;
 | 
				
			||||||
        private OcelotRequestDelegate _next;
 | 
					        private OcelotRequestDelegate _next;
 | 
				
			||||||
@@ -39,10 +40,11 @@
 | 
				
			|||||||
                x.WithDictionaryHandle();
 | 
					                x.WithDictionaryHandle();
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
            _cacheManager = new OcelotCacheManagerCache<CachedResponse>(cacheManagerOutputCache);
 | 
					            _cacheManager = new OcelotCacheManagerCache<CachedResponse>(cacheManagerOutputCache);
 | 
				
			||||||
 | 
					            _cacheKeyGenerator = new CacheKeyGenerator();
 | 
				
			||||||
            _downstreamContext = new DownstreamContext(new DefaultHttpContext());
 | 
					            _downstreamContext = new DownstreamContext(new DefaultHttpContext());
 | 
				
			||||||
            _downstreamContext.DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123"));
 | 
					            _downstreamContext.DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123"));
 | 
				
			||||||
            _next = context => Task.CompletedTask;
 | 
					            _next = context => Task.CompletedTask;
 | 
				
			||||||
            _middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cacheManager);
 | 
					            _middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cacheManager, _cacheKeyGenerator);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Fact]
 | 
					        [Fact]
 | 
				
			||||||
@@ -69,7 +71,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        private void ThenTheContentTypeHeaderIsCached()
 | 
					        private void ThenTheContentTypeHeaderIsCached()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var result = _cacheManager.Get("GET-https://some.url/blah?abcd=123", "kanken");
 | 
					            string cacheKey = MD5Helper.GenerateMd5("GET-https://some.url/blah?abcd=123");
 | 
				
			||||||
 | 
					            var result = _cacheManager.Get(cacheKey, "kanken");
 | 
				
			||||||
            var header = result.ContentHeaders["Content-Type"];
 | 
					            var header = result.ContentHeaders["Content-Type"];
 | 
				
			||||||
            header.First().ShouldBe("application/json");
 | 
					            header.First().ShouldBe("application/json");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user