Wednesday, September 30, 2009

Custom MS Build Task to tame System.Reflection.AmbiguousMatchException: Ambiguous match found.

Update (Oct 3, 2009): Thanks to the reader that pointed out that the MS Build task was using a hard-coded path. The example has been corrected to use a parameterized path that relies on the MSBuildProjectDirectory [3] reserved property.

An innocent “mistake” [1], if you can even call it that, has been punishing developers worldwide for a good part of the last decade – can you spot it?

   1: <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebApplication1._Default" %>
   2:  
   3: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   4:  
   5: <html xmlns="http://www.w3.org/1999/xhtml" >
   6: <head runat="server">
   7:     <title></title>
   8: </head>
   9: <body>
  10:     <form id="form1" runat="server">
  11:     <div>
  12:         <asp:HiddenField runat="server" ID="RecordId" />
  13:     </div>
  14:     </form>
  15: </body>
  16: </html>
   1: using System;
   2:  
   3: namespace WebApplication1
   4: {
   5:     public partial class _Default : System.Web.UI.Page
   6:     {
   7:         private int recordId = 0;
   8:  
   9:         protected void Page_Load(object sender, EventArgs e)
  10:         {
  11:             Response.Write(recordId);
  12:         }
  13:     }
  14: }

What’s worse is that the exception thrown doesn’t lend any clues as to what the source of the ambiguity is – even this level of detail requires some custom logging:

System.Reflection.AmbiguousMatchException: Ambiguous match found.
   at System.RuntimeType.GetField(String name, BindingFlags bindingAttr)
   at System.Web.UI.Util.GetNonPrivateFieldType(Type classType, String fieldName)
   at System.Web.Compilation.BaseTemplateCodeDomTreeGenerator.BuildFieldDeclaration
ControlBuilder builder)

So what’s wrong with the code?

Two fields differ only in case. Yup – that’s it. RecordId is both a HiddenField and an integer. C# allows this – not a word is raised during compilation. Based on what I’ve seen and read (it’s widely covered), it only happens only when you pre-compile either a Website or Web application and navigate to the page in question.

Now imagine having a page with 20+ controls and trying to figure out where this is coming from – how’s the reflector supposed to help here?

How can we catch it at compile time?

It’s surprisingly easy, actually:

image_thumb[5]

Until it gets fixed – and yes, it is a defect – here’s a custom task (MemberCaseTask) that you can add to your projects to catch it at build-time as opposed to already-in-staging-and-didn’t-think-to-navigate-to-each-view-after-precompilation-time.

Reference the custom Task in the project/solution script as follows:

   1: <UsingTask TaskName="MemberCaseTask"
   2:            AssemblyFile="C:\Users\admin\Documents\Visual Studio 2008\Projects\ClassLibrary1\ClassLibrary1\bin\Debug\ClassLibrary1.dll" /> 
   3:  <Target Name="AfterBuild">
   4:      <MemberCaseTask AssemblyPath="$(MSBuildProjectDirectory)\bin" />
   5:  </Target>  

Build this file and update the AssemblyFile reference above (note that we copy the original definition of GetNonPrivateFieldType from System.Web.UI.Util) :

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.Reflection;
   6: using System.IO;
   7: using Microsoft.Build.Framework;
   8: using Microsoft.Build.Utilities;
   9:  
  10: namespace ClassLibrary1
  11: {
  12:     public class MemberCaseTask: Task
  13:     {
  14:         [Required]
  15:         public string AssemblyPath { get; set; }
  16:  
  17:         public override bool Execute()
  18:         {
  19:             foreach (Assembly assembly in GetAllAssemblies())
  20:             {
  21:                 try
  22:                 {
  23:                     foreach (Type type in assembly.GetTypes())
  24:                         foreach (FieldInfo field in type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance))
  25:                             try
  26:                             {
  27:                                 GetNonPrivateFieldType(type, field.Name);
  28:                             }
  29:                             catch (Exception)
  30:                             {
  31:                                 Log.LogError(string.Format("{0} has a field conflict on field {1}.", type.Name, field.Name));
  32:                                 return false;
  33:                             }
  34:                 }
  35:                 catch (System.Reflection.ReflectionTypeLoadException)
  36:                 { }
  37:             }
  38:  
  39:             return true;
  40:         }
  41:  
  42:         private IEnumerable<Assembly> GetAllAssemblies()
  43:         {
  44:             foreach (FileInfo file in new DirectoryInfo(AssemblyPath).GetFiles("*.dll", SearchOption.TopDirectoryOnly))
  45:             {
  46:                 Assembly assembly = AppDomain.CurrentDomain.Load(AssemblyName.GetAssemblyName(file.FullName));
  47:                 yield return assembly;
  48:             }
  49:         }
  50:  
  51:         static Type GetNonPrivateFieldType(Type classType, string fieldName)
  52:         {
  53:             FieldInfo field = classType.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
  54:             if ((field != null) && !field.IsPrivate)
  55:             {
  56:                 return field.FieldType;
  57:             }
  58:             return null;
  59:         }
  60:     }
  61: }

References:

[1] - System.Reflection.AmbiguousMatchException: Ambiguous match found
http://dotnetdebug.net/2006/03/21/ambiguous-match-found-in-a-web-control-a-possible-bug/
http://weblogs.asp.net/pjohnson/archive/2006/08/11/Ambiguous-match-found.aspx
http://www.velocityreviews.com/forums/t153094-error-ambiguous-match-found.html

[2] - How to Write a Task:
http://msdn.microsoft.com/en-us/library/t9883dzc.aspx

[3] - MSBuild Reserved Properties:
http://msdn.microsoft.com/en-us/library/ms164309.aspx

6 comments:

Rahul Jain said...

Hey NARIMAN HAGHIGHI,

Can u please Share Your Sample Project With Us.



Thanks

north face outlet said...

Thank you for the informarion! nice post!sites are creating whole new ways for users to share and gain information.

WiseSolomon said...

how do you do this for an old asp.net web application? Create a custom task ?

John Grabanski said...

Here's a link to an MSDN article regarding custom tasks for MSBuild.

http://msdn.microsoft.com/en-us/magazine/cc163589.aspx

Hope this helps.

Anonymous said...

Thanks a ton for this fix, you're a rock star in my book!

Julio Negron said...

This code works great but it's locking the bin folder of the target project, preventing me from building my project. Any ideas why? I'm targeting an asp.net 4.5 web application using VS2013 Ultimate.