After migrating my Azure Functions project to .NET 5, it has started wrapping my responses in a weird wrapper class.
For instance, consider the following endpoint:
public record Response(string SomeValue);
[Function("Get")]
public async Task<IActionResult> Get(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "get-something")]
HttpRequestData request)
{
return new OkObjectResult(new Response("hello world"));
}
Before, it would return:
{
"someValue": "hello world"
}
But now, it returns:
{
"Value": {
"SomeValue": "hello world"
},
"Formatters": [],
"ContentTypes": [],
"DeclaredType": null,
"StatusCode": 200
}
I get that it must be because it just tries to serialize the object result, but I can't find any documentation on how this is supposed to work in .NET 5.
My main function currently looks like this:
public static async Task Main()
{
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults(x =>
x.UseDefaultWorkerMiddleware())
.ConfigureAppConfiguration((_, builder) => builder
.AddJsonFile("local.settings.json", true)
.Build())
.ConfigureServices(ConfigureServices)
.Build();
await host.RunAsync();
}
My project is located here, in case someone are interested: https://github.com/sponsorkit/sponsorkit.io
Currently, my .NET 5 work is on a branch called feature/signup-flow.
Using IActionResult with Azure Functions in .NET 5?
You can't return IActionResult with Azure Functions in .NET 5. Or more generally, you can't return IActionResult with Azure Functions using the isolated process model. Quote from the docs:
For HTTP triggers, you must use HttpRequestData and HttpResponseData to access the request and response data. This is because you don't have access to the original HTTP request and response objects when running out-of-process.
Instead of IActionResult, you need to return HttpResponseData. Example code is here.
To return an object from .NET 5 Azure Functions, use the following code:
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(obj);
return response;
Was pleasantly surprised to find that returning Task from a "dotnet-isolated" function workins in Core 6. Saved me a few hours as I do not need to change to Task in a number of functions
After searching around and wanting to stay in the context of the IActionResult object, I ended up leveraging the ObjectResult object so that I can standardize all the service responses with the standardize JSON messages.
Related
Problem is, I have an API that concatenates pdfs from Urls, and it's working in .NET 5 , but when starting to migrate to .NET 6, the use of IEnumerable<> , IFormFile, and IFormFileCollection , simply only accepts requests application/json.
Here is the endpoint in .NET 5 (Working)
[HttpPost]
public async Task<IActionResult> ConcatenarPdfsByUrl([FromForm] IEnumerable<string> urls)
{
var output = await TransformaPdfCore.PdfConcatenation(urls);
return File(output, "application/octet-stream");
}
result: Imagem 1 (.net 5)
And so is Endpoint in Minimal .net6
app.MapPost("/ConcatenaPdfsByUrl", async Task<IResult> (IEnumerable<string> urls, TransformaPdfCore transforma) =>
{
{
var output = await transforma.PdfConcatenation(urls);
return Results.File(output, "application/octet-stream");
}
}).Accepts<IEnumerable<string>>("multipart/form-data");
But the result is this: Imagem 2 (.net 6)
The question is, why does IEnumerable not have the same behavior? and if there is any solution, for example using IOperationFilter, so that I can make it work.
The IFormFileCollection Interface had the same problem
Currently binding from form is not supported for minimal APIs. You can track this issue on github - the support is investigated for .NET 7. You can try to implement custom binding from form via BindAsync method using HttpContext.Request.Form or just add HttpContext parameter to your handler method and use it there:
app.MapPost("/ConcatenaPdfsByUrl", async (HttpContext context, TransformaPdfCore transforma) =>
{
// use context.Request.Form or context.Request.Form.Files
var output = await transforma.PdfConcatenation(urls);
return Results.File(output, "application/octet-stream");
})
I've created some functions and have used this post https://www.rickvandenbosch.net/blog/azure-functions-binding-to-a-property/ to create them using custom properties
I set up the test project and files using the docs here https://learn.microsoft.com/en-us/azure/azure-functions/functions-test-a-function
My question is, how can functions that use property binding be unit tested? as all the examples I've looked at online use the test factory class to pass in a type of httpRequest.
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] GetStudyDetailsRequest req,
ILogger log)
{
try
{
var client = new ArtenaServiceClient(ArtenaServiceClient.EndpointConfiguration.BasicHttpBinding_IArtenaService);
var result = await client.GetStudyDetailsAsync(req.StudentID, req.Dev);
return CreateActionResult.Create(result);
}
catch (Exception e)
{
log.LogError(e, "", req);
return new OkObjectResult(new { e.Message, e.StackTrace });
}
}
I won't need to include Azure Function request property binding in my "unit" test scope firstly because that's not part of my code I am calling a unit here, secondly that's not my code at all :). That kind of testing might be relevant for end to end integration test where you would send http request to a running Function app. So, here in this case, I would just treat the function Run method as a regular C# method by passing required input directly from unit test.
I'm working on a project where we have to develop a web API with ASP .NET Core 3.x. So far, so good, it is running well. Now, I'm writing some integration tests for this web API and I have some trouble to get the tests for everything else than GET request to work.
We're using the Clean Architecture from Jason Taylor. This means we have a core project with all our request handler, a domain project with all database entities and a presentation project for the API controllers. We use MediatR and dependency injection for the communication between these projects.
Now, we have the problem that the body data of the reuqest doesn't reach the controller.
This is how the Update method in the controller looks like:
[ApiController]
[Route("api/[controller]/[action]")]
public abstract class BaseController : ControllerBase
{
private IMediator _mediator;
protected IMediator Mediator => _mediator ??= HttpContext.RequestServices.GetService<IMediator>();
}
public class FavoriteController : BaseController
{
[HttpPut("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Update(long id, UpdateFavoriteCommand command)
{
if (command == null || id != command.Id)
{
return BadRequest();
}
// Sends the request to the corresponding IRequestHandler
await Mediator.Send(command);
return NoContent();
}
}
We use xUnit.net as test framework.
For the integration tests, we're using an InMemory SQLite database which is setup in a fixture class.
The test looks like the following:
public class UpdateFavoritesTestSqlite : IClassFixture<WebApplicationFactoryWithInMemorySqlite<Startup>>
{
private readonly WebApplicationFactoryWithInMemorySqlite<Startup> _factory;
private readonly string _endpoint;
public UpdateFavoritesTestSqlite(WebApplicationFactoryWithInMemorySqlite<Startup> factory)
{
_factory = factory;
_endpoint = "api/Favorite/Update/";
}
[Fact]
public async Task UpdateFavoriteDetail_WithFullUpdate_ShouldUpdateCorrectly()
{
// Arange
var client = _factory.CreateClient(); // WebApplicationFactory.CreateClient()
var command = new UpdateFavoriteCommand
{
Id = 5,
Caption = "caption new",
FavoriteName = "a new name",
Public = true
};
// Convert to JSON
var jsonString = JsonConvert.SerializeObject(command);
var httpContent = new StringContent(jsonString, Encoding.UTF8, "application/json");
var stringUri = client.BaseAddress + _endpoint + command.Id;
var uri = new Uri(stringUri);
// Act
var response = await client.PutAsync(uri, httpContent);
response.EnsureSuccessStatusCode();
httpContent.Dispose();
// Assert
response.StatusCode.ShouldBe(HttpStatusCode.NoContent);
}
}
If we run the test, we get an 400 Bad Request error.
If we run the test in Debug mode, we can see that the code throws a custom ValidationException because of a model state error. This is configured in the DependencyInjection of the presentation project:
services
.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var failures = context.ModelState.Keys
.Where(k => ModelValidationState.Invalid.Equals(context.ModelState[k].ValidationState))
.ToDictionary(k => k, k => (IEnumerable<string>)context.ModelState[k].Errors.Select(e => e.ErrorMessage).ToList());
throw new ValidationException(failures);
};
})
.AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<IWebApiDbContext>());
The failures object contains one error which says:
The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. Path: $ | LineNumber: 0 | BytePositionInLine: 0.
Here is a screenshot: Visual Studio in Debugging mode with json error.
In one article in stackoverflow I've read, that removing the [ApiController] class attribute can result in a more detailed error description. During debugging again the test and setting a breakpoint int the Update method from the FavoriteController at the line with await Mediator.Send(command);, I was able to see, that the command object arriving the Update method contains only null or default values, except the id, which was 5.
command
Caption null string
FavoriteName null string
Id 5 long
Public false bool
The most confusing (and frustrating) part is, that a manual test with swagger or postman are both successfull. The way I understand it, there has to be a problem during the integration test.
I hope, someone can help me and see what I'm missing. Could it be possible that there is something wrong with the HttpClient of the Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory?
We've found the problem in our LoggingMiddleware of the web api presentation project.
Before writing this question, we already hat a look at another article on stackoverflow:
ASP.NET MVC Core 3.0 - Why API Request from body keeps returning !ModelState.IsValid?
But we already had the request.Body.Seek(0, SeekOrigin.Begin); in our code. So, we thought that's it and it couldn't be the problem.
But now, we've found this article:
.net core 3.0 logging middleware by pipereader
Instead of reading the request body like this:
await request.Body.ReadAsync(buffer, 0, buffer.Length);
... where the stream is closed after read, we're using now the BodyReader as a stream and leave the stream open:
var stream = request.BodyReader.AsStream(true); // AsStream(true) to let stream open
await stream.ReadAsync(buffer, 0, buffer.Length);
request.Body.Seek(0, SeekOrigin.Begin);
There are two applications:
Blazor server-side
Web API (written long ago)
WebApi action
public async Task<object> GetAllAsync(...)
{
...
// Какая то проверка
throw new Exception("Что то пошло не так");
...
}
An example view of a method in a client application(Blazor server-side)
public async Task GetAllAsync()
{
var httpClient = clientFactory.CreateClient();
var responseMessage = await httpClient.GetAsync($"{address}/api/foo");
if (responseMessage.IsSuccessStatusCode)
{
// successfully
}
else
{
// How to get the error message here?
}
}
}
The question is: how to properly handle this kind of error from API?
ps
var exception = await responseMessage.Content.ReadAsAsync<HttpError>();
HttpError pulls a dependency with .NetFramework 4.6 (but initially I use .net core 3 preview)
The HttpContent class does not define a ReadAsAsync method. This is an extension method which I believe is considered obsolete,and is no longer supported. You may use ReadAsStringAsync(), ReadAsStreamAsync(), etc.
Example how to serialize the exception content to string:
var exception = await responseMessage.Content.ReadAsStringAsync();
The following code snippet demonstrates how to use HttpContent.ReadAsByteArrayAsync() to serialize the HTTP content to a byte array as an asynchronous operation, and then parse the array to a type you may define to hold the http error (something similar to the HttpError object, which you should not use, but simpler).
var responseBytes = await responseMessage.Content.ReadAsByteArrayAsync();
Note: Change the type specifier T to your custom type, or use built-in types such as string, according to your design...
JsonSerializer.Parse<T>(responseBytes, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
Hope this helps
I am creating quite basic implementation of CRUD methods using Angular and ASP.NET Core in the back end.
The back-end work perfectly, but cannot get the response after redirect action.
Let me bring the code:
the C# Controller bit:
[HttpPost]
[ProducesResponseType(200, Type = typeof(IActionResult))]
public async Task<ActionResult<CapCustomerResponseDto>> CreateCustomer([FromBody] CreateCustomerRequestDto request)
{
try
{
///. .. *creating command*
await _createCustomercommandHandler.Execute(command);
return CreatedAtRoute("GetCapCustomer", new { id = command.Id.ToString() }, command);
}
catch (Exception e)
{
// Some custom handling
}
}
So at the end we have the magical CreatedAtRoute() method, which redirects to the get controller
the Angular 7 httpClient implementation
addCustomer(customer): Observable<Customer> {
return this.http.post<Customer>(apiUrl, customer, httpOptions).pipe(
tap((customer: Customer) =>
console.log(`added customer w/ id=${customer.customerId}`)
),
catchError(this.handleError<Customer>("addCustomer"))
);
}
which actually returns nothing.
My customer is added properly, I can see it in the database. When using swagger, I can see the controllers working. Even in Network monitor.
but I have no clue how to retrieve the newly created customer response into my TS classes
Instead I get this:
Error: The requested path contains undefined segment at index 1
from Console
Question.
How can I make this work?