Nowadays, a very common scenario is that you have your WPF app ready and running for some time, and your boss tells you that it’s time to go further and port this app to the web, to have a larger market and be run in multiple platforms.

This is not a simple thing, because WPF is not multi-platform and, although it was ported to .NET Core, it’s still a Windows Platform.

You can think of rewriting the whole app, but even if you decide to use Asp.Net Core and Razor pages, or Blazor, you will have a huge effort to do it, because writing the UI in these platforms is completely different than using XAML.

Xamarin is a viable alternative, it’s close to WPF and it uses XAML, but it’s not a web platform, you can only write native apps for iOs, Android or Mac.

But things are not lost, at all. The guys at Uno Platform (https://www.platform.uno) created a nice project that uses WebAssembly to run the code you’ve created on the browser.

And what is WebAssembly? WebAssembly is a technology that allow browsers to run non-javascript code. When people hear this, they usually think of browsers plugins, like Silverlight or Adobe Flash. In fact, these are completely different. While the plugins were apps created to run in the browser, they were installed and supported by the vendors, and could suffer all sort of security vulnerabilities and were, at some point, abandoned by the browsers.

WebAssembly, on the other side, is an open standard fully supported by the browsers and a web standard. You can create code in may languages and compile it into a wasm module, thus running your C++/Rust code in a browser. This link shows a list of languages that are used in WebAssembly.

Uno Platform uses WebAssembly to run UWP code in the web. So, if you have an UWP program, you can compile it with Uno and run it on the web, with minimal changes. That’s great when you want to port your UWP app to the web.

You may have noticed that in the previous paragraph I didn’t mention WPF, but only UWP/WinUI. Uno Platform works with UWP projects, and not WPF. But UWP/WinUI is still XAML and has many similarities with WPF. So, while it’s not a direct port, it’s not a complete rewrite either. Let’s see what must be done to port a WPF program to the web using Uno Platform.

For this article, I will be using the WPF project that shows how to use MVVM with the MVVM Toolkit, which I’ve shown in this article. The source code for this article is here.

Installing UNO Platform

To install the UNO Platform in Visual Studio, you need to have the UWP, Xamarin and Asp.NET workloads installed. You can open the Visual Studio installer and verify if the three workloads are installed:

One other prerequisite is to have .NET 5 SDK or later installed. If you haven’t done so, you can install .NET 5 from https://dotnet.microsoft.com/download/dotnet-core/5.0 or .NET 6 from https://dotnet.microsoft.com/en-us/download/dotnet/6.0.

Once they are installed, in Visual Studio, go to Extensions > Manage Extensions and install Uno Platform Templates.

With the extension installed, you can create a new solution using Uno:

We will create a new Multi-Platform App (Uno Platform|.net 6).

When you create the app, you will see multiple projects there:

As you can see, there are projects for Mobile (Android, iOS and Mac, GTK, WPF, UWP and Wasm). We will be using the Wasm project, so you should select it as the startup project. When you run it, it will open a browser window with the app:

It doesn’t seem too much, but when you think that the underlying code is XAML, that’s pretty nice!

Converting the WPF Project

As you can see in the Solution Explorer, there is a project .Shared, that contains the shared resources for our project. Putting your pages here will share the pages for all projects in the solution.

Before we port the solution, we should port the customer lib, that contains the classes and the repository. In fact this is an easy port, we don’t have to do anything, as it’s already a .NET Standard library.

Just add the files for the project in the same folder of the other projects and add the new project to the solution.

After that, you can add the project as a reference to the other projects you are creating.

Then, you should create the ViewModel for the project. Before that, we will have to add the package Microsoft.Toolkit.Mvvm to all the projects you are using. In the Solution Explorer, right-click the dependencies node in the Wasm project and select Manage Nuget Packages and add the Microsoft.Toolkit.Mvvm package. You must also add the reference to the other projects.

Then, create a folder named ViewModel in the Shared project and add the file MainViewModel.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using CustomerLib;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;

namespace MVVMUnoApp.ViewModel
{
    public class MainViewModel : ObservableObject
    {
        private readonly ICustomerRepository _customerRepository;
        private readonly IEnumerable<Customer> _allCustomers;
        private Customer _selectedCustomer;

        public MainViewModel(ICustomerRepository customerRepository)
        {
            _customerRepository = customerRepository ??
                                  throw new ArgumentNullException("customerRepository");
            _allCustomers = _customerRepository.Customers;
            AddCommand = new RelayCommand(DoAdd);
            RemoveCommand = new RelayCommand(DoRemove, () => SelectedCustomer != null);
            SaveCommand = new RelayCommand(DoSave);
            SearchCommand = new RelayCommand<string>(DoSearch);
        }

        public IEnumerable<Customer> Customers {get; private set;}

        public Customer SelectedCustomer
        {
            get => _selectedCustomer;
            set
            {
                SetProperty(ref _selectedCustomer, value);
                RemoveCommand.NotifyCanExecuteChanged();
            }
        }

        public IRelayCommand AddCommand { get; }
        public IRelayCommand RemoveCommand { get; }
        public IRelayCommand SaveCommand { get; }
        public IRelayCommand<string> SearchCommand { get; }

        private void DoAdd()
        {
            var customer = new Customer();
            _customerRepository.Add(customer);
            SelectedCustomer = customer;
            OnPropertyChanged("Customers");
        }

        private void DoRemove()
        {
            if (SelectedCustomer != null)
            {
                _customerRepository.Remove(SelectedCustomer);
                SelectedCustomer = null;
                OnPropertyChanged("Customers");
            }
        }

        private void DoSave()
        {
            _customerRepository.Commit();
        }

        private void DoSearch(string textToSearch)
        {
            if (!string.IsNullOrWhiteSpace(textToSearch))
                Customers = _allCustomers.Where(c => ((Customer)c).Country.ToLower().Contains(textToSearch.ToLower()));
            else
                Customers = _allCustomers;
            OnPropertyChanged("Customers");
        }
    }
}

This file is almost the same as the one in the original project, with a change in DoSearch due to the fact that the CollectionViewSource works in a different way in UWP/WinUI than in WPF. For that, we removed the code that filters CollectionViewSource:

private void DoSearch(string textToSearch)
{
    var coll = CollectionViewSource.GetDefaultView(Customers);
    if (!string.IsNullOrWhiteSpace(textToSearch))
        coll.Filter = c =>
            ((Customer)c).Country.ToLower().Contains(textToSearch.ToLower());
    else
        coll.Filter = null;
}

And did an explicit filter on the customer list returned by the repository.

The next step is to add the views to the Shared project. The main view uses a DataGrid that doesn’t exist in UWP/WinUI, but there is a solution, here: add the DataGrid of the Windows Control Toolkit, that works fine in UWP/WinUI. For that, add the NuGet reference to Uno.Microsoft.Toolkit.Uwp.UI.Controls.Datagrid to all the used projects. In the UWP project, you should not add this reference, but add Microsoft.Toolkit.Uwp.UI.Controls.Datagrid.

Then, change the code in MainPage.xaml in the Shared project to:

<Page
    x:Class="MVVMUnoApp.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
    xmlns:local="using:MVVMUnoApp"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"    
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="40" />
            <RowDefinition Height="*" />
            <RowDefinition Height="2*" />
            <RowDefinition Height="50" />
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="Country" VerticalAlignment="Center" Margin="5"/>
            <TextBox x:Name="searchText" VerticalAlignment="Center" Margin="5,3" Width="250" VerticalContentAlignment="Center"/>
            <Button x:Name="PesqBtn" Content="Find" Width="75" Margin="10,5" VerticalAlignment="Center" 
                    Command="{Binding SearchCommand}" CommandParameter="{Binding ElementName=searchText,Path=Text}"/>
        </StackPanel>
        <controls:DataGrid AutoGenerateColumns="False" x:Name="master" Grid.Row="1" 
                  ItemsSource="{Binding Customers}" SelectedItem="{Binding SelectedCustomer, Mode=TwoWay}">
            <controls:DataGrid.Columns>
                <controls:DataGridTextColumn x:Name="customerIDColumn" Binding="{Binding CustomerId}" Header="Customer ID" />
                <controls:DataGridTextColumn x:Name="companyNameColumn" Binding="{Binding CompanyName}" Header="Company Name" Width="160" />
                <controls:DataGridTextColumn x:Name="contactNameColumn" Binding="{Binding ContactName}" Header="Contact Name" Width="160" />
                <controls:DataGridTextColumn x:Name="contactTitleColumn" Binding="{Binding ContactTitle, Mode=TwoWay}" Header="Contact Title"  />
                <controls:DataGridTextColumn x:Name="addressColumn" Binding="{Binding Address}" Header="Address" Width="130" />
                <controls:DataGridTextColumn x:Name="cityColumn" Binding="{Binding City}" Header="City" />
                <controls:DataGridTextColumn x:Name="regionColumn" Binding="{Binding Region}" Header="Region" />
                <controls:DataGridTextColumn x:Name="postalCodeColumn" Binding="{Binding PostalCode}" Header="Postal Code" />
                <controls:DataGridTextColumn x:Name="countryColumn" Binding="{Binding Country}" Header="Country" />
                <controls:DataGridTextColumn x:Name="faxColumn" Binding="{Binding Fax}" Header="Fax" Width="100" />
                <controls:DataGridTextColumn x:Name="phoneColumn" Binding="{Binding Phone}" Header="Phone" Width="100" />
            </controls:DataGrid.Columns>
        </controls:DataGrid>
        <local:Detail Grid.Row="2" DataContext="{Binding ElementName=master, Path=SelectedItem, Mode=OneWay}" Margin="5" x:Name="detail"/>
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="5" Grid.Row="3">
            <Button Width="75" Margin="5,0" Content="Add" Command="{Binding AddCommand}" />
            <Button Width="75" Margin="5,0" Content="Remove" Command="{Binding RemoveCommand}" />
            <Button Width="75" Margin="5,0" Content="Save" Command="{Binding SaveCommand}" />
        </StackPanel>

    </Grid>
</Page>

In this case, there were almost no changes to do: the Toolkit DataGrid is very similar to the one in WPF. We must set the DataContext property in MainPage.xaml.cs:

public MainPage()
{
    this.InitializeComponent();
    DataContext = App.Current.MainVM;
}

We are using the same code we’ve used in the WPF project. For that, we must use the Dependency Injection provided in the MVVM toolkit in App.xaml.cs:

public App()
{
    InitializeLogging();

    this.InitializeComponent();
    
    var services = new ServiceCollection();

    services.AddSingleton<ICustomerRepository, CustomerRepository>();
    services.AddSingleton<MainViewModel>();

    Services = services.BuildServiceProvider();

#if HAS_UNO || NETFX_CORE
    this.Suspending += OnSuspending;
#endif
}
public new static App Current => (App)Application.Current;
public IServiceProvider Services { get; }
public MainViewModel MainVM => Services.GetService<MainViewModel>();

As you can see, there are no changes here.

The next step is to add the Details page, which has almost not changes from the WPF project:

<UserControl
    x:Class="MVVMUnoApp.Detail"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Name="detailControl"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">

    <Grid>
       
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <TextBlock Text="Customer Id:" Grid.Column="0" Grid.Row="0"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="0"   Margin="3" Name="customerIdTextBox" Text="{Binding CustomerId, Mode=TwoWay}" VerticalAlignment="Center"  />
            <TextBlock Text="Company Name:" Grid.Column="0" Grid.Row="1"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="1"   Margin="3" Name="companyNameTextBox" Text="{Binding CompanyName, Mode=TwoWay}" VerticalAlignment="Center"  />
            <TextBlock Text="Contact Name:" Grid.Column="0" Grid.Row="2"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="2"   Margin="3" Name="contactNameTextBox" Text="{Binding ContactName, Mode=TwoWay}" VerticalAlignment="Center"  />
            <TextBlock Text="Contact Title:" Grid.Column="0" Grid.Row="3"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="3"   Margin="3" Name="contactTitleTextBox" Text="{Binding ContactTitle, Mode=TwoWay}" VerticalAlignment="Center"  />
            <TextBlock Text="Address:" Grid.Column="0" Grid.Row="4" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="4" Margin="3" Name="addressTextBox" Text="{Binding Address, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="City:" Grid.Column="0" Grid.Row="5"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="5"   Margin="3" Name="cityTextBox" Text="{Binding City, Mode=TwoWay}" VerticalAlignment="Center"  />
            <TextBlock Text="Postal Code:" Grid.Column="0" Grid.Row="6"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="6"   Margin="3" Name="postalCodeTextBox" Text="{Binding PostalCode, Mode=TwoWay}" VerticalAlignment="Center"  />
            <TextBlock Text="Region:" Grid.Column="0" Grid.Row="7"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="7"   Margin="3" Name="regionTextBox" Text="{Binding Region, Mode=TwoWay}" VerticalAlignment="Center"  />
            <TextBlock Text="Country:" Grid.Column="0" Grid.Row="8"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="8"   Margin="3" Name="countryTextBox" Text="{Binding Country, Mode=TwoWay}" VerticalAlignment="Center"  />
            <TextBlock Text="Phone:" Grid.Column="0" Grid.Row="9"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="9"   Margin="3" Name="phoneTextBox" Text="{Binding Phone, Mode=TwoWay}" VerticalAlignment="Center"  />
            <TextBlock Text="Fax:" Grid.Column="0" Grid.Row="10"  Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="10"   Margin="3" Name="faxTextBox" Text="{Binding Fax, Mode=TwoWay}" VerticalAlignment="Center"  />
        </Grid>
</UserControl>

The only change is to remove the ValidatesOnExceptions=true and NotifyOnValidationError=true from the textboxes, because UWP/WinUI doesn’t have native validation. If you want to add validation to UWP/WinUI, you should check the Template10 Validation project.

Now, the project is ok and ready to be run. When you run it, it shows the main window, but doesn’t show any data:

That’s really strange. If you change the startup project to UWP and run it, it runs fine:

We should investigate a little more. When using the Wasm project, we have a web application. In this case, do the web tools work ? Run the Wasm project again and press F12 to open the dev tools:

As you can see, we have the diagnostic tools available and they show us that Customers.xml is not found. That makes sense, as we’re not running a local app, but a web app, and local files aren’t available. We should use another mechanism to get our data. Searching in the internet, we come to this site, which says that the Storage files available in UWP are also available in Wasm, so we should change our code from

var doc = XDocument.Load("Customers.xml");

To

var file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Customers.xml"));
var content = await FileIO.ReadTextAsync(file);
var doc = XDocument.Parse(content);

Unfortunately, this way is not supported in Wasm, when using a library file. We have two solutions here:

  • Create a server that will serve the data and use REST calls to the server in the repository
  • Add the repository files to the Shared project and use the UWP storage method

There is no doubt that in a normal project I would go to the first solution. It’s the way to go when you are creating web applications, it’s more flexible and extensible. But, as I’m only showing you how to port a small WPF project to the web and I don’t want to create a server and change the repository now (it would be beyond the scope of the article), I won’t do it (if you want to see that solution, write in the comments, if there are enough requests, I will do it 😃).

So, what we’ll do is to create a new folder named Repository in the Shared project and add all the files that are in the library to this folder. After that, you can delete the library from the solution.

If you run the application, you still won’t see the data, but the error will disappear. The issue now is that we have a synchronization problem: the data now is obtained in async mode (in the original project it was obtained in sync mode). While that is happening, the ViewModel is being built and asks for the customers, which haven’t been read, so the _allCustomers field will be empty and nothing will be shown.

To fix this issue, we must have a function named GetCustomersAsync that retrieves the customers and use it in the ViewModel.

The ICustomerRepository interface will be:

public interface ICustomerRepository
{
    bool Add(Customer customer);
    bool Remove(Customer customer);
    bool Commit();
    Task<IEnumerable<Customer>> GetCustomersAsync();
}

And the CustomerRepository class becomes:

public class CustomerRepository : ICustomerRepository
{
    private IList<Customer> customers;

    public async Task<IEnumerable<Customer>> GetCustomersAsync()
    {
        var file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Customers.xml"));
        var content = await FileIO.ReadTextAsync(file);
        var doc = XDocument.Parse(content);
        customers = new ObservableCollection<Customer>((from c in doc.Descendants("Customer")
                                                        select new Customer
                                                        {
                                                            CustomerId = GetValueOrDefault(c, "CustomerID"),
                                                            CompanyName = GetValueOrDefault(c, "CompanyName"),
                                                            ContactName = GetValueOrDefault(c, "ContactName"),
                                                            ContactTitle = GetValueOrDefault(c, "ContactTitle"),
                                                            Address = GetValueOrDefault(c, "Address"),
                                                            City = GetValueOrDefault(c, "City"),
                                                            Region = GetValueOrDefault(c, "Region"),
                                                            PostalCode = GetValueOrDefault(c, "PostalCode"),
                                                            Country = GetValueOrDefault(c, "Country"),
                                                            Phone = GetValueOrDefault(c, "Phone"),
                                                            Fax = GetValueOrDefault(c, "Fax")
                                                        }).ToList());
        return customers;
    }

    #region ICustomerRepository Members

    public bool Add(Customer customer)
    {
        if (customers.IndexOf(customer) < 0)
        {
            customers.Add(customer);
            return true;
        }
        return false;
    }

    public bool Remove(Customer customer)
    {
        if (customers.IndexOf(customer) >= 0)
        {
            customers.Remove(customer);
            return true;
        }
        return false;
    }

    public bool Commit()
    {
        try
        {
            var doc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
            var root = new XElement("Customers");
            foreach (Customer customer in customers)
            {
                root.Add(new XElement("Customer",
                                      new XElement("CustomerID", customer.CustomerId),
                                      new XElement("CompanyName", customer.CompanyName),
                                      new XElement("ContactName", customer.ContactName),
                                      new XElement("ContactTitle", customer.ContactTitle),
                                      new XElement("Address", customer.Address),
                                      new XElement("City", customer.City),
                                      new XElement("Region", customer.Region),
                                      new XElement("PostalCode", customer.PostalCode),
                                      new XElement("Country", customer.Country),
                                      new XElement("Phone", customer.Phone),
                                      new XElement("Fax", customer.Fax)
                             ));
            }
            doc.Add(root);
            doc.Save("Customers.xml");
            return true;
        }
        catch (Exception)
        {
            return false;
        }
    }

    #endregion

    private static string GetValueOrDefault(XContainer el, string propertyName)
    {
        return el.Element(propertyName)?.Value ?? string.Empty;
    }
}

To use this repository, we should change the ViewModel to:

private readonly ICustomerRepository _customerRepository;
private IEnumerable<Customer> _allCustomers;
private Customer _selectedCustomer;

public MainViewModel(ICustomerRepository customerRepository)
{
    _customerRepository = customerRepository ??
                          throw new ArgumentNullException("customerRepository");
    GetCustomers();
    AddCommand = new RelayCommand(DoAdd);
    RemoveCommand = new RelayCommand(DoRemove, () => SelectedCustomer != null);
    SaveCommand = new RelayCommand(DoSave);
    SearchCommand = new RelayCommand<string>(DoSearch);
}

private async void GetCustomers()
{
    _allCustomers = await _customerRepository.GetCustomersAsync();
    Customers = _allCustomers;
    OnPropertyChanged("Customers");
}

We must call the async method in the ViewModel constructor and, in this case, we cannot await for the call, so we fire the call and, when the data is ready, it fires the INotifyPropertyChanged event and populates the data.

If we run the program, we see that it opens a new browser window with the program:

We’ve ported our WPF program to the web using Uno Platform. Nice, no ? We can leverage our XAML expertise and port it to the web. It’s not a direct change but, nevertheless, it’s way less trouble than a complete rewrite. Uno Platform did a great work to port UWP to WebAssembly! And you get some extra bonuses: you still have an UWP and a WPF project running with the same code. The original Uno template also allows you to run the code in Android or iOs (but I removed these from the code for this article).

The full code for this article is in https://github.com/bsonnino/MVVMUnoApp

There are some times when you need to get the disk information in your system, to check what’s happening, for inventory check, or even to know the free and available space.
Getting the free and available space is fairly easy in .NET, just use the GetDrives method of the DriveInfo class:

DriveInfo[] drives = DriveInfo.GetDrives();

foreach (DriveInfo drive in drives)
{
    Console.WriteLine($"Name: {drive.Name}");
    Console.WriteLine($"VolumeLabel: {drive.VolumeLabel}");
    Console.WriteLine($"RootDirectory: {drive.RootDirectory}");
    Console.WriteLine($"DriveType: {drive.DriveType}");
    Console.WriteLine($"DriveFormat: {drive.DriveFormat}");
    Console.WriteLine($"IsReady: {drive.IsReady}");
    Console.WriteLine($"TotalSize: {drive.TotalSize}");
    Console.WriteLine($"TotalFreeSpace: {drive.TotalFreeSpace}");
    Console.WriteLine($"AvailableFreeSpace: {drive.AvailableFreeSpace}");
    Console.WriteLine();
}

If you run this code, you will get the info for all drives in your system:

This is ok for most apps, but sometimes you want more than that. If, for example, you want an inventory of your physical disks (not the logical disks, like DriveInfo gives) ?

The physical and logical structure of your disks can differ a lot. For example, if you have a disk with three partitions, you will have one physical disk and three logical disks.

On the other side, you can mount a drive in an empty folder and have two physical disks and one logical disk. And we are not considering multiple OSs, here.

.NET doesn’t have any dedicated class for the physical disk structure, so we should find another way to do it. In fact, there is a way to get this information in Windows, called WMI (Windows Management Instrumentation).

It’s the infrastructure for data management in Windows and you can use it to manage your local and remote systems. You can access it with C++, C# or even PowerShell, to get info and manage your computer.

With WMI, you can use SQL-like queries to query information in your system, using the available classes. For example, to get all the drives in your system, you would use:

SELECT * FROM Win32_DiskDrive

That would return a key-value set with all the properties of the system disks, and you can check the values you want. You can even filter the properties and drives that you want. Something like:

SELECT DeviceID, Model, Name, Size, Status, Partitions, 
  TotalTracks, TotalSectors, BytesPerSector, SectorsPerTrack, TotalCylinders, TotalHeads, 
  TracksPerCylinder, CapabilityDescriptions 
  FROM Win32_DiskDrive WHERE MediaType = 'Fixed hard disk media'

Will get only the device data and geometry for the fixed disks in your system (it won’t get info for the external HDs).

Now that we already know how to get the disk information, let’s see how to do it in C#.

To use WMI in a C# program, we must add the package System.Management to the project and start using it.

Open a new terminal window and create a new console application with

dotnet new console -o DiskInfoCSharp

Once you do that, a new console project will be created in the DiskInfoCSharp folder. You can cd to it and add the new package to the project with

dotnet add package System.Management

This will add the package to the project and will allow you to use WMI in it. You can open VS Code with code . and start editing the project.

The first step is to create a scope and connect to it:

using System.Management;

ManagementScope scope = new ManagementScope("\\\\.\\root\\cimv2");
scope.Connect();

This will allow you to query data in the local computer. If you want to connect to a remote computer, just change the \\. to the name of the remote computer.

Then, you must create the query for getting the disk information:

ObjectQuery query = new ObjectQuery(@"SELECT DeviceID, Model, Name, Size, Status, Partitions, 
  TotalTracks, TotalSectors, BytesPerSector, SectorsPerTrack, TotalCylinders, TotalHeads, 
  TracksPerCylinder, CapabilityDescriptions 
  FROM Win32_DiskDrive WHERE MediaType = 'Fixed hard disk media'");

As you can see, the query has a syntax similar to SQL, you can change it to get different fields (or use * to get all fields) or change (or remove) the WHERE clause. This query will only return the selected fields for the fixed hard disks.

Then, we should instantiate a ManagementObjectSearcher object and start iterating on all instances of fixed disks in your system:

ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope, query);
foreach (ManagementObject wmi_HD in searcher.Get())
{
    foreach (PropertyData property in wmi_HD.Properties)
        Console.WriteLine($"{property.Name} = {property.Value}");
    var capabilities = wmi_HD["CapabilityDescriptions"] as string[];
    if (capabilities != null)
    {
        Console.WriteLine("Capabilities");
        foreach (var capability in capabilities)
            Console.WriteLine($"  {capability}");
    }
    Console.WriteLine("-----------------------------------");
}

For each disk, we get all the properties and then we iterate on the capability descriptions, as this is a property that returns an array of strings.

If we run this program, we will get something like

As you can see, it’s very easy to get disk information using WMI in C#. There are some caveats in using this method:

  • You don’t have the properties in advance. Yes, you could create a class with the properties and populate it, but it’s not something that is built in
  • This method only works in Windows. If you are creating a multi-platform program, this method isn’t for you

On the other side, this is a very easy method to get the information in local and remote computers, you can easily filter data and use it as you want.

If you go to the Using WMI page, you will see a note:

As you can see, the System.Management namespace is not recommended anymore, and it must be replaced by the Microsoft.Management.Infrastructure, especially for .NET Core apps.

To port our app, the first thing to do is to ass the package Microsoft.Management.Infrastructure with

dotnet add package Microsoft.Management.Infrastructure

Once you do that, you should change the classes you are using to access the data. We create a CimSession and query the data with QueryInstances:

using Microsoft.Management.Infrastructure;

var instances = CimSession.Create(null)
                .QueryInstances(@"root\cimv2", "WQL", @"SELECT DeviceID, Model, Name, Size, Status, Partitions, 
   TotalTracks, TotalSectors, BytesPerSector, SectorsPerTrack, TotalCylinders, TotalHeads, 
   TracksPerCylinder, CapabilityDescriptions 
   FROM Win32_DiskDrive WHERE MediaType = 'Fixed hard disk media'");

Once you do that, you can iterate on the instances and get the properties with:

foreach (var instance in instances)
{
    foreach (var property in instance.CimInstanceProperties)
        Console.WriteLine($"{property.Name} = {property.Value}");
    var capabilities = instance.CimInstanceProperties["CapabilityDescriptions"].Value as string[];
    if (capabilities != null)
    {
        Console.WriteLine("Capabilities");
        foreach (var capability in capabilities)
            Console.WriteLine($"  {capability}");
    }
    Console.WriteLine("-----------------------------------");
}

If you run the program, you will get properties similar to the ones in the previous figure.

As you can see, WMI can ease the retrieval of information in a Windows machine. Unfortunately, that can’t be used in a Linux machine, that may be a subject for another article!

All the code for this article is at https://github.com/bsonnino/WMICsharp