Skip to main content
Published / updated

Visual Studio

Before you start

You need: any C# project to open — the console application from Track 03 is ideal.

Take this track alongside a programming track, not after it. The tools are only useful with something to point them at.

Time: about 50 minutes, at the keyboard.

Learning objective

Open an unfamiliar solution, build it, navigate it by keyboard, and debug a running application.

Topics

  • Solutions and projects
  • Opening and building
  • Navigation shortcuts
  • The debugger
  • Watch, Call Stack and Immediate
  • Refactoring
  • NuGet
  • Diagnosing a build failure

Solutions and projects

NexCoding.SchoolPortal.sln the solution — a container
├── NexCoding.SchoolPortal.Domain a project — compiles to one assembly
├── NexCoding.SchoolPortal.Data
├── NexCoding.SchoolPortal.Api
└── NexCoding.SchoolPortal.Tests

A solution groups projects. A project produces one .dll or .exe.

Solution Explorer is the map. Open a .sln and the first thing to read is the project list and the reference direction — which projects depend on which. In a layered application that tells you where everything lives before you open a single file.

Ctrl+Alt+L opens Solution Explorer. Ctrl+; searches it, which is faster than scrolling once there are more than about thirty files.

Set the startup project by right-clicking it — Visual Studio runs whichever is bold, and running the wrong one is a common first-day confusion in a multi-project solution.

Opening and building

ActionShortcut
Build solutionCtrl+Shift+B
Rebuild— (menu)
Clean— (menu)
Run with debuggingF5
Run without debuggingCtrl+F5
StopShift+F5

Build compiles what changed; Rebuild cleans and compiles everything. Reach for Rebuild when the build succeeds but the running code is clearly stale — usually a locked or orphaned .dll in bin.

The Error List (Ctrl+\, E) shows errors and warnings; Output (Ctrl+Alt+O) shows what MSBuild actually did. When the Error List is empty and the build still failed, read Output — the real message is there.

Fix the first error first. One missing using produces twenty cascading errors, and nineteen of them disappear when the first is fixed.

These are the shortcuts that separate someone comfortable in the IDE from someone scrolling.

ActionShortcut
Go to definitionF12
Peek definitionAlt+F12
Go to implementationCtrl+F12
Find all referencesShift+F12
Go to fileCtrl+,
Go to lineCtrl+G
Navigate backwardCtrl+-
Navigate forwardCtrl+Shift+-
Find in filesCtrl+Shift+F
RenameCtrl+R, R
Quick actionsCtrl+.
Comment selectionCtrl+K, C

F12 then Ctrl+- is how you read a codebase: follow a call, read the implementation, come back. Doing that ten times through a call chain is faster and more reliable than searching by name.

Shift+F12 before changing anything. Find all references answers "what breaks if I change this" in one keystroke, and it finds usages a text search misses — an interface implementation, a generic instantiation.

Ctrl+, accepts partial and camel-case matches. Typing stusrv finds StudentService.

Ctrl+. offers the fixes for whatever the cursor is on: add a missing using, generate a constructor, implement an interface, introduce a variable. It is the single most useful key in the IDE.

The debugger

Click the left gutter, or press F9, to set a breakpoint. Press F5 and execution stops there.

ActionShortcut
Toggle breakpointF9
ContinueF5
Step overF10
Step intoF11
Step outShift+F11
Run to cursorCtrl+F10
RestartCtrl+Shift+F5

Step over runs the called method and moves to the next line. Step into enters it. Stepping into every framework call is how debugging becomes slow — step over until you reach code you own.

Step out finishes the current method and returns to the caller. Use it the moment you realise you have stepped into something irrelevant.

Conditional breakpoints

Right-click a breakpoint → Conditions.

student.RollNumber == "NCA-2024-0012"

This is the feature that makes debugging a loop practical. Stopping on one student out of four hundred, instead of pressing F5 four hundred times, is the difference between a two-minute investigation and abandoning the debugger for Console.WriteLine.

Condition typeDoes
Conditional expressionBreak only when true
Hit countBreak on the 50th pass
FilterBreak on a specific thread

Actions on a breakpoint print a message and continue — a Console.WriteLine that is not in your source code and cannot be accidentally committed.

Watch, Call Stack and Immediate

Locals shows every variable in scope. Autos shows those used near the current line.

Watch (Ctrl+Alt+W, 1) evaluates expressions you add, re-evaluated at every step:

students.Count(s => s.Status == StudentStatus.Active)
account.TotalFees - account.PaidAmount

Any valid C# expression works, including method calls — though calling a method with side effects from a watch changes program state, which is a genuine trap.

Call Stack (Ctrl+Alt+C) shows how execution reached here. Click any frame to jump there and inspect its locals.

That is how you find a bug whose symptom is three calls below its cause: the value is wrong here, so walk up the stack until you find the frame where it was still right.

Immediate (Ctrl+Alt+I) runs arbitrary code against the paused process:

? students.Count
? repository.GetByRollNumber(1, "NCA-2024-0012")
student.ClassName = "11th"

You can read values, call methods and assign to variables — testing a hypothesis without editing, rebuilding and re-running.

Exception settings

Debug → Windows → Exception Settings (Ctrl+Alt+E), tick Common Language Runtime Exceptions.

The debugger now breaks at the throw site rather than at the catch. For an exception swallowed several layers down, that is the only practical way to find where it originated.

Turn it off again afterwards — a codebase using exceptions for control flow becomes unusable with it on.

Refactoring

RefactoringShortcut
RenameCtrl+R, R
Extract methodCtrl+R, M
Extract interfaceCtrl+R, I
Encapsulate fieldCtrl+R, E
Remove and sort usingsCtrl+R, G
Format documentCtrl+K, Ctrl+D

Always rename with Ctrl+R, R, never with find-and-replace. The refactoring is semantic — it renames the symbol and every reference to it, and leaves an unrelated variable of the same name alone. Find-and-replace does not know the difference.

Ctrl+. offers context-appropriate refactorings for whatever the cursor is on.

EditorConfig enforces formatting across the team:

# .editorconfig
root = true

[*.cs]
indent_style = space
indent_size = 4
csharp_new_line_before_open_brace = all
dotnet_sort_system_directives_first = true

Committed to the repository, it means every developer's IDE formats identically — and diffs stop containing whitespace changes.

NuGet

Right-click a project → Manage NuGet Packages. The four tabs do everything:

TabDoes
BrowseSearch and install. The version dropdown pins a specific one
InstalledWhat this project has, and Uninstall
UpdatesWhat has a newer version, with Update All
ConsolidateAppears when two projects use different versions of the same package — fix that

Consolidate is the tab people never open and should. Two projects on different versions of the same package builds fine and fails at run time.

The same operations in the Package Manager Console (Tools → NuGet Package Manager → Package Manager Console):

Install-Package Dapper
Install-Package Dapper -Version 2.1.35
Uninstall-Package Dapper
Get-Package

To check for vulnerable packages, right-click the solution → Manage NuGet Packages for SolutionUpdates, or run this one-liner in a terminal:

dotnet list package --vulnerable --include-transitive
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
</ItemGroup>

dotnet list package --vulnerable is worth running regularly. It reports known CVEs in your dependency tree, including transitive ones — and it takes seconds.

Pin exact versions in an application. A floating version means two developers, or the build server, get different code from the same commit. Libraries use ranges; applications pin.

Read the transitive dependencies before adding a package. One convenience package can pull in twenty others, each of which is now your problem to keep patched.

A Directory.Packages.props centralises versions across a multi-project solution, so every project uses the same version of a shared package.

Diagnosing a build failure

SymptomCause
Twenty errors from one changeFix the first; the rest usually cascade
"Type or namespace could not be found"Missing using, or a missing project reference
"Could not load file or assembly"Version mismatch, or a stale bin — Rebuild
Builds, but old code runsStale build, or the wrong startup project
Works locally, fails on the serverA dependency not committed, or a version difference
"The process cannot access the file"The application is still running — Shift+F5
Restore failsNo network, or a private feed not configured
dotnet build -v normal # more detail
dotnet build -v diagnostic # everything

When the Error List is unhelpful, dotnet build from a terminal often gives a clearer message than the IDE.

Delete bin and obj as a last resort. It fixes a genuine class of stale-artefact problem, and it is not a substitute for reading the error.

Errors you will hit

What you seeCauseFix
Breakpoint is hollow and never hitsStale build, or wrong startup projectRebuild; right-click → Set as Startup Project
A template you expect is missingWorkload not installedTools → Get Tools and Features
Ctrl+. offers nothingCursor not on the symbolPut it on the identifier
Find All References misses a usageLate binding or reflectionUse Find in Files as well
The console window closes instantlyRan with F5Use Ctrl+F5
IntelliSense stops workingCorrupted cacheClose, delete .vs, reopen

Deleting the hidden .vs folder fixes a surprising share of Visual Studio oddities. It is regenerated on the next open.

Common mistakes

  • Scrolling Solution Explorer instead of Ctrl+,
  • Not using F12 and Ctrl+- to read a codebase
  • Changing a method without Shift+F12 first
  • Renaming with find-and-replace instead of Ctrl+R, R
  • Console.WriteLine debugging instead of breakpoints
  • Not knowing conditional breakpoints exist
  • Stepping into every framework call
  • Leaving "break on all exceptions" enabled
  • Fixing the last error first
  • Floating package versions in an application
  • Never checking --vulnerable
  • Running the wrong startup project

Practice

The course exercise is open, build and debug a solution.

  1. Open a multi-project solution. List every project and its references without opening a file.
  2. Build it. Introduce a syntax error and read the Error List and the Output window.
  3. Introduce one missing using and count the resulting errors. Fix the first and re-count.
  4. Navigate a call chain four levels deep with F12, then return with Ctrl+-.
  5. Use Shift+F12 on a method before changing it. Note any usage a text search would miss.
  6. Find a class with Ctrl+, using a camel-case abbreviation.
  7. Set a breakpoint in a loop over 400 students. Press F5 and count how many times you have to continue.
  8. Add a condition for one roll number and repeat.
  9. Use a breakpoint action to print a value without editing the code.
  10. Step into a method, realise it is irrelevant, and step out.
  11. Add two Watch expressions and step through, watching them change.
  12. Use the Call Stack to jump three frames up and inspect that frame's locals.
  13. Use the Immediate window to call a repository method and assign to a variable.
  14. Enable break-on-all-CLR-exceptions and find where a swallowed exception originates. Turn it off.
  15. Rename a method with Ctrl+R, R. Then rename another with find-and-replace and find what it broke.
  16. Add an .editorconfig and format a badly formatted file.
  17. Run dotnet list package --outdated and --vulnerable.

Exercises 7, 8 and 14 are the three that most change how you debug.

You can now

  • Open an unfamiliar solution and navigate it by keyboard
  • Use F12, Shift+F12, Ctrl+, and Ctrl+.
  • Set a conditional breakpoint and a tracepoint
  • Read Watch, Locals and the Call Stack
  • Find a swallowed exception with Exception Settings
  • Manage NuGet packages and check for vulnerable ones

Review questions

  1. What does Shift+F12 tell you that a text search does not?
  2. When does a conditional breakpoint save the most time?
  3. Why rename with Ctrl+R, R rather than find-and-replace?
  4. Why pin exact package versions in an application?

Next: VS Code