عنوان:

‫ساخت MSBuild SDK سفارشی در دات‌نت: خداحافظی با کدهای تکراری!


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۱۰/۰۶ ۰۹:۳۵
آدرس: www.dntips.ir
بسیاری از ما توسعه‌دهندگان دات‌نت، بارها صفت Sdk="Microsoft.NET.Sdk" را در ابتدای فایل‌های .csproj دیده‌ایم و بدون اینکه دقیقاً بدانیم پشت پرده چه می‌گذرد، آن را به عنوان یک "پیش‌فرض" پذیرفته‌ایم. اما اگر بدانید که ساختن یک SDK اختصاصی چقدر ساده‌است و چه قدرت عظیمی به شما می‌دهد، احتمالاً همین امروز دست‌به‌کار خواهید شد.
در این مقاله، نه تنها یاد می‌گیریم که یک MSBuild SDK چیست و چگونه ساخته می‌شود، بلکه می‌بینیم چطور می‌توانیم با استفاده از آن، استانداردهای تیمی را یکپارچه کنیم و از شر کپی-پیست کردن تنظیمات تکراری در پروژه‌ها خلاص شویم.

چرا به یک SDK سفارشی نیاز داریم؟
شاید بپرسید: «چرا از یک پکیج NuGet معمولی استفاده نکنیم؟»
پاسخ در پویایی و زمان اجرا نهفته است. استفاده از پکیج‌های NuGet (مانند Meziantou.DotNet.CodingStandard) برای اعمال استایل کدنویسی عالی است، اما محدودیت‌هایی دارد. یک پکیج NuGet معمولی نمی‌تواند به صورت هوشمند و بر اساس نوع پروژه، وابستگی‌های (Dependencies) جدیدی اضافه کند.
یک MSBuild SDK قدرت این را دارد که:
  • پیکربندی پویا: بر اساس اینکه پروژه شما Web است یا Test، پکیج‌های خاصی را تزریق کند (مثلاً xUnit برای تست‌ها و OpenTelemetry برای وب).
  • کنترل کامل: قبل از اینکه پروژه حتی لود شود، تنظیمات پیش‌فرض را اعمال کند.
  • تمیزی فایل پروژه: صدها خط کد پیکربندی (Boilerplate) را به یک خط کاهش دهد.

کالبدشکافی یک SDK
در ساده‌ترین تعریف، یک MSBuild SDK چیزی نیست جز یک پکیج NuGet که حاوی فایل‌های .props و .targets است. وقتی شما Sdk="MySdk" را در پروژه می‌نویسید، MSBuild کار زیر را انجام می‌دهد:
  • فایل Sdk.props را در ابتدای فایل پروژه ایمپورت می‌کند.
  • فایل Sdk.targets را در انتهای فایل پروژه ایمپورت می‌کند.
  • محتوای پروژه شما در بین این دو قرار می‌گیرد.

ساختار پوشه‌بندی
ساختار پروژه SDK شما باید دقیقاً به این شکل باشد:
MyAwesome.Sdk/
├── Sdk/
│   ├── Sdk.props      ← اول اجرا می‌شود (تنظیمات پیش‌فرض)
│   └── Sdk.targets    ← آخر اجرا می‌شود (منطق بیلد)
└── MyAwesome.Sdk.csproj
نکته مهم: نام پوشه Sdk باید دقیقاً به همین شکل (حروف، بزرگ و کوچک) باشد، وگرنه MSBuild آن را پیدا نمی‌کند.

قدم‌به‌قدم تا ساخت اولین SDK
۱. فایل Sdk.props (تنظیم پیش‌فرض‌ها)
این فایل قبل از هر چیزی اجرا می‌شود. اینجا جایی است که متغیرها را تعریف می‌کنید، اما به کاربر اجازه می‌دهید آن‌ها را بازنویسی (Override) کند.
<Project>
  <PropertyGroup>
    <TargetFramework Condition="'$(TargetFramework)' == ''">net8.0</TargetFramework>
    <ImplicitUsings Condition="'$(ImplicitUsings)' == ''">enable</ImplicitUsings>

    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
</Project>

۲. فایل Sdk.targets (منطق اصلی)
این فایل بعد از لود شدن پروژه اجرا می‌شود. اینجا می‌توانید فایل‌هایی را به بیلد اضافه کنید، Target‌های جدید بسازید یا پکیج‌های شرطی اضافه کنید.
<Project>
  <ItemGroup Condition="'$(EnableDefaultItems)' != 'false'">
    <None Include="**/*.config" />
  </ItemGroup>

  <Target Name="ValidateStuff" BeforeTargets="BeforeBuild">
    <Warning Condition="'$(MyRequiredProp)' == ''" 
             Text="لطفا مقدار MyRequiredProp را مشخص کنید." />
  </Target>
</Project>

۳. گسترش SDKهای استاندارد (Wrapping)
احتمالاً نمی‌خواهید همه چیز را از صفر بسازید. معمولاً ما می‌خواهیم روی شانه‌های Microsoft.NET.Sdk بایستیم و فقط آن را شخصی‌سازی کنیم. الگوی صحیح به این صورت است:
در Sdk.props: ابتدا SDK مایکروسافت را ایمپورت کنید، سپس تنظیمات خود را اعمال کنید.
<Project>
  <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
  <PropertyGroup>
    <MyCustomRule>true</MyCustomRule>
  </PropertyGroup>
</Project>
در Sdk.targets: ابتدا منطق خود را بنویسید، سپس Targets مایکروسافت را ایمپورت کنید (معمولاً).
<Project>
  <ItemGroup>
    <PackageReference Include="Meziantou.Analyzer" Version="2.0.0" />
  </ItemGroup>
  <Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
</Project>

۴. فایل پروژه (csproj) برای خودِ SDK
پیکربندی پروژه SDK ساده اما دارای نکات ظریف است:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <PackageType>MSBuildSdk</PackageType> 
    <PackageId>MyCompany.Net.Sdk</PackageId>
    <Version>1.0.0</Version>
    <IncludeBuildOutput>false</IncludeBuildOutput>
    <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
  </PropertyGroup>

  <ItemGroup>
    <None Include="Sdk\**" Pack="true" PackagePath="Sdk" />
  </ItemGroup>
</Project>

سناریوهای پیشرفته: هوشمندی در عمل
یکی از جذاب‌ترین کاربردهای SDK، تشخیص نوع پروژه و اعمال تنظیمات خاص است.
مثال: پیکربندی خودکار پروژه‌های تست
شما می‌توانید در Sdk.targets تشخیص دهید که آیا پروژه یک پروژه تست است یا خیر، و سپس پکیج‌های لازم را اضافه کنید:
<ItemGroup Condition="'$(IsTestProject)' == 'true'">
  <PackageReference Include="Microsoft.NET.Test.Sdk" Version="..." />
  <PackageReference Include="xunit" Version="..." />
  <PackageReference Include="coverlet.collector" Version="..." />
  
  <PackageReference Include="GitHubActionsTestLogger" Version="..." Condition="'$(GITHUB_ACTIONS)' == 'true'" />
</ItemGroup>
با این کار، توسعه‌دهنده فقط دستور dotnet new classlib را می‌زند و با ارجاع به SDK شما، تمام زیرساخت تست، گزارش‌دهی و کاورج (Coverage) به صورت خودکار فراهم می‌شود.

چالش تست کردن SDK (و راه حل آن)
تست کردن SDK به صورت لوکال می‌تواند عذاب‌آور باشد زیرا ویژوال استودیو به راحتی SDKهای لوکال را پیدا نمی‌کند.
دو راه حل دارید:
  • راه رسمی: پکیج را با dotnet pack بسازید، یک فولدر لوکال به عنوان NuGet Source تعریف کنید و پکیج را آنجا کپی کنید. (نکته: کش NuGet دیوانه‌کننده است! هر بار ورژن را تغییر دهید، یا کش را با dotnet nuget locals all --clear پاک کنید).
  • راه میانبر (توصیه شده برای توسعه): در پروژه تستی که می‌خواهید SDK را روی آن امتحان کنید، به‌جای ارجاع به پکیج، فایل‌ها را مستقیماً ایمپورت کنید:
در فایل Directory.Build.props پروژه تست:
<Project>
  <Import Project="..\..\MySdk\Sdk\Sdk.props" />
</Project>
این کار به شما اجازه می‌دهد بدون نیاز به Pack کردن مداوم، تغییرات را تست کنید.

نحوه مصرف SDK توسط تیم
برای استفاده از SDK در سازمان، بهترین روش استفاده از فایل global.json است. این کار مدیریت ورژن را متمرکز می‌کند.
۱. در ریشه مخزن (Repository) فایل global.json بسازید:
{
  "msbuild-sdks": {
    "MyCompany.Net.Sdk": "1.0.0"
  }
}
۲. در فایل پروژه (csproj):
<Project Sdk="MyCompany.Net.Sdk">
  </Project>
حالا اگر بخواهید ورژن SDK را ارتقا دهید، فقط کافیست یک فایل (global.json) را تغییر دهید تا تمام پروژه‌ها آپدیت شوند.

نکات پایانی و دام‌های رایج
  • Syntax شرط‌ها: همیشه در Condition از تک‌کوتیشن استفاده کنید: Condition="'$(Prop)' == ''". فراموش کردن این مورد خطای رایجی است.
  • ترتیب Import: اگر تنظیمی کار نمی‌کند، احتمالاً جای اشتباهی ایمپورت شده است. پراپرتی‌ها باید قبل از استفاده تعریف شوند.
  • کش NuGet: اگر تغییراتی دادید و اعمال نشد، ۹۹٪ مشکل از کش شدن پکیج قبلی توسط NuGet است.

نتیجه‌گیری
ساخت یک MSBuild SDK سفارشی شاید در ابتدا ترسناک به نظر برسد، اما ابزاری قدرتمند برای استانداردسازی، کاهش پیچیدگی و افزایش بهره‌وری تیم است. اگر در سازمانتان چندین پروژه دات‌نت دارید که تنظیمات مشابهی دارند (مثل Analyzerها، تنظیمات Docker، یا پکیج‌های Logging)، ساخت یک SDK سفارشی بهترین سرمایه‌گذاری برای آینده کدبیس شماست.

منابع استفاده شده