🇪🇸 Spanish Version | 🇬🇧 English Version
This project is a proof of concept that demonstrates how to manage dependencies in a .NET environment where:
- In Production: The application consumes libraries as published NuGet packages
- In Development: Developers need to work with library source code for more efficient development and debugging
This approach combines the best of both worlds: the simplicity of NuGet packages in production and the flexibility of project references in development.
In large projects with multiple distributed libraries, developers face several challenges:
- Difficult debugging: With NuGet packages, it's hard to debug and understand dependency code
- Slow development cycle: Modifying a library requires packaging, publishing, and updating in each consuming project
- Complex synchronization: Keeping multiple repositories synchronized is error-prone
- Fragmented experience: Different configurations between development and production can cause inconsistencies
The project uses conditional references in the .csproj file that automatically change based on:
- Build Configuration (Debug vs Release)
- Local project existence (via junctions/symlinks)
<ItemGroup>
<!-- In Debug AND if local project exists → ProjectReference -->
<ProjectReference Include="../dev-shared\Lib.One\Lib.One.csproj"
Condition="'$(Configuration)'=='Debug' and Exists('../dev-shared/Lib.One/Lib.One.csproj')" />
<!-- In Release OR if local project does NOT exist → PackageReference -->
<PackageReference Include="Lib.One"
Condition="'$(Configuration)'!='Debug' or !Exists('../dev-shared/Lib.One/Lib.One.csproj')" />
</ItemGroup>distributed-monorepo/
├── MainApp/ # Main application (WPF .NET 5)
│ ├── MainApp.csproj # With conditional references
│ ├── Directory.Packages.props # Centralized version management
│ ├── add-ref.ps1 # Script: Enable library development
│ ├── del-ref.ps1 # Script: Disable library development
│ └── add-dep.ps1 # Script: Add new dependency
│
├── dev-shared/ # Folder for junctions to local projects
│ ├── Lib.One/ → (junction) # Points to Lib.One project when active
│ └── Lib.Two/ → (junction) # Points to Lib.Two project when active
│
├── Lib.One/ # Shared library (netstandard2.0)
│ └── Lib.One.csproj
│
└── Lib.Two/ # Shared library (netstandard2.0)
└── Lib.Two.csproj
To work with the source code of an existing library:
cd MainApp
.\add-ref.ps1 -csprojPath "Lib.One/Lib.One.csproj"What does this script do?
- Creates a junction (Windows symlink) in
dev-shared/Lib.Onepointing to the real project - Adds the project to
MainApp.slnfor easy navigation - MSBuild will now automatically use
ProjectReferenceinstead ofPackageReference
⚠️ Important: After runningadd-ref.ps1, it's recommended to do adotnet cleanand rebuild to ensure debugging works correctly:dotnet clean dotnet build -c DebugThis removes the previous NuGet package binaries and forces compilation from source code.
To go back to using the NuGet package:
.\del-ref.ps1 -projectName "Lib.One"What does this script do?
- Removes the junction from
dev-shared/Lib.One - Removes the project from the solution
- MSBuild automatically reverts to using
PackageReference
To configure a new dependency with support for both modes:
.\add-dep.ps1 -packageName "Lib.Three" -packageVersion "2.1.0"What does this script do?
- Adds the version to
Directory.Packages.props - Configures conditional references in
MainApp.csproj - Leaves the project ready to use
add-ref.ps1when needed
- Switch between NuGet package and source code without modifying project files
- Each developer can enable only the libraries they need to modify
- Direct step-through debugging into library code
- Full IntelliSense with all source code information
- Set breakpoints in libraries during development
- Changes in libraries are reflected immediately (no repackaging)
- Faster incremental compilation
- Immediate feedback when modifying dependencies
- Release builds always use versioned NuGet packages
- No risk of deploying unpublished code
- Centralized version control in
Directory.Packages.props
- Each developer configures only what they need
- No version control conflicts due to different configurations
- Junctions are not committed (they're in
.gitignore)
Directory.Packages.propscentralizes NuGet versions- Easy to update multiple dependencies at once
- Compatible with NuGet's Central Package Management
- Release configuration always uses exact NuGet versions
- CI/CD works without special configuration
- Guaranteed reproducibility across different environments
The system is based on two MSBuild conditions:
'$(Configuration)'=='Debug': Checks if we're in Debug modeExists('../dev-shared/Lib.One/Lib.One.csproj'): Checks if the local project exists
The combination creates three scenarios:
| Scenario | Configuration | Project Exists | Reference Used |
|---|---|---|---|
| Active development | Debug | ✅ Yes | ProjectReference |
| Normal development | Debug | ❌ No | PackageReference |
| Production | Release | ❓ Either | PackageReference |
Junctions (also called soft links or directory junctions) are like shortcuts at the file system level:
- Don't duplicate files (save space)
- MSBuild treats them as regular folders
- Created with
New-Item -ItemType Junction - Not included in Git (add
dev-shared/to.gitignore)
Contains the conditional references that allow automatic switching between modes.
Uses Central Package Management to:
- Centralize NuGet versions
- Avoid version conflicts
- Facilitate bulk updates
- add-ref.ps1: Automates enabling development mode
- del-ref.ps1: Automates disabling development mode
- add-dep.ps1: Configures new dependencies with the correct pattern
To test this proof of concept on your machine, you need to create a local NuGet repository and generate the packages.
First, create a folder that will act as your local NuGet feed:
# Create folder for local repository
mkdir C:\LocalNuGetAdd the local repository to your NuGet sources:
# Add local NuGet source
dotnet nuget add source C:\LocalNuGet --name "LocalDev"
# Verify it was added correctly
dotnet nuget list sourceYou should see something like:
Registered Sources:
1. nuget.org [Enabled]
https://api.nuget.org/v3/index.json
2. LocalDev [Enabled]
C:\LocalNuGet
For each library (Lib.One and Lib.Two), generate the NuGet package:
# Package Lib.One
cd Lib.One
dotnet pack -c Release -o C:\LocalNuGet
cd ..
# Package Lib.Two
cd Lib.Two
dotnet pack -c Release -o C:\LocalNuGet
cd ..This will create .nupkg files in C:\LocalNuGet:
Lib.One.1.0.0.nupkgLib.Two.1.0.0.nupkg
Make sure Directory.Packages.props has the correct versions:
<Project>
<ItemGroup>
<PackageVersion Include="Lib.One" Version="1.0.0" />
<PackageVersion Include="Lib.Two" Version="1.0.0" />
</ItemGroup>
</Project>cd MainApp
dotnet restore
dotnet build -c ReleaseThe Release build will use the NuGet packages from C:\LocalNuGet.
Now enable development mode for a library:
# Enable Lib.One development
.\add-ref.ps1 -csprojPath "Lib.One/Lib.One.csproj"
# Build in Debug (will use ProjectReference)
dotnet build -c DebugIn Release mode (NuGet):
dotnet build -c Release
# ✅ Uses packages from C:\LocalNuGetIn Debug mode without junction:
.\del-ref.ps1 -projectName "Lib.One"
dotnet build -c Debug
# ✅ Uses packages from C:\LocalNuGetIn Debug mode with junction:
.\add-ref.ps1 -csprojPath "Lib.One/Lib.One.csproj"
dotnet build -c Debug
# ✅ Uses project source codeIf you modify a library and want to update the NuGet package:
cd Lib.One
# Increment version in Lib.One.csproj (optional)
# <Version>1.0.1</Version>
# Repackage
dotnet pack -c Release -o C:\LocalNuGet
# Update version in MainApp/Directory.Packages.props
# <PackageVersion Include="Lib.One" Version="1.0.1" />
cd ../MainApp
dotnet restore --force-evaluate
dotnet build -c ReleaseIf you need to start from scratch:
# Clear NuGet cache
dotnet nuget locals all --clear
# Remove packages from local repository
Remove-Item C:\LocalNuGet\*.nupkg
# Regenerate packages
cd Lib.One
dotnet pack -c Release -o C:\LocalNuGet
cd ../Lib.Two
dotnet pack -c Release -o C:\LocalNuGetInstead of adding the source globally, you can create a nuget.config at the workspace root:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="LocalDev" value="C:\LocalNuGet" />
</packageSources>
</configuration>This makes the configuration portable and project-specific.
- .NET 5.0 or higher (for the main application)
- .NET Standard 2.0 (for the libraries)
- PowerShell 5.1 or higher
- Windows (due to junctions usage; use symlinks on Linux/Mac)
- Visual Studio 2019+ or VS Code with C# extensions
This pattern is ideal for:
- Teams with multiple microservices sharing common libraries
- Enterprise projects with internal libraries published to a private feed
- Framework development where some developers work on the core
- Modular applications with plugins or extensions as NuGet packages
- Gradual migration from monorepo to multiple repositories
✅ No code duplication
✅ Per-developer configuration (don't commit junctions)
✅ Compatible with existing CI/CD
✅ No additional tools required
✅ Works with any IDE
| Approach | Pros | Cons |
|---|---|---|
| Pure monorepo | Everything in one place | Grows indefinitely, slow builds |
| Git submodules | Integrated with Git | Complex, error-prone |
| Visual Studio Workspace | Native to VS | Doesn't work well with CI/CD |
| Conditional references ✅ | Perfect balance | Requires setup scripts |
- MSBuild Conditions
- Central Package Management
- Directory Junctions
- Project References vs Package References
This is a proof of concept. Feel free to adapt it to your specific needs.
- Script to synchronize all junctions automatically
- Integration with
dotnetCLI tool as an extension - Multi-platform support (detect OS and use symlinks on Linux/Mac)
- Automatic version validation between local code and NuGet
- Git hook to prevent commits with active junctions
This project is an educational example. Use it freely in your projects.
Author: Proof of Concept - Distributed Monorepo Pattern
Date: 2025
Technologies: .NET, MSBuild, PowerShell, NuGet
Este proyecto es un proof of concept que demuestra cómo gestionar dependencias en un entorno .NET donde:
- En Producción: La aplicación consume librerías como paquetes NuGet publicados
- En Desarrollo: Los desarrolladores necesitan trabajar con el código fuente de las librerías para desarrollo y depuración más eficiente
Este enfoque combina lo mejor de dos mundos: la simplicidad de paquetes NuGet en producción y la flexibilidad de referencias a proyectos en desarrollo.
En proyectos grandes con múltiples librerías distribuidas, los desarrolladores enfrentan varios desafíos:
- Depuración difícil: Con paquetes NuGet, es complicado depurar y entender el código de las dependencias
- Ciclo de desarrollo lento: Modificar una librería requiere empaquetar, publicar y actualizar en cada proyecto consumidor
- Sincronización compleja: Mantener múltiples repositorios sincronizados es propenso a errores
- Experiencia fragmentada: Diferentes configuraciones entre desarrollo y producción pueden causar inconsistencias
El proyecto utiliza referencias condicionales en el archivo .csproj que cambian automáticamente según:
- Configuración de Build (Debug vs Release)
- Existencia del proyecto local (mediante junctions/symlinks)
<ItemGroup>
<!-- En Debug Y si existe el proyecto local → ProjectReference -->
<ProjectReference Include="../dev-shared\Lib.One\Lib.One.csproj"
Condition="'$(Configuration)'=='Debug' and Exists('../dev-shared/Lib.One/Lib.One.csproj')" />
<!-- En Release O si NO existe el proyecto local → PackageReference -->
<PackageReference Include="Lib.One"
Condition="'$(Configuration)'!='Debug' or !Exists('../dev-shared/Lib.One/Lib.One.csproj')" />
</ItemGroup>distributed-monorepo/
├── MainApp/ # Aplicación principal (WPF .NET 5)
│ ├── MainApp.csproj # Con referencias condicionales
│ ├── Directory.Packages.props # Gestión centralizada de versiones
│ ├── add-ref.ps1 # Script: Activar desarrollo de una librería
│ ├── del-ref.ps1 # Script: Desactivar desarrollo de una librería
│ └── add-dep.ps1 # Script: Añadir nueva dependencia
│
├── dev-shared/ # Carpeta para junctions a proyectos locales
│ ├── Lib.One/ → (junction) # Apunta al proyecto Lib.One cuando está activo
│ └── Lib.Two/ → (junction) # Apunta al proyecto Lib.Two cuando está activo
│
├── Lib.One/ # Librería compartida (netstandard2.0)
│ └── Lib.One.csproj
│
└── Lib.Two/ # Librería compartida (netstandard2.0)
└── Lib.Two.csproj
Para trabajar con el código fuente de una librería existente:
cd MainApp
.\add-ref.ps1 -csprojPath "Lib.One/Lib.One.csproj"¿Qué hace este script?
- Crea una junction (symlink de Windows) en
dev-shared/Lib.Oneapuntando al proyecto real - Añade el proyecto a la solución
MainApp.slnpara fácil navegación - MSBuild ahora usará automáticamente
ProjectReferenceen lugar dePackageReference
⚠️ Importante: Después de ejecutaradd-ref.ps1, es recomendable hacer undotnet cleany recompilar para asegurar que la depuración funcione correctamente:dotnet clean dotnet build -c DebugEsto elimina los binarios del paquete NuGet anterior y fuerza la compilación desde el código fuente.
Para volver a usar el paquete NuGet:
.\del-ref.ps1 -projectName "Lib.One"¿Qué hace este script?
- Elimina la junction de
dev-shared/Lib.One - Remueve el proyecto de la solución
- MSBuild automáticamente vuelve a usar
PackageReference
Para configurar una nueva dependencia con soporte para ambos modos:
.\add-dep.ps1 -packageName "Lib.Three" -packageVersion "2.1.0"¿Qué hace este script?
- Añade la versión a
Directory.Packages.props - Configura las referencias condicionales en
MainApp.csproj - Deja el proyecto listo para usar
add-ref.ps1cuando sea necesario
- Cambia entre paquete NuGet y código fuente sin modificar archivos del proyecto
- Cada desarrollador puede activar solo las librerías que necesita modificar
- Step-through debugging directo en el código de las librerías
- IntelliSense completo con toda la información del código fuente
- Establecer breakpoints en librerías durante el desarrollo
- Los cambios en librerías se reflejan inmediatamente (sin reempaquetar)
- Compilación incremental más rápida
- Feedback inmediato al modificar dependencias
- Las builds de Release siempre usan paquetes NuGet versionados
- No hay riesgo de desplegar código no publicado
- Control de versiones centralizado en
Directory.Packages.props
- Cada desarrollador configura solo lo que necesita
- No hay conflictos en control de versiones por diferentes configuraciones
- Las junctions no se commitean (están en
.gitignore)
Directory.Packages.propscentraliza las versiones de NuGet- Facilita actualizar múltiples dependencias a la vez
- Compatible con Central Package Management de NuGet
- La configuración de Release siempre usa versiones exactas de NuGet
- CI/CD funciona sin configuración especial
- Reproducibilidad garantizada en diferentes entornos
El sistema se basa en dos condiciones de MSBuild:
'$(Configuration)'=='Debug': Verifica si estamos en modo DebugExists('../dev-shared/Lib.One/Lib.One.csproj'): Verifica si existe el proyecto local
La combinación crea tres escenarios:
| Escenario | Configuración | Proyecto Existe | Referencia Usada |
|---|---|---|---|
| Desarrollo activo | Debug | ✅ Sí | ProjectReference |
| Desarrollo normal | Debug | ❌ No | PackageReference |
| Producción | Release | ❓ Cualquiera | PackageReference |
Las junctions (también llamadas soft links o directory junctions) son como accesos directos a nivel de sistema de archivos:
- No duplican archivos (ahorran espacio)
- MSBuild las trata como carpetas normales
- Se crean con
New-Item -ItemType Junction - No se incluyen en Git (añadir
dev-shared/al.gitignore)
Contiene las referencias condicionales que permiten cambiar entre modos automáticamente.
Usa Central Package Management para:
- Centralizar versiones de NuGet
- Evitar conflictos de versiones
- Facilitar actualizaciones masivas
- add-ref.ps1: Automatiza la activación del modo desarrollo
- del-ref.ps1: Automatiza la desactivación del modo desarrollo
- add-dep.ps1: Configura nuevas dependencias con el patrón correcto
Para probar este proof of concept en tu máquina, necesitas crear un repositorio local de NuGet y generar los paquetes.
Primero, crea una carpeta que actuará como tu feed local de NuGet:
# Crear carpeta para el repositorio local
mkdir C:\LocalNuGetAñade el repositorio local a tus fuentes de NuGet:
# Añadir fuente local de NuGet
dotnet nuget add source C:\LocalNuGet --name "LocalDev"
# Verificar que se añadió correctamente
dotnet nuget list sourceDeberías ver algo como:
Registered Sources:
1. nuget.org [Enabled]
https://api.nuget.org/v3/index.json
2. LocalDev [Enabled]
C:\LocalNuGet
Para cada librería (Lib.One y Lib.Two), genera el paquete NuGet:
# Empaquetar Lib.One
cd Lib.One
dotnet pack -c Release -o C:\LocalNuGet
cd ..
# Empaquetar Lib.Two
cd Lib.Two
dotnet pack -c Release -o C:\LocalNuGet
cd ..Esto creará archivos .nupkg en C:\LocalNuGet:
Lib.One.1.0.0.nupkgLib.Two.1.0.0.nupkg
Asegúrate de que Directory.Packages.props tenga las versiones correctas:
<Project>
<ItemGroup>
<PackageVersion Include="Lib.One" Version="1.0.0" />
<PackageVersion Include="Lib.Two" Version="1.0.0" />
</ItemGroup>
</Project>cd MainApp
dotnet restore
dotnet build -c ReleaseLa build de Release usará los paquetes NuGet de C:\LocalNuGet.
Ahora activa el modo desarrollo para una librería:
# Activar desarrollo de Lib.One
.\add-ref.ps1 -csprojPath "Lib.One/Lib.One.csproj"
# Build en Debug (usará ProjectReference)
dotnet build -c DebugEn modo Release (NuGet):
dotnet build -c Release
# ✅ Usa paquetes de C:\LocalNuGetEn modo Debug sin junction:
.\del-ref.ps1 -projectName "Lib.One"
dotnet build -c Debug
# ✅ Usa paquetes de C:\LocalNuGetEn modo Debug con junction:
.\add-ref.ps1 -csprojPath "Lib.One/Lib.One.csproj"
dotnet build -c Debug
# ✅ Usa código fuente del proyectoSi modificas una librería y quieres actualizar el paquete NuGet:
cd Lib.One
# Incrementar versión en Lib.One.csproj (opcional)
# <Version>1.0.1</Version>
# Reempaquetar
dotnet pack -c Release -o C:\LocalNuGet
# Actualizar versión en MainApp/Directory.Packages.props
# <PackageVersion Include="Lib.One" Version="1.0.1" />
cd ../MainApp
dotnet restore --force-evaluate
dotnet build -c ReleaseSi necesitas empezar desde cero:
# Limpiar caché de NuGet
dotnet nuget locals all --clear
# Eliminar paquetes del repositorio local
Remove-Item C:\LocalNuGet\*.nupkg
# Regenerar paquetes
cd Lib.One
dotnet pack -c Release -o C:\LocalNuGet
cd ../Lib.Two
dotnet pack -c Release -o C:\LocalNuGetEn lugar de añadir la fuente globalmente, puedes crear un nuget.config en la raíz del workspace:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="LocalDev" value="C:\LocalNuGet" />
</packageSources>
</configuration>Esto hace la configuración portátil y específica del proyecto.
- .NET 5.0 o superior (para la aplicación principal)
- .NET Standard 2.0 (para las librerías)
- PowerShell 5.1 o superior
- Windows (por el uso de junctions; en Linux/Mac usar symlinks)
- Visual Studio 2019+ o VS Code con extensiones de C#
Este patrón es ideal para:
- Equipos con múltiples microservicios que comparten librerías comunes
- Proyectos empresariales con librerías internas publicadas en feed privado
- Desarrollo de frameworks donde algunos desarrolladores trabajan en el core
- Aplicaciones modulares con plugins o extensiones como paquetes NuGet
- Migración gradual de monorepo a múltiples repositorios
✅ Sin duplicación de código
✅ Configuración por desarrollador (no commitear junctions)
✅ Compatible con CI/CD existente
✅ No requiere herramientas adicionales
✅ Funciona con cualquier IDE
| Enfoque | Pros | Contras |
|---|---|---|
| Monorepo puro | Todo en un lugar | Crece indefinidamente, builds lentos |
| Git submodules | Integrado en Git | Complejo, propenso a errores |
| Workspace de Visual Studio | Nativo de VS | No funciona bien con CI/CD |
| Referencias condicionales ✅ | Balance perfecto | Requiere scripts de setup |
- MSBuild Conditions
- Central Package Management
- Directory Junctions
- Project References vs Package References
Este es un proof of concept. Siéntete libre de adaptarlo a tus necesidades específicas.
- Script para sincronizar todas las junctions automáticamente
- Integración con
dotnetCLI tool como extensión - Soporte multiplataforma (detectar OS y usar symlinks en Linux/Mac)
- Validación automática de versiones entre código local y NuGet
- Hook de Git para prevenir commits con junctions activas
Este proyecto es un ejemplo educativo. Úsalo libremente en tus proyectos.
Autor: Proof of Concept - Distributed Monorepo Pattern
Fecha: 2025
Tecnologías: .NET, MSBuild, PowerShell, NuGet