Migrating Signing of NuGet packages to new sign tool

|

I maintain MvvmCross which is a part of the .NET Foundation and one of the services member projects get is signing of software using certificates issued by the .NET Foundation. This way the project does not have to manage their own signing certificates and not have to spend money on these.

So when I publish NuGet packages for MvvmCross when merging to develop or when releasing a stable release, these NuGet packages are signed. This way consumers of the packages can validate the authenticity of the package and know that it has not been tampered with.

Historically the way signing worked was using the SignClient .NET tool, which is now depcrecated as well. The .NET Foundation has also moved over to use Azure Key Vault for their certificates, so a new tooling is required for member projects to sign their packages.

With help from the .NET foundation team, I have managed to get MvvmCross packages signed again after the deprecation of the other tool. Which was suprisingly straight forward. MvvmCross uses GitHub Actions Windows runners and signing is now done by:

  1. Download the new sign tool
  2. Sign in to Azure CLI
  3. Sign packages which are in the ${{ github.workspace}}/output folder
- name: Install sign tool
  run: dotnet tool install --tool-path . sign --version 0.9.1-beta.25278.1

- name: 'Az CLI login'
  uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 #v2.3.0
  with:
    client-id: ${{ secrets.SIGN_AZURE_CLIENT_ID }}
    tenant-id: ${{ secrets.SIGN_AZURE_TENANT_ID }}
    subscription-id: ${{ secrets.SIGN_AZURE_SUBSCRIPTION_ID }}

- name: Sign NuGet packages
  shell: pwsh
  run: >
    ./sign code azure-key-vault
    **/*.nupkg
    --base-directory "${{ github.workspace }}/output"
    --publisher-name "MvvmCross"
    --description "MvvmCross is a cross platform MVVM framework"
    --description-url "https://mvvmcross.com"
    --azure-key-vault-url "${{ secrets.SIGN_AZURE_VAULT_URL }}"
    --azure-key-vault-certificate "${{ secrets.SIGN_AZURE_KEY_VAULT_CERTIFICATE_ID }}"

The secrets are provided to you by the .NET Foundation.

You can double check that the package has been signed either using the NuGet package explorer and upload your signed package there. Which on the left side will show a “Digital Signatures” section showing something like:

Screenshot of NuGet Info showing Digital Signatures pane

Migrating AppCenter Analytics Events to Application Insights

|

With AppCenter closing 31st of March, I bet some people are scrambling to find out what to do instead. In my organization we’ve moved crashes into Sentry. However, there is still the question of what to do about Analytics events, which Sentry does not have an offering for.

We had AppCenter analytics events being exported into Application Insights. To patch this functionality, one could add not too many lines of code to replicate more or less what AppCenter exported.

In your Mobile App, you can add the Microsoft.ApplicationInsights NuGet package and create a TelemetryClient like so:

var config = new TelemetryConfiguration
{
    ConnectionString = MyConnectionString
};
telemetryClient = new TelemetryClient(config);

To set similar global properties on events which AppCenter did you add these properties:

telemetryClient.Context.Device.Id = your device id (you have to come up with something for this, I just save a GUID);
telemetryClient.Context.Device.OperatingSystem = GetOperatingSystem();
telemetryClient.Context.Device.Model = Microsoft.Maui.Devices.DeviceInfo.Manufacturer;
telemetryClient.Context.Device.Type = Microsoft.Maui.Devices.DeviceInfo.Model;
telemetryClient.Context.GlobalProperties.Add("AppBuild", Microsoft.Maui.ApplicationModel.VersionTracking.CurrentBuild);
telemetryClient.Context.GlobalProperties.Add("AppNamespace", Microsoft.Maui.ApplicationModel.AppInfo.PackageName);
telemetryClient.Context.GlobalProperties.Add("OsName", Microsoft.Maui.Devices.DeviceInfo.Platform.ToString());
telemetryClient.Context.GlobalProperties.Add("OsVersion", Microsoft.Maui.Devices.DeviceInfo.VersionString);
telemetryClient.Context.GlobalProperties.Add("OsBuild", Microsoft.Maui.Devices.DeviceInfo.Version.Build.ToString());
telemetryClient.Context.GlobalProperties.Add("ScreenSize",
    $"{Microsoft.Maui.Devices.DeviceDisplay.MainDisplayInfo.Width}x{Microsoft.Maui.Devices.DeviceDisplay.MainDisplayInfo.Height}");

static string GetOperatingSystem()
{
    var os = Microsoft.Maui.Devices.DeviceInfo.Platform.ToString();
    var version = Microsoft.Maui.Devices.DeviceInfo.VersionString;
    return $"{os} ({version})";
}

If you want to identify users you can do:

telemetryClient.Context.User.Id = userId;
telemetryClient.Context.GlobalProperties["UserId"] = userId;

Then to track events you can do:

public void TrackEvent(string eventName, IDictionary<string, string> properties = null)
{
    if (properties != null)
    {
        // AppCenter exported event properties in a nested way
        telemetryClient.TrackEvent(eventName,
            new Dictionary<string, string> { { "Properties", ToPropertiesValue(properties) } });
    }
    else
    {
        telemetryClient.TrackEvent(eventName);
    }
}

private static string ToPropertiesValue(IDictionary<string, string> dictionary) =>
    "{" + string.Join(",", dictionary.Select(kv => $"\"{kv.Key}\":\"{kv.Value}\"")) + "}";

This will get you most of the way and any dashboards you’ve made based on the data in customDimensions.Properties can be kept alive indefinitely or until you switch to something else.

Consuming private Swift Packages in GitHub Actions

|

I had a case for a native App we are working on where we already have some Swift Packages in Azure DevOps Repos, which we would like to consume in a project that lives in GitHub.

Locally on your machine this setup is pretty easy to work with if you are using something like Git Credential Manager. You just install the manager, use https urls and it will pop open a Web Browser to ask for your credentials when needed. This interactive way of authenticating is not really possible when you run in CI.

Stuff that didn’t work

I tried using git credential store and added something like this in the beginning of my workflow:

- name: Add Azure DevOps repo authentication for SwiftPM
  run: |
    git config --global credential.helper store
    git config --global --add http.https://[email protected]/$ORG/$PROJECT/_git/.extraHeader "AUTHORIZATION: Basic $BASE64PAT"
  env:
    ORG: orgname
    PROJECT: projectname
    BASE64PAT: $

This didn’t work at all, even though multiple source online says it was supposed to.

I also thought of using SSH keys, but I don’t want to do that since Azure DevOps Repos do not support LFS over SSH, so that would open up another can of worms for me.

.netrc to the rescue

.netrc is know from the *nix world for allowing you to store credentials for automatically log into services like ftp, http etc. So a help to avoid having to enter your credentials every time you want to connect to these services.

In GitHub Actions, there is a convenient action to create such file for you:

- uses: extractions/netrc@v1
  with:
    machine: dev.azure.com
    username: orgname
    password: $

Adding the .netrc now allows SwiftPM to resolve the package and CI is now happy

While this works for Swift Packages, it will likely work for other things for you as well.

Working with JsonSerializerContext in System.Text.Json and Refit

|

Recently I have added <EnableTrimAnalyzer>true</EnableTrimAnalyzer> to a bunch of my projects, which now yield warnings when code I wrote has potential issues when trimming the assembly.

What is trimming? In short terms, instead of shipping all the code in all assemblies in an Release version of your App, the trimming process scans what is actually used and cuts out all the other code that is unused. This leads to much smaller Apps in terms of size. There are other processes which also help with speed.

Additionally, in a mobile context, we often opt into AOT compilation, which also brings a set of gotchas. For example, if your App uses reflection or emits code, it will likely not work at runtime. To replace a lot of this kind of code, some time ago we got Source Generators to help us do some of these things at compile time, instead of doing them at runtime.

So because of enabling the Trim Analyzers, I now noticed a few warnings around serialization code I had, which told me I could use JsonSerializerContext to source generate the serializers to help with trimming but also speed. Whenever I read that something becomes faster, I get excited. I love speed! So, obviously I had to try this out.

So commonly when serializing/deserializing something with System.Text.Json you could have some code looking something like this:

record PersonDto(string Id, string Name);

var person = JsonSerializer.Deserialize<PersonDto>(json);

This code would now emit a Trimmer warning because the definition of the method looks as follows.

If you look carefully, there are two annotations that trigger the trimming warnings. Namely [RequiresUnreferencedCode] and [RequiresDynamicCode]. These essentially say, that the method uses code outside of its own knowledge and that this code could potentially be trimmed away and code will be generated at runtime, which is not compatible with (Native)AOT.

OK, what then? Source generators to the rescue! So to avoid the generated code at runtime and to avoid types getting trimmed, we can implement JsonSerializerContext and tell the serializer about this. Which is fairly simple.

[JsonSerializable(typeof(PersonDto))]
internal sealed partial SerializerContext : JsonSerializerContext;

This will generate the serializer needed to serialize and deserialize PersonDto. So this means, that any type you want to run through the serializer, you would need to add to a JsonSerializerContext. Additionally, you need to use the method overload that takes the JsonSerializerContext when serializing or deserializing. So the example from above becomes:

PersonDto person = JsonSerializer.Deserialize(json, SerializerContext.Default.PersonDto);

// or

PersonDto person = JsonSerializer.Deserialize(bayJson, typeof(PersonDto), SerializerContext.Default);

Obviously there are async variants as well, however, they will have a very similar signature to do the same.

So with these few steps you are now trimmer safe and AOT compatible when serializing and deserializing code with System.Text.Json.

Usage in Refit

If you are a user of Refit. The default serializer used is System.Text.Json, with its default configuration. If you want to use your newly added JsonSerializerContext you need to tell Refit about his using its settings. This can be done simply with something like this:

var settings = new RefitSettings
{
    ContentSerializer = new SystemTextJsonContentSerializer(SerializerContext.Default.Options)
};

var api = RestService.For<IPersonApi>(url, settings);

Now you are good to go and Refit will use your context. However, there are a few caveats to be aware of.

On the JsonSerializerContext you can set some options through JsonSourceGenerationOptions, like how to format json when serializing, or whether to allow case insensitivity on fields. So applied on the JsonSerializerContext from before, this could look like:

[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true, WriteIndented = true)]
[JsonSerializable(typeof(PersonDto))]
internal sealed partial SerializerContext : JsonSerializerContext;

These options are however ignored if you accidentally create your settings like this:

var settings = new RefitSettings
{
    ContentSerializer = new SystemTextJsonContentSerializer(
        new JsonSerializerOptions
        {
            TypeInfoResolver = SerializerContext.Default
        })
};

var api = RestService.For<IPersonApi>(url, settings);

So make sure to provide the full options like shown in the first example with RefitSettings.

If you are using HttpClientFactory and a ServiceCollection to register your Refit APIs you can also pass settings with:

serviceCollection.AddRefitClient<IPersonApi>(settings);

Central Package Management Transitive Pinning

|

I have been using the super nice Central Package Management feature, enabled by the ManagePackageVersionsCentrally property in many of my .NET projects for quite a while. What it allows you, is to define the package versions of the NuGet packages you are referencing in your solution a single Directory.Packages.props file in the root of your repo. So instead of having package versions scattered around your csproj files, they are defined a single place and aligned throughout the solution.

Such a Directory.Packages.Props file would look something like this:

<Project>
  <ItemGroup>
    <PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.0" />
    <!-- more package versions here -->
  </ItemGroup>
</Project>

Then in a projects .csproj file you would add an entry like so for each package you want to reference, just like you’d normally do, just with the version part at the end:

<ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup>

To enable this globally in your solution you would also have a Directory.Build.props file where you add a property like so:

<Project>
    <PropertyGroup>
        <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
    </PropertyGroup>
</Project>

This is great!

However, a lot of NuGet packages pull in other dependencies that you often don’t include directly in your Directory.Packages.props file. They are also known as transitive dependencies. With newer versions of .NET and in turn newer versions of NuGet you now have auditing of NuGet packages enabled per default. This will start warning you about packages you reference, when they have vulnerability warnings. With .NET 9, transitive packages will now also get audited too! If you have TreatWarningsAsErrors enabled, it prevent you from restoring packages and building your project.

Previously, if I wanted to use a specific version of a transitive package, I would add it to Directory.Packages.props and add it to a project to specify which version of it to use. This is a bit annoying to have to manage as you will need to add it to multiple files.

A colleague of mine made me aware that you can pin transitive packages. Not only top level packages that you reference directly. This is a huge help!

If you have ever worked with some of the AndroidX packages, then you will often need to do this as some package, might be referencing an older version of a transitive package, which isn’t directly compatible with a newer top level package you are referencing. This, at build time will complain about classes from the Java/Kotlin world being specified twice and will fail your build. To fix this you need to pin the transitive packages to a newer version which isn’t conflicting. Or similarly, recently we had warnings with System.Text.Json version 6.0.0 having a vulnerability warning.

So by setting the CentralPackageTransitivePinningEnabled property to true, you can now simply add those packages to your Directory.Packages.props to pin the version to a specific one, without having to reference it directly in a project.

So your Directory.Build.props would then contain something like:

<Project>
    <PropertyGroup>
        <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
        <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
    </PropertyGroup>
</Project>

And then I would recommend adding a section to your Directory.Packages.props with a comment for the pinned transitive packages, to know why they are here, but not added directly to a project like so:

<Project>
  <!-- packages referenced by projects -->
  <ItemGroup>
    <PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.0" />
  </ItemGroup>

  <!-- transitive pinned packages -->
  <ItemGroup>
    <PackageVersion Include="System.Text.Json" Version="9.0.0" />
    <PackageVersion Include="Xamarin.AndroidX.Activity.Ktx" Version="1.9.3.1" />
    <PackageVersion Include="Xamarin.AndroidX.Collection.Ktx" Version="1.4.5.1" />
    <PackageVersion Include="Xamarin.AndroidX.Lifecycle.Common" Version="2.8.7.1" />
    <PackageVersion Include="Xamarin.AndroidX.Lifecycle.Runtime" Version="2.8.7.1" />
    <PackageVersion Include="Xamarin.AndroidX.Lifecycle.LiveData.Core" Version="2.8.7.1" />
    <PackageVersion Include="Xamarin.AndroidX.Lifecycle.LiveData.Core.Ktx" Version="2.8.7.1" />
  </ItemGroup>
</Project>

That is it! Now you have pinned some transitive packages.