I have REST API in .net5.0.
My controller inherits ControllerBaseand has the ApiController attribute.
My simple model and I need to bind value from Claim to a single property.
My model looks like this:
public class CreatePlugin
{
[Required]
[JsonProperty("name"), JsonPropertyName("name")]
public string Name { get; set; }
[Required]
[JsonProperty("description"), JsonPropertyName("description")]
public string Description { get; set; }
[Required]
[JsonProperty("version"), JsonPropertyName("version")]
public string Version { get; set; }
[Required]
[JsonProperty("public"), JsonPropertyName("public")]
public bool Public { get; set; }
[BindProperty(BinderType = typeof(CreatedByModelBinder))]
[ModelBinder(BinderType = typeof(CreatedByModelBinder))]
[Newtonsoft.Json.JsonConverter(typeof(CreatedByConverter))]
[SwaggerIgnore]
[JsonProperty("created_by"), JsonPropertyName("created_by")]
public int CreatedBy { get; set; }
}
and a simple controller method:
[HttpPost]
[Produces("application/json")]
[ProducesResponseType(typeof(Plugin), (int)HttpStatusCode.OK)]
public async Task<IActionResult> Add(CreatePlugin plugin)
{
//some logic
return Ok();
}
My request looks like this:
{
"name": "test",
"description": "sample description",
"version": "1.0",
"public": true
}
Because of ApiController attribute, my model binder isn't working. The same happens when I remove ApiController attribute from my controller and add FromBody to Add method parameter (ref: Custom model binder not firing for a single property in ASP.NET Core 2).
When I remove both attributes, the binder works fine, but the rest of the dto is empty.
I've tried using a custom JsonConverter, but it only works if I pass something in the created_by field.
How can I bind a single property of the model to something that isn't coming from the request data (in my case to claims)?
My workaround for now is to manually assign property value in each method like so:
plugin.CreatedBy = _identityContext.UserId;
Most of my models have CreatedBy and EditedBy properties, and I'd like to automatically fill them with a specific value (in this case with a value from a Claim)
You can custom the model binding like below:
public class CreatedByModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var claim = int.Parse(bindingContext.HttpContext.User.FindFirst("KeyName").Value);
bindingContext.Result = ModelBindingResult.Success(claim);
return Task.CompletedTask;
}
}
Model:
public class CreatePlugin
{
[Required]
[JsonProperty("name"), JsonPropertyName("name")]
public string Name { get; set; }
//...
[NSwag.Annotations.SwaggerIgnore]
[ModelBinder(BinderType = typeof(CreatedByModelBinder))]
[JsonProperty("created_by"), JsonPropertyName("created_by")]
public int CreatedBy { get; set; }
}
Controller:
Not sure how do you add the claims, I just add the claim in cookie authentication.
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAsync()
{
var claims = new List<Claim>
{
new Claim("KeyName","4")
};
var authProperties = new AuthenticationProperties
{
IssuedUtc = DateTimeOffset.UtcNow,
ExpiresUtc = DateTimeOffset.UtcNow.AddHours(1),
IsPersistent = false
};
const string authenticationType = "Cookies";
var claimsIdentity = new ClaimsIdentity(claims, authenticationType);
await HttpContext.SignInAsync(authenticationType, new ClaimsPrincipal(claimsIdentity), authProperties);
return Ok();
}
[HttpPost]
[Produces("application/json")]
public async Task<IActionResult> Add([FromForm]CreatePlugin plugin) //add FromForm....
{
//some logic
return Ok();
}
}
Result:
UPDATE:
If you must using FromBody, you can custom JsonConverter for the model:
public class CreatedByConverter : Newtonsoft.Json.JsonConverter
{
private readonly IHttpContextAccessor httpContextAccessor;
public CreatedByConverter(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(CreatePlugin));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
{
var claim = int.Parse(httpContextAccessor.HttpContext.User.FindFirst("KeyName").Value);
JObject obj = JObject.Load(reader);
CreatePlugin root = new CreatePlugin();
root.Name = (string)obj["name"];
root.Description = (string)obj["description"];
root.Version = (string)obj["version"];
root.Public = (bool)obj["public"];
root.CreatedBy = claim;
return root;
}
public override void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
{
JToken t = JToken.FromObject(value);
if (t.Type != JTokenType.Object)
{
t.WriteTo(writer);
}
else
{
JObject o = (JObject)t;
o.WriteTo(writer);
}
}
}
Model:
Note: If you get the claim from HttpContext, you need inject the service and cannot use JsonConverter attribute any more. You need register it in Startup.cs
public class CreatePlugin
{
[Required]
[JsonProperty("name"), JsonPropertyName("name")]
public string Name { get; set; }
//more property...
[NSwag.Annotations.SwaggerIgnore]
//[Newtonsoft.Json.JsonConverter(typeof(CreatedByConverter))]
[JsonProperty("created_by"), JsonPropertyName("created_by")]
public int CreatedBy { get; set; }
}
Controller:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAsync()
{
//add claims like the first answer..
return Ok();
}
[HttpPost]
[Produces("application/json")]
public async Task<IActionResult> Add(CreatePlugin plugin)
{
//some logic
return Ok(plugin);
}
}
Startup.cs:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
var httpContextAccessor = new HttpContextAccessor();
services.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.Converters.Add(new CreatedByConverter(httpContextAccessor));
});
services.AddSingleton<IHttpContextAccessor>(httpContextAccessor);
services.AddSwaggerDocument();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o => o.LoginPath = new PathString("/account/login"));
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHttpsRedirection();
app.UseOpenApi();
app.UseSwaggerUi3();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
Related
In ASP.NET Core-6 Web API, I am implementing Fluent Validation. I have this code.
Model:
public class OAuthLoginRequest
{
public string username { get; set; }
public string password { get; set; }
}
public class OAuthLoginResponse
{
public string response_code { get; set; }
public string response_description { get; set; }
public Data data { get; set; }
public int size { get; set; }
public string access_token { get; set; }
public string refresh_token { get; set; }
public string expires_in { get; set; }
public string token_type { get; set; }
}
Validation:
public class OAuthLoginRequestValidator : AbstractValidator<OAuthLoginRequest>
{
public OAuthLoginRequestValidator()
{
RuleFor(user => user.username)
.NotNull()
.NotEmpty().WithMessage("Username field is required.");
RuleFor(user => user.password)
.NotNull()
.NotEmpty().WithMessage("Password field is required.");
}
}
AuthService:
public async Task<OAuthLoginResponse> Login(OAuthLoginRequest payload)
{
var response = new OAuthLoginResponse();
using (var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
try
{
var authEndpoint = _config.GetSection("Endpoints").GetValue<string>("authEndpoint");
string url = baseUrl + authEndpoint;
var request = new OAuthLoginRequest
{
username = payload.username,
password = payload.password
};
var header = new Dictionary<string, string> { };
var httpResponse = await _httpHelper.PostOrPutRequest(uri: url, methodType: HttpMethod.Post, model: request, headers: header);
if (httpResponse != null)
{
if (httpResponse.StatusCode == HttpStatusCode.OK)
{
var content = await httpResponse.Content.ReadAsStringAsync();
response = JsonConvert.DeserializeObject<OAuthLoginResponse>(content);
}
}
transaction.Complete();
}
catch (Exception ex)
{
_logger.Error("An Error occured " + ex.ToString());
response = null;
}
return response;
}
}
Controller:
[HttpPost]
[Route(ApiRoutes.Login)]
public async Task<ActionResult<OAuthLoginResponse>> Login([FromBody] OAuthLoginRequest request)
{
var result = await _myService.Login(request);
return Ok(result);
}
Dependency Injection:
public static class DIServiceExtension
{
public static void AddDependencyInjection(this IServiceCollection services)
{
// Validator
services.AddTransient<IValidator<OAuthLoginRequest>, OAuthLoginRequestValidator>();
}
}
Program.cs:
builder.Services.AddControllers()
.AddFluentValidation(options =>
{
// Automatic Validation
options.AutomaticValidationEnabled = false;
// Automatic registration of validators in assembly
options.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly());
});
// Register Dependency Injection Service Extension
builder.Services.AddDependencyInjection();
var app = builder.Build();
I registered it in DIServiceExtension and then in Program.cs.
I deliberately post the Login without username and password, but the application did not display any validation message.
This is what I got in Postman:
Response body
{
"response_code": null,
"response_description": null,
"data": null,
"size": 0,
"access_token": null,
"refresh_token": null,
"expires_in": null,
"token_type": null
}
I expected it to display the validation message.
How can I resolve this?
From this GitHub comment,
options.AutomaticValidationEnabled = false;
is used to disable the automatic validation feature.
Approach 1: Automatic validation
Remove options.AutomaticValidationEnabled = false; from registering FluentValidation services.
Enable automatic validation with builder.Services.AddFluentValidationAutoValidation();.
using FluentValidation.AspNetCore;
builder.Services.AddControllers()
.AddFluentValidation(options =>
{
// Automatic registration of validators in assembly
options.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly());
});
builder.Services.AddFluentValidationAutoValidation();
Reference: FluentValidation/FluentValidation.AspNetCore (Automatic Validation section)
Approach 2: Manual validation
In the Controller, get the injected IValidator<OAuthLoginRequest> service.
In the Login action, manually perform validation via await _validator.ValidateAsync(request);.
If fail validation, add the error(s) from the ValidationResult into ModelState and return the response with BadRequest.
public class AuthController : Controller
{
private readonly IValidator<OAuthLoginRequest> _validator;
public AuthController(IValidator<OAuthLoginRequest> validator)
{
_validator = validator;
}
[HttpPost]
[Route(ApiRoutes.Login)]
public async Task<ActionResult<OAuthLoginResponse>> Login([FromBody] OAuthLoginRequest request)
{
ValidationResult validationResult = await _validator.ValidateAsync(request);
if (!validationResult.IsValid)
{
// Add error into ModelState
validationResult.AddToModelState(ModelState);
return BadRequest(ModelState);
}
var result = await _myService.Login(request);
return Ok(result);
}
}
public static class FluentValidationExtensions
{
public static void AddToModelState(this ValidationResult result, ModelStateDictionary modelState)
{
foreach (var error in result.Errors)
{
modelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
}
}
Reference: FluentValidation documentation (Manual Validation section)
I'm struggling to find a way to catch an exception thrown by a model property (actually by its type struct), which must be bound to a POST request body data.
I have a general scenario where I need to treat very specific data types, so I'm using structs to validate them accordingly each case.
Despite of the following codes are just drafts, all suggestions are very welcome!
So the following is an example of a Controller:
[ApiController]
[TypeFilter(typeof(CustomExceptionFilter))]
public class OrdersController : ControllerBase
{
public OrdersController(ILogger<OrdersController> logger, IDataAccess dataAccess)
{
_dataAccess = dataAccess;
_logger = logger;
}
private readonly IDataAccess _dataAccess;
private readonly ILogger<OrdersController> _logger;
[EnableCors]
[Route("api/[controller]/Sales")]
[HttpPost]
public async Task<ActionResult> PostSaleAsync(
[FromBody] SaleOrder saleOrder)
{
try
{
Guid saleOrderId = Guid.NewGuid();
saleOrder.SaleOrderId = saleOrderId;
foreach (SaleOrderItem item in saleOrder.items)
item.SaleOrderId = saleOrderId;
OrderQuery query = new OrderQuery(_dataAccess);
await query.SaveAsync(saleOrder);
_dataAccess.Commit();
var response = new
{
Error = false,
Message = "OK",
Data = new
{
SaleOrderId = saleOrderId
}
};
return Ok(response);
}
catch (DataAccessException)
{
_dataAccess.Rollback();
//[...]
}
//[...]
}
}
and an example of a model, Order, and a struct, StockItemSerialNumber:
public class SaleOrder : Order
{
public Guid SaleOrderId { get => OrderId; set => OrderId = value; }
public Guid CustomerId { get => StakeholderId; set => StakeholderId = value; }
public Guid? SellerId { get; set; }
public SaleModelType SaleModelType { get; set; }
public SaleOrderItem[] items { get; set; }
}
public class SaleOrderItem : OrderItem
{
public Guid SaleOrderId { get; set; }
public StockItemSerialNumber StockItemSerialNumber { get; set; }
//[JsonConverter(typeof(StockItemSerialNumberJsonConverter))]
//public StockItemSerialNumber? StockItemSerialNumber { get; set; }
}
public struct StockItemSerialNumber
{
public StockItemSerialNumber(string value)
{
try
{
if ((value.Length != 68) || Regex.IsMatch(value, #"[^\w]"))
throw new ArgumentOutOfRangeException("StockItemSerialNumber");
_value = value;
}
catch(RegexMatchTimeoutException)
{
throw new ArgumentOutOfRangeException("StockItemSerialNumber");
}
}
private string _value;
public static implicit operator string(StockItemSerialNumber value) => value._value;
public override string ToString() => _value;
}
I would like to catch ArgumentOutOfRangeException thrown by StockItemSerialNumber struct and then return a response message informing a custom error accordingly.
Since this exception is not catch by the try...catch block from Controller, I've tried to build a class that extends IExceptionFilter and add as a filter:
public class CustomExceptionFilter : IExceptionFilter
{
private readonly IWebHostEnvironment _hostingEnvironment;
private readonly IModelMetadataProvider _modelMetadataProvider;
public CustomExceptionFilter(
IWebHostEnvironment hostingEnvironment,
IModelMetadataProvider modelMetadataProvider)
{
_hostingEnvironment = hostingEnvironment;
_modelMetadataProvider = modelMetadataProvider;
}
public void OnException(ExceptionContext context)
{
context.Result = new BadRequestObjectResult(new {
Error = false,
Message = $"OPS! Something bad happened, Harry :( [{context.Exception}]."
});
}
}
Startup.cs :
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
// [...]
});
services.AddTransient<IDataAccess>(_ => new DataAccess(Config.DBCredentials));
services.AddControllers(options => options.Filters.Add(typeof(CustomExceptionFilter)));
}
But that approach also doesn't work, I mean, the ArgumentOutOfRangeException remains not being catch and crashes the execution during a POST request.
Finally, here's an example of a JSON with request data:
{
"CustomerId":"fb2b0555-6d32-404b-b2f0-50032a7e0f59",
"SellerId":null,
"items": [
{
"StockItemSerialNumber":"22B6E75510AB459B8DB2874F20C722B6F3DC19C6E474337D5F73BB87699E9A1001"
}, // Invalid
{
"StockItemSerialNumber":"022B6E755122B659B8DB2874F20C780030F3DC19C6E47465AS1673BB87699E9A1001"
} // Valid
]
}
So I appreciate any help or suggestion! Thanks!
I'm new in .NET and Web API; I create a signup user API with .NET 5.
This is my controller:
namespace SignupUsers.Controllers
{
[Route("api/signup")]
[ApiController]
public class MySignupController : ControllerBase
{
private readonly IDataRepository<UserSignup> _dataRepository;
public MySignupController(IDataRepository<UserSignup> dataRepository)
{
_dataRepository = dataRepository;
}
[HttpGet]
public IActionResult Get()
{
IEnumerable<UserSignup> userSignups = _dataRepository.GetAll();
return Ok(userSignups);
}
[HttpGet("{id}", Name = "Get")]
public IActionResult Get(int id)
{
UserSignup userSignup = _dataRepository.Get(id);
if (userSignup == null)
{
return NotFound("User Not Found!!!");
}
return Ok(userSignup);
}
[HttpPost]
public IActionResult Post([FromBody] UserSignup userSignup)
{
if (userSignup == null)
{
return BadRequest("User is null!!!");
}
_dataRepository.Add(userSignup);
return CreatedAtRoute(
"Get",
new { Id = userSignup.Id },
userSignup);
}
[HttpPut("{id}")]
public IActionResult Put(int id, [FromBody] UserSignup userSignup)
{
if (userSignup == null)
{
return BadRequest("User is null!!!");
}
UserSignup userSignupToUpdate = _dataRepository.Get(id);
if (userSignupToUpdate == null)
{
return NotFound("The User record couldn't be found");
}
_dataRepository.Update(userSignupToUpdate, userSignup);
return NoContent();
}
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
UserSignup userSignup = _dataRepository.Get(id);
if (userSignup == null)
return NotFound("The User record couldn't be found");
_dataRepository.Delete(userSignup);
return NoContent();
}
}
}
I added the DbContext to another folder:
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SignupUsers.Models;
namespace SignupUsers.Data
{
public class UserSignupDbContext : DbContext
{
public UserSignupDbContext(DbContextOptions<UserSignupDbContext> options)
: base(options)
{
}
public DbSet<Models.UserSignup> SignupUsers { get; set; }
}
}
For using code first I use EF Core.
UserSignup model class:
namespace SignupUsers.Models
{
public class UserSignup
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int Password { get; set; }
}
}
For managing input and output, I use an interface and implemented that in datamanager.cs:
namespace SignupUsers.Models
{
public class DataManager : IDataRepository<UserSignup>
{
readonly UserSignupDbContext _userSignupDbContext;
public DataManager(UserSignupDbContext context)
{
_userSignupDbContext = context;
}
public void Add(UserSignup entity)
{
_userSignupDbContext.SignupUsers.Add(entity);
_userSignupDbContext.SaveChanges();
}
public void Delete(UserSignup entity)
{
_userSignupDbContext.SignupUsers.Remove(entity);
_userSignupDbContext.SaveChanges();
}
public UserSignup Get(int id)
{
return _userSignupDbContext.SignupUsers.
FirstOrDefault(s => s.Id == id);
}
public IEnumerable<UserSignup> GetAll()
{
return _userSignupDbContext.SignupUsers.ToList();
}
public void Update(UserSignup dbEntity, UserSignup entity)
{
dbEntity.Name = entity.Name;
dbEntity.Email = entity.Email;
dbEntity.Password = entity.Password;
_userSignupDbContext.SaveChanges();
}
}
}
This is my startup:
namespace SignupUsers
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<UserSignupDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("UserSignup")));
services.AddScoped<IDataRepository<UserSignup>, DataManager>();
//services.AddCors();
services.AddControllers();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
When I post data to that (with Postman), I get back an error 415 (unsupported media type).
What can I do?
Thank you in advance for your help.
in postman, set the "content-type" as JSON (application/json)
I have a Custom model binder that will convert posted values to another model.
Issue is bindingContext.ValueProvider.GetValue(modelName) returns none even if there are values posted from client.
Action Method
[HttpPost]
public ActionResult Update([DataSourceRequest] DataSourceRequest request,
[Bind(Prefix = "models")] AnotherModel items)
{
return Ok();
}
Target Model Class
[ModelBinder(BinderType = typeof(MyModelBinder))]
public class AnotherModel
{
IEnumerable<Dictionary<string, object>> Items { get; set; }
}
Cutomer Model Binder
public class MyModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var modelName = bindingContext.ModelName;
// ISSUE: valueProviderResult is always None
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
//here i will convert valueProviderResult to AnotherModel
return Task.CompletedTask;
}
}
Quick watch shows ValueProvider does have values
UPDATE1
Inside the Update action method when i can iterate through IFormCollection, The Request.Form has all the Key and Value pair. Not sure why model binder is not able to retrieve it.
foreach (var f in HttpContext.Request.Form)
{
var key = f.Key;
var v = f.Value;
}
My example
In my client I send a header in request, this header is Base64String(Json Serialized object)
Object -> Json -> Base64.
Headers can't be multiline. With base64 we get 1 line.
All of this are applicable to Body and other sources.
Header class
public class RequestHeader : IHeader
{
[Required]
public PlatformType Platform { get; set; } //Windows / Android / Linux / MacOS / iOS
[Required]
public ApplicationType ApplicationType { get; set; }
[Required(AllowEmptyStrings = false)]
public string UserAgent { get; set; } = null!;
[Required(AllowEmptyStrings = false)]
public string ClientName { get; set; } = null!;
[Required(AllowEmptyStrings = false)]
public string ApplicationName { get; set; } = null!;
[Required(AllowEmptyStrings = true)]
public string Token { get; set; } = null!;
public string ToSerializedString()
{
return JsonConvert.SerializeObject(this);
}
}
IHeader Interface
public interface IHeader
{
}
Model Binder
public class HeaderParameterModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
StringValues headerValue = bindingContext.HttpContext.Request.Headers.Where(h =>
{
string guid = Guid.NewGuid().ToString();
return h.Key.Equals(bindingContext.ModelName ?? guid) |
h.Key.Equals(bindingContext.ModelType.Name ?? guid) |
h.Key.Equals(bindingContext.ModelMetadata.ParameterName);
}).Select(h => h.Value).FirstOrDefault();
if (headerValue.Any())
{
try
{
//Convert started
bindingContext.Model = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(headerValue)), bindingContext.ModelType);
bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
}
catch
{
}
}
return Task.CompletedTask;
}
}
Model Binder Provider
We can work with any BindingSource.
Body
BindingSource Custom
BindingSource Form
BindingSource FormFile
BindingSource Header
BindingSource ModelBinding
BindingSource Path
BindingSource Query
BindingSource Services
BindingSource Special
public class ParametersModelBinderProvider : IModelBinderProvider
{
private readonly IConfiguration configuration;
public ParametersModelBinderProvider(IConfiguration configuration)
{
this.configuration = configuration;
}
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType.GetInterfaces().Where(value => value.Name.Equals(nameof(ISecurityParameter))).Any() && BindingSource.Header.Equals(context.Metadata.BindingSource))
{
return new SecurityParameterModelBinder(configuration);
}
if (context.Metadata.ModelType.GetInterfaces().Where(value=>value.Name.Equals(nameof(IHeader))).Any() && BindingSource.Header.Equals(context.Metadata.BindingSource))
{
return new HeaderParameterModelBinder();
}
return null!;
}
}
In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.ModelBinderProviders.Insert(0,new ParametersModelBinderProvider(configuration));
});
}
Controller action
ExchangeResult is my result class.
[HttpGet(nameof(Exchange))]
public ActionResult<ExchangeResult> Exchange([FromHeader(Name = nameof(RequestHeader))] RequestHeader header)
{
//RequestHeader previously was processed in modelbinder.
//RequestHeader is null or object instance.
//Some instructions
}
If you examine the source code of MVC's CollectionModelBinder, you'd notice that values of the form "name[index]" will return ValueProviderResult.None and need to be handled separately.
It seems like you're trying to solve the wrong problem. I'd suggest binding to a standard collection class like Dictionary.
Either;
public ActionResult Update([DataSourceRequest] DataSourceRequest request,
[Bind(Prefix = "models")] Dictionary<string, RecordTypeName> items)
Or;
public class AnotherModel : Dictionary<string, RecordTypeName> {}
If you don't know what type each dictionary value will have at compile time, that's where a custom binder would come in handy.
I have a controller like this:
using Microsoft.AspNetCore.Mvc;
public class Person {
public string FirstName { get; set; }
public string LastName { get; set; }
}
[Route("api/[controller]")]
public class ValuesController : Controller {
[HttpGet]
public IActionResult Get() {
return new OkObjectResult(new[] {
new Person { FirstName = "John", LastName = "Doe" }
});
}
}
I would like to be able to specify the properties I want from the response in a asp.net core REST API.
For example, a GET to api/values should return an object with all its properties:
{
"FirstName":"John",
"LastName":"Doe"
}
While, a GET to api/values?fields=FirstName should return only the FirstName property:
{
"FirstName":"John"
}
I tried specifying a ContractResolver in my Startup class like this:
class QueryStringResolver : DefaultContractResolver {
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization) {
HttpContextAccessor httpContextAccessor = new HttpContextAccessor();
string fieldsQuery = httpContextAccessor.HttpContext.Request.Query["fields"];
if (!string.IsNullOrEmpty(fieldsQuery)) {
var fields = from f in fieldsQuery.Split(",", StringSplitOptions.RemoveEmptyEntries) select f.Trim().ToLowerInvariant();
return base.CreateProperties(type, memberSerialization).Where(p => fields.Contains(p.PropertyName.ToLowerInvariant())).ToList();
} else {
return base.CreateProperties(type, memberSerialization);
}
}
}
public class Startup {
public void ConfigureServices(IServiceCollection services) {
services
.AddMvc()
.AddJsonOptions(options => {
options.SerializerSettings.ContractResolver = new QueryStringResolver();
});
}
// rest of Startup class omitted...
}
The problem is that the CreateProperties method is invoked only at the first request, and not for each request.
Is there a way to specify which properties should be serialized at each request?
Please note that I don't want to change the Get method implementation or the returned class definition, I would just like to act on the serialization, so that I can re-use the same query string parameter in several methods.
Create a generic object with the values you desire.
var obj = new {
FirstName = "John"
, LastName = "Doe"
};
var json = JsonConvert.SerializeObject(obj);