A nice improvement in .NET is the introduction of LINQ, in .NET 4.5. With that, working with data was simplified a lot and, when I go to a language that doesn’t have something like it, I feel lost (having to deal with for and foreach became painful for me :-).

The features available in LINQ made my code more synthetic and readable, but sometimes, there was something that wasn’t easily attained with the default features. Microsoft heard that and introduced new features, many of them I was expecting since a long time. These new features  came with no huge announcements, but, nevertheless, they are very nice improvements.

Index and Range parameters

Index and Ranges were introduced in C#8, they can ease a lot when you must get a subrange of an array or list:

var arr = Enumerable.Range(1,100).ToArray();
Console.WriteLine($"[{string.Join(",",arr[10..15])}]");
Console.WriteLine(arr[^1]);
var list = new List<int>(arr);
Console.WriteLine(list[^1]);
Console.WriteLine(list[^5]);

When you run this code, you will get something like this:

[11,12,13,14,15]
100
100
96

This is nice, but when you wanted to work with Linq, you were not able to use indexes and ranges. Until now. .NET 6 allows using Ranges and Indices in Linq queries. This is very nice, because when you wanted a subrange of an IEnumerable, you should do something like:

list.Skip(10).Take(5)

Now, you can do the same with:

list.Take(10..15)

Or take the last 10 elements with

list.Take(^10..)

You can also take a single element with ElementAt using Indices. To get the last element in the list, you can use:

list.ElementAt(^1)

Chunking

One thing that is very common is to divide our data in chunks, in order to present it in pieces, so the user does not have to scroll long lists of information. Until now, you had to program that by hand, which could lead to errors. With the new Chunk method, you can split your data in chunks. For example, if you want to split the data in blocks of seven elements, you can do:

var chunked = list.Chunk(7);

With this code, you will obtain something like

[1,2,3,4,5,6,7]
[8,9,10,11,12,13,14]
[15,16,17,18,19,20,21]
[22,23,24,25,26,27,28]
[29,30,31,32,33,34,35]
[36,37,38,39,40,41,42]
[43,44,45,46,47,48,49]
[50,51,52,53,54,55,56]
[57,58,59,60,61,62,63]
[64,65,66,67,68,69,70]
[71,72,73,74,75,76,77]
[78,79,80,81,82,83,84]
[85,86,87,88,89,90,91]
[92,93,94,95,96,97,98]
[99,100]

And you can get one chunk with

chunked.ElementAt(3)
chunked.ElementAt(^1)

Zipping

Sometimes you want to combine three Enumerables into one. Combining Enumerables is done using the Zip method, which allowed to combine only two items at once. .NET 6 introduced the possibility of zipping three sequences at once (if you want more sequences, you must chain Zip functions). For example, if you have these three enumerables:

var list1 = Enumerable.Range(1,100).Select(i => $"ID {i}").ToList();
var list2 = Enumerable.Range(1,100).Select(i => $"Name {i}").ToList();
var list3 = Enumerable.Range(1,100).Select(i => $"Address {i}").ToList();

You can combine it into an IEnumerable of tuples with three elements each with:

var zipped = list1.Zip(list2,list3);

One note, here: in .NET 5 you could use a function to zip two sequences int another one and generate anything else than a tuple. .NET 6 didn’t change that and, if you want to zip the three IEnumerables into an IEnumerable of a class, for example, you must still do something like this:

var zipped1 = list1.Zip(list2, (l1, l2) => new { ID = l1, Name = l2 })
    .Zip(list3, (l1, l2) => new { ID = l1.ID, Name = l1.Name, Address = l2 });

DistinctBy, ExceptBy, UnionBy, InterceptBy

One thing that I use a lot is the distinct operator, to get unique values in a sequence. Until now, when I had a class and wanted to get distinct values in a class by some field, and I’m not interested in the other fields, I had to do something like:

public record Person(string Name, int Age);
var people = new List
{
    new Person("John", 30 ),
    new Person("Peter", 40),
    new Person("Mary", 20 ),
    new Person("Jane", 30 ),
    new Person("Larry", 50),
    new Person("Anne", 50 ),
    new Person("Paul", 20),
};
var distinctByAge = people.GroupBy(p => p.Age).Select(g => g.Key);

That worked fine, but lacked clarity – the intent was not explicit and it was hard to understand – Why this GroupBy is there ?

In .NET 6, the DistinctBy comes to solve that. Now, you can use something like this to get the distinct values :

var distinctAges = people.DistinctBy(p => p.Age).Select(p => p.Age);

Now the intent is clear and the code is easier to follow.

You can also use ExceptBy, to filter a sequence depending on another, like in

var excludedAges = new List<int> {30,40};
var people1 = people.ExceptBy(excludedAges, p => p.Age);

One note, here. Due to the way ExceptBy is coded (it uses a HashSet), it will only add the first duplicate element in the result. In our code, it should show:

Person { Name = Mary, Age = 20 }
Person { Name = Larry, Age = 50 }
Person { Name = Anne, Age = 50 }
Person { Name = Paul, Age = 20 }

But it only shows:

Person { Name = Mary, Age = 20 }
Person { Name = Larry, Age = 50 }

If you want all items that don’t match the excluded ages, you should still go with:

var people2 = people.Where(p => !excludedAges.Contains(p.Age));

If you want to join two sequences, removing duplicates between them, you can use the UnionBy method. This code joins the two lists into another, removing the duplicates:

var people3 = new List<Person>
{
    new Person("John", 20 ),
    new Person("Peter", 25),
    new Person("Paul", 20 ),
    new Person("Ringo", 22 ),
    new Person("George", 23),
    new Person("Anne", 50 ),
    new Person("Mark", 20),
};
var people4 = people.UnionBy(people3, p => p.Name);

If you want the have the names present in both lists, you can use the IntersectBy method:

var includedAges = new List<int> {30,40};
var people5 = people.IntersectBy(includedAges, p => p.Age);

In the same way of the ExceptBy, the duplicates are not included. If you want to include them, you should use:

var people6 = people.Where(p => includedAges.Contains(p.Age));

MaxBy and MinBy

When using the methods Max and Min, the sequences should implement the IComparable interface, so they could be compared and the maximum and minimum evaluated. That posed a problem, especially if the class you wanted to compare didn’t implement the IComparable interface. Now, with MinBy and MaxBy you don’t have to use the IComparable and can use something like:

var minByAge = people.MinBy(p => p.Age);
var maxByAge = people.MaxBy(p => p.Age);

This code won’t show all the elements with minimum age. To get that, you should use something like

var minAge = people.Select(p => p.Age).Min();
var allMinByAge = people.Where(p => p.Age == minAge);
var maxAge = people.Select(p => p.Age).Max();
var allMaxByAge = people.Where(p => p.Age == maxAge);

FirstOrDefault, LastOrDefault, SingleOrDefault with a default parameter

These three functions returned Default(T) if the element was not found or the list was empty. This could pose a problem or extra checks if the element was not found. Now, we can set a default value when the element is not found and, in this case, we don’t have to deal with null checks:

var firstOrDefault = people.FirstOrDefault(p => p.Age == 25,new Person("Unknown",25));
var lastOrDefault = people.LastOrDefault(p => p.Age == 25,new Person("Unknown",25));
var singleOrDefault = people.SingleOrDefault(p => p.Age == 25,new Person("Unknown",25));

In all the three cases, the code will return a Person with name Unknown and Age = 25

TryGetNonEnumeratedCount

When you have an IEnumerable and you use the Count() method, it will enumerate the collection, even if it has another method to get the count in another way, thus penalizing the performance. For that, .NET 6 implemented the TryGetNonEnumeratedCount method to try to use another method to get the count, if available. This function will return true if a faster method was available, or false, if not. That way, you can take an action to use something more performant and avoid multiple enumerations. For example:

IEnumerable<Person> people7 = people;
Console.WriteLine(people7.TryGetNonEnumeratedCount(out int count));

Will return true, because The List implements the Count property to get the count. When  you have an IEnumerable, result of a Linq operation, like in

var people6 = people.Where(p => includedAges.Contains(p.Age));
Console.WriteLine(people6.TryGetNonEnumeratedCount(out int count1));

It will return false and the count1 variable will have the actual count of the sequence.

Conclusion

As you can see, there are several improvements to Linq in .NET 6. They were not huge improvements, but brought some ease to the development. I’m sure that I will use them a lot.

The sample code for this article is at https://github.com/bsonnino/LinqImprovements

You have an old .NET app and would like to upgrade to .NET 6, to be up-to date with the latest features and go forward with the app.

Every time you think about it, you notice that you have no time to do it and there are more important things to do. And you see that you’re stuck with an old .NET version. But, in this case, Microsoft has helped you to be able to accomplish your New Year’s resolutions. Now, you have the .NET upgrade assistant to help you to migrate to the latest .NET version: the >NET upgrade assistant.

With it, you can migrate your old .NET app to .NET 6 with very simple steps. It may not be a full migration, but it’s a starting point, which eases a lot the migration.

The first step is have dotnet installed in your machine. If you haven’t done so, you can install it from https://dotnet.microsoft.com/en-us/download/dotnet/6.0. Once installed, you can check the installed version by opening a terminal window and  typing the dotnet --versioncommand.

Then, you should install the upgrade assistant with

dotnet tool install -g upgrade-assistant

Once installed, you can use it with

upgrade-assistant

To demonstrate the procedure, I will use the project DiskAnalisys, from https://github.com/bsonnino/DiskAnalysis. This is a .NET 4.5 project and has many features that need to be upgraded: the FolderPicker, Charts, etc. The first step is to clone the project and run the solution in Visual Studio.

When you click the start button, you will be asked for a folder to analyze and, then, a list of files will be shown, ordered by size. On the other tabs, you can see a chart for the extensions and another for the contribution of each file for the total size:

You can check the .NET version in the properties for the project:

Now we can upgrade the project. Open a terminal window, change to the solution’s directory and type

upgrade-assistant analyze diskanalysis.sln

This will show a report for the upgrade of the project:

If you want the detailed output, you can use the -v option to the command line. In the project’s folder, you can see two files, AnalysisReport.sarif, a json file with the report and upgrade-assistant.clef, a copy of what was output to the screen.

The report shows that the project can be updated, so we will update it with

upgrade-assistant upgrade diskanalysis.sln

That will show the steps that will be followed in the upgrade:

You will move from one step to the other by pressing 1. The steps are:

1. Back up project

Backup the project, so it can be used, if the upgrade fails. The program asks for the path for the backup then copies the project

2. Convert project file to SDK style

Converts the csproj file from the old style to the new style. The new file is changed to something like this:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net452</TargetFramework>
    <OutputType>WinExe</OutputType>
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
    <UseWPF>true</UseWPF>
    <ImportWindowsDesktopTargets>true</ImportWindowsDesktopTargets>
  </PropertyGroup>
  <ItemGroup>
    <AppDesigner Include="Properties\" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="DotNetProjects.Wpf.Toolkit" Version="5.0.31" />
    <PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
    <PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
    <PackageReference Include="WPFFolderBrowser" Version="1.0.2" />
  </ItemGroup>
</Project>

3. Clean up NuGet package references

This task will clean up the NuGet package references and add the reference to Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers

4. Update TFM

This task will change the target framework in the csproj to net6.0-windows

5. Update NuGet Packages

This step will check if there are updates in the NuGet packages, update them and add a reference to Microsoft.Windows.Compatibility in the csproj

When you press 1 to go to the next step, the following steps are skipped

6. Add template files
7. Upgrade app config files
a. Convert Application Settings
b. Convert Connection Strings
c. Disable unsupported configuration sections
8. Update source code

a. Apply fix for UA0002: Types should be upgraded
b. Apply fix for UA0012: ‘UnsafeDeserialize()’ does not exist

And the conversion is finished. You must only press 1 to go to the last step:

9. Move to next project

There were no changes in the source code, just in the csproj. If you have your project still opened in Visual Studio, it will ask you to reload it. If you reload it, you will see that it changed to .NET 6 (if you are using VS 2019 you won’t see that, because VS 2019 doesn’t support .NET 6). If you go to the Solution Explorer, you will see that there are two warnings in the dependency packages:

The WPF Toolkit and the WPF Folder Browser packages don’t have the versions for .NET 6, but we’ll run the project to see if it runs fine. Once you run it, you can see that it woks fine, the same way as it did originally. But this time, you get a .NET 6 app running.

We can fix the issues and have a full .NET 6 app running with some extra effort. The WPF Toolkit has been updated and you can use the new packages from https://github.com/dotnetprojects/WpfToolkit. You just have to remove the old package and add the package DotNetProjects.WPF.Toolkit.DataVisualization. For the WPF Folder Browser, you don’t have an updated package, but you can use the source code from https://github.com/McNeight/WpfFolderBrowser.

Clone the repository, run the Upgrade Assistant in the project, to convert it to .NET 6 the same way you did with the main project and add it to the solution. Remove the WPFFolderBrowser package form the project and add a reference for the modified project. When you rebuild the solution, you will get an error Error CA0052 : No targets were selected.

You must edit the project file and remove this property group:

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
   <RunCodeAnalysis>true</RunCodeAnalysis>
   <CodeAnalysisRules>
   </CodeAnalysisRules>
   <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
   <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
 </PropertyGroup>

Once you do that, the project compiles fine, but when you try to run it, you get an error saying that WPFFolderBrowserDialog was not found. The only change here is to change the namespace in MainWindow.xaml.cs to WpfFolderBrowser.

Now you can run the project, it runs fine and there are no more compatibility issues.

As you can see, the upgrade assistant eases the migration to .NET 6 a lot. Sometimes, you will need an extra effort to have your app running, but it’s surely easier than doing it by hand. This is a nice step to evolve your apps and port them to the latest version of .NET.

The full code for this article is at https://github.com/bsonnino/UpgradeAssistant

In the last post I showed how to transform your Asp.Net Core MVC app into a PWA, thus allowing to install it and access the OS features. We can go a step further and transform it into a native app, using Electron. You may ask “Electron, what’s it ? I’ve never heard of it”. You may never heard of it, but I’m sure that you’ve already used it: VS Code, Teams and Slack are some examples of Electron apps: they are apps developed with web technologies and packaged with the Electron shell.

The two types of applications, although similar, are completely different. A PWA is an app provided by the browser and has the features that are available on the used browser – for example, if you install it from Chrome, it will have the features offered by Chrome (which may not be the same offered by Edge). An Electron app is a native app and is independent of the browser (as a matter of fact, an Electron app creates its own chromium window). It offers full OS interaction and is a real desktop application.

It also have its downsides: the app size is larger, as it will have all the support needed to create a desktop application and you will have to install it like a desktop application. Which one you should choose? It depends on what do you want – if you want a lightweight app with easy install with mostly web features, the a PWA is a way to go. If, on the other side, you want a desktop app that accesses the full OS and does not depend on the installed browser, then Electron is for you.

We will create a new Dotnet 6 Web App and convert it to an Electron app. For that, we will use Electron.NET, a wrapper around the full Electron, which provides a toolset for transforming your ASP.NET apps into an Electron app.

The first step is to create the app in the command line. Open Windows Terminal and type:

dotnet new mvc -o MvcElectron

This will create the app in the MvcElectron folder. Change to that folder and then install the Electron.NET NuGet package, with this command:

dotnet add package electronnet.api

Once you’ve installed it, we must tell to use Electron in the app. Open VS Code (using the command Code .), and, in Program.cs, add:

using ElectronNET.API;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseElectron(args);

Then, in the end of the file we’ll create the main Electron window:

if (HybridSupport.IsElectronActive)
{
    CreateElectronWindow();
}

app.Run();

async void CreateElectronWindow()
{
    var window = await Electron.WindowManager.CreateWindowAsync();
    window.OnClosed += () => Electron.App.Quit();
}

Then, we must install the Electron CLI with

dotnet tool install ElectronNET.CLI -g

Once we do that, we can use the tool using the electronizecommand. We will initialize the project with

electronize init

You should open a command line in the project’s folder. This command will add a manifest and will add it to your project file. Once you do that, you will be able to run it with

electronize start

This is a native app, with an icon and a full menu. You can notice that, in the View menu, you can open the developer tools for the web app. This app is not dependent on any browser, thus you can have any installed browser and it will run the same. The application is built for the OS where you ran the commands: if you are developing on Windows, it’s a Windows app. If you are developing on a Mac or on Linux, it will be a Mac or a Linux app.

Now, let’s customize it. The first customization is to change the window size. The window size is an option that you will pass when creating the main window. In Program.cs, add this code:

async void CreateElectronWindow()
{
    var options = new BrowserWindowOptions
    {
        Width = 1024,
        Height = 1024
    };
    var window = await Electron.WindowManager.CreateWindowAsync(options);
    window.OnClosed += () => Electron.App.Quit();
}

To use this code, you must add ElectronNET.API.Entities to the usings. After you save and restart the app, you will see that the window has the new size.

The next customization is the app menu. Unfortunately, there is no way to add a single menu option to the main menu, you should replace the entire menu. We create the menu before creating the window:

if (HybridSupport.IsElectronActive)
{
    CreateMenu();
    CreateElectronWindow();
}

The CreateMenu function is:

void CreateMenu()
{
    var fileMenu = new MenuItem[]
    {
        new MenuItem { Label = "Home", 
                                Click = () => Electron.WindowManager.BrowserWindows.First().LoadURL($"http://localhost:{BridgeSettings.WebPort}/") },
        new MenuItem { Label = "Privacy", 
                                Click = () => Electron.WindowManager.BrowserWindows.First().LoadURL($"http://localhost:{BridgeSettings.WebPort}/Privacy") },
        new MenuItem { Type = MenuType.separator },
        new MenuItem { Role = MenuRole.quit }
    };

    var viewMenu = new MenuItem[]
    {
        new MenuItem { Role = MenuRole.reload },
        new MenuItem { Role = MenuRole.forcereload },
        new MenuItem { Role = MenuRole.toggledevtools },
        new MenuItem { Type = MenuType.separator },
        new MenuItem { Role = MenuRole.resetzoom },
        new MenuItem { Role = MenuRole.zoomin },
        new MenuItem { Role = MenuRole.zoomout },
        new MenuItem { Type = MenuType.separator },
        new MenuItem { Role = MenuRole.togglefullscreen }
    };

    var menu = new MenuItem[] 
    {
        new MenuItem { Label = "File", Type = MenuType.submenu, Submenu = fileMenu },
        new MenuItem { Label = "View", Type = MenuType.submenu, Submenu = viewMenu }
    };

    Electron.Menu.SetApplicationMenu(menu);
}

As you can see, we added the two pages in the File menu and recreated the View menu. That way, we don’t need the menu and the footer in the main web page. In _Layout.cshtml, you can remove the header and the footer. That way, we are only using the main menu for changing pages.

We can go further and access the computer’s files. But before that, we’ll setup the watch feature, that will watch for any file changes and will reload the app automatically. For that you must start the app with

electronize start /watch

That way, every change will be detected and the app will be updated with no need to restart. In order to run the Dotnet 6 app in watch mode, a small change must be made in the file Properties\launchsettings.json: change the launch profile to use the port used by Electron:

"MvcElectron": {
    "commandName": "Project",
    "dotnetRunMessages": true,
    "launchBrowser": true,
    "applicationUrl": "http://localhost:8001",
    "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
    }
},

Once you do that, you can launch the app in watch mode. Now we can start making our changes in the main page.

We’ll get the 15 larger files in My Documents folder and display them in the page. For that, in the folder Models, create a new file FilesViewModel.cs and add this code:

namespace MvcElectron.Models;

public class FilesViewModel
{
    public List Files => new DirectoryInfo(Path)
        .GetFiles()
        .OrderByDescending(f => f.Length)
        .Take(15)
        .ToList();
        
    public string Path => Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
}

In the HomeController.cs file, we must pass the ViewModel to the view:

public IActionResult Index()
{
    return View(new FilesViewModel());
}

We must change the page Index.cshtml with this code:

@model FilesViewModel
@{
    ViewData["Title"] = "Files list";
}

<div>
    <h3>Files List @Model?.Path</h3>
    <table class="table table-sm table-striped">
        <thead class="thead-dark">
            <tr>
                <th scope="col" class="col-sm-4">Name</th>
                <th scope="col" class="col-sm-3">Size</th>
                <th scope="col" class="col-sm-4">Last Write</th>
                <th scope="col" class="col-sm-1"></th>
            </tr>
        </thead>
        <tbody>
            @foreach (var item in Model?.Files ?? new List<FileInfo>())
            {
                <tr class="align-middle">
                    <td scope="col" class="col-sm-4">@item.Name</td>
                    <td scope="col" class="col-sm-3">@item.Length</td>
                    <td scope="col" class="col-sm-4">@item.LastWriteTime</td>
                    <td scope="col" class="col-sm-1">
                        <button type="button" class="btn btn-primary" onclick="location.href='@Url.Action("DeleteFile","Home", new {fileName=item.Name})'">
                            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
                            class="bi bi-trash" viewBox="0 0 16 16">
                                <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0
                                V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" />
                                <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1
                                H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1
                                V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z" />
                            </svg>
                        </button>
                    </td>
                </tr>
            }
        </tbody>
    </table>
</div>

It will show the files in a table, ordered (the largest come first):

To delete a file, we must add a new action to the Home controller:

public IActionResult DeleteFile(string fileName)
{
    var viewModel = new FilesViewModel();
    viewModel.DeleteFile(fileName);
    return RedirectToAction("Index");
}

The FilesViewModel class must be changed to delete the file:

namespace MvcElectron.Models;

public class FilesViewModel
{
    public List<FileInfo> Files { get; private set; }

    public FilesViewModel()
    {
        Files = GetFiles();
    }

    private List<FileInfo> GetFiles()
    {
        return new DirectoryInfo(Path)
        .GetFiles()
        .OrderByDescending(f => f.Length)
        .Take(15)
        .ToList();
    }

    public string Path => Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);

    public void DeleteFile(string fileName)
    {
        var filePath = Path + "\\" + fileName;
        if (System.IO.File.Exists(filePath))
        {
            System.IO.File.Delete(filePath);
        }
        Files = GetFiles();
    }
}

Once you do that, you can click on the button to delete a file and the files list is refreshed. There is only one thing to do, now: generate an executable, so we can distribute the file. To do that, we need to use the command:

electronize build /target win

With that, Electron will build a package for Windows (if you need other platforms, you should change the target). It will generate an install file (the target file is pointed in the output), that can be installed and, then, you can run the program.

As you can see, you can transform your Asp.Net MVC app into a native file that can access the computer’s resources as any native app. This is a multi-platform app, you can generate it for Windows, Linux or Mac, with no change at all.

The full code for the app is at https://github.com/bsonnino/MvcElectron