How can I help you?
Connecting Firebase Firestore to Blazor Data Grid
29 May 202624 minutes to read
The Blazor DataGrid supports binding data from a Firebase Firestore database using the Google.Cloud.Firestore library. This approach provides a flexible and scalable solution for working with NoSQL cloud-based databases.
What is Firebase Firestore?
Firebase Firestore is a cloud-hosted, NoSQL database that stores data in flexible, JSON-like documents. Unlike traditional relational databases that use tables and rows, Firestore uses collections and documents, making it ideal for applications that require flexible schemas, real-time synchronization, and seamless scalability with minimal infrastructure management.
Key Benefits of Firebase Firestore
- Cloud-Hosted: No infrastructure management required; automatic scaling and maintenance.
- Flexible Schema: Store documents with different structures in the same collection without predefined schemas.
- Global Scale: Distributed architecture with automatic replication and backups.
- Security Rules: Built-in security with fine-grained access control at the document and field level.
- Rich Query Language: Supports complex queries, aggregation, and indexing.
What is Google.Cloud.Firestore?
Google.Cloud.Firestore is the official .NET client library for Firebase Firestore. It provides a comprehensive API for interacting with Firestore databases, allowing applications to perform CRUD operations, execute queries, and manage real-time listeners efficiently.
Prerequisites
Ensure the following software, accounts, and packages are in place before proceeding:
| Software/Package | Version | Purpose |
|---|---|---|
| Visual Studio 2026 | 18.0 or later | Development IDE with Blazor workload |
| .NET SDK | net10.0 or compatible | Runtime and build tools |
| Firebase Account | Active account | Cloud project hosting and Firestore database |
| Google Cloud Project | Active project | Required for Firebase services and API credentials |
| Google.Cloud.Firestore | 3.7.0 or later | Official .NET client library for Firestore |
| Syncfusion.Blazor.Grid | 33.2.3 | DataGrid and UI components |
| Syncfusion.Blazor.Themes | 33.2.3 | Styling for DataGrid components |
Setting Up the Firebase Firestore Environment
Step 1: Create a Firebase Project and Firestore Database
Firebase projects host Firestore databases and handle authentication, storage, and other cloud services.
Instructions:
-
Access Firebase Console
- Navigate to Firebase Console.
- Sign in with your Google account.
-
Create a New Firebase Project
- Click “Add Project” button.
- Enter a project name (e.g.,
LibraryManagement). - Accept the default settings or customize as needed.
- Click “Create Project” and wait for completion.
-
Create a Firestore Database
- In the Firebase Console, navigate to “Build” → “Firestore Database”.
- Click “Create Database” button.
-
Location: Choose the region closest to users (e.g.,
us-east1). - Security Rules: Start with “Start in test mode” for development (production requires authentication).
- Click “Create” and wait for the database to initialize.
-
Create a Collection and Add Sample Data
- In Firestore Console, click ”+ Start collection” button.
-
Collection ID:
Books. - Click “Next”.
- Click “Auto ID” to generate a document ID.
- Add the following sample document fields:
Sample Book Document:
| Field | Type | Value |
|---|---|---|
| bookId | string | BOOK001 |
| title | string | The Great Gatsby |
| author | string | F. Scott Fitzgerald |
| isbn | string | 978-0743273565 |
| category | string | Fiction |
| publishDate | timestamp | 1925-04-10 |
| language | string | English |
| totalCopies | number | 5 |
| availableCopies | number | 3 |
| location | string | Section A-1 |
| status | string | Available |
| lastUpdated | timestamp | (current date) |
-
Add More Sample Documents (Optional)
- Click ”+ Add document” to add more books
- Repeat the process for additional test records
The Firestore database is now ready with the Books collection and sample data.
Step 2: Generate Service Account Key for Authentication
Service account keys allow server-side applications (for example, a Blazor backend) to authenticate with Firebase.
Instructions:
-
Access Google Cloud Console
- In Firebase Console, click the Settings gear icon (⚙️) in the top-left.
- Select “Project Settings”.
- Navigate to the “Service Accounts” tab.
-
Generate a New Private Key
- Click the “Generate New Private Key” button.
- A JSON file (
serviceAccountKey.json) will be automatically downloaded. - Important: Save this file securely—it contains sensitive credentials.
-
Store the Service Account Key
- For development: Put the JSON file in the project root or a secure location.
- For production: Use environment variables or Azure Key Vault to store sensitive credentials.
-
Never commit this file to version control (add to
.gitignore).
The service account key is now available for authentication.
Step 3: Enable Required APIs in Google Cloud Console
Enable the required APIs in the Google Cloud project for Firestore.
Instructions:
-
Access Google Cloud Console
- Open Google Cloud Console.
- Select the Firebase project.
-
Navigate to APIs
- Go to “APIs & Services” → “Library”.
- Search for “Firestore API”.
- Click on the result.
- Click “Enable” button.
-
Verify Cloud Firestore API is Active
- Go to “APIs & Services” → “Enabled APIs & services”.
- Confirm “Cloud Firestore API” is in the list.
Required APIs are now enabled for the application.
Step 4: Install Required NuGet Packages
Before installing the necessary NuGet packages, a new Blazor Web Application must be created using the default template.
This template automatically generates essential starter files—such as Program.cs, appsettings.json, the wwwroot folder, and the Components folder.
For this guide, a Blazor application named Grid_Firebase_Firestore has been created. Once the project is set up, the next step involves installing the required NuGet packages. NuGet packages are software libraries that add functionality to the Blazor application. These packages enable Firebase Firestore integration and Syncfusion DataGrid components.
Method 1: Using Package Manager Console
- Open Visual Studio 2026.
- Navigate to Tools → NuGet Package Manager → Package Manager Console.
- Run the following commands:
Install-Package Google.Cloud.Firestore -Version 3.7.0
Install-Package Syncfusion.Blazor.Grid -Version 33.2.3
Install-Package Syncfusion.Blazor.Themes -Version 33.2.3Method 2: Using NuGet Package Manager UI
- Open Visual Studio 2026 → Tools → NuGet Package Manager → Manage NuGet Packages for Solution.
- Search for and install each package individually:
- Google.Cloud.Firestore (version 3.7.0 or later)
- Syncfusion.Blazor.Grid (version 33.2.3)
- Syncfusion.Blazor.Themes (version 33.2.3)
All required packages are now installed.
Step 5: Create the Data Model
A data model is a C# class that represents the structure of a Firestore document. This model defines the properties that correspond to the fields in the Books collection.
Instructions:
- Create a new folder named
Modelsin the Blazor application project. - Inside the
Modelsfolder, create a new file named Book.cs. - Define the Book class with the following code:
using System;
using Google.Cloud.Firestore;
namespace Grid_Firebase_Firestore.Models
{
/// <summary>
/// Represents a book record in the library management system.
/// This model defines the structure of book-related data used throughout the application.
/// Maps to Cloud Firestore 'books' collection.
/// </summary>
[FirestoreData]
public class Book
{
/// <summary>
/// Gets or sets the unique book identifier (e.g., BOOK001, BOOK002).
/// This serves as the document ID in Firestore.
/// </summary>
[FirestoreProperty]
public string BookId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the title of the book.
/// </summary>
[FirestoreProperty]
public string Title { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the name of the book author.
/// </summary>
[FirestoreProperty]
public string Author { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the unique International Standard Book Number (ISBN).
/// </summary>
[FirestoreProperty]
public string ISBN { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the category or genre of the book.
/// Examples: Fiction, Non-Fiction, Science, History, etc.
/// </summary>
[FirestoreProperty]
public string Category { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the publication date of the book.
/// Stored as a Firestore Timestamp for proper date handling.
/// </summary>
[FirestoreProperty]
public DateTime? PublishDate { get; set; }
/// <summary>
/// Gets or sets the language in which the book is written.
/// Examples: English, Spanish, French, etc.
/// </summary>
[FirestoreProperty]
public string? Language { get; set; }
/// <summary>
/// Gets or sets the total number of copies available in the library.
/// </summary>
[FirestoreProperty]
public int TotalCopies { get; set; }
/// <summary>
/// Gets or sets the number of copies currently available for borrowing.
/// </summary>
[FirestoreProperty]
public int AvailableCopies { get; set; }
/// <summary>
/// Gets or sets the physical location or shelf number of the book in the library.
/// </summary>
[FirestoreProperty]
public string? Location { get; set; }
/// <summary>
/// Gets or sets the name of the person who has borrowed the book (if any).
/// Null if the book is not currently borrowed.
/// </summary>
[FirestoreProperty]
public string? BorrowedBy { get; set; }
/// <summary>
/// Gets or sets the current status of the book.
/// Valid values: Available, Borrowed, Reserved, Lost, Damaged, Maintenance
/// </summary>
[FirestoreProperty]
public string Status { get; set; } = "Available";
/// <summary>
/// Gets or sets the date and time when the book record was last updated.
/// Stored as a Firestore Timestamp for proper timestamp handling.
/// Automatically set to the current UTC time on each update.
/// </summary>
[FirestoreProperty]
public DateTime LastUpdated { get; set; } = DateTime.UtcNow;
/// <summary>
/// Normalizes all DateTime fields to UTC to ensure compatibility with Firestore.
/// This is necessary because JSON deserialization may create DateTime objects
/// with DateTimeKind.Unspecified, which Firestore cannot convert to Timestamp.
/// </summary>
public void NormalizeToUtc()
{
// Normalize PublishDate
if (PublishDate.HasValue)
{
PublishDate = PublishDate.Value.Kind == DateTimeKind.Utc
? PublishDate.Value
: PublishDate.Value.ToUniversalTime();
}
// Normalize LastUpdated
if (LastUpdated.Kind != DateTimeKind.Utc)
{
LastUpdated = LastUpdated.Kind == DateTimeKind.Unspecified
? DateTime.SpecifyKind(LastUpdated, DateTimeKind.Utc)
: LastUpdated.ToUniversalTime();
}
}
}
}Explanation:
- The
Bookclass directly maps to Firestore document structure. - The
[FirestoreData]maps to documents. - The
[FirestoreProperty]explicitly maps C# properties to Firestore fields. - Each property represents a field in the Firestore document.
- The
?symbol indicates that a property is nullable (optional in Firestore). - The
NormalizeToUtc()method ensures consistent DateTime handling across different timezones. - Properties use C# conventions (PascalCase) which automatically map to Firestore document fields.
The data model has been successfully created in Models/Book.cs.
Step 6: Configure the Firebase Connection String
A Firebase connection requires the path to the service account key file. This configuration is managed in the appsettings.json file.
Instructions:
- Open the
appsettings.jsonfile in the project root. - Add a new section for Firebase configuration with the service account key path:
{
"Firebase": {
"ServiceAccountPath": "serviceAccountKey.json",
"ProjectId": "your-project-id"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}Configuration Details:
| Setting | Value | Purpose |
|---|---|---|
Firebase:ServiceAccountPath |
serviceAccountKey.json |
Path to the service account JSON file (development) |
Important Security Notes:
-
Development: Place the
serviceAccountKey.jsonfile in the project root. - Production: Use environment variables or secure credential storage (Azure Key Vault, AWS Secrets Manager).
-
Never Commit: Add
serviceAccountKey.jsonto.gitignoreto prevent accidentally uploading credentials to version control.
Example .gitignore entry:
# Firebase credentials
serviceAccountKey.json
*.json.backup
The Firebase connection configuration is now in place in appsettings.json.
Step 7: Create the Firebase Service Class
The Firebase service class manages database connections and operations with Firestore. It handles all interactions with the Firestore database without requiring Entity Framework Core.
Instructions:
- Create a new folder named
Servicesin the Blazor application project. - Inside the
Servicesfolder, create a new file named FirebaseService.cs. - Define the FirebaseService class with the following code:
using Google.Cloud.Firestore;
using Grid_Firebase_Firestore.Models;
namespace Grid_Firebase_Firestore.Services
{
/// <summary>
/// Service class for Firebase Firestore database operations
/// Handles all CRUD operations and business logic for books
/// </summary>
public class FirebaseService
{
private readonly FirestoreDb _firestoreDb;
private const string BooksCollection = "Books";
private const string BookIdPrefix = "BOOK";
public FirebaseService(IConfiguration configuration)
{
// Try environment variable first, then fall back to appsettings
var serviceAccountPath = configuration["Firebase:ServiceAccountPath"]
?? Path.Combine(AppContext.BaseDirectory, "serviceAccountKey.json");
if (!File.Exists(serviceAccountPath))
{
throw new FileNotFoundException($"Service account key not found at: {serviceAccountPath}");
}
// Set the service account credentials environment variable for Google Cloud authentication
Environment.SetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS", serviceAccountPath);
// Get project ID from configuration or service account key
var projectId = configuration["Firebase:ProjectId"];
if (string.IsNullOrEmpty(projectId))
{
throw new InvalidOperationException("Firebase project ID not found. Please configure 'Firebase:ProjectId' in appsettings.json");
}
// Initialize Firestore database
_firestoreDb = FirestoreDb.Create(projectId);
}
/// <summary>
/// Retrieves all books from the Firestore database
/// </summary>
/// <returns>List of all books</returns>
public async Task<List<Book>> GetBooksAsync()
{
try
{
var query = _firestoreDb.Collection(BooksCollection);
var snapshot = await query.GetSnapshotAsync();
var books = new List<Book>();
foreach (var doc in snapshot.Documents)
{
var book = doc.ConvertTo<Book>();
if (book != null)
{
book.BookId = doc.Id; // Use Firestore document ID
books.Add(book);
}
}
return books;
}
catch (Exception ex)
{
throw new Exception($"Error fetching books: {ex.Message}");
}
}
/// <summary>
/// Inserts a new book into the Firestore database
/// </summary>
public async Task<Book> InsertBookAsync(Book book)
{
// Handle logic to add a new book to the database
}
/// <summary>
/// Updates an existing book in the Firestore database
/// </summary>
public async Task<bool> UpdateBookAsync(string bookId, Book book)
{
// Handle logic to update an existing book to the database
}
/// <summary>
/// Deletes a book from the Firestore database
/// </summary>
public async Task<bool> DeleteBookAsync(string? bookId)
{
// Handle logic to delete an existing book from the database
}
}
}Explanation:
- The
FirebaseServiceclass manages all Firestore operations. - The
FirestoreDbinstance represents the connection to Firestore database. - Service account authentication is handled through the credentials path.
-
GetBooksAsync()retrieves all documents from theBookscollection.
The Firebase service class has been successfully created in Services/FirebaseService.cs.
Step 8: Implementing Bulk Data Import (Optional)
Bulk data import is essential for loading large volumes of data into Firebase Firestore efficiently. This section explains how to implement bulk import functionality with data validation, error handling, and progress tracking.
Overview of Bulk Import:
Bulk import allows to:
- Initial Data Seeding: Load initial datasets when launching an application.
- Data Migration: Migrate data from legacy systems to Firestore.
- Batch Processing: Regular imports of bulk data from JSON files.
- Testing: Load test data for development environments.
Firestore Limits:
- Batch Size: Maximum 500 documents per batch write.
- Document Size: Up to 1 MB per document.
- Rate Limits: 25,000 writes per second per database.
- Atomicity: All-or-nothing semantics per batch using transactions.
How Bulk Import Works:
- Data Chunking: Split large datasets into 500-document batches (Firestore limit).
- Validation: Validate each document before import.
- Atomic Transactions: Use transactions to ensure consistency (all-or-nothing).
- Error Handling: Continue processing batches even if one fails.
- Progress Tracking: Report progress to UI for user feedback.
Sample JSON data format:
Create or use a JSON file with the following structure (wwwroot/sample-books-import.json):
{
"books": [
{
"bookId": "BOOK001",
"title": "The Great Gatsby",
"author": "F. Scott Fitzgerald",
"isbn": "978-0743273565",
"category": "Fiction",
"publishDate": "1925-04-10",
"language": "English",
"totalCopies": 5,
"availableCopies": 3,
"location": "Section A-1",
"status": "Available"
},
{
"bookId": "BOOK002",
"title": "To Kill a Mockingbird",
"author": "Harper Lee",
"isbn": "978-0061120084",
"category": "Fiction",
"publishDate": "1960-07-11",
"language": "English",
"totalCopies": 4,
"availableCopies": 2,
"location": "Section A-2",
"status": "Available"
}
]
}Implement Bulk Import UI in Home.razor
Update the Components/Pages/Home.razor file to include bulk import functionality with file upload:
@page "/"
@using System.Collections
@using System.Text.Json
@rendermode InteractiveServer
@inject FirebaseService FirebaseService
@implements IAsyncDisposable
<PageTitle>Library Management</PageTitle>
<!-- Bulk Import Section - Minimal UI -->
<div class="row mb-3">
<div class="col-md-12">
<div class="d-flex gap-2 align-items-center">
<label for="bulkImportFile" class="form-label mb-0 fw-bold">📁 Bulk Import JSON:</label>
<InputFile id="bulkImportFile" class="form-control form-control-sm" style="max-width: 300px;" accept=".json" OnChange="@OnFileSelected" disabled="@ImportInProgress" />
<button class="btn @GetImportButtonClass()" @onclick="ImportBooks" disabled="@(!IsReadyToImport || ImportInProgress)">
<i class="@GetImportButtonIcon()"></i> @GetImportButtonText()
</button>
</div>
</div>
</div>
<!-- Grid Section -->@code {
private string SelectedFileName = string.Empty;
private string FileContent = string.Empty;
private List<Book> PreviewedBooks = new List<Book>();
private double FileSizeKB = 0;
private bool IsReadyToImport = false;
private bool ImportInProgress = false;
private bool ImportCompleted = false;
/// <summary>
/// Handles file selection and validates JSON format
/// </summary>
private async Task OnFileSelected(InputFileChangeEventArgs e)
{
try
{
var file = e.File;
if (file == null) return;
// Validate file
const long maxFileSize = 5 * 1024 * 1024; // 5MB
if (file.Size > maxFileSize)
{
ResetForm();
return;
}
// Read file
using (var stream = file.OpenReadStream(maxFileSize))
{
using (var reader = new StreamReader(stream))
{
FileContent = await reader.ReadToEndAsync();
}
}
SelectedFileName = file.Name;
FileSizeKB = file.Size / 1024.0;
// Parse and validate JSON
ParseJsonAndPreview();
}
catch
{
ResetForm();
}
}
/// <summary>
/// Parses JSON content and previews books
/// </summary>
private void ParseJsonAndPreview()
{
try
{
PreviewedBooks.Clear();
IsReadyToImport = false;
using (JsonDocument doc = JsonDocument.Parse(FileContent))
{
var root = doc.RootElement;
// Try to find 'books' array
if (root.TryGetProperty("books", out var booksElement) && booksElement.ValueKind == JsonValueKind.Array)
{
foreach (var bookElement in booksElement.EnumerateArray())
{
try
{
var jsonString = bookElement.GetRawText();
var book = JsonSerializer.Deserialize<Book>(jsonString, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (book != null)
{
book.NormalizeToUtc();
PreviewedBooks.Add(book);
}
}
catch
{
// Skip invalid book entries
}
}
}
}
if (PreviewedBooks.Count > 0)
{
IsReadyToImport = true;
}
else
{
ResetForm();
}
}
catch
{
ResetForm();
}
}
/// <summary>
/// Performs the bulk import operation
/// </summary>
private async Task ImportBooks()
{
if (PreviewedBooks.Count == 0 || ImportInProgress) return;
ImportInProgress = true;
ImportCompleted = false;
try
{
bool success = await FirebaseService.BulkImportBooksAsync(PreviewedBooks);
if (success)
{
ImportCompleted = true;
// Refresh the grid
if (_grid != null)
{
await _grid.Refresh();
}
// Reset after a brief delay
await Task.Delay(2000);
ResetForm();
}
else
{
// Failed - reset to allow retry
ImportInProgress = false;
}
}
catch
{
// Failed - reset to allow retry
ImportInProgress = false;
}
}
/// <summary>
/// Resets the form for another import
/// </summary>
private void ResetForm()
{
SelectedFileName = string.Empty;
FileContent = string.Empty;
PreviewedBooks.Clear();
FileSizeKB = 0;
IsReadyToImport = false;
ImportInProgress = false;
ImportCompleted = false;
}
/// <summary>
/// Gets the CSS class for the import button based on state
/// </summary>
private string GetImportButtonClass()
{
if (ImportCompleted) return "btn btn-success btn-lg";
if (ImportInProgress) return "btn btn-warning btn-lg";
return "btn btn-primary btn-lg";
}
/// <summary>
/// Gets the icon class for the import button based on state
/// </summary>
private string GetImportButtonIcon()
{
if (ImportCompleted) return "bi bi-check-circle";
if (ImportInProgress) return "bi bi-hourglass-split";
return "bi bi-cloud-upload";
}
/// <summary>
/// Gets the text for the import button based on state
/// </summary>
private string GetImportButtonText()
{
if (ImportCompleted) return "Import Successful";
if (ImportInProgress) return "Importing...";
return "Import to Firestore";
}
}Update FirebaseService with bulk import methods
Open Services/FirebaseService.cs and add the following methods to support bulk import operations:
using System.Text.Json;
/// <summary>
/// Performs bulk import of books from JSON data using batch writes.
/// Stops at the first failure to ensure data integrity.
/// </summary>
/// <param name="books">List of books to import</param>
/// <returns>True if all books imported successfully; false otherwise</returns>
public async Task<bool> BulkImportBooksAsync(List<Book> books)
{
if (books == null || books.Count == 0)
{
return false;
}
try
{
int batchSize = 500; // Firestore batch limit
// Process books in batches using transactions
for (int i = 0; i < books.Count; i += batchSize)
{
var batchBooks = books.Skip(i).Take(batchSize).ToList();
// Use a transaction to write multiple documents atomically
await _firestoreDb.RunTransactionAsync(async transaction =>
{
foreach (var book in batchBooks)
{
var docRef = _firestoreDb.Collection(BooksCollection).Document(book.BookId);
transaction.Set(docRef, book);
}
});
}
return true;
}
catch
{
// Stop at first failure and return false
return false;
}
}Step 9: Register Services in Program.cs
The Program.cs file is where application services are registered and configured. This file must be updated to enable Firestore service and Syncfusion components.
Instructions:
- Open the
Program.csfile at the project root. - Add the following code after the line
var builder = WebApplication.CreateBuilder(args);:
using Grid_Firebase_Firestore.Components;
using Grid_Firebase_Firestore.Services;
using Syncfusion.Blazor;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
// Register Syncfusion Blazor services
builder.Services.AddSyncfusionBlazor();
// Register Firebase Firestore service for dependency injection
builder.Services.AddScoped<FirebaseService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();Explanation:
-
using Grid_Firebase_Firestore.Services;: Imports the namespace to access theFirebaseServiceclass -
builder.Services.AddSyncfusionBlazor();: Registers Syncfusion Blazor services globally -
builder.Services.AddScoped<FirebaseService>();: RegistersFirebaseServicewith scoped lifetime (creates a new instance per HTTP request)
The service registration has been completed successfully in Program.cs.
Integrating Syncfusion Blazor DataGrid
Step 1: Install and Configure Blazor DataGrid Components
Syncfusion is a library that provides pre-built UI components like DataGrid, which is used to display data in a table format.
Instructions:
- The Syncfusion.Blazor.Grid package was installed in Step 4 of the previous section.
- Import the required namespaces in the
Components/_Imports.razorfile:
@using Grid_Firebase_Firestore.Models
@using Grid_Firebase_Firestore.Services
@using Syncfusion.Blazor.Grids
@using Syncfusion.Blazor.Data
@using Syncfusion.Blazor.DropDowns- Add the Syncfusion stylesheet and scripts in the
Components/App.razorfile. Find the<head>section and add:
<!-- Syncfusion Blazor Stylesheet -->
<link href="_content/Syncfusion.Blazor.Themes/tailwind3.css" rel="stylesheet" />
<!-- Syncfusion Blazor Scripts -->
<script src="_content/Syncfusion.Blazor.Core/scripts/syncfusion-blazor.min.js" type="text/javascript"></script>For this project, the tailwind3 theme is used. A different theme can be selected or the existing theme can be customized based on project requirements. Refer to the Syncfusion Blazor Components Appearance documentation to learn more about theming and customization options.
Syncfusion components are now configured and ready to use. For additional guidance, refer to the Grid component’s getting‑started documentation.
Step 2: Update the Blazor DataGrid
The Home.razor component displays the book data in a Blazor DataGrid with search, filter, sort, and pagination capabilities.
Instructions:
- Open the file named
Home.razorin theComponents/Pagesfolder. - Replace the entire file content with the following code to create a DataGrid with bulk import functionality:
@page "/"
@using System.Collections
@using System.Text.Json
@rendermode InteractiveServer
@inject FirebaseService FirebaseService
<PageTitle>Library Management</PageTitle>
<!-- Grid Section -->
<div class="row">
<div class="col-md-12">
<div class="card shadow-sm">
<div class="card-body p-0">
<SfGrid @ref="_grid" TValue="Book" AllowPaging="true" AllowSorting="true" AllowFiltering="true" AllowGrouping="true" Toolbar="@ToolbarItems">
<SfDataManager AdaptorInstance="@typeof(CustomAdaptor)" Adaptor="Adaptors.CustomAdaptor"></SfDataManager>
<GridPageSettings PageSize="10"></GridPageSettings>
<GridColumns>
//columns configuration
</GridColumns>
</SfGrid>
</div>
</div>
</div>
</div>
@code {
// CustomAdaptor class will be added in the next step
}Component Explanation:
-
@rendermode InteractiveServer: Enables interactive server-side rendering for real-time updates -
@inject FirebaseService: Injects the FirebaseService to access database methods -
<SfGrid>: The DataGrid component that displays books in rows and columns -
<GridColumns>: Defines individual columns with field mappings, types, and formatting -
AllowPaging,AllowSorting,AllowFiltering,AllowGrouping: Enable grid features -
<GridEditSettings>: Configures add, edit, and delete operations -
Toolbar: Provides buttons for CRUD and search operations
The Home component has been successfully updated with the DataGrid and CustomAdaptor.
Step 3: Implement the CustomAdaptor
The Blazor DataGrid can bind data from a Firebase Firestore database using DataManager and set the Adaptor property to CustomAdaptor for scenarios that require full control over data operations.
The CustomAdaptor is a bridge between the DataGrid and the database. It handles all data operations including reading, searching, filtering, sorting, paging, and CRUD operations. Each operation in the CustomAdaptor’s ReadAsync method handles specific grid functionality. The Blazor DataGrid sends operation details to the API through a DataManagerRequest object. These details can be applied to the data source using methods from the DataOperations class.
Instructions:
- Open the
Components/Pages/Home.razorfile. - Add the following
CustomAdaptorclass code inside the@codeblock:
@code {
public static FirebaseService? _firebaseService { get; set; }
/// <summary>
/// CustomAdaptor class bridges DataGrid interactions with database operations.
/// This adaptor handles all data retrieval and manipulation for the DataGrid.
/// </summary>
public class CustomAdaptor : DataAdaptor
{
public FirebaseService? FirebaseService
{
get => _firebaseService;
set => _firebaseService = value;
}
public override async Task<object> ReadAsync(DataManagerRequest dataManagerRequest, string? key = null)
{
try
{
// Fetch all books from the Firebase database
IEnumerable dataSource = await _firebaseService!.GetBooksAsync();
// Apply search operation if search criteria exists
if (dataManagerRequest.Search != null && dataManagerRequest.Search.Count > 0)
{
dataSource = DataOperations.PerformSearching(dataSource, dataManagerRequest.Search);
}
// Apply filter operation if filter criteria exists
if (dataManagerRequest.Where != null && dataManagerRequest.Where.Count > 0)
{
dataSource = DataOperations.PerformFiltering(dataSource, dataManagerRequest.Where, dataManagerRequest.Where[0].Operator);
}
// Apply sort operation if sort criteria exists
if (dataManagerRequest.Sorted != null && dataManagerRequest.Sorted.Count > 0)
{
dataSource = DataOperations.PerformSorting(dataSource, dataManagerRequest.Sorted);
}
// Calculate total record count before paging for accurate pagination
int totalRecordsCount = dataSource.Cast<Book>().Count();
// Apply paging skip operation
if (dataManagerRequest.Skip != 0)
{
dataSource = DataOperations.PerformSkip(dataSource, dataManagerRequest.Skip);
}
// Apply paging take operation to retrieve only the requested page size
if (dataManagerRequest.Take != 0)
{
dataSource = DataOperations.PerformTake(dataSource, dataManagerRequest.Take);
}
// Handling Group operation in CustomAdaptor.
if (dataManagerRequest.Group != null)
{
foreach (var group in dataManagerRequest.Group)
{
dataSource = DataUtil.Group<Book>(dataSource, group, dataManagerRequest.Aggregates, 0, dataManagerRequest.GroupByFormatter);
}
}
// Return the result with total count for pagination metadata
return dataManagerRequest.RequiresCounts
? new DataResult() { Result = dataSource, Count = totalRecordsCount }
: (object)dataSource;
}
catch (Exception ex)
{
Console.WriteLine($"ReadAsync Error: {ex.Message}");
throw new Exception($"An error occurred while retrieving data: {ex.Message}");
}
}
}
}The CustomAdaptor class has been successfully implemented with all data operations.
Common methods in data operations
-
ReadAsync(DataManagerRequest) - Retrieve and process records (search, filter, sort, page, group)
- PerformSearching - Applies search criteria to the collection.
- PerformFiltering - Filters data based on conditions.
- PerformSorting - Sorts data by one or more fields.
- PerformSkip - Skips a defined number of records for paging.
- PerformTake - Retrieves a specified number of records for paging.
- PerformAggregation – Calculates aggregate values such as Sum, Average, Min, and Max.
Step 4: Add Toolbar with CRUD and Search Options
The toolbar provides buttons for adding, editing, deleting records, and searching the data.
Instructions:
- Open the
Components/Pages/Home.razorfile. - Update the
<SfGrid>component to include the Toolbar property with CRUD and search options:
<SfGrid TValue="Book"
AllowPaging="true"
AllowSorting="true"
AllowFiltering="true"
Toolbar="@ToolbarItems">
<SfDataManager AdaptorInstance="@typeof(CustomAdaptor)" Adaptor="Adaptors.CustomAdaptor"></SfDataManager>
<!-- Grid columns configuration -->
</SfGrid>- Add the toolbar items list in the
@codeblock:
@code {
private List<string> ToolbarItems = new List<string> { "Add", "Edit", "Delete", "Update", "Cancel", "Search"};
// CustomAdaptor class code...
}Toolbar Items Explanation:
| Item | Function |
|---|---|
Add |
Opens a form to add a new book record. |
Edit |
Enables editing of the selected record. |
Delete |
Deletes the selected record from the database. |
Update |
Saves changes made to the selected record. |
Cancel |
Cancels the current edit or add operation. |
Search |
Displays a search box to find records. |
The toolbar has been successfully added.
Step 5: Running the Application
Build the Application
- Open the terminal or Package Manager Console.
- Navigate to the project directory.
- Run the following command:
dotnet buildRun the Application
Execute the following command:
dotnet runAccess the Application
- Open a web browser.
- Navigate to
https://localhost:5001(or the port shown in the terminal). - The library management application is now running and ready to use.

Step 6: Implement Paging Feature
Paging divides large datasets into smaller pages to improve performance and usability.
Instructions:
- The paging feature is already partially enabled in the
<SfGrid>component with AllowPaging=”true”. - The page size is configured with GridPageSettings.
- No additional code changes are required from the previous steps.
<SfGrid TValue="Book"
AllowPaging="true">
<SfDataManager AdaptorInstance="@typeof(CustomAdaptor)" Adaptor="Adaptors.CustomAdaptor"></SfDataManager>
<GridPageSettings PageSize="10"></GridPageSettings>
<!-- Grid columns configuration -->
</SfGrid>- Update the
ReadAsyncmethod in theCustomAdaptorclass to handle paging:
@code {
/// <summary>
/// CustomAdaptor class to handle grid data operations with Firebase Firestore
/// </summary>
public class CustomAdaptor : DataAdaptor
{
public static FirebaseService? _firebaseService { get; set; }
public FirebaseService? FirebaseService
{
get => _firebaseService;
set => _firebaseService = value;
}
public override async Task<object> ReadAsync(DataManagerRequest dataManagerRequest, string? key = null)
{
IEnumerable dataSource = await _firebaseService!.GetBooksAsync();
int totalRecordsCount = dataSource.Cast<Book>().Count();
// Handling Paging
if (dataManagerRequest.Skip != 0)
{
dataSource = DataOperations.PerformSkip(dataSource, dataManagerRequest.Skip);
}
if (dataManagerRequest.Take != 0)
{
dataSource = DataOperations.PerformTake(dataSource, dataManagerRequest.Take);
}
return dataManagerRequest.RequiresCounts
? new DataResult() { Result = dataSource, Count = totalRecordsCount }
: (object)dataSource;
}
}
}Fetches book data by calling the GetBooksAsync method, which is implemented in the FirebaseService.cs file.
/// <summary>
/// Retrieves all books from the Firestore database
/// </summary>
/// <returns>List of all books</returns>
public async Task<List<Book>> GetBooksAsync()
{
try
{
var query = _firestoreDb.Collection(BooksCollection);
var snapshot = await query.GetSnapshotAsync();
var books = new List<Book>();
foreach (var doc in snapshot.Documents)
{
var book = doc.ConvertTo<Book>();
if (book != null)
{
book.BookId = doc.Id; // Use Firestore document ID
books.Add(book);
}
}
return books;
}
catch (Exception ex)
{
throw new Exception($"Error fetching books: {ex.Message}");
}
}How Paging Works:
- The DataGrid displays 10 records per page (as set in
GridPageSettings). - Navigation buttons allow the user to move between pages.
- When a page is requested, the
ReadAsyncmethod receives skip and take values. - The
DataOperations.PerformSkip()andDataOperations.PerformTake()methods handle pagination. - Only the requested page of records is transmitted from the server.
Paging feature is now active with 10 records per page.
Step 7: Implement Searching Feature
Searching allows the user to find records by entering keywords in the search box.
Instructions:
- Ensure the toolbar includes the “Search” item.
- No additional code changes are required.
<SfGrid TValue="Book"
AllowPaging="true"
Toolbar="@ToolbarItems">
<SfDataManager AdaptorInstance="@typeof(CustomAdaptor)" Adaptor="Adaptors.CustomAdaptor"></SfDataManager>
<GridPageSettings PageSize="10"></GridPageSettings>
<!-- Grid columns configuration -->
</SfGrid>- Update the
ReadAsyncmethod in theCustomAdaptorclass to handle searching:
@code {
private List<string> ToolbarItems = new List<string> { "Search"};
/// <summary>
/// CustomAdaptor class to handle grid data operations with Firebase Firestore
/// </summary>
public class CustomAdaptor : DataAdaptor
{
public static FirebaseService? _firebaseService { get; set; }
public FirebaseService? FirebaseService
{
get => _firebaseService;
set => _firebaseService = value;
}
public override async Task<object> ReadAsync(DataManagerRequest dataManagerRequest, string? key = null)
{
IEnumerable dataSource = await _firebaseService!.GetBooksAsync();
// Handling Search
if (dataManagerRequest.Search != null && dataManagerRequest.Search.Count > 0)
{
dataSource = DataOperations.PerformSearching(dataSource, dataManagerRequest.Search);
}
int totalRecordsCount = dataSource.Cast<Book>().Count();
// Handling Paging
if (dataManagerRequest.Skip != 0)
{
dataSource = DataOperations.PerformSkip(dataSource, dataManagerRequest.Skip);
//Add custom logic here if needed and remove above method
}
if (dataManagerRequest.Take != 0)
{
dataSource = DataOperations.PerformTake(dataSource, dataManagerRequest.Take);
//Add custom logic here if needed and remove above method
}
return dataManagerRequest.RequiresCounts
? new DataResult() { Result = dataSource, Count = totalRecordsCount }
: (object)dataSource;
}
}
}How Searching Works:
- When the user enters text in the search box and presses Enter, the DataGrid sends a search request to the CustomAdaptor.
- The
ReadAsyncmethod receives the search criteria indataManagerRequest.Search. - The
DataOperations.PerformSearching()method filters the data based on the search term. - Results are returned and displayed in the DataGrid.
Searching feature is now active.
Step 8: Implement Filtering Feature
Filtering allows the user to restrict data based on column values using a menu interface.
Instructions:
- Open the
Components/Pages/Home.razorfile. - Add the AllowFiltering property and GridFilterSettings to the
<SfGrid>component:
<SfGrid TValue="Book"
AllowPaging="true"
AllowFiltering="true"
Toolbar="@ToolbarItems">
<SfDataManager AdaptorInstance="@typeof(CustomAdaptor)" Adaptor="Adaptors.CustomAdaptor"></SfDataManager>
<GridFilterSettings Type="Syncfusion.Blazor.Grids.FilterType.Menu"></GridFilterSettings>
<!-- Grid columns configuration -->
</SfGrid>- Update the
ReadAsyncmethod in theCustomAdaptorclass to handle filtering:
/// <summary>
/// CustomAdaptor class to handle grid data operations with Firebase Firestore
/// </summary>
public class CustomAdaptor : DataAdaptor
{
public static FirebaseService? _firebaseService { get; set; }
public FirebaseService? FirebaseService
{
get => _firebaseService;
set => _firebaseService = value;
}
public override async Task<object> ReadAsync(DataManagerRequest dataManagerRequest, string? key = null)
{
IEnumerable dataSource = await _firebaseService!.GetBooksAsync();
// Handling Search
if (dataManagerRequest.Search != null && dataManagerRequest.Search.Count > 0)
{
dataSource = DataOperations.PerformSearching(dataSource, dataManagerRequest.Search);
}
// Handling Filtering
if (dataManagerRequest.Where != null && dataManagerRequest.Where.Count > 0)
{
dataSource = DataOperations.PerformFiltering(dataSource, dataManagerRequest.Where, dataManagerRequest.Where[0].Operator);
}
int totalRecordsCount = dataSource.Cast<Book>().Count();
// Handling Paging
if (dataManagerRequest.Skip != 0)
{
dataSource = DataOperations.PerformSkip(dataSource, dataManagerRequest.Skip);
//Add custom logic here if needed and remove above method
}
if (dataManagerRequest.Take != 0)
{
dataSource = DataOperations.PerformTake(dataSource, dataManagerRequest.Take);
//Add custom logic here if needed and remove above method
}
return dataManagerRequest.RequiresCounts
? new DataResult() { Result = dataSource, Count = totalRecordsCount }
: (object)dataSource;
}
}How Filtering Works:
- Click on the dropdown arrow in any column header to open the filter menu.
- Select filtering criteria (equals, contains, greater than, less than, etc.).
- Click the “Filter” button to apply the filter.
- The
ReadAsyncmethod receives the filter criteria indataManagerRequest.Where. - Results are filtered accordingly and displayed in the DataGrid.
Filtering feature is now active.
Step 9: Implement Sorting Feature
Sorting enables the user to arrange records in ascending or descending order based on column values.
Instructions:
- Open the
Components/Pages/Home.razorfile. - Add the AllowSorting property to the
<SfGrid>component:
<SfGrid TValue="Book"
AllowPaging="true"
AllowSorting="true"
AllowFiltering="true"
Toolbar="@ToolbarItems">
<SfDataManager AdaptorInstance="@typeof(CustomAdaptor)" Adaptor="Adaptors.CustomAdaptor"></SfDataManager>
<GridPageSettings PageSize="10"></GridPageSettings>
<GridFilterSettings Type="Syncfusion.Blazor.Grids.FilterType.Menu"></GridFilterSettings>
<!-- Grid columns configuration -->
</SfGrid>- Update the
ReadAsyncmethod in theCustomAdaptorclass to handle sorting:
/// <summary>
/// CustomAdaptor class to handle grid data operations with Firebase Firestore
/// </summary>
public class CustomAdaptor : DataAdaptor
{
public static FirebaseService? _firebaseService { get; set; }
public FirebaseService? FirebaseService
{
get => _firebaseService;
set => _firebaseService = value;
}
public override async Task<object> ReadAsync(DataManagerRequest dataManagerRequest, string? key = null)
{
IEnumerable dataSource = await _firebaseService!.GetBooksAsync();
// Handling Search
if (dataManagerRequest.Search != null && dataManagerRequest.Search.Count > 0)
{
dataSource = DataOperations.PerformSearching(dataSource, dataManagerRequest.Search);
}
// Handling Filtering
if (dataManagerRequest.Where != null && dataManagerRequest.Where.Count > 0)
{
dataSource = DataOperations.PerformFiltering(dataSource, dataManagerRequest.Where, dataManagerRequest.Where[0].Operator);
}
// Handling Sorting
if (dataManagerRequest.Sorted != null && dataManagerRequest.Sorted.Count > 0)
{
dataSource = DataOperations.PerformSorting(dataSource, dataManagerRequest.Sorted);
}
int totalRecordsCount = dataSource.Cast<Book>().Count();
// Handling Paging
if (dataManagerRequest.Skip != 0)
{
dataSource = DataOperations.PerformSkip(dataSource, dataManagerRequest.Skip);
//Add custom logic here if needed and remove above method
}
if (dataManagerRequest.Take != 0)
{
dataSource = DataOperations.PerformTake(dataSource, dataManagerRequest.Take);
//Add custom logic here if needed and remove above method
}
return dataManagerRequest.RequiresCounts
? new DataResult() { Result = dataSource, Count = totalRecordsCount }
: (object)dataSource;
}
}How Sorting Works:
- Click on the column header to sort in ascending order.
- Click again to sort in descending order.
- The
ReadAsyncmethod receives the sort criteria indataManagerRequest.Sorted. - Records are sorted accordingly and displayed in the DataGrid.
Sorting feature is now active.
Step 10: Implement Grouping Feature
Grouping organizes records into hierarchical groups based on column values.
Instructions:
- Open the
Components/Pages/Home.razorfile. - Add the AllowGrouping property to the
<SfGrid>component:
<SfGrid TValue="Book"
AllowPaging="true"
AllowSorting="true"
AllowFiltering="true"
AllowGrouping="true"
Toolbar="@ToolbarItems">
<SfDataManager AdaptorInstance="@typeof(CustomAdaptor)" Adaptor="Adaptors.CustomAdaptor"></SfDataManager>
<GridPageSettings PageSize="10"></GridPageSettings>
<GridFilterSettings Type="Syncfusion.Blazor.Grids.FilterType.Menu"></GridFilterSettings>
<!-- Grid columns -->
</SfGrid>- Update the
ReadAsyncmethod in theCustomAdaptorclass to handle grouping:
/// <summary>
/// CustomAdaptor class to handle grid data operations with Firebase Firestore
/// </summary>
public class CustomAdaptor : DataAdaptor
{
public static FirebaseService? _firebaseService { get; set; }
public FirebaseService? FirebaseService
{
get => _firebaseService;
set => _firebaseService = value;
}
public override async Task<object> ReadAsync(DataManagerRequest dataManagerRequest, string? key = null)
{
IEnumerable dataSource = await _firebaseService!.GetBooksAsync();
// Handling Search
if (dataManagerRequest.Search != null && dataManagerRequest.Search.Count > 0)
{
dataSource = DataOperations.PerformSearching(dataSource, dataManagerRequest.Search);
}
// Handling Filtering
if (dataManagerRequest.Where != null && dataManagerRequest.Where.Count > 0)
{
dataSource = DataOperations.PerformFiltering(dataSource, dataManagerRequest.Where, dataManagerRequest.Where[0].Operator);
}
// Handling Sorting
if (dataManagerRequest.Sorted != null && dataManagerRequest.Sorted.Count > 0)
{
dataSource = DataOperations.PerformSorting(dataSource, dataManagerRequest.Sorted);
}
int totalRecordsCount = dataSource.Cast<Book>().Count();
// Handling Paging
if (dataManagerRequest.Skip != 0)
{
dataSource = DataOperations.PerformSkip(dataSource, dataManagerRequest.Skip);
//Add custom logic here if needed and remove above method
}
if (dataManagerRequest.Take != 0)
{
dataSource = DataOperations.PerformTake(dataSource, dataManagerRequest.Take);
//Add custom logic here if needed and remove above method
}
// Handling Group operation in CustomAdaptor.
if (dataManagerRequest.Group != null)
{
foreach (var group in dataManagerRequest.Group)
{
dataSource = DataUtil.Group<Book>(dataSource, group, dataManagerRequest.Aggregates, 0, dataManagerRequest.GroupByFormatter);
}
}
return dataManagerRequest.RequiresCounts
? new DataResult() { Result = dataSource, Count = totalRecordsCount }
: (object)dataSource;
}
}How Grouping Works:
- Columns can be grouped by dragging the column header into the group drop area.
- Each group can be expanded or collapsed by clicking on the group header.
- The
ReadAsyncmethod receives the grouping instructions throughdataManagerRequest.Group. - The grouping operation is processed using DataUtil.Group, which organizes the records into hierarchical groups based on the selected column.
- Grouping is performed after search, filter, and sort operations, ensuring the grouped data reflects all applied conditions.
- The processed grouped result is then returned to the Grid and displayed in a structured, hierarchical format.
Grouping feature is now active.
Step 11: Perform CRUD Operations
CustomAdaptor methods enable users to create, read, update, and delete records directly from the DataGrid. Each operation calls corresponding data layer methods in FirebaseService.cs to execute Firestore commands.
Add the Grid EditSettings and Toolbar configuration to enable create, read, update, and delete (CRUD) operations.
<SfGrid TValue="Book"
AllowPaging="true"
AllowSorting="true"
AllowFiltering="true"
AllowGrouping="true"
Toolbar="@ToolbarItems">
<SfDataManager AdaptorInstance="@typeof(CustomAdaptor)" Adaptor="Adaptors.CustomAdaptor"></SfDataManager>
<GridPageSettings PageSize="10"></GridPageSettings>
<GridFilterSettings Type="Syncfusion.Blazor.Grids.FilterType.Menu"></GridFilterSettings>
<GridEditSettings AllowEditing="true" AllowAdding="true" AllowDeleting="true" Mode="EditMode.Normal"></GridEditSettings>
<!-- Grid columns -->
</SfGrid>Add the toolbar items list in the @code block:
@code {
private List<string> ToolbarItems = new List<string> { "Add", "Edit", "Delete", "Update", "Cancel", "Search"};
// CustomAdaptor class code...
}Insert
Record insertion allows new books to be added directly through the DataGrid component. The adaptor processes the insertion request, performs any required business‑logic validation, and saves the newly created record to the Firebase Firestore database.
In Home.razor, implement the InsertAsync method to handle record insertion within the CustomAdaptor class:
public class CustomAdaptor : DataAdaptor
{
public override async Task<object?> InsertAsync(DataManager dataManager, object value, string? key = null)
{
try
{
if (value is Book newBook)
{
var result = await _firebaseService!.InsertBookAsync(newBook);
return result;
}
return default;
}
catch (Exception ex)
{
Console.WriteLine($"Error in Insert operation: {ex.Message}");
throw;
}
}
}In Services/FirebaseService.cs, implement the insert method:
public async Task<Book> InsertBookAsync(Book book)
{
try
{
// Auto-generate BookId if not provided
if (string.IsNullOrEmpty(book.BookId))
{
book.BookId = await GenerateBookIdAsync();
}
// Set LastUpdated to current UTC time
book.LastUpdated = DateTime.UtcNow;
// Add book to Firestore with custom document ID
await _firestoreDb.Collection(BooksCollection).Document(book.BookId).SetAsync(book);
return book;
}
catch (Exception ex)
{
throw new Exception($"Error inserting book: {ex.Message}");
}
}
private async Task<string> GenerateBookIdAsync()
{
try
{
var existingBooks = await GetBooksAsync();
int maxNumber = existingBooks
.Where(book => !string.IsNullOrEmpty(book.BookId) && book.BookId.StartsWith(BookIdPrefix))
.Select(book =>
{
string numberPart = book.BookId.Substring(BookIdPrefix.Length);
if (int.TryParse(numberPart, out int number))
return number;
return 0;
})
.DefaultIfEmpty(0)
.Max();
int nextNumber = maxNumber + 1;
string newBookId = $"{BookIdPrefix}{nextNumber:D3}";
return newBookId;
}
catch (Exception ex)
{
throw new Exception($"Error generating BookId: {ex.Message}");
}
}Helper methods explanation:
-
GenerateBookIdAsync(): A new BookId is generated in the format BOOK001, BOOK002, etc.
What happens behind the scenes:
- The form data is collected and validated in the CustomAdaptor’s
InsertAsync()method. - The
FirebaseService.InsertBookAsync()method is called. - A unique BookId is auto-generated if not provided.
- The LastUpdated is set to the current UTC time.
-
SetAsync()adds the document to the Firestore collection. - The DataGrid automatically refreshes to display the new record.
Now the new book is persisted to the database and reflected in the grid.
Update
Record modification allows book details to be updated directly within the DataGrid. The adaptor processes the edited row, validates the updated values, and applies the changes to the Firebase Firestore database while ensuring data integrity is preserved.
In Home.razor, implement the UpdateAsync method to handle record updates within the CustomAdaptor class:
public class CustomAdaptor : DataAdaptor
{
public override async Task<object?> UpdateAsync(DataManager dataManager, object value, string? keyField, string key)
{
try
{
if (value is Book book)
{
var result = await _firebaseService!.UpdateBookAsync(book.BookId, book);
return result ? value : default;
}
return default;
}
catch (Exception ex)
{
Console.WriteLine($"Error in Update operation: {ex.Message}");
throw;
}
}
}In Services/FirebaseService.cs, implement the update method:
public async Task<bool> UpdateBookAsync(string bookId, Book book)
{
try
{
if (string.IsNullOrEmpty(bookId))
{
return false;
}
// Set LastUpdated to current UTC time
book.LastUpdated = DateTime.UtcNow;
book.BookId = bookId;
// Update book in Firestore
await _firestoreDb.Collection(BooksCollection).Document(bookId).SetAsync(book, SetOptions.MergeAll);
return true;
}
catch (Exception ex)
{
throw new Exception($"Error updating book: {ex.Message}");
}
}What happens behind the scenes:
- The modified data is collected from the form.
- The CustomAdaptor’s
UpdateAsync()method is called. - The
FirebaseService.UpdateBookAsync()method is called. - The document ID is used to identify the book in Firestore.
-
SetAsync()withMergeAlloption updates only the specified fields. - The method returns true if the document was modified successfully.
- The DataGrid refreshes to display the updated record.
Now modifications are synchronized to the database and reflected in the grid UI.
Delete
Record deletion allows books to be removed directly from the DataGrid. The adaptor captures the delete request, executes the corresponding Firestore DELETE operation, and updates both the database and the grid to reflect the removal.
In Home.razor, implement the RemoveAsync method to handle record deletion within the CustomAdaptor class:
public class CustomAdaptor : DataAdaptor
{
public override async Task<object?> RemoveAsync(DataManager dataManager, object value, string? keyField, string key)
{
try
{
await _firebaseService!.DeleteBookAsync(value as string);
return value;
}
catch (Exception ex)
{
Console.WriteLine($"Error in Delete operation: {ex.Message}");
throw;
}
}
}In Services/FirebaseService.cs, implement the delete method:
public async Task<bool> DeleteBookAsync(string? bookId)
{
try
{
if (string.IsNullOrEmpty(bookId))
{
return false;
}
// Delete book from Firestore
await _firestoreDb.Collection(BooksCollection).Document(bookId).DeleteAsync();
return true;
}
catch (Exception ex)
{
throw new Exception($"Error deleting book: {ex.Message}");
}
}What happens behind the scenes:
- The user selects a record and clicks “Delete”.
- A confirmation dialog appears (built into the DataGrid).
- If confirmed, the CustomAdaptor’s
RemoveAsync()method is called. - The
FirebaseService.DeleteBookAsync()method is called. - The document ID is used to identify the book in Firestore.
-
DeleteAsync()removes the document from the Firestore collection. - The method returns true if the document was deleted successfully.
- The DataGrid refreshes to remove the deleted record from the UI.
Now books are removed from the database and the grid UI reflects the changes immediately.
Batch Update
Batch operations combine multiple insert, update, and delete actions into a single request, minimizing network overhead and ensuring transactional consistency by applying all changes atomically to the Firebase Firestore database.
In Home.razor, implement the BatchUpdateAsync method to handle multiple record updates in a single request within the CustomAdaptor class:
public class CustomAdaptor : DataAdaptor
{
public override async Task<object> BatchUpdateAsync(DataManager dataManager, object changedRecords, object addedRecords, object deletedRecords, string? keyField, string key, int? dropIndex)
{
// Process updated records
if (changedRecords != null)
{
foreach (var record in (IEnumerable<Book>)changedRecords)
{
var book = record as Book;
if (book != null)
{
await _firebaseService!.UpdateBookAsync(book.BookId, book);
}
}
}
// Process newly added records
if (addedRecords != null)
{
foreach (var record in (IEnumerable<Book>)addedRecords)
{
await _firebaseService!.InsertBookAsync(record as Book);
}
}
// Process deleted records
if (deletedRecords != null)
{
foreach (var record in (IEnumerable<Book>)deletedRecords)
{
await _firebaseService!.DeleteBookAsync((record as Book)?.BookId);
}
}
return key;
}
}This method is triggered when the DataGrid is operating in Batch Edit mode.
What happens behind the scenes:
- The DataGrid collects all added, edited, and deleted records in Batch Edit mode.
- The combined batch request is passed to the CustomAdaptor’s
BatchUpdateAsync()method. - Each modified record is processed using
FirebaseService.UpdateBookAsync(). - Each newly added record is saved using
FirebaseService.InsertBookAsync(). - Each deleted record is removed using
FirebaseService.DeleteBookAsync(). - All service operations persist changes to the Firebase Firestore database.
- The DataGrid refreshes to display the updated, added, and removed records in a single response.
Now the adaptor supports bulk modifications with atomic database synchronization. All CRUD operations are now fully implemented, enabling comprehensive data management capabilities within the Blazor DataGrid.
Reference links
- InsertAsync(DataManager, object) - Create new records in Firestore
- UpdateAsync(DataManager, object, string, string) - Edit existing records in Firestore
- RemoveAsync(DataManager, object, string, string) - Delete records from Firestore
- BatchUpdateAsync(DataManager, object, object, object, string, string, int?) - Handle bulk operations
Step 12: Complete Code
Here is the complete and final Home.razor component with all features integrated:
@page "/"
@using System.Collections
@using System.Text.Json
@rendermode InteractiveServer
@inject FirebaseService FirebaseService
<PageTitle>Library Management</PageTitle>
<div class="container-fluid mt-4">
<div class="row mb-4">
<div class="col-md-12">
<h1 class="text-primary">
<i class="bi bi-book"></i> Library Management Dashboard
</h1>
</div>
</div>
<!-- Bulk Import Section - Minimal UI -->
<div class="row mb-3">
<div class="col-md-12">
<div class="d-flex gap-2 align-items-center">
<label for="bulkImportFile" class="form-label mb-0 fw-bold">📁 Bulk Import JSON:</label>
<InputFile id="bulkImportFile" class="form-control form-control-sm" style="max-width: 300px;" accept=".json" OnChange="@OnFileSelected" disabled="@ImportInProgress" />
<button class="btn @GetImportButtonClass()" @onclick="ImportBooks" disabled="@(!IsReadyToImport || ImportInProgress)">
<i class="@GetImportButtonIcon()"></i> @GetImportButtonText()
</button>
</div>
</div>
</div>
<!-- Grid Section -->
<div class="row">
<div class="col-md-12">
<div class="card shadow-sm">
<div class="card-body p-0">
<SfGrid @ref="_grid" TValue="Book" AllowPaging="true" AllowSorting="true" AllowGrouping="true" AllowFiltering="true" Toolbar="@ToolbarItems">
<SfDataManager AdaptorInstance="@typeof(CustomAdaptor)" Adaptor="Adaptors.CustomAdaptor"></SfDataManager>
<GridPageSettings PageSize="10"></GridPageSettings>
<GridFilterSettings Type="Syncfusion.Blazor.Grids.FilterType.Menu"></GridFilterSettings>
<GridEditSettings AllowAdding="true" AllowEditing="true" AllowDeleting="true" Mode="EditMode.Normal"></GridEditSettings>
<GridColumns>
<GridColumn Field=@nameof(Book.BookId) HeaderText="Book ID" IsPrimaryKey="true" IsIdentity="true" Width="110"></GridColumn>
<GridColumn Field=@nameof(Book.Title) HeaderText="Title" Width="200" ValidationRules="@(new ValidationRules { Required = true })"></GridColumn>
<GridColumn Field=@nameof(Book.Author) HeaderText="Author" Width="180"></GridColumn>
<GridColumn Field=@nameof(Book.ISBN) HeaderText="ISBN" Width="180"></GridColumn>
<GridColumn Field=@nameof(Book.Category) HeaderText="Category" Width="120" EditType="EditType.DropDownEdit" EditorSettings="@CategoryDropDownParams">
<Template>
@{
var book = context as Book;
var badgeClass = GetCategoryBadgeClass(book?.Category);
}
<span class="@badgeClass">@book?.Category</span>
</Template>
</GridColumn>
<GridColumn Field=@nameof(Book.PublishDate) HeaderText="Publish Date" Type="ColumnType.DateTime" Format="MM/dd/yyyy" Width="140"></GridColumn>
<GridColumn Field=@nameof(Book.Language) HeaderText="Language" Width="120"></GridColumn>
<GridColumn Field=@nameof(Book.TotalCopies) HeaderText="Total Copies" Type="ColumnType.Integer" Width="150" TextAlign="TextAlign.Center"></GridColumn>
<GridColumn Field=@nameof(Book.AvailableCopies) HeaderText="Available" Type="ColumnType.Integer" Width="120" TextAlign="TextAlign.Center"></GridColumn>
<GridColumn Field=@nameof(Book.Location) HeaderText="Location" Width="130"></GridColumn>
<GridColumn Field=@nameof(Book.BorrowedBy) HeaderText="Borrowed By" Width="150"></GridColumn>
<GridColumn Field=@nameof(Book.Status) HeaderText="Status" EditType="EditType.DropDownEdit" EditorSettings="@StatusDropDownParams" Width="120">
<Template>
@{
var book = context as Book;
var badgeClass = GetStatusBadgeClass(book?.Status);
}
<span class="@badgeClass">@book?.Status</span>
</Template>
</GridColumn>
<GridColumn Field=@nameof(Book.LastUpdated) HeaderText="Last Updated" Type="ColumnType.DateTime" Format="MM/dd/yyyy HH:mm" Width="160"></GridColumn>
</GridColumns>
</SfGrid>
</div>
</div>
</div>
</div>
</div>
- Set IsPrimaryKey to true for a column that contains unique values.
- If the database includes an auto-generated column, set IsIdentity for that column to disable editing during add or update operations.
- The EditType property can be used to specify the desired editor for each column. 🔗
- The behavior of default editors can be customized using the EditorSettings property of the
GridColumncomponent. 🔗- Type property of the
GridColumncomponent specifies the data type of a grid column.- The Template property that allows rendering custom elements in a column instead of the default field value. 🔗
@code {
private List<string> ToolbarItems = new List<string> { "Add", "Edit", "Delete", "Update", "Cancel", "Search" };
private SfGrid<Book>? _grid;
private string SelectedFileName = string.Empty;
private string FileContent = string.Empty;
private List<Book> PreviewedBooks = new List<Book>();
private double FileSizeKB = 0;
private bool IsReadyToImport = false;
private bool ImportInProgress = false;
private bool ImportCompleted = false;
private CustomAdaptor? _customAdaptor;
protected override void OnInitialized()
{
// Initialize the CustomAdaptor with the injected FirebaseService
_customAdaptor = new CustomAdaptor { FirebaseService = FirebaseService };
}
/// <summary>
/// CustomAdaptor class to handle grid data operations with Firebase Firestore Database
/// </summary>
public class CustomAdaptor : DataAdaptor
{
public static FirebaseService? _firebaseService { get; set; }
public FirebaseService? FirebaseService
{
get => _firebaseService;
set => _firebaseService = value;
}
public override async Task<object> ReadAsync(DataManagerRequest dataManagerRequest, string? key = null)
{
try
{
// Fetch all books from the Firebase database
IEnumerable dataSource = await _firebaseService!.GetBooksAsync();
// Apply search operation if search criteria exists
if (dataManagerRequest.Search != null && dataManagerRequest.Search.Count > 0)
{
dataSource = DataOperations.PerformSearching(dataSource, dataManagerRequest.Search);
}
// Apply filter operation if filter criteria exists
if (dataManagerRequest.Where != null && dataManagerRequest.Where.Count > 0)
{
dataSource = DataOperations.PerformFiltering(dataSource, dataManagerRequest.Where, dataManagerRequest.Where[0].Operator);
}
// Apply sort operation if sort criteria exists
if (dataManagerRequest.Sorted != null && dataManagerRequest.Sorted.Count > 0)
{
dataSource = DataOperations.PerformSorting(dataSource, dataManagerRequest.Sorted);
}
// Calculate total record count before paging for accurate pagination
int totalRecordsCount = dataSource.Cast<Book>().Count();
// Apply paging skip operation
if (dataManagerRequest.Skip != 0)
{
dataSource = DataOperations.PerformSkip(dataSource, dataManagerRequest.Skip);
}
// Apply paging take operation to retrieve only the requested page size
if (dataManagerRequest.Take != 0)
{
dataSource = DataOperations.PerformTake(dataSource, dataManagerRequest.Take);
}
// Handling Group operation in CustomAdaptor.
if (dataManagerRequest.Group != null)
{
foreach (var group in dataManagerRequest.Group)
{
dataSource = DataUtil.Group<Book>(dataSource, group, dataManagerRequest.Aggregates, 0, dataManagerRequest.GroupByFormatter);
}
}
// Return the result with total count for pagination metadata
return dataManagerRequest.RequiresCounts
? new DataResult() { Result = dataSource, Count = totalRecordsCount }
: (object)dataSource;
}
catch (Exception ex)
{
Console.WriteLine($"ReadAsync Error: {ex.Message}");
throw new Exception($"An error occurred while retrieving data: {ex.Message}");
}
}
public override async Task<object?> InsertAsync(DataManager dataManager, object value, string? key = null)
{
try
{
if (value is Book newBook)
{
var result = await _firebaseService!.InsertBookAsync(newBook);
return result;
}
return default;
}
catch (Exception ex)
{
Console.WriteLine($"Error in Insert operation: {ex.Message}");
throw;
}
}
public override async Task<object?> UpdateAsync(DataManager dataManager, object value, string? keyField, string key)
{
try
{
if (value is Book book)
{
var result = await _firebaseService!.UpdateBookAsync(book.BookId, book);
return result ? value : default;
}
return default;
}
catch (Exception ex)
{
Console.WriteLine($"Error in Update operation: {ex.Message}");
throw;
}
}
public override async Task<object?> RemoveAsync(DataManager dataManager, object value, string? keyField, string key)
{
try
{
await _firebaseService!.DeleteBookAsync(value as string);
return value;
}
catch (Exception ex)
{
Console.WriteLine($"Error in Delete operation: {ex.Message}");
throw;
}
}
public override async Task<object> BatchUpdateAsync(DataManager dataManager, object changedRecords, object addedRecords, object deletedRecords, string? keyField, string key, int? dropIndex)
{
// Process updated records
if (changedRecords != null)
{
foreach (var record in (IEnumerable<Book>)changedRecords)
{
var book = record as Book;
if (book != null)
{
await _firebaseService!.UpdateBookAsync(book.BookId, book);
}
}
}
// Process newly added records
if (addedRecords != null)
{
foreach (var record in (IEnumerable<Book>)addedRecords)
{
await _firebaseService!.InsertBookAsync(record as Book);
}
}
// Process deleted records
if (deletedRecords != null)
{
foreach (var record in (IEnumerable<Book>)deletedRecords)
{
await _firebaseService!.DeleteBookAsync((record as Book)?.BookId);
}
}
return key;
}
}
/// <summary>
/// Handles file selection and validates JSON format
/// </summary>
private async Task OnFileSelected(InputFileChangeEventArgs e)
{
try
{
var file = e.File;
if (file == null) return;
// Validate file
const long maxFileSize = 5 * 1024 * 1024; // 5MB
if (file.Size > maxFileSize)
{
ResetForm();
return;
}
// Read file
using (var stream = file.OpenReadStream(maxFileSize))
{
using (var reader = new StreamReader(stream))
{
FileContent = await reader.ReadToEndAsync();
}
}
SelectedFileName = file.Name;
FileSizeKB = file.Size / 1024.0;
// Parse and validate JSON
ParseJsonAndPreview();
}
catch
{
ResetForm();
}
}
/// <summary>
/// Parses JSON content and previews books
/// </summary>
private void ParseJsonAndPreview()
{
try
{
PreviewedBooks.Clear();
IsReadyToImport = false;
using (JsonDocument doc = JsonDocument.Parse(FileContent))
{
var root = doc.RootElement;
// Try to find 'books' array
if (root.TryGetProperty("books", out var booksElement) && booksElement.ValueKind == JsonValueKind.Array)
{
foreach (var bookElement in booksElement.EnumerateArray())
{
try
{
var jsonString = bookElement.GetRawText();
var book = JsonSerializer.Deserialize<Book>(jsonString, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (book != null)
{
book.NormalizeToUtc();
PreviewedBooks.Add(book);
}
}
catch
{
// Skip invalid book entries
}
}
}
}
if (PreviewedBooks.Count > 0)
{
IsReadyToImport = true;
}
else
{
ResetForm();
}
}
catch
{
ResetForm();
}
}
/// <summary>
/// Performs the bulk import operation
/// </summary>
private async Task ImportBooks()
{
if (PreviewedBooks.Count == 0 || ImportInProgress) return;
ImportInProgress = true;
ImportCompleted = false;
try
{
bool success = await FirebaseService.BulkImportBooksAsync(PreviewedBooks);
if (success)
{
ImportCompleted = true;
// Refresh the grid
if (_grid != null)
{
await _grid.Refresh();
}
// Reset after a brief delay
await Task.Delay(2000);
ResetForm();
}
else
{
// Failed - reset to allow retry
ImportInProgress = false;
}
}
catch
{
// Failed - reset to allow retry
ImportInProgress = false;
}
}
/// <summary>
/// Resets the form for another import
/// </summary>
private void ResetForm()
{
SelectedFileName = string.Empty;
FileContent = string.Empty;
PreviewedBooks.Clear();
FileSizeKB = 0;
IsReadyToImport = false;
ImportInProgress = false;
ImportCompleted = false;
}
/// <summary>
/// Gets the CSS class for the import button based on state
/// </summary>
private string GetImportButtonClass()
{
if (ImportCompleted) return "btn btn-success btn-lg";
if (ImportInProgress) return "btn btn-warning btn-lg";
return "btn btn-primary btn-lg";
}
/// <summary>
/// Gets the icon class for the import button based on state
/// </summary>
private string GetImportButtonIcon()
{
if (ImportCompleted) return "bi bi-check-circle";
if (ImportInProgress) return "bi bi-hourglass-split";
return "bi bi-cloud-upload";
}
/// <summary>
/// Gets the text for the import button based on state
/// </summary>
private string GetImportButtonText()
{
if (ImportCompleted) return "Import Successful";
if (ImportInProgress) return "Importing...";
return "Import to Firestore";
}
/// <summary>
/// Provides a list of category options for the Category dropdown editor
/// </summary>
private static List<Book> CustomCategories = new List<Book> {
new Book() { Category = "Fiction" },
new Book() { Category = "Non-Fiction" },
new Book() { Category = "Science" },
new Book() { Category = "Technology" },
new Book() { Category = "History" },
new Book() { Category = "Biography" },
new Book() { Category = "Romance" },
new Book() { Category = "Mystery" },
new Book() { Category = "Education" },
};
/// <summary>
/// Provides a list of status options for the Status dropdown editor
/// </summary>
private static List<Book> CustomStatus = new List<Book> {
new Book() { Status = "Available" },
new Book() { Status = "Borrowed" },
new Book() { Status = "Reserved" },
new Book() { Status = "Lost" },
new Book() { Status = "Damaged" },
new Book() { Status = "Maintenance" },
};
/// <summary>
/// Dropdown editor settings configured with category options
/// </summary>
private IEditorSettings CategoryDropDownParams = new DropDownEditCellParams
{
Params = new DropDownListModel<object, object>() { DataSource = CustomCategories, Query = new Syncfusion.Blazor.Data.Query() },
};
/// <summary>
/// Dropdown editor settings configured with status options
/// </summary>
private IEditorSettings StatusDropDownParams = new DropDownEditCellParams
{
Params = new DropDownListModel<object, object>() { DataSource = CustomStatus, Query = new Syncfusion.Blazor.Data.Query() },
};
/// <summary>
/// Gets badge CSS class for Category column
/// </summary>
private string GetCategoryBadgeClass(string? category)
{
return category switch
{
"Fiction" => "badge bg-primary",
"Non-Fiction" => "badge bg-info",
"Science" => "badge bg-success",
"Technology" => "badge bg-warning",
"History" => "badge bg-danger",
"Biography" => "badge bg-secondary",
"Romance" => "badge bg-info",
"Mystery" => "badge bg-dark",
"Education" => "badge bg-info",
_ => "badge bg-secondary"
};
}
/// <summary>
/// Gets badge CSS class for Status column
/// </summary>
private string GetStatusBadgeClass(string? status)
{
return status switch
{
"Available" => "badge bg-success",
"Borrowed" => "badge bg-info",
"Reserved" => "badge bg-primary",
"Lost" => "badge bg-danger",
"Damaged" => "badge bg-warning",
"Maintenance" => "badge bg-secondary",
_ => "badge bg-secondary"
};
}
}Complete Sample Repository
A complete, working sample implementation is available in the GitHub repository.
Summary
This guide demonstrates how to:
- Create a Firebase Firestore project and database with book collections. 🔗
- Generate service account credentials for server-side authentication. 🔗
- Enable required Google Cloud APIs for Firestore access. 🔗
- Install necessary NuGet packages for Google.Cloud.Firestore and Syncfusion. 🔗
- Create data models mapping to Firestore document structure. 🔗
- Configure service account paths and register services in Program.cs. 🔗
- Implement the Firebase service class for Firestore CRUD operations. 🔗
- Create a Blazor component with a DataGrid that supports searching, filtering, sorting, paging, grouping, and CRUD operations. 🔗
- Handle bulk operations and batch updates. 🔗
The application now provides a complete solution for managing library book data with a modern, user-friendly interface powered by Firebase Firestore’s cloud-hosted NoSQL database and real-time synchronization capabilities.