xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

.NET 6 で Microsoft.Extensions.Configuration から DateOnly / TimeOnly 型に直接マッピングする

.NET 6 で DateOnly および TimeOnly 型が追加されました。日付や時間のみを扱う (若干残念な名前を除けば) 待望の子ですね。

ところで、アプリケーション構成として日付や時間 (特に時間) を扱うことはちょくちょくあるのではないかと思います。現代の .NET (Core 系) でアプリケーションの構成情報を利用すると言えば Microsoft.Extensions.ConfigurationIConfiguration ですが、実はここから DateOnlyTimeOnly に直接マッピングできません、残念ながら。DateTimeDateTimeOffset などはできるのに!

Microsoft.Extensions.Configuration の実装を追いかけてみると TypeConverter を通してマッピングを図るようになっているのですが、.NET 6 時点では DateOnlyTimeOnly に対応する TypeConverter が登録されていないために動作しないんですね。この問題は .NET Runtime Team も認識していて、.NET 7 Preview 6 で既に修正されています。

でも我々は (というか僕は) .NET 6 でも使いたい!ということで .NET 6 でも DateOnlyConverterTimeOnlyConverter を利用できるようにしてしまいましょう。

TypeConverter を実装

まず .NET 7 で実装された DateOnlyConverterTimeOnlyConverter を持ってきます。一部コンパイルが通らない箇所があるのでそこだけ修正します。若干ずるいけどこれがベストなのだ...(ゴゴゴ

修正箇所

  • .NET 7 以降ではこの実装は不要なので #if NET6_0 を追加
  • String Resources を直接展開
  • .NET 7 で新規に追加された TimeOnly.Microsecond プロパティに関する箇所を削除

実装の全貌

そこそこ実装が長いので折り畳み状態にしました。ご了承ください。

DateOnlyConverter.cs

#if NET6_0

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel.Design.Serialization;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;

namespace System.ComponentModel;

/// <summary>
/// Provides a type converter to convert <see cref='System.DateOnly'/> objects to and from various other representations.
/// </summary>
public class DateOnlyConverter : TypeConverter
{
    /// <summary>
    /// Gets a value indicating whether this converter can convert an object in the given source type to a <see cref='System.DateOnly'/>
    /// object using the specified context.
    /// </summary>
    /// <inheritdoc />
    public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
    {
        return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
    }

    /// <inheritdoc />
    public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
    {
        return destinationType == typeof(InstanceDescriptor) || base.CanConvertTo(context, destinationType);
    }

    /// <summary>
    /// Converts the given value object to a <see cref='System.DateOnly'/> object.
    /// </summary>
    /// <inheritdoc />
    public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
    {
        if (value is string text)
        {
            text = text.Trim();
            if (text.Length == 0)
            {
                return DateOnly.MinValue;
            }

            try
            {
                // See if we have a culture info to parse with. If so, then use it.
                DateTimeFormatInfo? formatInfo = null;

                if (culture != null)
                {
                    formatInfo = (DateTimeFormatInfo?)culture.GetFormat(typeof(DateTimeFormatInfo));
                }

                if (formatInfo != null)
                {
                    return DateOnly.Parse(text, formatInfo);
                }
                else
                {
                    return DateOnly.Parse(text, culture);
                }
            }
            catch (FormatException e)
            {
                var message = $"{text} is not a valid value for {nameof(DateOnly)}.";
                throw new FormatException(message, e);
            }
        }

        return base.ConvertFrom(context, culture, value);
    }

    /// <summary>
    /// Converts the given value object from a <see cref='System.DateOnly'/> object using the arguments.
    /// </summary>
    /// <inheritdoc />
    public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
    {
        if (destinationType == typeof(string) && value is DateOnly dateOnly)
        {
            if (dateOnly == DateOnly.MinValue)
            {
                return string.Empty;
            }

            culture ??= CultureInfo.CurrentCulture;

            DateTimeFormatInfo? formatInfo = (DateTimeFormatInfo?)culture.GetFormat(typeof(DateTimeFormatInfo));

            if (culture == CultureInfo.InvariantCulture)
            {
                return dateOnly.ToString("yyyy-MM-dd", culture);
            }

            string format = formatInfo!.ShortDatePattern;

            return dateOnly.ToString(format, CultureInfo.CurrentCulture);
        }

        if (destinationType == typeof(InstanceDescriptor) && value is DateOnly date)
        {
            return new InstanceDescriptor(typeof(DateOnly).GetConstructor(new Type[] { typeof(int), typeof(int), typeof(int) }), new object[] { date.Year, date.Month, date.Day });
        }

        return base.ConvertTo(context, culture, value, destinationType);
    }
}

#endif

TimeOnlyConverter.cs

#if NET6_0

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel.Design.Serialization;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;

namespace System.ComponentModel;

/// <summary>
/// Provides a type converter to convert <see cref='System.TimeOnly'/> objects to and from various other representations.
/// </summary>
public class TimeOnlyConverter : TypeConverter
{
    /// <summary>
    /// Gets a value indicating whether this converter can convert an object in the given source type to a <see cref='System.TimeOnly'/>
    /// object using the specified context.
    /// </summary>
    /// <inheritdoc />
    public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
    {
        return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
    }

    /// <inheritdoc />
    public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
    {
        return destinationType == typeof(InstanceDescriptor) || base.CanConvertTo(context, destinationType);
    }

    /// <summary>
    /// Converts the given value object to a <see cref='System.TimeOnly'/> object.
    /// </summary>
    /// <inheritdoc />
    public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
    {
        if (value is string text)
        {
            text = text.Trim();
            if (text.Length == 0)
            {
                return TimeOnly.MinValue;
            }

            try
            {
                // See if we have a culture info to parse with. If so, then use it.
                DateTimeFormatInfo? formatInfo = null;

                if (culture != null)
                {
                    formatInfo = (DateTimeFormatInfo?)culture.GetFormat(typeof(DateTimeFormatInfo));
                }

                if (formatInfo != null)
                {
                    return TimeOnly.Parse(text, formatInfo);
                }
                else
                {
                    return TimeOnly.Parse(text, culture);
                }
            }
            catch (FormatException e)
            {
                var message = $"{text} is not a valid value for {nameof(TimeOnly)}.";
                throw new FormatException(message, e);
            }
        }

        return base.ConvertFrom(context, culture, value);
    }

    /// <summary>
    /// Converts the given value object from a <see cref='System.TimeOnly'/> object using the arguments.
    /// </summary>
    /// <inheritdoc />
    public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
    {
        if (destinationType == typeof(string) && value is TimeOnly timeOnly)
        {
            if (timeOnly == TimeOnly.MinValue)
            {
                return string.Empty;
            }

            culture ??= CultureInfo.CurrentCulture;

            DateTimeFormatInfo formatInfo = (DateTimeFormatInfo)culture.GetFormat(typeof(DateTimeFormatInfo))!;

            return timeOnly.ToString(formatInfo.ShortTimePattern, CultureInfo.CurrentCulture);
        }

        if (destinationType == typeof(InstanceDescriptor) && value is TimeOnly time)
        {
            if (time.Ticks == 0)
            {
                return new InstanceDescriptor(typeof(TimeOnly).GetConstructor(new Type[] { typeof(long) }), new object[] { time.Ticks });
            }

            return new InstanceDescriptor(typeof(TimeOnly).GetConstructor(new Type[] { typeof(int), typeof(int), typeof(int), typeof(int) }),
                                            new object[] { time.Hour, time.Minute, time.Second, time.Millisecond });
        }

        return base.ConvertTo(context, culture, value, destinationType);
    }
}

#endif

参照元

TypeConverter を登録

TypeConverter の porting ができたら、.NET Runtime が認識できるよう登録していきましょう。ザックリ以下のようにします。

#if NET6_0

public static class TypeConverterShims
{
    public static void Register()
    {
        register<DateOnly, DateOnlyConverter>();
        register<TimeOnly, TimeOnlyConverter>();

        static void register<TObject, TConverter>()
        {
            var attribute = new TypeConverterAttribute(typeof(TConverter));
            TypeDescriptor.AddAttributes(typeof(TObject), attribute);
        }
    }
}

#endif

最後にこれをアプリケーション起動時に一度呼び出せば OK です。IConfiguration.Get<T>(); などの前に呼び出すことを忘れずに!

TypeConverterShims.Register();

Azure App Service の Always On リクエストにのみ応答する

Azure App Service を使っている場合、特に本番環境では Always On を有効化することになると思います。日本語の Azure Portal だと「常時接続」と表記されるもので、一定間隔でホストしている Web アプリに対してリクエストを投げることで、アプリがアイドル状態にならないようにするものです。Cold Start になると初速が出ないので、その対策に使われるものですね。

この Always On 設定を有効化していると、Azure App Service が定期的に Root URL (= /) に対して GET メソッドでアクセスしてきます。「それが何だよ」って話なのですが、Web アプリケーションの作りに依っては 404 (Not Found) を返してエラーとして検知してしまうことがあります。

UI のある Web サービスであれば GET / がメインの Landing Page になるため特段問題にならないのですが、Web API なサービスをホストしている場合には API 定義として GET / でアクセス可能な Endpoint を用意していないことが結構よくあるんですよね。ってゆーか用意しないですよね、ほぼ!

Always On は有効にしたいけど 404 は検知したくない!だって気持ち悪いもん!

となるわけです。なりませんか?w

どう対策するか

ということで 200 OK を返すだけの GET / な Endpoint を用意すればよいですね。以上終了。

とはならないです。エラーを回避するためだけに実際の運用要件として不要な GET / な API を公開するのは好ましくありません。特段問題にもなりにくいですが、不要なものを workaround として用意するのは気持ち悪いです。なので、可能であれば Azure App Service から飛んでくる Always On リクエストにのみ 200 OK で応答したい です。

そこで Always On リクエストについて調べてみると、かなり特殊なアクセスのされ方をしていることが分かりました。下記記事に詳細があるので引用します。

REQUEST_URI     = /
REQUEST_METHOD  = GET
SERVER_PROTOCOL = HTTP/1.1
REMOTE_ADDR     = ::1
REMOTE_PORT     = 21353
REMOTE_HOST     = ::1
HTTP_REFERER    =
HTTP_USER_AGENT = AlwaysOn
HTTP_CONNECTION = Keep-Alive

このうち以下であるかどうかを見分けられれば良さそうですね。

  • GET メソッドでのリクエスト
  • アクセス URL は /
  • リクエスト元が自分自身 (= Loopback アドレス)
  • User-Agent が AlwaysOn

実装してみる

Always On リクエストに対しては 200 OK を返すだけなので、Request Pipeline の早い段階で応答してしまうのが効率がよさそうです。ということで Middleware を実装しましょう。ASP.NET Core Middleware 自体やカスタム Middleware の作り方については公式ドキュメントをご覧ください。

internal sealed class AzureAppServiceAlwaysOnResponseMiddleware
{
    private RequestDelegate Next { get; }

    public AzureAppServiceAlwaysOnResponseMiddleware(RequestDelegate next)
        => this.Next = next;

    public async Task InvokeAsync(HttpContext http)
    {
        // Always On リクエストなら 200 OK を返す
        if (isAlwaysOn(http))
        {
            http.Response.StatusCode = (int)HttpStatusCode.OK;
            return;
        }

        // それ以外は Request Pipeline を継続
        await this.Next(http);

        // ローカル関数
        static bool isAlwaysOn(HttpContext http)
        {
            // アクセス元 IP を取得
            var ip = http.Connection.RemoteIpAddress;
            if (ip is null)
                return false;

            // Loopback アドレスか
            if (!IPAddress.IsLoopback(ip))
                return false;

            // GET でアクセスされているか
            var request = http.Request;
            if (!HttpMethods.IsGet(request.Method))
                return false;

            // 「/」へのリクエストか
            var path = request.Path;
            if (!path.HasValue)
                return false;

            const StringComparison comparison = StringComparison.Ordinal;
            if (!path.Value.AsSpan().Equals("/", comparison))
                return false;

            // User-Agent が「AlwaysOn」であるか
            foreach (var ua in request.Headers.UserAgent)
            {
                if (ua.AsSpan().Equals("AlwaysOn", comparison))
                    return true;  // カカロット、お前が Always On だ
            }

            return false;
        }
    }
}
public static class IApplicationBuilderExtensions
{
    public static IApplicationBuilder UseAzureAppServiceAlwaysOnResponse(this IApplicationBuilder builder)
        => builder.UseMiddleware<AzureAppServiceAlwaysOnResponseMiddleware>();
}

上記の実装ができたら、例えば以下のように Request Pipeline のそこそこ早い段階に差し込みます。これで完成です!

// Configure application
var builder = WebApplication.CreateBuilder(args);
builder.Host.ConfigureServices(static (context, services) =>
{
    services.AddControllers();
});

// Configure HTTP pipelines
var app = builder.Build();
app.UseHsts();
app.UseHttpsRedirection();
app.UseAzureAppServiceAlwaysOnResponse();  // 例えばこの辺とか
app.UseEndpoints(static endpoints =>
{
    endpoints.MapControllers();
});

// Run application
app.Run();

まとめ

いかがでしたか?今回は Azure App Service の Always On にのみ応答する ASP.NET Core Middleware を実装してみました。Azure App Service ともっと仲良くなれるといいですね!

あいうえお表、10 分で作れますか?

Burikaigi 2022 が開催されました。運営のお仕事と「C# ドキドキ・ライブコーディング対決」の登壇をしました。もう 10 年近く毎年ライブコーディング対決をしていますが、一見「こんなの簡単だろ」と思えるような問題でも全然解けないんですよねぇ...。

今回は問題を載せておきましたので、もしご興味があれば是非トライしてみてください。本記事の最後に回答例を載せておきましたが、解法はいくつもあるのでご参考まで。回答例が表示されないよう勢いよく下までスクロールせず、脳トレとして遊んでみていただけますと幸いです。

f:id:xin9le:20220130002120p:plain

「あいうえお表」を作れ (制限時間 : 10 分)

ということで今回出されたお題がこちら (↑↑) です。事前に用意された初期コードと期待される結果は以下の通りです。SharpLab にも出題内容を用意しておきました。

初期コード

これだけです。

const string aiueo = "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもや ゆ よらりるれろわ   をん";

期待される結果

右寄せ & 縦書き...!これをコンソール画面に表示します。

んわらやまはなたさかあ
  り みひにちしきい
  るゆむふぬつすくう
  れ めへねてせけえ
 をろよもほのとそこお

ちっともできない言い訳

  1. 事前に一切問題を知らされないまま
  2. セッション中に突然出題されて
  3. 納期たった 10 分で
  4. 間をつなぐトークをしながら

できるわけないだろ...!と言いたくもなりますが、本当にできませんでしたww

回答例

一応の回答例を載せておきます。冷静であれば十分解けそうな気はするんですが、人間緊張するとほんと真っ白になりますね...w

using System;
using System.Linq;

const string aiueo = "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもや ゆ よらりるれろわ   をん";
const int rowsCount = 5;
var columns = aiueo.Chunk(rowsCount).Reverse();
for (var i = 0; i < rowsCount; i++)
{
    foreach (var column in columns)
    {
        var value = (i < column.Length) ? column[i] : ' ';
        Console.Write(value);
    }
    Console.WriteLine();
}

起動速度は?メモリ使用量は?Azure App Service における環境ごと (Windows / Linux) の ASP.NET Core 実行時のパフォーマンス差を調べてみた!

業務で (最近では副業でも) ずっと利用している Azure App Service!大変便利で使いやすく、大好きです。ところで最近の風潮的には ASP.NET Core も Docker とか Linux インスタンスでホストするのがポピュラーな感じがします。時代は Linux、ということなんでしょうか。

我らが (?) App Service は Windows インスタンスと Linux インスタンスのどちらも対応しているので、Windows / Linux の両インスタンスの実行時のパフォーマンスの違いをザックリ調べてみたくなりました。というのも、なんか体感的に Linux の起動が Windows よりも遅い気がしたので気になりました。気になったから駆動調査。

あ、「いかがでしたかブログ」的なタイトルはただただ付けてみたかっただけです。深い意味はありません!

メモリ使用量

それぞれの環境にホストしたときのインスタンスのご様子を Application Insights の Live Metrics Stream で確認してみます。結果は以下の通り。だいぶ違いますね。

環境 Committed Memory
Windows 47 MB
Linux 278 MB

f:id:xin9le:20220121150132p:plain

Windows (IIS)、優秀ですね。

起動速度

次に App Service を「停止 -> 再開」としたときに、画面が表示されるまでの時間を計測します。それぞれ 3 回ずつ計測しました。結果は以下の通りで、これまただいぶ違いますね。

環境 1 回目 2 回目 3 回目 平均
Windows 19 秒 18 秒 15 秒 17.3 秒
Linux 78 秒 101 秒 75 秒 84.6 秒

Windows (IIS)、超優秀ですね...!!

まとめ

いかがでしたか?今回は Azure App Service における環境ごと (Windows / Linux) の ASP.NET Core 実行時のパフォーマンス差を調べてみました。調査結果は載せてないですが、レスポンス速度には特段の差はなさそうでした。App Service の内部実装に明るくないのであくまで勘ではあるのですが、

  • Windows は IIS の内部 API をダイレクトに叩いているから速い
  • Linux は Docker ベースで動いている分のフットプリントが大きい

ということなのかなぁなどと思ってみたりしました。実際にそうなのかを教えてくださる方がいらっしゃったら @xin9le まで!

App Service は Linux インスタンスの方がお安いのでコストメリットはありますが、デプロイ時の初速が気になったり、Kudu の使いやすいさなどを重視する場合は (ちょっとコストがかさんでも) Windows を選択するが良さそうかなって思うなどしました。サーバーのホスト環境としては Windows にも Linux にも拘りないですし。

2021 年の振り返りと 2022 年の抱負

明けましておめでとうございます!今年もいい年にしよう ZE!

ということで年末年始。大事なひと区切りなので、今年も忘れないように振り返りと抱負を書き残しておこうと思います。2021 年は一瞬で過ぎ去ったというか、働き過ぎのせいでそれ以外の記憶がかなり薄い。

お仕事

個人事業主として開業

2021/8 に個人事業主になりました。会社員であることをやめたわけではなく、副業としてです。もちろん事前の計画なんて全くなく、行き当たりばったりというか、出たとこ勝負的な勢いだけでの開業でした。なぜ開業することになったかというと、仲の良い友人の @_y_minami からお仕事を依頼されたからでした。当初はだいぶ渋っていたというか、夏は特に本業でデスマってる勢いだったので副業の時間だけで力添えできるとは全く思っていなかったからです。なのですが、当時の本業のタスクが精神的にあまりにも辛く、何か別のことに目を向けたい (= 本業から少しでも目を背ける時間が欲しい) と癒し (?) を求めて受けることにしました。

こうなると本業とは別の収入が発生してしまうということになり、これまで会社員としてしか生きてきたことがない僕にとっては未経験なことが多く発生しました。

  • 開業届の提出
  • 契約書
  • 経費の扱い
  • 確定申告 (← 近々体験することに

正直なところ、こう言った手続きや雑務は非常に苦手でなのでやりたくはなかったのですが、何事も経験だろうと思って踏み切った次第です。ただやってみると (当然) 新しい知識を得るもので、非常に良い経験になっています。ビジネスや税制の仕組み、あと簿記が以前よりも分かった気がします。とにかく freee さんの体験が凄まじくよく、心の底からビギナーの味方という感じで助かっています。

副業で何してるの?

fingger というコメント連動型ゲーム配信サービスの開発のお手伝いと技術顧問をしています。と言うとなんか凄そうな響きの肩書ですが、CTO である @_y_minami の社外相談役と言うと適当でしょうか。それと爆裂コード書きマンです。もう普通にコード書いてます。なんならまだ携わって 4 か月程度ですが、GitHub 上ではすでに No.1 Contributor になっていますw

f:id:xin9le:20220101022353p:plain

やること / やれることが多過ぎて、気が付いたら年が明けてました。仕事が納まらないまま仕事始めになりました...(

C# 10 / .NET 6 全開でコード書きまくっているので、もしご興味がある方は一緒にお仕事しましょう!ご連絡は @_y_minami まで!

本業はどうだった?

まず、2021 年の目標として昨年掲げていた「担当 EC プロジェクトの .NET 5 移植」ですが、これはやりきって 5 月に本番環境にデプロイすることができました。めでたしめでたし。驚くほど安定稼働するようになって、アラートに悩まされることは完全になくなりました。セール等のアクセスのバーストも平気で乗り越えます。今は .NET 6 への移行まで終わっていて大変平和です。

また、それとは別に 2021 年の第一四半期は新しい EC サービスのローンチに向けた開発をしていました。びっくりするほどスムーズなプロジェクトで、前半は本当に良いスタートを切れたなぁという感じでした。そのとき Paidy 決済の実装が必要になったので、.NET 向けの SDK を作ったりしました。もし C# / .NET で Paidy 決済の開発をされる方がいらっしゃったら、是非使ってみてくださいませ。

夏頃からは大規模リファクタリングにかかりっきりでした。新機能を実装するのにリファクタリングをしないとダメなのに「納期だけ決まっている」という散々な有様で、自分が作った部分じゃないだけにヘイトの溜まりっぷりとイライラがピークでした。ほんの数ページのリファクタリングだけで 3 か月近くかかっていて、その期間に副業に癒しを求めたといった感じでした。こんな精神的に擦り減る仕事の仕方は二度としたくないですね、さすがに。

あとは LINE 認証のライブラリを作ったりもしました。LINE Profile+ や BotPrompt に対応した .NET SDK がなかったといことで必要に迫られて作ったものではありますが、かなりシンプルで綺麗に仕上がっていると思います。LINE 認証が必要な方がいらっしゃったら是非。

コミュニティ活動

YouTube 配信

2021 年も COVID-19 の影響で人と直接会う機会がほぼなく、岩永さんとの C# YouTube 配信 が主な活動でした。最新 / 最先端の C# について隔週くらいで話ができる機会があることは、C# ヲタにとって最高の環境です。いつも一緒してくださっている岩永さんかずき先生に感謝!

C# Japan Discord

引き続きゆるーくサーバー管理者を続けています。現時点で 882 名の方にご参加いただいております。本当に感謝!参加してみたいという方は、下記リンクからどうぞ!

GitHub

のいえ先生が作った CloudStructures を数年 (勝手に) 保守をしていたのですが、なんとリポジトリが僕のところに移管されました。Cysharp 配下に入れないでこっちに来るとか、そういうこともあるんですねw

地味ぃに改修を続けていまして、Azure Redis Cache のメンテナンスイベントを受けられるようにしてみたり、いくつか新しくコマンドを追加したりしています。

プライベート

ゲーム

数年遅れと言われても仕方ないくらいの時代遅れ感がありますが、ようやく NINTENDO Switch を買いました。我が家で唯一の据え置き型ゲーム機です。NINTENDO 64 がやりたかったので...。64 版のスマブラが Switch Online で配信されないかなーと心待ちにしています。

YouTube

この 1 年も毎晩のように YouTube を観ていました。特に将棋解説と高校数学 / 高校物理あたりに興味が出て、以下のチャンネルには大変お世話になりました。

高校生の頃は (当然?) 勉強が好きではなかったわけですが、なぜ今になって知識欲が出るのか不思議。理由があるとすれば、娘に教えられるように先回り (?) して復習してるという感じでしょうか。

2022 年の目標

新卒からこれまでのプログラマのキャリアの間はずっと本業だけに明け暮れていたのですが、昨年から副業もはじめたということで上手に双方のバランスをとれるようにしていきたいと思っております。単純に分野が違うというだけでも面白いですし、片方の知識がもう片方に生きることも当然あるはずなので、そういう相乗効果が生まれることを楽しみにしています。

あとは健康には真剣に目を向けたい。最近腰痛がかなりひどく、湿布と飲み薬に頼り切りというアカンコレ状態...。健康が理由でプログラマ人生が短くならないようにしていかなければ。