The Wiert Corner – irregular stream of stuff

Jeroen W. Pluimers on .NET, C#, Delphi, databases, and personal interests

  • My badges

  • Twitter Updates

  • My Flickr Stream

  • Pages

  • All categories

  • Enter your email address to subscribe to this blog and receive notifications of new posts by email.

    Join 1,407 other followers

.NET/C# – finding the attribute values for an assembly

Posted by jpluimers on 2012/07/05

For some version management features, I needed to find the attribute values of loaded assemblies. Googling around, I didn’t find many nice solutions. In fact the solutions I found didn’t work well (AssemblyName is not an Attribute!), and/or contained lots of duplicate code, or uses RegEx and other string comparisons (lesson: if you can do it with either string comparison or proper type checking, use the latter). Below is the code; here some explanation:

  • It uses a C# translation of the RetrieveLinkerTimestamp function found through Jeff Atwood (Coding Horror) which was published by Kevin Gearing and determines the linker timestamp from the PE Header. It corrects the time into your time zone. The C# version has two overloads (one with an Assembly parameter, the other with a string filePath), and the latter now contains some exception handling just in case you pass it nonsense.
  • The RunOnAssemblyAttribute generic method takes a T parameter of type Attribute, then runs the Action<T> action if that attribute is found in the Assembly assembly. It retreived the attribute using the Assembly.GetCustomAttributes method.
  • When a null parameter is passed, it gets the information from the executing assembly (if you want it to be another one, just change the code if you want it to)
    An alternative way of getting the executable name is at the very bottom: you need it when your assembly is loaded from an unmanaged application (GetEntryAssembly then returns null)
  • The constructor runs it on all known assembly related attributes in the System.Reflection namespace, calling RunOnAssemblyAttribute with an action that fills the property for each existing attribute (you wish all those attributes would have a Value property, but their property names are specific for each attribute). For non existing attributes, the default property values are used (which incidently are the default values for those attributes anyway).
  • There are a couple of casts (for instance AssemblyNameFlags) as the underlying attribute contains a constructor with that parameter (in this case AssemblyFlagsAttribute), but stores it in an integer without providing a cast property. Maybe there should be proper type checking; that is left as an exercise…
  • The constructor also fills a couple of non attribute properties – AssemblyName – LinkerTimestamp
  • The Name property is derived from the AssemblyName.Name property if it exists

Enjoy the code:

using System;
using System.Reflection;
using System.Configuration.Assemblies;
using System.IO;

namespace bo.System
{
    // gets the assembly attributes defined in the System.Reflection namespace http://msdn.microsoft.com/en-us/library/136wx94f.aspx
    public class AssemblyAttributes
    {
        public AssemblyHashAlgorithm AssemblyHashAlgorithm { get; private set; }
        public AssemblyNameFlags AssemblyFlags { get; private set; }
        public string Company { get; private set; }
        public string Configuration { get; private set; }
        public string Copyright { get; private set; }
        public string Culture { get; private set; }
        public string DefaultAlias { get; private set; }
        public bool DelaySign { get; private set; }
        public string Description { get; private set; }
        public string FileVersion { get; private set; }
        public string InformationalVersion { get; private set; }
        public string KeyFile { get; private set; }
        public string KeyName { get; private set; }
        public string Product { get; private set; }
        public string Title { get; private set; }
        public string Trademark { get; private set; }
        public string Version { get; private set; }
        // not attributes, but easy to use
        public AssemblyName AssemblyName { get; private set; }
        public DateTime LinkerTimestamp { get; private set; }

        public string Name
        {
            get
            {
                return null == AssemblyName ? string.Empty : AssemblyName.Name;
            }
        }

        public AssemblyAttributes(Assembly assembly = null)
        {
            if (assembly == null)
                assembly = Assembly.GetCallingAssembly();

            AssemblyName = assembly.GetName();
            LinkerTimestamp = RetrieveLinkerTimestamp(assembly);

            // no need to set default values for the properties for non-existing attributes, as those values are empty strings
            RunOnAssemblyAttribute(assembly, x =&gt; { AssemblyHashAlgorithm = (AssemblyHashAlgorithm)(x.AlgorithmId); });
            RunOnAssemblyAttribute(assembly, x =&gt; { AssemblyFlags = (AssemblyNameFlags)(x.AssemblyFlags); });
            RunOnAssemblyAttribute(assembly, x =&gt; { Company = x.Company; });
            RunOnAssemblyAttribute(assembly, x =&gt; { Configuration = x.Configuration; });
            RunOnAssemblyAttribute(assembly, x =&gt; { Copyright = x.Copyright; });
            RunOnAssemblyAttribute(assembly, x =&gt; { Culture = x.Culture; });
            RunOnAssemblyAttribute(assembly, x =&gt; { DefaultAlias = x.DefaultAlias; });
            RunOnAssemblyAttribute(assembly, x =&gt; { DelaySign = x.DelaySign; });
            RunOnAssemblyAttribute(assembly, x =&gt; { Description = x.Description; });
            RunOnAssemblyAttribute(assembly, x =&gt; { FileVersion = x.Version; });
            RunOnAssemblyAttribute(assembly, x =&gt; { InformationalVersion = x.InformationalVersion; });
            RunOnAssemblyAttribute(assembly, x =&gt; { KeyFile = x.KeyFile; });
            RunOnAssemblyAttribute(assembly, x =&gt; { KeyName = x.KeyName; });
            RunOnAssemblyAttribute(assembly, x =&gt; { Product = x.Product; });
            RunOnAssemblyAttribute(assembly, x =&gt; { Title = x.Title; });
            RunOnAssemblyAttribute(assembly, x =&gt; { Trademark = x.Trademark; });
            RunOnAssemblyAttribute(assembly, x =&gt; { Version = x.Version; });
        }

        public static void RunOnAssemblyAttribute(Assembly assembly, Action action) where T : Attribute
        {
            if (assembly == null)
                throw new ArgumentNullException(Reflector.GetName(new { assembly }));

            object[] attribs = assembly.GetCustomAttributes(typeof(T), false);
            if (attribs.Length == 0)
                return;

            T value = (T)attribs[0];
            action(value);
        }

        public static DateTime RetrieveLinkerTimestamp(Assembly assembly)
        {
            string filePath = assembly.Location;
            DateTime result = RetrieveLinkerTimestamp(filePath);
            return result;
        }

        /// <summary> /// http://www.codinghorror.com/blog/2005/04/determining-build-date-the-hard-way.html /// </summary>
        ///
        ///
        public static DateTime RetrieveLinkerTimestamp(string filePath)
        {
            try
            {
                const int PEHeaderOffset = 60;
                const int LinkerTimestampOffset = 8;

                const int PEHeaderSize = 2048;
                byte[] b = new byte[PEHeaderSize];
                using (Stream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
                {
                    stream.Read(b, 0, PEHeaderSize);
                }

                // not sure why BitConverter does not take uint parameters; negative offsets sound a bit silly to me
                int i = BitConverter.ToInt32(b, PEHeaderOffset);

                int SecondsSince1970 = BitConverter.ToInt32(b, i + LinkerTimestampOffset);
                DateTime result = new DateTime(1970, 1, 1, 0, 0, 0);
                result = result.AddSeconds(SecondsSince1970);
                result = result.AddHours(TimeZone.CurrentTimeZone.GetUtcOffset(result).Hours);
                return result;
            }
            catch (Exception ex)
            {
                return default(DateTime);
            }
        }
    }
}

Alternative way of getting the executable name:

using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;

namespace BeSharp
{
    public class AssemblyHelper
    {

        // Logic borrowed from the Delphi.NET RTL
        public static string ExecutableName
        {
            get
            {
                string fullyQualifiedName;

                Assembly entryAssembly = Assembly.GetEntryAssembly();
                if (null != entryAssembly)
                {
                    fullyQualifiedName = entryAssembly.GetModules()[0].FullyQualifiedName;
                }
                else
                {
                    // http://msdn.microsoft.com/en-us/library/system.reflection.assembly.getentryassembly.aspx
                    // might be null when started from an unmanaged application.
                    // or from a WCF process, or as an addin
                    // http://stackoverflow.com/a/2848696/29290
                    // http://stackoverflow.com/a/616606/29290
                    fullyQualifiedName = Process.GetCurrentProcess().MainModule.FileName;
                }
                return Path.GetFileName(fullyQualifiedName);
            }
        }

    }
}

–jeroen

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s

 
%d bloggers like this: