2
\$\begingroup\$

Sometimes we need to map a .NET type to some custom nice looking text and resolve it back to the type latter. We could use it while implementing polymorphic behaviors in JSON API, or persisting/rehydrating class hierarchy.

Sometimes we need something more complex than 1:1 mapping. Let's say that we backed up part of our log database, so it's not so easy to update to comply with event discriminator modifications – we still need to understand old version of the terms, but use the new ones on output.

public abstract class Discriminator<T>
{
    public static Discriminator<T> Null = new NullDiscriminator<T>();

    public T Resolve(string discriminator, params object[] args) =>
        discriminator != null ?
            (T)Activator.CreateInstance(this[discriminator], args) :
            default(T);

    public string this[T obj] => this[obj?.GetType()];
    public abstract string this[Type type] { get; }
    public abstract Type this[string discriminator] { get; }
}

The class above defines two-directional mapping strategy between Type and some text discriminator. There are some implementations of it.

The one to do nothing:

class NullDiscriminator<T> : Discriminator<T>
{
    public override Type this[string discriminator] => null;
    public override string this[Type type] => null;
}

The one to transform type name by trimming namespace prefix and some optional suffix:

public class TrimmerDiscriminator<T> : Discriminator<T>
{
    public TrimmerDiscriminator(string prefix, string suffix = "")
    {
        Prefix = prefix;
        Suffix = suffix;
    }

    string Prefix { get; }
    string Suffix { get; }

    public override Type this[string discriminator]
    {
        get
        {
            if (discriminator == null)
                return null;

            return Type.GetType($"{Prefix}{discriminator}{Suffix}, {typeof(T).Assembly.FullName}");
        }
    }

    public override string this[Type type]
    {
        get
        {
            if (type == null)
                return null;

            var name = type.FullName;
            if (!name.StartsWith(Prefix))
                throw new FormatException("Non matching prefix.");
            if (!name.EndsWith(Suffix))
                throw new FormatException("Non matching suffix.");

            return name
                .Substring(0, name.Length - Suffix.Length)
                .Substring(Prefix.Length);
        }
    }
}

And a decorating one, to explicitly override mapping:

public static class OverrideDiscriminator
{
    public static Discriminator<T> Override<T>(this Discriminator<T> source, Type type, string discriminator) =>
        new OverrideDiscriminator<T>(source, type, discriminator);
}

public class OverrideDiscriminator<T> : Discriminator<T>
{
    public OverrideDiscriminator(Discriminator<T> source, Type type, string discriminator)
    {
        Source = source;
        Type = type;
        Discriminator = discriminator;
    }

    Discriminator<T> Source { get; }
    Type Type { get; } 
    string Discriminator { get; }

    public override Type this[string discriminator]
    {
        get
        {
            if (discriminator == Discriminator)
                return Type;

            return Source[discriminator];
        }
    }

    public override string this[Type type]
    {
        get
        {
            if (Type == type)
                return Discriminator;

            return Source[type];
        }
    }
}  

Let's say we have a demo class hierarchy:

namespace Demo
{
    public abstract class UserId
    {
        public static Discriminator<UserId> Discriminator =
             new TrimmerDiscriminator<UserId>("Demo.", "Id");

        protected UserId(int value)
        { Value = value; }

        public int Value { get; }
    }

    public class EmployeeId : UserId
    {
        Public EmployeeId(int value) : base(value) { }
    }

    public class AdminId : EmployeeId
    {
        public AdminId(int value) : base(value) { }
    }

    public class ClientId : UserId
    {
        public ClientId(int value) : base(value) { }
    }
}

Repository example:

class UserRepository
{
    int _value;
    string _role;

    public UserId UserId
    {
        set
        {
            _value = value.Value;
            _role = UserId.Discriminator[value];
        }
        get
        {
            return UserId.Discriminator
                .Resolve(_role, _value);
        }
    }
}

We could modify this code by locally overriding Discriminator before use:

class UserRepository
{
    static Discriminator<UserId> { get; } Discriminator = 
        UserId.Discriminator
            .Override(typeof(AdminId), "Administrator");

    int _value;
    string _role;

    public UserId UserId
    {
        set
        {
            _value = value.Value;
            _role = Discriminator[value];
        }
        get
        {
            return Discriminator
                .Resolve(_role, _value);
        }
    }
}

Now it rehydrates "Admin" and "Administrator", but writes "Administrator" only. We could start from Discriminator.Null instead of UserId.Discriminator and define mapping schema explicitly with multiple overrides.

\$\endgroup\$
2
\$\begingroup\$
public static Discriminator<T> Null = new NullDiscriminator<T>();

Shouldn't this be a const, or readonly?

And why is it defined in Discriminator? It feels wrong if a parent class knows about its child class.

public TrimmerDiscriminator(string prefix, string suffix = "")
{
    Prefix = prefix;
    Suffix = suffix;
}

string Prefix { get; }
string Suffix { get; }

What's the purpose of using properties where fields would do?

if (!name.StartsWith(Prefix))
    throw new FormatException("Non matching prefix.");
if (!name.EndsWith(Suffix))
    throw new FormatException("Non matching suffix.");

return name
    .Substring(0, name.Length - Suffix.Length)
    .Substring(Prefix.Length);

That's probably not a practical problem, but for what it's worth it wouldn't work properly if prefix and suffix overlapped. I'd use a regex...

I would also include name and the expected (but not matching) prefix or suffix in your FormatException messages. The more contextual info in an exception message, the better. Otherwise if you don't catch it red-handed, you'd be wondering what exactly went wrong.

public UserId UserId
{
    set
    {
        _value = value.Value;
        _role = UserId.Discriminator[value];
    }

This is nitpicky now, but I would change the order of these assignments. Because If Discriminator throws an exception, the object will be left in inconsistent state. Again this is a consideration for easier debugging.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ 1) Thanks, I forgot readonly. 2) Null pattern is usually treated as a special case. It is common to have Singleton as a member of base class and hide Null class itself from consumers by making it nested (not recomended for .NET) or just not public. 3) I could add validation to them latter without changing consuming code: here it should be no null values. \$\endgroup\$ – Dmitry Nogin Apr 2 '16 at 23:55
  • \$\begingroup\$ Ad. 2. I see, the point taken. Ad. 3. I don't understand - validation for getters? Aren't they only set in the constructor? Shouldn't these discriminators be immutable? (readonly would come in handy again :) ) \$\endgroup\$ – Konrad Morawski Apr 2 '16 at 23:59
  • \$\begingroup\$ I would do validation at the scope when it becomes possible, no latter than that - preferring property setter (narrow scope) over ctor (wide scope). Yes, we need create private setters for that. To be pedantic I would allow null>!null transition in the property, but not null>null, !null>null, !null>!null. I mean that ctor should do validation when multiple values need to be orchestrated - see self validating property pattern. All this stuff starting making sense when complexity grows during maintenance. But the first step is to use properties by default. \$\endgroup\$ – Dmitry Nogin Apr 3 '16 at 0:16
  • \$\begingroup\$ Interesting. Yes if that's where it's heading, this is the way to go I guess. Thanks for accepting my answer \$\endgroup\$ – Konrad Morawski Apr 3 '16 at 10:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.