Reflection on IQueryable<T> can't find property - c#

I created a custom gridview control and exported it into a dll so I can reuse it. Inside the dll I created a function to get the DataSource, I'm trying to fill a dropdown from there but is failing.
So on my website I have this
public partial class _Management : System.Web.UI.Page
{
public class _ManagementHelper
{
public int ID;
public string CompanyName;
public string ResourceName;
}
protected void Page_Load(object sender, EventArgs e)
{
ucGridViewEx.DataSource = ucGridViewEx_Source();
ucGridViewEx.DataBind();
}
private List<dynamic> ucGridViewEx_Source()
{
var source = dl.ComapniesResources.Select(x => new _ManagementHelper
{
ID = x.ResourceID,
CompanyName = x.Supplier1.SupplierName,
ResourceName = x.Name
});
return ucGridViewEx.GridViewExDataSource(source);
}
Then the custom control inside the dll have this relevant code
public List<dynamic> GridViewExDataSource<T>(IQueryable<T> query)
{
foreach (var column in this.Columns)
{
var gridViewExColumn = column as ColumnEx;
if (gridViewExColumn != null
&& gridViewExColumn.SearchType == SearchTypeEnum.DropDownList)
{
gridViewExColumn.DropDownDataSource = query.GetDropDownDataSource(gridViewExColumn.DataField);
}
}
return ((IQueryable<dynamic>)query).ToList<dynamic>();
}
Function GetDropDownDataSource() is inside another extension class inside the same dll as the gridview
internal static List<ListItem> GetDropDownDataSource<T>(this IQueryable<T> query,
string dataField)
{
var ddlSource = new List<ListItem>();
// x =>
var xParameter = Expression.Parameter(typeof(T), "x");
// x.Property
var propery = typeof(T).GetProperty(dataField);
// x => x.Property
var columnLambda = Expression.Lambda(Expression.Property(xParameter, propery), xParameter);
return ddlSource;
}
Code fails in this where I'm assingning the value to columnLambda because property is null, not because it does not exist (it does) because is not getting any property. I tried with GetProperties() and is not returning anything.
Is curious than this is happening since I moved to the DataSource to select into _ManagementHelper. I was using a dynamic ( Select(x => new {}) ) on ucGridViewEx_Source() before and it worked perfectly. Please don't provide the solution to keep using the dynamic because I need to allow both types, with dynamic and using custom objects.

_ManagementHelper has no property. It just contains three fields (as far as you told us). So GetPrperty returns nothing. Change the members of _ManagementHelper to properties:
public class _ManagementHelper
{
public int ID { get; set; }
public string CompanyName { get; set; }
public string ResourceName { get; set; }
}

I see one bug --
var source = dl.ComapniesResources.Select(x => new _ManagementHelper
{
// ResourceID = x.ResourceID, this was the old code
ID = x.ResourceID, // fixed code
CompanyName = x.Supplier1.SupplierName,
ResourceName = x.Name
});
also, where is ListItem defined?

Related

Set property value from database

I'm trying to set property values straight from database so i could initiate new object
Object AllValues = new Object(); and access it in code like this AllValues.MediumRunTime;
using Entity Framework and it's returning 0's which it should't.
Also ReSharper is throwing Field value is never assigned warning
My table is Values{name, value, units} and contains around ~100 records.
This is my AllValues class
using System.Linq;
namespace RunTimes
{
public class AllValues
{
private long value;
public long ShortRunTime
{
get => value;
set
{
using (var data = new ModelData())
{
var prop = data.Values.FirstOrDefault(c => c.name == "ShortRunTime");
value = prop.value;
}
}
}
public long MediumRunTime
{
get => value;
set
{
using (var data = new ModelData())
{
var prop = data.Values.FirstOrDefault(c => c.name == "MediumRunTime");
value = prop.value;
}
}
}
...
And im trying to access it on Program.cs like this
AllValues av = new AllValues();
Console.WriteLine($"Medium run time: {av.MediumRunTime}");
A property setter is not the place to 'set a field with the value from the database', you use it to set the value of the property after you first loaded it from the database.
So instead of your AllValues class as it currently stands, you would have a class with some simple properties, such as:
class AllValues
{
public string ShortRunTime { get; set; }
public string MediumRunTime { get; set; }
// ...
}
and then instantiate it like this:
var av = new AllValues();
using (var data = new ModelData())
{
av.ShortRunTime = data.Values.FirstOrDefault(c => c.name == "ShortRunTime")?.Value;
av.MediumRunTime = data.Values.FirstOrDefault(c => c.name == "MediumRunTime")?.Value;
// ...
}
After this you can work with your class in memory. The 'trick' is to separate concerns: your 'transformed class' (AllValues) and loading data from the database are 2 different concerns that should be treated separately.

Using [JsonProperty("name")] in ModelState.Errors

We have a couple of models that override the name via JsonProperty, but this causes an issue when we get validation errors through ModelState. For example:
class MyModel
{
[JsonProperty("id")]
[Required]
public string MyModelId {get;set;}
}
class MyModelController
{
public IHttpActionResult Post([FromBody] MyModel model)
{
if (!ModelState.IsValid)
{
return HttpBadRequest(ModelState);
}
/* etc... */
}
}
The above Post will return the error The MyModelId field is required. which isn't accurate. We'd like this to say The id field is required.. We've attempted using [DataMember(Name="id")] but get the same result.
Question 1: Is there a way we can get ModelState errors to show the JSON property name rather than the C# property name aside from providing our own error messages on every [Required] attribute?
-- Update --
I've been playing around with this and found a "do-it-yourself" method for re-creating the error messages using custom property names. I'm really hoping there's a built-in way to do this, but this seems to do the job...
https://gist.github.com/Blackbaud-JasonTremper/b64dc6ddb460afa1698daa6d075857e4
Question 2: Can ModelState.Key be assumed to match the <parameterName>.<reflectedProperty> syntax or are there cases where this might not be true?
Question 3: Is there an easier way to determine what the JSON parameter name is expected to be rather than searching via reflection on [DataMember] or [JsonProperty] attributes?
Did you try using DisplayName attribute?
displayname attribute vs display attribute
Also, you can assign an error message to [Required] attribute.
[Required(ErrorMessage = "Name is required")]
I also faced this problem, I modified some code from your link to fit my WebAPI. modelState will also store the old key which is the variable name of the model, plus the Json Property names.
First, create the filter ValidateModelStateFilter
Add [ValidateModelStateFilter] above controller method
The filter source code:
public class ValidateModelStateFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
var descriptor = actionContext.ActionDescriptor;
var modelState = actionContext.ModelState;
if (descriptor != null)
{
var parameters = descriptor.GetParameters();
var subParameterIssues = modelState.Keys
.Where(s => s.Contains("."))
.Where(s => modelState[s].Errors.Any())
.GroupBy(s => s.Substring(0, s.IndexOf('.')))
.ToDictionary(g => g.Key, g => g.ToArray());
foreach (var parameter in parameters)
{
var argument = actionContext.ActionArguments[parameter.ParameterName];
if (subParameterIssues.ContainsKey(parameter.ParameterName))
{
var subProperties = subParameterIssues[parameter.ParameterName];
foreach (var subProperty in subProperties)
{
var propName = subProperty.Substring(subProperty.IndexOf('.') + 1);
var property = parameter.ParameterType.GetProperty(propName);
var validationAttributes = property.GetCustomAttributes(typeof(ValidationAttribute), true);
var value = property.GetValue(argument);
modelState[subProperty].Errors.Clear();
foreach (var validationAttribute in validationAttributes)
{
var attr = (ValidationAttribute)validationAttribute;
if (!attr.IsValid(value))
{
var parameterName = GetParameterName(property);
// modelState.AddModelError(subProperty, attr.FormatErrorMessage(parameterName));
modelState.AddModelError(parameterName, attr.FormatErrorMessage(parameterName));
}
}
}
}
}
}
}
private string GetParameterName(PropertyInfo property)
{
var dataMemberAttribute = property.GetCustomAttributes<DataMemberAttribute>().FirstOrDefault();
if (dataMemberAttribute?.Name != null)
{
return dataMemberAttribute.Name;
}
var jsonProperty = property.GetCustomAttributes<JsonPropertyAttribute>().FirstOrDefault();
if (jsonProperty?.PropertyName != null)
{
return jsonProperty.PropertyName;
}
return property.Name;
}
}
You can access the parameter type, get the json name, and then replace the property name with the json name. Something like this:
var invalidParameters = (from m in actionContext.ModelState
where m.Value.Errors.Count > 0
select new InvalidParameter
{
ParameterName = m.Key,
ConstraintViolations = (from msg in m.Value.Errors select msg.ErrorMessage).ToArray()
}).AsEnumerable().ToArray();
if (actionContext.ActionDescriptor.Parameters.Count == 1)
{
var nameMapper = new Dictionary<string, string>();
foreach (var property in actionContext.ActionDescriptor.Parameters[0].ParameterType.GetProperties())
{
object[] attributes = property.GetCustomAttributes(typeof(JsonPropertyNameAttribute), false);
if (attributes.Length != 1) continue;
nameMapper.Add(property.Name, ((JsonPropertyNameAttribute) attributes[0]).Name);
}
var modifiedInvalidParameters = new List<InvalidParameter>();
foreach (var invalidParameter in invalidParameters)
{
if(invalidParameter.ParameterName != null && nameMapper.TryGetValue(invalidParameter.ParameterName, out var mappedName))
{
var modifiedConstraintViolations = new List<string>();
foreach (var constraintViolation in invalidParameter.ConstraintViolations ?? Enumerable.Empty<string>())
{
modifiedConstraintViolations.Add(constraintViolation.Replace(invalidParameter.ParameterName, mappedName));
}
modifiedInvalidParameters.Add(new InvalidParameter
{
ParameterName = mappedName,
ConstraintViolations = modifiedConstraintViolations.ToArray()
});
}
else
{
modifiedInvalidParameters.Add(invalidParameter);
}
}
invalidParameters = modifiedInvalidParameters.ToArray();
}
public struct InvalidParameter
{
[JsonPropertyName("parameter_name")]
public string? ParameterName { get; set; }
[JsonPropertyName("constraint_violations")]
public string[]? ConstraintViolations { get; set; }
}

How to map two list with automapper?

I have two classes in the different location.
namespace IVR.MyEndpointApi.POCO
{
[Table("MyServiceUrl")]
public class MyURL
{
[Key]
[Column("FacilityID")]
public int FacilityId { get; set; }
[Column("Url")]
public string Url { get; set; }
}
}
namespace OpsTools.Models
{
public class MyServiceEndpoint
{
public int FacilityId { get; set; }
public string Url { get; set; }
}
}
In another method, I get the list and want to convert then return it as the desired type. I manually do it as below:
public List<MyServiceEndpoint> GetAllUrls()
{
var management = GetMyEndpointManagement();
var list = management.GetAllUrls();
var urlList = new List<MyServiceEndpoint>();
foreach (var item in list)
{
// the type of item is MyURL
var MyUrl = new MyServiceEndpoint();
myUrl.FacilityId = item.FacilityId;
myUrl.Url = item.Url;
urlList.Add(myUrl);
}
return urlList;
}
My question: can I apply AutoMapper to it?
EDIT:
I used the code:
var myUrls = management.GetAllUrls();
var urlList = new List<MyServiceEndpoint>();
Mapper.CreateMap<MyServiceEndpoint, MyURL>();
urlList = Mapper.Map<List<MyServiceEndpoint>, List<MyURL>>(myUrls);
Mapper.AssertConfigurationIsValid();
However, it has the error:
Error CS1503 Argument 1: cannot convert from 'System.Collections.Generic.List' to ....
Oops, come on. I change the order, then it works.
From
urlList = Mapper.Map<List<MyServiceEndpoint>, List<MyURL>>(myUrls);
To
urlList = Mapper.Map<List< List<MyURL>,MyServiceEndpoint>>(myUrls);
If you inspect the actual exception (you left the relevant part off), you'll see that Mapper.Map<TSource, TDestination>() tries to map from and to the wrong types.
The full error will read:
cannot convert from System.Collections.Generic.List<MyURL> to System.Collections.Generic.List<MyServiceEndpoint>
Which means that call will actually try to map from List<MyServiceEndpoint>, requiring an argument of that type, which your source list isn't.
Simply switch the types in the Map() call:
urlList = Mapper.Map<List<MyURL>, List<MyServiceEndpoint>>(myUrls);
Or remove the new list creation entirely, move the declaration and use type inference:
var urlList = Mapper.Map<List<MyServiceEndpoint>>(myUrls);

dynamic queries in dapper

I am trying to work with this. I have written a unit test as a starter for ten like so:
[Fact]
public void TestOredGuids()
{
// Arrange
const string expectedSql = "SELECT * FROM Products WHERE SomeExternalForeignKey = #SomeExternalForeignKey OR Name = SomeExternalForeignKey = #SomeExternalForeignKey";
// Act
var result = DynamicQuery.GetDynamicQuery<Product>("Products", p => p.SomeExternalForeignKey == new Guid("28D3BCFB-9472-4141-BD88-BE5E7E1230F0") || p.SomeExternalForeignKey == new Guid("0F0DBA45-F842-4E46-9ED4-F50B5BCF0509"));
// Assert
}
internal class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
public DateTime ExpiryDate { get; set; }
public int CategoryId { get; set; }
public Guid SomeExternalForeignKey { get; set; }
}
Unfortunately, I am always getting:
Additional information: 'System.Linq.Expressions.NewExpression' does not contain a definition for 'Value'
in the WalkTree method. Am I using GetDynamicQuery wrongly?
If there are any other implementation of dynamic sql mappers like this for dapper, I would appreciate any pointers. Thanks!
From what I see in the source code of the component you are trying to use, it expects the right operand of the expression to be a ConstantExpression (although for some unknown reason the author is using dynamic and expects a Value property), so to make it work, modify you code as follows
var someExternalForeignKey1 = new Guid("28D3BCFB-9472-4141-BD88-BE5E7E1230F0");
var someExternalForeignKey2 = new Guid("0F0DBA45-F842-4E46-9ED4-F50B5BCF0509");
var result = DynamicQuery.GetDynamicQuery<Product>("Products", p => p.SomeExternalForeignKey == someExternalForeignKey1 || p.SomeExternalForeignKey == someExternalForeignKey2);
Update: It turns out that the above also doesn't work, because of course it produces a closure which is not ConstantExpression. To make it work (as well as your original code), here are the required modifications of the DynamicQuery class
private static void WalkTree(BinaryExpression body, ExpressionType linkingType,
ref List<QueryParameter> queryProperties)
{
if (body.NodeType != ExpressionType.AndAlso && body.NodeType != ExpressionType.OrElse)
{
string propertyName = GetPropertyName(body);
var propertyValue = GetPropertyValue(body.Right);
string opr = GetOperator(body.NodeType);
string link = GetOperator(linkingType);
queryProperties.Add(new QueryParameter(link, propertyName, propertyValue, opr));
}
else
{
WalkTree((BinaryExpression)body.Left, body.NodeType, ref queryProperties);
WalkTree((BinaryExpression)body.Right, body.NodeType, ref queryProperties);
}
}
private static object GetPropertyValue(Expression source)
{
var constantExpression = source as ConstantExpression;
if (constantExpression != null)
return constantExpression.Value;
var evalExpr = Expression.Lambda<Func<object>>(Expression.Convert(source, typeof(object)));
var evalFunc = evalExpr.Compile();
var value = evalFunc();
return value;
}
But note that the whole class (as the author states) is just an example, and for instance maps just one parameter (thus one value) per property, so in order to make it really useful, the GetDynamicQuery method needs additional work. You might try this one instead. Hope that helps.

Get fields from anonymous type

I have this class:
public class allFields
{
public string EAN { get; set; }
public string title { get; set; }
public string qty { get; set; }
public string price { get; set; }
public DateTime date { get; set; }
}
And a function that return an anonymous type:
public IEnumerable<object> stockEtatQty()
{
List<allFields> afList = new List<allFields>();
var query = from x in ctx.book
where x.qty > 0
select x;
foreach (var item in query)
{
allFields af = new allFields();
af.EAN = item.EAN;
af.title = item.Titre;
af.qty = ""+item.Quantite;
afList.Add(af);
}
var q = from x in afList
select new { EAN=x.EAN, Title=x.title, Quantity=x.qty };
return q; //q is a IEnumerable<'a> where a is new {string EAN, string Title, string Quantity}
}
In my WinForm a use this function as below:
private void QuantityToolStripMenuItem_Click(object sender, EventArgs e)
{
ServiceStock sstock = new ServiceStock();
var q = sstock.stockEtatQty().ToList();// q is a list<object>
string str = "";
foreach (var item in q)
{
str += item + Environment.NewLine;
}
MessageBox.Show(str);
}
The result is:
{ EAN = 1, Title = CSharp Security, Quantity = 970 }
{ EAN = 2, Title = MISC, Quantity = 100 }
...
What I want?
I want not like the result above, but separate each field apart of the item in the loop foreach, e.g get item.EAN, item.Title and item.Quantity.
If there is no solution for my problem I would like to know an alternative,
Thanks for help.
The obvious solution is to create a custom type (let's call it BookInfo) and return a IEnumerable<BookInfo> instead of a IEnumerable<object> (and maybe override ToString if you want to put the formatting into this class itself).
Then you can easily format the output.
public class BookInfo
{
public string EAN {get;set;}
public string Title {get;set;}
public int Quantity {get;set;}
}
public IEnumerable<BookInfo> stockEtatQty()
{
...
var q = from x in afList
select new BookInfo { EAN=x.EAN, Title=x.title, Quantity=x.qty };
return q;
}
private void QuantityToolStripMenuItem_Click(object sender, EventArgs e)
{
ServiceStock sstock = new ServiceStock();
var q = sstock.stockEtatQty();
var message = string.Join(Environment.NewLine,
q.Select(item => String.Format("{0} {1} {2}", item.EAN, item.Title, item.Quantity)));
MessageBox.Show(message);
}
Since the static type information about the object of anonymous type is lost by the time that you exit stockEtatQty() method, you could cast the object to dynamic and access fields like this:
str = string.Join(Environment.NewLine, q.Cast<dynamic>().Select(item =>
string.Format("{0} {1} {2}", item.EAN, item.Title, item.Quantity)
));
The cast to dynamic tells the compiler that EAN, Title, and Quantity need to be resolved at runtime.
Note that I also replaced the foreach loop with a call to string.Join to improve performance: repeated string concatenation creates unnecessary partial string objects, which string.Join avoids. Another solution would be to use StringBuider instead of string concatenation +=.
stockEtatQty is in a project (Service) and QuantityToolStripMenuItem_Click is in another project (View)
Unfortunately, this means that you would not be able to use anonymous types: anonymous types are generated with internal visibility, limiting their use to the assembly in which they are produced. You can use a work-around based on ExpandoObject described in this answer:
var q = afList.Select(x => {
dynamic res = new ExpandoObject();
res.EAN=x.EAN;
res.Title=x.title;
res.Quantity=x.qty;
return res;
});
Create a new class that represents the new object structure and return that.
var q = from x in afList
select new SmallerType { EAN=x.EAN, Title=x.title, Quantity=x.qty };
WinForm Function
foreach (SmallerType item in q)
{
//
}
You can use collection of dynamic objects instead of simple objects as return type of your method:
public IEnumerable<dynamic> stockEtatQty()
Then you will not have IntelliSense but at runtime properties will be found:
foreach (var item in sstock.stockEtatQty())
str += String.Format("{0}", item.EAN) + Environment.NewLine;
But I suggest you to create custom class with EAN, Title and Quantity properties. Or just use your allFields instead of anonymous objects.
Consider also to use StringBuilder for string creation to avoid creating lot of in-memory strings:
var builder = new StringBuilder();
foreach (var item in sstock.stockEtatQty())
builder.AppendFormat("{0}{1}", item.EAN, Environment.NewLine);
MessageBox.Show(builder.ToString());

Categories

Resources