0
votes

in my WP7/8 app I get sometimes the following error message from my users (can't reproduce the issue here).

[Type]:[ArgumentException]
[ExceptionMessage]:[Value does not fall within the expected range.]
[StackTrace]:[
   at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
   at System.Collections.Generic.Dictionary`2.Insert(Date key, WorkDay value, Boolean add)
   at MyProject.Core.Data.WorkDayCache.GetWorkDay(Date date, Boolean returnCorrected)
   at MyProject.Core.Calculations.CalculationHelper.WorkTimeDay(Date date)
   at MyProject.WP.UI.InfoBoxes.IBWorkTimeToday.UpdateMinute()
   at MyProject.WP.UI.InfoBoxes.IBWorkTimeToday.Update()
   at MyProject.WP.UI.MainPage.UpdateInfoBoxes()
   at MyProject.WP.UI.MainPage.ButtonStart_Click(Object sender, RoutedEventArgs e)
   at System.Windows.Controls.Primitives.ButtonBase.OnClick()
   at System.Windows.Controls.Button.OnClick()
   at System.Windows.Controls.Primitives.ButtonBase.OnMouseLeftButtonUp(MouseButtonEventArgs e)
   at System.Windows.Controls.Control.OnMouseLeftButtonUp(Control ctrl, EventArgs e)
   at MS.Internal.JoltHelper.FireEvent(IntPtr unmanagedObj, IntPtr unmanagedObjArgs, Int32 argsTypeIndex, Int32 actualArgsTypeIndex, String eventName)
]
[InnerException]:[none]

Here is the code of the GetWorkDay method:

/// <summary>
/// Returns the cached work day for a given date
/// </summary>
/// <param name="date"></param>
/// <returns></returns>
public WorkDay GetWorkDay(Date date, bool returnCorrected = true)
{
    // return the cached value in case it is cached and the filter is disabled
    if (!TagFilter.IsFilterEnabled)
    {
        if (_cachedWorkDays.ContainsKey(date))
        {
            if (returnCorrected)
            {
                var correctedWorkDay = new TimeCalculatorInterface().GetCorrectedWorkDay(_cachedWorkDays[date]);
                return correctedWorkDay;
            }
            return _cachedWorkDays[date];
        }
    }

    // nothing cached, thus get the result and cache it
    var workDays = _databaseController.Wait().GetWorkDays(date, date, false);
    if (workDays != null && workDays.Count > 0)
    {
        if (!TagFilter.IsFilterEnabled)
            _cachedWorkDays.Add(date, workDays[0]);

        // correct the work day times with the break times if enabled
        if (returnCorrected)
        {
            var correctedWorkDay = new TimeCalculatorInterface().GetCorrectedWorkDay(workDays[0]);
            return correctedWorkDay;
        }

        return workDays[0];
    }

    return new WorkDay();
}

My main issue is that I don't understand what causes the exception. I've been under the impression for the last two days that this message just means that it tries to add a keyvaluepair into the dictionary where the key already exists. But this cache checks right before whether the key already exists and returns the cached value in this case. I wrote a couple of detailed unit tests with thousands of inserts and nothing happened.

What's odd in the stack trace is the fact that right after GetWorkDay() Dictionary2.Insert() is called. But all the stack traces I found that have a duplicate key issue call the Dictionary2.Add() before (which I actually do in the code because I can't call Insert() directly.

So is there anything I miss which might throw this exception?

Some more things to know:

_cachedWorkDays is the only dictionary there with key type Date and value type WorkDay

Date is my own implementation of a date (Needed some more methods for working with dates than DateTime provided me. Additionally, I wanted to make sure the time part in DateTime doesn't influence my date processings). As I use Date as key in the dictionary it requires an Equals and GetHashCode override which is as follows)

public static bool operator ==(Date d1, Date d2)
{
    return d1.Day == d2.Day && d1.Month == d2.Month && d1.Year == d2.Year;
}   

public override bool Equals(object obj)
{
    if (obj.GetType() == this.GetType())
    {
        Date obj1 = (Date)obj;
        return obj1 == this;
    }
    return false;
}

public override int GetHashCode()
{
    return (Year*100 + Month)*100 + Day;
}

Any help is highly appreciated.

Regards, Stephan

1
Typical error when an object is added with the same name as another of same type. Maybe the New Workday object?OneFineDay
WorkDay is the type of the values so it shouldn't matter whether the dictionary contains it twice or even more times as long as the corresponding keys are different. Also, I'm using _cachedWorkDays.ContainsKey(date) before adding the new value to make sure the key exists only once (the implementation is basically a simple cache). Or am I looking at the wrong line?Stephan
If your code is executed on parallel threads, you should consider to make it thread-safe, since at the moment it's not (and it could show that behavior in case of multithreading)NinjaCross
So the dictionary itself is not thread safe? In other words I need to make sure that while the ContainsKey and Add methods are executed no other thread is allowed to work on the same dictionary, right?Stephan
That is correct, as NinjaCross stated, you should wrap the entire function contents with "lock(_cachedWorkDays)"Jon

1 Answers

1
votes

Possible reason #1: concurrency issue. While thread A is executing _databaseController.Wait(), thread B has added that day to the cache, after the thread A wakes up you'll see exactly the same exception.

Possible reason #2: your Date class is mutable. Using mutable data types as keys will completely screw the dictionary. Please note System.DateTime is (a) immutable (b) struct (c) implements IEquatable<DateTime>.

P.S. Using dict.TryGetValue(..) API is more efficient than dict.ContainsKey(..) and subsequent dict[..]