This article shows how to upload and download files in ASP.NET 5 MVC 6 and save the files to a MS SQL Server using FileTable. The data access for the application is implemented in a separate project and EF7 migrations is used to setup the select logic for the database.
Code: https://github.com/damienbod/AspNet5FileUploadFileTable
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 WebApiFileTable
ON PRIMARY
(Name = WebApiFileTable,
FILENAME = 'C:\damienbod\WebApiFileTable\FTDB.mdf'),
FILEGROUP FTFG CONTAINS FILESTREAM
(NAME = WebApiFileTableFS,
FILENAME='C:\damienbod\WebApiFileTable\FS')
LOG ON
(Name = WebApiFileTableLog,
FILENAME = 'C:\damienbod\WebApiFileTable\FTDBLog.ldf')
WITH FILESTREAM (NON_TRANSACTED_ACCESS = FULL,
DIRECTORY_NAME = N'WebApiFileTable');
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:
![FileTableWebApi01]()
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 7 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 7 to the project.json file in your project. The EF7 dependencies need to be added and also the ef commands.
{
"version": "1.0.0-*",
"description": "DataAccess Class Library",
"authors": [ "damien.bowden" ],
"tags": [ "" ],
"projectUrl": "",
"licenseUrl": "",
"dependencies": {
"EntityFramework.Core": "7.0.0-rc1-final",
"EntityFramework.Commands": "7.0.0-rc1-final",
"EntityFramework.MicrosoftSqlServer": "7.0.0-rc1-final",
"Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc1-final",
"Microsoft.AspNet.Mvc": "6.0.0-rc1-final",
"Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final",
"Microsoft.AspNet.StaticFiles": "1.0.0-rc1-final",
"Microsoft.AspNet.Tooling.Razor": "1.0.0-rc1-final",
"Microsoft.Extensions.Configuration.Abstractions": "1.0.0-rc1-final",
"Microsoft.Extensions.Configuration.FileProviderExtensions": "1.0.0-rc1-final",
"Microsoft.Extensions.Configuration.Json": "1.0.0-rc1-final",
"Microsoft.Extensions.Logging": "1.0.0-rc1-final",
"Microsoft.Extensions.Logging.Console": "1.0.0-rc1-final",
"Microsoft.Extensions.Logging.Debug": "1.0.0-rc1-final",
"System.ComponentModel.Annotations": "4.0.11-beta-23516"
},
"frameworks": {
"dnx451": { },
"dnxcore50": { }
},
"commands": {
"ef": "EntityFramework.Commands"
}
}
An entity context class has to be created to use the database. This is used for the migrations and also the data access. The OnConfiguring method is required because the migrations are running in a separate project. This can be done in the startup class, if the migrations are in the same project as the MVC 6 application.
using Microsoft.Data.Entity;
using DataAccess.Model;
using Microsoft.Extensions.Configuration;
namespace DataAccess
{
public class FileContext : DbContext
{
public DbSet<FileDescription> FileDescriptions { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<FileDescription>().HasKey(m => m.Id);
base.OnModelCreating(builder);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var builder = new ConfigurationBuilder()
.AddJsonFile("config.json")
.AddEnvironmentVariables();
var configuration = builder.Build();
var sqlConnectionString = configuration["ApplicationConfiguration:SQLConnectionString"];
optionsBuilder.UseSqlServer(sqlConnectionString);
}
}
}
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.
{
"ApplicationConfiguration": {
"SQLConnectionString": "Data Source=N275\\MSSQLSERVER2014;Initial Catalog=WebApiFileTable;Integrated Security=True;"
}
}
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.
>
> dnu restore
>
> dnx ef migrations add testMigration
>
> dnx 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].[FileDescription](
[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: MVC 6 Single or Multiple file upload and download
The MVC 6 application is a simple project with razor views and a FileUpload MVC 6 controller to upload and download the files. The data access project is added as a reference in the project.json file in the dependencies. It does not matter if the dependencies uses sources from NuGet or from local projects.
{
"version": "1.0.0-*",
"compilationOptions": {
"emitEntryPoint": true
},
"dependencies": {
"EntityFramework.Core": "7.0.0-rc1-final",
"EntityFramework.MicrosoftSqlServer": "7.0.0-rc1-final",
"Microsoft.AspNet.Diagnostics": "1.0.0-rc1-final",
"Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc1-final",
"Microsoft.AspNet.Mvc": "6.0.0-rc1-final",
"Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc1-final",
"Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final",
"Microsoft.AspNet.StaticFiles": "1.0.0-rc1-final",
"Microsoft.AspNet.Tooling.Razor": "1.0.0-rc1-final",
"Microsoft.Extensions.Configuration.FileProviderExtensions": "1.0.0-rc1-final",
"Microsoft.Extensions.Configuration.Json": "1.0.0-rc1-final",
"Microsoft.Extensions.Logging": "1.0.0-rc1-final",
"Microsoft.Extensions.Logging.Console": "1.0.0-rc1-final",
"Microsoft.Extensions.Logging.Debug": "1.0.0-rc1-final",
"Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0-rc1-final",
"DataAccess": "1.0.0"
},
"commands": {
"web": "Microsoft.AspNet.Server.Kestrel"
},
"frameworks": {
"dnx451": { },
"dnxcore50": { }
},
"exclude": [
"wwwroot",
"node_modules"
],
"publishExclude": [
"**.user",
"**.vspscc"
],
"scripts": {
"prepublish": [ "npm install", "bower install", "gulp clean", "gulp min" ]
}
}
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.
namespace AspNet5FileUploadFileTable.Controllers
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using DataAccess;
using DataAccess.Model;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc;
using Microsoft.Extensions.OptionsModel;
using Microsoft.Net.Http.Headers;
using FileResult = DataAccess.Model.FileResult;
[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)
{
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);
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 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 System.Net;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.Extensions.Logging;
namespace AspNet5FileUploadFileTable
{
public class ValidateMimeMultipartContentFilter : ActionFilterAttribute
{
private readonly ILogger _logger;
public ValidateMimeMultipartContentFilter(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger("ctor ValidateMimeMultipartContentFilter");
}
public override void OnActionExecuting(ActionExecutingContext context)
{
_logger.LogWarning("ClassFilter OnActionExecuting");
if (!IsMultipartContentType(context.HttpContext.Request.ContentType))
{
// TODO improve this with 415 response, instead of 500.
throw new Exception("UnsupportedMediaType:" + HttpStatusCode.UnsupportedMediaType.ToString());
}
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 Microsoft.AspNet.Builder;
using Microsoft.AspNet.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace AspNet5FileUploadFileTable
{
using AspNet5FileUploadFileTable.Controllers;
using DataAccess;
using Microsoft.Data.Entity;
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; set; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<ApplicationConfiguration>( Configuration.GetSection("ApplicationConfiguration"));
var connection = Configuration["ApplicationConfiguration:SQLConnectionString"];
services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<FileContext>(options => options.UseSqlServer(connection));
services.AddMvc();
services.AddScoped<IFileRepository, FileRepository>();
services.AddScoped<ValidateMimeMultipartContentFilter>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseBrowserLink();
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseIISPlatformHandler();
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
public static void Main(string[] args) => WebApplication.Run<Startup>(args);
}
}
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.
![aspnet5upload_01]()
If clicked, the file can be downloaded:
http://localhost:20828/api/test/download/{id}
The files can be uploaded as follows:
![aspnet5upload_02]()
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. Because the data access project config.json is used in the migrations and also the application when running, this is copied to both projects so that it always works, which is not very nice. Some other workarounds exists for this, see stack overflow.
Links:
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
http://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/