כבר באיך קוראים ל-DLL של C# Native AOT מ-C/C++ כתבנו על שימוש ב-Native AOT כדי לקרוא מ-C/C++ ל-C#. אבל למען האמת, נכון היה יותר לשים לפני זה את השאלה מה זה בכלל Native AOT. הסדר התהפך קצת.
הסיפור על Native AOT הוא בעיקר סיפור שבו קל מאוד למונחים להתערבב כבר בהתחלה.
- האם זה סיפור על ביטול JIT
- מה ההבדל מ-self-contained ומ-single-file
- האם זה מאותה משפחה כמו ReadyToRun
- מה קורה שיוצאות המון אזהרות trimming
- האם אפשר להשתמש בו באותה טמפרטורה גם ב-WPF / WinForms / ASP.NET Core
כשכל זה מתערבב יחד, Native AOT יכול להיראות כ’קסם שסתם מאיץ’, או ההפך — כ’משהו מלא מגבלות ומפחיד’. שני המבטים האלה קצת גסים.
flowchart TB
accTitle: אי-ההבנה שנוצרת כשהמונחים מתערבבים
accDescr: כשמונחים כמו JIT, self-contained, ReadyToRun ו-trimming מתערבבים יחד, Native AOT יכול להיראות כקסם שסתם מאיץ, או כמשהו מלא מגבלות ומפחיד, ושני המבטים האלה גסים למדי.
mix["המונחים מתערבבים יחד"] --> m1["נראה כקסם שמאיץ"]
mix --> m2["נראה מלא מגבלות ומפחיד"]
sort["מפרידים בין המילים"] --> fair["הופך לבחירה עם מקום שימוש ברור"]
איור 1: מקור אי-ההבנה הוא בלבול המונחים. מתחילים מהפרדת המילים.
במאמר הזה, בהתבסס בעיקר על התחושה המעשית הנוכחית מ-.NET 8 ואילך, נסדר תחילה את ארבעת אלה.
- מהותו של Native AOT
- מה משמח, ואיפה זה נהיה קשה
- מה ההבדל מ-ReadyToRun ומ-trimming
- מאיזו אפליקציה נעים להתחיל לנסות
תוכן העניינים
- תחילה המסקנה (בקצרה)
- תחילה, טבלת סידור
- 2.1. המילים סביב Native AOT
- 2.2. ההבדל בין JIT / ReadyToRun / Native AOT
- התמונה הכללית של Native AOT (תרשים)
- מה משמח ב-Native AOT
- 4.1. ההפעלה נוטה להיות קלה יותר
- 4.2. לא חייבים להניח התקנה מראש של סביבת ריצה
- 4.3. מתאים לסביבת הרצה מוגבלת
- מה נהיה קשה ב-Native AOT
- 5.1. רפלקציה ויצירת קוד דינמית
- 5.2. צריך לחשוב במונחי trimming כברירת מחדל
- 5.3. מפרסמים לכל פלטפורמה בנפרד
- 5.4. הקשר שולחן העבודה של Windows / COM דורש זהירות רבה
- הצעדים המינימליים
- 6.1.
csproj - 6.2. publish
- 6.3. איך כותבים JSON
- 6.1.
- מקרים שמתאימים
- מקרים שלא מתאימים
- מוקשים
- סיכום
- מקורות
מפת הידע של המאמר
.NET Native AOT היא שיטת פרסום שמקמפלת מראש לקוד נייטיבי בזמן ה-publish בלי להשתמש ב-JIT בזמן ריצה, וקל לה לשפר את זמן ההפעלה ואת תפוסת הזיכרון, וגם קל להפיץ אותה לסביבות שבהן אין התקנה מראש של סביבת ריצה של .NET. בעוד ש-ReadyToRun נוטה לכיוון שבו ה-IL נשאר וחלק מה-JIT מוקדם, Native AOT לא מניח JIT בזמן ריצה כלל, ולכן trimming הופך כמעט לברירת מחדל, וההתאמה נחלשת עם רפלקציה חופשית, יצירת קוד דינמית ו-built-in COM. ל-Native AOT של Windows אין built-in COM, WPF לא מתיישב עם trimming, ו-WinForms תלוי חזק ב-built-in COM marshalling — בשתיהן תמיכת ה-trimming מושבתת בצד ה-.NET SDK, ולכן מומלץ להסתכל בזהירות רבה עליהן כמועמד ראשון ל-Native AOT. כשדרוש COM, תכנון מחדש סביב ComWrappers הוא אפשרות חזקה.
flowchart LR
accTitle: מפת הידע של .NET Native AOT
accDescr: תרשים שמראה איך Native AOT שונה מ-JIT ומ-ReadyToRun, איך הוא מתנגש עם trimming, עם רפלקציה ועם built-in COM, ולמה WPF ו-WinForms דורשים זהירות רבה.
native_aot["Native AOT"]
jit_compilation["קומפילציית JIT (Just-In-Time)"]
readytorun["ReadyToRun"]
trimming["trimming (גיזום)"]
reflection_dotnet["רפלקציה"]
dynamic_code_generation["יצירת קוד דינמית (Reflection.Emit וכדומה)"]
builtin_com_interop["built-in COM interop"]
comwrappers["ComWrappers / source-generated COM"]
wpf["WPF"]
windows_forms["Windows Forms"]
source_generator["source generator"]
system_text_json_source_gen["source generation של System.Text.Json"]
self_contained_deployment["פרסום self-contained"]
single_file_deployment["פרסום single-file"]
dotnet[".NET (מ-Core ואילך)"]
native_aot -.->|"אינו מתיישב עם"| jit_compilation
readytorun -.->|"מחייב"| jit_compilation
native_aot -->|"מחייב"| trimming
native_aot -.->|"אינו מתיישב עם"| reflection_dotnet
native_aot -->|"אינו מתיישב עם"| dynamic_code_generation
native_aot -->|"אינו מתיישב עם"| builtin_com_interop
comwrappers -->|"מענה מומלץ ל"| native_aot
wpf -->|"אינו מתיישב עם"| trimming
windows_forms -->|"מחייב"| builtin_com_interop
wpf -->|"שימוש לא מומלץ ל"| native_aot
windows_forms -->|"שימוש לא מומלץ ל"| native_aot
native_aot -->|"משתמש ב"| source_generator
system_text_json_source_gen -->|"מצמצם"| reflection_dotnet
native_aot -->|"מחייב"| self_contained_deployment
native_aot -.->|"משתמש ב"| single_file_deployment
native_aot -->|"מחייב"| dotnet
בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 16, עם אסמכתה ורמת ודאות) והגדרות המושגים המרכזיים מרוכזות בעמוד המפורט של מפת הידע (ביפנית). נתונים: JSON-LD / Turtle
1. תחילה המסקנה (בקצרה)
- Native AOT היא שיטה שמקמפלת מראש אפליקציית .NET לקוד נייטיבי בזמן ה-publish, ומפיצה כך.
- מכיוון שלא נעשה שימוש ב-JIT בזמן ריצה, זמן ההפעלה ותפוסת הזיכרון נוטים להשתפר, וקל יותר להפיץ גם לסביבות שבהן אין התקנה מראש של סביבת ריצה של .NET.
- עם זאת, ההתאמה נחלשת עם רפלקציה חופשית, יצירת קוד דינמית, built-in COM וספריות שלא תומכות ב-trimming.
- כלומר, זה לא קסם שמאיץ, אלא מודל פרסום שמוותר במקצת על העולם הדינמי ונוטה לעבר עולם סטטי, לטובת צרכי ההפעלה, ההפצה וסביבת הריצה.
Native AOT הוא “מנגנון להפצת .NET בצורה שדומה לנייטיבי”, ולא סתם תיבת סימון להאצת קומפילציה.
flowchart TB
accTitle: מה מקבלים ומה מוותרים עליו
accDescr: Native AOT הוא מודל פרסום שבזכות קומפילציה מראש בזמן ה-publish מקבל זמן הפעלה, תפוסת זיכרון והפצה בלי צורך בסביבת ריצה, ובתמורה מוותר על רפלקציה חופשית, יצירת קוד דינמית וההתאמה עם built-in COM.
aot["Native AOT"] --> gain["מה שמקבלים"]
aot --> lose["מה שמוותרים עליו"]
gain --> g1["קלות בהפעלה, בזיכרון ובהפצה"]
lose --> l1["רפלקציה חופשית"]
lose --> l2["יצירת קוד דינמית ו-built-in COM"]
איור 2: לא קסם שמאיץ, אלא עסקת חליפין — מוותרים במקצת על העולם הדינמי לטובת עולם סטטי.
2. תחילה, טבלת סידור
2.1. המילים סביב Native AOT
תחילה, אם מפרידים בין אלה, ההמשך קל יותר.
| מונח | מה הוא עושה | הקשר ל-Native AOT |
|---|---|---|
| JIT | יוצר קוד נייטיבי מ-IL בזמן ריצה | Native AOT מסיים את זה מראש |
| self-contained | מפיץ יחד את כל חבילת .NET הדרושה להרצה | Native AOT נחשב מהמשפחה הזו |
| single-file | מרכז את חומר ההפצה לקובץ אחד | לא זהה למהות Native AOT, אבל המראה בפועל נוטה להידמות |
| trimming | מוריד קוד שלא בשימוש | ב-Native AOT זה כמעט תמיד ברירת מחדל |
| ReadyToRun | משאיר את ה-IL, ומקדים במקצת חלק מעבודת ה-JIT | דומה ל-Native AOT אבל שונה בבסיסו |
| source generator | מעביר עיבוד דינמי בזמן ריצה ליצירת קוד בזמן build | מתאים היטב ל-Native AOT |
מה שקל להתערבב הוא ש-Native AOT הוא לא פיצ’ר אחד בודד, אלא מודל פרסום שפועל בשיתוף פעולה עם self-contained, trimming, source generation, publish עם RID קבוע ועוד.
flowchart TB
accTitle: המנגנונים שפועלים יחד עם Native AOT
accDescr: Native AOT הוא לא פיצ'ר בודד, אלא מודל פרסום שפועל בשילוב עם self-contained, trimming, source generation ו-publish עם RID קבוע.
aot["Native AOT〔מודל פרסום〕"] --> s1["מהמשפחה של self-contained"]
aot --> s2["trimming כמעט תמיד ברירת מחדל"]
aot --> s3["התאמה טובה עם source generator"]
aot --> s4["publish עם RID קבוע"]
איור 3: Native AOT הוא לא פיצ’ר עצמאי, אלא מודל פרסום שפועל יחד עם מנגנונים סובבים.
2.2. ההבדל בין JIT / ReadyToRun / Native AOT
גם כאן הכי מהיר לראות תחילה בטבלה אחת.
| היבט | הרצת JIT רגילה | ReadyToRun | Native AOT |
|---|---|---|---|
| JIT בזמן ריצה | בשימוש | עדיין יש מצבים בשימוש | לא בשימוש |
| תוכן חומר ההפצה | מתמקד ב-IL | IL + קוד שנוצר מראש | מתמקד בקובץ הרצה נייטיבי |
| הפעלה | קו הבסיס | נוטה להשתפר | נוטה להשתפר משמעותית |
| התאמה | הרחבה ביותר | רחבה | מגבלות חזקות |
| יכולות דינמיות | נוח לשימוש | נוח לשימוש ברובו | הרבה מגבלות |
| מטרה מתאימה | פיתוח .NET רגיל בכללותו | רוצים קודם לשפר הפעלה | לוקחים בכוח הפעלה, הפצה וסביבה מוגבלת |
אם ReadyToRun הוא בכיוון ‘להקל קצת על ה-JIT’, Native AOT הוא בכיוון ‘לא להניח JIT בזמן ריצה מלכתחילה’. גם אם שתיהן נושאות את אותה מילה AOT, ההבדל בטמפרטורה משמעותי.
flowchart TB
accTitle: ההבדל בכיוון בין ReadyToRun ל-Native AOT
accDescr: ReadyToRun משאיר את ה-IL ומקדים במקצת עבודת JIT כדי להקל עליה, ואילו Native AOT לא מניח JIT בזמן ריצה מלכתחילה, וגם אם שתיהן נושאות את המילה AOT ההבדל בטמפרטורה משמעותי.
q{"מה רוצים לעשות עם ה-JIT"}
q -->|"להקל קצת"| r2r["ReadyToRun〔ה-IL נשאר〕"]
q -->|"לא להניח מלכתחילה"| aot["Native AOT〔מתמקד בנייטיבי〕"]
r2r --> soft["ההתאמה נשארת רחבה"]
aot --> hard["ההפעלה מהירה אבל המגבלות חזקות"]
איור 4: גם אם שתיהן AOT, ‘להקל על ה-JIT’ ו’לא להניח JIT’ הם שני דברים שונים.
לאחר מכן, אם מרכזים לתרשים אחד את השאלה “אז איזו צורת הפצה בוחרים בסוף”, מתקבל זה:
flowchart TD
accTitle: בחירת צורת ההפצה
accDescr: תרשים החלטה הבוחן אם אפשר להתקין סביבת ריצה ביעד, אם חשוב לצמצם את זמן ההפעלה, ואם יש תלות ברפלקציה, ביצירת קוד דינמית או ב-COM המובנה, ומוביל משם ל-framework-dependent, ReadyToRun, self-contained או Native AOT.
S["רוצים להחליט צורת הפצה"] --> Q1{"אפשר להתקין סביבת ריצה של .NET ביעד ההפצה?"}
Q1 -->|"אפשר"| Q2{"רוצים לצמצם את זמן ההפעלה?"}
Q2 -->|"לא עד כדי כך"| P1["framework-dependent
פרסום רגיל"]
Q2 -->|"רוצים לצמצם"| P2["ReadyToRun
שיפור הפעלה תוך שמירה על ההתאמה"]
Q1 -->|"לא רוצים / אי אפשר להתקין"| Q3{"האם יש תלות ברפלקציה, יצירת קוד דינמית או built-in COM?"}
Q3 -->|"יש תלות"| P3["self-contained
מרכזים ל-single-file אם צריך"]
Q3 -->|"אין תלות"| Q4{"היעד הוא console / worker / API קטן?"}
Q4 -->|"כן"| P4["Native AOT"]
Q4 -->|"לא"| P5["קודם self-contained
לצמצם תלות דינמית ואז לבחון מחדש"]
איור 5: איך בוחרים צורת הפצה. מסתעפים לפי הסדר — האם אפשר להניח סביבת ריצה, והאם אפשר לצמצם מנגנונים דינמיים.
ההסתעפות הראשונה היא האם אפשר להניח סביבת ריצה ביעד ההפצה, וההסתעפות השנייה היא עד כמה אפשר לצמצם את המנגנונים הדינמיים. self-contained ו-single-file הם סיפור של “איך מפיצים”, ואילו ReadyToRun ו-Native AOT הם סיפור של “מתי יוצרים את הקוד הנייטיבי”, כך שבפועל צריך לחשוב עליהם בשילוב.
3. התמונה הכללית של Native AOT (תרשים)
אם מציירים תרשים גס ל-Native AOT, זה כך:
flowchart LR
accTitle: המסלול מהקוד אל ההרצה
accDescr: תרשים המשווה בין המסלול הרגיל, שבו ה-IL מתקמפל ב-JIT בזמן ריצה, לבין המסלול של PublishAot, שבו ניתוח AOT ו-trim מורידים קוד מיותר ואז נוצר קובץ הרצה נייטיבי ספציפי ל-RID.
Src["קוד מקור C# / .NET"] --> IL["ה-assembly של ה-IL"]
IL -->|"הרצה רגילה"| JIT["JIT בזמן ריצה"]
JIT --> Run1["הרצת האפליקציה"]
IL -->|"dotnet publish + PublishAot"| Analyze["ניתוח AOT / trim"]
Analyze --> Trim["הורדת קוד מיותר"]
Trim --> AOT["יצירת קוד נייטיבי"]
AOT --> Run2["קובץ הרצה ספציפי ל-RID"]
איור 6: בהרצה רגילה עושים JIT בזמן ריצה, אבל Native AOT מקדים את הניתוח, ההורדה והיצירה הנייטיבית לזמן ה-publish.
ב-.NET רגיל, קודם יוצרים IL, ובזמן ריצה עושים JIT רק לחלק שדרוש. Native AOT מקדים חלק גדול מהשלב שאחרי זה לזמן ה-publish.
מה שחשוב כאן הוא ש-בזמן ה-publish כמעט צריך לדעת מראש את כל הקוד שיידרש בזמן ריצה. כאן משתנות ההנחות של מה מותר לכתוב.
- מציאת טיפוס בזמן ריצה
- יצירת קוד בזמן ריצה
- טעינת Assembly בזמן ריצה
- פתרון מושהה (lazy) בגישת “יסתדר איכשהו” בזמן ריצה
צורות כתיבה כאלה מתחילות פתאום להתנגש עם Native AOT.
flowchart TB
accTitle: צריך לדעת הכול כבר בזמן ה-publish
accDescr: ב-Native AOT צריך לדעת כמעט את כל הקוד שיידרש בזמן ריצה כבר בזמן ה-publish, ולכן צורות כתיבה כמו מציאת טיפוס בזמן ריצה, יצירת קוד בזמן ריצה, טעינת Assembly בזמן ריצה ופתרון מושהה מתנגשות איתו.
need["הקוד הדרוש נקבע בזמן ה-publish"] --> ng1["מציאת טיפוס בזמן ריצה"]
need --> ng2["יצירת קוד בזמן ריצה"]
need --> ng3["קריאת Assembly בזמן ריצה"]
need --> ng4["פתרון מושהה כדי להסתדר"]
ng1 -.-> bad["כל אלה מתנגשים עם AOT"]
איור 7: מהות שינוי ההנחות. ככל שהכתיבה מתבססת על ‘קביעה בזמן ריצה’, ההתנגשות עם Native AOT חזקה יותר.
4. מה משמח ב-Native AOT
4.1. ההפעלה נוטה להיות קלה יותר
ההשפעה הכי ברורה של Native AOT היא בכל זאת ההפעלה.
- כלי CLI
- תהליכים קצרי-חיים
- הפעלה בסגנון serverless
- הפעלה והחלפה של קונטיינרים
- כלי ניטור או תהליך רקע קטן
בתחומים האלה עלות ה-JIT בולטת, ומכיוון ש-Native AOT מקדים אותה, ההפעלה הראשונה נעשית קלה יותר.
גם תפוסת הזיכרון נוטה להשתפר, כך שיש יתרון גם כשרוצים לדחוס יותר מופעים באותה כמות משאבים. בייחוד בצד ענן שבו אותו תהליך עולה בכמויות גדולות, ההבדל הזה מצטבר לאט לאט.
4.2. לא חייבים להניח התקנה מראש של סביבת ריצה
אפליקציה שפורסמה עם Native AOT קל יותר להריץ גם בסביבה שבה אין התקנה מראש של סביבת ריצה של .NET.
זו נקודה שקטה אבל משמעותית.
- לא רוצים לומר ליעד ההפצה “קודם תתקינו .NET 9 Runtime”
- רוצים להקטין את תמונת הקונטיינר
- רוצים למקם כלי קטן אחד ולהריץ אותו
- לא רוצים / אי אפשר לאפשר JIT בסביבת הריצה
במצבים כאלה, עצם ההיעדר של “הנחה שצריך להכין סביבת ריצה בנפרד” משקיט את הסיפור הרבה.
“ללא צורך בסביבת ריצה” כאן פירושו שלא צריך להתקין .NET בנפרד ביעד ההפצה. זה לא אומר שהחלק המקביל ל-runtime נעלם לגמרי מתוך האפליקציה.
flowchart TB
accTitle: המשמעות הנכונה של "ללא צורך בסביבת ריצה"
accDescr: "ללא צורך בסביבת ריצה" ב-Native AOT פירושו שלא צריך להתקין .NET בנפרד ביעד ההפצה, ולא שהחלק המקביל ל-runtime נעלם לגמרי מתוך האפליקציה.
word["המילים 'ללא צורך בסביבת ריצה'"] --> ok["לא צריך להתקין .NET ביעד ההפצה"]
word -.-> ngx["החלק המקביל ל-runtime לא נעלם"]
ok --> merit["פחות הנחות בהפצה ובהפעלה"]
איור 8: “ללא צורך בסביבת ריצה” הוא סיפור על יעד ההפצה. החלק המקביל ל-runtime כלול בתוך האפליקציה.
4.3. מתאים לסביבת הרצה מוגבלת
מכיוון ש-Native AOT לא משתמש ב-JIT בזמן ריצה, קל יותר להריץ אותו גם בסביבה שבה JIT אסור.
זה נקודה שבולטת יותר בכיוון ענן, קונטיינרים ומובייל מאשר בכיוון desktop. עם זאת, גם בהקשר של פיתוח Windows, במובן של “רוצים להקטין הנחות מיותרות ביעד ההפצה” — זו נקודה משמחת דיה.
5. מה נהיה קשה ב-Native AOT
5.1. רפלקציה ויצירת קוד דינמית
המגבלה המרכזית של Native AOT היא כאן.
- טעינה דינמית כמו
Assembly.LoadFile - יצירת קוד בזמן ריצה כמו
System.Reflection.Emit - רפלקציה שעוקבת אחר טיפוסים ללא הגבלה בזמן ריצה
- כתיבה שמרכיבה generic באופן חופשי בזמן ריצה
קשה לקבוע מראש בזמן ה-publish את הקוד הדרוש עבור אלה, ולכן הם הופכים לחממה לאזהרות AOT.
כמובן, זה לא פשוט עד כדי כך שאם יש שורת רפלקציה אחת בלבד — זה כישלון מיידי. עם זאת, המגמה ש-כתיבה שנוטה לכיוון “קובעים ורואים בזמן ריצה” נעשית קשה יותר — לא זזה.
השם שנפוץ להופיע בשמות האזהרות הוא משפחת RequiresDynamicCode.
משמעותו “הקריאה הזו עלולה להישבר תחת AOT”, כך שבטוח יותר לא לדכא אותה (suppress) בקלות דעת.
כשעושים Native AOT, מועיל לחשוב על זה כניסיון להקטין את “החוכמה בזמן ריצה” ולהגדיל את “ההצהרה בזמן build”.
flowchart TB
accTitle: מ"חוכמה בזמן ריצה" ל"הצהרה בזמן build"
accDescr: טעינה דינמית, יצירת קוד בזמן ריצה ורפלקציה ללא הגבלה הופכים לחממה לאזהרות AOT, ולכן מכוונים את התכנון להקטין את החוכמה בזמן ריצה ולהגדיל את ההצהרה בזמן build.
dynamicway["תכנון שנשען על חוכמה בזמן ריצה"] --> warn["אזהרות ממשפחת RequiresDynamicCode"]
warn --> shift["מגדילים הצהרה בזמן build"]
shift --> calm["הקוד הדרוש נקבע בזמן ה-publish"]
warn -.-> nosup["לא לדכא בקלות דעת"]
איור 9: הדרך היחידה להתמודד עם המגבלה המרכזית. הופכים “קובעים בזמן ריצה” ל”מצהירים בזמן build”.
5.2. צריך לחשוב במונחי trimming כברירת מחדל
Native AOT קשור בחוזקה ל-trimming. מה שקל לפספס כאן הוא ש-זה משפיע לא רק על הקוד שלכם, אלא גם על אופן הכתיבה של ספריות התלות.
הנקודות שדורשות זהירות הן בערך אלה:
- מסדר טורי (serializer) מבוסס רפלקציה
- מבנה DI / plugin שאוסף טיפוסים בסריקה בזמן ריצה
- מנגנון שמוצא טיפוס לפי שם מחרוזת ומייצר אותו
- ספרייה שנשענת על proxy דינמי או על יצירת IL
אם יש אזהרה כאן, וקובעים “ה-publish עבר אז זה בסדר” — זה יכול לגרום לצרות אמיתיות בהמשך. ב-Native AOT, לרוב כדאי לקרוא את האזהרות ברצינות.
flowchart TB
accTitle: trimming שלא נקבע רק לפי הקוד שלכם
accDescr: הצלחת ה-trimming תלויה לא רק באופן הכתיבה של הקוד שלכם אלא גם באופן הכתיבה של ספריות התלות, ומסדר טורי מבוסס רפלקציה, DI עם סריקה בזמן ריצה, פתרון טיפוס לפי מחרוזת וספריות עם proxy דינמי דורשים זהירות.
trim["הצלחת ה-trimming"] --> own["אופן הכתיבה של הקוד שלכם"]
trim --> dep["אופן הכתיבה של ספריות התלות"]
dep -.-> ex["מסוג רפלקציה או סריקה בזמן ריצה"]
dep --> care["קוראים את האזהרות ברצינות ומוודאים"]
איור 10: מה שקל לפספס הוא צד ספריות התלות. “ה-publish עבר אז זה בסדר” יכול להתגלגל לצרה בהמשך.
ובכל זאת, סתם “תקראו ברצינות” לא מספיק כדי לדעת איך לפעול, אז נכתוב את סדר הטיפול העדיף. גם התיעוד הרשמי ממליץ לנסות בסדר הזה.
| סדר | טיפול | מתי משתמשים בו |
|---|---|---|
| 1 | להפסיק להשתמש ברפלקציה | כשאפשר להחליף Activator.CreateInstance(Type) בארגומנט generic, או לעבור ל-source generator |
| 2 | להוסיף DynamicallyAccessedMembers |
כשרפלקציה עדיין נדרשת, אבל הטיפוס היעד ידוע כבר בזמן הקומפילציה |
| 3 | להוסיף RequiresUnreferencedCode |
כשקובעים שם טיפוס לפי מחרוזת בזמן ריצה וכדומה, ולא ניתן לבצע ניתוח סטטי מהותית |
| 4 | לדכא (suppress) עם UnconditionalSuppressMessage |
האמצעי האחרון, כשכל האמור לעיל בלתי אפשרי ואישרו שזה בטוח |
הדפוס הנפוץ ביותר כצעד ראשון הוא מספר 2 — “הטיפוס ידוע, אבל בכל זאת יוצאת אזהרה”.
לדוגמה, הקוד הבא מייצר IL2070.
// IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods'
void PrintMethodNames(Type type)
{
foreach (var method in type.GetMethods())
{
Console.WriteLine(method.Name);
}
}
אם מצהירים באמצעות attribute שרוצים לשמר את השיטות ה-public כשקוראים ל-GetMethods(), האזהרה נעלמת.
using System.Diagnostics.CodeAnalysis;
void PrintMethodNames(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
foreach (var method in type.GetMethods())
{
Console.WriteLine(method.Name);
}
}
// אם הצד הקורא מעביר עם typeof, הדרישה מתמלאת אוטומטית
PrintMethodNames(typeof(DateTime));
ה-API שקוראים לו וההצהרה הדרושה בדרך כלל תואמים זה לזה. עבור GetMethod / GetMethods זה PublicMethods, עבור GetProperty / GetProperties זה PublicProperties, ועבור Activator.CreateInstance זה PublicParameterlessConstructor או PublicConstructors.
DynamicallyAccessedMemberTypes.All נוח, אבל שומר את כל חברי הטיפוס היעד וזה מנפח את הגודל, וגם החברים שנשמרו מביאים אזהרות נוספות — לכן העיקרון הוא לציין את המינימום ההכרחי.
יצוין שאם הוספת ה-attribute לא מבטלת את האזהרה, בודקים מהמקום שבו נעשה שימוש ברפלקציה חזרה עד הצד הקורא, שהתוספת קיימת לאורך כל המסלול. אם חוליה אחת בדרך חסרה, הדרישה נקטעת שם.
flowchart TB
accTitle: כשהוספת attribute לא מבטלת אזהרה
accDescr: מציינים ב-DynamicallyAccessedMembers את המינימום ההכרחי, וכשהאזהרה לא נעלמת, בודקים מהמקום שבו נעשה שימוש ברפלקציה חזרה עד הצד הקורא שהתוספת קיימת לאורך כל המסלול. חוליה חסרה אחת קוטעת את הדרישה שם.
warnleft["הוספת attribute והאזהרה עדיין נשארת"] --> back["בודקים חזרה עד הצד הקורא"]
back --> chain["מוודאים שהתוספת קיימת לאורך כל המסלול"]
chain -.-> cut["חוליה חסרה אחת קוטעת את הדרישה"]
warnleft -.-> minrule["העיקרון הוא לציין מינימום הכרחי"]
איור 11: מציינים את ה-attribute על המסלול ולא על נקודה בודדת. חוליה חסרה אחת קוטעת את הדרישה שם.
5.3. מפרסמים לכל פלטפורמה בנפרד
Native AOT מפרסם עם RID (Runtime Identifier) קבוע.
כלומר, זה לא עולם שבו מריצים כמו שהוא ב-linux-x64 משהו שנבנה עבור win-x64.
- Windows x64
- Windows Arm64
- Linux x64
- Linux Arm64
- macOS Arm64
וכן הלאה — ההנחה היא ליצור חומר הפצה נפרד לכל יעד.
כאן התחושה נעשית הרבה יותר “דומה לאפליקציה נייטיבית” בהשוואה ל-.NET רגיל מסוג framework-dependent.
5.4. הקשר שולחן העבודה של Windows / COM דורש זהירות רבה
בהקשר של KomuraSoft, זו נקודה חשובה במיוחד.
ל-Native AOT של Windows אין built-in COM. בנוסף, WPF לא מתיישב טוב עם trimming, ו-WinForms תלוי חזק ב-built-in COM marshalling, ולכן לפחות בשלב הזה, מומלץ להסתכל בזהירות רבה על שתיהן כ”מועמד ראשון ל-Native AOT”.
נכתוב כאן את הבסיס ל”בשלב הזה”. הכתוב במאמר הזה מבוסס על התיעוד הרשמי של דורות .NET 8 / 9 / 10 (נכון ליולי 2026). לגבי WPF ו-WinForms, ב-“Known trimming incompatibilities” של Microsoft נכתב במפורש שמכיוון ש-WPF תלוי חזק ברפלקציה ובבדיקת קוד בזמן ריצה וכמעט לא פועל אחרי trimming, ומכיוון ש-WinForms תלוי חזק ב-built-in COM marshalling, בשתיהן תמיכת ה-trimming מושבתת בצד ה-.NET SDK. כלומר, זה לא “מומלץ להסתכל בזהירות” אלא מצב שבו ה-SDK בפועל עוצר את זה כרגע. יש אפשרות שהניסוח הזה ישתנה בעתיד, אז מומלץ לוודא באותו עמוד בזמן הקריאה.
flowchart TB
accTitle: הסיבה ש-WPF ו-WinForms נעצרים
accDescr: ל-Native AOT של Windows אין built-in COM, WPF תלוי חזק ברפלקציה ובבדיקת קוד בזמן ריצה וכמעט לא פועל אחרי trimming, ו-WinForms תלוי חזק ב-built-in COM marshalling, ולכן בשתיהן תמיכת ה-trimming מושבתת בצד ה-.NET SDK.
win["Native AOT של Windows"] --> nocom["אין built-in COM"]
wpf["WPF"] --> refl["תלות חזקה ברפלקציה"]
wf["WinForms"] --> commar["תלות חזקה ב-COM marshalling"]
refl --> off["תמיכת trimming מושבתת בצד ה-SDK"]
commar --> off
איור 12: זה לא “בזהירות”, אלא מצב שבו ה-SDK עוצר בפועל כרגע. גוף שולחן העבודה לא מועמד ראשון.
בקיצור:
- להפוך את גוף WPF / WinForms ל-Native AOT מיד
- להביא COM interop כמו שהוא, באותה תחושה רגילה
אלה נוטים לגרום לזינוק פתאומי באזהרות ה-publish ובמגבלות זמן הריצה, וגם עלות הטיפול קופצת מהר.
ולהפך:
- קונסולה
- worker
- Web API קטן
- רכיב שגם בתוך אינטגרציה נייטיבית קל להוריד לגבול פונקציית C
אלה טבעיים יותר כנקודת כניסה.
אם דרוש COM, יש מקרים שבהם עדיף להישאר עם JIT, או לתכנן מחדש סביב ComWrappers / source-generated COM.
6. הצעדים המינימליים
לפני זה, ל-publish של Native AOT דרושה שרשרת כלים נייטיבית.
אם מריצים dotnet publish רק עם PublishAot, הכישלון לא קורה בקומפילציה, אלא בשלב הקישור הנייטיבי האחרון. זה המכשול הראשון, אז מומלץ להתקין מראש.
| סביבה | מה נדרש |
|---|---|
| Windows | Visual Studio 2022 ומעלה. להתקין את עומס העבודה ‘פיתוח שולחן עבודה עם C++’ עם כל הרכיבים המוגדרים כברירת מחדל |
| Ubuntu 18.04 ומעלה | sudo apt-get install clang zlib1g-dev |
| Alpine 3.15 ומעלה | sudo apk add clang build-base zlib-dev |
| Fedora 39 ומעלה / RHEL 8 ומעלה | sudo dnf install clang zlib-ng-devel zlib-ng-compat-devel zlib-devel |
| macOS | Xcode Command Line Tools (נתמך מ-.NET 8 ואילך) |
בקיצור, מדובר בשרשרת כלי הקומפילציה ובחבילות הפיתוח של הספריות שסביבת הריצה של .NET תלויה בהן. אם מריצים ב-CI, בדוגמאות ה-Native AOT של dotnet/samples יש Dockerfile גם ל-Linux וגם ל-Windows, ולכן הכי מהיר להביא משם את שלבי ההתקנה ההתחלתיים.
יצוין שקובץ בינארי שנבנה ב-Linux פועל רק ב-Linux זהה או חדש יותר. משהו שנבנה ב-Ubuntu 20.04 פועל מ-20.04 ואילך, אבל לא ב-18.04. בחירת סביבת ה-build הופכת ישירות לטווח ההפצה האפשרי.
flowchart TB
accTitle: המכשול לפני ה-publish וטווח ההפצה האפשרי
accDescr: ל-publish של Native AOT דרושה שרשרת כלים נייטיבית, ובלעדיה נכשל בשלב הקישור הנייטיבי האחרון. בנוסף, קובץ בינארי שנבנה ב-Linux פועל רק ב-Linux זהה או חדש יותר, ובחירת סביבת ה-build הופכת לטווח ההפצה האפשרי.
tool{"האם קיימת שרשרת כלים"}
tool -->|"לא"| fail["נכשל בשלב הקישור הנייטיבי"]
tool -->|"כן"| bin["יוצא בינארי לכל RID"]
bin -.-> range["ב-Linux פועל רק בסביבה זהה או חדשה יותר"]
range -.-> pick["בחירת סביבת ה-build קובעת את טווח ההפצה"]
איור 13: המכשול הראשון הוא שרשרת הכלים. ב-Linux, ותק סביבת ה-build קובע את טווח ההפצה האפשרי.
6.1. csproj
קודם כול מכניסים PublishAot לקובץ הפרויקט.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PublishAot>true</PublishAot>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
הדוגמה מספיקה עם net8.0. הרעיון עצמו כמעט זהה גם ב-.NET 9 / 10.
מה שחשוב הוא לא להוסיף זמנית רק בשורת הפקודה של dotnet publish, אלא
להשאיר את זה קבוע בפרויקט מההתחלה, ולעקוב יומיומית אחר הניתוח בזמן ה-build / publish.
יצוין שגם אם מכניסים <PublishAot>true</PublishAot>, ההרצה המקומית הרגילה לא הופכת פתאום ל-Native AOT.
dotnet run היומיומי וההרצה הרגילה עדיין עם JIT — מופע ה-Native AOT מתרחש בזמן ה-publish.
flowchart TB
accTitle: החיים היומיומיים אחרי הוספת PublishAot
accDescr: גם אם PublishAot מוגדר בפרויקט, dotnet run היומיומי נשאר עם JIT, וקומפילציית Native AOT מתרחשת בזמן ה-publish. חשוב להשאיר את ההגדרה קבועה בפרויקט ולעקוב יומיומית אחר ניתוח ה-build וה-publish.
put["מכניסים PublishAot ל-csproj"] --> daily["ה-dotnet run היומיומי נשאר עם JIT"]
put --> pub["קומפילציית AOT בזמן ה-publish"]
daily -.-> watch["עוקבים יומיומית אחר ניתוח ואזהרות"]
איור 14: היומיום עם JIT, המופע עם publish. לכן מכניסים את ההגדרה קבוע ולא זמנית, ועוקבים אחר הניתוח.
6.2. publish
לדוגמה, עבור Windows x64, זו הצורה:
dotnet publish -c Release -r win-x64
עבור Linux x64, זה כך:
dotnet publish -c Release -r linux-x64
הפלט הוא עם RID קבוע. במקום מבט של “DLL אחד של .NET שפועל בכל מקום”, המבט משתנה ל”קובץ הרצה שנבנה עבור מערכת ההפעלה / הארכיטקטורה הזו”.
אם רוצים לגעת מצד Web API, נוח יותר להתחיל מתבנית מבוססת Native AOT.
dotnet new webapiaot -o MyFirstAotWebApi
עבור worker, זו הדרך:
dotnet new worker -o WorkerWithAot --aot
6.3. איך כותבים JSON
מה שנוטים להיתקל בו לעיתים קרובות ב-Native AOT הוא JSON.
System.Text.Json נוטה לגלוש לכיוון רפלקציה כשמשתמשים בו בתחושה הרגילה, כך שבטוח יותר לעבור ל-source generation.
using System.Text.Json;
using System.Text.Json.Serialization;
[JsonSerializable(typeof(AppConfig))]
internal partial class AppJsonContext : JsonSerializerContext
{
}
public sealed class AppConfig
{
public string? Name { get; init; }
public int RetryCount { get; init; }
}
var config = new AppConfig
{
Name = "sample",
RetryCount = 3
};
string json = JsonSerializer.Serialize(config, AppJsonContext.Default.AppConfig);
בפועל, כדאי לזכור את זה לא כ”התאמה ל-Native AOT” אלא כמגמה של “לא לתת לו לחפש טיפוסים בזמן ריצה” — כך לא טועים בכיוון.
7. מקרים שמתאימים
Native AOT מתאים בנוחות במקרים כמו אלה:
- כלי CLI / כלי שבו ההפעלה היא הכוכב
- API קטנים שמפיצים בכמות גדולה בקונטיינרים
- worker / שירות רקע
- תהליכים חסרי שרת (serverless) או קצרי-חיים
- רכיב .NET קטן שמוחדר לתוך אפליקציה נייטיבית
- מצב שבו לא רוצים לדרוש התקנה מראש של סביבת ריצה של .NET בסביבת ההרצה
המשותף הוא ש-הגבול ברור יחסית, וקל להקטין את המנגנונים הדינמיים.
8. מקרים שלא מתאימים
ולהפך, יש גם מקרים שבהם ברור שלא כדאי לעשות מ-Native AOT את הזירה הראשית כבר מההתחלה.
- גוף אפליקציה גדולה קיימת של WPF / WinForms
- מבנה שמניח built-in COM interop
- אפליקציה שבה טעינת plugin בזמן ריצה היא הכוכב
- מבנה עם תלות חזקה במסגרת עבודה שמחפשת טיפוסים דרך רפלקציה
- ספרייה שמשתמשת כדבר מובן מאליו ב-
System.Reflection.Emitאו ב-proxy דינמי - תכנון שמשלב C++/CLI
במקרים כאלה עדיף .NET רגיל מבוסס JIT, או ReadyToRun, או שבחינה מחדש של חלוקת התכנון.
9. מוקשים
לסיום, סיכום של נקודות שקל להיתקל בהן בצעד הראשון עם Native AOT.
- להתייחס בקלות ראש לאזהרות ה-publish
- כאמור, אזהרות Native AOT הן יעד שצריך לקרוא ברצינות.
- ה-build עובר, אבל ה-publish נשבר
- בזמן ה-publish רץ ניתוח רציני שכולל גם את ספריות התלות, ולכן יש דברים שנראים לראשונה רק כאן.
- להתייחס ל-ReadyToRun ול-Native AOT באותה תחושה
- המילים דומות, אבל חוזק המגבלות שונה מאוד.
- להתחיל ישר מגוף אפליקציית desktop
- נעים יותר להתחיל מ-console / worker / API קטן.
- לכתוב JSON וקישור הגדרות בגישה הרגילה
- כתיבה שמניחה רפלקציה תתפוס בהמשך.
- להפיץ מתוך כוונה שזה בלתי תלוי בפלטפורמה
- חומר ההפצה של Native AOT הוא עם RID קבוע.
- לחשוב ש”Native AOT = הכול מואץ”
- הכוכבים הם ההפעלה, ההפצה וסביבת הריצה. אם מפספסים את זה, הציפיות מתעוותות.
ב-Native AOT, מה שקובע בסוף אם אפשר או לא הוא לא dotnet build אלא dotnet publish.
אם מתחילים להריץ את זה מוקדם, פחות סביר להיתקע בהמשך.
flowchart TB
accTitle: מי שקובע אפשר/לא-אפשר הוא ה-publish
accDescr: זה שה-build עובר אבל ה-publish נשבר קורה כי בזמן ה-publish רץ ניתוח רציני שכולל גם את ספריות התלות, ולכן מי שקובע בסוף אם אפשר או לא ב-Native AOT הוא לא dotnet build אלא dotnet publish.
buildok["dotnet build עובר"] --> notyet["האפשרות עוד לא נקבעה"]
pubx["dotnet publish"] --> deep["רץ ניתוח רציני שכולל תלויות"]
deep --> verdict["רק כאן מתבררת לראשונה האפשרות"]
verdict -.-> early["לכן מריצים publish מוקדם"]
איור 15: המשותף למוקשים. לא להסתפק ב”ה-build עבר” — מריצים publish מוקדם.
10. סיכום
אם אומרים את Native AOT במשפט אחד, זה מנגנון שמזיז את אפליקציית .NET ממודל הרצה דינמי למודל הפצה שקל יותר לקבוע באופן סטטי.
הנקודות ששווה לזכור מסתכמות בחמש אלה.
- Native AOT מקמפל מראש לקוד נייטיבי בזמן ה-publish
- משפיע היטב על צרכי ההפעלה, הזיכרון וההפצה
- בתמורה, קשה עם רפלקציה, יצירת קוד דינמית, built-in COM וקוד שלא תומך ב-trimming
- היעד הראשון נעים יותר כ-console / worker / API קטן מאשר כגוף desktop
- חשוב לבטל אזהרות תוך כדי, ולוודא מוקדם ולעיתים קרובות מבוסס publish
Native AOT הוא לא מתג סטנדרטי שמצרפים לכל אפליקציית .NET. אבל במצב שבו ההפעלה חשובה, רוצים להקל את ההפצה, ורוצים להקטין הנחות בסביבת הריצה — זה כלי חזק למדי.
ולהפך, בעולם הצפוף של WPF / WinForms / COM, יש עדיין מקרים רבים שבהם .NET רגיל עדיף. כשמבחינים בין אלה, Native AOT הופך מ”פיצ’ר חדש ומסובך” לבחירה עם מקום שימוש ברור.
11. מקורות
- Native AOT deployment overview - .NET
- Native AOT deployment overview - .NET (יפנית)
- Introduction to AOT warnings - .NET
- Fixing trim warnings - .NET
- DynamicallyAccessedMembersAttribute Class - .NET
- Prepare .NET libraries for trimming - .NET
- Known trimming incompatibilities - .NET
- How to use source generation in System.Text.Json - .NET
- ASP.NET Core support for Native AOT
- ReadyToRun deployment overview - .NET
- Building native libraries - .NET
- ComWrappers source generation - .NET
- מאמר קשור: איך קוראים ל-DLL של C# Native AOT מ-C/C++
- מאמר קשור: קריאה ל-DLL נייטיבי מ-C#: wrapper של C++/CLI מול P/Invoke
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
מה זה .NET Generic Host - התשתית ל-DI, תצורה ולוגים
מסדרים את התפקיד של Generic Host דרך היחסים בין DI, תצורה, לוגים, IHostedService ו-BackgroundService, ומסכמים מנקודת מבט מעשית איפה הוא ב...
שלושה סוגי טיימרים ב-.NET — מתי להשתמש ב-PeriodicTimer/Timer/DispatcherTimer
מסודר כאן ההבדל בין PeriodicTimer / System.Threading.Timer / DispatcherTimer, ואיך לבחור ביניהם לעיבוד async, callback ב-ThreadPool וע...
איך קוראים ל-DLL של C# Native AOT מ-C/C++
פרסום ספריית מחלקות C# כ-DLL נייטיבי באמצעות Native AOT, וקריאה לנקודות הכניסה מסוג UnmanagedCallersOnly מ-C/C++ — סקירה לפי מקום השימוש...
למה להשתמש ב-Generic Host וב-BackgroundService של .NET באפליקציית desktop
בכלי Windows ובאפליקציות שרצות ברקע, מסודר כאן איך להשתמש ב-Generic Host וב-BackgroundService כדי לסדר הפעלה, עיבוד תקופתי, סיום, לוג, הג...
מדריך מעשי ל-FileSystemWatcher — התמודדות עם פספוסים וכפילויות
המאמר מסדר את אופן השימוש ואת נקודות התשומת לב של FileSystemWatcher, מנקודת המבט של פספוסים, התראות כפולות, מלכודות בקביעת סיום, סריקה חו...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
יכולת פעולה הדדית בין 32 ל-64 סיביות
תאימות 32/64 סיביות, גבולות native והחלטות תכנון ב-Windows.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
יישומים עסקיים, חיבור התקנים וכלי תקשורת, מהגדרת הדרישות ועד הפיתוח.
תחזוקה ומודרניזציה של תוכנת Windows
הרחבות, תחזוקה ומודרניזציה הדרגתית של תוכנת Windows קיימת.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- מה זה Native AOT?
- שיטת פרסום שמקמפלת מראש אפליקציית .NET לקוד נייטיבי בזמן ה-publish, ומפיצה אותה כך. מכיוון שלא נעשה שימוש ב-JIT בזמן ריצה, זמן ההפעלה ותפוסת הזיכרון נוטים להשתפר, וקל יותר להפיץ גם לסביבות שבהן אין התקנה מראש של סביבת ריצה של .NET. בתמורה, ההתאמה נחלשת עם רפלקציה חופשית, יצירת קוד דינמית, built-in COM וספריות שלא תומכות ב-trimming. זו לא קסם שמאיץ הכול, אלא מודל פרסום שמוותר במקצת על העולם הדינמי ונוטה לעבר עולם סטטי, לטובת צרכי ההפעלה, ההפצה וסביבת הריצה.
- מה ההבדל בין Native AOT ל-ReadyToRun?
- ReadyToRun הוא שיטה שמשאירה את ה-IL ומקדימה במקצת חלק מעבודת ה-JIT, ועדיין יש מצבים שבהם נעשה שימוש ב-JIT בזמן ריצה. ההתאמה רחבה, וגם היכולות הדינמיות נשארות בדרך כלל נוחות לשימוש. לעומת זאת, Native AOT לא מניח מלכתחילה JIT בזמן ריצה, החומר המופץ מתמקד בקובץ הרצה נייטיבי, וההפעלה נוטה להשתפר משמעותית בתמורה למגבלות חזקות יותר. גם אם שתיהן נושאות את המילה AOT, ReadyToRun נוטה לכיוון 'להקל קצת על ה-JIT', ואילו Native AOT נוטה לכיוון 'לא להניח JIT מלכתחילה' — ההבדל בטמפרטורה משמעותי.
- האם אפשר להשתמש ב-Native AOT באפליקציות WPF או WinForms?
- בשלב הזה מומלץ להסתכל על זה בזהירות רבה. ל-Native AOT של Windows אין built-in COM, WPF לא מתיישב טוב עם trimming, ו-WinForms תלוי חזק ב-built-in COM marshalling, ולכן שתיהן לא מתאימות כמועמד ראשון ל-Native AOT. אם דרוש COM, יש מקרים שבהם עדיף להישאר עם JIT, או לתכנן מחדש סביב ComWrappers / source-generated COM. כנקודת כניסה, קונסולה, worker או Web API קטן טבעיים יותר.
- לאילו אפליקציות Native AOT מתאים?
- מתאים לכלי CLI שההפעלה שלהם היא הכוכב, ל-API קטנים שמפיצים בכמות גדולה בקונטיינרים, ל-worker או שירות רקע, לתהליכים חסרי שרת (serverless) או קצרי-חיים, לרכיב .NET קטן שמוחדר לתוך אפליקציה נייטיבית, ולמצב שבו לא רוצים לדרוש התקנה מראש של סביבת ריצה של .NET בסביבת ההרצה. המשותף הוא שהגבול ברור יחסית וקל להקטין את המנגנונים הדינמיים. ולהפך, זה לא מתאים לאפליקציה שהכוכב שלה הוא טעינת plugin בזמן ריצה, או למבנה עם תלות חזקה במסגרת עבודה שמחפשת טיפוסים דרך רפלקציה.