Managing Dependencies in Windows and Cross Platform Applications

As with all software managing dependencies in often a lot harder than it seems, particularly when there are conflicting priorities. In this post I’m going to talk about some of the challenges developers face relating to dependencies when building Windows and cross/multi platform applications using .NET, the Windows App SDK and the Uno Platform. I’ll start off by saying that there’s no absolutes here, and no right or wrong answer.

Application Dependencies

Let’s start by talking about dependencies in general. The minute you run File, New Project and select a template to create your application from, you’re immediately buying into a set of dependencies. For example, take a minimal example of a console application, which you’d assume, given it’s simplicity, that it has almost no dependencies. This is basically correct but you are still taking a dependency on .NET. This is evident when you go to package and distribute the application where you’re givens options on how you want to reference the framework (see Application Publishing for more information).

Once we start to look at a Windows application (and here I’m referring to an application built using Windows UI and the Windows App SDK), the list of dependencies increases. The Windows App SDK has it’s own documentation on deployment options.

Now if we go cross platform with the Uno Platform, this adds not only the dependencies for .NET for iOS, Android, MacCatalyst but also the runtime for WASM and then the abstraction layer of Uno itself.

All of this before we even get to writing our application and deciding which third party dependencies we need to add. For example, if you want to add maps to your application you might use MapsUI, and for charts you can add LiveCharts. For more advanced controls that may not be offered natively for Uno, you can add any of the controls written for .NET MAUI such as those from SyncfusionGrial KitTelerikDevExpressEsriGrape City, and of course, the .NET MAUI Community Toolkit.

Updating Dependencies

As a general rule of thumb, applications that you’re going to be releasing either via the web, or via one of the app stores, should only reference stable releases of dependencies. For the purposes of this post when I talk about a stable release, I’m just referring about a release that isn’t marked as preview, beta, dev or any other non-numeric suffix (eg 1.5.0-dev is not a stable release, whereas 1.6.3 is). Of course, there may be times when you’re waiting on a bug fix, that you might reference a preview/beta/dev version of a dependency but when you go to publish your application, you should do so only with references to stable release.

The question then becomes when should you update your dependencies:

  • Should you only update when there are security updates?
  • Or perhaps only when there are major versions?
  • Or perhaps only when you require a feature that’s in a newer version?
  • What’s the benefit to updating more or less frequently?

These are just some of the questions that you should consider when deciding on a policy for your team as to when to update dependencies.

Let’s start with a quick discussion as to why you wouldn’t update dependencies as soon as they’re available. This often falls into a couple of buckets:

  • Not aware of new versions – If you’re in this bucket, then you probably haven’t started using dependabot (or equivalent). I would highly recommend using a service like dependabot so you know as soon as there is a dependency available, afterall, you can’t make a decision about something that you’re not aware of.
  • Unscheduled work – There’s no scope in the backlog for the current release to update to the latest dependency and test it to make sure it hasn’t caused any regressions. Whilst a lot of updates will be relatively benign, there may be some that require significant rework.

The latter is probably one of the main reasons that so many teams don’t frequently update their dependencies. Each time you update a dependency, there’s a risk that it will impact your application. It might be as subtle as changing the time it takes for a method to execute, resulting in cascading effects within the application, or it maybe that a dependency changes signficiantly as part of a new major version, resulting in the application logic being reworked to accommodate the change in behaviour. In all of these scenarios, there’s extra work that subsequently needs to go into testing the application to ensure no existing functionality is broken.

Always Update Dependencies

Ok, so at this point I’m going to make a bold statement that you should always update all dependencies as soon as they become available.

Let’s revisit the above issues that tend to drive delays to updating dependencies, starting with not being aware that updates exist. In this space, having a tool such as dependabot gives you almost immediate notification that there’s a new dependency, along with a PR that increases the version to the latest dependency. Whilst I wouldn’t say these tools are amazing, they do a sufficient job to make you aware of new dependency versions. I say that they’re not amazing because I’ve often seen cases where there are multiple related dependencies that have a new version but they get raised as independent PRs, making it harder than it needs to be to increment them. It’s not hard to cherry-pick commits from various PRs into a single PR but it’s extra work that could potentially be avoided.

Of course, just creating PRs to increment the version is only part of the story. With a suitable CI pipeline, there should be strong confidence that changing a dependency won’t break the application. If an update to a dependency causes one or more tests to fail, or perhaps the build itself fails, this will drive developers to update the application for the updated dependency.

Now we’re into the territory of unscheduled work, where dependency updates are causing the team to have to focus on modifying the application for no other reason than because a dependency changed. In some cases, the updated dependency has some intrinsic benefit to the application, for example better performance, that can be used to justify the effort required to update the application. However, in a lot of cases, updating dependencies won’t affect the application in any significant way. This begs the question as to why it’s important to update dependencies?

Lazy Dependency Updating

To answer the question of why updating dependencies is important, let’s take the reverse position of only updating dependencies when necessary. There may be a variety of reasons that require a dependency to be updated, for example new features, bug fixes (for bugs that manifest themselves in the application) and security concerns in the version used by the application. The underlying rule is to only update dependencies when there is a justifable reason to.

So the issue is that, like code that you have inside your application where it will naturally evolve with your application, dependencies evolve over time too. If you don’t pick up dependencies as they become available, it’s a bit like having a long running, stale, branch; at some point you’re going to have to rebase and/or mege the branch back to main. With dependencies, at some point in the future you will inevitably need to update them, and then you’ll experience the full pain of having to update through not one or two minor updates but possibly multiple major version updates, depending on how long it was since you updated. This could result in massive rework of your application and in most cases more timeconsuming as you work out what the migration path should be between versions. Now, multiply this by the number of dependencies you have and you can quite easily see this is where this strategy fails.

Interdependencies

To make matters worse, often the dependencies for your application will have interdependencies between them. Further, those relationships will be based on certain version of the dependencies, and not all combinations of the dependency versions will work nicely together.

If you take the lazy updating approach, you’ll find that you will have to bulk update to the latest changes, with perhaps no simple migration path for your code. Alternatively, you can incrementally attempt to update dependencies and having to balance or match any interpdendencies – at this point you’ll be wanting to reconsider life choices, well at least choices pertaining to updating dependencies.

Looking Forward using Canaries

In addition to using tools like dependabot, which typically will be used to highlight stable version updates, another technique is to use canary builds. By this I mean having a dedicated branch to building a version of your application using the absolute latest (including dev, beta, preview etc versions) of each dependency. A separate build process would be configured to run the same build pipeline you do for your application but using the canary branch. The basic approach is:

  • Check out the canary branch
  • Update source code to latest main but merging in all changes
  • Update package references to latest version of dependencies
  • Run regular build pipeline including all tests

Having this periodically run, perhaps once a day, will give you an even earlier heads up for when new dependencies might cause issues for your application. There may be specific workarounds you need to apply in order to keep your application building/running. The workarounds can be pushed to the canary branch until the stable version of those dependencies are available. At that point, the workarounds can be applied to the main source code branch for the application without much additional effort.

Summary

In this post I’ve covered some of the pros and cons of eager and lazy dependency updating. Hopefully for your application you’ve thought about this and have a strategy that works for you and your team.

2 thoughts on “Managing Dependencies in Windows and Cross Platform Applications”

Leave a comment