2015-04-23 17 views
5

Nella mia API Web, il metodo di azione POST carica un file sul server.Come creare un file per popolare HttpContext.Current.Request.Files?

Per Unit Testing questo metodo, ho bisogno di creare un HttpContext e mettere un file all'interno della sua richiesta:

HttpContext.Current.Request.Files 

Finora sto fingendo HttpContext con questo codice che funziona perfettamente:

HttpRequest request = new HttpRequest("", "http://localhost/", ""); 
    HttpResponse response = new HttpResponse(new StringWriter()); 
    HttpContext.Current = new HttpContext(request, response); 

Nota che NON voglio usare Moq o altre librerie di Mocking.

Come posso realizzare questo? (MultipartContent forse?)

Grazie

+0

ho provato lo stesso codice sostituendo il primo parametro di HttpRequest al percorso fisico del file, ma non è stato possibile ottenere il file nel controller. Puoi spiegare come farlo? – Srini

risposta

3

Di solito si tratta di una cattiva pratica di utilizzare gli oggetti che è difficile prendere in giro a controllori (oggetti come HttpContext, HttpRequest, HttpResponse ecc). Ad esempio nelle applicazioni MVC abbiamo ModelBinder e HttpPostedFileBase oggetto che possiamo usare nel controller per evitare di lavorare con HttpContext (per l'applicazione Web Api abbiamo bisogno di scrivere la nostra logica).

public ActionResult SaveUser(RegisteredUser data, HttpPostedFileBase file) 
{ 
    // some code here 
} 

Quindi non è necessario lavorare con HttpContext.Current.Request.Files. È difficile da testare. Questo tipo di lavoro deve essere eseguito in un altro livello dell'applicazione (non nel controller). In Web Api possiamo scrivere MediaTypeFormatter a tale scopo.

public class FileFormatter : MediaTypeFormatter 
{ 
    public FileFormatter() 
    { 
     SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data")); 
    } 

    public override bool CanReadType(Type type) 
    { 
     return typeof(ImageContentList).IsAssignableFrom(type); 
    } 

    public override bool CanWriteType(Type type) 
    { 
     return false; 
    } 

    public async override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContent content, IFormatterLogger logger) 
    { 
     if (!content.IsMimeMultipartContent()) 
     { 
      throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); 
     } 

     var provider = new MultipartMemoryStreamProvider(); 
     var formData = await content.ReadAsMultipartAsync(provider); 

     var imageContent = formData.Contents 
      .Where(c => SupportedMediaTypes.Contains(c.Headers.ContentType)) 
      .Select(i => ReadContent(i).Result) 
      .ToList(); 

     var jsonContent = formData.Contents 
      .Where(c => !SupportedMediaTypes.Contains(c.Headers.ContentType)) 
      .Select(j => ReadJson(j).Result) 
      .ToDictionary(x => x.Key, x => x.Value); 

     var json = JsonConvert.SerializeObject(jsonContent); 
     var model = JsonConvert.DeserializeObject(json, type) as ImageContentList; 

     if (model == null) 
     { 
      throw new HttpResponseException(HttpStatusCode.NoContent); 
     } 

     model.Images = imageContent; 
     return model; 
    } 

    private async Task<ImageContent> ReadContent(HttpContent content) 
    { 
     var data = await content.ReadAsByteArrayAsync(); 
     return new ImageContent 
     { 
      Content = data, 
      ContentType = content.Headers.ContentType.MediaType, 
      Name = content.Headers.ContentDisposition.FileName 
     }; 
    } 

    private async Task<KeyValuePair<string, object>> ReadJson(HttpContent content) 
    { 
     var name = content.Headers.ContentDisposition.Name.Replace("\"", string.Empty); 
     var value = await content.ReadAsStringAsync(); 

     if (value.ToLower() == "null") 
      value = null; 

     return new KeyValuePair<string, object>(name, value); 
    } 
} 

Quindi qualsiasi contenuto che verrà inviato con multipart/form-data tipo di contenuto (e file devono essere inviati con quel tipo di contenuto) sarà analizzato per la classe figlia di ImageContentList (quindi con i file è possibile inserire qualsiasi altra informazione) . Se vuoi pubblicare 2 o 3 file, funzionerà anche tu.

public class ImageContent: IModel 
{ 
    public byte[] Content { get; set; } 
    public string ContentType { get; set; } 
    public string Name { get; set; } 
} 

public class ImageContentList 
{ 
    public ImageContentList() 
    { 
     Images = new List<ImageContent>(); 
    } 
    public List<ImageContent> Images { get; set; } 
} 

public class CategoryPostModel : ImageContentList 
{ 
    public int? ParentId { get; set; } 
    public string Name { get; set; } 
    public string Description { get; set; } 
} 

Quindi è possibile utilizzarlo in qualsiasi controller dell'applicazione. Ed è facile da testare perché il codice del tuo controller non dipende più da HttpContext.

public ImagePostResultModel Post(CategoryPostModel model) 
{ 
    // some code here 
} 

Inoltre è necessario registrarsi MediaTypeFormatter per Web Api configurazione

configuration.Formatters.Add(new ImageFormatter()); 
6

ero finalmente in grado di aggiungere file falsi per HttpContext per i test unitari WebAPI facendo uso pesante di reflection, visto che la maggior parte del Request.Files l'infrastruttura è nascosta in classi sigillate o interne.

Una volta aggiunto il codice qui sotto, i file possono essere aggiunti in modo relativamente facile per HttpContext.Current:

var request = new HttpRequest(null, "http://tempuri.org", null); 
AddFileToRequest(request, "File", "img/jpg", new byte[] {1,2,3,4,5}); 

HttpContext.Current = new HttpContext(
    request, 
    new HttpResponse(new StringWriter()); 

Con il sollevamento di carichi pesanti fatto da:

static void AddFileToRequest(
    HttpRequest request, string fileName, string contentType, byte[] bytes) 
{ 
    var fileSize = bytes.Length; 

    // Because these are internal classes, we can't even reference their types here 
    var uploadedContent = ReflectionHelpers.Construct(typeof (HttpPostedFile).Assembly, 
     "System.Web.HttpRawUploadedContent", fileSize, fileSize); 
    uploadedContent.InvokeMethod("AddBytes", bytes, 0, fileSize); 
    uploadedContent.InvokeMethod("DoneAddingBytes"); 

    var inputStream = Construct(typeof (HttpPostedFile).Assembly, 
     "System.Web.HttpInputStream", uploadedContent, 0, fileSize); 

    var postedFile = Construct<HttpPostedFile>(fileName, 
      contentType, inputStream); 
    // Accessing request.Files creates an empty collection 
    request.Files.InvokeMethod("AddFile", fileName, postedFile); 
} 

public static object Construct(Assembly assembly, string typeFqn, params object[] args) 
{ 
    var theType = assembly.GetType(typeFqn); 
    return theType 
     .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, 
      args.Select(a => a.GetType()).ToArray(), null) 
     .Invoke(args); 
} 

public static T Construct<T>(params object[] args) where T : class 
{ 
    return Activator.CreateInstance(
     typeof(T), 
     BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, 
     null, args, null) as T; 
} 

public static object InvokeMethod(this object o, string methodName, 
    params object[] args) 
{ 
    var mi = o.GetType().GetMethod(methodName, 
      BindingFlags.NonPublic | BindingFlags.Instance); 
    if (mi == null) throw new ArgumentOutOfRangeException("methodName", 
     string.Format("Method {0} not found", methodName)); 
    return mi.Invoke(o, args); 
} 
+0

Non sono riuscito a trovare lo spazio dei nomi per la classe ReflectionHelpers. Posso sapere quale spazio dei nomi o libreria di terze parti dovrei usare per questo? – Srini

+0

Apols: i seguenti metodi statici erano in una classe statica denominata ReflectionHelpers. Se si mettono tutti i metodi nella stessa classe, è possibile eliminare del tutto lo spazio dei nomi di ReflectionHelpers oppure è possibile rifattorizzarli nella propria classe. – StuartLC

Problemi correlati