An even better way of handling a singleton WURFL in asp.net

by Mikael Henriksson 4. January 2010 20:28

As soon as I find a new cool tool I try to find ways of using it. I have been using StructureMap for a while and love it. Certainly so now that some of the old crap get’s cleaned up for version 3.0 (big thanks to Jeremy for taking the time during holidays). Ideally since it’s a 13 MB file to hold in memory I prefer to have only one instance running and my attempt on a singleton sucked. Initially I did some locking and stuff but when running locally I often found myself running out of ram so it didn’t work and it was slow to initialize (startup time was around a minute). After refactoring “a little” and letting StructureMap handle the instance for me it starts up in less than a second which means I can have IIS refresh the application pool regularly (I find it best to do this when handling really big files in memory) and I am confident someone like Jeremy D Miller writes better code than I do (for now).

This example can of course be refined a lot. It consists of just a few classes. One data class used to hold the xml document, one loader class used to prepare the data we need from the xml document and one navigator class used to search in the data class and one class that implements the below interface and acts as a wrapper for the other two classes.

Could I have some code please?

 

public interface IDetection
{
  string UserAgent { get; set; }
  Handset GetAllInformation(string userAgent);
  string GetBrowserAndVersion();
  string GetCharSet();
  string GetContentType();
  string GetManufacturer();
  string GetModel();
  string GetPlatform();
  string GetScreenSize();
  string GetUAProfile();
  bool IsMobileDevice();
}

 

Note: The Handset class is just a value object used to temporarily hold information. 

The only reason this interface exists is because I want to separate patches from devices. The patches contains information about non-mobile devices like bots and computers and the implementing classes are pretty dumb so I’ll save you the read copy/paste on those.

public interface IDeviceData
{
  SortedList<string, IDictionary<string, string>> DevicesAndCapabilities { get; }
  IDictionary<string, string> DevicesAndFallback { get; }
  IDictionary<string, string> UserAgentsAndDevices { get; }
}	

Now comes one of the main classes I removed all patch loading from this one to save some space. It’s somewhat optimized and I at least like the size of the methods. Managed to get them down to a decent number of lines.

///<summary>
/// Holds mobile _wurfl information from the _wurfl.xml
///</summary>
public class DeviceDataLoader
{
    private const int AproximateNumberOfDevices = 600;
    private static readonly Stopwatch Sw = new Stopwatch();
    private bool _disposed;

    public DeviceDataLoader()
    {
        CapabilityNames = new ArrayList();
        DeviceData = new DeviceData(AproximateNumberOfDevices);
        Patches = new Patches(40);
        CompactAgents = new Dictionary<string, string>();
    }

    ///<summary>
    /// Returns true if the Wurfl document is loaded into memory
    ///</summary>
    public bool IsLoaded { get; private set; }
    public IDeviceData DeviceData { get; private set; }
    public IDeviceData Patches { get; private set; }
    public IDictionary<string, string> CompactAgents { get; set; }
    public ArrayList CapabilityNames { get; private set; }

    /// <exception cref="FileNotFoundException"><c>FileNotFoundException</c>.</exception>
    public IDeviceData Load(string xmlPath)
    {
        if (!File.Exists(xmlPath))
        {
            throw new FileNotFoundException(string.Format(CultureInfo.InvariantCulture,
                                                          "The wurfl database was not located in : {0}", xmlPath));
        }

        PrepareXmlDatabaseForusage(xmlPath);
        IsLoaded = true;
        return DeviceData;
    }

    private void PrepareXmlDatabaseForusage(string xmlPath)
    {
        try
        {
            if (!IsLoaded)
            {
                XDocument wurlf = XDocument.Load(xmlPath);
                GetMobileDevices(wurlf.Descendants());
                SetInitialCapabilities();
            }
        }
        catch (Exception ex)
        {
            LogManager.GetLogger("Providers").Error(ex);
        }
    }

    private void GetMobileDevices(IEnumerable<XElement> xElements)
    {
        foreach (XElement xElement in xElements)
        {
            if (xElement.Name == "device")
            {
                string deviceId = GetDeviceId(xElement);
                string userAgent = GetUserAgent(xElement);
                IDictionary<string, string> capabilities = GetCapabilities(xElement);

                AddMobileDevicesCapabilities(deviceId, userAgent, capabilities);
                AddMobileUserAgentsAndDeviceIds(deviceId, userAgent);
                AddMobileDevicesAndFallbacks(xElement, deviceId);

                capabilities.Clear();
            }
        }
    }

    private void GetPatchDevices(IEnumerable<XElement> xElements)
    {
        foreach (XElement xElement in xElements)
        {
            if (xElement.Name == "device")
            {
                string deviceId = GetDeviceId(xElement);
                string userAgent = GetUserAgent(xElement);
                IDictionary<string, string> capabilities = GetCapabilities(xElement);

                AddPatchDevicesCapabilities(deviceId, userAgent, capabilities);
                AddPatchUserAgentsAndDevices(deviceId, userAgent);
                AddPatchDevicesAndFallbacks(xElement, deviceId);

                capabilities.Clear();
            }
        }
    }

    private void AddMobileDevicesAndFallbacks(XElement xElement, string deviceId)
    {
        string value;
        if (!DeviceData.DevicesAndFallback.TryGetValue(deviceId, out value))
        {
            XAttribute x = xElement.Attribute("fall_back");
            if (x != null)
            {
                DeviceData.DevicesAndFallback.Add(deviceId, x.Value);
            }
        }
    }

    private void AddMobileUserAgentsAndDeviceIds(string deviceId, string userAgent)
    {
        string value;
        if (!DeviceData.UserAgentsAndDevices.TryGetValue(userAgent, out value))
        {
            DeviceData.UserAgentsAndDevices.Add(userAgent, deviceId);
        }
    }

    private void AddMobileDevicesCapabilities(string deviceId, string userAgent,
                                              IDictionary<string, string> capabilities)
    {
        string value;
        if (!string.IsNullOrEmpty(deviceId))
        {
            if (!string.IsNullOrEmpty(userAgent) && !DeviceData.DevicesAndCapabilities.ContainsKey(deviceId))
            {
                DeviceData.DevicesAndCapabilities.Add(deviceId, new Dictionary<string, string>(capabilities));
            }
        }
    }

    private static string GetDeviceId(XElement xElement)
    {
        string deviceId = string.Empty;
        if (xElement.Name == "device")
        {
            deviceId = xElement.GetAttributeValue("id");
        }

        return deviceId;
    }

    private static string GetUserAgent(XElement xElement)
    {
        string userAgent = xElement.GetAttributeValue("user_agent") ?? string.Empty;

        if (string.IsNullOrEmpty(userAgent))
        {
            userAgent = "generic";
        }
        return userAgent;
    }

    private static IDictionary<string, string> GetCapabilities(XContainer xElement)
    {
        IDictionary<string, string> capabilities = new Dictionary<string, string>();
        foreach (XElement groups in xElement.Descendants("group"))
        {
            foreach (XElement capability in groups.Descendants("capability"))
            {
                string value;
                if (!capabilities.TryGetValue(capability.GetAttributeValue("name"), out value))
                {
                    capabilities.Add(capability.GetAttributeValue("name"), capability.GetAttributeValue("value"));
                }
            }
        }

        return capabilities;
    }

    private void SetInitialCapabilities()
    {
        DeviceData.DevicesAndCapabilities.TrimExcess();
        IDictionary<string, string> tmp = DeviceData.DevicesAndCapabilities["generic"];
        foreach (string key in tmp.Keys)
        {
            CapabilityNames.Add(key);
        }
        CapabilityNames.Sort();
    }
}


Lastly I created a detection class implementing the first interface. This will be the one instance in the IoC container of choice (mine is StructureMap).  

 

public class WurflDetection : IDetection
{
    private DataNavigator _device;
    public IDeviceData DeviceData { get; private set; }
    private DeviceDataLoader _loader;

    public void Initialize(string pathToXml)
    {
        if (string.IsNullOrEmpty(pathToXml))
        {
            throw new FileNotFoundException("PathToXml :", PathToXml);
        }

        if (_loader == null)
        {
            _loader = new DeviceDataLoader();
        }

        if (!_loader.IsLoaded)
        {
            DeviceData = _loader.Load(PathToXml);
            _device = new DataNavigator(DeviceData.Devices, DeviceData.Patches, DeviceData.CapabilityNames);
        }
    }
}

 

So I add a StructureMap Registry like follows:

 

public class WurflRegistry : Registry
{
  public WurflRegistry()
  {
    IDetectionProvider provider = new DeviceProviderOne(HostingEnvironment.MapPath("/App_Data/wurfl.xml"));

      ForSingletonOf<IDetectionProvider>()
          .Use(provider);
  }
}


The really nice thing about the whole registry approach is that I don’t necessarily need any project or dll references other than to StructureMap I can let the tool scan the assemblies in the output directory for me which is sometimes the only solution visible solutions to some problems. 

 

Tags: ,

IoC | wurfl

Thread safe .NET wurfl API (hopefully)

by Mikael Henriksson 29. April 2009 11:32

First of all I’d like to thank Luca Passani for all his great work with wurfl. He’s done a good job. The database is updated at least once a month and the community I suppose has done a good job with the different API’s. I can however not use the .NET API’s. They are not thread safe and I thought I’d share with you how I improved the old .NET API.

I started of with renaming pretty much everything since the names did not talk to me. We can call the in-memory wurfldatabase the WurflHolder and the class to navigate the holder is called WurflNavigator. The implementation details was changed a bit as well since it did not give me the correct results but that is a different blog post. Why did I make it a Singleton class you ask? Well I tried with a static class first but it caused issues all over the planet. I had people getting detected with a completely wrong mobile phone. Also the logs was totally not right. It basically cause the whole application to go wrong. I though maybe this is a good place for the Singleton pattern and I was right. I am not sure my Singleton implementation is completely thread safe but it is sufficient for now.

Let’s start with the Singleton instance:

public class Singleton<T> where T : class, new()
{
    protected Singleton()
    {

    }

    public static T Instance
    {
        get { return SingletonCreator<T>.CreatorInstance; }
    }

    private sealed class SingletonCreator<S> where S : class, new()
    {
        private static readonly S instance = new S();

        public static S CreatorInstance
        {
            get { return instance; }
        }
    }
}

I think the code speaks for itself. Basically if it’s not created I create it at runtime. The reason for making it generic is that I actually use it for two more classes. Then the API needs a couple of modifications, first in Global.asax we need to load the xml document:

Singleton<WurflHolder>.Instance.Load(HostingEnvironment.MapPath("/wurfl.xml"));

Then the default constructor for the Navigator needs to be changed to something like:

public WurlfNavigator()
{
    try
    {
        _wurflFileNotLoaded = Singleton<WurflHolder>.Instance.WurflFileNotLoaded;
        if (!Singleton<WurflHolder>.Instance.IsLoaded)
        {
            ThrowException(_wurflFileNotLoaded + ". Be sure that the creation of _wurfl have ran OK",
                           "prepareNavigatorModel");
        }
        _userAgentAndId = Singleton<WurflHolder>.Instance.UserAgentAndId;
        _idAndFallback = Singleton<WurflHolder>.Instance.IdAndFallback;
        _idAndCapabilities = Singleton<WurflHolder>.Instance.IdAndCapabilities;
        _capabilityNames = Singleton<WurflHolder>.Instance.CapabilityNames;
    }
    catch (Exception ex)
    {
        ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
        log.Error("WurlfNavigator", ex.InnerException);
    }
}

This totally stops all randomness in the entire website. maybe not the best thing to keep it in memory but hey, it’s better than total unreliability!

Tags: ,

C# | wurfl

About the author

Life architect specialized in programming