Uploading Files In ASP.NET Core

Uploading files in ASP.net core is largely the same as standard full framework MVC, with the large exception being how you can now stream large files. We will go over both methods of uploading a file in ASP.net core.

Model Binding IFormFile (Small Files)

When uploading a file via this method, the important thing to note is that your files are uploaded in their entirety before execution hits your controller action. What this means is that the disk on your server holds a temporary file while you decide where to push it. With this small files this is fine, larger files you run into issues of scale. If you have many users all uploading large files you are liable to run out of ram (where the file is stored before moving it to disk), or disk space itself.

For your HTML, it should look something like this :

<form method="post" enctype="multipart/form-data" action="/Upload">
    <div>
        <p>Upload one or more files using this form:</p>
        <input type="file" name="files" />
    </div>
    <div>
         <input type="submit" value="Upload" />
    </div>
</form>

The biggest thing to note is that the the encoding type is set to “multipart/form-data”, if this is not set then you will go crazy trying to hunt down why your file is showing up in your controller.

Your controller action is actually very simple. It will look something like :

[HttpPost]
public IActionResult Index(List<IFormFile> files)
{
	//Do something with the files here. 
	return Ok();
}

Note that the name of the parameter “files” should match the name on the input in HTML.

Other than that, you are there and done. There is nothing more than you need to do.

Streaming Files (Large Files)

Instead of buffering the file in its entirety, you can stream the file upload. This does introduce challenges as you can no longer use the built in model binding of ASP.NET core. Various tutorials out there show you how to get things working with massive pieces of code, but I’ll give you a helper class that should alleviate most of the work. Most of this work is taken from Microsoft’s tutorial on file uploads here. Unfortunately it’s a bit all over the place with helper classes that you need to dig around the web for.

First, take this helper class and stick it in your project. This code is taken from a Microsoft project here.

public static class MultipartRequestHelper
{
    // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
    // The spec says 70 characters is a reasonable limit.
    public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
    {
        //var boundary = Microsoft.Net.Http.Headers.HeaderUtilities.RemoveQuotes(contentType.Boundary);// .NET Core <2.0
        var boundary = Microsoft.Net.Http.Headers.HeaderUtilities.RemoveQuotes(contentType.Boundary).Value; //.NET Core 2.0
        if (string.IsNullOrWhiteSpace(boundary))
        {
            throw new InvalidDataException("Missing content-type boundary.");
        }

        if (boundary.Length > lengthLimit)
        {
            throw new InvalidDataException(
                $"Multipart boundary length limit {lengthLimit} exceeded.");
        }

        return boundary;
    }

    public static bool IsMultipartContentType(string contentType)
    {
        return !string.IsNullOrEmpty(contentType)
                && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
    }

    public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
    {
        // Content-Disposition: form-data; name="key";
        return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && string.IsNullOrEmpty(contentDisposition.FileName.Value) // For .NET Core <2.0 remove ".Value"
                && string.IsNullOrEmpty(contentDisposition.FileNameStar.Value); // For .NET Core <2.0 remove ".Value"
        }

    public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
    {
        // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
        return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && (!string.IsNullOrEmpty(contentDisposition.FileName.Value) // For .NET Core <2.0 remove ".Value"
                    || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value)); // For .NET Core <2.0 remove ".Value"
    }
}

Next, you can need this extension class which again is taken from Microsoft code but moved into a helper so that it can be reused and it allows your controllers to be slightly cleaner. It takes an input stream which is where your file will be written to. This is kept as a basic stream because the stream can really come from anywhere. It could be a file on the local server, or a stream to AWS/Azure etc

public static class FileStreamingHelper
{
	private static readonly FormOptions _defaultFormOptions = new FormOptions();

	public static async Task<FormValueProvider> StreamFile(this HttpRequest request, Stream targetStream)
	{
		if (!MultipartRequestHelper.IsMultipartContentType(request.ContentType))
		{
			throw new Exception($"Expected a multipart request, but got {request.ContentType}");
		}

		// Used to accumulate all the form url encoded key value pairs in the 
		// request.
		var formAccumulator = new KeyValueAccumulator();
		string targetFilePath = null;

		var boundary = MultipartRequestHelper.GetBoundary(
			MediaTypeHeaderValue.Parse(request.ContentType),
			_defaultFormOptions.MultipartBoundaryLengthLimit);
		var reader = new MultipartReader(boundary, request.Body);

		var section = await reader.ReadNextSectionAsync();
		while (section != null)
		{
			ContentDispositionHeaderValue contentDisposition;
			var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition);

			if (hasContentDispositionHeader)
			{
				if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
				{
					await section.Body.CopyToAsync(targetStream);
				}
				else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition))
				{
					// Content-Disposition: form-data; name="key"
					//
					// value

					// Do not limit the key name length here because the 
					// multipart headers length limit is already in effect.
					var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name);
					var encoding = GetEncoding(section);
					using (var streamReader = new StreamReader(
						section.Body,
						encoding,
						detectEncodingFromByteOrderMarks: true,
						bufferSize: 1024,
						leaveOpen: true))
					{
						// The value length limit is enforced by MultipartBodyLengthLimit
						var value = await streamReader.ReadToEndAsync();
						if (String.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase))
						{
							value = String.Empty;
						}
						formAccumulator.Append(key.Value, value); // For .NET Core <2.0 remove ".Value" from key

						if (formAccumulator.ValueCount > _defaultFormOptions.ValueCountLimit)
						{
							throw new InvalidDataException($"Form key count limit {_defaultFormOptions.ValueCountLimit} exceeded.");
						}
					}
				}
			}

			// Drains any remaining section body that has not been consumed and
			// reads the headers for the next section.
			section = await reader.ReadNextSectionAsync();
		}

		// Bind form data to a model
		var formValueProvider = new FormValueProvider(
			BindingSource.Form,
			new FormCollection(formAccumulator.GetResults()),
			CultureInfo.CurrentCulture);

		return formValueProvider;
	}

	private static Encoding GetEncoding(MultipartSection section)
	{
		MediaTypeHeaderValue mediaType;
		var hasMediaTypeHeader = MediaTypeHeaderValue.TryParse(section.ContentType, out mediaType);
		// UTF-7 is insecure and should not be honored. UTF-8 will succeed in 
		// most cases.
		if (!hasMediaTypeHeader || Encoding.UTF7.Equals(mediaType.Encoding))
		{
			return Encoding.UTF8;
		}
		return mediaType.Encoding;
	}
}

Now, you actually need to create a custom action attribute that completely disables form binding. This is important otherwise C# will still try and load the contents of the request regardless. The attribute looks like :

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var formValueProviderFactory = context.ValueProviderFactories
            .OfType<FormValueProviderFactory>()
            .FirstOrDefault();
        if (formValueProviderFactory != null)
        {
            context.ValueProviderFactories.Remove(formValueProviderFactory);
        }

        var jqueryFormValueProviderFactory = context.ValueProviderFactories
            .OfType<JQueryFormValueProviderFactory>()
            .FirstOrDefault();
        if (jqueryFormValueProviderFactory != null)
        {
            context.ValueProviderFactories.Remove(jqueryFormValueProviderFactory);
        }
    }

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }
}

Next, your controller should look similar to the following. We create a stream and pass it into the StreamFile extension method. The output is our FormValueProvider which we use to bind out model manually after the file streaming. Remember to put your custom attribute on the action to force the request to not bind.

[HttpPost]
[DisableFormValueModelBinding]
public async Task<IActionResult> Index()
{
	FormValueProvider formModel;
	using (var stream = System.IO.File.Create("c:\\temp\\myfile.temp"))
	{
		formModel = await Request.StreamFile(stream);
	}

	var viewModel = new MyViewModel();

	var bindingSuccessful = await TryUpdateModelAsync(viewModel, prefix: "",
	   valueProvider: formModel);

	if (!bindingSuccessful)
	{
		if (!ModelState.IsValid)
		{
			return BadRequest(ModelState);
		}
	}

	return Ok(viewModel);
}

In my particular example I have created a stream that writes to a temp file, but obviously you can do anything you want with it. The model I am using is the following :

public class MyViewModel
{
	public string Username { get; set; }
}

Again, nothing special. It’s just showing you how to bind a view model even when you are streaming files.

My HTML form is pretty standard :

<form method="post" enctype="multipart/form-data" action="/Upload">
    <div>
        <p>Upload one or more files using this form:</p>
        <input type="file" name="files" multiple />
    </div>
    <div>
        <p>Your Username</p>
        <input type="text" name="username" />
    </div>
    <div>
         <input type="submit" value="Upload" />
    </div>
</form>

And that’s all there is to it. Now when I upload a file, it is streamed straight to my target stream which could be an upload stream to AWS/Azure or any other cloud provider, and I still managed to get my view model out too. The biggest downside is ofcourse that the viewmodel details are not available until the file has been streamed. What this means is that if there is something in the viewmodel that would normally determine where the file goes, or the name etc, this is not available to you without a bit of rejigging (But it’s definitely doable).

44 thoughts on “Uploading Files In ASP.NET Core”

  1. I came here from your comment in docs.microsoft.com for file uploads. Looking through what you’ve pieced together here, I notice the lack of the antiforgery token attribute and the disabling of the binding attribute. Was that intentional as they are probably a good thing but not necessary? I see you discuss the binding issue but no attribute. And testing your code here appears to work just fine without it.
    Thanks!

    Reply
    • Woops. Two things, you do need the disabling of binding attribute (This is very important otherwise the form is bound on the request anyway, will fix this, I had it earlier but didn’t copy it across into the blog post!). Secondly, the antiforgery token can be used here regardless as long as it’s sent in a header rather than the body (If you are using something like Angular you are likely doing this already).

      Reply
  2. The problem of FileStreamingHelper.StreamFile method that it doesn’t take into account case when Multipart HTTP request contain several files inside form. Right now it has single Stream as input parameter.

    Reply
    • Hey Alex

      That was my issue as well. All you need to do is not to pass in a target stream and loop through the files in the request in the FileStream method. I do this:

      FileMultipartSection currentFile = section.AsFileSection();
      string filePath = Path.Combine(targetFilePath,"NEW-" + currentFile.FileName);
                              
      using (var targetStream = File.Create(filePath))
      {
          await section.Body.CopyToAsync(targetStream).ConfigureAwait(false);
      }
      

      If you want to cater for different store locations create overloads of the method. I think that would be the best

      Reply
    • Hi Dan,

      All the source code is contained in this post, but let me know if you have any particular issues getting up and running.

      Reply
  3. Does this mean that the HTTP request will be formed in such a way that one large file will be divided into multiple sections in request body and the server side code will try and get one section (part of a large file) at a time from the client (browser) ?

    Reply
  4. “What this means is that if there is something in the viewmodel that would normally determine where the file goes, or the name etc, this is not available to you without a bit of rejigging (But it’s definitely doable).”

    Any tips about how I would do this?

    Reply
    • Any help with this?
      The code works great and I am able to upload files; however, I need to be able to rename the file to the original filename.
      Thanks

      Reply
  5. Hi Wade,
    Thanks for this tutorial 🙂

    I have an error and don’t manage to know why :
    IOException: Unexpected end of Stream, the content may have already been read by another component.
    It happens in the FileStreamingHelper when I try to do the reader.ReadNextSectionAsync.

    I only use a simple dotnet core 2 razor pages application.
    Can you help me to resolve this please ?

    Reply
      • Hi,
        Got same problem I use MVC Controller. (.net core 1.1)
        I check DisableFormValueModelBinding attribute is call before controller.
        but it not work.

        Reply
      • I post by Postman like this

        POST /api/UserManage/UploadUserProfile HTTP/1.1
        Host: localhost:4661
        Content-Type: multipart/form-data; boundary=—-WebKitFormBoundary7MA4YWxkTrZu0gW
        Cache-Control: no-cache
        Postman-Token: 648876bd-d815-5531-a781-1ffcad5aaf54

        ——WebKitFormBoundary7MA4YWxkTrZu0gW
        Content-Disposition: form-data; name=”
        “; filename=”239657-2.jpg”
        Content-Type: image/jpeg

        ——WebKitFormBoundary7MA4YWxkTrZu0gW–

        Reply
  6. Thanks for this post, I notice though that there are no using statements for your code so I am not sure what namespaces I need to pull from to get this to work, I gather the ones from the GitHub on Microsoft aspnet core should work? . I too spent some time looking at the spaghetti that is supposed to help on the fileuploads in aspnetcore documentation. I am guessing that I will need some angular pieces to get this to work properly, where can I find an example of how to put it all together? I need to build a file upload as part of a project I am working on, once the files are uploaded I have to run some processes on them for publishing but this is a large part of what I need to get my project working.

    Reply
  7. API has changed the return value for HeaderUtilities.RemoveQuotes.

    ASP.NET Core < 2.0: Returns String https://docs.microsoft.com/en-us/dotnet/api/microsoft.net.http.headers.headerutilities.removequotes?view=aspnetcore-1.1

    ASP.NET Core 2.0: Returns StringSegment https://docs.microsoft.com/en-us/dotnet/api/microsoft.net.http.headers.headerutilities.removequotes?view=aspnetcore-2.0

    Add ".Value" where the build breaks – HasValue check not explicitly needed because all those instances of StringSegment are already in string.IsNullOrEmpty() checks.

    Reply
    • Thanks for that! I’ve gone through and added “.Value” where needed (On a side note, how annoying is that change!). And left comments incase someone gives this a go on .NET Core <2.0

      Reply
  8. Just like the Microsoft guide that I came from you give the source code for a class but don’t include the using’s at the top of the class.
    Like the MultipartRequestHelper class there are 2 options for MediaTypeHeaderValue (either System.Net.Http.Headers or Microsoft.Net.Http.Headers), I don’t know which one to use.

    I get an error on this line “if (string.IsNullOrWhiteSpace(boundary))” saying Cannot convert from “Microsoft.Extensions.Primitives.StringSegment to string plus other errors with the class.

    Reply
    • Hi Paul,

      For that particular header, it’s supposed to use Microsoft.Net.Http.Headers. I’ve added in the full namespace to the call to make it easier.

      For the second part of your comment, Microsoft recently updated a couple of API’s so they no longer return strings. I’ve updated the code to reflect this and left comments incase someone is still running .NET Core 1.X.

      Reply
  9. Is there a way to get the file name prior to streaming? It would be neater to have that so as not to create a temp file that requires renaming later.

    Reply
    • What I found is this:

                  var webRoot = _env.ContentRootPath;
                  var uploadPath = webRoot + "\\uploads";
      
                  var filename = Request.Form.Files[0].FileName;
                  using (var stream = System.IO.File.Create(uploadPath + "\\" + filename))
                  {
                      formModel = await Request.StreamFile(stream);
                  }

      But using this creates the following error:

      IOException: Unexpected end of Stream, the content may have already been read by another component at

      83 (var section = await reader.ReadNextSectionAsync();)

      The error occurs even if I change the extension of filename to .temp.

      Reply
      • I’ve narrowed it down to this:

        var filename = Request.Form.Files[0].FileName;

        Accessing Request before creating the stream creates the error, which would make sense. Is there another way to get the filename ahead of time?

        Reply
  10. Hi, in case of using ReactJS to post both File and other properties like string and/or int, how to receive these values in ASP.Net Core 2.0 controller action? Thanks.

    Reply
  11. In ASP.NET Core 2.0 I am using Code as follows:

    JavaScript:

    function uploadCaptureFileViaStreaming() {
        console.log("streaming active...");
        window.$("#myInput").click();
    }

    C#-Controller:

    [HttpPost("UploadCaptureFileViaStreaming")]
            [AuxiliaryFunctions.DisableFormValueModelBinding]
            public async Task UploadCaptureFileViaStreaming()
            {
                // Specify paths.
                string fileName = "myFile.pcap";
                var pathToFile = Path.Combine(PathToTempFolder, fileName);
    
                FormValueProvider formModel;
                using (var stream = System.IO.File.Create(pathToFile))
                {
                    formModel = await Request.StreamFile(stream);
                }
    
                var viewModel = new MyViewModel();
    
                var bindingSuccessful = await TryUpdateModelAsync(viewModel, prefix: "",
                    valueProvider: formModel);
    
                if (!bindingSuccessful)
                {
                    if (!ModelState.IsValid)
                    {
                        return BadRequest(ModelState);
                    }
                }
    
                return Ok(viewModel);
            } 

    I am able to upload (stream) large files this way, but how can I use AJAX to avoid reloading of the main Index site?

    Reply
  12. After reading all the comments, can I be sure, the code above is updated with latest changes to work with .net core 1.1, so that I can plug and play

    Reply
  13. net core 2.1 razor pages here, I get an error :

    System.IO.IOException: Unexpected end of Stream, the content may have already been read by another component. How to fix this, please

    Reply
  14. How do you pass the viewmodel data, and use that to set the path to save the files??
    There are many uses for this – including checking for the user’s max upload limit, save directory, etc…

    Reply
  15. What if I want this to be a pure REST API and upload my 200MB file using POSTMAN for eg. How will the controller look then? Could you please help?

    Reply
    • Hi Shane,

      For a file that large, you probably want to stream the file (So the second example). Have a go and if you get stuck you are welcome to email (wade at dotnetcoretutorials dot com) with a Github repo of where you are up to and I can take a look 🙂

      Reply
  16. Hi! Thank you very much for this code, very useful!
    I have a question: how to get the original file name (the name before it is uploaded/streamed)?

    Reply
  17. Hi Wade, Just wanted to say… what an awesome article (THANK YOU). I ‘waded’ (no pun intended) through quite a few other articles before seeing this one. I have to say, I don’t understand everything fully, but plugged this code into my project and it just seemed to work with no issues (The only thing I needed to do was add a route for my API) !!! You’re a star!

    Reply
  18. Yes, Thank you! I’m using your code in an MVC Core 2.2 project without problem. I modified the StreamFile() extension into a StreamFiles(), plural, extension that saves different file sections into different temp files. I need to figure out how to get the filename for each so I can add the code that moves them to our file server once streamed up. I also added a static file count variable to the extension class and a FileCount() method so the POST method can retrieve how many files were uploaded etc. I added try-catch to the POST method for appropriate logging and result feedback to the UI as well. Your work above was AWESOME in fast-tracking my project!

    Reply
    • I’m currently using
      string fileName = HttpUtility.UrlEncode(disposition.FileName.Value.Replace(“/”, “”), Encoding.UTF8);
      to get the file name, and
      section.ContentType;
      to get the content type. How did you set up your code to cycle through multiple upload files?

      Reply
  19. As of .NET Core 3.0, DisableFormValueModelBindingAttribute will not work as shown in this example. OnResourceExecuting() needs to be updated as follows, otherwise the binding exception still occurs:

    public void OnResourceExecuting(ResourceExecutingContext context)
    {
    	var factories = context.ValueProviderFactories;
    	factories.RemoveType();
    	factories.RemoveType();
    	factories.RemoveType();
    }
    
    Reply
    • above core 3.0 you need add context.HttpContext.Request.EnableBuffering();

      using System;
      using System.Linq;
      using Microsoft.AspNetCore.Http;
      using Microsoft.AspNetCore.Mvc.Filters;
      using Microsoft.AspNetCore.Mvc.ModelBinding;
      
      namespace Planned.MVC.Filters
      {
          #region snippet_DisableFormValueModelBindingAttribute
          [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
          public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
          {
              public void OnResourceExecuting(ResourceExecutingContext context)
              {
                  context.HttpContext.Request.EnableBuffering();
      
                  var factories = context.ValueProviderFactories;
                  factories.RemoveType();
                  factories.RemoveType();
                  factories.RemoveType();
              }
      
              public void OnResourceExecuted(ResourceExecutedContext context)
              {
              }
          }
          #endregion
      }
      
      Reply

Leave a Comment