Net 5 unit test with multiple using httpClientFactory - c#

this is a problem that I have only when trying to write a test.
In a Net 5 solution, I test GetResult() in MyController:
public class MyController : ControllerBase
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _config;
public MyController(IHttpClientFactory httpClientFactory, IConfiguration config)
{
_httpClientFactory = httpClientFactory;
_config = config;
}
public async Task<IActionResult> GetResult(int id)
{
var firstResult = await GetFirstResult(id);
var secondResult = await GetSecondResult(id);
return Ok("")
}
private async Task<int> GetFirstResult(int id)
{
using (var httpClient = _httpClientFactory.CreateClient("MySetting"))
{
var response = await httpClient.GetAsync($"MyUrl1{id}");
return (response.IsSuccessStatusCode ? 0 : 1);
}
}
private async Task<int> GetSecondResult(int id)
{
using (var httpClient = _httpClientFactory.CreateClient("MySetting"))
{
var response = await httpClient.GetAsync($"MyUrl2{id}");
return (response.IsSuccessStatusCode ? 0 : 1);
}
}
My test:
[Test]
public async Task Get_Should_Return_OK_String()
{
var httpClientFactory = new Mock<IHttpClientFactory>();
var client = new HttpClient();
client.BaseAddress = new Uri("https://sthing/server.php");
httpClientFactory.Setup(_ => _.CreateClient(It.IsAny<string>))).Returns(client);
var config = InitConfiguration();
var controller = new MyController(httpClientFactory.Object, config);
var result = await controller.GetResult(1);
Assert.NotNull(result);
}
An exception is thrown in GetSecondResult() at the line of return(response...).
Message: "Cannot access a disposed object.
Object name: 'System.Net.Http.HttpClient'."
I am aware of this case
Why is this HttpClient usage giving me an "Cannot access a disposed object." error?
but there is no use of httpClientFactory.
Is there a way to pass false to the constructor of the client through the factory?
Why would it work out of the test anyway?

First of all, .NET 5 goes out of support today. You should migrate to .NET 6, the current Long-Term-Support version. This isn't a sudden change, .NET Core's lifecycle was announced several years ago. For the most part, all you need to do is change net5.0 to net6.0 in your projects and update NuGet packages.
As for the error, it's caused by the using block :
using (var httpClient = _httpClientFactory.CreateClient("MySetting"))
This isn't needed (it's actually discouraged), HttpClient instances and sockets are pooled and recycled by the HttpClientFactory.
The test code is configured to return the same HttpClient instance every time :
var client = new HttpClient();
client.BaseAddress = new Uri("https://sthing/server.php");
httpClientFactory.Setup(_ => _.CreateClient(It.IsAny<string>)))
.Returns(client);
The call to GetFirstResult() disposes this instance and any subsequent use throws
To fix this just don't use using. HttpClient is meant to be reused anyway:
var httpClient = _httpClientFactory.CreateClient("MySetting");

Related

Timeout error in trying to access swagger.json in IHostedService through a httpclient call

I wanted to access the swagger.json file and deserialized it and store it into the cache.
I am making the async call in the IHostedService.
public class InitializeCacheService : IHostedService
{
private IMemoryCache _cache;
public static readonly string swaggerJson = "swagger.json";
private readonly HttpClient _httpClient;
private readonly IServiceProvider _serviceProvider;
public InitializeCacheService(IMemoryCache cache, HttpClient client, IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_httpClient = client;
_cache = cache;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
if (!_cache.TryGetValue(swaggerJson, out SwaggerDocumentCache cacheEntry))
{
_httpClient.BaseAddress = new Uri("https://localhost:44397");
var response = await _httpClient.GetAsync("swagger/v1/swagger.json");
response.EnsureSuccessStatusCode();
using var responseStream = await response.Content.ReadAsStreamAsync();
cacheEntry = await JsonSerializer.DeserializeAsync
<SwaggerDocumentCache>(responseStream);
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromSeconds(3));
_cache.Set(swaggerJson, cacheEntry, cacheEntryOptions);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
But the application is spinning and spinning and it is getting time out in
var response = await _httpClient.GetAsync("swagger/v1/swagger.json");
The error it is giving is:
IOException: Unable to read data from the transport connection: The I/O operation has been aborted because of either a thread exit or an application request.
What I am missing here? What is the correct way of accessing the swagger.json to build a custom API playground application?
SwaggerMiddleware handles serving OpenAPI documents. We can use that as reference to build the document ourselves.
First register SwaggerGenerator with DI:
// Startup.Configure
services.AddTransient<SwaggerGenerator>();
Then inject it inside a class, here I'm using an endpoint to serve it directly:
// Startup.Configure
app.UseEndpoints(e =>
{
// ...
e.MapGet("/openapi.json", context =>
{
// inject SwaggerGenerator
var swaggerGenerator = context.RequestServices.GetRequiredService<SwaggerGenerator>();
// document name is defined in Startup.ConfigureServices method inside the AddSwaggerGen call
var doc = swaggerGenerator.GetSwagger("v1");
// serialize the document as json
using var writer = new StringWriter(CultureInfo.InvariantCulture);
var serializer = new OpenApiJsonWriter(writer);
doc.SerializeAsV3(serializer);
var json = writer.ToString(); // this is the openapi document
// serve it as json
context.Response.ContentType = MediaTypeNames.Application.Json;
return context.Response.WriteAsync(json, new UTF8Encoding(false));
});
});
I believe the actual JSON is not necessarily a valid endpoint which may be a problem in an of itself. Have you attempted the call without the explicit file name on the endpoint route?

Why second HttpClient.PostAsync returns 500?

In the following code I receive 200 in the first request and 500 for all after that. I am new to .Net Core C# so I learned I should assign certain HttpClient attributes only once i.e. timeout.
However, my automated tests are successful first time but 500 after that. I am using HttpClientFactory.
if (httpClient.Timeout == null)
{
httpClient.Timeout = TimeSpan.FromSeconds(Foo.timeout);
}
if (httpClient.BaseAddress == null) {
httpClient.BaseAddress = new Uri(Foo.url);
}
response = await httpClient.PostAsync("", content);
I found the issue with the code. I should have created a new instance of the HttpClient using the HttpClientFactory for every Http request:
_httpClient = _httpClientFactory.CreateClient(command.ClientId);
public class Foo
{
private readonly IHttpClientFactory _httpClientFactory;
public Foo(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<HttpRequestMessage> Bar()
{
var httpClient = _httpClientFactory.CreateClient("Foo");
using var response = await httpClient.PostAsync("example.com", new StringContent("Foo"));
return response.RequestMessage;
// here as there is no more reference to the _httpclient, the garbage
//collector will clean
// up the _httpclient and release that instance. Next time the method is
//called a new
// instance of the _httpclient is created
}
}

Blazor WebAssembly blocks WebApi AllowAnonymous

I have created an Blazor WebAssembly project and want to provide a WebAPI with one public available function.
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class SystemEvalApiController : ControllerBase
{
public SystemEvalApiController(AppDbContext context, IMapper mapper)
{...}
[Route("LatestEvals")]
[AllowAnonymous]
public ActionResult LatestEvals()
that is my Api controller and I should be able to call it with:
SystemEvalPublicViewModel = await Http
.GetFromJsonAsync<SystemEvalPublicViewModel>(
HttpService.BuildUrl("api/SystemEvalApi/LatestEvals"));
When i am not logged into any account. But instead I get this error:
info: System.Net.Http.HttpClient.JPB.BorannRemapping.ServerAPI.LogicalHandler[100]
Start processing HTTP request GET https://localhost:44330/api/SystemEvalApi/LatestEvals
blazor.webassembly.js:1 info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed.
It looks like the "DefaultAuthorizationService" does not recognize the Anonymous attribute but I cannot find the point where it fails directly.
How do I declare an WebAPI function to be accessable from the HttpClient without Login.
Microsoft.AspNetCore.Components.WebAssembly.Server 3.2.0.-rc1.20223.4
Edit:
Here is the declaration for ClientServices:
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddHttpClient("JPB.BorannRemapping.ServerAPI", client =>
{
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
})
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
// Supply HttpClient instances that include access tokens when making requests to the server project
builder.Services.AddTransient(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("JPB.BorannRemapping.ServerAPI"));
builder.Services.AddTransient(e => new HttpService(e.GetService<HttpClient>()));
builder.Services.AddApiAuthorization();
builder.Services.AddBlazoredLocalStorage();
await builder.Build().RunAsync();
So each time you acquire an HttpClient it use the BaseAddressAuthorizationMessageHandler which try to authentify the request. But it this case your request should not be authentified, so you can make something like :
Registration
builder.Services.AddHttpClient("JPB.BorannRemapping.ServerAPI.Anonymous", client =>
{
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
});
Usage
#inject IHttpClientFactory _factory
#code {
...
var httpClient = _factory.CreateClient("JPB.BorannRemapping.ServerAPI.Anonymous");
var httpService = new HttpService(httpClient);
SystemEvalPublicViewModel = await httpClient
.GetFromJsonAsync<SystemEvalPublicViewModel>(
httpService.BuildUrl("api/SystemEvalApi/LatestEvals"));
}
Building on the answer from #agua from mars.
Registration in Program.cs
You could add 2 named HttpClient to the services collection (the first for authenticated calls the second for anonymous):
builder.Services.AddHttpClient("YourProject.ServerAPI",
client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
builder.Services.AddHttpClient("YourProject.ServerAPI.Anonymous",
client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));
// Supply HttpClient instances that include access tokens when making requests to the server project
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("YourProject.ServerAPI"));
//Register a new service for getting an Anonymous HttpClient
builder.Services.AddScoped<IHttpAnonymousClientFactory, HttpAnonymousClientFactory>();
Add new Interface and Implementation for Dependency Injection:
public interface IHttpAnonymousClientFactory
{
HttpClient HttpClient { get; }
}
public class HttpAnonymousClientFactory : IHttpAnonymousClientFactory
{
private readonly IHttpClientFactory httpClientFactory;
public HttpAnonymousClientFactory(IHttpClientFactory httpClientFactory)
{
this.httpClientFactory = httpClientFactory;
}
public HttpClient HttpClient => httpClientFactory.CreateClient("YourProject.ServerAPI.Anonymous");
}
Usage in Razor Component (for Anonymous HttpClient)
[Inject]
private IHttpAnonymousClientFactory httpAnonymousClientFactory { get; set; }
private MyViewModel myModel;
protected override async Task OnInitializedAsync()
{
myModel = await httpAnonymousClientFactory.HttpClient.GetFromJsonAsync<MyViewModel>($"api/mycontroller/myendpoint");
}
Usage in Razor Component (for Authenticated HttpClient)
[Inject]
private HttpClient httpClient { get; set; }
private MyOtherViewModel myModel;
protected override async Task OnInitializedAsync()
{
myModel = await httpClient.GetFromJsonAsync<MyOtherViewModel>($"api/mycontroller/mysecureendpoint");
}

How do I test the integration between the Global error handling middleware and my controller in ASP.Net Core?

I'm trying to write some test with XUnit, specifically I'd like to have a test that ensures that when a certain exception is thrown it gets remapped into a meaningful error code.
I already set up the Global error handling middleware and it works correctly.
Here there is some example code of how my solution works:
My controller with a post endpoint that can return 200 or 404
//Controller
[HttpPost]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<StatusCodeResult> Create([FromBody] Request request) {
//Process request
handler.Handle(request);
return Ok();
}
The Middleware for the Global error handling that remaps exceptions into Error codes
//StartUp Middleware
app.UseExceptionHandler(builder => {
builder.Run(handler: async context => {
IExceptionHandlerFeature error = context.Features.Get<IExceptionHandlerFeature>();
if (error != null) {
int statusCode = (int)GetStatusCodeForException(error.Error);
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(new ErrorDetails { StatusCode = statusCode, Message = error.Error.Message }.ToString());
}
});
});
And then my test in where I arrange some mocks, instantiate the controller and call the Create method
//UnitTest
[Fact]
public async Task Test()
{
//Arrange
var mockHandler = new Mock<IHandler>();
mockHandler.Setup(handler => handler.Handle(It.IsAny<Request>())).Throws(new CustomException(It.IsAny<string>()));
MyController myController = new MyController();
//Act
var statusCodeResult = await myController.Create(request);
//Assert
StatusCodeResult result = Assert.IsType<NotFoundResult>(statusCodeResult);
}
Here I want to ensure that the CustomException is remapped into a 404 status code. How do I do it? Any help is appreciated.
In your test the middleware is not available. You need to spin up a hosting environment to do that, the package Microsoft.AspNetCore.TestHost provides you with one that you can use for testing:
[Fact]
public async Task Test1()
{
using var host = new TestServer(Program.CreateHostBuilder(null));
var client = host.CreateClient();
var requestMessage = new HttpRequestMessage(HttpMethod.Get, "/api/controller");
var result = await client.SendAsync(requestMessage);
var status = result.StatusCode;
// TODO: assertions
}
Now when you call your API in a way an exception is thrown, the middleware should be executed and covered.
You can use the WebApplicationFactory class from the Microsoft.AspNetCore.Mvc.Testing nuget package. This bootstraps your application in-memory to allow for end to end functional tests. Rather than calling individual action methods you can make calls via HttpClient to ensure all middleware etc is called.
You use the Startup class that you have already defined in your main entry project and can add mock/fake objects to the IoC container as required. This allows you to verify/setup any dependencies.
You can then inject this as an IClassFixture. Once injected calling .CreateClient() on the instance will return an HttpClient, through which you can make requests.
Here is an example implementation:
// app factory instance
public class TestAppFactory : WebApplicationFactory<Startup>
{
// mock for setup/verify
public Mock<IHandler> MockHandler { get; } = new Mock<IHandler>();
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
services.AddSingleton(MockHandler);
});
}
}
public class MyTestClass : IClassFixture<TestAppFactory>
{
private readonly TestAppFactory _factory;
private readonly HttpClient _client;
private readonly Mock<IHandler> _mockHandler;
public MyTestClass(TestAppFactory factory)
{
_factory = factory;
_client = factory.CreateClient();
_mockHandler = factory.MockHandler;
}
[Fact]
public async Task Test()
{
// make calls via _client
}
}

Need Guidance for Intregration Testing with the new .Net Core 3.0 grpc service

I am playing around with the new GRPC Service for .Net Core 3.0 template. I have successfully created the Hello World service. I am now trying to add a test project to this solution.
I can see how to make an in-memory server for integration testing for a regular HTTP Client/Server .Net Core service. But I can find no example or guidance anywhere on the net for when using grpc protodef client/server. The issue appears to be with the CreateClient() method which returns a plain old HTTP client, and not a grpc one.
It would be good if a barebones integration test was included with the project created by the VS 2019 template. The main reason for me to look at Microservices and in particular using grpc was the idea that I can have small blocks that are easily testable with an automated testing framework. But I seem to have fallen at the first hurdle.
[Added]
Below is the simple test class. It uses NUnit. The issue I have is finding a way to run the in-memory TestHost, the same way as I do for a .Net Core API or MVC app to allow integration testing.
namespace Project.Tests
{
public class Tests
{
private Greeter.GreeterClient _client;
private TestServer _server;
[OneTimeSetUp]
public void OneTimeSetup()
{
var server = new TestServer(WebHost.CreateDefaultBuilder()
.UseStartup<TestStartup>()
.UseEnvironment("Development")
.UseUrls("https://localhost:5001"));
}
[SetUp]
public void Setup()
{
var channel = GrpcChannel.ForAddress("https://localhost:5001");
_client = new Greeter.GreeterClient(channel);
}
[Test]
public async Task Test1()
{
var reply = await _client.SayHelloAsync(new HelloRequest { Name = "GreeterClient" });
Assert.IsTrue(reply.Message == "Hello GreeterClient");
}
}
}
Could you please try to pass an HTTPClient created by test server into GRPC channel?
It works when I using WebApplicationFactory, haven't tested for a test server before.
var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions
{
HttpClient = _server.CreateClient()
});
var client = new Greeter.GreeterClient(channel);
var response await client.SayHelloAsync(new HelloRequest());
// Assert anything you want
.net 5 integration test:
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Net.Client;
using Microsoft.AspNetCore.Mvc.Testing;
using WebApplication.Greeter;
using Xunit;
namespace IntegrationTests.GreeterDemo
{
public class GreeterServiceTest : IClassFixture<WebApplicationFactory<WebApplication.Startup>>, IDisposable
{
static readonly Marshaller<HelloRequest> __Marshaller_HelloRequest = Marshallers.Create((arg) => global::Google.Protobuf.MessageExtensions.ToByteArray(arg), HelloRequest.Parser.ParseFrom);
static readonly Marshaller<HelloReply> __Marshaller_HelloReply = Marshallers.Create((arg) => global::Google.Protobuf.MessageExtensions.ToByteArray(arg), HelloReply.Parser.ParseFrom);
private readonly WebApplicationFactory<WebApplication.Startup> _factory;
static Method<HelloRequest, HelloReply> __Method_SayHello = new Method<HelloRequest, HelloReply>(
MethodType.Unary,
"Greeter",
"SayHello",
__Marshaller_HelloRequest,
__Marshaller_HelloReply);
public GreeterServiceTest(WebApplicationFactory<WebApplication.Startup> factory)
{
_factory = factory;
}
[Fact]
public async Task test1Async()
{
var client = _factory.CreateClient();
GrpcChannel channel = GrpcChannel.ForAddress(client.BaseAddress, new GrpcChannelOptions
{
HttpClient = client
});
CallInvoker callInvoker = channel.CreateCallInvoker();
HelloRequest request = new()
{
Name = "Stackoverflow Developer"
};
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken cancellationToken = cts.Token;
CallOptions callOptions = new CallOptions(null, DateTime.MaxValue, cancellationToken);
HelloReply reply = await callInvoker.AsyncUnaryCall<HelloRequest, HelloReply>(__Method_SayHello, null, callOptions, request);
Debug.Print(reply.Message);
}
public void Dispose()
{
_factory.Dispose();
}
}
I found a pretty simple solution, this is my test class:
public class TestClass:IClassFixture<WebApplicationFactory<Startup>>
{
readonly WebApplicationFactory<Startup> factory;
//Grpc client class, generated from proto
readonly GrpcServices.IntegrationService.IntegrationServiceClient client;
static GrpcChannel CreateHttpsChannel(string webPath, HttpMessageHandler messageHandler)
{
var handler = new GrpcWebHandler(GrpcWebMode.GrpcWebText, messageHandler);
return GrpcChannel.ForAddress(webPath, new GrpcChannelOptions { HttpClient = new HttpClient(handler) });
}
//Startup is startup class from your web app
public TestClass (WebApplicationFactory<Startup> factory)
{
this.factory = factory;
var handler = this.factory.Server.CreateHandler();
var channel = GrpcConnectionFactory.CreateHttpsChannel(this.factory.Server.BaseAddress.ToString(), handler);
client = new GrpcServices.IntegrationService.IntegrationServiceClient(channel);
}
[Fact]
public void TestMethod ()
{
var response = client.Run(new GrpcServices.ServiceRequest { Name = "Name" }); //set your request paramaters
Assert.IsTrue(string.IsNullOrEmpty(response.ErrorMessage));
}
}
Offcourse you have to install xunit and xunit.runner.visualstudio nuget packages.

Categories

Resources