← Back to Projects
Blazor.NETAzure SQLEntra IDGitHub ActionsQuestPDFModernization

Legacy VB.NET App Modernization: From SSRS to Blazor in a Weekend

A 12-year-old VB.NET application — SSRS reports, AD group auth, on-prem SQL Server — fully modernized to Blazor, Azure SQL, QuestPDF, and Entra SSO with GitHub Actions CI/CD. 20 hours start to finish, two weekends.

The Starting Point

A 12-year-old VB.NET desktop application that had outlived its era. The application managed a real business workflow with an established SQL Server database — the data model was solid and worth keeping. Everything around it wasn't.

The specific pain points:

  • SSRS dependency for all reporting — heavy, locked to on-prem infrastructure, impossible to run anywhere else
  • Active Directory group authentication — groups managed manually, no MFA enforcement, no conditional access
  • No CI/CD — deployments were manual file copies
  • VB.NET codebase — no modern tooling, no GitHub, no one under 45 was comfortable in it
  • On-prem SQL Server — no redundancy, no managed backups, no scalability path

The goal was a complete modernization without losing a decade of business data or changing how reports looked.

💡 Tip

The database schema was the one thing worth preserving. 12 years of production data, well-normalized, with real business meaning baked in. The migration strategy was built around protecting that.

What Changed

Database: SQL Server → Azure SQL

The schema migrated to Azure SQL Database with minimal changes — a few column type adjustments and the removal of some SSRS-specific stored procedures. Azure SQL's compatibility level handles the rest.

This gave the application managed backups, point-in-time restore, geo-redundancy, and elastic scaling without touching the application's data layer.

Frontend: VB.NET Windows Forms → Blazor Server

The UI was rebuilt as a Blazor Server application. Blazor was chosen over React or Next.js deliberately — the team maintaining this application is .NET-native. Blazor keeps them in C# and Razor syntax, with no JavaScript framework to learn or maintain.

The component structure mirrors the original application's workflow layout, making the UX transition feel evolutionary rather than disorienting for end users.

Reporting: SSRS → QuestPDF

This was the most technically interesting constraint. SSRS reports had been in use for 12 years. The rendered output — column widths, fonts, page breaks, headers — was what users considered "correct." Any deviation would be rejected.

QuestPDF replaced SSRS entirely, with reports defined in fluent C# that produce pixel-accurate PDF output matching the original SSRS layouts.

Document.Create(container => {
    container.Page(page => {
        page.Size(PageSizes.Letter);
        page.Margin(1, Unit.Centimetre);
        page.Header().Element(ComposeHeader);
        page.Content().Element(ComposeContent);
        page.Footer().AlignCenter().Text(x => {
            x.Span("Page ");
            x.CurrentPageNumber();
            x.Span(" of ");
            x.TotalPages();
        });
    });
});

The reports now generate server-side in Blazor and stream as PDF downloads — no SSRS server, no report manager, no deployment complexity.

Note

QuestPDF is MIT licensed, actively maintained, and generates production-quality PDFs in pure C#. For anyone still running SSRS dependencies, this is the migration path.

Authentication: AD Groups → Entra SSO with Application Roles

Authentication moved from LDAP-queried Active Directory groups to Entra ID SSO with application roles defined in the app registration.

This gave the application MFA enforcement via conditional access policies, full audit logging through Entra sign-in logs, and role assignment managed in the Entra portal rather than AD group membership.

// Blazor auth policy using Entra application roles
builder.Services.AddAuthorization(options => {
    options.AddPolicy("AdminOnly", policy =>
        policy.RequireRole("App.Admin"));
    options.AddPolicy("ReadOnly", policy =>
        policy.RequireRole("App.Read", "App.Admin"));
});

CI/CD: Manual Deploys → GitHub Actions

The codebase moved to GitHub. Two environments: dev (auto-deploys on every push to main) and prod (gated — requires a manual approval in GitHub Actions before deploying).

deploy-prod:
  needs: deploy-dev
  environment:
    name: production
    url: https://app.greyhelixit.com
  steps:
    - name: Deploy to Azure App Service (prod)
      uses: azure/webapps-deploy@v3
      with:
        app-name: ${{ secrets.AZURE_PROD_APP_NAME }}
        package: ./publish

This gave the team a proper testing surface and removed the risk of untested changes going directly to production.

Build Time

The entire project — database migration, Blazor rebuild, QuestPDF report porting, Entra SSO integration, and GitHub Actions CI/CD setup — took approximately 20 hours across two weekends, with Claude Code handling the heavy lifting on boilerplate, report layout code, and the Entra configuration.

Outcome

BeforeAfter
VB.NET Windows FormsBlazor Server (C#)
On-prem SQL ServerAzure SQL Database
SSRS reportsQuestPDF (in-process, streaming)
AD group authenticationEntra ID SSO + application roles
Manual file deploymentsGitHub Actions (dev + prod gated)
No redundancyManaged backups, geo-redundancy

The application is running in production. Users reported the new UI as "basically the same but faster" — which, for a modernization project, is the ideal response. Reports are identical to the original SSRS output. The SSRS server has been decommissioned.

💡 Tip

This project was an experiment: how much can a single consultant modernize in a weekend using Claude Code as a pair? The answer, for a well-scoped application with a clean data model, is: all of it.