Загрузка изображений в базу через FileUpload и вывод через HttpHandler

РазделASP.NETУровеньСредний Чтение9 минОпубликовано ТегиASP.NETGridViewFileUploadSQL Server

Фото сотрудника или товара можно хранить прямо в базе и показывать в GridView без единого файла на диске. Собираем цепочку: FileUpload, массив байтов, SQL и HttpHandler.

Главное. FileUpload отдаёт файл массивом байтов, параметризованный запрос сохраняет его в varbinary, а лёгкий HttpHandler выводит картинку по идентификатору. Не забывайте про проверку файла и кэширование.

План решения

Хранить изображения в файловой системе привычно, но это добавляет хлопот: права на папку, синхронизация между серверами фермы, осиротевшие файлы после удаления записей. Для небольших картинок вроде аватаров и фото сотрудников удобнее держать данные прямо в таблице. Тогда резервная копия базы автоматически включает и изображения.

Обратная сторона тоже есть: база растёт, а каждая картинка проходит через приложение, а не отдаётся веб-сервером напрямую. Поэтому подход хорош для изображений размером в десятки и сотни килобайт и для сотен или тысяч записей, но не для фотобанка.

В примере используется демонстрационная база Northwind, таблица Employees с полем Photo. Цепочка выглядит так:

  • FileUpload в строке GridView принимает файл от пользователя.
  • Содержимое читается в массив байтов, на диск ничего не пишется.
  • Массив сохраняется в базу параметризованным UPDATE.
  • В колонке GridView стоит Image, ссылающийся на обработчик .ashx с идентификатором.
  • HttpHandler достаёт байты из базы и отдаёт их браузеру с нужным Content-Type.

Разметка GridView

В шаблонных колонках размещены картинка и форма загрузки. DataKeyNames хранит первичный ключ каждой строки, так что в обработчике события не придётся прятать идентификатор в скрытые поля. Кнопка передаёт номер строки через CommandArgument, а CommandName позволяет отличить её команду от других кнопок таблицы.

Важно: загрузка файлов работает только при полноценном PostBack. Если GridView лежит внутри UpdatePanel, кнопку нужно зарегистрировать как PostBackTrigger, иначе PostedFile на сервере окажется пустым.

Default.aspxASPX
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="false"
              DataKeyNames="EmployeeID">
  <Columns>
    <asp:BoundField DataField="LastName" HeaderText="Фамилия" />
    <asp:BoundField DataField="FirstName" HeaderText="Имя" />
    <asp:TemplateField HeaderText="Фото">
      <ItemTemplate>
        <asp:Image runat="server" Width="80"
          AlternateText='<%# Eval("LastName") %>'
          ImageUrl='<%# "~/DbImage.ashx?id=" & Eval("EmployeeID") %>' />
      </ItemTemplate>
    </asp:TemplateField>
    <asp:TemplateField>
      <ItemTemplate>
        <asp:FileUpload ID="fuPhoto" runat="server" accept="image/*" />
        <asp:Button runat="server" Text="Загрузить" CommandName="UPLOAD"
          CommandArgument='<%# Container.DataItemIndex %>' />
      </ItemTemplate>
    </asp:TemplateField>
  </Columns>
</asp:GridView>

Чтение файла и сохранение в базу

В событии RowCommand по номеру строки находим FileUpload через FindControl, а идентификатор берём из DataKeys. Свойство FileBytes возвращает содержимое файла сразу массивом, вручную читать InputStream не нужно. Заодно сохраняем тип содержимого: без него обработчик не узнает, отдавать ли JPEG или PNG.

Перед сохранением обязательно проверяем файл: размер, разрешённые типы и то, что это действительно изображение. Доверять расширению и ContentType из запроса нельзя, их подставляет клиент. Надёжная проверка: попробовать открыть данные через System.Drawing.Image.FromStream.

Default.aspx.vbVB.NET
Protected Sub GridView1_RowCommand(sender As Object, e As GridViewCommandEventArgs) _
        Handles GridView1.RowCommand
    If e.CommandName <> "UPLOAD" Then Return
    Dim rowIndex = Convert.ToInt32(e.CommandArgument)
    Dim fu = CType(GridView1.Rows(rowIndex).FindControl("fuPhoto"), FileUpload)
    Dim id = CInt(GridView1.DataKeys(rowIndex).Value)

    If Not fu.HasFile OrElse fu.FileBytes.Length > 512 * 1024 Then
        lblMsg.Text = "Выберите изображение до 512 КБ" : Return
    End If

    Const sql = "UPDATE Employees SET Photo = @Photo, PhotoType = @Type " &
                "WHERE EmployeeID = @Id"
    Using cn As New SqlConnection(ConnStr), cmd As New SqlCommand(sql, cn)
        cmd.Parameters.Add("@Photo", SqlDbType.VarBinary, -1).Value = fu.FileBytes
        cmd.Parameters.Add("@Type", SqlDbType.NVarChar, 50).Value = fu.PostedFile.ContentType
        cmd.Parameters.Add("@Id", SqlDbType.Int).Value = id
        cn.Open()
        cmd.ExecuteNonQuery()
    End Using
    LoadGrid() ' перечитываем таблицу, чтобы показать новое фото
End Sub
Поле PhotoType в Northwind отсутствует, его нужно добавить: ALTER TABLE Employees ADD PhotoType nvarchar(50) NULL. Тип image устарел, для новых столбцов используйте varbinary(max).

HttpHandler для вывода картинки

Страница .aspx для отдачи картинки избыточна: у неё есть жизненный цикл, ViewState и рендеринг разметки. Обработчик .ashx реализует интерфейс IHttpHandler всего с одним методом ProcessRequest и отдаёт байты напрямую в поток ответа. Это быстрее и проще.

Обработчик берёт идентификатор из строки запроса, проверяет, что это число, читает данные и пишет их через BinaryWrite. Если фото нет, лучше вернуть 404 или картинку-заглушку, чем пустой ответ: браузер тогда покажет значок битого изображения.

DbImage.ashxVB.NET
<%@ WebHandler Language="VB" Class="DbImage" %>
Imports System
Imports System.Data
Imports System.Web
Imports System.Data.SqlClient

Public Class DbImage : Implements IHttpHandler
    Public Sub ProcessRequest(ctx As HttpContext) Implements IHttpHandler.ProcessRequest
        Dim id As Integer
        If Not Integer.TryParse(ctx.Request.QueryString("id"), id) Then
            ctx.Response.StatusCode = 400 : Return
        End If
        Using cn As New SqlConnection(Config.ConnStr), _
              cmd As New SqlCommand("SELECT Photo, PhotoType FROM Employees WHERE EmployeeID=@Id", cn)
            cmd.Parameters.Add("@Id", SqlDbType.Int).Value = id
            cn.Open()
            Using rd = cmd.ExecuteReader()
                If Not rd.Read() OrElse rd.IsDBNull(0) Then
                    ctx.Response.StatusCode = 404 : Return
                End If
                ctx.Response.ContentType = If(rd.IsDBNull(1), "image/jpeg", rd.GetString(1))
                ctx.Response.Cache.SetCacheability(HttpCacheability.Public)
                ctx.Response.Cache.SetMaxAge(TimeSpan.FromHours(1))
                ctx.Response.BinaryWrite(CType(rd(0), Byte()))
            End Using
        End Using
    End Sub

    Public ReadOnly Property IsReusable As Boolean Implements IHttpHandler.IsReusable
        Get
            Return True
        End Get
    End Property
End Class

Подводные камни

Фото в самой Northwind хранятся в старом формате OLE: перед данными картинки идёт заголовок длиной 78 байт. Если вывести их как есть, браузер покажет битое изображение. Для демонстрационной базы этот заголовок приходится отрезать, а для своих загрузок такой проблемы нет.

Ещё один момент касается размера страницы. Каждая картинка в GridView это отдельный запрос к обработчику и отдельное обращение к базе. На таблице в сотню строк это ощутимо, поэтому кэширование в браузере, как в примере, и разумный размер страницы (пейджинг) здесь обязательны. Большие фотографии стоит уменьшать при загрузке, а не масштабировать атрибутом Width.

  • Ограничьте maxRequestLength в web.config, иначе большой файл даст ошибку до вашего кода.
  • Не храните в базе файлы размером в десятки мегабайт, для них есть FILESTREAM или файловое хранилище.
  • Добавляйте alt к картинкам: это нужно и для доступности, и для случаев, когда фото нет.

Как это делают сегодня

В ASP.NET Core файл приходит как IFormFile, а картинку отдают из контроллера или Minimal API через Results.File(bytes, contentType) с заголовками кэширования. Сами изображения сегодня чаще кладут в объектное хранилище (S3, Azure Blob) и раздают через CDN, а в базе хранят только ссылку. Для ресайза и проверки формата используют библиотеку ImageSharp вместо System.Drawing.

Вопросы и ответы

Почему PostedFile пустой после нажатия кнопки?

Скорее всего, GridView находится внутри UpdatePanel, а асинхронный запрос не передаёт файлы. Зарегистрируйте кнопку как PostBackTrigger.

Почему фото из Northwind не отображаются?

В них есть 78-байтный OLE-заголовок от старого Access. Пропустите первые 78 байт при выводе, для собственных загрузок это не требуется.

Что выбрать: хранить картинки в базе или на диске?

Небольшие изображения удобно держать в базе ради целостности и бэкапов. Крупные файлы и большие объёмы лучше выносить в файловое или объектное хранилище.

Читайте также