Check out my new blog at https://shibumiware.blogspot.com

Thursday, June 22, 2017

More on Setup & Updates: Programmatically Accessible .NET Version Information + Some History

Version
Related Posts: Automatic Updates - Tips & Tricks

Introduction

Off and on for years,  I have been involved in developing software for setup and servicing technology for various products.  On of my first projects, when I was a wee 20-year old, working for a startup in Redmond, WA, required an early version of "Office Update."

The Microsoft main campus was just a few blocks from our first "office," which doubled as the company's owner and his wife's house and our work area.  Later,  we moved into real office space in downtown Redmond, and we were the bee's knees; but that's another story.  It was a tremendous time of learning the trade from the inside out.  I was blessed (Glenn Minch, Garin Pangburn, Ken Inglis, and Adrian Jenkins, I am in your debt).

The company, Critical Path Technical Services, Inc., landed a contract with the Office Product Group (Building 18 for you 'Softies' out there) at Microsoft to develop an "Office Solution Automatic Update Service."  Keep in mind this was circa 1995 and, if you have forgotten how unsophisticated the Internet was, take a walk down memory lane with the image below of Microsoft's main site around that time.

It wasn't much.  That year I hand-coded CPTS' first website using Notepad.exe, FTP, and a book I had bought from Barnes & Noble about HTML.

The Call

Microsoft's Website Circa 1995 Courtesy of Microsoft Corporation
 
Microsoft's Website 1995

There is nothing like getting a call from the Mothership offering money to accelerate the pace of learning.  Essentially the idea was this: Microsoft was pushing Office automation heavily in those days, particularly the use of Visual Basic for Applications, which premiered in 1995. Microsoft wanted to combine the Internet and Office so that Office ISVs (Integrated Software Vendors) would have direct access to the burgeoning body of knowledge regarding VBA and Office solution development.

The problem was MSDN shipped on CD (no, not DVD) every quarter and there was very little by way of online content to help the ISV except for a few boards, chat channels, and a tiny number of dedicated websites.

To make a very long story short, CPTS was contracted to build an Internet-based Solutions Delivery Platform--or as we would think of it today, Office Update for developers. The idea was a grand vision. Microsoft hosted an FTP site, and CPTS contracted to write an automatic download system. The system would look for updates and new solution starters, provide a web-based menu of technical assets the developer could choose to download, and take care of downloading and installing the solution-starter (think template with a code-behind). Not unlike the first MSDN down-loader, an ActiveX control would create a modal Windows dialog that did the actual download to the local disk. I hand crafted, in C++, the download engine while another developer worked on the UI portion. The download engine was straight sockets and WinINet programming. Many of you may not know that WinINet.dll is the grandfather of all Internet-related DLLs in Windows. A simple search shows browsers, Google Drive, Excel, and 40 other processes running on my machine that take a dependency on WinINet.dll.  Talk about first-generation.

WinINet.Dll

That was my introduction to servicing and setup because each download was completely self-contained, installed itself into Office through the VBE Object and provided an updating mechanism to detect and install or update VBA solutions in the various Office products. It was some kind of strange, let me say that--but I thoroughly enjoyed writing the download engine (C & C++ remain my favorite languages) using an API that is vastly closer to the wire than what we .NET developers are accustomed today. I can also so say that, before anybody else at Microsoft had an automatic update service, the Office Product Group and CPTS built one.

Setup ImageA Fascination Developed

After that experience, I developed a fascination with software that upgraded itself.  General setup technology, too, became a favorite topic.  For better or worse (probably worse), I wrote the setup technology for Project Office v3.0 at Pacific Edge and EVMS forProject v1.0 - v2.0 at forProject Technologies. I always felt that out of the box setup technologies were geared toward simple end-user products and not enterprise products. I suppose this tendency grew out of my years at Microsoft, where each product, ranging from Microsoft SQL Server to SharePoint, had an in-group setup system with very little, if any, shared concepts or code.  These days, with WIX and others, there is a better platform but if you know what you are doing, you can build a much better setup experience for the user if you combine off-the-shelf with custom setup work.

Usually, when setup is mentioned in product planning, nobody raises their hand to volunteer to take on that part of the product, but there I was, gleefully, hand raised way up, ready to take on the challenge.   I always took it on and I am not sure why.  There must be some deep psychological reason in me that whispers "set it up." uot;

You see, the other thing about setup is that it's the first thing the customer experiences with your product.  If your setup experience is horrible, there is a pretty good chance the product also has issues. Notice the various customer experience programs and other telemetry Microsoft and other software companies use to gather end-user experience data-- you see how important it is to get it right.

Enough Story Telling

I have enough content regarding my adventures at Microsoft as the youngest product manager in the Office Product Group plus the start-ups I was involved with after to write post-after-post.  This post is really about something I was working on this past weekend on one of my geeky hobby projects.  I mentioned an earlier post "Automatic Updates - Tips & Tricks".  That post didn't originate from out of the blue. Aside from the setup work I did at previous companies, I have always kept one side-project alive, in one form or another, regarding setup and servicing.  For a while it was QuickPatch , which is a fairly complete piece of work but I am dissatisfied with it (plus the user interface really bothers me).

Nowadays, under the banner of my new hobby company ShibumiWare, I have developed a system for building, packaging, deploying, setting up, and servicing my hobby products. In fact, I have become so engrossed in the effort, all of my hobby product development has stalled out while I finish this work. Unlike QuickPatch, I don’t have a catchy name for it, but it's the best work I have done in this area to date. Everything goes very slowly at ShibumiWare because I love my day job, have kids, and  have this medical thing continually to deal with, but I plow on.

.NET Version Information

Version detection one of the first things done: check to see if your product's prerequisites are present and provide options to download or otherwise obtain the necessary dependencies before installing the product. I mentioned in the Tips & Tricks post that a baseline is selected for your products to run.

In my case, I choose .NET 2.0 as the baseline version for the installer and update system. The products or utilities individually are written in a later version of .NET but to even get a .NET-based installer to run, you have to pick a baseline. .NET 2.0 is a good baseline selection for several reasons. It is broadly distributed for down-version operating systems like Windows XP, it ships with later OS's like Windows 7, 8 and 10 plus if it's not there from Microsoft, there stands a good chance that some other product installed on the customer's computer required .NET 2.0, and it's there already. Regardless, my setup flow begins with a C++ "bootstrapper" that doesn't require .NET whatsoever. Its purpose is to determine if .NET 2.0 is installed. If it isn't, the bootstrapper provides a link to installation guidance for the user and halts the installation process. Bootstrapping provides an excellent user experience.

Beyond setup, as mentioned, my hobby products and tools require different versions of .NET. Mostly I stick to version 4.5.1 these days, but in my setup builder system, I provide a mechanism for choosing prerequisites, including .NET.  I was working on this part of the system this weekend and found that I could not find in the 2.0 base class libraries a complete list of the versions of .NET.  Of course not, this is 2.0, so it has no future knowledge of versions of .NET. I started thinking about a class that would have all of the versions (subject to new releases I would have to add that's no problem), plus information about what's new associated with each version; and finally, a higher-level source of information for each major grouping of .NET versions. 1.0 to 3.5 comprise one group, while 4.0 and beyond comprise another group—the distinction is the version of the CLR changed between groups.

Custom Attributes

First things first: Custom Attributes.  For those of you who aren't familiar with attributes, you probably are, you just don't know it.  Here is a pretty common example of importing an external function call from a specific DLL:

[DllImport("kernel32.dll", EntryPoint = "TerminateProcess")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool TerminateProcess([In] IntPtr hProcess, uint uExitCode);

DLLImport is just one of the attributes in use here and is defined exactly the way a custom attribute is defined.  I have included a complete listing of DLLImport's implementation here .  I think of attributes as declarative markup for code, like XML tags.  Attributes have properties that are accessed by the run-time or your own custom framework to direct how the code is executed, prepared for, or to enforce run-time or compile-time directives, among other things.

In short, custom attributes are very powerful, as you can see from the DLLImport listing.  So, as I started thinking about a class the would encapsulate .NET version information, the first thing that popped in my mind was the use of attributes in two ways: one at the class level to provide version grouping information and second at the property level to provide specific information about the .NET version.  In this way, I could read the custom attributes and provide the user with contextual information about the various versions.  This ability would come in handy when defining a user interface where by a user selects a specific .NET version.  The user could be presented additional information about the version directly in the user interface.

The Version Description Attribute

Attributes inherit directly or indirectly from System.Attribute , which in turn implements the _Attribute interface. When an attribute is defined, it is important that an attribute is applied to the attribute called the AttributeUsage attribute:

[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]

Attributes receive their property information through constructors so everything between the open parenthesis and the closing parenthesis above can be thought of a constructor and its parameters.  Just as with regular constructors, you can provided overloads and optional parameters.  Let's think about the AttributeUsage attribute above.  The first parameter is an enum value that defines the "scope" of the attribute.  In other words, it indicates where the attribute is valid to apply, such as a class, property, struct, assembly and so on.  There are sixteen values to choose from.  The next parameter, Inherited = false,  indicates that classes derived from classes that carry this attribute do not inherit the attribute.  The default value is true because you would expect a "base" anything to be overridden explicitly by the derived class rather than either forcing it to be present or burying its presence in the inheritance.  The 3rd parameter is important for our scenario: AllowMultiple=True.  The default is false because typically an attribute is self-contained and applying it twice to a member seems strange--except for when the attribute is purely an information attribute, which we will see an example of soon.

Here is the implementation of the VersionDescriptionAttribute:

[AttributeUsage(AttributeTargets.Property)]
public class VersionDescriptionAttribute : Attribute
{
    public VersionDescriptionAttribute(string description)
    {
        Description = description;
    }
 
    public string Description { getset; }
}

The first thing you notice is the application of the AttributeUsage attribute, which allows this attribute to be applied to properties, it is inherited by derived classes (default value) and it only allows one application per property (again, the default value).  Applied, the attribute is used like this:

[VersionDescription(DotNetVersionDescriptions._1_0_0)]
public static Version _1_0_0 { getset;}

A rule in developing attributes is that all parameters must be a constant value, a typeof expression, or an array creation. In other words, you cannot have computed values as inbound data on the parameters. If you could, I would opt to put the version description text in a resource file, but that resolves to a call to ResourceManager.GetObject, which is a computation. The specific rules are here.

The first two components of the rule are easily understood.  The third requires a little explanation.  It simply means that a parameter can take the form of: type[] {x,y,z} as long as z, y, and z are constants.   The following example is acceptable: string[] {"Additional", "Subtraction", "Division"} There are means by which "dynamic" attributes are achievable.  I leave this topic to the reader to noodle over.

A final note to those not steeped in C# is variable naming constraints.  You probably noticed that version 1.0.0 is _1_0_0 because the following rules apply:

  1. The first character of a variable name must be either a letter, an underscore character (_), or the at symbol (@)
  2. Subsequent characters may be letters, underscore characters, or numbers
  3. The use of C# keywords as variable names is prohibited, although the variable name may contain the key word subject to rule #1 and #2.
Variables and Expressions - https://msdn.microsoft.com/en-us/library/gg615485(v=vs.88).aspx

VersionAdditionalLinksAttribute

public static partial class DotNetVersions
{
    [AttributeUsage(AttributeTargets.ClassAllowMultiple = true)]
    public class VersionAdditionalLinksAttribute : Attribute
    {
        public VersionAdditionalLinksAttribute(string majorVersionGroup, string additionalInformationLink)
        {
            MajorVersionGroup = majorVersionGroup;
            AdditionalInformationLink = additionalInformationLink;
        }
 
        public string MajorVersionGroup { getset; }
        public string AdditionalInformationLink { getset; }
    }
}

After the detailed description of the first attribute, I think what's going on with this attribute is straightforward.  One thing to take note of is the AttributeUsage attribute is applicable only to classes and more than one may be applied to the class.  As mentioned earlier, I want to provide detailed information about the two major groups of .NET versions: those that run on the original CLR and those that run on the second version of the CLR.   Here is an application of the attribute:

[VersionAdditionalLinks(DotNetVersionDescriptions.CLRDotNetVersionDescriptions.CLR_LINK)]
[VersionAdditionalLinks(DotNetVersionDescriptions.CLR2DotNetVersionDescriptions.CLR2_LINK)]
public static partial class DotNetVersions

Putting It All Together

Let's start with the end result and walk backwards--you may figure out how it was done given what I have shown you thus far.   The following is output from a unit test that exercises all of the major pieces of the solution: provide additional information links for the two groups of .NET versions, provide a brief description of each version, provide a sane output from all of the _X_X_X variables for use in user interfaces (such as binding the version list to a combo box).

The OutputOutput


Major Release Collection = v1.1 - v3.x
Major Release Link = https://msdn.microsoft.com/library/ms171868(v=vs.90).aspx
Major Release Collection = v4.0 - v4.x
Major Release Link = https://docs.microsoft.com/en-us/dotnet/framework/whats-new/index

Version = 1.0.0.0
Additional Information =
- First version of the.NET Framework.
Version = 1.1.0.0
Additional Information =
- ASP.NET and ADO.NET updates
- Side-by-side execution
Version = 2.0.0.0
Additional Information =
- Generics
- ASP.NET additions
Version = 3.0.0.0
Additional Information =
- WPF, WCF, WF, CardSpace
Version = 3.5.0.0
Additional Information =
- AJAX-enabled websites
- LINQ
- Dynamic data
Version = 4.0.0.0
Additional Information =
- Expanded base class libraries
- Cross-platform development with Portable Class Library
- MEF, DLR, code contracts
Version = 4.5.0.0
Additional Information =
- Support for Windows Store apps
- WPF, WCF, WF, ASP.NET updates
Version = 4.5.1.0
Additional Information =
- Support for Windows Phone Store apps
- Automatic binding redirection
- Performance and debugging improvements
Version = 4.5.2.0
Additional Information =
- New APIs for transactional systems and ASP.NET
- System DPI resizing in Windows Forms controls
- Profiling improvements
- ETW and stress logging improvements
Version = 4.6.0.0
Additional Information =
- Compilation using .NET Native
- ASP.NET Core 5
- Event tracing improvements
- Support for page encodings
Version = 4.6.1.0
Additional Information =
- Support for X509 certificates containing ECDSA
- Always Encrypted support for hardware protected keys in ADO.NET
- Spell checking improvements in WPF
Version = 4.6.2.0
Additional Information =
- Cryptography enhancements, including support for X509 certificates containing FIS 186-3 DSA, support for persisted-key symmetric encryption, SignedXml
support for SHA-2 hashing, and increased clarity for inputs to Elliptic Curve Diffie–Hellman key derivation routines.
- For Windows Presentation Foundation (WPF) apps, soft keyboard support and per-monitor DPI.
- ClickOnce support for the TLS 1.1 and TLS 1.2 protocols.
- Support for converting Windows Forms and WPF apps to UWP apps.
Version = 4.7.0.0
Additional Information =
- Support for the level of TLS support provided by the operating system.
- Ability to configure default message security settings for TLS1.1 or TLS1.2.
- Improved reliability of the DataContractJsonSerializer.
- Improved +reliability of serialization and deserialization with WCF applications.
- Ability to extend the ASP.NET object cache.
- Support for a touch/stylus stack based on WM_POINTER Windows messages instead of the Windows Ink Services Platform (WISP) for WPF applications.
- Use of Window's Print Document Package API for printing in WPF applications.
- Enhanced high DPI and multi-monitor support for Windows Forms applications running on Windows 10 Creators Update.

Version = 1.0.0.0
Version = 1.1.0.0
Version = 2.0.0.0
Version = 3.0.0.0
Version = 3.5.0.0
Version = 4.0.0.0
Version = 4.5.0.0
Version = 4.5.1.0
Version = 4.5.2.0
Version = 4.6.0.0
Version = 4.6.1.0
Version = 4.6.2.0
Version = 4.7.0.0

The Unit Test


[TestMethod]
public void VersionsClassTests()
{
    DotNetVersions.VersionInfo versionInfo = new DotNetVersions.VersionInfo();
 
    foreach (KeyValuePair<stringstringkvp in versionInfo.MajorReleaseDictionary)
    {
        Debug.WriteLine("Major Release Collection = " +  kvp.Key);
        Debug.WriteLine("Major Release Link = " + kvp.Value);
    }
 
    Debug.WriteLine(Environment.NewLine);
 
    foreach (KeyValuePair<Versionstringkvp in versionInfo.VersionDictionary)
    {
        Debug.WriteLine("Version = " + kvp.Key);
        Debug.WriteLine("Additional Information = " + kvp.Value);
        Debug.WriteLine(Environment.NewLine);
    }
 
    Debug.WriteLine(Environment.NewLine);
 
    foreach (Version version in versionInfo.Versions)
    {
        Debug.WriteLine("Version = " + version);
    }
}

The Implementation


[VersionAdditionalLinks(DotNetVersionDescriptions.CLRDotNetVersionDescriptions.CLR_LINK)]
[VersionAdditionalLinks(DotNetVersionDescriptions.CLR2DotNetVersionDescriptions.CLR2_LINK)]
public static partial class DotNetVersions
{
static DotNetVersions()
{
    _4_7_0 = new Version(4, 7, 0, 0);
    _4_6_2 = new Version(4, 6, 2, 0);
    _4_6_1 = new Version(4, 6, 1, 0);
    _4_6_0 = new Version(4, 6, 0, 0);
    _4_5_2 = new Version(4, 5, 2, 0);
    _4_5_1 = new Version(4, 5, 1, 0);
    _4_5_0 = new Version(4, 5, 0, 0);
    _4_0_0 = new Version(4, 0, 0, 0);
    _3_5_0 = new Version(3, 5, 0, 0);
    _3_0_0 = new Version(3, 0, 0, 0);
    _2_0_0 = new Version(2, 0, 0, 0);
    _1_1_0 = new Version(1, 1, 0, 0);
    _1_0_0 = new Version(1, 0, 0, 0);
}
 
[VersionDescription(DotNetVersionDescriptions._1_0_0)]
public static Version _1_0_0 { getset; }
 
[VersionDescription(DotNetVersionDescriptions._1_1_0)]
public static Version _1_1_0 { getset; }
 
[VersionDescription(DotNetVersionDescriptions._2_0_0)]
public static Version _2_0_0 { getset; }
 
[VersionDescription(DotNetVersionDescriptions._3_0_0)]
public static Version _3_0_0 { getset; }
 
[VersionDescription(DotNetVersionDescriptions._3_5_0)]
public static Version _3_5_0 { getset; }
 
[VersionDescription(DotNetVersionDescriptions._4_0_0)]
public static Version _4_0_0 { getset; }
 
[VersionDescription(DotNetVersionDescriptions._4_5_0)]
public static Version _4_5_0 { getset; }
 
[VersionDescription(DotNetVersionDescriptions._4_5_1)]
public static Version _4_5_1 { getset; }
 
[VersionDescription(DotNetVersionDescriptions._4_5_2)]
public static Version _4_5_2 { getset; }
 
[VersionDescription(DotNetVersionDescriptions._4_6_0)]
public static Version _4_6_0 { getset; }
 
[VersionDescription(DotNetVersionDescriptions._4_6_1)]
public static Version _4_6_1 { getset; }
 
[VersionDescription(DotNetVersionDescriptions._4_6_2)]
public static Version _4_6_2 { getset; }
 
[VersionDescription(DotNetVersionDescriptions._4_7_0)]
public static Version _4_7_0 { getset; }
 
public class VersionInfo
{
    public VersionInfo()
    {
        MajorReleaseDictionary = new Dictionary<stringstring>();
        VersionDictionary = new Dictionary<Versionstring>();
        Versions = new List<Version>();
 
        Type parentType = GetType().DeclaringType;
 
        Debug.Assert(parentType != null,
                        DotNetVersionDescriptionErrors.ASSERT_PARENT_TYPE_NOT_NULL);
 
        List<VersionAdditionalLinksAttribute> linksAttributes = parentType.GetCustomAttributes()?
                                                                            .OfType<VersionAdditionalLinksAttribute>()
                                                                            .ToList();
 
        if (linksAttributes == null)
        {
            throw new InvalidOperationException(DotNetVersionDescriptionErrors.ERROR_MALFORMED_VERSION_INFO);
        }
 
        foreach (VersionAdditionalLinksAttribute linksAttribute in linksAttributes)
        {
            MajorReleaseDictionary.Add(linksAttribute.MajorVersionGroup, linksAttribute.AdditionalInformationLink);
        }
 
        PropertyInfo[] properties = parentType.GetProperties();
 
        foreach (PropertyInfo propertyInfo in properties)
        {
            object versionInfoObject = propertyInfo.GetValue(nullnull);
 
            if (versionInfoObject == null)
            {
                continue;
            }
 
            Version version = new Version(versionInfoObject.ToString());
            Versions.Add(version);
 
            VersionDescriptionAttribute versionDescriptionAttribute =
                (VersionDescriptionAttribute) propertyInfo.GetCustomAttributes(typeof(VersionDescriptionAttribute)).FirstOrDefault();
 
            if (versionDescriptionAttribute == null)
            {
                throw new InvalidOperationException(DotNetVersionDescriptionErrors.ERROR_MALFORMED_VERSION_INFO);
            }
 
            VersionDictionary.Add(version, versionDescriptionAttribute.Description);
        }
    }
 
    public Dictionary<stringstringMajorReleaseDictionary { getset; }
    public Dictionary<VersionstringVersionDictionary { getset; }
    public List<VersionVersions { getset; }
}

Wrap-Up

I hope you enjoyed a little meandering down memory lane with our look back on the Internet in 1995, plus some of my own personal experiences. I hope to have taught a few something about custom attributes and, in the end, I hope the DotNetVersions classes are useful to others.

A link to a ZIP archive with everything you need to add the components to your solution follows. You may use this as you see fit. If you have questions, leave a comment.


Colby-Tait

No comments :

Disclaimer

Content on this site is provided "AS IS" with no warranties and confers no rights. Additionally, all content on this site is my own personal opinion and does not represent my employer's view in any way.