When calling HttpClient's extension method PostAsXmlAsync, it ignores the XmlRootAttribute on the class. Is this behaviour a bug?
Test
[Serializable]
[XmlRoot("record")]
class Account
{
[XmlElement("account-id")]
public int ID { get; set }
}
var client = new HttpClient();
await client.PostAsXmlAsync(url, new Account())
Looking at the source code of PostAsXmlAsync, we can see that it uses XmlMediaTypeFormatter which internally uses DataContractSerializer and not XmlSerializer. The former doesn't respect the XmlRootAttribute:
public static Task<HttpResponseMessage> PostAsXmlAsync<T>(this HttpClient client, Uri requestUri, T value, CancellationToken cancellationToken)
{
return client.PostAsync(requestUri, value, new XmlMediaTypeFormatter(),
cancellationToken);
}
In order to achieve what you need, you can create a your own custom extension method which explicitly specifies to use XmlSerializer:
public static class HttpExtensions
{
public static Task<HttpResponseMessage> PostAsXmlWithSerializerAsync<T>(this HttpClient client, Uri requestUri, T value, CancellationToken cancellationToken)
{
return client.PostAsync(requestUri, value,
new XmlMediaTypeFormatter { UseXmlSerializer = true },
cancellationToken);
}
}
Related
I am using asp.net core in one of my projects and I am making some https requests with a client certificate. To achieve this, I created a typed http client and injected it in my startup.cs like the following:
services.AddHttpClient<IClientService, ClientService>(c =>
{
}).ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new HttpClientHandler();
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls | SslProtocols.Tls11;
handler.ClientCertificates.Add(clientCertificate);
handler.ServerCertificateCustomValidationCallback = delegate { return true; };
return handler;
}
);
My service is implemented like the following:
public class ClientService : IClientService
{
public HttpClient _httpClient { get; private set; }
private readonly string _remoteServiceBaseUrl;
private readonly IOptions<AppSettings> _settings;
public ClientService(HttpClient httpClient, IOptions<AppSettings> settings)
{
_httpClient = httpClient;
_httpClient.Timeout = TimeSpan.FromSeconds(60);
_settings = settings;
_remoteServiceBaseUrl = $"{settings.Value.ClientUrl}"; /
}
async public Task<Model> GetInfo(string id)
{
var uri = ServiceAPI.API.GetOperation(_remoteServiceBaseUrl, id);
var stream = await _httpClient.GetAsync(uri).Result.Content.ReadAsStreamAsync();
var cbor = CBORObject.Read(stream);
return JsonConvert.DeserializeObject<ModelDTO>(cbor.ToJSONString());
}
}
In my calling class, I am using this code:
public class CommandsApi
{
IClientService _clientService;
public CommandsApi( IclientService clientService)
: base(applicationService, loggerFactory)
{
_clientService = clientService;
_loggerfactory = loggerFactory;
}
public async Task<IActionResult> Post(V1.AddTransaction command)
{
var result = await _clientService.GetInfo(command.id);
}
}
It works just fine however after sending many requests I am receiving the foloowing error:
Cannot access a disposed object. Object name: 'SocketsHttpHandler'.
at System.Net.Http.SocketsHttpHandler.CheckDisposed()
at System.Net.Http.SocketsHttpHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Net.Http.DelegatingHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Net.Http.DiagnosticsHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at Microsoft.Extensions.Http.Logging.LoggingHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)
I tried some solutions that I found in previous issues (asp.net core github )and stackoverflow but they did not work.
Any idea ? Thank you
I suspect it's caused by resources not being disposed properly. There's an unnecessary call to .Result and you create a stream, but you don't dispose of it. If you use using statement, then the stream should be disposed. (you can always call stream.dispose() but I wouldn't recommend it).
var stream = await _httpClient.GetAsync(uri).Result.Content.ReadAsStreamAsync();
I've not run this, but consider:
public async Task<Model> GetInfo(string id)
{
var uri = ServiceAPI.API.GetOperation(_remoteServiceBaseUrl, id);
var response = await _httpClient.GetAsync(uri);
using (var stream = await response.Content.ReadAsStreamAsync())
{
var cbor = CBORObject.Read(stream);
return JsonConvert.DeserializeObject<ModelDTO>(cbor.ToJSONString());
}
}
So after many tests I followed #Greg advice and implemented a class inheriting from HttpClientHandler and injected it like the following:
services.AddTransient<MyHttpClientHandler>();
services.AddHttpClient<IClientService, ClientService>().
ConfigurePrimaryHttpMessageHandler<MyHttpClientHandler>();
this solved my problem.
Thank you #Greg for the link
How to use ConfigurePrimaryHttpMessageHandler generic
This is not what the original question was for, but I stumbled onto this question while searching for a similar issue.
When adding an HttpMessageHandler to the HttpClient, make sure to always return a new HttpMessageHandler instance and not re-use the same HttpMessageHandler instance.
builder.Services
.AddHttpClient<IClient>()
.ConfigurePrimaryHttpMessageHandler(() =>
{
// Always return a new instance!!
return new HttpClientHandler();
});
The document of the ConfigurePrimaryHttpMessageHandler extension method states that
The configureHandler delegate should return a new instance of the message handler each time it is invoked.
See this github answer for more information
https://github.com/dotnet/runtime/issues/27610#issuecomment-433101757
I need to Unit test RequestToken method that uses HttpClient and extension method RequestPasswordTokenAsync.
I have got a null reference exception even when a delegatingHandler is passed to the HttpClient.
var delegatingHandler = new DelegatingHandlerStub(false);
var httpClient = new HttpClient(delegatingHandler);
var tokenServices = new TokenServices(httpClient)
tokenServices.RequestToken(passwordTokenRequest); //exception
public class TokenServices : ITokenServices
{
private readonly HttpClient _httpClient;
public TokenServices(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<TokenResponse> RequestToken(PasswordTokenRequest request)
{
var response = await _httpClient.RequestPasswordTokenAsync(request);
}
}
public class DelegatingHandlerStub : DelegatingHandler
{
private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _handlerFunc;
public DelegatingHandlerStub(bool toThrowException)
{
_handlerFunc = (request, cancellationToken) =>
{
if (toThrowException)
throw new Exception();
return Task.FromResult(request.CreateResponse(HttpStatusCode.OK));
};
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return _handlerFunc(request, cancellationToken);
}
}
namespace IdentityModel.Client
{
public static class HttpClientTokenRequestExtensions
{
public static Task<TokenResponse> RequestPasswordTokenAsync(this HttpMessageInvoker client, PasswordTokenRequest request, CancellationToken cancellationToken = default(CancellationToken));
}
}
I have to write a wrapper around HttpClient.
If there are a better solution, please post it here
I am trying to change the JSON parser in my web API project.
I have followed the following tutorials:
https://learn.microsoft.com/en-us/aspnet/core/web-api/advanced/custom-formatters?view=aspnetcore-2.2
https://www.youtube.com/watch?v=tNzgXjqqIjI
https://weblog.west-wind.com/posts/2012/Mar/09/Using-an-alternate-JSON-Serializer-in-ASPNET-Web-API
I now have the following code:
public class MyJsonFormatter : MediaTypeFormatter
{
public MyJsonFormatter()
{
base.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
}
public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext, CancellationToken cancellationToken)
{
return null;
}
public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
{
return null;
}
public override void SetDefaultContentHeaders(Type type, HttpContentHeaders headers, MediaTypeHeaderValue mediaType)
{
base.SetDefaultContentHeaders(type, headers, mediaType);
}
public override bool CanReadType(Type type)
{
return true;
}
public override bool CanWriteType(Type type)
{
return true;
}
public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type, HttpRequestMessage request, MediaTypeHeaderValue mediaType)
{
return base.GetPerRequestFormatterInstance(type, request, mediaType);
}
public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
{
return null;
}
public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger, CancellationToken cancellationToken)
{
return null;
}
public override IRequiredMemberSelector RequiredMemberSelector { get => base.RequiredMemberSelector; set => base.RequiredMemberSelector = value; }
}
public static void Register(HttpConfiguration config)
{
///Other stuff...
GlobalConfiguration.Configuration.Formatters.Clear();
GlobalConfiguration.Configuration.Formatters.Insert(0, new MyJsonFormatter());
}
My issue is that whatever I do, JSON gets parsed and it seems to ignore my code - I can throw exceptions in the read or write methods and nothing will happen, break points do not get hit etc.
I know this formatter is being added as only the content types in my class are visible and if I set CanReadType to return false then nothing gets parsed.
My question is, how can I make the code execute my overrides?
Update how the formatter is registered
public static class WebApiConfig {
public static void Register(HttpConfiguration config) {
// Other stuff...
var jsonFormatter = new MyJsonFormatter();
config.Formatters.Clear();
config.Formatters.Insert(0, jsonFormatter);
//...
}
}
Making sure the suggested syntax is followed in Startup or where ever the application is started.
// configure Web Api
GlobalConfiguration.Configure(WebApiConfig.Register);
There is also the process of content negotiation as suggested in the following article
Supporting only JSON in ASP.NET Web API – the right way
Adapting it to your example, it would look like
public class JsonContentNegotiator : IContentNegotiator {
MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse("application/json;charset=utf-8");
private readonly MyJsonFormatter _jsonFormatter;
public JsonContentNegotiator(MyJsonFormatter formatter) {
_jsonFormatter = formatter;
}
public ContentNegotiationResult Negotiate(Type type, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters) {
var result = new ContentNegotiationResult(_jsonFormatter, mediaType);
return result;
}
}
And registered against your HttpConfiguration
var jsonFormatter = new MyJsonFormatter();
config.Formatters.Clear();
config.Formatters.Insert(0, jsonFormatter);
//update content negotiation
config.Services.Replace(typeof(IContentNegotiator), new JsonContentNegotiator(jsonFormatter));
Finally one piece of information to note is that the framework did tightly couple its JSON formatting to its own JsonMediaTypeFormatter
/// <summary>
/// Gets the <see cref="MediaTypeFormatter"/> to use for Json.
/// </summary>
public JsonMediaTypeFormatter JsonFormatter
{
get { return Items.OfType<JsonMediaTypeFormatter>().FirstOrDefault(); }
}
Reference source
So depending on how much of the pipeline actually depends on the existence of an instance of JsonMediaTypeFormatter, it would probably affect JSON related formatting.
If it is in fact a problem then my suggestion would be to derive from JsonMediaTypeFormatter and override its members as needed.
public class MyJsonFormatter : JsonMediaTypeFormatter {
//... removed for brevity
}
But that might bring with it, its own problems depending on what that base class is coupled to.
You need to register your formatter in the startup config.
I'm integrating a 3rd part API using TDD, and so I am implementing a HttpClient wrapper interface that exposes the possible api calls and so on.
I want to test that the correct payload was sent in a post method, but when I try to read the string content from my injected fake HttpMessageHandler I get an ObjectDisposedException. Is there a better way to test this?
Test code:
[Fact]
public async void PostSignupRequest_RequestSent_PostedSerializedRequestAsContent()
{
var client = MakeOnboardingClient();
_fakeJsonSerializer.SerializedResult = "some json";
await client.PostSignupRequest(_someSignupRequest);
Assert.Equal("some json", await _fakeMessageHandler.Request.Content.ReadAsStringAsync());
}
My HttpMessageHandler spy/test double:
public class FakeHttpMessageHandler : HttpMessageHandler
{
public HttpRequestMessage Request;
public string ResponseContent = string.Empty;
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Request = request;
return await Task.FromResult(new HttpResponseMessage
{
Content = new StringContent(ResponseContent)
});
}
}
Production code:
public async Task<SignupRequestResponse> PostSignupRequest(SignupRequest request)
{
var json = _jsonSerializer.Serialize(request);
await _httpClient.PostAsync(/* url */, new StringContent(json));
return null;
}
I've found a fix now. In my HttpMessageHandler fake I don't just save the Request now, I also explicitly save the content string (which can be extracted at that point since the HttpClient hasn't disposed the request yet). My fake now looks like this:
public class FakeHttpMessageHandler : HttpMessageHandler
{
public HttpRequestMessage Request;
public string LastRequestString = string.Empty;
public string ResponseContent = string.Empty;
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (request.Content != null) // needed this to prevent some NPEs in other tests, YMMV
{
LastRequestString = await request.Content.ReadAsStringAsync();
}
Request = request;
return await Task.FromResult(new HttpResponseMessage
{
Content = new StringContent(ResponseContent)
});
}
}
I'm implementing basic authentication using MVC5's IAuthenticationFilter interface. My understanding is that this is now the preferred approach instead of using a DelegatingHandler. I've got it working but the www-authenticate header is not being returned in the response. This is my implementation of ChallengeAsync:
public async Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
var result = await context.Result.ExecuteAsync(cancellationToken);
if (result.StatusCode == HttpStatusCode.Unauthorized)
{
result.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Basic", "realm=localhost"));
}
}
The header is returned if I set it in AuthenticateAsync but I think I'm supposed to set it in ChallengeAsync. Sample implementations have been hard to find.
In ChallengeAsync, set context.Result to an instance of type IHttpActionResult, like so.
public Task ChallengeAsync(HttpAuthenticationChallengeContext context,
CancellationToken cancellationToken)
{
context.Result = new ResultWithChallenge(context.Result);
return Task.FromResult(0);
}
Provide an implementation, like so.
public class ResultWithChallenge : IHttpActionResult
{
private readonly IHttpActionResult next;
public ResultWithChallenge(IHttpActionResult next)
{
this.next = next;
}
public async Task<HttpResponseMessage> ExecuteAsync(
CancellationToken cancellationToken)
{
var response = await next.ExecuteAsync(cancellationToken);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
response.Headers.WwwAuthenticate.Add(
new AuthenticationHeaderValue("Basic", "realm=localhost"));
}
return response;
}
}