Scott Hanselman

Optimizing an ASP.NET Core site with Chrome's Lighthouse Auditor

April 5, '18 Comments [5] Posted in ASP.NET | DotNetCore
Sponsored By

I'm continuing to update my podcast site. I've upgraded it from ASP.NET "Web Pages" (10 year old code written in WebMatrix) to ASP.NET Core 2.1 developed with VS Code. Here's some recent posts:

I was talking with Ire Aderinokun today for an upcoming podcast episode and she mentioned I should use Lighthouse (It's built into Chrome, can be run as an extension, or run from the command line) to optimize my podcast site. I, frankly, had not looked at that part of Chrome in a long time and was shocked and how powerful it was!

Performance 73, PWA 55, Accessbiolity 68, Best Practices 81, SEO 78

Lighthouse also told me that I was using an old version of jQuery (I know that) that had known security issues (I didn't know that!)

It told me about Accessibility issues as well, pointing out that some of my links were not discernable to a screen reader.

Some of these issues were/are easily fixed in minutes. I think I spent about 20 minutes fixing up some links, compressing a few images, and generally "tidying up" in ways that I knew wouldn't/shouldn't break my site. Those few minutes took my Accessibility and Best Practices score up measurably, but I clearly have some work to do around Performance. I never even considered my Podcast Site as a potential Progressive Web App (PWA) but now that I have a new podcast host and a nice embedded player, that may be a possibility for the future!

Performance 73, PWA 55, Accessbiolity 85, Best Practices 88, SEO 78

My largest issue is with my (aging) CSS. I'd like to convert the site to use FlexBox or a CSS Grid as well as fixed up my Time to First Meaningful Paint.

I went and updated my Archives page a while back with Lazy Image loading, but it was using jQuery and some older (4+ year old) techniques. I'll revisit those with modern techniques AND apply them to the grid of 16 shows on the site's home page as well.

There are opportunities to speed up my application using offscreen images

I have only just begun but I'll report back as I speed things up!

What tools do YOU use to audit your websites?


Sponsor: Get the latest JetBrains Rider for debugging third-party .NET code, Smart Step Into, more debugger improvements, C# Interactive, new project wizard, and formatting code in columns.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

Easier functional and integration testing of ASP.NET Core applications

March 30, '18 Comments [11] Posted in ASP.NET | DotNetCore
Sponsored By

.NET Test ExplorerIn ASP.NET 2.1 (now in preview) there's apparently a new package called Microsoft.AspNetCore.Mvc.Testing that's meant to help streamline in-memory end-to-end testing of applications that use the MVC pattern. I've been re-writing my podcast site at https://hanselminutes.com in ASP.NET Core 2.1 lately, and recently added some unit testing and automatic unit testing with code coverage. Here's a couple of basic tests. Note that these call the Razor Pages directly and call their OnGet() methods directly. This shows how ASP.NET Core is nicely factored for Unit Testing but it doesn't do a "real" HTTP GET or perform true end-to-end testing.

These tests are testing if visiting URLs like /620 will automatically redirect to the correct full canonical path as they should.

[Fact]
public async void ShowDetailsPageIncompleteTitleUrlTest()
{
// FAKE HTTP GET "/620"
IActionResult result = await pageModel.OnGetAsync(id:620, path:"");

RedirectResult r = Assert.IsType<RedirectResult>(result);
Assert.NotNull(r);
Assert.True(r.Permanent); //HTTP 301?
Assert.Equal("/620/jessica-rose-and-the-worst-advice-ever",r.Url);
}

[Fact]
public async void SuperOldShowTest()
{
// FAKE HTTP GET "/default.aspx?showId=18602"
IActionResult result = await pageModel.OnGetOldShowId(18602);

RedirectResult r = Assert.IsType<RedirectResult>(result);
Assert.NotNull(r);
Assert.True(r.Permanent); //HTTP 301?
Assert.StartsWith("/615/developing-on-not-for-a-nokia-feature",r.Url);
}

I wanted to see how quickly and easily I could do these same two tests, except "from the outside" with an HTTP GET, thereby testing more of the stack.

I added a reference to Microsoft.AspNetCore.Mvc.Testing in my testing assembly using the command-line equivalanet of "Right Click | Add NuGet Package" in Visual Studio. This CLI command does the same thing as the UI and adds the package to the csproj file.

dotnet add package Microsoft.AspNetCore.Mvc.Testing -v 2.1.0-preview1-final

It includes a new WebApplicationTestFixture that I point to my app's Startup class. Note that I can take store the HttpClient the TestFixture makes for me.

public class TestingMvcFunctionalTests : IClassFixture<WebApplicationTestFixture<Startup>>
{
public HttpClient Client { get; }

public TestingMvcFunctionalTests(WebApplicationTestFixture<Startup> fixture)
{
Client = fixture.Client;
}
}

No tests yet, just setup. I'm using SSL redirection so I'll make sure the client knows that, and add a test:

public TestingMvcFunctionalTests(WebApplicationTestFixture<Startup> fixture)
{
Client = fixture.Client;
Client.BaseAddress = new Uri("https://localhost");
}

[Fact]
public async Task GetHomePage()
{
// Arrange & Act
var response = await Client.GetAsync("/");

// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

This will fail, in fact. Because I have an API Key that is needed to call out to my backend system, and I store it in .NET's User Secrets system. My test will get an InternalServerError instead of OK.

Starting test execution, please wait...
[xUnit.net 00:00:01.2110048] Discovering: hanselminutes.core.tests
[xUnit.net 00:00:01.2690390] Discovered: hanselminutes.core.tests
[xUnit.net 00:00:01.2749018] Starting: hanselminutes.core.tests
[xUnit.net 00:00:08.1088832] hanselminutes_core_tests.TestingMvcFunctionalTests.GetHomePage [FAIL]
[xUnit.net 00:00:08.1102884] Assert.Equal() Failure
[xUnit.net 00:00:08.1103719] Expected: OK
[xUnit.net 00:00:08.1104377] Actual: InternalServerError
[xUnit.net 00:00:08.1114432] Stack Trace:
[xUnit.net 00:00:08.1124268] D:\github\hanselminutes-core\hanselminutes.core.tests\FunctionalTests.cs(29,0): at hanselminutes_core_tests.TestingMvcFunctionalTests.<GetHomePage>d__4.MoveNext()
[xUnit.net 00:00:08.1126872] --- End of stack trace from previous location where exception was thrown ---
[xUnit.net 00:00:08.1158250] Finished: hanselminutes.core.tests
Failed hanselminutes_core_tests.TestingMvcFunctionalTests.GetHomePage
Error Message:
Assert.Equal() Failure
Expected: OK
Actual: InternalServerError

Where do these secrets come from? In Development they come from user secrets.

public Startup(IHostingEnvironment env)
{
this.env = env;
var builder = new ConfigurationBuilder();

if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}
Configuration = builder.Build();
}

But in Production they come from the ENVIRONMENT. Are these tests Development or Production...I must ask myself.  They are Production unless told otherwise. I can override the Fixture and tell it to use another Environment, like "Development." Here is a way (given this preview) to make my own TestFixture by deriving and grabbing and override to change the Environment. I think it's too hard and should be easier.

Either way, the real question here is for me - do I want my tests to be integration tests in development or in "production." Likely I need to make a new environment for myself - "testing."

public class MyOwnTextFixture<TStartup> : WebApplicationTestFixture<Startup> where TStartup : class
{
public MyOwnTextFixture() { }

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Development");
}
}

However, my User Secrets still aren't loading, and that's where the API Key is that I need.

BUG?: There is either a bug here, or I don't know what I'm doing. I'm loading User Secrets in builder.AddUserSecrets<Startup> and later injecting the IConfiguration instance from builder.Build() and going "_apiKey = config["SimpleCastAPIKey"];" but it's null. The config that's injected later in the app isn't the same one that's created in Startup.cs. It's empty. Not sure if this is an ASP.NE Core 2.0 thing or 2.1 thing but I'm going to bring it up with the team and update this blog post later. It might be a Razor Pages subtlety I'm missing.
For now, I'm going to put in a check and manually fix up my Config. However, when this is fixed (or I discover my error) this whole thing will be a pretty nice little set up for integration testing.

I will add another test, similar to the redirect Unit Test but a fuller integration test that actually uses HTTP and tests the result.

[Fact]
public async Task GetAShow()
{
// Arrange & Act
var response = await Client.GetAsync("/620");

// Assert
Assert.Equal(HttpStatusCode.MovedPermanently, response.StatusCode);
Assert.Equal("/620/jessica-rose-and-the-worst-advice-ever",response.Headers.Location.ToString());
}

There's another issue here that I don't understand. Because have to set Client.BaseAddress to https://localhost (because https) and the Client is passed into fixture.Client, I can't set the Base address twice or I'll get an exception, as the Test's Constructor runs twice, but the HttpClient that's passed in as a lifecycler that's longer. It's being reused, and it fails when setting its BaseAddress twice.

Error Message:
System.InvalidOperationException : This instance has already started one or more requests. Properties can only be modified before sending the first request.

BUG? So to work around it I check to see if I've done it before. Which is gross. I want to set the BaseAddress once, but I am not in charge of the creation of this HttpClient as it's passed in by the Fixture.

public TestingMvcFunctionalTests(MyOwnTextFixture<Startup> fixture)
{
Client = fixture.Client;
if (Client.BaseAddress.ToString().StartsWith("https://") == false)
Client.BaseAddress = new Uri("https://localhost");
}

Another option is that I create a new client every time, which is less efficient and perhaps a better idea as it avoids any side effects from other tests, but also feels weird that I should have to do this, as the new standard for ASP.NET Core sites is to be SSL/HTTPS by default..

public TestingMvcFunctionalTests(MyOwnTextFixture<Startup> fixture)
{
Client = fixture.CreateClient(new Uri(https://localhost));
}

I'm still learning about how it all fits together, but later I plan to add in Selenium tests to have a full, complete, test suite that includes the browser, CSS, JavaScript, end-to-end integration tests, and unit tests.

Let me know if you think I'm doing something wrong. This is preview stuff, so it's early days!


Sponsor: Get the latest JetBrains Rider for debugging third-party .NET code, Smart Step Into, more debugger improvements, C# Interactive, new project wizard, and formatting code in columns.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

Command line "tab" completion for .NET Core CLI in PowerShell or bash

March 27, '18 Comments [14] Posted in DotNetCore
Sponsored By

Lots of people are using open source .NET Core and the "dotnet" command line, but few know that the .NET CLI supports command "tab" completion!

You can ensure you have it on .NET Core 2.0 with this test:

C:\Users\scott>  dotnet complete "dotnet add pac"
package

You can see I do, as it proposed "package" as the completion for "pac"

Now, just go into PowerShell and run:

notepad $PROFILE

And add this code to the bottom to register "dotnet complete" as the "argument completer" for the dotnet command.

# PowerShell parameter completion shim for the dotnet CLI 
Register-ArgumentCompleter -Native -CommandName dotnet -ScriptBlock {
    param($commandName, $wordToComplete, $cursorPosition)
        dotnet complete --position $cursorPosition "$wordToComplete" | ForEach-Object {
           [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
        }
}

Then just use it! You can do the same not only in PowerShell, but in bash, or zsh as well!

It's super useful for "dotnet add package" because it'll make smart completions like this:

It also works for adding/removing local project preferences as it is project file aware. Go set it up NOW, it'll take you 3 minutes.

RANDOM BUT ALSO USEFUL: "dotnet serve" - A simple command-line HTTP server.

Here's a useful little global tool - dotnet serve. It launches a server in the current working directory and serves all files in it. It's not kestrel, the .NET Application/Web Server. It's just a static webserver for development.

The latest release of dotnet-serve requires the 2.1.300-preview1 .NET Core SDK or newer. Once installed, run this command:

dotnet install tool --global dotnet-serve 

Then whenever I'm in a folder where I want to server something static (CSS, JS, PNGs, whatever) I can just

dotnet serve

It can also optionally open a web browser navigated to that localhost URL.

NOTE: Here's a growing list of .NET Global Tools.


Sponsor: Get the latest JetBrains Rider for debugging third-party .NET code, Smart Step Into, more debugger improvements, C# Interactive, new project wizard, and formatting code in columns.do

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

Automatic Unit Testing in .NET Core plus Code Coverage in Visual Studio Code

March 22, '18 Comments [13] Posted in DotNetCore | Open Source
Sponsored By

I was talking to Toni Edward Solarin on Skype yesterday about his open source spike (early days) of Code Coverage for .NET Core called "coverlet." There's a few options out there for cobbling together .NET Core Code Coverage but I wanted to see if I could use the lightest tools I could find and make a "complete" solution for Visual Studio Code that would work for .NET Core cross platform. I put my own living spike of a project up on GitHub.

Now, keeping in mind that Toni's project is just getting started and (as of the time of this writing) currently supports line and method coverage, and branch coverage is in progress, this is still a VERY compelling developer experience.

Using VS Code, Coverlet, xUnit, plus these Visual Studio Code extensions

Here's what we came up with.

Auto testing, code coverage, line coloring, test explorers, all in VS Code

There's a lot going on here but take a moment and absorb the screenshot of VS Code above.

  • Our test project is using xunit and the xunit runner that integrates with .NET Core as expected.
    • That means we can just "dotnet test" and it'll build and run tests.
  • Added coverlet, which integrates with MSBuild and automatically runs when you "dotnet test" if you "dotnet test /p:CollectCoverage=true"
    • (I think this should command line switch should be more like --coverage" but there may be an MSBuild limitation here.)

I'm interested in "The Developer's Inner Loop." . That means I want to have my tests open, my code open, and as I'm typing I want the solution to build, run tests, and update code coverage automatically the way Visual Studio proper does auto-testing, but in a more Rube Goldbergian way. We're close with this setup, although it's a little slow.

Coverlet can produce opencover, lcov, or json files as a resulting output file. You can then generate detailed reports from this. There is a language agnostic VS Code Extension called Coverage Gutters that can read in lcov files and others and highlight line gutters with red, yellow, green to show test coverage. Those lcov files look like this, showing file names, file numbers, coverage, and number of exceptions.

SF:C:\github\hanselminutes-core\hanselminutes.core\Constants.cs
DA:3,0
end_of_record
SF:C:\github\hanselminutes-core\hanselminutes.core\MarkdownTagHelper.cs
DA:21,5
DA:23,5
DA:49,5

I should be able to pick the coverage file manually with the extension, but due to a small bug, it's easier to just tell Coverlet to generate a specific file name in a specific format.

dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov /p:CoverletOutput=./lcov.info .\my.tests

The lcov.info files then watched by the VSCode Coverage Gutters extension and updates as the file changes if you click watch in the VS Code Status Bar.

You can take it even further if you add "dotnet watch test" which will compile and re-run tests if code changes:

dotnet watch --project .\my.tests test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov /p:CoverletOutput=./lcov.info 

I can run "WatchTests.cmd" in another terminal, or within the VS Code integrated terminal.

tests automatically running as code changes

NOTE: If you're doing code coverage you'll want to ensure your tests and tested assembly are NOT the same file. You might be able to get it to work but it's easier to keep things separate.

Next, add in the totally under appreciated .NET Core Test Explorer extension (this should have hundreds of thousands of downloads - it's criminal) to get this nice Test Explorer pane:

A Test Explorer tree view in VS Code for NET Core projects

Even better, .NET Test Explorer lights up some "code lens" style interfaces over each test as well as a green checkmark for passing tests. Having "debug test" available for .NET Core is an absolute joy.

Check out "run test" and "debug test"

Finally we make some specific improvements to the .vscode/tasks.json file that drives much of VS Code's experience with our app. The "BUILD" label is standard but note both the custom "test" and "testwithcoverage" labels, as well as the added group with kind: "test."

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "build",
            "command": "dotnet",
            "type": "process",
            "args": [
                "build",
                "${workspaceFolder}/hanselminutes.core.tests/hanselminutes.core.tests.csproj"
            ],
            "problemMatcher": "$msCompile",
            "group": {
                "kind": "build",
                "isDefault": true
            }
        },
        {
            "label": "test",
            "command": "dotnet",
            "type": "process",
            "args": [
                "test",
                "${workspaceFolder}/hanselminutes.core.tests/hanselminutes.core.tests.csproj"
            ],
            "problemMatcher": "$msCompile",
            "group": {
                "kind": "test",
                "isDefault": true
            }
        },
        {
            "label": "test with coverage",
            "command": "dotnet",
            "type": "process",
            "args": [
                "test",
                "/p:CollectCoverage=true",
                "/p:CoverletOutputFormat=lcov",
                "/p:CoverletOutput=./lcov.info",
                "${workspaceFolder}/hanselminutes.core.tests/hanselminutes.core.tests.csproj"
            ],
            "problemMatcher": "$msCompile",
            "group": {
                "kind": "test",
                "isDefault": true
            }
        },
    ]
}

This lets VS Code know what's for building and what's for testing, so if I use the Command Palette to "Run Test" then I'll get this dropdown that lets me run tests and/or update coverage manually if I don't want the autowatch stuff going.

Test or Test with Coverage

Again, all this is just getting started but I've applied it to my Podcast Site that I'm currently rewriting and the experience is very smooth!

Here's a call to action for you! Toni is just getting started on Coverlet and I'm sure he'd love some help. Head over to the Coverlet github and don't just file issues and complain! This is an opportunity for you to get to know the deep internals of .NET and create something cool for the larger community.

What are your thoughts?


Sponsor: Get the latest JetBrains Rider for debugging third-party .NET code, Smart Step Into, more debugger improvements, C# Interactive, new project wizard, and formatting code in columns.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

Setting up Application Insights took 10 minutes. It created two days of work for me.

March 13, '18 Comments [15] Posted in ASP.NET | Azure | DotNetCore
Sponsored By

I've been upgrading my podcast site from a 10 year old WebMatrix site to modern open-source ASP.NET Core with Razor Pages. The site is now off the IIS web server and  running cross-platform in Azure.

I added Application Insights to the site in about 10 min just a few days ago. It was super easy to setup and basically automatic in Visual Studio 2017 Community. I left the defaults, installed a bit of script on the client, and enabled the server-side profiler, and AppInsights already found a few interesting things.

It took 10 minutes to set up App Insights. It took two days (and work continues) to fix what it found. I love it. This tool has already given me a deeper insight into how my code runs and how it's behaving - and I'm just scratching the service. I'll need to do some videos and/or more blog posts to dig deeper. Truly, you need to try it.

Slow performance in other countries

I could fill this blog post with dozens of awesome screenshots of the useful charts, graphs, and filters that I got by just turning on AppInsights. But the most interesting part is that I turned it on really expecting nothing. I figured I'd get some "Google Analytics"-type behavior.

Then I got this email:

Browser Time is slow in Bangladesh

Huh. I had set up the Azure CDN at images.hanselminutes.com to handle all the faces for each episode. I then added lazy loading so that the webite only loads the images that enter the browser's viewport. I figured I was pretty much done.

However I didn't really think about the page itself as it loads for folks from around the world - given that it's hosted on Azure in the West US.

18.4 secs to load the page in Bangladesh

Ideally I'd want the site to load in less than a second, but this is my archives page with 600 shows so it's pretty heavy.

That's some long load times

Yuck. I have a few options. I could pay and load up another copy of the site in South Asia and then do some global load balancing. However, I'm hosting this on a single small (along with a dozen other sites) so I don't want to really pay much to fix this.

I ended up signing up for a free account at CloudFlare and set up caching for my HTML. The images stay the same. served by the Azure CDN.

Lots of requests from Cloudflare

Fixing Random and regular Server 500 errors

I left the site up for a while and came back later to a warning. You can see my site availability is just 93%. Note that there's "2 Servers?" That's because one is my local machine! Very cool that AppInsights also (optionally) tracks your local development server as well.

1 Alert!

When I dig in I see a VERY interesting sawtooth pattern.

Pro Tip - Recognizing that a Sawtooth Pattern is a Bad Thing (tm) is an important DevOps thing. Why is this happening regularly? Is it exactly regularly (like every 4 hours on a schedule?) or somewhat regularly (like a garbage collection issue?)

What do these operations have in common? Look closely.

scarygraph

It's not a GET it's a HEAD. Remember that HTTP Verbs are more than GET, POST, PUT, DELETE. There's also HEAD. It literally is a HEADer call. Like a GET, but no body.

HTTP HEAD - The HEAD method is identical to GET except that the server MUST NOT return a message-body in the response.

I installed HTTPie - which is like curl or wget for humans - and issue a HEAD command from my local machine while under the debugger.

C:>http --verify=no HEAD https://localhost:5001
HTTP/1.1 500 Internal Server Error
Content-Type: text/html; charset=utf-8
Date: Tue, 13 Mar 2018 03:41:51 GMT
Server: Kestrel

Ok that is bad. See the 500? I check out AppInsights and see it has the full call stack. See it's getting a NullReferenceException as it tries to Render() the Razor page?

Null Reference Exception

It turns out since I'm using Razor Pages, I have implemented "OnGet" where I do my data base work then pass a model to the pages to generate HTML. However, if someone issues a HEAD, then the pages still run but the local data work never happened (I have no OnHead() call). I have a few options here. I could handle HEAD myself. I could no-op it, but that'd be a lie.

THOUGHT: I think this behavior is sub-optimal. While GET and POST are distinct and it makes sense to require an OnGet() and OnPost(), I think that HEAD is special. It's basically a GET with a "don't return the body" flag set. So why not have Razor Pages automatically delegate OnHead to OnGet, unless there's an explicit OnHead() declared? I'll file an issue on GitHub because I don't like this behavior and I find it counter-intuitive. I could also register a global IPageFilter to make this work for all my site's pages.

The simplest thing to do is just to delegate the OnHead to to the OnGet handler.

public Task OnHeadAsync(int? id, string path) => OnGetAsync(id, path);

Then double check and test it with HTTPie:

C:\>http --verify=no HEAD https://localhost:5001
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Date: Tue, 13 Mar 2018 03:53:55 GMT
Request-Context: appId=cid-v1:e310025f-88e9-4133-bc15-e775513c67ac
Server: Kestrel

Bonus - Application Map

Since I have AppInsights enabled on both the client and the server, I can see this cool live Application Map. I'll check again in a few days to see if I have fewer errors. You can see where my Podcast Site calls into the backend data service at Simplecast.

An application map that shows all the components, both client and server

I saw a few failures in my call to SimpleCast's API as I was failing to consistently set my API key. Everything in this map can be drilled down into.

Bonus - Web Performance Testing

I figured while I was in the Azure Portal I would also take advantage of the free performance testing. I did a simulated aggressive 250 users beating on the site. Average response time is 1.22 seconds and I was doing over 600 req/second.

38097 successful calls

I am learning a ton of stuff. I have more things to fix, more improvements to make, and more insights to dig into. I LOVE that it's creating all this work for me because it's giving me a better application/website!

You can get a free Azure account at http://azure.com/free or check out Azure for Startups https://azure.microsoft.com/overview/startups/ and get a bunch of free Azure time. AppInsights works with Node, Docker, Java, ASP.NET, ASP.NET Core, and other platforms. It even supports telemetry in Electron or Windows Apps.


Sponsor: Get the latest JetBrains Rider for debugging third-party .NET code, Smart Step Into, more debugger improvements, C# Interactive, new project wizard, and formatting code in columns.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb
Page 1 of 11 in the DotNetCore category Next Page

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.