عنوان:

‫کاهش تعداد بار تعریف using ها در C# 10.0 و NET 6.0.


نویسنده: وحید نصیری
تاریخ: ۱۴۰۰/۰۷/۲۷ ۱۱:۳۰
آدرس: www.dntips.ir
در مطلب «روش بازگشت به قالب‌های کلاسیک پروژه‌ها در دات نت 6» مشاهده کردیم که قالب پیش‌فرض یک برنامه‌ی کنسول دات نت 6، چنین فایل Program.cs ای را تولید می‌کند:
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
که در حقیقت همان اجبار به استفاده‌ی از سبک «Top Level Programs» ارائه شده‌ی در C# 9.0 است. اما اگر به همین دو سطر هم دقت کنید، یک تفاوت مهم را با نمونه‌ی C# 9.0 دارد و آن هم عدم ذکر عبارت using System در ابتدای آن است. علت اینجا است که فایل csproj پیش‌فرض پروژه‌های مبتنی بر NET 6.0.، دو تغییر مهم دیگر را هم دارند:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>
الف) فعال بودن nullable reference types که در C# 8.0 ارائه شد.
ب) فعال بودن ImplicitUsings که مختص به C# 10.0 است.


بررسی مفهوم  global using directives در C# 10.0

هدف اصلی از وجود Using directives در زبان #C که از نگارش 1 آن در دسترس هستند، خلاصه نویسی نام طولانی اشیاء و متدها است. برای مثال نام اصلی متد Console.WriteLine به صورت System.Console.WriteLine است که با درج فضای نام System در ابتدای فایل، می‌توان از ذکر مجدد آن جلوگیری کرد. از این دست می‌توان به نوع System.Collections.Generic.List نیز اشاره کرد که کمتر کسی علاقمند است تا این نام طولانی را تایپ کند. به همین جهت با استفاده از یک using directive متناظر با فضای نام System.Collections.Generic، ذکر نام این نوع، به List خلاصه می‌شود.
طراحی دات نت 6 مبتنی بر سبک minimalism است! برای نمونه خلاصه کردن نزدیک به 10 سطر فایل Program.cs کلاسیک، به تنها یک سطر که به همراه ذکر using System در ابتدای آن هم نیست. در C# 10.0 دیگر نیازی نیست تا برای مثال ذکر using System را در ده‌ها و یا صدها فایل، بارها و بارها تکرار کرد. برای اینکار تنها کافی است یکبار آن‌را به صورت global تعریف کنیم و پس از آن دیگر نیازی به ذکر آن در کل پروژه نیست:
global using System;
می‌توان این سطر را در ابتدای یک تک فایل cs. قرار داد و ذکر آن به معنای الحاق خودکار آن، در ابتدای تک تک فایل‌های cs. برنامه است.

چند نکته:
- امکان ترکیب global using‌ها و using‌ها معمولی در یک فایل هست.
- امکان تعریف global using‌های استاتیک نیز پیش‌بینی شده‌است:
global using static System.Console;
که برای نمونه در این حالت بجای ذکر Console.WriteLine، تنها ذکر نام متد WriteLine در سراسر برنامه کفایت می‌کند.


مفهوم جدید implicit global using directives در C# 10.0 و به کمک NET SDK 6.0.

تا اینجا دریافتیم که می‌توان دایرکتیوهای سراسری using را در برنامه به صورت دستی تعریف و استفاده کرد. اما ... پروژه‌ی کنسولی که به صورت پیش‌فرض توسط NET SDK 6.0. ایجاد می‌شود، به همراه هیچ global using ای نیست. این مورد توسط تنظیم زیر که جزئی از NET SDK 6.0. است، فعال می‌شود:
<ImplicitUsings>enable</ImplicitUsings>
زمانیکه ImplicitUsings را در فایل csproj برنامه فعال می‌کنیم، یعنی قرار است از یکسری global using‌های از پیش تعریف شده‌ی توسط SDK استفاده کنیم. بنابراین «global using directives» جزئی از ویژگی‌های جدید C# 10.0 است اما « implicit global using directives» تنها یک لطف ارائه شده‌ی توسط NET SDK. است. برای یافتن لیست آن‌ها، پروژه را build کرده و سپس به پوشه‌ی obj\Debug\net6.0 مراجعه کنید. در اینجا به دنبال فایلی مانند MyProjectName. GlobalUsings.g.cs بگردید. محتویات آن به صورت زیر است:
// <auto-generated/>
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Threading;
global using global::System.Threading.Tasks;
این‌ها همان global using هایی هستند که با فعالسازی تنظیم ImplicitUsings در فایل csproj، به صورت خودکار توسط NET SDK. تولید و به برنامه الحاق می‌شوند.
البته این فایل ویژه به ازای نوع‌های پروژه‌های مختلف، محتوای متفاوتی را دارد. برای مثال در برنامه‌های ASP.NET Core، چنین محتوای پیش‌فرضی را پیدا می‌کند:
// <autogenerated />
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Threading;
global using global::System.Threading.Tasks;
global using global::System.Net.Http.Json;
global using global::Microsoft.AspNetCore.Builder;
global using global::Microsoft.AspNetCore.Hosting;
global using global::Microsoft.AspNetCore.Http;
global using global::Microsoft.AspNetCore.Routing;
global using global::Microsoft.Extensions.Configuration;
global using global::Microsoft.Extensions.DependencyInjection;
global using global::Microsoft.Extensions.Hosting;
global using global::Microsoft.Extensions.Logging;
این تعاریف در اصل در پوشه‌ی C:\Program Files\dotnet\sdk\6.0.100-rc.2.21505.57\Sdks\Microsoft.NET.Sdk\targets و در فایل Microsoft.NET.GenerateGlobalUsings.targets آن قرار دارند.


روش حذف و یا اضافه‌ی global using‌های پیش‌فرض

اگر به هر دلیلی نمی‌خواهید تعدادی از global usingهای پیش‌فرض به همراه گزینه‌ی ImplicitUsings استفاده کنید، می‌توانید آن‌ها را در فایل csproj به صورت زیر، Remove و یا حتی موارد جدیدی را Include کنید:
<ItemGroup>
   <Import Remove="System.Threading" />
   <Import Include="Microsoft.Extensions.Logging" />
</ItemGroup>
یکی از کاربردهای این قابلیت، تولید کتابخانه‌های multi-target است که می‌توان توسط Conditionها، فضاهای نامی را که نباید برای target خاصی include کرد، مشخص نمود:
<ItemGroup Condition="'$(TargetFramework)' == 'net472'">
</ItemGroup>

نظرات

  • وحید نصیری در ۱۴۰۰/۰۸/۰۲ ۱۰:۶
    یک نکته‌ی تکمیلی: امکان تعریف Alias
    در اینجا نیز امکان تعریف یک Alias برای فضای نام خاصی وجود دارد:
    global using DT = System.DateTime;
    و همچنین تمام این موارد (حالت global معمولی، استاتیک و یا به همراه Alias) را:
    global using App.Models;
    global using static System.Console;
    global using DT = System.DateTime;
    می‌توان در فایل csproj. هم تعریف کرد:
    <ItemGroup>
        <Using Include="App.Models"/>
        <Using Include="System.Console" Static="True" />
        <Using Include="System.DateTime" Alias="DT" />
    </ItemGroup>
    • هادی مزارعی در ۱۴۰۴/۰۵/۰۵ ۱۱:۱۴
      فایل Directory.Build.props رو در ریشه Solution ایجاد کردم تا نیاز تمام پروژه‌ها در Solution رو یکجا حل کنم. در حال حاضر هر پروژه بدلیل ارجاعات مختلفی که به سایر پروژه‌ها دارند مجبورم در هر پروژه یک GlobalUsing.cs ایجاد کنم که برام دردسر داره. اگر یک فایل جامع رو در همه پروژه‌ها کپی کنم بدلیل عدم ارجاع بعضی پروژه‌ها به اسمبلی‌ها باید خطوط اضافه را حذف کنم و...

      آنچه که متوجه شدم راه حل نهایی در ایجاد فایل Directory.Build.props در ریشه Solution هستش. فقط آنچه که متوجه شدم امکان استفاده از Alias وجود نداره ضمن آنکه این راه کار هم به عدم وجود Reference در پروژه‌ها اشاره میکنه و عملیات Built متوقف میشه. آیا راه حلی برای این مشکل وجود داره یا منطق کاری که میکنم اشتباهه؟

      • وحید نصیری در ۱۴۰۴/۰۵/۰۵ ۱۱:۲۲
        • امکان تعریف global usings داخل فایل Directory.Build.props هم وجود دارد؛ یک مثال.
        • امکان تعریف alias در این حالت:
        <ItemGroup>
            <Using Include="System.DateTime" Alias="DT" />
        </ItemGroup>
        • امکان تعریف شرطی یکسری از موارد:
        <ItemGroup Condition="'$(TargetFramework)' == 'net472'">
        • هادی مزارعی در ۱۴۰۴/۰۵/۰۵ ۱۱:۳۸
          کدهای زیر بخشی از فایل Directory.Build.props هستش:
          <Project>
          	<ItemGroup Label="Exceptions">
          		<Using Include="Ces.Caspian.Constants.Constants.Application.MapTargets" Alias="conMapTargets" />
          	</ItemGroup>
          
          	<ItemGroup Label="OperationResult">
          		<Using Include="Ces.Caspian.Models.OperationResult" Alias="oResult" />
          	</ItemGroup>
          
          	<ItemGroup Label="Constants">
          		<!-- Application -->
          		<Using Include="Ces.Caspian.Constants.Constants.Application.MapTargets" Alias="conMapTargets" />
          		<Using Include="Ces.Caspian.Constants.Constants.Application.Profiles" Alias="conProfiles" />
          		<Using Include="Ces.Caspian.Constants.Constants.Application.Settings" Alias="conSettings" />
          
          		<!-- Piping -->
          		<Using Include="Ces.Caspian.Constants.Constants.Piping.AGUGs" Alias="conAGUGs" />
          		<Using Include="Ces.Caspian.Constants.Constants.Piping.ConnectionTypes" Alias="conConnectionTypes" />
          		<Using Include="Ces.Caspian.Constants.Constants.Piping.EndStatuses" Alias="conEndStatuses" />
          		<Using Include="Ces.Caspian.Constants.Constants.Piping.EndTypes" Alias="conEndTypes" />
          
          		<!-- Project -->
          		<Using Include="Ces.Caspian.Constants.Constants.Project.Disciplines" Alias="conDisciplines" />
          
          		<!-- TestPackage -->
          		<Using Include="Ces.Caspian.Constants.Constants.TestPackage.TestPackageActions" Alias="conTestPackageActions" />
          		<Using Include="Ces.Caspian.Constants.Constants.TestPackage.TestPackageActivities" Alias="conTestPackageActivities" />
          
          	</ItemGroup>
          </Project>

          در زمان Built پروژه Abstraction بدلیل آنکه هیچ ارجاعی به سایر پروژه‌ها نداره فرآیند Built با خطا مواجه میشه. در واقع میخوام فایل Global Using همه پروژه‌ها را پوشش بده. این مورد امکان پذیره؟

          // <auto-generated/>
          global using global::System;
          global using global::System.Collections.Generic;
          global using global::System.IO;
          global using global::System.Linq;
          global using global::System.Net.Http;
          global using global::System.Threading;
          global using global::System.Threading.Tasks;
          global using acActionMethods = global::Ces.Caspian.Application.Handlers.Application.ActionMethods.Commands; //خطا
          global using acActivities = global::Ces.Caspian.Application.Handlers.TestPackage.Activities.Commands; //خطا
          global using acAfterTests = global::Ces.Caspian.Application.Handlers.Piping.AfterTests.Commands; //خطا
          global using acAGUGs = global::Ces.Caspian.Application.Handlers.Piping.AGUGs.Commands; //خطا
          global using acAreas = global::Ces.Caspian.Application.Handlers.Project.Areas.Commands; //خطا
          • وحید نصیری در ۱۴۰۴/۰۵/۰۵ ۱۱:۵۴
            بله؛ می‌توان این مشکل را با استفاده از شرط‌های MSBuild حل کرد. فایل Directory.Build.props به تمام پروژه‌ها اعمال می‌شود. وقتی شما یک Using سراسری برای یک فضای‌نام (namespace) خاص تعریف می‌کنید، کامپایلر تلاش می‌کند آن را در همه‌ی پروژه‌ها اعمال کند. اگر پروژه‌ای (مانند پروژه Abstraction شما) ارجاعی (ProjectReference) به اسمبلی حاوی آن فضای‌نام نداشته باشد، با خطای "The type or namespace name ... could not be found" مواجه می‌شوید. این رفتار کاملاً طبیعی است. راه حل این است که ItemGroup های حاوی دستورات Using را مشروط کنید.

            فرض کنیم که فضاهای نام شما در پروژه‌های زیر قرار دارند:
            • Ces.Caspian.Application.* در پروژه Ces.Caspian.Application.csproj
            • Ces.Caspian.Constants.* در پروژه Ces.Caspian.Constants.csproj
            • Ces.Caspian.Models.* در پروژه Ces.Caspian.Models.csproj

            فایل Directory.Build.props شما می‌تواند به شکل زیر بازنویسی شود:
            <Project>
            
                <ItemGroup Condition="'@(ProjectReference->WithMetadataValue('Filename', 'Ces.Caspian.Constants')->Count())' > '0'">
                    <Using Include="Ces.Caspian.Constants.Constants.Application.MapTargets" Alias="conMapTargets" />
                    <Using Include="Ces.Caspian.Constants.Constants.Application.Profiles" Alias="conProfiles" />
                    <Using Include="Ces.Caspian.Constants.Constants.Application.Settings" Alias="conSettings" />
                    <Using Include="Ces.Caspian.Constants.Constants.Piping.AGUGs" Alias="conAGUGs" />
                    <Using Include="Ces.Caspian.Constants.Constants.Piping.ConnectionTypes" Alias="conConnectionTypes" />
                    <Using Include="Ces.Caspian.Constants.Constants.Piping.EndStatuses" Alias="conEndStatuses" />
                    <Using Include="Ces.Caspian.Constants.Constants.Piping.EndTypes" Alias="conEndTypes" />
                    <Using Include="Ces.Caspian.Constants.Constants.Project.Disciplines" Alias="conDisciplines" />
                    <Using Include="Ces.Caspian.Constants.Constants.TestPackage.TestPackageActions" Alias="conTestPackageActions" />
                    <Using Include="Ces.Caspian.Constants.Constants.TestPackage.TestPackageActivities" Alias="conTestPackageActivities" />
                </ItemGroup>
            
                <ItemGroup Condition="'@(ProjectReference->WithMetadataValue('Filename', 'Ces.Caspian.Models')->Count())' > '0'">
                    <Using Include="Ces.Caspian.Models.OperationResult" Alias="oResult" />
                </ItemGroup>
            
                <ItemGroup Condition="'@(ProjectReference->WithMetadataValue('Filename', 'Ces.Caspian.Application')->Count())' > '0'">
                    <Using Include="Ces.Caspian.Application.Handlers.Application.ActionMethods.Commands" Alias="acActionMethods" />
                    <Using Include="Ces.Caspian.Application.Handlers.TestPackage.Activities.Commands" Alias="acActivities" />
                    <Using Include="Ces.Caspian.Application.Handlers.Piping.AfterTests.Commands" Alias="acAfterTests" />
                    <Using Include="Ces.Caspian.Application.Handlers.Piping.AGUGs.Commands" Alias="acAGUGs" />
                    <Using Include="Ces.Caspian.Application.Handlers.Project.Areas.Commands" Alias="acAreas" />
                </ItemGroup>
            
            </Project>

            توضیحات:

            • Condition="...": این یک ویژگی استاندارد در MSBuild است که به شما اجازه می‌دهد یک ItemGroup (یا هر عنصر دیگری) را فقط در صورت برقراری یک شرط خاص، پردازش کنید.
            • '@(ProjectReference...)': این دستور لیست تمام ارجاعات پروژه را در فایل .csproj پروژه فعلی انتخاب می‌کند.
            • ->WithMetadataValue('Filename', 'Ces.Caspian.Constants'): این بخش، لیست ProjectReference ها را فیلتر می‌کند و فقط آنهایی را نگه می‌دارد که نام فایل‌شان (بدون پسوند) برابر با Ces.Caspian.Constants باشد.
            • ->Count(): تعداد آیتم‌های باقی‌مانده در لیست فیلتر شده را می‌شمارد.
            • > '0': شرط نهایی بررسی می‌کند که آیا تعداد ارجاعات پیدا شده بزرگتر از صفر است یا خیر. اگر چنین باشد، شرط برقرار است و ItemGroup مربوطه به پروژه اضافه می‌شود.
            با این روش، هر پروژه فقط Using های سراسری مربوط به وابستگی‌های خودش را دریافت می‌کند و مشکل بیلد در پروژه‌هایی مانند Abstraction که وابستگی‌های کمتری دارند، به طور کامل حل می‌شود. در ضمن، قابلیت استفاده از Alias همچنان پابرجاست و مشکلی از این بابت وجود ندارد.
            • هادی مزارعی در ۱۴۰۴/۰۵/۰۵ ۲۰:۴۷
              ضمن تشکر، با توجه به شرایط پروژه و جستجو متوجه شدم که از عملگرهای OR , AND برای افزودن پارامتر به condition و عملگر != برای بررسی استفاده کرد. همچنین استفاده از condition در تگ‌های Include هم امکانپذیر است:
              <ItemGroup Label="Constants" Condition="
              			   '$(MSBuildProjectName)' == 'Ces.Caspian.Api'
              			   OR '$(MSBuildProjectName)' == 'Ces.Caspian.Infrastructure'
              			   OR '$(MSBuildProjectName)' == 'Ces.Caspian.Domain'
              			   OR '$(MSBuildProjectName)' == 'Ces.Caspian.Models'
              			   OR '$(MSBuildProjectName)' == 'Ces.Caspian.WinClient'
              			   ">
  • وحید نصیری در ۱۴۰۱/۰۱/۱۴ ۱۰:۵۸
    یک نکته‌ی تکمیلی: ترفندی برای معرفی Stubs توسط ویژگی global using statements در unit tests

    برای نمونه متد زیر را درنظر بگیرید:
    public static string CurrentInvariantMonthName()
        {
            var month = DateTimeOffset.UtcNow.Month;
            return CultureInfo
                .InvariantCulture
                .DateTimeFormat
                .GetMonthName(month);
        }
    در کل نوشتن آزمون واحد برای متدهایی که با زمان و خواصی مانند UtcNow و یا Now، کار می‌کنند، مشکل است. در آزمون‌های واحد نیاز داریم تا یک خروجی مشخص را با مقداری از پیش معلوم، مقایسه کنیم تا اطمینان حاصل شود که عملیات صورت گرفته، صحیح است. اما UtcNow هر بار متغیر است.
    برای حل این مشکل، با استفاده از ویژگی global using statements و compiler directives، می‌توان دو مفهوم متفاوت را برای زمان ارائه داد:
    #if MOCK_TIME
    global using DateTimeOffset = StubDateTimeOffset; 
    #else
    global using DateTimeOffset = System.DateTimeOffset;
    #endif
    
    public static class StubDateTimeOffset
    {
        private static System.DateTimeOffset? value;
        
        public static System.DateTimeOffset UtcNow 
            => value ?? System.DateTimeOffset.UtcNow ;
        
        public static void Set(System.DateTimeOffset dateTimeOffset) {
            value = dateTimeOffset;
        }
    
        public static void Reset() => value = null;
    }
    در اینجا یک فایل خالی cs. ایجاد شده و قطعه کد فوق در آن درج می‌شود. در این حالت اگر توسط تنظیمات کامپایلر در فایل csproj برنامه، MOCK_TIME فعال شود، مفهوم DateTimeOffset، دیگر همان System.DateTimeOffset استاندارد نخواهد بود؛ بلکه از یک کلاس بدلی به نام StubDateTimeOffset تامین می‌شود. برای فعالسازی MOCK_TIME هم می‌توان به صورت زیر عمل کرد که این تعریف، در فایل csproj پروژه‌ی آزمایشی قرار می‌گیرد:
    <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
       <DefineConstants>MOCK_TIME</DefineConstants>
    </PropertyGroup>
    پس از این تغییر، اینبار کلاس StubDateTimeOffset، تامین کننده‌ی DateTimeOffset در آزمون‌های واحد خواهد بود و در این آزمون‌ها می‌توان با مقدار دهی DateTimeOffset به صورت زیر، هربار آزمایشات را بر اساس یک UtcNow «مشخص» انجام داد:
    // set time
    StubDateTimeOffset.Set(new(new(2022, 7, 1)));
  • رضا تنها در ۱۴۰۱/۰۴/۰۶ ۱۰:۴۹
    سلام؛ وقتی از امکان ساخت قالب برای پروژه‌های NET Core. برای پروژه DNTIdentity  استفاده میکنیم فایلهای MyProjectName. GlobalUsings.g.cs به پروژه جدید انتقال پیدا نمی‌کنند و خطلاهای فراوانی به علت نبود using‌ها ایجاد می‌شود.وقتی دستی هم فایل MyProjectName. GlobalUsings.g.cs را به پوشه obj\Debug\net6.0اضافه می‌کنم مشکل برطرف نمی‌شود. آیا راهکاری برای این مشکل وجود دارد؟ 
    • وحید نصیری در ۱۴۰۱/۰۴/۰۶ ۱۱:۲۶
      global usings داخل فایل Directory.Build.props تعریف شده‌اند. همین فایل حضور داشته باشد، کافی است.
      • رضا تنها در ۱۴۰۱/۰۴/۰۶ ۱۱:۵۱
        بله با اضافه شدن فایل  Directory.Build.props به پوشه اصلی پروژه، مشکل برطرف شد. با تشکر.
  • وحید نصیری در ۱۴۰۲/۰۲/۳۱ ۷:۲۷