Skip to main content

ASP.NET Core supports uploading one or more files using buffered model binding for smaller files and unbuffered streaming for larger files.

// ----------------------------------------------------------------------------
// AppFile.cs
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Models/AppFile.cs
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Models/AppFile.cs
// ----------------------------------------------------------------------------

using System;
using System.ComponentModel.DataAnnotations;

namespace SampleApp.Models
{
    public class AppFile
    {
        public int Id { get; set; }

        public byte[] Content { get; set; }

        [Display(Name = "File Name")]
        public string UntrustedName { get; set; }

        [Display(Name = "Note")]
        public string Note { get; set; }

        [Display(Name = "Size (bytes)")]
        [DisplayFormat(DataFormatString = "{0:N0}")]
        public long Size { get; set; }

        [Display(Name = "Uploaded (UTC)")]
        [DisplayFormat(DataFormatString = "{0:G}")]
        public DateTime UploadDT { get; set; }
    }
}

// ----------------------------------------------------------------------------
// SampleAppContext.cs
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Data/SampleAppContext.cs
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Data/SampleAppContext.cs
// ----------------------------------------------------------------------------

using Microsoft.EntityFrameworkCore;
using SampleApp.Models;

namespace SampleApp.Data
{
    public class AppDbContext : DbContext
    {
        public AppDbContext (DbContextOptions<AppDbContext> options)
            : base(options)
        {
        }

        public DbSet<AppFile> File { get; set; }
    }
}

// ----------------------------------------------------------------------------
// FileHelpers.cs
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Utilities/FileHelpers.cs
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Utilities/FileHelpers.cs
// ----------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Net.Http.Headers;

namespace SampleApp.Utilities
{
    public static class FileHelpers
    {
        // If you require a check on specific characters in the IsValidFileExtensionAndSignature
        // method, supply the characters in the _allowedChars field.
        private static readonly byte[] _allowedChars = { };
        // For more file signatures, see the File Signatures Database (https://www.filesignatures.net/)
        // and the official specifications for the file types you wish to add.
        private static readonly Dictionary<string, List<byte[]>> _fileSignature = new Dictionary<string, List<byte[]>>
        {
            { ".gif", new List<byte[]> { new byte[] { 0x47, 0x49, 0x46, 0x38 } } },
            { ".png", new List<byte[]> { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } } },
            { ".jpeg", new List<byte[]>
                {
                    new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
                    new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
                    new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
                }
            },
            { ".jpg", new List<byte[]>
                {
                    new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
                    new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 },
                    new byte[] { 0xFF, 0xD8, 0xFF, 0xE8 },
                }
            },
            { ".zip", new List<byte[]>
                {
                    new byte[] { 0x50, 0x4B, 0x03, 0x04 },
                    new byte[] { 0x50, 0x4B, 0x4C, 0x49, 0x54, 0x45 },
                    new byte[] { 0x50, 0x4B, 0x53, 0x70, 0x58 },
                    new byte[] { 0x50, 0x4B, 0x05, 0x06 },
                    new byte[] { 0x50, 0x4B, 0x07, 0x08 },
                    new byte[] { 0x57, 0x69, 0x6E, 0x5A, 0x69, 0x70 },
                }
            },
        };

        // **WARNING!**
        // In the following file processing methods, the file's content isn't scanned.
        // In most production scenarios, an anti-virus/anti-malware scanner API is
        // used on the file before making the file available to users or other
        // systems. For more information, see the topic that accompanies this sample
        // app.

        public static async Task<byte[]> ProcessFormFile<T>(IFormFile formFile,
            ModelStateDictionary modelState, string[] permittedExtensions,
            long sizeLimit)
        {
            var fieldDisplayName = string.Empty;

            // Use reflection to obtain the display name for the model
            // property associated with this IFormFile. If a display
            // name isn't found, error messages simply won't show
            // a display name.
            MemberInfo property = typeof(T).GetProperty(
                formFile.Name.Substring(formFile.Name.IndexOf(".",
                StringComparison.Ordinal) + 1));

            if (property != null)
            {
                if (property.GetCustomAttribute(typeof(DisplayAttribute)) is DisplayAttribute displayAttribute)
                {
                    fieldDisplayName = $"{displayAttribute.Name} ";
                }
            }

            // Don't trust the file name sent by the client. To display
            // the file name, HTML-encode the value.
            var trustedFileNameForDisplay = WebUtility.HtmlEncode(formFile.FileName);

            // Check the file length. This check doesn't catch files that only have
            // a BOM as their content.
            if (formFile.Length == 0)
            {
                modelState.AddModelError(formFile.Name,
                    $"{fieldDisplayName}({trustedFileNameForDisplay}) is empty.");

                return new byte[0];
            }

            if (formFile.Length > sizeLimit)
            {
                var megabyteSizeLimit = sizeLimit / 1048576;
                modelState.AddModelError(formFile.Name,
                    $"{fieldDisplayName}({trustedFileNameForDisplay}) exceeds " +
                    $"{megabyteSizeLimit:N1} MB.");

                return new byte[0];
            }

            try
            {
                using (var memoryStream = new MemoryStream())
                {
                    await formFile.CopyToAsync(memoryStream);

                    // Check the content length in case the file's only
                    // content was a BOM and the content is actually
                    // empty after removing the BOM.
                    if (memoryStream.Length == 0)
                    {
                        modelState.AddModelError(formFile.Name,
                            $"{fieldDisplayName}({trustedFileNameForDisplay}) is empty.");
                    }

                    if (!IsValidFileExtensionAndSignature(
                        formFile.FileName, memoryStream, permittedExtensions))
                    {
                        modelState.AddModelError(formFile.Name,
                            $"{fieldDisplayName}({trustedFileNameForDisplay}) file " +
                            "type isn't permitted or the file's signature " +
                            "doesn't match the file's extension.");
                    }
                    else
                    {
                        return memoryStream.ToArray();
                    }
                }
            }
            catch (Exception ex)
            {
                modelState.AddModelError(formFile.Name,
                    $"{fieldDisplayName}({trustedFileNameForDisplay}) upload failed. " +
                    $"Please contact the Help Desk for support. Error: {ex.HResult}");
                // Log the exception
            }

            return new byte[0];
        }

        public static async Task<byte[]> ProcessStreamedFile(
            MultipartSection section, ContentDispositionHeaderValue contentDisposition,
            ModelStateDictionary modelState, string[] permittedExtensions, long sizeLimit)
        {
            try
            {
                using (var memoryStream = new MemoryStream())
                {
                    await section.Body.CopyToAsync(memoryStream);

                    // Check if the file is empty or exceeds the size limit.
                    if (memoryStream.Length == 0)
                    {
                        modelState.AddModelError("File", "The file is empty.");
                    }
                    else if (memoryStream.Length > sizeLimit)
                    {
                        var megabyteSizeLimit = sizeLimit / 1048576;
                        modelState.AddModelError("File",
                        $"The file exceeds {megabyteSizeLimit:N1} MB.");
                    }
                    else if (!IsValidFileExtensionAndSignature(
                        contentDisposition.FileName.Value, memoryStream,
                        permittedExtensions))
                    {
                        modelState.AddModelError("File",
                            "The file type isn't permitted or the file's " +
                            "signature doesn't match the file's extension.");
                    }
                    else
                    {
                        return memoryStream.ToArray();
                    }
                }
            }
            catch (Exception ex)
            {
                modelState.AddModelError("File",
                    "The upload failed. Please contact the Help Desk " +
                    $" for support. Error: {ex.HResult}");
                // Log the exception
            }

            return new byte[0];
        }

        private static bool IsValidFileExtensionAndSignature(string fileName, Stream data, string[] permittedExtensions)
        {
            if (string.IsNullOrEmpty(fileName) || data == null || data.Length == 0)
            {
                return false;
            }

            var ext = Path.GetExtension(fileName).ToLowerInvariant();

            if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
            {
                return false;
            }

            data.Position = 0;

            using (var reader = new BinaryReader(data))
            {
                if (ext.Equals(".txt") || ext.Equals(".csv") || ext.Equals(".prn"))
                {
                    if (_allowedChars.Length == 0)
                    {
                        // Limits characters to ASCII encoding.
                        for (var i = 0; i < data.Length; i++)
                        {
                            if (reader.ReadByte() > sbyte.MaxValue)
                            {
                                return false;
                            }
                        }
                    }
                    else
                    {
                        // Limits characters to ASCII encoding and
                        // values of the _allowedChars array.
                        for (var i = 0; i < data.Length; i++)
                        {
                            var b = reader.ReadByte();
                            if (b > sbyte.MaxValue || !_allowedChars.Contains(b))
                            {
                                return false;
                            }
                        }
                    }

                    return true;
                }

                // Uncomment the following code block if you must permit
                // files whose signature isn't provided in the _fileSignature
                // dictionary. We recommend that you add file signatures
                // for files (when possible) for all file types you intend
                // to allow on the system and perform the file signature
                // check.
                /*
                if (!_fileSignature.ContainsKey(ext))
                {
                    return true;
                }
                */

                // File signature check
                // --------------------
                // With the file signatures provided in the _fileSignature
                // dictionary, the following code tests the input content's
                // file signature.
                var signatures = _fileSignature[ext];
                var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));

                return signatures.Any(signature => headerBytes.Take(signature.Length).SequenceEqual(signature));
            }
        }
    }
}

// ----------------------------------------------------------------------------
// MultipartRequestHelper.cs
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Utilities/MultipartRequestHelper.cs
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Utilities/MultipartRequestHelper.cs
// ----------------------------------------------------------------------------

using System;
using System.IO;
using Microsoft.Net.Http.Headers;

namespace SampleApp.Utilities
{
    public static class MultipartRequestHelper
    {
        // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
        // The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit.
        public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
        {
            var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;

            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)
                && string.IsNullOrEmpty(contentDisposition.FileNameStar.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)
                    || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
        }
    }
}

// ----------------------------------------------------------------------------
// Antiforgery.cs
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Filters/Antiforgery.cs
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Filters/Antiforgery.cs
// ----------------------------------------------------------------------------

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;

namespace SampleApp.Filters
{
    public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
    {
        public override void OnResultExecuting(ResultExecutingContext context)
        {
            var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();

            // Send the request token as a JavaScript-readable cookie
            var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);

            context.HttpContext.Response.Cookies.Append(
                "RequestVerificationToken",
                tokens.RequestToken,
                new CookieOptions() { HttpOnly = false });
        }

        public override void OnResultExecuted(ResultExecutedContext context)
        {
        }
    }
}

// ----------------------------------------------------------------------------
// ModelBinding.cs
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Filters/ModelBinding.cs
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Filters/ModelBinding.cs
// ----------------------------------------------------------------------------

using System;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace SampleApp.Filters
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
    {
        public void OnResourceExecuting(ResourceExecutingContext context)
        {
            var factories = context.ValueProviderFactories;
            factories.RemoveType<FormValueProviderFactory>();
            factories.RemoveType<FormFileValueProviderFactory>();
            factories.RemoveType<JQueryFormValueProviderFactory>();
        }

        public void OnResourceExecuted(ResourceExecutedContext context)
        {
        }
    }
}

// ----------------------------------------------------------------------------
// StreamingController.cs
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Controllers/StreamingController.cs
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Controllers/StreamingController.cs
// ----------------------------------------------------------------------------

using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using SampleApp.Data;
using SampleApp.Filters;
using SampleApp.Models;
using SampleApp.Utilities;

namespace SampleApp.Controllers
{
    public class StreamingController : Controller
    {
        private readonly AppDbContext _context;
        private readonly long _fileSizeLimit;
        private readonly ILogger<StreamingController> _logger;
        private readonly string[] _permittedExtensions = { ".txt" };
        private readonly string _targetFilePath;

        // Get the default form options so that we can use them to set the default
        // limits for request body data.
        private static readonly FormOptions _defaultFormOptions = new FormOptions();

        public StreamingController(ILogger<StreamingController> logger,
            AppDbContext context, IConfiguration config)
        {
            _logger = logger;
            _context = context;
            _fileSizeLimit = config.GetValue<long>("FileSizeLimit");

            // To save physical files to a path provided by configuration:
            _targetFilePath = config.GetValue<string>("StoredFilesPath");

            // To save physical files to the temporary files folder, use:
            //_targetFilePath = Path.GetTempPath();
        }

        // The following upload methods:
        //
        // 1. Disable the form value model binding to take control of handling
        //    potentially large files.
        //
        // 2. Typically, antiforgery tokens are sent in request body. Since we
        //    don't want to read the request body early, the tokens are sent via
        //    headers. The antiforgery token filter first looks for tokens in
        //    the request header and then falls back to reading the body.

        #region snippet_UploadDatabase
        [HttpPost]
        [DisableFormValueModelBinding]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> UploadDatabase()
        {
            if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
            {
                ModelState.AddModelError("File",
                    $"The request couldn't be processed (Error 1).");
                // Log error

                return BadRequest(ModelState);
            }

            // Accumulate the form data key-value pairs in the request (formAccumulator).
            var formAccumulator = new KeyValueAccumulator();
            var trustedFileNameForDisplay = string.Empty;
            var untrustedFileNameForStorage = string.Empty;
            var streamedFileContent = new byte[0];

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

            var section = await reader.ReadNextSectionAsync();

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

                if (hasContentDispositionHeader)
                {
                    if (MultipartRequestHelper
                        .HasFileContentDisposition(contentDisposition))
                    {
                        untrustedFileNameForStorage = contentDisposition.FileName.Value;
                        // Don't trust the file name sent by the client. To display
                        // the file name, HTML-encode the value.
                        trustedFileNameForDisplay = WebUtility.HtmlEncode(
                                contentDisposition.FileName.Value);

                        streamedFileContent =
                            await FileHelpers.ProcessStreamedFile(section, contentDisposition,
                                ModelState, _permittedExtensions, _fileSizeLimit);

                        if (!ModelState.IsValid)
                        {
                            return BadRequest(ModelState);
                        }
                    }
                    else if (MultipartRequestHelper
                        .HasFormDataContentDisposition(contentDisposition))
                    {
                        // Don't limit the key name length because the
                        // multipart headers length limit is already in effect.
                        var key = HeaderUtilities
                            .RemoveQuotes(contentDisposition.Name).Value;
                        var encoding = GetEncoding(section);

                        if (encoding == null)
                        {
                            ModelState.AddModelError("File",
                                $"The request couldn't be processed (Error 2).");
                            // Log error

                            return BadRequest(ModelState);
                        }

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

                            if (formAccumulator.ValueCount >
                                _defaultFormOptions.ValueCountLimit)
                            {
                                // Form key count limit of
                                // _defaultFormOptions.ValueCountLimit
                                // is exceeded.
                                ModelState.AddModelError("File",
                                    $"The request couldn't be processed (Error 3).");
                                // Log error

                                return BadRequest(ModelState);
                            }
                        }
                    }
                }

                // Drain any remaining section body that hasn't been consumed and
                // read the headers for the next section.
                section = await reader.ReadNextSectionAsync();
            }

            // Bind form data to the model
            var formData = new FormData();
            var formValueProvider = new FormValueProvider(
                BindingSource.Form,
                new FormCollection(formAccumulator.GetResults()),
                CultureInfo.CurrentCulture);
            var bindingSuccessful = await TryUpdateModelAsync(formData, prefix: "",
                valueProvider: formValueProvider);

            if (!bindingSuccessful)
            {
                ModelState.AddModelError("File",
                    "The request couldn't be processed (Error 5).");
                // Log error

                return BadRequest(ModelState);
            }

            // **WARNING!**
            // In the following example, the file is saved without
            // scanning the file's contents. In most production
            // scenarios, an anti-virus/anti-malware scanner API
            // is used on the file before making the file available
            // for download or for use by other systems.
            // For more information, see the topic that accompanies
            // this sample app.

            var file = new AppFile()
            {
                Content = streamedFileContent,
                UntrustedName = untrustedFileNameForStorage,
                Note = formData.Note,
                Size = streamedFileContent.Length,
                UploadDT = DateTime.UtcNow
            };

            _context.File.Add(file);
            await _context.SaveChangesAsync();

            return Created(nameof(StreamingController), null);
        }
        #endregion

        #region snippet_UploadPhysical
        [HttpPost]
        [DisableFormValueModelBinding]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> UploadPhysical()
        {
            if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
            {
                ModelState.AddModelError("File", $"The request couldn't be processed (Error 1).");
                // Log error

                return BadRequest(ModelState);
            }

            var boundary = MultipartRequestHelper.GetBoundary(
                MediaTypeHeaderValue.Parse(Request.ContentType),
                _defaultFormOptions.MultipartBoundaryLengthLimit);
            var reader = new MultipartReader(boundary, HttpContext.Request.Body);
            var section = await reader.ReadNextSectionAsync();

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

                if (hasContentDispositionHeader)
                {
                    // This check assumes that there's a file
                    // present without form data. If form data
                    // is present, this method immediately fails
                    // and returns the model error.
                    if (!MultipartRequestHelper
                        .HasFileContentDisposition(contentDisposition))
                    {
                        ModelState.AddModelError("File", $"The request couldn't be processed (Error 2).");
                        // Log error

                        return BadRequest(ModelState);
                    }
                    else
                    {
                        // Don't trust the file name sent by the client. To display
                        // the file name, HTML-encode the value.
                        var trustedFileNameForDisplay = WebUtility.HtmlEncode(
                                contentDisposition.FileName.Value);
                        var trustedFileNameForFileStorage = Path.GetRandomFileName();

                        // **WARNING!**
                        // In the following example, the file is saved without
                        // scanning the file's contents. In most production
                        // scenarios, an anti-virus/anti-malware scanner API
                        // is used on the file before making the file available
                        // for download or for use by other systems.
                        // For more information, see the topic that accompanies
                        // this sample.

                        var streamedFileContent = await FileHelpers.ProcessStreamedFile(
                            section, contentDisposition, ModelState,
                            _permittedExtensions, _fileSizeLimit);

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

                        using (var targetStream = System.IO.File.Create(
                            Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
                        {
                            await targetStream.WriteAsync(streamedFileContent);

                            _logger.LogInformation(
                                "Uploaded file '{TrustedFileNameForDisplay}' saved to " +
                                "'{TargetFilePath}' as {TrustedFileNameForFileStorage}",
                                trustedFileNameForDisplay, _targetFilePath,
                                trustedFileNameForFileStorage);
                        }
                    }
                }

                // Drain any remaining section body that hasn't been consumed and
                // read the headers for the next section.
                section = await reader.ReadNextSectionAsync();
            }

            return Created(nameof(StreamingController), null);
        }
        #endregion

        private static Encoding GetEncoding(MultipartSection section)
        {
            var hasMediaTypeHeader =
                MediaTypeHeaderValue.TryParse(section.ContentType, out var mediaType);

            // UTF-7 is insecure and shouldn't be honored. UTF-8 succeeds in
            // most cases.
            if (!hasMediaTypeHeader || Encoding.UTF7.Equals(mediaType.Encoding))
            {
                return Encoding.UTF8;
            }

            return mediaType.Encoding;
        }
    }

    public class FormData
    {
        public string Note { get; set; }
    }
}

// ----------------------------------------------------------------------------
// BufferedMultipleFileUploadPhysicalModel.cs
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/BufferedMultipleFileUploadPhysical.cshtml.cs
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/BufferedMultipleFileUploadPhysical.cshtml.cs
// ----------------------------------------------------------------------------

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Configuration;
using SampleApp.Utilities;

namespace SampleApp.Pages
{
    public class BufferedMultipleFileUploadPhysicalModel : PageModel
    {
        private readonly long _fileSizeLimit;
        private readonly string[] _permittedExtensions = { ".txt" };
        private readonly string _targetFilePath;

        public BufferedMultipleFileUploadPhysicalModel(IConfiguration config)
        {
            _fileSizeLimit = config.GetValue<long>("FileSizeLimit");

            // To save physical files to a path provided by configuration:
            _targetFilePath = config.GetValue<string>("StoredFilesPath");

            // To save physical files to the temporary files folder, use:
            //_targetFilePath = Path.GetTempPath();
        }

        [BindProperty]
        public BufferedMultipleFileUploadPhysical FileUpload { get; set; }

        public string Result { get; private set; }

        public void OnGet()
        {
        }

        public async Task<IActionResult> OnPostUploadAsync()
        {
            if (!ModelState.IsValid)
            {
                Result = "Please correct the form.";

                return Page();
            }

            foreach (var formFile in FileUpload.FormFiles)
            {
                var formFileContent =
                    await FileHelpers
                        .ProcessFormFile<BufferedMultipleFileUploadPhysical>(
                            formFile, ModelState, _permittedExtensions,
                            _fileSizeLimit);

                if (!ModelState.IsValid)
                {
                    Result = "Please correct the form.";

                    return Page();
                }

                // For the file name of the uploaded file stored
                // server-side, use Path.GetRandomFileName to generate a safe
                // random file name.
                var trustedFileNameForFileStorage = Path.GetRandomFileName();
                var filePath = Path.Combine(_targetFilePath, trustedFileNameForFileStorage);

                // **WARNING!**
                // In the following example, the file is saved without
                // scanning the file's contents. In most production
                // scenarios, an anti-virus/anti-malware scanner API
                // is used on the file before making the file available
                // for download or for use by other systems.
                // For more information, see the topic that accompanies
                // this sample.

                using (var fileStream = System.IO.File.Create(filePath))
                {
                    await fileStream.WriteAsync(formFileContent);

                    // To work directly with the FormFiles, use the following
                    // instead:
                    //await formFile.CopyToAsync(fileStream);
                }
            }

            return RedirectToPage("./Index");
        }
    }

    public class BufferedMultipleFileUploadPhysical
    {
        [Required]
        [Display(Name="File")]
        public List<IFormFile> FormFiles { get; set; }

        [Display(Name="Note")]
        [StringLength(50, MinimumLength = 0)]
        public string Note { get; set; }
    }
}

// ----------------------------------------------------------------------------
// BufferedMultipleFileUploadPhysical.cshtml
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/BufferedMultipleFileUploadPhysical.cshtml
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/BufferedMultipleFileUploadPhysical.cshtml
// ----------------------------------------------------------------------------

@page
@model BufferedMultipleFileUploadPhysicalModel
@{
    ViewData["Title"] = "Buffered Multiple File Upload (Physical)";
}

<h1>Upload multiple buffered files to physical storage with one file upload control</h1>

<p>One or more files can be selected by the visitor. The following form's page handler saves the file(s) to disk.</p>

<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFiles"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFiles" type="file" multiple />
            <span asp-validation-for="FileUpload.FormFiles"></span>
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload" />
</form>

<p class="result">
    @Model.Result
</p>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

// ----------------------------------------------------------------------------
// BufferedSingleFileUploadPhysical.cshtml.cs
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/BufferedSingleFileUploadPhysical.cshtml.cs
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/BufferedSingleFileUploadPhysical.cshtml.cs
// ----------------------------------------------------------------------------

using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Configuration;
using SampleApp.Utilities;

namespace SampleApp.Pages
{
    public class BufferedSingleFileUploadPhysicalModel : PageModel
    {
        private readonly long _fileSizeLimit;
        private readonly string[] _permittedExtensions = { ".txt" };
        private readonly string _targetFilePath;

        public BufferedSingleFileUploadPhysicalModel(IConfiguration config)
        {
            _fileSizeLimit = config.GetValue<long>("FileSizeLimit");

            // To save physical files to a path provided by configuration:
            _targetFilePath = config.GetValue<string>("StoredFilesPath");

            // To save physical files to the temporary files folder, use:
            //_targetFilePath = Path.GetTempPath();
        }

        [BindProperty]
        public BufferedSingleFileUploadPhysical FileUpload { get; set; }

        public string Result { get; private set; }

        public void OnGet()
        {
        }

        public async Task<IActionResult> OnPostUploadAsync()
        {
            if (!ModelState.IsValid)
            {
                Result = "Please correct the form.";

                return Page();
            }

            var formFileContent =
                await FileHelpers.ProcessFormFile<BufferedSingleFileUploadPhysical>(
                    FileUpload.FormFile, ModelState, _permittedExtensions,
                    _fileSizeLimit);

            if (!ModelState.IsValid)
            {
                Result = "Please correct the form.";

                return Page();
            }

            // For the file name of the uploaded file stored
            // server-side, use Path.GetRandomFileName to generate a safe
            // random file name.
            var trustedFileNameForFileStorage = Path.GetRandomFileName();
            var filePath = Path.Combine(_targetFilePath, trustedFileNameForFileStorage);

            // **WARNING!**
            // In the following example, the file is saved without
            // scanning the file's contents. In most production
            // scenarios, an anti-virus/anti-malware scanner API
            // is used on the file before making the file available
            // for download or for use by other systems.
            // For more information, see the topic that accompanies
            // this sample.

            using (var fileStream = System.IO.File.Create(filePath))
            {
                await fileStream.WriteAsync(formFileContent);

                // To work directly with a FormFile, use the following instead:
                //await FileUpload.FormFile.CopyToAsync(fileStream);
            }

            return RedirectToPage("./Index");
        }
    }

    public class BufferedSingleFileUploadPhysical
    {
        [Required]
        [Display(Name="File")]
        public IFormFile FormFile { get; set; }

        [Display(Name="Note")]
        [StringLength(50, MinimumLength = 0)]
        public string Note { get; set; }
    }
}

// ----------------------------------------------------------------------------
// BufferedSingleFileUploadPhysical.cshtml
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/BufferedSingleFileUploadPhysical.cshtml
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/BufferedSingleFileUploadPhysical.cshtml
// ----------------------------------------------------------------------------

@page
@model BufferedSingleFileUploadPhysicalModel
@{
    ViewData["Title"] = "Buffered Single File Upload (Physical)";
}

<h1>Upload one buffered file to physical storage with one file upload control</h1>

<p>The following form's page handler saves the file to disk.</p>

<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFile"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFile" type="file" />
            <span asp-validation-for="FileUpload.FormFile"></span>
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload" />
</form>

<p class="result">
    @Model.Result
</p>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

// ----------------------------------------------------------------------------
// _ValidationScriptsPartial.cshtml
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/Shared/_ValidationScriptsPartial.cshtml
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/Shared/_ValidationScriptsPartial.cshtml
// ----------------------------------------------------------------------------

<environment include="Development">
    <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</environment>

<environment exclude="Development">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.17.0/jquery.validate.min.js"
            asp-fallback-src="~/lib/jquery-validation/dist/jquery.validate.min.js"
            asp-fallback-test="window.jQuery && window.jQuery.validator"
            crossorigin="anonymous"
            integrity="sha256-F6h55Qw6sweK+t7SiOJX+2bpSAa3b/fnlrVCJvmEj1A=">
    </script>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.js"
            asp-fallback-src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"
            asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive"
            crossorigin="anonymous"
            integrity="sha256-9GycpJnliUjJDVDqP0UEu/bsm9U+3dnQUH8+3W10vkY=">
    </script>
</environment>

// ----------------------------------------------------------------------------
// _Layout.cshtml
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/Shared/_Layout.cshtml
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/Shared/_Layout.cshtml
// ----------------------------------------------------------------------------

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewData["Title"]</title>
    <style>body{margin:0;padding-bottom:20px;font-family:"Helvetica Neue", Helvetica, Arial, sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff;}h1{font-size:24px;margin:.67em 0;}pre{overflow:auto;}code,pre{font-family:monospace, monospace;font-size:1em;}input[type="text"]{width:50%;}button,input{margin:0;font:inherit;color:inherit;}button{overflow:visible;}button{text-transform:none;}:before,*:after{box-sizing:border-box;}a{color:#337ab7;text-decoration:none;}h1,h2,h3,h4{font-family:inherit;font-weight:500;line-height:1.1;color:inherit;margin-top:20px;margin-bottom:10px;}h3{font-size:16px;}h4{font-size:18px;}p{margin:0 0 10px;}ol{margin-top:0;margin-bottom:10px;}code,pre{font-family:Menlo, Monaco, Consolas, "Courier New", monospace;}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px;}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;-radius:4px;}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0;}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto;}.container{width:800px;}.btn{display:inline-block;padding:6px 12px;margin:10px 5px;font-size:14px;font-weight:normal;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;background-image:none;border:1px solid transparent;border-radius:4px;;color:#000;background-color:#fff;border-color:gray;}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;box-shadow:0 1px 1px rgba(0, 0, 0, .05);}.panel-body{padding:15px;}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px;}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit;}.panel-default{border-color:#ddd;}.panel-default > .panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd;}.clearfix:before,.clearfix:after,.container:before,.container:after,.panel-body:before,.panel-body:after{display:table;content:" ";}.clearfix:after,.container:after,.panel-body:after{clear:both;}.body-content{padding-left:15px;padding-right:15px;}.panel-body{font-size:16px;}table{border-collapse:collapse;width:100%;}th,td{padding:8px;border-bottom:1px solid #ddd;}th{text-align:center;}dt{font-weight:bold;text-decoration:underline;}dd{margin:0;padding:0 0 0.5em 0;}.text-center{text-align:center}.field-validation-error{color:red;display:block}.result{color:red}</style>
</head>
<body>
    <nav>
        <div class="container">
            <ul>
                <li><a asp-page="/Index">Home</a></li>
                <li>
                    Database upload examples
                    <ul>
                        <li><a asp-page="/BufferedSingleFileUploadDb">Upload one buffered file with one file upload control</a></li>
                        <li><a asp-page="/BufferedMultipleFileUploadDb">Upload multiple buffered files with one file upload control</a></li>
                        <li><a asp-page="/BufferedDoubleFileUploadDb">Upload two buffered files with two file upload controls</a></li>
                        <li><a asp-page="/StreamedSingleFileUploadDb">Stream a file with AJAX to a controller endpoint</a></li>
                    </ul>
                </li>
                <li>
                    Physical storage upload examples
                    <ul>
                        <li><a asp-page="/BufferedSingleFileUploadPhysical">Upload one buffered file with one file upload control</a></li>
                        <li><a asp-page="/BufferedMultipleFileUploadPhysical">Upload multiple buffered files with one file upload control</a></li>
                        <li><a asp-page="/BufferedDoubleFileUploadPhysical">Upload two buffered files with two file upload controls</a></li>
                        <li><a asp-page="/StreamedSingleFileUploadPhysical">Stream a file with AJAX to a controller endpoint</a></li>
                    </ul>
                </li>

            </ul>
        </div>
    </nav>

    <div class="container body-content">
        @RenderBody()
    </div>

    <environment include="Development">
        <script src="~/lib/jquery/dist/jquery.js"></script>
    </environment>

    <environment exclude="Development">
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"
                asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
                asp-fallback-test="window.jQuery"
                crossorigin="anonymous"
                integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=">
        </script>
    </environment>

    @RenderSection("Scripts", required: false)
</body>
</html>

// ----------------------------------------------------------------------------
// DeletePhysicalFile.cshtml.cs
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/DeletePhysicalFile.cshtml.cs
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/DeletePhysicalFile.cshtml.cs
// ----------------------------------------------------------------------------

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.FileProviders;

namespace SampleApp.Pages
{
    public class DeletePhysicalFileModel : PageModel
    {
        private readonly IFileProvider _fileProvider;

        public DeletePhysicalFileModel(IFileProvider fileProvider)
        {
            _fileProvider = fileProvider;
        }

        public IFileInfo RemoveFile { get; private set; }

        public IActionResult OnGet(string fileName)
        {
            if (string.IsNullOrEmpty(fileName))
            {
                return RedirectToPage("/Index");
            }

            RemoveFile = _fileProvider.GetFileInfo(fileName);

            if (!RemoveFile.Exists)
            {
                return RedirectToPage("/Index");
            }

            return Page();
        }

        public IActionResult OnPost(string fileName)
        {
            if (string.IsNullOrEmpty(fileName))
            {
                return RedirectToPage("/Index");
            }

            RemoveFile = _fileProvider.GetFileInfo(fileName);

            if (RemoveFile.Exists)
            {
                System.IO.File.Delete(RemoveFile.PhysicalPath);
            }

            return RedirectToPage("./Index");
        }
    }
}

// ----------------------------------------------------------------------------
// StreamedSingleFileUploadPhysical.cshtml.cs
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/StreamedSingleFileUploadPhysical.cshtml.cs
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/StreamedSingleFileUploadPhysical.cshtml.cs
// ----------------------------------------------------------------------------

using Microsoft.AspNetCore.Mvc.RazorPages;

namespace SampleApp.Pages
{
    public class StreamedSingleFileUploadPhysicalModel : PageModel
    {
        public void OnGet()
        {
        }
    }
}

// ----------------------------------------------------------------------------
// StreamedSingleFileUploadPhysical.cshtml
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/StreamedSingleFileUploadPhysical.cshtml
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/StreamedSingleFileUploadPhysical.cshtml
// ----------------------------------------------------------------------------

@page
@model StreamedSingleFileUploadPhysicalModel
@{
    ViewData["Title"] = "Streamed Single File Upload with AJAX (Physical)";
}

<h1>Stream a file with AJAX to physical storage with a controller endpoint</h1>

<p>
    The following form's <code>action</code> points to a controller endpoint that only receives the file and saves it to disk.
    If additional form data is added to the form, the additional data is ignored by the controller action.
</p>

<form id="uploadForm" action="Streaming/UploadPhysical" method="post"
    enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return false;">
    <dl>
        <dt>
            <label for="file">File</label>
        </dt>
        <dd>
            <input id="file" type="file" name="file" />
        </dd>
    </dl>

    <input class="btn" type="submit" value="Upload" />

    <div style="margin-top:15px">
        <output form="uploadForm" name="result"></output>
    </div>
</form>

@section Scripts {
  <script>
    "use strict";
    async function AJAXSubmit (oFormElement) {
      const formData = new FormData(oFormElement);
      try {
        const response = await fetch(oFormElement.action, {
          method: 'POST',
          headers: {
            'RequestVerificationToken': getCookie('RequestVerificationToken')
          },
          body: formData
        });
        oFormElement.elements.namedItem("result").value =
          'Result: ' + response.status + ' ' + response.statusText;
      } catch (error) {
        console.error('Error:', error);
      }
    }
    function getCookie(name) {
      var value = "; " + document.cookie;
      var parts = value.split("; " + name + "=");
      if (parts.length == 2) return parts.pop().split(";").shift();
    }
  </script>
}

// ----------------------------------------------------------------------------
// Index.cshtml.cs
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/Index.cshtml.cs
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/Index.cshtml.cs
// ----------------------------------------------------------------------------

using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Mime;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileProviders;
using SampleApp.Data;
using SampleApp.Models;

namespace SampleApp.Pages
{
    public class IndexModel : PageModel
    {
        private readonly AppDbContext _context;
        private readonly IFileProvider _fileProvider;

        public IndexModel(AppDbContext context, IFileProvider fileProvider)
        {
            _context = context;
            _fileProvider = fileProvider;
        }

        public IList<AppFile> DatabaseFiles { get; private set; }
        public IDirectoryContents PhysicalFiles { get; private set; }

        public async Task OnGetAsync()
        {
            DatabaseFiles = await _context.File.AsNoTracking().ToListAsync();
            PhysicalFiles = _fileProvider.GetDirectoryContents(string.Empty);
        }

        public async Task<IActionResult> OnGetDownloadDbAsync(int? id)
        {
            if (id == null)
            {
                return Page();
            }

            var requestFile = await _context.File.SingleOrDefaultAsync(m => m.Id == id);

            if (requestFile == null)
            {
                return Page();
            }

            // Don't display the untrusted file name in the UI. HTML-encode the value.
            return File(requestFile.Content, MediaTypeNames.Application.Octet, WebUtility.HtmlEncode(requestFile.UntrustedName));
        }

        public IActionResult OnGetDownloadPhysical(string fileName)
        {
            var downloadFile = _fileProvider.GetFileInfo(fileName);

            return PhysicalFile(downloadFile.PhysicalPath, MediaTypeNames.Application.Octet, fileName);
        }
    }
}

// ----------------------------------------------------------------------------
// Index.cshtml
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/Index.cshtml
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/Index.cshtml
// ----------------------------------------------------------------------------

@page
@model IndexModel
@{
    ViewData["Title"] = "Upload Files Sample";
}

<h1>Files in the database</h1>

@if (Model.DatabaseFiles.Count == 0)
{
    <p>
        No files are available. Visit one of the file upload scenario pages to upload one or more files.
    </p>
}
else
{
    <table>
        <thead>
            <tr>
                <th></th>
                <th>
                    @Html.DisplayNameFor(model => model.DatabaseFiles[0].UntrustedName) /
                    @Html.DisplayNameFor(model => model.DatabaseFiles[0].Note)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.DatabaseFiles[0].UploadDT)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.DatabaseFiles[0].Size)
                </th>
                <th>
                    <code>FileStreamResult</code> from database
                </th>
            </tr>
        </thead>
        <tbody>
            @foreach (var file in Model.DatabaseFiles) {
                <tr>
                    <td>
                        <a asp-page="./DeleteDbFile" asp-route-id="@file.Id">Delete</a>
                    </td>
                    <td>
                        <b>@file.UntrustedName</b><br>
                        @Html.DisplayFor(modelItem => file.Note)
                    </td>
                    <td class="text-center">
                        @Html.DisplayFor(modelItem => file.UploadDT)
                    </td>
                    <td class="text-center">
                        @Html.DisplayFor(modelItem => file.Size)
                    </td>
                    <td class="text-center">
                        <a asp-page-handler="DownloadDb" asp-route-id="@file.Id">Download</a>
                    </td>
                </tr>
            }
        </tbody>
    </table>
}

<h1>Files stored on disk</h1>

@if (Model.PhysicalFiles.Count() == 0)
{
    <p>
        No files are available. Visit one of the file upload scenario pages to upload one or more files.
    </p>
}
else
{
    <table>
        <thead>
            <tr>
                <th></th>
                <th>
                    Name / Path
                </th>
                <th>
                    Size (bytes)
                </th>
                <th>
                    <code>PhysicalFileResult</code> from storage
                </th>
            </tr>
        </thead>
        <tbody>
            @foreach (var file in Model.PhysicalFiles) {
                <tr>
                    <td>
                        <a asp-page="./DeletePhysicalFile" asp-route-fileName="@file.Name">Delete</a>
                    </td>
                    <td>
                        <b>@file.Name</b><br>
                        @file.PhysicalPath
                    </td>
                    <td class="text-center">
                        @file.Length.ToString("N0")
                    </td>
                    <td class="text-center">
                        <a asp-page-handler="DownloadPhysical" asp-route-fileName="@file.Name">Download</a>
                    </td>
                </tr>
            }
        </tbody>
    </table>
}

// ----------------------------------------------------------------------------
// Startup.cs
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Startup.cs
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Startup.cs
// ----------------------------------------------------------------------------

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using SampleApp.Data;
using SampleApp.Filters;

namespace SampleApp
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            #region snippet_AddRazorPages
            services.AddRazorPages()
                .AddRazorPagesOptions(options =>
                    {
                        options.Conventions
                            .AddPageApplicationModelConvention("/StreamedSingleFileUploadDb",
                                model =>
                                {
                                    model.Filters.Add(new GenerateAntiforgeryTokenCookieAttribute());
                                    model.Filters.Add(new DisableFormValueModelBindingAttribute());
                                });
                        options.Conventions
                            .AddPageApplicationModelConvention("/StreamedSingleFileUploadPhysical",
                                model =>
                                {
                                    model.Filters.Add(new GenerateAntiforgeryTokenCookieAttribute());
                                    model.Filters.Add(new DisableFormValueModelBindingAttribute());
                                });
                    });
            #endregion

            // To list physical files from a path provided by configuration:
            var physicalProvider = new PhysicalFileProvider(Configuration.GetValue<string>("StoredFilesPath"));

            // To list physical files in the temporary files folder, use:
            //var physicalProvider = new PhysicalFileProvider(Path.GetTempPath());

            services.AddSingleton<IFileProvider>(physicalProvider);

            services.AddDbContext<AppDbContext>(options => options.UseInMemoryDatabase("InMemoryDb"));
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }

            app.UseStaticFiles();
            app.UseRouting();

            app.UseEndpoints(endpoints => {
                endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
                endpoints.MapRazorPages();
            });
        }
    }
}

// ----------------------------------------------------------------------------
// appsettings.json
//
// Article: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1
// Path: AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/appsettings.json
// Source: https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/appsettings.json
// ----------------------------------------------------------------------------

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "StoredFilesPath": "c:\\files",
  "FileSizeLimit": 2097152
}