Search code examples
neventstore

Event Up-Conversion With Keeping Event-Class Name


NEventStore 3.2.0.0

As far as I found out it is required by NEventStore that old event-types must kept around for event up-conversion.
To keep them deserializing correctly in the future they must have an unique name. It is suggested to call it like EventEVENT_VERSION.

Is there any way to avoid EventV1, EventV2,..., EventVN cluttering up your domain model and simply keep using Event?
What are your strategies?


Solution

  • In a question long, long time ago, an answer was missing...

    In the discussion referred in the comments, I came up with an - I would say - elegant solution:

    Don't save the type-name but an (versioned) identifier

    The identifier is set by an attribute on class-level, i.e.

    namespace CurrentEvents
    {
        [Versioned("EventSomethingHappened", 0)] // still version 0
        public class EventSomethingHappened
        {
            ...
        }
    }
    

    This identifier should get serialized in/beside the payload. In serialized form
    "Some.Name.Space.EventSomethingHappened" -> "EventSomethingHappened|0"

    When another version of this event is required, the current version is copied in an "legacy" assembly or just in another Namespace and renamed (type-name) to "EventSomethingHappenedV0" - but the Versioned-attribute remains untouched (in this copy)

    namespace LegacyEvents
    {
        [Versioned("EventSomethingHappened", 0)] // still version 0
        public class EventSomethingHappenedV0
        {
            ...
        }
    }
    

    In the new version (at the same place, under the same name) just the version-part of the attribute gets incremented. And that's it!

    namespace CurrentEvents
    {
        [Versioned("EventSomethingHappened", 1)] // new version 1
        public class EventSomethingHappened
        {
            ...
        }
    }
    

    Json.NET supports binders which maps type-identifiers to types and back. Here is a production-ready binder:

    public class VersionedSerializationBinder : DefaultSerializationBinder
    {
        private Dictionary<string, Type> _getImplementationLookup = new Dictionary<string, Type>();
        private static Type[] _versionedEvents = null;
    
        protected static Type[] VersionedEvents
        {
            get
            {
                if (_versionedEvents == null)
                    _versionedEvents = AppDomain.CurrentDomain.GetAssemblies()
                        .Where(x => x.IsDynamic == false)
                        .SelectMany(x => x.GetExportedTypes()
                            .Where(y => y.IsAbstract == false &&
                                y.IsInterface == false))
                        .Where(x => x.GetCustomAttributes(typeof(VersionedAttribute), false).Any())
                        .ToArray();
    
                return _versionedEvents;
            }
        }
    
        public VersionedSerializationBinder()
        {
    
        }
    
        private VersionedAttribute GetVersionInformation(Type type)
        {
            var attr = type.GetCustomAttributes(typeof(VersionedAttribute), false).Cast<VersionedAttribute>().FirstOrDefault();
    
            return attr;
        }
    
        public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
        {
            var versionInfo = GetVersionInformation(serializedType);
            if (versionInfo != null)
            {
                var impl = GetImplementation(versionInfo);
    
                typeName = versionInfo.Identifier + "|" + versionInfo.Revision;
            }
            else
            {
                base.BindToName(serializedType, out assemblyName, out typeName);
            }
    
            assemblyName = null;
        }
    
        private VersionedAttribute GetVersionInformation(string serializedInfo)
        {
            var strs = serializedInfo.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
    
            if (strs.Length != 2)
                return null;
    
            return new VersionedAttribute(strs[0], strs[1]);
        }
    
        public override Type BindToType(string assemblyName, string typeName)
        {
            if (typeName.Contains('|'))
            {
                var type = GetImplementation(GetVersionInformation(typeName));
                if (type == null)
                    throw new InvalidOperationException(string.Format("VersionedEventSerializationBinder: No implementation found for type identifier '{0}'", typeName));
                return type;
            }
            else
            {
                var versionInfo = GetVersionInformation(typeName + "|0");
                if (versionInfo != null)
                {
                    var type = GetImplementation(versionInfo);
                    if (type != null)
                        return type;
                    // else: continue as it is a normal serialized object...
                }
            }
    
            // resolve assembly name if not in serialized info
            if (string.IsNullOrEmpty(assemblyName))
            {
                Type type;
                if (typeName.TryFindType(out type))
                {
                    assemblyName = type.Assembly.GetName().Name;
                }
            }
    
            return base.BindToType(assemblyName, typeName);
        }
    
        private Type GetImplementation(VersionedAttribute attribute)
        {
            Type eventType = null;
    
            if (_getImplementationLookup.TryGetValue(attribute.Identifier + "|" + attribute.Revision, out eventType) == false)
            {
                var events = VersionedEvents
                    .Where(x =>
                    {
                        return x.GetCustomAttributes(typeof(VersionedAttribute), false)
                            .Cast<VersionedAttribute>()
                            .Where(y =>
                                y.Revision == attribute.Revision &&
                                y.Identifier == attribute.Identifier)
                            .Any();
                    })
                    .ToArray();
    
                if (events.Length == 0)
                {
                    eventType = null;
                }
                else if (events.Length == 1)
                {
                    eventType = events[0];
                }
                else
                {
                    throw new InvalidOperationException(
                        string.Format("VersionedEventSerializationBinder: Multiple types have the same VersionedEvent attribute '{0}|{1}':\n{2}",
                            attribute.Identifier,
                            attribute.Revision,
                            string.Join(", ", events.Select(x => x.FullName))));
                }
                _getImplementationLookup[attribute.Identifier + "|" + attribute.Revision] = eventType;
            }
            return eventType;
        }
    }
    

    ...and the Versioned-attribute

    [AttributeUsage(AttributeTargets.Class)]
    public class VersionedAttribute : Attribute
    {
        public string Revision { get; set; }
        public string Identifier { get; set; }
    
        public VersionedAttribute(string identifier, string revision = "0")
        {
            this.Identifier = identifier;
            this.Revision = revision;
        }
    
        public VersionedAttribute(string identifier, long revision)
        {
            this.Identifier = identifier;
            this.Revision = revision.ToString();
        }
    }
    

    At last use the versioned binder like this

    JsonSerializer.Create(new JsonSerializerSettings
    {
        TypeNameHandling = TypeNameHandling.All,
        TypeNameAssemblyFormat = FormatterAssemblyStyle.Simple,
        Binder = new VersionedSerializationBinder()
    });
    

    For a full Json.NET ISerialize-implementation see (an little outdated) gist here:
    https://gist.github.com/warappa/6388270