.NET on AWS Blog
Modernize your legacy WCF services: Automated migration to ASP.NET Core Web API using AWS Transform custom
Enterprises who need to migrate business-critical Windows Communication Foundation (WCF) services to ASP.NET Core Web API sometimes struggle with the scope of the change. These services still run on .NET Framework, the pool of engineers fluent in service contracts keep shrinking, and modern .NET does not support WCF. The community CoreWCF project (a .NET Foundation project that Microsoft officially recommends) fills the server-side hosting gap, but it preserves the SOAP architecture rather than replacing it. When the goal is to move away from SOAP entirely, a REST-based Web API is the appropriate target, and it addresses the broader challenges with cross-platform hosting, modern tooling, and an actively developed runtime.
However, manually converting a WCF service to a Web API is tedious and error-prone, because it is an architecture change. Every [ServiceContract] becomes an attribute-routed controller, every operation that returned null now owes an HTTP 404, and Entity Framework 6 (EF6) patterns that tolerated in-memory filtering become genuine performance defects under Entity Framework Core (EF Core), across every operation in every service.
This post walks through using AWS Transform custom to automate the migration of a sample .NET Framework WCF SOAP service to an ASP.NET Core Web API on .NET 10. You will learn how to create a custom transformation definition, execute the transformation, and verify the result, all from PowerShell on Windows, with minimal manual effort.
About AWS Transform custom
AWS Transform custom is an agentic AI service that performs large-scale modernization of software, code, libraries, and frameworks. It handles API and service migrations, language version upgrades, framework upgrades, code refactoring, and organization-specific transformations.
AWS Transform custom offers out-of-the-box (AWS-managed) transformations for scenarios such as Java SDK version upgrades, .NET modernization, Node.js and Python version upgrades. This walkthrough uses a custom definition for AWS Transform custom that targets a specific architectural conversion: WCF SOAP to ASP.NET Core REST Web API, with explicit decisions and correctness checks for routing, DTOs, EF Core, and HTTP semantics. You define a transformation using natural language, and the AI agent executes it against your codebase. Once saved, the definition is reusable across every WCF service in your organization.
Solution overview
This is the migration workflow:
- Start with a .NET Framework WCF SOAP service using Entity Framework 6.
- Use AWS Transform custom to convert it to an ASP.NET Core Web API on .NET 10 with EF Core.
- Optionally, continue with AWS Transform Full-Stack Windows Modernization for the surrounding estate (databases, UI frameworks, Linux deployment).
The AWS Transform custom workflow follows these phases:
- Define — Create a transformation definition describing the WCF-to-Web-API conversion rules in natural language
- Plan — The AI agent analyzes the codebase and generates a step-by-step execution plan
- Execute — The agent transforms files incrementally, verifying the build after each step
- Validate — Confirm all exit criteria are met: successful build, no remaining WCF artifacts, correct REST semantics
Prerequisites
Before you start, verify that you have the following:
- An AWS account with AWS Transform custom access configured.
- The AWS Transform CLI (atx), which requires Node.js 22+ and Git.
- The .NET Framework 4.8 SDK and MSBuild.
The .NET 10 SDK. - An Amazon Elastic Compute Cloud (Amazon EC2) Windows instance.
Environment setup: Amazon EC2 Windows instance
The original project requires the .NET Framework build toolchain (MSBuild), and the transformed project requires the .NET 10 SDK. To meet both requirements, run the AWS Transform CLI (atx) natively on Windows with PowerShell on a Windows EC2 instance. All commands in this post are PowerShell.
To set up the environment:
- Launch a Windows EC2 instance (m6i.xlarge is sufficient) and connect over RDP.
- Install Git for Windows and Node.js 22+.
- Install the AWS Transform CLI and confirm it resolves in PowerShell.
- Install .NET Framework 4.8 SDK and MSBuild on the Windows host.
- Install the .NET 10 SDK.
Sample application
This walkthrough uses a sample WCF catalog service, the eShopWCFService project from the open source eShopModernizing reference application, reproduced as a standalone sample to demonstrate the migration:
- Repository:
sample-aws-transform-custom-wcf-to-webapi mainbranch: Original WCF service source codeatx-result-staging-20260805_064143_27e9dc2cbranch: ASP.NET Core Web API converted by AWS Transform custom – browse on GitHub
The atx-result-staging-20260403_074329_289c738e branch was created and committed automatically by atx as a local branch during the transformation process. It was then pushed to the remote repository manually.
Application structure
The service targets .NET Framework and includes the following components:
The service demonstrates WCF and Entity Framework 6 (EF6) patterns commonly found in enterprise applications:
[ServiceContract]interface with 10[OperationContract]operations over basicHttpBinding[DataContract]entities that also carry EF mapping attributes- In-memory filtering
.ToList()followed by.Where(...)in three operations Max(Id) + 1key assignment with[DatabaseGenerated(DatabaseGeneratedOption.None)]HasPrecision(19, 4)and amoneycolumn type on the price propertyCreateDatabaseIfNotExistsseeding with preconfigured reference data- Fully synchronous data access throughout
Before running the transformation, create a baseline tag. Since ATX modifies files in place and commits to its own branch, that tag serves as your checkpoint to the original, untouched source.
PS C:\repos\eshop-wcf-service-sample> git tag atx-baseline
Walkthrough
Step 1: Start AWS Transform custom
Launch atx from the project directory. The interactive CLI displays the session banner and accepts natural language commands.
Step 2: Create the transformation definition
When you ask atx to transform WCF to a Web API, it first checks the transformation registry. The agent calls list_available_transformations_from_registry, finds the AWS-managed transformations but none for WCF to ASP.NET Core Web API. The agent recognizes the gap and decides to create a custom transformation definition, asking clarifying questions to shape it:
The agent continued the authoring interview across service-layer design, connection-string handling and DTOs, each question carrying its own recommendation. Two answers are worth calling out because they overrode the defaults: keeping the legacy ConnectionString environment variable in the configuration fallback chain (existing deployments set it), and requiring DTOs without ReferenceHandler.Preserve, $id/$ref pairs and broken ordinary JSON clients. The interview also supplied a set of correctness rules for the definition to encode status-code semantics for absence (404, not a 200 with a null body), SQL-translatable date comparisons, database-generated keys, preserved decimal precision, non-destructive seeding, and no invented API surface. Each rule corresponds to a defect class that compiles cleanly and is still wrong.
After the interview, atx generates the transformation definition as a markdown file. The definition includes an objective, entry criteria, 10 implementation steps, and numbered validation/exit criteria. Here is a snippet of the generated definition:
Before saving, the agent reviewed the quality of its own draft and proposed five fixes including one to the instructions it was given: the interview had described the discount date lookup as a half-open range like the stock lookup, but the legacy GetDiscount check is a closed inclusive range (Start <= day && End >= day) and porting it half-open silently exclude discounts on their end date. Finally, the agent saves the definition as a draft:
The transformation definition is saved and is reusable across other WCF project. Publish it to the registry so team members use it directly:
PS C:\repos> atx custom def publish -n "wcf-to-webapi-dotnet10" --sd .\wcf-to-webapi-dotnet10 --json{"name":"wcf-to-webapi-dotnet10","version":"<minted-version-id>"}
Harden the definition before you publish it
A draft definition is a hypothesis until its output has been read. Before publishing, apply the draft to the codebase and inspect what it produced, and this loop is worth walking through, because it is where a definition earns its reuse.
The first application looked like success. The build was clean and the Validate phase reported 27 of 28 exit criteria passing:
However, reading the generated code told a different story:
This is the legacy defect, preserved in async form: the whole table still materializes before the filter runs. The definition’s original criterion grepped for the legacy .ToList().Where() shape, which this code no longer matches so the gate stayed green. Reading further found five more defects of the same kind, all compiling, none caught by a criterion: the Max(Id)+1 key assignment retained along with the DatabaseGeneratedOption.None attribute that forces it, PUT and DELETE returning 204 for a missing entity instead of 404, DELETE binding a [FromBody] entity instead of the route id, and DateTime.Date comparisons EF Core cannot translate.
The fix is a feedback message to the agent aimed at the definition, not the files:
> The build passes but the correctness rules were not all applied. Fix the definition, not just the generated files, then re-apply. GetCatalogItemsAsync filters after ToListAsync (Services/CatalogService.cs), CreateCatalogItemAsync still computes MaxAsync(i => i.Id) + 1, and DatabaseGeneratedOption.None is still on all four models.
The agent amended the definition, adding a mechanically checkable exit criterion per defect class. This is the one it wrote for the filtering defect, note it targets the async shape that evaded the original check:
31. grep -nE "ToListAsync\(\)\s*;" -A2 Services/CatalogService.cs | grep -cE "\.(Where|FirstOrDefault|Any)\(" returns 0 — no LINQ predicate may appear after a materializing call. Converting .ToList().Where(...) to .ToListAsync() then .Where(...) preserves the whole-table scan and is not a fix.
Re-applying the amended definition to a fresh checkout produced code with all six defects fixed, and the validation gate now 34 criteria prove it. That is the whole economics of a reusable definition: patching generated code fixes one run; amending the definition fixes every subsequent run, and the criteria added in this loop are exactly what lets the next execution pass with no human steering at all.
Step 3: Execute the transformation
Once the definition published, execution is the short path a teammate ever needs to run. Pin the version for reproducibility, and pass the build command that becomes the agent’s per-step verification gate:
PS C:\repos\...\eShopWCFService> atx custom def exec ` -n "wcf-to-webapi-dotnet10" ` --tv "<minted-version-id>" ` -p (Get-Location).Path ` -c "dotnet build eShopWCFService.csproj"
atx analyzes the codebase and generates a transformation plan:
atx creates new branch automatically before any edit, so the original branch is preserved:
The agent then works through the implementation steps, building after each step. Because the first 3 steps are interdependent, the WCF artifacts must be deleted before their ASP.NET Core replacements compile, the definition batches them, and the run lands as a single comprehensive commit on the staging branch:
PS C:\repos\eshop-wcf-service-sample> git log --oneline main..atx-result-staging-20260805_064143_27e9dc2c298d295 Step 1: Complete WCF to ASP.NET Core Web API migration - replaced csproj with SDK-style net10.0, deleted WCF artifacts, migrated EF6 to EF Core, created async service layer, REST controllers with DTOs, Program.cs with DI, and MIGRATION-NOTES.md. Build status: Success
After all steps are complete, the validation summary confirms every exit criterion, with evidence per criterion:
As a result, 36 files changed, 10 SOAP operations became 10 REST endpoints across 5 controllers, and the project builds clean on .NET 10 with the assembly name preserved:
PS C:\repos\...\eShopWCFService> dotnet build eShopWCFService.csproj eShopWCFService -> bin\Debug\net10.0\WcfService1.dll
Build succeeded. 0 Warning(s) 0 Error(s)
Step 4: Verify beyond the gate
A passing validation gate proves your criteria pass, nothing more. Two minutes of independent checks are worth running after any transformation. The following checks verify the defect classes that matter for this migration, no legacy namespaces or artifacts, no filtering after materialization, no sync-over-async, endpoint parity, identity keys, preserved decimal precision, and all pass:
Reading the generated code still surfaced one quirk the gate had no criterion for: the list endpoint accepts pageSize and pageIndex parameters that are never applied, no Skip/Take in the query. Behavior matches the legacy service, so nothing fails, but the API surface advertises pagination does not perform. The fix is the same discipline as before: feed it back into the definition as a new rule, and the next repository benefits.
Before and after: Code comparison
The following four examples show what AWS Transform custom produced.
Service contract to attribute-routed controllerWCF (ICatalogService.cs / CatalogService.svc.cs):
[ServiceContract]
public interface ICatalogService : IDisposable
{
[OperationContract]
CatalogItem FindCatalogItem(int id);
}
public CatalogItem FindCatalogItem(int id)
{
CatalogItem item = ents.CatalogItems.FirstOrDefault(x => x.Id == id);
if (item != null)
{
item.CatalogBrand = ents.CatalogBrands.FirstOrDefault(x => x.Id == item.CatalogBrandId);
item.CatalogType = ents.CatalogTypes.FirstOrDefault(x => x.Id == item.CatalogTypeId);
return item;
}
else
return null;
}
C# (CatalogItemsController.cs) converted by AWS Transform custom:
[ApiController]
[Route("api/catalog/items")]
public class CatalogItemsController : ControllerBase
{
private readonly ICatalogService _service;
public CatalogItemsController(ICatalogService service)
{
_service = service;
}
[HttpGet("{id}")]
public async Task<ActionResult<CatalogItemDto>> GetItem(int id)
{
var item = await _service.FindCatalogItemAsync(id);
if (item == null)
return NotFound();
return Ok(MapToDto(item));
}
}
Conversions: [ServiceContract] → [ApiController] with [Route], [OperationContract] → HTTP verb attributes, IDisposable removed (the DI container owns the DbContext lifetime), null return → NotFound(), manual navigation-property attachment → Include() in the service, entity → DTO to avoid serialization cycles.
In-memory filtering to SQL-translated query
WCF (CatalogService.svc.cs):
public List<CatalogItem> GetCatalogItems(int brandIdFilter, int typeIdFilter)
{
bool brandFilterIsNull = brandIdFilter == 0;
bool typeFilterIsNull = typeIdFilter == 0;
return ents.CatalogItems.ToList().Where(x =>
(brandFilterIsNull ? true : x.CatalogBrandId == brandIdFilter) &&
(typeFilterIsNull ? true : x.CatalogTypeId == typeIdFilter)).ToList();
}
C# (Services/CatalogService.cs):
public async Task<List<CatalogItem>> GetCatalogItemsAsync(int? brandIdFilter, int? typeIdFilter)
{
var query = _db.CatalogItems.AsNoTracking().AsQueryable();
if (brandIdFilter.HasValue && brandIdFilter.Value != 0)
{
query = query.Where(x => x.CatalogBrandId == brandIdFilter.Value);
}
if (typeIdFilter.HasValue && typeIdFilter.Value != 0)
{
query = query.Where(x => x.CatalogTypeId == typeIdFilter.Value);
}
return await query.ToListAsync();
}
Conversions: predicates composed before the materializing call so EF Core translates them to SQL, sentinel 0 → nullable parameters, .ToList() → await .ToListAsync(), AsNoTracking on the read path.
Date comparison EF Core can translate
WCF (CatalogService.svc.cs):
CatalogItemsStock s = ents.CatalogItemsStocks.Where(x => x.CatalogItemId == catalogItemId)
.ToList().Where(y => y.Date.Date == date.Date).FirstOrDefault();
if (s != null)
return s.AvailableStock;
else
return 0;
C# (Services/CatalogService.cs):
var dayStart = date.Date;
var dayEnd = dayStart.AddDays(1);
var stock = await _db.CatalogItemsStocks
.AsNoTracking()
.Where(x => x.CatalogItemId == catalogItemId && x.Date >= dayStart && x.Date < dayEnd)
.FirstOrDefaultAsync();
return stock?.AvailableStock ?? 0;
Conversions: DateTime.Date inside the predicate (untranslatable by EF Core) → half-open range on the raw column, computed into locals first.
Non-SDK project file to SDK-style
The 142-line non-SDK .csproj with packages.config became this, in its entirety:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>WcfService1</RootNamespace>
<AssemblyName>WcfService1</AssemblyName>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.*" PrivateAssets="all" />
</ItemGroup>
</Project>
Conversions: packages.config → PackageReference, EF 6.1.3 → EF Core 10, RootNamespace/AssemblyName preserved verbatim as required.
Complete conversion reference
The following table summarizes the WCF-to-Web-API conversions that AWS Transform custom handled in this migration:
| WCF / .NET Framework construct | ASP.NET Core equivalent |
[ServiceContract] interface |
[ApiController] classes with [Route] |
[OperationContract] method |
[HttpGet] / [HttpPost] / [HttpPut] / [HttpDelete] action |
.svc host file |
Kestrel + MapControllers() in Program.cs |
basicHttpBinding endpoint (Web.config) |
Attribute routing; no binding configuration |
mex metadata endpoint / WSDL |
OpenAPI-ready controllers |
[DataContract] / [DataMember] |
DTOs in Contracts\ + System.Text.Json |
Return null for absence |
NotFound() (404) |
void create / update / delete |
CreatedAtAction (201) / NoContent (204) / 404 on missing |
EF6 DbContext (self-constructed) |
EF Core DbContext via constructor injection, scoped |
CreateDatabaseIfNotExists initializer |
Database.Migrate() + seed guarded by existence check |
Max(Id) + 1 with DatabaseGeneratedOption.None |
Database identity columns |
.ToList().Where(...) |
Composed IQueryable + await ToListAsync() |
DateTime.Date comparisons |
Half-open range on the raw column |
| Synchronous data access | async/await end to end, AsNoTracking on reads |
IDisposable service + manual Dispose |
DI-managed lifetime |
Web.config connection strings |
appsettings.json + environment-variable fallback |
Next steps: From a migrated service to a modernized estate
A service on cross-platform .NET unlocks the infrastructure work that .NET Framework blocked. AWS Transform Full-Stack Windows Modernization provides AI-driven capabilities to:
- Port remaining .NET Framework applications to cross-platform .NET
- Modernize SQL Server databases to Amazon Aurora PostgreSQL
- Update UI frameworks from ASP.NET Web Forms to Blazor
- Deploy applications to Amazon EC2 Linux or Amazon ECS
This two-step approach, first WCF to Web API using AWS Transform custom, then the surrounding estate using Full-Stack Windows Modernization, provides a clear, incremental path to fully modernize legacy WCF-based systems. The transformation definition is reusable: once hardened, apply it across every WCF service in your organization.
Clean up
If you launched a Windows EC2 instance for this walkthrough, terminate it to avoid ongoing charges. AWS Transform custom does not create other resources during this process. Transformation definitions remain in your registry at no cost; delete one with atx custom def delete -n <name> if you no longer need it. Locally, the atx-result-staging-20260805_064143_27e9dc2c branch holds the result – merge or delete it, and the atx-baseline tag returns you to pristine input.
Conclusion
This post showed how to use AWS Transform custom natively on Windows with PowerShell to automate the migration of a .NET Framework WCF SOAP service to an ASP.NET Core Web API on .NET 10. The AI agent created a 10-step, 34-criterion transformation definition through a short authoring interview, converted 10 SOAP operations into ten REST endpoints across five controllers with zero build errors, and preserved the business logic and assembly identity.
Two practices made the difference between output that compiles and output that is correct:
- Writing exit criteria for the defect classes that survive compilation, status-code semantics, query translation, key generation and
- Reading the generated code rather than trusting a green gate.
Feed every finding back into the definition, and the next repository is cheaper than this one.
The sample source code used in this post is available at sample-aws-transform-custom-wcf-to-webapi. To get started with AWS Transform custom, visit the AWS Transform getting started guide to learn more about AWS Transform custom.