Building File-Based Apps with C# 14 and .NET 10
الـ File-based apps دي عبارة عن برامج بتكون موجودة بالكامل جوة ملف واحد بس بامتداد *.cs، وبتقدر تعملها build و run من غير ما تحتاج لملف مشروع (*.csproj). الـ apps دي مثالية جداً لو بتتعلم C# لأنها بتقلل التعقيد: البرنامج كله محفوظ في ملف واحد. كمان الـ File-based apps دي مفيدة جداً لو عايز تبني (command line utilities). وعلى أنظمة Unix، تقدر تشغل الـ apps دي باستخدام توجيهات الـ Shebang (#!).
Create a File-Based App
افتح Visual Studio Code واعمل ملف جديد وسميه AsciiArt.cs. واكتب فيه النص ده:
Console.WriteLine("Hello, world!");احفظ الملف. بعدين، افتح الـ terminal المدمج في Visual Studio Code واكتب:
dotnet AsciiArt.cs
# Output in Console
Hello, world!أول مرة بتشغل فيها البرنامج ده، الـ dotnet host بيعمل build للملف (executable) من الـ source file بتاعك، وبيحفظ ملفات الـ build في temporary folder، وبعد كده بيشغل الـ executable اللي اتعمل. تقدر تتأكد من التجربة دي لو كتبت dotnet AsciiArt.cs تاني. المرة دي، الـ dotnet host هيعرف إن الـ executable ده (current) وهيشغله من غير ما يعمل له build من تاني. ومش هتشوف أي output للـ build.
الخطوات اللي فاتت دي بتوضح إن الـ file-based apps مش عبارة عن script files. دي ملفات C# source عادية جداً الـ dotnet host بيعملها build باستخدام ملف مشروع (project file) بيتكريت تلقائياً في temporary folder. واحد من السطور اللي بتظهر في المخرجات لما بتعمل build للبرنامج المفروض يكون شبه ده (على Windows):
AsciiArt succeeded (7.3s) → AppData\Local\Temp\dotnet\runfile\AsciiArt-85c58ae0cd68371711f06f297fa0d7891d0de82afde04d8c64d5f910ddc04ddc\bin\debug\AsciiArt.dllوعلى أنظمة Unix، الـ (output folder) بيكون حاجة شبه كده:
AsciiArt succeeded (7.3s) → Library/Application Support/dotnet/runfile/AsciiArt-85c58ae0cd68371711f06f297fa0d7891d0de82afde04d8c64d5f910ddc04ddc/bin/debug/AsciiArt.dllالـ Outputs دي بتقولك فين مكان الـ temporary files والـ build outputs. طول التوتوريال ده، في أي وقت هتعدل فيه الـ source file، الـ dotnet host هيعمل تحديث للـ executable قبل ما يشغله.
sequenceDiagram participant Developer participant DotnetHost as dotnet CLI participant TempFolder as Temporary Folder Developer->>DotnetHost: dotnet AsciiArt.cs alt First run or file changed DotnetHost->>TempFolder: Generate .csproj & build TempFolder-->>DotnetHost: Output Executable (AsciiArt.dll) else File unchanged DotnetHost->>TempFolder: Check if current TempFolder-->>DotnetHost: Return existing Executable end DotnetHost->>Developer: Run Application
الـ File-based apps دي برامج C# عادية جداً. التقييد الوحيد هو إنك لازم تكتبهم في source file واحد. تقدر تستخدم top-level statements أو طريقة الـ Main method كـ (entry point). وتقدر تعرف أي نوع (types): (classes)، (interfaces)، أو structs.
وتقدر تعمل اللوجيك بتاعك جوة الـ app ده بنفس الطريقة اللي بتعملها في أي برنامج C#. حتى إنك تقدر تعمل اكتر من namespace عشان تنظم الكود بتاعك. لو حسيت إن الـ app كبر أوي على إنه يكون في ملف واحد، تقدر تحوله لبرنامج (project-based) وتقسم الكود على كذا ملف.
الـ File-based apps دي أداة ممتازة جداً للـ prototyping. تقدر تبدأ تجرب بأقل مجهود عشان تثبت فكرة معينة أو تتعلم او حتى تحل مشكلة.
Unix Shebang (#!) Support
ملاحظة: دعم الـ
#!شغال على أنظمة Unix بس. مفيش طريقة مشابه ليه في Windows عشان يشغل برنامج C# بشكل مباشر. على Windows، لازم تستخدمdotnetفي الـ command line.
على Unix، تقدر تشغل الـ file-based apps بشكل مباشر عن طريق إنك تكتب اسم الـ source file بس. بدل ما تستخدم dotnet AsciiArt.cs، اكتب اسم الملف على طول في الـ command line. بس هتحتاج تعمل تغييرين:
- إدي صلاحيات (execute permissions) للـ source file:
chmod +x AsciiArt.cs- ضيف الـ shebang (
#!) كأول سطر في ملفAsciiArt.cs:
#!/usr/local/share/dotnet/dotnetمكان الـ dotnet ممكن يختلف من إصدار Unix للتاني. استخدم which dotnet عشان تحدد مكان الـ dotnet host في الجهاز بتاعك.
كبديل، تقدر تستخدم #!/usr/bin/env dotnet عشان تخلي النظام يجيب مسار الـ dotnet من enviroment PATH بشكل تلقائي:
#!/usr/bin/env dotnetبعد ما تعمل التغييرين دول، تقدر تشغل البرنامج من الـ command line بشكل مباشر:
./AsciiArt.csولو حابب، تقدر تشيل (extension) خالص عشان تكتب ./AsciiArt بس. وتقدر تضيف الـ #! للـ source file بتاعك حتى لو بتستخدم Windows. الـ command line بتاع Windows مش بيدعم الـ #!، لكن الـ C# compiler بيسمح تستخدمها ده في الـ file-based apps على كل الأنظمة.
Read Command Line Arguments
دلوقتي، خلينا نطبع كل الـ arguments اللي بتيجي من الـ command line.
هنغير الكود بتاع AsciiArt.cs بالكود ده:
if (args.Length > 0)
{
string message = string.Join(' ', args);
Console.WriteLine(message);
}dotnet AsciiArt.cs -- This is the command line.الـ -- option بيوضح إن كل الـ arguments اللي هتيجي بعد كده لازم تتبعت لبرنامج AsciiArt. الـ arguments اللي هي This is the command line. بتتبعت على هيئة (array of strings)، بحيث إن كل كلمة بتعتبر string لوحدها: This, is, the, command, و line..
النسخة دي بتشرح مفاهيم جديدة زي:
- الـ
argsعبارة عن (predefined variable) بيبعت الـ command line arguments للبرنامج. الـargsده عبارة عنstring[]. لو كان طول الـargsبيساوي 0، ده معناه إن مفيش arguments اتبعتت. غير كده، كل كلمة في قايمة الـ arguments بتتخزن في مكانها جوة Array. - الـ
string.Joinmethod بتدمج كذا string مع بعض في string واحد، وبيفصل بينهم بـ separator إنت بتحدده. في الحالة دي، الـ separator هو مسافة واحدة.
Handle Standard Input
الكود اللي فات بيتعامل مع الـ command line arguments بشكل صح. دلوقتي، ضيف كود عشان يتعامل مع Inputs من الـ User من الـ standard input (stdin) بدل الـ command line arguments.
ضيف جملة الـ else دي لجملة الـ if اللي ضفتها في الكود اللي فات:
else
{
while (Console.ReadLine() is string line && line.Length > 0)
{
Console.WriteLine(line);
}
}الكود اللي فات ده بيقرا Inputs الـ console لحد ما يلاقي سطر فاضي (blank line) أو يقرا null. (الـ Console.ReadLine method بترجع null لو الـ input stream اتقفل عن طريق الضغط على ctrl+C).
جرب قراية الـ standard input عن طريق إنك تعمل ملف text جديد في نفس الفولدر. سمي الملف input.txt وضيف فيه السطور دي:
Hello from ...
dotnet!
You can create
file-based apps
in .NET 10 and
C# 14
Have fun writing
useful utilitiesخلي السطور قصيرة عشان تتنسق صح لما تضيف الميزة بتاعت الـ ASCII art.
شغل البرنامج تاني.
لو بتستخدم Bash:
cat input.txt | dotnet AsciiArt.csأو لو بتستخدم PowerShell:
Get-Content input.txt | dotnet AsciiArt.csدلوقتي برنامجك يقدر يقبل يا إما command line arguments أو standard input.
Write ASCII Art Output
الخطوة اللي جاية، هنضيف package بتدعم الـ ASCII art اسمها Colorful.Console. عشان تضيف package لـ file-based app، استخدم الـ #:package.
ضيف الأمر ده بعد الـ #! في ملف AsciiArt.cs بتاعك:
#:package Colorful.Console@1.2.15غير السطور اللي بتنادي على Console.WriteLine عشان تستخدم Colorful.Console.WriteAscii method بدل منها:
Colorful.Console.WriteAscii(line);شغل البرنامج. هتلاقي المخرجات ظهرت على شكل ASCII art بدل ما كانت بتطبع النص العادي.
Process Command Options
الخطوة الجاية، هنضيف parsing للـ command line. النسخة الحالية بتكتب كل كلمة في سطر لوحدها. الـ command line arguments اللي هتضيفها هتدعم ميزتين:
- إنك تحط كذا كلمة بين علامات تنصيص عشان يتكتبوا في سطر واحد:
AsciiArt.cs "This is line one" "This is another line" "This is the last line"- إضافة
--delayoption عشان تعمل توقف (pause) بين كل سطر والتاني:
AsciiArt.cs --delay 1000والمستخدمين يقدروا يستخدموا الحاجتين مع بعض.
معظم تطبيقات الـ command line بتحتاج تعمل parsing للـ arguments عشان تتعامل مع الـ options، و (commands)، ومدخلات المستخدم بكفاءة. مكتبة System.CommandLine بتقدم طرق شاملة للتعامل مع الـ commands، الـ subcommands، الـ options، والـ arguments. بكده تقدر تركز على برنامجك بيعمل إيه بدل ما تضيع وقتك في تفاصيل الـ parsing.
مكتبة System.CommandLine بتقدم كذا ميزة أساسية:
- تعملك الـ help text والـ validation بشكل تلقائي.
- دعم لقواعد الـ command-line بتاعت POSIX و Windows.
- قدرات مبنية جواها للـ tab completion.
- سلوك parsing ثابت وموحد بين التطبيقات.
ضيف مكتبة System.CommandLine. ضيف ده بعد الـ package اللي موجود:
#:package System.CommandLine@2.0.0ضيف جمل الـ using المطلوبة في أول الملف (بعد توجيهات الـ #! والـ #:package):
using System.CommandLine;
using System.CommandLine.Parsing;عرف الـ delay option والـ messages argument. ضيف الكود ده عشان تكريت الـ CommandLine.Option والـ CommandLine.Argument objects اللي هتمثلهم:
Option<int> delayOption = new("--delay")
{
Description = "Delay between lines, specified as milliseconds.",
DefaultValueFactory = parseResult => 100
};
Argument<string[]> messagesArgument = new("Messages")
{
Description = "Text to render."
};في تطبيقات الـ command-line، الـ options غالباً بتبدأ بـ -- (double dash) وممكن تقبل arguments. الـ --delay option بيقبل integer argument بيحدد التأخير بالمللي ثانية. الـ messagesArgument بيحدد إزاي أي كلمات (tokens) تانية بعد الـ options هيتعملها parsing كنص. كل token بيبقى string منفصل في المصفوفة، بس لو النص اتحط بين علامات تنصيص هيتعامل كـ token واحد. مثلاً "This is one message" هتبقى token واحد، بينما This is four tokens هتبقى أربع كلمات منفصلين.
الكود اللي فات بيحدد نوع الـ argument للـ --delay option، وبيحدد إن الـ arguments دي عبارة عن مصفوفة من قيم الـ string. التطبيق ده فيه أمر واحد بس، فهنستخدم الـ root command.
كريت root command واعمل له إعدادات بالـ option والـ argument اللي عملناهم. ضيف الـ argument والـ option للـ root command:
RootCommand rootCommand = new("Ascii Art file-based app sample");
rootCommand.Options.Add(delayOption);
rootCommand.Arguments.Add(messagesArgument);ضيف الكود اللي هيعمل parsing للـ command line arguments ويتعامل مع أي أخطاء. الكود ده بيعمل validate للـ arguments وبيخزن الـ arguments اللي اتعملها parsing في System.CommandLine.ParseResult object:
ParseResult result = rootCommand.Parse(args);
foreach (ParseError parseError in result.Errors)
{
Console.Error.WriteLine(parseError.Message);
}
if (result.Errors.Count > 0)
{
return 1;
}الكود اللي فات بيعمل validation لكل الـ command line arguments. لو الـ validation فشل، التطبيق بيكتب Errors في الكونسول وبيخرج.
Use Parsed Command Line Results
دلوقتي، هنخلص التطبيق عشان نستخدم الـ options اللي اتعملها parsing ونكتب Outputs. أول حاجة، هنعرف record عشان نحفظ فيه الـ options دي. الـ file-based apps ممكن يكون فيها تصريحات لأنواع (type declarations) زي الـ records والـ classes. بس لازم يكونوا مكتوبين بعد كل الـ top-level statements والـ local functions.
ضيف الـ record ده عشان تحفظ فيه الـ messages وقيمة الـ delay option:
public record AsciiMessageOptions(string[] Messages, int Delay);ضيف الـ local function دي قبل الـ record. الـ method دي بتتعامل مع الـ command line arguments والـ standard input مع بعض، وبترجع نسخة جديدة من الـ record:
async Task<AsciiMessageOptions> ProcessParseResults(ParseResult result)
{
int delay = result.GetValue(delayOption);
List<string> messages =[.. result.GetValue(messagesArgument) ?? Array.Empty<string>()];
if (messages.Count == 0)
{
while (Console.ReadLine() is string line && line.Length > 0)
{
Colorful.Console.WriteAscii(line);
await Task.Delay(delay);
}
}
return new([.. messages], delay);
}اعمل local function تانية عشان تكتب الـ ASCII art بالتأخير (delay) المطلوب. الـ function دي بتكتب كل رسالة موجودة في الـ record مع التأخير المحدد بين كل رسالة والتانية:
async Task WriteAsciiArt(AsciiMessageOptions options)
{
foreach (string message in options.Messages)
{
Colorful.Console.WriteAscii(message);
await Task.Delay(options.Delay);
}
}بدل جملة الـ if اللي كتبتها قبل كده بالكود ده اللي بيعمل processing للـ command line arguments وبيكتب المخرجات:
var parsedArgs = await ProcessParseResults(result);
await WriteAsciiArt(parsedArgs);
return 0;كده إنت عملت نوع record بيدي هيكلة (structure) للـ command line options والـ arguments اللي اتعملها parsing. واستخدمت local functions جديدة بتعمل نسخة من الـ record وبتستخدمه عشان تكتب الـ ASCII art.
Test the Final Application
جرب التطبيق عن طريق تشغيل كذا أمر مختلف. لو واجهت أي مشكلة، ده الكود النهائي عشان تقارنه باللي إنت عملته:
#!/usr/bin/env dotnet
#:package Colorful.Console@1.2.15
#:package System.CommandLine@2.0.0
using System.CommandLine;
using System.CommandLine.Parsing;
Option<int> delayOption = new("--delay")
{
Description = "Delay between lines, specified as milliseconds.",
DefaultValueFactory = parseResult => 100
};
Argument<string[]> messagesArgument = new("Messages")
{
Description = "Text to render."
};
RootCommand rootCommand = new("Ascii Art file-based app sample");
rootCommand.Options.Add(delayOption);
rootCommand.Arguments.Add(messagesArgument);
ParseResult result = rootCommand.Parse(args);
foreach (ParseError parseError in result.Errors)
{
Console.Error.WriteLine(parseError.Message);
}
if (result.Errors.Count > 0)
{
return 1;
}
var parsedArgs = await ProcessParseResults(result);
await WriteAsciiArt(parsedArgs);
return 0;
async Task<AsciiMessageOptions> ProcessParseResults(ParseResult result)
{
int delay = result.GetValue(delayOption);
List<string> messages =[.. result.GetValue(messagesArgument) ?? Array.Empty<string>()];
if (messages.Count == 0)
{
while (Console.ReadLine() is string line && line.Length > 0)
{
// Write Ascii
Colorful.Console.WriteAscii(line);
await Task.Delay(delay);
}
}
return new([.. messages], delay);
}
async Task WriteAsciiArt(AsciiMessageOptions options)
{
foreach (string message in options.Messages)
{
Colorful.Console.WriteAscii(message);
await Task.Delay(options.Delay);
}
}
public record AsciiMessageOptions(string[] Messages, int Delay);Notes
- لو حسيت إن الـ file-based app كبر، تقدر “تحوله لبرنامج معتمد على مشروع”. في .NET 10، العملية دي بقت أسهل بكتير وتقدر تعملها بأمر مباشر في الـ CLI وهو:
dotnet project convert file.cs، واللي بيكريتلك الـ.csprojويظبطلك كل حاجة أوتوماتيك بدون أي مجهود يدوي. - استخدام الـ Shebang (
#!/usr/bin/env dotnet) في أنظمة Unix بيخلي نظام التشغيل يستخدم الـenvعشان يدور على مسار الـdotnetفي بيئة النظام. دي الطريقة الأفضل و(Standard POSIX) بدل ما تكتب المسار الثابت اللي ممكن يتغير من جهاز للتاني. - الـ
#:packagedirective هو الإضافة السحرية في C# 14 اللي بتخليك تسحب NuGet packages دايركت جوة الملف بدون الحاجة لأي ملفات إضافية، وده اللي بيخلي الـ C# تنافس لغات الـ scripting التانية بقوة. بما إننا بنتكلم عن تحديثات .NET 10، ففيه كام ميزة قوية جداً اتضافت للـ File-based apps لازم تكون عارفها:
- فيه New Directives: خدنا
#:package، .NET 10 فيه حاجات تانية بتديك تحكم كامل:- الـ
#:project: عشان تعمل reference لمشروع تاني (مثال:#:project ../SharedLibrary/SharedLibrary.csproj). - الـ
#:property: عشان تظبط إعدادات الـ MSBuild (مثال:#:property TargetFramework=net10.0). - الـ
#:sdk: عشان تحدد الـ SDK اللي هتستخدمه (مثال مفيد جداً لبناء بيئة متكاملة مع .NET Aspire زي#:sdk Aspire.AppHost.Sdk@13.0.0). - الـPackaging as .NET Tools: تقدر تحول السكريبت بتاعك لـ .NET Tool بسهولة باستخدام أمر
dotnet pack file.cs، وتشاركه مع التيم بتاعك بسهولة.
- الـ