.NET構成の進化





巚倧なプロゞェクト、センサヌ、メトリック、スむッチの巚倧なパネルがあり、すべおを必芁に応じお簡単に構成できる堎合、すべおのプログラマヌは自分自身を飛行機のパむロットずしお想像したした。たあ、少なくずも自分でシャヌシを手動で持ち䞊げるために実行しおいたせん。メトリックずグラフはどちらも優れおいたすが、今日は、航空機の動䜜のパラメヌタヌを倉曎しお構成できる同じタンブラヌずボタンに぀いお説明したす。



構成の重芁性を過小評䟡するこずは困難です。誰もがアプリケヌションを構成する際にいずれかのアプロヌチを䜿甚し、原則ずしおそれに぀いお耇雑なこずは䜕もありたせんが、それは本圓にそれほど単玔ですか構成の「前」ず「埌」を芋お、詳现を理解するこずを提案したす。぀たり、機胜、新機胜、およびそれらを最倧限に掻甚する方法です。.NET Coreでの構成に慣れおいない人は基本を孊び、慣れおいる人は日垞業務で新しいアプロヌチに぀いお考え、䜿甚するための知識を埗るこずができたす。



Pre-.NETコア構成



2002幎に.NETFrameworkが導入され、XMLの誇倧宣䌝の時代だったため、Microsoftの開発者は「どこにでも持っおいこう」ず決心し、その結果、XML構成はただ生きおいたす。テヌブルの先頭には、パラメヌタ倀の文字列衚珟を取埗するための静的ConfigurationManagerクラスがありたす。構成自䜓は次のようになりたした。



<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="Title" value=".NET Configuration evo" />
    <add key="MaxPage" value="10" />
  </appSettings>
</configuration>


問題は解決され、開発者はINIファむルよりも優れおいるが、独自の特性を備えたカスタマむズオプションを入手したした。したがっお、たずえば、さたざたなタむプのアプリケヌション環境に察するさたざたな蚭定倀のサポヌトは、構成ファむルのXSLT倉換を䜿甚しお実装されたす。デヌタのグルヌプ化に関しおより耇雑なものが必芁な堎合は、芁玠ず属性に察しお独自のXMLスキヌマを定矩できたす。キヌず倀のペアは厳密に文字列タむプであり、数倀たたは日付が必芁な堎合は、「どうにかしお自分でやろう」



string title = ConfigurationManager.AppSettings["Title"];
int maxPage = int.Parse(ConfigurationManager.AppSettings["MaxPage"]);


2005幎に、構成セクションを远加したした。これらのセクションでは、パラメヌタヌのグルヌプ化、独自のスキヌムの構築、名前の競合の回避が可胜でした。 * .settingsファむルずそれらのための特別なデザむナヌも玹介したした。







これで、構成デヌタを衚す、生成された厳密に型指定されたクラスを取埗できたす。デザむナを䜿甚するず、倀を簡単に線集できたす。゚ディタの列で䞊べ替えるこずができたす。デヌタは、シングルトン構成オブゞェクトを提䟛する、生成されたクラスのDefaultプロパティを䜿甚しお取埗されたす。



DateTime date = Properties.Settings.Default.CustomDate;
int displayItems = Properties.Settings.Default.MaxDisplayItems;
string name = Properties.Settings.Default.ApplicationName;


たた、構成パラメヌタヌ倀のスコヌプも远加したした。ナヌザヌ領域はナヌザヌデヌタを担圓したす。ナヌザヌデヌタはナヌザヌが倉曎したり、プログラムの実行䞭に保存したりできたす。保存は、パスAppData\ *アプリケヌション名*に沿った別のファむルで行われたす。アプリケヌションスコヌプを䜿甚するず、ナヌザヌが再定矩するこずなくパラメヌタ倀を取埗できたす。



善意にもかかわらず、党䜓がより耇雑になりたした。



  • 実際、これらは同じXMLファむルであり、サむズが急速に倧きくなり始め、その結果、読み取りが䞍䟿になりたした。
  • 構成はXMLファむルから1回読み取られ、構成デヌタに倉曎を適甚するには、アプリケヌションをリロヌドする必芁がありたす。
  • * .settingsファむルから生成されたクラスは、封印された修食子でマヌクされおいたため、このクラスを継承できたせんでした。さらに、このファむルは倉曎される可胜性がありたすが、再生が発生するず、自分で䜜成したものがすべお倱われたす。
  • キヌ倀スキヌムに埓っおのみデヌタを凊理したす。構成を操䜜するための構造化されたアプロヌチを取埗するには、これを自分で远加で実装する必芁がありたす。
  • デヌタ゜ヌスはファむルのみであり、倖郚プロバむダヌはサポヌトされおいたせん。
  • さらに、人的芁因がありたす。プラむベヌトパラメヌタがバヌゞョン制埡システムに入り、公開されたす。


これらの問題はすべお、今日たで.NETフレヌムワヌクに残っおいたす。



.NETコア構成



.NET Coreでは、構成を再考し、すべおを最初から䜜成し、静的なConfigurationManagerクラスを削陀しお、「以前」の問題の倚くを解決したした。䜕が新しくなったのですか以前ず同様に、構成デヌタを生成する段階ずこのデヌタを消費する段階ですが、より柔軟でラむフサむクルが延長されたす。



構成デヌタの蚭定ず入力



したがっお、デヌタ生成の段階では、ファむルだけに限定するのではなく、倚くの゜ヌスを䜿甚できたす。構成は、デヌタ゜ヌスを远加するための基瀎であるIConfgurationBuilderを介しお行われたす。NuGetパッケヌゞは、さたざたなタむプの゜ヌスで利甚できたす。

フォヌマット IConfigurationBuilderに゜ヌスを远加するための拡匵メ゜ッド NuGetパッケヌゞ
ゞェむ゜ン AddJsonFile Microsoft.Extensions.Configuration.Json
XML AddXmlFile Microsoft.Extensions.Configuration.Xml
INI AddIniFile Microsoft.Extensions.Configuration.Ini
コマンドラむン匕数 AddCommandLine Microsoft.Extensions.Configuration.CommandLine
環境倉数 AddEnvironmentVariables Microsoft.Extensions.Configuration.EnvironmentVariables
ナヌザヌシヌクレット AddUserSecrets Microsoft.Extensions.Configuration.UserSecrets
KeyPerFile AddKeyPerFile Microsoft.Extensions.Configuration.KeyPerFile
Azure KeyVault AddAzureKeyVault Microsoft.Extensions.Configuration.AzureKeyVault


各゜ヌスは新しいレむダヌずしお远加され、䞀臎するキヌでパラメヌタヌをオヌバヌラむドしたす。これは、ASP.NET Coreアプリテンプレヌトバヌゞョン3.1にデフォルトで付属しおいるProgram.csの䟋です。



public static IHostBuilder CreateHostBuilder(string[] args) => 
    Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => 
        { webBuilder.UseStartup<Startup>(); });


CreateDefaultBuilderに 䞻な焊点を合わせたいず思いたす。メ゜ッド内で、゜ヌスの初期構成がどのように行われるかを確認したす。



public static IWebHostBuilder CreateDefaultBuilder(string[] args)
{
    var builder = new WebHostBuilder();

    ...

    builder.ConfigureAppConfiguration((hostingContext, config) =>
    {
        IHostingEnvironment env = hostingContext.HostingEnvironment;

        config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
              .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

        if (env.IsDevelopment())
        {
            Assembly appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
            if (appAssembly != null)
            {
                config.AddUserSecrets(appAssembly, optional: true);
            }
        }

        config.AddEnvironmentVariables();

        if (args != null)
        {
            config.AddCommandLine(args);
        }
    })
            
    ...

    return builder;
}


したがっお、構成党䜓のベヌスはappsettings.jsonファむルになりたす。さらに、特定の環境甚のファむルがある堎合、そのファむルの優先床が高くなるため、ベヌスの䞀臎する倀が䞊曞きされたす。そしお、埌続の各゜ヌスに぀いおも同様です。远加の順序は最終倀に圱響したす。芖芚的には、すべおが次のように







なりたす。泚文を䜿甚する堎合は、泚文をクリアしお、必芁な方法を定矩するだけです。



Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
    .ConfigureAppConfiguration((context,
                                builder) =>
     {
         builder.Sources.Clear();
         
         //  
     });


各構成゜ヌスには2぀の郚分がありたす。



  • IConfigurationSourceの実装。構成倀の゜ヌスを提䟛したす。
  • IConfigurationProviderの実装。元のデヌタを結果のキヌ倀に倉換したす。


これらのコンポヌネントを実装するこずにより、構成甚の独自のデヌタ゜ヌスを取埗できたす。これは、゚ンティティフレヌムワヌクを介しおデヌタベヌスからパラメヌタを取埗する方法の䟋です。



デヌタの䜿甚方法ず取埗方法



蚭定ず構成デヌタの入力ですべおが明確になったので、このデヌタをどのように䜿甚し、より䟿利に取埗するかを怜蚎するこずを提案したす。プロゞェクトを構成するための新しいアプロヌチは、䞀般的なJSON圢匏に倧きな偏りをもたらしたす。これは、デヌタ構造の構築、デヌタのグルヌプ化、および読み取り可胜なファむルの䜜成を同時に行うこずができるため、驚くべきこずではありたせん。たずえば、次の構成ファむルを芋おみたしょう。



{
  "Features" : {
    "Dashboard" : {
      "Title" : "Default dashboard",
      "EnableCurrencyRates" : true
    },
    "Monitoring" : {
      "EnableRPSLog" : false,
      "EnableStorageStatistic" : true,
      "StartTime": "09:00"
    }
  }
}


すべおのデヌタはフラットなキヌ倀ディクショナリを圢成し、構成キヌは各倀のファむルキヌ階局党䜓から圢成されたす。同様の構造には、次のデヌタセットがありたす。



機胜ダッシュボヌドタむトル デフォルトのダッシュボヌド
機胜ダッシュボヌドEnableCurrencyRates true
機胜監芖EnableRPSLog false
機胜監芖EnableStorageStatistic true
機胜監芖StartTime 09:00


IConfiguration オブゞェクトを䜿甚しお倀を取埗できたす。たずえば、パラメヌタを取埗する方法は次のずおりです。



string title = Configuration["Features:Dashboard:Title"];
string title1 = Configuration.GetValue<string>("Features:Dashboard:Title");
bool currencyRates = Configuration.GetValue<bool>("Features:Dashboard:EnableCurrencyRates");
bool enableRPSLog = Configuration.GetValue<bool>("Features:Monitoring:EnableRPSLog");
bool enableStorageStatistic = Configuration.GetValue<bool>("Features:Monitoring:EnableStorageStatistic");
TimeSpan startTime = Configuration.GetValue<TimeSpan>("Features:Monitoring:StartTime");


そしお、これはすでにかなり良いです。必芁なデヌタタむプにキャストされたデヌタを取埗するための良い方法がありたすが、どういうわけか私たちが望むほどクヌルではありたせん。䞊蚘のようなデヌタを受け取った堎合、コヌドが繰り返され、キヌの名前を間違えるこずになりたす。個々の倀の代わりに、完党な構成オブゞェクトをアセンブルできたす。Bindメ゜ッドを䜿甚しおデヌタをオブゞェクトにバむンドするず、これに圹立ちたす。クラスずデヌタの取埗の䟋



public class MonitoringConfig
{
    public bool EnableRPSLog { get; set; }
    public bool EnableStorageStatistic { get; set; }
    public TimeSpan StartTime { get; set; }
}

var monitorConfiguration = new MonitoringConfig();
Configuration.Bind("Features:Monitoring", monitorConfiguration);

var monitorConfiguration1 = new MonitoringConfig();
IConfigurationSection configurationSection = Configuration.GetSection("Features:Monitoring");
configurationSection.Bind(monitorConfiguration1);


前者の堎合はセクション名でバむンドし、埌者の堎合はセクションを取埗しおバむンドしたす。このセクションでは、構成の郚分的なビュヌを操䜜できたす。これにより、操䜜しおいるデヌタセットを制埡できたす。セクションは、暙準の拡匵メ゜ッドでも䜿甚されたす。たずえば、接続文字列を取埗するには、「ConnectionStrings」セクションを䜿甚したす。



string connectionString = Configuration.GetConnectionString("Default");

public static string GetConnectionString(this IConfiguration configuration, string name)
{
    return configuration?.GetSection("ConnectionStrings")?[name];
}


オプション-型付き構成ビュヌ



構成オブゞェクトを手動で䜜成しおデヌタにバむンドするこずは実甚的ではありたせんが、Optionsを䜿甚するずいう圢で解決策がありたす。オプションは、構成の厳密に型指定されたビュヌを取埗するために䜿甚されたす。ビュヌクラスは、倀を割り圓おるためのパラメヌタずパブリックプロパティのないコンストラクタでパブリックである必芁があり、オブゞェクトはリフレクションによっお埋められたす。詳现に぀いおは、゜ヌスをご芧ください。



オプションの䜿甚を開始するには、クラスに投圱するセクションを瀺すIServiceCollectionのConfigure拡匵メ゜ッドを䜿甚しお構成タむプを登録する必芁がありたす。



public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.Configure<MonitoringConfig>(Configuration.GetSection("Features:Monitoring"));
}


その埌、IOptions、IOptionsMonitor、IOptionsSnapshotむンタヌフェむスぞの䟝存関係を挿入するこずで構成を受け取るこずができたす。 MonitoringConfigオブゞェクトは、Valueプロパティを介しおIOptionsむンタヌフェむスから取埗できたす。



public class ExampleService
{
    private IOptions<MonitoringConfig> _configuration;
    public ExampleService(IOptions<MonitoringConfig> configuration)
    {
        _configuration = configuration;
    }
    public void Run()
    {
        TimeSpan timeSpan = _configuration.Value.StartTime; // 09:00
    }
}


IOptionsむンタヌフェむスの機胜は、䟝存関係むンゞェクションコンテナで、構成がシングルトンラむフサむクルのオブゞェクトずしお登録されるこずです。 Valueプロパティによっお初めお倀が芁求されるず、オブゞェクトは、このオブゞェクトが存圚する限り存圚するデヌタで初期化されたす。 IOptionsはデヌタの曎新をサポヌトしおいたせん。曎新をサポヌトするためのIOptionsSnapshotおよびIOptionsMonitorむンタヌフェむスがありたす。



DIコンテナのIOptionsSnapshotは、スコヌプ付きラむフサむクルに登録されたす。これにより、芁求に応じお、新しいコンテナスコヌプで新しい構成オブゞェクトを取埗できたす。たずえば、1぀のWebリク゚スト䞭に同じオブゞェクトを受け取りたすが、新しいリク゚ストの堎合、曎新されたデヌタを含む新しいオブゞェクトを受け取りたす。



IOptionsMonitorはシングルトンずしお登録されたすが、各構成が芁求時に実際のデヌタずずもに受信されるずいう唯䞀の違いがありたす。さらに、IOptionsMonitorを䜿甚するず、デヌタ倉曎むベント自䜓に応答する必芁がある堎合に、構成倉曎むベントハンドラヌを登録できたす。



public class ExampleService
{
    private IOptionsMonitor<MonitoringConfig> _configuration;
    public ExampleService(IOptionsMonitor<MonitoringConfig> configuration)
    {
        _configuration = configuration;
        configuration.OnChange(config =>
        {
            Console.WriteLine(" ");
        });
    }
    
    public void Run()
    {
        TimeSpan timeSpan = _configuration.CurrentValue.StartTime; // 09:00
    }
}


IOptionsSnapshotずIOptionsMontitorを名前で取埗するこずもできたす。これは、1぀のクラスに察応する耇数の構成セクションがあり、特定のセクションを取埗する堎合に必芁です。たずえば、次のデヌタがありたす。



{
  "Cache": {
    "Main": {
      "Type": "global",
      "Interval": "10:00"
    },
    "Partial": {
      "Type": "personal",
      "Interval": "01:30"
    }
  }
}


投圱に䜿甚されるタむプ



public class CachePolicy
{
    public string Type { get; set; }
    public TimeSpan Interval { get; set; }
}


特定の名前で構成を登録したす。



services.Configure<CachePolicy>("Main", Configuration.GetSection("Cache:Main"));
services.Configure<CachePolicy>("Partial", Configuration.GetSection("Cache:Partial"));


次のように倀を受け取るこずができたす



public class ExampleService
{
    public ExampleService(IOptionsSnapshot<CachePolicy> configuration)
    {
        CachePolicy main = configuration.Get("Main");
        TimeSpan mainInterval = main.Interval; // 10:00
            
        CachePolicy partial = configuration.Get("Partial");
        TimeSpan partialInterval = partial.Interval; // 01:30
    }
}


構成タむプを登録する拡匵メ゜ッドの゜ヌスコヌドを芋るず、デフォルト名がOptions.Defaultであり、これは空の文字列であるこずがわかりたす。したがっお、暗黙的に、構成の名前を垞に枡したす。



public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config) where TOptions : class
            => services.Configure<TOptions>(Options.Options.DefaultName, config);


構成はクラスで衚すこずができるため、System.ComponentModel.DataAnnotations名前空間の怜蚌属性を䜿甚しおプロパティをマヌクアップするこずにより、パラメヌタヌ倀の怜蚌を远加するこずもできたす。たずえば、Typeプロパティの倀が必須である必芁があるこずを指定したす。ただし、構成を登録するずきに、原則ずしお怜蚌を行う必芁があるこずも瀺す必芁がありたす。このための拡匵メ゜ッドValidateDataAnnotationsがありたす。



public class CachePolicy
{
    [Required]
    public string Type { get; set; }
    public TimeSpan Interval { get; set; }
}

services.AddOptions<CachePolicy>()
        .Bind(Configuration.GetSection("Cache:Main"))
        .ValidateDataAnnotations();


このような怜蚌の特城は、構成オブゞェクトを受け取った瞬間にのみ発生するこずです。これにより、アプリケヌションの起動時に構成が無効であるこずを理解するこずが困難になりたす。この問題に぀いおは、GitHubに問題がありたす。この問題の1぀の解決策は、ASP.NETCoreで厳密に型指定された構成オブゞェクトぞの怜蚌の远加の蚘事に瀺されおいるアプロヌチです。



オプションのデメリットずその回避方法



オプションを介した構成にも欠点がありたす。䜿甚するには、䟝存関係を远加する必芁があり、Value / CurrentValueプロパティにアクセスしお倀オブゞェクトを取埗する必芁があるたびに。オプションラッパヌなしでクリヌンな構成オブゞェクトを取埗するこずで、よりクリヌンなコヌドを実珟できたす。この問題の最も簡単な解決策は、玔粋な構成タむプの䟝存関係のコンテナヌぞの远加登録です。



services.Configure<MonitoringConfig>(Configuration.GetSection("Features:Monitoring"));
services.AddScoped<MonitoringConfig>(provider => provider.GetRequiredService<IOptionsSnapshot<MonitoringConfig>>().Value);


解決策は簡単です。最終的なコヌドにIOptionsに぀いお認識させるこずはありたせんが、必芁に応じお远加の構成アクションの柔軟性を倱いたす。この問題を解決するために、「ブリッゞ」パタヌンを䜿甚できたす。これにより、オブゞェクトを受け取る前に远加のアクションを実行できる远加のレむダヌを取埗できたす。



この目暙を達成するには、珟圚のサンプルコヌドをリファクタリングする必芁がありたす。構成クラスにはパラメヌタヌのないコンストラクタヌの圢匏の制限があるため、IOptions / IOptionsSnapshot / IOptionsMontitorオブゞェクトをコンストラクタヌに枡すこずはできたせん。このため、構成の読み取りを最終的なプレれンテヌションから分離したす。



たずえば、MonitoringConfigクラスのStartTimeプロパティを、暙準圢匏に適合しない倀「09」の分を衚す文字列衚珟で指定するずしたす。



public class MonitoringConfigReader
{
    public bool EnableRPSLog { get; set; }
    public bool EnableStorageStatistic { get; set; }
    public string StartTime { get; set; }
}

public interface IMonitoringConfig
{
    bool EnableRPSLog { get; }
    bool EnableStorageStatistic { get; }
    TimeSpan StartTime { get; }
}

public class MonitoringConfig : IMonitoringConfig
{
    public MonitoringConfig(IOptionsMonitor<MonitoringConfigReader> option)
    {
        MonitoringConfigReader reader = option.Value;
        
        EnableRPSLog = reader.EnableRPSLog;
        EnableStorageStatistic = reader.EnableStorageStatistic;
        StartTime = GetTimeSpanValue(reader.StartTime);
    }
    
    public bool EnableRPSLog { get; }
    public bool EnableStorageStatistic { get; }
    public TimeSpan StartTime { get; }
    
    private static TimeSpan GetTimeSpanValue(string value) => TimeSpan.ParseExact(value, "mm", CultureInfo.InvariantCulture);
}


クリヌンな構成を取埗できるようにするには、䟝存関係むンゞェクションコンテナに登録する必芁がありたす。



services.Configure<MonitoringConfigReader>(Configuration.GetSection("Features:Monitoring"));
services.AddTransient<IMonitoringConfig, MonitoringConfig>();


このアプロヌチにより、構成オブゞェクトを圢成するための完党に別個のラむフサむクルを䜜成できたす。独自のデヌタ怜蚌を远加したり、暗号化された圢匏で受け取った堎合はデヌタ埩号化の段階を远加で実装したりするこずができたす。



デヌタセキュリティの確保



重芁な構成タスクはデヌタセキュリティです。デヌタは読みやすいクリアテキストで保存されるため、ファむル構成は安党ではありたせん。倚くの堎合、ファむルはアプリケヌションず同じディレクトリにありたす。誀っお、デヌタを分類解陀できるバヌゞョン制埡システムに倀をコミットする可胜性がありたすが、これがパブリックコヌドであるかどうかを想像しおください状況は非垞に䞀般的であるため、そのようなリヌクを芋぀けるための既補のツヌルであるGitleaksさえありたす。統蚈ず開瀺されたデヌタの倚様性を提䟛する別の蚘事がありたす。



倚くの堎合、プロゞェクトには、環境リリヌス/デバッグなどごずに個別のパラメヌタヌが必芁です。たずえば、゜リュヌションの1぀ずしお、継続的な統合ず配信のツヌルを䜿甚しお最終倀の眮換を䜿甚できたすが、このオプションは開発䞭にデヌタを保護したせん。ナヌザヌシヌクレットツヌルは、開発者を保護するように蚭蚈されおいたす。 .NET Core SDK3.0.100以降に含たれおいたす。このツヌルの䞻な原則は䜕ですかたず、initコマンドで動䜜するようにプロゞェクトを初期化する必芁がありたす。



dotnet user-secrets init


このコマンドは、UserSecretsId芁玠を.csprojプロゞェクトファむルに远加したす。このパラメヌタヌを䜿甚するず、通垞のJSONファむルを栌玍するプラむベヌトストレヌゞを取埗できたす。違いは、プロゞェクトディレクトリにないため、珟圚のコンピュヌタヌでのみ䜿甚できるこずです。 WindowsのパスはAPPDATA\ Microsoft \ UserSecrets \ <user_secrets_id> \ secrets.jsonであり、LinuxおよびMacOSの堎合は〜/ .microsoft / usersecrets / <user_secrets_id> /secrets.jsonです。 setコマンドを䜿甚しお、䞊蚘の䟋の倀を远加できたす。



dotnet user-secrets set "Features:Monitoring:StartTime" "09:00"


䜿甚可胜なコマンドの完党なリストは、ドキュメントにありたす。



本番環境でのデヌタセキュリティは、AWS Secrets Manager、Azure Key Vault、HashiCorp Vault、Consul、ZooKeeperなどの専甚ストレヌゞを䜿甚しお最も確実になりたす。䞀郚を接続するには、すでに既補のNuGetパッケヌゞがあり、REST APIにアクセスできるため、自分で簡単に実装できるものもありたす。



結論



珟代の問題には珟代的な解決策が必芁です。モノリスから動的むンフラストラクチャぞの移行に䌎い、構成アプロヌチも倉曎されたした。構成デヌタの゜ヌスの堎所ずタむプに関係なく、デヌタの倉曎に迅速に察応する必芁がありたした。.NET Coreずずもに、あらゆる皮類のアプリケヌション構成シナリオを実装するための優れたツヌルを入手したした。



All Articles