20191203のC#に関する記事は11件です。

C#/WinRT - Windows.Data.Pdfを使ってみた

このサンプルでは、PDFを読み込んでページ数を出力します。

環境 Windows10 (Visual Studioいれてないとダメかも)

ソースコード

※await/asyncを使い慣れていないので、良い書き方かどうかは自信はないです。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

using Windows.Data.Pdf;

namespace MyPdfTest
{
    public class MyPdf
    {
        public static async Task<uint> LoadPdfFile(string fileName)
        {
            Windows.Storage.StorageFile sampleFile = await Windows.Storage.StorageFile.GetFileFromPathAsync(fileName);
            uint t = LoadPdfDocumentAsync(sampleFile).Result;
            return t;
        }

        static async Task<uint> LoadPdfDocumentAsync(Windows.Storage.StorageFile pdfFile)
        {
            PdfDocument _pdfDoc = await PdfDocument.LoadFromFileAsync(pdfFile);
            return _pdfDoc.PageCount;
        }

        [STAThread]
        static void Main(string[] args)
        {
            Task<uint> t = LoadPdfFile(@"C:\SvnLocal\trunk\Pdf_WinRT\PDF32000_2008.pdf");
            uint t2 = t.Result;
            Console.WriteLine(t2);
        }
    }
}

コンパイル用バッチファイル

compile.bat ソースファイル名.cs でコンパイル

compile.bat
csc /r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Runtime.WindowsRuntime\v4.0_4.0.0.0__b77a5c561934e089\system.runtime.windowsruntime.dll ^
/r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Runtime.InteropServices.WindowsRuntime\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Runtime.InteropServices.WindowsRuntime.dll ^
/r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Runtime\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Runtime.dll ^
"/r:C:\Program Files (x86)\Windows Kits\8.1\References\CommonConfiguration\Neutral\Annotated\Windows.winmd" %*

実行結果

C:\SvnLocal\trunk\Pdf_WinRT>PdfWinRT_Test.exe
756

以下はAdobe Reader DCで確認したページ数。
image.png

正しいページ数が得られている。

参考サイト

  1. https://www.atmarkit.co.jp/ait/articles/1310/24/news070.html
  2. https://docs.microsoft.com/en-us/uwp/api/windows.storage.storagefile.getfilefrompathasync

追記

参考サイト1によると、ページを画像として抽出するのには使えそうだが、テキストを抽出するのには使えなさそうである。ILSpyでメンバーとかみた感じ、その通りっぽい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google.Protobuf.Reflectionを利用してC#でProtocol Buffersを汎用的に解析する話

この記事は DeNA Advent Calendar 2019 の12/9(月)の記事です。

はじめに

この記事ではC#でProtocol Buffers(以下、protobuf)の中身を解析していくための機能について紹介します

※本題に入るまで少し前置きがあるので、そういうのは飛ばしたいという方はこちらの本編へ直接どうぞ

経緯

普段はゲームのクライアントエンジニアとしてUnityによるゲーム開発の仕事をしている人間なのですが、とある案件でUnityでprotobufを扱いたいというニーズが発生し、C#でゴリゴリprotobufのデータを弄ることになりました

その際に調べながら実装を進めていたところ「C#でprotobufを扱う内容ってあまり深堀りされていないなぁ」と感じ、今回の記事公開に至りました

ターゲット

protobufがどういったものなのかを知っている前提で話を進めていきます

「C#(Unity)でゴリゴリにprotobufを扱いたいんじゃ〜」という人がドンピシャなメインターゲットですが、中々に稀有な存在だと思われるので、実際にそういう人ではなくても「そういうこともできるんだ」と思ってもらえたら幸いです

また、今回取り扱うprotobufはproto3のバージョンを対象としています

それぞれに関連する用語の詳細な解説は省いていきますので予めご了承ください

得られるもの

今まであまり語られることがなかったGoogle.Protobuf.Reflectionの機能について学ぶことができます

UnityのEditor拡張を使って直接protobufのデータを弄りたいというのが具体的な要件でしたが、Editor拡張の部分はとくに目新しいことはしていないため、今回はGoogle.Protobuf.Reflectionを使ってprotobufのデータを汎用的に読み書きする部分(非Unity依存・PureなC#コード)をメインに解説していきます

C#でprotobufを扱う最小サンプル

https://developers.google.com/protocol-buffers/docs/csharptutorial
公式のチュートリアルにサンプルコードが載っています

ロード

Person john;
// john.datというファイルをロードし
using (var input = File.OpenRead("john.dat"))
{
    // インスタンスに内容を格納する
    john = Person.Parser.ParseFrom(input);
}

セーブ

// johnという変数名のインスタンスを予め用意する
Person john = ...;
// john.datファイルを新たに作成し
using (var output = File.Create("john.dat"))
{
    // johnの内容をファイルに書き出す
    john.WriteTo(output);
}

これらは抜粋ですが、全体のコードを確認したい場合は以下のリンクから辿ることができます
https://github.com/protocolbuffers/protobuf/tree/master/csharp/src/AddressBook

このようにして各スキーマごとにprotobufのデータをロードして、プロダクトコードでゴリゴリにデータ弄ってそれをまたセーブをすればOK!

各スキーマごとにメンテナンスするのが大変!

はい、ここからが本題です

スキーマ定義が少ないうちは個別にゴリゴリ実装をしていってもそこまで問題はないのですが、ガッツリ使い込んでスキーマ定義が増えていくとそのスキーマ定義に依存した実装のメンテナンスが非常に大変になってきます

新しくスキーマの定義を増やしたり、スキーマの内容を変更したりするたびにメンテナンスをすることになるので、なるべく依存するコードは最低限に留めて汎用的に取り扱えるような実装にしたいです

今回の要件では、UnityEditor上からパラメータの調整をできるようにしたいというものがあり、スキーマごとに入力エディタを作るのは現実的ではありませんでした

Google.Protobuf.Reflection

ここで登場するのがGoogle.Protobuf.Reflectionのnamespace下にある機能です

protobufがスキーマ定義に対応して自動生成してくれるC#コードは、スキーマ定義ごとにGoogle.Protobuf.IMessageを継承したクラスになっています
(以下、スキーマ定義に対応したprotobufが自動生成するC#クラスをメッセージと呼びます)

Google.Protobuf.IMessageの定義に着目するとGoogle.Protobuf.Reflection.MessageDescriptor Descriptor { get; } というプロパティが存在します

このGoogle.Protobuf.Reflectionにある機能を使うことで、メッセージの具体的な型に依存せずにそのメッセージの中身を調べることができます
※要するにprotobufの仕様に限定させたC#のReflection機能のようなものです

これを使えばメッセージの具象型に依存した実装をある程度省略することができそうです

メッセージを解析する

ではどのようにしてGoogle.Protobuf.Reflectionの機能を使っていくのか
protobufはメッセージ単位でデータを扱うので、メッセージから掘り下げていきましょう

下記のようなスキーマ定義を例にどのような内容が取れるのかを解説していきます

SampleMessage.proto
syntax = "proto3";
package Sample;

option csharp_namespace = "Sample.Messages";

// 列挙型の宣言(外側)
enum SampleEnum {
    Hoge = 0;
    Fuga = 1;
    Piyo = 2;
}

// メッセージの宣言(外側)
message ExternalMessage {
    float float_field = 1;
}

// メッセージの宣言
message SampleMessage {
    // フィールドの定義(Scalar Value Types)
    int32 int_field = 1;
    // フィールドの定義(Enum Type)
    SampleEnum enum_field = 2;
    // フィールドの定義(Message Type)
    InternalMessage message_field = 3;
    // フィールドの定義(Repeated Type)
    repeated string repeated_field = 4;
    // フィールドの定義(Map Type)
    map<int32, string> map_field = 5;
    // Oneof
    oneof oneof_field {
        string oneof_string = 11;
        uint32 oneof_uint = 12;
        ExternalMessage oneof_message = 13;
    }
    // 列挙型の宣言(入れ子)
    enum InternalEnum {
        First = 0;
        Second = 1;
        Third = 2;
    }
    // メッセージの宣言(入れ子)
    message InternalMessage {
        bool bool_field = 1;
    }
}

MessageDescriptor

先程も紹介したように、IMessageには MessageDescriptor Descriptor { get; } というプロパティが定義されています
ここからメッセージの内容を解析していくことができます
メッセージ内に定義されている内容が整理された状態でこのMessageDescriptorに詰まっています

以下に今回のケースで利用するものを列挙します

IMessage message = new SampleMessage(); // データの用意は省略
MessageDescriptor descriptor = message.Descriptor;

// メッセージの名前 SampleMessage
descriptor.Name;

// パッケージ名を含めた名前 Sample.SampleMessage
descriptor.FullName;

// C#上でのメッセージの型情報 typeof(Sample.Messages.SampleMessage)
descriptor.ClrType;

// 親のメッセージの情報 SampleMessageは入れ子に宣言されていないのでnull
descriptor.ContainingType;

// メッセージ内に定義されているEnum全て
descriptor.EnumTypes;

// メッセージ内に定義されているメッセージ全て
descriptor.NestedTypes;

// メッセージ内に定義されているフィールド全て ※後述で解説
descriptor.Fields;
// FindFieldByName/FindFieldByNumberメソッドで任意のフィールドを取得できる
FieldDescriptor field1 = message.Descriptor.FindFieldByName("int_field");
FieldDescriptor field2 = message.Descriptor.FindFieldByNumber(2);
// InDeclarationOrder/InFieldNumberOrderメソッドで中身を列挙できる
foreach(var fieldDescriptor in descriptor.Fields.InDeclarationOrder())
foreach(var fieldDescriptor in descriptor.Fields.InFieldNumberOrder())

// メッセージ内に定義されているOneof全て ※後述で解説
descriptor.Oneofs;

Enumを解析する

Enumは宣言されている定数の情報がメインになるため、今回の要件では解析することは必須ではありません
一応どのような情報が入っているか確認しましょう

EnumDescriptor

// SampleMessage内に宣言されているInternalEnumを取り出す
IMessage message = new SampleMessage(); // データの用意は省略
EnumDescriptor descriptor = message.EnumTypes[0];

// Enumの名前 InternalEnum
descriptor.Name;

// パッケージ名を含めた名前 Sample.SampleMessage.InternalEnum
descriptor.FullName;

// C#上でのEnumの型情報 typeof(Sample.Messages.SampleMessage.Types.InternalEnum)
descriptor.ClrType;

// 親のメッセージの情報 SampleMessageの入れ子になっているのでSampleMessageのMessageDescriptorが入っている
descriptor.ContainingType;

// 定義されている定数全て(EnumValueDescriptor)
descriptor.Values;

EnumValueDescriptor

// SampleMessage.protoに宣言されているSampleEnumを取り出す
// ※SampleMessageReflectionクラスのDescriptorプロパティからSampleMessage.protoファイルのFileDescriptorが取得できる
EnumDescriptor enumDescriptor = SampleMessageReflection.Descriptor.EnumTypes[0];

// SampleEnumの2番目に定義されている定数を取り出す
EnumValueDescriptor enumValueDescriptor = enumDescriptor.Values[1];

//  定数の名前 Fuga
enumValueDescriptor.Name;

// パッケージ名を含めた名前 Sample.SampleEnum.Fuga
descriptor.FullName;

// 定数の宣言順序 1 (0 origin)
enumValueDescriptor.Index;

// 定数の値 10
enumValueDescriptor.Number;

フィールドを解析する

繰り返しになりますが、今回の要件ではメッセージの型に依存せずに中身を読み書きしたいので、実際の値が入っているフィールドの中身が最重要です
詳しく掘り下げていきましょう

まず初めに、protobufのフィールドには大きく分類すると以下の種類が存在します

  • Scalar Value Types
  • Enum
  • Message
  • Repeated
  • Map

Oneofと呼ばれるものも存在しますが、これはGoogle.Protobuf.Reflectionでは厳密にはフィールドという扱いではないので別枠で解説します
1つずつ見ていきたいところですがまずはどのフィールドでも共通の内容を見ていきます

FieldDescriptor

フィールドの情報は FieldDescriptorで知ることができます
この中身を調べることによって対象のフィールドが前述のどの種別に値するのかを判断することができます

IMessage message = new SampleMessage(); // データの用意は省略
FieldDescriptor enumField = message.Descriptor.FindFieldByName("enum_field");
FieldDescriptor messageField = message.Descriptor.FindFieldByName("message_field");
FieldDescriptor boolField = messageField.MessageType.FindFieldByName("bool_field");
FieldDescriptor repeatedField = message.Descriptor.FindFieldByName("repeated_field");
FieldDescriptor mapField = message.Descriptor.FindFieldByName("map_field");

// フィールドの名前 ※C#上のプロパティ名ではなくprotoファイル内で宣言した名前
enumField.Name;     // enum_field
messageField.Name;  // message_field
boolField.Name;     // bool_field
repeatedField.Name; // repeated_field
mapField.Name;      // map_field

// パッケージ名を含めた名前
enumField.FullName;     // Sample.SampleMessage.enum_field
messageField.FullName;  // Sample.SampleMessage.message_field
boolField.FullName;     // Sample.SampleMessage.InternalMessage.bool_field
repeatedField.FullName; // Sample.SampleMessage.repeated_field
mapField.FullName;      // Sample.SampleMessage.map_field

// フィールドの種別
enumField.FieldType;     // FieldType.Enum
messageField.FieldType;  // FieldType.Message
boolField.FieldType;     // FieldType.Bool
repeatedField.FieldType; // FieldType.String ※repeated stringで宣言されているのでString扱い
mapField.FieldType;      // FieldType.Message ※map<,>で宣言されているのでMessage扱い

// protoファイル内でフィールド宣言時に設定した数値
enumField.FieldNumber;     // 2
messageField.FieldNumber;  // 3
boolField.FieldNumber;     // 1
repeatedField.FieldNumber; // 4
mapField.FieldNumber;      // 5

// FieldTypeがMessageだった場合にMessageDescriptorが入っている
enumField.MessageType;     // null
messageField.MessageType;  // InternalMessageのMessageDescriptor
boolField.MessageType;     // null
repeatedField.MessageType; // null
mapField.MessageType;      // MapFieldEntryのMessageDescriptor ※後述で解説

// FieldTypeがEnumだった場合にEnumDescriptorが入っている
enumField.EnumType;     // SampleEnumのEnumDescriptor
messageField.EnumType;  // null
boolField.EnumType;     // null
repeatedField.EnumType; // null
mapField.EnumType;      // null

// 親のメッセージの情報
enumField.ContainingType;     // SampleMessageのMessageDescriptor
messageField.ContainingType;  // SampleMessageのMessageDescriptor
boolField.ContainingType;     // InternalMessageのMessageDescriptor(messageField.MessageTypeと同じ)
repeatedField.ContainingType; // SampleMessageのMessageDescriptor
mapField.ContainingType;      // SampleMessageのMessageDescriptor

// Oneofに含まれている場合Oneofの情報が入る ※後述で解説
enumField.ContainingOneof;     // null
messageField.ContainingOneof;  // null
boolField.ContainingOneof;     // null
repeatedField.ContainingOneof; // null
mapField.ContainingOneof;      // null

// Repeated属性がある場合にtrueになる
enumField.IsRepeated;     // false
messageField.IsRepeated;  // false
boolField.IsRepeated;     // false
repeatedField.IsRepeated; // true
mapField.IsRepeated;      // true

// Map属性がある場合にtrueになる
enumField.IsRepeated;     // false
messageField.IsRepeated;  // false
boolField.IsRepeated;     // false
repeatedField.IsRepeated; // false
mapField.IsRepeated;      // true

// 実体にアクセスするためのラッパークラス ※後述で解説
enumField.Accessor;     // SingleFieldAccessor
messageField.Accessor;  // SingleFieldAccessor
boolField.Accessor;     // SingleFieldAccessor
repeatedField.Accessor; // RepeatedFieldAccessor
mapField.Accessor;      // MapFieldAccessor

フィールドの種別を判断するために必要な情報が分かったのでそれぞれ具体的な種別ごとに掘り下げていきましょう

Scalar Value Types

Scalar Value Typesにどのようなものが含まれるのかは下記の公式ドキュメントにまとまっています
https://developers.google.com/protocol-buffers/docs/proto3#scalar

  • double
  • float
  • int32
  • int64
  • uint32
  • uint64
  • sint32
  • sint64
  • fixed32
  • fixed64
  • sfixed32
  • sfixed64
  • bool
  • string
  • bytes

また、これらは前述で紹介したFieldDescriptor.FieldTypeで判断することができます

実体にアクセスするためにはFieldDescriptor.Accessorを使います
Scalar Value TypesなFieldTypeの場合、SingleFieldAccessorが使われており、GetValue()/SetValue()メソッドで値の読み書きができます

var message = new SampleMessage { IntField = 10 };
var field = message.Descriptor.FindFieldByName("int_field");
var intFieldValue = (int)field.Accessor.GetValue(message); // 中身は10
field.Accessor.SetValue(message, 20);
message.IntField; // 中身は20

unity appendix

// 宣言などは省略しています
var fieldValue = fieldDescriptor.Accessor.GetValue(parentMessage);
switch(fieldDescriptor.FieldType)
{
    case FieldType.Float:
        fieldValue = (float) EditorGUILayout.FloatField(fieldDescriptor.Name, (float) fieldValue);
        break;
    case FieldType.Int64:
    case FieldType.SInt64:
    case FieldType.SFixed64:
        fieldValue = (long) EditorGUILayout.LongField(fieldDescriptor.Name, (long) fieldValue);
        break;
    // それ以外のFieldTypeも同様にTypeごとに実装する...
}

fieldDescriptor.Accessor.SetValue(parentMessage, fieldValue);

Enum

Enumの場合もSingleFieldAccessorが使われているため、Scalar Value Typesと同じ方法で値の読み書きができます

unity appendix

ここで取り出した値はSystem.Enum型として扱うことができるので、今回の要件のようなUnityEditorで取り扱う際には下記のようにして標準のEditor拡張コードに組み込む事ができます

var enumValue = (Enum)enumField.Accessor.GetValue(parentMessage);
var updatedValue = EditorGUILayout.EnumPopup("Enum", enumValue);
enumField.Accessor.SetValue(parentMessage, updatedValue);

Message

Messageの場合も前述の2種同様SingleFieldAccessorが使われています
IMessageを継承したメッセージクラスが格納されているため、IMeesageにキャストした上で更にDescriptorで掘り下げることによって更に中身を1つずつ操作することが可能です

var childMessage = (IMessage)messageField.Accessor.GetValue(parentMessage);
foreach(var field in childMessage.Descriptor.Fields.InDeclarationOrder())
{
    // 実際にはFieldTypeごとに処理をしていく
    var fieldValue = field.Accessor.GetValue(childMessage);
    // modify ...
    field.Accessor.SetValue(childMessage, fieldValue);
}

protobufは任意でメッセージの入れ子を連続させて定義することができる仕様になっているので汎用的に対応するためにはメッセージに対する処理は再帰的に行う必要があります

Repeated

FieldDescriptor.IsRepeatedtrueだった場合、そのフィールドはRepeated属性があることになります
Repeated属性のフィールドはRepeatedFieldAccessorが使われますが、これはSetValue()に対応していません
値を書き換えるには少し工夫が必要になります

if (repeatedField.IsRepeated)
{
    // 実際はRepeatedField<T>型になっているがIListを実装しているためキャストが可能
    var repeatedValue = (IList)repeatedField.Accessor.GetValue(parentMessage);
    // IList的な扱いができる ※ただし要素はobject型
    var element0 = repeatedValue[0];
    repeatedValue.Add(addValue);
    foreach(var element in repeatedValue)

    // 要素の型はFieldTypeで取得できる ※通常のフィールドにrepeated属性が付いているという扱い
    var elementType = repeatedField.FieldType;
    // メッセージだった場合は更にMessageTypeを掘り下げることで具体的な型が判別できる
    if (elementType == FieldType.Message)
    {
        var messageType = repeatedField.MessageType.ClrType;
    }
    // Enumの場合も同様
    if (elementType == FieldType.Enum)
    {
        var enumType = repeatedField.EnumType.ClrType;
    }

    // 要素の具体的な型が判別できるのであればRepeatedField<T>にもキャスト可能
    if (elementType == FieldType.Int32)
    {
        var repeatedIntValue = (RepeatedField<int>)repeatedField.Accessor.GetValue(parentMessage);
    }
}

このようにして取得したIListRepeatedField<T>のインスタンスに対してAdd()Remove()などをしてあげることでRepeatedなフィールドの中身も書き換えることが可能になります

unity appendix

IListを継承しているのでUnityEditorInternal.ReorderableListをそのまま使うことができます(結構便利です

var repeatedValue = (IList)fieldDescriptor.Accessor.GetValue(parentMessage);
var elementType = GetSystemType(fieldDescriptor);
var reorderableList = new ReorderableList(repeatedValue, elementType);

...

Type GetSystemType(FieldDescriptor descriptor)
{
    switch(descriptor.FieldType)
    {
        case FieldType.Message;
            return descriptor.MessageType.ClrType;
        case FieldType.Enum;
            return descriptor.EnumType.ClrType;
        case FieldType.String;
            return typeof(string);
        // ScalarValueTypesはFieldTypeに合わせて直接typeofする 
    }
}

Map

実はmap<key,value>として宣言したものもRepeatedなフィールドとして扱われます
内部的にはMapFieldEntryなメッセージ型にRepeated属性を付けているという扱いになっているのです
https://developers.google.com/protocol-buffers/docs/proto3#maps

なので、フィールドごとに処理を分岐させる場合はRepeatedなフィールドよりも前にMapかどうかを判断する必要があります

// フィールドに対する何かしらの処理を行う関数
void ProccessField(FieldDescriptor field)
{
    if (field.IsMap)
    {
        // Mapの処理
    }
    else if (field.IsRepeated)
    {
        // Repeatedの処理
    }
    else
    {
        // SingleFieldAccessorに対応した処理
    }
}

MapなフィールドにはMapFieldAccessorが使われますが、これもRepeatedFieldAccessorと同様にSetValue()に対応していません
自分で中身を掘り下げていく必要があります

if (mapField.IsMap)
{
    // 実際はMapField<TKey, TValue>型になっているがIDictionaryを実装しているためキャストが可能
    var mapValue = (IDictionary)mapValue.Accessor.GetValue(parentMessage);
    // IDictionary的な扱いができる ※ただしkeyもvalueもobject型
    mapValue.Add(addKey, addValue);
    foreach(var kvp in mapValue)

    // keyとvalueの型はMapFieldEntryからそれぞれのFieldDescriptorを掘り起こす必要がある
    var mapFieldEntry = mapField.MessageType;
    var keyField = mapFieldEntry.FindFieldByName("key");
    var valueField = mapFieldEntry.FindFieldByName("value");
    // … ここから更にFieldType, MessageType, EnumTypeを見て最終的に判断する
}

Mapの仕様としてはkeyに設定できる型がint32,int64, sint32,sint64, uint32,uint64, fixed32,fixed64, sfixed32,sfixed64, bool,stringに限定されてはいるものの、真面目にIDictinary<TKey, TValue>の形式にキャストしようとするとかなり骨が折れます

扱うデータの仕様によってはMapの解析は対象外としてしまうのもアリでしょう

Oneofを解析する

フィールドの種別の話でOneofはGoogle.Protobuf.Reflectionでは厳密にはフィールドの扱いになっていないと話しました
これは、Oneofの定義に対しては専用のOneofDescriptorが存在し、そのOneof内で宣言されているフィールドに対してそれぞれFieldDescriptorが存在しているという仕様になっているためです

IMessage message = new SampleMessage(); // データの用意は省略
// 名前を指定してDescriptorを取得する
OneofDescriptor oneofField = message.Descriptor.FindDescriptor<OneofDescriptor>("oneof_field");
// 全てのOneofを列挙する
foreatch (var oneofDescriptor in message.Descriptor.Oneofs)
{
    // そのOneofに宣言されているフィールドを列挙する
    foreach (var field in oneofDescriptor.Fields)
    {
        // フィールド個別の処理...
    }
}

OneofDescriptor

このようにOneofには専用のOneofDescriptorが用意されていますが、値を読み書きする方法は少し特殊です

C#上ではOneof内に宣言されているフィールドに対応したプロパティに対して値を設定することで内部的にOneofの仕様に沿った状態で値を更新してくれるようになっています
また、各フィールドに対応したプロパティへはFieldDescriptorからアクセス可能になっています

これにより、新しくOneofとして扱いたいフィールドに対して値を設定することでOneofとしての値も書き換わったことになります

// SampleMessage.oneof_fieldの場合
IMessage message = new SampleMessage();
OneofDescriptor oneofField = message.Descriptor.FindDescriptor<OneofDescriptor>("oneof_field");

// 現在のOneofの値を取得する
var currentOneof = oneofField.Accessor.GetCaseFieldDescriptor(message);
var currentOneofValue = currentOneof.Accsessor.GetValue(message);

// 新しくOneofの値にexternal_messageを設定する ※MessageDescriptorからもOneofに属しているフィールドが取得できる
var externalMessageField = message.Descriptor.FindFieldByName("external_message");
externalMessageField.Accessor.SetValue(message, new ExternalMessage());

注意点

OneofDescriptorの項目で紹介したコードにもある通り、Oneofの中に定義したフィールドはそのOneofを定義したメッセージのフィールドとしても扱われているため、MessageDescriptor.FieldsOneofDescriptor.Fieldsのどちらにも存在していることになります
下記のようにしてContainingOneofをチェックすることで処理の重複を防ぎましょう

// メッセージに対する何かしらの処理を行う関数
void ProcessMessage(IMessage message)
{
    // フィールドの処理
    foreach (var field in message.Descriptor.Fields.InDeclarationOrder())
    {
        if (field.ContainingOneof != null)
        {
            continue;
        }

        // Oneofに含まれない通常のフィールドの処理
    }

    // Oneofの処理
    foreach (var oneof in message.Descriptor.Oneofs)
    {
        foreach (var field in oneof.Fields)
        {
            // Oneofに含まれるフィールドの処理
        }
    }
}

さいごに

Google.Protobuf.Reflectionを使ってメッセージの基本的な中身を掘り下げるコードをズラーッと紹介しました

今回紹介した以外にもまだReservedやOptionsなど、応用的な使い方がたくさん残っていますので、今後も別途それらの機能についても掘り下げていきたいと思います

最後まで読んでいただきありがとうございました

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ASP.NET Core3.0 RazorPages事始め(12)番外編 - Startup.csとProgram.cs

ASP.NET Core 3.0 Razor Pagesの公式チュートリアルをやって感じたのは、Startup.cs が一番の鬼門かな、ということ。

もう少し、Startup.csの理解を深めておく必要がありそうです。

僕もまだわかっていないことが多いので、もし間違い等あれば指摘していただけると嬉しいです。

Startup.cs

ということで、チュートリアルで作成した Startup.cs を開いてみます。
このクラスでは、アプリの動作を構成するコードを記述するようです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using RazorPagesMovie.Models;
using Microsoft.EntityFrameworkCore;

namespace RazorPagesMovie
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();

            services.AddDbContext<RazorPagesMovieContext>(options =>
                options.UseSqlite(Configuration.GetConnectionString("MovieContext")));
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }
    }
}

ConfigureServicesConfigureの2つのメソッドがあります。呼び出される順番は、ConfigureServicesConfigure の順です。

ConfigureServicesメソッド

コメントを読むと、ランタイムから呼び出されるメソッドで、このメソッド内で、コンテナにサービスを追加するコードを書くということのようです。

コンテナとサービスが何かが良くわかってませんが、このWebアプリに必要な機能をここで追加するということだと思います。ASP.NET Coreはプラガブル(といっていいのかな)な構造になっていて、開発者が必要な機能を明示的に組み込むようになっています。

最初の

services.AddRazorPages();

では、Razor pagesの機能を有効にしています。

次の

services.AddDbContext<RazorPagesMovieContext>(options => options.UseSqlite(Configuration.GetConnectionString("MovieContext")));

では、チュートリアルで作成した DBアクセスのための RazorPagesMovieContextをアプリケーションから利用できるようにするためにコードです。

SQLiteを利用し、その接続文字列は、構成ファイルの "MovieContext" から取得しています。
これによって、各ページモデルで RazorPagesMovieContextのインスタンスを生成する必要はなくなります。

チュートリアルのコードでは、

    public class IndexModel : PageModel
    {
        private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;

        public IndexModel(RazorPagesMovie.Models.RazorPagesMovieContext context)
        {
            _context = context;
        }
    ...

と、ページモデルのコンストラクタで、RazorPagesMovieContext のインスタンスを受け取っていましたが、ConfigureServicesメソッドで、サービスを登録していることでこれが実現できていたということですね。

AddEntityFrameworkStores といったメソッドも用意されているようです。これら Add で始まるメソッドは、IServiceCollectionインターフェースに対する拡張メソッドとして定義されています。

なお、ここで追加したサービスは、依存関係の挿入(DI) または ApplicationServices を利用して利用することができます。

Configure メソッド

Configure メソッドもランタイムから呼び出されます。
コメントには、このメソッドを使ってHTTPリクエストパイプラインを設定します、とあります。

ということは、この順番が意味をも持つってことですね。

まだ、完全に理解していないけど、

app.UseHttpsRedirection();

は、httpをhttpsにリダイレクトさせる。

app.UseStaticFiles();

は、静的ファイルを提供できるようにする。

app.UseRouting();

は、ルーティングを標準設定で構成する。

app.UseAuthorization();

は、認証を構成する。

ということをやっているようです。

Useで始まるメソッドは、IApplicationBuilder の拡張メソッドとして定義されています。

env.IsDevelopment

それと、Configureメソッドの最初では、env.IsDevelopmentの値を見て、if文で分岐させている個所があります。envは、引数で渡ってくる IWebHostEnvironment のインスタンスです。

開発環境と運用環境で動作を変更するために利用しています。

調べたところ、ASP.NET Core はアプリの起動時に環境変数 ASPNETCORE_ENVIRONMENT の値を読み込み、このプロパティの値を設定しているようです。

ASPNETCORE_ENVIRONMENT には、"Development"、"Staging"、"Production" という 3 つの値を指定できます。ASPNETCORE_ENVIRONMENT が設定されていない場合、既定で Production になります。

Visual Studio Codeの、launch.json を見ると、

    "configurations": [
        {
            "name": ".NET Core Launch (web)",
            "type": "coreclr",
            "request": "launch",
            "preLaunchTask": "build",
            "program": "${workspaceFolder}/bin/Debug/netcoreapp3.0/RazorPagesMovie.dll",
            "args": [],
            "cwd": "${workspaceFolder}",
            "stopAtEntry": false,
            "serverReadyAction": {
                "action": "openExternally",
                "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)"
            },
            "env": {
                "ASPNETCORE_ENVIRONMENT": "Development"
            },
            "sourceFileMap": {
                "/Views": "${workspaceFolder}/Views"
            }
        },

となっています。なので、VS Codeから起動する場合は、env.IsDevelopmentプロパティは、trueになります。

なお、env.IsDevelopmentプロパティが、falseの時には、

    app.UseExceptionHandler("/Error");
    app.UseHsts();

となっているので、例外発生時は、"/Error"にリダイレクトされるようです。つまり、Error.cshtml, Error.cshtml.cs が利用されるということですね。

app.UseHsts() は、 HSTS (Hypertext Strict Transport Security) をブラウザに通知するようにしているコードです。

Visual Studio IDEでは、プロジェクトのプロパティページでASPNETCORE_ENVIRONMENT 環境変数の値を設定できます。

appsettings.Development.json

そういえば、チュートリアルで appsettings.json についてすこし触れましたが、appsettings.Development.json というファイルもプロジェクトには存在していました。

開発時(ASPNETCORE_ENVIRONMENT=Development)には、appsettings.jsonの内容に、appsettings.Development.jsonの内容が上書きされて、利用されることになるようです。

利用される接続文字列は、appsettings.jsonに書かれているのですが、appsettings.Development.jsonにも書けば、デバッグ時は、

appsettings.Development.jsonに書かれているConnectionStringsの値が利用されるということですね。

つまり、開発中と運用で接続文字列を簡単に切り替えることができるということです。

web.configweb.debug.config,web.release.configとの関係に似ていますね。
でも、web.debug.config,web.release.configでの特殊な記法が必要ないので、理解しやすいですね。

Startup コンストラクター

Startコンストラクタは、以下のパラメータを受け取りことができます。

  • IHostingEnvironment (環境別にサービスを構成するため)。

  • IConfiguration (スタートアップ時にアプリケーションを構成するため)。

  • ILoggerFactory (ロギングを構成するため)

これらのコンストラクタは省略することもできます。

実際、チュートリアルで利用したコンストラクタは、以下のように IConfigurationだけを受け取っています。

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

全てを受け取る場合は、以下のように書きます。

        public Startup(IHostingEnvironment env, IConfiguration configuration, ILoggerFactory loggerFactory) {
            Configuration = configuration;
            _env = env;
            _loggerFactory = loggerFactory;
        }

        public IConfiguration Configuration { get; }

        private readonly IHostingEnvironment _env;
        private readonly ILoggerFactory _loggerFactory;

Program.cs

これまで見てきた Startupクラスは、Program.csのMainメソッドから呼び出されるCreateWebHostBuilderメソッドで指定されています。

チュートリアルで作成した Program.cs は、以下の通り。

using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Models;

namespace RazorPagesMovie
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;

                try
                {
                    SeedData.Initialize(services);
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred seeding the DB.");
                }
            }

            host.Run();

        }

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

ドキュメントによると、Mainメソッドで、ホスト といわれるものを組み立てています。ホストとは、以下をカプセル化するオブジェクトです。

  • HTTP サーバーの実装
  • ミドルウェア コンポーネント
  • ログの記録
  • DI
  • 構成

上記のコードでは、

  • Web サーバーとして Kestrel を使用し、IIS 統合を有効にする。
  • appsettings.json、"appsettings.{環境名}.json"、環境変数、コマンド ライン引数、およびその他の構成ソースから構成を読み込む。
  • ログ出力をコンソールとデバッグ プロバイダーに送る。

というオプションとともにホストを構成しています。

なお、サービスの追加とリクエストパイプラインの構成以外の初期化が必要ならば、Program.cs で行うってことですね。

Mainメソッドでは、DIが利用できないので、

SeedData.Initialize(services);

var logger = services.GetRequiredService<ILogger<Program>>();

のように IServiceProvider のインスタンスを使って、サービスにアクセスしているってことですね。

デバッグで確かめたところ

Main → CreateHostBuilder → Startup.ConfigureServices → SeedData.Initialize → host.Run() → Startup.Configure

の順で呼び出されていました。

Startup クラスがすこし理解できたように思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityで2つのベクトルの向きを同じにするように回転させる

はじめに

Unityでスクリプトからオブジェクトを回転させるときに
そのオブジェクトが持つベクトルとあるベクトルの向きを同じにするように回転させました。
たまに使うので備忘録です。

サンプルコードとイメージ図

VectorRotationSample.cs
    // Vector3 p1, p2, p3, p4;
    // GameObject target;
    Vector3 refVec = (p1 - p2).normalized; // これと同じになるように
    Vector3 vec = (p3 - p4).normalized; // これを動かす(p3, p4はtargetに含まれる任意の2点)
    Quaternion rot = Quaternion.FromToRotation(refVec, vec);

    target.transform.rotation = rot * target.transform.rotation; // 左から掛ける

image.png

p3とp4のベクトルの向きがp1とp2のベクトルの向きに合うようにp3とp4を持つオブジェクトを回転させています。

使用例

こんな感じの用途に使いました

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityで2つのベクトルの向きが同じになるように回転させる

はじめに

Unityでスクリプトからオブジェクトを回転させるときに
そのオブジェクトが持つベクトルとあるベクトルの向きを同じになるように回転させました。
たまに使うので備忘録です。

サンプルコードとイメージ図

VectorRotationSample.cs
    // Vector3 p1, p2, p3, p4;
    // GameObject target;
    Vector3 refVec = (p1 - p2).normalized; // これと同じになるように
    Vector3 vec = (p3 - p4).normalized; // これを動かす(p3, p4はtargetに含まれる任意の2点)
    Quaternion rot = Quaternion.FromToRotation(refVec, vec);

    target.transform.rotation = rot * target.transform.rotation; // 左から掛ける

image.png

p3とp4のベクトルの向きがp1とp2のベクトルの向きに合うようにp3とp4を持つオブジェクトを回転させています。

使用例

こんな感じの用途に使いました。
右手と左手間のベクトルに合うようにコントローラを回転させています

関連文献

【Unity】Quaterion API解説

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python for .NETを使ってPandas DataFrame から System.Data.DataTableへ変換する

Pythonと.NETの表データ交換

そもそもPython -> .NETでデータフレームを交換する場合、.NET側はDataTableという標準ライブラリのクラスがあるので、それが適しています。決してPandasやdplyrのような高機能なデータ処理能力は無いのですが、とりあえずマッピングすることができます。
BayesServerでPythonからも呼びたい場合は、以下にあるようにJPypeを使ったJavaライブラリへのアクセスを推奨されているようです。

Setup Python

一方で、pythonnetでもいいよ、と一言書いてあり、何で.NETネイティブなのに、Javaラッパーを通さないといけないのかよくわからないので、そちらで行くことにしました。RのラッパーもJavaラッパーを通します。これだと、APIリファレンスを2種類見ないといけない羽目になります。(どっちみち.NETライブラリに改造を加えて、再ラップするので)
最初からPythonにしておけば良かった。。。

pythonnet

pythonnet はPython for .NETと呼ばれる、Pythonから.NETのアクセスライブラリです。
http://pythonnet.github.io/
Pythonから.NETを呼び出す場合、.NETからPythonを呼び出す場合の双方で使えますね。

Python + .NETですと、IronPythonを思い出す人もいるのでしょうけども、そちらは.NET上で動くCLI言語です。これだと、データサイエンスなどで定評のあるライブラリも対応は限られます。というかnumpyが動きません。

pythonnetは言語間の接続を行うだけなので、両者の良いところを残したまま使えます。最新の.NET Coreなどにも対応します。
感覚的に言うと、

  • Pythonから、.NETのライブラリを using ではなく importで使える。
import clr
clr.AddReference("System.Windows.Forms")
from System.Windows.Forms import Form

import clrというのが趣があります。

  • .NETから、Pythonのライブラリを .NET 上でimportできる
dynamic np = Py.Import("numpy");
var x = np.cos(np.pi * 2);

こっち方向は、 @hogegex さんの記事があります。

.NET (C#)からpythonを呼び出す

用途としては、Pythonから.NET独自のライブラリ(ビジネス系など)を呼び出したり、.NETから数値演算のタスクを投げたりなどが考えられます。Python上で.NETのGUIライブラリを使って、アプリを作れるのは中々良さそうですね。
ただ、私の用途はあくまでBayesServerのライブラリをPythonから呼ぶだけなので、以下のことができれば、おおむね完了です。

Python DataFrame -> .NET DataTable変換

BayesServerのリファレンスにはヘルパークラスとして、jpype1を使ったデータフレームのマッピングヘルパー関数が用意されています。

Pandas DataFrame helper functions

おそらく、jpype1を使った変換は癖があるので、こういったものを用意してくれていると思うのですが、pythonnetだともっとストレートに書けます。
ほぼ、コピペで.NET向けに書き直してみました。

# %%
import numpy as np
import pandas as pd
import clr
from System.Data import *
# %%
def _to_net_class(data_type):
    """
    Converts numpy data type to equivalent .NET class
    :param data_type: the numpy data type
    :return: The Net Class
    """
    if data_type == np.int32:
        return clr.GetClrType(Int32)  
    if data_type == np.int64:
        return clr.GetClrType(Int64)  
    if data_type == np.float32:
        return clr.GetClrType(Single) 
    if data_type == np.float64:
        return clr.GetClrType(Double) 
    if data_type == np.bool:
        return clr.GetClrType(Boolean) 
    if data_type == np.object:
        return clr.GetClrType(Object) 

    raise ValueError('dtype [{}] not currently supported'.format(data_type))

# %%
def to_data_table(df):
    data_table = DataTable()

    for name, data_type in df.dtypes.iteritems():
        net_class = _to_net_class(data_type)
        data_table.Columns.Add(str(name),net_class)

    for index, row in df.iterrows():
        xs = [None if pd.isnull(x) else x for x in row]
        data_table.Rows.Add(xs)

    return data_table

これで、pandasのデータをするっと.NETライブラリに持ち込めるので、いわゆるExcel表のようなGUIや、データベースとの交換も楽になるかもです。
clr.GetClrType()というのがtypeof(やObject.GetType())の代わりみたいなものだと思ってください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ASP.NET Core / ASP.NET Web API 2 Owin で Web API の自動テスト環境を整える

概要

C# でバックエンド開発を行う場合、近年では、やはりWeb APIによる開発事例が多いかと思います。
本記事では、C# における Web API 開発フレームワークの代表格である ASP.NET Core、及び ASP.NET API 2 Owin を題材とし、Azure DevOpsを使って自動テスト環境を整えるまでの流れを紹介します。

お品書き

  1. ユニットテストを実装する
  2. Azure DevOpsでビルドパイプラインを作成する
  3. Azure DevOpsのテスト結果をSlackへ通知する

環境

  • Windows10
  • Visual Studio 2019
  • ASP.NET Core
  • ASP.NET Web API 2 Owin
  • Azure DevOps

サンプルコード

サンプルコードを Github にアップしています。
一部、今回の記事と無関係な実装も含まれていますが、ご了承ください。

ASP.NET Core
https://github.com/tYoshiyuki/dotnet-core-mediatr-sample

ASP.NET Web API 2 Owin
https://github.com/tYoshiyuki/dotnet-owin-webapi-sample

ユニットテストを実装する

ASP.NET で Web API を作成した場合、APIのエンドポイントとなる 「コントローラ」 と、業務ロジックを担当する 「ロジック」 のレイヤーを分ける事が多いかと思います。
今回は、上記レイヤー分けに従って、ユニットテストを MsTest 及び xUnit を使って実装していきます。

テストのカテゴリ分けについて

C# のテストフレームワークでユニットテストを実装する場合、テストの内容や種別によってカテゴリ分けをすることをお勧めします。

カテゴリ分けをすることにより、Visual Studio のテストエクスプローラーで表示をフィルタリングしたり、テスト結果の分析を行うことが出来るようになります。ユニットテストの総ケース数が増えてきた場合でも、柔軟なテスト運用が可能となります。テストケース総数を勘案した上で、カテゴリ分けを設計すると良いかと思います。

以下、カテゴリ分けの例になります。

  • 権限の名称

    • 一般ユーザ, 管理ユーザ など
  • テスト種別名

    • ロジックのテスト, Web APIサーバを用いたインテグレーションテスト など
  • 処理時間

    • 通常のテスト, 処理時間の長いテスト
  • 業務ドメインの名称

1. ロジックテストを実装する

MsTest

まずは、ASP.NET Web API 2 Owin / MsTest の実装例です。
テストカテゴリは、アノテーションで指定します。MsTest v1 ではメソッド単位にしかカテゴリを付与できませんでしたが、
MsTest v2 からクラス単位にカテゴリを付与出来るようになっています。

TodoServiceTest.cs
    [TestClass]
    [TestCategory("Todo"), TestCategory("Logic")]
    public class TodoServiceTest
    {
        // ・・・一部省略

前述した通りですが、テストエクスプローラーでカテゴリに従い、表示項目をフィルタリングすることが出来ます。
テストケース数が膨大になった場合は、有効な機能になるため活用しましょう。
image.png

テストパターンが類似しているテストケースについては、パラメタライズドテストを検討すると良いです。
MsTest v2 はパラメタライズドテストに対応しています。実装例を以下に示します。
インプットデータや期待値を纏めてパラメータとすることで、データパターンを網羅したテストを効率よく実装出来ます。

TodoServiceTest.cs
        [DataTestMethod]
        [DynamicData(nameof(TestData), DynamicDataSourceType.Method)]
        public void Update_正常系(Todo todo)
        {
            // Arrange
            _mock.Setup(_ => _.Get())
                .Returns(_data);
            _service = new TodoService(_mock.Object);

            // Act
            _service.Update(todo);

            // Assert
            var expect = _service.Get(todo.Id);
            Assert.AreEqual(todo.Id, expect.Id);
            Assert.AreEqual(todo.Description, expect.Description);
        }

        public static IEnumerable<object[]> TestData()
        {
            yield return new object[] { new Todo { Id = 1, Description = "Test 991", CreatedDate = DateTime.Now } };
            yield return new object[] { new Todo { Id = 2, Description = "Test 992", CreatedDate = DateTime.Now } };
            yield return new object[] { new Todo { Id = 3, Description = "Test 993", CreatedDate = DateTime.Now } };
        }

上記は、DynamicData のパターンを紹介していますが、それ以外にも DataRow を利用したり、
データソースをカスタマイズしたりと様々な例がありますので、興味のある方は公式ドキュメントを確認することをお勧めします。
https://github.com/Microsoft/testfx-docs

xUnit

次は、ASP.NET Core / xUnit の実装例です。
テストカテゴリは、Traitアノテーションで指定します。MsTestと異なり、キー(name)・バリュー(value)のような形で設定します。

InMemoryUserRepositoryTests.cs
    [Trait("Category", "Logic")]
    public class InMemoryUserRepositoryTests
    {
        // ・・・一部省略

テストメソッドは Fact アノテーション で実装します。

InMemoryUserRepositoryTests.cs
        [Fact]
        public void FindAll()
        {
            // Arrange
            PrepareUsers();
            var expect = _users;

            // Act
            var result = _userRepository.FindAll().ToList();

            // Assert
            result.Count.Is(3);
            foreach (var user in expect)
            {
                var target = result.First(_ => _.UserId.Equals(user.UserId));
                target.UserId.Is(user.UserId);
                target.UserName.Is(user.UserName);
                target.FullName.Is(user.FullName);
            }
        }

続いて、パラメタライズドテストの実装例です。パラメタライズドテストには Theory アノテーションを使用します。
MsTest v2 とほぼ同じように記載が可能です。

InMemoryUserRepositoryTests.cs
        [Theory]
        [MemberData(nameof(TestData))]
        public void RemoveTest(User user)
        {
            // Arrange
            PrepareUsers();
            var expect = user;

            // Act
            _userRepository.Remove(expect);

            // Assert
            _userRepository.Find(expect.UserId).IsNull();
        }

        public static IEnumerable<object[]> TestData()
        {
            yield return new object[] { new User(new UserId("1"), new UserName("Taro"), new FullName("Taro", "Yamada")) };
            yield return new object[] { new User(new UserId("2"), new UserName("Jiro"), new FullName("Jiro", "Suzuki")) };
            yield return new object[] { new User(new UserId("3"), new UserName("Saburo"), new FullName("Saburo", "Tanaka"))};
        }

インテグレーションテストを実装する

続いて、コントローラから一気通貫でテストを行うインテグレーションテストを実装します。
Web API を呼び出すためには HTTPサーバ が必要になりますが、.NETのユニットテストでは専用の TestServer があるため、これを利用すると、いい感じにテストの実装が出来ます。
image.png

以下、ASP.NET Web API 2 Owin の実装例です。

TodoControllerTest.cs
        [ClassInitialize]
        public static void Setup(TestContext context)
        {
            Server = TestServer.Create<Startup>();
            HttpClient = Server.HttpClient;
        }

        [TestMethod]
        public async Task Get_正常系()
        {
            // Arrange・Act
            var response = await HttpClient.GetAsync(_url);

            // Assert
            Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
            var result = await response.Content.ReadAsAsync<List<Todo>>();
            Assert.IsTrue(result.Any());
        }

ASP.NET Core の場合は、WebApplicationFactoryを利用します。
WebApplicationFactory を継承したクラスを準備します。(尚、サンプルソースでは初期データの投入も行っています。)

IntegrationTestWebApplicationFactory.cs
    public class IntegrationTestWebApplicationFactory<TStartup>
        : WebApplicationFactory<TStartup> where TStartup : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            base.ConfigureWebHost(builder);
            builder.ConfigureServices(services =>
            {
                var sp = services.BuildServiceProvider();
                using var scope = sp.CreateScope();
                var repository = scope.ServiceProvider.GetRequiredService<IUserRepository>();
                repository.Save(new User(new UserId("1"), new UserName("Taro"), new FullName("Tanaka", "Tanaka")));
                repository.Save(new User(new UserId("2"), new UserName("Jiro"), new FullName("Suzuki", "Suzuki")));
                repository.Save(new User(new UserId("3"), new UserName("Saburo"), new FullName("Sato", "Sato")));
            });
        }
    }


次に、WebApplicationFactory をユニットテストで利用した場合の実装例です。
WebApplicationFactory を継承したクラスを、コンストラクタインジェクションで受け取り、そこから HTTP Client を取得し、HTTPリクエストを実行するような感じです。

UsersControllerTest.cs
        public UsersControllerTest(IntegrationTestWebApplicationFactory<Startup> webApplicationFactory)
        {
            _client = webApplicationFactory.CreateClient();
        }

        [Fact]
        public async Task Get()
        {
            // Arrange
            const string url = "/api/users/1";

            // Act
            var response = await _client.GetAsync(url);

            // Assert
            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
            var json = await response.Content.ReadAsStringAsync();
            var result = JsonSerializer.Deserialize<UserViewModel>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

            Assert.Equal("1", result.Id);
            Assert.Equal("Tanaka", result.FirstName);
            Assert.Equal("Taro", result.UserName);
        }

Azure DevOpsでビルドパイプラインを作成する

続いて、Azure DevOpsでビルドパイプラインを作成します。パイプラインの作成方法は、旧形式では GUI で作成する必要がありましたが、現在では YAML での作成が可能となっています。今回は、ビルドパイプラインでユニットテストの実行とカバレッジレポートを取得する方法を紹介します。

まずは、GUIでの作成例です。ASP.NET Web API 2 Owin のプロジェクトをサンプルに作成しています。

image.png

ポイントとしては、テストの実行・カバレッジの取得 (Test and output coverage) と レポートの出力 (Generate coverage report)です。

カバレッジの取得には OpenCover を使用します。Nugetより取得しましょう。

image.png

OpenCover より MsTest をコマンドラインで実行し、カバレッジの取得を行います。
テスト対象となる DLL の指定や、カバレッジ取得対象とする 名前空間 をコマンドラインのパラメータで指定します。
また、カバレッジの取得は デバッグビルド で実行する必要があるため注意が必要です。

image.png

以下、設定内容の詳細 YAML になります。

steps:
- script: |
   %OPEN_COVER_PATH% -register -target:%MSTEST_PATH% -targetargs:%TARGET_FILE_AND_ARGS% -targetdir:%TARGET_DIR% -filter:%FILTER% -output:%OUTPUT_FILE%

  displayName: 'Test and output coverage'
  env:
    MSTEST_PATH: "C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe"
    TARGET_DIR: ".\DotNetOwinWebApiSample.Api.Test\bin\Debug"
    TARGET_FILE_AND_ARGS: "DotNetOwinWebApiSample.Api.Test.dll /Logger:trx;LogFileName=DotNetOwinWebApiSample.Api.Test.trx"
    FILTER: "+[DotNetOwinWebApiSample*]* -[*.Test.*]*"
    OUTPUT_FILE: "coverage.xml"
    OPEN_COVER_PATH: ".\packages\OpenCover.4.7.922\tools\OpenCover.Console.exe"

OpenCover の設定 (特にfilter) に関しては、若干クセがあるので、公式のドキュメントを読んでおくと良いです。
https://github.com/opencover/opencover/wiki/Usage

続いてカバレッジレポートの取得です。
ReportGenerator を Visual Studio Marketplace から取得し、Azure DevOps に追加しましょう。

image.png

上記拡張を追加すると、ReportGenerator のタスクを作成出来るようになります。

image.png

steps:
- task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
  displayName: 'Generate coverage report'

パイプラインの実行後、Code Coverage のタブが追加され、カバレッジレポートが HTML で閲覧出来るようになります。
Tests のタブと合わせて確認することで、効率的にテスト結果を可視化することが出来ます。

image.png

次に、YAMLでの作成例です。
ASP.NET Core のプロジェクトをサンプルに実装します。

trigger:
- master

pool:
  vmImage: 'windows-latest'

variables:
  buildConfiguration: 'Debug'

steps:
- script: dotnet restore
  displayName: 'dotnet restore'

- script: dotnet build --configuration $(buildConfiguration)
  displayName: 'dotnet build $(buildConfiguration)'

- task: DotNetCoreCLI@2
  inputs:
    command: test
    projects: '*.Test/*.Test.csproj'
    arguments: -c $(BuildConfiguration) --collect:"XPlat Code Coverage" -- RunConfiguration.DisableAppDomain=true
  displayName: Run Tests

- task: DotNetCoreCLI@2
  inputs:
    command: custom
    custom: tool
    arguments: install --tool-path . dotnet-reportgenerator-globaltool
  displayName: Install ReportGenerator tool

- script: reportgenerator -reports:$(Agent.TempDirectory)/**/coverage.cobertura.xml -targetdir:$(Build.SourcesDirectory)/coverlet/reports -reporttypes:"Cobertura"
  displayName: Create reports

- task: PublishCodeCoverageResults@1
  displayName: 'Publish code coverage'
  inputs:
    codeCoverageTool: Cobertura
    summaryFileLocation: $(Build.SourcesDirectory)/coverlet/reports/Cobertura.xml  

- task: DotNetCoreCLI@2
  displayName: 'dotnet publish $(buildConfiguration)'
  inputs:
    command: publish
    publishWebProjects: True
    arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
    zipAfterPublish: True

- task: PublishBuildArtifacts@1
  displayName: 'publish artifacts'

ASP.NET Core の場合、dotnet test のコマンドでカバレッジの取得が可能です。
また、reportgeneratorもコマンドでインストール出来るため、ASP.NET Web API 2 Owinに比べてシンプルにパイプラインが作成出来ます。

.NET Core のパイプラインに関しては、公式にもドキュメントがあります。導入の際には、合わせて確認いただくと良いかと思います。
https://docs.microsoft.com/en-us/azure/devops/pipelines/ecosystems/dotnet-core?view=azure-devops

Azure DevOpsのテスト結果をSlackへ通知する

最後に、Azure DevOps と Slack 連携し、ビルドパイプラインによるテスト結果を通知してみます。
Azure DevOps と Slack は専用の連携アプリがあるため、これを利用します。
詳細な手順は公式のドキュメントがあるため、本記事では割愛します。
https://docs.microsoft.com/en-us/azure/devops/pipelines/integrations/slack?view=azure-devops

設定が完了すると、画像の通りにビルド完了時にSlackへ通知が送付されます。

image.png

しかし、上記通知内容からではテスト成功件数、失敗件数といった詳細な結果を知ることが出来ません。
勿論、メッセージのリンクから Azure DevOps へ遷移する事は可能ですが、今回は通知内容を Incoming Webhooks を利用してカスタマイズしてみます。

上記、ASP.NET Web API 2 Owinのパイプラインを用いて、Incoming Webhooksの送信を作成します。
PowerShellを使って、ビルドパイプライン内でSlackへの通知を行いましょう。


# ---------------------------------------------------
#  テスト実行結果のメッセージを構築します
# ---------------------------------------------------
function CreateTestResultMessage($file) {
    $filename = $file.name
    $xml = [XML](Get-Content $file)

    $start = $xml.TestRun.Times.start
    $finish = $xml.TestRun.Times.finish
    $executed = $xml.TestRun.ResultSummary.Counters.executed
    $passed = $xml.TestRun.ResultSummary.Counters.passed
    $failed = $xml.TestRun.ResultSummary.Counters.failed

    return "テスト[" + $filename + "]の実行結果だよー" +  `
    "`n" + "> 開始時刻: " + ([DateTime]$start).ToString("yyyy/MM/dd HH:mm:ss") + `
    "`n" + "> 終了時刻: " + ([DateTime]$finish).ToString("yyyy/MM/dd HH:mm:ss") + `
    "`n" + "> 実行件数: " + $executed + `
    "`n" + "> 成功件数: " + $passed + `
    "`n" + "> 失敗件数: " + $failed
}

# ---------------------------------------------------
#  カバレッジ取得結果のメッセージを構築します
# ---------------------------------------------------
function CreateCoverageMessage($file) {
    $filename = $file.name
    $xml = [XML](Get-Content $file)

    $sequenceCoverage = $XML.CoverageSession.Summary.sequenceCoverage
    $branchCoverage = $XML.CoverageSession.Summary.branchCoverage
    $numSequencePoints = $XML.CoverageSession.Summary.numSequencePoints
    $visitedSequencePoints = $XML.CoverageSession.Summary.visitedSequencePoints
    $numBranchPoints = $XML.CoverageSession.Summary.numBranchPoints
    $visitedBranchPoints = $XML.CoverageSession.Summary.visitedBranchPoints

    return "テストカバレッジ[" + $filename + "]の実行結果だよー" +  `
    "`n" + "> Sequence Coverage: " + $sequenceCoverage + "% " + "(" + $visitedSequencePoints + "/" + $numSequencePoints + ")" + `
    "`n" + "> Branch Coverage: " + $branchCoverage + "% " + "(" + $visitedBranchPoints + "/" + $numBranchPoints + ")"
}

# ---------------------------------------------------
#  Slackへ通知します
# ---------------------------------------------------
function PostSlack($message) {
    $encode = [System.Text.Encoding]::GetEncoding('ISO-8859-1')
    $utf8Bytes = [System.Text.Encoding]::UTF8.GetBytes($message)

    $notificationPayload = @{ 
        text = $encode.GetString($utf8Bytes);
        username = "Azure DevOps Test Report"; 
        icon_url = "https://4.bp.blogspot.com/-CtY5GzX0imo/VCIixcXx6PI/AAAAAAAAmfY/AzH9OmbuHZQ/s800/animal_penguin.png"
    }

    $postUri = "xxx" # Incoming WebhooksのエンドポイントURLを設定します
    Invoke-RestMethod -Method POST -Uri $postUri -Body  (ConvertTo-Json $notificationPayload) -ContentType application/json
}

# テスト実行結果の送信
$files = Get-ChildItem -Recurse -File -Include *.trx
Foreach ($file in $files) {
    $message = CreateTestResultMessage($file)
    PostSlack($message)
}

# カバレッジ取得結果の送信
$files = Get-ChildItem -Recurse -File -Include coverage.xml
Foreach ($file in $files) {
    $message = CreateCoverageMessage($file)
    PostSlack($message)
}

ポイントとして、テスト実行結果は .trx ファイル、カバレッジ取得結果は coverage.xml にそれぞれ存在するため、
XMLパーサーを使用して値の読み取りを行っています。カバレッジ取得結果は、利用したツールによってフォーマットが異なっており、OpenCover以外の別のツールを用いた場合は適宜調整が必要なため、ご注意ください。

image.png

また、同様に Microsoft Teams に対してもメッセージの送信が可能です。


# ---------------------------------------------------
#  Teamsへ通知します
# ---------------------------------------------------
function PostTeams($message) {
    $encode = [System.Text.Encoding]::GetEncoding('ISO-8859-1')
    $utf8Bytes = [System.Text.Encoding]::UTF8.GetBytes($message)

    $notificationPayload = @{ 
        text = $encode.GetString($utf8Bytes);
    }

    $postUri = 'xxx' # Incoming WebhooksのエンドポイントURLを設定します

    Invoke-RestMethod -Method POST -Uri $postUri -Body  (ConvertTo-Json $notificationPayload) -ContentType application/json
}

image.png

まとめ

後半、C# というよりも Azure DevOps の記事が中心になってしまいました。。。
C#でモダンな継続的開発を行う場合は、ユニットテストの実装とAzure DevOpsの運用がポイントになってくると思います。
本記事が、少しでも皆様の参考情報となれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者向け】処理負荷の軽減に繋がるコーディング【Unity】

簡単だけど意識しておくだけで負荷軽減に繋がるコードの書き方を紹介します。

1.毎回取得する必要のないものはキャッシュする

    void Start()
    {
    }

    void Update()
    {
      // 毎回呼ばれる
        Text text = GetComponent<Text>();

        // テキストの文字を変更
        text.text = "文字";
    }

    // キャッシュ用メンバ変数
    Text m_text = null;

    void Start()
    {
        // 最初の一回だけGetComponentを呼び出しキャッシュする
        m_text = GetComponent<Text>();
    }

    void Update()
    {
        // テキストの文字を変更
        m_text.text = "文字";
    }

2.毎回処理する必要のないものはUpdateに書かない

    Text m_text = null;      
    void Start()
    {
        m_text = GetComponent<Text>();
    }

    void Update()
    {
        // テキストの文字を変更
        m_text.text = "文字";
    }

    Text m_text = null;      
    void Start()
    {
        m_text = GetComponent<Text>();
        // テキストの文字を変更
        m_text.text = "文字";
    }

   // 不要なUpdate関数は削除しましょう

   

頭の隅に入れて書くだけでも後々負荷軽減に繋がりチームメンバーからいいねを貰えると思います。

偉大な公式の最適化マニュアル

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C# NuGetパッケージ

対象読者

C#初心者だけど、少しコード分かる。

NuGetとは?

外部パッケージを取得するためのシステム。取得することによって外部パッケージが使えるようになる。

具体的に

macになるが、プロジェクトのManager NuGet Packagesって書かれたところをクリックすると下画像のようなものが出てくる。

スクリーンショット 2019-12-01 16.30.51.png

ここでダウンロードしたいものを選択。一番上のNewtonsoft.Jsonをダウンロード。これはJsonを扱うためのパッケージである。
これで、Newtonsoft.Jsonのパッケージが使えるようになる。

具体的なパッケージ

以下自分が使ったことのあるパッケージを書いていく。
ただ現状少ないので、ちまちま新しいものに挑戦しつつ追加していく。

Newtonsoft.Jsonパッケージ

今までと同じように、usingで新しくパッケージを使うことができる。

ObjectからJson,JsonからObject
Program.cs
using System;
using Newtonsoft.Json;

namespace Sample
{
    class Program
    {
        static void Main(string[] args)
        {
            var Apple = new Fruit("Apple", 5, 180);
            var Json = JsonConvert.SerializeObject(Apple);
            Console.WriteLine(Json);
            Console.WriteLine("**********");
            var Obj = JsonConvert.DeserializeObject<Fruit>(Json);
            Console.WriteLine(Obj);
        }
    }

    public class Fruit
    {
        public Fruit(string Name, int Number, int Price)
        {
            this.Name = Name;
            this.Number = Number;
            this.Price = Price;
        }

        public override string ToString()
        {
            var Result = "Name = " + Name +
                         ",Number = " + Number +
                         ",Price = " + Price;
            return Result;
        }

        public string Name { get; set; }
        public int Number { get; set; }
        public int Price { get; set; }
    }
}

実行結果

{"Name":"Apple","Number":5,"Price":180}
**********
Name = Apple,Number = 5,Price = 180

JsonConvert.SerializaObject(object)でobjctをjsonに変換する。
JsonConvert.DeserializaObject(json)でjsonをobjectに変換する。

ListからJson,JsonからList
Program.cs
using System;
using System.Collections.Generic;
using Newtonsoft.Json;

namespace Sample
{
    class Program
    {
        static void Main(string[] args)
        {
            var Apple = new Fruit("Apple", 5, 180);
            var Banana = new Fruit("Banana", 8, 220);
            var Cherry = new Fruit("Cherry", 4, 190);
            var list = new List<Fruit>() { Apple, Banana, Cherry };
            //var array = new Fruit[] { Apple, Banana, Cherry };
            var Json = JsonConvert.SerializeObject(list);
            Console.WriteLine(Json);
            Console.WriteLine("**********");
            var Objs = JsonConvert.DeserializeObject<List<Fruit>>(Json);
            foreach(var item in Objs)
            {
                Console.WriteLine(item);
            }
        }
    }

実行結果

[{"Name":"Apple","Number":5,"Price":180},{"Name":"Banana","Number":8,"Price":220},{"Name":"Cherry","Number":4,"Price":190}]
**********
Name = Apple,Number = 5,Price = 180
Name = Banana,Number = 8,Price = 220
Name = Cherry,Number = 4,Price = 190

Fruitクラスは上で定義しているので省略。
listでも配列でもjsonは同様のものとなる。

System.Data.SQLite.Core

Program.cs
using System;
using System.Collections.Generic;
using System.Data.SQLite;

namespace ConsoleApp7
{
    class Program
    {
        static void Main(string[] args)
        {
            Create();
            var fruit = new Fruit();
            fruit.Id = 1;
            fruit.Name = "Apple";
            fruit.Number = 3;
            fruit.Price = 280;
            INSERT(fruit);
            fruit = new Fruit();
            fruit.Id = 2;
            fruit.Name = "Banana";
            fruit.Number = 8;
            fruit.Price = 180;
            INSERT(fruit);
            All();
            DELETE();
        }

        public static void Create()
        {
            var ConnectionStr = new SQLiteConnectionStringBuilder() { DataSource = "test.db" };
            using (var Connection = new SQLiteConnection(ConnectionStr.ToString()))
            {
                Connection.Open();

                using(var Command = new SQLiteCommand(Connection))
                {
                    Command.CommandText = "CREATE TABLE IF NOT EXISTS fruit(id INTEGER, name TEXT, number INTEGER, price INTEGER)";
                    Command.ExecuteNonQuery();
                }
            }
        }

        public static void DELETE()
        {
            var ConnectionStr = new SQLiteConnectionStringBuilder() { DataSource = "test.db" };
            using (var Connection = new SQLiteConnection(ConnectionStr.ToString()))
            {
                Connection.Open();

                using (var Command = new SQLiteCommand(Connection))
                {
                    Command.CommandText = "DELETE FROM fruit";
                    Command.ExecuteNonQuery();
                }
            }
        }

        public static void INSERT(Fruit fruit)
        {
            var ConnectionStr = new SQLiteConnectionStringBuilder() { DataSource = "test.db" };
            using (var Connection = new SQLiteConnection(ConnectionStr.ToString()))
            {
                Connection.Open();

                using (var Command = new SQLiteCommand(Connection))
                {
                    Command.CommandText = "INSERT INTO fruit(id, name, number, price)VALUES(:id, :name, :number, :price)";
                    Command.Parameters.Add(new SQLiteParameter("id", fruit.Id));
                    Command.Parameters.Add(new SQLiteParameter("name", fruit.Name));
                    Command.Parameters.Add(new SQLiteParameter("number", fruit.Number));
                    Command.Parameters.Add(new SQLiteParameter("price", fruit.Price));
                    Command.ExecuteNonQuery();
                }
            }
        }

        public static void All()
        {
            var list = new List<Fruit>();
            var ConnectionStr = new SQLiteConnectionStringBuilder() { DataSource = "test.db" };
            using (var Connection = new SQLiteConnection(ConnectionStr.ToString()))
            {
                Connection.Open();

                using (var Command = new SQLiteCommand(Connection))
                {
                    Command.CommandText = "SELECT id, name, number, price FROM fruit";
                    using (var Reader = Command.ExecuteReader())
                    {
                        while (Reader.Read())
                        {
                            var fruit = new Fruit();
                            fruit.Id = Reader.GetInt32(0);
                            fruit.Name = Reader.GetString(1);
                            fruit.Number = Reader.GetInt32(2);
                            fruit.Price = Reader.GetInt32(3);
                            list.Add(fruit);
                        }
                    }
                }
            }

            foreach(var item in list)
            {
                Console.WriteLine(item);
            }
        }
    }

    public class Fruit
    {
        public override string ToString()
        {
            return "Id = " + Id + ", Name = " + Name + ", Number = " + Number + ", Price = " + Price;
        }
        public int Id { get; set; }
        public string Name { get; set; }
        public int Number { get; set; }
        public int Price { get; set; }
    }
}

SQLITEの場合データベースを作成していなくてもDataSourceは勝手に作成されるので特に気にしなくてもよい。
var Connection = new SQLiteConnection(ConnectionStr.ToString())の部分では設定からConnectionを作成する。Connectionはメモリリークの原因となるので必ずCloseしないとだめ。なので、usingを使う。
var Command = new SQLiteCommand(Connection)はConnectionからコマンドを実行するためのインスタンスを取得する。これもクローズする必要がありusingを使う。
SELECT以外はCommandText にSQL文を書いた後ExecuteNonQueryを実行する。
SELECTの場合は少し面倒でExecuteReader メソッドから読み込みのためのインスタンスを取得。これもクローズが必要なのでusingが必要。Reader.Read() はReaderを次の行に進めつつレコードがあればtrueを返しなければfalseを返す。値の取得はGetInt32()GetString() メソッドで引数にはカラムの順番を入れる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Code Cleanupでコードを整える

コードのインデントや改行などのフォーマットは、フォーマッタを使えば沿って整えることができます。

ところで、C#において

  • varを使うか使わないか
  • privateなアクセス修飾子を明示するか
  • 式形式のメソッドを使うかどうか

などのスタイルも統一したい、という場面はありませんか?

.NET向けJetBrains IDEである「Rider」では、Code Cleanupを使えば、フォーマットもスタイルも整えることができます。

また、余分なパラメーターや余分なコードも削除することができます。

この投稿では、RiderのCode Cleanupを紹介します。

Code Cleanupとは?

公式ドキュメント Code Cleanupより、

JetBrains Rider allows you to apply formatting and other code style preferences in a bulk mode to instantly eliminate code style violations in one or more files, in a project or in the entire solution.

Code Cleanupは、対象のコードのフォーマット・スタイルを規約・設定にそって整える機能です。

例えば、以下のコードはCode Cleanupによって

public class Player
{
   int hp;
    public int Hp { get { return hp; } }

    public Player(int hp)   {
        this.hp = hp;
    }
}

次のようなコードになります。

public class Player
{
    public Player(int hp)
    {
        Hp = hp;
    }

    public int Hp { get; }
}

Code Cleanupによって、フォーマットが整えられ、getter onlyの自動実装プロパティに書き換えられました。

Code Cleanupの内容一部紹介

公式ドキュメント Code Cleanupより、一部を紹介します。

  • Apply file layout : メンバの順番を並び替える
  • Apply 'var' style : ローカル変数において、varを使う、もしくは使わない。Code Styleの設定に依存。
  • Use explicit or implicit modifier definition for types : 型のinternalアクセス修飾子を明示するか、しない。Code Styleの設定に依存
  • Use explicit or implicit modifier definition for type members : 型のメンバのprivateアクセス修飾子を明示するか、しない。Code Styleの設定に依存
  • Remove redundant parentheses : 余分なカッコを減らす
  • Remove code redundancies : 余分なコードを排除する
  • Apply code body style : 式形式のメソッドなどに置換する
  • Use auto-property, if possible : 可能なら自動実装プロパティに置換する
  • Make field read-only, if possible : 可能なら、readonlyキーワードをフィールドに追加する
  • Make auto-property get-only, if possible : getterのみの自動実装プロパティに変換する

JetBrains Rider allows you to apply formatting and other code style preferences in a bulk mode to instantly eliminate code style violations in one or more files, in a project or in the entire solution.

この一文からは、フォーマットとスタイルだけを変えるという想像をしますが、ガッツリコードを書き換える点に注意してください。(原則、同じ動作はするコードに書き換えます。)

同じような動作はしますがコンパイル後のMSILとしては、全く別のコードになります。

Code Cleanupの使い方

メニューから、Code | Code Cleanup ...を選択すると、以下のウィンドウが開きます。

スクリーンショット 2019-11-24 18.52.27.png

Code Cleanupの対象と設定をそれぞれ選択してOKボタンを押すと、Code Cleanupが実行されます。

または、Show Reformat Code Dialogから、Code Cleanupの有無を選択することもできます。

スクリーンショット 2019-11-24 19.02.21.png

Code Cleanupの対象

Code Cleanupは、ソリューション全体、プロジェクト全体、コミットされていないファイル、カスタムスコープ、開いているファイルから選択できます。

また、テキストを選択し、選択したテキストの中身に対してCode Cleanupをかけることもできます。Code Cleanupしたいテキストを選択し、アクションリストを表示し(Alt + Enter)、リストの中から、Cleanup selectionを選んでください。

スクリーンショット 2019-11-24 18.58.07.png

Code Cleanupの設定

Code Cleanupの設定は、Built-inでいくつか設定が作成されています。

Preference | Editor | Code Cleanupにおいて、その設定を編集したり、コピーして編集したりできます。

また、自分で0から作ることも可能です。自分のプロジェクトに合った設定を作ってみてください。

スクリーンショット 2019-11-24 19.21.37.png

Commit時のCode Cleanup

Riderからgit commitをした際に、Code Cleanupを実行するよう、設定できます。

https://www.jetbrains.com/help/rider/2019.2/Commit_Changes_Dialog.html#before_commit

Unityに関するバグ

Unityにおける、Code Cleanupに関する問題として、次のようなコードが

public class Mover : MonoBehaviour
{
    [SerializeField] private float speed;

    public float Speed
    {
        get => speed;
        set => speed = value;
    }
}

次のようなコードに変更されてしまうというものがありました。

public class Mover : MonoBehaviour
{
    [field: SerializeField] public float Speed { get; set; }
}

普通のC#としては問題がないのですが、Unityとしてはバッキングフィールドの名前が大事なので、こうなってしまうCode Cleanupの項目が使えませんでした。

この問題は、Rider 2019.3で解消される予定です。

https://youtrack.jetbrains.com/issue/RIDER-27839

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RiderのCode CleanupでC#のコードを整える

揃っていないコードのインデントや改行などのフォーマットは、フォーマッタを使えば整えることができます。

ところで、C#において

  • varを使うか使わないか
  • privateなアクセス修飾子を明示するか
  • 式形式のメソッドを使うかどうか

などのスタイルが統一されていない場合、どうすればいいでしょうか?

.NET向けJetBrains IDEである「Rider」では、Code Cleanupを使えば、フォーマットもスタイルも整えることができます。

また、余分なパラメーターや余分なコードも削除することができます。

この投稿では、RiderのCode Cleanupを紹介します。

Code Cleanupとは?

公式ドキュメント Code Cleanupより、

JetBrains Rider allows you to apply formatting and other code style preferences in a bulk mode to instantly eliminate code style violations in one or more files, in a project or in the entire solution.

Code Cleanupは、対象のコードのフォーマット・スタイルを規約・設定にそって整える機能です。

例えば、以下のコードはCode Cleanupによって

public class Player
{
   int hp;
    public int Hp { get { return hp; } }

    public Player(int hp)   {
        this.hp = hp;
    }
}

次のようなコードになります。

public class Player
{
    public Player(int hp)
    {
        Hp = hp;
    }

    public int Hp { get; }
}

Code Cleanupによって、フォーマットが整えられ、getter onlyの自動実装プロパティに書き換えられました。

Code Cleanupの内容一部紹介

公式ドキュメント Code Cleanupより、一部を紹介します。

  • Apply file layout : メンバの順番を並び替える
  • Apply 'var' style : ローカル変数において、varを使う、もしくは使わない。Code Styleの設定に依存。
  • Use explicit or implicit modifier definition for types : 型のinternalアクセス修飾子を明示するか、しない。Code Styleの設定に依存
  • Use explicit or implicit modifier definition for type members : 型のメンバのprivateアクセス修飾子を明示するか、しない。Code Styleの設定に依存
  • Remove redundant parentheses : 余分なカッコを減らす
  • Remove code redundancies : 余分なコードを排除する
  • Apply code body style : 式形式のメソッドなどに置換する
  • Use auto-property, if possible : 可能なら自動実装プロパティに置換する
  • Make field read-only, if possible : 可能なら、readonlyキーワードをフィールドに追加する
  • Make auto-property get-only, if possible : getterのみの自動実装プロパティに変換する

JetBrains Rider allows you to apply formatting and other code style preferences in a bulk mode to instantly eliminate code style violations in one or more files, in a project or in the entire solution.

この一文からは、フォーマットとスタイルだけを変えるという想像をしますが、ガッツリコードを書き換える点に注意してください。(原則、同じ動作はするコードに書き換えます。)

同じような動作はしますがコンパイル後のMSILとしては、全く別のコードになります。

Code Cleanupの使い方

メニューから、Code | Code Cleanup ...を選択すると、以下のウィンドウが開きます。

スクリーンショット 2019-11-24 18.52.27.png

Code Cleanupの対象と設定をそれぞれ選択してOKボタンを押すと、Code Cleanupが実行されます。

または、Show Reformat Code Dialogから、Code Cleanupの有無を選択することもできます。

スクリーンショット 2019-11-24 19.02.21.png

Code Cleanupの対象

Code Cleanupは、ソリューション全体、プロジェクト全体、コミットされていないファイル、カスタムスコープ、開いているファイルから選択できます。

また、テキストを選択し、選択したテキストの中身に対してCode Cleanupをかけることもできます。Code Cleanupしたいテキストを選択し、アクションリストを表示し(Alt + Enter)、リストの中から、Cleanup selectionを選んでください。

スクリーンショット 2019-11-24 18.58.07.png

Code Cleanupの設定

Code Cleanupの設定は、Built-inでいくつか設定が作成されています。

Preference | Editor | Code Cleanupにおいて、その設定を編集したり、コピーして編集したりできます。

また、自分で0から作ることも可能です。自分のプロジェクトに合った設定を作ってみてください。

スクリーンショット 2019-11-24 19.21.37.png

Commit時のCode Cleanup

Riderからgit commitをした際に、Code Cleanupを実行するよう、設定できます。

https://www.jetbrains.com/help/rider/2019.2/Commit_Changes_Dialog.html#before_commit

Unityに関するバグ

Unityにおける、Code Cleanupに関する問題として、次のようなコードが

public class Mover : MonoBehaviour
{
    [SerializeField] private float speed;

    public float Speed
    {
        get => speed;
        set => speed = value;
    }
}

次のようなコードに変更されてしまうというものがありました。

public class Mover : MonoBehaviour
{
    [field: SerializeField] public float Speed { get; set; }
}

普通のC#としては問題がないのですが、Unityとしてはバッキングフィールドの名前が大事なので、こうなってしまうCode Cleanupの項目が使えませんでした。

この問題は、Rider 2019.3で解消される予定です。

https://youtrack.jetbrains.com/issue/RIDER-27839

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む