Getting expression text - c#

I want to pass the name of a property of a model to a method. Instead of using the name as string, I am using lambda expression as it is easy to make a typo, and also property names may be changed. Now if the property is a simple property (e.g: model.Name) I can get the name from the expression. But if it is a nested property (e.g: model.AnotherModel.Name) then how can I get full text ("AnotherModel.Name") from the expression. For example, I have the following classes:
public class BaseModel
{
public ChildModel Child { get; set; }
public List<ChildModel> ChildList { get; set; }
public BaseModel()
{
Child = new ChildModel();
ChildList = new List<ChildModel>();
}
}
public class ChildModel
{
public string Name { get;set; }
}
public void GetExpressionText<T>(Expression<Func<T, object>> expression)
{
string expText;
//what to do??
return expText;
}
GetExpressionText<BaseModel>(b => b.Child); //should return "Child"
GetExpressionText<BaseModel>(b => b.Child.Name); //should return "Child.Name"
GetExpressionText<BaseModel>(b => b.ChildList[0].Name); //should return "ChildList[0].Name"

My first thought was to use expression.Body.ToString() and tweak that a bit, but you would still need to deal with Unary (convert) etc. Assuming this is for logging and you want more control, the below can be used for formatting as wanted (e.g. if you want Child->Name for display purposes, string.Join("->",..) can be used). It may not be complete, but should you find any unsupported types, they should be easy to add.
PS: this post was generated before the question was closed. Just noticed it was reopend and submitting it now, but I haven't checked if particulars have been changed.
public string GetName(Expression e, out Expression parent)
{
if(e is MemberExpression m){ //property or field
parent = m.Expression;
return m.Member.Name;
}
else if(e is MethodCallExpression mc){
string args = string.Join(",", mc.Arguments.SelectMany(GetExpressionParts));
if(mc.Method.IsSpecialName){ //for indexers, not sure this is a safe check...
return $"{GetName(mc.Object, out parent)}[{args}]";
}
else{ //other method calls
parent = mc.Object;
return $"{mc.Method.Name}({args})";
}
}
else if(e is ConstantExpression c){ //constant value
parent = null;
return c.Value?.ToString() ?? "null";
}
else if(e is UnaryExpression u){ //convert
parent= u.Operand;
return null;
}
else{
parent =null;
return e.ToString();
}
}
public IEnumerable<string> GetExpressionParts(Expression e){
var list = new List<string>();
while(e!=null && !(e is ParameterExpression)){
var name = GetName(e,out e);
if(name!=null)list.Add(name);
}
list.Reverse();
return list;
}
public string GetExpressionText<T>(Expression<Func<T, object>> expression) => string.Join(".", GetExpressionParts(expression.Body));

You could use the C# 6.0 feature: nameof(b.Child) "Used to obtain the simple (unqualified) string name of a variable, type, or member."
which will also change on renaming. But this will only return the propertyname and not the complete path. Returning a complete path will be difficult, because only one instance is passed.

Closest i know right now is by simply using expression.Body.ToString() which would result in b.ChildList.get_Item(0).Name as a result.
You would still have to remove the first b. from the string if not wanted, and you could go even further to your intended output with Regex by replacing the get_Item(0) with the typical Index-Accessor.
(Also i had to make the ChildList and the Name-Property of ChildModel public to get it to work)

This Should get you most of the way there:
public static string GetFullPath<T>(Expression<Func<T>> action)
{
var removeBodyPath = new Regex(#"value\((.*)\).");
var result = action.Body.ToString();
var replaced = removeBodyPath.Replace(result, String.Empty);
var seperatedFiltered = replaced.Split('.').Skip(1).ToArray();
return string.Join(".", seperatedFiltered);
}

It gets ugly quite quickly...
public static string GetExpressionText<T>(Expression<Func<T, object>> expression)
{
bool needDot = false;
Expression exp = expression.Body;
string descr = string.Empty;
while (exp != null)
{
if (exp.NodeType == ExpressionType.MemberAccess)
{
// Property or field
var ma = (MemberExpression)exp;
descr = ma.Member.Name + (needDot ? "." : string.Empty) + descr;
exp = ma.Expression;
needDot = true;
}
else if (exp.NodeType == ExpressionType.ArrayIndex)
{
// Array indexer
var be = (BinaryExpression)exp;
descr = GetParameters(new ReadOnlyCollection<Expression>(new[] { be.Right })) + (needDot ? "." : string.Empty) + descr;
exp = be.Left;
needDot = false;
}
else if (exp.NodeType == ExpressionType.Index)
{
// Object indexer (not used by C#. See ExpressionType.Call)
var ie = (IndexExpression)exp;
descr = GetParameters(ie.Arguments) + (needDot ? "." : string.Empty) + descr;
exp = ie.Object;
needDot = false;
}
else if (exp.NodeType == ExpressionType.Parameter)
{
break;
}
else if (exp.NodeType == ExpressionType.Call)
{
var ca = (MethodCallExpression)exp;
if (ca.Method.IsSpecialName)
{
// Object indexer
bool isIndexer = ca.Method.DeclaringType.GetDefaultMembers().OfType<PropertyInfo>().Where(x => x.GetGetMethod() == ca.Method).Any();
if (!isIndexer)
{
throw new Exception();
}
}
else if (ca.Object.Type.IsArray && ca.Method.Name == "Get")
{
// Multidimensiona array indexer
}
else
{
throw new Exception();
}
descr = GetParameters(ca.Arguments) + (needDot ? "." : string.Empty) + descr;
exp = ca.Object;
needDot = false;
}
}
return descr;
}
private static string GetParameters(ReadOnlyCollection<Expression> exps)
{
var values = new string[exps.Count];
for (int i = 0; i < exps.Count; i++)
{
if (exps[i].NodeType != ExpressionType.Constant)
{
throw new Exception();
}
var ce = (ConstantExpression)exps[i];
// Quite wrong here... We should escape string values (\n written as \n and so on)
values[i] = ce.Value == null ? "null" :
ce.Type == typeof(string) ? "\"" + ce.Value + "\"" :
ce.Type == typeof(char) ? "'" + ce.Value + "\'" :
ce.Value.ToString();
}
return "[" + string.Join(", ", values) + "]";
}
The code is quite easy to read, but it is quite long... There are 4 main cases: MemberAccess, that is accessing a property/field, ArrayIndex that is using the indexer of a single-dimensional array, Index that is unused by the C# compiler, but that should be using the indexer of an object (like the [...] of the List<> you are using), and Call that is used by C# for using an indexer or for accessing multi-dimensional arrays (new int[5, 4]) (and for other method calls, but we disregard them).
I support multidimensional arrays, jagged array s(arrays of arrays, new int[5][]) or arrays of indexable objects (new List<int>[5]) or indexable objects of indexable objects (new List<List<int>>). There is even support for multi-property indexers (indexers that use more than one key value, like obj[1, 2]). Small problem: printing the "value" of the indexers: I support only null, integers of various types, chars and strings (but I don't escape them... ugly... if there is a \n then it won't be printed as \n). Other types are not really supported... They will print what they will print (see GetParameters() if you want)

Related

Using "IndexOf" on a List<T> of objects

In my Visual Studio2019 C# console program (named 'codeTester') I have this object:
public class ipData
{
private string ip;
private string region;
private string country;
public ipData(string ip, string region, string country)
{
this.ip = ip;
this.region = region;
this.country = country;
}
public string Ip
{
get { return ip; }
set { ip = value; }
}
public string Region
{
get { return region; }
set { region = value; }
}
public string Country
{
get { return country; }
set { country = value; }
}
}
and I created a List of this object and add some data:
List<ipData> ipInfo = new List<ipData>();
ipInfo.Add(new ipData("192.168.0.199", "UT", "USA"));
ipInfo.Add(new ipData("251.168.0.963", "NB", "CAN"));
Now I want to search the list on one of its fields so I ask the user for the data to search for:
Console.WriteLine("Enter searh criteria: ");
string searchparam = Console.ReadLine();
Next I want the index of the found item, if any:
int x = ipInfo.IndexOf(searchparam);
but this statement throws a design-time exception which says:
"Argument 1: cannot convert from 'string' to 'codeTester.Program.ipData'"
So I've been stuck at this point for hours and all my searches have not yielded anything pertinent. Where am I going wrong?
It does not work, because the search parameter is expected to be of the same type as the element type of the list. In this case ipData.
You could use FindIndex which accepts a lambda expression as parameter:
int x =
ipInfo.FindIndex(ip => ip.Region == searchparam || ip.Country == searchparam);
if (x >= 0) {
Console.WriteLine($"The IP address is {ipInfo[x]}");
} else {
Console.WriteLine("not found");
}
or you can use LINQ like this:
string ipAddress = ipInfo
.FirstOrDefault(ip => ip.Region == searchparam || ip.Country == searchparam)?.Ip;
This will return a null string if no entry was found.
You can also have it return the whole record instead:
ipData data = ipInfo
.FirstOrDefault(ip => ip.Region == searchparam || ip.Country == searchparam);
if (data != null) {
Console.WriteLine(
$"Country = {data.Country}, Region = {data.Region}, IP = {data.ip}");
}
LINQ also allows you to return more than one result. E.g. you can return all data corresponding to one country like this:
var result = ipInfo.Where(ip => ip.Country == "USA");
foreach (ipData data in result) {
Console.WriteLine(
$"Country = {data.Country}, Region = {data.Region}, IP = {data.ip}");
}
The C# naming conventions state the class names should be written in PascalCase. Another convention says that acronyms with up to two characters in length are written all upper case (IP). According to these conventions, the class name should be IPData.
See also:
Capitalization Conventions (Microsoft Docs)
C# Coding Standards and Naming Conventions
That is because IndexOf searches for the exact object in list. For it to work, you would have to pass ipData to it instead.
For this purpose, you should use Find or FindIndex.

How do to create string ciphers

I have an error on this part foreach( string code in text ) the error is saying can not convert char to string. how do i convert this to string
my list
class MyCipher : ICipherDecipher
{
private List<Code> alphabet;
public MyCipher()
{
alphabet = new List<Code>();
alphabet.Add(new Code("Aca", " 1234"));
alphabet.Add(new Code("Bb", " 1234"));
alphabet.Add(new Code("C1", " 1234"));
}
this is where im gtting the error on the foreach part , its saying cant convert to string from char
private string Cipher( string text )
{
StringBuilder result = new StringBuilder();
foreach( string code in text )
{
Code element =
alphabet.Where(x => x.MyCode == code.ToString()).SingleOrDefault();
if ( element != null)
{
result.Append(element.MyDecoded);
}
}
return result.ToString();
}
Edited code
class MyCipher : ICipherDecipher
{
private List<Code> alphabet;
public MyCipher()
{
alphabet = new List<Code>();
alphabet.Add(new Code("4", " take 4"));
alphabet.Add(new Code(" ", " a"));
alphabet.Add(new Code("4d", " for 4 days"));
}
public string Cipher(params string[] codes)
{
StringBuilder result = new StringBuilder();
foreach (string code in codes)
{
Code element =
alphabet.Where(x => x.MyCode == code).SingleOrDefault();
if (element != null)
{
result.Append(element.MyDecoded);
}
}
return result.ToString();
}
class Code
{
public string MyCode;
public string MyDecoded;
public Code(string code, string decode)
{
MyCode = code;
MyDecoded = decode;
}
}
}
Button code
public partial class Form1 : Form
{
private ICipherDecipher myCipher;
public Form1()
{
myCipher = new MyCipher();
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
string textToBeCiphered = textBox1.Text;
string textCiphered = myCipher.Cipher(textToBeCiphered);
textBox2.Text = textCiphered;
}
}
Change this part of your code:
foreach( char code in text )
{
Code element =
alphabet.Where(x => x.MyCode == code.ToString()).SingleOrDefault();
if ( element != null)
{
result.Append(element.MyDecoded);
}
}
Then everywhere you use code you convert it to string by using code.ToString()
You're iterating over a string (text), so the code variable should be a char, not a string:
foreach( char code in text )
Alternatively, you can use var:
foreach( var code in text )
to make the compiler automatically assign the type it thinks it should be, but you should still be aware of the type it actually is because it impacts the operations the variable supports.
Going a bit deeper into it, some other languages (especially dynamic languages like JavaScript and Python) don't make this distinction but C# (like most other C-like languages) has a char datatype that holds a single element of the text. In the case of C#, that type holds a UTF-16 code unit. (Which is not always the same as a user-visible character, but that's a whole other story).
Based on the discussion in the comments, it seems you want to match the whole string instead of characters of the string. In that case you have two options, depending on what you want to do:
If you want the Cypher method to receive a single string and get the code that matches that string, just get rid of the loop and match against the parameter directly:
private string Cipher( string code )
{
StringBuilder result = new StringBuilder();
Code element =
alphabet.Where(x => x.MyCode == code).SingleOrDefault();
if ( element != null)
{
result.Append(element.MyDecoded);
}
return result.ToString();
}
Alternatively, if you want to pass multiple strings and process all of them, pass an IEnumerable<string> (from System.Collections.Generic) or a derived type such as string[]:
private string Cipher( IEnumerable<string> codes )
{
StringBuilder result = new StringBuilder();
foreach( string code in codes )
{
Code element =
alphabet.Where(x => x.MyCode == code).SingleOrDefault();
if ( element != null)
{
result.Append(element.MyDecoded);
}
}
return result.ToString();
}
// Usage examples:
private string CallSite() {
// (These are just some of the options, other enumerable types
// will work as well as long as they enumerate over strings)
return Cypher( new[] { "Aca", "Bb" });
return Cypher( new List<string> { "Aca", "Bb" });
}
Alternatively, if you replace IEnumerable<string> with params string[] you can call the function as if it had any number of string parameters:
private string Cipher( params string[] codes )
{
// ... (as above)
}
// Usage example:
private string CallSite() {
return Cypher( "Aca", "Bb" );
}
In any of these cases, you can get replace code.ToString() with simply code because it already is a string (and "string".ToString() just returns itself).

Get source code representation of generic type?

I'm trying to get generic type in string I get something more like IL representation, I need the real source representation for emitting it.
Type t = typeof(Stack<string>);
string source = t.Name; //Stack`1[System.String]
string source = t.ToString(); //System.Collections.Generic.Stack`1[System.String]
I just need:
string source //Stack<string>
I've got this extension method, GetPrettyName(). This is basically it:
public static string GetPrettyName(this Type type)
{
var retval = type.Name;
if (type.IsGenericType)
{
var genargNames = type.GetGenericArguments().Select(t => GetPrettyName(t));
var idx = type.Name.IndexOf('`');
var typename = (idx > 0) ? type.Name.Substring(0, idx) : type.Name;
retval = String.Format("{0}.{1}<{2}>", type.Namespace, typename, String.Join(", ", genargNames));
}
else if (type.IsArray)
{
retval = GetPrettyName(type.GetElementType()) + "[]";
}
else if (String.IsNullOrEmpty(retval))
{
retval = type.Name;
}
return retval;
}
It operates recursively on each generic type parameter and builds out the full name in a format that's close to the code representation. It's good enough for our purposes (its just used in logging messages here). It can handle generics and arrays, but does not handle Entity Framework proxies that well.
Here's a version of Amy's answer with support for nested classes and multi-dimensional arrays.
public static string GetSourceCodeRepresentation(this Type type)
{
return GetSourceCodeRepresentationInt(type, new List<Type>());
}
private static string GetSourceCodeRepresentationInt(Type type, List<Type> travesed)
{
travesed.Add(type);
var prefixName = string.Empty;
if (type.DeclaringType != null)
{
if (!travesed.Contains(type.DeclaringType))
prefixName = GetSourceCodeRepresentationInt(type.DeclaringType, travesed) + ".";
}
else if (!string.IsNullOrEmpty(type.Namespace))
prefixName = type.Namespace + ".";
if (type.IsGenericType)
{
var genargNames = type.GetGenericArguments().Select(type1 => GetSourceCodeRepresentationInt(type1, new List<Type>()));
var idx = type.Name.IndexOf('`');
var typename = idx > 0 ? type.Name.Substring(0, idx) : type.Name;
return $"{prefixName}{typename}<{string.Join(", ", genargNames.ToArray())}>";
}
if (type.IsArray)
{
return $"{GetSourceCodeRepresentation(type.GetElementType())}[{new string(Enumerable.Repeat(',', type.GetArrayRank() - 1).ToArray())}]";
}
return $"{prefixName}{type.Name}";
}

Generic extension method for retrieving 'default' value?

I'm playing around a bit with determining the default values of objects, based on the example here:
https://stackoverflow.com/a/3195792/1293496
This particular extension method was created for System.Type. What I was trying to accomplish was to make this even more generic, where I could do something like this:
int i = 3;
bool amIaDefaultValue = i.IsDefaultValue();
I would expect this to return true if i == 0 (the default value for an int), and false for all other instances.
Here is my initial attempt:
public static bool IsDefaultValue<T>(this T value)
{
var t = typeof(T); // always comes back as object..?
if (t.IsValueType && Nullable.GetUnderlyingType(t) == null)
{
return value.Equals(Activator.CreateInstance<T>());
}
else
{
var defaultValue = default(T);
if (value == null)
return defaultValue == null;
else
return value.Equals(defaultValue);
}
}
On the plus side, I'm able to attach .IsDefaultValue() to any object. Unfortunately, the type of T always comes back as System.Object. I can get the correct type if I set it up this way:
var t = typeof(value);
But if the value happens to be null, I'll get an error straight away. Is there a good workaround for implementing an extension method like this? Or should I stick to the tried and tested route from the example?
Edit
As pointed out by comments, it seems I oversimplified this a bit and missed the root of the problem. Here's what was actually calling my IsDefaultValue():
foreach (var imprintProperty in deltas.GetType().GetProperties())
{
var value = imprintProperty.GetValue(deltas);
if (!value.IsDefaultValue())
{
// get corresponding prop in programmable area
var programmableProp = progarea.GetType().GetProperty(imprintProperty.Name);
if (programmableProp != null)
programmableProp.SetValue(progarea, value);
}
}
And now it becomes obvious that .GetValue is always returning as System.Object. Uff.
Is it still possible to treat the object as its underlying type in the extension method? Sorry for the confusion with this.
Take a look at this:
static class Program
{
static void Main()
{
int a = 1;
Console.WriteLine("a.IsDefaultValue() : " + a.IsDefaultValue());
a = 0;
Console.WriteLine("a.IsDefaultValue() : " + a.IsDefaultValue());
object obj = new object();
Console.WriteLine("obj.IsDefaultValue() : " + obj.IsDefaultValue());
obj = null;
Console.WriteLine("obj.IsDefaultValue() : " + obj.IsDefaultValue());
int? b = 1;
Console.WriteLine("b.IsDefaultValue() : " + b.IsDefaultValue());
b = null;
Console.WriteLine("b.IsDefaultValue() : " + b.IsDefaultValue());
Console.ReadKey(true);
}
static bool IsDefaultValue<T>(this T value)
{
if (ReferenceEquals(value, null))
{
return true;
}
var t = value.GetType();
if (t.IsValueType)
{
return value.Equals(Activator.CreateInstance(value.GetType()));
}
return false;
}
}
Apparently works (I've to say that I was convinced that the other way should have worked but not)

Compare Two Liste <T>

how can i compare 2 list ?
public class Pers_Ordre : IEqualityComparer<Pers_Ordre>
{
int _ordreId;
public int LettreVoidID
{
get { return _LettreVoidID; }
set { _LettreVoidID = value; }
}
string _OrdreCummul;
public string OrdreCummul
{
get { return _OrdreCummul; }
set { _OrdreCummul = value; }
}
// Products are equal if their names and product numbers are equal.
public bool Equals(Pers_Ordre x, Pers_Ordre y)
{
//Check whether the compared objects reference the same data.
if (Object.ReferenceEquals(x, y)) return true;
//Check whether any of the compared objects is null.
if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null))
return false;
//Check whether the products' properties are equal.
return x.LettreVoidID == y.LettreVoidID && x.OrdreCummul == y.OrdreCummul;
}
// If Equals() returns true for a pair of objects
// then GetHashCode() must return the same value for these objects.
public int GetHashCode(Pers_Ordre product)
{
//Check whether the object is null
if (Object.ReferenceEquals(product, null)) return 0;
//Get hash code for the Name field if it is not null.
int hashProductName = product.OrdreCummul == null ? 0 : product.OrdreCummul.GetHashCode();
//Get hash code for the Code field.
int hashProductCode = product.LettreVoidID.GetHashCode();
//Calculate the hash code for the product.
return hashProductName ^ hashProductCode;
}
}
and i compare like this:
private void simpleButton_Comparer_Click(object sender, EventArgs e)
{
string LeFile_Client = System.IO.Path.Combine(appDir, #"FA.csv");
string LeFile_Server = System.IO.Path.Combine(appDir, #"FA_Server.csv");
List<Pers_Ordre> oListClient = Outils.GetCsv(LeFile_Client).OrderBy(t => t.LettreVoidID).ToList();
List<Pers_Ordre> oListServert = Outils.GetCsvServer(LeFile_Server).OrderBy(t => t.LettreVoidID).ToList();
List<Pers_Ordre> LeDiff = new List<Pers_Ordre>();
LeDiff = oListServert.Except(oListClient).ToList();
string Noid = "", OdreID = "";
foreach (var oDiff in LeDiff)
{
Noid += oDiff.LettreVoidID + " ";
OdreID += oDiff.OrdreCummul + " ";
}
MessageBox.Show(Noid + "--" + OdreID);
}
i can not get the right result.
The Lists contain class objects and we would like to iterate through one list, looking for the same item in a second List and report any differences.
to get object that contains in List A but not in List B
and vice versa.
Your current .Except() call will find items from Server that are missing on the client, but it will not find items on the client that are missing on the server.
Try this:
private void simpleButton_Comparer_Click(object sender, EventArgs e)
{
string LeFile_Client = System.IO.Path.Combine(appDir, #"FA.csv");
string LeFile_Server = System.IO.Path.Combine(appDir, #"FA_Server.csv");
var ListClient = Outils.GetCsv(LeFile_Client).OrderBy(t => t.LettreVoidID);
var ListServer = Outils.GetCsvServer(LeFile_Server).OrderBy(t => t.LettreVoidID);
var LeDiff = ListServer.Except(ListClient).Concat(ListClient.Except(ListServer));
var result = new StringBuilder();
foreach (var Diff in LeDiff)
{
result.AppendFormat("{0} --{1} ", Diff.LettreVoidID, Diff.OrdreCummul);
}
MessageBox.Show(Noid.ToString() + "--" + OdreID);
}
This code should also be significantly faster than your original, as it avoids loading the results into memory until it builds the final string. This code in performs the equivalent of two separate sql LEFT JOINs. We could make it faster still by doing one FULL JOIN, but that would require writing our own linq operator method as well.

Categories