Comment savoir quelle clé n'a pas été trouvée?

J'ai une classe Configuration qui contient un certain nombre de paramètres. Un exemple simple:

class Configuration
{
    public string Name { get; set; }
    public string Url { get; set; }
    public string Password { get; set; }
    //There are about 20 of these
}

Je souhaite offrir à l'appelant la possibilité de renseigner cet objet à partir d'un dictionnaire de chaînes, comme suit:

static Configuration CreateFromDictionary(Dictionary dict)
{
    try
    {
        return new Configuration
        {
            Name     = dict["Name"],
            Url      = dict["Url"],
            Password = dict["Password"]
        }
    }
    catch(KeyNotFoundException exception)
    {
        throw new ArgumentException("Unable to construct a Configuration from the information given.");
    }
}

Cela fonctionne bien, sauf que c'est une conversion tout ou rien. Si l'appelant fournit un dictionnaire généralement correct, mais que l'une des entrées est mal orthographiée, la conversion échoue avec une exception.

J'aimerais pouvoir fournir un meilleur message d'exception indiquant à l'appelant quelle clé n'a pas été trouvée. Cela semble important. Mais je ne parviens pas à récupérer ces informations à partir de KeyNotFoundException .

Je pourrais écrire du code pour analyser le dictionnaire une ligne à la fois et vérifier chaque clé individuellement, mais cela me semble très pénible. Existe-t-il un moyen de savoir quelle clé n'a pas été trouvée à partir des informations sur l'exception ou sans rechercher les clés ligne par ligne avec ContainsKey ou TryGetValue ?

0
collecter les valeurs au début.
ajouté l'auteur Daniel A. White, source

7 Réponses

Unfortunately the exception 'KeyNotFoundException' thrown from indexer in Dictionary<,> doesn't provide the 'key' value in error message.

Vous trouverez ci-dessous la méthode d'extension générique que vous pouvez utiliser pour obtenir une exception détaillée. Également dans votre code dans le bloc catch, vous avalez l'exception. Au lieu de le mettre dans InnerException afin qu'il soit enregistré correctement.

static Configuration CreateFromDictionary(Dictionary dict)
{
            try
    {
        return new Configuration
        {
            Name = dict.GetValue("Name"),
            Url = dict.GetValue("Url"),
            Password = dict.GetValue("Password")
        }
    }
    catch (KeyNotFoundException ex)
    {
        throw new ArgumentException("Unable to construct a Configuration from the information given.", ex);
    }
 }

public static class ExtensionsUtil
{
    public static Tvalue GetValue(this Dictionary dict, TKey key)
    {
        Tvalue val = default(Tvalue);
        if (dict.TryGetValue(key, out val))
        {
            return val;
        }
        else
        {
            throw new KeyNotFoundException($"'{key}' not found in the collection.");
        }
    }
}
1
ajouté

Ma solution jusqu'ici consiste à utiliser une méthode d'extension:

public static T ParseFor(this IDictionary source, string key)
{
    string val;
    if (!source.TryGetValue(key, out val)) throw new KeyNotFoundException(string.Format("An entry for '{0}' was not found in the dictionary", key));
    try
    {
        return (T)Convert.ChangeType(val, typeof(T));
    }
    catch
    {
        throw new FormatException(string.Format("The value for '{0}' ('{1}') was not in a correct format to be converted to a {2}", key, val, typeof(T).Name));
    }
}

Et utilisez-le comme ceci:

return new Configuration
    {
        Name     = dict.ParseFor("Name"),
        Url      = dict.ParseFor("Url"),
        Password = dict.PasreFor("Password")
    }

La méthode d'extension lève une exception utile contenant le nom de la clé. A également l'avantage de prendre en charge la conversion de type, par ex. si l'un des éléments de configuration est un bool , il tentera de le convertir.

0
ajouté
Et si deux clés sont mal orthographiées?
ajouté l'auteur Sergey Berezovskiy, source
Votre méthode générique ne fonctionnera que sur Dictionary . De plus, vous avalez l’exception levée à partir de la méthode Extension. Voir ma réponse ci-dessous.
ajouté l'auteur vendettamit, source

La façon dont j'ai résolu ce problème dans le passé consiste à envelopper le vrai dictionnaire:

public class MyDictionary : IDictionary
{
    public MyDictionary(IDictionary realDictionary)
    {
         _dictionary = realDictionary;
    }

    ...

    public TValue this[TKey key]
    {
        get
        {
            try
            {
                return _dictionary[key];
            }
            catch (KeyNotFoundException e)
            {
                throw new KeyNotFoundException($"Key {key} is not in the dictionary", e);
            }
        }
        ...
    }
    ...
}

Malheureusement, l'exception intégrée ne fournit simplement pas les informations.

0
ajouté

Vous pouvez écrire un générateur qui trouvera les propriétés de type 'chaîne' existant dans votre type Configuration , puis effectuera une boucle sur ces propriétés en essayant d'extraire chacune d'elles d'un dictionnaire donné. Ce code même peut être générique:

public T Build(Dictionary source)
    where T : new()
{
    var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty;
    var properties = typeof(T).GetProperties(flags)
         .Where(p => p.PropertyType == typeof(string));

    var missingKeys = properties.Select(p => p.Name).Except(source.Keys);
    if (missingKeys.Any())
        throw new FooException($"These keys not found: {String.Join(", ", missingKeys)}");

    var obj = new T();

    foreach (var p in properties)
        p.SetValue(obj, source[p.Name]);

    return obj;
}

Usage:

// throws FooException if any key  is missing
var config = Something.Build(dict); 

Mais généralement, les configurations ne contiennent pas de chaînes.

  • Que faire si URL n'est pas valide uri?
  • Que faire si Nom est vide?
  • Que se passe-t-il si mot de passe est null dans le dictionnaire?
  • Que faire si un paramètre numérique n'est pas une chaîne analysable? Etc.

Dans tous ces cas, votre code sera satisfait et vous obtiendrez une exception lors de l'exécution. Les choses deviennent bien meilleures si vous utilisez des types spécifiques pour vos paramètres chaîne , Uri , int , DateTime , etc. Et validé ces valeurs dès que possible. En outre, certains paramètres sont généralement facultatifs - vous ne devez pas les lancer si ce paramètre est absent du dictionnaire ou de la source que vous utilisez.

public class Configuration
{
    public string Name { get; set; }
    public Uri Url { get; set; }
    public int RequiredNumber { get; set; }
    public int? NotRequiredNumber { get; set; }
}

Ensuite, vous pouvez créer un ensemble de méthodes d'extension qui essaient d'obtenir et d'analyser les valeurs du dictionnaire:

public static Uri GetUri(this Dictionary source, string key)
{
    if (!source.TryGetValue(key, out string value))
        throw new ConfigurationException($"{key} not found");

    if (!Uri.TryCreate(value, UriKind.Absolute, out Uri result))
        throw new ConfigurationException($"{key} is not a valid uri: {value}");

    return result;
}

Et créer une configuration ressemblera à

var config = new Configuration {
    Name = dict.GetString("Name"),
    Url = dict.GetUri("Uri"),
    RequiredNumber = dict.GetInt("RequiredNumber"),
    NotRequiredNumber = dict.GetNullableInt("NotRequiredNumber")
};

Votre code échouera rapidement si quelque chose est mal configuré. Et vous serez en sécurité si la configuration est créée.

0
ajouté

Voici ce que vous pouvez faire.

  static Configuration CreateConfiguration(Dictionary dict)
    {
        try
        {
            return
                new Configuration
                {
                    FirstName = dict.getValue("FirstName"),
                    LastName = dict.getValue("LastName"),
                    ID = dict.getValue("ID")                        
                };
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }

    public static class Extension
{
    public static string getValue(this Dictionary dict, string keyName)
    {
        string data = null;
        var result = dict.TryGetValue(keyName, out data);
        if (!result)
            throw new KeyNotFoundException($"The given key '{keyName}' was not present in the dictionary. keyname is not found");
        return data;
    }
}
0
ajouté

Vous pouvez stocker les clés avec leurs mappages dans un dictionnaire et valider l'entrée avant de mapper les valeurs:

public static Configuration CreateFromDictionary(Dictionary dict)
{
    var mappings = new Dictionary>
    {
        [nameof(Name)] = (x, value) => x.Name = value,
        [nameof(Url)] = (x, value) => x.Url = value,
        [nameof(Password)] = (x, value) => x.Password = value,
    };

    var missingKeys = mappings.Keys
        .Except(dict.Keys)
        .ToArray();
    if (missingKeys.Any())
        throw new KeyNotFoundException("The given keys are missing: " + string.Join(", ", missingKeys));

    return mappings.Aggregate(new Configuration(), (config, mapping) =>
    {
        mapping.Value(config, dict[mapping.Key]);
        return config;
    });
}
0
ajouté

Utilisez la méthode "TryGetValue" du dictionnaire et abandonnez le gestionnaire de captures. Le résultat de TryGetValue sera false si la clé n'est pas trouvée ou true si elle l'est. Il y a un paramètre "out" où la valeur sera, si true est renvoyé. Au lieu de votre gestionnaire d'attrape, il vous suffit de garder trace des clés non présentes (à nouveau indiqué par la valeur de retour false de TryGetValue).

Exemple rapide:

string name;
Configuration config = new Configuration();
if (dict.TryGetValue("Name", out name))
{
    config.Name = name;
}
else
{
    throw new ArgumentException("'Name' was not present.");
}

MODIFIER: À la lumière des éclaircissements apportés à la question, je vais modifier ma réponse à la question suivante: Non, il n’est pas possible d’obtenir cette information à partir de l’exception.

0
ajouté
Je pense que vous manquez l'esprit de la question. Bien sûr, je pourrais vérifier les clés une par une et je sais comment faire. La question est de savoir si c'est nécessaire.
ajouté l'auteur John Wu, source
@JohnWu Je suppose que vous avez raison, j'ai dû manquer à l'esprit de la question et je pense que je le suis toujours.
ajouté l'auteur mars-red, source