pmunin
4/11/2016 - 3:24 PM

helpers for ASP.NET, ASP.NET MVC app server side testing

helpers for ASP.NET, ASP.NET MVC app server side testing

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using System.Web.Hosting;
using System.Web.Routing;



    /// <summary>
    /// Allow to run and communicate with ASP.NET application in separate app domain (memory is not shared).
    /// Required for UnitTests. Had to place it here, because it does not load test assembly otherwise. 
    /// (http://stackoverflow.com/questions/1373100/how-to-add-folder-to-assembly-search-path-at-runtime-in-net
    /// alternative was either to create a copy script puting test assemblies in bin folder, or register test assembly in GAC)
    /// Taken from here: https://lostechies.com/chadmyers/2008/06/25/hosting-an-entire-asp-net-mvc-request-for-testing-purposes/
    /// </summary>
    [System.Diagnostics.DebuggerStepThrough]
    public class WebAppHostHelper : MarshalByRefObject
    {
        public static WebAppHostHelper Create(string webAppDir)
        {
            ///More flexible way is here: http://weblog.west-wind.com/posts/2005/Jul/20/AspNET-Runtime-Hosting-Update
            var res = System.Web.Hosting.ApplicationHost.CreateApplicationHost(typeof(WebAppHostHelper), "/", webAppDir) as WebAppHostHelper;
            return res;
        }

        public WebAppHostHelper()
        {
            AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
        }

        public void SetAssemblyLocations(params string[] binPaths)
        {
            this.binPaths = binPaths;
        }
        public void LoadAssemblies(params string[] assemblyFiles)
        {
            assemblyFiles.ToList().ForEach(a => {
                Assembly.LoadFrom(a);
            });
        }

        string[] binPaths = null;
        private System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            var lookingForFilename = new AssemblyName(args.Name).Name+".dll";
            var dirs = (binPaths?? Enumerable.Empty<string>()).Select(bp => new DirectoryInfo(bp));
            foreach (var dir in dirs)
            {
                var assemblyFile = dir.GetFiles().FirstOrDefault(f=>string.Equals(f.Name, lookingForFilename, StringComparison.InvariantCultureIgnoreCase));
                if (assemblyFile != null)
                    return Assembly.LoadFrom(assemblyFile.FullName);
            }
            return null;
        }

        public object CreateInstance(Type type, params object[] args)
        {
            return Activator.CreateInstance(type, args: args);
        }
        public AppDomain GetAppDomain()
        {
            return AppDomain.CurrentDomain;
        }

        public override object InitializeLifetimeService()
        {
            // This tells the CLR not to surreptitiously 
            // destroy this object -- it's a singleton
            // and will live for the life of the appdomain
            return null;
        }

        /// <summary>
        /// Wake up ASP.NET
        /// </summary>
        /// <param name="relativeUrl"></param>
        /// <returns></returns>
        public string WakeUpAspNetMvc(string relativeUrl = null)
        {
            using (var response = new StringWriter())
            {
                HttpRuntime.ProcessRequest(new SimpleWorkerRequest(relativeUrl ?? "/", "", response));
                response.Flush();
                return response.ToString();
            }
        }

        public string ProcessRequest(string url, string query)
        {
            var writer = new StringWriter();
            var request = new SimpleWorkerRequest(url, query, writer);
            var context = HttpContext.Current = new HttpContext(request);
            var contextBase = new HttpContextWrapper(context);
            var routeData = RouteTable.Routes.GetRouteData(contextBase);
            var routeHandler = routeData.RouteHandler;
            var requestContext = new RequestContext(contextBase, routeData);
            var httpHandler = routeHandler.GetHttpHandler(requestContext);
            httpHandler.ProcessRequest(context);
            context.Response.End();
            writer.Flush();
            return writer.GetStringBuilder().ToString();
        }

        protected internal T Invoke<T>(SerializableDelegate<Func<WebAppHostHelper, T>> func)
        {
            if (func == null || func.Delegate==null)
                throw new SerializationException("Could not serialize your delegate for cross domain execution");
            return func.Delegate(this);
        }
        protected internal void Invoke(SerializableDelegate<Action<WebAppHostHelper>> action)
        {
            if (action == null || action.Delegate == null)
                throw new SerializationException("Could not serialize your delegate for cross domain execution");
            action.Delegate(this);
        }
    }


    [System.Diagnostics.DebuggerStepThrough]
    public static class WebAppHostHelperExtensions
    {
        public static T Invoke<T>(this WebAppHostHelper host, Func<WebAppHostHelper,T> crossDomainAction)
        {
            var serDel = new SerializableDelegate<Func<WebAppHostHelper, T>>(crossDomainAction);
            return host.Invoke(serDel);
        }
        public static void Invoke(this WebAppHostHelper host, Action<WebAppHostHelper> crossDomainAction)
        {
            var serDel = new SerializableDelegate<Action<WebAppHostHelper>>(crossDomainAction);
            host.Invoke(serDel);
        }
    }

    /// <summary>
    /// Makes delegates serializable where possible
    /// Taken from here: 
    /// https://github.com/chrisortman/MvcIntegrationTest/blob/master/src/MvcIntegrationTestFramework/Hosting/SerializableDelegate.cs
    /// Used to pass test delegates from the test appdomain into the ASP.NET host appdomain
    /// Adapted from http://www.codeproject.com/KB/cs/AnonymousSerialization.aspx
    /// </summary>
    [Serializable]
    [System.Diagnostics.DebuggerStepThrough]
    public class SerializableDelegate<TDelegate> : ISerializable where TDelegate : class
    {
        public TDelegate Delegate { get; private set; }

        internal SerializableDelegate(TDelegate @delegate)
        {
            Delegate = @delegate;
        }

        internal SerializableDelegate(SerializationInfo info, StreamingContext context)
        {
            var delegateType = (Type)info.GetValue("delegateType", typeof(Type));

            if (info.GetBoolean("isSerializable"))
                //If it's a "simple" delegate we just read it straight off
                Delegate = (TDelegate)info.GetValue("delegate", delegateType);
            else {
                //otherwise, we need to read its anonymous class
                var methodInfo = (MethodInfo)info.GetValue("method", typeof(MethodInfo));
                var anonymousClassWrapper = (AnonymousClassWrapper)info.GetValue("class", typeof(AnonymousClassWrapper));
                Delegate = (TDelegate)(object)System.Delegate.CreateDelegate(delegateType, anonymousClassWrapper.TargetInstance, methodInfo);
            }
        }

        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("delegateType", Delegate.GetType());
            var untypedDelegate = (Delegate)(object)Delegate;
            //If it's an "simple" delegate we can serialize it directly
            if ((untypedDelegate.Target == null || untypedDelegate.Method.DeclaringType.GetCustomAttributes(typeof(SerializableAttribute), false).Length > 0) && Delegate != null)
            {
                info.AddValue("isSerializable", true);
                info.AddValue("delegate", Delegate);
            }
            else {
                //otherwise, serialize anonymous class
                info.AddValue("isSerializable", false);
                info.AddValue("method", untypedDelegate.Method);
                info.AddValue("class", new AnonymousClassWrapper(untypedDelegate.Method.DeclaringType, untypedDelegate.Target));
            }
        }

        [Serializable]
        private class AnonymousClassWrapper : ISerializable
        {
            public object TargetInstance { get; private set; }
            private readonly Type targetType;

            internal AnonymousClassWrapper(Type targetType, object targetInstance)
            {
                this.targetType = targetType;
                TargetInstance = targetInstance;
            }

            internal AnonymousClassWrapper(SerializationInfo info, StreamingContext context)
            {
                var classType = (Type)info.GetValue("classType", typeof(Type));
                TargetInstance = Activator.CreateInstance(classType);

                foreach (FieldInfo field in classType.GetFields())
                {
                    if (typeof(Delegate).IsAssignableFrom(field.FieldType))
                        //If the field is a delegate
                        field.SetValue(TargetInstance, ((SerializableDelegate<TDelegate>)info.GetValue(field.Name, typeof(SerializableDelegate<TDelegate>))).Delegate);
                    else if (!field.FieldType.IsSerializable)
                        //If the field is an anonymous class
                        field.SetValue(TargetInstance, ((AnonymousClassWrapper)info.GetValue(field.Name, typeof(AnonymousClassWrapper))).TargetInstance);
                    else
                        //otherwise
                        field.SetValue(TargetInstance, info.GetValue(field.Name, field.FieldType));
                }
            }

            void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
            {
                info.AddValue("classType", targetType);

                foreach (FieldInfo field in targetType.GetFields())
                {
                    //See corresponding comments above
                    if (typeof(Delegate).IsAssignableFrom(field.FieldType))
                        info.AddValue(field.Name, new SerializableDelegate<TDelegate>((TDelegate)field.GetValue(TargetInstance)));
                    else if (!field.FieldType.IsSerializable)
                        info.AddValue(field.Name, new AnonymousClassWrapper(field.FieldType, field.GetValue(TargetInstance)));
                    else
                        info.AddValue(field.Name, field.GetValue(TargetInstance));
                }
            }
        }
    }