How to trim strings in all properties in objects with C#.
The problem
I often work with data objects in C# like this:
class Person
{
public string ID { get ; }
public string FirstName { get ; set ; }
public string LastName { get ; set ; }
public int Age { get ; set ; }
public List < Person > Children { get ; set ; } = new List < Person >();
}
In some situations, like if the object was created from some user input, I want
to trim all strings in the properties. Could I create a simple function that takes any kind
of object and does the work for me? Could it also take care of all child objects?
The solution
With a little help of reflection, a recursive function could do the job. The code
is not trivial, but also not too complicated:
public void TrimProperties ( object obj , bool recursive = true )
{
TrimProperties ( obj , recursive , recursive ? new List < object >() : null );
}
private void TrimProperties ( object obj , bool recursive , List < object > visitedObjects )
{
if ( obj == null )
{
return ;
}
if ( visitedObjects != null )
{
if ( visitedObjects . Contains ( obj ))
{
return ;
}
}
Type t = obj . GetType ();
if ( recursive && t . IsClass )
{
visitedObjects . Add ( obj );
}
foreach ( var prop in t . GetProperties ( BindingFlags . Instance | BindingFlags . Public ))
{
if (! prop . CanWrite )
{
continue ;
}
if ( prop . PropertyType == typeof ( string ))
{
string val = ( string ) prop . GetValue ( obj );
if ( val != null )
{
prop . SetValue ( obj , val . Trim ());
}
}
else if ( recursive )
{
if ( typeof ( System . Collections . IEnumerable ). IsAssignableFrom ( prop . PropertyType ))
{
System . Collections . IEnumerable en = ( System . Collections . IEnumerable ) prop . GetValue ( obj );
foreach ( var e in en )
{
TrimProperties ( e , true , visitedObjects );
}
}
else if ( prop . PropertyType . IsClass )
{
var subobj = prop . GetValue ( obj );
TrimProperties ( subobj , true , visitedObjects );
}
else if (! prop . PropertyType . IsPrimitive )
{
var subobj = prop . GetValue ( obj );
TrimProperties ( subobj , true , visitedObjects );
prop . SetValue ( obj , subobj );
}
}
}
}
And here is some unit test too:
public class UnitTestStringHelper
{
struct TestPersonDetails
{
public string LastName { get ; set ; }
public int Age { get ; set ; }
}
class BaseTestPerson
{
public string FirstName { get ; set ; }
}
class TestPerson : BaseTestPerson
{
public static string MagicName { get ; set ; }
public TestPerson ( string id )
{
ID = id ;
}
public string ID { get ; }
public TestPersonDetails Details { get ; set ; }
public TestPerson Parent { get ; set ; }
public List < TestPerson > Children { get ; set ; } = new List < TestPerson >();
protected string SecretWithSpaces { get ; set ; } = " spaces! " ;
public bool SecretContainsSpaces => SecretWithSpaces . Contains ( " " );
}
[ Fact ]
public void TrimProperties_Null ()
{
TestPerson p = null ;
new StringHelper (). TrimProperties ( p );
}
[ Fact ]
public void TrimProperties_NotRecursive ()
{
TestPerson parent = new TestPerson ( " 1 " )
{
FirstName = null ,
Details = new TestPersonDetails ()
{
LastName = " last-parent " ,
Age = 50
}
};
TestPerson child = new TestPerson ( " 2 " )
{
FirstName = " first-child " ,
Details = new TestPersonDetails ()
{
LastName = " last-child " ,
Age = 25
}
};
TestPerson grandchild = new TestPerson ( " 3 " )
{
FirstName = " first-grandchild " ,
Details = new TestPersonDetails ()
{
LastName = " last-grandchild " ,
Age = 0
}
};
TestPerson . MagicName = " Santa " ;
parent . Children . Add ( child );
child . Children . Add ( grandchild );
grandchild . Parent = child ;
child . Parent = parent ;
new StringHelper (). TrimProperties ( child , false );
Assert . Equal ( " 1 " , parent . ID );
Assert . Null ( parent . FirstName );
Assert . Equal ( " last-parent " , parent . Details . LastName );
Assert . Equal ( 50 , parent . Details . Age );
Assert . True ( parent . SecretContainsSpaces );
Assert . Equal ( " 2 " , child . ID );
Assert . Equal ( "first-child" , child . FirstName );
Assert . Equal ( " last-child " , child . Details . LastName );
Assert . Equal ( 25 , child . Details . Age );
Assert . True ( child . SecretContainsSpaces );
Assert . Equal ( " 3 " , grandchild . ID );
Assert . Equal ( " first-grandchild " , grandchild . FirstName );
Assert . Equal ( " last-grandchild " , grandchild . Details . LastName );
Assert . Equal ( 0 , grandchild . Details . Age );
Assert . True ( grandchild . SecretContainsSpaces );
Assert . Equal ( " Santa " , TestPerson . MagicName );
}
[ Theory ]
[ InlineData ( 0 )]
[ InlineData ( 1 )]
[ InlineData ( 2 )]
public void TrimProperties_Recursive ( int level )
{
TestPerson parent = new TestPerson ( " 1 " )
{
FirstName = null ,
Details = new TestPersonDetails ()
{
LastName = " last-parent " ,
Age = 50
}
};
TestPerson child = new TestPerson ( " 2 " )
{
FirstName = " first-child " ,
Details = new TestPersonDetails ()
{
LastName = " last-child " ,
Age = 25
}
};
TestPerson grandchild = new TestPerson ( " 3 " )
{
FirstName = " first-grandchild " ,
Details = new TestPersonDetails ()
{
LastName = " last-grandchild " ,
Age = 0
}
};
TestPerson . MagicName = " Santa " ;
parent . Children . Add ( child );
child . Children . Add ( grandchild );
grandchild . Parent = child ;
child . Parent = parent ;
if ( level == 1 )
{
new StringHelper (). TrimProperties ( child , true );
}
else if ( level == 2 )
{
new StringHelper (). TrimProperties ( grandchild , true );
}
else
{
new StringHelper (). TrimProperties ( parent , true );
}
Assert . Equal ( " 1 " , parent . ID );
Assert . Null ( parent . FirstName );
Assert . Equal ( "last-parent" , parent . Details . LastName );
Assert . Equal ( 50 , parent . Details . Age );
Assert . True ( parent . SecretContainsSpaces );
Assert . Equal ( " 2 " , child . ID );
Assert . Equal ( "first-child" , child . FirstName );
Assert . Equal ( "last-child" , child . Details . LastName );
Assert . Equal ( 25 , child . Details . Age );
Assert . True ( child . SecretContainsSpaces );
Assert . Equal ( " 3 " , grandchild . ID );
Assert . Equal ( "first-grandchild" , grandchild . FirstName );
Assert . Equal ( "last-grandchild" , grandchild . Details . LastName );
Assert . Equal ( 0 , grandchild . Details . Age );
Assert . True ( grandchild . SecretContainsSpaces );
Assert . Equal ( " Santa " , TestPerson . MagicName );
}
}
Summary
I rarely use reflection, so this was a fun little experiment for me. In C# 9,
that will be released very soon, there is support for source generators. With
this, it might be possible to automatically generate code that does the
trimming. Someday I might give this a try.