- 投稿日:2019-12-03T22:31:11+09:00
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.batcsc /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正しいページ数が得られている。
参考サイト
- https://www.atmarkit.co.jp/ait/articles/1310/24/news070.html
- https://docs.microsoft.com/en-us/uwp/api/windows.storage.storagefile.getfilefrompathasync
追記
参考サイト1によると、ページを画像として抽出するのには使えそうだが、テキストを抽出するのには使えなさそうである。ILSpyでメンバーとかみた感じ、その通りっぽい。
- 投稿日:2019-12-03T21:36:10+09:00
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.protosyntax = "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; // 中身は20unity 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.IsRepeated
がtrue
だった場合、そのフィールドは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); } }このようにして取得した
IList
やRepeatedField<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.Fields
とOneofDescriptor.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など、応用的な使い方がたくさん残っていますので、今後も別途それらの機能についても掘り下げていきたいと思います
最後まで読んでいただきありがとうございました
- 投稿日:2019-12-03T21:25:47+09:00
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(); }); } } }
ConfigureServices
とConfigure
の2つのメソッドがあります。呼び出される順番は、ConfigureServices
→Configure
の順です。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.config
とweb.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 クラスがすこし理解できたように思います。
- 投稿日:2019-12-03T19:53:28+09:00
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; // 左から掛けるp3とp4のベクトルの向きがp1とp2のベクトルの向きに合うようにp3とp4を持つオブジェクトを回転させています。
使用例
こんな感じの用途に使いました
むにむに pic.twitter.com/5VYClMBWod
— がとーしょこら@VRChat (@gatosyocora_vrc) November 10, 2019
- 投稿日:2019-12-03T19:53:28+09:00
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; // 左から掛けるp3とp4のベクトルの向きがp1とp2のベクトルの向きに合うようにp3とp4を持つオブジェクトを回転させています。
使用例
こんな感じの用途に使いました。
右手と左手間のベクトルに合うようにコントローラを回転させています
むにむに pic.twitter.com/5VYClMBWod
— がとーしょこら@VRChat (@gatosyocora_vrc) November 10, 2019関連文献
- 投稿日:2019-12-03T15:18:40+09:00
Python for .NETを使ってPandas DataFrame から System.Data.DataTableへ変換する
Pythonと.NETの表データ交換
そもそもPython -> .NETでデータフレームを交換する場合、.NET側はDataTableという標準ライブラリのクラスがあるので、それが適しています。決してPandasやdplyrのような高機能なデータ処理能力は無いのですが、とりあえずマッピングすることができます。
BayesServerでPythonからも呼びたい場合は、以下にあるようにJPypeを使ったJavaライブラリへのアクセスを推奨されているようです。一方で、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 Formimport clrというのが趣があります。
- .NETから、Pythonのライブラリを .NET 上でimportできる
dynamic np = Py.Import("numpy"); var x = np.cos(np.pi * 2);こっち方向は、 @hogegex さんの記事があります。
用途としては、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())の代わりみたいなものだと思ってください。
- 投稿日:2019-12-03T01:29:00+09:00
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を使って自動テスト環境を整えるまでの流れを紹介します。お品書き
- ユニットテストを実装する
- Azure DevOpsでビルドパイプラインを作成する
- 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-sampleASP.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 { // ・・・一部省略前述した通りですが、テストエクスプローラーでカテゴリに従い、表示項目をフィルタリングすることが出来ます。
テストケース数が膨大になった場合は、有効な機能になるため活用しましょう。
テストパターンが類似しているテストケースについては、パラメタライズドテストを検討すると良いです。
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-docsxUnit
次は、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 があるため、これを利用すると、いい感じにテストの実装が出来ます。
以下、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.cspublic 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.cspublic 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 のプロジェクトをサンプルに作成しています。
ポイントとしては、テストの実行・カバレッジの取得 (Test and output coverage) と レポートの出力 (Generate coverage report)です。
カバレッジの取得には OpenCover を使用します。Nugetより取得しましょう。
OpenCover より MsTest をコマンドラインで実行し、カバレッジの取得を行います。
テスト対象となる DLL の指定や、カバレッジ取得対象とする 名前空間 をコマンドラインのパラメータで指定します。
また、カバレッジの取得は デバッグビルド で実行する必要があるため注意が必要です。以下、設定内容の詳細 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 に追加しましょう。上記拡張を追加すると、ReportGenerator のタスクを作成出来るようになります。
steps: - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4 displayName: 'Generate coverage report'パイプラインの実行後、Code Coverage のタブが追加され、カバレッジレポートが HTML で閲覧出来るようになります。
Tests のタブと合わせて確認することで、効率的にテスト結果を可視化することが出来ます。次に、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-devopsAzure DevOpsのテスト結果をSlackへ通知する
最後に、Azure DevOps と Slack 連携し、ビルドパイプラインによるテスト結果を通知してみます。
Azure DevOps と Slack は専用の連携アプリがあるため、これを利用します。
詳細な手順は公式のドキュメントがあるため、本記事では割愛します。
https://docs.microsoft.com/en-us/azure/devops/pipelines/integrations/slack?view=azure-devops設定が完了すると、画像の通りにビルド完了時にSlackへ通知が送付されます。
しかし、上記通知内容からではテスト成功件数、失敗件数といった詳細な結果を知ることが出来ません。
勿論、メッセージのリンクから 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以外の別のツールを用いた場合は適宜調整が必要なため、ご注意ください。また、同様に 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 }まとめ
後半、C# というよりも Azure DevOps の記事が中心になってしまいました。。。
C#でモダンな継続的開発を行う場合は、ユニットテストの実装とAzure DevOpsの運用がポイントになってくると思います。
本記事が、少しでも皆様の参考情報となれば幸いです。
- 投稿日:2019-12-03T01:13:57+09:00
【初心者向け】処理負荷の軽減に繋がるコーディング【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関数は削除しましょう
頭の隅に入れて書くだけでも後々負荷軽減に繋がりチームメンバーからいいねを貰えると思います。
- 投稿日:2019-12-03T01:03:51+09:00
C# NuGetパッケージ
対象読者
C#初心者だけど、少しコード分かる。
NuGetとは?
外部パッケージを取得するためのシステム。取得することによって外部パッケージが使えるようになる。
具体的に
macになるが、プロジェクトのManager NuGet Packagesって書かれたところをクリックすると下画像のようなものが出てくる。
ここでダウンロードしたいものを選択。一番上のNewtonsoft.Jsonをダウンロード。これはJsonを扱うためのパッケージである。
これで、Newtonsoft.Jsonのパッケージが使えるようになる。具体的なパッケージ
以下自分が使ったことのあるパッケージを書いていく。
ただ現状少ないので、ちまちま新しいものに挑戦しつつ追加していく。Newtonsoft.Jsonパッケージ
今までと同じように、usingで新しくパッケージを使うことができる。
ObjectからJson,JsonからObject
Program.csusing 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.csusing 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 = 190Fruitクラスは上で定義しているので省略。
listでも配列でもjsonは同様のものとなる。System.Data.SQLite.Core
Program.csusing 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()
メソッドで引数にはカラムの順番を入れる。
- 投稿日:2019-12-03T00:25:43+09:00
Code Cleanupでコードを整える
コードのインデントや改行などのフォーマットは、フォーマッタを使えば沿って整えることができます。
ところで、C#において
- varを使うか使わないか
- privateなアクセス修飾子を明示するか
- 式形式のメソッドを使うかどうか
などのスタイルも統一したい、という場面はありませんか?
.NET向けJetBrains IDEである「Rider」では、Code Cleanupを使えば、フォーマットもスタイルも整えることができます。
また、余分なパラメーターや余分なコードも削除することができます。
この投稿では、Riderの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 ...
を選択すると、以下のウィンドウが開きます。Code Cleanupの対象と設定をそれぞれ選択してOKボタンを押すと、Code Cleanupが実行されます。
または、
Show Reformat Code Dialog
から、Code Cleanupの有無を選択することもできます。Code Cleanupの対象
Code Cleanupは、ソリューション全体、プロジェクト全体、コミットされていないファイル、カスタムスコープ、開いているファイルから選択できます。
また、テキストを選択し、選択したテキストの中身に対してCode Cleanupをかけることもできます。Code Cleanupしたいテキストを選択し、アクションリストを表示し(Alt + Enter)、リストの中から、
Cleanup selection
を選んでください。Code Cleanupの設定
Code Cleanupの設定は、Built-inでいくつか設定が作成されています。
Preference | Editor | Code Cleanup
において、その設定を編集したり、コピーして編集したりできます。また、自分で0から作ることも可能です。自分のプロジェクトに合った設定を作ってみてください。
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で解消される予定です。
- 投稿日:2019-12-03T00:25:43+09:00
RiderのCode CleanupでC#のコードを整える
揃っていないコードのインデントや改行などのフォーマットは、フォーマッタを使えば整えることができます。
ところで、C#において
- varを使うか使わないか
- privateなアクセス修飾子を明示するか
- 式形式のメソッドを使うかどうか
などのスタイルが統一されていない場合、どうすればいいでしょうか?
.NET向けJetBrains IDEである「Rider」では、Code Cleanupを使えば、フォーマットもスタイルも整えることができます。
また、余分なパラメーターや余分なコードも削除することができます。
この投稿では、Riderの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 ...
を選択すると、以下のウィンドウが開きます。Code Cleanupの対象と設定をそれぞれ選択してOKボタンを押すと、Code Cleanupが実行されます。
または、
Show Reformat Code Dialog
から、Code Cleanupの有無を選択することもできます。Code Cleanupの対象
Code Cleanupは、ソリューション全体、プロジェクト全体、コミットされていないファイル、カスタムスコープ、開いているファイルから選択できます。
また、テキストを選択し、選択したテキストの中身に対してCode Cleanupをかけることもできます。Code Cleanupしたいテキストを選択し、アクションリストを表示し(Alt + Enter)、リストの中から、
Cleanup selection
を選んでください。Code Cleanupの設定
Code Cleanupの設定は、Built-inでいくつか設定が作成されています。
Preference | Editor | Code Cleanup
において、その設定を編集したり、コピーして編集したりできます。また、自分で0から作ることも可能です。自分のプロジェクトに合った設定を作ってみてください。
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で解消される予定です。