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
Table of Contents
- File operations on Google Cloud Storage bucket
- Set up GCP Project
- Create Project in Visual Studio
- Create GCS Config Options class
- Install Nuget Packages
- Create a Cloud Storage Service
- Create an interface to interact with Cloud Storage
- Initialize the Constructor of CloudStorageService
- Implement Upload File Async for Google Cloud Storage
- Implement Delete File Async for Google Cloud Storage
- Implement Get Signed Url Async for Google Cloud Storage
- Register Cloud Storage Service as singleton
- Implement MVC application to handle file uploads on GCP Cloud Storage
- Add a model with IFormFile field
- Scaffold the Model and Add a Controller
- Uninstall Nuget – Microsoft.EntityFrameworkCore.SqlServer
- Install Nuget – Npgsql.EntityFrameworkCore.PostgreSQL
- UseNpgSql in Program.cs & Update Connection String
- Update appsettings.json to reflect Connection String for PostgreSQL
- Add Migrations
- Update Database
- Add changes to Controller Actions and Views to support file uploads to Google Cloud Storage
- Implement MVC for Create View with File Upload functionality to Cloud Storage
- Implement MVC for Details View with Show file from Signed URL of a file in Cloud Storage
- Implement MVC for Edit View with File Upload functionality to Cloud Storage
- Implement MVC for Delete View with File Delete functionality from Cloud Storage
- Implement MVC for Index View with Show file from Signed URL of a file in Cloud Storage
- Video
- Summary
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 selectConfigure HTTPS
. - Also choose to select
Enable Docker
and ensure you have Docker Desktop installed on your PC. - Choose
Linux
as Docker OS and click onCreate
.
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 argumentsfileNameToRead
and optionaltimeOutInMinutes
. 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 namelyfileToUpload
andfileNameToSave
. This method returns theMediaLink
of the uploaded file identified by string data type. –fileToUpload
is of typeIFromFile
which will be uploaded to the Cloud Storage bucket andfileNameToSave
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 argumentfileNameToDelete
.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 theStorageClient
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 theStorageClient
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 Create, Details, Edit 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.
- How to install PostgreSQL (latest) without Admin Rights on Windows 11 OS or
- Install PostgreSQL 12 on 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 views, Reference script libraries, use 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
inwwwroot
. - 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 theimage 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 GetSignedUrlAsync, UploadFileAsync 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 onAnimal
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.
Pingback: Google Cloud for ASP.NET Core Web Apps – TutLinks