回归基础案例研究:使用ASP.NET MVC实现HTTP文件上传,包括测试和模拟

A number of folks have asked me how to "implement the ASP.NET File Upload Control" except using ASP.NET MVC. This is a really interesting question for a number of reasons and a great opportunity to explore some fundamentals.

除了使用ASP.NET MVC之外,许多人都问我如何“实现ASP.NET文件上载控件”。 由于许多原因,这是一个非常有趣的问题,并且是探索某些基础知识的绝好机会。

First, ASP.NET MVC is different since we don't get to use ASP.NET Server Controls as we're used to them. There's no "server controls" in the way that we're used to them.

首先,ASP.NET MVC有所不同,因为我们不习惯使用ASP.NET Server控件。 我们习惯的方式没有“服务器控件”。

Second, it'd be important to write Unit Tests for something like File Upload, and since ASP.NET MVC tries to be Unit Test friendly, it's an interesting problem to do tests. Why is it interesting? Well, ASP.NET MVC sits on top of ASP.NET. That means ASP.NET MVC didn't do any special work for File Upload support. It uses whatever stuff is built into ASP.NET itself. This may or not be helpful or interesting or even easy to test.

其次,为诸如文件上载之类的内容编写单元测试非常重要,并且由于ASP.NET MVC试图对单元测试友好,因此进行测试是一个有趣的问题。 为什么有趣呢? 好吧,ASP.NET MVC位于ASP.NET之上。 这意味着ASP.NET MVC对于文件上传支持没有做任何特殊的工作。 它使用ASP.NET本身内置的任何东西。 这可能不是有用的,还是有趣的,甚至很容易测试。

It seems then, that this is a good exercise in understanding a number of things:

这样看来,这是理解许多事情的好练习:

  • HTTP and How File Upload works via HTTP

    HTTP以及如何通过HTTP上传文件
  • What ASP.NET offers for to catch File Uploads

    ASP.NET为捕获文件上载提供了什么
  • How to Mock things that aren't really Mock Friendly

    如何模拟并非真正模拟友好的事物
  • And ultimately, How to do File Upload with ASP.NET MVC

    最后,如何使用ASP.NET MVC上传文件

Here we go.

开始了。

HTTP以及如何通过HTTP上传文件 (HTTP and How File Upload works via HTTP)

It's always better, for me, to understand WHY and HOW something is happening. If you say "just because" or "whatever, you just add that, and it works" then I think that's sad. For some reason while many folks understand FORM POSTs and generally how form data is passed up to the server, when a file is transferred many just conclude it's magic.

对我来说,了解为什么以及为什么发生什么总是更好。 如果您说“仅仅是因为”或“随便什么,只要加上它,它就可以了”,那么我认为这很可悲。 由于某些原因,虽然许多人都了解FORM POST以及通常如何将表单数据传递到服务器,但是当文件被传输时,许多人只是认为这很神奇。

Why do we have to add enctype="multipart/form=data" on our forms that include file uploads? Because the form will now be POSTed in multiple parts.

为什么我们必须在包含文件上传的表单上添加enctype =“ multipart / form = data” ? 因为现在该表单将分多个部分发布。

If you have a form like this:

如果您有这样的表格:

<form action="/home/uploadfiles" method="post" enctype="multipart/form-data">
<label for="file">Filename:</label>
<input type="file" name="file" id="file" /><br />
<input type="submit" name="submit" value="Submit" />
</form>

The resulting Form POST will look like this (slightly simplified):

生成的Form POST将如下所示(略有简化):

POST /home/uploadfiles HTTP/1.1
Content-Type: multipart/form-data; boundary=---------------------------7d81b516112482
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; WOW64)
Content-Length: 324

-----------------------------7d81b516112482
Content-Disposition: form-data; name="file"; filename="\\SERVER\Users\Scott\Documents\test.txt"
Content-Type: text/plain

foo
-----------------------------7d81b516112482
Content-Disposition: form-data; name="submit"

Submit
-----------------------------7d81b516112482--

Notice a few things about this POST. First, notice the content-type and boundary="" and how the boundary is used later, as exactly that, a boundary between the multiple parts. See how the first part shows that I uploaded a single file, of type text/plain. You can interpolate from this how you'd expect multiple files to show up if they were all POSTed at once.

请注意有关此POST的一些信息。 首先,请注意content-type和boundary =“”以及稍后如何使用边界,准确地说,就是多个部分之间的边界。 查看第一部分如何显示我上载了一个文本/纯文本类型的文件。 您可以据此插值,如果一次将所有文件都发布,则希望显示多个文件。

And of course, look at how different this would look if it were just a basic form POST without the enctype="multipart/form=data" included:

当然,看看这只是基本表单POST而没有包括enctype =“ multipart / form = data”的情况,这会有什么不同:

POST /home/uploadfiles HTTP/1.1
Content-Type: application/x-www-form-urlencoded
UA-CPU: x86
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; WOW64)
Content-Length: 13

submit=Submit

See how the content type is different? This is a regular, typical form POST. Perhaps atypical in that it includes only a Submit button!

看看内容类型有何不同? 这是常规的典型形式POST。 可能是非典型的,因为它仅包含一个“提交”按钮!

The point is, when folks add a ASP.NET FileUpload Control to their designer, it's useful to remember that you're buying into an abstraction over something. In this case, you're using a control that promises to hide the whole multipart MIME way of looking at things, and that's totally cool.

关键是,当人们向他们的设计师添加ASP.NET FileUpload控件时,记住您正在购买某种东西的抽象很有用。 在这种情况下,您将使用一个控件,该控件承诺隐藏看东西的整个多部分MIME方式,这非常酷。

Back To Basics Tip Know what your library is hiding from you and why you chose it.

返回基础技巧了解您的图书馆您隐藏的内容以及您为什么选择它。

As an aside, if you looked at an email of yours with multiple attached files, it would look VERY similar to the body of the first HTTP message as multipart MIME encoding is found everywhere, as is common with most good ideas.

顺便说一句,如果您查看一封带有多个附件的电子邮件,它看起来与第一封HTTP消息的正文非常相似,因为随处可见多部分MIME编码,这与大多数好主意很常见。

ASP.NET为捕获文件上载提供了什么 (What ASP.NET offers for to catch File Uploads)

The FileUpload control is just a control that sits on top of a bunch of support for FileUploads in ASP.NET, starting with the classes Request.Files and HttpPostedFile. Those are the things that actually do the hold on to the parsed Files from an HTTP Request. You can use them to get a hold of a stream (a bunch of bytes in memory that are the file) or just save the file.

FileUpload控件只是一个位于ASP.NET中对FileUploads的支持之上的控件,从类Request.Files和HttpPostedFile开始。 这些实际上是保留来自HTTP请求的已解析文件的功能。 您可以使用它们来保存流(在文件中是内存中的一堆字节),也可以只保存文件。

Since we can't use ASP.NET Server Controls in ASP.NET MVC, we'll use these classes instead. Here's how you usually grab all the files from an upload and save them:

由于我们不能在ASP.NET MVC中使用ASP.NET服务器控件,因此将改用这些类。 通常,这是从上传中获取所有文件并保存它们的方式:

foreach (string file in Request.Files)
{
HttpPostedFile hpf = Request.Files[file] as HttpPostedFile;
if (hpf.ContentLength == 0)
continue;
string savedFileName = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
Path.GetFileName(hpf.FileName));
hpf.SaveAs(savedFileName);
}

Of course, you might want to change the directory and filename, maybe check the mimeType to allow only certain kinds of files, or check the length to limit your uploads, but this is the general idea.

当然,您可能想要更改目录和文件名,可能要检查mimeType以仅允许某些类型的文件,或者要检查长度以限制您的上传,但这是总的思路。

Note that Request.Files has been around since 1.x and isn't a strongly typed collection of anything, so the GetEnumerator() of .Files that we're using in the foreach returns strings that are then used as keys into the Files[] indexer. It's a little wonky as it's old.

请注意,Request.Files从1.x开始就存在,并且不是任何东西的强类型集合,因此我们在foreach中使用的.Files的GetEnumerator()返回字符串,然后将这些字符串用作文件中的键[]索引器。 它有点旧了,有点老样子。

However, don't let me get ahead of myself, let's write the tests first!

但是,不要让我超越自己,让我们先编写测试!

如何模拟不是真的模拟友好的事物 (How to Mock things that aren't really Mock Friendly )

After creating a new ASP.NET MVC Project and making sure to select a test framework, I'll drop into a Controller Test and make a new TestMethod that kind of looks like I expect my method to be used.

创建新的ASP.NET MVC项目并确保选择测试框架之后,我将进入Controller Test并创建一个新的TestMethod,看起来像我希望使用的方法。

[TestMethod]
public void FakeUploadFiles()
{
HomeController controller = new HomeController();

ViewResult result = controller.UploadFiles() as ViewResult;
var uploadedResult = result.ViewData.Model as List<ViewDataUploadFilesResult>;
Assert.AreEqual(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "foo.doc"), uploadedResult[0].Name);
Assert.AreEqual(8192, uploadedResult[0].Length);
}

This is incomplete, though, as I'm writing the tests before I the implementation exists. I need to think about how this should be implemented, and as I learn what should be mocked, I need to go back and forth between the tests and the implementation.

但是,这是不完整的,因为我在实现存在之前就编写了测试。 我需要考虑应该如何实现它,并且当我了解应该嘲笑什么时,我需要在测试和实现之间来回走动。

If we tried to compile this test, it won't, until I add a few types and methods. Once it actually compiles, but before I write the method itself, I'll want to see it FAIL. If you get a test to PASS on the first try, you don't really know yet if it CAN fail. Making it fail first proves that it's broken. Then you get to fix it.

如果我们尝试编译该测试,则直到我添加一些类型和方法后,它才会编译。 一旦它实际编译,但是在我编写方法本身之前,我会希望它失败。 如果您第一次尝试通过PASS的测试,您还真的不知道它是否会失败。 首先使其失败就证明它已损坏。 然后,您可以对其进行修复。

Back To Basics Tip Remember, in TDD, if it ain't broke, you don't get to fix it.

回到基础技巧记住,在TDD中,如果它没有损坏,就无法修复它。

image

There's a bit of a chicken and the egg because it's unclear what will need to be mocked out until I start the implementation. However, this draft method above generally says what I want to do. I want to my controller to have a method called UploadFiles() that will grab the uploaded files from Request.Files, save them, then put a type in the ViewData saying which files were saved and how large they were.

有点鸡和蛋,因为在我开始执行之前尚不清楚需要模拟什么。 但是,上面的草稿方法通常说明了我想做的事情。 我希望控制器有一个名为UploadFiles()的方法,该方法将从Request.Files抓取上传的文件,保存它们,然后在ViewData中键入一个类型,说明保存了哪些文件以及它们的大小。

Ok, take a breath. The following code may look freaky, but it's really cool actually. You can use any Mock Framework you like, but I like Moq for it's fluency.

好吧,喘口气。 以下代码可能看起来很怪异,但实际上确实很酷。 您可以使用任何喜欢的Mock框架,但是我很喜欢Moq,因为它很流畅。

We're having to "mock" things because we need to lie to our controller, who's expecting an HTTP Post, remember? It's going to go and spin through Request.Files and try to save each file. Since we want to test this without the web server or web browser, we'll want to tell the Moq framework about our expectations.

我们必须“嘲笑”事情,因为我们需要对我们的控制器撒谎,谁在期待HTTP Post,还记得吗? 它将遍历Request.Files并尝试保存每个文件。 由于我们要在没有Web服务器或Web浏览器的情况下进行测试,因此我们要告诉Moq框架 关于我们的期望。

Back To Basics Tip Be careful to mock context and assert outputs but don't mock away the whole test!

回到基础技巧小心模拟上下文并声明输出,但不要模拟整个测试!

I've commented the code to explain...

我评论了代码以解释...

[TestMethod]
public void FakeUploadFiles()
{
//We'll need mocks (fake) of Context, Request and a fake PostedFile
var request = new Mock<HttpRequestBase>();
var context = new Mock<HttpContextBase>();
var postedfile = new Mock<HttpPostedFileBase>();

//Someone is going to ask for Request.File and we'll need a mock (fake) of that.
var postedfilesKeyCollection = new Mock<HttpFileCollectionBase>();
var fakeFileKeys = new List<string>() { "file" };

//OK, Mock Framework! Expect if someone asks for .Request, you should return the Mock!
context.Expect(ctx => ctx.Request).Returns(request.Object);
//OK, Mock Framework! Expect if someone asks for .Files, you should return the Mock with fake keys!
request.Expect(req => req.Files).Returns(postedfilesKeyCollection.Object);

//OK, Mock Framework! Expect if someone starts foreach'ing their way over .Files, give them the fake strings instead!
postedfilesKeyCollection.Expect(keys => keys.GetEnumerator()).Returns(fakeFileKeys.GetEnumerator());

//OK, Mock Framework! Expect if someone asks for file you give them the fake!
postedfilesKeyCollection.Expect(keys => keys["file"]).Returns(postedfile.Object);

//OK, Mock Framework! Give back these values when asked, and I will want to Verify that these things happened
postedfile.Expect(f => f.ContentLength).Returns(8192).Verifiable();
postedfile.Expect(f => f.FileName).Returns("foo.doc").Verifiable();

//OK, Mock Framework! Someone is going to call SaveAs, but only once!
postedfile.Expect(f => f.SaveAs(It.IsAny<string>())).AtMostOnce().Verifiable();

HomeController controller = new HomeController();
//Set the controller's context to the mock! (fake)
controller.ControllerContext = new ControllerContext(context.Object, new RouteData(), controller);

//DO IT!
ViewResult result = controller.UploadFiles() as ViewResult;

//Now, go make sure that the Controller did its job
var uploadedResult = result.ViewData.Model as List<ViewDataUploadFilesResult>;
Assert.AreEqual(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "foo.doc"), uploadedResult[0].Name);
Assert.AreEqual(8192, uploadedResult[0].Length);

postedfile.Verify();
}

如何使用ASP.NET MVC上传文件 (How to do File Upload with ASP.NET MVC )

Now, what is the least amount of code in our Controller do we need to write to make this test pass? Here we get to use the Request.Files method that ASP.NET (not ASP.NET MVC) has had for years, and use it as advertised. It works in the tests and it works in production.

现在,为了使测试通过,我们需要编写最少数量的Controller代码吗? 在这里,我们可以使用ASP.NET(不是ASP.NET MVC)拥有多年的Request.Files方法,并按广告使用它。 它可以在测试中工作,也可以在生产中工作。

Important Note: We have to use the HttpPostedFileBase class, rather than the HttpPostedFile because every Request, Response, HttpContext and all related ASP.NET intrinsic abstractions are one layer farther way in ASP.NET MVC. If you get an HttpRequest in ASP.NET, then in ASP.NET MVC at runtime...

重要说明:我们必须使用HttpPostedFileBase类,而不是HttpPostedFile,因为在ASP.NET MVC中,每个请求,响应,HttpContext和所有相关的ASP.NET固有抽象都是另一层。 如果在ASP.NET中获得HttpRequest,则在运行时在ASP.NET MVC中...

  • you'll get an HttpRequestWrapper while running under a Webserver

    在Web服务器下运行时,您将获得HttpRequestWrapper
  • you'll get a dynamically generated derived Mock of an HttpRequestBase while running outside a Webserver (like inside a test) when you've made your own ControllerContext.

    当您创建自己的ControllerContext时,在Web服务器外部(例如在测试内部)运行时,您将获得HttpRequestBase的动态生成的派生模拟。

In each case, the instances you'll get are both (ultimately) of type HttpRequestBase, but it's this extra layer of abstraction that makes ASP.NET MVC easy to test and ASP.NET WebForms less so. I hope these Wrappers will be included in a future release of WebForms. The fact that they live in the System.Web.Abstractions.dll and not System.Web.Mvc.Abstractions.dll tells me someone has their eye on that particular ball.

在每种情况下,您都将(最终)都获得HttpRequestBase类型的实例,但是正是这种额外的抽象层使ASP.NET MVC易于测试,而ASP.NET WebForms却不那么容易。 我希望这些包装程序将包含在WebForms的将来版本中。 他们生活在System.Web.Abstractions.dll中,而不是System.Web.Mvc.Abstractions.dll中,这一事实告诉我,有人在关注这个特定的球。

At any rate, here's the Controller that takes File Upload requests:

无论如何,这是接受文件上载请求的控制器:

public class ViewDataUploadFilesResult
{
public string Name { get; set; }
public int Length { get; set; }
}

public class HomeController : Controller
{
public ActionResult UploadFiles()
{
var r = new List<ViewDataUploadFilesResult>();

foreach (string file in Request.Files)
{
HttpPostedFileBase hpf = Request.Files[file] as HttpPostedFileBase;
if (hpf.ContentLength == 0)
continue;
string savedFileName = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
Path.GetFileName(hpf.FileName));
hpf.SaveAs(savedFileName);

r.Add(new ViewDataUploadFilesResult()
{ Name = savedFileName,
Length = hpf.ContentLength });
}
return View("UploadedFiles",r);
}
}

At the bottom where I ask for the "UploadedFiles" view, and I pass in my list of ViewDataUploadFilesResults. This will appea in the ViewData.Model property. The View then displays them, and that's ALL the View does.

在底部,我要求“ UploadedFiles”视图,并传递ViewDataUploadFilesResults列表。 这将适用于ViewData.Model属性。 然后,视图将显示它们,这就是视图所要做的。

<ul>
<% foreach (ViewDataUploadFilesResult v in this.ViewData.Model) { %>
<%=String.Format("<li>Uploaded: {0} totalling {1} bytes.</li>",v.Name,v.Length) %>
<% } %>
</ul>

结论 (Conclusion)

I always encourage people to take the little bit of time to use Fiddler or SysInternals or look at your call stack or just to take a breath and remind oneself, "so how is this supposed to work?" Otherwise, one is just cargo-cult programming.

我总是鼓励人们花一点时间使用FiddlerSysInternals或查看您的调用堆栈,或者只是屏住呼吸并提醒自己,“那么这应该如何工作?” 否则,就只是一种“崇高礼节”的编程

This post was a long answer to the question "How do I do FileUpload with ASP.NET MVC?" but I feel better having written in this way.

这篇文章是对“如何使用ASP.NET MVC进行FileUpload?”这一问题的长答案。 但是用这种方式写会更好。

翻译自: https://www.hanselman.com/blog/a-back-to-basics-case-study-implementing-http-file-upload-with-aspnet-mvc-including-tests-and-mocks

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值