Concepts
What you will learn: Work with a legacy system or external partner without letting its protocol shape the domain. Follow the path from reading a contract to consuming a service, isolating it and migrating pieces while preserving behavior.
Before you start: It is enough to understand request/response and that a service owns data or an operation. is a message protocol with a contract; describes operations and data types; is a structured text format used to carry them. If these terms are new, begin with the first two cards before comparing migration paths.
Select a term with a dotted underline for its meaning, then continue reading from the same place.
#SOAP, WSDL and XML: start with the contract
A modern store may still need an older invoicing service. is an messaging protocol, not simply any XML sent over . Messages contain an Envelope, an optional Header, and a Body holding an operation or Fault. describes operations, messages, bindings and endpoints; defines data types.
<!-- (1) SOAP 1.1 envelope namespace -->
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<!-- (2) Operation namespace belongs to the service contract -->
<GetInvoice xmlns="urn:store:invoices">
<invoiceId>42</invoiceId>
</GetInvoice>
</s:Body>
</s:Envelope>
- (1) This is SOAP 1.1's namespace; SOAP 1.2 uses a different namespace and HTTP content type.
- (2) Both the operation and invoiceId are in the service namespace. Prefix spelling is arbitrary; the matters.
Choose a binding matching the WSDL. SOAP is not restricted to HTTP, and WS-Security is distinct from . Preserve an existing contract when consumers depend on it; and are not wire-compatible replacements.
#Consuming SOAP from .NET 10
Generate a proxy from a trusted using dotnet-svcutil or Visual Studio's Web Service Reference instead of assembling manually. The tool adds suitable System.ServiceModel packages, such as System.ServiceModel.Http and System.ServiceModel.Primitives. These provide client support, not the original WCF server.
dotnet tool install --global dotnet-svcutil
# (1) Local, reviewed WSDL and its imported schemas
dotnet-svcutil ./contracts/invoices.wsdl
dotnet restore
using System.ServiceModel;
// (2) Generated names depend on your WSDL.
var binding = new BasicHttpsBinding
{
SendTimeout = TimeSpan.FromSeconds(15),
MaxReceivedMessageSize = 1_000_000
};
var endpoint = new EndpointAddress("https://partner.example/Invoices.svc");
var client = new InvoiceServiceClient(binding, endpoint);
try
{
var result = await client.GetInvoiceAsync(42); // (3)
client.Close();
}
catch
{
client.Abort(); // (4)
throw;
}
- (1) Version the contract and schemas, and review generated-code changes when refreshing them.
- (2) This assumes a generated InvoiceServiceClient with an int operation argument; adapt both to Reference.cs. BasicHttpsBinding suits 1.1 over , not every binding.
- (3) The call is asynchronous, but generated methods may not accept CancellationToken. SendTimeout bounds the operation here; verify authentication, security and quotas against the partner.
- (4) If the call or Close fails, Abort cleans up the channel. Do not reuse faulted clients or share them concurrently without a documented guarantee.
#Parsing external XML safelyBonus
Optional but valuable: not asked in every interview, but knowing it deepens your understanding and sets you apart.
An payload can attempt external-entity resolution or consume excessive memory. When parsing XML yourself, prohibit DTDs and bound the input size. Use a parser or serializer rather than string concatenation for escaping and namespaces.
using System.Xml;
using System.Xml.Linq;
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit, // (1)
XmlResolver = null,
MaxCharactersInDocument = 1_000_000 // (2)
};
using var reader = XmlReader.Create(inputStream, settings);
var document = XDocument.Load(reader); // (3)
- (1) DTDs are rejected, and the resolver cannot open external files or URLs.
- (2) Choose a contract-appropriate limit alongside transport and decompression limits.
- (3) XDocument loads the document into memory; use streaming XmlReader for large files. Parsing does not replace or business validation.
#An anti-corruption layer at the boundary
- A legacy system returns abbreviated fields and codes unrelated to your application's model. Spreading them everywhere couples many layers to its changes.
- An anti-corruption layer validates and translates at the boundary, for example mapping legacy status codes to your own order states.
- Specify unknown values and temporary failures rather than converting every error into a successful default. Protect model meaning, not merely property names.
If a partner calls a status X9, that should not become a domain concept everywhere. An adapter translates interfaces; an anti-corruption layer also translates meaning: currencies, quantity units, time zones and failures.
public record InvoiceStatus(bool Paid);
public record LegacyInvoice(string StateCode);
public interface ILegacyInvoices
{
Task<LegacyInvoice> FetchAsync(int id, CancellationToken ct);
}
public interface IInvoiceStatus
{
Task<InvoiceStatus> GetAsync(int id, CancellationToken ct);
}
public sealed class InvoiceAdapter(ILegacyInvoices legacy) : IInvoiceStatus
{
public async Task<InvoiceStatus> GetAsync(int id, CancellationToken ct)
{
var dto = await legacy.FetchAsync(id, ct); // (1)
return dto.StateCode switch // (2)
{
"X9" => new(true),
"N0" => new(false),
_ => throw new InvalidDataException("Unknown invoice state")
};
}
}
- (1) ILegacyInvoices is the transport-wrapper seam; its implementation knows the actual proxy and cancellation capabilities.
- (2) Map known values into our contract and reject unknown states rather than silently treating them as unpaid. Consumers depend only on IInvoiceStatus.
Cover mapping with contract tests and log correlation IDs without sensitive payloads. This boundary adds code; avoid layers of forwarding when models already match and the integration is simple.
#Moving a WCF server: CoreWCF or gRPC?
Changing the target framework does not bring the original server to 10. If existing clients must remain, evaluate CoreWCF and test required bindings, security and behaviors. If all consumers can change, is an option for a new contract, requiring regenerated clients and a different wire protocol.
| Older approach | Modern path | Cost |
|---|---|---|
| WCF SOAP server | CoreWCF with a compatible contract where possible | Verify feature support; not a universal drop-in |
| Internal contract | gRPC and Protobuf | Client, hosting and observability changes |
| SOAP consumer | System.ServiceModel client packages | Compatibility depends on the required binding |
Prototype one operation with the hardest security requirement first. Separate runtime migration from wholesale contract redesign. A SOAP facade can remain over a new implementation while clients migrate.
#Strangler fig with YARP: migrate route by route
A complete rewrite delays feedback and concentrates risk. Put one entry point in front of old and new systems, then move complete features gradually. Here serves migrated routes locally, and forwards everything else to the legacy application.
// Package: Yarp.ReverseProxy; configuration shown below.
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); // (1)
var app = builder.Build();
app.MapGet("/products/{id:int}", (int id) => new { Id = id }); // (2)
app.MapReverseProxy(); // (3)
app.Run();
{
"ReverseProxy": {
"Routes": {
"fallback": {
"ClusterId": "legacy",
"Order": 1000,
"Match": { "Path": "{**catch-all}" }
}
},
"Clusters": {
"legacy": {
"Destinations": {
"primary": { "Address": "https://legacy.example/" }
}
}
}
}
}
- (1) Routes and destinations come from configuration.
- (2) This illustrative migrated route would call an application service in a real application.
- (3) A lower-priority fallback forwards remaining requests. Endpoint selection uses route matching and Order, not simply source-code order.
Define data write ownership, compatible schemas, cookies, sessions, authentication and rollback. Shadow reads can compare behavior; shadow writes must not charge twice. Monitor remaining legacy traffic and define retirement criteria.
Source: Incremental ASP.NET migration.
#A verifiable .NET Framework to .NET 10 migration
- Inventory dependencies, Windows-specific and integration contracts; baseline failures and performance.
- Capture current behavior with characterization tests, and choose a valuable small slice with rollback.
- Upgrade leaf libraries required by that slice first.
netstandard2.0bridges shared libraries to Framework; it is not a runtime. Framework 4.7.2 or later is preferable in practice to avoid older compatibility problems. Framework does not supportnetstandard2.1. - Replace hosting and configuration boundaries, then validate end-to-end behavior, deployment and load before shifting traffic.
| Older approach | Modern path | Verify |
|---|---|---|
| System.Web | , HttpContext and | Request lifecycle, sessions and authentication; temporary adapters where needed |
| EF6 | when justified | translation, tracking and migrations; not a package rename |
| web.config application settings | appsettings.json, environment variables and Options | Keep secrets outside files; web.config may remain for hosting |
| Framework-only library | netstandard2.0 or multi-targeting | All dependencies must be compatible; new application code targets net10.0 |
#Characterization tests and seams
Before refactoring, establish what the code actually does, including surprising behavior. A characterization test captures current behavior; a seam lets you substitute a dependency such as a clock or without changing the business logic.
// TimeProvider is the seam; this example captures an existing date rule.
public sealed class InvoiceDates(TimeProvider clock)
{
public DateOnly Today() =>
DateOnly.FromDateTime(clock.GetUtcNow().UtcDateTime); // (1)
}
public sealed class FixedClock : TimeProvider
{
public override DateTimeOffset GetUtcNow() =>
new(2026, 10, 22, 0, 0, 0, TimeSpan.Zero);
}
[Fact]
public void PreservesUtcInvoiceDate()
{
var dates = new InvoiceDates(new FixedClock()); // (2)
Assert.Equal(new DateOnly(2026, 10, 22), dates.Today());
}
- (1) Preserve the current rule rather than changing to local time during refactoring.
- (2) The xUnit test uses a fixed clock, avoiding midnight failures. For legacy code, compare actual output before and after inserting the seam.
For golden masters, normalize only nondeterministic fields such as generated IDs, never meaningful differences. Record incorrect behavior explicitly and fix it as a separate agreed change; characterization does not permanently endorse a bug.
#Reliable integration: outbox, idempotency and reconciliation
Saving an order and sending a message are separate operations: the process can crash between them. Store the order and an row in the same local , then dispatch later. Delivery can repeat if the process crashes after sending but before marking success.
(1) Transaction: save Order + Outbox(MessageId, OrderId, Version, Payload)
(2) Dispatcher: lease pending row, send with stable MessageId, record success
(3) Receiver transaction: unique Inbox(MessageId) + apply business update
(4) Reconciliation: compare by business key, version, amount and currency
- (1) Both writes commit or roll back in one database transaction.
- (2) A lease reduces worker contention but cannot prevent duplicates; apply bounded backoff and jitter only to transient failures.
- (3) A unique constraint and the effect in one transaction prevent duplicate application; external payments need the same provider key.
- (4) Compare actual state and safely repair or alert. Preserve ordering per order where required, not an unnecessary global order.
An outbox provides neither exactly-once delivery nor a distributed transaction. If the partner cannot deduplicate, a timeout is an unknown outcome: query status or reconcile rather than retry blindly. Monitor oldest-message age, retries and stuck messages.
#CSV, SFTP and batch processing
Not every partner exposes an . Agree a file contract covering encoding, delimiters, quoted fields, version, time zone, business keys, row counts and checksums. transfers over ; it differs from FTPS, which uses .
- Verify the SSH host key, upload under a temporary name, then use an agreed rename or completion marker.
- Register the file ID and checksum, stage and validate records, and quarantine rejected rows with reasons and line numbers.
- Apply small batches with unique business keys and durable checkpoints. Record progress after commit; a crash before checkpointing requires safe replay.
- Return accepted and rejected counts and reconcile totals. Specify partial acceptance versus whole-file atomicity.
File: invoices-20261022-v1.csv
FileId: stable partner batch identifier
Staging: FileId + RowNumber + BusinessKey + ValidationStatus
Checkpoint: FileId + LastCommittedBatch
Result: accepted count + rejected count + totals by currency
Do not treat Split(',') as a general parser: quoted fields can contain commas and newlines. Streaming limits loaded rows, but a HashSet of every key may still exhaust memory. Use database staging or external sorting when needed.
BackgroundService hosts a worker but is not a durable scheduler by itself. Hangfire, Quartz.NET or an external scheduler can manage schedules according to their configuration, but jobs still need . Define overlap protection across instances, misfires, daylight saving behavior, cancellation, retries and missed-run alerts. Do not launch critical jobs with Task.Run inside requests.
Interview questions
No results
Check your reasoning
Why does this choice fit? When would it fail? What changes under higher load or a dependency failure?
26.1What are SOAP, WSDL and XSD?Junior
I separate the message protocol from its description. defines an envelope with a body, optional headers and faults. describes the service operations, message contracts, bindings and endpoints, while defines the XML types those messages use. I usually generate a typed client from the WSDL rather than build envelopes manually. Before integrating, I check the SOAP version, binding and security requirements because a generated proxy alone does not guarantee interoperability.
26.2Why do XML namespaces matter?Junior
I treat an element's identity as its namespace plus local name. Two elements called Invoice can represent different contracts if their namespace URIs differ. Conversely, changing a prefix from a to b does not change meaning when both map to the same URI. When debugging , I compare qualified names and schemas rather than just the visible tag spelling. I use XML or generated serializers so namespace handling and escaping are explicit.
26.3How do you consume a SOAP service in .NET 10?Mid
I generate a proxy from a trusted using dotnet-svcutil or Visual Studio's Web Service Reference. The generated code uses System.ServiceModel client packages. I inspect the generated operations and configure the appropriate binding, endpoint, authentication, timeouts and message limits. I keep those types behind an adapter implementing our own interface. I also test faults and timeout behavior, and close healthy channels or abort failed ones instead of reusing a faulted client.
26.4Does modern .NET support WCF servers?Mid
I distinguish client packages from the original WCF server runtime. Modern can consume supported contracts with System.ServiceModel clients, but it does not host the original Framework WCF server. I evaluate CoreWCF when existing SOAP clients must remain compatible, checking bindings, security and behaviors individually. If we control all consumers, I may design a contract instead. That changes the wire protocol and clients, so it is a separate migration decision.
26.5What is an anti-corruption layer?Mid
I use an anti-corruption layer to keep a legacy or partner model from spreading into our domain. It contains adapters and translation rules for data, behavior and failures. For example, an external invoice status code becomes our own meaningful status, and unknown codes are handled explicitly. This gives consumers a stable interface and keeps partner-specific changes localized. I test the mapping against real contract examples, while avoiding unnecessary abstraction when the integration is already simple.
26.6What is the strangler fig pattern?Mid
I migrate one capability at a time while old and new implementations coexist behind a routing boundary. A proxy sends migrated routes to the new application and leaves the rest on the legacy system. Each slice has tests, observability and a rollback plan. I also define data ownership and cross-cutting concerns such as authentication before moving traffic. The pattern reduces the size of each change, but coexistence costs money and complexity, so I track explicit legacy retirement criteria.
26.7What would your .NET Framework migration plan look like?Senior
I start with dependency and platform assessment, production baselines and characterization tests. Then I choose a small business slice and upgrade its leaf libraries, using netstandard2.0 or multi-targeting only where sharing with Framework is needed. I replace System.Web boundaries, evaluate hosting, and validate query behavior and configuration changes separately. I use migration tooling for mechanical edits, review its output, and move traffic gradually with rollback. A successful build is only the beginning of validation.
26.8What is a characterization test?Junior
I write characterization tests to capture what existing code currently does before changing its structure. They are useful when requirements are incomplete or behavior has evolved over years. I select representative inputs, record outputs and side effects, and make unstable values deterministic where possible. The tests may preserve a known defect temporarily, so I label that explicitly and change it in a separate agreed fix. Their purpose is to expose accidental behavior changes during modernization.
26.9What is a seam in legacy code?Mid
I think of a seam as a place where I can change a dependency's behavior without rewriting the business logic being tested. Constructor injection for a clock, a file reader or a payment client is a common example. I introduce the smallest useful seam, preserve behavior with characterization tests, and supply a deterministic test implementation. I avoid a broad redesign just to add one test, because that increases the unverified change before the safety net exists.
26.10Why is migrating EF6 to EF Core more than renaming packages?Senior
I treat as a different implementation rather than an EF6 package upgrade. Query translation, relationship conventions, loading, tracking and migrations can differ, so code that compiles may behave differently or issue more queries. I compare representative results, generated and performance against a real database. I also plan evolution independently so both application versions remain compatible. Depending on dependencies, I may postpone the ORM migration to avoid combining too many risks.
26.11How do you prevent lost integration messages?Mid
I save the business change and an record in the same local database . A dispatcher reads pending records, sends them with a stable message identifier and marks them complete after acknowledgment. If it crashes after sending, the record can be sent again, so the receiver must deduplicate and apply its effect atomically where possible. I monitor backlog age and failed attempts, and keep a repair path for messages that cannot progress automatically.
26.12A payment call timed out. Should you retry it?Senior
I do not interpret a timeout as proof that the payment failed. The provider may have completed the charge before the response was lost. I reuse the original key if the provider supports it and understands its retention rules, or query the payment by a stable business reference. Without either capability, I record an unknown outcome and reconcile before resending. A generic retry policy can otherwise turn a temporary network problem into a duplicate charge.
26.13Why do integrations need reconciliation if they already use an outbox?Senior
I use an to protect local event recording, but it cannot prove that both systems represent the same final business state. Mapping defects, manual edits, rejected records and partner-side failures can still create differences. Reconciliation compares stable business keys, versions, statuses and monetary totals after allowing for expected delay. It classifies discrepancies and either repairs them idempotently or raises an actionable exception. I audit repairs so reconciliation does not become an invisible source of new changes.
26.14What is the difference between SFTP and FTPS?Junior
I check the actual protocol rather than treating secure file transfer as one interchangeable option. is a file-transfer protocol over , while FTPS is FTP secured with . They use different clients and server configurations. For SFTP I verify the server's host key and use managed credentials or keys. Encryption protects the connection, but I still need file completeness checks, duplicate detection and acknowledgments to know whether the business import succeeded.
26.15How would you import a large CSV safely?Mid
I agree the , encoding, quoting rules and file completion protocol first. I stream records through a proper parser into staging, validate them, and apply bounded batches with stable business keys. I track file identity and durable progress so replay after a crash is safe. Invalid records go to a quarantine with clear reasons. Finally I return accepted and rejected counts and reconcile totals, with an explicit policy for partial acceptance versus rejecting the entire file.
26.16Is BackgroundService enough for reliable scheduled jobs?Mid
I use BackgroundService as a hosting mechanism, not as a guarantee of durable scheduling. In-memory timers and progress disappear on restart, and multiple application replicas may start the same job. Depending on the requirement, I use a scheduler or durable job store and define overlap protection, retry policy, missed-run behavior and time-zone rules. The job itself remains and checkpointed, because even a durable scheduler can deliver work more than once after failures.
26.17How would you roll back a partially migrated feature?Senior
I design rollback before routing production traffic. The old application must still understand data written by the new version, so I use compatible changes and delay destructive cleanup. I define one writer for each business operation and verify authentication and session behavior through the proxy. If metrics or business checks regress, I can route the slice back while preserving completed writes. For irreversible changes, I need a recovery or compensation plan rather than pretending a route switch reverses them.
26.18How do you handle ordering and poison messages?Mid
I first identify which operations actually require ordering, usually changes for the same order rather than every message globally. I use an entity key and version to detect stale updates or gaps, and define whether a failure blocks later changes for that entity. Transient failures get bounded retries; invalid payloads or mapping failures are quarantined with diagnostics and an owner. After fixing the cause, replay uses the original identifiers so recovery does not create duplicate business effects.
Test yourself
Quiz answers and completed-topic marks are saved in this browser; they do not sync across devices.
What does WSDL primarily describe?
Which statement about .NET 10 and WCF is correct?
A payment request times out. What do you know?
What belongs inside an anti-corruption layer?
An outbox dispatcher crashes after sending but before marking success. What is likely?
Which library target can bridge suitable Framework and .NET 10 code?
What is a characterization test's immediate purpose?
What indicates a CSV file is ready for import?
What must be considered before routing a migrated feature back to the old app?
What does streaming CSV processing guarantee by itself?