عنوان:

‫لینوکس برای توسعه‌دهندگان دات‌نت: از ویندوز تا کراس-پلتفرم؛ بخش دوم: کدنویسی کراس-پلتفرم در دات‌نت


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۴/۲۶ ۰۸:۱۰
آدرس: www.dntips.ir
در بخش قبل، با تفاوت‌های بنیادین بین ویندوز و لینوکس آشنا شدیم. اکنون زمان آن است که ببینیم فریمورک دات‌نت چگونه ابزارهایی را در اختیار ما قرار می‌دهد تا این تفاوت‌ها را مدیریت کرده و کدی تمیز، قابل نگهداری و مستقل از پلتفرم (Platform-Agnostic) بنویسیم.

۱. کار با مسیرها و فایل‌ها به صورت امن

همانطور که در بخش اول دیدیم، ساختن مسیرها با چسباندن رشته‌ها (string concatenation) یک ضد-الگو (anti-pattern) است. کلاس System.IO.Path مجموعه‌ای غنی از ابزارها را برای کار درست با مسیرها فراهم می‌کند.
  • Path.Combine (یادآوری): همیشه برای ترکیب کردن بخش‌های مختلف یک مسیر از این متد استفاده کنید.
  • Path.GetFullPath(string path): یک مسیر نسبی (relative) را به مسیر کامل و مطلق (absolute) تبدیل می‌کند. این متد بسیار مفید است زیرا نحوه‌ی مدیریت مسیرهای نسبی در پلتفرم‌های مختلف، متفاوت است.
  • Path.GetDirectoryName(string path): نام پوشه‌ی حاوی یک فایل یا پوشه را برمی‌گرداند.
  • Path.GetFileName(string path): نام فایل (با پسوند) را از یک مسیر کامل استخراج می‌کند.

مهم‌ترین نکته: پیدا کردن پوشه‌های سیستمی
شما هرگز نباید مسیر پوشه‌هایی مانند "My Documents" یا "ProgramData" را به صورت ثابت در کد بنویسید. برای این کار باید از Environment.GetFolderPath() استفاده کنید. این متد یک مقدار از شمارنده‌ی Environment.SpecialFolder را به عنوان ورودی می‌گیرد و مسیر صحیح را در هر سیستم‌عاملی برمی‌گرداند.

مثال عملی:
فرض کنید می‌خواهیم یک فایل لاگ را در پوشه‌ای مخصوص داده‌های برنامه (Application Data) ذخیره کنیم.
// این کد مسیر صحیح را در هر سه پلتفرم اصلی پیدا می‌کند
var appDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var myAppFolder = Path.Combine(appDataFolder, "MyCoolApp");
var logFilePath = Path.Combine(myAppFolder, "events.log");

// قبل از نوشتن، اطمینان حاصل می‌کنیم که پوشه وجود دارد
Directory.CreateDirectory(myAppFolder); 
File.AppendAllText(logFilePath, $"Log entry at {DateTime.Now}{Environment.NewLine}");

Console.WriteLine($"Log file created at: {logFilePath}");
خروجی احتمالی:
  • در ویندوز:C:\Users\YourUser\AppData\Roaming\MyCoolApp\events.log
  • در لینوکس:/home/youruser/.config/MyCoolApp/events.log
  • در macOS:/Users/youruser/.config/MyCoolApp/events.log

۲. متغیرهای محیطی (Environment Variables)

متغیرهای محیطی یک روش استاندارد و جهانی برای پیکربندی برنامه‌ها بدون تغییر در کد هستند. نحوه‌ی تعریف آن‌ها در سیستم‌عامل‌های مختلف متفاوت است، اما نحوه‌ی خواندنشان در دات‌نت یکسان است.
  • تعریف در ویندوز (Command Prompt):set MYAPP_CONNECTION_STRING="some_value"
  • تعریف در ویندوز (PowerShell):$env:MYAPP_CONNECTION_STRING="some_value"
  • تعریف در لینوکس/macOS:export MYAPP_CONNECTION_STRING="some_value"

نحوه‌ی خواندن در #C:
برای خواندن این مقادیر، از متد استاتیک Environment.GetEnvironmentVariable استفاده می‌کنیم.
string connStr = Environment.GetEnvironmentVariable("MYAPP_CONNECTION_STRING");

if (string.IsNullOrEmpty(connStr))
{
    Console.WriteLine("Connection string is not set!");
}
else
{
    Console.WriteLine("Using connection string from environment.");
    // Connect to database using connStr...
}

یکپارچگی با سیستم Configuration مدرن دات‌نت:
در برنامه‌های ASP.NET Core یا Worker Service، سیستم IConfiguration به طور پیش‌فرض متغیرهای محیطی را می‌خواند و آن‌ها را بر مقادیر موجود در appsettings.jsonاولویت می‌دهد. این ویژگی برای محیط‌های داکر و پروداکشن فوق‌العاده کاربردی است.
برای مثال، اگر در appsettings.json داشته باشید:
"Logging": { "LogLevel": { "Default": "Information" } }
می‌توانید در سرور لینوکس با تنظیم یک متغیر محیطی، سطح لاگ را بدون تغییر فایل json تغییر دهید:
export Logging__LogLevel__Default="Warning"
(توجه: در متغیرهای محیطی، از دو خط زیر (__) برای جدا کردن بخش‌های تو در توی JSON استفاده می‌شود.)

۳. اجرای پروسه‌های خارجی (External Processes)

گاهی نیاز است که از داخل کد دات‌نت، یک دستور یا اسکریپت سیستمی را اجرا کنید (مثلاً برای کار با ابزارهایی مانند git یا ffmpeg). برای این کار از کلاس System.Diagnostics.Process استفاده می‌شود.
تفاوت کلیدی:
  • در ویندوز، ممکن است cmd.exe، powershell.exe یا یک فایل .bat را اجرا کنید.
  • در لینوکس، معمولاً bash، zsh یا یک اسکریپت .sh را اجرا می‌کنید.

مثال: اجرای یک دستور و خواندن خروجی آن
using System.Diagnostics;

var processStartInfo = new ProcessStartInfo
{
    FileName = "ls", // در ویندوز می‌توانید "dir" را جایگزین کنید
    Arguments = "-l /home/youruser", // در ویندوز مثلاً "C:\\Users"
    RedirectStandardOutput = true, // برای خواندن خروجی
    UseShellExecute = false, // مهم: برای ریدایرکت کردن خروجی باید false باشد
    CreateNoWindow = true, // پنجره جدیدی باز نشود
};

using var process = new Process { StartInfo = processStartInfo };
process.Start();

string output = process.StandardOutput.ReadToEnd(); // خواندن تمام خروجی
process.WaitForExit();

Console.WriteLine(output);
نکات مهم:
  • مجوز اجرا در لینوکس: اگر می‌خواهید یک اسکریپت (.sh) را اجرا کنید، ابتدا باید به آن مجوز اجرا بدهید: chmod +x your_script.sh.
  • امنیت: هرگز ورودی کاربر را مستقیماً در Arguments قرار ندهید تا از حملات Command Injection جلوگیری کنید.
  • UseShellExecute: تنظیم این گزینه روی false به شما کنترل بیشتری بر روی پروسه (مانند خواندن خروجی) می‌دهد و روش امن‌تری محسوب می‌شود.

۴. کتابخانه‌های وابسته به پلتفرم (Platform-Specific Libraries)

چه اتفاقی می‌افتد اگر بخشی از کد شما مجبور باشد از یک API مخصوص ویندوز (مثلاً کار با رجیستری) استفاده کند؟ آیا کل برنامه دیگر کراس-پلتفرم نیست؟
خیر! دات‌نت به شما اجازه می‌دهد بخش‌هایی از کد را به صورت شرطی فقط بر روی یک پلتفرم خاص اجرا کنید.
راه‌حل: استفاده از System.Runtime.InteropServices.RuntimeInformation
این کلاس به شما امکان می‌دهد سیستم‌عامل میزبان را در زمان اجرا تشخیص دهید.
using System.Runtime.InteropServices;

public void PerformPlatformSpecificAction()
{
    if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
    {
        Console.WriteLine("Running on Windows. Accessing registry...");
        // کد مخصوص ویندوز در اینجا قرار می‌گیرد
        // مثلاً: Microsoft.Win32.Registry.CurrentUser.OpenSubKey(...)
    }
    else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
    {
        Console.WriteLine("Running on Linux. Reading from /etc/os-release...");
        // کد مخصوص لینوکس در اینجا قرار می‌گیرد
        // مثلاً: File.ReadAllText("/etc/os-release")
    }
    else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
    {
        Console.WriteLine("Running on macOS.");
        // کد مخصوص مک در اینجا قرار می‌گیرد
    }
}

الگوی پیشنهادی (Best Practice):
به جای استفاده از بلوک‌های if/else در سراسر کد، بهتر است منطق وابسته به پلتفرم را پشت یک اینترفیس (Interface) مخفی کنید و با استفاده از تزریق وابستگی (Dependency Injection)، پیاده‌سازی صحیح را در زمان اجرا به برنامه تزریق کنید.
مثال معماری:
  • اینترفیس:public interface INotificationService { void ShowNotification(string message); }
  • پیاده‌سازی ویندوزی:public class WindowsNotificationService : INotificationService { ... } (مثلاً با استفاده از Toast Notifications)
  • پیاده‌سازی لینوکسی:public class LinuxNotificationService : INotificationService { ... } (مثلاً با استفاده از notify-send در خط فرمان)
  • ثبت در کانتینر DI:
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
    services.AddSingleton<INotificationService, WindowsNotificationService>();
}
else
{
    services.AddSingleton<INotificationService, LinuxNotificationService>(); // fallback
}
این رویکرد باعث می‌شود که ۹۹٪ کد شما کاملاً تمیز و مستقل از پلتفرم باقی بماند.