53 votes

Formulaires multipart depuis un client C#

Je cherche à remplir un formulaire dans une application php depuis un client C# (Outlook addin). J'ai utilisé Fiddler pour voir la requête d'origine à partir de l'application php et le formulaire est transmis en tant que multipart/form. Malheureusement, .Net ne propose pas de support natif pour ce type de formulaires (WebClient dispose uniquement d'une méthode pour téléverser un fichier). Est-ce que quelqu'un connaît une bibliothèque ou a du code pour accomplir cela? Je veux envoyer différents valeurs et éventuellement (mais seulement parfois) un fichier.

Merci pour votre aide, Sebastian

0 votes

Cela fonctionne comme un charme www.briangrinstead.com/blog

0 votes

Si vous n'êtes pas contre une petite dépendance de bibliothèque, Flurl rend cela aussi simple que possible. [Avis de non-responsabilité : Je suis l'auteur]

72voto

Brian Grinstead Points 2248

Merci pour les réponses, tout le monde! J'ai récemment dû faire fonctionner cela, et j'ai beaucoup utilisé vos suggestions. Cependant, il y a eu quelques parties délicates qui n'ont pas fonctionné comme prévu, principalement en ce qui concerne l'inclusion du fichier (ce qui était une partie importante de la question). Il y a déjà beaucoup de réponses ici, mais je pense que cela pourrait être utile à quelqu'un à l'avenir (je n'ai pas trouvé beaucoup d'exemples clairs en ligne). J'ai écrit un article de blog qui l'explique un peu plus.

En gros, j'ai d'abord essayé de passer les données du fichier en tant que chaîne encodée en UTF8, mais j'avais des problèmes avec l'encodage des fichiers (cela fonctionnait bien pour un fichier texte brut, mais lors de l'envoi d'un document Word, par exemple, si j'essayais d'enregistrer le fichier qui était transmis dans le formulaire envoyé utilisant Request.Files[0].SaveAs(), l'ouverture du fichier dans Word ne fonctionnait pas correctement. J'ai trouvé que si vous écrivez les données du fichier directement en utilisant un Stream (plutôt qu'un StringBuilder), cela fonctionnait comme prévu. De plus, j'ai apporté quelques modifications qui m'ont facilité la compréhension.

Par ailleurs, le Request for Comments sur les formulaires multipartes et la Recommandation de la W3C pour multipart/form-data sont des ressources utiles au cas où quelqu'un aurait besoin d'une référence pour la spécification.

J'ai modifié la classe WebHelpers pour la rendre un peu plus petite et avoir des interfaces plus simples, elle s'appelle maintenant FormUpload. Si vous passez un FormUpload.FileParameter, vous pouvez transmettre les octets[] contenu avec un nom de fichier et un type de contenu, et si vous passez une chaîne, elle le traitera comme une combinaison de nom/valeur standard.

Voici la classe FormUpload :

// Implémente une méthode POST multipart/form-data en C# http://www.ietf.org/rfc/rfc2388.txt
// http://www.briangrinstead.com/blog/multipart-form-post-in-c
public static class FormUpload
{
    private static readonly Encoding encoding = Encoding.UTF8;
    public static HttpWebResponse MultipartFormDataPost(string postUrl, string userAgent, Dictionary postParameters)
    {
        string formDataBoundary = String.Format("----------{0:N}", Guid.NewGuid());
        string contentType = "multipart/form-data; boundary=" + formDataBoundary;

        byte[] formData = GetMultipartFormData(postParameters, formDataBoundary);

        return PostForm(postUrl, userAgent, contentType, formData);
    }
    private static HttpWebResponse PostForm(string postUrl, string userAgent, string contentType, byte[] formData)
    {
        HttpWebRequest request = WebRequest.Create(postUrl) as HttpWebRequest;

        if (request == null)
        {
            throw new NullReferenceException("request is not a http request");
        }

        // Configure les propriétés de la requête.
        request.Method = "POST";
        request.ContentType = contentType;
        request.UserAgent = userAgent;
        request.CookieContainer = new CookieContainer();
        request.ContentLength = formData.Length;

        // Vous pouvez également ajouter une authentification ici si nécessaire :
        // request.PreAuthenticate = true;
        // request.AuthenticationLevel = System.Net.Security.AuthenticationLevel.MutualAuthRequested;
        // request.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(System.Text.Encoding.Default.GetBytes("nom d'utilisateur" + ":" + "mot de passe")));

        // Envoie les données du formulaire à la requête.
        using (Stream requestStream = request.GetRequestStream())
        {
            requestStream.Write(formData, 0, formData.Length);
            requestStream.Close();
        }

        return request.GetResponse() as HttpWebResponse;
    }

    private static byte[] GetMultipartFormData(Dictionary postParameters, string boundary)
    {
        Stream formDataStream = new System.IO.MemoryStream();
        bool needsCLRF = false;

        foreach (var param in postParameters)
        {
            // Grâce aux retours des commentateurs, ajoutez un CRLF pour permettre l'ajout de plusieurs paramètres.
            // Sautez-le sur le premier paramètre, ajoutez-le aux paramètres suivants.
            if (needsCLRF)
                formDataStream.Write(encoding.GetBytes("\r\n"), 0, encoding.GetByteCount("\r\n"));

            needsCLRF = true;

            if (param.Value is FileParameter)
            {
                FileParameter fileToUpload = (FileParameter)param.Value;

                // Ajoutez juste la première partie de ce paramètre, car nous écrirons directement les données du fichier dans le Stream
                string header = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\";\r\nContent-Type: {3}\r\n\r\n",
                    boundary,
                    param.Key,
                    fileToUpload.FileName ?? param.Key,
                    fileToUpload.ContentType ?? "application/octet-stream");

                formDataStream.Write(encoding.GetBytes(header), 0, encoding.GetByteCount(header));

                // Écrivez directement les données du fichier dans le Stream, plutôt que de les sérialiser dans une chaîne.
                formDataStream.Write(fileToUpload.File, 0, fileToUpload.File.Length);
            }
            else
            {
                string postData = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}",
                    boundary,
                    param.Key,
                    param.Value);
                formDataStream.Write(encoding.GetBytes(postData), 0, encoding.GetByteCount(postData));
            }
        }

        // Ajoutez la fin de la requête. Commencez par une nouvelle ligne
        string footer = "\r\n--" + boundary + "--\r\n";
        formDataStream.Write(encoding.GetBytes(footer), 0, encoding.GetByteCount(footer));

        // Videz le Stream dans un byte[]
        formDataStream.Position = 0;
        byte[] formData = new byte[formDataStream.Length];
        formDataStream.Read(formData, 0, formData.Length);
        formDataStream.Close();

        return formData;
    }

    public class FileParameter
    {
        public byte[] File { get; set; }
        public string FileName { get; set; }
        public string ContentType { get; set; }
        public FileParameter(byte[] file) : this(file, null) { }
        public FileParameter(byte[] file, string filename) : this(file, filename, null) { }
        public FileParameter(byte[] file, string filename, string contenttype)
        {
            File = file;
            FileName = filename;
            ContentType = contenttype;
        }
    }
}

Voici le code d'appel, qui télécharge un fichier et quelques paramètres de post normaux :

// Lire les données du fichier
FileStream fs = new FileStream("c:\\people.doc", FileMode.Open, FileAccess.Read);
byte[] data = new byte[fs.Length];
fs.Read(data, 0, data.Length);
fs.Close();

// Générer des objets de post
Dictionary postParameters = new Dictionary();
postParameters.Add("nomfichier", "People.doc");
postParameters.Add("formatfichier", "doc");
postParameters.Add("fichier", new FormUpload.FileParameter(data, "People.doc", "application/msword"));

// Créer et recevoir la requête de réponse
string postURL = "http://localhost";
string userAgent = "Quelqu'un";
HttpWebResponse webResponse = FormUpload.MultipartFormDataPost(postURL, userAgent, postParameters);

// Traiter la réponse
StreamReader responseReader = new StreamReader(webResponse.GetResponseStream());
string fullResponse = responseReader.ReadToEnd();
webResponse.Close();
Response.Write(fullResponse);

0 votes

Remarquable. J'ai modifié pour afficher le progrès, prendre en charge les requêtes PUT et utiliser mon propre cookiecontainer mais cela m'a été d'une grande aide pour me mettre sur la bonne voie.

0 votes

Excellent ligne de base. J'ai apporté des ajustements mineurs pour répondre à nos besoins, mais cela a définitivement été d'une ÉNORME aide pour la publication de limites multi-parties. Cela a considérablement simplifié mon travail. Merci

0 votes

Brian J'ai suivi exactement votre code mais cela ne fonctionne pas :(

35voto

dnolan Points 1442

Ceci est coupé et collé à partir de quelques exemples de code que j'ai écrits, espérons que cela devrait donner les bases. Il ne prend en charge que les données de fichier et les données de formulaire pour le moment.

public class PostData
{

    private List m_Params;

    public List Params
    {
        get { return m_Params; }
        set { m_Params = value; }
    }

    public PostData()
    {
        m_Params = new List();

        // Ajouter un paramètre d'exemple
        m_Params.Add(new PostDataParam("email", "MyEmail", PostDataParamType.Field));
    }

    /// 
    /// Renvoie le tableau de paramètres formaté pour les données multi-part/form
    /// 
    /// 
    public string GetPostData()
    {
        // Obtenir la limite, par défaut est --AaB03x
        string boundary = ConfigurationManager.AppSettings["ContentBoundary"].ToString();

        StringBuilder sb = new StringBuilder();
        foreach (PostDataParam p in m_Params)
        {
            sb.AppendLine(boundary);

            if (p.Type == PostDataParamType.File)
            {
                sb.AppendLine(string.Format("Content-Disposition: file; name=\"{0}\"; filename=\"{1}\"", p.Name, p.FileName));
                sb.AppendLine("Content-Type: text/plain");
                sb.AppendLine();
                sb.AppendLine(p.Value);                 
            }
            else
            {
                sb.AppendLine(string.Format("Content-Disposition: form-data; name=\"{0}\"", p.Name));
                sb.AppendLine();
                sb.AppendLine(p.Value);
            }
        }

        sb.AppendLine(boundary);

        return sb.ToString();           
    }
}

public enum PostDataParamType
{
    Field,
    File
}

public class PostDataParam
{

    public PostDataParam(string name, string value, PostDataParamType type)
    {
        Name = name;
        Value = value;
        Type = type;
    }

    public string Name;
    public string FileName;
    public string Value;
    public PostDataParamType Type;
}

Pour envoyer les données, vous devez ensuite :

HttpWebRequest oRequest = null;
oRequest = (HttpWebRequest)HttpWebRequest.Create(oURL.URL);
oRequest.ContentType = "multipart/form-data";                       
oRequest.Method = "POST";
PostData pData = new PostData();

byte[] buffer = encoding.GetBytes(pData.GetPostData());

// Définit la longueur du contenu de nos données
oRequest.ContentLength = buffer.Length;

// Déversez nos données de postdata mises en mémoire tampon dans le flux, booyah
oStream = oRequest.GetRequestStream();
oStream.Write(buffer, 0, buffer.Length);
oStream.Close();

// obtenir la réponse
oResponse = (HttpWebResponse)oRequest.GetResponse();

J'espère que c'est clair, j'ai coupé et collé de quelques sources pour rendre cela plus propre.

0 votes

Je ne pense pas que cela fonctionne, la limite n'est pas définie dans le Content-Type et il faut ajouter à gauche "--" de la limite pour chacun qui est écrit et "--" à droite du dernier. Ne me demandez pas comment ce "--" magique apparaît, ils sont implicites dans la documentation officielle: w3.org/TR/html401/interact/forms.html#h-17.13.4.2 Si vous n'aimez pas les exigences magiques comme moi, vous aurez du mal à faire fonctionner cela comme je l'ai fait.

0 votes

Ce code fonctionne définitivement pour les scénarios dans lesquels je l'ai utilisé. Cependant, je remarque que bien que je définisse la limite complète à envoyer dans les données de la requête POST, je n'ai pas spécifié d'ajouter ceci à l'en-tête de contenu global en tant que délimiteur de limite. Quant au nombre magique, celui-ci est déterminé par le client pour s'assurer qu'il est unique et n'apparaîtra nulle part dans le contenu, voir ici pour plus de détails: w3.org/Protocols/rfc1341/7_2_Multipart.html

34voto

codevision Points 236

Avec .NET 4.5, vous pouvez actuellement utiliser l'espace de noms System.Net.Http. Ci-dessous l'exemple pour téléverser un seul fichier en utilisant des données de formulaire multipart.

utilisation de System;
utilisation de System.IO;
utilisation de System.Net.Http;

espace de noms HttpClientTest
{
    classe Programme
    {
        static vide Principal(string[] args)
        {
            var client = nouveau HttpClient();
            var contenu = nouveau MultipartFormDataContent();
            contenu.Ajouter(nouveau StreamContent(Fichier.Ouvrir("../../Image1.png", FileMode.Open)), "Image", "Image.png");
            contenu.Ajouter(nouveau StringContent("Insérer le contenu de la chaîne ici"), "Content-Id dans le HTTP"); 
            var résultat = client.PostAsync("https://hostname/api/Account/UploadAvatar", contenu);
            Console.WriteLine(résultat.Résultat.ToString());
        }
    }
}

0 votes

Qu'arrive-t-il si je reçois des détails d'un formulaire multipart dans l'URL, ici je parle de fichiers comme un PDF

0 votes

Vous pouvez ajouter plus de contenu de formulaire (comme un ID) avec content.Add(new StringContent("1"),"MyId");

13voto

jmoeller Points 555

En s'appuyant sur l'exemple de dnolan, voici la version que j'ai réellement réussi à faire fonctionner (il y avait des erreurs avec la frontière, l'encodage n'était pas défini) :-)

Pour envoyer les données :

HttpWebRequest oRequest = null;
oRequest = (HttpWebRequest)HttpWebRequest.Create("http://you.url.here");
oRequest.ContentType = "multipart/form-data; boundary=" + PostData.boundary;
oRequest.Method = "POST";
PostData pData = new PostData();
Encoding encoding = Encoding.UTF8;
Stream oStream = null;

/* ... définir les paramètres, lire les fichiers, etc. Par exemple :
   pData.Params.Add(new PostDataParam("email", "exemple@example.com", PostDataParamType.Field));
   pData.Params.Add(new PostDataParam("fileupload", "nomfichier.txt", "contenufichier", PostDataParamType.File));
*/

byte[] buffer = encoding.GetBytes(pData.GetPostData());

oRequest.ContentLength = buffer.Length;

oStream = oRequest.GetRequestStream();
oStream.Write(buffer, 0, buffer.Length);
oStream.Close();

HttpWebResponse oResponse = (HttpWebResponse)oRequest.GetResponse();

La classe PostData doit ressembler à ceci :

public class PostData
{
    // Changer si nécessaire, non obligatoire
    public static string boundary = "AaB03x";

    private List m_Params;

    public List Params
    {
        get { return m_Params; }
        set { m_Params = value; }
    }

    public PostData()
    {
        m_Params = new List();
    }

    /// 
    /// Renvoie le tableau de paramètres formaté pour les données multi-part/form
    /// 
    /// 
    public string GetPostData()
    {
        StringBuilder sb = new StringBuilder();
        foreach (PostDataParam p in m_Params)
        {
            sb.AppendLine("--" + boundary);

            if (p.Type == PostDataParamType.File)
            {
                sb.AppendLine(string.Format("Content-Disposition: file; name=\"{0}\"; filename=\"{1}\"", p.Name, p.FileName));
                sb.AppendLine("Content-Type: application/octet-stream");
                sb.AppendLine();
                sb.AppendLine(p.Value);
            }
            else
            {
                sb.AppendLine(string.Format("Content-Disposition: form-data; name=\"{0}\"", p.Name));
                sb.AppendLine();
                sb.AppendLine(p.Value);
            }
        }

        sb.AppendLine("--" + boundary + "--");

        return sb.ToString();
    }
}

public enum PostDataParamType
{
    Field,
    File
}

public class PostDataParam
{
    public PostDataParam(string name, string value, PostDataParamType type)
    {
        Name = name;
        Value = value;
        Type = type;
    }

    public PostDataParam(string name, string filename, string value, PostDataParamType type)
    {
        Name = name;
        Value = value;
        FileName = filename;
        Type = type;
    }

    public string Name;
    public string FileName;
    public string Value;
    public PostDataParamType Type;
}

8voto

eeeeaaii Points 1077

Dans la version de .NET que j'utilise, vous devez également faire ceci :

System.Net.ServicePointManager.Expect100Continue = false;

Si vous ne le faites pas, la classe HttpWebRequest ajoutera automatiquement l'en-tête de demande Expect:100-continue ce qui compliquera tout.

J'ai également appris à mes dépens que vous devez avoir le bon nombre de tirets. Tout ce que vous déclarez comme "boundary" dans l'en-tête Content-Type doit être précédé de deux tirets

--THEBOUNDARY

et à la fin

--THEBOUNDARY--

exactement comme dans l'exemple de code. Si votre limite est une série de tirets suivie d'un nombre, cette erreur ne sera pas évidente en regardant la demande http dans un serveur proxy.

0 votes

C'EST IMPORTANT! Merci beaucoup d'avoir mentionné cela, je n'y aurais jamais pensé autrement et c'était la seule chose qui me perturbait!

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X