Я в основном пытаюсь кэшировать некоторые организации в словаре с их идентификатором и именем. Постоянно получаю ошибку: Item with Same Key has already been added. Для меня это странно, поскольку первый блок if проверяет, содержит ли словарь ключ, и выходит из метода, если он есть.
Почему я получаю сообщение об ошибке, учитывая (упрощенный) код ниже?
public async Task<IList<Stat>> GetStats(DateTime start, DateTime end, string role, long tenantId)
{
...
var tasks = new List<Task>();
foreach (var org in query)
tasks.Add(GetOrgName(org));
await Task.WhenAll(tasks);
return query;
}
private Dictionary<string, string> Orgs = new Dictionary<string, string>();
private async Task GetOrgName(Organization org)
{
if (Orgs.ContainsKey(org.Id))
{
org.Name = Orgs[org.Id];
return;
}
var result = await _directoryService.GetOrganization(org.Id);
if (result != null)
org.Name = result.DisplayName;
else
org.Name = org.Id;
Orgs.Add(org.Id, org.Name);
}
Возможно, несколько одновременных вызовов GetOrgName с одного и того же Organization?
Вы работаете в многопоточной среде?
@Ilian: Может быть, ты что-то понял. Я добавил в пример еще код. Метод вызывается с помощью Task.WhenAll
что-то делать с учетом регистра /





Этот код выглядит опасным
if (org != null)
org.Name = org.DisplayName;
else
org.Name = org.Id;
Если org == null, я думаю, ваш пункт else вызовет исключение NullReference. Но ты этого не понимаешь
Проблема может быть в этой функции
var org = await _directoryService.GetOrganization(org.Id);
Обратите внимание, что эта команда означает, что org может быть изменен, и она может не содержать исходный Id.
Например: если это никогда не возвращает нулевое значение, просто пустой объект. Вы можете получить исключение Item with Same Key has already been added.
Думаю, это можно исправить двойной проверкой
if (!Orgs.ContainsKey(org.Id))
{
Orgs.Add(org.Id, org.Name);
}
У меня была опечатка в части org != null. Должна была быть другая переменная. Моя ошибка. :) Только что внес правки.
Поскольку к словарю обращаются несколько потоков, следует использовать ConcurrentDictionary вместо Dictionary. В противном случае может случиться так, что два потока попытаются добавить один и тот же ключ одновременно, что приведет к упомянутому вами исключению. Более того, Dictionary не является потокобезопасным и может быть поврежден при обращении к нескольким потокам.
Сделайте поиск по словарю и добавление к регистру без учета регистра. Вы можете сослаться на следующее:
private async Task GetOrgName(Organization org)
{
if (Orgs.ContainsKey(org.Id.ToUpper()))
{
org.Name = Orgs[org.Id.ToUpper()];
return;
}
var result = await _directoryService.GetOrganization(org.Id.ToUpper());
if (result != null)
org.Name = result.DisplayName;
else
org.Name = org.Id;
Orgs.Add(org.Id.ToUpper(), org.Name);
}
Чтобы сделать словарь с учетом регистра, лучше использовать new Dictionary<A, B, StringComparer.OrdinalIgnoreCase) (или, в зависимости от варианта использования, любые другие средства сравнения строк). Почему? 1. о вызове ToUpper легко забыть, 2. Остерегайтесь Индейка Тест
Я просто хотел указать, что он должен сделать регистр нечувствительным. Есть несколько способов добиться этого. Спасибо, что предложили лучший вариант.
С вашим последним редактированием похоже, что это не многопоточная проблема, а просто проблема с несколькими асинхронными вызовами - вы, скорее всего, ожидаете и выполняете несколько запросов к GetOrganization для одного и того же org.Id, когда первый еще не завершен (так что словарь еще не обновлялся).
Так что ConcurrentDictionary вам, скорее всего, не понадобится. Вместо этого вы можете сделать что-то вроде следующего:
private Dictionary<string, Task> Orgs = new Dictionary<string, Task>();
private Task GetOrgName(Organization org)
{
Task nameTask;
if (!Orgs.TryGetValue(org.Id, out nameTask)
{
nameTask = GetOrganization(.org);
Orgs.Add(org.Id, nameTask);
}
return nameTask;
}
private async Task GetOrganization(Organization org)
{
// Consider using .ConfigureAwait(false) here...
var result = await _directoryService.GetOrganization(org.Id);
if (result != null)
org.Name = result.DisplayName;
else
org.Name = org.Id;
}
Обратите внимание на подпись для Dictionary.
Проблема в том, что к словарю одновременно обращаются несколько задач. Итак, пока одна задача проверяет, существует ли имя, другая его добавляет. Кроме того, поскольку словарь не является потокобезопасным, вы можете столкнуться со случайными исключениями с вашей текущей реализацией.
Если вы измените это:
var tasks = new List<Task>();
foreach (var org in query)
tasks.Add(GetOrgName(org))
к:
foreach (var org in query)
await GetOrgName(org);
У тебя не будет этой проблемы.
Другой вариант - использовать потокобезопасную коллекцию для кеша.
Но если запрос содержит, скажем, 1000 элементов, ваш код может сделать тысячи одновременных вызовов _directoryService.GetOrganization. Что может быть проблемой?
Интересный. Я уже начинал этот путь раньше, но казалось, что foreach игнорирует ожидание. Я, должно быть, смешиваю свой js с моим C#.
Вы работаете в многопоточной среде?
Dictionaryне является потокобезопасным.