This article shows how to upload and download files in ASP.NET Core 3.0 MVC and save the files to a MS SQL Server using FileTable. The data access for the application is implemented in a separate project and EF Core migrations is used to setup the select logic for the database.

Code: https://github.com/damienbod/AspNetCoreFileUploadFileTable

2019-12-21: Updated to .NET Core 3.1

2019-10-05: Updated to .NET Core 3.0

2018-12-05: Updated to .NET Core 2.2

2018-11-22: Updated bootstrap 4, nuget packages, ui build

2018-06-07: Updated to ASP.NET Core 2.1

2017-08-21: Updated to ASP.NET Core 2.0

2017-01-07: Updated to VS2017 csproj

2016-02-07: Updated to ASP.NET Core RTM

2016-05-18: Updated to ASP.NET Core RC2

2015-12-08: config files cleaned up and returning proper 415 response for unsupported media type

Step 1: Settings up the database FileTable

A new database is created in MS SQL Server which has Filestreams enabled. This feature only works with windows authentication. Firstly if not already configured, the Filestream access level is set to 2.

EXEC sp_configure filestream_access_level, 2 RECONFIGURE GO

Once this has been set, create a new directory to save the files. In this example, C:\damienbod\WebApiFileTable is used. Now execute the following command:

CREATE DATABASE AspNetCoreFileUploadFileTable ON PRIMARY (Name = AspNetCoreFileUploadFileTable, FILENAME = 'C:\databases\AspNetCoreFileUploadFileTable\FTDB.mdf'), FILEGROUP FTFG CONTAINS FILESTREAM (NAME = AspNetCoreFileUploadFileTableFS, FILENAME='C:\databases\AspNetCoreFileUploadFileTable\FS') LOG ON (Name = AspNetCoreFileUploadFileTableLog, FILENAME = 'C:\databases\AspNetCoreFileUploadFileTable\FTDBLog.ldf') WITH FILESTREAM (NON_TRANSACTED_ACCESS = FULL, DIRECTORY_NAME = N'AspNetCoreFileUploadFileTable'); GO

Now you can check if your database settings are ok.

SELECT DB_NAME(database_id), non_transacted_access, non_transacted_access_desc FROM sys.database_filestream_options; GO

The database should be configured as follows:



Now create a table for the file uploads:

USE WebApiFileTable GO CREATE TABLE WebApiUploads AS FileTable WITH (FileTable_Directory = 'WebApiUploads_Dir'); GO

The files can be saved, deleted or updated using the following path:

\\{yourPCname}\{mssqlserver}\WebApiFileTable\WebApiUploads_Dir

The files can also be accessed using plain SQL.

INSERT INTO [dbo].[WebApiUploads] ([name],[file_stream]) SELECT 'NewFile.txt', * FROM OPENROWSET(BULK N'd:\NUnit-2.6.1.msi', SINGLE_BLOB) AS FileData GO>

Step 2: Adding the Entity Framework Core data access layer

A file description table is created for searching and returning multiple records. This is used to setup a download link and provide a small description of the file. To create the table, Entity Framework code first is used in this example.

Add Entity framework Core to the csproj file in your project. The EF Core dependencies need to be added and also the ef commands.

<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.App" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> </ItemGroup> <ItemGroup> <DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" /> </ItemGroup> <PropertyGroup Condition=" '$(Configuration)' == 'Release' "> <DefineConstants>$(DefineConstants);RELEASE</DefineConstants> </PropertyGroup> </Project>

An entity context class has to be created to use the database. This is used for the migrations and also the data access.

using DataAccess.Model; using Microsoft.EntityFrameworkCore; namespace DataAccess { public class FileContext : DbContext { public FileContext(DbContextOptions<FileContext> options) :base(options) { } public DbSet<FileDescription> FileDescriptions { get; set; } protected override void OnModelCreating(ModelBuilder builder) { builder.Entity<FileDescription>().HasKey(m => m.Id); base.OnModelCreating(builder); } } }

The class used as the entity also needs to be created. The primary key for this class is also defined in the context class.

using System; using System.ComponentModel.DataAnnotations; namespace DataAccess.Model { public class FileDescription { public int Id { get; set; } public string FileName { get; set; } public string Description { get; set; } public DateTime CreatedTimestamp { get; set; } public DateTime UpdatedTimestamp { get; set; } public string ContentType { get; set; } } }

The connection string needs to be added to the config file which is used in the context. This is required for migrations and also running the application.

{ "ConnectionStrings": { "DefaultConnection": "Data Source=...." }, "ApplicationConfiguration": { "ServerUploadFolder": "\\\\..." } }

The migrations can be created and the database can be updated. Open the application using the command line in the src folder where the project is defined. The migrations are executed from the project containing the Startup file where the DbContext is used. The migrations needs to be set to this project then in the AddDbContext method.

> > dotnet restore > > dotnet ef migrations add testMigration > > dotnet ef database update > >

If you don’t want to use EF7 migrations, you could just create the SQL table using plain TSQL.

USE [WebApiFileTable] GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[FileDescriptions]( [Id] [int] IDENTITY(1,1) NOT NULL, [FileName] [nvarchar](max) NULL, [Description] [nvarchar](max) NULL, [CreatedTimestamp] [datetime] NOT NULL, [UpdatedTimestamp] [datetime] NOT NULL, [ContentType] [nvarchar](max) NULL, CONSTRAINT [PK_dbo.FileDescription] PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] GO

Step 3: ASP.NET Core MVC Single or Multiple file upload and download

The ASP.NET Core MVC application is a simple project with razor views and a FileUpload ASP.NET Core MVC controller to upload and download the files. The data access project is added as a reference in the csproj file in the dependencies. It does not matter if the dependencies uses sources from NuGet or from local projects.

<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\DataAccess\DataAccess.csproj" /> </ItemGroup> <ItemGroup> <PackageReference Include="BuildBundlerMinifier" Version="3.2.435" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> </ItemGroup> <ItemGroup> <DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" /> </ItemGroup> <ItemGroup> <Folder Include="Migrations\" /> </ItemGroup> <Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('node_modules') "> <!-- Ensure Node.js is installed --> <Exec Command="node --version" ContinueOnError="true"> <Output TaskParameter="ExitCode" PropertyName="ErrorCode" /> </Exec> <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." /> <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." /> <Exec WorkingDirectory="" Command="npm install" /> </Target> </Project>

ASP.NET 5 provides the IFormFile class for file upload. This class is used inside the FileDescriptionShort, which is used for single or multiple file uploads.

public class FileDescriptionShort { public int Id { get; set; } public string Description { get; set; } public string Name { get; set; } public ICollection<IFormFile> File { get; set; } }

The FileUploadController has two action methods. The controller uses the default DI with constructor injection to add the dependencies. The UploadFiles action method uses the FileDescriptionShort class as a parameter. The method takes all the files and saves each file directly to the MS SQL Server FileTable. Then the file descriptions are saved to the database. The descriptions are used to list and download to files.

The file upload logic was built using the following two blogs:

http://www.mikesdotnetting.com/article/288/asp-net-5-uploading-files-with-asp-net-mvc-6

http://dotnetthoughts.net/file-upload-in-asp-net-5-and-mvc-6/

Thanks for these articles.

using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using DataAccess; using DataAccess.Model; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using FileResult = DataAccess.Model.FileResult; namespace AspNet5FileUploadFileTable.Controllers { [Route("api/test")] public class FileUploadController : Controller { private readonly IFileRepository _fileRepository; private readonly IOptions<ApplicationConfiguration> _optionsApplicationConfiguration; public FileUploadController(IFileRepository fileRepository, IOptions<ApplicationConfiguration> o) { _fileRepository = fileRepository; _optionsApplicationConfiguration = o; } [Route("files")] [HttpPost] [ServiceFilter(typeof(ValidateMimeMultipartContentFilter))] public async Task<IActionResult> UploadFiles(FileDescriptionShort fileDescriptionShort) { var names = new List<string>(); var contentTypes = new List<string>(); if (ModelState.IsValid) { // http://www.mikesdotnetting.com/article/288/asp-net-5-uploading-files-with-asp-net-mvc-6 // http://dotnetthoughts.net/file-upload-in-asp-net-5-and-mvc-6/ foreach (var file in fileDescriptionShort.File) { if (file.Length > 0) { var fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"'); contentTypes.Add(file.ContentType); names.Add(fileName); // Extension method update RC2 has removed this await file.SaveAsAsync(Path.Combine(_optionsApplicationConfiguration.Value.ServerUploadFolder, fileName)); } } } var files = new FileResult { FileNames = names, ContentTypes = contentTypes, Description = fileDescriptionShort.Description, CreatedTimestamp = DateTime.UtcNow, UpdatedTimestamp = DateTime.UtcNow, }; _fileRepository.AddFileDescriptions(files); return RedirectToAction("ViewAllFiles", "FileClient"); } [Route("download/{id}")] [HttpGet] public FileStreamResult Download(int id) { var fileDescription = _fileRepository.GetFileDescription(id); var path = _optionsApplicationConfiguration.Value.ServerUploadFolder + "\\" + fileDescription.FileName; var stream = new FileStream(path, FileMode.Open); return File(stream, fileDescription.ContentType); } } }

The IFormFile SaveAsAsync extension needs to be added.

using System; using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace AspNetCoreFileUploadFileTable { public static class FormFileExtensions { private static int DefaultBufferSize = 80 * 1024; /// <summary> /// Asynchronously saves the contents of an uploaded file. /// </summary> /// <param name="formFile">The <see cref="IFormFile"/>.</param> /// <param name="filename">The name of the file to create.</param> public async static Task SaveAsAsync( this IFormFile formFile, string filename, CancellationToken cancellationToken = default(CancellationToken)) { if (formFile == null) { throw new ArgumentNullException(nameof(formFile)); } using (var fileStream = new FileStream(filename, FileMode.Create)) { var inputStream = formFile.OpenReadStream(); await inputStream.CopyToAsync(fileStream, DefaultBufferSize, cancellationToken); } } } }

The upload method also uses a service filter to validate the mime type. The ValidateMimeMultipartContentFilter class implements the ActionFilterAttribute which provides virtual methods which can be overridden. This attribute throws an exception, if the mime type is incorrect. A file upload requires a multipart content type.

using System; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Logging; namespace AspNetCoreFileUploadFileTable { public class ValidateMimeMultipartContentFilter : ActionFilterAttribute { private readonly ILogger _logger; public ValidateMimeMultipartContentFilter(ILoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger("ctor ValidateMimeMultipartContentFilter"); } public override void OnActionExecuting(ActionExecutingContext context) { if (!IsMultipartContentType(context.HttpContext.Request.ContentType)) { context.Result = new StatusCodeResult(415); return; } base.OnActionExecuting(context); } private static bool IsMultipartContentType(string contentType) { return !string.IsNullOrEmpty(contentType) && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0; } } }

All the required application configurations are implemented in the Startup class. Entity Framework, configuration, attribute, and class dependencies are defined here.

using DataAccess; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace AspNetCoreFileUploadFileTable { public class Startup { private readonly IWebHostEnvironment _environment; public Startup(IWebHostEnvironment env, IConfiguration configuration) { Configuration = configuration; _environment = env; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.Configure<ApplicationConfiguration>(Configuration.GetSection("ApplicationConfiguration")); var sqlConnectionString = Configuration.GetConnectionString("DefaultConnection"); services.AddDbContext<FileContext>(options => options.UseSqlServer( sqlConnectionString, b => b.MigrationsAssembly("AspNetCoreFileUploadFileTable") ) ); services.AddControllersWithViews() .SetCompatibilityVersion(CompatibilityVersion.Version_3_0); services.AddScoped<IFileRepository, FileRepository>(); services.AddScoped<ValidateMimeMultipartContentFilter>(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseStaticFiles(); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=FileClient}/{action=ViewAllFiles}/{id?}"); }); } } }

The razor view implements a HTML form which is used to upload the files. The form attributes enctype and method are important for file upload, these should be defined as follows: enctype=”multipart/form-data” method=”post”

<!doctype html> <html> <head> <title>Test</title> </head> <body> <form enctype="multipart/form-data" method="post" action="http://localhost:20828/api/test/files" id="ajaxUploadForm" novalidate="novalidate"> <fieldset> <legend style="padding-top: 10px; padding-bottom: 10px;">Uploaded Form</legend> <div class="col-xs-12" style="padding: 10px;"> <div class="col-xs-4"> <label>Description</label> </div> <div class="col-xs-7"> <textarea rows="2" placeholder="Description" class="form-control" name="description" id="description"></textarea> </div> </div> <div class="col-xs-12" style="padding: 10px;"> <div class="col-xs-4"> <label>Upload</label> </div> <div class="col-xs-7"> <input type="file" id="fileInput" name="file" multiple> </div> </div> <div class="col-xs-12" style="padding: 10px;"> <div class="col-xs-4"> <input type="submit" value="Upload" id="ajaxUploadButton" class="btn"> </div> <div class="col-xs-7"> </div> </div> </fieldset> </form> </body> </html>

Testing the application

The application displays all the existing files which where uploaded when started.



If clicked, the file can be downloaded:

http://localhost:20828/api/test/download/id

The files can be uploaded as follows:



Conclusion

The application is relatively easy to implement and is simple to understand. This is a big improvement compared to the same application implemented in .NET 4.5 with Web API. One problem which exists is the configuration when using separate projects. The paths depends on where the project is run. The config file can be saved to a common path to all the projects and the application will work. (Might be a problem with deployment.)

Links:

https://docs.microsoft.com/en-us/sql/relational-databases/blob/filestream-sql-server?view=sql-server-2017

https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-2.1

http://dotnetthoughts.net/file-upload-in-asp-net-5-and-mvc-6/

http://www.mikesdotnetting.com/article/288/asp-net-5-uploading-files-with-asp-net-mvc-6

https://damienbod.com/2014/04/08/web-api-file-upload-with-ms-sql-server-filetable

http://senvichet.com/how-to-upload-file-from-web-form-in-asp-net-5-mvc-6/