【问题标题】:How to create a file to populate HttpContext.Current.Request.Files?如何创建一个文件来填充 HttpContext.Current.Request.Files?
【发布时间】:2015-04-23 22:50:12
【问题描述】:

在我的 Web API 中,POST 操作方法将文件上传到服务器。

为了对该方法进行单元测试,我需要创建一个 HttpContext 并在其请求中放入一个文件:

HttpContext.Current.Request.Files

到目前为止,我正在使用这段完美运行的代码来伪造 HttpContext:

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

请注意,我不想使用 Moq 或任何其他 Mocking 库。

我怎样才能做到这一点? (可能是多部分内容?)

谢谢

【问题讨论】:

  • 我尝试了相同的代码,将HttpRequest的第一个参数替换为文件物理路径,但无法在控制器中获取文件。你能解释一下怎么做吗?

标签: c# unit-testing mocking asp.net-web-api


【解决方案1】:

我最终能够通过大量使用 reflection 将假文件添加到 HttpContext 以进行 WebApi 单元测试,因为大部分 Request.Files 基础架构隐藏在密封或内部类中。

添加以下代码后,可以相对轻松地将文件添加到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());

由以下人员完成繁重的工作:

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);
}

【讨论】:

  • 我找不到 ReflectionHelpers 类的命名空间。我可以知道我应该为此使用什么命名空间或第三方库吗?
  • Apols - 下面的静态方法位于名为 ReflectionHelpers 的静态类中。如果您将所有方法放在同一个类中,您可以完全删除 ReflectionHelpers 命名空间,或者您可以将它们重构到您自己的类中。
  • 嗨斯图尔特。在我的项目中,我在 InvokeMethod 出现错误“类型没有 InvokeMethod 的定义”。我忘记了一些参考或组件吗?
  • @PatrickPirzer InvokeMethod 是我回答中代码 sn-p 底部的静态扩展方法。您需要将其添加为静态类(我称之为“反射助手”)
  • @StuartLC:谢谢。我想我需要一只导盲犬 :) 还有一个问题。您根据哪个条件设置要上传的文件大小的字节数组?在我的一个案例中,我上传了一个 sqlite 数据库,后来收到错误消息“文件不是数据库”。
【解决方案2】:

通常在控制器中使用难以模拟的对象(HttpContext, HttpRequest, HttpResponse 等对象)是一种不好的做法。例如,在 MVC 应用程序中,我们有 ModelBinderHttpPostedFileBase 对象,我们可以在控制器中使用它们来避免使用 HttpContext(对于 Web Api 应用程序,我们需要编写自己的逻辑)。

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

因此您无需使用HttpContext.Current.Request.Files。很难测试。这种类型的工作必须在应用程序的另一个级别(而不是控制器)中完成。在Web Api 中,我们可以为此编写 MediaTypeFormatter。

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);
    }
}

因此,任何将以multipart/form-data 内容类型发布的内容(并且文件必须以该内容类型发布)将被解析为ImageContentList 的子类(因此您可以使用文件发布任何其他信息) .如果您想发布 2 或 3 个文件 - 它也可以。

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; }
}

然后您可以在应用程序的任何控制器中使用它。而且很容易测试,因为你的控制器的代码不再依赖于 HttpContext。

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

您还需要注册MediaTypeFormatter 以进行Web Api 配置

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-11-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-12
    • 1970-01-01
    相关资源
    最近更新 更多