Comment gérer à la fois un élément unique et un tableau pour la même propriété à l'aide de JSON.net

102

J'essaie de réparer ma bibliothèque SendGridPlus pour gérer les événements SendGrid, mais j'ai des problèmes avec le traitement incohérent des catégories dans l'API.

Dans l'exemple de charge utile suivant extrait de la référence de l'API SendGrid , vous remarquerez que la categorypropriété de chaque élément peut être une chaîne unique ou un tableau de chaînes.

[
  {
    "email": "[email protected]",
    "timestamp": 1337966815,
    "category": [
      "newuser",
      "transactional"
    ],
    "event": "open"
  },
  {
    "email": "[email protected]",
    "timestamp": 1337966815,
    "category": "olduser",
    "event": "open"
  }
]

Il semble que mes options pour créer JSON.NET comme celui-ci consistent à corriger la chaîne avant son entrée ou à configurer JSON.NET pour accepter les données incorrectes. Je préfère ne faire aucune analyse de chaîne si je peux m'en tirer.

Existe-t-il un autre moyen de gérer cela en utilisant Json.Net?

Robert McLaws
la source

Réponses:

205

La meilleure façon de gérer cette situation est d'utiliser une coutume JsonConverter.

Avant d'arriver au convertisseur, nous devons définir une classe dans laquelle désérialiser les données. Pour la Categoriespropriété qui peut varier entre un élément unique et un tableau, définissez-la en tant que List<string>et marquez-la avec un [JsonConverter]attribut afin que JSON.Net sache qu'il doit utiliser le convertisseur personnalisé pour cette propriété. Je recommanderais également d'utiliser des [JsonProperty]attributs afin que les propriétés des membres puissent recevoir des noms significatifs indépendamment de ce qui est défini dans le JSON.

class Item
{
    [JsonProperty("email")]
    public string Email { get; set; }

    [JsonProperty("timestamp")]
    public int Timestamp { get; set; }

    [JsonProperty("event")]
    public string Event { get; set; }

    [JsonProperty("category")]
    [JsonConverter(typeof(SingleOrArrayConverter<string>))]
    public List<string> Categories { get; set; }
}

Voici comment j'implémenterais le convertisseur. Notez que j'ai rendu le convertisseur générique afin qu'il puisse être utilisé avec des chaînes ou d'autres types d'objets selon les besoins.

class SingleOrArrayConverter<T> : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(List<T>));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JToken token = JToken.Load(reader);
        if (token.Type == JTokenType.Array)
        {
            return token.ToObject<List<T>>();
        }
        return new List<T> { token.ToObject<T>() };
    }

    public override bool CanWrite
    {
        get { return false; }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

Voici un court programme démontrant le convertisseur en action avec vos exemples de données:

class Program
{
    static void Main(string[] args)
    {
        string json = @"
        [
          {
            ""email"": ""[email protected]"",
            ""timestamp"": 1337966815,
            ""category"": [
              ""newuser"",
              ""transactional""
            ],
            ""event"": ""open""
          },
          {
            ""email"": ""[email protected]"",
            ""timestamp"": 1337966815,
            ""category"": ""olduser"",
            ""event"": ""open""
          }
        ]";

        List<Item> list = JsonConvert.DeserializeObject<List<Item>>(json);

        foreach (Item obj in list)
        {
            Console.WriteLine("email: " + obj.Email);
            Console.WriteLine("timestamp: " + obj.Timestamp);
            Console.WriteLine("event: " + obj.Event);
            Console.WriteLine("categories: " + string.Join(", ", obj.Categories));
            Console.WriteLine();
        }
    }
}

Et enfin, voici la sortie de ce qui précède:

email: [email protected]
timestamp: 1337966815
event: open
categories: newuser, transactional

email: [email protected]
timestamp: 1337966815
event: open
categories: olduser

Violon: https://dotnetfiddle.net/lERrmu

ÉDITER

Si vous devez aller dans l'autre sens, c'est-à-dire sérialiser, tout en gardant le même format, vous pouvez implémenter la WriteJson()méthode du convertisseur comme indiqué ci-dessous. (Assurez-vous de supprimer le CanWriteremplacement ou de le modifier en retour true, sinon WriteJson()il ne sera jamais appelé.)

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        List<T> list = (List<T>)value;
        if (list.Count == 1)
        {
            value = list[0];
        }
        serializer.Serialize(writer, value);
    }

Violon: https://dotnetfiddle.net/XG3eRy

Brian Rogers
la source
5
Parfait! Tu es l'homme. Heureusement, j'avais déjà fait toutes les autres choses sur l'utilisation de JsonProperty pour rendre les propriétés plus significatives. Merci pour une réponse incroyablement complète. :)
Robert McLaws
Aucun problème; heureux que vous ayez trouvé cela utile.
Brian Rogers
1
Excellent! C'est ce que je recherchais. @BrianRogers, si jamais vous êtes à Amsterdam, les boissons sont sur moi!
Mad Dog Tannen
2
@israelaltar Vous n'avez pas besoin d'ajouter le convertisseur à l' DeserializeObjectappel si vous utilisez l' [JsonConverter]attribut sur la propriété de liste dans votre classe, comme indiqué dans la réponse ci-dessus. Si vous n'utilisez pas l'attribut, alors, oui, vous devrez passer le convertisseur à DeserializeObject.
Brian Rogers
1
@ShaunLangley Pour faire l'utilisation du convertisseur un tableau au lieu d'une liste, changer toutes les références à List<T>dans le convertisseur T[]et le changement .Countà .Length. dotnetfiddle.net/vnCNgZ
Brian Rogers
6

Je travaillais là-dessus depuis des lustres, et merci à Brian pour sa réponse. Tout ce que j'ajoute, c'est la réponse vb.net!:

Public Class SingleValueArrayConverter(Of T)
sometimes-array-and-sometimes-object
    Inherits JsonConverter
    Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
        Throw New NotImplementedException()
    End Sub

    Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
        Dim retVal As Object = New [Object]()
        If reader.TokenType = JsonToken.StartObject Then
            Dim instance As T = DirectCast(serializer.Deserialize(reader, GetType(T)), T)
            retVal = New List(Of T)() From { _
                instance _
            }
        ElseIf reader.TokenType = JsonToken.StartArray Then
            retVal = serializer.Deserialize(reader, objectType)
        End If
        Return retVal
    End Function
    Public Overrides Function CanConvert(objectType As Type) As Boolean
        Return False
    End Function
End Class

puis dans votre classe:

 <JsonProperty(PropertyName:="JsonName)> _
 <JsonConverter(GetType(SingleValueArrayConverter(Of YourObject)))> _
    Public Property YourLocalName As List(Of YourObject)

J'espère que cela vous fera gagner du temps

subvention
la source
Fautes de frappe: <JsonConverter (GetType (SingleValueArrayConverter (Of YourObject)))> _ Public Property YourLocalName As List (Of YourObject)
GlennG
3

En tant que variante mineure de la grande réponse de Brian Rogers , voici deux versions modifiées de SingleOrArrayConverter<T>.

Tout d'abord, voici une version qui fonctionne pour tous List<T>pour chaque type Tqui n'est pas lui-même une collection:

public class SingleOrArrayListConverter : JsonConverter
{
    // Adapted from this answer https://stackoverflow.com/a/18997172
    // to /programming/18994685/how-to-handle-both-a-single-item-and-an-array-for-the-same-property-using-json-n
    // by Brian Rogers https://stackoverflow.com/users/10263/brian-rogers
    readonly bool canWrite;
    readonly IContractResolver resolver;

    public SingleOrArrayListConverter() : this(false) { }

    public SingleOrArrayListConverter(bool canWrite) : this(canWrite, null) { }

    public SingleOrArrayListConverter(bool canWrite, IContractResolver resolver)
    {
        this.canWrite = canWrite;
        // Use the global default resolver if none is passed in.
        this.resolver = resolver ?? new JsonSerializer().ContractResolver;
    }

    static bool CanConvert(Type objectType, IContractResolver resolver)
    {
        Type itemType;
        JsonArrayContract contract;
        return CanConvert(objectType, resolver, out itemType, out contract);
    }

    static bool CanConvert(Type objectType, IContractResolver resolver, out Type itemType, out JsonArrayContract contract)
    {
        if ((itemType = objectType.GetListItemType()) == null)
        {
            itemType = null;
            contract = null;
            return false;
        }
        // Ensure that [JsonObject] is not applied to the type.
        if ((contract = resolver.ResolveContract(objectType) as JsonArrayContract) == null)
            return false;
        var itemContract = resolver.ResolveContract(itemType);
        // Not implemented for jagged arrays.
        if (itemContract is JsonArrayContract)
            return false;
        return true;
    }

    public override bool CanConvert(Type objectType) { return CanConvert(objectType, resolver); }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        Type itemType;
        JsonArrayContract contract;

        if (!CanConvert(objectType, serializer.ContractResolver, out itemType, out contract))
            throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), objectType));
        if (reader.MoveToContent().TokenType == JsonToken.Null)
            return null;
        var list = (IList)(existingValue ?? contract.DefaultCreator());
        if (reader.TokenType == JsonToken.StartArray)
            serializer.Populate(reader, list);
        else
            // Here we take advantage of the fact that List<T> implements IList to avoid having to use reflection to call the generic Add<T> method.
            list.Add(serializer.Deserialize(reader, itemType));
        return list;
    }

    public override bool CanWrite { get { return canWrite; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var list = value as ICollection;
        if (list == null)
            throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), value.GetType()));
        // Here we take advantage of the fact that List<T> implements IList to avoid having to use reflection to call the generic Count method.
        if (list.Count == 1)
        {
            foreach (var item in list)
            {
                serializer.Serialize(writer, item);
                break;
            }
        }
        else
        {
            writer.WriteStartArray();
            foreach (var item in list)
                serializer.Serialize(writer, item);
            writer.WriteEndArray();
        }
    }
}

public static partial class JsonExtensions
{
    public static JsonReader MoveToContent(this JsonReader reader)
    {
        while ((reader.TokenType == JsonToken.Comment || reader.TokenType == JsonToken.None) && reader.Read())
            ;
        return reader;
    }

    internal static Type GetListItemType(this Type type)
    {
        // Quick reject for performance
        if (type.IsPrimitive || type.IsArray || type == typeof(string))
            return null;
        while (type != null)
        {
            if (type.IsGenericType)
            {
                var genType = type.GetGenericTypeDefinition();
                if (genType == typeof(List<>))
                    return type.GetGenericArguments()[0];
            }
            type = type.BaseType;
        }
        return null;
    }
}

Il peut être utilisé comme suit:

var settings = new JsonSerializerSettings
{
    // Pass true if you want single-item lists to be reserialized as single items
    Converters = { new SingleOrArrayListConverter(true) },
};
var list = JsonConvert.DeserializeObject<List<Item>>(json, settings);

Remarques:

  • Le convertisseur évite d'avoir à pré-charger la valeur JSON entière en mémoire sous forme de JTokenhiérarchie.

  • Le convertisseur ne s'applique pas aux listes dont les éléments sont également sérialisés en tant que collections, par exemple List<string []>

  • L' canWriteargument booléen passé au constructeur contrôle s'il faut re-sérialiser les listes à un seul élément en tant que valeurs JSON ou en tant que tableaux JSON.

  • Le convertisseur ReadJson()utilise le existingValueif pré-alloué afin de prendre en charge le remplissage des membres de la liste en lecture seule.

Deuxièmement, voici une version qui fonctionne avec d'autres collections génériques telles que ObservableCollection<T>:

public class SingleOrArrayCollectionConverter<TCollection, TItem> : JsonConverter
    where TCollection : ICollection<TItem>
{
    // Adapted from this answer https://stackoverflow.com/a/18997172
    // to /programming/18994685/how-to-handle-both-a-single-item-and-an-array-for-the-same-property-using-json-n
    // by Brian Rogers https://stackoverflow.com/users/10263/brian-rogers
    readonly bool canWrite;

    public SingleOrArrayCollectionConverter() : this(false) { }

    public SingleOrArrayCollectionConverter(bool canWrite) { this.canWrite = canWrite; }

    public override bool CanConvert(Type objectType)
    {
        return typeof(TCollection).IsAssignableFrom(objectType);
    }

    static void ValidateItemContract(IContractResolver resolver)
    {
        var itemContract = resolver.ResolveContract(typeof(TItem));
        if (itemContract is JsonArrayContract)
            throw new JsonSerializationException(string.Format("Item contract type {0} not supported.", itemContract));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        ValidateItemContract(serializer.ContractResolver);
        if (reader.MoveToContent().TokenType == JsonToken.Null)
            return null;
        var list = (ICollection<TItem>)(existingValue ?? serializer.ContractResolver.ResolveContract(objectType).DefaultCreator());
        if (reader.TokenType == JsonToken.StartArray)
            serializer.Populate(reader, list);
        else
            list.Add(serializer.Deserialize<TItem>(reader));
        return list;
    }

    public override bool CanWrite { get { return canWrite; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        ValidateItemContract(serializer.ContractResolver);
        var list = value as ICollection<TItem>;
        if (list == null)
            throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), value.GetType()));
        if (list.Count == 1)
        {
            foreach (var item in list)
            {
                serializer.Serialize(writer, item);
                break;
            }
        }
        else
        {
            writer.WriteStartArray();
            foreach (var item in list)
                serializer.Serialize(writer, item);
            writer.WriteEndArray();
        }
    }
}

Ensuite, si votre modèle utilise, par exemple, un ObservableCollection<T>pour certains T, vous pouvez l'appliquer comme suit:

class Item
{
    public string Email { get; set; }
    public int Timestamp { get; set; }
    public string Event { get; set; }

    [JsonConverter(typeof(SingleOrArrayCollectionConverter<ObservableCollection<string>, string>))]
    public ObservableCollection<string> Category { get; set; }
}

Remarques:

  • En plus des notes et restrictions pour SingleOrArrayListConverter, le TCollectiontype doit être en lecture / écriture et avoir un constructeur sans paramètre.

Démo violon avec les tests unitaires de base ici .

dbc
la source
0

J'ai eu un problème très similaire. Ma requête Json était totalement inconnue pour moi. Je savais seulement.

Il y aura un objectId et des paires de valeurs clés anonymes ET des tableaux.

Je l'ai utilisé pour un modèle EAV que j'ai fait:

Ma requête JSON:

{objectId ": 2," firstName ":" Hans "," email ": [" [email protected] "," [email protected] "]," name ":" Andre "," something ": [" 232 "," 123 "]}

Ma classe j'ai défini:

[JsonConverter(typeof(AnonyObjectConverter))]
public class AnonymObject
{
    public AnonymObject()
    {
        fields = new Dictionary<string, string>();
        list = new List<string>();
    }

    public string objectid { get; set; }
    public Dictionary<string, string> fields { get; set; }
    public List<string> list { get; set; }
}

et maintenant que je veux désérialiser des attributs inconnus avec sa valeur et ses tableaux, mon convertisseur ressemble à ça:

   public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        AnonymObject anonym = existingValue as AnonymObject ?? new AnonymObject();
        bool isList = false;
        StringBuilder listValues = new StringBuilder();

        while (reader.Read())
        {
            if (reader.TokenType == JsonToken.EndObject) continue;

            if (isList)
            {
                while (reader.TokenType != JsonToken.EndArray)
                {
                    listValues.Append(reader.Value.ToString() + ", ");

                    reader.Read();
                }
                anonym.list.Add(listValues.ToString());
                isList = false;

                continue;
            }

            var value = reader.Value.ToString();

            switch (value.ToLower())
            {
                case "objectid":
                    anonym.objectid = reader.ReadAsString();
                    break;
                default:
                    string val;

                    reader.Read();
                    if(reader.TokenType == JsonToken.StartArray)
                    {
                        isList = true;
                        val = "ValueDummyForEAV";
                    }
                    else
                    {
                        val = reader.Value.ToString();
                    }
                    try
                    {
                        anonym.fields.Add(value, val);
                    }
                    catch(ArgumentException e)
                    {
                        throw new ArgumentException("Multiple Attribute found");
                    }
                    break;
            }

        }

        return anonym;
    }

Alors maintenant, chaque fois que j'obtiens un AnonymObject, je peux parcourir le dictionnaire et à chaque fois qu'il y a mon drapeau "ValueDummyForEAV" je passe à la liste, lis la première ligne et divise les valeurs. Après cela, je supprime la première entrée de la liste et continue avec l'itération du dictionnaire.

Peut-être que quelqu'un a le même problème et peut l'utiliser :)

Cordialement Andre

André Fritzsche
la source
0

Vous pouvez utiliser un JSONConverterAttributecomme trouvé ici: http://james.newtonking.com/projects/json/help/

En supposant que vous avez une classe qui ressemble à

public class RootObject
{
    public string email { get; set; }
    public int timestamp { get; set; }
    public string smtpid { get; set; }
    public string @event { get; set; }
    public string category[] { get; set; }
}

Vous décoreriez la propriété de catégorie comme on le voit ici:

    [JsonConverter(typeof(SendGridCategoryConverter))]
    public string category { get; set; }

public class SendGridCategoryConverter : JsonConverter
{
  public override bool CanConvert(Type objectType)
  {
    return true; // add your own logic
  }

  public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
  {
   // do work here to handle returning the array regardless of the number of objects in 
  }

  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
  {
    // Left as an exercise to the reader :)
    throw new NotImplementedException();
  }
}
Tim Gabrhel
la source
Merci pour cela, mais cela ne résout toujours pas le problème. Lorsqu'un tableau réel entre, il génère toujours une erreur avant que mon code ne puisse même s'exécuter pour un objet qui a un tableau réel. 'Informations supplémentaires: jeton inattendu lors de la désérialisation de l'objet: String. Chemin «[2] .category [0]», ligne 17, position 27. »
Robert McLaws
+ "\" événement \ ": \" traité \ ", \ n" + "} \ n" + "]";
Robert McLaws
Il a traité le premier objet très bien et n'a traité aucun tableau magnifiquement. Mais lorsque j'ai créé un tableau pour le 2ème objet, cela a échoué.
Robert McLaws
@AdvancedREI Sans voir votre code, je suppose que vous laissez le lecteur mal positionné après avoir lu le JSON. Au lieu d'essayer d'utiliser directement le lecteur, il est préférable de charger un objet JToken depuis le lecteur et de partir de là. Voir ma réponse pour une implémentation fonctionnelle du convertisseur.
Brian Rogers
Beaucoup plus de détails dans la réponse de Brian. Utilisez celui-là :)
Tim Gabrhel
0

Pour gérer cela, vous devez utiliser un JsonConverter personnalisé. Mais vous aviez probablement déjà cela à l'esprit. Vous recherchez simplement un convertisseur que vous pouvez utiliser immédiatement. Et cela offre plus qu'une simple solution à la situation décrite. Je donne un exemple avec la question posée.

Comment utiliser mon convertisseur:

Placez un attribut JsonConverter au-dessus de la propriété. JsonConverter(typeof(SafeCollectionConverter))

public class SendGridEvent
{
    [JsonProperty("email")]
    public string Email { get; set; }

    [JsonProperty("timestamp")]
    public long Timestamp { get; set; }

    [JsonProperty("category"), JsonConverter(typeof(SafeCollectionConverter))]
    public string[] Category { get; set; }

    [JsonProperty("event")]
    public string Event { get; set; }
}

Et voici mon convertisseur:

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;

namespace stackoverflow.question18994685
{
    public class SafeCollectionConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return true;
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            //This not works for Populate (on existingValue)
            return serializer.Deserialize<JToken>(reader).ToObjectCollectionSafe(objectType, serializer);
        }     

        public override bool CanWrite => false;

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }
}

Et ce convertisseur utilise la classe suivante:

using System;

namespace Newtonsoft.Json.Linq
{
    public static class SafeJsonConvertExtensions
    {
        public static object ToObjectCollectionSafe(this JToken jToken, Type objectType)
        {
            return ToObjectCollectionSafe(jToken, objectType, JsonSerializer.CreateDefault());
        }

        public static object ToObjectCollectionSafe(this JToken jToken, Type objectType, JsonSerializer jsonSerializer)
        {
            var expectArray = typeof(System.Collections.IEnumerable).IsAssignableFrom(objectType);

            if (jToken is JArray jArray)
            {
                if (!expectArray)
                {
                    //to object via singel
                    if (jArray.Count == 0)
                        return JValue.CreateNull().ToObject(objectType, jsonSerializer);

                    if (jArray.Count == 1)
                        return jArray.First.ToObject(objectType, jsonSerializer);
                }
            }
            else if (expectArray)
            {
                //to object via JArray
                return new JArray(jToken).ToObject(objectType, jsonSerializer);
            }

            return jToken.ToObject(objectType, jsonSerializer);
        }

        public static T ToObjectCollectionSafe<T>(this JToken jToken)
        {
            return (T)ToObjectCollectionSafe(jToken, typeof(T));
        }

        public static T ToObjectCollectionSafe<T>(this JToken jToken, JsonSerializer jsonSerializer)
        {
            return (T)ToObjectCollectionSafe(jToken, typeof(T), jsonSerializer);
        }
    }
}

Que fait-il exactement? Si vous placez l'attribut convertisseur, le convertisseur sera utilisé pour cette propriété. Vous pouvez l'utiliser sur un objet normal si vous attendez un tableau json avec 1 ou aucun résultat. Ou vous l'utilisez sur un IEnumerableoù vous attendez un objet json ou un tableau json. (Sachez qu'un array- object[]- est un IEnumerable) Un inconvénient est que ce convertisseur ne peut être placé qu'au-dessus d'une propriété car il pense pouvoir tout convertir. Et soyez prévenu . A stringest également un IEnumerable.

Et il offre plus qu'une réponse à la question: si vous recherchez quelque chose par identifiant, vous savez que vous obtiendrez un tableau avec un ou aucun résultat. La ToObjectCollectionSafe<TResult>()méthode peut gérer cela pour vous.

Ceci est utilisable pour Single Result vs Array à l'aide de JSON.net et gère à la fois un seul élément et un tableau pour la même propriété et peut convertir un tableau en un seul objet.

J'ai fait cela pour les demandes REST sur un serveur avec un filtre qui renvoyait un résultat dans un tableau mais je voulais récupérer le résultat sous la forme d'un seul objet dans mon code. Et aussi pour une réponse de résultat OData avec un résultat étendu avec un élément dans un tableau.

Aie du plaisir avec ça.

Roberto B
la source
-2

J'ai trouvé une autre solution qui peut gérer la catégorie sous forme de chaîne ou de tableau en utilisant un objet. De cette façon, je n'ai pas besoin de gâcher le sérialiseur json.

Jetez-y un œil si vous avez le temps et dites-moi ce que vous en pensez. https://github.com/MarcelloCarreira/sendgrid-csharp-eventwebhook

Il est basé sur la solution à https://sendgrid.com/blog/tracking-email-using-azure-sendgrid-event-webhook-part-1/ mais j'ai également ajouté la conversion de date à partir de l'horodatage, mis à jour les variables pour refléter modèle SendGrid actuel (et fait fonctionner les catégories).

J'ai également créé un gestionnaire avec l'authentification de base en option. Voir les fichiers ashx et les exemples.

Je vous remercie!

MarcelloCarreira
la source