I have made a custom binder for an abstract class. The binder decides which implementation to use.
It works well, but when I add a property which does not exist in the abstract class to a child class, it is always null.
Here is the code for the abstract class Pet and derived classes Dog and Cat.
public abstract class Pet
{
public string name { get; set; }
public string species { get; set; }
abstract public string talk { get; }
}
public class Dog : Pet
{
override public string talk { get { return "Bark!"; } }
}
public class Cat : Pet
{
override public string talk { get { return "Miaow."; } }
public string parasite { get;set; }
}
public class DefaultPetBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext,ModelBindingContext bindingContext,Type modelType)
{
bool hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName);
string prefix = ((hasPrefix)&&(bindingContext.ModelName!="")) ? bindingContext.ModelName + "." : "";
// get the parameter species
ValueProviderResult result;
result = bindingContext.ValueProvider.GetValue(prefix+"species");
if (result.AttemptedValue.Equals("cat")){
//var model = base.CreateModel(controllerContext, bindingContext, typeof(Cat));
return base.CreateModel(controllerContext,bindingContext,typeof(Cat));
}
else (result.AttemptedValue.Equals("dog"))
{
return base.CreateModel(controllerContext,bindingContext,typeof(Dog));
}
}
}
The controller just takes a Pet parameter and returns it as JSON.
If I send
{name:"Odie", species:"dog"}
I get back
{"talk":"Bark!","name":"Odie","species":"dog"}
For the Cat, there is a parasite property which does not exist in the abstract class Pet. If I send
{"parasite":"cockroaches","name":"Oggy","species":"cat"}
I get back
{"talk":"Miaow.","parasite":null,"name":"Oggy","species":"cat"}
I have tried this with other more complex classes, this is just a simple example.
I have looked in the debugger, the parasite value is in the value provider, the model the binder returns contains a field for the parasite.
Can anyone see where the problem is?
Try like this:
protected override object CreateModel(ControllerContext controllerContext,ModelBindingContext bindingContext,Type modelType)
{
bool hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName);
string prefix = ((hasPrefix)&&(bindingContext.ModelName!="")) ? bindingContext.ModelName + "." : "";
// get the parameter species
ValueProviderResult result;
result = bindingContext.ValueProvider.GetValue(prefix+"species");
if (result.AttemptedValue.Equals("cat"))
{
var model = Activator.CreateInstance(typeof(Cat));
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, typeof(Cat));
return model;
}
else if (result.AttemptedValue.Equals("dog"))
{
var model = Activator.CreateInstance(typeof(Dog));
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, typeof(Dog));
return model;
}
throw new Exception(string.Format("Unknown type \"{0}\"", result.AttemptedValue));
}
Related
I am trying to print out an object that implements TableEntity class, without those that should be Ignored regarding the persistence. The approach I generally use to print out objects is to use the StatePrinter.
public class MyEntity : TableEntity
{
public string MyProperty { get; set; }
[IgnoreProperty]
public string MyIgnoredProperty { get; set; }
public override string ToString()
{
Stateprinter printer = new Stateprinter();
return printer.PrintObject(this);
}
}
While this works pretty good for any kind of classes, with this MyEntity class it also prints the MyIgnoredProperty. Is there a clever way to also ignore the properties that have [IgnoredProperty] as attribute when printing out the object?
You can configure what fields/properties the Stateprinter cares about by configuring what "field harvester" to use.
Here's a simple field harvester that only returns public properties without the 'IgnoreProperty' attribute.
class PersistencePropertiesHarvester : IFieldHarvester
{
public bool CanHandleType(Type type)
{
return typeof(TableEntity).IsAssignableFrom(type);
}
public List<SanitizedFieldInfo> GetFields(Type type)
{
var fields = new HarvestHelper().GetFieldsAndProperties(type);
return fields.Where(IsPerstistenceProperty).ToList();
}
private static bool IsPerstistenceProperty(SanitizedFieldInfo field)
{
return
// Only return properties ...
field.FieldInfo.MemberType == MemberTypes.Property
&&
// ... that has a public get method ...
(field.FieldInfo as PropertyInfo)?.GetGetMethod(false) != null
&&
// ... that does not have the IgnoreProperty attribute
field.FieldInfo.GetCustomAttribute<IgnoreProperty>() == null
;
}
}
Then you use it like this:
public class MyEntity : TableEntity
{
public string MyProperty { get; set; }
[IgnoreProperty]
public string MyIgnoredProperty { get; set; }
public override string ToString()
{
Stateprinter printer = new Stateprinter();
printer.Configuration.Add(new PersistencePropertiesHarvester());
return printer.PrintObject(this);
}
}
And the result of new MyEntity().ToString() is now
new MyEntity()
{
MyProperty = null
}
I have one interface and two classes that implements it.
namespace FirebaseNet.Messaging
{
public interface INotification
{
string Title { get; set; }
}
public class AndroidNotification : INotification
{
public string Title { get; set; }
}
public class IOSNotification : INotification
{
public string Title { get; set; }
}
}
Now I have another class like this.
public class Message
{
public INotification Notification { get; set; }
}
The Message parameter is passed to the class like this
[HttpPost]
public async Task<JsonResult> SendMessage(Message message)
This parameter can be either
var message = new Message()
{
Notification = new AndroidNotification()
{
Title = "Portugal vs. Denmark"
}
};
or
var message = new Message()
{
Notification = new IOSNotification()
{
Title = "Portugal vs. Denmark"
}
};
So far, all works. Now I want to AJAX POST to SendMessage. I tried using this DTO.
JavaScript Code
var message = {
Notification : {
Title : "Portugal vs. Denmark"
}
};
This obviously failed with
Cannot create an instance of an interface.
What would be an ideal workaround for it?
P.S: Thought of changing the Message class to
public class Message
{
public INotification Notification { get; set; }
public AndroidNotification AndroidNotification { get; set; }
public IOSNotification IOSNotification { get; set; }
}
It's a third party DLL and I don't wanna touch it ideally.
One way to achieve that would be to write a custom model binder:
public class NotificationModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var typeValue = bindingContext.ValueProvider.GetValue("ModelType");
var type = Type.GetType(
(string)typeValue.ConvertTo(typeof(string)),
true
);
if (!typeof(INotification).IsAssignableFrom(type))
{
throw new InvalidOperationException("Bad Type");
}
var model = Activator.CreateInstance(type);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
return model;
}
}
that you would register when bootstrapping your application:
ModelBinders.Binders.Add(
typeof(INotification),
new NotificationModelBinder()
);
this would now allow the client to specify the concrete type of the notification:
var message = {
ModelType: "WebApplication1.Models.AndroidNotification",
Notification : {
Title : "Portugal vs. Denmark"
}
};
or:
var message = {
ModelType: "WebApplication1.Models.IOSNotification",
Notification : {
Title : "Portugal vs. Denmark"
}
};
Of course you could tweak the model binder for the exact property name that indicates the concrete type and the possible values. In my example the fully qualified type name should be used but you could have some mapping with friendlier names.
I am trying to make a binder for an abstract class. The binder decides which implementation of the class to use.
public abstract class Pet
{
public string name { get; set; }
public string species { get; set; }
abstract public string talk { get; }
}
public class Dog : Pet
{
override public string talk { get { return "Bark!"; } }
}
public class Cat : Pet
{
override public string talk { get { return "Miaow."; } }
}
public class Livestock : Pet
{
override public string talk { get { return "Mooo. Mooo. Fear me."; } }
}
So I have a controller which takes a Pet, the binder decides (depending on the species string) if it is a Dog, Cat or Livestock.
public class PetBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var values = (ValueProviderCollection)bindingContext.ValueProvider;
var name = (string)values.GetValue("name").ConvertTo(typeof(string));
var species = (string)values.GetValue("species").ConvertTo(typeof(string));
if (species == "dog")
{
return new Dog { name = name, species = "dog" };
}
else if (species == "cat")
{
return new Cat { name = name, species = "cat" };
}
else
{
return new Livestock { name = name, species = species };
}
}
}
public class HomeController : Controller
{
public JsonResult WorksFine(Pet pet)
{
return Json(pet);
}
public JsonResult DoesntWork(List<Pet> pets)
{
return Json(pets);
}
}
This works well, but as soon as the pet is in another structure (like List<Pet> or another object), I get a NullReferenceException (on the line var name = (string)values.GetValue("name").ConvertTo(typeof(string));
in the PetBinder). What am I doing wrong?
I added a Person class to test. It also gave me a NullReferenceException.
public class Person
{
public string name { get; set; }
public Pet pet { get; set; }
}
public class HomeController : Controller
{
public JsonResult PersonAction(Person p)
{
return Json(p);
}
}
ccurrens said the reason var name = (string)values.GetValue("name").ConvertTo(typeof(string));
returned null was because it couldn't get the values from a list.
I see they are named [n].name and [n].species when in a List<Pet>, but when in the Person object they are named pet.name and pet.species and when they are in a single Pet, they are just named name and species.
Solution
To get the parameter names with the right prefix ([n] or pet or anything else) for GetValue, I used the following code:
bool hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName);
string prefix = ((hasPrefix)&&(bindingContext.ModelName!="")) ? bindingContext.ModelName + "." : "";
If anyone is interested, I ended up inheriting from DefaultModelBinder using something similar to this answer. Here is the full code I used:
public class DefaultPetBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext,ModelBindingContext bindingContext,Type modelType)
{
//https://stackoverflow.com/questions/5460081/asp-net-mvc-3-defaultmodelbinder-inheritance-problem
bool hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName);
string prefix = ((hasPrefix)&&(bindingContext.ModelName!="")) ? bindingContext.ModelName + "." : "";
// get the parameter species
ValueProviderResult result;
result = bindingContext.ValueProvider.GetValue(prefix+"species");
if (result.AttemptedValue.Equals("cat"))
return base.CreateModel(controllerContext,bindingContext,typeof(Cat));
else if (result.AttemptedValue.Equals("dog"))
return base.CreateModel(controllerContext,bindingContext,typeof(Dog));
return base.CreateModel(controllerContext, bindingContext, typeof(Livestock)); // livestock
}
}
In the line you're getting your error, values could be null or what GetValue("name") returns could be null.
I'm assuming when you're calling the List<Pet> method, ValueProvider is returning the entire List instead of each individual Pet, so it can't get the value "name" since it doesn't exist in the List class.
I can't be more sure without seeing more code.
I use different Models in my CreateView, all inherit from BaseModel. To call the right EditorFor I have created a HtmlHelper that gets the Model and the actual property. But I donĀ“t know how to invoke it.
BaseModel:
public abstract class BaseModel
{
protected IEnumerable<PropertyInfo> PropertyInfoCache { get; set; }
protected IEnumerable<EnumeratedProperty> EnumeratedPropertyCache { get; set; }
protected BaseModel()
{
PropertyInfoCache = this.GetType().GetProperties();
EnumeratedPropertyCache = PropertyInfoCache.Select(p=> new EnumeratedProperty(p.Name,p.GetType()));
}
public IEnumerable<EnumeratedProperty> EnumerateProperties()
{
return EnumeratedPropertyCache;
}
public object GetPropertyValue(string PropertyName)
{
var property = PropertyInfoCache.SingleOrDefault(i=>i.Name==PropertyName);
if(property!=null)
return property.GetValue(this,null);
return null;
}
}
public class EnumeratedProperty
{
public string Name { get; private set; }
public Type Type { get; private set; }
public EnumeratedProperty(string PropertyName, Type PropertyType)
{
this.Name = PropertyName;
this.Type = PropertyType;
}
}
in my View:
#foreach (var property in Model.EnumerateProperties())
{
#Html.EditorForProperty(Model,property);
}
HtmlHelper:
public static class ExtensionMethods
{
public static MvcHtmlString EditorForProperty(this HtmlHelper html, BaseModel Model, EnumeratedProperty property)
{
// creates an error: The type arguments for method 'EditorFor' cannot be inferred from the usage. Try specifying the type arguments explicitly.
return System.Web.Mvc.Html.EditorExtensions.EditorFor(html, Model => Model.GetPropertyValue(property.Name) );
}
}
EditorFor recognizes the type of an object, so what you would want to do is extract the type from the value in the EnumeratedProperty class, instead of passing the class directly, and pass in the value from it too.
I am new about attributes. I just try it on my console application.
So how can i validate my person instance below example ?
class Person
{
[StringLength(8,ErrorMessage="Please less then 8 character")]
public string Name { get; set; }
}
Here is simple code example without reflection.
class Program
{
static void Main(string[] args)
{
var invalidPerson = new Person { Name = "Very long name" };
var validPerson = new Person { Name = "1" };
var validator = new Validator<Person>();
Console.WriteLine(validator.Validate(validPerson).Count);
Console.WriteLine(validator.Validate(invalidPerson).Count);
Console.ReadLine();
}
}
public class Person
{
[StringLength(8, ErrorMessage = "Please less then 8 character")]
public string Name { get; set; }
}
public class Validator<T>
{
public IList<ValidationResult> Validate(T entity)
{
var validationResults = new List<ValidationResult>();
var validationContext = new ValidationContext(entity, null, null);
Validator.TryValidateObject(entity, validationContext, validationResults, true);
return validationResults;
}
}
The only function that Attribute can handle is describe, provide some descriptive data with member. They are purely passive and can't contain any logic. (There are some AOP frameworks that can make attributes active). So if you want logic you have to create another class that will read attributes using MemberInfo.GetCustomAttributes and do the validation and return results.
Below code shows how to determine validation for only properties and give idea validation for methods ,classes etc.
public class DataValidator
{
public class ErrorInfo
{
public ErrorInfo(string property, string message)
{
this.Property = property;
this.Message = message;
}
public string Message;
public string Property;
}
public static IEnumerable<ErrorInfo> Validate(object instance)
{
return from prop in instance.GetType().GetProperties()
from attribute in prop.GetCustomAttributes(typeof(ValidationAttribute), true).OfType<ValidationAttribute>()
where !attribute.IsValid(prop.GetValue(instance, null))
select new ErrorInfo(prop.Name, attribute.FormatErrorMessage(string.Empty));
}
}
After adding this code to project we can use it like:
var errors =DataValidator.Validate(person);
foreach (var item in errors)
{
Console.WriteLine(item.Property +" " + item.Message);
}