.NET AddIn Framework With Backwards Compatibility

Posted on: February 24, 2016 6:30:27 PM

At work with my teammate, Jeremy, the application we're working on currently supports plugins built by 3rd parties. The problem we started running into once we thought about making it public was that anytime we try to update the libraries, it would break any plugin that was developed that wasn't compiled using those libraries. To solve this problem, we started looking into the .NET AddIn Framework that can support backwards compatibility. Figuring out this framework took quite a bit of work, there isn't a ton of documentation nor are there many examples of what we needed. It's relatively easy to have the Host application call into a Plugin, but quite a bit more complex if you want to support the reverse. The whole process is still quite complex and takes some time to wrap your head around, I'll post some of the important code here and then a link to my test project that has the fully working source and a test project to demonstrate.

To setup the basic Host-to-AddIn structure, we followed this MSDN Example. Once you get that working, in order to get the callback to the Host setup, you need to add a new contract and modify your existing contract to support passing in a reference to your Host's handler.

ICallbackContract
using System.AddIn.Contract;

namespace ContractV2.V2
{
    public interface ICallbackContractV2 : IContract
    {
        void DoWork();
    }
}
IContractV2
using System.AddIn.Contract;
using System.AddIn.Pipeline;

namespace ContractV2.V2
{
    [AddInContract]
    public interface IContractV2 : IContract
    {
        string GetName();

        void Initialize(ICallbackContractV2 callback);

        void WriteToConsole(string output);

        object GetSource();
    }
}

You can follow the MSDN tutorial once again to propagate this contract all the way through the project, it's virtually the same with the exception of your callback interfaces and adapters don't require the same Attributes that your first contract will because it isn't technically a full AddIn contract. The next step is to create the normal AddInAdapater.

AddinAdapterV2
using AddinViewV2.V2;
using ContractV2.V2;
using System.AddIn.Pipeline;

namespace AddinAdapterV2.V2
{
    [AddInAdapter]
    public class AddinAdapter : ContractBase, IContractV2
    {
        private IV2 _view;

        public AddinAdapter(IV2 view)
        {
            _view = view;
        }

        public string GetName()
        {
            return _view.GetName();
        }

        public void Initialize(ICallbackContractV2 callback)
        {
            _view.Initialize(CallbackConverter.FromContract(callback));
        }

        public void WriteToConsole(string output)
        {
            _view.WriteToConsole(output);
        }

        public object GetSource()
        {
            return _view.GetSource();
        }
    }
}

The important thing to note in this class is the converter. This is needed to translate the contract to the callback.

CallbackConverter
using AddinViewV2.V2;
using ContractV2.V2;

namespace AddinAdapterV2.V2
{
    public class CallbackConverter
    {
        internal static ICallbackV2 FromContract(ICallbackContractV2 callback)
        {
            return new CallbackImpl(callback);
        }
    }
}

In order for this converter to work is to have a concrete implementation of the class you're converting to.

CallbackImpl
using AddinViewV2.V2;
using ContractV2.V2;

namespace AddinAdapterV2.V2
{
    public class CallbackImpl : ICallbackV2
    {
        private ICallbackContractV2 _contract;

        public CallbackImpl(ICallbackContractV2 contract)
        {
            _contract = contract;
        }

        public void DoWork()
        {
            _contract.DoWork();
        }
    }
}

You probably have noticed that my interfaces are listed as V2. This is because I also wanted to show what an upgrade scenario looked like and how to handle conversions to support backwards compatibility. To do this, you'll need to create a new Adapter project to handle the conversion.

AddinAdapterV1ToV2
using AddinView.V1;
using ContractV2.V2;
using System.AddIn.Pipeline;
using System.Diagnostics;

namespace AddinAdapterV1ToV2.V2
{
    [AddInAdapter]
    public class AddinAdapterV1ToV2 : ContractBase, IContractV2
    {
        private IV1 _view;

        public AddinAdapterV1ToV2(IV1 view)
        {
            _view = view;
        }

        public string GetName()
        {
            return _view.GetName();
        }

        public void Initialize(ICallbackContractV2 callback)
        {
            _view.Initialize(CallbackConverter.FromContractV2(callback));
        }

        public void WriteToConsole(string output)
        {
            Debug.WriteLine("Outout is ignored: ", output);
            _view.WriteToConsole();
        }

        public object GetSource()
        {
            return null;
        }
    }
}

Here you'll notice that this class implements the IContractV2 interface but the constructor requires a IV1 interface to be passed. This is what will convert a V1 Plugin to the current Host, allowing for reverse compatibility. You can also do the inverse where you convert the Host to a new Plugin; however, this is only required if you'll be releasing these libraries separate from your Host application. In my code example, I did this just to show what is necessary.

The next issue we ran into was that because it used to be tightly integrated, there were some objects that were passed around by reference. We couldn't get away from this model without a lot of restructuring and re-architecting the base code. We found that the AddIn framework Activator allowed you to specify the AppDomain. So we could use that to load the AddIn into the same AppDomain as the existing project which allows us to once again pass by reference without the need of serialization like you'd normally have to.

Program snippet
                IV2 v2 = token.Activate(AppDomain.CurrentDomain);
                v2.Initialize(new CallbackHandler());

                // Run the add-in.
                v2.WriteToConsole("Hello World From Host!");
                Console.WriteLine(v2.GetName());

                var test = (Stopwatch)v2.GetSource();

                Task.Delay(500).Wait();

                test.Stop();
                Console.WriteLine(test.ElapsedTicks);

You'll notice in the contract that the GetSource method returns an object. I've set it up to return a Stopwatch, which isn't serializable and wouldn't normally be allowed to pass through this framework. By loading the AddIn into the same AppDomain, this works.

The full code base can be found here. This is only a brief description over the trouble areas that I came across. If you have any questions, feel free to post them in the comments section below and I'll do my best to answer them for you.

File.Exists with File/Dir Permissions

Posted on: February 22, 2016 1:48:16 AM
While attempting to troubleshoot a failing unit test on my work machine that didn't fail on my peer's machine or on the build machine I came across an interesting situation. Basically, I found the only difference between my code base and everyone else's was it's location on my computer. Mine was in my Windows user directory whereas everyone else's existed in custom folders directly off the root directory. I began to investigate permissions because of this scenario and what I found was very interesting and a bit misleading around System.IO.File.Exists. The MSDN Documenation for File.Exists says, "If the caller does not have sufficient permissions to read the specified file, no exception is thrown and the method returns false regardless of the existence of path." Based on this line, I wrote out a simple test program:
using System;
using System.DirectoryServices;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;

namespace ConsoleApplication1
{
    internal class Program
    {
        private const string DirName = "TestDir";
        private const string FileName = "File.txt";
        private const string Password = "Password1";
        private const string UserName = "PermissionTestUser";
        private static WindowsImpersonationContext Identity = null;
        private static IntPtr LogonToken = IntPtr.Zero;

        public enum LogonProvider
        {
            LOGON32_PROVIDER_DEFAULT = 0,
            LOGON32_PROVIDER_WINNT35 = 1,
            LOGON32_PROVIDER_WINNT40 = 2,
            LOGON32_PROVIDER_WINNT50 = 3
        };

        public enum LogonType
        {
            LOGON32_LOGON_INTERACTIVE = 2,
            LOGON32_LOGON_NETWORK = 3,
            LOGON32_LOGON_BATCH = 4,
            LOGON32_LOGON_SERVICE = 5,
            LOGON32_LOGON_UNLOCK = 7,
            LOGON32_LOGON_NETWORK_CLEARTEXT = 8, // Win2K or higher
            LOGON32_LOGON_NEW_CREDENTIALS = 9 // Win2K or higher
        };

        public static void Main(string[] args)
        {
            string filePath = Path.Combine(DirName, FileName);
            try
            {
                CreateUser();
                CreateDir();
                CreateFile(filePath);

                // grant user full control to the dir
                SetAccess(DirName, AccessControlType.Allow);
                // deny user full control to the file
                SetAccess(filePath, AccessControlType.Deny);

                // impersonate user
                Impersonate();
                Console.WriteLine("File.Exists (with dir permissions): {0}", File.Exists(filePath));
                UndoImpersonate();

                // deny access to dir
                SetAccess(DirName, AccessControlType.Deny);

                // impersonate user
                Impersonate();
                Console.WriteLine("File.Exists (without dir permissions): {0}", File.Exists(filePath));
                UndoImpersonate();
            }
            finally
            {
                UndoImpersonate();
                DeleteDir();
                DeleteUser();
            }
        }

        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        private static extern bool CloseHandle(IntPtr handle);

        private static void CreateDir()
        {
            Directory.CreateDirectory(DirName);
        }

        private static void CreateFile(string path)
        {
            File.Create(path).Dispose();
        }

        private static void CreateUser()
        {
            DirectoryEntry ad = new DirectoryEntry("WinNT://" + Environment.MachineName + ",computer");
            DirectoryEntry newUser = ad.Children.Add(UserName, "user");
            newUser.Invoke("SetPassword", new object[] { Password });
            newUser.Invoke("Put", new object[] { "Description", "Test user" });
            newUser.CommitChanges();
        }

        private static void DeleteDir()
        {
            Directory.Delete(DirName, true);
        }

        private static void DeleteUser()
        {
            DirectoryEntry ad = new DirectoryEntry("WinNT://" + Environment.MachineName + ",computer");
            DirectoryEntries users = ad.Children;
            DirectoryEntry user = users.Find(UserName, "user");

            if (user != null)
            {
                users.Remove(user);
            }
        }

        private static void Impersonate()
        {
            if (LogonUser(UserName, ".", Password, (int)LogonType.LOGON32_LOGON_INTERACTIVE, (int)LogonProvider.LOGON32_PROVIDER_DEFAULT, ref LogonToken))
            {
                Identity = WindowsIdentity.Impersonate(LogonToken);
                return;
            }
        }

        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        private static extern bool LogonUser(string lpszUserName,
            string lpszDomain,
            string lpszPassword,
            int dwLogonType,
            int dwLogonProvider,
            ref IntPtr phToken);

        private static void SetAccess(string path, AccessControlType type)
        {
            FileSecurity fs = File.GetAccessControl(path);
            FileSystemAccessRule far = new FileSystemAccessRule(UserName, FileSystemRights.FullControl, type);
            fs.AddAccessRule(far);
            File.SetAccessControl(path, fs);
        }

        private static void UndoImpersonate()
        {
            if (Identity != null)
            {
                Identity.Undo();
                Identity = null;
            }

            if (LogonToken != IntPtr.Zero)
            {
                CloseHandle(LogonToken);
                LogonToken = IntPtr.Zero;
            }
        }
    }
}
This program basically tests file permissions and folder permissions with File.Exists. What I expected to see based on the documentation was both instances should return false; however, I found that when the user has permissions on the directory but none on the file, it still returns true. This ends up being due to a very subtle keyword in the documentation, "sufficient". If the user has the LIST permission in the directory the file lives in, File.Exists has sufficient permissions to return whether or not the file exists.

Introduction

Posted on: February 15, 2016 1:47:48 AM

I suppose the best way to begin this blog is an introduction about myself. My name is Justin Sommercorn, I was born in Salt Lake City, UT. I am currently 29 years old and have been married happily to my wife Leah for the past 5 years. I've had two wonderful children with a third on the way.

From a very young age, I've always had an interest in math and science. My family bought their first computer when I was approximately 8 years old and from that time on, I spent as much time as I could on it. I enjoyed playing games and learning how to do things. I very quickly became the resident expert on the computer and when things went wrong it was up to me to fix it. I knew from a very early age that I wanted to go into software development; although, I wanted to go into game development. In high school, I had the opportunity to intern at Microsoft Games in Salt Lake City. It was there that I learned that game development was very fast paced and not as rewarding as I thought it would be. I still knew that development was my career choice, so I started learning about Windows application development and web development.

Shortly after high school, I lucked into getting my first software development job. I had no college degree and knew that my best bet was to send my resume everywhere hoping to impress during the interview. I was hired on as a junior software engineer at Paraben and began work on a Java project. At the time, it was me and one other developer on the project, but it didn't remain that way very long. The other developer moved into a management position and I was left as the only developer on the project. I learned a lot about coding on this project and was able to start a few other projects there based on the knowledge that I picked up.

After Paraben, I worked for several other companies finally ending up where I currently am at Domo. I still enjoy learning about coding and why things are done the way they are. I make a big effort to stay up to date with the latest releases of the languages and frameworks that I use. I also have many side projects that I work on in my own time, including some game development. I am very happy with what I am doing and look forward to a long career.