I want to scan a type for it's properties and the annotated attributes and return an object with the following structure
public class PropertyContext
{
public object PropertyValue { get; set; }
public object SourceType { get; set; }
public Attribute Annotation { get; set; }
}
I have this query
var query = from property in _target.GetType().GetProperties()
from attribute in Attribute.GetCustomAttributes(property, true)
select new PropertyContext
{
Annotation = attribute,
SourceType = _target,
};
This is executed deferred so i only generate the PropertyContext while the calling method needs them.
Now i want to fill the PropertyValue property of the PropertyContext object.
To get the value of the property i have have a call to an other component like this
_propertyValueAccessor.GetValue(_target, property)
My question is, how i can modify the query in a way that
*
the value is only read once
but only if a PropertyContext is created
How about:
var query = from property in _target.GetType().GetProperties()
let attributes = Attribute.GetCustomAttributes(property, true)
where attributes.Any()
let val = _propertyValueAccessor.GetValue(_target, property)
from attribute in attributes
select new PropertyContext
{
PropertyValue = val,
Annotation = attribute,
SourceType = _target,
};
Related
I have a get method on a .net Core 3.1 Web API controller that returns an expando object which is generated from a model class.
The model contains an enum value:
public class MyModel
{
public int Id { get; set; }
public string Name { get; set; }
public Gender Gender { get; set; }
}
public enum Gender
{
Male,
Female
}
[HttpGet("{id}")]
public async Task<IActionResult> GetAsync(int id)
{
var recordFromDB = await dbService.GetAsync(id);
if (recordFromDB == null)
return NotFound();
var returnModel = mapper.Map<MyModel>(recordFromDB).ShapeData(null);
return Ok(returnModel);
}
public static ExpandoObject ShapeData<TSource>(this TSource source, string fields)
{
var dataShapedObject = new ExpandoObject();
if (string.IsNullOrWhiteSpace(fields))
{
var propertyInfos = typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var propertyInfo in propertyInfos)
{
var propertyValue = propertyInfo.GetValue(source);
((IDictionary<string, object>)dataShapedObject).Add(propertyInfo.Name, propertyValue);
}
return dataShapedObject;
}
... more of the method here but it's never hit so I've removed the code
}
If I do a get request for this record with the accept header application/json it all works fine.
if I change the accept header to application/xml I receive the following error:
System.Runtime.Serialization.SerializationException: Type 'Gender'
with data contract name 'Gender' is not expected. Add any types not
known statically to the list of known types - for example, by using
the KnownTypeAttribute attribute or by adding them to the list of
known types passed to DataContractSerializer.
If I remove the enum value from the model the XML is generated fine.
Where do I add the KnownTypeAttribute or how do I get around this error?
Thanks in advance
Here is my development requirement,
My label values are stored in the database, and I still want to use the data annotation in a declarative way, this is to make my model more readable.
And here is my approach,
I decided to write custom DisplayNameAttribute, where the default value provided by my model will be overwritten by the value retrieved from the database.
Here is the property defined in the model,
[CustomDisplay(Name: "First Name")]
[CustomRequired(ErrorMessage: "{0} is required")]
public String FirstName { get; set; }
Here is the custom display name attribute class,
public class CustomDisplayAttribute : DisplayNameAttribute
{
private string _defaultName;
private string _displayName;
public CustomDisplayAttribute(string Name)
{
_defaultName = Name;
}
public override string DisplayName
{
get
{
if (String.IsNullOrEmpty(_displayName))
{
_displayName = DAO.RetrieveValue(**ModelName**, _defaultName);
}
return _displayName;
}
}
}
Now, you can see in the above code, ModelName is something I need, but I don't have!!
While debugging, I dig into ModelMetadataProviders.Current and can see the availability of the current model in action. But, as it is part of non-public static members I am unable to access it through my code.
I have written the below method to retrieve the model name through reflection,
private static string GetModelName()
{
var modelName = String.Empty;
FieldInfo info = typeof(CachedAssociatedMetadataProvider<CachedDataAnnotationsModelMetadata>)
.GetField("_typeIds", BindingFlags.NonPublic | BindingFlags.Static);
var types = (ConcurrentDictionary<Type, string>)info.GetValue(null);
modelName = types.FirstOrDefault().Key.Name;
return modelName;
}
But the problem is, the types collection provides me entries for all the models (visited at least once by the user). And there is no clue to know, which is currently in action!!
IMHO Attributes should not be used to make database calls. Attributes should be used to add metadata to Classes/Properties etc...
So If you're willing to change your code to be more like the Microsoft architecture for MVC then you'd have your custom Attribute and a custom ModelMetadataProvider:
public class CustomDisplayAttribute : Attribute
{
public CustomDisplayAttribute(string name)
{
Name = name;
}
public string Name { get; private set; }
}
Then a new ModelMetadataProvider:
public class DatabaseModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
public DatabaseModelMetadataProvider()
{
}
protected override ModelMetadata CreateMetadata(
IEnumerable<Attribute> attributes,
Type containerType,
Func<object> modelAccessor,
Type modelType,
string propertyName)
{
var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
var displayAttribute = containerType == null
? null as CustomDisplayAttribute
: containerType.GetProperty(propertyName)
.GetCustomAttributes(false)
.OfType<CustomDisplayAttribute>()
.FirstOrDefault();
if (displayAttribute != null)
{
var displayValue = DAO.RetrieveValue(containerType.ToString(), displayAttribute.Name)
metadata.DisplayName = displayValue;
}
return metadata;
}
}
Where
public class MyViewModel
{
public MyPropertyType PropertyName { get; set; }
}
containerType = MyViewModel
modelType = MyPropertyType
propertyName = PropertyName
Then register the provider (global.asax or whatever):
ModelMetadataProviders.Current = new LocalizedModelMetadataProvider();
Also you can take a look at the ModelMetadata it has a few other things you might want to change in the future.
I have the following viewModel:
namespace Flashcard.Models
{
public class CreateCardViewModel
{
[HiddenInput(DisplayValue = false)]
public int SetId { get; set; }
[Required]
public ICollection<Side> Sides { get; set; }
}
}
I use this ViewModel against the Card Controller:
public class CardController : Controller
{
//
// GET: /Card/
public ActionResult Create(int setId)
{
var model = new CreateCardViewModel();
var side = new Side() {Content = "Blank Side"};
model.SetId = setId;
model.Sides.Add(side);
return View(model);
}
}
However when I call the Create action, I get a nullReferenceException because model.Sides is null, which does not seem to be the same as empty. I believe I created an empty ICollection Sides in the ViewModel - why is it null in the controller?
For some context - a Card can have one or several Sides. I'm trying to always add a Side whenever a Card is created.
you need to initiate a Collection and assign it to the property of your object as follows:
public class CardController : Controller
{
//
// GET: /Card/
public ActionResult Create(int setId)
{
var model = new CreateCardViewModel();
var side = new Side() {Content = "Blank Side"};
model.SetId = setId;
model.Sides = new List<Side>();
model.Sides.Add(side);
return View(model);
}
}
Your collection is null. make one and assign to your prop.
I want to inject a UIHint attribute into a model object on the fly. I have been using the ICustomTypeDescriptor to create a class that will inject a UIHint into an instance of an object:
public sealed class UIHintDescriptionProvider : TypeDescriptionProvider
{
private string PropertyName;
private string HintValue;
public UIHintDescriptionProvider(TypeDescriptionProvider parent, string propertyName, string hintValue)
: base(parent)
{
this.PropertyName = propertyName;
this.HintValue = hintValue;
}
public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
{
return new UIHintDescriptor(base.GetTypeDescriptor(objectType, instance), this.PropertyName, this.HintValue);
}
}
public sealed class UIHintDescriptor : CustomTypeDescriptor
{
private string PropertyName;
private string HintValue;
internal UIHintDescriptor(ICustomTypeDescriptor parent, string propertyName, string hintValue)
: base(parent)
{
this.PropertyName = propertyName;
this.HintValue = hintValue;
}
public override PropertyDescriptorCollection GetProperties()
{
// Enumerate the original set of properties and create our new set with it
PropertyDescriptorCollection originalProperties = base.GetProperties();
List<PropertyDescriptor> newProperties = new List<PropertyDescriptor>();
foreach (PropertyDescriptor pd in originalProperties)
{
if (pd.Name == this.PropertyName)
{
Attribute attr = new UIHintAttribute(this.HintValue);
var newProp = TypeDescriptor.CreateProperty(typeof(object), pd, attr);
newProperties.Add(newProp);
}
else
{
newProperties.Add(pd);
}
}
// Finally return the list
return new PropertyDescriptorCollection(newProperties.ToArray(), true);
}
}
I then set this in my controller using:
UIHintDescriptionProvider provider =
new UIHintDescriptionProvider(TypeDescriptor.GetProvider(typeof(PageContentItem)), "Text",
"wysiwyg");
TypeDescriptor.AddProvider(provider, item);
Inspection in the controller of this object using the functions of TypeDescriptor indicate that this attribute has indeed been set however it does not appear in my view at all. Stepping through the MVC3 source shows all the other attributes but not the one I have just set.
Does MVC3 do any caching of object type descriptions in the background that could account for that fact?
Any other suggestions for injecting an attribute into an object instance at runtime?
This is probably because of 'timing'.
Try using a custom ModelMetadataProvider to programmatically set model property attributes like 'UIHint' or 'DisplayName' or ...
Have a look here.
I am creating a fluent HtmlHelper in MVC - to create a grid based on HTML.
I am aware of mvc contrib and WebGrid - but I am making my own and have a specific problem:
I have to enter this:
#Html.DSGridFor().AddColumn(x=>x.FirstOrDefault().Message)
but I want to be able to type this:
#Html.DSGridFor().AddColumn(x=>x.Message)
The code that gets called when I start with #Html.DSGridFor() - taking in the page based model.
public static DSGridHelper<TModel> DSGridFor<TModel>(this HtmlHelper<TModel> html)
{
return new DSGridHelper<TModel>(html);
}
and then within the class DSGridHelper I have this:
public DSGridHelper<TModel> AddColumn(Expression<Func<TModel, dynamic>> property, string HeaderText = null)
{
string ColumnName = (property.Body as MemberExpression).Member.Name;
DSGridColumn DSGC = new DSGridColumn();
DSGC.ColumnName = ColumnName;
DSGC.HeaderText = HeaderText ?? ColumnName;
DSColumnList.Add(DSGC);
return this;
}
public List<DSGridColumn> DSColumnList { get; set; }
and the column class at the moment is really basic:
public class DSGridColumn
{
public DSGridColumn()
{
}
public string ColumnName { get; set; }
public string HeaderText { get; set; }
}
I can get this code working fine with string based column names, but I want the declaring code in the razor page to be simple in format and strongly typed. At the moment I have to type x=>x.First().Message but I really only need x=>x.Message to identify the column.
I appreciate any help.
UPDATE
Thanks to Justin I can now provide my/our code.
View:
#(Html.DSGridFor3().AddColumn(x => x.Message)
.AddColumn(x => x.Host)
.ToMvcString())
HTML Helper call:
public static DSGridHelper3<T> DSGridFor3<T>(this HtmlHelper<IEnumerable<T>> htmlHelper)
{
return new DSGridHelper3<T>(htmlHelper);
}
Returning class:
public class DSGridHelper3<T>
{
private HtmlHelper _htmlHelper;
//private IEnumerable<T> _dataList;
public List<DSGridColumn> DSColumnList { get; set; }
public DSGridHelper3(HtmlHelper<IEnumerable<T>> htmlHelper)
{
_htmlHelper = htmlHelper;
// _dataList = htmlHelper.ViewData.Model;
DSColumnList = new List<DSGridColumn>();
}
public DSGridHelper3<T> AddColumn(Expression<Func<T, object>> property)
{
string columnName = (property.Body as MemberExpression).Member.Name;
DSGridColumn DSGC = new DSGridColumn();
DSGC.ColumnName = columnName;
DSGC.HeaderText = columnName;
DSColumnList.Add(DSGC);
return this;
}
public MvcHtmlString ToMvcString()
{
sb.Append("<table>");
sb.Append("<tr>");
sb.Append("<td>");
sb.Append("hello world within a table");
sb.Append(#"</td>");
sb.Append("<td>");
sb.Append("hello world within a table");
sb.Append(#"</td>");
sb.Append(#"</tr>");
sb.Append(#"</table>");
return new MvcHtmlString(sb.ToString());
}
}
UPDATE 2
If you wanted to manually insert a different type (perhaps because you are going to get a small amount of table data from ViewData rather than the model of the page) then here is some more code:
View:
#(Html.DSGridFor3<DanSoftware.MVC.Areas.Errors.Code.ELMAH_Error>().AddColumn(x => x.Message).ToMvcString();)
Alternative signature for the DSGridHelper ...helper
public static DSGridHelper3<T> DSGridFor3<T>(this HtmlHelper htmlHelper)
{
return new DSGridHelper3<T>(htmlHelper);
}
Additional constructor:
public DSGridHelper3(HtmlHelper htmlHelper)
{
_htmlHelper = htmlHelper;
// _dataList = htmlHelper.ViewData.Model;
DSColumnList = new List<DSGridColumn>();
}
Hope this helps someone and thanks Justin!
I dont have Visual Studio with me but I'll take a stab at this...
I would take in a collection as a datatype either in your DsGridFor method or in the AddColumn method. This will allow you to send Strongly-typed arguments from a collection. Say you wanted a generic method of AddColumn for a given collection with access to the class properties vs the collection methods, it would look something like this (just an example):
public static DSGridHelper<T> AddColumn<T>(this HtmlHelper<IEnumerable<T>> htmlHelper, Expression<Func<T, object>> property) where T : class
{
string columnName = (property.Body as MemberExpression).Member.Name;
DSGridColumn DSGC = new DSGridColumn();
DSGC.ColumnName = ColumnName;
DSGC.HeaderText = HeaderText ?? ColumnName;
DSColumnList.Add(DSGC);
return this;
}
For your situation, to new-up a DsGridHelper class I might explicitly set a model-type first and then add overloads as I go:
public static DSGridHelper<T> DSGridFor<T>(this HtmlHelper<IEnumerable<T>> htmlHelper) where T : class
{
return new DSGridHelper<T>(htmlHelper);
}
And then my DsGridHelper might look something like this:
public class DsGridHelper<T>
{
private HtmlHelper _htmlHelper;
private IEnumerable<T> _dataList;
public DsGridHelper(HtmlHelper<IEnumerable<T>> htmlHelper)
{
_htmlHelper = htmlHelper;
_dataList = htmlHelper.ViewData.Model;
}
public DsGridHelper<T> AddColumn(Expression<Func<T, object>> property)
{
string columnName = (property.Body as MemberExpression).Member.Name;
DSGridColumn DSGC = new DSGridColumn();
DSGC.ColumnName = ColumnName;
DSGC.HeaderText = HeaderText ?? ColumnName;
DSColumnList.Add(DSGC);
return this;
}
}