مقدمه

در درس قبل، دیدیم که فضاهای نام (Namespaces) یک راهکار منطقی برای سازماندهی کد هستند. اکنون به بررسی واحد فیزیکی استقرار و نسخه‌بندی کد در .NET می‌پردازیم: اسمبلی (Assembly). هر زمان که شما یک پروژه‌ی C# را کامپایل می‌کنید، خروجی آن یک یا چند اسمبلی خواهد بود. اسمبلی‌ها بلوک‌های سازنده‌ی اصلی برنامه‌های .NET هستند. آن‌ها حاوی کد کامپایل‌شده به زبان میانی (CIL)، فراداده (metadata) مربوط به آن کد، و منابعی مانند تصاویر و رشته‌های متنی می‌باشند. درک ساختار و نقش اسمبلی‌ها برای مدیریت وابستگی‌ها، نسخه‌بندی و استقرار برنامه‌ها ضروری است.

اسمبلی چیست؟

یک اسمبلی، واحد اصلی استقرار، استفاده مجدد، کنترل نسخه و امنیت در .NET است. اسمبلی‌ها می‌توانند به دو شکل وجود داشته باشند:

  • فایل‌های اجرایی (.exe): این نوع اسمبلی، نقطه‌ی شروع یک برنامه را نشان می‌دهد و می‌تواند به طور مستقیم توسط سیستم‌عامل اجرا شود.
  • کتابخانه‌ها (.dll): این نوع اسمبلی به تنهایی قابل اجرا نیست، اما حاوی انواع داده و کدهای قابل استفاده مجدد است که می‌تواند توسط دیگر اسمبلی‌ها (چه .exe و چه .dll های دیگر) مورد ارجاع و استفاده قرار گیرد.

ساختار داخلی یک اسمبلی

هر اسمبلی از چندین بخش کلیدی تشکیل شده است. مهم‌ترین بخش آن مانیفست اسمبلی (Assembly Manifest) است.

مانیفست اسمبلی

مانیفست، فراداده‌ای در مورد خود اسمبلی است. این بخش حاوی اطلاعات حیاتی زیر است:

  • هویت اسمبلی: شامل یک نام ساده (مانند MyLibrary)، یک شماره نسخه (مثلاً `1.0.0.0`)، یک فرهنگ (برای اسمبلی‌های چندزبانه) و یک کلید عمومی (برای اسمبلی‌های با نام قوی).
  • لیست تمام انواع داده: فهرستی از تمام انواع داده‌ی عمومی (public) که در این اسمبلی تعریف شده و برای کدهای خارجی قابل استفاده هستند.
  • لیست وابستگی‌ها: فهرستی از تمام اسمبلی‌های دیگری که این اسمبلی برای کار کردن به آن‌ها نیاز دارد.
  • مجموعه مجوزهای امنیتی: اطلاعات امنیتی مورد نیاز برای اجرای صحیح اسمبلی.

این مانیفست باعث می‌شود که اسمبلی‌ها کاملاً خودتوصیف (self-describing) باشند. برخلاف مدل‌های قدیمی‌تر مانند COM، یک اسمبلی .NET برای استفاده، نیازی به ثبت‌نام در رجیستری ویندوز ندارد. تمام اطلاعات مورد نیاز برای بارگذاری و استفاده از آن، در خود اسمبلی گنجانده شده است.

ماژول‌ها و کد CIL

علاوه بر مانیفست، یک اسمبلی حاوی یک یا چند ماژول است. هر ماژول یک فایل کامپایل‌شده است که حاوی کد CIL (زبان میانی مشترک) و فراداده‌ی مربوط به انواع تعریف‌شده در آن ماژول است. در ۹۹ درصد موارد، هر اسمبلی تنها از یک ماژول تشکیل شده است (یعنی همان فایل .dll یا .exe).

اسمبلی‌های خصوصی در مقابل اشتراکی

اسمبلی‌ها در .NET به دو دسته‌ی اصلی از نظر استقرار تقسیم می‌شوند:

اسمبلی‌های خصوصی (Private Assemblies)

این روش استاندارد و پیش‌فرض در .NET مدرن است. یک اسمبلی خصوصی، اسمبلی‌ای است که مستقیماً در پوشه‌ی پایه‌ی برنامه (یا یکی از زیرپوشه‌های آن) قرار می‌گیرد. این اسمبلی فقط توسط همان برنامه استفاده می‌شود.

  • مزایا: این مدل بسیار ساده است و از مشکل معروف به "جهنم DLL" (DLL Hell) جلوگیری می‌کند. هر برنامه کپی خصوصی خود را از وابستگی‌هایش دارد، بنابراین تغییر یا به‌روزرسانی یک کتابخانه برای یک برنامه، تأثیری بر برنامه‌های دیگر نخواهد داشت.
  • معایب: اگر چندین برنامه از یک اسمبلی مشترک استفاده کنند، چندین کپی از آن اسمبلی بر روی دیسک ذخیره خواهد شد.

اسمبلی‌های اشتراکی (Shared Assemblies)

یک اسمبلی اشتراکی، اسمبلی‌ای است که در یک مکان مرکزی و شناخته‌شده برای تمام برنامه‌ها قرار می‌گیرد و می‌تواند توسط چندین برنامه به صورت همزمان استفاده شود. در .NET Framework، این مکان مرکزی به نام Global Assembly Cache (GAC) شناخته می‌شد.

  • الزامات: برای قرار گرفتن در GAC، یک اسمبلی باید دارای یک نام قوی (Strong Name) باشد. نام قوی با استفاده از رمزنگاری کلید عمومی، یک امضای دیجیتال به اسمبلی اضافه می‌کند که هویت و یکپارچگی آن را تضمین می‌کند.
  • وضعیت در .NET مدرن: مفهوم GAC به شکلی که در .NET Framework وجود داشت، در .NET Core و نسخه‌های جدیدتر وجود ندارد. رویکرد مدرن، استفاده از پکیج منیجر NuGet است. NuGet پکیج‌ها را در یک کش سراسری در کامپیوتر کاربر ذخیره می‌کند، اما هر پروژه به نسخه‌ی مشخصی از آن پکیج که نیاز دارد ارجاع می‌دهد و این وابستگی‌ها در کنار برنامه مستقر می‌شوند. این مدل، مزایای هر دو روش خصوصی و اشتراکی را با هم ترکیب می‌کند.