Ideas Engineered for Tomorrow
We Engineer Services & Solutions for Your Business Needs
Consulting Services Hire Book Consulting

Hire .NET Developers in India

Before you hire .NET developers in India, settle one question: which .NET do you have? A Framework 4.x application pinned to Windows Server and a modern cross-platform .NET service are different jobs, different hosting and frequently different people. Most buyers find out which they own only after somebody looks.

See the Screening Questions

The word ".NET" has been carrying two quite different meanings for years now, and job adverts rarely say which one they mean. On one side there is the original Windows-only Framework, running behind IIS on a server somebody racked a long time ago, holding a warehouse system or a claims portal or a broker back office together. On the other there is the rebuilt, cross-platform runtime that ships on its own release cadence and runs perfectly happily in a Linux container. A CV that says "8 years .NET" can mean either. So can a brief.

That ambiguity is the reason so many .NET hires disappoint. You interview somebody who is genuinely good, they arrive, and within a fortnight it becomes clear they have never opened a Global.asax file or never containerised anything, depending on which way the gap runs. This page is written to close that gap before you spend money. It covers how to read the age of your own codebase, the handful of runtime behaviours that separate a competent .NET engineer from a confident one, and the migration question that sits underneath most .NET hiring briefs whether or not anyone has said it out loud.

Which .NET Do You Actually Have?

You do not need a developer to answer this. You need about twenty minutes with a file browser and somebody who knows where to look. Here is what to look at, in the order that gives you the answer fastest.

Open one project file and read the top of it

Every C# project has a .csproj file. If it opens with a single short line naming an SDK, and a few lines below that a TargetFramework or TargetFrameworks entry, you are on the modern tooling. Those files are usually under fifty lines because the build system infers the rest. If instead the file runs to several hundred lines, lists every single .cs file in the project one by one, contains a ProjectTypeGuids block of long hexadecimal identifiers and imports a targets file from a Program Files path, that is the old format and almost certainly Framework.

The TargetFramework value itself is the clincher. A moniker beginning net4 is .NET Framework. A moniker that is net followed by a small number, with no 4 at the front, is the modern runtime. There is a middle case worth knowing about: netstandard, which is a portability contract rather than a runtime, used by libraries that need to be consumable from both worlds. A solution full of netstandard libraries usually means somebody already started thinking about this.

Look for packages.config

If your project directory contains a packages.config file and a packages folder sitting next to the solution, your dependency management predates the current approach. The modern equivalent is a set of PackageReference lines inside the csproj itself, with packages resolved into a shared cache rather than copied into the repository. The practical difference is transitive dependencies: PackageReference works out the full graph for you, packages.config makes it your problem, which is why old solutions accumulate binding redirect blocks in web.config that nobody dares touch.

Converting packages.config to PackageReference is generally an early step in any migration, and it is one of the few steps that is mostly mechanical. It is also a decent first task to hand a candidate you are trialling, because doing it carelessly breaks the build in ways that show up only at runtime.

Work out what kind of web application it is

Files ending .aspx, each with a .aspx.cs and a .aspx.designer.cs beside it, mean Web Forms. Markup with runat="server" attributes and a hidden field carrying view state confirms it. Web Forms has no port to the modern runtime, which is the single most consequential thing you can discover about your own codebase, so find out now rather than three months into a migration plan.

A Global.asax file, an App_Start folder with RouteConfig and BundleConfig inside it, and controllers inheriting from a Controller type in the System.Web namespace mean MVC 5 or Web API 2. Those port with contained effort. A Program.cs that builds and runs a host, an appsettings.json, and a wwwroot folder for static files mean you are already on ASP.NET Core and this whole conversation is about something else.

Read the configuration and the deployment story

A web.config with connection strings, appSettings and a system.webServer section is Framework. Configuration in appsettings.json layered with environment variables and user secrets is modern. Look at how the thing is deployed, too. A publish profile that pushes to an IIS site, a scheduled task on the same box, and a stored procedure heavy SQL Server database is a very different operational world from a Dockerfile and a pipeline that pushes an image to a registry.

Also check what else is in the solution. A WCF service project, anything referencing .NET Remoting, code that creates AppDomains, or a Windows service built on the old ServiceBase pattern all carry migration weight far beyond their line count. So does anything that touches the registry, COM interop, System.Drawing against GDI, or Windows-only cryptography APIs.

What the answer changes about who you should hire

If you are wholly on Framework and staying there for now, hire for maintenance temperament: somebody who reads before they change, who is comfortable in a codebase with no tests, who can debug through a decompiled assembly when the source for a dependency is long gone. Modern .NET fluency is a bonus rather than a requirement.

If you are wholly on modern .NET, hire for the current toolchain: minimal APIs or controllers, the built-in dependency injection container, structured logging, health checks, containers, and whatever cloud you run on. If you are somewhere in between, which is the common case, you want somebody who has actually done a migration rather than read about one, and you should expect to pay attention to that experience specifically rather than to years-of-.NET on a CV.

What the Job Looks Like in a Normal Week

Hiring pages tend to describe .NET work as though it were architecture all day. Here is a more honest picture of where the hours go on the engagements we actually staff.

Changing behaviour inside a system with strong opinions

Business applications built on .NET tend to be layered, and the layers tend to have been imposed by someone who has since left. A request to add a field arrives, and satisfying it means touching an entity, a configuration class, a migration, a data transfer object, a mapping profile, a validator, a view model and a view. None of that is difficult. All of it is easy to get subtly wrong, and the failure mode is a nullable column somebody forgot to backfill that produces a support ticket eleven days later.

Reading generated SQL

A .NET developer who cannot read the SQL their ORM produces will eventually ship something that works fine on a developer machine with two hundred rows and falls over on a table with four million. Watching the query log during development, rather than during the incident, is one of the clearest signals of experience we look for. It costs almost nothing to turn on and it catches the majority of ORM mistakes before they leave the branch.

Debugging things that only happen in production

Intermittent timeouts, a scheduled job that occasionally runs twice, memory that climbs across the working day, a request that succeeds nine times and fails the tenth. Work like this is not solved by reading the code harder. It is solved by knowing which counters to watch, how to capture a dump from a process that is misbehaving without killing it, and how to read that dump afterwards. That skill is uncommon and it is the one worth interviewing for hardest.

Being the person who answers when the estate is old

On Framework engagements, the engineer often ends up being the institutional memory: why that application pool recycles at 03:00, which stored procedure the finance report actually calls, what the third-party DLL with no source does. That role is undervalued when hiring and painfully missed when the person leaves. Write down what they work out, and make writing it down part of the job from week one.

What Happens When You Call .Result on a Task?

This is the first question we ask, and it sorts candidates faster than anything else on the list. Almost every .NET CV claims async experience. Very few people can explain what goes wrong when it is used badly, and the failure is common enough that most production .NET applications contain at least one instance of it.

The classic ASP.NET deadlock

In a classic ASP.NET request there is a synchronisation context that permits one thread inside it at a time. Suppose your controller action calls an async method and then blocks on the returned Task, using .Result or .Wait or GetAwaiter().GetResult(). The awaited work completes on a thread pool thread, and its continuation then tries to resume on the original context, because that is what an await does by default. The context is still occupied by the thread that is blocking on the result. The continuation waits for the context. The thread waits for the continuation. Neither ever moves. The request hangs until it times out, and because the blocked thread stays out of circulation, a handful of concurrent hits can take out the whole site.

The tell that this is happening is a site that is fine under light load and locks solid under moderate load, with no CPU usage and no error in the log. Teams often misdiagnose it as a database problem because the symptom points at whatever the async call was doing.

Why the same code behaves differently on ASP.NET Core

ASP.NET Core removed the request synchronisation context, so this exact deadlock does not occur. Candidates who know that much often stop there, and it is the wrong place to stop. Blocking still consumes a thread pool thread for the whole duration of the call. Under load you exhaust the pool, and the runtime replaces threads deliberately slowly rather than all at once, so the application enters a state where latency rises steeply while CPU sits near idle. That combination of high queueing and low CPU is the fingerprint. A candidate who names it without prompting has debugged it before.

ConfigureAwait, and where it still earns its place

The advice to append ConfigureAwait(false) to every await inside library code exists because library code does not know whose context it is running on. In an ASP.NET Core application it makes little practical difference. In a library that might be called from a desktop application, an older web application, or anything else with a context, it prevents the whole class of problem above. What we listen for is whether the candidate applies it as a rule they were told or as a decision they understand.

async void and the exception that disappears

A method declared async void cannot be awaited, so nothing can observe its completion, and an exception thrown inside it does not travel back to any caller. It is raised on whatever context happens to be current, which in a server process typically means the process falls over or, worse, the failure is swallowed and the work silently never happened. The only defensible use is an event handler. Anything else should return Task. We grep for async void in any code sample a candidate sends us.

Dependency Injection, Lifetimes, and the Captive Dependency

Modern .NET ships with a container in the box, which means every developer uses one and rather fewer understand what they have configured. Three lifetimes exist and choosing between them is not a style preference.

The three lifetimes, and what each one commits you to

Transient gives you a new instance every time something asks for one. Scoped gives you one instance per scope, which in a web application means one per HTTP request. Singleton gives you one instance for the life of the process. The interesting consequence is that a singleton is shared across every concurrent request, so any mutable field on it is shared state, and any code inside it that assumed single-threaded access is a bug waiting for traffic. A candidate who has been bitten by this will mention thread safety before you do.

The captive dependency

Register a service as a singleton, give it a constructor parameter that is registered as scoped, and the container hands it a scoped instance at first construction and then keeps that same instance alive forever. The scoped service has been captured. If it is a database context, you now have one context shared across every request for the lifetime of the process, accumulating tracked entities and eventually throwing threading exceptions that make no sense in isolation.

The framework can catch this for you. The default host builder turns on scope validation in the development environment, which makes the application fail at startup with a message naming the offending pair rather than misbehaving quietly in production. Asking a candidate whether they run with that validation on, and what it caught last time, is a short question with a lot of signal in it.

Database contexts inside background work

The standard registration for a database context is scoped, which is correct for request handling and useless inside a hosted background service, because that service is a singleton and there is no ambient request scope. The right move is to inject the scope factory, create a scope per unit of work, resolve the context inside it and dispose it when the unit finishes. Injecting the context directly into a background service compiles, runs, and then produces one of the least pleasant bug reports in .NET: intermittent failures under concurrency, hours after deployment.

The service locator smell

If you see the service provider itself being injected and then queried at call sites, dependencies have stopped being visible in constructors. The class now depends on things nothing declares, tests need a whole container to run, and nobody can tell what a change breaks. There are legitimate uses, mostly at composition boundaries, but a codebase where it is the normal pattern is a codebase where nobody has been made responsible for the design.

Entity Framework Is Where the Performance Went

When a .NET application is slow and nobody has changed the infrastructure, the odds strongly favour the data access layer. Entity Framework is a good tool that makes it very easy to write something expensive without noticing. These are the specific traps we screen for.

Tracking, and the cost of reads you will never save

By default the context tracks every entity it materialises so it can work out what changed when you save. On a read-only path that bookkeeping buys you nothing and costs both memory and time, and the cost grows with the number of rows. Calling AsNoTracking on queries that only feed a screen or a report is one of the cheapest improvements available, and a candidate who reaches for it unprompted has read a profiler at some point.

The reverse mistake also exists: disabling tracking on a path that then tries to update entities, and wondering why nothing persists. Both mistakes come from treating tracking as a setting rather than as a decision about what that query is for.

Include, and the query that multiplies rows

Ask for a parent and one collection of children in a single query and you get a join, which repeats the parent columns once per child. Ask for two collections and the two child sets multiply against each other. A hundred orders with fifteen lines and eight status events each does not return a hundred and something rows, it returns twelve thousand, each carrying the full order record, and the runtime then has to deduplicate all of that back into an object graph. This is why a screen gets dramatically slower after an apparently harmless Include was added to fix a null reference.

There are two fixes and they are not interchangeable. Splitting the query issues separate statements per collection, which avoids the multiplication at the cost of more round trips and a small consistency caveat if the data changes between them. Projecting into a purpose-built type, selecting only the fields the screen actually renders, is usually better still because it stops loading entities you were never going to display. Knowing when each applies is the skill.

Lazy loading and the N+1

Lazy loading turns a property access into a database round trip. Loop over five hundred parents, touch a navigation property inside the loop, and you have quietly issued five hundred and one queries. In the older Framework-era Entity Framework this was on by default with virtual navigation properties, which is why so many legacy applications have this problem baked in. In EF Core it is opt-in and needs a proxies package, which is an improvement, but plenty of teams switch it on to make a migration compile and then leave it.

The diagnostic is not subtle once you look: turn on command logging in a development environment, load the slow page, and count the statements. Any candidate who says "check how many queries the page issues" as their first move is worth talking to further.

Migrations when more than one person is working

Migrations are fine on a single branch and awkward on several. Two developers each generate a migration from the same starting point, both regenerate the model snapshot file, and the merge conflicts in a generated file that no one should hand-edit. The workable habits are small migrations merged quickly, a convention that the snapshot conflict is resolved by regenerating rather than patching, and a rule that migrations are applied by the deployment pipeline rather than by the application at startup. The startup-migration pattern looks convenient until two instances start at once.

Where the two Entity Frameworks differ enough to hurt

If you are migrating, do not assume the data layer comes across mechanically. Behaviour around client-side evaluation changed: the older version would silently pull data into memory to finish evaluating an expression it could not translate, while EF Core refuses and throws instead. That is better in the long run and it means a migration surfaces every query that was quietly doing something expensive, all at once, usually in the week you least want them. Budget for it rather than being surprised by it.

LINQ That Runs in SQL Against LINQ That Runs in Your Process

The two look identical in the editor. The difference is one interface, and the consequences run into gigabytes.

The interface decides where the work happens

A query typed as IQueryable is an expression tree that the provider turns into SQL when you enumerate it. A query typed as IEnumerable is ordinary code running in your process against whatever is already in memory. Add a ToList, an AsEnumerable or a call that returns the wrong type in the middle of a chain, and everything after that point stops being translated. The filter you wrote still filters, so the results are right, which is exactly why nobody notices. What changed is that the table came over the wire first.

A well-worn version of this hides behind a repository method that returns a list rather than a queryable. The caller adds a Where on top, the code reads beautifully, and the database is being asked for everything every time. When we review a candidate's code sample, repository signatures are one of the first things we look at.

Expressions the provider cannot translate

Call your own helper method inside a Where clause and the provider has no idea what it means. Modern versions throw a clear exception rather than silently falling back, which is the behaviour you want, but it means code that ran on the old stack stops running on the new one. The fix is usually to express the condition in terms the provider understands, or to move it into a computed column or a database function. String comparison rules, culture-sensitive casing and date arithmetic are the recurring offenders.

How we test for it in an interview

We hand over a small repository class and a controller that uses it, containing one method that returns a list where it should return a queryable and one query with a helper method inside the predicate. We do not say anything is wrong. Candidates who have run into this in production spot the return type within a minute and explain the consequence in terms of rows crossing the network. Candidates who have not tend to comment on naming.

Why Does Memory Climb All Day on a .NET Service?

Long-running .NET services that need a nightly restart are common enough that some teams treat the restart as the fix. It is worth understanding what is actually happening, because the diagnosis is usually a couple of hours of work and the cause is usually one line.

First, decide whether it is a leak at all

Managed memory is collected generationally: short-lived objects die cheaply in generation zero, survivors get promoted, and generation two collections are comparatively rare and expensive. A process that has grown to a large working set and stayed there is not necessarily leaking. It may simply have never been under enough pressure to give memory back to the operating system. The number in the task manager is a poor signal on its own.

The right first step is to watch counters over time: allocation rate, the count of collections per generation, and the size of the heap after a generation two collection. If the post-collection heap keeps rising across the day, objects are being kept alive by something. That is a leak, and it is a reference problem rather than a memory management problem, because managed code cannot leak in the C sense.

The four causes that account for most of them

A static collection used as a cache, with nothing ever removing entries. An event on a long-lived publisher that short-lived subscribers attach to and never detach from, so the publisher holds every subscriber that ever existed. A timer or a background task capturing a large object graph in a closure. And an in-memory cache configured without any size limit or expiry, which is the same problem wearing a library's clothes. In each case the object is perfectly reachable, so the collector is correct not to free it.

Objects that hold something other than memory

Disposal is a separate discipline and it fails in both directions. Not disposing things that wrap file handles, sockets or database connections exhausts a resource that has nothing to do with the heap, and the symptom is a pool timeout rather than a memory graph. Meanwhile the most famous .NET example of the opposite mistake is creating a new HTTP client per call and disposing it, which leaves sockets waiting to close and eventually exhausts the ports on the machine. The modern answer is a client factory that pools and rotates handlers, and a candidate who explains why a single static client also has a problem, because it never notices a DNS change, is telling you they have run this in anger.

What we ask a candidate to do first

Given a service whose memory climbs all day, the answer we want is: capture the state now, capture it again after the climb, and compare which types grew. Not raise the memory limit. Not restart it nightly and move on. The command-line diagnostic tools that ship with the SDK make this straightforward on Linux and Windows alike, and on older Framework estates the same job is done with a full process dump and a debugger extension. Either way the process is snapshot, snapshot, diff, and then find who is holding the reference.

Where It Runs: IIS, Kestrel, Containers and App Service

Hosting is where the Framework question stops being academic. It also decides how much of your migration is code and how much is operations, which is usually the part that overruns.

IIS, and the behaviours it imposes

A Framework application under IIS inherits a set of behaviours that people forget are configurable: application pools that recycle on a schedule and on memory thresholds, an idle timeout that shuts the worker process down when nobody has visited, and a start-up cost on the first request afterwards. Teams routinely blame the application for a slow first hit in the morning when the pool settings are doing exactly what they were told. Modern .NET can also be hosted under IIS, either with the runtime loaded inside the IIS worker process or with IIS acting as a proxy in front of the application's own server. Knowing which mode a deployment uses matters, because they differ in how configuration, logging and process lifetime behave.

Kestrel, and what sits in front of it

Modern .NET applications serve HTTP themselves. In most production estates something still sits in front, whether that is IIS, nginx, or a cloud load balancer, and the thing that catches people out is the header handling. If the proxy terminates TLS and you do not configure the application to honour forwarded headers, the application believes every request arrived over plain HTTP from the proxy's address. Redirects then go to the wrong scheme and rate limiting counts every visitor as the same client. It is a two-line fix that costs a day to find.

Containers, and the Windows question

Modern .NET containerises straightforwardly on Linux, and that is where most of the ecosystem's attention has gone. Framework applications can be containerised too, but only in Windows containers, which are substantially larger, tie you to matching host and container versions, and are supported by a much thinner layer of tooling. If somebody has told you that containerising your Framework application is a quick win, ask what base image they intend to use and watch the answer. Sometimes it is still the right call, usually as a stepping stone for consistency of deployment rather than as an end state.

Managed platforms, and what they quietly assume

Platform hosting removes a great deal of work and introduces its own set of assumptions. Applications get shut down when idle unless you say otherwise, so the first visitor of the day pays for a cold start. Deployment slots with a warm-up step remove that from the user's path if you set them up. Scaling out means your application must not hold session state in process, which is exactly the assumption a decade-old Framework application is most likely to be built on. If your .NET estate is heading towards Azure specifically, the infrastructure side of that is a separate skill set and often a separate person, and we cover it on our Azure developers in India page.

Testing a Codebase Nobody Wrote Tests For

A large share of .NET applications in production have a test project containing four tests written during the first sprint and never touched again. Adding coverage to code that was not designed for it is a distinct skill, and it is worth being explicit about what you expect.

The frameworks, briefly, and why the choice matters least

xUnit, NUnit and MSTest all work. Arguments about them are mostly aesthetic. What matters is whether the code under test can be constructed without standing up half the application, which is a design question rather than a tooling one. If every test needs a live database and a configuration file, the tests will be slow, they will be flaky, and within two quarters somebody will mark them as skipped.

Integration tests that boot the real application

Modern ASP.NET Core gives you a way to start the whole application in memory and issue real requests against it, with selected services swapped out. This is the highest-value test type for most business applications, because it exercises routing, model binding, filters, authorisation and serialisation in one go, and those are where the regressions actually happen. Ask a candidate whether they have used it and what they replaced in the service registrations when they did.

The in-memory database trap

The in-memory provider is convenient and it is not a relational database. It does not enforce referential integrity, it does not fail on the constraint your real schema has, and it does not tell you whether your query translates to SQL, which is the single thing most worth knowing. Tests that pass against it and fail against SQL Server are a well-documented waste of an afternoon. Running the real engine in a throwaway container for the duration of the test run costs a few seconds of start-up and gives you answers you can trust. Where the testing discipline itself is the gap rather than the .NET knowledge, a dedicated tester is often the better hire, and we staff those from our QA engineers in India pool.

Getting Off .NET Framework, Which Is the Work Most Buyers Need

Plenty of briefs that arrive as "we need two .NET developers" are really "we need to get off Framework and nobody here has done it before". Treating migration as a first-class project rather than as something that happens between features changes both who you hire and how long it takes.

Inventory before estimate, always

The first deliverable is not code. It is a list: every project in the solution, its type, its target, its third-party dependencies and whether each of those has a version that runs on modern .NET. Then every Windows-specific API the code touches, every integration point, every scheduled job, every deployment step that assumes a Windows box. Tooling helps with the mechanical parts of this and Microsoft ships an upgrade assistant that automates a good deal of the file rewriting. The judgement it cannot make for you is what to do about the things with no equivalent.

Anyone who quotes a migration timeline before this list exists is guessing. We would rather charge you for a fortnight of assessment and give you a number you can plan against than agree to a date and discover in month three that the payments integration depends on a COM component.

What crosses over easily

Class libraries with no Windows dependency usually move with a project file conversion and a package update. MVC and Web API controller code ports with contained effort because the shapes are recognisable, even though the namespaces, the startup model and the filter pipeline all changed. Business logic and domain models are typically the easiest part of the whole exercise, which is worth saying out loud because it is the part everybody worries about most.

What has no path and must be replaced

Web Forms is the big one. There is no port. The page lifecycle, view state and the server control model do not exist in the modern stack, so those screens are rebuilt, and the rebuild is a redesign whether or not you wanted one. Server-side WCF is the second: the hosting model did not come across, and the options are a community-maintained implementation that reproduces much of it, or a move to a different protocol entirely. Remoting is gone. Multiple AppDomains are gone. Code access security is gone. Anything relying on the static request context that Framework exposed globally has to be rewritten to receive the context explicitly, and that change tends to ripple further than expected because that static was very convenient and therefore used everywhere.

The order of operations that keeps you shippable

The pattern that works is to move from the leaves inwards. Convert projects to the modern file format first, still targeting Framework, so the build changes and the runtime does not. Move packages.config to PackageReference. Then take the libraries with no Windows dependencies and target both frameworks at once, so both the old application and the new one can consume them. Then stand the new application up beside the old one and move functionality across a route at a time, with the proxy in front deciding which one serves what. Every one of those stages ends with something you could deploy on the Friday. A migration plan whose first shippable moment is nine months away is a plan that gets cancelled in month five.

The parts that are not code at all

Build agents need the new SDK. Deployment scripts that assume an IIS site need rewriting. Monitoring agents, log shipping, performance counters and whatever your operations team currently uses to know the application is alive all have to be reproduced. Licences and installed components on the server need checking. In the migrations we have been part of, this operational tail is routinely a third of the work and almost never appears in the original estimate. If your existing infrastructure move is the bigger half of the project, our cloud migration services in India page covers that side in more depth.

How We Screen .NET Engineers in India

Screening is a conversation about specific failures, not a quiz. The four technical questions below are the ones we run on every .NET candidate, and you are welcome to run them again yourself. They are printed here deliberately: a candidate who has memorised the answers to these still has to explain the reasoning, and the reasoning is what we are listening to.

What happens when you call .Result on a Task in an ASP.NET request?

Covered in detail above. The complete answer distinguishes the classic stack from ASP.NET Core, names the context as the reason for the difference, and describes what the failure looks like from the outside. A partial answer that says "it blocks" is fine from a mid-level candidate and thin from a senior one.

Why did this query get slow after somebody added an Include?

We show a before and after, with row counts. We are listening for row multiplication across collection navigations, for the instinct to look at the generated SQL rather than to guess, and for a discussion of the trade between splitting the query and projecting into a smaller shape. Candidates who reach immediately for an index have skipped the diagnosis.

How would you move this Framework application to modern .NET?

We describe a real-shaped application: MVC 5, a couple of Web Forms admin screens, a WCF service, a Windows service and a SQL Server database. The answer we want starts with an inventory and ends with a staged plan, flags Web Forms and WCF as the expensive parts without being prompted, and refuses to give a duration. Anyone who offers a confident timeline in the interview will offer you a confident timeline on your project too.

A service uses more memory every hour. What do you check first?

Snapshot, wait, snapshot, compare. Then find what is holding the reference. We are equally interested in what they do not do: raise the limit, schedule a restart, blame the garbage collector. This question also usefully separates people who have only ever run applications locally from people who have supported one.

Written English, judged on the thing they will actually write

Spoken fluency in a call is a weak predictor of remote effectiveness. What matters is whether a pull request description explains why a change is safe, whether a bug report contains what was expected and what happened, and whether a handover note is usable by someone who was not there. We ask for a written explanation of a technical decision and read it the way you would read a pull request at eight in the morning.

Reading Seniority on a .NET CV

Job titles travel badly between companies, and .NET is worse than most because the ecosystem is old enough that a title can be twenty years stale. Here is how we grade the levels, so that your brief and our shortlist mean the same thing.

Junior

Writes controllers and services against an established pattern, follows the existing structure, needs the review. Should not be given the data access layer of a system with real volume, and should not be the only person on a Framework codebase, because there is nobody to ask about the parts that are not written down. Productive quickly on a team with a strong reviewer and a slow drain on a team without one.

Mid-level

Owns a feature end to end, knows what tracking does, reads the generated SQL without being told to, writes tests that would fail if the code were wrong. Can be trusted alone in a codebase they have been in for a month. The line between mid and senior on .NET work is usually diagnosis: whether they can work out why something is slow, as opposed to making a slow thing they wrote faster.

Senior

Can be dropped into an unfamiliar system and produce a useful assessment of it in a week. Debugs production, reads a dump, reasons about lifetimes and concurrency, argues for the boring option when the boring option is right. Says no to a timeline they think is fiction, which is exactly the person you want on a migration.

The specialists worth naming separately

If your problem is the database rather than the application, ask for someone whose depth is SQL Server rather than C#. If it is the pipeline and the servers, that is a platform role. If it is a Web Forms rebuild, the front-end skill you need may not be a .NET skill at all. Describing the symptom rather than the job title gets you the right person more often, and it costs us nothing to tell you when the answer is not a .NET developer.

Four Situations That Bring Teams to Us

The four below are composites, drawn from the shape of the briefs that reach us rather than from any one engagement. Recognising your own situation in one of them is a good sign, because it means the diagnosis is already half done.

The person who knew the system has gone

A Framework application, one long-serving developer, no documentation, a deployment process that lived in their head. Now they have left and the finance team still needs the month-end report. The first job here is not features, it is recovering knowledge: getting a build running from a clean checkout, mapping which jobs run when, and writing down what nobody wrote down. Expect the first month to look slow and to be the most valuable month of the engagement.

The migration that has been on the roadmap for three years

Everybody agrees it should happen. Nobody has time, and each year the dependency list gets slightly worse. What usually breaks the stalemate is the assessment: a concrete inventory, a staged plan with shippable checkpoints, and an honest statement of which parts have no path. It is much easier to fund a decision when somebody has written down what is actually in the box.

The application that outgrew its data access

It was fine for four years, then a larger customer arrived and the reporting screens started timing out. Nothing about the architecture is wrong in principle. The queries were written for a table that used to be small. Work here is measurement first, then targeted changes to the worst offenders, and it usually returns more than a rewrite would for a fraction of the disruption.

Capacity, plainly

The system is fine, the team is competent, the backlog is longer than the year. You want two more people who can hold their own in code review and not create work for everybody else. This is the most straightforward case and the one where the timezone conversation matters most, because a capacity hire who cannot be reached is not capacity.

What Overlap Do You Get With a .NET Team in India?

Offshore pages tend to go soft here, so take the arithmetic instead. The Indian offset is fixed at UTC+5:30 and never changes, so the size of the gap between the two offices moves only when your own country puts its clocks forward or back.

The numbers in your own clock

Take a standard Indian office day of 09:30 to 18:30. In London during GMT that is 04:00 to 13:00, so if your people start at nine you share four hours, and during British Summer Time you share five. In Sydney on AEST it is 14:00 to 23:00, giving you three hours at the start of the Indian day and the end of yours. In Toronto and New York on Eastern Standard Time it is 23:00 to 08:00, and in Chicago 22:00 to 07:00, which means zero overlap with a normal working day: the Indian day has finished before yours begins. In Auckland the Indian day lands in your evening, so at best you get the tail of your afternoon.

Any page that tells you a nine and a half hour offset produces comfortable collaboration is not doing the sum. What it produces is a clean handover if you design for one, and frustration if you do not.

What buying overlap actually costs

You can move the Indian working day later. A shift of 13:30 to 22:30 in India is 03:00 to 12:00 Eastern Standard Time, which gives you three hours with your morning. That is a real option and it is not free: the engineer is working into their evening every day, which affects who will take the role, how long they stay in it, and how they feel about a meeting that overruns. We would rather agree that pattern with you in writing before anyone starts than let it happen by accident through calendar invitations. Anyone offering round-the-clock coverage without describing the rotation that delivers it is selling you something they have not staffed.

Why the written record carries more weight here

On a codebase where a decade of decisions are undocumented, the questions an offshore engineer needs answered are exactly the ones only your long-serving people can answer, and they are asleep. The practical countermeasure is written-first working: questions raised as issues rather than saved for a call, pull requests that explain the reasoning, and a daily written summary that makes the next morning's read useful rather than a status meeting nobody wants. Teams that adopt this find it improves things on their own side too, which is a pleasant side effect rather than a sales point.

Review, ceremonies and the handover

Pull request review across a gap is asynchronous by necessity, so the discipline is small changes and fast turnaround. A change that takes two days to review costs four days of calendar time when the reviewer is on the other side of the planet. Whatever ceremony you run, keep the synchronous part inside the shared window and push everything else into writing. If the shared window is zero, one scheduled call a week at an hour that is uncomfortable for somebody is generally worth more than five that never happen.

The Objections Worth Raising Before You Sign

Every buyer asks some version of these six. Reassurance is not an answer to any of them, so here is what we would actually say across a table.

How do I keep quality visible when nobody is in the room?

You read the output. The controls that work are the ones you already have or can add in an afternoon: branch protection so nothing merges without a review from your side, a pipeline that runs the tests and fails the build honestly, and a written definition of done that names what a change must include. On a .NET codebase specifically, add a rule that any change touching data access includes the generated SQL in the pull request description. It takes thirty seconds to produce and it makes an entire category of problem visible before merge.

How is English and communication assessed?

Through writing, for the reasons given in the screening section, and then by you. We do not put anybody in front of you without a written sample, and we expect you to run your own conversation before you agree to anyone. If a candidate cannot explain a technical decision clearly in a paragraph, that will not improve once they are on your team and nine and a half hours away.

Who ends up owning what gets written?

Your company does. How that assignment is worded, what confidentiality covers, and the rules around any data an engineer can see are all fixed in the agreement before the first commit, and fixed between your legal people and ours rather than declared on a marketing page, since the wording that satisfies a firm in Ontario is not the wording that satisfies one in Frankfurt. Operationally the point is simpler: the work happens in your repositories, under your branch protection, so nothing of value ever collects on our side of the line.

What if the person is not right?

Say so, and a different engineer takes the seat inside 48 hours. You also choose from a pool, so you are never stuck with the single name that happened to arrive first. What matters more is that you say something in week two rather than month two. On .NET work the mismatch signals are fairly consistent: review comments that never get past formatting, hesitation around the parts of the codebase that carry the money, and an answer to "is this change safe" that is really an answer to "does it compile". Those are visible early if you are looking.

How is access and security handled?

Least privilege, and start narrower than feels comfortable. Read access to the repository and a working local environment on day one, write access when the first review has been through, production access only if the role genuinely requires it and then with its own credentials and an audit trail. Production data should not be copied to any developer machine, on your side or ours; a masked or synthetic dataset is worth the day it takes to build. Whatever your regulatory position requires, tell us at the start rather than after the engineer has been chosen, because it changes who is appropriate.

Which costs are not in the spreadsheet?

Ramp-up, mostly. Someone new to a mature .NET codebase spends the first weeks reading, and their output in that period is questions rather than commits. There is also the management cost on your side, which is real: a remote engineer with a nine and a half hour offset needs clearer written direction than a colleague at the next desk, and that time comes from your senior people. Budget for both. An engagement priced as though productivity starts on day one is an engagement that disappoints in month one.

Ways to Work With Us, and How Hiring Runs

Three shapes cover most .NET engagements. The right one depends on whether you have a deadline, a backlog, or a system you are nervous about.

One or more engineers inside your team

They join your standups, your board and your repositories, and they report to your engineering manager. This is the model that suits a long-lived product with an existing team and an unending backlog. It puts the management load on you, which is the honest trade: you keep the control and you carry the coordination.

A scoped piece of work with a defined finish

A migration assessment, a rebuild of a set of Web Forms screens, a performance investigation with a target. This suits work that has a shape and an end, and it suits buyers who do not have an engineering manager with time to spare. Scope and acceptance are agreed up front, in writing, and the finish line is written down before anyone starts rather than negotiated afterwards.

Retained time against an old system

Some Framework estates do not need a full-time developer. They need someone who knows the system, is reachable when the month-end job fails, and can pick up small changes. A retained arrangement keeps that knowledge alive without paying for capacity you cannot use, and it is generally cheaper than rediscovering the system every time something breaks.

How the hiring itself runs

Tell us the symptom rather than the job title: what the application is, roughly how old, what it runs on, what breaks, and what you are afraid to change. We come back within 48 hours with developers matched against that brief, and once you have run your own technical conversation and chosen, the engineer can be working inside 7 days. That speed exists because the people are already with us rather than being recruited after your brief arrives, which is also why it is worth telling us what you need before you have finished writing the requisition. If you are not sure a .NET specialist is what the problem calls for, our broader hire developers in India page sets out the other roles we staff.

Frequently Asked Questions

We are not sure whether our application is .NET Framework or modern .NET. Can you tell us?

Send us one project file and one configuration file and we can usually tell you in an afternoon. A short SDK-style csproj with a TargetFramework line pointing at a net-something moniker is modern. A long file listing every source file individually, with ProjectTypeGuids and a companion packages.config, is Framework. Neither answer is bad news on its own. It just decides which engineer we put in front of you.

Is a .NET Framework application something you would refuse to work on?

No. A large share of the .NET work we are asked to staff is Framework code on Windows Server that earns money every day and has no business case for a rewrite. We keep it healthy, ship features into it, and give you an honest read on whether migration is worth funding yet. Engineers who only ever worked on greenfield ASP.NET Core tend to be poor at that, so we screen separately for it.

How do you check that someone really understands async in .NET?

We ask what happens when you call .Result on a Task inside a classic ASP.NET request. The answer we want covers the request context that only lets one thread in at a time, the continuation that cannot get back into it, and the deadlock that follows. Then we ask how the same mistake behaves on ASP.NET Core, where there is no such context and the symptom becomes pool starvation instead.

A report got slower after a developer added an Include. Why would that happen?

Because a single query that joins two or more collection navigations multiplies rows before it reaches you. Fifty parents with twenty children and ten notes each is not eighty rows, it is thousands, all carrying duplicated parent columns. Entity Framework then spends real time collapsing that back into objects. Reading the generated SQL settles it, and splitting the query or projecting only the columns you display usually fixes it.

Our service uses more memory every hour until it is restarted. Where do you start?

First we establish whether it is a leak or just a heap that has never been asked to shrink, by watching allocation and collection counters rather than the number in Task Manager. If gen 2 keeps growing after collections, we take two snapshots an hour apart and compare them. In .NET the usual culprits are a static dictionary with no eviction, event handlers nobody unsubscribed, and objects held alive by a timer or a cache.

What overlap do we get with a .NET team based in India?

Work it out in your own clock. An Indian day of 09:30 to 18:30 lands as 04:00 to 13:00 in London on GMT, 14:00 to 23:00 in Sydney on AEST, 23:00 to 08:00 in Toronto on EST and 22:00 to 07:00 in Chicago. London shares about four working hours. Sydney shares three. Neither North American city shares any, because the Indian day has finished before yours opens. Overlap with North America therefore has to be purchased by moving the Indian day later, and that pattern gets fixed in writing before anybody is proposed to you.

Is it worth containerising our .NET Framework application?

Sometimes, as long as you know what you are buying. Framework code only runs in Windows containers, which are far larger than Linux images, tie the container to a matching host version and sit on thinner tooling. What you gain is deployment consistency: one artefact, no more configuration drift between servers. What you do not gain is any progress towards modern .NET. Treat it as an operations improvement rather than as a migration step.

Tell us which .NET you have

Send the shape of it: how old the codebase is, what it runs on, which parts you are nervous about. We will tell you whether the answer is a maintenance engineer, a migration, or neither.