Click here to Skip to main content
15,892,746 members
Articles / Desktop Programming / WPF

Catel - Part 4 of n: Unit testing with Catel

Rate me:
Please Sign up or sign in to vote.
4.55/5 (10 votes)
28 Jan 2011CPOL11 min read 49.1K   572   11  
This article explains how to write unit tests for MVVM using Catel.
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="RedirectDeserializationBinder.cs" company="Catel development team">
//   Copyright (c) 2008 - 2011 Catel development team. All rights reserved.
// </copyright>
// <summary>
//   <see cref="SerializationBinder" /> class that supports backwards compatible serialization.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Runtime.Serialization;
using Catel.Properties;
using Catel.Reflection;
using log4net;

namespace Catel.Runtime.Serialization
{
    /// <summary>
    /// <see cref="SerializationBinder"/> class that supports backwards compatible serialization.
    /// </summary>
    internal sealed class RedirectDeserializationBinder : SerializationBinder
    {
        #region Variables
        /// <summary>
        /// The <see cref="ILog">log</see> object.
        /// </summary>
        private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

        /// <summary>
        /// A dictionary of all <see cref="RedirectTypeAttribute"/> found.
        /// </summary>
        private readonly Dictionary<string, RedirectTypeAttribute> _redirectAttributes = new Dictionary<string, RedirectTypeAttribute>();

        /// <summary>
        /// A list of microsoft public key tokens.
        /// </summary>
        private static List<string> _microsoftPublicKeyTokens;
        #endregion

        #region Constructor & destructor
        /// <summary>
        /// Initializes a new instance of the <see cref="RedirectDeserializationBinder"/> class.
        /// <para />
        /// Creates a custom binder that redirects all the types to new types if required. All properties
        /// decorated with the <see cref="RedirectTypeAttribute"/> will be redirected.
        /// </summary>
        /// <remarks>
        /// This constructor searches for attributes of the current application domain.
        /// </remarks>
        public RedirectDeserializationBinder()
            : this(AppDomain.CurrentDomain) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="RedirectDeserializationBinder"/> class.
        /// <para />
        /// Creates a custom binder that redirects all the types to new types if required. All properties
        /// decorated with the <see cref="RedirectTypeAttribute"/> will be redirected.
        /// </summary>
        /// <param name="appDomain"><see cref="AppDomain"/> to search in.</param>
        /// <remarks>
        /// This constructor searches for attributes in a specific application domain.
        /// </remarks>
        public RedirectDeserializationBinder(AppDomain appDomain)
        {
            Log.Debug(TraceMessages.CreatingRedirectDeserializationBindinger);

            List<Assembly> assemblies = new List<Assembly>();

            foreach (Assembly assembly in AssemblyHelper.GetLoadedAssemblies(appDomain))
            {
                // Skip dynamic assemblies
                if (string.Compare(assembly.FullName, "Anonymously Hosted DynamicMethods Assembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null") != 0)
                {
                    // Skip Microsoft assemblies
                    string company = assembly.Company();
                    if (!company.Contains("Microsoft"))
                    {
                        assemblies.Add(assembly);
                    }
                }
            }

            Initialize(assemblies);
            
            Log.Debug(TraceMessages.CreatedRedirectDeserializationBindinger);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="RedirectDeserializationBinder"/> class.
        /// <para />
        /// This method automatically loads all the assemblies into the current AppDomain and then uses the
        /// <see cref="RedirectDeserializationBinder(IEnumerable{Assembly})"/> constructor.
        /// <para />
        /// Creates a custom binder that redirects all the types to new types if required. All properties
        /// decorated with the <see cref="RedirectTypeAttribute"/> will be redirected.
        /// </summary>
        /// <param name="assemblies">Array of assembly locations that should be searched.</param>
        /// <remarks>
        /// This constructor searches for attributes in specific assemblies.
        /// <para />
        /// No exceptions are caught during the loading of the assemblies. The caller is responsible for handling
        /// the exceptions correctly.
        /// </remarks>
        public RedirectDeserializationBinder(IEnumerable<string> assemblies)
        {
            Log.Debug(TraceMessages.CreatingRedirectDeserializationBindinger);

            List<Assembly> loadedAssemblies = new List<Assembly>();
            foreach (string assembly in assemblies)
            {
                loadedAssemblies.Add(Assembly.Load(assembly));
            }

            Initialize(loadedAssemblies);

            Log.Debug(TraceMessages.CreatedRedirectDeserializationBindinger);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="RedirectDeserializationBinder"/> class.
        /// <para />
        /// Creates a custom binder that redirects all the types to new types if required. All properties
        /// decorated with the <see cref="RedirectTypeAttribute"/> will be redirected.
        /// </summary>
        /// <param name="assemblies">Array of assembly locations that should be searched.</param>
        /// <remarks>
        /// This constructor searches for attributes in specific assemblies.
        /// </remarks>
        public RedirectDeserializationBinder(IEnumerable<Assembly> assemblies)
        {
            Log.Debug(TraceMessages.CreatingRedirectDeserializationBindinger);

			Initialize(assemblies);

            Log.Debug(TraceMessages.CreatedRedirectDeserializationBindinger);
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets the Microsoft public key tokens.
        /// </summary>
        /// <value>The Microsoft public key tokens.</value>
        private static IEnumerable<string> MicrosoftPublicKeyTokens
        {
            get
            {
                if (_microsoftPublicKeyTokens == null)
                {
                    _microsoftPublicKeyTokens = new List<string>();
                    _microsoftPublicKeyTokens.Add("b77a5c561934e089");
                    _microsoftPublicKeyTokens.Add("b03f5f7f11d50a3a");
                    _microsoftPublicKeyTokens.Add("31bf3856ad364e35");
                }

                return _microsoftPublicKeyTokens;
            }
        }
        #endregion

        #region Methods
        /// <summary>
        /// Initializes the binder by searching for all <see cref="RedirectTypeAttribute"/> in the
        /// assemblies passed to this method.
        /// </summary>
        /// <param name="assemblies">Array of assembly locations that should be searched.</param>
        private void Initialize(IEnumerable<Assembly> assemblies)
        {
            Log.Debug(TraceMessages.InitializingRedirectDeserializationBinder);

            Type attributeType = typeof(RedirectTypeAttribute);

            foreach (Assembly assembly in assemblies)
            {
                try
                {
                    foreach (Type type in assembly.GetTypes())
                    {
                        InitializeAttributes(type, (RedirectTypeAttribute[])type.GetCustomAttributes(attributeType, true));

                        foreach (MemberInfo member in type.GetMembers())
                        {
                            InitializeAttributes(member, (RedirectTypeAttribute[])member.GetCustomAttributes(attributeType, true));
                        }
                    }
                }
                catch (Exception ex)
                {
                    Log.Warn(ex, TraceMessages.FailedToGetTypesOfAssembly, assembly.FullName);
                }
            }

            Log.Debug(TraceMessages.InitializedRedirectDeserializationBinder);
        }

        /// <summary>
        /// Initializes the binder by searching for all <see cref="RedirectTypeAttribute"/> in the
        /// attributes passed to this method.
        /// </summary>
        /// <param name="decoratedObject">object that was decorated with the attribute.</param>
        /// <param name="attributes">Array of attributes to search for.</param>
        private void InitializeAttributes(object decoratedObject, RedirectTypeAttribute[] attributes)
        {
            if (decoratedObject == null)
            {
                return;
            }

            if (attributes.Length == 0)
            {
            	return;
            }

            Type type = null;

            if (decoratedObject is Type)
            {
                type = decoratedObject as Type;
            }

            foreach (RedirectTypeAttribute attribute in attributes)
            {
                if (type != null)
                {
                    string typeName = TypeHelper.FormatType(type.Assembly.FullName, type.FullName);
                    typeName = ConvertTypeToVersionIndependentType(typeName);
                    string finalTypeName, finalAssemblyName;
                    SplitType(typeName, out finalAssemblyName, out finalTypeName);

                    attribute.NewTypeName = finalTypeName;
                    attribute.NewAssemblyName = finalAssemblyName;
                }

                if (_redirectAttributes.ContainsKey(attribute.OriginalType))
                {
                    Log.Warn(TraceMessages.RedirectAlreadyAdded, attribute.OriginalType, _redirectAttributes[attribute.OriginalType].TypeToLoad, attribute.TypeToLoad);
                }
                else
                {
                    Log.Debug(TraceMessages.AddingRedirectFromTypeToType, attribute.OriginalTypeName, attribute.NewTypeName);

                    _redirectAttributes.Add(attribute.OriginalType, attribute);
                }
            }
        }

        /// <summary>
        /// Binds an assembly and typename to a specific type.
        /// </summary>
        /// <param name="assemblyName">Original assembly name.</param>
        /// <param name="typeName">Original type name.</param>
        /// <returns><see cref="Type"/> that the serialization should actually use.</returns>
        public override Type BindToType(string assemblyName, string typeName)
        {
            string currentType = TypeHelper.FormatType(assemblyName, typeName);
            string currentTypeVersionIndependent = ConvertTypeToVersionIndependentType(currentType);
            string newType = ConvertTypeToNewType(currentTypeVersionIndependent);

            Type typeToDeserialize = LoadType(newType) ?? (LoadType(currentTypeVersionIndependent) ?? LoadType(currentType));

            if (typeToDeserialize == null)
            {
                Log.Error(Resources.CouldNotLoadNewType, currentType, newType);
            }

            return typeToDeserialize;
        }

        /// <summary>
        /// Tries to load a type on a safe way.
        /// </summary>
        /// <param name="type">The type to load.</param>
        /// <returns>The type or <c>null</c> if this method fails.</returns>
        /// <remarks>
        /// In case this method fails to load the type, a warning will be traced with additional information.
        /// </remarks>
        private static Type LoadType(string type)
        {
            Type loadedType = null;

            try
            {
                loadedType = Type.GetType(type);
            }
            catch (Exception ex)
            {
                Log.Warn(ex, TraceMessages.FailedToLoadType, type);
            }

            return loadedType;
        }

        /// <summary>
        /// Splits the type into a type name and assembly name.
        /// </summary>
        /// <param name="type">Type to split.</param>
        /// <param name="assemblyName">Assemby name.</param>
        /// <param name="typeName">Type name.</param>
        private static void SplitType(string type, out string assemblyName, out string typeName)
        {
            int splitterPos = type.IndexOf(", ");

            typeName = (splitterPos != -1) ? type.Substring(0, splitterPos).Trim() : type;
            assemblyName = (splitterPos != -1) ? type.Substring(splitterPos + 1).Trim() : type;
        }

        /// <summary>
        /// Converts a string representation of a type to a version independent type by removing the assembly version information.
        /// </summary>
        /// <param name="type">Type to convert.</param>
        /// <returns>String representing the type without version information.</returns>
        private static string ConvertTypeToVersionIndependentType(string type)
        {
            const string InnerTypesEnd = ",";

            string newType = type;
            string[] innerTypes = GetInnerTypes(newType);

            if (innerTypes.Length > 0)
            {
                // Remove inner types
                newType = newType.Replace(string.Format(CultureInfo.InvariantCulture, "[{0}]", TypeHelper.FormatInnerTypes(innerTypes)), string.Empty);
                for (int i = 0; i < innerTypes.Length; i++)
                {
                    innerTypes[i] = ConvertTypeToVersionIndependentType(innerTypes[i]);
                }
            }

            string typeName, assemblyName;
            SplitType(newType, out assemblyName, out typeName);

            bool isMicrosoftAssembly = false;
            foreach (string t in MicrosoftPublicKeyTokens)
            {
                if (assemblyName.Contains(t))
                {
                    isMicrosoftAssembly = true;
                    break;
                }
            }

            // Remove version info from assembly (if not signed by Microsoft)
            if (!isMicrosoftAssembly)
            {
                int splitterPos = assemblyName.IndexOf(", ");
                if (splitterPos != -1)
                {
                	assemblyName = assemblyName.Substring(0, splitterPos);
                }
            }

            newType = TypeHelper.FormatType(assemblyName, typeName);

            if (innerTypes.Length > 0)
            {
                int innerTypesIndex = newType.IndexOf(InnerTypesEnd);
                if (innerTypesIndex >= 0)
                {
                    newType = newType.Insert(innerTypesIndex, string.Format(CultureInfo.InvariantCulture, "[{0}]", TypeHelper.FormatInnerTypes(innerTypes)));
                }
            }

            return newType;
        }

        /// <summary>
        /// Converts a string representation of a type to a redirected type.
        /// </summary>
        /// <param name="type">Type to convert.</param>
        /// <returns>String representing the type that represents the redirected type.</returns>
        private string ConvertTypeToNewType(string type)
        {
            const string InnerTypesEnd = ",";

            string newType = type;
            string[] innerTypes = GetInnerTypes(newType);

            if (innerTypes.Length > 0)
            {
                newType = newType.Replace(string.Format(CultureInfo.InvariantCulture, "[{0}]", TypeHelper.FormatInnerTypes(innerTypes)), string.Empty);
                for (int i = 0; i < innerTypes.Length; i++)
                {
                    innerTypes[i] = ConvertTypeToNewType(innerTypes[i]);
                }
            }

            if (_redirectAttributes.ContainsKey(newType))
            {
                newType = _redirectAttributes[newType].TypeToLoad;
            }

            if (innerTypes.Length > 0)
            {
                int innerTypesIndex = newType.IndexOf(InnerTypesEnd);
                if (innerTypesIndex >= 0)
                {
                    newType = newType.Insert(innerTypesIndex, string.Format(CultureInfo.InvariantCulture, "[{0}]", TypeHelper.FormatInnerTypes(innerTypes)));
                }
            }

            return newType;
        }

        /// <summary>
        /// Returns the inner type of a type, for example, a generic array type.
        /// </summary>
        /// <param name="type">Full type which might contain an inner type.</param>
        /// <returns>Array of inner types.</returns>
        private static string[] GetInnerTypes(string type)
        {
            const char InnerTypeCountStart = '`';
            char[] InnerTypeCountEnd = new[] { '[', '+' };
            const char InternalTypeStart = '+';
            const char InternalTypeEnd = '[';
            const string AllTypesStart = "[[";
            const char SingleTypeStart = '[';
            const char SingleTypeEnd = ']';

            List<string> innerTypes = new List<string>();

            try
            {
                int countIndex = type.IndexOf(InnerTypeCountStart);
                if (countIndex == -1)
                {
                	return innerTypes.ToArray();
                }

                // This is a generic, but does the type definition also contain the inner types?
                if (!type.Contains(AllTypesStart))
                {
                	return innerTypes.ToArray();
                }

                // Get the number of inner types
                int innerTypeCountEnd = -1;
                foreach (char t in InnerTypeCountEnd)
                {
                    int index = type.IndexOf(t);
                    if ((index != -1) && ((innerTypeCountEnd == -1) || (index < innerTypeCountEnd)))
                    {
                        // This value is more likely to be the one
                        innerTypeCountEnd = index;
                    }
                }

                int innerTypeCount = int.Parse(type.Substring(countIndex + 1, innerTypeCountEnd - countIndex - 1));

                // Remove all info until the first inner type
                if (!type.Contains(InternalTypeStart.ToString()))
                {
                    // Just remove the info
                    type = type.Substring(innerTypeCountEnd + 1);
                }
                else
                {
                    // Remove the index, but not the numbers
                    int internalTypeEnd = type.IndexOf(InternalTypeEnd);
                    type = type.Substring(internalTypeEnd + 1);
                }

                // Get all the inner types
                for (int i = 0; i < innerTypeCount; i++)
                {
                    // Get the start & end of this inner type
                    int innerTypeStart = type.IndexOf(SingleTypeStart);
                    int innerTypeEnd = innerTypeStart + 1;
                    int openings = 1;

                    // Loop until we find the end
                    while (openings > 0)
                    {
                        if (type[innerTypeEnd] == SingleTypeStart)
                        {
                            openings++;
                        }
                        else if (type[innerTypeEnd] == SingleTypeEnd)
                        {
                            openings--;
                        }

                        // Increase current pos if we still have openings left
                        if (openings > 0)
                        {
                        	innerTypeEnd++;
                        }
                    }

                    innerTypes.Add(type.Substring(innerTypeStart + 1, innerTypeEnd - innerTypeStart - 1));
                    type = type.Substring(innerTypeEnd + 1);
                }
            }
            catch (Exception ex)
            {
                Log.Warn(ex, TraceMessages.FailedToRetrieveInnerTypes);
            }

            return innerTypes.ToArray();
        }
        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Netherlands Netherlands
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions