びぼーろくっ!

誰かに見せるわけでもないけど、備忘録として。。

.NET MVC RazorでTODOList(動的なフォーム)を作成する方法があった

何気に痒い所に手が届かない.NET MVC RazorですがTODOListのような動的フォームを作成する方法がありました
本来ならScriptメインで記述すればいいのですが、Validationや共通のヘルパーなどを使用したかったのでRazorでやりたかったのです。

使ったライブラリ
www.nuget.org


んで実現のために作ったサンプルコードはこちら

Controller

// SampleController.cs
public class SampleController
{
  /// <summary>
  /// 初期レンダリング時にkickされる
  /// </summary>
  /// <returns></returns>
  [HttpGet]
  public ActionResult Index()
  {
    // サンプルの為、DBロジックをハードコーディングしてます。
    var parent = new Parent()
    {
      Age = 4,
      Name = "ねこのきなこ",
      Skills = new List<Skill>()
      {
        new Skill() {Name = "お皿洗い", Experience = 2 },
        new Skill() {Name = "あまえる", Experience = 4 },
      }
    };
    return View(parent);
  }

  /// <summary>
  /// submit時にkick
  /// </summary>
  /// <param name="model"></param>
  /// <returns></returns>
  [HttpPost]
  public ActionResult Create(Parent model)
  {
    // 登録処理
    return View("Index");
  }

  /// <summary>
  /// スキル追加
  /// </summary>
  /// <returns></returns>
  [HttpGet]
  public ActionResult AddChild()
  {
    return PartialView("Skills", new Skill());
  }
}

Model側

// Parent.cs
public class Parent
{
  [Display(Name = "おなまえ")]
  public string Name { get; set; }

  [Display(Name = "おとし")]
  public int Age { get; set; }

  [Display(Name = "できることリスト")]
  public List<Skill> Skills { get; set; }
}
// Skill.cs
public class Skill
{
  [Display(Name = "できること")]
  public string Name { get; set; }

  [Display(Name = "つづけたねんすう")]
  public int Experience { get; set; }
}

View側
Index.cshmlt

@model HogeHogeNameSpace.Parent
@section Scripts {
    <script src="~/js/sample.js"></script>
}

@using (Html.BeginForm("Create", "Sample"))
{
    <div class="mdl-grid">
        @Html.CustomTextEditorFor(m => m.Name, 50)
    </div>
    <div class="mdl-grid">
        @Html.CustomNumericEditorFor(m => m.Age, 2)
        <span>さい</span>
    </div>
    <table id="children">
        <tbody>
            @foreach (var skill in Model.Skills)
            {
                @Html.Partial("_Skills", skill)
            }
        </tbody>
    </table>
    @Html.CustomTextButton("addChild", "Skill追加", "mode_edit")
    @Html.CustomTextButton("Save", "Save", "mode_edit", null, "submit");
}

_Skills.cshtml

@model HogeHogeNameSpace.Skill
@using HtmlHelpers.BeginCollectionItem;

<!---使用するとHtmlのName属性の衝突を避けることが出来て、CollectionのItemに応じたIdが割り当てされる-->
@using (Html.BeginCollectionItem("Skills"))
{
    <tr>
        <td>
            @Html.CustomTextEditorFor(m => m.Name, 50, false)
            @Html.CustomNumericEditorFor(m => m.Experience, 2, false)
            <span>ねん</span>
        </td>
        <td>
            @Html.CustomButtonIcon("removeItem", "close", "close", new Dictionary<string, string>() { { "data-action", "removeItem" } })
        </td>
    </tr>
}

sample.js

$("#addChild").on("click", function () {
    $.ajax({
        // urlの置換
        url: '/Sample/AddChild'
        , success: function (partialView) {
            $('#children> tbody:last-child').append(partialView);
        }
    });
});


完成した画面はこちら。Frameworkをカスタムヘルパーで参照しています。これがしたかったのです。
画面初期時
f:id:kinachan0725:20190116183305p:plain

スキル追加ボタン押下時
f:id:kinachan0725:20190116183312p:plain

実装のコツとしては各リストをパーシャル化してBeginCollectionItemを使用する。
第一引数に対応するCollectionNameを入力
この場合、Parentから呼び出されているので、"Skills"ですね。

次にコントローラーにスキルを追加する為のルーティングを用意します。
AddChildで空のSkillを返してあげてAjaxでRequest、Responseを処理します。

結構簡単な事なのにRazorでやろうとしたら大変だったなぁ。
でもValidationとか割とモデル内で書けるから便利だったりするので、この方法で動的リストを作ろうと思いましたっ。

参考にしたリンク
stackoverflow.com

Node.jsでコマンド一発で全プロジェクトを最新にするツールをつくった。

ちょっと担当しているプロジェクトが増えすぎたので作成しました。
いちいちgitコマンドだったりgit Extensionでpullやcloneするのが面倒だったので・・・

やっぱツール作りは楽しいなー。

使い方。
・ProjectList.js -- 定義ファイル
・run.js -- 実行ファイル
・./shell 実行させるshellを格納

とりあえず現時点ではcloneとpullができればいいかなと思ってたので拡張はしてません。
でも拡張しやすいように作りました。

//gitCmd.sh

cd $2 && git $1;


// ProjectList.js

const projectList = [
  {
    dir: 'Hoge', // 格納したいディレクトリ
    gitRepository: 'RepositoryName', // git上のリポジトリ名
    category: 'parentDirectory', // gitRepositoryの親階層
  },
  {
    ...
  },
]
module.exports = projectList;


//run.js

const { exec } = require('child_process');

const gitUrl = 'https://exsample.com/gitbucket/git';
const projectListObjects = require('./projectList');
const shellDir = './shell';

const execute = (cmd) => {
  exec(cmd, {encoding: 'utf8'}, (err, stdout, stderr) => {
    console.log(stdout);
    if (stderr) console.error(stderr);
  });
}

const gitRun = (dir, cmd) => {
  execute(`sh ${shellDir}/gitCmd.sh "${cmd}" ${dir}`);
}

const getDir = pj => `~/Code/${pj.dir}`;

const createCloneCmd = (pj) => {
  const baseUrl = pj.category != null ? `${gitUrl}/${pj.category}` : `${gitUrl}`;
  const url = `${baseUrl}/${pj.gitRepository}.git`;

  const branch = pj.branch || 'develop';
  return `clone -b ${branch} ${url}`;
}

const clone = (pj) => {
  const dir = getDir(pj);
  const cloneCmd = createCloneCmd(pj);

  gitRun(dir, cloneCmd);
  gitRun(dir, 'pull');
}

const run = () => {
  projectListObjects.forEach((project) => {
    clone(project);
  });
}
run();

トランザクションで迷い頭の中で議論が起きた話。

こういう処理をしたいとします。
・大量のデータ&APIのリクエストを伴うため、1件ずつトランザクションを切らないと駄目。
そこで大量のデータのループ内でトランザクションを作成して、commit or rollbackすると思うのですが問題は例外発生後の処理ですね。

例:x~z~yのループ内で1件ずつTransactionを切るとして、z件目に例外が発生した場合
・z件目はrollbackして例外を握り潰してしてz~y件目を実行。
・z件目で例外発生して以後の処理は実行しない
↑2つのどちらかだと思うのだけど・・・結構ケースバイケースな事象な気がしてならないんです。

        //ケース1
        public async Task LoopTransactionCase1()
        {
            var idList = await repository.GetIdList().ConfigureAwait(false);
            foreach (var id in idList)
            {
                using (var scope = new TransactionScope(TransactionScopeOption.Required, 
                    new TransactionOptions() { IsolationLevel = IsolationLevel.ReadCommitted },
                    TransactionScopeAsyncFlowOption.Enabled))
                {
                    //POSTデータを取得して作成
                    var data = await repository.GetPostData(id).ConfigureAwait(false);
                    var content = new StringContent(JsonConvert.SerializeObject(data));
                    content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

                    //POST経由でAPIにリクエストを飛ばす。戻りはboolean
                    //httpClientが失敗した場合、例外を潰しfalseを返却する。
                    var response = await httpClient.PostAsync("url", content).ConfigureAwait(false);
                    if (!await deserializeResponse<bool>(response))
                    {
                        throw new SystemException("failed to data registration.");
                    }
                    scope.Complete();
                }
            }
        }

        //ケース2
        public async Task LoopTransactionCase2()
        {
            var idList = await repository.GetIdList().ConfigureAwait(false);
            foreach (var id in idList)
            {
                using (var scope = new TransactionScope(TransactionScopeOption.Required,
                    new TransactionOptions() { IsolationLevel = IsolationLevel.ReadCommitted },
                    TransactionScopeAsyncFlowOption.Enabled))
                {
                    var data = await repository.GetPostData(id).ConfigureAwait(false);
                    var content = new StringContent(JsonConvert.SerializeObject(data));
                    content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

                    var response = await httpClient.PostAsync("url", content).ConfigureAwait(false);
                    if (!await deserializeResponse<bool>(response))
                    {
                        
                        scope.Dispose();
                        continue;
                    }
                    scope.Complete();
                }
            }
        }

問題はケース1の場合、不正データがz件目に存在していた場合以後のデータが処理されないことです。
そのため不正なデータのせいでz + 1 ~ y件までのデータ処理が正常であっても走らない事が問題です。

ケース2の問題としては仮にx~z~yが10000件を超えるデータは全て正常だけどリクエスト先の鯖が落ちてる or clientの通信が切れてる。
などの問題があった場合、無駄な処理を10000件を実行することになります。

うーん、これはどっちがいいんだろう。。。

モデル同士の値を比較する共通メソッドをつくった。

C#やっぱしんどいです。
今オブジェクトの比較事態はJSON文字列に変換して行っているのですが、Nunitの仕様上擦り切れてしまうんですよね。。
コンソール画面で色つきで表示されたら不正な値が見れるからいいのになー。と思ってて探してみたけど無かったので作りました。

結構無理やりな勢いで作ったので普通にバグるかもしれません。。使用するのは自己責任で・・・
JavaScriptなら簡単にいけちゃうのにな。。。やっぱC#しんどry
実際にモデルの比較ならJSONに変換したほうがイケてると思います。今回は細かい検証用なので使い道が少ないかも。。

    public void AssertResultConsoleView<T>(T modelA, T modelB)
    {
        var propsA = modelA.GetType().GetProperties();
        var propsB = modelB.GetType().GetProperties();

        var i = 0;
        foreach (var prop in propsA)
        {
            Console.ResetColor();
            var propB = propsB[i];
            if (prop.Name == propB.Name)
            {
                var aValue = prop.GetValue(modelA).ToString();
                var bValue = propB.GetValue(modelB).ToString();

                if (aValue != bValue)
                {
                    Console.BackgroundColor = ConsoleColor.Red;
                }
                Console.WriteLine("比較結果:{0}: exp:{1} result:{2}", prop.Name, aValue, bValue);
            }
            i++;
        }
    }

知らなかった!SELECT文の結果をINSERTする方法

今回はSQLです。(postgresqlを使用)
本番環境のマスタースクリプトを更新する作業があったのですが、本番環境のデータは参照できないので
正規表現などで一気にスクリプトファイルを組むわけにもいかずもやもやしてました。

調べた結果SELECT-INSERTなるものがあったのですね!
(DB周りは他の人が担当してたので詳しい知識はなかった orz)

こんなテーブルやレコードがあるとします。

insert_table

id colomn2 colomn3
null null null

select_table

id birthday job
1 1995/01/01 developer
2 1996/01/01 lawyer
3 1997/01/01 docter

やりたい事はinsert_tableにselect_tableのidを反映したい場合です。
固定値が混ざったレコードを登録したい場合はSELECT文に固定値を書き込めばいいです。

    • insert_table(登録したいレコードのテーブル
    • select_table(既に存在するテーブル)
INSERT INTO insert_table(
     id, column2, column3
)
SELECT id, '固定値', now()
FROM   select_table;

結果をみてみましょう・・・

SELECT * FROM insert_table;

insert_table

id colomn2 colomn3
1 固定値 2017/4/6
2 固定値 2017/4/6
3 固定値 2017/4/6

今日は以上です。

どうでもいいけど・・・はてぶろってpostgresqlの記法ってないのかな・・
mysqlでコードハイライトしたけど、それっぽいのが見つからなかった・・・

MockHttpでHttpResponseをモック化!

最近C#ばっかりです。
なかなか型の制約とかHttpContextがHttpContextBaseを継承してなかったりとか色々としんどいです。

今回戸惑ったのがAPIのRequest⇔ResponseをMock化したかったのですが
NUGET辺りで探してたらいいのがありました。

これです。
github.com


今回はNUnitFrameWorkを使用しています。

using Newtonsoft.Json;
using NUnit.Framework;
using RichardSzalay.MockHttp;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Exsample.Test.Common.Util;
using Exsample.Net.Http;
using Moq;
using System.Web;
using System;

namespace Exsample.Module.Test
{
    [TestFixture]
    public class GetStreamAsync : ClientContext
    {
        private string url;
        private Stream stream;
        private Error error;

        /// <summary>
        /// 成功時(status=OK)のMockResponseMessageをセット
        /// </summary>
        private void setSuccessContent()
        {
            mockHttpMessage.Expect($"{baseUrl}*")
                .Respond(() =>
                {
                    // Status 200時に実行されるCallBack。
                    // Task<HttpResponseMessage>型を返すようにする。
                    return Task.Factory.StartNew<HttpResponseMessage>(() =>
                    {
                        var res = new HttpResponseMessage();
                        res.Content = new StreamContent(stream);
                        return res;
                    });
                });
        }

        /// <summary>
        /// 失敗時(status=NG)のMockResponseMessageをセット
        /// </summary>
        private void setFailContent()
        {
            error = new Error();
            // シリアライズして文字列として第二引数に渡してます。
            var content = JsonConvert.SerializeObject(error);
            mockHttpMessage.Expect($"{baseUrl}*")
                .Respond(HttpStatusCode.InternalServerError, "application/json", content);
        }

        /// <summary>
        /// MOCKのセットアップ。
        /// </summary>
        /// <param name="isSuccess">成功テストか否か</param>
        public void mockSetup(bool isSuccess)
        {
            if (isSuccess)
            {
                // テスト用のダミーStreamを作成
                stream = StreamCreator.Create();
                url = "unittest/getasync";
                setSuccessContent();
            }
            else
            {
                url = "unittest/notfound";
                setFailContent();
            }
        }

        /// <summary>
        /// Status=OKのテスト
        /// </summary>
        /// <returns></returns>
        [Test]
        public async Task Success()
        {
            mockSetup(true);
            var result = await client.GetStreamAsync(url, null);
            Assert.AreEqual(stream, result);
        }

        /// <summary>
        /// Status=NGのテスト
        /// </summary>
        /// <returns></returns>
        [Test]
        public void Fail()
        {
            mockSetup(false);
            var exception = Assert.ThrowsAsync<ResponsException>(() =>
            {
                return client.GetStreamAsync(url, null);
            });
            Assert.AreEqual(HttpStatusCode.InternalServerError, exception.StatusCode);
            // クラス同時の比較は出来ないのでJSON化して比較
            AssertObject(error, JsonConvert.DeserializeObject<Error>(exception.Message));
        }
    }

    /// <summary>
    /// パラメーターのインスタンス化や共通のMock化を行う
    /// </summary>
    public class ClientContext : TestBase
    {
        protected TargetClient client; // テスト対象のメソッド
        Mock<HttpContextBase> mockContext;
        protected MockHttpMessageHandler mockHttpMessage;
        protected Guid userId;

        // 適当なURL
        protected string baseUrl = "http://exsample.com:8080/api/";

        protected class Error
        {
            public string ErrorDiscription { get; set; }
            public string ErrorMessage { get; set; }
            public Error()
            {
                this.ErrorDiscription = "error_description";
                this.ErrorMessage = "server_error";
            }
        }

        [SetUp]
        public void Setup()
        {
            // MockのHttpContextBaseを作成
            mockContext = FakeHttpContextBase.Create(); // HttpContextBaseをMock化をする。

            // Sessionを上書きする
            var session = new Mock<HttpSessionStateBase>();
            userId = Guid.NewGuid();
            session.Setup(x => x["userId"]).Returns(userId.ToString());
            mockContext.Setup(x => x.Session).Returns(session.Object);

            // 今回紹介するmockHttpMessageHandlerのインスタンスを作成。
            mockHttpMessage = new MockHttpMessageHandler();
            var httpClient = new HttpClient(mockHttpMessage);

            client = new TargetClient(mockContext.Object);
            client.HttpClient = httpClient;
        }
    }
}

テスト対象メソッドはStatus=NGならばResponsExceptionをスロー。
メッセージ部分はJSON形式にて返却されます。
成功時は返却されたHttpResponseMessage.ContentをStreamにして返却しています。

APIのクライアント部分だし通信されると困るので、何かいいメソッドがないか見てたらいいのを見つけました。
今度もこれを使ってゴリゴリ書いていきたいと思います。

gulpを使ってlessをコンパイルする方法

今Electronを使ってデスクトップアプリを趣味で開発しているのですが
猛烈にlessを導入したくなったので色々と設定してみました。

less
lesscss.org


ただ、Client変換方式でlessを導入するのはブラウザやパフォーマンスの問題があるので
lessファイルをコンパイルしてくれてコンパイル後のcssを読み取るように設定してみます。
・・もちろんlessの公式にあるとおり

 lessc styles.less styles.css

このようなコマンドでも実行は可能ですが、いちいちCSSの編集後にこのコマンドを投げるのは面倒です。
そのため保存時に自動的にコンパイルが走り、特定のフォルダ内にCSSが作成されるようにします。

上記のことをやりたい場合、gulp + gulp-lessを使用すると便利です。

gulp.js

www.npmjs.com

まずはプロジェクトにlessを導入

npm install --save-dev less

そしてgulp-less(gulp上でlessをビルドしてくれる機能)も導入します。

npm install --save-dev gulp-less

そして新規にgulpfile.jsをルートに作成します。

//gulpfile.js
const gulp = require('gulp');
const less = require('gulp-less');

// less → cssにコンパイルするタスク
gulp.task('less', function(){
  src('./src/less/*.less')
  .pipe(less())
  .pipe(gulp.dest('/src/css/*'));
});

//ファイルの変更があった場合、実行されるタスク
gulp.watch('watch', ['less'], function() {
  gulp.watch('./src/less/*.less', ['less']);
});

// デフォルトで実行してもらいたいタスクを配列で定義する。
gulp.task('default', ['less']);

すごく直感的に書けると思います。
まずはgulp.taskの第一引数に'less'という名前を指定しました。
これでgulp lessというコマンドを実行したら第二引数のcallbackが実行されます。
callbackの中身はlessファイルをcssコンパイルするという動作ですね。

そしてgulp.watchでファイルの変更があった場合(このソースの場合、.src.less/*.lessの変更)
gulp.taskで指定されたlessを実行するという定義になります。

また、第二引数の['less']ですがgulp watchを実行する前に一度走らせたいタスクを配列で記入していきます。
gulp watchを実行する前にlessを変更した場合にも実行して貰いたいので。

最後にpackage.jsonにこちらを記入します。
今回はlessのコマンド実行なのでスクリプト名をlessにしていますが任意の名前で大丈夫です。

//package.json
scripts: {
    "watch":  "gulp watch";
}

そしてpackage.jsonのある階層で下記を実行するとコンパイルされるはずです。

npm run watch

設定自体楽チンで、何よりcssよりも綺麗に書けるのでlessの導入はお勧めです。ぜひgulpをお試し下さい!