How to get HttpContext from Controller - asp.net-core-mvc

I'm using ASP.NET Core (MVC)
If I call an endpoint, then this.HttpContext is not null.
Within the same class as my endpoint, if I put a break point in the controller, this.HttpContext is always null.
How do I get the value of HttpContext from the controller?
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class LoginController : ControllerBase
{
public LoginController()
{
var isNull = this.HttpContext; //always null
}
[HttpGet]
public async Task Get()
{
var isNull = this.HttpContext; //not null
}
}
The purpose for this, is on each end point, I want to access some values (which are from a cookie). In NET Framework, I'd store the cookie values in a base class (from within the constructor).
Whilst I can access HTTPContext on each each end point, doing it in the constructor means code it once per class.
The goal is very much about coding this less. I'm hoping I'm not just being lazy

No, it is not the correct way to do it. you need to use Filter or middleware to do it. HttpContext class is always null in the constructor of a controller
Sample middleware code (for logging)
you can do whatever in this like read cookies or whatnot
public class LoggingMiddleware
{
private static readonly TelemetryConfiguration telemetryConfiguration = TelemetryConfiguration.CreateDefault();
private readonly TelemetryClient telemetryClient;
private IConfiguration configuration;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
private readonly string appName;
private readonly bool loggingEnabled;
private readonly RequestDelegate _next;
public LoggingMiddleware(RequestDelegate next, IConfiguration config)
{
_next = next;
configuration = config;
_recyclableMemoryStreamManager = new RecyclableMemoryStreamManager();
telemetryConfiguration.InstrumentationKey = configuration.GetValue<string>("ApplicationInsights:InstrumentationKey");
telemetryClient = new TelemetryClient(telemetryConfiguration);
appName = configuration.GetValue<string>("AppName");
loggingEnabled = configuration.GetValue<bool>("Logging:LogRequestResponse");
}
public async Task Invoke(HttpContext httpContext)
{
if(loggingEnabled)
{
await LogRequest(httpContext);
await LogResponse(httpContext);
}
}
private async Task LogRequest(HttpContext context)
{
context.Request.EnableBuffering();
await using var requestStream = _recyclableMemoryStreamManager.GetStream();
await context.Request.Body.CopyToAsync(requestStream);
string correlationId = context.Request.Headers.Keys.FirstOrDefault(h => h.ToLower() == "correlationid");
if (correlationId == null) correlationId = string.Empty;
if (context.Request.Path != "/")
{
telemetryClient.TrackEvent($"{appName}-RequestMiddleware", new Dictionary<string, string>
{
{ "AppName", appName },
{ "CorrelationId" , correlationId },
{ "Method" , context.Request.Method },
{ "Scheme", context.Request.Scheme},
{ "Host", context.Request.Host.Value },
{ "Path", context.Request.Path },
{ "QueryString", context.Request.QueryString.Value },
{ "Request Body", ReadStreamInChunks(requestStream) }
});
}
context.Request.Body.Position = 0;
}
private static string ReadStreamInChunks(Stream stream)
{
const int readChunkBufferLength = 4096;
stream.Seek(0, SeekOrigin.Begin);
using var textWriter = new StringWriter();
using var reader = new StreamReader(stream);
var readChunk = new char[readChunkBufferLength];
int readChunkLength;
do
{
readChunkLength = reader.ReadBlock(readChunk,
0,
readChunkBufferLength);
textWriter.Write(readChunk, 0, readChunkLength);
} while (readChunkLength > 0);
return textWriter.ToString();
}
private async Task LogResponse(HttpContext context)
{
var originalBodyStream = context.Response.Body;
await using var responseBody = _recyclableMemoryStreamManager.GetStream();
context.Response.Body = responseBody;
await _next(context);
context.Response.Body.Seek(0, SeekOrigin.Begin);
var text = await new StreamReader(context.Response.Body).ReadToEndAsync();
context.Response.Body.Seek(0, SeekOrigin.Begin);
if (context.Request.Path != "/")
{
telemetryClient.TrackEvent($"{appName}-ResponseMiddleware", new Dictionary<string, string> {
{"Scheme", context.Request.Scheme},
{ "AppName", appName },
{"Host", context.Request.Host.Value},
{"Path" , context.Request.Path},
{"QueryString", context.Request.QueryString.Value},
{"Response Body" , text}
});
}
await responseBody.CopyToAsync(originalBodyStream);
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class LoggingMiddlewareExtensions
{
public static IApplicationBuilder UseLoggingMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<LoggingMiddleware>();
}
}

No, you can't do it that way, controller constructors is danger zone (unless you know what you're doing) and should be used for DI only.
Instead, you should look at custom middleware:
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/write?view=aspnetcore-3.1
More info on asp.net Core life-cycles:
https://www.c-sharpcorner.com/article/asp-net-core-mvc-request-life-cycle/

Related

Can API Key and JWT Token be used in the same .Net 6 WebAPI

I am building a new .Net 6 WebAPI that will be consumed by many applications so I need to implement API Keys to limit access to only those applications. Only a very small amount of the individual users will require authorization (admins) so I would like to combine with JWT for the Admin endpoints. We do not want to require users to have to crate an account where not necessary (non-admins). Is this possible? Thank You.
Yes it is possible.
The solution I recommend is to setup multiple authentication methods in asp.net core 6 using two authentication schemes that you have to specify inside Authorize attribute.
Here a simple implementation of ApiKey authentication:
namespace MyAuthentication;
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
private enum AuthenticationFailureReason
{
NONE = 0,
API_KEY_HEADER_NOT_PROVIDED,
API_KEY_HEADER_VALUE_NULL,
API_KEY_INVALID
}
private readonly Microsoft.Extensions.Logging.ILogger _logger;
private AuthenticationFailureReason _failureReason = AuthenticationFailureReason.NONE;
public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options,
ILoggerFactory loggerFactory,
ILogger<ApiKeyAuthenticationHandler> logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, loggerFactory, encoder, clock)
{
_logger = logger;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
//ApiKey header get
if (!TryGetApiKeyHeader(out string providedApiKey, out AuthenticateResult authenticateResult))
{
return authenticateResult;
}
//TODO: you apikey validity check
if (await ApiKeyCheckAsync(providedApiKey))
{
var principal = new ClaimsPrincipal(); //TODO: Create your Identity retreiving claims
var ticket = new AuthenticationTicket(principal, ApiKeyAuthenticationOptions.Scheme);
return AuthenticateResult.Success(ticket);
}
_failureReason = AuthenticationFailureReason.API_KEY_INVALID;
return AuthenticateResult.NoResult();
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
//Create response
Response.Headers.Append(HeaderNames.WWWAuthenticate, $#"Authorization realm=""{ApiKeyAuthenticationOptions.DefaultScheme}""");
Response.StatusCode = StatusCodes.Status401Unauthorized;
Response.ContentType = MediaTypeNames.Application.Json;
//TODO: setup a response to provide additional information if you want
var result = new
{
StatusCode = Response.StatusCode,
Message = _failureReason switch
{
AuthenticationFailureReason.API_KEY_HEADER_NOT_PROVIDED => "ApiKey not provided",
AuthenticationFailureReason.API_KEY_HEADER_VALUE_NULL => "ApiKey value is null",
AuthenticationFailureReason.NONE or AuthenticationFailureReason.API_KEY_INVALID or _ => "ApiKey is not valid"
}
};
using var responseStream = new MemoryStream();
await JsonSerializer.SerializeAsync(responseStream, result);
await Response.BodyWriter.WriteAsync(responseStream.ToArray());
}
protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
{
//Create response
Response.Headers.Append(HeaderNames.WWWAuthenticate, $#"Authorization realm=""{ApiKeyAuthenticationOptions.DefaultScheme}""");
Response.StatusCode = StatusCodes.Status403Forbidden;
Response.ContentType = MediaTypeNames.Application.Json;
var result = new
{
StatusCode = Response.StatusCode,
Message = "Forbidden"
};
using var responseStream = new MemoryStream();
await JsonSerializer.SerializeAsync(responseStream, result);
await Response.BodyWriter.WriteAsync(responseStream.ToArray());
}
#region Privates
private bool TryGetApiKeyHeader(out string apiKeyHeaderValue, out AuthenticateResult result)
{
apiKeyHeaderValue = null;
if (!Request.Headers.TryGetValue("X-Api-Key", out var apiKeyHeaderValues))
{
_logger.LogError("ApiKey header not provided");
_failureReason = AuthenticationFailureReason.API_KEY_HEADER_NOT_PROVIDED;
result = AuthenticateResult.Fail("ApiKey header not provided");
return false;
}
apiKeyHeaderValue = apiKeyHeaderValues.FirstOrDefault();
if (apiKeyHeaderValues.Count == 0 || string.IsNullOrWhiteSpace(apiKeyHeaderValue))
{
_logger.LogError("ApiKey header value null");
_failureReason = AuthenticationFailureReason.API_KEY_HEADER_VALUE_NULL;
result = AuthenticateResult.Fail("ApiKey header value null");
return false;
}
result = null;
return true;
}
private Task<bool> ApiKeyCheckAsync(string apiKey)
{
//TODO: setup your validation code...
return Task.FromResult<bool>(true);
}
#endregion
}
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "ApiKey";
public static string Scheme => DefaultScheme;
public static string AuthenticationType => DefaultScheme;
}
public static class AuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddApiKeySupport(this AuthenticationBuilder authenticationBuilder, Action<ApiKeyAuthenticationOptions> options)
=> authenticationBuilder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationOptions.DefaultScheme, options);
}
Then register inside builder setup:
_ = services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = ApiKeyAuthenticationOptions.DefaultScheme;
options.DefaultChallengeScheme = ApiKeyAuthenticationOptions.DefaultScheme;
})
.AddApiKeySupport(options => { });
You have to also setup the standard JWT Bearer validation (I don't post it for the sake of brevity).
To protect your endpoint add the Authorize attribute like:
[Authorize(AuthenticationSchemes = ApiKeyAuthenticationOptions.DefaultScheme)] //ApiKey
[HttpGet]
public async Task<IActionResult> Get()
{
//...omissis...
return null;
}
//or..
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] //Jwt
[HttpGet]
public async Task<IActionResult> Get()
{
//...omissis...
return null;
}
//or..
[Authorize(AuthenticationSchemes = $"{JwtBearerDefaults.AuthenticationScheme},{ApiKeyAuthenticationOptions.DefaultScheme}" )] //ApiKey and Jwt
[HttpGet]
public async Task<IActionResult> Get()
{
//...omissis...
return null;
}
For me it is the best way so as to carry out the authorization check before the start of the application pipeline (fail fast) and to be able to create the user identity.
But if you don't need to put informations about the Api Key inside the ClaimsPrincipal and only check the validity of Api Key the simplest way to do that is:
Protect the "admin" actions with JWT auth (with Authorize attribute)
Setup and register a middleware to only check the Api Key in all actions
Here is an example:
public class SimpleApiKeyMiddleware
{
private static readonly string API_KEY_HEADER = "X-Api-Key";
private readonly RequestDelegate _next;
private readonly ILogger<SimpleApiKeyMiddleware> _logger;
public SimpleApiKeyMiddleware(RequestDelegate next, ILogger<SimpleApiKeyMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task Invoke(HttpContext httpContext)
{
//Get apikey header
if (!httpContext.Request.Headers.TryGetValue(API_KEY_HEADER, out var apiKey))
{
_logger.LogError("ApiKey not found inside request headers");
//Error and exit from asp.net core pipeline
await GenerateForbiddenResponse(httpContext, "ApiKey not found inside request headers");
}
else if (!await ApiKeyCheckAsync(apiKey))
{
_logger.LogError("ApiKey is not valid: {ApiKey}", apiKey);
//Error and exit from asp.net core pipeline
await GenerateForbiddenResponse(httpContext, "ApiKey not valid");
}
else
{
_logger.LogInformation("ApiKey validated: {ApiKey}", apiKey);
//Proceed with pipeline
await _next(httpContext);
}
}
private Task<bool> ApiKeyCheckAsync(string apiKey)
{
//TODO: setup your validation code...
return Task.FromResult<bool>(true);
}
private async Task GenerateForbiddenResponse(HttpContext context, string message)
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
context.Response.ContentType = MediaTypeNames.Application.Json;
using var responseStream = new MemoryStream();
await System.Text.Json.JsonSerializer.SerializeAsync(responseStream, new
{
Status = StatusCodes.Status403Forbidden,
Message = message
});
await context.Response.BodyWriter.WriteAsync(responseStream.ToArray());
}
}
Registration:
_ = app.UseMiddleware<ApiKeyMiddleware>(); //Register as first middleware to avoid other middleware execution before api key check
Usage:
//Admin: Jwt and Api Key check
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] //Jwt and Api Key
[HttpGet]
public async Task<IActionResult> MyAdminApi()
{
//...omissis...
}
//Non Admin: Api Key check only
[HttpGet]
public async Task<IActionResult> MyNonAdminApi()
{
//...omissis...
}
Note: the middleware code above forces exit from pipeline returning an http result so as to stop next middleware execution. Also note that the asp.net core 6 pipeline executes Authorization first and then all the registered middlewares.

Cannot seed database with data

I wrote a class to seed the database with data. When debugging it it stacks on await IdentitySeedData.EnsurePopulated(service); Probably the problem is with usermanager.createasync(). Take a look:
namespace SportStore1
{
public class Program
{
public static void Main(string[] args)
{
var Host = BuildWebHost(args);
var Scopes = Host.Services.GetRequiredService<IServiceScopeFactory>();
using (var scope = Scopes.CreateScope())
{
var service = scope.ServiceProvider;
SeedDataFunc(service);
SeedIdentityDataFunc(service);
}
Host.Run();
}
public static void SeedDataFunc(IServiceProvider service)
{
SeedData.EnsurePopulated(service);
}
public static async void SeedIdentityDataFunc(IServiceProvider service)
{
await IdentitySeedData.EnsurePopulated(service);
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
/*
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
*/
}
}
namespace SportStore1.Data
{
public static class IdentitySeedData
{
public static async Task EnsurePopulated(IServiceProvider service)
{
UserManager<IdentityUser> userManager = service.GetRequiredService<UserManager<IdentityUser>>();
IConfiguration configuration = service.GetRequiredService<IConfiguration>();
ApplicationDbContext dbContext = service.GetRequiredService<ApplicationDbContext>();
if (!dbContext.Roles.Any())
{
foreach (var role in Roles)
{
dbContext.Add(role);
await dbContext.SaveChangesAsync();
}
}
if (!dbContext.Users.Any())
{
var user = new IdentityUser() { UserName = configuration["Email"], Email = configuration["Email"], EmailConfirmed = true };
await userManager.CreateAsync(user, configuration["Password"]);
}
if (!dbContext.UserRoles.Any())
{
var roleID = dbContext.Roles.Where(p => p.Name == "Administrator").FirstOrDefault().Id;
var userID = dbContext.Users.Where(p => p.Email == configuration["Email"]).FirstOrDefault().Id;
var userRole = new IdentityUserRole<string>()
{
RoleId = roleID,
UserId = userID
};
dbContext.UserRoles.Add(userRole);
await dbContext.SaveChangesAsync();
}
}
private static List<IdentityRole> Roles = new List<IdentityRole>()
{
new IdentityRole { Name = "Administrator", NormalizedName = "Administrator", ConcurrencyStamp = Guid.NewGuid().ToString() },
new IdentityRole { Name = "Manager", NormalizedName = "Manager", ConcurrencyStamp = Guid.NewGuid().ToString() },
new IdentityRole { Name = "User", NormalizedName = "User", ConcurrencyStamp = Guid.NewGuid().ToString()}
};
}
}
It stacks on await IdentitySeedData.EnsurePopulated(service) with a message System.ObjectDisposedException: 'Cannot access a disposed object.
Object name: 'UserManager`1'.' Any solution?
You need to await the async method. It seems you're getting stuck on the fact that Main is synchronous, but you can change that. In C# 7.2+ you can actually just have an async main:
public static async Task Main(string[] args)
In lesser versions, you'd just do:
public static void Main(string[] args) =>
MainAsync(args).GetAwaiter().GetResult();
public static async Task MainAsync(string[] args)
{
...
}
The compiler just builds that for you under the hood when you use async Main, anyways. Remember that you'll need to also do await host.RunAsync();, if you do this.
That said, this is not the method for doing database seeding any more. See the documentation.
You also need to use using() statement to ensure that the dbcontext is disposed as soon as it goes out of scope.
I tried your code in a brand new asp.net core 2.0 Individual User Account which uses ApplicationUser and it works well after using using block:
public static class IdentitySeedData
{
public static async Task EnsurePopulated(IServiceProvider service)
{
UserManager<ApplicationUser> userManager = service.GetRequiredService<UserManager<ApplicationUser>>();
IConfiguration configuration = service.GetRequiredService<IConfiguration>();
using (var dbContext = service.GetRequiredService<ApplicationDbContext>())
{
//ApplicationDbContext dbContext = service.GetRequiredService<ApplicationDbContext>();
if (!dbContext.Roles.Any())
{
foreach (var role in Roles)
{
dbContext.Add(role);
await dbContext.SaveChangesAsync();
}
}
if (!dbContext.Users.Any())
{
var user = new ApplicationUser() { UserName = configuration["Email"], Email = configuration["Email"], EmailConfirmed = true };
await userManager.CreateAsync(user, configuration["Password"]);
}
if (!dbContext.UserRoles.Any())
{
var roleID = dbContext.Roles.Where(p => p.Name == "Administrator").FirstOrDefault().Id;
var userID = dbContext.Users.Where(p => p.Email == configuration["Email"]).FirstOrDefault().Id;
var userRole = new IdentityUserRole<string>()
{
RoleId = roleID,
UserId = userID
};
dbContext.UserRoles.Add(userRole);
await dbContext.SaveChangesAsync();
}
}
}
private static List<IdentityRole> Roles = new List<IdentityRole>()
{
new IdentityRole { Name = "Administrator", NormalizedName = "Administrator", ConcurrencyStamp = Guid.NewGuid().ToString() },
new IdentityRole { Name = "Manager", NormalizedName = "Manager", ConcurrencyStamp = Guid.NewGuid().ToString() },
new IdentityRole { Name = "User", NormalizedName = "User", ConcurrencyStamp = Guid.NewGuid().ToString()}
};
}
Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddTransient<IEmailSender, EmailSender>();
services.AddMvc();
}
Refer to Cannot access a disposed object in ASP.NET Core when injecting DbContext

Xamarin.Forms.Maps.Map sa as image

I can`t find how to save map (Xamarin.Forms.Maps.Map) as image? There is common approach for iOS and Android? There is possible create implementation in common project?
Thanks for answer.
I have resolved problem, that is working code
public class ScreenshotManager : IScreenshotManager
{
private readonly Activity _activity;
public ScreenshotManager(Activity activity)
{
_activity = activity;
}
public async Task<byte[]> CaptureMapAsync(Map map)
{
if (_activity == null)
{
throw new Exception("You have to set ScreenshotManager.Activity in your Android project");
}
var renderer = Platform.GetRenderer(map);
Platform.SetRenderer(map, renderer);
var viewRenderer = renderer.View;
var mapRenderer = (MapRenderer)viewRenderer;
var mapView = mapRenderer.Control;
var mapDownloaderTask = new TaskCompletionSource<byte[]>();
mapView.GetMapAsync(new OnMapReadyCallback(m =>
{
mapDownloaderTask.SetResult(m);
}));
return await mapDownloaderTask.Task;
}
}
internal class OnMapReadyCallback : Java.Lang.Object, IOnMapReadyCallback
{
readonly Action<byte[]> handler;
public OnMapReadyCallback(Action<byte[]> handler)
{
this.handler = handler;
}
void IOnMapReadyCallback.OnMapReady(GoogleMap googleMap)
{
googleMap.Snapshot(new SnapshotMapCallBack(handler));
}
}
internal class SnapshotMapCallBack : Java.Lang.Object, GoogleMap.ISnapshotReadyCallback
{
readonly Action<byte[]> handler;
public SnapshotMapCallBack(Action<byte[]> handler)
{
this.handler = handler;
}
public void OnSnapshotReady(Bitmap snapshot)
{
using (var stream = new MemoryStream())
{
snapshot.Compress(Bitmap.CompressFormat.Png, 0, stream);
byte[] bitmapData = stream.ToArray();
handler?.Invoke(bitmapData);
}
}
}

Swagger does not recognize WebAPI controller parameter with custom attribute [FromContent]

I want to have a custom attribute to parse data as stream and be testable with Swagger.
So I created controller which reads from POST body:
[SwaggerOperation("Create")]
[SwaggerResponse(HttpStatusCode.Created)]
public async Task<string> Post([FromContent]Stream contentStream)
{
using (StreamReader reader = new StreamReader(contentStream, Encoding.UTF8))
{
var str = reader.ReadToEnd();
Console.WriteLine(str);
}
return "OK";
}
How to define stream so it is visible in Swagger UI?
Here is my implementation of FromContent attribute and ContentParameterBinding binding:
public class ContentParameterBinding : HttpParameterBinding
{
private struct AsyncVoid{}
public ContentParameterBinding(HttpParameterDescriptor descriptor) : base(descriptor)
{
}
public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
HttpActionContext actionContext,
CancellationToken cancellationToken)
{
var binding = actionContext.ActionDescriptor.ActionBinding;
if (binding.ParameterBindings.Length > 1 ||
actionContext.Request.Method == HttpMethod.Get)
{
var taskSource = new TaskCompletionSource<AsyncVoid>();
taskSource.SetResult(default(AsyncVoid));
return taskSource.Task as Task;
}
var type = binding.ParameterBindings[0].Descriptor.ParameterType;
if (type == typeof(HttpContent))
{
SetValue(actionContext, actionContext.Request.Content);
var tcs = new TaskCompletionSource<object>();
tcs.SetResult(actionContext.Request.Content);
return tcs.Task;
}
if (type == typeof(Stream))
{
return actionContext.Request.Content
.ReadAsStreamAsync()
.ContinueWith((task) =>
{
SetValue(actionContext, task.Result);
});
}
throw new InvalidOperationException("Only HttpContent and Stream are supported for [FromContent] parameters");
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public sealed class FromContentAttribute : ParameterBindingAttribute
{
public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
{
if (parameter == null)
throw new ArgumentException("Invalid parameter");
return new ContentParameterBinding(parameter);
}
}
Update
When I create Stream using [FromBody] is shows correctly in Swagger, however Stream is not initiated and ==null
[SwaggerOperation("Create")]
[SwaggerResponse(HttpStatusCode.Created)]
public async Task<string> Post([FromBody]Stream contentStream)
{
using (StreamReader reader = new StreamReader(contentStream, Encoding.UTF8))
{
var str = reader.ReadToEnd();
Console.WriteLine(str);
}
return "OK";
}
So I want to have the same UI but with my custom attribute which let's me have Stream from content.
With my custom attribute it shows without TextArea for the parameter but could be tested using Postman and work correctly and Stream is available
Inherit your binding from FormatterParameterBinding class:
public class ContentParameterBinding : FormatterParameterBinding
{
public ContentParameterBinding(HttpParameterDescriptor descriptor)
: base(descriptor,
descriptor.Configuration.Formatters,
descriptor.Configuration.Services.GetBodyModelValidator())
{
}
//your code
}
Try implementing the interface IValueProviderParameterBinding:
public class ContentParameterBinding
: HttpParameterBinding, IValueProviderParameterBinding
{
public IEnumerable<ValueProviderFactory> ValueProviderFactories
{
get
{
return this.Descriptor.Configuration.Services.GetValueProviderFactories();
}
}
}
In my case it helped.
Also it's generally cleaner as it doesn't inherit FormatterParameterBinding logic, which may not be required.

Web API Serialize/Deserialize Derived types

I have a Web API that returns a list of objects, when the client passes Accept application/json I want my globally registered json formatter to include TypeNameHandling for the derived types during serialization. However this doesn't work and I can't see why this shouldn't work ?
My objects
public class BaseClass
{
public int Id { get; set; }
}
public class SubClass : BaseClass
{
public string SubClassProp { get; set; }
}
public class SubClassA : SubClass
{
public string SubClassAProp { get; set; }
}
public class SubClassB : SubClass
{
public string SubClassBProp { get; set; }
}
WebApiConfig
public static void Register(HttpConfiguration config)
{
var formatters = GlobalConfiguration.Configuration.Formatters;
var jsonFormatter = formatters.JsonFormatter;
var settings = jsonFormatter.SerializerSettings;
settings.Formatting = Formatting.Indented;
settings.NullValueHandling = NullValueHandling.Ignore;
settings.TypeNameHandling = TypeNameHandling.Auto;
}
Web API Controller
public class MyController : ApiController
{
[HttpGet]
public async Task<IList<BaseClass>> GetClasses()
{
return new List<BaseClass>
{
new SubClassA
{
Id = 1,
SubClassProp = "SubClass",
SubClassAProp = "SubClassAProp"
},
new SubClassB
{
Id = 2,
SubClassProp = "SubClass",
SubClassBProp = "SubClassBProp"
}
};
}
}
Call from API Client in same solution
var client = new HttpClient() { BaseAddress = new Uri("uri goes here...")}
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var resp = await client.GetAsync("uri goes here..."));
var jsonContent = await resp.Content.ReadAsStringAsync();
var ListOfClasses = JsonConvert.DeserializeObject<IList<BaseClass>>(jsonContent, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto });
I'am expecting to get one element which is SubClassA and one that is SubClassB, but both is BaseClass ?
I also want it to be possible to Deserialize json to object in Post method.
And this should be possible for both json and xml

Resources