MSIX: how to copy data outside the installation folder during the deployment
Published Jan 28 2020 11:13 AM 16.1K Views
Microsoft

When you deploy a MSIX package on Windows 10, all the files are copied in a special folder called C:\Program Files\WindowsApps. Each application has its own folder, which name is represented by the Package Full Name, which is composed by:

 

  • The Package Family Name (which is composed by Publisher and Name)
  • The version number
  • The target CPU architecture

For example, if you download the MSIX Packaging Tool from the Microsoft Store, it will be deployed in the folder

 

C:\Program Files\WindowsApps\Microsoft.MsixPackagingTool_1.2019.1220.0_x64__8wekyb3d8bbwe

In order to track all the files which belong to the application more efficiently, Windows doesn't allow to deploy files outside this folder. However, you might have a scenario where you actually need to do this. For example, you must copy a dependency in a system folder; or your application comes with a set of data or configuration files you need to deploy in the local AppData folder of the user.

 

To overcome this requirement, MSIX provides a feature called Virtual File System, which gives you the possibility to map the most common system folders (like C:\Windows or C:\Program Files) inside a special folder inside the package called VFS. When the application looks for a file inside these folders, Windows will redirect the call to the VFS folder and will try to see if the file is included in the Virtual File System. This feature provides two main benefits:

 

  • You get the opportunity to create truly standalone MSIX packages. Even if your application has one or more dependencies, you can bundle everything together, without asking to the user to manually install a framework or a library your application depends on.
  • It helps to minimize the problem known as DLL Hell, which happens when you have multiple applications depending on different version of the same framework. Typically this framework can be installed only system-wide, so you can't keep multiple versions of the same one on the machine. This means that, if the most recent version contains some breaking changes, your applications which depends on older versions might brake. With the Virtual File System you can include, inside the package, the specific version of the dependency you need and it won't interfere with the already installed ones.

The Virtual File System is a great way to overcome this MSIX limitation when it comes to deploy a dependency. But what about when you need to copy data or configuration files in the local AppData folder or a library like Documents? In this case, the Virtual File System can't really help since, as per the documentation, it supports mostly system folders, not user's folder (except the generic C:\ProgramData one).

 

Let's see which options we have.

 

The sample app

As a starting point, we're going to use a simple Windows Forms application, which retrieves a list of employees from a local SQLite database. The whole code is available here. The connection with the database is handled using the System.Data.SQLite library, which is an ADO.NET provider for SQLite, the popular self-contained database. When the application starts, it uses a class called SQLiteConnection to establish a connection with a local SQLIte file. Then, using a SQLiteCommand object, it performs a SELECT query on the database to retrieve the list of employees. In the end, using a SQLDataReader object, it iterates through all the resulting rows and it adds them to a BindingSource object, which is connected to a DataGrid control in the UI.

 

This is the complete code:

 

private void LoadData()
{
    string dbPath = $"{Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData)}\\MyEmployees\\Employees.db";
    if (File.Exists(dbPath))
    {

        SQLiteConnection connection = new SQLiteConnection($"Data Source= {dbPath}");
        using (SQLiteCommand command = new SQLiteCommand(connection))
        {
            connection.Open();
            command.CommandText = "SELECT * FROM Employees";
            using (SQLiteDataReader reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    Employee employee = new Employee
                    {
                        EmployeeId = int.Parse(reader[0].ToString()),
                        FirstName = reader[1].ToString(),
                        LastName = reader[2].ToString(),
                        Email = reader[3].ToString()
                    };

                    employeeBindingSource.Add(employee);
                }
            }
        }

        dataGridView1.DataSource = employeeBindingSource;
    }
}

As you can see from the first line of the method, the application is expecting to find the SQLite database in the C:\ProgramData folder. The Environment.SpecialFolder.CommonApplicationData folder is translated with the %PROGRAMDATA% environment variable. This means that the application looks for the database in the path C:\ProgramData\MyEmployees\Employees.db. The package of the application includes a file called Employees.db, which we want to copy in this folder. Without doing this, the app would display an empty DataGrid when it starts. However, this is a task that we can't achieve directly with MSIX, since the content of the package (including our Employees.db file) will be copied only in the special folder under C:\Program Files\WindowsApps. We could use the Virtual File System, since C:\ProgramData is one of the mapped folders, but we want our database to be modifiable. If we leave it in the package folder, it would be read-only instead.

 

Option 1: copy the files when the application starts

This might seem obvious, but it's the best option if we're talking about an application which is still actively developed. When the application starts for the first time, we can just copy the file into the local C:\ProgramData folder. We can use multiple ways to detect if the it's the first launch: we can add a key in the registry; we can store the information in a configuration file; or we can simply look if the database already exists in the C:\ProgramData folder.

Here is an example of a method that we can add to our application:

 

private void CopyDatabase()
{
    string result = Assembly.GetExecutingAssembly().Location;
    int index = result.LastIndexOf("\\");
    string dbPath = $"{result.Substring(0, index)}\\Employees.db";

    string destinationPath = $"{Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData)}\\MyEmployees\\Employees.db";
    string destinationFolder = $"{Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData)}\\MyEmployees\\";

    if (!File.Exists(destinationPath))
    {
        Directory.CreateDirectory(destinationFolder);
        File.Copy(dbPath, destinationPath, true);
    }
}

First we get a reference to the Employees.db file which is included in the package folder. We use reflection since, when an application runs as packaged, the Current Working Directory is mapped with the C:\Windows\System32 or C:\Windows\SysWOW64 folder. As such, we can't simply use the Directory.GetCurrentDirectory() API to achieve this task. Once we have the path of the database file, we use the Environment.SpecialFolder.CommonApplicationData property to compose the full path of the destination folder. In this case, we want to copy the database inside the C:\ProgramData\MyEmployees folder.

 

In the end, if the Employees.db file doesn't exist in the C:\ProgramData folder, it means that it's the first execution of the app. As such, we go on and we copy the database from the package inside the destination folder.

 

That's it. Now we just need to make sure that the CopyDatabase() method we have just defined is invoked before the LoadData() one we have previously seen. This is how the event handler of the Load event of the form will look like:

 

private void Form1_Load(object sender, EventArgs e)
{
    CopyDatabase();
    LoadData();        
}

Now we can package our application with MSIX by adding a Windows Application Packaging Project to our solution. If we did everything properly, when we launch the application we will see the list of the employees in the DataGrid. And if we use File Explorer to open the C:\ProgramData\MyEmployees path, we will see the Employees.db file we have copied.

 

DatabaseFile.png

 

Option 2: copy the files with a PowerShell script

In some cases, we might not be able to change the code of our application. It might be because you aren't a developer, but you manage the deployment of applications in your enterprise and, as such, you don't have the source code; or it's a legacy app built with a very old technology and it would be risky to make any change. In this scenario, we can leverage a feature that has been recently added to the Package Support Framework: scripting support. We have already talked about this framework on this blog: it's an open source framework which provides support to fixups, which are DLLs that are able to change the behavior of your application at runtime. This way, you can solve common MSIX blockers (like apps which write to the installation folder or need to access to the current working directory) without changing the code.

 

Since a few months, the Package Support Framework has added supports to script. You can include inside your package one or more PowerShell scripts and execute them on various conditions: when the application starts or closes, only the first time after the MSIX package has been deployed or every time, etc.

 

This feature is a great fit for our scenario: we can include inside the package a script that takes care of copying the database file to the C:\ProgramData folder, without having to change the code. Let's see how to do it. We're going to use Visual Studio and the Windows Application Packaging Project since I'm a developer, but you can perform also the following steps manually, by unpacking the MSIX package, injecting the Package Support Framework and then repackaging it again. This way, you can leverage the same approach even if you aren't a developer or you don't use Visual Studio. This approach is described in the official documentation.

 

Step 1: create the PowerShell script

As first step, let's create the PowerShell script which is going to copy the database from the package to the C:\ProgramData folder. You can use any text editor, from Notepad to Visual Studio code. This is the script to copy and paste:

 

New-Item -Path $env:ProgramData -Name "MyEmployees" -ItemType "directory"
Copy-Item -Path "Employees.db" -Destination "$env:ProgramData\MyEmployees" -Recurse

First we use the New-Item command to create a new folder called MyEmployees in the C:\ProgramData path. Then we use the Copy-Item command to copy the file Employees.db to the folder we have just created.

 

Step 2: add the Package Support Framework

First we need to download the Package Support Framework, which is distributed through NuGet. However, it isn't a traditional library, so we won't have to add it to a project in Visual Studio, but we're going to download it and copy some files inside our Visual Studio solution. The easiest way to achieve this task is by using the NuGet CLI, which can be downloaded from the official website. Download the most recent Windows x86 command line version:

 

NuGet.png

 

Once you have downloaded and copied it somewhere on your computer, open a command prompt on that location and run the following command:

 

nuget install Microsoft.PackageSupportFramework

At the end of the process, you will find in the same location a folder called Microsoft.PackageSupportFramework followed by the version number of the most recent version. For example, at the moment of writing this post, the folder is called Microsoft.PackageSupportFramework.1.0.200102.1. Go inside it and move to the bin folder. We're going to copy a few files inside the Windows Application Packaging Project, by dragging them from File Explorer to the WAP project in Visual Studio. If your application is compiled for x86, copy the PsfLauncher32.exe, PsfRunDll32.exe and PsfRuntime32.dll files. If instead, it's compiled for x64, copy the PsfLauncher64.exe, PsfRunDll64.exe and PsfRuntime64.dll files. My sample on GitHub is compiled for 64 bits, so I've added the files with 64 suffix.

The next step is to include inside the package a PowerShell script, which is used by the Package Support Framework to invoke the other custom scripts you include. This file is called StartingScriptWrapper.ps1 and it's included in the bin folder as well. However, unlike for the previous files, it doesn't have to be included in the root of the package, but in the same folder which contains the main executable of the application. In some cases the two things might coincide, but you can't take it for granted. For example, in case you're using the Windows Application Packaging Project, Visual Studio will put the main application's executable and the DLLs inside a sub-folder of the package, with the same name of the application. For instance, my sample app is called MyEmployees, so the main executable and files of the application will be stored in a folder called MyEmployees inside the package. This is where we need to copy the StartingScriptWrapper.ps1 file. To achieve this goal, it's enough to manually add a folder called MyEmployees inside the Windows Application Packaging Project. It will be merged with the one generated by Visual Studio.

 

Inside this folder we have to include also the PowerShell script that we have created at Step 1. This is how the final project should look like:

 

ProjectFile.png

 

Step 3: configure the Package Support Framework

The last step is to configure the Package Support Framework. So far we have just injected the required files, but we didn't set it up. If we would create a MSIX package now out from the Windows Application Packaging Project, we won't notice any difference. The main application would simply be launched, as before.

 

To configure the framework we need to include a file inside the Windows Application Packaging Project called config.json. Then copy and paste the following content:

 

{
  "applications": [
    {
      "id": "App",
      "executable": "MyEmployees/MyEmployees.exe",
      "workingDirectory": "MyEmployees",
      "stopOnScriptError": true,
      "startScript": {
        "scriptPath": "CopyDatabase.ps1",
        "showWindow": false,
        "waitForScriptToFinish": true,
        "runOnce": true
      }
    }
  ]
}

Let's take a look at the most important settings:

 

  • id is the identifier of the application. We can find it by right clicking on the Package.appxmanifest file and choosing View code. Look for the Application entry, which will look like this:

     

    <Application Id="App"
      Executable="$targetnametoken$.exe"
      EntryPoint="$targetentrypoint$">
    

    The identifier of the application is the value of the Id attribute. If you're using a standard Windows Application Packaging Project, its default value is always App.

     

  • executable is the main executable of the application. You need to specify the full path starting from the root of the package, in my case it's MyEmployees\MyEmployees.exe.

  • In order to redirect all the file system calls to the Current Working Directory, you need to specify it through the workingDirectory property. It must be the path of the folder which contains the main executable, in my case MyEmployees.

The other settings are dedicated to configure the scripting support. These are just some settings, but in the documentation you'll find all of them.

 

  • We include the configuration of the script inside the startScript section, since we want to run it when the application starts.
  • scriptPath is the name of the PowerShell script we have created in Step 1.
  • showWindow is set to false, since we want the script to be completely transparent to the user. We want him to just see the main application starting up. However, for testing purposes, it might be helpful to keep it to true.
  • waitForScriptToFinish is set to true, since we don't want to start the MyEmployees application until the script has finished to copy the database, otherwise the data won't be loaded properly.
  • runOnce is set to true because we don't want to repeat the script every time the application starts. We want to copy the database only the first time after the deployment.

 

Step 4: configure the manifest

The last step is to change the entry point of the application. When it starts, it doesn't have to launch anymore the MyEmployees.exe executable, but the Package Support Framework. It will be up to it to load the required DLLs and then execute the main application. This is why, in the config.json, we have specified the main executable in the executable property. However, if we're using Visual Studio and the Windows Application Packaging Project, we can't do it directly in the Package.appxmanifest file. Even if we manually change it in the manifest, in fact, Visual Studio will always replace it with the entry point set in the WAP project during the MSIX generation. As such, we have first to generate the package and then make the changes.

 

To generate the package, you just need to follow the normal procedure. Right click on the WAP project, choose Publish → Create app package and follow the wizard. Some important notes:

 

  • You can skip the package signing at this point, because we'll need to sign it again after having changed the manifest.

  • In the Select and configure packages section make sure to:

    • Set Generate bundle to Never, since we want a plain MSIX package
    • In the configuration mappings, include only the CPU architecture which matches the same one you've used for the Package Support Framework.

      PublishPackage.png

 

Once you complete the wizard and you have a MSIX package, you need to edit it. The easiest way to do it is to have the MSIX Packaging Tool installed from the Microsoft Store. Once you have installed it, you can just right click on the MSIX package and choose Edit with MSIX Packaging Tool. As first thing we need to change the entry point, so click on the Open file button under the Manifest file section.

 

MSIXPackagingTool.png

 

The manifest will be opened in your favorite text editor and any change you're going to make will be automatically included back in the main package. Look again for the Application entry, which will look like this:

 

<Application Id="App" Executable="MyEmployees\MyEmployees.exe" EntryPoint="Windows.FullTrustApplication">

You must replace the Executable attribute with the path of the PsfLauncher64.exe file (or PsfLauncher32.exe if your application is 32-bit). This is how the manifest should look like after the change:

 

<Application Id="App" Executable="PsfLauncher64.exe" EntryPoint="Windows.FullTrustApplication">

Save and close the text editor. Now we need to sign the package, otherwise we won't be able to install it. From the Signing preference dropdown choose the option which works best for you. For example, if you're used to sign your MSIX package with a certificate, choose the Sign with a certificate (.pfx) option and select if from your hard drive. Then provide the password in the new field that will appear. Regardless of the option you choose, once you have configured the signing you can generate an updated package by pressing the Save button. The tool will propose you to upgrade the version number. It isn't really required in our case since we aren't creating an update, so feel free to choose No.

 

Test the package

That's it! Now double click on the MSIX package you have just created with the MSIX Packaging Tool and install it. At the end, Windows will launch automatically the application. If everything went well, you should see the list of employees displayed in the main window. And, as a confirm, you should find also the Employees.db file inside the C:\ProgramData\MyEmployees folder. All of this without changing the code of the main application.

 

What about the AppData folder?

In some scenarios the data might be stored in the local AppData folder. Can we use the same approach also in this scenario? The answer is yes! You just need to take in consideration that the AppData folder is virtualized by Windows when an application is packaged as MSIX. This means that every time the application tries to read or write a file from the AppData folder, the operation is redirected to the local storage of the application. This approach helps Windows to maintain a clean installation. When the application is uninstalled, Windows will simply delete the local storage, without leaving leftovers behind.

This means that if your application copies some files from the package to the AppData folder, the operation will work just fine but you won't actually find the files in the real AppData folder of the user, but you will have to look in the local folder of the application, which is stored in the %LOCALAPPDATA%\Packages folder. For example, in case of the MyEmployees applications, files would be copied in the %LOCALAPPDATA%\Local\Packages\MyEmployees_mfkd8f81tn1vy\LocalCache\Local folder.

 

Wrapping up

Having an application which comes with a default set of data that must be deployed to a folder like ProgramData or AppData is a common scenario. MSIX, out of the box, isn't able to directly support it, since all the files included in the package can be deployed only to a special folder where Windows keeps all the packaged apps. In this post we have seen a couple of options to overcome this limitation:

 

  • If we're talking about an application which is actively developed, we can just add some code to copy the files the first time the application launches.
  • If you don't have the opportunity to make code changes, either because you don't own it or you're unable to them, you can leverage the Package Support Framework and the recently added scripting support. Thanks to a PowerShell script, you can copy the files you need in the destination folder only the first time the application starts after it has been deployed.

As usual, you will find the sample used in this blog post on GitHub.

Happy coding!

5 Comments
Version history
Last update:
‎Jan 28 2020 11:13 AM
Updated by: