Archive for August, 2009

Build Your Own Data Access Layer: Mapping Classes To Tables

11 commentsWritten on August 23rd, 2009 by
Categories: Build Your Own DAL

Note: This post is part of a series. Be sure to read the introduction here.

When you need to populate entity instances with data from a database, you need to know which table the data needs to come from, which columns will map to which property on the entity class, and you'll need to deal with a variety of types. The approach that i've chosen to use tries to make this as simple as possible. The idea is basically to place an attribute with the name of the table on top of the entity class, and an attribute on each property with the name of the column it maps to. For foreign keys, i wanted to be able to just use properties of the type of the referenced entity, instead of having foreign keys in my entities. For these references, we will use an attribute with the name of the foreign key column.

First, we'll need to define these attributes:

    [AttributeUsage(AttributeTargets.Class, Inherited = true)]
    public class TableAttribute : Attribute
    {
        public string TableName { get; private set; }
 
        public TableAttribute(string tableName)
        {
            TableName = tableName;
        }
    }

    [AttributeUsage(AttributeTargets.Property, Inherited = true)]
    public class PrimaryKeyAttribute : Attribute
    {
        public string ColumnName { get; private set; }
 
        public PrimaryKeyAttribute(string columnName)
        {
            ColumnName = columnName;
        }
    }

    [AttributeUsage(AttributeTargets.Property, Inherited = true)]
    public class ColumnAttribute : Attribute
    {
        public string ColumnName { get; private set; }
 
        public ColumnAttribute(string columnName)
        {
            ColumnName = columnName;
        }
    }

    [AttributeUsage(AttributeTargets.Property, Inherited = true)]
    public class ReferenceAttribute : Attribute
    {
        public string ColumnName { get; private set; }
 
        public ReferenceAttribute(string columnName)
        {
            ColumnName = columnName;
        }
    }

Notice how none of these properties have any indication of types to use. The .NET type will be inferred automatically, and it will be mapped to a compatible DbType without having to specify these types all over the place.

We will use the following helper class to map .NET types to their respective DbTypes:

    public static class TypeConverter
    {
        private static readonly Dictionary<Type, DbType> typeToDbType = new Dictionary<Type, DbType>
        {
            { typeof(string), DbType.String },
            { typeof(DateTime), DbType.DateTime },
            { typeof(DateTime?), DbType.DateTime },
            { typeof(int), DbType.Int32 },
            { typeof(int?), DbType.Int32 },
            { typeof(long), DbType.Int64 },
            { typeof(long?), DbType.Int64 },
            { typeof(bool), DbType.Boolean },
            { typeof(bool?), DbType.Boolean },
            { typeof(byte[]), DbType.Binary },
            { typeof(decimal), DbType.Decimal },
            { typeof(decimal?), DbType.Decimal },
            { typeof(double), DbType.Double },
            { typeof(double?), DbType.Double },
            { typeof(float), DbType.Single },
            { typeof(float?), DbType.Single },
            { typeof(Guid), DbType.Guid },
            { typeof(Guid?), DbType.Guid }
        };
 
        public static DbType ToDbType(Type type)
        {
            if (!typeToDbType.ContainsKey(type))
            {
                throw new InvalidOperationException(string.Format("Type {0} doesn't have a matching DbType configured", type.FullName));
            }
 
            return typeToDbType[type];
        }
    }

Obviously, more type conversions can be added... these are just the ones i've needed so far.

Once you've placed all the attributes on top of your entities and properties, we can start building a model of all this metadata. This will all be stored in a MetaDataStore class that i'll show later on in this post. Having access to the MetaDataStore makes the implementation of some of these metadata types easier, so i have the following abstract class:

    public abstract class MetaData
    {
        protected MetaDataStore MetaDataStore { get; private set; }
 
        protected MetaData(MetaDataStore metaDataStore)
        {
            MetaDataStore = metaDataStore;
        }
    }

Now we can go over each piece of metadata. First, the ColumnInfo class:

    public class ColumnInfo : MetaData
    {
        public string Name { get; private set; }
        public Type DotNetType { get; private set; }
        public DbType DbType { get; private set; }
        public PropertyInfo PropertyInfo { get; private set; }
 
        public ColumnInfo(MetaDataStore store, string name, Type dotNetType, PropertyInfo propertyInfo)
            : this(store, name, dotNetType, TypeConverter.ToDbType(dotNetType), propertyInfo)
        {
        }
 
        public ColumnInfo(MetaDataStore store, string name, Type dotNetType, DbType dbType, PropertyInfo propertyInfo)
            : base(store)
        {
            Name = name;
            DotNetType = dotNetType;
            DbType = dbType;
            PropertyInfo = propertyInfo;
        }
    }

As you can see, we have all the information we need to be able to do something with this column. We have its Name, the .NET type that is used in the mapped class, the DbType and a PropertyInfo reference to its respective property in the mapped class so we can get and set its value.

For references, we need to know something more:

    public class ReferenceInfo : ColumnInfo
    {
        public Type ReferenceType { get; private set; }
 
        public ReferenceInfo(MetaDataStore store, string name, Type referenceType, PropertyInfo propertyInfo)
            : base(store, name, store.GetTableInfoFor(referenceType).PrimaryKey.DotNetType,
                    store.GetTableInfoFor(referenceType).PrimaryKey.DbType, propertyInfo)
        {
            ReferenceType = referenceType;
        }
    }

For a regular column, it's sufficient to know the .NET type of the property and the DbType. But for a reference, you need to know the actual type of the referenced entity, as well as the .NET type of it's primary key column. As you can see in the constructor, we retrieve the TableInfo of the referenced entity, and use the .NET type and the DbType of the primary key of the referenced entity. The PrimaryKey property of a TableInfo class (which i'll show below) is also a ColumnInfo object. We obviously also store the actual type of the referenced entity. And of course, we again store a PropertyInfo so we can get/set the value of the reference.

The TableInfo class can now hold all of the information that we need. We know all about its primary key (through the PrimaryKeyAttribute), its regular properties (through the ColumnAttribute) and its referenced properties (through the ReferenceAttribute). With all of that information, the TableInfo class is able to build your typical default SQL statements for CRUD functionality:

    public class TableInfo : MetaData
    {
        public string Name { get; private set; }
        public Type EntityType { get; private set; }
        public ColumnInfo PrimaryKey { get; set; }
        public IEnumerable<ReferenceInfo> References { get { return references.Values; } }
        public IEnumerable<ColumnInfo> Columns { get { return columns.Values; } }
 
        private readonly Dictionary<string, ColumnInfo> columns = new Dictionary<string, ColumnInfo>();
        private readonly Dictionary<string, ReferenceInfo> references = new Dictionary<string, ReferenceInfo>();
 
        public TableInfo(MetaDataStore store, string name, Type entityType)
            : base(store)
        {
            Name = name;
            EntityType = entityType;
        }
 
        public void AddColumn(ColumnInfo column)
        {
            if (columns.ContainsKey(column.Name))
            {
                throw new InvalidOperationException(string.Format("An item with key {0} has already been added", column.Name));
            }
 
            columns.Add(column.Name, column);
        }
 
        public void AddReference(ReferenceInfo reference)
        {
            if (references.ContainsKey(reference.Name))
            {
                throw new InvalidOperationException(string.Format("An item with key {0} has already been added", reference.Name));
            }
 
            references.Add(reference.Name, reference);
        }
 
        public ColumnInfo GetColumn(string columnName)
        {
            if (!columns.ContainsKey(columnName))
            {
                throw new InvalidOperationException(string.Format("The table '{0}' does not have a '{1}' column", Name, columnName));
            }
 
            return columns[columnName];
        }
 
        public StringBuilder GetSelectStatementForAllFields()
        {
            StringBuilder builder = new StringBuilder("SELECT " + Escape(PrimaryKey.Name) + ", ");
 
            AddReferenceColumnNames(builder);
            AddRegularColumnNames(builder);
            RemoveLastCommaAndSpaceIfThereAreAnyColumns(builder);
            builder.Append(" FROM " + Escape(Name));
 
            return builder;
        }
 
        public string GetInsertStatement()
        {
            StringBuilder builder = new StringBuilder("INSERT INTO " + Escape(Name) + " (");
 
            AddReferenceColumnNames(builder);
            AddRegularColumnNames(builder);
            RemoveLastCommaAndSpaceIfThereAreAnyColumns(builder);
            builder.Append(") VALUES (");
            AddReferenceColumnParameterNames(builder);
            AddRegularColumnParameterNames(builder);
            RemoveLastCommaAndSpaceIfThereAreAnyColumns(builder);
            builder.Append("); SELECT SCOPE_IDENTITY();");
 
            return builder.ToString();
        }
 
        public string GetUpdateStatement()
        {
            StringBuilder builder = new StringBuilder("UPDATE " + Escape(Name) + " SET ");
 
            AddReferenceColumnsNameWithParameterName(builder);
            AddRegularColumnsNameWithParameterName(builder);
            RemoveLastCommaAndSpaceIfThereAreAnyColumns(builder);
            AddWhereByIdClause(builder);
            builder.Append(";");
 
            return builder.ToString();
        }
 
        public string GetDeleteStatement()
        {
            StringBuilder builder = new StringBuilder("DELETE FROM " + Escape(Name) + " ");
 
            AddWhereByIdClause(builder);
            builder.Append(";");
 
            return builder.ToString();
        }
 
        public IEnumerable<AdoParameterInfo> GetParametersForInsert(object entity)
        {
            return GetParametersForAllReferenceAndRegularColumns(entity);
        }
 
        public IEnumerable<AdoParameterInfo> GetParametersForUpdate(object entity)
        {
            var parameters = GetParametersForAllReferenceAndRegularColumns(entity);
            parameters.Add(new AdoParameterInfo(PrimaryKey.Name, PrimaryKey.DbType, PrimaryKey.PropertyInfo.GetValue(entity, null)));
            return parameters;
        }
 
        public StringBuilder AddWhereByIdClause(StringBuilder query)
        {
            query.Append(" WHERE " + Escape(PrimaryKey.Name) + " = " + GetPrimaryKeyParameterName());
            return query;
        }
 
        public string GetPrimaryKeyParameterName()
        {
            return "@" + PrimaryKey.Name;
        }
 
        private List<AdoParameterInfo> GetParametersForAllReferenceAndRegularColumns(object entity)
        {
            var parameters = new List<AdoParameterInfo>();
 
            foreach (var referenceInfo in References)
            {
                var referencedEntity = referenceInfo.PropertyInfo.GetValue(entity, null);
                var referencePrimaryKeyProperty = MetaDataStore.GetTableInfoFor(referenceInfo.ReferenceType).PrimaryKey.PropertyInfo;
 
                if (referencedEntity == null)
                {
                    parameters.Add(new AdoParameterInfo(referenceInfo.Name, referenceInfo.DbType, null));
                }
                else
                {
                    parameters.Add(new AdoParameterInfo(referenceInfo.Name, referenceInfo.DbType, referencePrimaryKeyProperty.GetValue(referencedEntity, null)));
                }
            }
 
            foreach (var columnInfo in Columns)
            {
                parameters.Add(new AdoParameterInfo(columnInfo.Name, columnInfo.DbType, columnInfo.PropertyInfo.GetValue(entity, null)));
            }
 
            return parameters;
        }
 
        private void RemoveLastCommaAndSpaceIfThereAreAnyColumns(StringBuilder builder)
        {
            if ((References.Count() + Columns.Count()) > 0)
            {
                RemoveLastCharacters(builder, 2);
            }
        }
 
        private void AddReferenceColumnNames(StringBuilder builder)
        {
            foreach (var referenceInfo in References)
            {
                builder.Append(Escape(referenceInfo.Name) + ", ");
            }
        }
 
        private void AddReferenceColumnParameterNames(StringBuilder builder)
        {
            foreach (var referenceInfo in References)
            {
                builder.Append("@" + referenceInfo.Name + ", ");
            }
        }
 
        private void AddReferenceColumnsNameWithParameterName(StringBuilder builder)
        {
            foreach (var referenceInfo in References)
            {
                builder.Append(Escape(referenceInfo.Name) + " = @" + referenceInfo.Name + ", ");
            }
        }
 
        private void AddRegularColumnNames(StringBuilder builder)
        {
            foreach (var columnInfo in Columns)
            {
                builder.Append(Escape(columnInfo.Name) + ", ");
            }
        }
 
        private void AddRegularColumnParameterNames(StringBuilder builder)
        {
            foreach (var columnInfo in Columns)
            {
                builder.Append("@" + columnInfo.Name + ", ");
            }
        }
 
        private void AddRegularColumnsNameWithParameterName(StringBuilder builder)
        {
            foreach (var columnInfo in Columns)
            {
                builder.Append(Escape(columnInfo.Name) + " = @" + columnInfo.Name + ", ");
            }
        }
 
        private string Escape(string name)
        {
            return "[" + name + "]";
        }
 
        private void RemoveLastCharacters(StringBuilder stringBuilder, int numberOfCharacters)
        {
            stringBuilder.Remove(stringBuilder.Length - numberOfCharacters, numberOfCharacters);
        }
    }

This is actually the biggest class in this DAL. I probably should move the building of the SQL statements and providing parameter info into some kind of helper class because this is a bit of a Single Responsability Principle violation. Speaking of parameter info, i'm using the following helper class to store this information:

    public class AdoParameterInfo
    {
        public DbType DbType { get; private set; }
        public string Name { get; private set; }
        public object Value { get; private set; }
 
        public AdoParameterInfo(string name, DbType dbType, object value)
        {
            Name = name;
            DbType = dbType;
            Value = value;
        }
    }

One thing that you may have noticed is that the generated INSERT statement assumes that SQL Server identity-style generators are being used for primary key values. Not only that, i'm not even trying to target any other database then SQL Server with this DAL. Those are 2 rather significant shortcomings of this DAL. First of all, dealing with multiple identifier strategies can become pretty complex pretty fast. For this DAL, SQL Server Identity primary keys are sufficient but in a lot of cases you will probably want support for assigned identifier strategies, for GUIDs (preferably locally generated with a sequential GUID algorithm), HiLo and maybe even other ones. If you really want to, you can do all of this yourself, but you'll quickly spend an entire week (or more) to properly implement all of these identifier strategies.

As for only targeting SQL Server, that is sufficient in our scenario but a proper DAL should be able to deal with multiple databases. Of course, this has a direct impact on a lot of implementation details. For starters, you'd never be able to just construct a SQL statement directly in your code and you will need something to make sure the correct statements are generated for your specific database. NHibernate does a pretty nice job of this by providing a strategy-like implementation through its Dialect class and its derivatives. Also, some of your identifier strategies will be different for each database that you need to support. If you got a headache just from reading these last 2 paragraphs, just imagine implementing this and getting it all 'right' in a maintainable matter.

Anyways, back to the topic at hand. We now have the classes we need to build up our metadata model of all of the tables we need to provide data access functionality for. Well, we still need something to hold all of this information and to actually build up this model:

    public class MetaDataStore
    {
        private readonly Dictionary<Type, TableInfo> typeToTableInfo = new Dictionary<Type, TableInfo>();
 
        public TableInfo GetTableInfoFor<TEntity>()
        {
            return GetTableInfoFor(typeof(TEntity));
        }
 
        public TableInfo GetTableInfoFor(Type entityType)
        {
            if (!typeToTableInfo.ContainsKey(entityType))
            {
                return null;
            }
 
            return typeToTableInfo[entityType];
        }
 
        public void BuildMetaDataFor(Assembly assembly)
        {
            BuildMapOfEntityTypesWithTheirTableInfo(assembly);
 
            foreach (KeyValuePair<Type, TableInfo> pair in typeToTableInfo)
            {
                // we need this info for each entity before we can deal with references to other entities
                LoopThroughPropertiesWith<PrimaryKeyAttribute>(pair.Key, pair.Value, SetPrimaryKeyInfo);
            }
 
            foreach (KeyValuePair<Type, TableInfo> pair in typeToTableInfo)
            {
                LoopThroughPropertiesWith<ReferenceAttribute>(pair.Key, pair.Value, AddReferenceInfo);
                LoopThroughPropertiesWith<ColumnAttribute>(pair.Key, pair.Value, AddColumnInfo);
            }
        }
 
        private void BuildMapOfEntityTypesWithTheirTableInfo(Assembly assembly)
        {
            foreach (var type in assembly.GetTypes())
            {
                var typeAttributes = Attribute.GetCustomAttributes(type, typeof(TableAttribute));
 
                if (typeAttributes.Length > 0)
                {
                    var tableAttribute = (TableAttribute)typeAttributes[0];
                    var tableInfo = new TableInfo(this, tableAttribute.TableName, type);
                    typeToTableInfo.Add(type, tableInfo);
                }
            }
        }
 
        private void LoopThroughPropertiesWith<TAttribute>(Type entityType, TableInfo tableInfo,
            Action<TableInfo, PropertyInfo, TAttribute> andExecuteFollowingCode)
            where TAttribute : Attribute
        {
            foreach (var propertyInfo in entityType.GetProperties())
            {
                var attribute = GetAttribute<TAttribute>(propertyInfo);
 
                if (attribute != null)
                {
                    andExecuteFollowingCode(tableInfo, propertyInfo, attribute);
                }
            }
        }
 
        private void SetPrimaryKeyInfo(TableInfo tableInfo, PropertyInfo propertyInfo, PrimaryKeyAttribute primaryKeyAttribute)
        {
            tableInfo.PrimaryKey = new ColumnInfo(this, primaryKeyAttribute.ColumnName, propertyInfo.PropertyType, propertyInfo);
        }
 
        private void AddColumnInfo(TableInfo tableInfo, PropertyInfo propertyInfo, ColumnAttribute columnAttribute)
        {
            tableInfo.AddColumn(new ColumnInfo(this, columnAttribute.ColumnName, propertyInfo.PropertyType, propertyInfo));
        }
 
        private void AddReferenceInfo(TableInfo tableInfo, PropertyInfo propertyInfo, ReferenceAttribute referenceAttribute)
        {
            tableInfo.AddReference(new ReferenceInfo(this, referenceAttribute.ColumnName, propertyInfo.PropertyType, propertyInfo));
        }
 
        private TAttribute GetAttribute<TAttribute>(PropertyInfo propertyInfo) where TAttribute : Attribute
        {
            var attributes = Attribute.GetCustomAttributes(propertyInfo, typeof(TAttribute));
            if (attributes.Length == 0) return null;
            return (TAttribute)attributes[0];
        }
    }

This class gives you the ability to retrieve the TableInfo class for a specfic entity type. It also allows you to build the metadata model by passing in an assembly. It will then loop through all of the types in the assembly to discover the types that have a TableAttribute, and it will then build the TableInfo objects with all of the information we need.

And that's all we need to create mappings between tables and our entities. This wasn't hard, but it's not very powerful either. We can't define custom user types that our DAL needs to be able to deal with, nor can we define any database inheritance strategies. Our attributes are all inheritable, so you can use some inheritance with your entities, but you are essentially limited to the Table Per Class inheritance strategy. Implementing support for the other inheritance strategies would obviously introduce a lot more complexity in the whole mapping aspect.

In the next post, i'll show you how this DAL will use TableInfo's methods to create CRUD statements to offer out-of-the-box CRUD functionality for each mapped entity.

Build Your Own Data Access Layer Series

19 commentsWritten on August 23rd, 2009 by
Categories: Build Your Own DAL

I'm definitely not a fan of building your own Data Access Layer (DAL), since there are plenty of powerful and mature options already available. However, we have 2 customers at work who simply don't let us use any existing libraries/tools as a DAL and want us to just use straight ADO.NET. I don't want to get into their reasons for this, but the reality of the situation is that whenever we have to develop projects for them, we need to use a custom built DAL. I've never seen a custom built DAL that i found acceptable, let alone one that i actually wanted to use.

A lot of people typically go the code generation route when faced with this situation, which is exactly what we have done in the past. Been there, done that, hated it with a passion for various reasons. One of my coworkers recently started a new project for one of these customers, and he started implementing a new DAL. I had to review this, and while it had some good ideas there was a large amount of repetitive and error-prone code that still needed to be written by developers for every table. So i set out to come up with something better. If we did have to use a custom DAL for these customers, i wanted to make sure that it would at least avoid having us write repetitive, error-prone code for every table that we needed to use. Oh, and without having to resort to code generation. Since we are all NHibernate users (when customers don't have a problem with us using it, that is) i wanted something that was somewhat similar in ease-of-use though it could obviously never match its feature set, power and maturity.

I spent about 24 working hours (in total) on this, and i believe i came up with something that is acceptable for most simple forms-over-data applications. This DAL allows you to write your entity classes as POCO's, offers 'out-of-the-box' CRUD functionality for every mapped table, and has lazy loading for reference properties (so you don't need to pollute your entity classes with foreign key properties). There is also a simple session-level cache, and there is some functionality to ease the pain of using simple, custom queries (with that i mean: every query that is not a select all or select by id and that doesn't join other tables).

Compared to a real ORM, it is missing a lot: there is no Unit Of Work implementation, no automated change tracking of entities, no dirty checks, no collection support, no advanced querying possibilities, no statement batching, no serious caching functionality, no transitive persistence, and a whole host of features that something like NHibernate gives you for free. Each and every one of those features comes with a great cost of complexity and development time to get 'right' so it truly doesn't make a lot of sense to do all of this yourself.

In this series, we're going to go over the entire implementation of this DAL and throughout the series i will point out its shortcomings and try to explain the complexity that would be required to make it truly powerful. The purpose of this series is basically to:

  • Show you that you really don't need to resort to code generation to build your own custom DAL
  • Show you what kind of complexity is involved with the implementation of a good DAL
  • Convince you that you typically are better off with simply using something that is already available as a mature, powerful and proven solution

These are the posts that this series consists of:

  1. Mapping Classes To Tables
  2. Out Of The Box CRUD Functionality
  3. Hydrating Entities
  4. Session Level Cache
  5. Lazy Loading
  6. Executing Custom Queries
  7. Bringing It All Together
  8. Conclusions
  9. Enabling Bulk Inserts

I will update the list above with actual links to the posts as soon as they are published.

Note: the code of this series can be found here

Of Course NHibernate Is Slow When You Use It Incorrectly

16 commentsWritten on August 19th, 2009 by
Categories: NHibernate, Performance

Just saw the following post where the performance of NHibernate and Entity Framework is compared for a couple of different operations. Spoiler alert: NHibernate loses. As it typically does in these kinds of 'benchmarks' or 'comparisons' that seem to pop up frequently lately.

For some reason, a lot of people seem to think that opening an NHibernate session and performing thousands of operations is a valid use case. It's not. Far from it actually. And with all of the features that NHibernate offers, it can't possibly perform well in such a scenario. See, an NHibernate session is a unit of work. A unit of work is a business transaction which is typically short and small, but it should never be something huge. Your DBA probably won't appreciate huge database transactions on the database either.

Whenever you load an object through NHibernate, it will be tracked by the session that loaded it. That means that the NHibernate session keeps a reference to it, and performs a series of checks on it periodically, depending on what you're doing and some configuration settings such as the FlushMode. For instance, suppose you've loaded a hundred different entities in one session. If the FlushMode is set to automatic, it means that NHibernate will perform a dirty check for each entity associated with the session before each query is executed. The more entity instances you've loaded, the longer this takes (obviously). If you take this to an extreme level, like loading thousands of entities like most of these 'benchmarks' do, performance will naturally be horrible.

Each entity instance is also stored in the first level (or session level) cache. That means that whenever you retrieve a row from the database, NHibernate will check if an instance of that row already exists in the first level cache. Again, the more instances you've loaded, the larger the overhead of this will be. There are also a lot of possible extension points where you can plug in custom logic. Again, there is a very minor cost that comes with this extensibility and as you can expect, that minor cost can add up to something much more noticable once you start dealing with an unreasonably large number of instances in your session.

Always keep in mind that an ORM (and this goes for every ORM) is most suitable for OLTP. Using an ORM for batch processing jobs or large data processing operations in general is simply put a bad idea. And they will never perform as well as other solutions in these scenarios. So please don't bother even benchmarking ORM performance in non OLTP usage because it quite simply doesn't make sense to do so, and the results will be completely untrustworthy anyway.

An ORM can offer you nice performance gains in OLTP scenarios simply by trying to minimize database connectivity, minimizing the number of database operations, and relatively sane caching usage. Unfortunately, these are aspects that are never tested in these 'benchmarks' or 'comparisons'.

Choose The Right Goals

4 commentsWritten on August 10th, 2009 by
Categories: Opinions, work/career

One of the biggest forms of waste that i've encountered in my (admittedly short) career so far is that of having the wrong goals. I spent the first 5 years of my career working at a rather large financial institution and one of the things that bothered me the most was that too many of the developers working there were too focused on their goal of moving up, of reaching a certain position or status. Well actually, it wasn't just the developers. Pretty much every kind of manager i met there was also mostly driven by advancing his/her own career.

Most of the developers wanted to advance to the position of 'lead developer' as fast as possible. The funny thing is (to me at least) that at this particular client, with their organization and way of working, being a 'lead developer' was pretty much one of the worst jobs you could have over there. It basically meant you had a lot less time to do the things you (supposedly) enjoy doing the most: writing interesting code. You'd have to spend a lot more time doing more project-related administrative work, typically whatever your project manager was either unwilling to do, incapable of, or in some cases just enjoyed passing off to one of his 'subordinates'.

Yet, a lot of the developers who worked there absolutely wanted to become lead developers. Some of them made it, and some of them didn't and would keep saying they wanted to be promoted to lead developers. Some of them even threatened to quit or would just become a huge pain in the ass if they didn't get that position. I honestly can't believe that even one of them truly enjoyed the job of lead developer more than that of being a 'regular' developer, but that's probably somewhat specific to this particular company. Unless of course they weren't happy doing development but then the motivation behind wanting to advance at least comes from some kind of value that they seek in their job. Which is obviously a good thing. The majority of them however, merely wanted the status that was associated with their new job title.

To avoid any misunderstandings, i don't have any problems with people wanting to move up or advance their positions. Self-improvement and growth are important qualities that you'd wish everyone would have. But merely striving to move up, just for the status that is associated with a certain position or title is definitely not a path to a happy career. Eventually, it's only going to lead to frustrations and unhappiness about your job, and let's face it, if you're spending pretty much half of your active adult life doing your job, you might as well want to make sure you're doing something you really enjoy doing.

In my case, i only had two goals when i started working as a software developer. The first was to continue improving as a developer throughout my career. The second was to avoid being put in a position where i had to develop software in a way that isn't up to the standards that i know are attainable. Those are the only two goals that i've focused on, and it has payed off a lot for me. Not necessarily in the financial sense (but then again, that's hardly my primary concern) but in the way that truly matters to me: how i get to spend half of my active adult life.

And that is the advice that i would give to everyone who's either starting their career, or are wondering what they should focus on or wondering how they should proceed with their career. Focus on what is truly important to you, the values that make you enjoy whatever it is that you do all day. Improve whatever skills you need for job, focus on producing value and being happy with what you do. After a while, everything else (the status, the money, the responsibilities, ...) that you may want in your job just might come to you automatically. And odds are high that you'll avoid a huge amount of frustrations and inter-office-politics that you really don't want to have to deal with. Do yourself a huge favor, and don't get misled by that fancy job-title or a certain position or status. Because it typically isn't worth it.

Finding Memory Leaks In Silverlight With WinDbg

16 commentsWritten on August 8th, 2009 by
Categories: Memory Management, Silverlight

As i mentioned in a previous post, you can attach WinDbg to a browser to find memory leaks in your Silverlight applications. I figured it would be a good idea to write down how this process works, since i always end up having to look it up again whenever i need to do this.

I wrote a very simple Silverlight application which has a rather typical memory leak. Here's the actual code:

        public event EventHandler<MyEventArgs> MyEvent = delegate { };
 
        private void CreateNewViewButton_OnClick(object sender, RoutedEventArgs e)
        {
            ViewContainer.Children.Clear();
 
            var newView = new MyView();
            MyEvent += newView.EventHandler;
 
            ViewContainer.Children.Add(newView);
        }

For some of you, the memory leak is already very clear. Like i said, it's a very simple example ;)

Let's go through the process of finding and fixing the memory leak using WinDbg. First of all, download Debugging Tools For Windows (which contains the WinDbg executable) here and install it.

Then we start our application in Internet Explorer (for some reason i can't use WinDbg to inspect the managed memory heap with Firefox, so i just use Internet Explorer for this stuff) and use it. In the case of my example, that means clicking the button which creates a new view a couple of times.

Open WinDbg.exe and select the 'Attach to a Process' menu item in the 'File' menu and select the iexplore.exe process.

Then you need to load the correct version of sos.dll:

step1

After that we can see which types of our MySilverlightApplication namespace are present in the managed heap, including how many instances of them:

step2

As you can see, there are 13 instances of our MyView type present in the heap. Using the value in the MT column, we can drill down further:

step3

This shows the memory address of each instance of the MyView type in the heap. Now we can see if there are any live references to these instances:

step4

This is actually for the first address that was listed. As you can see, it is still a reachable reference, which means it will not be collected by the garbage collector. The chain of references clearly indicates that the instance is still referenced from our event in the MainPage instance. All of the previously listed instances show the same reference chain, so this is clearly a memory leak. Even though we should only have one active reference of MyView at any point in time of this application, the MyEvent event on MainPage clearly keeps each instance of MyView alive.

The correct way to fix this is to make sure that whenever we remove an instance of MyView, we need to unsubscribe it from the MainPage's MyEvent handler. Always remember this rule when it comes to dealing with events: if the publisher of the event has a longer lifetime than the subscriber of the event, then you absolutely have to unsubscribe each subscriber from the event or the publisher will keep references to each subscriber (preventing them from being garbage collected) for as long as the publisher is alive.

Here's the modified version of the above code which avoids the memory leak:

        public event EventHandler<MyEventArgs> MyEvent = delegate { };
 
        private void CreateNewViewButton_OnClick(object sender, RoutedEventArgs e)
        {
            CleanUpPreviousView();
 
            var newView = new MyView();
            MyEvent += newView.EventHandler;
 
            ViewContainer.Children.Add(newView);
        }
 
        private void CleanUpPreviousView()
        {
            if (ViewContainer.Children.Count > 0)
            {
                var myView = ViewContainer.Children[0] as MyView;
                if (myView != null) MyEvent -= myView.EventHandler;
                ViewContainer.Children.Clear();
            }
        }

Let's see if this really fixed the memory leak. If we fire up the application and press the button a couple of times, the application should normally only have one live reference of MyView in memory.

step5

I clicked the button 5 times, and the above output shows that there are 5 instances of MyView on the heap. So did we fix the leak or not? Check the output below:

step6

As you can see, only the last instance of MyView is actively referenced somewhere. That means that the first 4 instances are ready to be collected during the next garbage collection.

One thing i don't understand though, is that the reference chain of the last instance doesn't mention MainPage or the event handler anymore. But when i attached Visual Studio's debugger to the browser instance i could clearly see that the MyEvent of MainPage indeed contained an event handler that pointed to this MyView instance. I'm far from a WinDbg and SOS expert so i have no idea why the reference chain doesn't reflect this. Perhaps someone with more WinDbg and SOS knowledge can shed some light on this?

Either way, this approach is a pretty good way of finding memory leaks in your Silverlight code. In a real application it's obviously a bit more complicated to find the exact cause of a leak compared to this simple example, but it's still pretty doable. Just execute the !dumpheap -stat -type YourRootNameSpaceHere and look for unusually high numbers of instances of your types. Then you can start looking at each instance to figure out what's going on. And for a nice list of commands that you can execute in WinDbg with SOS, be sure to check this out.

Also, keep in mind that you can do this for every .NET process, and not just Silverlight. Though you would need to load the sos.dll file of your particular .NET version.