Evincia

SQL Server modernization risk: when the database is the application

Why a .NET modernization that treats SQL Server as "just storage" under-scopes the work -- and what to look at instead.

In a lot of legacy .NET shops, the application is not where the work happens. The C# is a thin layer over a database that validates, calculates, routes, and decides -- in stored procedures (saved blocks of logic that run inside the database itself) written over fifteen years, triggers nobody remembers adding, and SQL Agent jobs (scheduled tasks the database runs on its own) that run the month-end close. The application is the part you can see. SQL Server is the part that's load-bearing.

That's the problem with modernization plans that scope from the application code. They read the C#, size the rewrite, pick a target, and treat the database as a storage layer that comes along for the ride. Then the team rebuilds the application, points it at the same database, and discovers that a third of the business logic was in T-SQL (the programming language that runs inside SQL Server) the whole time. The discovery happens during testing, which is the most expensive place to find scope -- the cost of the missed work doesn't go away, it just gets found late, when changing the plan is hardest.

This page is about the SQL Server half of a .NET modernization: the risk that doesn't show up when you only read the application. It's the deep companion to the triage checklist -- where the checklist says "look at SQL Server," this explains what you're looking for. For the lifecycle dates themselves (which SQL Server versions are out of support, and when), see the end-of-support page; this page is about architecture, not the calendar.

Modernizing SQL Server: what sets the scope

There are two different things people mean by "SQL Server risk," and conflating them is how modernization budgets go wrong.

Engine risk -- the platform itself

This is the version, edition, patch level, and support status of the SQL Server instance. An instance running SQL Server 2012 or 2014 is out of support; a Standard edition that's hit a scaling limit is a different problem; an unpatched instance is an audit finding. Engine risk is real, but it's bounded -- it has a known shape and teams generally budget for it. It maps to the Platform Obsolescence dimension. The dates live on the end-of-support page, with a dedicated SQL Server 2016 end-of-support guide; we don't repeat them here.

Architecture risk -- how the application uses SQL Server

This is the one that hurts. It's the business logic sitting in stored procedures and triggers (logic the database runs automatically when data changes), the cross-database queries that assume two databases live on one instance, the jobs that run outside the application, the reports that read production tables directly. None of it is visible from the application code, and none of it is described by the version number. Architecture risk maps to the Architectural Coupling and Dependency Risk dimensions, and it's where the scope a team didn't plan for actually lives.

The pattern is consistent: teams budget for engine risk, because a version number is easy to see, and get surprised by architecture risk, because it isn't. The rest of this page is about the second kind.

The SQL patterns that change scope

These are the recurring patterns that turn "migrate the database" into a much larger piece of work. Each one is field-observed, invisible to a code-only review, and moves the estimate.

  • Business logic in stored procedures and triggers. The rule that decides whether an order ships, the calculation behind a customer's price, the validation that keeps bad data out -- if these live in T-SQL rather than the application, modernizing the application doesn't touch them. Where to look: sys.sql_modules for procedure and view definitions, sys.triggers for DML triggers, and the size of those definitions. A 4,000-line stored procedure is not a storage detail; it's an application nobody named.
  • Cross-database and cross-server coupling. Queries that use three- or four-part names (OtherDatabase.dbo.Table, LinkedServer.Db.dbo.Table) assume the databases and servers will always sit together. They won't, in a cloud target. Where to look: sys.servers for linked servers (direct connections from one database to another), and a search of procedure text for cross-database references. This pattern alone can decide which Azure target is even viable -- see below.
  • SQL Agent jobs doing real work. ETL, billing runs, nightly reconciliation, data cleanup, the export the finance team depends on -- scheduled inside SQL Server, outside the application. Where to look: msdb.dbo.sysjobs and sysjobsteps to see what each job actually runs. Jobs are the single most common "we didn't know that was there" finding, because nobody opens SQL Server Agent during an application review.
  • Reporting that reads production tables directly. SSRS reports, Excel workbooks, and Power BI datasets pointed straight at the operational schema. The moment the schema changes during modernization, these break silently, and they're often the most politically visible thing in the building. Where to look: the SSRS ReportServer catalog, and connection strings in the reporting estate.
  • SSIS packages as hidden integration. The data flows between systems frequently run as SSIS packages (Microsoft's tool for moving data between systems) on a schedule, not as application code. Where to look: the SSIS catalog (SSISDB) or msdb, and .dtsx files on the server. An SSIS package is an integration that has to be rebuilt or re-hosted, not a query.
  • Engine features that don't port to every target. SQL CLR assemblies, Service Broker queues, FILESTREAM and FileTable, and distributed transactions are all specialized SQL Server capabilities that a modern managed cloud database may not support. Where to look: sys.assemblies for CLR, sys.service_queues for Service Broker, and column metadata for FILESTREAM. Each one narrows your migration options -- if your system depends on one the cheaper targets don't offer, those targets are simply off the table.
  • Compatibility level masking behavior. A database running on a current SQL Server instance but set to an old compatibility level (a setting that makes a new SQL Server act like an older one) behaves, in places, like the old version -- including the query optimizer. Modernizing can quietly change the level and change query plans with it. Where to look: sys.databases.compatibility_level. Azure SQL Database won't accept a level below 100 at all.
  • Data quality the old system tolerates. Disabled or untrusted foreign keys, cleanup logic that runs in the application instead of constraints, "we fix that in the nightly job" data. Where to look: sys.foreign_keys and sys.check_constraints for is_disabled / is_not_trusted. Data the old system tolerated will break the new one's assumptions, and reconciling it is its own workstream.

What this means for the target

The patterns above aren't academic. They decide which cloud target is actually available to you, and that decision is one of the more expensive ones in the whole modernization. There are three broad targets for a SQL Server database, and the right one is driven by how much of your logic lives inside the engine.

  • SQL Server on an Azure VM (IaaS -- you rent the machine, you still run the database). Full surface-area compatibility: everything that runs on-premises runs here, including SQL Agent, linked servers, CLR, Service Broker, and FILESTREAM. The cost is that it modernizes nothing -- you still own patching, high availability, and the operating system. It's the answer when the architecture risk is too high to re-engineer right now, or when a feature like FILESTREAM rules out the managed options.
  • Azure SQL Managed Instance (high-compatibility PaaS -- Microsoft runs the database for you, but keeps most of the old features). Microsoft positions Managed Instance as high-compatibility with the SQL Server engine, and most instance-level features survive: SQL Server Agent, cross-database queries within the instance, SQL CLR, Service Broker, distributed transactions, and linked servers to other SQL targets. The notable gaps are FILESTREAM and FileTable, which it does not support. This is usually the lowest-re-engineering path off a coupled, T-SQL-heavy database.
  • Azure SQL Database (most-managed PaaS). The most modern and most managed target, and the one that assumes your logic lives in the application. It does not support SQL Server Agent, cross-database or linked-server queries using three- and four-part names, SQL CLR assemblies, Service Broker, or FILESTREAM, and it won't accept a compatibility level below 100. Distributed transactions are a partial case -- it supports limited client-coordinated elastic transactions across Azure SQL databases, but not MSDTC or server-side BEGIN DISTRIBUTED TRANSACTION, so treat existing distributed-transaction patterns as non-portable until proven otherwise. The more your application leans on those, the more a move here is a re-engineering project, not a migration.
SQL Server feature support by Azure target -- what survives the move
Feature SQL Server on VM (IaaS) Azure SQL Managed Instance Azure SQL Database
SQL Server Agent Supported Supported Not supported
Cross-database queries Supported Supportedwithin the instance Not supported
Linked servers Supported Supportedto SQL targets Not supported
SQL CLR assemblies Supported Supported Not supported
Service Broker Supported Supported Not supported
Distributed transactions (MSDTC) Supported Supported Not supported
FILESTREAM / FileTable Supported Not supported Not supported
Managed Instance keeps cross-database queries within the instance and linked servers to SQL targets. On distributed transactions, Azure SQL Database supports only limited client-coordinated elastic transactions across Azure SQL databases -- not MSDTC or server-side patterns. Azure SQL Database also requires a compatibility level of 100 or higher. Source: Microsoft Learn features comparison (linked below); verify for your target.

So the architecture risk you inventoried is the input to the target decision. A database whose logic is genuinely in the application can go to Azure SQL Database cleanly. A database full of jobs, linked servers, CLR, and cross-database queries either goes to Managed Instance, gets re-engineered to shed those dependencies, or stays on a VM. Picking the target before you've done the inventory is how teams commit to Azure SQL Database and then spend a year discovering why they can't get there.

The specifics are documented and worth reading directly: Microsoft's features comparison for Azure SQL Database and Managed Instance ↗ and the assessment rules for SQL Server to Azure SQL Database migration ↗ are the primary sources for what each target does and doesn't support.

The SQL inventory

This is the database-side inventory the triage checklist points to. Every item below is a query against system metadata, which means you can produce most of it in an afternoon without guessing. An afternoon of looking now replaces a year of finding out later, and it converts the database from a hopeful assumption in the budget into a measured number.

  • Programmable objects and their weight. sys.procedures, sys.sql_modules (the definition text), sys.triggers, and the function objects in sys.objects. Don't just count them -- measure them. The total line count of T-SQL logic is a direct proxy for how much application lives in the database.
  • Scheduled work. msdb.dbo.sysjobs and msdb.dbo.sysjobsteps for SQL Agent jobs and what each step runs. Note which jobs touch other systems -- those are integrations, not maintenance.
  • Cross-boundary coupling. sys.servers for linked servers, and sys.sql_expression_dependencies to surface cross-database references. These determine which targets are viable.
  • Integration and reporting. The SSISDB catalog (or msdb) for SSIS packages, and the SSRS ReportServer database for reports and their data sources. Both are work that doesn't appear in the application repository.
  • Engine-feature dependencies. sys.assemblies (user-defined CLR), sys.service_queues and the database's is_broker_enabled flag (Service Broker), and FILESTREAM usage via is_filestream in sys.columns. Each one constrains the target.
  • Behavior and integrity flags. sys.databases.compatibility_level for each database, and is_disabled / is_not_trusted on sys.foreign_keys and sys.check_constraints. These predict where data reconciliation will be needed.
  • Backups that have actually been restored. Backup history is in msdb, but the real question is whether a restore has ever been tested. A backup that has never been restored is a theory with a timestamp.

What comes next

The SQL inventory turns "the database is fine, it's just storage" into a measured statement: this much logic is in T-SQL, these jobs do this work, these features pin us to these targets, this data needs reconciling. That's the input a modernization plan needs and rarely has. Without it, the database half of the estimate is a guess wearing a number.

This is the database half of Modernization Shield. The SQL Server and T-SQL layer -- schema complexity, stored-procedure risk, jobs, and data dependencies -- is assessed through expert review during the engagement alongside the .NET application. The deliverable ties the database findings to the same Modernization Readiness picture as the application, so the two halves of the system are scoped together instead of one being discovered later.

How much of the business logic lives in the database?

Modernization Shield reviews stored procedures, schema complexity, SQL Agent jobs, linked servers, and application data-access patterns before the database becomes missing scope. It shows where business logic is hiding, what is tightly coupled, and which target platforms the current design can support.

The SocialGoal sample Modernization Risk Report shows how those database findings are evidenced and sequenced alongside the application work.

About this page

Last updated: .

This page reflects patterns observed across the legacy SQL Server and .NET systems we assess. Azure SQL feature support is current as of the Microsoft Learn references linked above and does change over time; verify the specifics for your target before committing. If a claim here contradicts your direct experience, we'd rather hear about it than ship something inaccurate. Email info@evincia.co.

Companion pages: the legacy .NET and SQL Server triage checklist for the breadth inventory, the .NET Framework migration risk guide for the application half, Microsoft platform end-of-support dates for the lifecycle calendar, and the methodology for how the dimensions are scored.