Scott Hanselman

Update on the dasBlog Turkish-I bug and a reminder to me on Globalization

June 5, '05 Comments [5] Posted in ASP.NET | DasBlog | Internationalization | Bugs
Sponsored By

I had blogged earlier about a bug in dasBlog that affected Turkish users. When a Turkish browser reported an HTTP Accept-Language header indicating Turkish as the preferred language, no blog posts would show up.  As fix, I suggested that users change their blog templates, but I knew that wasn't an appropriate fix.

For background, here's what an Accept-Language header looks like. This user would prefer Turkish, but would take English as their second choice.

GET /DasBlog/default.aspx HTTP/1.1
Accept-Language: tr,en-us;q=0.5
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.0.3705; .NET CLR 1.1.4322; Tablet PC 1.7; .NET CLR 2.0.50215)

Why would the browser affect the underlying engine you say? Here's some background before I show you how I fixed the bug. Typically globalized ASP.NET applications set the current thread's Culture and UICulture to a specific culture based on the user's preferences. 

For example:

CultureInfo Turkey = CultureInfo.CreateSpecificCulture("tr");
Thread.CurrentThread.CurrentCulture = Turkey;
           
Thread.CurrentThread.CurrentUICulture = Turkey;

Why two properties? From the MSDN Documentation:

The CurrentCulture property's default value is the system's User Locale, which is set in the Regional Options control panel. The CurrentUICulture property's default value is the system's UI Language, which is the language of your system UI. On Windows 2000 and Windows XP MultiLanguage Edition, the CurrentUICulture defaults to the current user UI language settings.

The CurrentUICulture is used by the ResourceManager to look up resources like strings at run time. The CurrentCulture is set per-thread also, but is used for formatting dates, times, currencies and string manipulation methods.

Earlier I blogged that I suspected a problem with the Reflection method we were using to invoke Macros in dasBlog. We were passed in "items" from a dasBlog tempate and were looking for a property called "Items." We used a reflection method like this:

MemberInfo[] members = subexObject.GetType().FindMembers(

    MemberTypes.Field|MemberTypes.Method|MemberTypes.Property,

    BindingFlags.IgnoreCase|BindingFlags.Instance|BindingFlags.Public,

    new MemberFilter(this.IsMemberEligibleForMacroCall), subex.Trim() );

if ( members.Length == 0 )

{

    throw new MissingMemberException(subexObject.GetType().FullName,subex.Trim());

}

Notice that we're indicating via the BindingFlags.IgnoreCase flag that we want the FindMembers() method to handle the case-insensitivity issue for us. My initial thought was that they must be doing something wrong inside. I mentioned this to Michael Kaplan, but he said there was a big push to fix "Turkish-I" bugs in the 1.1 timeframe. However, he said he'd look at the problem and see if it would need to be fixed if I had a simple repro to prove it was still a problem in 2.0. At this point, I started thinking that it's probably NOT Microsoft, but as I started writing a small repro as a separate project, I was thinking, we (dasBlog) MUST be doing something wrong, otherwise how have the Turks been doing reflection all this time? They'd have run into this before. I googled and couldn't find any Turks suffering the slings and arrows of reflection.

I went back and looked at the code again with fresh eyes. Then I noticed (brain fart here):

MemberInfo[] members = subexObject.GetType().FindMembers(

    MemberTypes.Field|MemberTypes.Method|MemberTypes.Property,

    BindingFlags.IgnoreCase|BindingFlags.Instance|BindingFlags.Public,

    new MemberFilter(this.IsMemberEligibleForMacroCall), subex.Trim() );

if ( members.Length == 0 )

{

    throw new MissingMemberException(

        subexObject.GetType().FullName,subex.Trim());

}

I had been glossing over that. We are/were passing in a delegate for filtering. That method looked like this:

private bool IsMemberEligibleForMacroCall(MemberInfo m, object filterCriteria )

{

    return String.Compare(m.Name,(string)filterCriteria,true)==0;

}

Doh! That's a culture-sensitive string compare. We were comparing the MemberInfo that we got back from the reflection call with the string we knew we needed. Even though the Reflection call to FindMembers succeeded, we were filtering out our method with a bad compare.

Here's where the lesson comes in (and this had bitten me before, so I was kicking myself).

Scott's Rule Number 0x5F: Think about your string compares and their context. Make sure you've expressed your true intent correctly.

Here is the fixed method. It'll be in the next dasBlog point release and won't require anyone to change their templates.

private bool IsMemberEligibleForMacroCall(MemberInfo m, object filterCriteria )

{

    //This has to be case-insensitive and culture-invariant or

    // "Item" and "item" won't match if the current culture is Turk

    return String.Compare(m.Name,(string)filterCriteria, true, System.Globalization.CultureInfo.InvariantCulture) == 0;

}

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by ORCS Web
Sunday, June 05, 2005 6:38:48 PM UTC
Now that's the type of thing FxCop could have helped with, right?
Sunday, June 05, 2005 7:43:40 PM UTC
Oh, he may be right -- there are FxCop rules to check this sort of thing.

Of course if you look at the info in the latest string guidelines, it points out that you may want to use something other than 'Invariant' here. :-)

We'll be talking here soon, I would imagine....
Monday, June 06, 2005 3:02:36 AM UTC
Cool. Just one thing: is is possible to add some padding between the code and the border? Just curious. :P
Janus
Tuesday, June 07, 2005 8:05:30 AM UTC
Would the Type class's filter delegate Type.FilterNameIgnoreCase have done the trick for you?
mikeb
Wednesday, June 08, 2005 1:29:11 PM UTC
We experienced the same problem a few months back and ended up doing the same thing you were doing with CultureInfo.InvariantCulture.

There is a related KB article
PRB: Letter "i" Problems When You Use XML Encoding with Turkish Settings
http://support.microsoft.com/default.aspx?scid=kb;en-us;317505
xjin
Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.