Background: I am using ASP.NET Core 3.1, and integration testing a REST service that requires cookie authentication.
Candidate solution below.
Note:
The reason I use a vanilla Host instead of TestServer is because of the cookie requirement. When using TestServer, it provides an HttpClient for you, but the client does not pass cookies back to the server.
I also attempted to use a custom HttpClient with TestServer. That consistently generated a System.Net.Sockets.SocketException (No connection could be made because the target machine actively refused it.)
using Microsoft.Extensions.Hosting;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using WebApi; // Contains my Startup.cs
namespace WebApiTest
{
[TestClass]
public class UserTest
{
static IHost HttpHost;
[ClassInitialize]
public static async Task ClassStartup(TestContext context)
{
HttpHost = Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.Build();
await HttpHost.StartAsync();
}
[ClassCleanup]
public static async Task ClassCleanup()
{
await HttpHost.StopAsync();
}
public static HttpContent GetHttpContent(object content)
{
HttpContent httpContent = null;
if (content != null)
{
httpContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(content, content.GetType()));
httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
}
return httpContent;
}
public static HttpClient GetCookieHttpClient()
{
SocketsHttpHandler handler = new SocketsHttpHandler
{
AllowAutoRedirect = false,
CookieContainer = new CookieContainer(),
UseCookies = true
};
return new HttpClient(handler);
}
[TestMethod]
public async Task GetUserData_ReturnsSuccess()
{
using (HttpClient client = GetCookieHttpClient())
{
var credentials = new
{
Email = "test#test.com",
Password = "password123",
};
HttpResponseMessage response = await client.PostAsync("http://localhost:5000/api/auth/login", GetHttpContent(credentials));
response = await client.GetAsync(String.Format("http://localhost:5000/api/users/{0}", credentials.Email));
Assert.IsTrue(response.StatusCode == HttpStatusCode.OK);
}
}
}
}
HttpClient is a thin-client; it doesn't do anything unless you explicitly tell it to. In other words, it will never send the cookie for you; you must add a Cookie header to the request with the cookie value for each request. The test server "client" is just an HttpClient instance set up to proxy requests to the test server. You should use the test server, as prescribed, along with its client, and then add the Cookie header the requests you make with that.
Solutions based on Chris Pratt's suggestions
After some further digging, Microsoft provides a solution for this (WebApplicationFactory):
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using WebApi;
namespace WebApiTest
{
[TestClass]
public class Class2
{
static WebApplicationFactory<Startup> Factory;
static WebApplicationFactoryClientOptions ClientOptions;
[ClassInitialize]
public static async Task ClassStartup(TestContext context)
{
Factory = new WebApplicationFactory<Startup>();
ClientOptions = new WebApplicationFactoryClientOptions();
ClientOptions.AllowAutoRedirect = false;
ClientOptions.HandleCookies = true;
ClientOptions.BaseAddress = new Uri("http://localhost:5000");
}
public static HttpContent GetHttpContent(object content)
{
HttpContent httpContent = null;
if (content != null)
{
httpContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(content, content.GetType()));
httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
}
return httpContent;
}
[TestMethod]
public async Task GetUserData_ReturnsSuccess()
{
using (HttpClient client = Factory.CreateClient(ClientOptions))
{
var credentials = new
{
Email = "test#test.com",
Password = "password123",
};
HttpResponseMessage response = await client.PostAsync("http://localhost:5000/api/auth/login", GetHttpContent(credentials));
response = await client.GetAsync(String.Format("http://localhost:5000/api/users/{0}", credentials.Email));
Assert.IsTrue(response.StatusCode == HttpStatusCode.OK);
}
}
}
}
In case you want to stick with TestServer, here is a manual Cookie-passing implementation:
using Microsoft.AspNetCore.TestHost;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using WebApi;
namespace WebApiTest
{
public class CookieHttpClient : IDisposable
{
private static HttpContent GetHttpContent(object content)
{
HttpContent httpContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(content, content.GetType()));
httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
return httpContent;
}
private static IEnumerable<string> GetCookieStrings(CookieCollection collection)
{
List<string> output = new List<string>(collection.Count);
foreach (Cookie cookie in collection)
{
output.Add(cookie.Name + "=" + cookie.Value);
}
return output;
}
private HttpClient client;
private CookieContainer container;
public CookieHttpClient(HttpClient client)
{
this.client = client;
this.container = new CookieContainer();
}
public async Task<HttpResponseMessage> SendAsync(HttpMethod method, Uri uri)
{
return await this.SendAsync(method, uri, null);
}
public async Task<HttpResponseMessage> SendAsync(HttpMethod method, Uri uri, object data)
{
HttpRequestMessage request = new HttpRequestMessage(method, uri);
// Add data
if (data != null)
{
request.Content = GetHttpContent(data);
}
// Add cookies
CookieCollection collection = this.container.GetCookies(uri);
if (collection.Count > 0)
{
request.Headers.Add("Cookie", GetCookieStrings(collection));
}
HttpResponseMessage response = await this.client.SendAsync(request);
// Remember cookies before returning
if (response.Headers.Contains("Set-Cookie"))
{
foreach (string s in response.Headers.GetValues("Set-Cookie"))
{
this.container.SetCookies(uri, s);
}
}
return response;
}
public void Dispose()
{
this.client.Dispose();
}
}
[TestClass]
public class Class1
{
static TestServer TestServer;
[ClassInitialize]
public static async Task ClassStartup(TestContext context)
{
IWebHostBuilder builder = new WebHostBuilder()
.UseStartup<Startup>();
TestServer = new TestServer(builder);
}
[TestMethod]
public async Task GetUserData_ReturnsSuccess()
{
using (CookieHttpClient client = new CookieHttpClient(TestServer.CreateClient()))
{
var credentials = new
{
Email = "test#test.com",
Password = "password123",
};
HttpResponseMessage response = await client.SendAsync(HttpMethod.Post, new Uri("http://localhost:5000/api/auth/login"), credentials);
response = await client.SendAsync(HttpMethod.Get, new Uri("http://localhost:5000/api/users/" + credentials.Email));
Assert.IsTrue(response.StatusCode == HttpStatusCode.OK);
}
}
}
}
Related
I am upgrading my C# function app from .net 3.1 to 6.0`.
When I run my test cases, I found that, 1 of my test case failed with the below error.
Castle.DynamicProxy.InvalidProxyConstructorArgumentsException : Can not instantiate proxy of class: System.Net.HttpWebRequest. Could not find a parameterless constructor.
Basically, I am trying to mock HttpWebRequest and below is my piece of code for that.
var httpWebRequest = new Mock<HttpWebRequest>();
It is working fine in .Net 3.1. I am using Moq version 4.16.1 in both the projects.
I spent a fair bit of time when .Net 6 was initially released getting my Unit Test suite established. Here's how I do it using the same Moq version 4.16.1:
The Unit Tests get a Moq HttpClientFactory from the BaseClass:
public class UnitTests : BaseUnitTest
{
[Fact]
public async Task Should_Return_GetSomethingAsync()
{
// Arrange
IHttpClientFactory httpClientFactory = base.GetHttpClientFactory(new Uri("ExternalWebsiteUrlToMockTheResponse"), new StringContent("A Mock Response JSON Object"));
YourService yourService = new YourService(httpClientFactory);
// Act
Something something = yourService.GetSomethingAsync().Result;
// Assert
Assert.IsType<Something>(Something);
//..
}
In a BaseUnitTest.cs Class:
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Moq.Protected;
public class BaseUnitTest
{
public IHttpClientFactory GetHttpClientFactory(Uri uri, StringContent content, HttpStatusCode statusCode = HttpStatusCode.OK)
{
Mock<HttpMessageHandler> httpMsgHandler = new Mock<HttpMessageHandler>();
httpMsgHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", new object[2]
{
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()
}).ReturnsAsync(new HttpResponseMessage
{
StatusCode = statusCode,
Content = content
});
HttpClient client = new HttpClient(httpMsgHandler.Object);
client.BaseAddress = uri;
Mock<IHttpClientFactory> clientFactory = new Mock<IHttpClientFactory>();
clientFactory.Setup((IHttpClientFactory cf) => cf.CreateClient(It.IsAny<string>())).Returns(client);
return clientFactory.Object;
}
Your Service Class or Controller:
public class YourService : IYourService
{
private readonly IHttpClientFactory _clientFactory;
private readonly HttpClient _client;
public YourService(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
_client = _clientFactory.CreateClient("YourAPI");
}
public async Task<Something> GetSomethingAsync()
{
using (var request = new HttpRequestMessage(HttpMethod.Post, _client.BaseAddress))
{
request.Content = new StringContent($#"{{""jsonrpc"":""2.0"",""method"":""Something"",""params"": [""{SomethingHash}""],""id"":1}}");
using (var response = await _client.SendAsync(request))
{
//System.Diagnostics.Debug.WriteLine(response?.Content.ReadAsStringAsync()?.Result);
if (response.IsSuccessStatusCode)
{
using (var responseStream = await response.Content.ReadAsStreamAsync())
{
var options = new JsonSerializerOptions { IncludeFields = true };
var something = await JsonSerializer.DeserializeAsync<Something>(responseStream, options);
// Check if the transactions from the address we're looking for...
if (something != null)
{
if (something.result?.from == address)
{
return something;
}
} } }
else {
string exceptionMsg = $"Message: {response.Content?.ReadAsStringAsync()?.Result}";
throw new YourGeneralException(response.StatusCode, exceptionMsg);
}
}
}
return null;
}
}
In your Program.cs
builder.Services.AddHttpClient("YourAPI", c =>
{
c.BaseAddress = new Uri("ExternalWebsiteUrlToMockTheResponse");
c.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
c.DefaultRequestHeaders.UserAgent.TryParseAdd("Your Agent");
});
You can expand the BaseUnitTest.ccs class to have chained tests as well.
Both HttpWebRequest constructors are obsolete and should not be used. You have to use the static function "Create" to create a new instance of the HttpWebRequest class:
HttpWebRequest myReq =
(HttpWebRequest)WebRequest.Create("http://www.contoso.com/");
To solve your issue, use the HttpClient class instead. This class has a parameterless constructor.
I have a Azure Function with 2 triggers:
I’m registering IService in my Startup like so:
I need a different configuration in the Service class depending on which trigger that is calling DoWork()? How can I achieve this using DI?
public class Service : IService
{
public Service(/*Configuration to be injected depends on calling trigger */)
{ }
public void DoWork()
{ }
}
Configuration extract:
Thankyou user1672994. Posting your suggestion as an answer so that it will be helpful for other community members who face similar kind of issues.
Below is the example code to implement todo work items where this will be helpful in resolving your issue.
using AZV3CleanArchitecture.Models;
using AZV3CleanArchitecture.Options;
using AZV3CleanArchitecture.Providers;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
namespace AZV3CleanArchitecture.Services
{
public class ToDoItemsService : IToDoItemsService
{
private readonly HttpClient httpClient;
private readonly ToDoItemsServiceOptions toDoItemsServiceOptions;
private readonly ILogger<ToDoItemsService> logger;
public ToDoItemsService(HttpClient httpClient, IOptions<ToDoItemsServiceOptions> toDoItemsServiceOptions, ILogger<ToDoItemsService> logger)
{
this.httpClient = httpClient;
this.toDoItemsServiceOptions = toDoItemsServiceOptions.Value;
this.logger = logger;
}
public async Task<ToDoItem> GetToDoItem(int id)
{
logger.LogInformation($"Retrieving item: {{{Constants.TodoItemId}}}", id);
var getUrl = $"{this.toDoItemsServiceOptions.BaseUrl.TrimEnd('/')}/todos/{id}";
using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, getUrl))
{
using (var response = await this.httpClient.SendAsync(requestMessage))
{
string responseString = await response.Content.ReadAsStringAsync();
logger.LogWarning($"Retrieved item: {{{Constants.TodoItemId}}}. Logged as warning for demo.", id);
return JsonConvert.DeserializeObject<ToDoItem>(responseString);
}
}
}
public async Task<IEnumerable<ToDoItem>> GetAllToDoItems(int id)
{
logger.LogInformation($"Retrieving all todo items");
var getUrl = $"{this.toDoItemsServiceOptions.BaseUrl.TrimEnd('/')}/todos";
using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, getUrl))
{
using (var response = await this.httpClient.SendAsync(requestMessage))
{
string responseString = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<IEnumerable<ToDoItem>>(responseString);
}
}
}
public async Task<ToDoItem> CreateToDoItem(ToDoItem toDoItem)
{
// call service and return the output
return await Task.FromResult(new ToDoItem() { Id = 1, UserId = 1, Title = "Some Dummy Title", Completed = true });
}
public Task<ToDoItem> UpdateToDoItem(ToDoItem toDoItem)
{
throw new System.NotImplementedException();
}
}
}
for further information check the ToDoItemServices link.
I have an ASP.NET MVC application which invokes an ASP.NET Web API REST Service each time a button is pressed in the UI.
Each time this button is pressed below DumpWarehouseDataIntoFile method is executed.
public class MyClass
{
private static HttpClient client = new HttpClient();
public async Task DumpWarehouseDataIntoFile(Warehouse myData, string path, string filename)
{
try
{
//Hosted web API REST Service base url
string Baseurl = "http://XXX.XXX.XX.X:YYYY/";
//using (var client = new HttpClient()) --> I have declared client as an static variable
//{
//Passing service base url
client.BaseAddress = new Uri(Baseurl);
client.DefaultRequestHeaders.Clear();
//Define request data format
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// Serialize parameter to pass to the asp web api rest service
string jsonParam = Newtonsoft.JsonConvert.SerializeObject(myData);
//Sending request to find web api REST service resource using HttpClient
var httpContent = new StringContent(jsonParam, Encoding.UTF8, "application/json");
HttpResponseMessage Res = await client.PostAsync("api/Warehouse/DumpIntoFile", httpContent);
//Checking the response is successful or not which is sent using HttpClient
if (Res.IsSuccessStatusCode)
{
// Some other sftuff here
}
//}
}
catch (Exception ex)
{
// Do some stuff here
} // End Try
} // End DumpWarehouseDataIntoFile method
} // End class
Warehouse class object:
public class Warehouse
{
public DataTable dt { get; set; }
public string Filepath { get; set; }
}
I have found in this post that pattern:
using (var myClient = new HttpClient())
{
}
is not recommended to be used since it leads to socket exhaustion (System.Net.Sockets.SocketException). There it is recommended to use HttpClient as static variable and reuse it as it helps to reduce waste of sockets. So I have used a static variable.
The problem with this approach (in my scenario) is that it only works first button is pressed, next times button is pressed and DumpWarehouseDataIntoFile method is executed, below exception is thrown:
An unhandled exception has occurred while executing the request.
System.InvalidOperationException: This instance has already started
one or more requests. Properties can only be modified before sending
the first request.
As error says, properties like base address, etc. can only be modified once before sending the first request.
I have googled and found some solutions proposed:
First solution
So it seems like singleton pattern would be a good option, as proposed here. Below the singleton proposed by Alper:
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
//You need to install package Newtonsoft.Json > https://www.nuget.org/packages/Newtonsoft.Json/
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
public class MyApiClient : IDisposable
{
private readonly TimeSpan _timeout;
private HttpClient _httpClient;
private HttpClientHandler _httpClientHandler;
private readonly string _baseUrl;
private const string ClientUserAgent = "my-api-client-v1";
private const string MediaTypeJson = "application/json";
public MyApiClient(string baseUrl, TimeSpan? timeout = null)
{
_baseUrl = NormalizeBaseUrl(baseUrl);
_timeout = timeout ?? TimeSpan.FromSeconds(90);
}
public async Task<string> PostAsync(string url, object input)
{
EnsureHttpClientCreated();
using (var requestContent = new StringContent(ConvertToJsonString(input), Encoding.UTF8, MediaTypeJson))
{
using (var response = await _httpClient.PostAsync(url, requestContent))
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
}
public async Task<TResult> PostAsync<TResult>(string url, object input) where TResult : class, new()
{
var strResponse = await PostAsync(url, input);
return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
}
public async Task<TResult> GetAsync<TResult>(string url) where TResult : class, new()
{
var strResponse = await GetAsync(url);
return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
}
public async Task<string> GetAsync(string url)
{
EnsureHttpClientCreated();
using (var response = await _httpClient.GetAsync(url))
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
public async Task<string> PutAsync(string url, object input)
{
return await PutAsync(url, new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, MediaTypeJson));
}
public async Task<string> PutAsync(string url, HttpContent content)
{
EnsureHttpClientCreated();
using (var response = await _httpClient.PutAsync(url, content))
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
public async Task<string> DeleteAsync(string url)
{
EnsureHttpClientCreated();
using (var response = await _httpClient.DeleteAsync(url))
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
public void Dispose()
{
_httpClientHandler?.Dispose();
_httpClient?.Dispose();
}
private void CreateHttpClient()
{
_httpClientHandler = new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip
};
_httpClient = new HttpClient(_httpClientHandler, false)
{
Timeout = _timeout
};
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ClientUserAgent);
if (!string.IsNullOrWhiteSpace(_baseUrl))
{
_httpClient.BaseAddress = new Uri(_baseUrl);
}
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeJson));
}
private void EnsureHttpClientCreated()
{
if (_httpClient == null)
{
CreateHttpClient();
}
}
private static string ConvertToJsonString(object obj)
{
if (obj == null)
{
return string.Empty;
}
return JsonConvert.SerializeObject(obj, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
}
private static string NormalizeBaseUrl(string url)
{
return url.EndsWith("/") ? url : url + "/";
}
}
Usage
using (var client = new MyApiClient("http://localhost:8080"))
{
var response = client.GetAsync("api/users/findByUsername?username=alper").Result;
var userResponse = client.GetAsync<MyUser>("api/users/findByUsername?username=alper").Result;
}
The problem I see here is that if you call above code many times (in my case would be each time I press the button on the UI and I call DumpWarehouseDataIntoFile method), you create and instance of MyApiClient each time and therefore a new instance of HttpClient is created and I want to reuse HttpClient, not to make many instances of it.
Second solution
Creating a kind of factory as proposed here by Nico. Below the code he proposes:
public interface IHttpClientFactory
{
HttpClient CreateClient();
}
public class HttpClientFactory : IHttpClientFactory
{
static string baseAddress = "http://example.com";
public HttpClient CreateClient()
{
var client = new HttpClient();
SetupClientDefaults(client);
return client;
}
protected virtual void SetupClientDefaults(HttpClient client)
{
client.Timeout = TimeSpan.FromSeconds(30); //set your own timeout.
client.BaseAddress = new Uri(baseAddress);
}
}
Usage
public HomeController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
readonly IHttpClientFactory _httpClientFactory;
public IActionResult Index()
{
var client = _httpClientFactory.CreateClient();
//....do your code
return View();
}
Here again you create a new instance of HttpClient each time you call CreateClient. You do not reuse HttpClient object.
Third Solution
Making HTTP requests using IHttpClientFactory as explained here.
The problem is that it is only available for .NET Core, not standard ASP.NET Framework, though it seems it is available by installing this nuget package. It seems like it automatically manages efficiently HttpClient instances and I would like to apply it to my scenario. I want to avoid to
reinvent the wheel.
I have never used IHttpClientFactory and I have no idea on how to use it: configure some features like base address, set request headers, create an instance of HttpClient and then invoke PostAsync on it passing as parameter the HttpContent.
I think this is the best approach so could someone tell me the necessary steps I need to do in order to make the same things I do in DumpWarehouseDataIntoFile method but using IHttpClientFactory? I am a bit lost, I do not know how to apply IHttpClientFactory to do the same as I do within DumpWarehouseDataIntoFile method.
Any others solutions not proposed here and also some code snippets will be highly appreciated.
HttpClient
The HttpClient can throw InvalidOperationException in the following cases:
When the BaseAddress setter is called after a request has been sent out
When the Timeout setter is called after a request has been sent out
When the MaxResponseContentBufferSize setter is called after a request has been sent out
When an operation has already started and resend was requested
In order to avoid these you can set the first two on per request level, for example:
CancellationTokenSource timeoutSource = new CancellationTokenSource(2000);
await httpClient.GetAsync("http://www.foo.bar", timeoutSource.Token);
HttpClientFactory
You can use the IHttpClientFactory in .NET Framework with the following trick:
AddHttpClient registers the DefaultHttpClientFactory for IHttpClientFactory
Then you can retrieve it from the DI container
var serviceProvider = new ServiceCollection().AddHttpClient().BuildServiceProvider();
container.RegisterInstance(serviceProvider.GetService<IHttpClientFactory>());
container.ContainerScope.RegisterForDisposal(serviceProvider);
This sample uses SimpleInjector but the same concept can be applied for any other DI framework.
I'm not sure but will what happen if you move this lines to constructor:
//Passing service base url
client.BaseAddress = new Uri(Baseurl);
client.DefaultRequestHeaders.Clear();
//Define request data format
client.DefaultRequestHeaders.Accept
.Add(new MediaTypeWithQualityHeaderValue("application/json"));
I think that re-initialization is problem.
Better to add the request url and the headers at the message. Don't use httpClient.BaseAddress or httpClient.DefaultRequestHeaders unless you have a default requirement.
HttpRequestMessage msg = new HttpRequestMessage {
Method = HttpMethod.Put,
RequestUri = new Uri(url),
Headers = httpRequestHeaders;
};
httpClient.SendAsync(msg);
It works well for reusing the HttpClient for many requests
I am attempting to PUT and POST to a URL using HttpClient in C#. I need to do so asynchronously for scaling purposes. However, I am only able to get my PUT and POST to work synchronously. Below is the code I am using to PUT the ZoomData objects in JSON format to the URL:
// takes the dataset to PUT and PUTs to server
public async Task<HttpResponseMessage> JsonPUTcall(ZoomData toPut)
{
string jsonString = JsonConvert.SerializeObject(toPut);
return await client.PutAsync(InsiteDatasetPutURL.Replace("sys_id", toPut.sys_id), new StringContent(jsonString, UnicodeEncoding.UTF8, "application/json"));
}
And here is the code I am using to actually pass ZoomData objects in a queue to JsonPUTcall:
public async void JsonPUTqueueCall(Queue<ZoomData> toPut)
{
if (toPut.Count == 0)
return;
foreach (var zoomData in toPut)
{
var result = await this.JsonPUTcall(zoomData);
}
}
However, when I attempt this, it simply hangs. So, as a test, I replaced "var result = await this.JsonPUTcall(zoomData);" with the following:
public async void JsonPUTqueueCall(Queue<ZoomData> toPut)
{
if (toPut.Count == 0)
return;
foreach (var zoomData in toPut)
{
var result = this.JsonPUTcall(zoomData);
result.Wait();
}
}
That works, but since it is synchronous, it defeats the purpose of using async. What am I missing?
this is my API client which is efficiently uses resources and the methods are async
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
//You need to install package Newtonsoft.Json > https://www.nuget.org/packages/Newtonsoft.Json/
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
public class MyApiClient : IDisposable
{
private readonly TimeSpan _timeout;
private HttpClient _httpClient;
private HttpClientHandler _httpClientHandler;
private readonly string _baseUrl;
private const string ClientUserAgent = "my-api-client-v1";
private const string MediaTypeJson = "application/json";
public MyApiClient(string baseUrl, TimeSpan? timeout = null)
{
_baseUrl = NormalizeBaseUrl(baseUrl);
_timeout = timeout ?? TimeSpan.FromSeconds(90);
}
public async Task<string> PostAsync(string url, object input)
{
EnsureHttpClientCreated();
using (var requestContent = new StringContent(ConvertToJsonString(input), Encoding.UTF8, MediaTypeJson))
{
using (var response = await _httpClient.PostAsync(url, requestContent))
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
}
public async Task<TResult> PostAsync<TResult>(string url, object input) where TResult : class, new()
{
var strResponse = await PostAsync(url, input);
return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
}
public async Task<TResult> GetAsync<TResult>(string url) where TResult : class, new()
{
var strResponse = await GetAsync(url);
return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
}
public async Task<string> GetAsync(string url)
{
EnsureHttpClientCreated();
using (var response = await _httpClient.GetAsync(url))
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
public async Task<string> PutAsync(string url, object input)
{
return await PutAsync(url, new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, MediaTypeJson));
}
public async Task<string> PutAsync(string url, HttpContent content)
{
EnsureHttpClientCreated();
using (var response = await _httpClient.PutAsync(url, content))
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
public async Task<string> DeleteAsync(string url)
{
EnsureHttpClientCreated();
using (var response = await _httpClient.DeleteAsync(url))
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
public void Dispose()
{
_httpClientHandler?.Dispose();
_httpClient?.Dispose();
}
private void CreateHttpClient()
{
_httpClientHandler = new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip
};
_httpClient = new HttpClient(_httpClientHandler, false)
{
Timeout = _timeout
};
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ClientUserAgent);
if (!string.IsNullOrWhiteSpace(_baseUrl))
{
_httpClient.BaseAddress = new Uri(_baseUrl);
}
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeJson));
}
private void EnsureHttpClientCreated()
{
if (_httpClient == null)
{
CreateHttpClient();
}
}
private static string ConvertToJsonString(object obj)
{
if (obj == null)
{
return string.Empty;
}
return JsonConvert.SerializeObject(obj, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
}
private static string NormalizeBaseUrl(string url)
{
return url.EndsWith("/") ? url : url + "/";
}
}
Usage;
using ( var client = new MyApiClient("http://localhost:8080"))
{
var response = client.GetAsync("api/users/findByUsername?username=alper").Result;
var userResponse = client.GetAsync<MyUser>("api/users/findByUsername?username=alper").Result;
}
These 2 lines is to make it synchronous :
var result = this.JsonPUTcall(zoomData);
result.Wait();
To make it async you hate to do that :
var result = await this.JsonPUTcall(zoomData);
I have a (legacy) method in my codebase which generates and returns a ready-to-send HTTP POST message as a System.Net.HttpWebRequest object:
public HttpWebRequest GetHttpWebRequest(string body, string url, string contentType)
{
HttpWebRequest request = HttpWebRequest.CreateHttp(url);
request.Method = "POST";
// (More setup stuff here...)
using (var writer = new StreamWriter(request.GetRequestStream()))
{
writer.Write(body);
}
return request;
}
I'd like to write a unit test which verifies that the HttpWebRequest instance returned by this method actually does have the message body text that was passed in to the method in the body parameter.
Question: How can I get the body text of an HttpWebRequest object (without ever actually sending the HTTP request)?
Stuff I've tried so far:
new StreamReader(myHttpWebRequest.GetRequestStream()).ReadToEnd() - Fails at runtime with ArgumentException: Stream was not readable.
The HttpWebRequest class doesn't seem to have any property that would allow getting/reading the HTTP message body such as Body, Message, Text, etc.
I would write a http listener and make real http requests.
Here is a sample server using WCF + the client code. Just call await TestClient.Test(); (You can also test the server with a browser like http://localhost:8088/TestServer/Dummy)
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Web;
using System.Text;
using System.Threading.Tasks;
namespace SO
{
[ServiceContract]
public class TestServer
{
static WebServiceHost _host = null;
public static Task Start()
{
var tcs = new TaskCompletionSource<object>();
try
{
_host = new WebServiceHost(typeof(TestServer), new Uri("http://0.0.0.0:8088/TestServer"));
_host.Opened += (s, e) => { tcs.TrySetResult(null); };
_host.Open();
}
catch(Exception ex)
{
tcs.TrySetException(ex);
}
return tcs.Task;
}
//A method that accepts anything :)
[OperationContract, WebInvoke(Method = "*", UriTemplate ="*")]
public Message TestMethod(Stream stream )
{
var ctx = WebOperationContext.Current;
var request = ctx.IncomingRequest.UriTemplateMatch.RequestUri.ToString();
var body = new StreamReader(stream).ReadToEnd();
Console.WriteLine($"{ctx.IncomingRequest.Method} {request}{Environment.NewLine}{ctx.IncomingRequest.Headers.ToString()}BODY:{Environment.NewLine}{body}");
return ctx.CreateTextResponse( JsonConvert.SerializeObject( new { status = "OK", data= "anything" }), "application/json", Encoding.UTF8);
}
}
public class TestClient
{
public static async Task Test()
{
await TestServer.Start();
var client = new HttpClient();
var objToSend = new { name = "L", surname = "B" };
var content = new StringContent( JsonConvert.SerializeObject(objToSend) );
var response = await client.PostAsync("http://localhost:8088/TestServer/TestMethod?aaa=1&bbb=2", content);
Console.WriteLine(response.StatusCode);
Console.WriteLine(await response.Content.ReadAsStringAsync());
}
}
}