Multipart body length limit exceeded exception - asp.net-core

Although having set the MaxRequestLength and maxAllowedContentLength to the maximum possible values in the web.config section, ASP.Net Core does not allow me to upload files larger than 134,217,728 Bytes. The exact error coming from the web server is:
An unhandled exception occurred while processing the request.
InvalidDataException: Multipart body length limit 134217728 exceeded.
Is there any way to work this around? (ASP.Net Core)

I found the solution for this problem after reading some posts in GitHub. Conclusion is that they have to be set in the Startup class. For example:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.Configure<FormOptions>(x => {
x.ValueLengthLimit = int.MaxValue;
x.MultipartBodyLengthLimit = int.MaxValue; // In case of multipart
})
}
This will solve the problem. However they also indicated that there is a [RequestFormSizeLimit] attribute, but I have been unable to reference it yet.

Alternatively use the attribute, so the equivalent for an action as resolved by Transcendant would be:
[RequestFormLimits(ValueLengthLimit = int.MaxValue, MultipartBodyLengthLimit = int.MaxValue)]

If you use int.MaxValue (2,147,483,647) for the value of MultipartBodyLengthLimit as suggested in other answers, you'll be allowing file uploads of approx. 2Gb, which could quickly fill up disk space on a server. I recommend instead setting a constant to limit file uploads to a more sensible value e.g. in Startup.cs
using MyNamespace.Constants;
public void ConfigureServices(IServiceCollection services)
{
... other stuff
services.Configure<FormOptions>(options => {
options.MultipartBodyLengthLimit = Files.MaxFileUploadSizeKiloBytes;
})
}
And in a separate constants class:
namespace MyNamespace.Constants
{
public static class Files
{
public const int MaxFileUploadSizeKiloBytes = 250000000; // max length for body of any file uploaded
}
}

in case some one still face this problem i've created a middle-ware which intercept the request and create another body
public class FileStreamUploadMiddleware
{
private readonly RequestDelegate _next;
public FileStreamUploadMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if (context.Request.ContentType != null)
{
if (context.Request.Headers.Any(x => x.Key == "Content-Disposition"))
{
var v = ContentDispositionHeaderValue.Parse(
new StringSegment(context.Request.Headers.First(x => x.Key == "Content-Disposition").Value));
if (HasFileContentDisposition(v))
{
using (var memoryStream = new MemoryStream())
{
context.Request.Body.CopyTo(memoryStream);
var length = memoryStream.Length;
var formCollection = context.Request.Form =
new FormCollection(new Dictionary<string, StringValues>(),
new FormFileCollection()
{new FormFile(memoryStream, 0, length, v.Name.Value, v.FileName.Value)});
}
}
}
}
await _next.Invoke(context);
}
private static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// this part of code from https://github.com/aspnet/Mvc/issues/7019#issuecomment-341626892
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
|| !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
}
}
and in the controller we can fetch the files form the request
[HttpPost("/api/file")]
public IActionResult GetFile([FromServices] IHttpContextAccessor contextAccessor,
[FromServices] IHostingEnvironment environment)
{
//save the file
var files = Request.Form.Files;
foreach (var file in files)
{
var memoryStream = new MemoryStream();
file.CopyTo(memoryStream);
var fileStream = File.Create(
$"{environment.WebRootPath}/images/background/{file.FileName}", (int) file.Length,
FileOptions.None);
fileStream.Write(memoryStream.ToArray(), 0, (int) file.Length);
fileStream.Flush();
fileStream.Dispose();
memoryStream.Flush();
memoryStream.Dispose();
}
return Ok();
}
you can improve the code for your needs eg: add form parameters in the body of the request and deserialize it.
its a workaround i guess but it gets the work done.

Related

IRazorViewEngine.FindView with GetView can't find view

In my asp.net core project I'm trying to find Razor view using this method:
private IView FindView(ActionContext actionContext, string viewName)
{
var getViewResult = _viewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: true);
if (getViewResult.Success)
{
return getViewResult.View;
}
var findViewResult = _viewEngine.FindView(actionContext, viewName, isMainPage: true);
if (findViewResult.Success)
{
return findViewResult.View;
}
var searchedLocations = getViewResult.SearchedLocations.Concat(findViewResult.SearchedLocations);
var errorMessage = string.Join(
Environment.NewLine,
new[] { $"Unable to find view '{viewName}'. The following locations were searched:" }.Concat(searchedLocations));
throw new InvalidOperationException(errorMessage);
}
where
viewName = "Views/Email/ResetPassword.cshtml"
and _viewEngine is IRazorViewEngine, but it doesn't find any.
My project structure:
IView.FindView method is called from Business.
I also have another project, that have the project structure and uses the same method for retrieving views and, more importantly, it finds this view, but it uses netcoreapp2.2, and my current project uses netcoreapp3.1 (Microsoft.AspNetCore.Mvc.Razor versions are the same - 2.2.0).
Why can't this method find views on .net core 3.1?
UPDATE
Both projects copy this Views folder to Api\bin\Debug\netcoreapp{version} folder on build.
Though I was building things from scratch in Core 3.1 and not upgrading from an earlier version, I ran into the same issue. I got things working by the doing the following:
I created an implementation of IWebHostEnvironment (I called mine DummyWebHostEnvironment.cs). I left all but one of the interface's properties with the default implementation; for that one property, I used the name of the project containing the views. (I just hardcoded it into the sample below for brevity; there are obviously slicker ways to obtain it.)
public class DummyWebHostEnvironment : IWebHostEnvironment
{
public IFileProvider WebRootFileProvider { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
public string WebRootPath { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
public string ApplicationName { get => "TheProjectContainingMyViews.RazorClassLibrary"; set => throw new System.NotImplementedException(); }
public IFileProvider ContentRootFileProvider { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
public string ContentRootPath { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
public string EnvironmentName { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
}
Note: As is evident from the above code, the project containing the Views is a RazorClassLibrary. (I was using this and this as guidesfor getting the RazorViewEngine to work in a console application.)
One I had the implementation above, I added it to my services collection along with some other goodies:
private static RazorViewToStringRenderer GetRenderer()
{
var services = new ServiceCollection();
var applicationEnvironment = PlatformServices.Default.Application;
services.AddSingleton(applicationEnvironment);
var appDirectory = Directory.GetCurrentDirectory();
var environment = new DummyWebHostEnvironment();
services.AddSingleton<IWebHostEnvironment>(environment);
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore");
services.AddSingleton<DiagnosticSource>(diagnosticSource);
services.AddSingleton<DiagnosticListener>(diagnosticSource);
services.AddLogging();
services.AddMvc();
services.AddSingleton<RazorViewToStringRenderer>();
var provider = services.BuildServiceProvider();
return provider.GetRequiredService<RazorViewToStringRenderer>();
}
Note: See the first of the links above for the code for RazorViewToStringRenderer. Here's the interface:
public interface IRazorViewToStringRenderer
{
Task<string> RenderViewToStringAsync<TModel>(string viewName, TModel model);
}
Then, in Program.cs, I can just do something like this:
static async Task Main(string[] args)
{
var dto = BuildDto();
var renderer = GetRenderer();
var renderedString = await renderer.RenderViewToStringAsync("Views/Path/To/Some.cshtml", dto);
// ...
}
I had the same issue. Although I am writing in .NET Core 5 already.
I am assuming you are writing based on this or similar solution: https://scottsauber.com/2018/07/07/walkthrough-creating-an-html-email-template-with-razor-and-razor-class-libraries-and-rendering-it-from-a-net-standard-class-library/
You have
var getViewResult = _viewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: true);
where executingFilePath is null.
Add executingFilePath so it leads to the view location on disk.
In my solution I have:
var getViewResult = _viewEngine.GetView(executingFilePath: executingFilePath, viewPath: viewName, isMainPage: true);
where executingFilePath is passed to RenderViewToStringAsync as additional parameter:
public class MessageBodyBuilderService : IMessageBodyBuilderService
{
private readonly IRazorViewToStringRenderer _razorViewToStringRenderer;
private readonly IWebHostEnvironment _hostingEnv;
private readonly string _pathToEmailTemplates = $"/Views/EmailTemplates/";
public MessageBodyBuilderService(
IWebHostEnvironment hostingEnv,
IRazorViewToStringRenderer razorViewToStringRenderer)
{
_hostingEnv = hostingEnv;
_razorViewToStringRenderer = razorViewToStringRenderer;
}
public async Task<BodyBuilder> BuildMessage<T>(string templateName, T modelForReplacement, bool isHtml = true)
{
string viewName = $"{_pathToEmailTemplates}{templateName}.cshtml";
string body = await _razorViewToStringRenderer.RenderViewToStringAsync(viewName, modelForReplacement, _hostingEnv.ContentRootPath);
var builder = new BodyBuilder()
{
HtmlBody = body
};
return builder;
}
}
where _hostingEnv.ContentRootPath comes from the ContentRootPath I declared on Startup:
AppDomain.CurrentDomain.SetData("ContentRootPath", webHostEnvironment.ContentRootPath);
and then you can pass executingFilePath (in your RazorViewToStringRenderer's RenderViewToStringAsync method) to FindView as additional parameter:
var view = FindView(executingFilePath, actionContext, viewName);
I hope it helps.

Is there a way to use any IDistributedCache as a ResponseCache in .net core?

I want to cache responses from APIs to DistributedSqlServerCache.
The default ResponseCaching only uses a memory cache. There is a constructor which allows to configure what cache to use, but it's internal.
I wrote a filter. If the response is not cached and the http response is OK and the ActionResult is an ObjectActionResult, it serializes the value as JSON and saves it to SQL cache.
If the response is cached, it deserializes it and sets the result as an OkObject result with the deserielized object.
It works ok, but it has some clumsy things (like, to use the attribute, you have to specify the type which will be de/serialized, with typeof()).
Is there a way to cache responses to a distributed sql cache, which doesn't involve me hacking together my own mostly working solution?
Another option would be to copy-pasta the netcore ResponseCacheMiddleWare, and modify it to use a diffirent cache. I could even make it a nuget package maybe.
Are there any other solutions out there?
Here's the filter I put together (simplified for display purposes)
namespace Api.Filters
{
/// <summary>
/// Caches the result of the action as data.
/// The action result must implement <see cref="ObjectResult"/>, and is only cached if the HTTP status code is OK.
/// </summary>
public class ResponseCache : IAsyncResourceFilter
{
public Type ActionType { get; set; }
public ExpirationType ExpirationType;
private readonly IDistributedCache cache;
public ResponseCache(IDistributedCache cache)
{
this.cache = cache;
}
public async Task OnResourceExecutionAsync(ResourceExecutingContext executingContext, ResourceExecutionDelegate next)
{
var key = getKey(executingContext);
var cachedValue = await cache.GetAsync(key);
if (cachedValue != null && executingContext.HttpContext.Request.Query["r"] == "cache")
{
await cache.RemoveAsync(key);
cachedValue = null;
}
if (cachedValue != null)
{
executingContext.Result = new OkObjectResult(await fromBytes(cachedValue));
return;
}
var executedContext = await next();
// Only cache a successful response.
if (executedContext.HttpContext.Response.StatusCode == StatusCodes.Status200OK && executedContext.Result is ObjectResult result)
{
await cache.SetAsync(key, await toBytes(result.Value), getExpiration());
}
}
private async Task<byte[]> toBytes(object value)
{
using var stream = new MemoryStream();
await JsonSerializer.SerializeAsync(stream, value, ActionType);
return stream.ToArray();
}
private async Task<object> fromBytes(byte[] bytes)
{
using var stream = new MemoryStream(bytes);
using var reader = new BinaryReader(stream, Encoding.Default, true);
return await JsonSerializer.DeserializeAsync(stream, ActionType);
}
}
public class ResponseCacheAttribute : Attribute, IFilterFactory
{
public bool IsReusable => true;
public ExpirationType ExpirationType;
public Type ActionType { get; set; }
public ResponseCacheAttribute(params string[] queryParameters)
{
this.queryParameters = queryParameters;
}
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
var cache = serviceProvider.GetService(typeof(IDistributedCache)) as IDistributedCache;
return new ResponseCache(cache)
{
ExpirationType = ExpirationType,
ActionType = ActionType
};
}
}
}
In the end I made a nuget package, sourced on github. See this issue for some more context as to why a new package was made.

Set a custom SessionStore for ConfigureApplicationCookie without BuildServiceProvider()

I have a .NET Core 3 project (recently upgraded from 2.2) that uses a Redis distributed cache and cookie authentication.
It currently looks something like this:
public void ConfigureServices(IServiceCollection services)
{
// Set up Redis distributed cache
services.AddStackExchangeRedisCache(...);
...
services.ConfigureApplicationCookie(options =>
{
...
// Get a service provider to get the distributed cache set up above
var cache = services.BuildServiceProvider().GetService<IDistributedCache>();
options.SessionStore = new MyCustomStore(cache, ...);
}):
}
The problem is that BuildServiceProvider() causes a build error:
Startup.cs(...): warning ASP0000: Calling 'BuildServiceProvider' from application code results in an additional copy of singleton services being created. Consider alternatives such as dependency injecting services as parameters to 'Configure'.
This doesn't appear to be an option - ConfigureApplicationCookie is in Startup.ConfigureServices and can only configure new services, Startup.Configure can use the new services, but can't override CookieAuthenticationOptions.SessionStore to be my custom store.
I've tried adding services.AddSingleton<ITicketStore>(p => new MyCustomRedisStore(cache, ...)) before ConfigureApplicationCookie, but this is ignored.
Explicitly setting CookieAuthenticationOptions.SessionStore appears to be the only way to get it to use anything other than the local memory store.
Every example I've found online uses BuildServiceProvider();
Ideally I want to do something like:
services.ConfigureApplicationCookieStore(provider =>
{
var cache = provider.GetService<IDistributedCache>();
return new MyCustomStore(cache, ...);
});
Or
public void Configure(IApplicationBuilder app, ... IDistributedCache cache)
{
app.UseApplicationCookieStore(new MyCustomStore(cache, ...));
}
And then CookieAuthenticationOptions.SessionStore should just use whatever I've configured there.
How do I make the application cookie use an injected store?
Reference Use DI services to configure options
If all the dependencies of your custom store are injectable, then just register your store and required dependencies with the service collection and use DI services to configure options
public void ConfigureServices(IServiceCollection services) {
// Set up Redis distributed cache
services.AddStackExchangeRedisCache(...);
//register my custom store
services.AddSingleton<ITicketStore, MyCustomRedisStore>();
//...
//Use DI services to configure options
services.AddOptions<CookieAuthenticationOptions>(IdentityConstants.ApplicationScheme)
.Configure<ITicketStore>((options, store) => {
options.SessionStore = store;
});
services.ConfigureApplicationCookie(options => {
//do nothing
}):
}
If not then work around what is actually registered
For example
//Use DI services to configure options
services.AddOptions<CookieAuthenticationOptions>(IdentityConstants.ApplicationScheme)
.Configure<IDistributedCache>((options, cache) => {
options.SessionStore = new MyCustomRedisStore(cache, ...);
});
Note:
ConfigureApplicationCookie uses a named options instance. - #KirkLarkin
public static IServiceCollection ConfigureApplicationCookie(this IServiceCollection services, Action<CookieAuthenticationOptions> configure)
=> services.Configure(IdentityConstants.ApplicationScheme, configure);
The option would need to include the name when adding it to services.
To implement Redis Tickets in .NET Core 3.0 we did the following which is the above in a bit more of a final form::
services.AddSingleton<ITicketStore, RedisTicketStore>();
services.AddOptions<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme)
.Configure<ITicketStore>((options, store) => {
options.SessionStore = store;
});
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
// ...configure identity server options
}).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
Here is a Redis implementation:
public class RedisTicketStore : ITicketStore
{
private const string KeyPrefix = "AuthSessionStore-";
private IDistributedCache cache;
public RedisTicketStore(IDistributedCache cache)
{
this.cache = cache;
}
public async Task<string> StoreAsync(AuthenticationTicket ticket)
{
var guid = Guid.NewGuid();
var key = KeyPrefix + guid.ToString();
await RenewAsync(key, ticket);
return key;
}
public Task RenewAsync(string key, AuthenticationTicket ticket)
{
var options = new DistributedCacheEntryOptions();
var expiresUtc = ticket.Properties.ExpiresUtc;
if (expiresUtc.HasValue)
{
options.SetAbsoluteExpiration(expiresUtc.Value);
}
byte[] val = SerializeToBytes(ticket);
cache.Set(key, val, options);
return Task.FromResult(0);
}
public Task<AuthenticationTicket> RetrieveAsync(string key)
{
AuthenticationTicket ticket;
byte[] bytes = null;
bytes = cache.Get(key);
ticket = DeserializeFromBytes(bytes);
return Task.FromResult(ticket);
}
public Task RemoveAsync(string key)
{
cache.Remove(key);
return Task.FromResult(0);
}
private static byte[] SerializeToBytes(AuthenticationTicket source)
{
return TicketSerializer.Default.Serialize(source);
}
private static AuthenticationTicket DeserializeFromBytes(byte[] source)
{
return source == null ? null : TicketSerializer.Default.Deserialize(source);
}
}
Redis implementation from: https://mikerussellnz.github.io/.NET-Core-Auth-Ticket-Redis/

ASP.NET Core 1.0 custom compression middleware and static files issue

My startup's middleware configuration looks something like this:
public void Configure(IApplicationBuilder app)
{
app.UseCompression();
app.UseIISPlatformHandler();
app.UseApplicationInsightsRequestTelemetry();
app.UseCors("CorsPolicy");
app.UseStaticFiles();
app.UseMvc();
app.UseApplicationInsightsExceptionTelemetry();
}
Since adding the app.UseCompression() middleware, static html files in wwwroot aren't loading correctly anymore. They don't resolve and tend to load indefinitely.
The compression middleware looks as follows and was sourced from here:
public class CompressionMiddleware
{
private readonly RequestDelegate nextDelegate;
public CompressionMiddleware(RequestDelegate next)
{
nextDelegate = next;
}
public async Task Invoke(HttpContext httpContext)
{
var acceptEncoding = httpContext.Request.Headers["Accept-Encoding"];
//Checking that we have been given the correct header before moving forward
if (!(String.IsNullOrEmpty(acceptEncoding)))
{
//Checking that it is gzip
if (acceptEncoding.ToString().IndexOf("gzip", StringComparison.CurrentCultureIgnoreCase) >= 0)
{
using (var memoryStream = new MemoryStream())
{
var stream = httpContext.Response.Body;
httpContext.Response.Body = memoryStream;
await nextDelegate(httpContext);
using (var compressedStream = new GZipStream(stream, CompressionLevel.Optimal))
{
httpContext.Response.Headers.Add("Content-Encoding", new string[] { "gzip" });
memoryStream.Seek(0, SeekOrigin.Begin);
await memoryStream.CopyToAsync(compressedStream);
}
}
}
else
{
await nextDelegate(httpContext);
}
}
//If we have are given to Accept Encoding header or it is blank
else
{
await nextDelegate(httpContext);
}
}
}
Does anyone know why this could be happening?
Note: I am using DNX 1.0.0-rc1-update1 and the 1.0.0-rc1-final libraries.
I just enabled compression in web.config in /wwwroot folder (like we used to do in IIs applicationHost.config), and it works. There's no need to add a compression middleware.
http://www.brandonmartinez.com/2015/08/20/enable-gzip-compression-for-azure-web-apps/
Compression defined in web.config only works with IIS NOT the self hosted web server.
I tested the CompressionMiddleware example and the problem You are seeing is caused by the Content-Length header.
If the Content-Length is already set, the browser gets confused as the actual size of the response doesn't match the value defined in the header as the content is zipped.
Removing the content-length header solved the problem:
httpContext.Response.Headers.Remove("Content-Length");
httpContext.Response.Headers.Add("Content-Encoding", new string[] { "gzip" });
...even better you could try to specify the actual content-length when the content is zipped.

Access Cookies inside unit test in AspNet.TestHost.TestServer context on ASP.NET 5 / MVC 6

There is no easy way to get an access to a CookieContainer in response object running integration tests with AspNet.TestHost.TestServer. Cookies have to be set by the controller action. What is the best way to achieve that?
var client = TestServer.Create(app =>
{
app.UseMvc(routes =>
routes.MapRoute("default", "{controller}/{action}/{id?}"));
app.UseIdentity();
}).CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "account/login");
var response = await client.SendAsync(request);
// how to get an access to cookie container ?????????
// response.Cookies prop doesn't exist
Assert.NotEmpty(response.Cookies["auth"]);
Solution that I see is to extend instance of the TestServer, return instance of a class CustomClientHandler : ClientHandler and override the whole process of sending a request in that handler, but it needs literally to change all logic except relatively small code of the TestServer.
Any better suggestion how to implement an access to Cookies in a response?
As an addition to #Oleh's response, you can achieve the same without reflection on newer frameworks like .NET 4.6.1+ / .NET Core
public class TestHttpClientHandler : DelegatingHandler
{
[NotNull]
private readonly CookieContainer cookies = new CookieContainer();
public TestHttpClientHandler([NotNull] HttpMessageHandler innerHandler)
: base(innerHandler) { }
[NotNull, ItemNotNull]
protected override async Task<HttpResponseMessage> SendAsync([NotNull] HttpRequestMessage request, CancellationToken ct)
{
Uri requestUri = request.RequestUri;
request.Headers.Add(HeaderNames.Cookie, this.cookies.GetCookieHeader(requestUri));
HttpResponseMessage response = await base.SendAsync(request, ct);
if (response.Headers.TryGetValues(HeaderNames.SetCookie, out IEnumerable<string> setCookieHeaders))
{
foreach (SetCookieHeaderValue cookieHeader in SetCookieHeaderValue.ParseList(setCookieHeaders.ToList()))
{
Cookie cookie = new Cookie(cookieHeader.Name.Value, cookieHeader.Value.Value, cookieHeader.Path.Value);
if (cookieHeader.Expires.HasValue)
{
cookie.Expires = cookieHeader.Expires.Value.DateTime;
}
this.cookies.Add(requestUri, cookie);
}
}
return response;
}
}
I've implemented custom HttpMessageHandler that tracks cookies.
It uses reflection to invoke the actual handler and just reads/sets Cookie headers.
class TestMessageHandler : HttpMessageHandler
{
delegate Task<HttpResponseMessage> HandlerSendAsync(HttpRequestMessage message, CancellationToken token);
private readonly HandlerSendAsync nextDelegate;
private readonly CookieContainer cookies = new System.Net.CookieContainer();
public TestMessageHandler(HttpMessageHandler next)
{
if(next == null) throw new ArgumentNullException(nameof(next));
nextDelegate = (HandlerSendAsync)
next.GetType()
.GetTypeInfo()
.GetMethod("SendAsync", BindingFlags.NonPublic | BindingFlags.Instance)
.CreateDelegate(typeof(HandlerSendAsync), next);
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Add("Cookie", cookies.GetCookieHeader(request.RequestUri));
var resp = await nextDelegate(request, cancellationToken).ConfigureAwait(false);
if (resp.Headers.TryGetValues("Set-Cookie", out var newCookies))
{
foreach (var item in SetCookieHeaderValue.ParseList(newCookies.ToList()))
{
cookies.Add(request.RequestUri, new Cookie(item.Name, item.Value, item.Path));
}
}
return resp;
}
}
And then you create your HttpClient like this:
var httpClient = new HttpClient(
new TestMessageHandler(
server.CreateHandler()));
TestMessageHandler now takes care of tracking cookies.
For a dotnet core integration test approach like the one described in the docs here, you can get cookies with the following code:
public class CookieTests : IClassFixture<WebApplicationFactory<Startup>>
{
private readonly WebApplicationFactory<Startup> _factory;
public CookieTests(WebApplicationFactory<Startup> factory)
{
_factory = factory;
}
[Fact]
public async Task GetPage_ShouldSetCookie_CookieSet()
{
using (var client = _factory.CreateClient())
{
var response = await client.GetAsync("/cookie_setting_url");
response.EnsureSuccessStatusCode();
//or other assertions
Assert.True(response.Headers.TryGetValues(HeaderNames.SetCookie, out IEnumerable<string> cookies));
}
}
}
The proper way, using minimal code getting cookies
in Asp.Net Core Functional Tests is as follows, (I leave out init code for setting up WebApplicationFactory, which is known stuff)
The given examples above, require either reflection (Since I think MS made a design bug on not exposing the default handlers) or require cookie parsing, which is annoying in 2023.
private (HttpClient, CookieContainerHandler) GetHttpClient()
{
CookieContainerHandler cookieContainerHandler = new();
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(defaultScheme: "YourSchema")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestAzure", options => { });
});
}).CreateDefaultClient(cookieContainerHandler);
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(scheme: "YourSchema");
return (client, cookieContainerHandler);
}
[Fact]
public async Task MyUnitTest()
{
// Arrange
var (client, cookieHandler) = GetHttpClient();
// Act PUT/GET/POST etc
var response = await client.PutAsync("youruri", null);
var sessionCookie = cookieHandler.Container.GetAllCookies().FirstOrDefault(f => f.Name == "yourcookie"); // note this ignores cookie domain policy
}