How to Save PDF and Image Files to Database as Byte Array in C#

In this blog post, we will be exploring how to save PDF and image files to a SQL Server database. We will build a form to capture the file from the user, before saving the data to the database. We will also demonstrate fetching the files from the database and displaying them.

Regardless of whether our file is a PDF or an image, we will be storing the file as a byte array. This is as storing the file as a string would consume significantly more memory.

How we display a PDF file is different from how we display an image. So in other to know how to display our file, we will also save the file extension alongside the file data.

In this example, we will be building a .NET 7 Blazor Server application.

Saving a PDF and Image File to the Database

Let’s start by creating the entity to represent our files in the database. Create the following FileRecord entity:

public class FileRecord
{
    public int Id { get; set; }

    public DateTime Created { get; set; }

    [StringLength(100)]
    public string? FileDisplayName { get; set; }

    public byte[]? FileContent { get; set; }

    [StringLength(10)]
    public string? FileExtension { get; set; }
}

Once we have obtained the file from the user, we will have to convert this to a byte array in order to save it. Let’s create a method that turns an IBrowserFile into a byte array. Create the following FileService, not forgetting to register the service in program.cs:

public interface IFileService
{
    Task<byte[]> GetFileAsByteArray(IBrowserFile file, int maxAllowedSize = 512000);
}

public class FileService : IFileService
{
    public async Task<byte[]> GetFileAsByteArray(IBrowserFile file, int maxAllowedSize = 512000)
    {
        using var memoryStream = new MemoryStream();
        await file.OpenReadStream(maxAllowedSize).CopyToAsync(memoryStream);
        return memoryStream.ToArray();
    }
}

Next, we’ll create a DTO to capture the file data to be saved, along with the method to save it to the database.

Create the following CreateFileRecordDTO:

public class CreateFileRecordDTO
{
    public string? FileDisplayName { get; set; }

    public byte[]? FileContent { get; set; }

    public string? FileExtension { get; set; }
}

Now we’ll create a method to save our data to the database. Create the following FileRecordRepository:

public interface IFileRecordRepository
{
    Task CreateFileRecordAsync(CreateFileRecordDTO createFileRecordDTO);
}

public class FileRecordRepository : IFileRecordRepository
{
    private readonly ApplicationDbContext context;

    public FileRecordRepository(ApplicationDbContext context)
    {
        this.context = context;
    }

    public async Task CreateFileRecordAsync(CreateFileRecordDTO createFileRecordDTO)
    {
        var fileRecord = new FileRecord
        {
            Created = DateTime.Now,
            FileDisplayName = createFileRecordDTO.FileDisplayName,
            FileContent = createFileRecordDTO.FileContent,
            FileExtension = createFileRecordDTO.FileExtension
        };

        await context.AddAsync(fileRecord);
        await context.SaveChangesAsync();
    }
}

With these methods in place, we can now implement our page with our form inside it:

@page "/file/create"
@inject IFileRecordRepository FileRecordRepository
@inject IFileService FileService
@inject NavigationManager NavigationManager

<PageTitle>Create File</PageTitle>

<h1>Create File</h1>

<div class="row">
    <div class="col-md-6">
        <EditForm Model="@createFileRecordDTO" OnValidSubmit="HandleValidSubmit">
            <CustomValidation @ref="customValidation" />
            <ValidationSummary />

            <div class="mb-3">
                <InputFile OnChange="@LoadFile" id="file" />
            </div>

            <button type="button" @onclick="Navigate" class="btn btn-secondary me-2">Back</button>
            <button type="submit" class="btn btn-success">Save</button>
        </EditForm>
    </div>
</div>

@code {
    CreateFileRecordDTO? createFileRecordDTO = new();
    IBrowserFile? file;
    CustomValidation? customValidation;

    private void LoadFile(InputFileChangeEventArgs e) =>
        file = e.File;

    private async Task HandleValidSubmit()
    {
        customValidation?.ClearErrors();
        var errors = new Dictionary<string, List<string>>();
        var fileExtension = file != null ? Path.GetExtension(file.Name) : null;
        string[] permittedExtensions = { ".pdf", ".jpg", ".jpeg", ".png" };
        int maxFileSize = 10485760; // 10MB

        if (file == null)
            errors.Add("", new() { "File is required." });
        else
        {
            if (string.IsNullOrEmpty(fileExtension) || !permittedExtensions.Contains(fileExtension.ToLower()))
                errors.Add("", new() { "File must be in .pdf, .jpg, .jpeg or .png format." });

            if (file.Size > maxFileSize)
                errors.Add("", new() { "File must be less than 10MB in size." });

            if (file.Name.Length > 100)
                errors.Add("", new() { "File name must be 100 characters or less." });
        }

        if (errors.Any())
            customValidation?.DisplayErrors(errors);
        else
        {
            createFileRecordDTO!.FileDisplayName = file!.Name;
            createFileRecordDTO!.FileContent = await FileService.GetFileAsByteArray(file!, maxFileSize);
            createFileRecordDTO!.FileExtension = fileExtension;
            await FileRecordRepository.CreateFileRecordAsync(createFileRecordDTO);
            Navigate();
        }
    }

    private void Navigate() =>
        NavigationManager.NavigateTo("/files");
}

We have implemented some simple validation to our form to ensure that the file being uploaded is in the correct format, that the size of the file does not exceed 10MB, and that the file name does not exceed 100 characters in total. If the file is valid, the file is converted to a byte array and saved to the database.

Displaying a File from the Database

Now that our files are being saved to the database, we can work on fetching them from the database and displaying them. Let’s first create a page in order to be able to view our files:

@page "/files"
@inject NavigationManager NavigationManager
@inject IFileRecordRepository FileRecordRepository
@inject IJSRuntime JSRuntime

<PageTitle>My Files</PageTitle>

<h1 class="d-inline me-2">My Files</h1>

<p class="d-inline align-text-bottom">
    <button type="button" @onclick="CreateFileRecord" class="btn btn-success">Create</button>
</p>

@if (files == null)
{
    <p>Loading...</p>
}
else
{
    <RadzenDataGrid Data="@files" TItem="FileRecordViewModel" AllowPaging="true" PageSize="5" AllowSorting="true" 
        AllowColumnResize="true" GridLines="DataGridGridLines.None" class="border-0">
        <Columns>
            <RadzenDataGridColumn TItem="FileRecordViewModel" Property="Created" Title="Created">
                <Template Context="data">
                    <text>@data.Created.ToString("dd/MM/yy HH:mm")</text>
                </Template>
            </RadzenDataGridColumn>
            <RadzenDataGridColumn TItem="FileRecordViewModel" Property="FileDisplayName" Title="Name" />
            <RadzenDataGridColumn TItem="FileRecordViewModel" CssClass="text-end">
                <Template Context="data">
                    <button type="button" @onclick="() => ViewFileRecord(data.Id)" class="btn btn-info">View</button>
                    <button type="button" @onclick="() => DeleteFileRecord(data.Id)" class="btn btn-danger">Delete</button>
                </Template>
            </RadzenDataGridColumn>
        </Columns>
    </RadzenDataGrid>
}

@code {
    FileRecordViewModel[]? files;

    protected override async Task OnInitializedAsync() =>
        files = await FileRecordRepository.GetFileRecordsAsync();

    private void CreateFileRecord() =>
        NavigationManager.NavigateTo("file/create");

    private void ViewFileRecord(int id) =>
        NavigationManager.NavigateTo($"file/view/{id}");

    private async Task DeleteFileRecord(int id)
    {
        bool confirmed = await JSRuntime.InvokeAsync<bool>("confirm",
            "Are you sure you wish to delete this file record?");
        if (confirmed)
        {
            await FileRecordRepository.DeleteFileRecord(id);
            files = await FileRecordRepository.GetFileRecordsAsync();
        }
    }
}

We will need the following FileRecordViewModel to represent each record:

public class FileRecordViewModel
{
    public int Id { get; set; }

    public DateTime Created { get; set; }

    public string? FileDisplayName { get; set; }

    public byte[]? FileContent { get; set; }

    public string? FileExtension { get; set; }
}

We will also need to add the following methods to our FileRecordRepository and its interface to enable us to fetch and delete records:

public async Task<FileRecordViewModel[]> GetFileRecordsAsync()
{
    return await context
        .FileRecords
        .Select(f => new FileRecordViewModel
        {
            Id = f.Id,
            Created = f.Created,
            FileDisplayName = f.FileDisplayName,
            FileContent = f.FileContent,
            FileExtension = f.FileExtension
        })
        .OrderByDescending(vm => vm.Created)
        .ToArrayAsync();
}

public async Task DeleteFileRecord(int id)
{
    FileRecord? fileRecord = await context
        .FileRecords
        .FindAsync(id) ??
        throw new InvalidOperationException($"Could not delete file record with Id {id}. Record does not exist.");

    context.Remove(fileRecord);
    await context.SaveChangesAsync();
}

Now we can build a page to be able to view our file. Create the following ViewFile.razor page:

@page "/file/view/{id:int}"
@inject IFileRecordRepository FileRecordRepository
@inject NavigationManager NavigationManager

<PageTitle>View File</PageTitle>

<h1>View File</h1>

@if (fileRecord == null)
{
    <p>Loading...</p>
}
else
{
    <FileDisplay FileContent="@fileRecord.FileContent"
             FileExtension="@fileRecord.FileExtension" />
}

<p>
    <button type="button" @onclick="Navigate" class="btn btn-secondary">Back</button>
</p>

@code {
    [Parameter] public int Id { get; set; }

    FileRecordViewModel? fileRecord;

    protected override async Task OnParametersSetAsync() =>
        fileRecord = await FileRecordRepository.GetFileRecordAsync(Id);

    private void Navigate() =>
        NavigationManager.NavigateTo("/files");
}

Add the following method to the FileRecordRepository and its interface to allow us to fetch a single record:

public async Task<FileRecordViewModel> GetFileRecordAsync(int id)
{
    return await context
        .FileRecords
        .Where(f => f.Id == id)
        .Select(f => new FileRecordViewModel
        {
            Id = f.Id,
            Created = f.Created,
            FileDisplayName = f.FileDisplayName,
            FileContent = f.FileContent,
            FileExtension = f.FileExtension
        })
        .FirstAsync();
}

Now create the following FileDisplay.razor component:

@inject IFileService FileService

@if (!string.IsNullOrEmpty(pdfContent))
{
    <embed src="@pdfContent" width="600" height="600" />
}
else if (!string.IsNullOrWhiteSpace(imageContent))
{
    <img src="@imageContent" class="w-100 h-auto" />
}

@code {
    [Parameter] public byte[] FileContent { get; set; } = default!;
    [Parameter] public string FileExtension { get; set; } = default!;

    string? pdfContent;
    string? imageContent;

    protected override void OnParametersSet()
    {
        string fileContentBase64 = FileService.GetFileAsBase64String(FileContent, FileExtension);

        if (FileExtension == ".pdf")
            pdfContent = fileContentBase64;
        else
            imageContent = fileContentBase64;
    }
}

Finally, add the following GetFileAsBase64String method to our FileService and its interface:

public string GetFileAsBase64String(byte[] fileContent, string fileExtension)
{
    string fileContentBase64 = string.Empty;

    if (fileExtension == ".pdf")
        fileContentBase64 = "data:application/pdf;base64,";
    else if (fileExtension == ".png")
        fileContentBase64 = "data:image/png;base64,";

    fileContentBase64 += Convert.ToBase64String(fileContent);

    return fileContentBase64;
}

Here we are returning the file in a base 64 format based on its file type. In our FileDisplay component, we then either display an image or embed a PDF.

That concludes this blog post and code example of saving PDF and image files to a database. We have built a form that allows us to obtain a file from the user, before turning this file into a byte array and storing it in the database. In order to display our file, we then turn our byte array into a base 64 string that can then be used to either display an image or embed a PDF to the page.

Scroll to Top