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.

.NET 9 Gotchas!

|

Updated 17. Nov 2024: Added a bit more info on the new registrar

.NET 9 was just released and it has brought with it a lot of cool new performance features. Some that might do a lot of good things. Some of them might cause you a little bit of a headache.

After the announcement I dove straight into upgrading stuff to .NET 9 and I have found a few things that tripped up the smoothness of the experience a bit. However, nothing that forced me to abandon all hope and stick to .NET 8. The experience overall was pretty good. Simply updating TFM from net8.0-{ios|android} to net9.0-{ios|android} and updating a few Microsoft.* and System.* packages I referenced to the new and shiny 9.0.0 versions on NuGet.

So what are the issues then?

So a few things you might want to know, to help you along.

New iOS Objective-C Registrar

On iOS as announced in their Release notes Wiki page, there is a new Objective C registrar, to better support NativeAOT. This may cause you issues with binding libraries, which for instance I found with the package ImageCaching.Nuke which I use and maintain. That at runtime, with the new registrar, I would get this nice runtime exception as soon as I would call in to the library:

ObjCRuntime.RuntimeException: Could not find the type 'ObjCRuntime.__Registrar__' in the assembly 'ImageCaching.Nuke'.
  ?, in MapInfo RegistrarHelper.GetMapEntry(string) x 2
  ?, in Type RegistrarHelper.LookupRegisteredType(Assembly, uint)
  ?, in MemberInfo Class.ResolveToken(Assembly, Module, uint)
  ?, in MemberInfo Class.ResolveFullTokenReference(uint)
  ?, in MemberInfo Class.ResolveTokenReference(uint, uint)
  ?, in Type Class.ResolveTypeTokenReference(uint)
  ?, in Type Class.FindType(NativeHandle, out bool)
  ?, in Type Class.Lookup(IntPtr, bool) x 2
  ?, in ImagePipeline Runtime.GetNSObject<ImagePipeline>(IntPtr, IntPtr, RuntimeMethodHandle, bool) x 3

Not super nice. But, as the release notes say, you can opt-out of the new registrar by adding this to your App’s csproj file:

<Target Name="SelectStaticRegistrar" AfterTargets="SelectRegistrar">
    <PropertyGroup Condition="'$(Registrar)' == 'managed-static'">
        <Registrar>static</Registrar>
    </PropertyGroup>
</Target>

This fixes the issue and iOS is happy again. Sweet!

This will supposedly get fixed in a future update to the iOS workloads and should not require you to change anything in the Binding Library.

Android Default Runtime Identifiers

On Android you should know that the default Runtime Identifiers (RID) now do not include 32-bit stuff. So if you have an App that you want to run on an older Android device, you may want to include it your self.

In your csproj you can specify the RIDs with:

<PropertyGroup>
    <RuntimeIdentifiers>android-arm;android-arm64</RuntimeIdentifiers>
</PropertyGroup>

There are also android-x86 and android-x64 you can specify if you are interested in those.

Android API 35 16KB Page Sizes

When targeting Android 35, which .NET9 does out of the box, you will now get warnings like this for native libraries that have not yet been recompiled with the latest tooling:

Warning XA0141 : NuGet package '<unknown>' version '<unknown>' contains a shared library 'libsentry.so' which is not correctly aligned. See https://developer.android.com/guide/practices/page-sizes for more details

This is a new thing in Android 15 (API 35), which is for new devices to optimize memory performance. Hopefully Sentry among other libraries rebuild with new tooling soon and add fixes these warnings.

Apart from this I haven’t found any issues so far. I will update the post if anything else comes up.

Also just as a reminder, the libraries you consume in your .NET 9 App, do not need to target .NET 9, so if you are consuming a .NET 6/7/8 package that is OK!

Sentry is great for crashes!

|

Update 27th September: Added information about uploading symbols for symbolication of exception

This blog post is not sponsored or paid. It is just me being a bit excited about a product I’ve tried and reflects the as of writing point of view about the product. Also I had written a section about Metrics in Sentry, which was a preview feature. However, they pulled this support to provide a better version of it later based on Spans, so I will omit this here. It shows though that new features in Sentry are thought through.

With the AppCenter announcement that it is retiring (March 31st 2025), I have been looking around a bit for a good replacement for it. Currently, I use AppCenter for crashes, analytics and App distribution and it would be great if there was a single product out there, which could replace it fully.

It seems like Firebase is the closest to a 100% replacement of AppCenter. However, there is a catch. Crashes on iOS when running on .NET iOS do not work. You will only ever end up with a native stack trace without any of the managed C# stacks. This is not great, and there doesn’t seem to be any traction on getting this to work. Hence, the search for a solution that works excellent for crashes for both .NET Android and iOS, which I primarily target in the Apps I work on.

Having looked at Sentry among other competitors a couple of times before and played a bit around with Sentry too, then now revisiting it again, it seems like a very solid choice for crashes and some of its other features.

What is cool about Sentry?

What I really like in Sentry, is that it really excels as a product for crash reporting. If you have used AppCenter, you will know that when you get a crash, you end up with a stack trace, with limited information about the device it crashed on. If you want extra information, you can enrich it yourself when the App restarts and upload file attachments to the crash. In my Apps, I’ve been using this to upload logs from the App on the crash, to help understand how the user ended up in the situation where it lead to a crash.

This is where Sentry, by far, beats AppCenter! Where Sentry excels is a feature called Breadcrumbs. This is a way to enrich the crash with events, log entries, navigation and much more. It essentially leaves little traces as the App runs from different sources. This feature is really powerful as it can help you much better understand what happened before the crash, so you can better understand what when on and reproduce.

Some of the things I’ve tried using breadcrumbs for are:

  • Adding log entries
  • Adding key events, such as user logged in
  • Navigation, such as user navigated to a specific screen
  • Button presses
  • App lifecycle
  • HTTP requests
  • Android ANRs (AppCenter doesn’t support these!)

Out of the box you also get a plethora of device information, such as storage, memory, ABI, display size, density and so much more. With this extra information, it helps a lot understanding what happened before the App crashed and in which state it was in.

Getting started

Depending on your App you might want to consume Sentry differently. I will cover .NET Android and iOS Apps here, but Sentry supports so many platforms and frameworks, so you will most definitely be able to add support for it in your code.

MvvmCross Apps

Currently MvvmCross still uses its own IoC container, so you cannot use some of the handy extension methods for Microsoft.Extensions.DependencyInjection. However, don’t fear, it is still possible to get it working!

The way I do it is to use the two NuGet packages Sentry and Sentry.Serilog. So I get the out of the box support for Android and iOS that the Sentry package provides. To get logs as breadcrumbs and to automatically report errors when you use LogError(exception, "message"), I use the Sentry.Serilog NuGet package.

<PackageReference Include="Sentry" Version="4.11.0" />
<PackageReference Include="Sentry.Serilog" Version="4.11.0" />

In your MainApplication.OnCreate() on Android and in AppDelegate.FinishedLaunching() (make sure to do this before base.FinishedLaunching() if you are inheriting from MvxAppDelegate) on iOS you add your Sentry setup like:

SentrySdk.Init(options =>
{
    options.Dsn = "<your DSN here>";
    options.ProfilesSampleRate = 1.0;
    options.TracesSampleRate = 1.0;
#if RELEASE
    options.Environment = "prod";
#else
    options.Environment = "dev";
#endif
});

On Android and iOS you can customize some of the platform specifics by enabling some of the stuff in the options.Native property. For instance, if your Application has permission to read logcat you could enable it to pull that and add to crashes with:

options.Android.LogCatIntegration = LogCatIntegrationType.Errors;

Explore that property, because there might be a bunch of interesting things in there for you.

To get the logs integration, using the handy extension method from Sentry.Serilog you can add a few lines to your Serilog configuration:

.WriteTo.Async(l => l.Sentry(options =>
{
    options.InitializeSdk = false;
    options.MinimumBreadcrumbLevel = LogEventLevel.Information;
}))

Since I am initializing Sentry myself, I’ve chosen to tell it to not do it here. MinimumBreadcrumbLevel and MinimumEventLevel helps you control what is added as breadcrumbs and as events to Sentry. If you don’t want logging errors as events in Sentry, you can control that with MinimumEventLevel, which defaults to Error, while MinimumBreadcrumbLevel defaults to Verbose.

What about MAUI Apps?

For the MAUI Framework, there is a dedicated Sentry.Maui package, which you can add by calling AddSentry() on your service collection, which makes it much easier than the setup above. It also adds Logging Providers to hook into HttpClient requests and Microsoft.Extensions.Logging out of the box. So consider using packages that match closely the framework you are using.

There are many more for ASP.NET, Google Cloud, Hangfire, Log4Net and EntityFramework.

Uploading symbols for symbolication

To get nice symbolicated stack traces you can use the sentry-cli commandline tool upload symbols. I add it to my pipeline like so:

- script: |
    sentry-cli debug-files upload --org $(sentryOrg) --project $(sentryProject) --auth-token $(sentryToken) MyProject/bin/Release/net8.0-android
  displayName: Upload Debug Symbols to Sentry

You will need to run sentry-cli login to get a token, once you do that, you are good to go!

Differences from AppCenter

Some few differences in usage compared to AppCenter are how extra information is annotated on events. In AppCenter, when you would report an error or analytics event, you would provide a Dictionary<string, string> with extra information you want added.

This is different in Sentry, where you instead create a scope. It supports both sync and async scope creation. So something like:

// local scope only for this exception
SentrySdk.CaptureException(exception, scope =>
{
    // adding key/value like in AppCenter
    foreach (var (key, value) in properties)
    {
        scope.SetExtra(key, value);
    }
});

This will create a local scope for the specific exception. You can also set global values with:

SentrySdk.ConfigureScope(scope =>
{
    scope.SetTag("my-tag", "my value");
    scope.User = new SentryUser { Id = userId };
});

I find this much nicer, especially that you can set global things for the session.

Overall, compared to AppCenter. Sentry, beats it hands down and goes way beyond what AppCenter crashes supports. Looking forward to some of the other App features the Sentry team comes out with.