tekitoumemo’s diary

C#、.NET系の技術ブログを書いています

運用しているサイトで使っている.Net Coreのミドルウェア

f:id:tekitoumemo:20180610201124p:plain

mygkrnk.com
地味ーな努力と運によってSEO4位(「洋楽ランキング」で)まで行きました。今月は12KPVまで行く見込みで、予想よりはるかに上回りました。

ということで、.Net Coreで作っているみんなの洋楽ランキングで使っているミドルウェアの紹介します。正直、超スモールサイトなので結構少ないです。

セッション管理

.Net Coreのセッション管理は「Microsoft.AspNetCore.Session」を使います。

Microsoft.AspNetCore.Session」をインストール

dotnet add package Microsoft.AspNetCore.Session --version 2.1.0

Startup.csに以下が必要です。

using Microsoft.Extensions.DependencyInjection;
...
public void ConfigureServices (IServiceCollection services) {
services.AddDistributedMemoryCache ();
services.AddSession ();
...
}
...
public void Configure (IApplicationBuilder app, IHostingEnvironment env) {
...
app.UseSession ();
...
}

URLを小文字にする

ここらへんはMVCでも同じですね。

public void ConfigureServices (IServiceCollection services) {
    services.Configure<RouteOptions> (options => {
        options.LowercaseUrls = true;
    });
}

エラー系ミドルウェア

エラーハンドリングの記事を書いたので以下を参考にしてください。
tekitoumemo.hatenablog.com
tekitoumemo.hatenablog.com

静的ファイルを使う系

みんなの洋楽ランキングはファイルサーバーとWEBサーバーを併用してるのでwwwrootにアクセスする必要がありました。なのでwwwrootのファイルを扱えるようにします。

using Microsoft.Extensions.FileProviders;
public void ConfigureServices (IServiceCollection services) {
services.AddSingleton<IFileProvider> (
    new PhysicalFileProvider (
    Path.Combine (Directory.GetCurrentDirectory (), "wwwroot")));
...
}
public void Configure (IApplicationBuilder app, IHostingEnvironment env) {
     app.UseStaticFiles ();
     ....
}

本当はファイルサーバー用意してやった方が良いけど、コストがかかるのでとりあえずと言う感じ。

リダイレクト

azureドメインから本ドメインにリダイレクトするためのミドルウェアです。IISのパターンのみ。Apacheの場合はAddApacheModRewriteを使えばよいです。
まずはwwwroot直下にIISUrlRewrite.xmlを用意します(名前はなんでも良いっぽいです。)。
IISUrlRewrite.xmlは以下のように設定しました。

<rewrite>
    <rules>
        <rule name="mygkrnk.azurewebsites.net" stopProcessing="true">
            <match url="(.*)" />
            <conditions>
                <add input="{HTTP_HOST}" pattern="^mygkrnk\.azurewebsites\.net$" />
            </conditions>
            <action type="Redirect" url="https://mygkrnk.com/{R:1}" redirectType="Permanent" />
        </rule>
    </rules>
</rewrite>

次にStartup.csに以下を設定します。

using (StreamReader iisUrlRewriteStreamReader = File.OpenText (Path.Combine (Directory.GetCurrentDirectory (), "wwwroot", "IISUrlRewrite.xml"))) {
    var options = new RewriteOptions ()
        .AddIISUrlRewrite (iisUrlRewriteStreamReader);
    app.UseRewriter (options);
}

公式に書いてあるのでそれ通りやった感じですね。
docs.microsoft.com

.Net Core2.1 MVCの互換性レベルの指定

.Net Core2.1rcから.Net Core2.1にアップデートしたので、その対応です。これは別の記事にする予定です。

services.AddMvc ()
    .SetCompatibilityVersion (CompatibilityVersion.Version_2_1);

SPAじゃないReactを.Net Coreで扱う

ちょっと特殊なReactの使い方を説明します。MVCで部分的にJqueryを使っていることが多いと思いますが、そのJqueryで作った部分がReactに置き換わる形です。ちなみに業務でAngularは触っているのですが、Reactは触っていませんので超ド素人です(通勤で調べて今試した感じ(笑))。なので、Reactの説明と言うよりは、.Net Coreのテンプレートをどう改造すれば部分コンポーネントとして扱えるかって記事になります。

なぜReactなのか?

ReactはViewに特化したフレームワークなので手軽に始められそうっていう浅はかな考えがきっかけ。フルスタックなAngularと違って最低限なライブラリしか用意されてないらしいので、無理にJSで作ろうと無茶な開発をしないっても大きいです。あとReact Nativeがあるから学習するなら一番コスパ高いです。

フルスタックの方が良いのでは?

個人的な意見ですが、今の段階でフルスタックなJSを使ってWEBサービスは作らない方が良いと思います。Angularなんか割となんでもできるので、サーバーサイドで作るかフロントで作るか結構曖昧になってしまってサーバーでもフロントでも同じ処理があるじゃんってオチになりがちな印象があります。さらにコンポーネントを分けたはいいものの、結局Input、Output、ViewChild、Subjectなど多用してわけわからなくなる状況に陥りがちです(前の現場はヒドかった。。)時間と意欲があればなんでも良いと思いますけどね。

で本題ですが、SPAじゃないReactとは以下の画像のような感じをイメージしてもらえればと思います。
f:id:tekitoumemo:20180608223633p:plain

緑がMVCで出力しているViewで水色、ピンクがReactのそれぞれのコンポーネントになっています。SNSだったらいいね機能、コメント機能でコンポーネントを分けられるので、綺麗に作れます。昔JquerySNSチックなので作ったのですが、メンテしづらいコードになっちゃいました。。これをJSフレームワークで作れたら結構綺麗になっただろうなーと思います。このサイトです(宣伝)

作り方

dotnet coreのテンプレートを使用しますので、以下のコマンドを入力します。特にdotnetに限ったことじゃないですが圧倒的に楽です。

dotnet new react -o {ディレクトリ名}

次にNodeのパッケージをインストールしてきます。

npm i

これでテンプレートは動くはずです。

boot.tsxを改造する

テンプレートだと以下のコードになっているはずです。

import "./css/site.css";
import "bootstrap";
import * as React from "react";
import * as ReactDOM from "react-dom";
import { AppContainer } from "react-hot-loader";
import { BrowserRouter } from "react-router-dom";
import * as RoutesModule from "./routes";
let routes = RoutesModule.routes;
function renderApp() {
    // This code starts up the React app when it runs in a browser. It sets up the routing
    // configuration and injects the app into a DOM element.
    const baseUrl = document.getElementsByTagName("base")[0].getAttribute("href")!;
    ReactDOM.render(
        <AppContainer>
            <BrowserRouter children={ routes } basename={ baseUrl } />
        </AppContainer>,
        document.getElementById("react-app")
    );
}

renderApp();

// Allow Hot Module Replacement
if (module.hot) {
    module.hot.accept("./routes", () => {
        routes = require<typeof RoutesModule>("./routes").routes;
        renderApp();
    });
}

これはid="react-app"と記載されているところに表示されます。SPA用のマウントなのでルーティング用のパラメータが指定されてます。このReactコンポーネントのマウントをベタがきします。

import "./css/site.css";
import "bootstrap";
import * as React from "react";
import * as ReactDOM from "react-dom";

import { Counter } from "../ClientApp/components/Counter";
import { FetchData } from "../ClientApp/components/FetchData";
function renderApp(): void {
    // これ
    ReactDOM.render(<Counter />,document.getElementById("counter-app"));
    ReactDOM.render(<FetchData />,document.getElementById("fetch-app"));
}
renderApp();

上記のコードを噛み砕きます。
まずはコンポーネントをimportします。

import { Counter } from "../ClientApp/components/Counter";
import { FetchData } from "../ClientApp/components/FetchData";

次にコンポーネントをマウントします。

    ReactDOM.render(<Counter />,document.getElementById("counter-app"));
    ReactDOM.render(<FetchData />,document.getElementById("fetch-app"));

これでdocument.getElementByIdで指定したタグにコンポーネントが紐づきます。Railsだとreact-railsっていうgemがあるみたいなんだけどnugetにはないのかな?

次にコンポーネント側の修正します。
以下のコードを修正します。

export class Counter extends React.Component<RouteComponentProps<{}>, CounterState> {
// ↓に変更する
export class Counter extends React.Component<{},any> {

テンプレートのままだとboot.tsxでエラーになりますので「RouteComponentProps<{}>, CounterState」を排除しましょう。Stateはコンポーネント内で定義すればいらないので排除しちゃって良いっぽいです(正直よくわかってない)

これでマウントされたコンポーネントがつかえるようになります。簡単でしたが意外と探しても少なかったので調べました。このソースコードgithubにおいたので興味ある方は試してみてください。これからReactバリバリ使う予定なので、もっと理解深めてブログに書いてきます。次は.NetCore MVCにReactを入れるミドルウェアとかの紹介かな<さっそくReactじゃない
github.com

ちなみに

.Net CoreのテンプレートだとTypeScriptがReactでもついてる!やば最高。

"typescript": "2.4.1",

参考
非SPAなサービスにReactを導入する - クックパッド開発者ブログ
【react】jqueryメインの非SPAシステムの特定ページでのみreactを導入してみたのでメモ - とりあえずphpとか

Macで作ったASP.NET MVC CoreのDocker ImageをHerokuで動かす【想像編】

タイトル長杉
f:id:tekitoumemo:20180601214759p:plain
最近Azureにハマってます。今回のタイトルはAzureで出来ますし楽ですしやる意味わからんと思う人がいると思いますが、それなりに理由があります。

なぜにへろく?

SSLを使った画面が必要になったためです。

で、SSL化するためにはインフラの選定が必要です。ほぼ稼働しないと考えて良いサイトなので、お金がかからないってのが一番重要でHerokuはHerokuドメインならSSLが付いてきます。AzureもSSLなんですが無料は1日60分です、アホ。有料だと最低でも7000円です、アホ。

いくつか候補をあげました。

・そのままxdomainでSSL化する
ない。絶対にない。有料だし意味わかんない。あ、ちなみにxdomainのPHPサーバで運用してるしょぼアプリです。
・Azure、GCPAWSのいずれかを使う
あり。でもほとんど稼働しないサイトにお金を使う意味があるのか?
railsで作ってHerokuにあげる
これかなり有力。今朝までこれで考えてたのでrailsを調べてました笑
・Core MVCのDocker ImageをHerokuで動かす
絶対これ。.Net CoreとDockerとHerokuって言うパワーワードにやられました。

Herokuは月550時間まで無料なので運用費0です。って言うことでDockerで作ってみます。

まだ実際に作ってないので想像編です。多少調べたんで作り方を書いて、実際試したときの違いなどを違う記事で書ければと思います。

OSはMacです。WindowsのDocker Imageはてんでダメで動かんとの記事を見たのでこれが良いでしょう。

Docker for Macを入れる

正しくインストールされているか確認する

docker version

HomebrewでHeroku CLI をインストール

brew install heroku/brew/heroku

ASP.NET Core MVC プロジェクトを作成

dotnet new mvc -o contact

いつも思うけど-oオプションが指定された記事少ない!わからんでプロジェクト作成するとカレントに作られてぐしゃぐしゃになるんでちゃんと指定する例があった方が良いと思うの。

プロジェクト発行

dotnet publish -c Release

Dockerファイル作成

FROM microsoft/aspnetcore

WORKDIR /app

COPY . .

CMD ASPNETCORE_URLS=http://*:$PORT dotnet contact.dll

Dockerイメージ作成

docker build contact . 

Heroku用のタグ付け

docker tag contact registry.heroku.com/<Heroku アプリ名>/web

デプロイ

docker push registry.heroku.com/<Heroku アプリ名>/web

これで動くのは実証されてるんですが、メールとか動くんかな?まぁ多分詰まるのはここじゃない感じがするけど

<追記>
実際にデプロイまでできたのですが、反映されません。もしかして.net core 2.1だと出来ない!?.net core2.0の記事をみてたから
違いがそこらぐらいしかわからない(´・ω・`)困った。

参考
Deploy asp.net core 2.0 apps on Heroku – Devcenter Square Blog
ASP.NET Core MVC アプリケーションの Docker コンテナを Heroku で動かしてみた - present

正式版の.NET Core 2.1がリリースされました。

.NET Core 2.1の正式版が5/30にリリースされました。
github.com

そこまで感動する変更はないのですが、まぁ良い感じだったのでリリースノートに添って書いてきます。

Linuxインストーラの変更とディストリビューションの更新

Debian系のOSでパッケージマネージャのアップデート(apt-get update)がサポートされるようです。以下のようなコマンドがいらなくなる感じだとおもう。

wget -q packages-microsoft-prod.deb https://packages.microsoft.com/config/ubuntu/16.04/packages-microsoft-prod.deb

NET Core 2.1はUbuntu 18.04とFedora 28で利用可能です。

サポート範囲狭杉

.NET Core Tools

いまいちよくわからないけど、以下のコマンドが使えるようになったらしい。

dotnet tool

dotnet build プロセス制御

ちょっと便利。
ビルドサーバープロセスを手動で終了。これCtrl+Cでできなかったっけ?

dotnet build-server shutdown

ワーカープロセスが作成されないようにする。地味に残るから結構よさそう。

dotnet build -nodeReuse:false

ネットワークパフォーマンス

プロキシを設定するHttpClientHandlerに代わってよりパフォーマンスの良いものがリリースされたそうです。
あまりここら辺理解できてないので割愛します。

APIも2.0から結構変わってます。
github.com

余裕があれば確実にバージョンを上げたほうが良いね!

ASP.NET Core MVCのエラーハンドリング【起動時編】

前回の記事の続きです。やっとみんなの洋楽ランキングの.NET Core対応が終わりましたので今週末に完全移行します!.Net CoreなのでAzureからGCPに行こうしようと思いましたがやっぱりAzure良い!特に不満がなければAzureで行こうと思ってます。近いうちにLinuxサーバーに切り替える予定。
tekitoumemo.hatenablog.com

話がそれましたが、「ASP.NET Core MVCのエラーハンドリング【起動時編】」です。

前回はあくまでMVCのエラーハンドリングなので、Startup.csやProgram.csのエラーが取れません。なので、起動時のエラーを取る必要があります。ということで公式を見てみます。

アプリの起動中に起こる例外はホスティング層だけが処理できます。 Web ホストを使うと、captureStartupErrors と detailedErrors キーを利用して、起動中のエラーに対するホストの動作を構成できます。

はい。captureStartupErrors と detailedErrorsを使えばなんとかなるらしいので試してみます。

CaptureStartupErrorsを扱う

Program.csのBuildWebHostに設定します。

public static IWebHost BuildWebHostDevelopment (string[] args) =>
    WebHost.CreateDefaultBuilder (args)
    .UseStartup<Startup> ()
    .CaptureStartupErrors (true) // これ
    .Build ();

Startup.csにエラーを仕込んで実行します。そうすると見慣れたエラー画面が出てきます。
f:id:tekitoumemo:20180528215528p:plain
Startupのエラーをキャッチするもののようです。本番でこんなの使えませんのでDevelopmentモードで使いましょう。

detailedErrorsを扱う

同じくProgram.csのBuildWebHostに設定します。

public static IWebHost BuildWebHostDevelopment (string[] args) =>
    WebHost.CreateDefaultBuilder (args)
    .UseStartup<Startup> ()
    .UseSetting ("detailedErrors", "true") // これ
    .Build ();

同じくStartup.csにエラーを仕込んで実行します。そうするとWEBサーバーが立ち上がることなく終了します。よくわからないのでググるIISのオプションでスタックトレースを返すオプションだそうです。

Controls whether a stack trace is returned when there is a fault generated from the Web service.

<detailedErrors> Element
こちらは本番のエラーハンドリングに使えそうです。

Production用とDevelopment 用のWeb ホストを用意する

CaptureStartupErrorsはDevelopmentに、detailedErrorsはProductionにとそれぞれWEBホストを用意します。

// Production
public static IWebHost BuildWebHosProduction (string[] args) =>
    WebHost.CreateDefaultBuilder (args)
    .UseStartup<Startup> ()
    .UseSetting ("detailedErrors", "true")
    .Build ();
// Development
public static IWebHost BuildWebHostDevelopment (string[] args) =>
    WebHost.CreateDefaultBuilder (args)
    .UseStartup<Startup> ()
    .CaptureStartupErrors (true)
    .Build ();
    }

これで環境によってエラーハンドリングの準備が整いました。次に切り替えを作成します。

環境によってWEBホストを切り替える

上記の例からProductionはBuildWebHosProduction、DevelopmentはBuildWebHostDevelopmentが実行されるよう以下のように記述します。

public static void Main (string[] args) {
    if (Environment.GetEnvironmentVariable ("ASPNETCORE_ENVIRONMENT") == "Development") {
        BuildWebHostDevelopment (args).Run ();
    } else {
        BuildWebHosProduction (args).Run ();
    }
}

Environment.GetEnvironmentVariable ("ASPNETCORE_ENVIRONMENT") で実行モードを取得できるので文字列によって、実行するWEBホストを切り替えます。最後に本番用にエラーを取得する処理を記述します。

起動時のエラーを取得する

Startup.csにtry、catchを記載します。正直これが綺麗なのかいまだに謎ですが、StackOverflowにそのやり方が書いてあったので信じましょう。

public static void Main (string[] args) {
    try {

        if (Environment.GetEnvironmentVariable ("ASPNETCORE_ENVIRONMENT") == "Development") {
            BuildWebHostDevelopment (args).Run ();
        } else {
            BuildWebHosProduction (args).Run ();
        }
    } catch (Exception ex) {
        var mailManager = new MailManager {
            Subject = "[みんなの洋楽ランキング]エラー報告",
            Body = string.Format ("{0}\n{1}", ex.Message, ex.StackTrace)
        };
        mailManager.SendEmailAsync ().Wait ();
    }
}

Developmentモードの場合はCaptureStartupErrorsがエラーをキャッチするので、tryで捕捉されずにブラウザに出力されます。Productionの場合はtryで捕捉し、メールを飛ばす仕様になっています。以下の例ではserilogの捕捉方法ですが、やりたいことはだいたい一緒です。C#の7.1からMainにasyncが使えるようですが、終了するだけなのでWaitを使ってます(MVCでもasyncが使えるのか不明)
github.com

アプリケーション全体にtry catchステートメントを追加することを躊躇しています

と言ってますので、やっぱりなんとも言えないやり方かもしれませんがエラーが取れるのでよしとしましょう。

ざっとですが、これでMVCアプリケーションのエラーが取れました。起動時のエラーハンドリングはなかなか定まってなさそうですが、状況にあった取得方法を考えていければと思います。


検索順位が1ページ目まで上がってきた!検索ボリュームがなかなか多いキーワードなので嬉しい!!ここらへんも何をやってるか随時ブログにあげていきます。
f:id:tekitoumemo:20180528223323p:plain

ASP.NET Core MVCのエラーハンドリング【MVC編】

みんなの洋楽ランキングにて.Net Coreのエラーハンドリングを実装する必要がありました。エラーハンドリングのやり方は公式でも載ってるのですが、実用的な記事があまりないので僕なりのやり方をここで紹介しようと思います ※これは私のオリジナールも含まれているのであくまで参考程度に

例外フィルター

MVC5でもあったOnExceptionを使用します。MVC5ではフィルターじゃなくても実装出来ましたが公式がフィルターの例を出しているのでその例を以下に記載します。

public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
    private readonly IHostingEnvironment _hostingEnvironment;
    private readonly IModelMetadataProvider _modelMetadataProvider;

    public CustomExceptionFilterAttribute(
        IHostingEnvironment hostingEnvironment,
        IModelMetadataProvider modelMetadataProvider)
    {
        _hostingEnvironment = hostingEnvironment;
        _modelMetadataProvider = modelMetadataProvider;
    }

    public override void OnException(ExceptionContext context)
    {
        if (!_hostingEnvironment.IsDevelopment())
        {
            // do nothing
            return;
        }
        var result = new ViewResult {ViewName = "CustomError"};
        result.ViewData = new ViewDataDictionary(_modelMetadataProvider,context.ModelState);
        result.ViewData.Add("Exception", context.Exception);
        // TODO: Pass additional detailed data via ViewData
        context.Result = result;
    }
}

MVC5とほぼ一緒です。_hostingEnvironment.IsDevelopment()はDevelopモードの場合はなにもしないという意味です。私の場合、こんなにいろいろ必要なかったのでめちゃくちゃシンプルにしました。

public override async void OnException (ExceptionContext context) {
    var mailManager = new MailManager {
        Subject = "[みんなの洋楽ランキング]エラー報告",
        Body = string.Format ("{0}\n{1}", context.Exception, context.Exception.StackTrace)
    };
    await mailManager.SendEmailAsync ();
    context.Result = new ViewResult { ViewName = "CustomError" };
}

MVCで例外が発生したらメールを送るだけです。メールの送信方法は以下を参考にしてください。
tekitoumemo.hatenablog.com

これでコントローラーに以下のフィルターを追加するとこちらが動きます。

[CustomExceptionFilter]

OnExceptionは例外を拾ってくれるのですが404とか拾ってくれないので以下のコードをStartup.csにDIします。

if (env.IsDevelopment ()) {
     app.UseDeveloperExceptionPage ();
} else {
     app.UseExceptionHandler ("/Home/Error");
}
app.UseStatusCodePagesWithReExecute("/Home/Error");
app.UseDeveloperExceptionPage

Depelopモードの際にエラーをブラウザに出力します。こちらは2.1ならプロジェクト作成したときに自動生成されます。
https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/error-handling/_static/developer-exception-page.png?view=aspnetcore-2.0

app.UseExceptionHandler

例外が発生した際に引数で指定したURLのハンドラを実行します。こちらも2.1ならプロジェクト作成したときに自動生成されます

app.UseStatusCodePagesWithReExecute

元のステータスコードをクライアントに返します。引数で指定したURLのハンドラも実行します。こちらは自動生成されないのでDIしなければいけません。

これで400番台のエラーの対応も出来ました。次にエラー用のアクションメソッドを用意します。

エラー用のアクションメソッド

こちらはデフォルトで出来ている( 2.1なら)はずなので僕の実装したコードを記載します。

public IActionResult Error () {

    switch (HttpContext.Response.StatusCode) {
        case StatusCodes.Status404NotFound:
    // 404の処理
            break;
        case StatusCodes.Status500InternalServerError:
    // 500の処理
            break;
    }
    return View ();
}

HttpContext.Response.StatusCodeからHTTPステータスコードを取得できるのでステータスコードに応じたビューモデルの生成などの処理を入れていけば良いと思います。

処理の順番

Controllerで例外が発生したらどの順番で動くか検証しました。

Controller(例外!!)→例外フィルター→UseExceptionHandlerで指定したURL

例外フィルタの動作は以下のようです。Controllerで補足したエラーをフィルターが拾ってそのあとにUseExceptionHandlerが動く感じですかね。
https://docs.microsoft.com/ja-jp/aspnet/core/mvc/controllers/filters/_static/filter-pipeline-1.png?view=aspnetcore-2.0

これらの実装が正しいのかわかりませんが、公式を見て自分なりに実装した感じではこれが妥当かなと思いました。
次のブログでは起動時の例外処理、つまりMVC以外でエラーが起きたときのエラーハンドリングを紹介しようかと思います。こちらは公式の説明がシンプルなので割と参考になると思います。公式の説明は以下です。

captureStartupErrors と detailedErrors キーを利用し、起動中のエラーに対するホストの動作を構成できます。

雑すぎぃ!!

参考
ASP.NET Core のエラーを処理する | Microsoft Docs
ASP.NET Core フィルター | Microsoft Docs

【.Net Core】.Net Coreでも正式にTransactionScopeが使えるようになった!

https://4.bp.blogspot.com/-95JagEL6FDQ/WVgFqpNQeEI/AAAAAAAAE3A/Gtl6RqYjufcVH3jA7ydyh9GZGsGuHTkxACLcBGAs/s1600/DBvsDW.png

トランザクションを書くとき、成功したらcommit、失敗したらrollbackと結構めんどいのですが、それらを解決してくれるのがTransactionScopeです。例で書いた方がわかりやすいので以下で説明します。例はDapperを使います。

通常のトランザクション
cn.Open ();
using (var tr = cn.BeginTransaction ()) {
    try {
        cn.Execute ("UPDATE TEST SET Hoge = @Hoge Where ID = @ID", new { Hoge = 1, ID = 1 }, tr);
        // コミット
        tr.Commit ();
    } catch (Exception e) {
        // ロールバック
        tr.Rollback ();
    }
}
TransactionScopeのトランザクション
using (var tr = new TransactionScope ()) {
    cn.Open ();
    cn.Execute ("UPDATE TEST SET Hoge = @Hoge Where ID = @ID", new { Hoge = 1, ID = 1 });
    // コミット 落ちたらここにたどり着かないのでコミットされずにrollback?される(元に戻る?)
    tr.Complete ();
}

通常に比べてエラー処理など必要なく非常に楽です。Dapperの拡張ライブラリなどクエリを発行ごとにコネクションを作ってたりするので通常で使うには拡張しなければいけない場合があります(拡張ライブラリを拡張するとか、もうね)

System.Data.SqlClientの4.5 previewでは使えたのですが、正式のNugetには上がっていませんでした。

10日前にNugetに上がっていました!4.5.0-rc1ってやつです。
www.nuget.org

ちなみに「-rc」はNugetパッケージのプレリリースを指していて完全な正式版ではないのでお間違えないように(Nugetに上がってればおっけいでしょ)

System.Data.SqlClientのほかにSystem.Diagnostics.DiagnosticSourceも入れる必要があります。
www.nuget.org

導入方法

System.Data.SqlClientを追加

dotnet add package System.Data.SqlClient --version 4.5.0-rc1

System.Diagnostics.DiagnosticSourceを追加

dotnet add package System.Diagnostics.DiagnosticSource --version 4.5.0-rc1

※ 注意!「System.Data.SqlClient」と「System.Diagnostics.DiagnosticSource」はバージョンを合わせてください!落ちます。

ソース

using System.Data.SqlClient;
using System.Transactions;
using (var tr = new TransactionScope ()) {
    cn.Open ();
    cn.Execute ("UPDATE TEST SET Hoge = @Hoge Where ID = @ID", new { Hoge = 1, ID = 1 });
    tr.Complete ();
}

正式に?ってのはちょっと語弊があったかもしれませんがNugetに上がったので利用しやすくなっていると思います!Nugetじゃなくても良いって人は4.6preiewが出ているので試しても良いかもしれません。公式は以下に貼っておきます。
dotnet-core - System.Data.SqlClient 4.6.0-preview3-26501-04 - MyGet - Hosting your NuGet, npm, Bower, Maven, PHP Composer and Vsix packages