Custom model binder not retrieving value from value-provider - c#

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.

Related

Querystring Model Binding ASP.NET WebApi

I have the following model
public class Dog
{
public string NickName { get; set; }
public int Color { get; set; }
}
and I have the following api controller method which is exposed through an API
public class DogController : ApiController
{
// GET /v1/dogs
public IEnumerable<string> Get([FromUri] Dog dog)
{ ...}
Now, I would like to issue the GET request as follows:
GET http://localhost:90000/v1/dogs?nick_name=Fido&color=1
Question: How do I bind the query string parameter nick_name to property NickName in the dog class? I know I can call the API without the underscore (i.e. nickname) or change NickName to Nick_Name and get the value, but I need the names to remain like that for convention.
Edit
This question is not a duplicate because it is about ASP.NET WebApi not ASP.NET MVC 2
Implementing the IModelBinder,
public class DogModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(Dog))
{
return false;
}
var model = (Dog)bindingContext.Model ?? new Dog();
var hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName);
var searchPrefix = (hasPrefix) ? bindingContext.ModelName + "." : "";
model.NickName = GetValue(bindingContext, searchPrefix, "nick_name");
int colorId = 0;
if (int.TryParse(GetValue(bindingContext, searchPrefix, "colour"), out colorId))
{
model.Color = colorId; // <1>
}
bindingContext.Model = model;
return true;
}
private string GetValue(ModelBindingContext context, string prefix, string key)
{
var result = context.ValueProvider.GetValue(prefix + key); // <4>
return result == null ? null : result.AttemptedValue;
}
}
And Create ModelBinderProvider,
public class DogModelBinderProvider : ModelBinderProvider
{
private CollectionModelBinderProvider originalProvider = null;
public DogModelBinderProvider(CollectionModelBinderProvider originalProvider)
{
this.originalProvider = originalProvider;
}
public override IModelBinder GetBinder(HttpConfiguration configuration, Type modelType)
{
// get the default implementation of provider for handling collections
IModelBinder originalBinder = originalProvider.GetBinder(configuration, modelType);
if (originalBinder != null)
{
return new DogModelBinder();
}
return null;
}
}
and using in controller something like,
public IEnumerable<string> Get([ModelBinder(typeof(DogModelBinder))] Dog dog)
{
//controller logic
}

Detecting Extra Fields in ASP.NET Web API Request

I have been trying to get the example from this link to work.
I'm trying to detect extra fields in a json request and return an error if they are present.
Here's what I have:
ApiController:
public class MyClassController : ApiController
{
public IHttpActionResult Add(MyClass myClass)
{
if (myClass.ContainsExtra)
{
return BadRequest(ModelState);
}
...
}
...
}
DynamicObject:
public class MyClass : DynamicObject
{
private Dictionary<string, object> fields =
new Dictionary<string, object>(
StringComparer.OrdinalIgnoreCase);
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
public bool ContainsExtra { get; private set; }
[JsonIgnore]
public Dictionary<string, object> Extra
{
get { return fields; }
}
public override bool TryGetMember(GetMemberBinder binder,
out object value)
{
return fields.TryGetValue(binder.Name, out value);
}
public override bool TrySetMember(SetMemberBinder binder,
object value)
{
this.ContainsExtra = true;
fields[binder.Name] = value;
return true;
}
}
If I send this Json from Fiddler
{“FirstName”:”Test”, “LastName”:”Test”, “City”:”New York”}
The TrySetMember method should fire and it should set the bool ContainsExtra to true so it can be evaluated in the Add method on MyClassController. When it does contain an extra field, it should return an error to the client.
Unfortunately I just can't seem to get the TrySetMember to fire.
What am I Missing ?
I got the functionality that i wanted by simply setting the MissingMemberHandling setting of the JSONMediaTypeFormatter to MissingMemberHandling.Error, in my case I:
WebConfigFile:
// Set up The Json Media Type Formatter
var JsonMTF = new JsonMediaTypeFormatter
{
SerializerSettings = { MissingMemberHandling = MissingMemberHandling.Error }
};
// Clear all formatters
config.Formatters.Clear();
// Add the JSON formatter
config.Formatters.Add(JsonMTF);
Then write an ActionFilterAttribute:
public class ValidateModelFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext filterContext)
{
if (!filterContext.ModelState.IsValid)
{
filterContext.Response = filterContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, filterContext.ModelState);
}
}
}
Finally add the filter in the WebConfigFile:
// Add Validate Model Filter Attribute
config.Filters.Add(new ValidateModelFilterAttribute());

Custom Model Binder not updating

I've been working on an MVC project that has a complex model with several nested classes, and one class has another class nested in it. I can get all of the other complex types to update correctly, but this last one never updates correctly. I've made sure to register its custom model binder, which gets executed and returns an object with the proper values assigned to its properties, but the original model never gets updated.
I've snipped out everything that works, leaving my structure only below:
Classes
public class Case
{
public Case()
{
PersonOfConcern = new Person();
}
public Person PersonOfConcern { get; set; }
}
[ModelBinder(typeof(PersonModelBinder))]
public class Person
{
public Person()
{
NameOfPerson = new ProperName();
}
public ProperName NameOfPerson { get; set; }
}
[TypeConverter(typeof(ProperNameConverter))]
public class ProperName : IComparable, IEquatable<string>
{
public ProperName()
: this(string.Empty)
{ }
public ProperName(string fullName)
{
/* snip */
}
public string FullName { get; set; }
}
Model Binder
public class PersonModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(Person))
{
HttpRequestBase request = controllerContext.HttpContext.Request;
string prefix = bindingContext.ModelName + ".";
if (request.Form.AllKeys.Contains(prefix + "NameOfPerson"))
{
return new Person()
{
NameOfPerson = new ProperName(request.Form.Get(prefix + "NameOfPerson"))
};
}
}
return base.BindModel(controllerContext, bindingContext);
}
}
Controller
[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
if (CurrentUser.HasAccess)
{
Case item = _caseData.Get(id);
if (TryUpdateModel(item, "Case", new string[] { /* other properties removed */ }, new string[] { "PersonOfConcern" })
&& TryUpdateModel(item.PersonOfConcern, "Case.PersonOfConcern"))
{
// ... Save here.
}
}
}
I'm at my wits' end. The PersonModelBinder gets executed and returns the correct set of values, but the model never gets updated. What am I missing here?
i think you should add it in global asax on Application_Start
ModelBinders.Binders.Add(typeof(PersonModelBinder ), new PersonModelBinder ());

Update method in generic repository does not update navigational properties

I would like to know if entity framework automatically tracks changes in navigational properties for an entity and also updates those changes when the related entity is updated using SaveChanges(). I am using a generic repository, unit of work, ninject with code first entity framework 5 in MVC 4. I have a post and tags tables that has a many-to-many relationship. I have a join table for this relationship, named PostTagMap, with two columns, Post_id and Tag_id. When I add a new post with its associated tags, the new post is successfully added to the post table and the PostTagMap also saves the tags associated with this post. However, when I try to edit/update the post and its associated tags, only the scalar properties of the post gets updated and not the associated tags. I tried debugging and I find that the post entity to be updated does have the updated values for the tags navigational property but entity framework only generates the update statement for the post table and not for the PostTagMap. What should I do to update the tags navigational property for the post as well.
Following are the classes used
DBContext class
public class EFDbContext : DbContext
{
public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany<Tag>(p => p.Tags)
.WithMany(u => u.Posts)
.Map(m =>
{
m.ToTable("PostTagMap");
m.MapLeftKey("Post_id");
m.MapRightKey("Tag_id");
});
}
}
Post class
[Table("Post")]
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public virtual ICollection<Tag> Tags { get; set; }
}
Tag class
[Table("Tag")]
public class Tag
{
public int Id { get; set; }
public string Name { get; set; }
[JsonIgnore]
public ICollection<Post> Posts { get; set; }
}
Generic repository that implements IGenericRepository
public class EFGenericRepository<T> : IGenericRepository<T> where T : class
{
private DbContext DbContext { get; set; }
private DbSet<T> DbSet { get; set; }
public virtual void Add(T newEntity)
{
DbSet.Add(newEntity);
}
public virtual void Update(T entityToUpdate)
{
DbSet.Attach(entityToUpdate);
DbContext.Entry(entityToUpdate).State = EntityState.Modified;
}
}
Unit of work that implements IUnitOfWork
public class EfUnitOfWork : IUnitOfWork
{
private EFDbContext dbContext;
private IGenericRepository<Post> postRepository;
private IGenericRepository<Tag> tagRepository;
public EfUnitOfWork()
{
this.dbContext = new EFDbContext();
}
public void Commit()
{
dbContext.SaveChanges();
}
public IGenericRepository<Post> PostRepository
{
get
{
if (this.postRepository == null)
{
this.postRepository = new EFGenericRepository<Post>(dbContext);
}
return postRepository;
}
}
public IGenericRepository<Tag> TagRepository
{
get
{
if (this.tagRepository == null)
{
this.tagRepository = new EFGenericRepository<Tag>(dbContext);
}
return tagRepository;
}
}
private bool disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
dbContext.Dispose();
}
}
this.disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
service layer
public class ServiceRepository : IServiceRepository
{
private readonly IUnitOfWork UoW;
public ServiceRepository(IUnitOfWork _UoW)
{
UoW = _UoW;
}
public int AddPost(Post post)
{
UoW.PostRepository.Add(post);
UoW.Commit();
return post.Id;
}
public void EditPost(Post post)
{
UoW.PostRepository.Update(post);
UoW.Commit();
}
}
admin controller
public class AdminController : Controller
{
private readonly IServiceRepository _serviceRepository;
public AdminController(IServiceRepository serviceRepository)
{
_serviceRepository = serviceRepository;
}
[HttpPost, ValidateInput(false)]
public ContentResult AddPost(Post post)
{
string json;
ModelState.Clear();
if (TryValidateModel(post))
{
var id = _serviceRepository.AddPost(post);
json = JsonConvert.SerializeObject(new
{
id = id,
success = true,
message = "Post added successfully"
});
}
else
{
json = JsonConvert.SerializeObject(new
{
id = 0,
success = false,
message = "Post was not created"
});
}
return Content(json, "application/json");
}
[HttpPost, ValidateInput(false)]
public ContentResult EditPost(Post post)
{
string json;
ModelState.Clear();
if (TryValidateModel(post))
{
_serviceRepository.EditPost(post, Tags);
json = JsonConvert.SerializeObject(new
{
id = post.Id,
success = true,
message = "Post updated successfully"
});
}
else
{
json = JsonConvert.SerializeObject(new
{
id = 0,
success = false,
message = "Failed to save the changes."
});
}
return Content(json, "application/json");
}
}
I am also using a custom model binder for Post which is as follows
public class PostModelBinder : DefaultModelBinder
{
private readonly IKernel _kernel;
public PostModelBinder(IKernel kernel)
{
_kernel = kernel;
}
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var post = (Post)base.BindModel(controllerContext, bindingContext);
var _blogRepository = _kernel.Get<IServiceRepository>();
}
var tags = bindingContext.ValueProvider.GetValue("Tags").AttemptedValue.Split(',');
if (tags.Length > 0)
{
post.Tags = new List<Tag>();
foreach (var tag in tags)
{
post.Tags.Add(_serviceRepository.Tag( int.Parse(tag.Trim()) ));
}
}
return post;
}

MVC 3 model binding with underscores

I'm posting json with variables names with underscores (like_this) and attempting to bind to a model that is camelcased (LikeThis), but the values are unable to be bound.
I know I could write a custom model binder, but since the underscored convention is so common I'd expect that a solution already existed.
The action/model I'm trying to post to is:
/* in controller */
[HttpPost]
public ActionResult UpdateArgLevel(UserArgLevelModel model) {
// do something with the data
}
/* model */
public class UserArgLevelModel {
public int Id { get; set; }
public string FirstName { get; set; }
public string Surname { get; set; }
public int ArgLevelId { get; set; }
}
and the json data is like:
{
id: 420007,
first_name: "Marc",
surname: "Priddes",
arg_level_id: 4
}
(Unfortunately I can't change either the naming of either the json or the model)
You can start writing a custom Json.NET ContractResolver:
public class DeliminatorSeparatedPropertyNamesContractResolver :
DefaultContractResolver
{
private readonly string _separator;
protected DeliminatorSeparatedPropertyNamesContractResolver(char separator)
: base(true)
{
_separator = separator.ToString();
}
protected override string ResolvePropertyName(string propertyName)
{
var parts = new List<string>();
var currentWord = new StringBuilder();
foreach (var c in propertyName)
{
if (char.IsUpper(c) && currentWord.Length > 0)
{
parts.Add(currentWord.ToString());
currentWord.Clear();
}
currentWord.Append(char.ToLower(c));
}
if (currentWord.Length > 0)
{
parts.Add(currentWord.ToString());
}
return string.Join(_separator, parts.ToArray());
}
}
This is for your particular case, becase you need a snake case ContractResolver:
public class SnakeCasePropertyNamesContractResolver :
DeliminatorSeparatedPropertyNamesContractResolver
{
public SnakeCasePropertyNamesContractResolver() : base('_') { }
}
Then you can write a custom attribute to decorate your controller actions:
public class JsonFilterAttribute : ActionFilterAttribute
{
public string Parameter { get; set; }
public Type JsonDataType { get; set; }
public JsonSerializerSettings Settings { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.Request.ContentType.Contains("application/json"))
{
string inputContent;
using (var reader = new StreamReader(filterContext.HttpContext.Request.InputStream))
{
inputContent = reader.ReadToEnd();
}
var result = JsonConvert.DeserializeObject(inputContent, JsonDataType, Settings ?? new JsonSerializerSettings());
filterContext.ActionParameters[Parameter] = result;
}
}
}
And finally:
[JsonFilter(Parameter = "model", JsonDataType = typeof(UserArgLevelModel), Settings = new JsonSerializerSettings { ContractResolver = new SnakeCasePropertyNamesContractResolver() })]
public ActionResult UpdateArgLevel(UserArgLevelModel model) {
{
// model is deserialized correctly!
}

Categories

Resources