rpf
rpf

Reputation: 3712

Trim all string properties

I need to trim some string properties in my objects, but I don't want to go to all objects and properties and in the set properties do the trim method (there is a lot of objects, 300+ and a lot of string properties).

One tip: all my objects have a super class called CoreTransaction, so I can use it (with some kind of reflection) to do this thing more easily.

Is that possible?

Upvotes: 26

Views: 26892

Answers (13)

College Code
College Code

Reputation: 1146

Here is a solution supporting nested collections and strings:

public static T TrimStringProperties<T>(this T input)
{
    if (input is null)
    {
        return input;
    }

    var props = input.GetType()
            .GetProperties(BindingFlags.Instance | BindingFlags.Public)
            .Where(prop => prop.GetIndexParameters().Length == 0)
            .Where(prop => prop.CanWrite && prop.CanRead);

    foreach (PropertyInfo prop in props)
    {
        var value = prop.GetValue(input, null);

        if (value is string stringValue && stringValue != null)
        {
            prop.SetValue(input, stringValue.Trim(), null);
        }
        else if (value is IEnumerable enumerable)
        {
            foreach (var item in enumerable)
            {
                TrimStringProperties(item);
            }
        }
    }

    return input;
}

Upvotes: 1

MYriad
MYriad

Reputation: 118

Thanks @Teter28 for the idea with code generation. It's much more effective than solutions with reflection. The provided code example does not work. Here's ready for use example.

public static class Trimmer<T>
{
    private static readonly Action<T> TrimAllStringFieldsAction = CreateTrimAllStringPropertiesMethod();

    public static void TrimAllStringProperties(T parameter)
    {
        TrimAllStringFieldsAction(parameter);
    }

    private static Action<T> CreateTrimAllStringPropertiesMethod()
    {
        var parameter = Expression.Parameter(typeof(T));
        var trimMethod = typeof(string).GetMethod(nameof(string.Trim), Type.EmptyTypes);
        return Expression.Lambda<Action<T>>(
            Expression.Block(
                parameter.Type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
                    .Where(propertyInfo => propertyInfo.PropertyType == typeof(string))
                    .Select(propertyInfo => Expression.Assign(
                        Expression.Property(parameter, propertyInfo),
                        Expression.Call(Expression.Property(parameter, propertyInfo), trimMethod!)))),
            parameter)
            .Compile();
    }
}

Upvotes: 1

Colin
Colin

Reputation: 2011

I took OwN's answer but made these changes:

  • used early exit to decrease the nesting of if
  • used var everywhere, renaming a few variables
  • added Unit Tests

ObjectExtensions.cs

using System;
using System.Collections;
using System.Reflection;

namespace YourProject.Infrastructure.Extensions
{
    public static class ObjectExtensions
    {
        // Derived from https://stackoverflow.com/a/50193184/
        public static void TrimAllStrings<TSelf>(this TSelf obj)
        {
            if (obj == null)
            {
                return;
            }

            if (obj is IEnumerable)
            {
                foreach (var item in obj as IEnumerable)
                {
                    item.TrimAllStrings();
                }
                return;
            }

            var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;
            foreach (var prop in obj.GetType().GetProperties(flags))
            {
                var nodeType = prop.PropertyType;
                if (nodeType == typeof(String))
                {
                    string currentValue = (string)prop.GetValue(obj, null);
                    if (currentValue != null)
                    {
                        prop.SetValue(obj, currentValue.Trim(), null);
                    }
                }
                // see http://stackoverflow.com/questions/4444908/detecting-native-objects-with-reflection
                else if (nodeType != typeof(object) && Type.GetTypeCode(nodeType) == TypeCode.Object)
                {
                    prop.GetValue(obj, null).TrimAllStrings();
                }
            }
        }
    }
}

ObjectExtensionsTests.cs

using System.Collections.Generic;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YourProject.Infrastructure.Extensions;

namespace YourProjectTests.Infrastructure.Extensions
{
    [TestClass]
    public class ObjectExtensionsTests
    {
        [TestMethod]
        public void NullObject_DoesNothing()
        {
            // Arrange
            SomeStringPropertiesClass test = null;

            // Act
            test.TrimAllStrings();
        }

        public class NoStringPropertiesClass
        {
            public int IntProperty { get; set; }
        }

        [TestMethod]
        public void NoStringProperties_DoesNothing()
        {
            // Arrange
            var test = new NoStringPropertiesClass()
            {
                IntProperty = 42
            };

            // Act
            test.TrimAllStrings();

            // Assert
            test.IntProperty.Should().Be(42);
        }

        public class SomeStringPropertiesClass
        {
            public int IntProperty { get; set; }
            public string StringProperty1 { get; set; }
            public string StringProperty2 { get; set; }
            public string StringProperty3 { get; set; }
            public List<SomeStringPropertiesClass> Children { get; set; } = new();
        }

        [TestMethod]
        public void SomeStringProperties_DoesTrimStrings()
        {
            // Arrange
            var test = new SomeStringPropertiesClass()
            {
                IntProperty = 42,
                StringProperty1 = "Already trimmed string",
                StringProperty2 = "  Needs trimming  ",
                StringProperty3 = "",
                Children = new()
                {
                    new SomeStringPropertiesClass()
                    {
                        StringProperty1 = "  Child that needs trimming  ",
                        StringProperty2 = null,
                        StringProperty3 = "  Child that needs trimming .  ",
                        Children = new()
                        {
                            null,
                            new SomeStringPropertiesClass()
                            {
                                StringProperty2 = "  Grandchild that needs trimming  ",
                            },
                            null
                        }
                    }
                }
            };

            // Act
            test.TrimAllStrings();

            // Assert
            test.IntProperty.Should().Be(42);
            test.StringProperty1.Should().Be("Already trimmed string");
            test.StringProperty2.Should().Be("Needs trimming");
            test.StringProperty3.Should().BeEmpty();
            test.Children[0].StringProperty1.Should().Be("Child that needs trimming");
            test.Children[0].StringProperty2.Should().BeNull();
            test.Children[0].StringProperty3.Should().Be("Child that needs trimming .");
            test.Children[0].Children[1].StringProperty1.Should().BeNull();
            test.Children[0].Children[1].StringProperty2.Should().Be("Grandchild that needs trimming");
            test.Children[0].Children[1].StringProperty3.Should().BeNull();
        }
    }
}

Upvotes: 1

Ben Morris
Ben Morris

Reputation: 282

Extended Own's solution and added a check that it's possible to write to property. Had some "Property set method not found" errors due to a Uri property.

public static class ExtensionMethods
{
    public static void TrimAllStrings<TSelf>(this TSelf obj)
    {
        if(obj != null)
        {
            if(obj is IEnumerable)
            {
                foreach(var listItem in obj as IEnumerable)
                {
                    listItem.TrimAllStrings();
                }
            }
            else
            {
                BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;

                foreach (PropertyInfo p in obj.GetType().GetProperties(flags))
                {
                    Type currentNodeType = p.PropertyType;
                    if (currentNodeType == typeof (String))
                    {
                        string currentValue = (string)p.GetValue(obj, null);
                        if (currentValue != null && p.CanWrite)
                        {
                            p.SetValue(obj, currentValue.Trim(), null);
                        }
                    }
                    // see http://stackoverflow.com/questions/4444908/detecting-native-objects-with-reflection
                    else if (currentNodeType != typeof (object) && Type.GetTypeCode(currentNodeType) == TypeCode.Object)
                    {
                        p.GetValue(obj, null).TrimAllStrings();
                    }
                }
            }
        }
    }
}

Upvotes: 2

thrawnis
thrawnis

Reputation: 341

Thank you to Bala R for your solution to the OP's problem. I converted your solution to an extension method and fixed a problem where null values were throwing errors.

    /// <summary>Trim all String properties of the given object</summary>
    public static TSelf TrimStringProperties<TSelf>(this TSelf input)
    {
        var stringProperties = input.GetType().GetProperties()
            .Where(p => p.PropertyType == typeof(string) && p.CanWrite);

        foreach (var stringProperty in stringProperties)
        {
            string currentValue = (string)stringProperty.GetValue(input, null);
            if (currentValue != null)
                stringProperty.SetValue(input, currentValue.Trim(), null);
        }
        return input;
    }

Upvotes: 24

OwN
OwN

Reputation: 1338

I fixed landi's answer to accommodate child nullable objects and handle IEnumerable collections (loop through a List of object and trim string properties). I made an edit to his answer which was rejected for not being on topic, but that's a load of garbage. Hopefully this helps someone, as landi's answer didn't work on every object type I had. Now it does.

public static class ExtensionMethods
{
    public static void TrimAllStrings<TSelf>(this TSelf obj)
    {
        if(obj != null)
        {
            if(obj is IEnumerable)
            {
                foreach(var listItem in obj as IEnumerable)
                {
                    listItem.TrimAllStrings();
                }
            }
            else
            {
                BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;

                foreach (PropertyInfo p in obj.GetType().GetProperties(flags))
                {
                    Type currentNodeType = p.PropertyType;
                    if (currentNodeType == typeof (String))
                    {
                        string currentValue = (string)p.GetValue(obj, null);
                        if (currentValue != null)
                        {
                            p.SetValue(obj, currentValue.Trim(), null);
                        }
                    }
                    // see http://stackoverflow.com/questions/4444908/detecting-native-objects-with-reflection
                    else if (currentNodeType != typeof (object) && Type.GetTypeCode(currentNodeType) == TypeCode.Object)
                    {
                        p.GetValue(obj, null).TrimAllStrings();
                    }
                }
            }
        }
    }
}

Upvotes: 11

Joe Mancuso
Joe Mancuso

Reputation: 2129

You could use reflection to do something like this:

// o is your instance object 
List<PropertyInfo> fields = o.GetType().GetProperties()
        .Where(i => i.PropertyType == typeof(string));
fields.ForEach(i => i.SetValue(o, ((string)i.GetValue(o, null)).Trim(), new object[]{}));

Upvotes: 4

Diogo Luis
Diogo Luis

Reputation: 228

Thank landi for his solution. I altered his method to add support for Classes with Index Parameters and also to check if the obj is null before continuing.

public static void TrimAllStrings<TSelf>(this TSelf obj)
    {
        if (obj == null)
            return;

        BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;

        foreach (PropertyInfo p in obj.GetType().GetProperties(flags))
        {
            Type currentNodeType = p.PropertyType;
            if (currentNodeType == typeof(String))
            {
                string currentValue = (string)p.GetValue(obj, null);
                if (currentValue != null)
                {
                    p.SetValue(obj, currentValue.Trim(), null);
                }
            }
            // see http://stackoverflow.com/questions/4444908/detecting-native-objects-with-reflection
            else if (currentNodeType != typeof(object) && Type.GetTypeCode(currentNodeType) == TypeCode.Object)
            {
                if (p.GetIndexParameters().Length == 0)
                {
                    p.GetValue(obj, null).TrimAllStrings();
                }else
                {
                    p.GetValue(obj, new Object[] { 0 }).TrimAllStrings();
                }
            }
        }
    }

Upvotes: 2

landi
landi

Reputation: 343

I have written a Extension Method which also takes care of subclasses and strings on referenced classes (like parent.Child.Name)

public static class ExtensionMethods
{
    public static void TrimAllStrings<TSelf>(this TSelf obj)
    {
        BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;

        foreach (PropertyInfo p in obj.GetType().GetProperties(flags))
        {
            Type currentNodeType = p.PropertyType;
            if (currentNodeType == typeof (String))
            {
                string currentValue = (string)p.GetValue(obj, null);
                if (currentValue != null)
                {
                    p.SetValue(obj, currentValue.Trim(), null);
                }
            }
            // see http://stackoverflow.com/questions/4444908/detecting-native-objects-with-reflection
            else if (currentNodeType != typeof (object) && Type.GetTypeCode(currentNodeType) == TypeCode.Object)
            {
                p.GetValue(obj, null).TrimAllStrings();
            }
        }
    }
}

Upvotes: 7

Teter28
Teter28

Reputation: 524

You can try it:

static public class Trim<T>
    where T : class
{
    static public readonly Action<T> TrimAllStringFields = Trim<T>.CreateTrimAllStringFields();

    static private Action<T> CreatTrimAllStringFields()
    {
        var instance = Expression.Parameter(typeof(T));
        return Expression.Lambda<Action<T>>(Expression.Block(instance.Type.GetFields(BindingsFlags.Instance| BindingFlags.NonPublic | BindingFlags.Public).Select(field => Expression.Assign(Expression.Field(instance, field)) as Expression), instance).Compile();
    }
}

Use it like this :

var myinstance = new MyClass();
Trim<MyClass>.TrimAllStringFields(myinstance);

Upvotes: 1

Anthony Huang
Anthony Huang

Reputation: 576

For those who use VB.NET, I converted thrawnis's answer and added a condition to only return those properties that are not ReadOnly. Otherwise if your class has readonly properties you will get runtime error when you try to SetValue for those properties.

''' <summary>
''' Trim all NOT ReadOnly String properties of the given object
''' </summary>
<Extension()>
Public Function TrimStringProperties(Of T)(ByVal input As T) As T
    Dim stringProperties = input.GetType().GetProperties().Where(Function(p) p.PropertyType = GetType(String) AndAlso p.CanWrite)

    For Each stringProperty In stringProperties
        Dim currentValue As String = Convert.ToString(stringProperty.GetValue(input, Nothing))
        If currentValue IsNot Nothing Then
            stringProperty.SetValue(input, currentValue.Trim(), Nothing)
        End If
    Next
    Return input
End Function

Upvotes: 1

Buh Buh
Buh Buh

Reputation: 7546

I'm not sure about changing the behaviour of your accessors. That doesn't sound easy at all. How about adding the trimming to your base class?

    class CoreTransaction
    {
        public void Trim()
        {
            IEnumerable<PropertyInfo> stringProperties =
                this.GetType().GetProperties()
                .Where(p => p.PropertyType == typeof(string) && p.CanRead && p.CanWrite);

            foreach (PropertyInfo property in stringProperties)
            {
                string value = (string)property.GetValue(this, null);
                value = value.Trim();
                property.SetValue(this, value, null);
            }
        }
    }

(Also, note the check that your fields can be both read and written to.)

 

EDIT: You could then add something like this to your base class, and trim all of them in one go. The WeakReference class will allow you to easily keep track of your instances without getting in the way of the garbage collector:

class CoreTransaction
{
    private static List<WeakReference> allCoreTransactions = new List<WeakReference>();

    public CoreTransaction()
    {
        allCoreTransactions.Add(new WeakReference(this));
    }

    public static void TrimAll()
    {
        foreach (WeakReference reference in allCoreTransactions)
        {
            if (reference.IsAlive)
            {
                ((CoreTransaction)reference.Target).Trim();
            }
        }
    }
}

Upvotes: 6

Bala R
Bala R

Reputation: 108957

var stringProperties = obj.GetType().GetProperties()
                          .Where(p => p.PropertyType == typeof (string));

foreach (var stringProperty in stringProperties)
{
    string currentValue = (string) stringProperty.GetValue(obj, null);
    stringProperty.SetValue(obj, currentValue.Trim(), null) ;
}

Upvotes: 54

Related Questions