Ok i have a wasm App that call the server side web api endpoints.
The problem is that I get the index.html page from the /wwwroot directory for an endpoint as an answer. But when I address the endpoint with Postman I get the json answer as expected.
Okay i will show how i do this with my code.
Client Side data flow
Search.razor Page
Here I call the Web API endpoint when a search text has been entered in the form field. That works as expected.
... Snip
// UPDATED INFO
<div class="form-group">
<label for="objectType">Select object type</label>
<select id="objectType" class="form-control" #bind="#_searchByNameObjectTypeUuid">
#if (_objectTypes != null)
{
#foreach (var objectType in _objectTypes)
{
#if (objectType.TypeName == "Music")
{
#* This selection value is not set. But why?
<option value="#objectType.Uuid.ToString("D")" selected="selected">#objectType.TypeName</option>
}
else
{
<option value="#objectType.Uuid.ToString("D")">#objectType.TypeName</option>
}
}
}
</select>
</div>
// UPDATED INFO END
<div class="form-group">
<label for="objectName">Object name:<br/></label>
<input type="text" class="form-control" id="objectName" #onkeydown="#SearchByNameOnEnter" #bind-value="#_searchByNameObjectNamePhrase" #bind-value:event="oninput"/>
</div>
...Snip
#code {
private string _searchByNameObjectNamePhrase = string.Empty;
private async Task SearchByNameOnEnter(KeyboardEventArgs e)
{
if ((e.Code == "Enter" || e.Code == "NumpadEnter") && !string.IsNullOrWhiteSpace(_searchByNameObjectNamePhrase))
{
_searchResult = await ServerApiClient.SearchObjectsByNamePhrase(_searchByNameObjectTypeUuid, _searchByNameObjectNamePhrase);
}
}
}
ServerApiClientService.cs Web API Client service
With this I call different Web API endpoints that get the data from a database in the backend.
The GetDdsObjectAttributeValueCount() method works as expected.
The method SearchObjectsByNamePhrase(string objTypeUuid, string searchTermPhrase) sends me the file /wwwroot/index.html as an answer. (Show comments in code for details)
namespace WebAssembly.Client.Services
{
public class ServerApiClientService : IServerApiClientService
{
#region Constants - Static fields - Fields
private readonly HttpClient _httpClient;
#endregion
#region Constructors and Destructors
public ServerApiClientService(HttpClient httpClient)
{
_httpClient = httpClient;
}
#endregion
#region Methods
// This endpoint request work as expected
public async Task<IEnumerable<ObjectAttributeValueCount>> GetDdsObjectAttributeValueCount()
{
IEnumerable<ObjectAttributeValueCount> result =
await _httpClient
.GetFromJsonAsync<List<ObjectAttributeValueCount>>("/api/DdsDashboard/GetDdsObjectAttributeValueCount");
return (result ?? Array.Empty<ObjectAttributeValueCount>()).AsQueryable();
}
// This endpoint request NOT work as expected
public async Task<IEnumerable<SearchResultItem>> SearchObjectsByNamePhrase(string objTypeUuid, string searchTermPhrase)
{
// For test i have called as string and i get HTML response. wwwroot/index.html is comming back.
var asJsonString =
await _httpClient
.GetStringAsync($"/api/DdsDashboard/SearchObjectsByNamePhrase/{objTypeUuid}/{searchTermPhrase}");
// And here i get the exception "System.Text.Json.JsonReaderException"
// '<' is an invalid start of a value
IEnumerable<SearchResultItem> result =
await _httpClient
.GetFromJsonAsync<List<SearchResultItem>>($"/api/DdsDashboard/SearchObjectsByNamePhrase/{objTypeUuid}/{searchTermPhrase}");
return (result ?? Array.Empty<SearchResultItem>()).AsQueryable();
}
#endregion
}
}
Server Side data flow
DdsDashboardController.cs as the Web API Controller
All methods (routes) in this controller work perfectly when I address them with Postman.
The route [HttpGet("GetDdsObjectAttributeValueCount")] and the route [HttpGet("GetDdsObjectTypeStatistic")] also work with the ServerApiClientService.cs.
Only the route [HttpGet ("SearchObjectsByNamePhrase / {objTypeId} / {searchTerm}")] only works in Postman.
namespace WebAssembly.Server.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class DdsDashboardController : ControllerBase
{
#region Constants - Static fields - Fields
private readonly IDdsRepository _repository;
#endregion
#region Constructors and Destructors
public DdsDashboardController(IDdsRepository repo)
{
_repository = repo;
}
#endregion
#region Methods
[HttpGet("GetDdsObjectAttributeValueCount")]
public async Task<IEnumerable<ObjectAttributeValueCount>> GetDdsObjectAttributeValueCount()
{
return await _repository.GetDdsObjectAttributeValueCount();
}
[HttpGet("GetDdsObjectTypeStatistic")]
public async Task<IEnumerable<ObjectTypeStatistic>> GetDdsObjectTypeStatistic()
{
return await _repository.GetDdsObjectTypeStatistic();
}
// This method is called and worked as expected. When i call this endpoint with Postman all is fine. Correct JSON response.
[HttpGet("SearchObjectsByNamePhrase/{objTypeId}/{searchTerm}")]
public async Task<IEnumerable<SearchResultItem>> SearchObjectsByNamePhrase(string objTypeId, string searchTerm)
{
// Correct result from my database. I have checked with an breakpoint.
var result = await _repository.SearchObjectsByNamePhrase(objTypeId, searchTerm);
return result;
}
#endregion
}
}
Startup.cs
Configure method
public void Configure(IApplicationBuilder app)
{
if (Env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
app.UseHttpsRedirection();
}
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});
}
ConfigureServices method
public void ConfigureServices(IServiceCollection services)
{
SqlMapper.AddTypeHandler(new MySqlGuidTypeHandler());
SqlMapper.RemoveTypeMap(typeof(Guid));
SqlMapper.RemoveTypeMap(typeof(Guid?));
services.AddControllersWithViews();
services.AddRazorPages();
services.AddScoped<IDdsRepository, DdsRepository>();
var dbConnectionSettings = new DdsDbConnectionConfiguration(Configuration.GetSection("DdsDbSettings"));
services.AddSingleton(dbConnectionSettings);
if (!Env.IsDevelopment())
{
services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect;
options.HttpsPort = 443;
});
}
}
Request with Postman
I hope I have given enough information to be able to give me an indication of why this not work.
Update
Ok. The problem is that the binding for the form control worked only when i make manualy a selection change. Set a selected while rendering is not working.
<div class="form-group">
<label for="objectType">Select object type</label>
<select id="objectType" class="form-control" #bind="#_searchByNameObjectTypeUuid">
#if (_objectTypes != null)
{
#foreach (var objectType in _objectTypes)
{
#if (objectType.TypeName == "Music")
{
<option value="#objectType.Uuid.ToString("D")" selected="selected">#objectType.TypeName</option>
}
else
{
<option value="#objectType.Uuid.ToString("D")">#objectType.TypeName</option>
}
}
}
</select>
</div>
And that's why the _searchByNameObjectTypeUuid value is not set.
And with that endpoints.MapFallbackToFile(" index.html ").
I have set the value of _searchByNameObjectTypeUuid in the OnInitializedAsync() method where also i load the _objectTypes.
protected override async Task OnInitializedAsync()
{
_objectTypes = await DdsApiClient.GetObjectTypes();
_searchByNameObjectTypeUuid = _objectTypes.SingleOrDefault(x => x.TypeName == "Music")?.Uuid.ToString("D");
}
If anyone knows how to set the value with the foreach loop while rendering, I would be grateful to hear about it.
Thanks to #Neil W for help.
I don't have an answer for your question directly, but when I first started encountering challenges with WebAPI from Blazor wasm client, I create a client API base class, thus:
public abstract class ClientAPI
{
protected readonly HttpClient Http;
private readonly string BaseRoute;
protected ClientAPI(string baseRoute, HttpClient http)
{
BaseRoute = baseRoute;
Http = http;
}
protected async Task<TReturn> GetAsync<TReturn>(string relativeUri)
=> await ProcessHttpResponse<TReturn>(await Http.GetAsync($"{BaseRoute}/{relativeUri}"));
protected async Task<TReturn> PostAsync<TReturn, TRequest>(string relativeUri, TRequest request)
=> await ProcessHttpResponse<TReturn>(await Http.PostAsJsonAsync($"{BaseRoute}/{relativeUri}", request));
private static async Task<TReturn> ProcessHttpResponse<TReturn>(HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
return await response.Content.ReadFromJsonAsync<TReturn>();
string msg = await response.Content.ReadAsStringAsync();
Console.WriteLine(msg);
throw new Exception(msg);
}
}
Then my derived Client API class would call GetAsync on the base class. That will then either resolve the Json response or if the HttpResponseMessage had a failure status code, it would log the error.
Use from derived class like this:
public class BackOfficeClientAPI : ClientAPI
{
public BackOfficeClientAPI(HttpClient http) : base("api/backoffice", http) { }
public async Task<IEnumerable<Category>> GetCategoriesAsync(Guid organisationId)
=> await GetAsync<IEnumerable<Category>>($"getcategories?organisationId={organisationId}");
public async Task<Category> AddCategoryAsync(AddCategoryRequest request)
=> await PostAsync<Category, AddCategoryRequest>("addcategory", request);
PS. I'm using querystring instead of route parameters, but the principle is the same.
I've found it a nice pattern to catch exceptions of this type.
Related
I am working on a project with blazor server and I am trying to make a voice call using Twilio.
I followed the documentation online from twilio for this to the letter but getting this error:
System.Net.Http.HttpRequestException: Response status code does not indicate success: 404 (Not Found).
at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
at QUBeMyGuest.Pages.GuestArrivals.EmergencyContact.GetClientToken() in C:\Pages\GuestArrivals\EmergencyContact.razor:line 98
at QUBeMyGuest.Pages.GuestArrivals.EmergencyContact.OnAfterRenderAsync(Boolean firstRender) in C:\Users\\Pages\GuestArrivals\EmergencyContact.razor:line 58
at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle)
The documentation stated that I should open a new asp.net core api project within my solution and add this class to the controller folder:
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Twilio.Jwt;
using Twilio.Jwt.Client;
using Twilio.TwiML;
using Twilio.Types;
using System.Net.Http;
namespace api.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TwilioBackEndController : ControllerBase
{
public readonly string AccountSid = "xxxxxxxxxx";
public readonly string AuthToken = "xxxxxxxx";
public readonly string AppSid = "xxxxxxxx";
public readonly string PhoneNumber = "xxxxxxxx";
[HttpGet("token")]
public async Task<IActionResult> GetToken()
{
var scopes = new HashSet<IScope>
{
new OutgoingClientScope(AppSid),
new IncomingClientScope("tester")
};
var capability = new ClientCapability(AccountSid, AuthToken, scopes: scopes);
return await Task.FromResult(Content(capability.ToJwt(), "application/jwt"));
}
[HttpPost("voice")]
public async Task<IActionResult> PostVoiceRequest([FromForm] string phone)
{
var destination = !phone.StartsWith('+') ? $"+{phone}" : phone;
var response = new VoiceResponse();
var dial = new Twilio.TwiML.Voice.Dial
{
CallerId = PhoneNumber
};
dial.Number(new PhoneNumber(destination));
response.Append(dial);
return await Task.FromResult(Content(response.ToString(), "application/xml"));
}
}
}
I then set up the page to make the call with my blazor project
#page "/guest/emergencycall"
#using System.ComponentModel.DataAnnotations
#inject HttpClient httpClient
#using Microsoft.Extensions.DependencyInjection
#using System.Net.Http
<EditForm Model="Input" OnValidSubmit="InitiatePhoneCall">
<DataAnnotationsValidator />
<ValidationSummary />
<p>
<label for="phoneNumber">Enter Phone Number:</label>
<InputText id="phoneNumber" #bind-Value="Input.PhoneNumber"></InputText>
<button type="submit" class="btn btn-primary" disabled="#IsDialDisabled">DIAL</button>
<button type="button" id="endBtn" class="btn btn-primary" disabled="#IsEndDisabled" #onclick="EndPhoneCall">END</button>
<button type="button" id="clearBtn" class="btn btn-primary" disabled="#IsClearDisabled" #onclick="ClearPhoneNumber">CLEAR</button>
</p>
</EditForm>
<hr />
#if (Logs.Count == 0)
{
<p>No Logs available yet</p>
}
else
{
<ul>
#foreach (var log in Logs)
{
<li>#log</li>
}
</ul>
}
#code {
private string _tokenUrl = "https://xxxxxxxxxxxxxxx";
private bool appSetupRun = false;
protected bool IsDialDisabled { get; set; } = false;
protected bool IsEndDisabled { get { return !IsDialDisabled; } }
protected bool IsClearDisabled { get { return string.IsNullOrEmpty(Input.PhoneNumber); } }
protected List<string> Logs { get; set; } = new List<string>();
protected InputModel Input { get; set; } = new InputModel();
[Inject]
protected IJSRuntime JSRuntime { get; set; }
[Inject]
protected IHttpClientFactory HttpClientFactory { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && !appSetupRun)
{
var token = await GetClientToken();
await JSRuntime.InvokeVoidAsync("appFunctions.setup", token);
appSetupRun = true;
}
}
protected async Task InitiatePhoneCall()
{
IsDialDisabled = true;
await LogMessage($"Calling the number {Input.PhoneNumber}");
await JSRuntime.InvokeVoidAsync("appFunctions.placeCall", Input.PhoneNumber);
await LogMessage($"Called the number {Input.PhoneNumber}");
StateHasChanged();
}
protected async Task EndPhoneCall()
{
IsDialDisabled = false;
await LogMessage($"Ending the call to {Input.PhoneNumber}");
await JSRuntime.InvokeVoidAsync("appFunctions.endCall");
await LogMessage($"Ended the call to {Input.PhoneNumber}");
StateHasChanged();
}
protected async Task ClearPhoneNumber()
{
await LogMessage("Clearing the phone number entry");
Input.PhoneNumber = string.Empty;
await LogMessage("Cleared the phone number entry");
StateHasChanged();
}
private async Task<string> GetClientToken()
{
var uri = new Uri(_tokenUrl);
using var client = HttpClientFactory.CreateClient();
var response = await client.GetAsync(uri);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
[JSInvokable]
public async Task LogMessage(string message)
{
Logs.Add($"{DateTimeOffset.Now} - {message}");
await Task.CompletedTask;
}
public class InputModel
{
[Required]
[Phone(ErrorMessage = "Please enter your phone number in a proper format")]
public string PhoneNumber { get; set; }
}
}
and then added this JS function:
window.appFunctions = {
setup: function (token) {
console.log('Getting connected');
// Setup Twilio Device
Twilio.Device.setup(token);
Twilio.Device.ready(() => {
console.log('We are connected and ready to do the thing');
});
Twilio.Device.error((err) => {
console.error('This should not have been reached. We need to do something here');
console.error(err);
});
},
placeCall: function (destination) {
console.log(`Calling ${destination}`);
Twilio.Device.connect({ phone: destination });
console.log(`Successfully called ${destination}`);
},
endCall: function () {
console.log('Ending the call');
Twilio.Device.disconnectAll();
console.log('Successfully ended the call');
}
};
in my startup file I then added this, but doesn't seem to have made a difference:
services.AddHttpClient();
if (!services.Any(x => x.ServiceType == typeof(HttpClient)))
{
services.AddScoped<HttpClient>(s =>
{
var uriHelper = s.GetRequiredService<NavigationManager>();
return new HttpClient
{
BaseAddress = new Uri(uriHelper.BaseUri)
};
});
}
Any suggestions on what I could be doing wrong?
When seeing a 404, the first thing that should come to mind is an incorrect URL. Since the 404 is coming from GetClientToken in your razor file. That means your _tokenUrl is probably incorrect. Looking at your controller, your URL should look something like https://{host}/token/. It's possible that you hardcoded the link in the documentation, meanwhile, it's supposed to be based on your controller.
PS: HTTP 404 means that the requested resource cannot be found. Get familiar with other HTTP response codes too, might save you a ton of debugging.
In my Blazor WASM application, I have written a (client-side) service class with a method to make an API call to the web API. The server will return either the expected result of IEnumerable<WeatherForecast> or a Microsoft.AspNetCore.Mvc.ProblemDetails object explaining what went wrong.
When calling the method, the UI (FetchData.razor) passes an Action<IEnumerable<WeatherForecast>> and an Action<ProblemDetails>. Only one of these actions should ever be executed, depending on what is returned by the server. This allows the service class to choose what to do based on the deserialized JSON result of the API call.
Usage (in FetchData.razor):
#page "/fetchdata"
#using BlazorApp1.Shared
#inject HttpClient Http
#inject WeatherForecastsService Service
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
#if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
#foreach (var forecast in forecasts)
{
<tr>
<td>#forecast.Date.ToShortDateString()</td>
<td>#forecast.TemperatureC</td>
<td>#forecast.TemperatureF</td>
<td>#forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
#code {
private IEnumerable<WeatherForecast> forecasts;
protected override async Task OnInitializedAsync()
{
await Service.GetAllAsync(
success => forecasts = success,
problem => Console.WriteLine("Handle this problem: " + problem.Detail));
}
}
My attempt at implementation, below, does not work. I am sure that the API call is reaching the correct API endpoint and getting JSON back, but my razor page is not getting populated with the WeatherForecasts and it is not writing the problem detail to the console either. Debugging in Blazor WASM (though much improved) is still quite difficult.
I have been fiddling with this code for days but have failed. Can anybody help me see what I am doing wrong please?
public class WeatherForecastsService : ServiceBase
{
public WeatherForecastsService(
HttpClient client) : base(client)
{
}
public async Task GetAllAsync(
Action<IEnumerable<WeatherForecast>> actionOnSuccess,
Action<ProblemDetails> actionOnFailure,
CancellationToken cancellationToken = default)
{
await GetManyAsync("weatherforecast",
actionOnSuccess,
actionOnFailure,
cancellationToken);
}
}
public abstract class ServiceBase
{
public ServiceBase(HttpClient client)
{
Client = client;
}
protected HttpClient Client
{
get;
}
protected virtual async Task GetManyAsync<TExpected>(
string path,
Action<IEnumerable<TExpected>> actionOnSuccess,
Action<ProblemDetails> actionOnProblem,
CancellationToken cancellationToken = default)
where TExpected : class
{
string json = await GetJsonAsync(path, cancellationToken);
ProblemDetails? problem = Deserialize<ProblemDetails>(json);
if (problem is { })
{
var taskOnProblem = TaskFromAction(actionOnProblem, problem);
await taskOnProblem;
}
else
{
IEnumerable<TExpected>? expected = Deserialize<IEnumerable<TExpected>>(json);
expected = EnsureNotNull(expected);
var taskOnSuccess = TaskFromAction(actionOnSuccess, expected);
await taskOnSuccess;
}
}
private Task TaskFromAction<T>(Action<T> action, T state)
{
return new Task(ActionOfObjectFromActionOfT(action), state);
}
private Action<object> ActionOfObjectFromActionOfT<T>(Action<T> actionOfT)
{
return new Action<object>(o => actionOfT((T)o));
}
private IEnumerable<T> EnsureNotNull<T>(IEnumerable<T>? enumerable)
{
if (enumerable is null)
{
enumerable = new List<T>();
}
return enumerable;
}
private async Task<string> GetJsonAsync(string path, CancellationToken cancellationToken = default)
{
var response = await Client.GetAsync(path, cancellationToken);
return await response.Content.ReadAsStringAsync();
}
private T? Deserialize<T>(string json)
where T : class
{
try
{
return JsonSerializer.Deserialize<T>(json, null);
}
catch (JsonException)
{
return default;
}
}
}
A minimal reproducible example of my failed attempt at this problem can be found here:
https://github.com/BenjaminCharlton/AsyncBlazorRepro
Thank you!
Fixed it!
This problem had nothing to do with async-await problems. It was all to do with deserialization problems.
Looking at the ASP .NET Core source code here:
https://github.com/dotnet/aspnetcore/blob/master/src/Components/Blazor/Http/src/HttpClientJsonExtensions.cs
You'll notice that the methods in Microsoft.AspNetCore.Components.HttpClientJsonExtensions all pass a JsonSerializerOptions to the Deserialize method, but in my code I was just passing null because I didn't think it was important. The JsonSerializer was ignoring every single property because of case-sensitivity!
I changed my Deserialize method as below:
private T? Deserialize<T>(string json)
where T : class
{
var jsonOptions = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};
try
{
return JsonSerializer.Deserialize<T>(json, jsonOptions);
}
catch (JsonException)
{
return default;
}
}
As Henk pointed out in the comments, I had also written in some unnecessary complexity. I didn't need to turn the Actions into Tasks using my pointless TaskFromAction method. You can just leave them as Actions. You can also create an overload that takes Func<TExpected, Task> if you want to give callers an asynchronous option too.
I have updated the repro project on GitHub with working code in case anybody else wishes to encapsulate their Blazor API calls this way.
https://github.com/BenjaminCharlton/AsyncBlazorRepro
I am calling a Web API method like this:
$.post("/api/attendances", { gigId: button.attr("data-gig-id") })
.done(function() {
button.removeAttr("btn-default")
.addClass("btn-primary")
.text("going");
})
.fail(function() {
alert("something went wrong");
});
And the Web API class looks like this:
[Authorize]
public class AttendancesController : ApiController
{
private ApplicationDbContext _context;
public AttendancesController()
{
_context = new ApplicationDbContext();
}
[HttpPost]
public IHttpActionResult SaveAttenance(AttendanceDto dto)
{
string userId = User.Identity.GetUserId();
if (_context.Attendances.Any(a => a.GigId == dto.GigId && a.AttendeeId == userId))
{
return BadRequest();
}
_context.Attendances.Add(new Attendance()
{
GigId = dto.GigId,
AttendeeId = userId
});
_context.SaveChanges();
return Ok();
}
}
I am testing the call with anonymous user,when calling the method, I get status code 200 back which is not what I am expecting. I am also receiving this:
responseText :"{"Message":"Authorization has been denied for this
request."}"
status:200
statusText : "OK"
Why isn't the Authorize attribute returning a status code that matches the responseText? In my case, the JavaScript code inside the .done function will execute regardless if the user is authorized or not. Any guidance is appreciated.
Update: Here's a link to my web.config if it helps: https://pastebin.com/B26QGjv8
That's because Although you are using the [Authorize] attribute, You are not doing anything with the result.
The method works as expected, you are issuing a request, you are not authorized but you continue on with your work in the controller.
Handle your exception in the controller:
Override the On Exception method and create an exception attribute:
public class NotImplExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
if (context.Exception is NotImplementedException)
{
context.Response = new HttpResponseMessage(HttpStatusCode.NotImplemented);
}
}
}
And in your controller call it like this:
public class ProductsController : ApiController
{
[NotImplExceptionFilter]
public Contact GetContact(int id)
{
throw new NotImplementedException("This method is not implemented");
}
}
In your WebApiConfig.cs add:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new ProductStore.NotImplExceptionFilterAttribute());
// Other configuration code...
}
}
Use This as reference, All snippets are taken from here:
Handling exceptions in Web Api.
I am porting my API from Web API 2 to ASP.NET Core Web API. I used to be able to add a custom header in the following manner:
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
response.Headers.Add("X-Total-Count", count.ToString());
return ResponseMessage(response);
How does one add a custom header in ASP.NET Core Web API?
You can just hi-jack the HttpContext from the incoming Http Request and add your own custom headers to the Response object before calling return.
If you want your custom header to persist and be added in all API requests across multiple controllers, you should then consider making a Middleware component that does this for you and then add it in the Http Request Pipeline in Startup.cs
public IActionResult SendResponse()
{
Response.Headers.Add("X-Total-Count", "20");
return Ok();
}
There is an example for simple GET action which returns top X records from some list as well as the count in the response header X-Total-Count:
using System;
using System.Linq;
using System.Net;
using Microsoft.AspNetCore.Mvc;
namespace WebApplication.Controllers
{
[Route("api")]
public class ValuesController : Controller
{
[HttpGet]
[Route("values/{top}")]
public IActionResult Get(int top)
{
// Generate dummy values
var list = Enumerable.Range(0, DateTime.Now.Second)
.Select(i => $"Value {i}")
.ToList();
list.Reverse();
var result = new ObjectResult(list.Take(top))
{
StatusCode = (int)HttpStatusCode.OK
};
Response.Headers.Add("X-Total-Count", list.Count.ToString());
return result;
}
}
}
URL looks like http://localhost:3377/api/values/5 and results (for 19 dummy records generated, so X-Total-Count value will be 19) are like:
["Value 18","Value 17","Value 16","Value 15","Value 14"]
For anyone who want to add custom header to all requests, middleware is the best way.
make some change in startup.cs like this:
app.Use(async (context, next) =>
{
context.Response.Headers.Add("X-Developed-By", "Your Name");
await next.Invoke();
});
Good luck.
A custom attribute can be a good way.
https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-2.2
public class AddHeaderAttribute : ResultFilterAttribute
{
private readonly string _name;
private readonly string _value;
public AddHeaderAttribute(string name, string value)
{
_name = name;
_value = value;
}
public override void OnResultExecuting(ResultExecutingContext context)
{
context.HttpContext.Response.Headers.Add(_name, new string[] { _value });
base.OnResultExecuting(context);
}
}
Then use it like this on your API method
[AddHeader("X-MyHeader", "123")]
If you have a common header you can just extend this class :
public class MySpecialHeaderAttribute : AddHeaderAttribute
{
public MySpecialHeaderAttribute() : base("X-MyHeader", "true")
{
}
}
I agree with #Ho3Ein that
if you want to add a custom header to all requests, middleware is the best way
but modifying Resposne directly in middleware is discouraged. From Microsoft Doc.
Changes to HttpResponse after the response has started, throw an exception. For example, changes such as setting headers and a status code throw an exception.
app.Use(async (context, next) =>
{
// Do work that doesn't write to the Response.
await next.Invoke();
// Do logging or other work that doesn't write to the Response.
});
So the better way to add a custom header in middleware is to use Response.OnStarting callback like below:
app.Use(async (context, next) =>
{
context.Response.OnStarting(() =>
{
context.Response.Headers.Add("X-Developed-By", "Your Name");
return Task.FromResult(0);
});
await next();
}
);
Other middleware might clear out headers after you set them. To make sure your headers are added, add them just before the response is sent.
app.Use(async (context, next) => {
context.Response.OnStarting(() => {
context.Response.Headers.Add("X-Developed-By", "Your Name");
return Task.FromResult(0);
});
await next();
});
Or in a real middleware
public class AddHeadersMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
context.Response.OnStarting(() => {
context.Response.Headers.Add("X-Developed-By", "Your Name");
return Task.FromResult(0);
});
await next();
}
}
The selected answer is OK but if you want to add AES-like encoded values on headers you will get an Error:
Invalid non-ASCII or control character in header
One way to pass is encoding the value one more time with URL encoding. To do it:
string urlEncodedValue = WebUtility.UrlEncode(value);
Vice versa to decode it:
string value = WebUtility.UrlDecode(urlEncodedValue);
FWIW, if you have an ApiController, instead of a Controller, here is how you can do it:
public class InfoController : ApiController
{
// Without custom header
public IHttpActionResult MyMethod(..)
{
var myObject= GetMyResult();
return Ok(myObject);
}
// With custom header
public IHttpActionResult MyMethod(..)
{
var myObject = GetMyResult();
// inspired from https://learn.microsoft.com/en-us/aspnet/web-api/overview/formats-and-model-binding/content-negotiation#how-content-negotiation-works
var negotiator = Configuration.Services.GetContentNegotiator();
var result = negotiator.Negotiate(typeof(TypeOfMyObject), Request, Configuration.Formatters);
var msg = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new ObjectContent<TypeOfMyObject>(myObject, result.Formatter,result.MediaType.MediaType)
};
msg.Headers.Add("MyCustomHeader", "MyCustomHeaderValue");
return ResponseMessage(msg);
}
}
I'm encountering an issue with CORS while using IAsyncResourceFilter implementation.
I want to be able to call my actions from other domains as well...
I've defined the CORS policy under my Startup file as the following:
services.AddCors(options =>
{
options.AddPolicy("AllowAllOrigins",
builder =>
{
builder.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
});
});
And under the Configure method:
app.UseCors("AllowAllOrigins");
It works fine without using a TypeFilterAttribute which use IAsyncResourceFilter.
For example calling my API action without any TypeFilterAttribute attribute works:
public bool Get()
{
return true;
}
But when adding my TypeFilterAttribute as follows it doesn't work and returns the error about the CORS:
[MyTypeFilterAttribute("test")]
public bool Get()
{
return true;
}
Anything I'm missing? What should I add when using IAsyncResourceFilter?
The following is the MyTypeFilterAttribute code: (With no real logic...)
public class MyTypeFilterAttribute : TypeFilterAttribute
{
public MyTypeFilterAttribute(params string[] name) : base(typeof(MyTypeFilterAttributeImpl))
{
Arguments = new[] { new MyTypeRequirement(name) };
}
private class MyTypeFilterAttributeImpl: Attribute, IAsyncResourceFilter
{
private readonly MyTypeRequirement_myTypeRequirement;
public MyTypeFilterAttributeImpl(MyTypeRequirement myTypeRequirement)
{
_myTypeRequirement= myTypeRequirement;
}
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
{
context.Result = new OkResult();
await next();
}
}
}
public class MyTypeRequirement : IAuthorizationRequirement
{
public string Name { get; }
public MyTypeRequirement(string name)
{
Name = name;
}
}
Cors middleware sets headers on the response result object.
I believe you are resetting these with context.Result = new OkResult();
See poke's reply below. If you set any result in an action filter, this result gets sent back immediately, thus overwriting any other one!