平衡二分探索木を優先度付きキューとして使う

.NET 6 では、優先度付きキューを表す PriorityQueue<TElement,TPriority> クラスが基本クラスライブラリ (BCL) に追加されました。
言い換えると、.NET 5 以前の BCL には優先度付きキューが存在しません。
そこで、.NET 5 以前の環境において、平衡二分探索木のクラスを利用して優先度付きキューに相当する処理を実現する方法を考えてみます。

優先度付きキューの要件

まず、優先度付きキュー (priority queue) に必要な機能を整理します:

  • Count: 現在の要素の数を取得する
  • Peek: 優先度が最も高い要素を取得する。削除はしない
  • Pop: 優先度が最も高い要素を取得し、同時に削除する
  • Push: 新たな要素を追加する

すなわち、初めから全ての要素がわかっている (静的データ) とは限らず、要素の動的な追加に対応します。
また、オプション機能として次のものが考えられます:

  • 一般的に、要素の重複を許す
  • 昇順・降順を指定できる
  • 要素に対して、優先度を表すキーを指定できる
  • 安定ソート (一般的な二分ヒープでは不可)

例えば、1組の生徒たち、2組の生徒たち、・・・のような順でデータを取得する要件に対応するには、優先度を表すキーを指定できる機能があると便利です。

平衡二分探索木に関連するクラス

ここでは、.NET 5 以前の BCL に存在する平衡二分探索木およびその周辺のクラスについてまとめます。
以下の3種類があり、「追加した要素が自動的にキーの順序でソートされる」「キーの重複は不許可」という点はこれらに共通です。
なお、SortedList<TKey,TValue> クラスは平衡二分探索木ではありません (以降の節では使いません)。

(1) SortedSet<T> クラス

  • 要素をキーとして使用する
  • 要素の動的な追加および削除の時間計算量は O(log n)
  • HashSet<T> が順序付きになったと考えてよい

(2) SortedDictionary<TKey,TValue> クラス

  • キーと値のペアを格納する
  • 要素の動的な追加および削除の時間計算量は O(log n)
  • Dictionary<TKey,TValue> が順序付きになったと考えてよい

(3) SortedList<TKey,TValue> クラス

  • キーと値のペアを格納する
  • 通常のリストのような構造であり、空間計算量は O(n)
    • 平衡二分探索木ではない
  • 要素の動的な追加および削除の時間計算量は O(n)
  • キーからインデックスの取得、インデックスから要素の取得の時間計算量は O(log n)
  • ソート済みの静的データで初期化するだけの場合や、末尾にくるデータを追加するだけであれば速い

実装

では、平衡二分探索木を利用して、優先度付きキューを実装していきます。
要件に合わせて以下の3種類を用意しました。

(1) 要素を重複させない場合

  • SortedSet<T> をほぼそのまま利用する
  • Min プロパティおよび Add, Remove メソッドを呼び出せばよい

(2) 要素の重複を許す場合 (一般的な優先度付きキュー)

  • SortedDictionary<T, int> を利用する
  • 要素ごとの個数を管理する

(3) 要素に対して優先度を表すキーを指定する場合 (重複を許可)

  • SortedDictionary<TKey, Queue<T>> を利用する
  • キーごとにキューで要素を管理する
  • したがって、安定ソートとなる

ソースコードは次の通りです。言語は C# です。
なお、降順を指定するには、前回の記事で作成した補助クラスで IComparer<T> を生成すればよいです。

using System;
using System.Collections.Generic;
using System.Linq;
namespace AlgorithmLab.DataTrees
{
// 要素が重複しない (すべての値の順序が異なる) 場合に利用できます。
public class DistinctPriorityQueue<T>
{
// 要素をそのままキーとして使用します。
SortedSet<T> ss;
public DistinctPriorityQueue(IComparer<T> comparer = null)
{
ss = new SortedSet<T>(comparer ?? Comparer<T>.Default);
}
public int Count => ss.Count;
public T Peek()
{
if (ss.Count == 0) throw new InvalidOperationException("The container is empty.");
return ss.Min;
}
public T Pop()
{
if (ss.Count == 0) throw new InvalidOperationException("The container is empty.");
var item = ss.Min;
ss.Remove(item);
return item;
}
public bool Push(T item) => ss.Add(item);
}
// 要素が重複する場合も利用できます (一般的な優先度付きキュー)。
public class BstPriorityQueue<T>
{
// 要素をそのままキーとして使用します。
SortedDictionary<T, int> sd;
public BstPriorityQueue(IComparer<T> comparer = null)
{
sd = new SortedDictionary<T, int>(comparer ?? Comparer<T>.Default);
}
public int Count { get; private set; }
public T Peek()
{
if (Count == 0) throw new InvalidOperationException("The container is empty.");
return sd.First().Key;
}
public T Pop()
{
if (Count == 0) throw new InvalidOperationException("The container is empty.");
Count--;
var (item, count) = sd.First();
if (count == 1) sd.Remove(item);
else sd[item] = count - 1;
return item;
}
public void Push(T item)
{
Count++;
sd.TryGetValue(item, out var count);
sd[item] = count + 1;
}
}
// 要素に対して優先度を表すキーを指定する場合に利用します。
public class KeyedPriorityQueue<T, TKey>
{
SortedDictionary<TKey, Queue<T>> sd;
Func<T, TKey> keySelector;
public KeyedPriorityQueue(Func<T, TKey> keySelector, IComparer<TKey> comparer = null)
{
this.keySelector = keySelector ?? throw new ArgumentNullException(nameof(keySelector));
sd = new SortedDictionary<TKey, Queue<T>>(comparer ?? Comparer<TKey>.Default);
}
public int Count { get; private set; }
public T Peek()
{
if (Count == 0) throw new InvalidOperationException("The container is empty.");
return sd.First().Value.Peek();
}
public T Pop()
{
if (Count == 0) throw new InvalidOperationException("The container is empty.");
Count--;
var (key, q) = sd.First();
if (q.Count == 1) sd.Remove(key);
return q.Dequeue();
}
public void Push(T item)
{
Count++;
var key = keySelector(item);
if (!sd.TryGetValue(key, out var q)) sd[key] = q = new Queue<T>();
q.Enqueue(item);
}
}
}

また、平衡二分探索木を利用する利点として、優先度の高いほうだけでなく、両側に対する優先度付きキューを実現できることが挙げられます。PopFirst, PopLast メソッドとして作成すればよいでしょう。

利用例

(1) DistinctPriorityQueue

(2) BstPriorityQueue

(3) KeyedPriorityQueue

前回: ソート用の比較関数の補助クラス

作成したサンプル

検証したバージョン

  • C# 8.0
  • .NET Standard 2.1

参照

ソート用の比較関数の補助クラス

.NET で配列やコレクションをソートするときに、ソートの条件を指定する方法がいくつかあります。
今回はそれらを一通り調べ、さらにソート条件の指定を補助するためのクラスを作成しました。

既存ライブラリのまとめ

まず、配列やコレクションをソートするために呼び出す主なメソッドを挙げてみます。

そして、これらのメソッドのオーバーロードでソートの条件を指定する方法をまとめると、次のようになります。

  • IComparable<T>
    • 対象となる型 T が IComparable<T> インターフェイスを実装している場合、引数を指定しなくてもソート可能
    • ただし、これだけでは降順にできない
  • Comparison<T> または Func<T, T, int>
    • 引数 (x, y) に対し、次の値を返す比較関数
      • x が小さいならば、負の値
      • 等しいならば、0
      • x が大きいならば、正の値
  • IComparer<T>
  • TKey[]
    • 各要素に対応するキーの配列
    • キーの型が IComparable<TKey> インターフェイスを実装していることが必要
  • Func<T, TKey> によるキーの指定、および昇順・降順の指定
    • LINQ の Enumerable.OrderBy メソッドなど
    • 第2キー以降を指定するには Enumerable.ThenBy メソッドなど
    • キーの型が IComparable<TKey> インターフェイスを実装していることが必要

課題と解決策

普段これらを使っていると、次のような実感があります。

  • 対象となる型 T が IComparable<T> インターフェイスを実装しており、その昇順でソートしたい場合は、引数のないオーバーロードを呼び出すだけであり簡単
  • それ以外の少し複雑な条件の場合、LINQ のように、Func<T, TKey> によりキーを指定したいことが多い
    • しかし、このオーバーロードは用意されていないことが多い
  • IComparer<T> オブジェクトを引数に指定するオーバーロードであれば、だいたいどのソート関数でも備えている

そこで、「Func<T, TKey> によるキーの指定、および昇順・降順の指定」から IComparer<T> オブジェクトを生成するためのヘルパー クラスを作成しました。言語は C# です。

using System;
using System.Collections.Generic;
namespace AlgorithmLab.DataTrees
{
public static class ComparerHelper
{
public static IComparer<T> GetDefault<T>()
{
// カルチャに依存しない場合に高速化します。
if (typeof(T) == typeof(string)) return (IComparer<T>)StringComparer.Ordinal;
return Comparer<T>.Default;
}
public static IComparer<T> ToDescending<T>(this IComparer<T> c)
{
if (c == null) throw new ArgumentNullException(nameof(c));
return Comparer<T>.Create((x, y) => c.Compare(y, x));
}
}
// クラスに型引数を指定することで、Create メソッドを呼び出すときに型引数 <T, Tkey> の指定を省略できます。
public static class ComparerHelper<T>
{
public static IComparer<T> Create(bool descending = false)
{
var c = ComparerHelper.GetDefault<T>();
return descending ? c.ToDescending() : c;
}
public static IComparer<T> Create<TKey>(Func<T, TKey> keySelector, bool descending = false)
{
if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
var c = ComparerHelper<TKey>.Create(descending);
return Comparer<T>.Create((x, y) => c.Compare(keySelector(x), keySelector(y)));
}
public static IComparer<T> Create<TKey1, TKey2>(Func<T, TKey1> keySelector1, bool descending1, Func<T, TKey2> keySelector2, bool descending2)
{
if (keySelector1 == null) throw new ArgumentNullException(nameof(keySelector1));
if (keySelector2 == null) throw new ArgumentNullException(nameof(keySelector2));
var c1 = ComparerHelper<TKey1>.Create(descending1);
var c2 = ComparerHelper<TKey2>.Create(descending2);
return Comparer<T>.Create((x, y) =>
{
var d = c1.Compare(keySelector1(x), keySelector1(y));
if (d != 0) return d;
return c2.Compare(keySelector2(x), keySelector2(y));
});
}
}
}
using System;
using AlgorithmLab.DataTrees;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
var a = new[] { 3, 14, 1, 592, 65, 358 };
// 文字数の降順、値の昇順
// 結果: { 358, 592, 14, 65, 1, 3 }
Array.Sort(a, ComparerHelper<int>.Create(x => x.ToString().Length, true, x => x, false));
}
}
}
view raw Program.cs hosted with ❤ by GitHub

これにより、LINQ 以外の場面でソートするときも、LINQ とほぼ同様の記述方法でキーを指定できるようになりました。

次回: 平衡二分探索木を優先度付きキューとして使う

作成したサンプル

検証したバージョン

  • C# 6
  • .NET Standard 2.0

参照

カテゴリー: .NET Core, .NET Framework. タグ: . 1 Comment »

Roslyn の構文解析を使ってデバッガーを自作する

// C# Advent Calendar 2018 の 23 日目の記事です。

デバッガーのようなものを自作してみました。

動機

  • 普段は Visual Studio を使っているが、デバッグ時に手動でステップ実行するのが面倒
    • ループなどでステップ数が多い場合
    • 分岐の様子や変数の状態を軽くチェックしたい場合

解決案

  • ステップの時間間隔だけを指定して、デバッガーを自動で実行させる
    • 変数の一覧が表示される
    • 時間間隔をリアルタイムで調節できる
  • .NET Compiler Platform (Roslyn) の構文解析の機能を使い、各ステップの間にデバッグ用のコードを差し込めば実現できそう

結果

というわけで、WPF でプロトタイプ「Tick-tack Debugger」を作ってみた結果、このようになりました。
例として、ニュートン法で平方根を求めています。 (クリックで拡大)

 

解説

以下は概略の技術解説です。
WPF アプリを作成する前に、まず .NET Framework 上のコンソール アプリで実験してみます。
C# の構文解析を使うには、NuGet で Microsoft.CodeAnalysis.CSharp をインストールします。

デバッグ対象となるソースコードにデバッグ コードを挿入し、それを動的にコンパイルして実行する、という方針です。
コンソール アプリのソースコードを以下に示します (全体のソリューションは SyntaxTreeSample にあります)。

using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using DebuggerLib;
namespace DebuggerConsole
{
class Program
{
const string SourcePath = @"..\..\..\NumericConsole\Program.cs";
const string GeneratedPath = @"Program.g.cs";
static void Main(string[] args)
{
// Generates the code for debugging.
var sourceCode = File.ReadAllText(SourcePath);
var generatedCode = SyntaxHelper.InsertBreakpoints(sourceCode);
File.WriteAllText(GeneratedPath, generatedCode, Encoding.UTF8);
// Compiles and loads the assembly.
var provider = CodeDomProvider.CreateProvider("CSharp");
var compilerOption = new CompilerParameters(new[] { "System.Core.dll", "DebuggerLib.dll" }) { GenerateExecutable = true };
var compilerResult = provider.CompileAssemblyFromFile(compilerOption, GeneratedPath);
if (compilerResult.Errors.HasErrors) return;
// Registers the action for breakpoints.
DebugHelper.InfoNotified += (spanStart, spanLength, variables) =>
{
Console.WriteLine(string.Join(", ", variables.Select(v => $"{v.Name}: {v.Value}")));
Console.WriteLine(sourceCode.Substring(spanStart, spanLength));
Thread.Sleep(1000);
};
// Calls the Main method.
var entryPoint = compilerResult.CompiledAssembly.EntryPoint;
entryPoint.Invoke(null, new object[] { new string[0] });
}
}
}
view raw Program.cs hosted with ❤ by GitHub
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using DebugStatement = System.ValueTuple<Microsoft.CodeAnalysis.CSharp.Syntax.StatementSyntax, string[]>;
namespace DebuggerConsole
{
public static class SyntaxHelper
{
public static string InsertBreakpoints(string sourceCode)
{
var root = ParseText(sourceCode);
var method = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.FirstOrDefault(m => m.Identifier.ValueText == "Main")
?? throw new FormatException("The Main method is not found.");
var statements = DetectStatements(method);
var result = sourceCode;
foreach (var (statement, variables) in statements.Reverse())
{
var (span, debugIndex) = GetSpan(statement);
result = result.Insert(debugIndex, $"DebugHelper.NotifyInfo({span.Start}, {span.Length}{ToParamsArrayText(variables)});\r\n");
}
return result.Insert(root.Usings.FullSpan.End, "using DebuggerLib;\r\n");
}
public static CompilationUnitSyntax ParseText(string text)
{
var tree = CSharpSyntaxTree.ParseText(text);
var diagnostics = tree.GetDiagnostics().ToArray();
if (diagnostics.Length > 0) throw new FormatException(diagnostics[0].ToString());
return tree.GetCompilationUnitRoot();
}
public static DebugStatement[] DetectStatements(MethodDeclarationSyntax method)
{
var statements = new List<DebugStatement>();
DetectStatements(method.Body, statements, new List<(string, SyntaxNode)>());
return statements.ToArray();
}
static void DetectStatements(SyntaxNode node, List<DebugStatement> statements, List<(string name, SyntaxNode scope)> variables)
{
// Adds variables.
if (node is VariableDeclarationSyntax varSyntax)
{
var varNames = varSyntax.Variables.Select(v => v.Identifier.ValueText).ToArray();
var scope = ((node.Parent is LocalDeclarationStatementSyntax) ? node.Parent : node)
.Ancestors()
.First(n => n is StatementSyntax);
variables.AddRange(varNames.Select(v => (v, scope)));
}
// Maps variables to the statement.
if ((node is StatementSyntax statement) &&
!(node is BlockSyntax) &&
!(node is BreakStatementSyntax))
statements.Add((statement, variables.Select(v => v.name).ToArray()));
// Recursively.
foreach (var child in node.ChildNodes())
DetectStatements(child, statements, variables);
// Maps variables to the last line of the block.
if (node is BlockSyntax block)
statements.Add((block, variables.Select(v => v.name).ToArray()));
// Clears variables out of the scope.
if (node is StatementSyntax)
for (var i = variables.Count - 1; i >= 0; i--)
if (variables[i].scope == node)
variables.RemoveAt(i);
else
break;
}
static (TextSpan, int) GetSpan(StatementSyntax statement)
{
switch (statement)
{
case ForStatementSyntax f:
var span = new TextSpan(f.ForKeyword.Span.Start, f.CloseParenToken.Span.End - f.ForKeyword.Span.Start);
return (span, statement.FullSpan.Start);
case BlockSyntax b:
return (b.CloseBraceToken.Span, b.CloseBraceToken.FullSpan.Start);
default:
return (statement.Span, statement.FullSpan.Start);
}
}
static string ToParamsArrayText(string[] variables) =>
string.Concat(variables.Select(v => $", new Var(\"{v}\", {v})"));
}
}
view raw SyntaxHelper.cs hosted with ❤ by GitHub

SyntaxHelper クラスでは、デバッグ対象の C# ソースコードを構文ツリー (SyntaxTree) に変換して走査し、
各ステートメントの前にデバッグ用のコード行を挿入していきます。

CSharpSyntaxTree.ParseText メソッドを使うことで、ソースコードを構文ツリーに変換できます。
また、メソッド・ステートメント・式など、すべてのノードを表す親クラスは SyntaxNode クラスであり、

  • Parent プロパティ: 親
  • Ancestors メソッド: 祖先
  • ChildNodes メソッド: 子
  • DescendantNodes メソッド: 子孫

が存在することを知っておけば、だいたいの探索ができるでしょう。

この他に、デバッグ用のコードから呼び出されるメソッドを定義するクラス ライブラリとして DebuggerLib を作成しています。
各ステートメントの位置、およびその直前で存在する変数とその値を通知するために、このライブラリを経由させます。

Program クラスでは、生成されたデバッグ用のソースコードをファイルに保存したら、
System.CodeDom.Compiler 名前空間の CodeDomProvider を使ってこれをコンパイルし、
そのエントリ ポイント (Main メソッド) を呼び出します。
また、デバッグ コードが実行されたときのイベントハンドラーを登録しておき、
Thread.Sleep メソッドを使って、指定した時間だけ停止させます。

これで、デバッグ対象の元のソースコードが次の Program.cs だとすると、
デバッグ用のソースコードとして下の Program.g.cs が生成されます。

using System;
namespace NumericConsole
{
class Program
{
static void Main(string[] args)
{
// Square root by the Newton's method.
var a = 5.0;
var x = a;
for (var i = 0; i < 100; i++)
{
var xi = (x + a / x) / 2;
if (x == xi) break;
x = xi;
}
Console.WriteLine(x);
}
}
}
view raw Program.cs hosted with ❤ by GitHub
using System;
using DebuggerLib;
namespace NumericConsole
{
class Program
{
static void Main(string[] args)
{
DebugHelper.NotifyInfo(188, 12);
// Square root by the Newton's method.
var a = 5.0;
DebugHelper.NotifyInfo(214, 10, new Var("a", a));
var x = a;
DebugHelper.NotifyInfo(240, 29, new Var("a", a), new Var("x", x));
for (var i = 0; i < 100; i++)
{
DebugHelper.NotifyInfo(302, 25, new Var("a", a), new Var("x", x), new Var("i", i));
var xi = (x + a / x) / 2;
DebugHelper.NotifyInfo(347, 19, new Var("a", a), new Var("x", x), new Var("i", i), new Var("xi", xi));
if (x == xi) break;
DebugHelper.NotifyInfo(384, 7, new Var("a", a), new Var("x", x), new Var("i", i), new Var("xi", xi));
x = xi;
DebugHelper.NotifyInfo(405, 1, new Var("a", a), new Var("x", x), new Var("i", i), new Var("xi", xi));
}
DebugHelper.NotifyInfo(422, 21, new Var("a", a), new Var("x", x));
Console.WriteLine(x);
DebugHelper.NotifyInfo(453, 1, new Var("a", a), new Var("x", x));
}
}
}
view raw Program.g.cs hosted with ❤ by GitHub

作成したコンソール アプリを実行すると、次の図のようになります (時間間隔は 0.3 秒)。

 

以上をもとに、WPF アプリでデバッグ ツールを作成しました。
左側の C# ソースコードの部分は TextBox で、編集もできます。
デバッグ実行時は、各ステートメントを選択状態にすることでハイライトしています。
右側の変数一覧が表示される部分は DataGrid です。

(図は円周率を求める例)

今回は上記の方法でプロトタイプを作ってみましたが、
デバッグ コードの挿入やコンパイルに関しては、よりスマートな方法があるのではないかと思います。

注意点
  • 考えられうるすべてのステートメントには対応できていません。また、Main メソッドしか構文解析していません。
  • コンパイル時に生成されるアセンブリ (EXE) は、%TEMP% フォルダー (ユーザーの AppData\Local\Temp) に保存されていきます。
  • TextBox で、IsInactiveSelectionHighlightEnabled を True に設定しても利かないことがあります。
    また、選択状態のハイライトがずれることがあります。
    RichTextBox で Run などを使うのがよいかもしれません。

 

作成したサンプル
バージョン情報
参照

URL エンコーディング

URL エンコーディングの定義と、それを扱うための .NET Framework のライブラリを検証しました。

パーセント エンコーディングと URL エンコーディング

パーセント エンコーディングとは、文字列を UTF-8 でエンコードし、各バイトをパーセント記号 % とその 16 進数を用いて表すことです。
例えば、"/""%2F" に、"あ""%E3%81%82" に変換されます。

URL エンコーディングとは、URI の中で使われている記号と混在しないように一部の文字列をパーセント エンコーディングにより
変換することです。両者の言葉を区別せずに使うこともあります。

URL エンコーディング

RFC 3986 では、文字は次のように分類されます。

  • 非予約文字
    • エンコードしなくても利用できる文字
    • アルファベット、数字、および 4 種類の記号 -._~
  • 予約文字
    • URI で意味を持つ記号
    • 18 種類の記号 !#$&'()*+,/:;=?@[]
  • その他の文字
    • エンコードが必要な文字
    • 11 種類の記号 " %<>\^`{|} 、その他のすべての文字 (日本語など)

URL エンコーディングは、主に次の 2 通りで利用されます。

  • URI の各セグメント (クエリ文字列を除く)
    • https://tempuri.org/messages/Hello%20World%21messagesHello%20World%21 の部分
    • 非予約文字以外をパーセント エンコーディング
      • ただし、Web フレームワーク個別の仕様により、パーセント エンコーディングしても使用を制限されることがある
  • URI のクエリ文字列や、POST などで送信するときの本文 (フォーム)
    • key=value&message=Hello+World%21keyHello+World%21 の部分
    • 非予約文字以外をパーセント エンコーディングし、さらに %20 (スペース) を + に変換
    • MIME タイプ application/x-www-form-urlencoded と定義されている

 

.NET Framework のライブラリ

.NET Framework では、URL エンコーディングのために次の方法が用意されています。

  • System.Uri.EscapeDataString メソッド
    • RFC 3986 に従って非予約文字以外をパーセント エンコーディング
  • System.Uri.EscapeUriString メソッド
    • RFC 3986 に従って非予約文字・予約文字以外をパーセント エンコーディング
    • 既に全体が URI の形式になっているときに利用する
      • クエリ文字列も同様の規則で変換される。application/x-www-form-urlencoded には変換されない
  • System.Uri インスタンスの AbsoluteUri プロパティ
    • 基本的に Uri.EscapeUriString メソッドと同じだが、下記の点が異なる
    • %XX の形式になっているかどうかで扱いが異なる
      • https://tempuri.org/%2https://tempuri.org/%252
      • https://tempuri.org/%25https://tempuri.org/%25 のまま
    • クエリ文字列でない部分の \/ に変換される
  • System.Net.WebUtility.UrlEncode メソッド
    • RFC 2396 (旧版) に近い仕様で非予約文字以外をパーセント エンコーディングし、さらに %20 (スペース) を + に変換
  • System.Web.HttpUtility.UrlEncode メソッド
    • System.Net.WebUtility.UrlEncode メソッドと同じだが、小文字になる
  • System.Net.Http.FormUrlEncodedContent クラス
    • key-value データをまとめて application/x-www-form-urlencoded に変換

 

Uri.AbsoluteUri

 

.NET では System.Uri.EscapeDataString メソッドSystem.Uri.EscapeUriString メソッド
System.Net.Http.FormUrlEncodedContent クラスを使えばよいでしょう。

アプリケーションから HTTP 接続をするために System.Net.Http.HttpClient クラスを使うことが多いと思いますが、
接続先の URI を string で渡しても、HttpClient の内部では Uri インスタンスで扱われます。
したがって、URI を HttpClient に渡す前に、セグメントもクエリ文字列も URL エンコーディングしておくのがよさそうです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace UnitTest.Client
{
public static class HttpHelper
{
public static string AddQuery(this string uri, IDictionary<string, string> data) =>
$"{uri}?{data.ToFormUrlEncoded()}";
public static string ToFormUrlEncoded(this IDictionary<string, string> data)
{
using (var content = new FormUrlEncodedContent(data))
return content.ReadAsStringAsync().GetAwaiter().GetResult();
}
async public static Task<T> GetAsync<T>(string uri)
{
using (var http = new HttpClient())
{
var response = await http.GetAsync(uri);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsAsync<T>();
}
}
async public static Task<T> PostAsFormAsync<T>(string uri, IDictionary<string, string> data)
{
using (var http = new HttpClient())
{
var response = await http.PostAsync(uri, new FormUrlEncodedContent(data));
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsAsync<T>();
}
}
}
}
view raw HttpHelper.cs hosted with ❤ by GitHub
using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTest.Client
{
[TestClass]
public class UriQueryTest
{
[TestMethod]
public void Get_Segment()
{
// http://localhost:1961/api/uriquery/Hello~%2C%20World%21
var uri = $"http://localhost:1961/api/uriquery/{Uri.EscapeDataString("Hello~, World!")}";
var result = HttpHelper.GetAsync<string>(uri).GetAwaiter().GetResult();
}
[TestMethod]
public void Get_Query()
{
var data = new Dictionary<string, string>
{
{ "id", "Hello, the \"World+\"." },
};
// http://localhost:1961/api/uriquery?id=Hello%2C+the+%22World%2B%22.
var uri = "http://localhost:1961/api/uriquery&quot;.AddQuery(data);
var result = HttpHelper.GetAsync<string>(uri).GetAwaiter().GetResult();
}
[TestMethod]
public void Post_Form()
{
var data = new Dictionary<string, string>
{
{ "name", "Hello, the \"World+\"." },
};
var uri = "http://localhost:1961/api/uriquery&quot;;
var result = HttpHelper.PostAsFormAsync<string>(uri, data).GetAwaiter().GetResult();
}
}
}
view raw UriQueryTest.cs hosted with ❤ by GitHub

 

作成したサンプル

バージョン情報

  • .NET Framework 4.5

参照

カテゴリー: .NET Framework, サービス. タグ: , . Leave a Comment »

dotnet コマンドによるビルド

前回の .NET Core と .NET Standard に引き続き、今回はコマンドラインでアプリやライブラリをビルドする方法を検証しました。
まず、ビルドに関連する dotnet コマンドの一覧を挙げます。
基本的にはプロジェクト フォルダー上で実行しますが、build や pack などは ソリューション フォルダー上でも実行できます。

  • dotnet restore
    • NuGet 参照を解決する
  • dotnet build
    • MSBuild.exe を実行する
    • 内部で restore する (ソースコードしかない状態でも実行できる)
  • dotnet msbuild
    • MSBuild.exe と同じ引数を指定する
    • 内部で restore しない (ソースコードしかない状態では失敗)
  • dotnet publish
    • publish フォルダーに発行する
    • 内部で build する (ソースコードしかない状態でも実行できる)
  • dotnet pack
    • NuGet パッケージを作成する
    • 参照先の DLL は含まれず、依存関係が設定される
    • 内部で build しない (ソースコードしかない状態では失敗)
  • dotnet clean
    • 前回のビルド結果を消去する
    • restore の結果は残る
  • dotnet run
    • ソースコードからアプリを実行する
    • 内部で build する
  • dotnet App1.dll
    • ビルド済みのアプリを実行する

 

以下、詳細について記述していきます。

dotnet msbuild と msbuild

dotnet msbuild と msbuild の動作は同じです。

dotnet msbuild /p:Configuration=Release /t:Rebuild
msbuild /p:Configuration=Release /t:Rebuild

ただし、msbuild は環境変数の PATH に設定されていないため、
cmd や PowerShell で実行するにはそのパスを指定しなければなりませんが、
dotnet は PATH に設定されているため cmd や PowerShell でそのまま実行できて便利です。

アセンブリのビルド・発行

リビルドするには --no-incremental を指定します。

dotnet build -c Release --no-incremental

ただし build では、.NET Core を対象とする場合、NuGet 参照の DLL がコピーされません。
build では開発環境が想定されており、.dev.json ファイルに NuGet 参照が記述されます。
(.NET Framework を対象とする場合は NuGet 参照の DLL もコピーされます。)

配置用にすべての DLL を含めるには publish を使います。
プロジェクトに対象のフレームワークが複数ある場合、-f で一つだけ指定します。

dotnet clean -c Release
dotnet publish -c Release -f netcoreapp2.0

なお、publish 単独ではリビルドができないため、先に clean を実行しています。

NuGet パッケージ作成

出力先のディレクトリを変更するには -o を指定します。

dotnet pack -c Release -o pkg

または、

dotnet msbuild /p:Configuration=Release /t:pack

[構築時に NuGet パッケージを生成する] (.csproj では GeneratePackageOnBuild) を設定して build する方法もあります。

dotnet build -c Release --no-incremental

GeneratePackageOnBuild

 

前回: .NET Core と .NET Standard
次回: ASP.NET Core Web API の Tips

作成したサンプル

バージョン情報

  • .NET Core 2.0

参照

カテゴリー: .NET Core, .NET Framework, ツール. タグ: . 2 Comments »
  • WordPress.com で次のようなサイトをデザイン
    始めてみよう