Microsoft Visual Studio is over 2 decades old at this point, and is currently still the way to work with the .NET ecosystem. It's a solid IDE, arguably one of the best around. Earlier last month Visual Studio saw the release its latest major version: version 2019. Despite having been around for such a long time, this is actually the first release of Visual Studio that took place in the face of genuine competition (in the form of JetBrains Rider), and in that light, version 2019 is kind of a disappointment.
Allow me to explain.
The core loop
Visual Studio, like any modern, fully-featured IDE, contains an incredibly wide breadth of functionality. However, not all of this functionality is equally important. There's a small number of tasks that make up 99% of my time spent in the IDE. These are:
- Navigating around the project code.
- Writing code.
- Writing and running unit tests.
- Working with version control.
These tasks form the core loop of working with the IDE, and it follows that any good IDE should excel primarily at these tasks. All functionality not directly related to these tasks is of secondary concern.
Visual Studio 2019 disappoints me because the improvements it brings to the core loop are meagre, and piecemeal. This becomes especially apparent after using Rider for a while. When I was finally forced back to Visual Studio because my Rider trial expired, I couldn't help but feel like Microsoft's IDE has somehow gotten stuck in time. Broadly speaking, Visual Studio 2019 still looks, feels, and behaves the same way as Visual Studio 2012 did. And that's not exactly because there is nothing left to improve. Rider has shown me that much.
So, let's compare how both IDEs approach the core loop.
Solution and working files
After first opening a solution, both IDEs use the familiar tree view for presenting its structure:
So far so good.
The difference starts after opening a few files. At this point we immediately run into one of Visual Studio's most glaring usability annoyances: tab placement. Open files are placed in a horizontal line of tabs at the top of the code editor. This is problematic for two reasons. First of all, because the tabs are spread across such a long horizonal line, it's hard to quickly find the one you're looking for because you're forced to read from left to right across the entire width of the screen to do so. Secondly, the number of open files that are visible at one time is severely limited, and any files that don't fit overflow into a dropdown menu that is conveniently placed under the tiniest possible button:
Not good, and sadly still the only behaviour Visual Studio 2019 offers out of the box.
So, what's the solution? Well, Rider allows you to place your tabs anywhere you want, including vertically, as God intended:
It's simple, but it solves both problems. You can have a bunch more open files (more than you'll need in practice), and they're still quick to find because a vertical list is much faster to scan than a horizontal one, especially if it's sorted in some way. Now initially this might seem like it takes up a lot of space, but since everyone and their dog uses 16:9 displays these days, it's actually a far more efficient way of utilizing your precious screen real estate. I mean, look at Visual Studio again:
At least in Visual Studio 2017 we could use the Custom Document Well plugin to correct this particular mistake, but I guess they didn't bother to make it compatible with version 2019. This effectively makes "upgrading" to Visual Studio 2019 a regression in terms of usability.
Both IDEs support a kind of "quick goto" function that allows you to quickly search and jump to any class or method in the open solution. Visual Studio is, again, inefficient in its use of screen space, insisting on using two lines for each result:
However, it scores points for the ability to quickly filter on classes, members, or files, and for the option of previewing the highlighted file.
Rider's approach presents more information in a less cluttered way:
It has the ability of opening multiple files at once from this dialog, which can be useful. It can also search non-solution items, although it lacks the ability to distinguish between classes and class members.
Neither IDE is a clear winner for me here. Quick goto is a very useful feature to have, and both Visual Studio and Rider have a solid implementation.
Another invaluable tool in finding your way around a large code base is the "find usages" or "find references" function. This allows you to quickly find all the places where some kind of symbol (be it a class or class member) is used. I really can't imagine writing code without this.
Unfortunately, in Visual Studio this is another one of those neglected areas that hasn't really improved much throughout the years, despite being such an integral part of the core loop. Finding all references in Visual Studio basically just gets you a big messy dump of results to sort through:
If you happen to have a project that targets multiple .NET versions, as in the screenshot above, it'll duplicate its results for each target. This forces you to group the results by project if you want to make any sense of them at all, which in turn means you can't use any of the other grouping functions. Not that any of them particularly useful. But I digress.
This screen works for small result sets, but for anything larger than a dozen usages it really doesn't help you that much. You'll still end up sifting through the results yourself trying to find that one usage you're looking for.
So how should something like this work? Again, we turn to Rider for answers. Here's how it presents the same results:
First of all, it doesn't choke on the fact that we're targeting multiple .NET versions. Secondly, it groups the results by usage type. That means if you're trying to find where an object is constructed, or where it's used as a method parameter, this screen has you covered. No need to manually scan through several dozen lines trying to find that one constructor call when the IDE already has that information, and presents it to you in a useful way. What a concept...
Something I end up using quite often in practice is the "find in files" function. There are plenty of times when you can't use symbolic search for whatever reason, so a good plain text search function is a definite must have.
In Visual Studio, a "find all" search is performed through this archaic dialog screen that hasn't been updated since the Berlin Wall fell:
Results are presented in a separate window and can be grouped in a few different ways. In a surprising turn of events, Visual Studio 2019 actually updated this screen to highlight the search keyword. You also have the option of searching within the results, which can be useful.
You might think there isn't a whole lot wrong with this, but that just means you haven't seen how Rider does this yet.
In Rider, the "find all" screen contains a live, as-you-type preview of your search results:
This makes it incredibly fast to iterate over different queries and narrow down your results. Visual Studio makes you specify your search, hit "find all", check the results, open the dialog again, try something else, hit "find all" again, check the new results, etc. It's a chore compared to Rider.
In addition to its live search dialog, Rider can also present its results in a separate results window, similar to Visual Studio:
There's syntax highlighting as well as the option to preview the context of the selected occurrence. You can also quickly rerun the query or go back and modify it in the live-search dialog, although there is no support for searching within the results.
Make no mistake: both Visual Studio and Rider are excellent code editors. They have all the basics down, and then some. But there's still a few key differences that, at least in my view, give Rider a noticeable edge.
Let's look at how both IDEs approach one of the most vital services an editor can offer: autocompletion.
As expected, both editors support your basic symbolic code completion:
This includes support for camel-hump typing:
What's interesting is that while Visual Studio limits its suggestions to symbols currently in scope, Rider also suggests things that aren't yet currently referenced. While the latter can lead to a cluttered suggestion list, I find it to be mostly beneficial. I mean, come on, look at this:
Visual Studio is just no help until you've already typed out the full type name, and even then adding the reference is hidden at the bottom of the quick action menu:
If you happen to work with generics, Visual Studio simply doesn't understand and/or doesn't display this information this context, whereas Rider does:
Rider is also smart enough to filter its autocompletion suggestions based on what actually fits the type of the expression it's trying to complete, whereas Visual Studio just gives up sometimes:
Finally, Rider is often nice enough to write some boilerplate for you:
These are small things, but you run into them constantly. Code completion is as deeply nested in the core loop as you can get. This means the little extra niceties that Rider offers quickly add up to a much more pleasant overall experience.
Yes, autocompletion is a form of coding assistance. But not all coding assitance has to do with autocompletion. Here, I look at some other frequently used assistance that the IDEs offer.
Both Visual Studio and Rider contain a kind of quick action context menu that lets you perform actions based on the current selection or cursor placement in your code. Visual Studio has always been a bit behind the curve on what it offers here, although with every major release, a few more refactorings get included. However, while Rider contains a lot more of these kind of actions than Visual Studio, most of them really aren't used that often in practice.
I was actually hard-pressed to find examples of coding assistance tricks I use on a daily basis that Rider has but Visual Studio doesn't (or vice-versa for that matter). Of course that doesn't mean there aren't any differences. For example: constructor parameters. A very common scenario is initializing a local field from a constructor parameter. Both Visual Studio and Rider offer coding assistance for this when starting from the constructor parameter:
However, when starting from the local field, only Rider knows what to do:
It's a simple example, but it's illustrative of the difference in coding assistance that both IDEs offer. Visual Studio feels picky about what kind of scenarios it supports, and it's easy to find yourself outside of its comfort zone. On the other hand, Rider comes with an abundance of assistance, and while not all of it is useful, it does at least cover the important stuff.
That said, I can't fault Visual Studio too much here. I do prefer Rider for its coding assistance, but it's not like I find myself limited by what Visual Studio can do on a daily basis. I mean, it's neat that Rider knows how to convert a for loop into a Linq aggregate, but how often do you really use that stuff?
What about IntelliCode?
One of the most heavily marketed new features of Visual Studio 2019 is IntelliCode. This is kind of funny, because not only is IntelliCode still in beta, it doesn't even ship with Visual Studio. It's actually a plugin that you have install separately, and which is also available for Visual Studio 2017. The basic gist of IntelliCode is that it attempts to provide better autocompletion suggestions based on your current context. It does this by training some kind of machine learning model on a bunch of popular GitHub projects, as well as (optionally) your own code. Caveat is that you can't train it on your own projects without signing into your Visual Studio account and sending your trained models to Microsoft. For all of these reasons I don't consider IntelliCode part of Visual Studio's base capabilities, but we'll take a look at it regardless.
When you have IntelliCode installed, you'll occasionally see starred autocompletion suggestions in places where IntelliCode has "learned" that a particular member is frequently used.
This can be useful, but there are also limitations. It only seems to work for suggesting type members and local variables; not for type suggestions. The examples I have shown so far where Visual Studio fails to suggest a type all have IntelliCode enabled. It also can't suggest anything from your own code unless you first train it (and send the trained model to Microsoft). This means you have a training model to keep up-to-date, and that your suggestions won't work for recent changes that took place after the most recent update. Feels a bit clumsy to do it this way.
While coding and not worrying too much about how everything is just indiscriminately labelled "AI" these days, IntelliCode can be nice, but I wonder if the effort put into it wouldn't have been better spent elsewhere. Think of it like this: switching from Rider to Visual Studio makes me miss Rider's autocompletion, but going back from Visual Studio to Rider doesn't really make me miss IntelliCode.
Unit testing is another thing that takes up a lot of time for a developer. Right? We all unit test our code, right? Both Visual Studio and Rider contain test runners that support several different unit testing frameworks, and both contain a test explorer window that allows you to view and run your tests.
For both IDEs, the functionality is solid but basic. There also aren't a whole lot of noticeable differences between the two implementations, as both offer pretty much the same features. I prefer Rider for my day-to-day unit testing activities, but it's not a strong preference.
The only real advantage Rider has is found in how test sessions work. A test session is basically some subset of your unit tests that you have selected. You can then easily run all tests from this session, without having to run every test in the project.
In Visual Studio, test sessions are called "playlists", and when creating one you are forced to create a playlist file that is written to disk somewhere. Rider, on the other hand, just creates your new session, and doesn't bother you with any file management:
It's a small thing, but it makes Rider more convenient to use.
Aside from that, both IDEs are roughly the same, and will get the job done.
Probably one of the more crucial aspects of any IDE, aside from writing code, is debugging. Fortunately, both Visual Studio and Rider offer solid debugging features. The basics are all there: running a program, breaking on breakpoints or exceptions, and stepping through code all works as expected.
Of course, there are a few differences.
Checking the values of variables is one of the core parts of debugging. Rider has a nice feature where it shows you the value of variables directly in the code editor:
It's not a crucial feature, but nice to have.
Another advantage Rider has over Visual Studio is the way it allows you to inspect variables from the code editor. Visual Studio has this annoying tree that just hovers over the code editor, without a window:
You have to be very careful maneuvering your mouse around that thing, because as soon as you leave the tree, the whole thing collapses. It happens more often than you might think. You also can't open more than one "path" at the same time. I hardly ever use this feature for these reasons; I always just right-click and open the variable in QuickWatch.
Rider, on the other hand, gives you this popup on mouse-over:
Clicking it opens the variable in a popup, where you can quickly inspect the tree:
This is basically just Visual Studio's QuickWatch, but more easily accessible.
Both IDEs also have a window that (automatically) shows variables currently in scope. There's also the option for adding your own expressions, and having these persist throughout your debugging session. In Visual Studio, the current scope variables and your own expressions are separated into two different windows, whereas Rider gives you the option of combining both in one view, which is nice:
Visual Studio 2019, on the other hand, introduced a very nice new feature where it lets you do a free text search on the whole tree:
This is pretty much the only new debugging feature that Visual Studio 2019 brings to the table, but it's a pretty nice one. Although I've found that applicability is limited in practice, it's fantastic to have when you need it.
Sometimes, the problem you're trying to debug isn't with your own code. No, really. This actually happens sometimes. In those cases, it would be quite useful if you'd be able to step into that library, framework, or NuGet package, and see what's going on there.
In Visual Studio, trying to accomplish this is a painful experience, if it works at all. Visual Studio is very picky about when it will actually do this: you'll need, at minimum, a PDB file (exactly matching the build you're trying to debug), the source code (because why not), and some dark magic. Not only is it unlikely you'll always have access to these things, but even if you do, results are not guaranteed. I have attempted this at least once with every version of Visual Studio that has ever been released, and I have not found a reliable way of making this work. At this stage I don't even bother anymore.
And then there's Rider.
In Rider this stuff just works. All you have to do is tick a checkbox for debugging external code, and you can F11 to your heart's content.
This probably isn't something that you'll need daily, but when you do, oh boy is it ever nice to have. For me this is the killer feature that makes me prefer Rider over Visual Studio for debugging. It's just so good.
This is probably the least "core" activity of the core loop. You could argue that version control isn't the responsibility of an IDE at all. Still, it's part of the daily workflow of a developer, and both Visual Studio and Rider offer version control features, so why not compare them?
In Visual Studio, version control is handled through the Team Explorer window. A relic from the days of Team Foundation Server, it also supports Git these days, although I wouldn't recommend using it for anything.
Don't get me wrong: it works. You can technically do all your daily stuff through here. It's just not very user friendly, with its annoying browser-like navigation and UX from the paleolithic era. I never use it. When I code with Visual Studio, I rely on an external Git client. The only version control stuff I do inside Visual Studio is viewing a file's history. Which is also kinda clunky to use, especially compared to Rider.
In Rider, version control feels much more like a first-class citizen, rather than something that was bolted on as an afterthought. File history works great, for instance, letting you immediately see the history, and the diff between whatever commits you select:
There's also a nice commit editor:
I mean, seriously now, compare this to what Visual Studio has to work with:
That's just silly.
Another cool feature Rider has is that when you pull new changes, it gives you an overview of the files affected by the pull as well as their diff. It's one of those things I never even thought about before using Rider, and now I kinda miss it when I'm back in Visual Studio.
Without spending too much time on this topic, I can sum up the version control discussion like this: with Visual Studio, I rely on an external Git client; with Rider, I don't.
This has turned into a much bigger article than I'd originally intended. It's still not a complete comparison between Visual Studio 2019 and JetBrains Rider (nor is it supposed to be), but it focuses on the areas that I personally find the most important, and where I saw the biggest differences between the two IDEs.
I was motivated to create this comparison because I am disappointed in Visual Studio 2019. I think the new features and improvements it brought to the table with regards to the "core loop" of daily development are insignificant. When compared to Rider, I feel Visual Studio is starting to lag behind, with important areas being neglected and left unchanged across multiple Visual Studio versions.
My hope is that competition from Rider motivates Microsoft to improve Visual Studio more, and provide more creature comforts for developers. As it stands right now, Rider is simply the more enjoyable IDE to work with.