C# Mixins With State

With the C# quasi-mixin pattern, one of the biggest limitations is that the mixins can't introduce state to a class. An alternative was for the mixin interface to require the composing classes to implement a property that the mixin would use as its own, as demonstrated in this example.

In the .NET Framework 4.0, the ConditionalWeakTable<TKey, TValue> class can be used to associate arbitrary state to any instance. It's thread safe and it keeps a dictionary of weak references from the target instance to its associated state. If a target instance has no references outside the conditional weak table, then it can be reclaimed by the garbage collector. When that happens, its entry in the table is removed.

This makes it possible for a mixin-like class to maintain state for the instances it extends. Let's look at a mixin for a message container:

public enum MessageType { 
  Error, 
  Warning, 
  Info 
}

public class Message {
  public readonly MessageType Type;
  public readonly string Text;
  public Message(MessageType type, string text) {
    Text = text;
    Type = type;
  }
  public static Message Error(string text) {
    return new Message(MessageType.Error, text);
  }
  // other factory methods...
}

// this is the mixin
public interface MMessageContainer { } public static class MessageContainerCode {
  private static readonly ConditionalWeakTable<MMessageContainer, List<Message>> 
    _messagesTable = new ConditionalWeakTable<MMessageContainer, List<Message>>();

  public static IEnumerable<Message> RetrieveMessages(this MMessageContainer self) {
    return _messagesTable.GetOrCreateValue(self);
  }

  public static void AddMessage(this MMessageContainer self, Message message) {
    _messagesTable.GetOrCreateValue(self).Add(message);
  }

  public static void ClearMessages(this MMessageContainer self) {
    _messagesTable.GetOrCreateValue(self).Clear();
  }
}

The MMessageContainer mixin (actually, its code class) declares a ConditionalWeakTable field to hold the list of messages for each extended class. The GetOrCreateValue method takes the target instance as a key and retrieves the associated list of messages, or creates a new one by calling the default constructor for List<Message>. The mixin can be used like this:

public class Person : MarshalByRefObject, MMessageContainer {
  public string Name { get; set; }
  public int Age { get; set; }

  public void Validate() {
    // needs to use "this" to access extensions to this class
    this.ClearMessages();
    if (Age < 0 || Age > 120) {
      this.AddMessage(Message.Error("Invalid age"));
    }
    if (string.IsNullOrWhiteSpace(Name)) {
      this.AddMessage(Message.Error("Invalid name"));
    }
  }
}

The class Person extends MarshalByRefObject to support remoting, and also uses the message container mixin to maintain a list of validation messages. Let's use this class now:

class Program {
  static void Main(string[] args) {
    var jeanne = new Person { Name = "Jeanne Calment", Age = 122 };
    jeanne.Validate();
    PrintMessages(jeanne);
  }

  private static void PrintMessages(MMessageContainer container) {
    foreach (var message in container.RetrieveMessages()) {
      Console.WriteLine("{0}: {1}", message.Type, message.Text);
    }
  }
}

By running it we find out that Jeanne Calment actually had an invalid age when she passed away. Or is it the program that's invalid?

Error: Invalid age

To associate a number of different values for a class, the mixin can declare a nested state class and use it as a value for the ConditionalWeakTable. So, here's the pattern to use:

public interface MMixin { 
  // required members go here
}
public static class MixinCode {
  // provided methods go here, as extension methods to MMixin

  // to maintain state:
  class State { 
    // public fields or properties for the desired state, e.g.:
    public string Name;
  }
  private static readonly ConditionalWeakTable<MMixin, State>
    _stateTable = new ConditionalWeakTable<MMixin, State>();

  // to access the state:
  public static string GetName(this MMixin self) {
    return _stateTable.GetOrCreateValue(self).Name;
  }
  public static void SetName(this MMixin self, string value) {
    _stateTable.GetOrCreateValue(self).Name = value;
  }
}

There're still limitations to this approach, but most of them are only related to syntax. For example, the mixin still has to be declared with two constructs, an interface and a static class; and it's weird to provide explicit getter and setter methods to create "extension properties", when the language has a nice property syntax. Also, any code that uses the mixin composition classes must also import the mixin namespace in order to access its introduced extensions.

Comments

  1. For a slightly more eloquent approach, you can utilize the generic type system to your advantage.
    Since there is no real benefit to define TKey as a specific type for ConditionalWeakTable,
    and under the covers it gets treated like an object reference type anyhow. We can use "object" as the key.

    The only benefits for a type parameter are to prevent attempts to use non-class types as the key, and to pass a strongly typed instance into the CreateValueCallback method.
    With extension methods we can overcome both of those problems by run-time type validation of the object being used and by using generic type inference to fill in the delegate type. In fact, it isn't even necessary to fill in either the entity or extension type parameters when offering the create delegate, since it is also inferred.

    Using the extension methods you can do the following:

    public class SomeType
    {
    }

    public class ExtendedType
    {
    public int IntProperty { get; set; }
    public string StringProperty { get; set; }
    }

    SomeType t = new SomeType();
    t.ExtendedWith().IntProperty = 5;
    t.ExtendedWith().StringProperty = "TestString";

    The previous is a little verbose and less efficient, but you can also do this:
    var extendedItem = t.ExtendedWith();
    extendedItem.IntProperty = 7;
    extendedItem.StringProperty = "Test String";

    Or:
    var extendedItem = t.ExtendedWith(x => new ExtendedType(x));
    extendedItem.IntProperty = 4;

    Extension methods:

    public static class ExtendableObjectExtensions
    {
    public static bool IsExtendedWith(this object entity)
    where TExtension : class, new()
    {
    ThrowIfNullOrNotReferenceType(entity, false);
    return ExtensionFor.IsExtended(entity);
    }

    public static TExtension ExtendedWith(this object entity)
    where TExtension : class, new()
    {
    ThrowIfNullOrNotReferenceType(entity, false);
    return ExtensionFor.GetFor(entity);
    }

    public static TExtension ExtendedWith(this TEntity entity, Func createExtension)
    where TEntity : class
    where TExtension : class, new()
    {
    ThrowIfNullOrNotReferenceType(entity, false);
    if (createExtension == null)
    throw new ArgumentNullException("createExtension");

    return ExtensionFor.GetFor(entity, x => createExtension(x as TEntity));
    }

    public static bool RemoveExtension(this object entity)
    where TExtension : class, new()
    {
    ThrowIfNullOrNotReferenceType(entity);
    return ExtensionFor.Remove(entity);
    }

    internal static void ThrowIfNullOrNotReferenceType(object entity, bool performTypeCheck = true)
    {
    if (entity == null)
    throw new ArgumentNullException("entity");

    if (!performTypeCheck)
    return;

    if (entity.GetType().IsClass)
    return;

    throw new ArgumentException("The entity must be a reference type.", "entity");
    }

    internal static class ExtensionFor
    where TExtension : class, new()
    {
    private static readonly ConditionalWeakTable _extensions = new ConditionalWeakTable();

    public static bool IsExtended(object entity)
    {
    TExtension value;
    return _extensions.TryGetValue(entity, out value);
    }

    public static bool Remove(object entity)
    {
    return _extensions.Remove(entity);
    }

    public static TExtension GetFor(object entity)
    {
    return _extensions.GetOrCreateValue(entity);
    }

    public static TExtension GetFor(object entity, Func createExtension)
    {
    return _extensions.GetValue(entity, new ConditionalWeakTable.CreateValueCallback(createExtension));
    }
    }
    }

    ReplyDelete
    Replies
    1. It broke my code and stripped the <> tags.

      Delete
    2. Interesting idea! Thanks for sharing.

      Delete
    3. Can Anonymous update his example with the <> tags back? Pretty please?

      Delete
  2. You could reduce all of this code to:

    public static class ObjectExtensions
    {
    private static ConditionalWeakTable extendedData = new ConditionalWeakTable();
    public static dynamic DynamicProperties(this object obj) => extendedData.GetValue(obj, _ => new ExpandoObject());
    }

    ReplyDelete
    Replies
    1. But vs can't show intellisense with dynamic objects.

      Delete
  3. I hate the getXXX/setXXX pattern, my solution:

    public interface ISomeMixin
    {
    public string Name {get; set;}
    }

    public static class SomeExtensions
    {
    public static ISomeMixin AsSomeMixin(this object @this)
    => Extensions.Extend(@this, () => AsSomeMixin, () => new Classes.SomeMixin);

    private static class Classes
    {
    public class SomeMixin: ISomeMixin
    {
    public string Name { get; set;}
    }
    }
    }

    And you use it like this:

    var x = "Hello";
    x.AsSomeMixin().Name = "Mary";
    Console.WriteLine(x.AsSomeMixin().Name;

    ReplyDelete

Post a Comment

Popular posts from this blog

The Acyclic Visitor Pattern

Some OO Design

NRoles: An experiment with roles in C#