views:

378

answers:

8

I want to enforce on my code base immutable rule with following test

[TestFixture]
public class TestEntityIf
{
    [Test]
    public void IsImmutable()
    {
        var setterCount =
            (from s in typeof (Entity).GetProperties(BindingFlags.Public | BindingFlags.Instance)
             where s.CanWrite
             select s)
                .Count();

        Assert.That(setterCount == 0, Is.True, "Immutable rule is broken");
    }
}

It passes for:

public class Entity
{
    private int ID1;
    public int ID
    {
        get { return ID1; }
    }
}

but doesn't for this:

public class Entity
{
    public int ID { get; private set; }
}

And here goes the question "WTF?"

+1  A: 

Surely it's your private set in the second one.

In the first, an instance of the class Entity can have its ID1 property written to by an external class.

In the latter, the setter is private to the class itself and so can only be called from within Entity (i.e. it's own constructor/initializer)

Take the private out of your setter in the second and it should pass

Eoin Campbell
ID1 is a backing field and not a property in the first class and it is readonly via a getter to the outside world. How can it be accessible by an external class?
Mehmet Aras
In his original example, he had a public setter on the first property. He has since edited the Question. http://stackoverflow.com/revisions/839066/list
Eoin Campbell
I see. Thanks Eoin.
Mehmet Aras
+2  A: 

I think the reason is that CanWrite says that it returns true if there's a setter. A private setter is also a setter.

What surprises me a bit is that the first passes, as it has a public setter, so unless I'm still too low on cafeine, the settercount is 1 so the assert should fail. They both should fail, as CanWrite simply returns true for both. (and the linq query simply retrieves public properties, including the ID, as it is public)

(edit) I now see you changed the code of the first class, so it doesn't have a setter anymore.

So the thing is your assumption that CanWrite looks at accessors of the setter method, that's not the case. You should do:

var setterCount =
            (from s in typeof (Entity).GetProperties(BindingFlags.Public | BindingFlags.Instance)
             where s.GetSetMethod().IsPublic
             select s)
                .Count();
Frans Bouma
I've just tried the code, and I agree. I get a count of 1 setter for both test classes.
Martin Harris
Unless I am too low on caffine too, I don't see the setter in the first case. ID1 is a member variable, not a property so it doesn't have a "setter" method?
Colin Desmond
exactly. What TS should do instead is a call to GetSetMethod() on the propertyinfo, and then check whether IsPublic is true on the returned MethodInfo.
Frans Bouma
@Colin: Ah, I see he now changed the code. When I replied he had a get AND set in the ID property of Entity.
Frans Bouma
You're too low on caffeine.
Joe
+6  A: 

The problem is, that the property is public - because of the public getter - and it is writeable - because of the private setter. You will have to refine your test.

Further I want to add, that you cannot guarantee immutability this way because you could modify private data inside methods. To guarantee immutability you have to check that all fields are declared readonly and there are no auto-implemented properties.

public static Boolean IsImmutable(this Type type)
{
    const BindingFlags flags = BindingFlags.Instance |
                               BindingFlags.NonPublic |
                               BindingFlags.Public;

    return type.GetFields(flags).All(f => f.IsInitOnly);
}

public static Boolean IsImmutable(this Object @object)
{
    return (@object == null) ? true : @object.GetType().IsImmutable();
}

With this extension method you can easily test types

typeof(MyType).IsImmutable()

and instances

myInstance.IsImmutable()

for their immutability.

Notes

  • Looking at instance fields allows you to have writable properties, but ensures that there are now fields that could be modified.
  • Auto-implemented properties will fail the immutability test as exspected because of the private, anonymous backing field.
  • You can still modify readonly fields using reflection.
  • Maybe one should check that the types of all fields are immutable, too, because this objects belong to the state.
  • This cannot be done with a simple FiledInfo.FieldType.IsImmutable() for all fields because of possible cycles and because the base types are mutable.
Daniel Brückner
Any reason for using `Object` and `Boolean` instead of `object` and `bool`?
Konrad Rudolph
They look nicer (syntax highlighting and upper case first letter) and they are real types and do not depend on what the compiler generates for string, bool, int, object, and all the others (while I know that the compiler has no choice).
Daniel Brückner
Are there good reasons against this?
Daniel Brückner
Good reasons against it: yes; consistency in code, shorter, well-established type names. Also, you don't need to import `System` to use them (something that actually only makes sense when you're not using an IDE to program C# – like me).
Konrad Rudolph
Always using Boolean, Object, Int32, and so on is consistent, too, and it looks even more consitent because of the uppercase first letter. Okay, they are a bit shorter, but - thanks to IntelliSense - this does not really matter. And the using statement comes for free from the IDE. So I will stick with that.
Daniel Brückner
+3  A: 

You probably need to call

propertyInfo.GetSetMethod().IsPublic

because the set-method and the get-method do not have the same access modifier, you can't rely on the PropertyInfo itself.

    var setterCount =
        (from s in typeof (Entity).GetProperties(
           BindingFlags.Public 
           | BindingFlags.NonPublic 
           | BindingFlags.Instance)
         where 
           s.GetSetMethod() != null       // public setter available
         select s)
Stefan Steinegger
+1 This is nearly there. But GetSetMethod() will return null for private setters or properties with no setters. You need to change this to test for null.
Joe
@Joe: Ok, fixed it, then I don't need the CanWrite anymore
Stefan Steinegger
You don't need s.GetSetMethod().IsPublic, too, because ProeprtyInfo.GetSetMethod() returns only public set methods unless you call ProeprtyInfo.GetSetMethod(Boolean nonPublic).
Daniel Brückner
Here the test for IsPublic is redundant, since the GetSetMethod() overload will not return a non-public accessor. You can use GetSetMethod(true) to return any set accessor, then test IsPublic or IsPrivate, depending on how you want to handle a protected accessor.
Joe
Stefan Steinegger
+1  A: 

If I understood you correctly, you want Entity to be immutable. If so, then the test should be changed to

var setterCount = (from s in typeof(string).GetProperties(BindingFlags.Public | BindingFlags.Instance).Select(p => p.GetSetMethod())
    where s != null && s.IsPublic
    select s).Count();

Assert.That(setterCount == 0, Is.True, "Immutable rule is broken");
SeeR
The assertion is correct - the type is immutable if setterCount == 0 is true and the assertion will show the message "Immutable rule is broken" if this test fails.
Daniel Brückner
Fixed, thanks Daniel
SeeR
+2  A: 

I suggest a change in the property condition to:

s.CanWrite && s.GetSetMethod().IsPublic
bruno conde
A: 

Have you tried to debug it to see what properties whose CanWrite property is true are being returned by your LINQ query? Apparently, it is picking up things that you don't expect. With that knowledge, you can make your LINQ query more selective to work the way you want it to. Also, I agree with @Daniel Bruckner that your class is not completely mutable unless fields are also readonly. The caller may call a method or use a getter that may internally modify private fields that're exposed publicly as readonly via getters and this would break the immutable rule, take the client by surprise, and cause problems. The operations on mutable object should not have side effects.

Mehmet Aras
+3  A: 

A small modification to answers posted elsewhere. The following will return non-zero if there is at least one property with a protected or public setter. Note the check for GetSetMethod returning null (no setter) and the test for IsPrivate (i.e. not public or protected) rather than IsPublic (public only).

    var setterCount =
           (from s in typeof(Entity).GetProperties(
              BindingFlags.Public
              | BindingFlags.NonPublic
              | BindingFlags.Instance)
            where
              s.GetSetMethod(true) != null // setter available
              && (!s.GetSetMethod(true).IsPrivate)
            select s).Count();

Nevertheless, as pointed out in Daniel Brückner's answer, the fact that a class has no publicly-visible property setters is a necessary, but not a sufficient condition for the class to be considered immutable.

Joe
The test s.GetSetMethod(true) != null is superfluous because CanWrite == true guarantees the existence of a setter.
Daniel Brückner
True, "s.GetSetMethod(true) != null" is equivalent to CanWrite being true, so one of them is redundant. I've removed CanWrite as I want to be explicit about testing for null before dereferencing the IsPrivate property.
Joe