Skip to content

Update / Upgrade Flow

Overview

Every folder under src/ in this repository is built and published as its own independent NuGet package by its own independent pipeline (see Pipelines.md). Components never reference each other via project references — they only ever consume each other's published NuGet packages. This means a change to a lower-level package is not visible to anything that depends on it until:

  1. The lower-level package is built and published, and
  2. Every dependent package explicitly bumps its NuGet package reference to the new version, is rebuilt, and republishes itself.

Because of the dependency shape described in Architecture.md — ServiceContracts (and Common) at the bottom, implementation libraries in the middle, ServiceExtensions at the top — a change that starts at the bottom of the graph must be propagated upward through every affected layer, one package release at a time, in order. You cannot skip a layer: ServiceExtensions cannot safely pick up a ServiceContracts change until the implementation package(s) it depends on have already picked up and republished with that change.

This document describes that propagation flow and the steps required at each layer. It is written to be followed exactly, in order, by a human contributor or by an AI coding agent performing the upgrade on a human's behalf.

The three layers, and why order matters

Layer Example package(s) Depends on
1. Foundation SAIF.NetCore.ServiceContracts, SAIF.NetCore.Common Nothing (SAIF-internal)
2. Implementation SAIF.NetCore.AuthService, SAIF.NetCore.Logging, SAIF.NetCore.HttpClients, etc. Foundation packages (ServiceContracts/Common)
3. Top / app entry point SAIF.NetCore.ServiceExtensions Foundation packages and every implementation package it composes

Each layer only knows about the published package version of the layer(s) below it — never the other repository's source. So if you edit ServiceContracts and want that change to eventually reach an application, you must walk up the graph, layer by layer, publishing a new package at each step and pulling that new package into the next layer up, until you reach ServiceExtensions.

Step-by-step procedure

Step 1 — Update the foundation package (if the change requires it)

Only needed if the change touches a shared interface, contract, or POCO in SAIF.NetCore.ServiceContracts (or a shared type in SAIF.NetCore.Common). Skip straight to Step 2 if the change is entirely internal to one implementation library.

  1. Make the code change under src/SAIF.NetCore.ServiceContracts/ (and/or src/SAIF.NetCore.Common/) on a feature branch.
  2. Open a pull request into main targeting only that component's path (src/SAIF.NetCore.ServiceContracts/**).
  3. Merge the PR. The component's pipeline (see Pipelines.md) is CI-triggered automatically by the merge and publishes a new version of the SAIF.NetCore.ServiceContracts NuGet package.
  4. Wait for the pipeline to succeed and note the new package version before proceeding — Step 2 needs it.

Step 2 — Pull the updated foundation package into each affected implementation project

Repeat this step once per implementation package that needs the change (there may be several; check Architecture.md's dependency table for which implementation packages consume the changed contract type).

  1. On a feature branch under src/{implementation-component}/, bump the SAIF.NetCore.ServiceContracts (and/or SAIF.NetCore.Common) NuGet reference to the new version published in Step 1.
  2. Make whatever implementation code changes are required to compile/work against the updated contract (e.g. implement a new interface member, adjust to a changed signature).
  3. Open a pull request into main, scoped to that component's path (src/{implementation-component}/**).
  4. Merge the PR. That component's pipeline publishes a new version of the implementation package.
  5. Wait for the pipeline to succeed and note the new package version — Step 3 needs it.
  6. Repeat for every other implementation package affected by the Step 1 change.

Step 3 — Pull the updated implementation(s) and foundation package into ServiceExtensions

ServiceExtensions is the application entry point — nothing above it consumes these packages directly except applications, so this is always the final, required step for any change to reach consuming applications, even if the change started in an implementation package rather than a contract.

  1. On a feature branch under src/SAIF.NetCore.ServiceExtensions/, bump the NuGet references for every implementation package updated in Step 2 (and the foundation package from Step 1, if ServiceExtensions references it directly).
  2. Make any wiring changes needed (e.g. new DI registrations, updated startup extension methods).
  3. Open a pull request into main, scoped to src/SAIF.NetCore.ServiceExtensions/**.
  4. Merge the PR. The ServiceExtensions pipeline publishes a new version of the SAIF.NetCore.ServiceExtensions package.
  5. Applications can now bump their SAIF.NetCore.ServiceExtensions package reference to pick up the full, end-to-end change.

Rules of thumb

  • One package, one PR, one pipeline run, in order. Never bundle a foundation change and its downstream implementation change into a single PR — they live in different components and are built by different pipelines; each layer's PR should only touch that layer's src/{component}/ folder.
  • Always wait for a layer's pipeline to succeed and publish before starting the next layer's PR. The next layer's PR needs to reference a real, published package version, not a guess.
  • Not every change touches all three layers. A change contained entirely within one implementation package (no contract change) only needs Step 2 (that package) and Step 3 (ServiceExtensions, to pick it up) — skip Step 1. A change entirely internal to ServiceExtensions only needs Step 3.
  • Identify every downstream consumer before you start. Before beginning Step 1, check Architecture.md's component table and the package's own NuGet dependents to know which implementation packages (Step 2) and whether ServiceExtensions (Step 3, always) will need follow-up PRs.
  • This applies equally to AI agents. If an AI agent is asked to "upgrade package X", it should walk this same three-step propagation — never modify a downstream package to depend on a not-yet-published upstream version, and never skip publishing/waiting between layers.

Flow diagram

flowchart TD
    A["Step 1: Modify ServiceContracts / Common<br/>(only if shared contract changes)"] --> B["PR merged to main<br/>(src/ServiceContracts/**)"]
    B --> C["Pipeline builds & publishes<br/>new ServiceContracts package"]

    C --> D["Step 2: Pull new ServiceContracts package<br/>into each affected implementation project"]
    D --> E["Make implementation code changes"]
    E --> F["PR merged to main<br/>(src/{implementation}/**)"]
    F --> G["Pipeline builds & publishes<br/>new implementation package"]

    G --> H["Step 3: Pull updated implementation package(s)<br/>+ ServiceContracts into ServiceExtensions"]
    H --> I["Update DI/startup wiring as needed"]
    I --> J["PR merged to main<br/>(src/ServiceExtensions/**)"]
    J --> K["Pipeline builds & publishes<br/>new ServiceExtensions package"]

    K --> L["Applications bump their<br/>ServiceExtensions package reference"]

    classDef codeChange fill:#e8590c,stroke:#8a3a08,stroke-width:2px,color:#ffffff
    classDef pipeline fill:#2b8a3e,stroke:#1a5c28,stroke-width:2px,color:#ffffff
    classDef appConsume fill:#1864ab,stroke:#0b3d66,stroke-width:2px,color:#ffffff

    class A,D,H codeChange
    class C,G,K pipeline
    class L appConsume
  • 🟧 Orange boxes: code changes made on a feature branch, resolved by a PR into main.
  • 🟩 Green boxes: automatic pipeline output — a new NuGet package version.
  • 🟦 Blue box: end state — applications can consume the fully-propagated change.