Building an AI-Powered Grading System with .NET 10 MVC and Azure AI
In the modern classroom, grading hundreds of handwritten exams can be a monumental task for professors. With the release of .NET 10, we now have even better tools to build robust, scalable, and intelligent applications. In this post, we’ll walk through how to build an automated grading system that uses AI to recognize handwriting and grade exams.
1. The Vision
The idea is simple: a professor uploads a PDF or image of a student’s exam. The system uses Optical Character Recognition (OCR) and Handwriting Recognition to extract the text, compares it against a master answer key, and assigns a grade.
Key Features:
- Handwriting Recognition: Capture student answers from scanned papers.
- Automated Grading: Compare recognized text with the correct answers.
- Professor Dashboard: Review and override AI-generated grades.
- Scalable Processing: Use background services for heavy AI tasks.
2. System Architecture
To understand how the data flows through our system, here is a high-level component diagram:
+----------------+ +------------------+ +---------------------------+
| | | | | |
| Professor |----->| ASP.NET MVC | | Azure AI Document |
| (Browser) | (1) | Web Portal | | Intelligence (OCR) |
| | | | | |
+-------^--------+ +----+-------+-----+ +-------------+-------------+
| | | ^ |
| (7) | (2) | (3) | (5) | (6)
| v v | |
+-------+--------+ +-------+ +-------+ | |
| | | SQL | | Azure | | |
| Review |<-----| DB | | Blob |--------------------+ |
| Dashboard | | ^ | | | |
| | +---|---+ +-------+ |
+----------------+ +-----------------------------------------+
Workflow:
- Upload: Professor uploads exam images or PDFs.
- Database: Web portal creates ‘Pending’ records in SQL Server.
- Storage: Files are uploaded to Azure Blob Storage for secure persistence.
- Trigger: A background worker identifies pending submissions.
- Analysis: The worker retrieves the file from Blob Storage and sends it to Azure AI Document Intelligence.
- Extraction: Azure returns recognized results, which are stored in the SQL Database.
- Review: Professor reviews results on the dashboard and confirms grades.
3. Implementation Plan
Following this structured plan will help you build the system incrementally:
Phase 1: Setup & Database Design
- Project Scaffolding: Create a new ASP.NET Core MVC project targeting .NET 10.
- Dependency Management: Install NuGet packages for
Azure.AI.DocumentIntelligence,Azure.Storage.Blobs, andMicrosoft.EntityFrameworkCore.SqlServer. - Identity Setup: Configure ASP.NET Core Identity to manage Professor accounts and secure the dashboard.
- Schema Implementation: Use EF Core Migrations to create the
Courses,Students,Exams,Questions,Submissions, andResultstables. - Environment Config: Securely store Azure credentials and connection strings using User Secrets (development) and Azure Key Vault (production).
Phase 2: File Storage & Pre-processing
- Azure Blob Storage: Create a container named
exam-uploadswith private access levels. - Upload Pipeline: Build a multi-part form upload that streams files directly to Blob Storage to minimize memory usage.
- Image Normalization: Use SkiaSharp to convert uploaded images to grayscale and resize them if they exceed 4K resolution (to optimize OCR performance).
- Metadata Persistence: Save the Blob URI and initial
Pendingstatus to the SQL database.
Phase 3: AI Integration (OCR & Handwriting)
- Service Layer: Build a
HandwritingServicethat wraps theDocumentIntelligenceClient. - Background Worker: Implement a
BackgroundServicethat polls the database forPendingsubmissions and marks them asProcessing. - OCR Execution: Send documents to the
prebuilt-readmodel and handle the asynchronous polling for analysis results. - Error Handling: Implement retry logic for transient Azure service failures and mark submissions as
Failedif processing persists in error.
Phase 4: Grading Logic Engine
- Master Key Retrieval: Fetch the correct answers from the
Questionstable associated with the exam. - Text Normalization: Create a pipeline to strip punctuation, handle casing, and remove common OCR artifacts (e.g., misreading ‘0’ as ‘O’).
- Similarity Scoring: Implement the Levenshtein Distance algorithm for short answers to calculate a similarity percentage.
- Multiple Choice Logic: Integrate detection of Selection Marks from the Azure AI output to grade MCQs automatically.
- Score Assignment: Automatically assign points based on match type and flag ambiguous results for manual review.
Phase 5: Dashboard & Human-in-the-loop
- Submission List: Build a dashboard showing all students, their current status, and calculated scores.
- Review Workspace: Implement a side-by-side UI component showing the original image (retrieved from Blob Storage) next to the AI-extracted text.
- Manual Adjustments: Allow the professor to click “Accept AI Grade” or type in a manual override.
- Finalization: Once reviewed, mark the submission as
Completedand generate a final grade report in CSV format.
4. The Technical Stack
To build this, we’ll use a modern Microsoft-centric stack:
- Framework: ASP.NET Core 10 MVC.
- Database: SQL Server with EF Core.
- File Storage: Azure Blob Storage for scalable and secure cloud storage of exam files.
- AI Service: Azure AI Document Intelligence (formerly Form Recognizer). This is the “brain” of our system, capable of high-accuracy handwriting recognition.
- Background Processing: .NET Worker Services.
Required NuGet Packages
Install these via CLI or NuGet Manager:
Azure.AI.DocumentIntelligenceAzure.Storage.BlobsMicrosoft.EntityFrameworkCore.SqlServerMicrosoft.Extensions.Configuration.UserSecretsSkiaSharp(for image pre-processing)
5. Designing the Database
A solid system starts with a good schema. We need to track exams, questions, submissions, and results. Below is the ER Diagram and the C# classes for our Entity Framework Core models.
ER Diagram (ASCII)
+--------------+ 1 * +--------------+ 1 * +--------------+
| Course |----------------| Exam |----------------| Question |
+--------------+ +--------------+ +--------------+
| PK: Id | | PK: Id | | PK: Id |
| Name | | FK: CourseId | | FK: ExamId |
| Code | | Title | | Text |
+--------------+ +--------------+ | Type |
| +--------------+
| 1 |
| |
+--------------+ 1 * +------v-------+ |
| Student |----------------| Submission | |
+--------------+ +--------------+ | *
| PK: Id | | PK: Id | * +--------------+
| Name | | FK: ExamId |----------------| Result |
| Email | | FK: StudentId| 1 +--------------+
+--------------+ | FilePath | | PK: Id |
| Status | | FK: SubId |
+--------------+ | FK: QuestId |
| Score |
+--------------+
Entity Framework Models
public enum SubmissionStatus
{
Pending,
Processing,
Completed,
Failed
}
public enum QuestionType
{
ShortAnswer,
MultipleChoice
}
public class Course
{
public int Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public List<Exam> Exams { get; set; }
}
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public List<Submission> Submissions { get; set; }
}
public class Exam
{
public int Id { get; set; }
public int CourseId { get; set; } // Foreign Key to Course
public string Title { get; set; }
public List<Question> Questions { get; set; }
public List<Submission> Submissions { get; set; }
}
public class Question
{
public int Id { get; set; }
public int ExamId { get; set; } // Foreign Key to Exam
public string Text { get; set; }
public string CorrectAnswer { get; set; }
public QuestionType Type { get; set; } // ShortAnswer or MultipleChoice
public int Points { get; set; }
}
public class Submission
{
public int Id { get; set; }
public int ExamId { get; set; } // Foreign Key to Exam
public int StudentId { get; set; } // Foreign Key to Student
public string FilePath { get; set; }
public SubmissionStatus Status { get; set; }
public List<Result> Results { get; set; }
}
public class Result
{
public int Id { get; set; }
public int SubmissionId { get; set; } // Foreign Key to Submission
public int QuestionId { get; set; } // Foreign Key to Question
public string RecognizedAnswer { get; set; }
public double Score { get; set; }
public string Feedback { get; set; }
}
6. Handling File Uploads to Azure Blob Storage
Instead of saving files to the local web server, we’ll use Azure Blob Storage. This allows our background workers to access the files easily from any instance.
using Azure.Storage.Blobs;
[HttpPost]
public async Task<IActionResult> Upload(IFormFile examFile, int examId, int studentId)
{
if (examFile != null && examFile.Length > 0)
{
// 1. Initialize Blob Client
var containerClient = new BlobContainerClient(_connectionString, "exams");
await containerClient.CreateIfNotExistsAsync();
var blobClient = containerClient.GetBlobClient(Guid.NewGuid() + Path.GetExtension(examFile.FileName));
// 2. Upload to Azure Blob Storage
using (var stream = examFile.OpenReadStream())
{
await blobClient.UploadAsync(stream, true);
}
// 3. Create a record in the database
var submission = new Submission
{
ExamId = examId,
StudentId = studentId,
FilePath = blobClient.Uri.ToString(),
Status = SubmissionStatus.Pending
};
_context.Submissions.Add(submission);
await _context.SaveChangesAsync();
return RedirectToAction("Dashboard");
}
return View();
}
7. Background Processing with Worker Services
Since OCR analysis can take several seconds per page, we shouldn’t process it in the web request. Instead, use a .NET Worker Service. This keeps your web application responsive for the professor while the “heavy lifting” happens in the background.
Implementing the GradingWorker
Here is a more complete implementation that handles the database scope and calls the grading service:
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
public class GradingWorker : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<GradingWorker> _logger;
public GradingWorker(IServiceProvider serviceProvider, ILogger<GradingWorker> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Grading Worker started.");
while (!stoppingToken.IsCancellationRequested)
{
using (var scope = _serviceProvider.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var gradingService = scope.ServiceProvider.GetRequiredService<IGradingService>();
// 1. Find the next pending submission
var pending = await context.Submissions
.Where(s => s.Status == SubmissionStatus.Pending)
.FirstOrDefaultAsync(stoppingToken);
if (pending != null)
{
try
{
// 2. Mark as processing to prevent other workers from picking it up
pending.Status = SubmissionStatus.Processing;
await context.SaveChangesAsync(stoppingToken);
_logger.LogInformation($"Processing Submission ID: {pending.Id}");
// 3. Hand off to the grading engine (defined in Section 9)
await gradingService.GradeSubmissionAsync(pending.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error processing submission {pending.Id}");
pending.Status = SubmissionStatus.Failed;
await context.SaveChangesAsync(stoppingToken);
}
}
}
// 4. Wait for 5 seconds before checking for more work
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
}
Registering the Service
In your Program.cs, you must register both your IGradingService and the HostedService:
builder.Services.AddScoped<IGradingService, GradingService>();
builder.Services.AddHostedService<GradingWorker>();
8. Deep Dive: Azure AI Document Intelligence
For a beginner, Azure AI Document Intelligence (formerly known as Form Recognizer) is a cloud-based service that uses AI to extract text, key-value pairs, and structured data from your documents.
8.1 Key Concepts & Models
Before we dive into the code, you should understand the “Models” provided by Azure:
- Prebuilt-Read: This is a general-purpose model for OCR. It’s fantastic at extracting all text and numbers, including complex handwriting, regardless of the document’s layout.
- Prebuilt-Layout: This model goes a step further by identifying structures like tables, selection marks (checkboxes, radio buttons), and document layout (headers, footers). This is the key model for handling multiple-choice questions automatically.
- Custom Neural Models: For exams with a fixed layout (like a standardized test), you can “train” a model with a few sample papers.
8.2 Creating a Free Azure Account
If you don’t have an Azure account, you can start for free:
- Go to the Azure Free Account Page.
- Sign up using a Microsoft account. You’ll get $200 credit for the first 30 days and many services (including AI) have a “Free Tier” that persists beyond that.
8.3 Setting Up Your AI Resource
Once you have your account:
- Search for Document Intelligence in the portal search bar.
- Click Create.
- Pricing Tier: Select
F0(Free) if it’s your first time, orS0(Standard) for production. - Once created, navigate to Keys and Endpoint to grab your:
- Endpoint: (e.g.,
https://my-resource.cognitiveservices.azure.com/) - Key: (A string like
5f6e7...)
- Endpoint: (e.g.,
8.4 Setting Up Azure Blob Storage
You’ll need a place to store the uploaded exam images:
- Search for Storage Accounts in the portal.
- Click Create. Give it a unique name (e.g.,
examstorage2026). - Once the account is ready, go to the Containers menu on the left.
- Create a new container named
exams. - Set the Public access level to
Private(no anonymous access). Your .NET code will use a secure connection string to access these files. - Go to Access keys on the left to copy your Connection String.
8.5 Integrating with .NET 10
With the Azure.AI.DocumentIntelligence SDK, sending a file for analysis is a few lines of code. Here is a more detailed implementation:
using Azure;
using Azure.AI.DocumentIntelligence;
using Azure.Storage.Blobs;
using System.Text;
public async Task<AnalyzeResult> AnalyzeDocumentAsync(string blobUri)
{
// 1. Initialize the client
var endpoint = _config["AzureAI:Endpoint"];
var key = _config["AzureAI:Key"];
var client = new DocumentIntelligenceClient(new Uri(endpoint), new AzureKeyCredential(key));
// 2. Use the Blob URI to stream content for analysis
var blobClient = new BlobClient(new Uri(blobUri));
using var stream = await blobClient.OpenReadAsync();
var content = new AnalyzeDocumentContent(BinaryData.FromStream(stream));
// 3. Start the analysis
// Use "prebuilt-read" for raw text/handwriting
// Use "prebuilt-layout" if you need to detect selection marks (MCQs)
var operation = await client.AnalyzeDocumentAsync(
WaitUntil.Completed,
"prebuilt-layout",
content);
// 4. Return the full result (includes Pages, Tables, SelectionMarks)
return operation.Value;
}
Tip: Use the Azure AI Document Intelligence Studio to test your files without writing any code. It’s a great way to see what the AI “sees” before you start building.
9. The Grading Engine
Once you have the recognized text from the OCR analysis, you need to compare it to the CorrectAnswer in your database (the “Master Key”). This is where the Grading Engine takes over.
The Grading Workflow
To understand how this works end-to-end, follow these steps:
- Retrieve the Master Key: The engine fetches the correct answers for the specific
ExamIdfrom the SQL database. - Mapping OCR Output:
- If using Custom Models, the OCR returns structured “fields” (e.g.,
Answer1,Answer2). - If using Prebuilt-Read, you’ll get a raw string. You must use regex or simple keyword searching (e.g., “1.”, “2.”) to split the text into individual answers.
- If using Custom Models, the OCR returns structured “fields” (e.g.,
- Normalization: Before comparing, “clean” both the student’s answer and the correct answer.
- Convert to
lowercase. - Remove trailing spaces (
.Trim()). - Remove punctuation (e.g., “Paris.” becomes “paris”).
- Convert to
- Comparison:
- Exact Match: If
studentAnswer == correctAnswer, assign 100% score. - Fuzzy Match: If they don’t match exactly, calculate the Levenshtein Distance to see how close they are.
- Exact Match: If
Fuzzy Matching Logic (Levenshtein Distance)
The Levenshtein Distance is the number of single-character changes (insertions, deletions, or substitutions) required to change one word into another. For example, the distance between “Photosynthesis” and “Photosyntesis” is 1 (the ‘h’ is missing).
For short answers, minor spelling mistakes shouldn’t fail a student. Here is a simple implementation:
public static int ComputeDistance(string s, string t)
{
int n = s.Length, m = t.Length;
int[,] d = new int[n + 1, m + 1];
if (n == 0) return m;
if (m == 0) return n;
for (int i = 0; i <= n; d[i, 0] = i++) ;
for (int j = 0; j <= m; d[0, j] = j++) ;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
int cost = (t[j - 1] == s[i - 1]) ? 0 : 1;
d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost);
}
}
return d[n, m];
}
Calculating the Similarity Percentage
Once you have the distance, you can convert it to a score:
public static double CalculateSimilarity(string studentAnswer, string correctAnswer)
{
int distance = ComputeDistance(studentAnswer, correctAnswer);
int maxLength = Math.Max(studentAnswer.Length, correctAnswer.Length);
// Similarity is 1 minus the percentage of changes needed
return 1.0 - ((double)distance / maxLength);
}
// Usage in Grading Engine:
double similarity = CalculateSimilarity("Photosyntesis", "Photosynthesis");
// Output: ~0.92 (92% match)
if (similarity >= 0.85) {
// Automatically mark as correct or flag for review
}
Handling Multiple-Choice Questions (MCQs)
For multiple-choice questions, the logic is slightly different. Instead of fuzzy matching text, you need to detect Selection Marks (checkboxes or radio buttons).
- Detection: Use the
prebuilt-layoutmodel in Azure AI Document Intelligence. It returns a collection ofSelectionMarksfor each page. - Mapping: Each selection mark has a coordinate (polygon) on the page. You’ll need to map these coordinates to your
Questionoptions. - State: The AI will return a
statefor each mark:selectedorunselected. - Grading Logic:
- Find the
SelectionMarkthat corresponds to the student’s choice. - If the state is
selected, compare the option’s value (e.g., “B”) with theCorrectAnswer. - Assign full points for a match, zero otherwise.
- Find the
public double GradeMultipleChoice(AnalyzeResult result, Question question)
{
// 1. Identify selection marks on the page
foreach (var page in result.Pages)
{
foreach (var mark in page.SelectionMarks)
{
// 2. Logic to determine if mark is within the question's area
if (IsMarkInQuestionBounds(mark, question))
{
// 3. Check if the mark is 'selected'
if (mark.State == SelectionMarkState.Selected)
{
// 4. Determine which option this mark represents (A, B, C, etc.)
string selectedOption = MapMarkToOption(mark);
return selectedOption == question.CorrectAnswer ? question.Points : 0;
}
}
}
}
return 0; // No selection found
}
- Exact Match: Good for multiple-choice.
- LLM Grading: For complex essay questions, you can pass the recognized text to Azure OpenAI (GPT-4o) to grade based on context and criteria.
10. Building the Review Dashboard
AI isn’t perfect. Always include a “Human-in-the-loop” step. Create a view that shows the scanned image side-by-side with the AI’s interpretation.
public async Task<IActionResult> Review(int id)
{
var submission = await _context.Submissions
.Include(s => s.Results)
.FirstOrDefaultAsync(s => s.Id == id);
return View(submission);
}
In your Razor View, you can use Bootstrap columns to display the scanned image on the left and an editable list of recognized answers on the right.
11. Configuration & Security
Never hardcode your API keys. Use User Secrets for development and Environment Variables or Azure Key Vault for production.
appsettings.json Template:
{
"AzureAI": {
"Endpoint": "https://your-resource.cognitiveservices.azure.com/",
"Key": "YOUR_SECRET_KEY"
},
"AzureStorage": {
"ConnectionString": "DefaultEndpointsProtocol=https;AccountName=yourname;AccountKey=yourkey;EndpointSuffix=core.windows.net"
},
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=GradingSys;Trusted_Connection=True;"
}
}
12. Azure Deployment Architecture
When you’re ready to take your system from localhost to the cloud, you’ll need a set of Azure resources to host the application, data, and AI services.
Deployment Diagram (ASCII)
+-------------------------------------------------------------+
| Azure Cloud |
| |
| +-------------------+ +---------------------------+ |
| | Azure App Service| (REST) | Azure AI | |
| | (Web + Worker) |------->| Document Intelligence | |
| +---------+---------+ +---------------------------+ |
| | ^ |
| | | +---------------------------+ |
| | +--------------| Azure Key Vault | |
| | (Secrets) | (API Keys & ConnStrings) | |
| | +---------------------------+ |
| | |
| | +---------------------------+ |
| +----------------->| Azure Blob Storage | |
| | (Files) | (Storage Account) | |
| | +---------------------------+ |
| | |
| | +---------------------------+ |
| +----------------->| Azure SQL Database | |
| (Data) | (PaaS SQL Server) | |
| +---------------------------+ |
+-------------------------------------------------------------+
^
| (HTTPS)
+------+-------+
| Professor |
| (Browser) |
+--------------+
Required Azure Resources
- Azure App Service: Hosts your ASP.NET Core MVC application. You can run both the Web UI and the Background Worker in the same App Service Plan to save costs during initial development.
- Azure SQL Database: A managed relational database for your
Exam,Submission, andResultdata. Use the “Serverless” tier for cost-efficiency in low-traffic scenarios. - Azure Blob Storage: A Storage Account with a container (e.g.,
exams) to hold the physical PDF and image files. - Azure AI Document Intelligence: The cognitive service that performs the OCR and handwriting analysis.
- Azure Key Vault: Essential for security. Store your Storage connection strings and AI API keys here instead of in
appsettings.jsonor environment variables.
13. Exporting Grades to LMS
Once the grading is complete and the professor has reviewed the scores, you’ll want to move that data into your school’s Learning Management System (LMS) like Canvas, Blackboard, or Moodle.
13.1 Simple CSV Export
Most LMS platforms allow you to “Import Grades” via a CSV file. You can easily generate this in .NET:
[HttpGet]
public async Task<IActionResult> ExportToCsv(int examId)
{
var submissions = await _context.Submissions
.Where(s => s.ExamId == examId && s.Status == SubmissionStatus.Completed)
.Include(s => s.Student)
.Include(s => s.Results)
.ToListAsync();
var csv = new StringBuilder();
csv.AppendLine("StudentName,StudentEmail,TotalScore");
foreach (var sub in submissions)
{
double total = sub.Results.Sum(r => r.Score);
csv.AppendLine($"{sub.Student.Name},{sub.Student.Email},{total}");
}
byte[] buffer = Encoding.UTF8.GetBytes(csv.ToString());
return File(buffer, "text/csv", $"Grades_Exam_{examId}.csv");
}
13.2 Direct Integration (LTI 1.3)
For a more seamless experience, you can implement the LTI (Learning Tools Interoperability) standard.
- How it works: Your application acts as an “LTI Tool.” The LMS sends a secure request to your app, and your app can “post back” the grades directly into the LMS gradebook using the Assignment and Grading Service.
- Library Recommendation: Use a library like
LtiAdvantageorLtiLibraryto handle the complex OAuth2 and JWT handshake required for LTI 1.3.
14. Summary & Next Steps
Building an AI grading system in .NET 10 is more accessible than ever. By combining ASP.NET Core MVC for the interface and Azure AI for the heavy lifting, you can create a tool that saves educators hundreds of hours.
Key Takeaways:
- Cloud Storage is Essential: Use Blob Storage to decouple your web server from your file storage.
- Asynchronous is Better: Never make a user wait for an AI process. Use background workers.
- Human-in-the-loop: AI is a helper, not a replacement. Always provide a review interface.
Leave a comment