Working with Google Cloud Storage for ASP.NET Core 6 MVC Application using Entity Framework Core 6

In this tutorial, we will see how to work with Google Cloud Storage when implementing ASP.NET Core MVC applications using Entity Framework Core. The same approach described in this step-by-step tutorial will be applicable to ASP.NET Core WebAPI projects or ASP.NET Core Minimal API projects.

This is the real time demonstration of performing file based CRUD operations directly on to the Google Cloud Storage. You can follow along with this tutorial if you have a GCP account.

The full source code of this project is available on github repository

Working with Google Cloud Storage for ASP.NET Core 6 MVC Application using Entity Framework Core 6 - TutLinks
Working with Google Cloud Storage for ASP.NET Core 6 MVC Application using Entity Framework Core 6 – TutLinks

Table of Contents

File operations on Google Cloud Storage bucket

The file CRUD operations on a Google Cloud Storage Bucket demonstrated in this tutorial performs the following in a step-by-step approach with real demonstration.

  • Create – Uploading a file to Google Cloud Storage Bucket
  • Read – Retrieving a signed URL of a file from GCS Bucket
  • Update – Delete and Upload a.k.a replace a file in GCS Bucket and
  • Delete – Removing a file from a GCS Bucket

Lets proceed and have some inital set up on GCP.

Set up GCP Project

Creating a GCP account is free and you will get $300 Credit if you sign up with your credit card.

Once you sign up follow the steps. (Or you can refer to the instructions demonstrated in the video)

  • Login to https://console.google.com
  • Crate A Project
  • Create A Bucket
  • Create A Service Account with account access to Storage Object Admin Or a owner.
  • Create a key for Service Account
  • Download the Service Account key in JSON format and save it on your PC. Let’s say it is at “C:\Users<YourUserName>\Downloads\asp-core-demo-c66a8a58dd35.json”

Create Project in Visual Studio

We will now create a new ASP.NET Core MVC project in Visual Studio, mentioned as follows.

  • Launch Visual Studio 2022. I have Visual Studio 2022 installed. You can have either Visual Studio 2019 or Visual Studio 2022.
  • Create ASP.NET Core 6 MVC Razor Project from the template ASP.NET Core Web App (Model-View-Controller)
  • In Configure your new project screen, provide necessary input and select the check box Place solution and project in the same directory.
  • In Additional Information screen, choose .NET 6.0 (Long Term Support) for Framework dropdown. Optionally choose to select Configure HTTPS.
  • Also choose to select Enable Docker and ensure you have Docker Desktop installed on your PC.
  • Choose Linux as Docker OS and click on Create.

The project will be created and you will be landed on IDE where you can start to code.

Update appsettings.json

Replace the appsettings.json file with the code shown as follows.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "GCPStorageAuthFile": "",
  "GoogleCloudStorageBucketName": ""
}

Now, add the values for GCPStorageAuthFile and GoogleCloudStorageBucketName

  "GCPStorageAuthFile": "C:\Users\<YourUserName>\Downloads\asp-core-demo-c63a8a58dd39.json",
  "GoogleCloudStorageBucketName": "demo-bucket-asp"

You can update value for GCPStorageAuthFile and GoogleCloudStorageBucketName accordingly.

Create GCS Config Options class

Now let’s create a file GCSConfigOptions.cs in the following path in our project Utils/ConfigOptions/GCSConfigOptions.cs

    public class GCSConfigOptions
    {
        public string? GCPStorageAuthFile { get; set; }
        public string? GoogleCloudStorageBucketName { get; set; }
    }

Register GCS Config Options class in services

Let’s register the GCSConfigOptions.cs in Program.cs Update Program.cs as follows.

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.Configure<GCSConfigOptions>(builder.Configuration); // <-- Add this line

Install Nuget Packages

In Visual Studio IDE, navigate to menu Tools -> Nuget Packet Manager -> Package Manager Console.

Now let’s proceed with the installation of packages that help with interacting with Google Cloud Storage.

Install Nuget – Google.Cloud.Storage.V1

In the Package Manager Console run the following command to install Google.Cloud.Storage.V1

NuGet\Install-Package Google.Cloud.Storage.V1

The version 4.1.0 of Google.Cloud.Storage.V1 was installed.

Now, we will proceed creating a Service that will allow us to interact with Google Cloud Storage bucket and facilitates upload, delete and obtaining signed url of file.

Create a Cloud Storage Service

Now let’s create a file CloudStorageService.cs in the following path in our project Services/CloudStorageService.cs

Create an interface to interact with Cloud Storage

In the CloudStorageService.cs or in a in a new file with in Services directory, add a interface ICloudStorageService with the following method signatures.

    public interface ICloudStorageService
    {
        Task<string> GetSignedUrlAsync(string fileNameToRead, int timeOutInMinutes=30);
        Task<string> UploadFileAsync(IFormFile fileToUpload, string fileNameToSave);
        Task DeleteFileAsync(string fileNameToDelete);
    }

The ICloudStorageService interface has three methods signatures as follows:

  • GetSignedUrlAsync to retrieved the file’s signed url from the Cloud Storage bucket. It takes input arguments fileNameToRead and optional timeOutInMinutes. This method returns the signed url identified by string data type.
    • fileNameToRead is a string and indicates the name as is stored in the Cloud Storage bucket.
    • timeOutInMinutes is an integer that indicates number of minutes that the signed url will be valid for.
  • UploadFileAsync to upload a file to the Cloud Storage bucket. It takes to arguments namely fileToUpload and fileNameToSave. This method returns the MediaLink of the uploaded file identified by string data type. –fileToUpload is of type IFromFile which will be uploaded to the Cloud Storage bucket and
    • fileNameToSave is the name that we want to save the file as in the Cloud Storage bucket.
  • DeleteFileAsync to delete a file form the Cloud Storage bucket. It takes input argument fileNameToDelete.
    • fileNameToDelete is the name of the file that we want to delete from the Cloud Storage bucket.

Now, we will implement the interface ICloudStorageService in the class CloudStorageService as follows.

Initialize the Constructor of CloudStorageService

Add Constructor with the code shown as follows for the class CloudStorageService.

        private readonly GCSConfigOptions _options;
        private readonly ILogger<CloudStorageService> _logger;
        private readonly GoogleCredential _googleCredential;

        public CloudStorageService(IOptions<GCSConfigOptions> options,
                                    ILogger<CloudStorageService> logger)
        {
            _options = options.Value;
            _logger = logger;
            try
            {
                var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
                if (environment == Environments.Production)
                {
                    // Store the json file in Secrets.
                    _googleCredential = GoogleCredential.FromJson(_options.GCPStorageAuthFile);
                }
                else
                {
                    _googleCredential = GoogleCredential.FromFile(_options.GCPStorageAuthFile);
                }
            }
            catch (Exception ex)
            {
                _logger.LogError($"{ex.Message}");
                throw;
            }
        }

The constructor injects the IOptions<GCSConfigOptions> and ILogger<CloudStorageService> which are initialized to local read only class level fields.

We are also maintaing the GoogleCredential based on the ASPNETCORE_ENVIRONMENT. In case of Production, we are obtaining the GoogleCredential from the Json, which will be stored as secret in Google Cloud Project.

But on local, as we have the file downloaded at "GCPStorageAuthFile": "C:\Users\<YourUserName>\Downloads\asp-core-demo-c63a8a58dd39.json",, we will obtain the GoogleCredential from file.

Implement Upload File Async for Google Cloud Storage

We will now proceed with the implementation of UploadFileAsync as shown in the code as follows.

        public async Task<string> UploadFileAsync(IFormFile fileToUpload, string fileNameToSave)
        {
            try
            {
                _logger.LogInformation($"Uploading: file {fileNameToSave} to storage {_options.GoogleCloudStorageBucketName}");
                using (var memoryStream = new MemoryStream())
                {
                    await fileToUpload.CopyToAsync(memoryStream);
                    // Create Storage Client from Google Credential
                    using (var storageClient = StorageClient.Create(_googleCredential))
                    {
                        // upload file stream
                        var uploadedFile = await storageClient.UploadObjectAsync(_options.GoogleCloudStorageBucketName, fileNameToSave, fileToUpload.ContentType, memoryStream);
                        _logger.LogInformation($"Uploaded: file {fileNameToSave} to storage {_options.GoogleCloudStorageBucketName}");
                        return uploadedFile.MediaLink;
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error while uploading file {fileNameToSave}: {ex.Message}");
                throw;
            }
        }

Notice that the UploadFileAsync is an async method that returns Task<string>.

In UploadFileAsync method we are doing the following,

  • we are building a memory stream from the IFromFile input variable.
  • We are then obtaining the instance of StorageClient using the static method Create available on the StorageClient with the credentials we have obtained from JSON file and stored in field _googleCredential.
  • We are invoking the UploadObjectAsync by providing the following four values as input arguments
    • bucket name to which the file will be uploaded.
    • name of the file to be saved after uploading to Cloud Storage bucket.
    • content type of the file being uploaded.
    • memory stream of the file being uploaded.
  • Finally we are returning the MediaLink of the uploaded file.
  • We are also having error handling with the help of try catch blocks with logging the info and errors in a detailed manner.

Implement Delete File Async for Google Cloud Storage

We will now proceed with the implementation of DeleteFileAsync as shown in the code as follows.

        public async Task DeleteFileAsync(string fileNameToDelete)
        {
            try
            {
                using (var storageClient = StorageClient.Create(_googleCredential))
                {
                    await storageClient.DeleteObjectAsync(_options.GoogleCloudStorageBucketName, fileNameToDelete);
                }
                _logger.LogInformation($"File {fileNameToDelete} deleted");
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error occured while deleting file {fileNameToDelete}: {ex.Message}");
                throw;
            }
        }

Notice that the DeleteFileAsync is an async method that returns nothing.

In DeleteFileAsync method we are doing the following based on the fileNameToDelete that is provided as an input argument,

  • We are first obtaining the instance of StorageClient using the static method Create available on the StorageClient with the credentials we have obtained from JSON file and stored in field _googleCredential.
  • We are invoking the DeleteObjectAsync by providing the following two values as input arguments
    • The bucket from which the file needs to be deleted.
    • And the second argument is the name of the file as it is stored in the Cloud Storage bucket.
  • We are also having error handling with the help of try catch blocks with logging the info and errors in a detailed manner.

Implement Get Signed Url Async for Google Cloud Storage

We will now proceed with the implementation of GetSignedUrlAsync as shown in the code as follows.

        public async Task<string> GetSignedUrlAsync(string fileNameToRead, int timeOutInMinutes=30)
        {
            try
            {
                var sac = _googleCredential.UnderlyingCredential as ServiceAccountCredential;
                var urlSigner = UrlSigner.FromServiceAccountCredential(sac);
                // provides limited permission and time to make a request: time here is mentioned for 30 minutes.
                var signedUrl = await urlSigner.SignAsync(_options.GoogleCloudStorageBucketName, fileNameToRead, TimeSpan.FromMinutes(timeOutInMinutes));
                _logger.LogInformation($"Signed url obtained for file {fileNameToRead}");
                return signedUrl.ToString();
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error occured while obtaining signed url for file {fileNameToRead}: {ex.Message}");
                throw;
            }
        }

Notice that the GetSignedUrlAsync is an async method that returns Task<string>.

Obtaining the Signed Url of a file from the Google Cloud Storage bucket is a different approach than uploading and deleting a file.

To Upload and Delete a file, we did invocation of the methods UploadObjectAsync and DeleteObjectAsync respectively on the instance of StorageClient class with the help of _googleCredential.

Whereas to obtain the signed url of a file from the Google Cloud Storage bucket, we need to obtain the instance of UrlSigner from the Service Account Credential.

To obtain Service Account Credential, we need to cast the UnderlyingCredential availabe on _googleCredential to the type ServiceAccountCredential as shown in the following code snippet.

var sac = _googleCredential.UnderlyingCredential as ServiceAccountCredential;
var urlSigner = UrlSigner.FromServiceAccountCredential(sac);

Then you can act on the method SignAsync on the instance of UrlSigner by providing the following three input arguements.

  • the name of the Cloud Storage bucket,
  • the name of the file on the Cloud Storage bucket for which the signed url need to be obtained and
  • the time span that defines the validity of the url

We will now proceed with registering our CloudStorageService in the next section.

Register Cloud Storage Service as singleton

As we are done with the implementation of the CloudStorageService, we will register it as singleton service on the WebApplication builder. For that update Program.cs and register ICloudStorageService as singleton as follows.

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.Configure<GCSConfigOptions>(builder.Configuration);
builder.Services.AddSingleton<ICloudStorageService, CloudStorageService>(); // <-- Add this line

Implement MVC application to handle file uploads on GCP Cloud Storage

We will now implement an MVC application that will facilitate uploading, replacing and deleting the file directly on the Google Cloud Storage bucket.

In the next steps, we will add a model named Animal with one of the fields of type IFormFile. We then scaffold the Animal model to create AnimalsController, which is a Controller that performs Create, Read, Update and Delete actions with the help of CreateDetailsEdit and Delete Action methods respectively on our model. The scaffolding activity also creates views that facilitates performing the aforementioned actions on the Animal model.

The upload functionality will be implemented in Create.cshtml The replace functionality will be implemented in Edit.cshtml The delete functionality will be implemented in Delete.cshtml

After the scaffolding is completed, before we modify our controller actions and views with the functionality to work with files, we will generate migrations and the apply those migrations and update the database. Our database is a locally running postgreSQL database.

You can refer the following to set up Postgres on Windows or Ubuntu.

Add a model with IFormFile field

Now let’s create a file Animal.cs in the following path in our project Models/Animal.cs

    public class Animal
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }

        [NotMapped]
        public IFormFile? Photo { get; set; }
        public string? SavedUrl { get; set; }

        [NotMapped]
        public string? SignedUrl { get; set; }
        public string? SavedFileName { get; set; }
    }

The Animal model is an Entity Framework representation of the model. It has various fileds. The filed attributed as [NotMapped] will not be a part of Animal table and doesn’t reflect in the list of columns. The fields that are optional are indicated by ? next to its type.

The Photo of the animal is of type IFormFile. This field will be used to hold the file object which can we passed to the ICloudStorageService method UploadFileAsync.

Scaffold the Model and Add a Controller

In the Solution Explorer, find the Controllers directory and right click and find Add -> New Scaffolded item….

In the Add New Scaffoled Item view, under the Installed tree, choose Common -> MVC and select MVC Controller with views, using Entity Framework.

You will be prompted with a window titled MVC Controller with views, using Entity Framework.

  • Find and choose the Model Class as Animal.
  • For Data context class as we do not have any at this point, click on + icon to automatically scaffold and create a Data context class for us.
  • Give a name for your data context class or proceed with the default one populated and click on Add. under the Views field, check all the three options Generate viewsReference script librariesuse a layout page and set ~/Views/Shared/_Layout.cshtml as layout page.
  • finally, provide the Controller name or leave it as default populated name AnimalsController and click on Add.

The Visual Studio will scaffold the controller and views for your model by doing the following

  • adds necessary packages including Entity Framework Core, Entity Framework supporting package for MSSQL (which we will delete as we rely on Postgres)
  • updates program.cs and registers our controller and Database context using Sql Server as database by default.

Now we will update our project to point to Postgres Database.

Uninstall Nuget – Microsoft.EntityFrameworkCore.SqlServer

As we point our database to PostgreSQL server, we will remove the Microsoft.EntityFrameworkCore.SqlServer package that got added by default when we scaffoled the Animal model.

To uninstall the Microsoft.EntityFrameworkCore.SqlServer package, in Visual Studio IDE, navigate to menu Tools -> Nuget Packet Manager -> Package Manager Console and run the following command.

NuGet\UnInstall-Package Microsoft.EntityFrameworkCore.SqlServer

Now let’s proceed with the installation of Npgsql.EntityFrameworkCore.PostgreSQL package that help with interacting with PostgreSQL database.

Install Nuget – Npgsql.EntityFrameworkCore.PostgreSQL

In Visual Studio IDE, navigate to menu Tools -> Nuget Packet Manager -> Package Manager Console.

In the Package Manager Console run the following command to install Npgsql.EntityFrameworkCore.PostgreSQL

NuGet\Install-Package Npgsql.EntityFrameworkCore.PostgreSQL

The version 6.0.7 of Google.Cloud.Storage.V1 was installed.

UseNpgSql in Program.cs & Update Connection String

Update the Program.cs such that the AddDbContext Service invokation appears as follows:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AnimalKingdomContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("AnimalKingdomContext") ?? throw new InvalidOperationException("Connection string 'AnimalKingdomContext' not found.")));

Now, lets update appsettings.json to add connection string for PostgreSQL database.

Update appsettings.json to reflect Connection String for PostgreSQL

Replace the appsettings.json file with the code shown as follows.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "AnimalKingdomContext": "Server=localhost;Port=5432;Database=AnimalKingdomDb;User Id=postgres;Password=121212"
  },
  "GCPStorageAuthFile": "C:\Users\<YourUserName>\Downloads\asp-core-demo-c63a8a58dd39.json",
  "GoogleCloudStorageBucketName": "demo-bucket-asp"
}

Notice that the ConnectionStrings has AnimalKingdomContext pointing to a PostgreSQL database. For PostgreSQL database, the connection string is of the following format.

"AnimalKingdomContext": "Server=<DATABSE_SERVER>;Port=<PORT_NUMBER>;Database=<DATABASENAME>;User Id=<POSTGRESUSERNAME>;Password=<YOURSECRETPASSWORD>"

Refer this video on how to install PostgreSQL (latest) without Admin Rights on Windows 11 OS.

Add Migrations

We will now generate migrations for our Animal model by running the following command in Tools -> Nuget Packet Manager -> Package Manager Console.

Add-Migration init-animal-kingdom

Now, let’s reflect migrations in Database

Update Database

In order to apply migrations that we just added on to the PostgreSQL database, ensure the Database Server is up and running.

Run the following command to apply Entity Frameowrk Core migrations to the PostgreSQL database.

Update-Database

The preceding command will create the database if it doesn’t exists. The name of the database will be the one that we have specified in our connection string. In our case it will be AnimalKingdomDb. Then it will run all the scripts that are a part of migration which will lead to creation of Animal table in the AnimalKingdomDb database of PostgreSQL Database Server.

Now we will modify the scaffolded controller actions and views to accommodate interacting with Photo of Animal.

Add changes to Controller Actions and Views to support file uploads to Google Cloud Storage

Before we proceed with implementing changes in MVC controller actions and views, we will do some cosmetic changes that will make our AnimalKingdom application look more appealing and intuitive.

Update Shared Layout

Add navigation menu item to access Animals Index view in the Views/Shared/_Layout.cshtml

                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Animals" asp-action="Index">Animals</a>
                        </li>

Update Homepage

We will now update the home page to show a poster of Animal Kingdom by performing the following steps.

  • Create a directory named images in wwwroot.
  • Add a image file to wwwroot/images/animals-home.webp to represent animal kingdom home page. You can get this file from the code repository of this tutorial.
  • Update Views/Home/Index.cshtml as that refers to the image animals-home.webp
@{
    ViewData["Title"] = "Welcome to Animal Kindgom!";
}

<div class="row">
    <div class="col-md-12 text-center">
        <h1 class="display-4">@ViewData["Title"]</h1>
        <img src="~/images/animals-home.webp" class="img-fluid" alt="@ViewData["Title"]" />
    </div>
</div>

Update Constructor

Let’s update the AnimalsController class’ constructor as follows.

        private readonly GCPFileUploadNETCore6Context _context;
        private readonly ICloudStorageService _cloudStorageService;

        public AnimalsController(GCPFileUploadNETCore6Context context, ICloudStorageService cloudStorageService)
        {
            _context = context;
            _cloudStorageService = cloudStorageService;
        }

Here the AnimalsController will be uploading and deleting files as a part of it’s controller actions. So we are injecting the ICloudStorageService. with this, we can access methods defined in this interface such as GetSignedUrlAsyncUploadFileAsync and DeleteFileAsync.

Now, lets proceed to update the code in Create animal with Photo file uploaded during creation.

Implement MVC for Create View with File Upload functionality to Cloud Storage

We will now implement the functionality that allows creating Animal with its Photo uploaded to GCS. Let’s see how the Create.cshtml view and the corresponding Create action will look like in this section.

Create Action

Ensure IFormFile property in the Bind method of HttpPost Create Action. [Bind("Id,Name,Age,Photo,SavedUrl,SavedFileName")] Animal animal

We will be calling _cloudStorageService.UploadFileAsync method only when Photo is uploaded from the client, which in our case is the Create.cshtml view.

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("Id,Name,Age,Photo,SavedUrl,SavedFileName")] Animal animal)
        {
            if (ModelState.IsValid)
            {
                // START: Handling file upload to GCS
                if (animal.Photo != null) 
                {
                    animal.SavedFileName = GenerateFileNameToSave(animal.Photo.FileName);
                    animal.SavedUrl = await _cloudStorageService.UploadFileAsync(animal.Photo, animal.SavedFileName);
                }
                // END: Handling file upload to GCS
                _context.Add(animal);
                await _context.SaveChangesAsync();
                return RedirectToAction(nameof(Index));
            }
            return View(animal);
        }

One we check the animal.Photo != null, we are proceeding with creating a unique filename as returned by GenerateFileNameToSave and then uploading it to the Google Cloud Storage bucket by invoking call to _cloudStorageService.UploadFileAsync.

The GenerateFileNameToSave that is described in the following code snippet returns a file name with timestamp. For uniquness, you can also generate a Guid to append to the file name.

        private string GenerateFileNameToSave(string incomingFileName)
        {
            var fileName = Path.GetFileNameWithoutExtension(incomingFileName);
            var extension = Path.GetExtension(incomingFileName);
            return $"{fileName}-{DateTime.Now.ToUniversalTime().ToString("yyyyMMddHHmmss")}{extension}";
        }

Now, lets proceed to update the code in Create view.

Create View

Add the enctype attribute with value multipart/form-data to the form control, like <form asp-action="Create" enctype="multipart/form-data">

Update form fields such that the Photo is of type="file"

            <div class="form-group">
                <label asp-for="Photo" class="control-label"></label>
                <input asp-for="Photo" type="file" class="form-control">
                <span asp-validation-for="Photo" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input asp-for="SavedUrl" type="hidden" class="form-control" />
            </div>
            <div class="form-group">
                <input asp-for="SavedFileName" type="hidden" class="form-control" />
            </div>

For brevity, only the specific code related to handling file upload is mentioned in the aforementioned code snippet.

Now, lets proceed to update the code in Details for animal with the signed url of Photo to show in details view..

Implement MVC for Details View with Show file from Signed URL of a file in Cloud Storage

We will now implement the functionality that allows to see Animal details with its Photo obtained as signed url from the GCS. Let’s see how the Details.cshtml view and the corresponding Details action will look like in this section.

Lets proceed to update the code in Details action.

Details Action

Now we will modify Details action to handle generation of signed url. We will generate Signed URL only when SavedFileName is available for Animal.

        public async Task<IActionResult> Details(int? id)
        {
            if (id == null || _context.Animal == null)
            {
                return NotFound();
            }

            var animal = await _context.Animal
                .FirstOrDefaultAsync(m => m.Id == id);
            if (animal == null)
            {
                return NotFound();
            }
            // START: Handling Signed Url Generation from GCS
            await GenerateSignedUrl(animal);
            // END: Handling Signed Url Generation from GCS
            return View(animal);
        }

Where as the method, GenerateSignedUrl will be as follows.

        private async Task GenerateSignedUrl(Animal? animal)
        {
            // Get Signed URL only when Saved File Name is available.
            if (!string.IsNullOrWhiteSpace(animal.SavedFileName))
            {
                animal.SignedUrl = await _cloudStorageService.GetSignedUrlAsync(animal.SavedFileName);
            }
        }

From the preceeding code, we notice that we are generating Signed URL only when SavedFileName is available.

Now, lets proceed to update the code in Details view.

Details View

To show image on the razor view, use img tag with src attribute set to the value of SignedUrl available on the Model object.

Optionally set alt attribute of the img tag with SavedFileName of the Model.

        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Photo)
        </dt>
        <dd class="col-sm-10">
            <img src="@Model.SignedUrl" class="figure-img img-fluid rounded" alt="@Model.SavedFileName">
        </dd>

For brevity, only the specific code related to showing image of the Animal is mentioned in the aforementioned code snippet.

Now, lets proceed to update the code in Edit animal with Photo file and see how it can be replaced during editing of a record.

Implement MVC for Edit View with File Upload functionality to Cloud Storage

We will now implement the functionality that allows editing Animal with its Photo uploaded to GCS. Let’s see how the Edit.cshtml view and the corresponding Edit actions will look like in this section.

Lets proceed to update the code in HttpGet for Edit action.

HttpGet for Edit Action

Now, for the action that performs HttpGet that offers a view to Edit, we will retrieve the Signed URL only when SavedFileName is available.

        public async Task<IActionResult> Edit(int? id)
        {
            if (id == null || _context.Animal == null)
            {
                return NotFound();
            }

            var animal = await _context.Animal.FindAsync(id);
            if (animal == null)
            {
                return NotFound();
            }
            // START: Handling Signed Url Generation from GCS
            await GenerateSignedUrl(animal);
            // END: Handling Signed Url Generation from GCS
            return View(animal);
        }

From the preceeding code, we notice that we are generating Signed URL only when the animal is found in the database.

Now, lets proceed to update the code in HttpPost for Edit action.

HttpPost for Edit Action

Ensure IFormFile property in the Bind list of HttpPost Edit Action. [Bind("Id,Name,Age,Photo,SavedUrl,SavedFileName")] Animal Animal

During edit we will replace the existing image if there is a Photo file being uploaded while edit.

We will first delete the exisiting image, if it’s already present. And then upload the latest one.

        private async Task ReplacePhoto(Animal animal)
        {
            if (animal.Photo != null)
            {
                //replace the file by deleting animal.SavedFileName file and then uploading new animal.Photo
                if (!string.IsNullOrEmpty(animal.SavedFileName))
                {
                    await _cloudStorageService.DeleteFileAsync(animal.SavedFileName);
                }
                animal.SavedFileName = GenerateFileNameToSave(animal.Photo.FileName);
                animal.SavedUrl = await _cloudStorageService.UploadFileAsync(animal.Photo, animal.SavedFileName);
            }
        }

Replace the Photo and then update the model and finally save it to the DbContext as shown.

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(int id, [Bind("Id,Name,Age,Photo,SavedUrl,SavedFileName")] Animal animal)
        {
            if (id != animal.Id)
            {
                return NotFound();
            }

            if (ModelState.IsValid)
            {
                try
                {
                    // START: Handling file replace in GCS
                    await ReplacePhoto(animal);
                    // END: Handling file replace in GCS
                    _context.Update(animal);
                    await _context.SaveChangesAsync();
                }
                catch (DbUpdateConcurrencyException)
                {
                    if (!AnimalExists(animal.Id))
                    {
                        return NotFound();
                    }
                    else
                    {
                        throw;
                    }
                }
                return RedirectToAction(nameof(Index));
            }
            return View(animal);
        }

Now, lets proceed to update the code in Edit view.

Edit View

Add the enctype attribute to the form control, like <form asp-action="Create" enctype="multipart/form-data">

Update form fields such that the Photo is of type="file"

@model AnimalKingdom.Models.Animal

@{
    ViewData["Title"] = "Edit";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Edit</h1>

<h4>Animal</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit" enctype="multipart/form-data">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Age" class="control-label"></label>
                <input asp-for="Age" class="form-control" />
                <span asp-validation-for="Age" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Photo" class="control-label"></label>
                <input asp-for="Photo" type="file" class="form-control">
                <span asp-validation-for="Photo" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input asp-for="SavedUrl" type="hidden" class="form-control" />
            </div>
            <div class="form-group">
                <input asp-for="SavedFileName" type="hidden" class="form-control" />
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>
<div class="row">
    @if (!String.IsNullOrWhiteSpace(Model.SignedUrl))
    {
        <div class="md-col-2">
            @Html.DisplayNameFor(model => model.Photo)
        </div>
        <div cite="md-col-10">
                <img src="@Model.SignedUrl" class="figure-img img-fluid rounded" alt="@Model.SavedFileName">
        </div>
    }
</div>
<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

In Edit view, in addition to allowing uploading a file during edit, we are also showing the previously uploaded Photo if available using the following cshtml code snippet.

<div class="row">
    @if (!String.IsNullOrWhiteSpace(Model.SignedUrl))
    {
        <div class="md-col-2">
            @Html.DisplayNameFor(model => model.Photo)
        </div>
        <div cite="md-col-10">
                <img src="@Model.SignedUrl" class="figure-img img-fluid rounded" alt="@Model.SavedFileName">
        </div>
    }
</div>

Now, lets proceed to update the code in Delete animal with Photo file and see how it can be removed before deleting of animal record from the database.

Implement MVC for Delete View with File Delete functionality from Cloud Storage

We will now implement the functionality that allows deleting Animal with its Photo deleted from GCS. Let’s see how the Delete.cshtml view and the corresponding Delete actions will look like in this section.

Now, lets proceed to update the code in HttpGet for Delete action.

HttpGet for Delete Action

Now, for the Action that performs HttpGet that offers a view to Delete, we will retrieve the Signed URL only when animal is available in the database.

        public async Task<IActionResult> Delete(int? id)
        {
            if (id == null || _context.Animal == null)
            {
                return NotFound();
            }

            var animal = await _context.Animal
                .FirstOrDefaultAsync(m => m.Id == id);
            if (animal == null)
            {
                return NotFound();
            }
            // START: Handling Signed Url Generation from GCS
            await GenerateSignedUrl(animal);
            // END: Handling Signed Url Generation from GCS
            return View(animal);
        }     

Now, lets proceed to update the code in HttpPost for Delete action.

HttpPost for Delete Action

And for the Delete Action, that does HttpPost to actually deletes an entity, we will

  • First delete the associated Photo from the Cloud Storage bucket.
  • And then delete the Animal record from the database.
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(int id)
        {
            if (_context.Animal == null)
            {
                return Problem("Entity set 'AnimalKingdomContext.Animal'  is null.");
            }
            var animal = await _context.Animal.FindAsync(id);
            if (animal != null)
            {
                // START: Handling file delete from GCS
                if (!string.IsNullOrWhiteSpace(animal.SavedFileName))
                {
                    await _cloudStorageService.DeleteFileAsync(animal.SavedFileName);
                    animal.SavedFileName = String.Empty;
                    animal.SavedUrl = String.Empty;
                }
                // END: Handling file delete from GCS
                _context.Animal.Remove(animal);
            }
            
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }

Now, lets proceed to update the code in Delete view.

Delete View

To show image on the razor view, use img tag with src attribute set to the value of SignedUrl available on the Model object. Optionally set alt attribute of the img tag with SavedFileName of the Model.

        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Photo)
        </dt>
        <dd class="col-sm-10">
            <img src="@Model.SignedUrl" class="figure-img img-fluid rounded" alt="@Model.SavedFileName">
        </dd>

For brevity, only the specific code related to showing image of the Animal is mentioned in the aforementioned code snippet.

Now, lets proceed to update the code in Index for list of animals with signed url of Photo to show for each animal in the Index view.

Implement MVC for Index View with Show file from Signed URL of a file in Cloud Storage

We will now implement the functionality that allows to see list of Animal with each animal’s Photo obtained as signed url from the GCS. Let’s see how the Index.cshtml view and the corresponding Index action will look like in this section.

Now, lets proceed to update the code in Index action.

Index Action

The Index shows the list of animals in a tabular format. In each row of the table we will see a record that corresponds to animal. Now we will modify our logic to show Photo of the respective animal in each row by obtaining it’s signed url as shown in the following code.

        public async Task<IActionResult> Index()
        {
            var animals = await _context.Animal.ToListAsync();
            // START: Handling Signed Url Generation from GCS
            foreach (var animal in animals)
            {
                await GenerateSignedUrl(animal);
            }
            // END: Handling Signed Url Generation from GCS
            return View(animals);
        }

Now, lets proceed to update the code in Index view.

Index View

We will add a column for displaying the Photo of the respective animal in each row as shown in the following code.

<thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Age)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Photo)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Age)
                </td>
                <td>
                    @if (!String.IsNullOrWhiteSpace(item.SignedUrl))
                    {
                        <img width="80px" src="@item.SignedUrl" class="figure-img img-fluid rounded" alt="@item.SavedFileName">
                    }else{
                        <span>No Photo Available</span>
                    }
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>

Now, with the changes in place, build the project in Visual Studio using the keyboard command Ctrl + Shift + B and run the application using the keyboard shortcut F5.

You will now be able to upload, delete and see the files from the Google Cloud Storage bucket.

Video

Summary

This real-time Hands on tutorial we learnt the following

  • We had set up a GCP Project with a Cloud Storage bucket and obtained a Service Account key.
  • We then created and Set up ASP.NET Core 6 MVC with EF Core project to work with Google Cloud Storage.
  • We later implemented Cloud Storage Service to perform upload, delete and read files from GCP Cloud Storage
  • Next, we implemented MVC application to handle file uploads on GCP Cloud Storage.
  • As a part of the MVC application, we created an Entity Model named Animal and scaffolded to generate Controller and Views that allowed to perform CRUD operations on Animal Model.
  • We then modified the scaffolded Views and Controllers for Create, Edit, Details and Delete actions on Animal entity to support attaching file in the form of IFormFile.

If you found this tutorial helpful, please do (Ctrl + D) to 🔖 bookmark this page. Also please do 📢 spread the knowledge by sharing this with your friends and colleagues.

Navule Pavan Kumar Rao

I am a Full Stack Software Engineer with the Product Development experience in Banking, Finance, Corporate Tax and Automobile domains. I use SOLID Programming Principles and Design Patterns and Architect Software Solutions that scale using C#, .NET, Python, PHP and TDD. I am an expert in deployment of the Software Applications to Cloud Platforms such as Azure, GCP and non cloud On-Premise Infrastructures using shell scripts that become a part of CI/CD. I pursued Executive M.Tech in Data Science from IIT, Hyderabad (Indian Institute of Technology, Hyderabad) and hold B.Tech in Electonics and Communications Engineering from Vaagdevi Institute of Technology & Science.

This Post Has One Comment

Leave a Reply