Developing with Multiple Repositories inside a Single Solution for .NET

Kanishk Tantia (HE/HIM)

Introduction

.NET projects can often become complex, with many interconnected parts. To stick to the principles of DRY, Don’t Repeat Yourself, a NuGet package may be used with common utilities, services, and clients for multiple .NET projects to consume. Separating a solution into different NuGet packages and application projects encourages code reusability but raises a question of how to organize projects for source control. Should the solution be split across multiple repositories (micro-repo) or should the solution live in a single repository (mono-repo)?

In a mono repository model, this is not a significant problem. A single .sln will suffice to build and debug multiple .csproj files. However, what happens when the repos must be split up? This is often required to enable independent build and release cycles.

In this article, we will discuss the ways in which our development team handled simultaneous local development for three repositories, each with its own .NET solution.

Context

In a recent engagement, our team needed to develop a service composed of a backend application, frontend application, and a NuGet package to host shared code. Each .NET component needed to be built, tested, and deployed with independent CI/CD pipelines.

There are pros and cons to using multiple repositories for development.

Pros of Multiple Repositories

  1. Each component can manage its own version of pinned dependencies
  2. Each component can use the minimum set of dependencies instead of being bloated with unneeded packages.
  3. Independent deployment and release allows only one component to be deployed if changes are made.
  4. Enables autonomous and simultaneous work which is especially useful in a hybrid work scenario.

Cons of Multiple Repositories

  1. Remote libraries need to be frequently resynced with each other and may be a version behind the latest code being run in local environments.
  2. Refactoring can take place across multiple repositories and may break unintended workflows.

After evaluating these pros and cons against the specific requirements of our project, we settled on using multiple repositories; each with their own .NET solution and deployment pipeline. One of the repositories held common data models and functionality as a NuGet package for other repositories to reference.

The Problem

We will showcase the solution by using a sample problem.

The Contoso Pet store needs to track vaccine records for all the pets inside the store. The existing vaccination records are uploaded when the pet is first brought to the store and may be updated later. These records will only be retrieved when a pet is being adopted by a customer at one of Contoso’s many pet stores.

The constraints are:

  1. Each pet has different vaccination records and schedules.
  2. Contoso has a frontend Registration Application used to record initial pet data.
  3. Contoso has a backend Adoption application which stores pet record data and makes them available for retrieval.

Both the Registration and Adoption applications use a shared Pets Library of common components including database clients and helper functions.

The diagram below shows how the three repositories are connected.

Architecture Diagram

Here you can see the three repositories involved, and how the two application repositories share components that are kept in the Shared Pets Library.

Each repository contains a unique .sln file so the project can be built and deployed. While this works well for the CI/CD pipelines, it is difficult to do local development for all repositories simultaneously. This is because the interdependent projects must establish dependencies with PackageReferences rather than ProjectReferences.

Both web applications rely on the pre-built version of the shared NuGet package for deployment. This means that local changes to the shared library are not immediately reflected in the frontend apps. In order for updates to the shared code to be used, the NuGet package must first be built and restored into each web application. This introduces a significant delay in the development cycle when changes span multiple solutions.

During development, a project reference to the local repository is desired.

<ItemGroup>
    <!-- Other dependencies -->
    <ProjectReference Include="../../../../pets.library.csproj" />
</ItemGroup>

In the build system, a NuGet package reference is desired.

<ItemGroup>
    <!-- Other dependencies -->
    <PackageReference Include="Pets.Library" Version="1.0" />
</ItemGroup>

The problem can be reduced to: How can we locally mimic a mono-repo model while still maintaining strict separation between all repositories?

The Solution

To resolve this, create an additional repository for local development. The Local Development Repository:

  1. Contains a single .sln file which referenced all existing .csproj files
  2. Use git submodule to pull updated versions of all existing repositories

This Local Development Repository allows the usage of a single .sln file which contains the existing projects, each of which could then reference the locally built shared library.

By using git submodule we achieve the following:

  • A mono repo for local development
  • predictable relative locations for specifying local project references between repositories.
  • Strict separation of all three repositories for remote deployment
  • An environment where all three repositories can depend on each other and be updated in parallel

For more details on git submodule, see: Git – Submodules (git-scm.com)

Modified .csproj Files

The .csproj files for both the Registration and Adoption apps will reference the shared library with the following conditionals:

<!-- Use a PackageReference when working within the local development solution. -->
<ItemGroup Condition="!Exists(../../local-dev.sln)">
    <PackageReference Include="Pets.Library" Version="1.0" />
</ItemGroup>

<!-- Use a ProjectReference when not working within the local development solution -->
<ItemGroup Condition="Exists(../../local-dev.sln)">
    <ProjectReference Include="../../../../pets.library.csproj" />
</ItemGroup>

The conditional ItemGroups allow the applications to dynamically change the package being used based on the current environment. Working within the Local Development Repository causes local project references to be used, and building in a deployment pipeline would cause the NuGet package references to be used.

Since the submodules are part of the local development repository, the path to local-dev.sln and pets.library.csproj do not change.

Other conditions could be used such as checking for an environment variable or specific build configuration. But, by checking for the presence of the local development solution file, the developer does not need to do any additional environment setup.

Development Best Practices

One of the major concerns with multi-repo development is ensuring that every repository can be updated without breaking the workflow for other repositories.

To do so, we attempted to streamline our PR merge and review process.

  1. PRs have a standardized naming scheme across repos: “Feature Number: Title” which allows the same PR to be tracked across multiple repos for easy review.
  2. PRs that need to be merged sequentially are tagged with [X/4] in the title. For example, the first PR is [1/4], the second is [2/4] etc.
  3. PRs that rely on other PRs to be merged have a blocking task in the PR review which cannot be resolved until the required PRs have been merged and deployed, to prevent broken builds.

These simple rules helped keep the PR review tempo up and allowed developers to quickly review relevant PRs in sequence. Even so, creating, reviewing and merging PRs was a particular pain point in the multi-repo solution.

Other Solutions

There are some other solutions which can be used. However, many of these have drawbacks or did not adequately fit our constraints.

Manually editing .csproj files to switch between the project and package reference

Pros:

  1. No need to maintain an additional development repository.
  2. Less complex .csproj file for better human readability.

Cons:

  1. Manually manipulating the .csproj is time consuming.
  2. Makes it harder to merge main into a develop branch since there may be conflicts in the .csproj file.
  3. The wrong version of the .csproj may be committed, causing build pipelines to fail.

NuGet Reference Switcher Extension

We also found a third-party extension, the NuGet Reference Switcher, which will switch between package and project references.

Pros:

  1. Works as an extension inside Visual Studio and requires no extraneous setup
  2. No need to maintain a fourth repository

Cons:

  1. Still requires manually toggling references before and after development.
  2. Only works for Visual Studio on Windows, not with other editors like VSCode.
  3. Third Party extensions may not be allowed by engagement or project policy.
  4. Extension is maintained by an external party and so it may not be reliable.

Summary

In this article, we discuss handling multiple interconnected .NET repositories for local development and remote deployment. We summarize the pros and cons of using multiple repositories to version and source control multiple .NET solutions instead of using a mono-repository development model.

When using multiple repositories to develop multiple interconnected solutions in parallel, we suggest using conditional statements in .csproj files to switch between packagereference and projectreference settings, allowing for seamless switching between the local and remote versions of different packages. We also demonstrate the use of git submodule to simulate a mono-repository local development environment while maintaining strict separation in remote environments. Finally, we discuss development best practices for working with multiple repositories in an enterprise team.

Feedback usabilla icon