Security change on December 1, 2026: TLS 1.2 will become the minimum standard for 365 business Print Agent. Details and system requirements
365 business development
Concepts

Dependency resolver

Resolve-BcDependencies is ALbuild's headline capability: a real transitive dependency resolver across package feeds that respects the actual Business Central build and the cross-dependencies of candidate packages.

Why it matters

Unlike a greedy "latest that satisfies the minimum version" approach, ALbuild solves for a mutually-compatible, transitively-complete set whose every member's own dependencies, including Microsoft first-party apps, are satisfied both by the chosen set and by the target build. The result is captured in a dependencies.lock.json for reproducible builds.


The problem it solves

Given the app(s) being built and the BC build they target, produce a mutually-compatible, transitively-complete set of dependency packages. For on-premise targets it selects the runtime package whose platform version matches the target build.

Get-BcContainerAppInfo feeds the separate -InstalledApps parameter: the apps a build container already has, pinned as a satisfied baseline so they are not fetched again.

`app.json` is a floor, not a ceiling

application and platform in app.json are your app's minimum supported versions. Using them as the resolution ceiling is a subtle and expensive mistake: the resolver then rejects real dependency apps that were built for the current BC and silently falls back to symbol-only AppSource packages, which compile but cannot be installed.

The ceiling is therefore chosen separately, by Resolve-BcDependencyCeiling, highest precedence first:

  1. An explicit target version. In the Resolve Dependencies task this defaults to $(bcArtifactVersion) from Get BC Artifact, so even a container-less build honours the BC version it targets.
  2. The platform app installed in the build container, which is the exact build.
  3. bcVersion pinned in albuild.json.

If none is available, the resolver falls back to the newest version each app.json is compatible with.


Feed providers

Feeds can be supplied three ways: passed via -Feeds, registered with Register-BcFeed, or, most conveniently, declared once in the project's albuild.json feeds array, which Resolve-BcDependencies loads automatically (see project config). Because the resolver detects runtime/indirect packages from metadata and finds packages by app id, a declared feed is usually just its URL.

Supported sources: NuGet v3 (nuget.org, Azure DevOps, GitHub Packages), Azure Universal Packages, GitHub Releases, the local workspace (.alpackages), and the Microsoft public feeds (MSSymbols, MSApps, AppSourceSymbols). Each provider maps an AL app id + publisher + name to a feed package id according to the package kind:

KindPackage id scheme
symbols{publisher}.{name}.symbols.{id}
apps{publisher}.{name}.{id}
runtime{publisher}.{name}.runtime-{version}
localized…{countryCode}

The empty GUID 00000000-0000-0000-0000-000000000000 denotes Platform/Application and is provided by the container, never downloaded. Microsoft apps are pinned to the target build and never downloaded from a third-party feed, recognised by a Microsoft publisher, a Microsoft. package-id prefix, or the empty GUID. This covers the first-party apps System Application (63ca2fa4-…), Base Application (437dbf0e-…) and Business Foundation (f3552374-…), as well as platform-shipped extensions an ISV app may depend on.

Real-world NuGet conventions consumed

ISVs publish to NuGet with Microsoft's BC packaging conventions, which the provider reads directly:

  • Dependencies come from the package's NuGet metadata (the nuspec <dependency> entries), not from the .app's NavxManifest.xml. This is read over the network without downloading the .app, and is the only thing that works for runtime apps. Microsoft.Application / Microsoft.Platform dependencies carry the required BC version.
  • Direct app packages (e.g. Continia, Simova) contain the .app/.runtime.app themselves.
  • Indirect "metapackages" (e.g. DYCE, 365 business development) contain no .app; their only payload dependency is a per-app-version runtime sub-package, whose own NuGet versions are the BC platform builds it was compiled for. For an on-premise target the provider selects the build closest to but not exceeding the target platform (e.g. BC 27.1.41698.51164 → 27.1.41698.41776) and downloads that sub-package, which also carries the localised runtime app.
  • The app id (trailing GUID) is the only stable key. Publishers vary the publisher/name segments. The provider therefore (1) carries the exact nuspec package id as a hint, (2) rewrites a plain {publisher}.{name}.{id} hint to this feed's id scheme infix, and (3) falls back to the feed's search service queried by app id: picking the package whose id ends in that GUID. Results are memoised so the backtracking solver does not re-fetch them.

Source precedence

Beyond feeds, a project can supply dependencies as committed local .app files (localPackages), direct-download URLs (urlPackages) and Azure DevOps Universal packages (universalPackages) — see dependency sources. When the same app is available from more than one source, the resolution order is:

  1. localPackages / urlPackages — authoritative. A dependency you ship directly (a committed .app or an ISV's download link) is pinned and always used, even when a registered feed offers a newer version. This keeps an ISV app that has no public feed — or one you deliberately fix to a known build — from being replaced by a feed copy.
  2. NuGet feeds.
  3. universalPackages — feed-subordinate. A registered feed's version wins over a universal copy; the universal package stays only as a fallback for apps no feed provides.

Name-based pinning (encrypted runtime apps)

A local/URL .app is normally read to get its app id and pinned by id. An encrypted BC runtime package cannot be read on the host, so instead its {Publisher}_{Name}_{Version}.app file name is parsed and the dependency is pinned by Publisher + Name (exact, case-insensitive). Such a package:

  • is never fetched from a feed (the declared dependency counts as satisfied locally), and is installed straight from localPackages — its real id/version are read inside the container;
  • is copied into .alpackages like every other local package, and both compile engines resolve against it from there — the host AL Tool as well as the container compiler. alc reads the runtime package's symbol reference itself, so a separate symbol .app is not required.

The name-based pinning therefore exists because ALbuild's host-side reader cannot open the package, not because the compiler cannot use it. When a build container is available, ALbuild reads the real app id through it (the container decrypts the manifest) and pins by id instead.


Choosing the BC version: downgrade on conflict

There is a chicken-and-egg problem in a Select = Latest pipeline: you want the newest Business Central build, but your ISV dependencies may not have shipped packages for it yet. Picking the latest artifact and then discovering that dependencies do not resolve wastes a container and fails the build.

Select-BcArtifactForDependencies inverts that. It enumerates the latest artifact and up to -MaxStepsBack earlier minor versions (crossing a major boundary to the previous major's newest minor when a minor reaches x.0) and, newest first, dry-runs the resolver (-SkipDownload) for every project against that build. The first version where all projects resolve is the one used.

  • If that is older than the latest, the result is flagged Downgraded and ConflictReason explains why the latest was skipped, which the pipeline surfaces as a warning rather than swallowing it.
  • Because it uses an explicit target build and a synthetic Microsoft baseline, no container is needed: the decision is made before the single container is created.
  • An explicit -Version pins the choice and skips the enumeration entirely.

Diagnosing a resolve without changing anything

Get-BcDependencyReconciliation is read-only: it installs nothing. For each dependency in app.json it reports whether the dependency is already satisfied by an app installed in the container, otherwise whether a configured feed can provide a compatible version, otherwise that it is missing. Microsoft first-party apps ship with the artifact, so they normally show up as satisfied by the container.

Use it when a resolve fails and you need to know which side the gap is on, before touching feeds or versions.


The algorithm (backtracking constraint solve)

  1. Pin Microsoft apps to the target build (discovered transitively, not just the three first-party apps); a target older than a Microsoft requirement is a precise, reported conflict.
  2. Pick candidate versions (≥ minimum) across feeds for each unresolved dependency, filtered to those whose platform/application are compatible with the target build. For an indirect metapackage this also requires that its runtime sub-package has a build at or below the target.
  3. Assign and recurse: assign a candidate, enqueue its transitive dependencies, and recurse; on conflict, backtrack.
  4. Write the result: on success, write the deduplicated, topologically sorted set (dependencies before dependants) to .alpackages and a dependencies.lock.json for reproducible builds, recording, for each package, both the app version and the concrete download package/version actually fetched. On failure, emit a precise conflict report.

Version-range checks reuse Test-BcVersionInRange / ConvertTo-BcVersion from the Core module.


Lock file

The dependencies.lock.json records, for every package, both the app version and the concrete downloadPackage / downloadVersion actually fetched (these differ from the app package for indirect metapackages). Commit it to get reproducible builds: the same dependency set every time, regardless of what newer versions later appear on a feed.


Last modified on