Getting HttpClient to POST/PUT Async - c#

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);

Related

HttpMessaheHandler hit 2 times instead 1 when do another HttpClient call from it

I did research and didn't find anything related to that. Please, I hope someone can help me.
What do I have?
I have HttpMessaheHandler that is inherited from DelegatingHandler
What do I do?
I'm trying to handle OAuth2 logic inside that HttpMessaheHandler
So I do another request inside HttpMessaheHandler from freshly created HttpClient
What is the problem?
The problem is that HttpResponseMessage is hit 2 times! It happens on line 70
HttpResponseMessage identityResponse = await this._client.SendAsync(requestMessage);
When this line is executed then we are coming back into HttpResponseMessage and starting this logic from the beginning.
What I have already tried?
I have tried to use ASP.NET Core native functionality services.AddHttpClient(...) the same issue.
What do I expect?
I do expect that my freshly created HttpClient does not hit this HttpMessageHandler as it is not bound to this HttpClient
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Common.Core.Abstractions;
using Common.Data;
using Common.Data.Enums;
using Common.Services.Shared;
using Hrimsoft.StringCases;
using Integrations.ECommerce.Storm.Configuration;
using Integrations.ECommerce.Storm.Models;
using Integrations.ECommerce.Storm.Models.Requests;
using Integrations.ECommerce.Storm.Services;
using Microsoft.Extensions.DependencyInjection;
using UrlCombineLib;
namespace Integrations.ECommerce.Storm
{
internal class StormIdentityHttpMessageHandler : DelegatingHandler
{
private readonly RequestOptions _requestOptions;
private readonly IContextProvider _contextProvider;
private readonly IStormIdentityService _stormIdentityService;
private readonly StormSettings _settings;
private readonly IServiceProvider _serviceProvider;
private readonly string _authorizationHeader = "Authorization";
private readonly string _applicationId = "ApplicationId";
private HttpClient _client;
private IJsonSerializer _jsonSerializer;
public StormIdentityHttpMessageHandler(
IContextProvider contextProvider,
IStormIdentityService stormIdentityService,
StormSettings settings,
IServiceProvider serviceProvider)
{
this._contextProvider = contextProvider;
this._stormIdentityService = stormIdentityService;
this._settings = settings;
this._serviceProvider = serviceProvider;
this._requestOptions = new RequestOptions { ContentType = RequestContentType.FormUrlEncoded, PropertyCaseType = PropertyNameCaseType.SnakeCase };
this._client = new HttpClient { BaseAddress = new Uri(this._settings.IdentityServerUrl) };
this._jsonSerializer = serviceProvider.GetRequiredService<IJsonSerializer>();
}
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
string culture = this._contextProvider.GetCulture();
var parameters = new StormOAuth2AuthenticationParameters
{
GrantType = this._settings.GrantType, ClientId = this._settings.ClientId, ClientSecret = this._settings.ClientSecret, Scope = this._settings.Scope
};
HttpRequestMessage requestMessage = this.Post(string.Empty, parameters, SetRequestOptions);
HttpResponseMessage identityResponse = await this._client.SendAsync(requestMessage);
StormAccessToken token = await RetrieveAccessToken(identityResponse);
// // Handle OAuth2. Requesting/Getting the access token.
// IStormIdentityService identityService = this._stormIdentityService;
// string accessToken = await identityService.RequestAccessToken();
//
// // Setting the AccessToken token to the outgoing request
// request.Headers.Add(this._applicationId, this._settings[culture].ApplicationId);
// request.Headers.Add(this._authorizationHeader, $"Bearer {accessToken}");
// HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
//
// if (response.StatusCode == HttpStatusCode.Unauthorized)
// {
// bool forceNewToken = true;
// accessToken = await identityService.RequestAccessToken(forceNewToken);
// request.Headers.Remove(this._authorizationHeader);
// request.Headers.Add(this._authorizationHeader, $"Bearer {accessToken}");
// response = await base.SendAsync(request, cancellationToken);
//
// if (response.StatusCode == HttpStatusCode.Unauthorized)
// {
// throw new ServiceException($"Can't authorize with current token. Token value:\n{accessToken}");
// }
// }
return response;
}
private async Task<StormAccessToken> RetrieveAccessToken(HttpResponseMessage httpResponse)
{
if (httpResponse.Content == null)
{
return default;
}
string content = await httpResponse.Content.ReadAsStringAsync();
if (string.IsNullOrEmpty(content))
{
return default;
}
return JsonSerializer.Deserialize<StormAccessToken>(content);
}
protected virtual async Task<Exception> OnNegativeResponse(HttpResponseMessage response)
{
HttpRequestMessage request = response.RequestMessage;
string details = null;
if (response.Content != null)
{
details = await response.Content.ReadAsStringAsync();
}
string message = $"REST request '{request.Method} - {request.RequestUri}' " +
$"error: {(int)response.StatusCode}, Message: {response.ReasonPhrase}{Environment.NewLine}{details}";
var exception = new Exception(message);
return exception;
}
protected HttpRequestMessage Post(string url = "", object data = null, Action<RequestOptions> setup = null)
=> BuildRequest(HttpMethod.Post, url, data, setup);
protected virtual string BuildResourceUrl(string baseUrl, string resource)
=> UrlCombine.Combine(baseUrl, resource);
private HttpRequestMessage BuildRequest(
HttpMethod method,
string resourceBase,
object body = null,
Action<RequestOptions> setup = null)
{
string fullUrl = BuildResourceUrl(this._client.BaseAddress.ToString(), resourceBase);
var request = new HttpRequestMessage(method, fullUrl);
if (body == null)
{
return request;
}
var options = new RequestOptions();
setup?.Invoke(options);
if (options.ContentType == RequestContentType.FormUrlEncoded)
{
Dictionary<string, string> content = GetFormContent(body, options);
request.Content = new FormUrlEncodedContent(content);
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
}
else
{
throw new Exception($"Unsupported content type {options.ContentType}");
}
return request;
}
private Dictionary<string, string> GetFormContent(object data, RequestOptions options)
{
var pairs = data
.GetType()
.GetProperties()
.Select(s => new { Name = ToRequiredCase(s.Name, options.PropertyCaseType), Value = s.GetValue(data, null)?.ToString() })
.Where(s => s.Value != null)
.ToDictionary(k => k.Name, v => v.Value);
return pairs;
string ToRequiredCase(string value, PropertyNameCaseType type)
{
string processedValue = string.Empty;
if (type == PropertyNameCaseType.CamelCase)
{
processedValue = value?.ToCamelCase();
}
else if (type == PropertyNameCaseType.SnakeCase)
{
processedValue = value?.ToSnakeCase();
}
return processedValue;
}
}
private void SetRequestOptions(RequestOptions options)
{
options.ContentType = this._requestOptions.ContentType;
options.PropertyCaseType = this._requestOptions.PropertyCaseType;
}
}
}

Http Post never returns from Twilio but Twilio action is performed

I can't use the Twilio SDK in Microsoft Dynamics 365 (Twilio library is not installed in Dynamics and can't include the dll in my plugin registration) so I've had to do a http post using the HttpClient. The call to Twilio happens successfully because Twilio is able to send me an verification email but the breakpoint after PostAsync never gets hit, nor does an exception get caught. I need to capture the output from the PostAsync. What am I doing wrong?
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
public class TwilioMessageInput
{
public string To { get; set; }
public string Channel { get; set; }
}
public class TwilioMessageOutput
{
public string Message { get; set; }
}
private void button1_Click(object sender, EventArgs e)
{
// https://www.twilio.com/docs/verify
// https://www.twilio.com/docs/verify/email
string url = "https://verify.twilio.com/v2/Services/VA********************************/Verifications/";
string authToken = "AC********************************:********************************"; //-u $TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN
string email = "***************#************.com";
var formContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("To", email),
new KeyValuePair<string, string>("Channel", "email")
});
using (var client = new Rest(url))
{
var response = client.PostAsync<TwilioMessageOutput>(url, formContent, authToken).Result;
}
}
}
public class Rest : IDisposable
{
private readonly TimeSpan _timeout;
private HttpClient _httpClient;
private HttpClientHandler _httpClientHandler;
private readonly string _baseUrl;
private const string ClientUserAgent = "twillio-client-v1";
private const string MediaTypeJson = "application/json";
public Rest(string baseUrl, TimeSpan? timeout = null)
{
_baseUrl = NormalizeBaseUrl(baseUrl);
_timeout = timeout ?? TimeSpan.FromSeconds(90);
//_timeout = TimeSpan.FromSeconds(1);
}
private async Task<string> PostAsyncInternal(string url, FormUrlEncodedContent input, string authToken)
{
try
{
EnsureHttpClientCreated();
var byteArray = Encoding.ASCII.GetBytes(authToken);
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
using (var response = await _httpClient.PostAsync(url, input))
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
catch (Exception ex)
{
throw ex;
}
}
public async Task<TResult> PostAsync<TResult>(string url, FormUrlEncodedContent input, string authToken) where TResult : class, new()
{
var strResponse = await PostAsyncInternal(url, input, authToken);
return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
}
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 + "/";
}
}
Twilio developer evangelist here.
I'm not a C# or Dynamics developer, so sorry if this doesn't help. When you make the request:
var response = client.PostAsync<TwilioMessageOutput>(url, formContent, authToken).Result;
it is an asynchronous request, but you do not seem to be waiting for the asynchronous response at all. Should that be?
var response = await client.PostAsync<TwilioMessageOutput>(url, formContent, authToken).Result;

Best way to setup MSTest for REST service with cookie-authentication?

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);
}
}
}
}

Post data with files using ASP.NET Core MVC to api (ASP.NET Core Web API)

I need to post data with files but I face this problem - all data is null.
It works when I use postman:
My post function in ASP.NET Core Web API:
public async Task<ActionResult<Category>> PostCategory([FromForm]CategoryViewModel model)
{
Category category = new Category()
{
Brief = model.Brief,
Color = model.Color,
IsDraft = model.IsDraft,
Name = model.Name,
Priority = model.Priority,
Update = DateTime.Now
};
if (model.Files != null)
{
category.IconUrl = ApplicationManager.UploadFiles(model.Files, "content/category")[0];
}
_context.Categories.Add(category);
await _context.SaveChangesAsync();
return CreatedAtAction("GetCategory", new { id = category.Id }, category);
}
My post function in ASP.NET Core MVC:
private ApiClient _client;
_client = new ApiClient(new Uri("https:localhost:55436/api/"));
public async Task<IActionResult> Create([FromForm]CategoryViewModel category)
{
var uri = new Uri(_appSettings.WebApiBaseUrl + "Categories");
var response = await _client.PostAsync<Category, CategoryViewModel>(uri, category);
return RedirectToAction("index");
}
My ApiClient class:
public partial class ApiClient
{
private readonly HttpClient _httpClient;
private Uri BaseEndpoint { get; set; }
public ApiClient(Uri baseEndpoint)
{
if (baseEndpoint == null)
{
throw new ArgumentNullException("baseEndpoint");
}
BaseEndpoint = baseEndpoint;
_httpClient = new HttpClient();
}
/// <summary>
/// Common method for making GET calls
/// </summary>
public async Task<T> GetAsync<T>(Uri requestUrl)
{
addHeaders();
var response = await _httpClient.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
var data = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(data);
}
public async Task<HttpResponseMessage> PostStreamAsync(Uri requestUrl, object content)
{
using (var client = new HttpClient())
using (var request = new HttpRequestMessage(HttpMethod.Post, requestUrl))
using (var httpContent = CreateHttpContentForStream(content))
{
request.Content = httpContent;
using (var response = await client
.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
.ConfigureAwait(false))
{
response.EnsureSuccessStatusCode();
return response;
}
}
}
public async Task<HttpResponseMessage> PostBasicAsync(Uri requestUrl, object content)
{
using (var client = new HttpClient())
using (var request = new HttpRequestMessage(HttpMethod.Post, requestUrl))
{
var json = JsonConvert.SerializeObject(content);
using (var stringContent = new StringContent(json, Encoding.UTF8, "application/json"))
{
request.Content = stringContent;
using (var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
.ConfigureAwait(false))
{
response.EnsureSuccessStatusCode();
return response;
}
}
}
}
public static void SerializeJsonIntoStream(object value, Stream stream)
{
using (var sw = new StreamWriter(stream, new UTF8Encoding(false), 1024, true))
using (var jtw = new JsonTextWriter(sw) { Formatting = Formatting.None })
{
var js = new JsonSerializer();
js.Serialize(jtw, value);
jtw.Flush();
}
}
private static HttpContent CreateHttpContentForStream<T>(T content)
{
HttpContent httpContent = null;
if (content != null)
{
var ms = new MemoryStream();
SerializeJsonIntoStream(content, ms);
ms.Seek(0, SeekOrigin.Begin);
httpContent = new StreamContent(ms);
httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
}
return httpContent;
}
/// <summary>
/// Common method for making POST calls
/// </summary>
public async Task<T> PostAsync<T>(Uri requestUrl, T content)
{
addHeaders();
var response = await _httpClient.PostAsync(requestUrl.ToString(), CreateHttpContent<T>(content));
response.EnsureSuccessStatusCode();
var data = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(data);
}
public async Task<T1> PostAsync<T1, T2>(Uri requestUrl, T2 content)
{
addHeaders();
var response = await _httpClient.PostAsync(requestUrl.ToString(), CreateHttpContent<T2>(content));
response.EnsureSuccessStatusCode();
var data = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T1>(data);
}
public Uri CreateRequestUri(string relativePath, string queryString = "")
{
var endpoint = new Uri(BaseEndpoint, relativePath);
var uriBuilder = new UriBuilder(endpoint);
uriBuilder.Query = queryString;
return uriBuilder.Uri;
}
public HttpContent CreateHttpContent<T>(T content)
{
var json = JsonConvert.SerializeObject(content, MicrosoftDateFormatSettings);
var value = new StringContent(json, Encoding.UTF8, "application/json");
return value;
}
public static JsonSerializerSettings MicrosoftDateFormatSettings
{
get
{
return new JsonSerializerSettings
{
DateFormatHandling = DateFormatHandling.MicrosoftDateFormat
};
}
}
public void addHeaders()
{
_httpClient.DefaultRequestHeaders.Remove("userIP");
_httpClient.DefaultRequestHeaders.Add("userIP", "192.168.1.1");
}
}
If you want to post the multipart/form-data using HttpClient, you should write a separate post method using MultipartFormDataContent as HttpContent type as shown:
PostCategoryAsync
public async Task<Category> PostCategoryAsync(Uri requestUrl, CategoryViewModel content)
{
addHeaders();
var response = new HttpResponseMessage();
var fileContent = new StreamContent(content.Files.OpenReadStream())
{
Headers =
{
ContentLength = content.Files.Length,
ContentType = new MediaTypeHeaderValue(content.Files.ContentType)
}
};
var formDataContent = new MultipartFormDataContent();
formDataContent.Add(fileContent, "Files", content.Files.FileName); // file
//other form inputs
formDataContent.Add(new StringContent(content.Name), "Name");
formDataContent.Add(new StringContent(content.Brief), "Brief");
formDataContent.Add(new StringContent(content.IsDraft.ToString()), "IsDraft");
formDataContent.Add(new StringContent(content.Color), "Color");
formDataContent.Add(new StringContent(content.Priority), "Priority");
response = await _httpClient.PostAsync(requestUrl.ToString(), formDataContent);
var data = await response.Content.ReadAsStringAsync();
response.EnsureSuccessStatusCode();
return JsonConvert.DeserializeObject<Category>(data);
}
MVC controller
var response = await _client.PostCategoryAsync(uri, category);
Web API

Mock returns null value when ReturnResult is a custom object but works as expected when it is a primitive type bool

I am using Moq in .net core(1.1) and having a bit of torrid time understanding this behavior as all the examples on interweb points to the fact the this should work with no issues.
I have already tried with:
Returns(Task.FromResult(...)
Returns(Task.FromResult(...)
ReturnsAsync(...)
Mocking a IHttpClient interface to wrap PostAsync, PutAsync and GetAsync. All of these return an ApiResponse object.
var mockClient = new Mock<IHttpClient>();
Does not work:
mockClient.Setup(x => x.PostAsync(url, JsonConvert.SerializeObject(body), null))
.Returns(Task.FromResult(new ApiResponse()));
PostSync definition:
public async Task<ApiResponse> PostAsync(string url, string body, string authToken = null)
Does work:
mockClient.Setup(x => x.PostAsync(url, JsonConvert.SerializeObject(body), null))
.Returns(Task.FromResult(bool));
PostSync definition:
public async Task<bool> PostAsync(string url, string body, string authToken = null)
Usage:
var api = new ApiService(mockClient.Object);
var response = api.LoginAsync(body.Username, body.Password);
UPDATE
[Fact]
public async void TestLogin()
{
var mockClient = new Mock<IHttpClient>();
mockClient.Setup(x => x.PostAsync(url, JsonConvert.SerializeObject(body), null)).Returns(Task.FromResult(new ApiResponse()));
var api = new ApiService(mockClient.Object);
var response = await api.LoginAsync(body.Username, body.Password);
Assert.IsTrue(response);
}
Return Type:
public class ApiResponse
{
public string Content { get; set; }
public HttpStatusCode StatusCode { get; set; }
public string Reason { get; set; }
}
LoginAsync:
public async Task<bool> LoginAsync(string user, string password)
{
var body = new { Username = user, Password = password };
try
{
var response = await _http.PostAsync(_login_url, JsonConvert.SerializeObject(body), null);
return response .State == 1;
}
catch (Exception ex)
{
Logger.Error(ex);
return false;
}
}
PostAsync:
public async Task<object> PostAsync(string url, string body, string authToken = null)
{
var client = new HttpClient();
var content = new StringContent(body, Encoding.UTF8, "application/json");
var response = await client.PostAsync(new Uri(url), content);
var resp = await response.Result.Content.ReadAsStringAsync();
return new ApiResponse
{
Content = resp,
StatusCode = response.Result.StatusCode,
Reason = response.Result.ReasonPhrase
};
}
Assuming a simple method under test like this based on minimal example provided above.
public class ApiService {
private IHttpClient _http;
private string _login_url;
public ApiService(IHttpClient httpClient) {
this._http = httpClient;
}
public async Task<bool> LoginAsync(string user, string password) {
var body = new { Username = user, Password = password };
try {
var response = await _http.PostAsync(_login_url, JsonConvert.SerializeObject(body), null);
return response.StatusCode == HttpStatusCode.OK;
} catch (Exception ex) {
//Logger.Error(ex);
return false;
}
}
}
The following test works when configured correctly
[Fact]
public async Task Login_Should_Return_True() { //<-- note the Task and not void
//Arrange
var mockClient = new Mock<IHttpClient>();
mockClient
.Setup(x => x.PostAsync(It.IsAny<string>(), It.IsAny<string>(), null))
.ReturnsAsync(new ApiResponse() { StatusCode = HttpStatusCode.OK });
var api = new ApiService(mockClient.Object);
//Act
var response = await api.LoginAsync("", "");
//Assert
Assert.IsTrue(response);
}
The above is just for demonstrative purposes only to show that it can work provided the test is configured properly and exercised based on the expected behavior.
Take some time and review the Moq quick start to get a better understanding of how to use the framework.

Categories