Home/Modernize/ColdFusion to .NET
Modernize

ColdFusion to .NET, one module at a time.

The most common destination for a CFML application, and for a good reason: the database, the Windows estate and the hiring market are usually already there. What it needs is a route that does not require the whole system to move at once.

orders.cfmbefore
<cfquery name="qOrders" datasource="app">
  SELECT order_id, total, status
  FROM   orders
  WHERE  customer_id = <cfqueryparam
           value="#url.custId#"
           cfsqltype="cf_sql_integer">
  ORDER BY created DESC
</cfquery>
OrdersController.csafter
var orders = await db.Orders
  .Where(o => o.CustomerId == custId)
  .OrderByDescending(o => o.Created)
  .Select(o => new OrderRow(
     o.OrderId, o.Total, o.Status))
  .ToListAsync(ct);
TargetC# on ASP.NET CoreData layerEF Core or Dapper; your callMethodStrangler, module by moduleDatabaseStays where it isURLsPreserved through the routerRollbackPer module, not per project
Why this is usually the answer

Most ColdFusion estates are already half-way to .NET.

A large share of the CFML applications still running were built against SQL Server, deployed on Windows, fronted by IIS and integrated with an Active Directory the business is not going to replace. Moving that application to .NET keeps every one of those decisions intact and changes only the layer that has become hard to staff.

That matters more than language preference. The expensive part of a migration is never the syntax, it is the integrations, the reporting, the scheduled jobs and the twenty years of behaviour nobody wrote down. Keeping the platform underneath unchanged removes most of that risk before you start.

The hiring argument is the second half. Almost nobody has learned CFML in the past decade; C# is taught, hired and contracted for in every market you operate in. If the reason you are migrating is that you cannot find maintainers, the target has to be a language you can recruit.

Where .NET is the wrong answer, we will say so. If you are on Oracle and the JVM, Java is a shorter trip. If the application is mostly JSON endpoints and a small team wants one language front and back, Node is simpler to run. We deliver all three, so the recommendation is not an argument for our bench.

The part nobody shows you

What your CFML becomes.

Three constructs that appear in every ColdFusion application we have ever opened, and what each looks like on the other side. If a migration proposal cannot show you this, it has not been written by anyone who has read your code.

01

A CFC becomes a service with injected dependencies

ColdFusion (CFML)
component accessors="true" {

  property name="gateway";
  property name="mailer";

  public struct function place(required struct cart) {
    transaction {
      var id = gateway.insert(arguments.cart);
      mailer.confirm(id);
      return { ok = true, orderId = id };
    }
  }
}
C# / ASP.NET Core
public sealed class OrderService(
    IOrderGateway gateway,
    IMailer mailer,
    AppDb db)
{
  public async Task<PlaceResult> PlaceAsync(
      Cart cart, CancellationToken ct)
  {
    await using var tx =
      await db.Database.BeginTransactionAsync(ct);
    var id = await gateway.InsertAsync(cart, ct);
    await mailer.ConfirmAsync(id, ct);
    await tx.CommitAsync(ct);
    return new PlaceResult(true, id);
  }
}

The shape survives. What changes is that dependencies arrive through the constructor instead of being reached for, which is what finally makes the thing unit-testable, usually for the first time in its life.

02

Application.cfc becomes the middleware pipeline

ColdFusion (CFML)
component extends="Application" {

  this.name = "portal";
  this.sessionManagement = true;
  this.sessionTimeout = createTimeSpan(0,2,0,0);

  public boolean function onRequestStart(string target) {
    if (!structKeyExists(session, "user")
        && !isPublic(arguments.target)) {
      location(url="/login.cfm", addToken=false);
    }
    return true;
  }
}
C# / ASP.NET Core
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSession(o =>
    o.IdleTimeout = TimeSpan.FromHours(2));
builder.Services
    .AddAuthentication(CookieDefaults.Scheme)
    .AddCookie(o => o.LoginPath = "/login");

var app = builder.Build();
app.UseSession();
app.UseAuthentication();
app.UseAuthorization();   // replaces onRequestStart
app.MapControllers();
app.Run();

This is the translation teams underestimate. onRequestStart is a single function doing authentication, authorisation, locale, logging and often a little routing. In .NET those are five separate pieces of middleware, and pulling them apart is where the hidden business rules surface.

03

cfoutput over a query becomes a typed view

ColdFusion (CFML)
<cfoutput query="qOrders">
  <tr>
    <td>#qOrders.order_id#</td>
    <td>#DollarFormat(qOrders.total)#</td>
    <td>#DateFormat(qOrders.created, "dd/mm/yyyy")#</td>
  </tr>
</cfoutput>
C# / ASP.NET Core
@foreach (var o in Model.Orders)
{
  <tr>
    <td>@o.OrderId</td>
    <td>@o.Total.ToString("C")</td>
    <td>@o.Created.ToString("dd/MM/yyyy")</td>
  </tr>
}

Straightforward, with one trap worth naming: CFML is loosely typed and forgiving about nulls, and .NET is not. Columns that have quietly contained nulls for fifteen years will announce themselves during this step. That is a feature, but it needs to be budgeted for.

The reference table

Every construct, and where it lands.

We work through this list on every .NET migration. It is also the fastest way for your own team to sanity-check a quote from anybody else.

ColdFusion.NET equivalentDifficulty
cfquery / cfqueryparamEF Core or Dapper with parametersLow
cfcomponent / CFCClass registered in DILow
Application.cfcMiddleware pipeline in Program.csMedium
APPLICATION scopeSingleton service or distributed cacheMedium
SESSION scopeCookie auth, or Redis if you scale outMedium
cfmailMailKit or a transactional mail APILow
cfdocument (PDF)QuestPDF, or a headless-browser rendererMedium
cfspreadsheetClosedXML / EPPlusLow
cffile upload handlingIFormFile plus explicit validationMedium
Scheduled tasks in CF AdminHosted service or an external schedulerMedium
Custom tagsTag helpers or view componentsMedium
cfinclude chainsPartial views, often the messiest stepHigh
Undocumented business rulesDiscovery. This is the real project.High
The method

Both systems run. A router decides which one answers.

A big-bang rewrite asks the business to accept a date, a budget and a period of feature freeze, and to trust that the new system reproduces behaviour nobody has fully written down. That is the shape of project that gives migrations their reputation.

We put a router in front instead. Every URL keeps working. Each module is moved when it is ready and sent back to ColdFusion the moment it misbehaves, without touching anything else.

Reporting and search usually go first: high read volume, low write risk, and quick to verify against the old system by running both and comparing output.

Some modules never move, and that is a legitimate outcome. A batch job that runs twice a year and works is not worth the regression risk.

A router sending each module either to the existing ColdFusion application or to the new .NET services, one slice at a time
The date the last module moves is the only one that is hard to predict, and the only one that does not matter.
From projects we have run

Six things that go wrong, and what we do about them.

None of these are exotic. All of them have derailed somebody's CFML migration, usually in the last third of the project when the budget is gone.

Loose typing meets a strict compiler

CFML will happily compare a string to a number. C# will not, and neither will your database once nulls stop being silently coerced. We surface this during comprehension by profiling actual column contents, not trusting the schema.

Scope creep, literally

APPLICATION and SESSION get used as a global dumping ground over twenty years. Each entry has to be traced to an owner before it becomes a singleton, a cache entry or a claim, getting this wrong produces bugs that only appear under load.

Business logic living in views

Rules embedded in cfinclude chains and display templates do not show up in any component inventory. Reading the templates, not just the CFCs, is the difference between an estimate and a guess.

Date and locale handling

CFML's date functions are forgiving in ways .NET is not, and applications serving several countries usually have at least one place where that forgiveness has been silently load-bearing for years.

Scheduled tasks nobody owns

The CF Administrator's scheduler is where the reports live. It is almost never in source control and almost never documented. We inventory it in week one because it is the most common cause of a quiet post-cutover failure.

Integrations that authenticate as the server

Legacy CFML often talks to partners from a fixed IP with credentials embedded in the file system. Moving the runtime moves the IP, which means partner-side changes with their own lead times. Found late, this stops a cutover dead.

Questions worth asking

Before you commit to .NET.

The questions we get asked in the first call, answered the way we answer them there.

How long does a ColdFusion to .NET migration take?

The honest answer is that the size of the CFML codebase predicts it poorly. What predicts it is how much behaviour is undocumented, how many integrations exist and how clean the separation is between logic and display. A well-structured application with CFCs doing the work and templates doing display can move in a few months of staged releases. An application where twenty years of rules live inside cfinclude chains takes considerably longer, and most of that time is comprehension, not coding. This is why we do not quote a duration before the assessment: a number produced without reading the code is a sales number, and you will be the one holding it when it turns out to be wrong.

Do we have to move the database as well?

No, and we would usually argue against doing both at once. The database is typically the healthiest part of a legacy estate, it holds the data you cannot afford to lose, and moving it at the same time as the application removes your ability to tell which change caused a problem. Leaving SQL Server or Oracle exactly where it is means the new .NET modules and the remaining CFML modules read and write the same tables during the transition, which is what makes a staged migration possible. If the database does need work, it is a separate project with its own plan, run after the application is stable.

Can our existing team maintain the result?

That is the question the whole exercise is for, so we build for it explicitly. Your engineers work alongside ours from the first module rather than receiving a finished system, and the documentation produced during comprehension is written for your team, not for our delivery process. If you do not currently have .NET people, say so early, it changes the architecture we recommend. A small team new to .NET is better served by a conventional layered application they can read than by a fashionable distributed one that looks impressive in a diagram and is miserable to debug at 2am.

What happens to our URLs and our search rankings?

They are preserved, deliberately and from day one. The router keeps every existing path answering, so a module moving from CFML to .NET is invisible from outside. Where a URL has to change, it gets a 301 to the closest equivalent, not to a homepage. For applications with public, indexed pages this is not a detail; we have seen more commercial damage from a careless URL change during a migration than from any technical defect in the new code.