I am trying to use the streaming mode in an ASP.NET WebAPI action:
I setup Startup.cs with
public static void Configuration(IAppBuilder app)
{
if (HostingEnvironment.IsHosted)
{
GlobalConfiguration.Configure(config => Configuration(app, config));
GlobalConfiguration.Configuration.Services.Replace(typeof(IHostBufferPolicySelector), new CustomWebHostBufferPolicySelector());
}
else
{
var config = new HttpConfiguration();
Configuration(app, config);
}
}
The class handling the buffer / streaming mode
// Try the worst case, everything has to be streamed...
public class CustomWebHostBufferPolicySelector : WebHostBufferPolicySelector
{
// Check incoming requests and modify their buffer policy
public override bool UseBufferedInputStream(object hostContext)
{
return false;
}
// You could also change the response behaviour too...but for this example, we are not
// going to do anything here...
// I override this method just to demonstrate the availability of this method.
public override bool UseBufferedOutputStream(System.Net.Http.HttpResponseMessage response)
{
return base.UseBufferedOutputStream(response);
}
}
But in my WebAPI controller action the value is set Classic instead of None...
[Route("api/v1/presentations/{presentationId:int}/documents/{documentId}")]
public async Task<HttpResponseMessage> Post(int presentationId, string documentId)
{
try
{
var readEntityBodyMode = HttpContext.Current.Request.ReadEntityBodyMode;
// Classic... but why?
Related
I'm working on an integration test for a Web API which communicates through Redis, so I tried to replace the Redis Server with a containerized one and run some tests.
The issue is that it is first running the Api with project's appsettings.Development.json configuration and the old IConnectionMultiplexer instance which obviously won't connect because the hostname is offline. The question is how do I make it run the project with the new IConnectionMultiplexer that uses the containerized Redis Server? Basically the sequence is wrong there. What I did is more like run the old IConnectionMultiplexer and replace it with the new one but it wouldn't connect to the old one, so that exception prevents me from continuing. I commented the line of code where it throws the exception but as I said it's obvious because it's first running the Api with the old configuration instead of first overriding the configuration and then running the Api.
I could have done something like the following but I'm DI'ing other services based on configuration as well, meaning I must override the configuration first and then run the actual API code.
try
{
var redis = ConnectionMultiplexer.Connect(redisConfig.Host);
serviceCollection.AddSingleton<IConnectionMultiplexer>(redis);
}
catch
{
// We discard that service if it's unable to connect
}
Api
public static class RedisConnectionConfiguration
{
public static void AddRedisConnection(this IServiceCollection serviceCollection, IConfiguration config)
{
var redisConfig = config.GetSection("Redis").Get<RedisConfiguration>();
serviceCollection.AddHostedService<RedisSubscription>();
serviceCollection.AddSingleton(redisConfig);
var redis = ConnectionMultiplexer.Connect(redisConfig.Host); // This fails because it didn't override Redis:Host
serviceCollection.AddSingleton<IConnectionMultiplexer>(redis);
}
}
Integration tests
public class OrderManagerApiFactory : WebApplicationFactory<IApiMarker>, IAsyncLifetime
{
private const string Password = "Test1234!";
private readonly TestcontainersContainer _redisContainer;
private readonly int _externalPort = Random.Shared.Next(10_000, 60_000);
public OrderManagerApiFactory()
{
_redisContainer = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("redis:alpine")
.WithEnvironment("REDIS_PASSWORD", Password)
.WithPortBinding(_externalPort, 6379)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(6379))
.Build();
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Development");
builder.ConfigureLogging(logging =>
{
logging.ClearProviders();
});
builder.ConfigureAppConfiguration(config =>
{
config.AddInMemoryCollection(new Dictionary<string, string>
{
{ "Redis:Host", $"localhost:{_externalPort},password={Password},allowAdmin=true" },
{ "Redis:Channels:Main", "main:new:order" },
});
});
builder.ConfigureTestServices(services =>
{
services.RemoveAll(typeof(IConnectionMultiplexer));
services.AddSingleton<IConnectionMultiplexer>(_ =>
ConnectionMultiplexer.Connect($"localhost:{_externalPort},password={Password},allowAdmin=true"));
});
}
public async Task InitializeAsync()
{
await _redisContainer.StartAsync();
}
public new async Task DisposeAsync()
{
await _redisContainer.DisposeAsync();
}
}
public class OrderManagerTests : IClassFixture<OrderManagerApiFactory>, IAsyncLifetime
{
private readonly OrderManagerApiFactory _apiFactory;
public OrderManagerTests(OrderManagerApiFactory apiFactory)
{
_apiFactory = apiFactory;
}
[Fact]
public async Task Test()
{
// Arrange
var configuration = _apiFactory.Services.GetRequiredService<IConfiguration>();
var redis = _apiFactory.Services.GetRequiredService<IConnectionMultiplexer>();
var channel = configuration.GetValue<string>("Redis:Channels:Main");
// Act
await redis.GetSubscriber().PublishAsync(channel, "ping");
// Assert
}
public Task InitializeAsync()
{
return Task.CompletedTask;
}
public Task DisposeAsync()
{
return Task.CompletedTask;
}
}
Problem solved.
If you override WebApplicationFactory<T>.CreateHost() and call IHostBuilder.ConfigureHostConfiguration() before calling base.CreateHost() the configuration you add will be visible between WebApplication.CreateBuilder() and builder.Build().
The following two links might help someone:
https://github.com/dotnet/aspnetcore/issues/37680
https://github.com/dotnet/aspnetcore/issues/9275
public sealed class OrderManagerApiFactory : WebApplicationFactory<IApiMarker>, IAsyncLifetime
{
private const string Password = "Test1234!";
private const int ExternalPort = 7777; // Random.Shared.Next(10_000, 60_000);
private readonly TestcontainersContainer _redisContainer;
public OrderManagerApiFactory()
{
_redisContainer = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("redis:alpine")
.WithEnvironment("REDIS_PASSWORD", Password)
.WithPortBinding(ExternalPort, 6379)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(6379))
.Build();
}
public async Task InitializeAsync()
{
await _redisContainer.StartAsync();
}
public new async Task DisposeAsync()
{
await _redisContainer.DisposeAsync();
}
protected override IHost CreateHost(IHostBuilder builder)
{
builder.ConfigureHostConfiguration(config =>
config.AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Redis:Host", $"localhost:{ExternalPort},password={Password},allowAdmin=true"),
new KeyValuePair<string, string>("Redis:Channels:Main", "main:new:order")
}));
return base.CreateHost(builder);
}
}
How do I configure the response type of a void/Task action method to be 204 No Content rather than 200 OK?
For example, consider a simple controller:
public class MyController : Controller
{
[HttpPost("foo")]
public async Task Foo() {
await Task.CompletedTask;
}
[HttpPost("bar")]
public async Task<IActionResult> Bar() {
await Task.CompletedTask;
return NoContent();
}
}
I'd like both methods to return 204 No Content, but the default (and thus what /foo returns) seems to be 200 OK. I tried various things, such as adding a [ProducesResponseType(204)] attribute to /foo, but I haven't found anything that seems to have any effect.
Here is a solution that returns 204 instead of 200 for all controller methods that return void or Task. First, create a result filter:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using static Microsoft.AspNetCore.Http.StatusCodes;
namespace StackOverflow.SampleCode
{
/// <summary>
/// A filter that transforms http status code 200 OK to 204 No Content for controller actions that return nothing,
/// i.e. <see cref="System.Void"/> or <see cref="Task"/>.
/// </summary>
internal class VoidAndTaskTo204NoContentFilter : IResultFilter
{
/// <inheritdoc/>
public void OnResultExecuting(ResultExecutingContext context)
{
if (context.ActionDescriptor is ControllerActionDescriptor actionDescriptor)
{
var returnType = actionDescriptor.MethodInfo.ReturnType;
if (returnType == typeof(void) || returnType == typeof(Task))
{
context.HttpContext.Response.StatusCode = Status204NoContent;
}
}
}
/// <inheritdoc/>
public void OnResultExecuted(ResultExecutedContext context)
{
}
}
}
Then register the filter globally:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options => options.Filters.Add<VoidAndTaskTo204NoContentFilter>());
}
This will affect all your controller methods.
A workable solution provided by Nish26.
But if you don't want to clog up the controller by an excess code then another option is to create a ResultFilter:
public class ResponseCodeAttribute : Attribute, IResultFilter
{
private readonly int _statusCode;
public ResponseCode(int statusCode)
{
this._statusCode = statusCode;
}
public void OnResultExecuting(ResultExecutingContext context)
{
}
public void OnResultExecuted(ResultExecutedContext context)
{
context.HttpContext.Response.StatusCode = _statusCode;
}
}
And then use it with the action method:
[ResponseCode(204)]
[HttpPost("foo")]
public async Task Foo() {
await Task.CompletedTask;
}
BTW, ProducesResponseType is just an attribute that helps to create an API metadata.
First of all, I don't think this is a good idea. Controller actions should be testable without creating the entire pipeline. You can test Bar() to ensure it returns 204 simply by checking the return result. You can't do that with an action that modifies the return result through filters.
That said, it is possible to modify the result by using Result Filters, attributes that implement the IResultFilter, IAsyncResultFilter interfaces. There's also an abstract ResultFilterAttribute class that implements both interfaces provides an implementation for IAsyncResultFilter that calls the IResultFilter methods.
You could create an attribute that modifies the status code like this :
public class ResponseCodeAttribute : ResultFilterAttribute
{
//Public property to enable reflection, inspection
public int StatusCode {get;}
public ResponseCodeAttribute(int statusCode)=>StatusCode=statusCode;
public override void OnResultExecuted(ResultExecutedContext context)
{
context.HttpContext.Response.StatusCode = StatusCode;
}
}
And use it with :
[HttpPost,ResponseCode(204)]
public async Task Foo() {
await Task.CompletedTask;
}
This is not enough though.
Callers of this method have no way of knowing that it will return 204 instead of the expected 200. That's where the metadata-only attribute ProducesResponseTypeAttribute comes in. This attribute implements the IApiResponseMetadataProvider which is used to provide metadata to API Explorer and the proxy/documentation tools like Swagger. At the very least you should use both attributes, eg :
[HttpPost,ResponseCode(204),ProducesResponseType(204)]
public async Task Foo() {
await Task.CompletedTask;
}
Even better, combine ResponseCodeAttribute with IApiResponseMetadataProvider :
public class ResponseCodeAttribute : ResultFilterAttribute,IApiResponseMetadataProvider
{
public int StatusCode {get;}
public Type Type { get; }=typeof(void);
public ResponseCodeAttribute(int statusCode)=>StatusCode=statusCode;
public override void OnResultExecuted(ResultExecutedContext context)
{
context.HttpContext.Response.StatusCode = StatusCode;
}
void IApiResponseMetadataProvider.SetContentTypes(MediaTypeCollection contentTypes)
{
}
}
And apply it with the initial :
[HttpPost,ResponseCode(204)]
public async Task Foo() {
await Task.CompletedTask;
}
Controller base class has a Response property of type HttpResponse.
You can directly set :
Response.StatusCode = 204 ;
You can remove HttpNoContent formatter from outputformatters.Ms doc
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
// requires using Microsoft.AspNetCore.Mvc.Formatters;
options.OutputFormatters.RemoveType<StringOutputFormatter>();
options.OutputFormatters.RemoveType<HttpNoContentOutputFormatter>();
});
}
We develop ASP.NET MVC5 app with WebApi2 and AngularJs. For serialization and deserialization we use custum JsonNetFormatter as follow:
public class JsonNetFormatter : MediaTypeFormatter
{
// other codes for formatting
public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
{
string NameOfSet = "";
ObjectWrapperWithNameOfSet obj = value as ObjectWrapperWithNameOfSet;
if (obj != null)
{
NameOfSet = obj.NameOfSet;
value = obj.WrappedObject;
}
_jsonSerializerSettings.ContractResolver = new CustomContractResolver(NameOfSet);
// Create a serializer
JsonSerializer serializer = JsonSerializer.Create(_jsonSerializerSettings);
// Create task writing the serialized content
return Task.Factory.StartNew(() =>
{
using (JsonTextWriter jsonTextWriter = new JsonTextWriter(new StreamWriter(writeStream, SupportedEncodings[0])) { CloseOutput = false })
{
serializer.Serialize(jsonTextWriter, value);
jsonTextWriter.Flush();
}
});
}
}
and WebApiConfig as follow:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.Formatters.Clear();
config.Formatters.Insert(0, new JsonNetFormatter());
}
}
The application work fine. but in some situation we get an errors when deserialization json data. My question is How can we handle these errors and send to the client side?
Example for the errors:
You can create your own custom exception handler to modify any error messages before sending it to client
public class ErrorHandler : ExceptionHandler
{
public override void Handle(ExceptionHandlerContext context)
{
context.Result = new TextPlainErrorResult()
{
Request = context.ExceptionContext.Request,
Content = "Oops! Sorry! Something went wrong." + "Please contact support so we can try to fix it."
};
}
private class TextPlainErrorResult : IHttpActionResult
{
public HttpRequestMessage Request { get; set; }
public string Content { get; set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.InternalServerError, Content);
return Task.FromResult(response);
}
}
}
and then register this custom class in config class like following
config.Services.Replace(typeof(IExceptionHandler), new ErrorHandler());
I am interested in building a startup routine for my API to check that certain configuration values in the web.config are present. If the routine does not contain values I would like to redirect to a route, log the missing configuration item and display a custom application offline page.
Any assistance in pointing me in the right direction would be appreciated.
Guard Class
public static class Guard
{
public static bool ConfigurationValueExists(string key, [CallerMemberName] string caller = null)
{
if (!string.IsNullOrEmpty(Configuration.GetAppConfig(key, string.Empty))) return true;
ApiLogger.Log($"The configuration value {key} is not present and needs to be defined. Calling method is {caller}.");
return false;
}
}
Configuration Class
public static class Configuration
{
public static T GetAppConfig<T>(string key, T defaultVal = default(T))
{
if (null == ConfigurationManager.AppSettings[key])
{
return defaultVal;
}
return string.IsNullOrEmpty(key)
? defaultVal
: Generic.Turn(ConfigurationManager.AppSettings[key], defaultVal);
}
public static bool ConfigurationsAreInPlace()
{
return AssertMainApplicationConfiguration();
}
private static bool AssertMainApplicationConfiguration()
{
return Guard.ConfigurationValueExists("MyKey1");
}
}
I would like to be able to call ConfigurationsAreInPlace on the startup routine and redirect to my custom offline page.
I decided to create an Index Controller and use the Route Attribute of root to override what happens on the page. I then do the check if configurations are in place and issue a new response as needed.
Code if interested:
public class IndexController : ApiController
{
[AllowAnonymous]
[Route]
public HttpResponseMessage GetIndex()
{
string startUrl = "/help/";
if (!Helpers.Configuration.ConfigurationsAreInPlace())
{
startUrl += "offline";
}
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Moved);
string fullyQualifiedUrl = Request.RequestUri.GetLeftPart(UriPartial.Authority);
response.Headers.Location = new Uri(fullyQualifiedUrl + startUrl);
return response;
}
}
I've created a web api application, to expose an ODATA API, to a front-end application. One of the reasons for doing this was to be able to return different content types, such as Excel files, for the same data.
I've made use of a custom Media Formatter to output my Excel data, however, I've noticed that when I call it, from the client, there is no security in place.
When making a GET, with no ACCEPT header, then the OAuth bearer token is checked and access is either accepted or revoked. The Authorization is set via [Authorize] on the controller.
When I make the same GET, with the ACCEPT header set to request an Excel file, the controller is called regardless of the token, bypassing the security on the controller.
I've obviously done something wrong, however, I can't work out what it could be. It's the same controller but for some reason, it's always allowing access when ACCEPT is set to a supported media type.
A cut-down version of my set up is below.
Owin Startup:
[assembly: OwinStartup(typeof(Rest.Startup))]
namespace Rest
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureOAuth(app);
HttpConfiguration config = new HttpConfiguration();
WebApiConfig.Register(config);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
}
private void ConfigureOAuth(IAppBuilder app)
{
OAuthAuthorizationServerOptions oauthServerOptions = new OAuthAuthorizationServerOptions
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new SimpleAuthorisationServerProvider()
};
// Token generation
app.UseOAuthAuthorizationServer(oauthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
}
The call into WebApiConfig.Register()
namespace Rest
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var json = config.Formatters.JsonFormatter;
json.SerializerSettings.PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.Objects;
config.Formatters.Remove(config.Formatters.XmlFormatter);
config.Formatters.Add(new ExcelSimpleFormatter());
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
// Configure CORS globally
var cors = new EnableCorsAttribute(
origins:"*",
headers:"*",
methods:"*");
config.EnableCors(cors);
}
}
}
My media formatter (code removed to save space):
namespace Rest.Formatters
{
public class ExcelSimpleFormatter : BufferedMediaTypeFormatter
{
public ExcelSimpleFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/excel"));
}
public override bool CanWriteType(Type type)
{
return true;
}
public override bool CanReadType(Type type)
{
return false;
}
public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content)
{
// This gets called regardless of authorization
}
}
}
An example / simplified controller:
namespace Rest.Controllers
{
[Authorize]
public class TestController : ApiController
{
private dbSDSContext db = new dbSDSContext();
// GET: api/Test
public IQueryable<test> GetTests()
{
return db.test;
}
// GET: api/Test/5
[ResponseType(typeof(test))]
public async Task<IHttpActionResult> GetTest(int id)
{
test test = await db.test.FindAsync(id);
if (test == null)
{
return NotFound();
}
return Ok(test);
}
// PUT: api/Test/5
[ResponseType(typeof(void))]
public async Task<IHttpActionResult> PutTest(int id, test test)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != test.testID)
{
return BadRequest();
}
db.Entry(test).State = EntityState.Modified;
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!TestExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return StatusCode(HttpStatusCode.NoContent);
}
// POST: api/Test
[ResponseType(typeof(test))]
public async Task<IHttpActionResult> PostTest(test test)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.test.Add(test);
await db.SaveChangesAsync();
return CreatedAtRoute("DefaultApi", new { id = test.testID}, test);
}
// DELETE: api/Test/5
[ResponseType(typeof(test))]
public async Task<IHttpActionResult> DeleteTest(int id)
{
test test = await db.test.FindAsync(id);
if (test == null)
{
return NotFound();
}
db.test.Remove(test);
await db.SaveChangesAsync();
return Ok(test);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
private bool TestExists(int id)
{
return db.test.Count(e => e.testID == id) > 0;
}
}
}
The error was caused by using the wrong namespace in the controllers affected.
When using WebAPI ensure to use:
using System.Web.Http;
and not:
using System.Web.Mvc;