Hay funcionalidades que aparecen en cada conferencia, tienes cientos de artículos y twitter habla de ellas todos los días.
Pero luego tenemos otras que no hacen tanto ruido y aun así nos mejoran la vida una barbaridad, hoy vamos a hablar de una de ellas el uso de Directory.Packages.props, Directory.Packages.props, y targets.
No son funcionalidades recientes, bueno, de los últimos años, y no son cool dentro del nuevo mundo de la IA, pero nos facilitan muchísimo la vida en el día a día y hoy las vamos a explorar.
Tabla de contenidos
1 - En .NET todo son múltiples proyectos
Cuando creamos aplicaciones en .NET por pequeña que sea siempre nos gusta separar todo en proyectos que separan las diferentes capas, y esto está bien, pero esos proyectos siempre acaban referenciando librerías, ya sean nuestras, de la propia microsoft o de terceros.
Si ponemos como ejemplo mi cualquiera de nuestros repositorios, cada uno de los proyectos tiene un csproj y estos proyectos referencian librerías, y por mucho que los mantengas limpios e intentes tenerlo bien estructurado, mínimo tienes que mantener las versiones de los paquetes.
Esto significa en muchos casos que acabas con una colección de paquetes, referenciados en múltiples capas de tu aplicación donde cada proyecto utiliza una versión.
Quizá en la capa de datos utilizas EntityFramework version 10.0.1, en otra capa estas utilizando la versión 10.0.0 y en la de los test puede ser que incluso la versión 9…
Ahora, los IDEs han intentado mejorar la experiencia y tenemos opciones de actualizar todas las referencias a la vez.

Pero sabemos que en la práctica, en proyectos grandes, esto no suele pasar.
2 - Centralizar los paquetes con Directory.Packages.Props
El problema que acabamos de explicar pasa en todas las empresas, y de hecho, en todos los lenguajes, no es algo tuyo ni mio, así que Microsoft se puso manos a la obra e invirtió tiempo en intentar solucionar este problema. Y en mi opinión lo ha solucionado.
Ahora, tenemos la posibilidad de definir un único fichero con los paquetes y las versiones que vamos a implementar. El fichero Directory.Packages.Props, y la funcionalidad oficialmente se llama Central Package Management y para activarla simplemente crea un fichero con ese nombre en la raíz del repo, junto al fichero de la solución (.sln)
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Dapper" Version="2.0.35" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="3.2.1" />
<PackageVersion Include="MySql.Data" Version="8.0.20" />
<PackageVersion Include="Netmentor.ROP" Version="1.0.13" />
</ItemGroup>
</Project>
El contenido del fichero en sí no tiene ningún misterio, tenemos un PackageVersion por cada librería o paquete que hemos movido y así como su versión, dentro del propio fichero podemos agrupar a los paquetes a través del tag ItemGroup.
Y ahora simplemente nos queda referenciar en los diferentes proyectos de nuestra aplicacion.
Ojo, no vamos a estar referenciando el fichero directamente, sino que cuando necesitemos utilizar cierta librería, vamos a indicarlo, pero únicamente para la libreria que queremos
Este es el ejemplo del csproj de la capa de datos, donde únicamente cargamos las librerias que necesitamos:
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Abstractions" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" />
<PackageReference Include="Netmentor.ROP" />
</ItemGroup>
Como vemos, no estamos indicando la versión, ya que la versión está siendo definida en el otro proyecto.
Lo que sucede por detrás es que NuGet encuentra el primer Directory.Packages.Props e importa la información en el proyecto.
Al final lo que significa es que hemos cambiado de esto:
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="3.1.8" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Netmentor.ROP" Version="1.0.13" />
</ItemGroup>
A esto otro:
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Abstractions" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" />
<PackageReference Include="Netmentor.ROP" />
</ItemGroup>
Donde a pesar de ser un cambio pequeño, cuando tienes una aplicación grande, con 40 proyectos dentro que apuntan a 10 versiones diferentes, ya deja de ser un cambio pequeño para ser uno que es bastante grande.
Antes de terminar con este punto, para actualizar una versión, simplemente tenemos que modificar la versión en el fichero, pero si por algún motivo, necesitas sobreescribir la versión en uno de los proyectos específicos, también lo puedes hacer con versionOverride.
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Abstractions" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" />
<PackageReference Include="Netmentor.ROP" versionOverride="2.2.0"/> <---- HERE
</ItemGroup>
De todas formas, salvo algún caso específico donde por ejemplo estás migrando o similar, no recomiendo sobre escribir, ya que pierde la gracia de usar el Packages.Props.
3 - Compartir la configuración de tus proyectos de .NET
El tema de los paquetes no es lo único que repetimos en todos los proyectos. También lo hacemos con cierta configuración como habilitar nullable, marcar los warnings como errores, para eliminar toda esta repeticion tenemos el fichero Directory.Build.props, que funciona de la misam manera, un fichero en la raiz del repositorio con la información común a tener:
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Por detrás lo que sucede es que MsBuild importa este fichero automáticamente para los proyectos de las subcarpetas, lo que implica que podemos eliminar muchísimo contenido del csproj de un proyecto, quedando únicamente con los paquetes a utilizar:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>WebPersonal.BackEnd.Service</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Abstractions" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" />
<PackageReference Include="Netmentor.ROP" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Shared\Shared.DTO\Shared.DTO.csproj" />
<ProjectReference Include="..\WebPersonal.BackEnd.Data\WebPersonal.BackEnd.Data.csproj" />
<ProjectReference Include="..\WebPersonal.BackEnd.Model\WebPersonal.BackEnd.Model.csproj" />
<ProjectReference Include="..\WebPersonal.BackEnd.Translations\WebPersonal.BackEnd.Translations.csproj" />
</ItemGroup>
</Project>
Como antes, si quieres sobreescribir puedes hacerlo tanto en el propio csproj o incluso creando en una subcarpeta otro directory.Build.Props con configuración diferente.
3.1 - Acciones comunes en proyectos de .NET
Cuando trabajas con MsBuild en .NET tenemos un bloque llamado Target, este bloque ejecuta tareas o scripts que permiten personalizar el proceso de compilación ejecución, etc.
Estas configuraciones también se pueden poner en un fichero Directory.Build.Targets el cual será común para todos los proyectos:
<Project>
<Target Name="PreventDebugPackage" BeforeTargets="Pack" Condition="'$(Configuration)' == 'Debug'">
<Error Text="Packages must be generated using release" />
</Target>
</Project>
Esto quizá sea menos común ya que suelen ser acciones muy específicas en casos concretos, así que no siempre tiene sentido añadirlos todos a todos los proyectos de una solución.
4 - Conclusión
Es obvio que lo que se busca es simplificar, la IA está simplificando el desarrollo, y todo lo que salga para facilitarle la vida y reducir el coste de tokens, bienvenido sea.
Especialmente tanto Directory.Pacakges.Props cómo Directory.Build.Props pienso que deberían ser utilizados en todas las soluciones, porque todos tenemos un montón de propiedades y librerías compartidas, al principio puede parecerte no gran cosa, pero a cuando te acostumbras, no hay forma de volver atrás.