I am trying to create a web service to handle all logging for application asynchronous, where I can send two of the same object and compare each property. Then create of log of property that changed.
The issue i am having, i don't know how can the apicontroller accept any object:
This is i have so far:
public class PropertyLogApiCall
{
public string Id { get; set; }
public string Username { get; set; }
public string Task { get; set; }
public object OldEntity { get; set; }
public object NewEntity { get; set; }
public string OldEntityType { get; set; }
public string NewEntityType { get; set; }
}
POST - ApiController
public void Post(PropertyLogApiCall paramList)
{
try
{
var id = paramList.Id;
var username = paramList.Username;
var task = paramList.Task;
var newType = Type.GetType(paramList.NewEntityType);
var oldType = Type.GetType(paramList.OldEntityType);
var newEntity = Convert.ChangeType(paramList.NewEntity, newType);
var oldEntity = Convert.ChangeType(paramList.OldEntity, oldType);
var properties = oldEntity.GetType().GetProperties();
var logsToSave = new List<PropertyLog>();
var dateTimeStamp = DateTime.Now;
foreach (var property in properties)
{
var oldValue = property.GetValue(oldEntity, null);
var newValue = newEntity.GetType().GetProperty(property.Name).GetValue(newEntity, null);
var propertyType = property.PropertyType;
var name = oldEntity.GetType().Name;
var propName = property.Name;
PropertyLog log = null;
if (propertyType == typeof(string))
{
log = CreateLogString(oldValue, newValue);
}
else if (propertyType == typeof(int))
{
log = CreateLogInt(oldValue, newValue);
}
if (log != null)
{
log.Created = dateTimeStamp;
log.EntityId = id;
log.Username = username;
log.EntityName = name;
log.Property = propName;
log.Task = task;
logsToSave.Add(log);
}
}
//Save Logs
.....
}
catch (Exception e)
{
//send email
}
This doesn't work because most of the time newType/oldType is null. And some entity don't implement the IConvertible interface.
Is this even possible to do?
As you say that in many occassions you don't have the object type, you need to do it dynamically. It's impossible to recreate an object if you don't know its type.
Change the parameter of your action to dynamic, and the object will be dynamically created including all the properties coming in the request. Then you need to step through all the properties of the OldEntity and compare them with those on the NewEntity. If a property can be a complex object with additional properties you'll have to do it recursively. However in SO you have this information: How do I enumerate through a JObject?
You can get ideas from this SO Q&A: Comparing dynamic objects in C#.
Alternatively, if you're posting it as JSON, you can receive the parameter as a JObject, and make the same opeartions on it. At this moment http://www.newtonsoft.com/ is down, so I cannot give you an exact pointer to JObject docs.
Related
I found this answer here at SO, Get nested property values through reflection C#, though when I run it in my case, it also tries to dump/recurse on e.g. a string's property, like Name, and when, it throws an exception.
My classes look like this
public class MyModels
{
public int Id { get; set; }
public DateTime EditDate { get; set; }
public string EditBy { get; set; }
}
public class Person
{
public string Name { get; set; }
}
public class Organization
{
public Person Person { get; set; }
public Organization()
{
Person = new Person();
}
public string Name { get; set; }
}
public class Company : MyModels
{
public Organization Organization { get; set; }
public Company()
{
Organization = new Organization();
}
public string Description { get; set; }
}
And here's the code from the linked answer
var objtree = "";
void DumpObjectTree(object propValue, int level = 0)
{
if (propValue == null)
return;
var childProps = propValue.GetType().GetProperties();
foreach (var prop in childProps)
{
var name = prop.Name;
var value = prop.GetValue(propValue, null);
// add some left padding to make it look like a tree
objtree += ("".PadLeft(level * 4, ' ') + $"{name} = {value}") + Environment.NewLine;
// call again for the child property
DumpObjectTree(value, level + 1);
}
}
DumpObjectTree(itemData);
What I want is to iterate all the properties and check their value.
When I run the above code sample:
it first finds Organization, and recurse
at 1st level it finds Person, and recurse
at 2nd level if finds Name, and recurse
at 3rd level it throws an exception when it tries to GetValue for Name
If I remove my nested classes, and run it:
it first finds Description, and recurse
at 1st level it throws an exception when it tries to GetValue for Description
How do I make it to not try to dump/recurse on properties of type string, datetime, etc., like e.g. Name, Description?
The exception message says: "Parameter count mismatch."
As a note , the expected output/content in the objtree variable is e.g.
Organization = MyNameSpace.Models.Organization
Person = MyNameSpace.Models.Person
Name = TestName
Name = TestCompany
Description = Some info about the company...
Id = 1
EditDate = 31/08/2019
EditBy = user#domain.com
The reason for the exception is that string has a property named Chars. You normally don't see this property, because it's the indexer used when you do something like char c = myString[0];.
This property obviously needs a paramter (the index), and since you don't provide one, an exception is thrown.
To filter the types you don't want to recurse you need to extend the first line in the method. For example
if (propValue == null) return;
if (propValue.GetType().Assembly != Assembly.GetExecutingAssembly())
return;
This will only recurse through types declared in your assembly. If you want special filtering you need to adjust it.
Your current specification ("of type string, datetime etc") is not specific enough to give an exact solution, but I think the idea is clear.
Note that this won't prevent an exception to be raised if you declare an indexer in your own classes. So a better way might be to check for indexers directly:
foreach (var prop in childProps)
{
if (prop.GetIndexParameters().Any()) continue;
Second note: The current code has another flaw: You should keep track of which types you already dumped and abort the recursion when you come across a type the second time. That's possibly the reason for the exception at DateTime. A DateTime has a Date property, which is - hurray - of type DateTime. And so your objtree string grows infinitly until an OutOfMemoryException or StackOverflowException is thrown.
You need to skip recursion when:
Property is a value type
Property is a string
Property value contains reference to the object from the previous recursion level (ie, ParentObject) so that you don't get a stack overflow exception
Edit: Also when property is a collection type. If you want to get creative, you can have your recursor iterate through each object in the collection and then recurse through those
This PropertyInfo recursor seems to do the trick.
[Flags]
public enum PropertyRecursionOverflowProtectionType
{
SkipSameReference,
SkipSameType
}
public class PropertyRecursionBot
{
public object ParentObject { get; set; }
public object CurrentObject { get; set; }
public PropertyInfo PropertyInfo { get; set; }
public Type ParentType { get; set; }
public int Level { get; set; }
}
public static IEnumerable<PropertyRecursionBot> GetAllProperties(object entity,
PropertyRecursionOverflowProtectionType overflowProtectionType = PropertyRecursionOverflowProtectionType.SkipSameReference)
{
var type = entity.GetType();
var bot = new PropertyRecursionBot { CurrentObject = entity };
IEnumerable<PropertyRecursionBot> GetAllProperties(PropertyRecursionBot innerBot, PropertyInfo[] properties)
{
var currentParentObject = innerBot.ParentObject;
var currentObject = innerBot.CurrentObject;
foreach (var pi in properties)
{
innerBot.PropertyInfo = pi;
var obj = pi.GetValue(currentObject);
innerBot.CurrentObject = obj;
//Return the property and value only if it's a value type or string
if (pi.PropertyType == typeof(string) || !pi.PropertyType.IsClass)
{
yield return innerBot;
continue;
}
//This overflow protection check will prevent stack overflow if your object has bidirectional navigation
else if (innerBot.CurrentObject == null ||
(overflowProtectionType.HasFlag(PropertyRecursionOverflowProtectionType.SkipSameReference) && innerBot.CurrentObject == currentParentObject) ||
(overflowProtectionType.HasFlag(PropertyRecursionOverflowProtectionType.SkipSameType) && innerBot.CurrentObject.GetType() == currentParentObject?.GetType()))
{
continue;
}
innerBot.Level++;
innerBot.ParentObject = currentObject;
foreach (var innerPi in GetAllProperties(innerBot, pi.PropertyType.GetProperties()))
{
yield return innerPi;
}
innerBot.Level--;
innerBot.ParentObject = currentParentObject;
innerBot.CurrentObject = obj;
}
}
foreach (var pi in GetAllProperties(bot, type.GetProperties()))
{
yield return pi;
}
}
Use it like this:
public class RecursionTest
{
public string StringValue { get; set; }
public int IntValue { get; set; }
public RecursionTest Test { get; set; }
public RecursionTest ParentTest { get; set; }
}
var rec1 = new RecursionTest
{
IntValue = 20,
StringValue = Guid.NewGuid().ToString()
};
rec1.Test = new RecursionTest
{
IntValue = 30,
StringValue = Guid.NewGuid().ToString(),
ParentTest = rec1
};
rec1.Test.Test = new RecursionTest
{
IntValue = 40,
StringValue = Guid.NewGuid().ToString(),
ParentTest = rec1.Test
};
foreach (var bot in GetAllProperties(rec1, PropertyRecursionOverflowProtectionType.SkipSameReference))
{
Console.WriteLine($"{new string(' ', bot.Level * 2)}{bot.PropertyInfo.Name}: {bot.CurrentObject}");
}
SO, I am trying to retrieve users feed likes for the first 25 records as below:
var access_token = HttpContext.Items["access_token"].ToString();
if (!string.IsNullOrEmpty(access_token))
{
var appsecret_proof = access_token.GenerateAppSecretProof();
var fb = new FacebookClient(access_token);
dynamic myFeed = await fb.GetTaskAsync(
("me/feed?fields=likes{{name,pic_large}}")
//GraphAPICall formats the json retrieved from facebook
.GraphAPICall(appsecret_proof));
string feed = myFeed;
var postList = new List<FBAnalyseViewModel>();
foreach (dynamic post in myFeed.data)
{
postList.Add(DynamicExtension.ToStatic<FBAnalyseViewModel>(post));
}
The GraphAPI above get the base GraphAPICall then formats the Json retrieved from facebook and appends the appsecret_proof plus arguments as here below:
public static string GraphAPICall(this string baseGraphApiCall, params object[] args)
{
//returns a formatted Graph Api Call with a version prefix and appends a query string parameter containing the appsecret_proof value
if (!string.IsNullOrEmpty(baseGraphApiCall))
{
if (args != null &&
args.Count() > 0)
{
//Determine if we need to concatenate appsecret_proof query string parameter or inject it as a single query string paramter
string _graphApiCall = string.Empty;
if (baseGraphApiCall.Contains("?"))
_graphApiCall = string.Format(baseGraphApiCall + "&appsecret_proof={" + (args.Count() - 1) + "}", args);
else
_graphApiCall = string.Format(baseGraphApiCall + "?appsecret_proof={" + (args.Count() - 1) + "}", args);
//prefix with Graph API Version
return string.Format("v2.8/{0}", _graphApiCall);
}
else
throw new Exception("GraphAPICall requires at least one string parameter that contains the appsecret_proof value.");
}
else
return string.Empty;
}
I use an auto mapping technique for my property values as here below:
//Iterate through the dynamic Object's list of properties, looking for match from the facebook mapping lookup
foreach (var entry in properties)
{
var MatchedResults = PropertyLookup.Where(x => x.facebookparent == entry.Key || x.facebookfield == entry.Key);
if (MatchedResults != null)
foreach (propertycontainer DestinationPropertyInfo in MatchedResults)
{
object mappedValue =null;
if (entry.Value.GetType().Name == "JsonObject")
{
//drill down on level to obtain a list of properties from the child set of properties
//of the dynamic Object
mappedValue = FindMatchingChildPropertiesRecursively(entry, DestinationPropertyInfo);
//child properity was not matched so apply the parent FacebookJson object as the entry value
if (mappedValue == null &&
DestinationPropertyInfo.facebookfield == entry.Key)
mappedValue = entry.Value;
}
else
{
if (String.IsNullOrEmpty(DestinationPropertyInfo.facebookparent) &&
DestinationPropertyInfo.facebookfield == entry.Key)
mappedValue = entry.Value;
}
//copy mapped value into destination class property
if (mappedValue != null)
if (DestinationPropertyInfo.facebookMappedProperty.PropertyType.Name == "DateTime")
{
DestinationPropertyInfo.facebookMappedProperty.SetValue(entity, System.DateTime.Parse(mappedValue.ToString()), null);
}
else
DestinationPropertyInfo.facebookMappedProperty.SetValue(entity, mappedValue, null);
}
}
return entity;
}
While in debugging mode I get null values on my model view properties just like below picture.
And the Json returned by facebook is also not a valid json in order to serialize/deserialize. Any workaround? Help please.
EDIT:
Model Values are mapped to facebook items are as follow:
[Required]
[FacebookMapping("likes")]
public dynamic Likes { get; set; }
[Required]
[FacebookMapping("name")]
public string Name { get; set; }
[FacebookMapping("pic_large")]
public string ImageURL { get; set; }
Edit2:
The Json retrieved from facebook GrapAPICall is as below:
{"data":[{"id":"1264038093655465_1274837905908817","likes":{"data":[{"name":"Sayed Zubair Hashimi","pic_large":"https://scontent.xx.fbcdn.net/v/t1.0-1/p200x200/14909900_10154513795037597_3241587822245799922_n.jpg?oh=54ead7e0ba74b45b632d96da1515ccf8&oe=591C4938","id":"10154729171332597"}
As you noticed Json that you provide is not valid, at least ]}}]} was missed. After adding missing elements relevants objects look like as shown below:
public class PostDetail
{
public string name { get; set; }
public string pic_large { get; set; }
public string id { get; set; }
}
public class Likes
{
public List<PostDetail> data { get; set; }
}
public class Post
{
public string id { get; set; }
public Likes likes { get; set; }
}
public class RootObject
{
public List<Post> data { get; set; }
}
I have a view model that have a lot of properties, and I created a custom attribute for send the data to hubspot, because hubspot need a specific nomenclature, then I need to create a method that have some king of iterator, that for every property that contain my custom attribute he put a specific output, here is the code:
public class CreateTrialUserHubspotViewModel {
[HubspotAttribute("firstname")]
[Display(Name = "First Name")]
[StringLength(50)]
[Required]
public string FirstName { get; set; }
[HubspotAttribute("lastname")]
[Display(Name = "Last Name")]
[StringLength(50)]
[Required]
public string LastName { get; set; }
[HubspotAttribute("jobtitle")]
[Display(Name = "Job title")]
[StringLength(50)]
[Required]
public string JobTitle { get; set; }
}
Now this is my custom attribute
[AttributeUsage(AttributeTargets.All)]
public class HubspotAttribute : System.Attribute {
public readonly string hubspotValue;
public HubspotAttribute(string value)
{
this.hubspotValue = value;
}
}
And then I need to create a method that take a viewmodel object and create my output, I need some suggest about how to do that, will be something like this:
private static RowValidation ValidateRowWithManifest<T>(CreateTrialUserHubspotViewModel trialUser) {
RowValidation validation = new RowValidation();
FieldInfo[] fields = typeof(T).GetPropertiesOfSomeWay;
foreach (DataType field in fields) {
output+=whatINeed
}
return validation;
}
}
The needed output will be like: [firstname:"pepe", lastname="perez", jobtitle"none"]. just calling that method will return all the data I need.
public string GetString<T>(T #object)
{
var output = new StringBuilder();
var type = typeof(T);
var properties = type.GetProperties();
foreach (var property in properties)
{
var attributes = property.GetCustomAttributes(typeof(HubspotAttribute), true);
if (attributes.Length == 0)
continue;
var name = ((HubspotAttribute)attributes[0]).hubspotValue;
var value = property.GetValue(#object) ?? "none";
output.AppendFormat("{0}:\"{1}\",", name, value);
}
var fields = output.ToString().TrimEnd(',');
return string.Format("[{0}]", fields);
}
If you are looking for something that will concatenate properties into a string that looks like a JSON string (and that would be a better way to handle it), you can use something like the following:
private static string CreateOutput(CreateTrialUserHubspotViewModel trialUser)
{
var properties = trialUser.GetType().GetProperties().Where(x => Attribute.IsDefined(x, typeof(HubspotAttribute))).ToList();
var values = properties.Select(x =>
{
var att = x.GetCustomAttribute(typeof(HubspotAttribute));
var key = ((HubspotAttribute)att).hubspotValue;
var val = x.GetValue(trialUser);
return $"{key}:{val}";
});
var sb = new StringBuilder();
values.ToList().ForEach(v =>
{
sb.Append(v);
if (values.Last() != v) sb.Append(',');
});
return sb.ToString();
}
I have a class structure below. I am getting this error. Am i missing something here?
Object does not match target type.
Class Structure
public class Schedule
{
public Schedule() { Name = ""; StartDate = DateTime.MinValue; LectureList = new List<Lecture>(); }
public string Name { get; set; }
public DateTime StartDate { get; set; }
public List<Lecture> LectureList { get; set; }
}
public class Lecture
{
public string Name { get; set; }
public int Credit { get; set; }
}
What i am trying:
Schedule s = new Schedule();
Type t = Type.GetType("Lecture");
object obj = Activator.CreateInstance(t);
obj.GetType().GetProperty("Name").SetValue(obj, "Math");
obj.GetType().GetProperty("Credit").SetValue(obj, 1);
PropertyInfo pi = s.GetType().GetProperty("LectureList");
Type ti = Type.GetType(pi.PropertyType.AssemblyQualifiedName);
ti.GetMethod("Add").Invoke(pi, new object[] { obj });
It should be something like this:
// gets metadata of List<Lecture>.Add method
var addMethod = pi.PropertyType.GetMethod("Add");
// retrieves current LectureList value to call Add method
var lectureList = pi.GetValue(s);
// calls s.LectureList.Add(obj);
addMethod.Invoke(lectureList, new object[] { obj });
UPD. Here's the fiddle link.
The problem is that you get the Add method of List<Lecture> and try to invoke it with PropertyInfo as the instance invoking the method.
Change:
ti.GetMethod("Add").Invoke(pi, new object[] { obj });
to:
object list = pi.GetValue(s);
ti.GetMethod("Add").Invoke(list, new object[] { obj });
That way pi.GetValue(s) gets the List<Lecture> itself from the PropertyInfo (which only represents the property itself along with its get and set methods, and invoke its Add method with your object[] as arguments.
One more thing. why using:
Type ti = Type.GetType(pi.PropertyType.AssemblyQualifiedName);
When you can just use:
Type ti = pi.PropertyType;
Currently, am adding the properties and values to the object manually like this example and sending to Dapper.SimpleCRUD to fetch data from Dapper Orm. This is the desired output I would like to achieve.
object whereCriteria = null;
whereCriteria = new
{
CountryId = 2,
CountryName = "Anywhere on Earth",
CountryCode = "AOE",
IsActive = true
};
The following class should build the object in the above mentioned format and return the ready-made object.
public static class WhereClauseBuilder
{
public static object BuildWhereClause(object model)
{
object whereObject = null;
var properties = GetProperties(model);
foreach (var property in properties)
{
var value = GetValue(property, model);
//Want to whereObject according to the property and value. Need help in this part!!!
}
return whereObject;
}
private static object GetValue(PropertyInfo property, object model)
{
return property.GetValue(model);
}
private static IEnumerable<PropertyInfo> GetProperties(object model)
{
return model.GetType().GetProperties();
}
}
This function WhereClauseBuilder.BuildWhereClause(object model) should return the object in expected format (mentiond above). Here is the implementation of how I would like to use.
public sealed class CountryModel
{
public int CountryId { get; set; }
public string CountryName { get; set; }
public string CountryCode { get; set; }
public bool IsActive { get; set; }
}
public class WhereClauseClass
{
public WhereClauseClass()
{
var model = new CountryModel()
{
CountryCode = "AOE",
CountryId = 2,
CountryName = "Anywhere on Earth",
IsActive = true
};
//Currently, won't return the correct object because the implementation is missing.
var whereClauseObject = WhereClauseBuilder.BuildWhereClause(model);
}
}
Maybe something like that:
private const string CodeTemplate = #"
namespace XXXX
{
public class Surrogate
{
##code##
}
}";
public static Type CreateSurrogate(IEnumerable<PropertyInfo> properties)
{
var compiler = new CSharpCodeProvider();
var compilerParameters = new CompilerParameters { GenerateInMemory = true };
foreach (var item in AppDomain.CurrentDomain.GetAssemblies().Where(x => !x.IsDynamic))
{
compilerParameters.ReferencedAssemblies.Add(item.Location);
}
var propertiesCode =
string.join("\n\n", from pi in properties
select "public " + pi.PropertyType.Name + " " + pi.Name + " { get; set; }");
var source = CodeTemplate.Replace("##code##", propertiesCode);
var compilerResult = compiler.CompileAssemblyFromSource(compilerParameters, source);
if (compilerResult.Errors.HasErrors)
{
throw new InvalidOperationException(string.Format("Surrogate compilation error: {0}", string.Join("\n", compilerResult.Errors.Cast<CompilerError>())));
}
return compilerResult.CompiledAssembly.GetTypes().First(x => x.Name == "Surrogate");
}
And now use it:
public static object BuildWhereClause(object model)
{
var properties = GetProperties(model);
var surrogateType = CreateSurrogate(properties);
var result = Activator.CreateInstance(surrogateType);
foreach (var property in properties)
{
var value = GetValue(property, model);
var targetProperty = surrogateType.GetProperty(property.Name);
targetProperty.SetValue(result, value, null);
}
return result;
}
I didn't compile that. It's only written here. Maybe there are some errors. :-)
EDIT:
To use ExpandoObject you can try this:
public static object BuildWhereClause(object model)
{
var properties = GetProperties(model);
var result = (IDictionary<string, object>)new ExpandoObject();
foreach (var property in properties)
{
var value = GetValue(property, model);
result.Add(property.Name, value);
}
return result;
}
But I don't know whether this will work for you.