I'm relatively new to MVC, and I've never had to deal with uploading a file (an image, specifically) to a SQL Server database. To be honest, I don't know what I'm doing here.
Here's what I have so far - here's my domain model (note the HttpPostedFileBase
in my model - this is what I want to upload):
public class Profile
{
[Key]
public int Id { get; set; }
[Required(ErrorMessage="Years of service is required")]
[DisplayName("Years Service:")]
public int YearsService { get; set; }
[DataType(DataType.MultilineText)]
[DisplayName("Notable Achivements:")]
public string NotableAchivements { get; set; }
[Required(ErrorMessage = "Technical skills are required")]
[DataType(DataType.MultilineText)]
[DisplayName("Technical Skills:")]
public string TechnicalSkills { get; set; }
[DisplayName("Upload Image: ")]
public HttpPostedFileBase Photo { get; set; }
public string CreatedBy { get; set; }
public DateTime CreatedDate { get; set; }
public string ModifiedBy { get; set; }
public DateTime ModifiedDate { get; set; }
}
And here's my view:
@using (Html.BeginForm("Create", "Profiles", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
<div class="editor-label">
@Html.LabelFor(model => model.YearsService)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.YearsService)
@Html.ValidationMessageFor(model => model.YearsService)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.NotableAchivements)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.NotableAchivements)
@Html.ValidationMessageFor(model => model.NotableAchivements)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.TechnicalSkills)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.TechnicalSkills)
@Html.ValidationMessageFor(model => model.TechnicalSkills)
</div>
<input type="file" name="photo" />
<input type="submit" name="Submit" id="Submit" value="Upload" />
}
I hope there's something glaringly obvious that I'm doing wrong. Can anyone provide advice on how to do a simple file upload to a SQL Server database?
First and foremost, don't save the image to your database. There's countless "guides" floating around online that suggest using a byte array or "image" type in MSSQL. Databases are not for binary data storage. They'll accommodate binary storage, sure, but just because you can shoot yourself in the foot with an AR-15, doesn't mean you should.
Instead, store the image where it belongs: the filesystem. And, then, merely reference it's path in the database.
Now, the easiest way to handle all this is with a view model. If you're not using view models, already, now's a great time to start. In general, you don't want to send your actual database-backed model directly to the view. In forms, there's potential for all sorts of nasty data-tampering by malicious users and even in simple views, you're generally passing along more data than they need.
So, the first step is to create a view model for Profile
. It's common to name this something like ProfileViewModel
or ProfileVM
. Then, on this model, you'll only add the properties of Profile
that you'll be editing or otherwise interacting with in your view. Then, you can actually add additional properties not on your database-backed model for special-purpose view functions, as well, such as a SelectList
to use with DropDownListFor
, so you can actually remain strongly-typed instead of resorting to ViewBag
for such things.
For your image, you'll want two fields. I usually go with something like the following:
public string Photo { get; set; }
public HttpPostedFileBase PhotoUpload { get; set; }
In your edit/create view, you'll only reference PhotoUpload
, which will be your file upload field (though you can use Photo
to display the currently set image, as well, if you like).
@Html.TextBoxFor(m => m.PhotoUpload, new { type = "file" })
Then, to handle the posted file in your controller action:
if (model.PhotoUpload.ContentLength > 0) {
// A file was uploaded
var fileName = Path.GetFileName(model.PhotoUpload.FileName);
var path = Path.Combine(Server.MapPath(uploadPath), fileName);
model.PhotoUpload.SaveAs(path);
model.Photo = uploadPath + fileName;
}
Where uploadPath
should be a home directory-relative path where you want to store the uploaded image, i.e. something like ~/uploads/profile/photos
. Make sure you create the directory before trying to post the form, or add in some logic to check for the directory existence and create it if necessary (this requires higher trust on the server, though, so is not ideal in most environments where security is a big concern).
Then, you just need some way to map the data from your view model back to your database-backed model. You can do this manually, but using something like AutoMapper will make your life much easier. With automapper (where model
is an instance of ProfileViewModel
):
var profile = AutoMapper.Mapper.Map<Profile>(model);
Or, since with something like a profile, it's more common that you'll be editing an existing model than creating a new one:
var profile = db.Profiles.Find(userId);
...
Automapper.Mapper.Map(model, profile);
On your actual model, then, you won't have the PhotoUpload
property, just Photo
. The path you set for Photo
on your view model will be mapped on your model's property of the same name, so all that's left is to save your updated profile.
Also, since you're talking about uploading photos, you'll probably want to add some content type checking to make sure user's are uploading an image instead of something silly like a Word document. Before your if (ModelState.IsValid)
check, add:
var validTypes = new[] { "image/jpeg", "image/pjpeg", "image/png", "image/gif" };
if (!validTypes.Contains(model.PhotoUpload.ContentType))
{
ModelState.AddModelError("PhotoUpload", "Please upload either a JPG, GIF, or PNG image.");
}
Change the mime types as necessary to match your business case.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With