django-rulesを使って、オブジェクトレベルの認可判定をViewとテンプレートでそれぞれ実装してみた

Djangoには認可機能が標準で用意されています。

ただ、標準の認可機能の場合、モデルごとの認可判定は可能な一方で、オブジェクトごとの認可判定ができないようです。

Djangoパーミッションフレームワークはオブジェクトパーミッション基盤を持っていますが、コアには実装されていません。これにより、オブジェクトパーミッションのチェックは常に False または空のリスト(実行されたチェックに応じていずれか)が返されます。

https://docs.djangoproject.com/ja/3.0/topics/auth/customizing/#handling-object-permissions

 
自作する方法はstackoverflowにありましたが、正直なところ自作したくはありません。
Steps to add model level permission in Django - Stack Overflow

そんな中、jbkingさんの「どじゃんご本#1」を読んだところ、オブジェクトごとに認可判定ができるライブラリとして

  • django-guardian
  • django-rules

の紹介がありました。
どじゃんご本 #1 - どじゃんご@jbking - BOOTH

違いとして、 django-guardian はDBで制御、 django-rules はルールベースでの制御とのことでした。

 
そこで、ルールベースでの制御とはどのようなものか気になったため、 django-rules を試してみることにしました。

 
目次

 

環境

 

認可処理を試すシナリオ

今回は以下のシナリオに沿って試してみます。

ある会社にてお知らせを閲覧するWebアプリが必要になりました。

仕様は

  • ある部門に向けたお知らせを作成する
    • お知らせは部門間でまたがることはない
  • 職務ごとにお知らせの閲覧範囲が異なる
  • ログインしないとお知らせは閲覧できない

です。

職務は

  • 一般
  • 管理職
  • システム管理者

の3種類です。

 
閲覧範囲は

  • ログインしていない場合は、閲覧不可
  • 一般は、自分の所属する部門のお知らせのみ閲覧可能
    • 他部門は、タイトルのみ把握可能
  • 役職者とシステム管理者は、すべてのお知らせを閲覧可能

です。

 
このシナリオを django-rules を使ってDjangoで実装してみます。

 

実装方針

今回は

  1. 職務により、全てのお知らせを閲覧可能 or 一部のお知らせのみ閲覧可能が切り替わること
  2. 一部の職務では、所属部門のお知らせのみ閲覧できること(ただし、タイトルは閲覧可能)

という2つの観点で認可処理を考えることにします。

 
そのため、「それぞれの観点ごとに、ユーザークラスに対して制御用項目を用意する」方針にします。

  • 1.については、 権限タイプ を用意
    • 目的:職務ごとの権限範囲を指定するため
      • 所属部門だけなのか、全体なのか
  • 2.については 所属部門 を用意
    • 目的:どの部門のお知らせを見れるのかを判断するため

 
この方針に従い、 django-rules を使って実装していきます。

なお、スペースの都合上、django-rulesまわりや必要だと思ったところのみ、Blogに記載します。

もし、ソースコード全体を眺めたい場合は、以下となります。
https://github.com/thinkAmi-sandbox/django-rules-sample

 

実装

Djangoプロジェクトとアプリの用意
$ pip install django rules
$ django-admin startproject config .

# ユーザーを管理するアプリ
$ python manage.py startapp accounts

# お知らせを管理するアプリ
$ python manage.py startapp myapp

 

権限タイプを models.IntegerChoices として用意

今回は

  • システム管理者
  • 管理職
  • 一般

の3種類の権限を用意します。

Userモデルに権限タイプを紐付けるため、Django3から使えるようになった models.IntegerChoices を使って定義します。

# accounts/models.py

class PermissionType(models.IntegerChoices):
    ADMIN = (1, 'システム管理者')
    MANAGER = (2, '管理職')
    EMPLOYEE = (3, '一般')

 

部門モデルの作成

フィールドとして name だけを用意しました。

# accounts/models.py

class Section(models.Model):
    name = models.CharField(max_length=100)

 

標準Userモデルを差し替え

属性タイプ・部門をUserモデルに追加するため、標準のUserモデルから AbstractBaseUser を拡張したモデルへと差し替えます。

なお、差し替えるモデルの内容は、 AbstractUser から不要なものをコメントアウトした後、必要なものを追加する形にします。

必要なものは

  • 権限タイプ(permission_type)
  • 部門(section)
  • 表示名 (display_name)

の3項目とします。

# accounts/models.py

class User(AbstractBaseUser, PermissionsMixin):
    username = models.CharField(...)
    display_name = models.CharField(max_length=100)

    # Django3.0からはIntegerChoicesを使える
    permission_type = models.IntegerField(choices=PermissionType.choices,
                                          default=PermissionType.EMPLOYEE)

    section = models.ForeignKey('accounts.Section', on_delete=models.PROTECT)
    # ...

 

お知らせモデルの作成

部門に紐付けるため、 Section モデルを外部キーとして用意します。

# myapp/models.py

class News(models.Model):
    title = models.CharField(max_length=50, verbose_name='タイトル')
    content = models.CharField(max_length=100, verbose_name='内容')
    section = models.ForeignKey('accounts.Section', on_delete=models.PROTECT)

 

ログイン・ログアウト機能の作成

未ログインは閲覧不可、ログイン済は職務に応じて閲覧可能とするため、ログイン機能を作成します。

ログイン・ログアウトのViewは、Django標準のViewを使います。

また、ログイン画面・ログアウト画面のテンプレートは、今回手抜きをしてDjango adminのものを流用します。
Is there a built-in login template in Django? - Stack Overflow

そのため、ログイン・ログアウトまわりのViewはurls.pyだけで完結できます。

# accounts/urls.py

from django.contrib.auth.views import LoginView, LogoutView
from django.urls import path

app_name = 'accounts'

urlpatterns = [
    path('login/',
         LoginView.as_view(template_name='admin/login.html'),
         name='login'),
    path('logout/', LogoutView.as_view(), name='logout'),
]

 

ログイン判定を行うミドルウェアを作成

今回、未ログイン時はどのお知らせも閲覧不可とします。

そのため、ログイン判定処理はミドルウェアにて一括で実行します。

もしログインしていない場合は、ログイン画面へと遷移します。

from django.shortcuts import redirect
from django.urls import reverse


class LoginRequiredMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        return self.get_response(request)

    def process_view(self, request, view_func, view_args, view_kwargs):
        # 全画面ログイン必須なので、ログインしてなかったらログイン画面へ遷移
        if not request.user.is_authenticated and request.path != reverse('accounts:login'):
            return redirect('accounts:login')

 

権限の作成
権限タイプ用

指定したユーザーがどの権限タイプであるかを判定するルールを作成します。
https://github.com/dfunckt/django-rules#creating-predicates

ルールの判定処理は、デコレータ @rules.predicate を付けた関数で行います。

関数の戻り値は

  • True
    • ルールを満たす(=権限あり)
  • False
    • ルールを満たさない(=権限なし)
  • None
    • ルールの判定をパスする(=決定に影響を及ぼさない)

のいずれかです。

例えば、与えられたユーザーが管理者権限を持つかどうかは以下のとおりです。

import rules
from .models import PermissionType

@rules.predicate
def is_admin(user):
    return user.permission_type == PermissionType.ADMIN.value

なお、PermissionTypeクラスは models.IntegerChoices のサブクラスなので、 PermissionType.ADMIN.value とすることで ADMIN = (1, 'システム管理者')1 が取得できます。
https://docs.djangoproject.com/ja/3.0/ref/models/fields/#enumeration-types

 
次に、 rules.add_perm で、指定した権限にルールを割り当てます。
https://github.com/dfunckt/django-rules#setting-up-rules

rules.add_perm('accounts.admin', is_admin)

 
ちなみに、一つの権限に対して、複数のルールをすべて満たす(=AND条件)やいずれかを満たす(=OR条件)を割り当てることもできます。
https://github.com/dfunckt/django-rules#combining-predicates

例えば以下の場合は、is_admin is_manager is_employee のいずれかがTrueであれば accounts.employee の権限があるとみなされます。

rules.add_perm('accounts.employee', is_admin | is_manager | is_employee)

 
権限タイプ用はこんな感じになります。

# accounts/rules.py

import rules
from .models import PermissionType


@rules.predicate
def is_admin(user):
    return user.permission_type == PermissionType.ADMIN.value


@rules.predicate
def is_manager(user):
    return user.permission_type == PermissionType.MANAGER.value


@rules.predicate
def is_employee(user):
    return user.permission_type == PermissionType.EMPLOYEE.value


rules.add_perm('accounts.admin', is_admin)
rules.add_perm('accounts.manager', is_admin | is_manager)
rules.add_perm('accounts.employee', is_admin | is_manager | is_employee)

 

所属部門用

同様にして作成します。

is_same_section() の第二引数 obj は、お知らせクラス(News)のオブジェクトが入ってくる想定です。

# myapp/rules.py

import rules
from accounts.rules import is_admin, is_manager


@rules.predicate
def is_same_section(user, obj):
    return user.section == obj.section


rules.add_perm('myapp.same_section', is_same_section | is_admin | is_manager)

 

テンプレートの作成
ベーステンプレート

ログインしているユーザーが見えるよう、以下のようなベーステンプレートを用意します。

<!-- templates/base.html -->

{% block load %}{% endblock %}

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}{% endblock %}</title>
</head>
<body>
<p>ログインユーザ:{{ request.user.display_name }}</p>
<p>所属部署:{{ request.user.section.name }}</p>
<p>権限種類:{{ request.user.permission_type }}</p>

<a href="{% url 'accounts:logout' %}">ログアウト</a>

{% block content %}{% endblock %}
</body>
</html>

 

一覧画面用(django-rulesのテンプレートタグを試す)

一覧画面ではNewsの idtitle だけ表示します。

この画面は一般権限でも閲覧可能としています*1

django-rules のテンプレートタグ has_perm の動作確認をするため、権限ごとに li タグの表示/非表示を切り替えます。https://github.com/dfunckt/django-rules#permissions-and-rules-in-templates

例えば、システム管理者だけが閲覧可能にする方法です。

{% load rules %}
...
{% has_perm 'accounts.admin' request.user as is_admin %}
{% if is_admin %}
    <li>システム管理者だけが見える</li>
{% endif %}

 
{% load rules %}django-rulesを読み込み、{% has_perm 'accounts.admin' request.user as is_admin %} でテンプレートタグ has_perm を使っています。

has_permの引数はそれぞれ

  • 第一引数 'accounts.admin'
    • 権限を指定する
    • ここでは管理者権限があるかを確認
  • 第二引数 request.user
    • ルール用の関数 is_admin(user) に渡すUserオブジェクト

です。

 
あとは、ルール用関数からTrue/Falseのいずれかが返ってくるので、 as で変数に入れて判定を行います。
https://docs.djangoproject.com/en/3.0/howto/custom-template-tags/#django.template.Library.simple_tag

 

全体像はこちら

<!-- templates/myapp/news_list.html -->

{% extends 'base.html' %}

{% block load %}
{% load rules %}
{% endblock %}

{% block title %}お知らせ一覧{% endblock %}

{% block content %}

<h3>お知らせ一覧</h3>

<p>権限によって、表示/非表示を切り替える</p>
<ul>
    {% has_perm 'accounts.admin' request.user as is_admin %}
    {% if is_admin %}
        <li>システム管理者だけが見える</li>
    {% endif %}

    {% has_perm 'accounts.manager' request.user as is_manager %}
    {% if is_manager %}
        <li>管理職以上が見える</li>
    {% endif %}

    {% has_perm 'accounts.employee' request.user as is_employee %}
    {% if is_employee %}
        <li>一般が見える</li>
    {% endif %}
</ul>

<table border="1" rules="all">
    <thead>
    <tr>
        <th>News ID</th>
        <th>タイトル</th>
    </tr>
    </thead>
    <tbody>
    {% for news in object_list %}
        <tr>
            <td>{{ news.id }}</td>
            <td><a href="{% url 'myapp:detail' news.id %}">{{ news.title }}</a></td>
        </tr>
    {% endfor %}
    </tbody>
</table>

{% endblock %}

 

詳細画面

一覧画面と異なり、詳細画面の表示可否はViewで行います。

そのため、テンプレートでは権限による表示制御ロジックは入れていません。

<!-- templates/myapp/news_detail.html -->

{% extends 'base.html' %}

{% block title %}お知らせ詳細{% endblock %}

{% block content %}
<h3>お知らせ詳細</h3>
<p>タイトル: {{ object.title }}</p>
<p>内容: {{ object.content }}</p>

<td><a href="{% url 'myapp:index' %}">一覧へ</a></td>
{% endblock %}

 

403ページ

デフォルトの403ページだと、どのユーザーで403が表示されたのかがわからないため、今回はあえて403ページも自作します。

<!-- templates/403.html -->

{% extends 'base.html' %}

{% block title %}403エラー{% endblock %}

{% block content %}
<p>権限がありません</p>
<td><a href="{% url 'myapp:index' %}">一覧へ</a></td>
{% endblock %}

 

Viewの作成
一覧画面View

権限制御は行わないため、ふつうのListViewです。

class NewsListView(ListView):
    model = News

 

詳細画面View

詳細画面は権限の有無により、画面を表示/非表示を制御します。

そのため、

  • DetailViewに rules.contrib.views.PermissionRequiredMixin を追加
  • 属性として permission_required を追加し、権限を割り当てる

を行います*2
https://github.com/dfunckt/django-rules#using-the-class-based-view-mixin

class NewsDetailView(PermissionRequiredMixin, DetailView):
    model = News
    permission_required = 'myapp.same_section'

今回は、一般職の場合だけ、ユーザーの所属部門がお知らせの部門と一致する場合のみお知らせ詳細を表示するため、 permission_requiredmyapp.same_section を設定しています。

 

settings.pyの修正

ここまでの動作をさせるために、settings.pyを修正します。

django-rules用としては、

です。

主な変更箇所はこちら。

INSTALLED_APPS = [
    # ...
    # rules.pyを自動的に探してくれる書き方
    'rules.apps.AutodiscoverRulesConfig',

    # 自作アプリ
    'accounts.apps.AccountsConfig',
    'myapp.apps.MyappConfig',
]

MIDDLEWARE = [
    ...
    # 末尾にログイン必須ミドルウェアを追加
    'accounts.middlewares.LoginRequiredMiddleware',
]
TEMPLATES = [
    {
        # ...
        # テンプレートディレクトリを修正
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        # ...
    },
]


# 差し替えたユーザーモデル
AUTH_USER_MODEL = 'accounts.User'

# ログインURL
LOGIN_URL = 'accounts:login'
# ログイン後のリダイレクトURL
LOGIN_REDIRECT_URL = 'myapp:index'

# django-rulesの設定
AUTHENTICATION_BACKENDS = (
    'rules.permissions.ObjectPermissionBackend',
    'django.contrib.auth.backends.ModelBackend',
)

 
主な実装箇所は以上です。

細かいところはソースコードを見てください。

 

動作確認

用意したデータ

fixtureとして用意しました。

 
部門は3種類です。この部門ごとに1つのお知らせも用意しました。

  • システム部
  • 製造部
  • 営業部

 
ユーザーは以下の4種類を用意しました。

  • システム管理者
  • 管理職
  • 製造部一般
  • 営業部一般

なお、ユーザーもfixtureで作成しています。

ただ、fixtureだけではユーザーのパスワードを生成できません。

そのため、以下のsolution2を参考に、django shellで生成したパスワードをfixtureに入れてました。
How to add superuser in Django from fixture - Stack Overflow

./manage.py shell

>>> from django.contrib.auth.hashers import make_password
>>> make_password('test')
'pbkdf2_sha256$180000$7bi7okafLu5T$aDHSnnujzykcAbxzibg7MoynKozIF+D1VwS1Kx2mQcg='

 

一覧画面
未ログイン

未ログインでは一覧画面を見れないため、ログイン画面へ遷移します。

f:id:thinkAmi:20200425084844p:plain

 

システム管理者

すべての項目が見えています。

f:id:thinkAmi:20200425084919p:plain

 

役職者

システム管理者の項目が見えなくなりました。

f:id:thinkAmi:20200425084937p:plain

 

製造部一般

一般だけの項目になりました。

f:id:thinkAmi:20200425084953p:plain

 

詳細画面

製造部一覧向けのお知らせを、それぞれのユーザーで閲覧してみます。

未ログイン

一覧同様、ログイン画面へ遷移します。

f:id:thinkAmi:20200425085006p:plain

 

システム管理者

閲覧できます。

f:id:thinkAmi:20200425085019p:plain

 

役職者

閲覧できます。

f:id:thinkAmi:20200425085046p:plain

 

製造部一般

閲覧できます。

f:id:thinkAmi:20200425085103p:plain

 

営業部一般

権限がないため、403ページが表示されています。

f:id:thinkAmi:20200425085119p:plain

 

ソースコード

GitHubに上げました。
https://github.com/thinkAmi-sandbox/django-rules-sample

*1:本来の権限的には一般権限ではタイトルも見えないようにすべきなんですが、今回はサンプルなのでそこまで細かく制御しません

*2:関数ベースビューの場合はデコレータを使います:https://github.com/dfunckt/django-rules#using-the-function-based-view-decorator

Django + django-rules + 独自Userモデルで、has_permテンプレートタグを使うときの注意点

Djangoには標準で認可(Permission)の仕組みがあります。

ただ、Django標準の認可はモデルレベルです。オブジェクトレベルは

Djangoパーミッションフレームワークはオブジェクトパーミッション基盤を持っていますが、コアには実装されていません。

https://docs.djangoproject.com/ja/3.0/topics/auth/customizing/#handling-object-permissions

と公式ドキュメントにあります。

 
そのため、オブジェクトレベルで認可を扱いたい時はライブラリを使います。その選択肢の一つとして django-rules があります。
dfunckt/django-rules: Awesome Django authorization, without the database

 
そんな中、Django + Django-rules + 自作UserモデルのDjangoアプリにて、django-rulesの has_perm テンプレートタグを使おうとした時にハマったため、メモを残します。

 
目次

   

環境

 

問題

PermissionsMixinを継承せずに独自Userモデルを作成したところ、django-rulesによる認可が動作しませんでした。

 

再現方法
独自Userモデルの作成

Django標準のUserモデルではなく、 AbstractBaseUser を継承して独自Userモデルを作成しました。

独自Userモデルを作成する場合、 PermissionsMixin も継承しておくことで、パーミッションまわりの機能を独自ユーザーへ簡単に組み込めます。
https://docs.djangoproject.com/ja/3.0/topics/auth/customizing/#custom-users-and-permissions

ただ、この時は

  • このDjangoアプリが検証目的だったこと
  • django-rulesのREADMEには独自Userモデルのことが書かれていなかったこと

より、 PermissionsMixin を継承しない独自Userモデルを作成しました。

class User(AbstractBaseUser):
    pass

 

INSTALLED_APPS

ドキュメントには

  • rules
  • rules.apps.AutodiscoverRulesConfig

のどちらかを指定するよう記載されています。

ただ、 rules.apps.AutodiscoverRulesConfig を指定しておくと、django-rulesが自動的に rules.py という名前のファイルを探してくれるため、 rules.apps.AutodiscoverRulesConfig を指定します。

rules may optionally be configured to autodiscover rules.py modules in your apps and import them at startup. To have rules do so, just edit your INSTALLED_APPS setting:

https://github.com/dfunckt/django-rules#best-practices

 

AUTHENTICATION_BACKENDS

認証バックエンドとしてdjango-rulesのものが必要なため、settings.pyに追加しておきます。

AUTHENTICATION_BACKENDS = (
    'rules.permissions.ObjectPermissionBackend',
    'django.contrib.auth.backends.ModelBackend',
)

 

rules.py

django-rules用に、 rules.py にルールを実装しました。

import rules

@rules.predicate
def is_admin(user):
    return user.is_admin

rules.add_perm('accounts.admin', is_admin)

 

テンプレートでdjango-rulesを利用

rules をloadした後、 has_perm で権限により表示/非表示を切り替えます。

{% load rules %}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>お知らせ</title>
</head>
<body>
<h3>お知らせ一覧</h3>
<ul>
    {% has_perm 'accounts.admin' request.user as is_admin %}
    {% if is_admin %}
        <li>システム管理者だけが見える</li>
    {% endif %}
<ul>

 
ここまででdjango-rulesの設定が終わっているものの、runserverしても has_perm テンプレートタグが動作しませんでした。

 

原因

django-rulesのテンプレートタグ has_permソースコードを見たところ、

@register.simple_tag
def has_perm(perm, user, obj=None):
    if not hasattr(user, 'has_perm'):  # pragma: no cover
        return False  # swapped user model that doesn't support permissions
    else:
        return user.has_perm(perm, obj)

# https://github.com/dfunckt/django-rules/blob/v2.2.0/rules/templatetags/rules.py#L15

と書かれていました。

テンプレートタグ has_perm は、Userモデルに has_perm() メソッドがある前提で動作するようです。

そのため、 PermissionsMixin を継承しないなどでUserモデルに has_perm() メソッドが無い場合は、常に False を返します。

その結果、rules.pyに設定したコードは動作しなかったようです。

 

対応

簡単な対応としては、を継承した独自Userモデルでは PermissionsMixin を継承するようにします。

class User(AbstractBaseUser, PermissionsMixin):
    pass

 
もし、 PermissionsMixin を継承できない場合にテンプレートタグ has_perm を使いたい場合は、何らかの形で独自ユーザークラスに has_perm メソッドを実装しておきます。

 
なお、AbstractBaseUser ではなく AbstractUser を継承した独自ユーザーの場合は PermissionsMixin を継承済のため、このようなことは起きません。
https://github.com/django/django/blob/3.0.5/django/contrib/auth/models.py#L316

Vue.js + vue-svg-loaderでSVGファイルを表示したところ、SVGファイルのidが消えたので対応した

Vue.jsでSVGファイルをVueコンポーネントとして扱う方法がないかを探したところ、 vue-svg-loader がありました。
visualfanatic/vue-svg-loader: 🔨 webpack loader that lets you use SVG files as Vue components

 
試しに

<svg width="144" height="72" viewBox="0 0 144 72" xmlns="http://www.w3.org/2000/svg">
 <g id="layer">
  <title>rectangle</title>
  <path d="M0,0 L144,0 144,72 0,72 0,0" stroke-width="1.5" stroke="#000" fill="#fff" id="myRect" class="foo" />
 </g>
</svg>

という四角のSVGファイルを用意してみたところ、Vueコンポーネントとして利用できました。

ただ、ブラウザの表示をよく見てみたところ、SVGファイルのid myRect が消えているようでした。

HTML

f:id:thinkAmi:20200411222052p:plain:w450

 
そこで、SVGファイルのidを残したままブラウザで表示する方法を調べたときのメモを残します。

 
目次

 

環境

  • @vue/cli 4.3.1
  • vue 2.6.11
  • vue-svg-loader 0.16.0
  • SVGO 1.3.2

 

vue-svg-loaderでSVGファイルを読み込むまでの環境構築方法

vue-cliでBabelを含めてプロジェクト作成

vue-svg-loader はBabelが必要なため、最初に追加しておきます。

RouterやVuexも含めてありますが、こちらは今回のものには関係ありません。

$ vue create svgvue

Vue CLI v4.3.1
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Vuex
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No

 

vue-svg-loaderのインストールと設定ファイル作成
$ cd svgvue

$ npm i -D vue-svg-loader

 
プロジェクトのルートに vue-svg-loader の設定を行う vue.config.js を作成します。

内容はGitHubのREADMEに書かれている Vue CLI の内容そのままです。

module.exports = {
  chainWebpack: (config) => {
    const svgRule = config.module.rule('svg');

    svgRule.uses.clear();

    svgRule
      .use('babel-loader')
      .loader('babel-loader')
      .end()
      .use('vue-svg-loader')
      .loader('vue-svg-loader');
  },
};

 

アプリの作成
SVGファイルの保管先

src/assets/rectangle.svg として保管します。

 

SVGファイルを読み込むコンポーネントの作成

src/components/SvgFileComponent.vue として作成します。

SVGファイルを読み込み、 rectangle タグとして使います。

また、確認用として、SVG画像をクリックした時にSVGファイルの属性を出力するようメソッドを作成しています。

<template>
  <rectangle id="rectangle" @click="handleClick" />
</template>

<script>
  import Rectangle from '../assets/rectangle.svg';

  export default {
    name: "SvgFileComponent",

    components: {
      Rectangle,
    },

    methods: {
      handleClick: function() {
        const svgDoc = document.getElementById('rectangle');
        const paths = svgDoc.getElementsByTagName('path');
        console.log(paths);
        console.log(paths[0]);
        console.log(paths[0].id);
        console.log(paths[0].className);
        console.log(paths[0].classList);
        console.log(paths[0]._prevClass);
      }
    }
  }
</script>

 

viewの作成

上記で作成したコンポーネントを読み込むViewを作成します。

<template>
  <div>
    <svg-file-component />
  </div>
</template>

<script>
  import SvgFileComponent from "../components/SvgFileComponent";
  export default {
    name: "SvgFileView",

    components: {
      SvgFileComponent,
    }
  }
</script>

 

routerやApp.vueの修正

上記のViewを使うよう、routerやApp.vueを修正します。

 

動作確認

npm run serve で起動して動作を確認すると、冒頭のような結果になります。

 

調査・対応

stackoverflowに似た事例がありました。
javascript - vue-svg-loader removes some tags while loading SVGs - Stack Overflow

このことから、SVGO を使った設定へと変えれば良さそうでした。ただ、例として示されていたものが vue.config.js 向けではありませんでした。

 
vue-svg-loaderの公式ドキュメントを見たところ、SVGOを使った書き方がありました。
How to prefix id attributes? - Frequently Asked Questions | Documentation

公式ドキュメントでは prefixIds の設定方法のみ記載されていました。

そこで、 cleanupIDs pluginを無効化するよう設定しました。

const { basename } = require('path');

module.exports = {
  chainWebpack: (config) => {
    const svgRule = config.module.rule('svg');

    svgRule.uses.clear();

    svgRule
      .use('babel-loader')
      .loader('babel-loader')
      .end()
      .use('vue-svg-loader')
      .loader('vue-svg-loader')
      
      // プラグインを追加
      .options({
        svgo: {
          plugins: [
            {
              // デフォルトではSVGファイル中のidを消してしまうため、idをそのままにする
              cleanupIDs: false,
            },
          ],
        },
      });
  },
};

 

動作確認

グローバルにSVGOをインストール
$ npm install -g svgo

 

起動

再度 npm run serve して確認したところ、SVGファイルのidがHTMLに含まれていました。

HTML

f:id:thinkAmi:20200411222123p:plain:w450

コンソール

f:id:thinkAmi:20200411222205p:plain:w450

 

参考:prefixIds pluginについて

READMEには

prefix IDs and classes with the SVG filename or an arbitrary string

と書かれていたため、prefixIds pluginでも良さそうでしたが、

.options({
  svgo: {
    plugins: [
      {
        // 下のは、classNameにprefixが付与される
        prefixIds: true,
      },
    ],
  },
});

と設定したものの、クラスのみprefixが付与されただけでした*1

f:id:thinkAmi:20200411222224p:plain:w450

 

ソースコード

GitHubに上げました。
https://github.com/thinkAmi-sandbox/SVG_Vuejs-sample

*1:使い方が違うだけかもしれませんが...

Windows for Docker & docker composeにて、top-level volumes option で named volume を定義してPostgreSQLのデータを永続化する

WindowsPostgreSQLを使って開発する際、コンテナを破棄してもデータが残るデータの永続化を考えました。

ただ、macと同じように

version: '3'
services:
  postgres:
    image: postgres:12.2-alpine
    tty: true
    restart: always

    volumes:
      # pgdataをホストに置く
      - ./pgdata:/var/lib/postgresql/data

    stdin_open: true
    ports:
      - "44321:5432"

と、pgdata というローカルのファイルシステム上のフォルダを指定したところ

postgres_1  | running bootstrap script ... 2020-03-27 15:56:59.023 UTC [51] FATAL:  data directory "/var/lib/postgresql/data" has wrong ownership
postgres_1  | 2020-03-27 15:56:59.023 UTC [51] HINT:  The server must be started by the user that owns the data directory.
postgres_1  | child process exited with exit code 1
postgres_1  | initdb: removing contents of data directory "/var/lib/postgresql/data"

というエラーが発生してPostgreSQLが起動しませんでした。

 
そんな中、同僚の @moon_in_nagano よりデータボリュームについて教わったため、メモを残します。

 

目次

 

環境

 
また、PostgreSQLでデータの永続化ができているかを確認するのに、Pythonを使いました。

  • Python 3.8.2
  • SQLAlchemy 1.3.15
  • psycopg2 2.8.4

 
テーブル作成 & データ投入のスクリプトは以下です。

# insert.py

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import sessionmaker

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(String)


if __name__ == "__main__":

    engine = create_engine('postgresql://{user}:{password}@localhost:{port}/{database_name}'.format(
        user='postgres',
        password='postgres',
        port=44321,
        database_name='postgres',
    ))
    Base.metadata.create_all(engine)

    Session = sessionmaker(bind=engine)
    session = Session()
    session.add(User(name='foo'))
    session.commit()

 

対応

データボリュームを使うにはコマンドラインのオプションを指定すれば良さそうでした。
Docker の Data Volume まわりを整理する - Qiita

docker-composeでの方法を調べたところ、 top-level volumes option を使えば良さそうでした。
Compose file version 3 reference | Docker Documentation

 

top-level volumes optionの設定

docker-compose.ymlでnamed volumeを設定します。

まずは、トップレベルにvolumeの記載を追加します。今回は pgdata というvolume名とします。

volumes:
  pgdata:

 
次に、serviceのvolumeの中で pgdata を使うように修正します。

services:
  postgres:
    image: postgres:12.2-alpine
    ...
    volumes:
      # ファイルシステムの指定をやめて、PostgreSQLのデータを名前付きボリュームを使って永続化
      # - ./pgdata:/var/lib/postgresql/data
      # top-level volumes optionで指定した "pgdata" を ":" の左側へ設定
      - pgdata:/var/lib/postgresql/data

 
全体はこちら。

# マイナーバージョンを書かないと自動的にマイナーバージョンが"0"になるので、"3.7"と明示的に設定
# https://docs.docker.com/compose/compose-file/compose-versioning/#version-3
version: '3.7'
services:
  postgres:
    image: postgres:12.2-alpine

    environment:
        # エンコーディングを指定しておく
        # initdbで指定できる内容はここで指定可能
        # https://www.postgresql.org/docs/10/static/app-initdb.html を参照。
        POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=ja_JP.UTF-8"
        
        # パスワード
        POSTGRES_PASSWORD: postgres

    # ttyをtrueに設定しておくと、コンテナが起動し続ける
    tty: true
    restart: always

    volumes:
      # ファイルシステムの指定をやめて、PostgreSQLのデータを名前付きボリュームを使って永続化
      # - ./pgdata:/var/lib/postgresql/data
      - pgdata:/var/lib/postgresql/data

    stdin_open: true
    ports:
      - "44321:5432"
        
volumes:
  pgdata:

 

動作確認

あとはいつもどおり起動します。

docker-compose up

 
Pythonスクリプトを流した後、PostgreSQL上にデータが存在することを確認します。

# スクリプトの実行
python insert.py

# コンテナのPostgreSQLを確認
>docker container exec -it temp_postgres_1 sh
/ # psql -U postgres -h 127.0.0.1 -p 5432 postgres
psql (12.1)
Type "help" for help.

postgres=# SELECT * FROM users;
 id | name
----+------
  1 | foo
(1 row)

 
down後に再度upしても、データが残っていることを確認します。

# 停止
docker-compose down

# 起動
docker-compose up

# 確認
## Dockerの中に入ってshを実行
>docker container exec -it temp_postgres_1 sh

## PostgreSQLの中に入る
/ # psql -U postgres -h 127.0.0.1 -p 5432 postgres
psql (12.1)
Type "help" for help.

## SELECT実行
postgres=# SELECT * FROM users;
 id | name
----+------
  1 | foo
(1 row)

 

named volumeの削除

もし不要になった場合は、down時にオプション -v を追加することで、コンテナと同時にさくじょできます。 ( docker volume rm で別々に削除することも可能)

>docker-compose down -v
Stopping temp_postgres_1 ... done                                                                                       Removing temp_postgres_1 ... done                                                                                       Removing network temp_default
Removing volume temp_pgdata

 
再度起動しても、PostgreSQLのデータはありません。

>docker container exec -it temp_postgres_1 sh
/ # psql -U postgres -h 127.0.0.1 -p 5432 postgres
psql (12.1)
Type "help" for help.

postgres=# SELECT * FROM users;
ERROR:  relation "users" does not exist
LINE 1: SELECT * FROM users;

 

その他

手元ではダメだったこと
PostgreSQLのパスからdataを削除する

docker-compose.ymlで、

services:
  postgres:
    image: postgres:12.2-alpine
    volumes:
      # - ./pgdata:/var/lib/postgresql/data
      - ./pgdata:/var/lib/postgresql

と、 ./pgdata:/var/lib/postgresql とやれば動作するというのも見かけました。

ただ、手元では動作はしたものの、データの永続化がなされませんでした。

 

top-level volumes optionで保存先をローカルのファイルシステムにする

このstackoverflowを見ると、top-level volumes optionでもローカルのファイルシステムを指定できそうでした。

そのため、ローカルの D:\temp\pgdata2 に保存するよう

volumes:
  pgdata:
    driver: local
    driver_opts:
        type: none
        device: "/host_mnt/d/temp/pgdata2"
        o: bind

と設定して docker-compose up したところ

postgres_1  | running bootstrap script ... 2020-03-27 22:48:46.370 UTC [50] FATAL:  data directory "/var/lib/postgresql/data" has wrong ownership
postgres_1  | 2020-03-27 22:48:46.370 UTC [50] HINT:  The server must be started by the user that owns the data directory.
postgres_1  | child process exited with exit code 1
postgres_1  | initdb: removing contents of data directory "/var/lib/postgresql/data"

と冒頭と同じようなエラーになりました。ローカルのファイルシステム自体に割り当てるのがダメですね。

 

Windowsにおけるnamed volumeのありか

上記では特にパスを指定していないので、named volumeがどこに置かれるのか分かりませんでした。

調べてみたところ

When running linux based containers on a windows host, the actual volumes will be stored within the linux VM and will not be available on the host's fs, otherwise windows running on windows => C:\ProgramData\Docker\volumes\

Locating data volumes in Docker Desktop (Windows) - Stack Overflow

With Docker for Windows, the volume is saved inside of the VM running Linux which is managed by Docker. It wouldn't be inside of a container, or your Windows box directly.

volume mount point - does not exist (Docker for Windows / WSL) - Course: Docker Fundamentals

とありました。

 
そこで、Docker for WindowsにおけるLinuxコンテナについて調べてみたところ、Microsoftの公式ドキュメントに

- Linux コンテナーを完全な Linux VM で実行する-これは、現在、Docker が行うものです。
- Hyper-v 分離(lcow) を使用して Linux コンテナーを実行する-これは Docker for Windows の新しいオプションです。

https://docs.microsoft.com/ja-jp/virtualization/windowscontainers/deploy-containers/linux-containers

とありました。

手元ではどちらが使われているのかを調べたところ、Hyper-VマネージャーにDockerDesktopVMがいました。

f:id:thinkAmi:20200329090905j:plain:w450

 
また、 C:\Program Files\Linux Containers フォルダ自体がありませんでした。

C:\Program Files>dir Linux*

 C:\Program Files のディレクトリ

ファイルが見つかりません

これらより、手元では Linux コンテナーを完全な Linux VM で実行する の設定でDocker for Windowsが動作しているようです。

 
そのため、Hyper-V上のLinuxコンテナにデータボリュームが保存されており、Windowsファイルシステム上では見れないのだと考えました。

Mac + C# + .NET Core3.1で、ActiveDirectoryのLDAP認証をしてみた

Mac .NET Core 3.1のアプリを開発する中、ユーザーがActiveDirectory (以降、AD)に参加していない環境で、ADサーバーを使ったLDAP認証をする機会があったため、メモを残します。

 
目次

 

環境

  • Mac OSX 10.14.6
  • .NET Core 3.1
  • Novell.Directory.Ldap.NETStandard 3.0.1

 
また、ADまわりは以下の環境です。

項目
ADサーバ Windows Server 2016
ドメインの機能 2016
Macユーザ ドメインに不参加、ただしADユーザのIDとパスワードは分かる
Mac ドメインのコンピュータとして参加済、ただしドメインのComputersへ登録のみ
AD - Mac間接続 直接 (Proxyなし)
ADドメイン sub.example.co.jp

 
ADユーザ情報はこんな感じ

項目
ユーザーログオン名 foo_user@sub.example.co.jp
ユーザーログオン名(Windows2000以前) foo_user
foo
bar
表示名 foo bar

 

そもそも、なぜWindows認証ではないのか

ADを使った認証というと、まずはWindows認証が思い浮かびます。

ASP.NET Core 3.1でWindows認証が可能な方法を調べたところ、以下の3つでした。
ASP.NET Core での Windows 認証を構成します。 | Microsoft Docs

  • IIS (IIS Express)
  • HTTP.sys
  • Kestrel

 
このうち、Mac上で ASP.NET Coreをホストして使えるのは、 Kestrel だけでした。

とはいえ、Kestrelは機能不足なところがありそうなので、できれば避けたいと考えました。
ASP.NET Coreを動かすためのIISの構築方法 - Qiita

 
他に方法がないか考えたところ、以前PythonRubyLDAP認証を使っていたことを思い出しました。

 
そこで、 .NET Coreで使えるLDAP認証ライブラリを探したところ、 dsbenghe/Novell.Directory.Ldap.NETStandard がありました。
dsbenghe/Novell.Directory.Ldap.NETStandard: LDAP client library for .NET Standard 1.3 and up - works with any LDAP protocol compatible directory server (including Microsoft Active Directory).

最近リリースされたばかり(2020年1月下旬)だったので、以下を参考に試してみました。
.NET Core LDAP authentication

 

準備

ソリューションとプロジェクトの作成

今回は、コンソールアプリケーションで作ります。

$ dotnet new sln -n LdapAuth

$ dotnet new console -o LdapAuthConsole

$ dotnet sln add ./LdapAuthConsole

 

NuGetでインストール

Novell.Directory.Ldap.NETStandard をインストールします。

$ cd LdapAuthConsole/
$ dotnet add package Novell.Directory.Ldap.NETStandard

 

使い方の流れ

コネクションの作成

まずはLDAPコネクションを作成します。

using (var connection = new LdapConnection { SecureSocketLayer = false })

 

Bind

続いてBindします。

この時、ADのユーザIDとパスワードが必要になります。Macのユーザとは異なっていて問題ないです。

connection.Connect(ipAddress, port);
connection.Bind(userDn, password);

 
接続が成功した場合は、 connection.Boundtrue になります。

 

ADの検索

検索するには、 connection.Search() を使います。

引数は5つあります。

 

第一引数 (searchBase)

どこを探すかを指定します。

今回はドメインすべてを検索するため、 dc=sub, dc=example, dc=co, dc=jp を指定します。

なお、dcの数を動的に指定したかったため、以下のような関数を用意しました。

これで、 sub.example.co.jpdc=sub, dc=example, dc=co, dc=jp になります。

static string ConvertDomain(string domain)
{
    return domain.Split(".")
        .Select(s => $"dc={s}")
        .Aggregate((result, element) => $"{result}, {element}");
}

 

第二引数 (LdapConnectionクラス)

どの範囲で検索するかを指定します。

今回は、ドメイン内全てを検索するため、 LdapConnection.ScopeSub とします*1

 

第三引数 (フィルタ)

絞り込み条件を指定します。

今回は、ADユーザのログインIDをキーにデータを取得するため、 SAMAccountName を使います。

またあいまい検索もしたいため、 * を使った

$"(SAMAccountName=*{userName}*)"

を指定します。

 

第四引数 (取得したい項目)

ADから取得したい項目をここで指定します。

どのような値を指定できるかは以下が参考になりました。

 
今回は以下を指定しました。

new []
{
    "displayName",        // 表示名
    "cn",                 // 表示名と同じ
    "sn",                 // 姓
    "givenName",          // 名
    "userPrincipalName",  // ユーザーログオン名
    "sAMAccountName",     // ユーザーログオン名(Windows 2000以前)
    "description"         // 説明
},

 
主な引数はこんな感じです。

 

値の取得

Search()で取得した値は、 GetAttribute() を使って取得できます。

var user = result.Next();
var displayName = user.GetAttribute("displayName").StringValue;

 

ソースコード全体

ここまでをまとめると以下の感じです。

using System;
using System.Linq;
using Novell.Directory.Ldap;

namespace LdapAuthConsole
{
    class Program
    {
        static string ConvertDomain(string domain)
        {
            var r = domain.Split(".")
                .Select(s => $"dc={s}")
                .Aggregate((result, element) => $"{result}, {element}");
            
            Console.WriteLine(r);
            return r;
        }
        
        static void Main(string[] args)
        {
            var userName = "foo_user";  // Domain Users
            var password = "YOUR_PASSWORD";
            var domain = "sub.example.co.jp";
            var ipAddress = "192.168.xxx.xxx";
            var port = 389;
            var userDn = $"{userName}@{domain}";
            
            try
            {
                using (var connection = new LdapConnection { SecureSocketLayer = false })
                {
                    connection.Connect(ipAddress, port);
                    connection.Bind(userDn, password);

                    if (connection.Bound){
                        Console.WriteLine("接続できました");
                        
                        // 検索
                        var searchBase = ConvertDomain(domain);
                        
                        // あいまい検索が使える
                        var searchFilter = $"(SAMAccountName=*{userName}*)";
                        var result = connection.Search(
                            searchBase,
                            // ドメイン直下からすべてのサブを調べる
                            LdapConnection.ScopeSub,
                            searchFilter,
                            new []
                            {
                                "displayName",        // 表示名
                                "cn",                 // 表示名と同じ
                                "sn",                 // 姓
                                "givenName",          // 名
                                "userPrincipalName",  // ユーザーログオン名
                                "sAMAccountName",     // ユーザーログオン名(Windows 2000以前)
                                "description"         // 説明
                            },
                            false
                        );
                        var user = result.Next();
                        var displayName = user.GetAttribute("displayName").StringValue;
                        
                        Console.WriteLine(user);
                        Console.WriteLine(displayName);
                    }
                    else
                    {
                        Console.WriteLine("接続できませんでした");
                    }
                }
            }
            catch (LdapException ex)
            {
                // Log exception
                // TODO 例外処理を実装
                Console.WriteLine("例外が出ました");
                Console.WriteLine(ex);
            }
        }
    }
}

 

結果

実行すると、MacでADユーザを使っていなくても、LDAP認証を使ってADから情報を取得できました。

$ dotnet run
接続できました
dc=sub, dc=example, dc=co, dc=jp

# LDAPで取得した中身
LdapEntry: CN=foo bar,CN=Users,DC=sub,DC=example,DC=co,DC=jp; LdapAttributeSet: LdapAttribute: {type='cn', value='foo bar'} LdapAttribute: {type='sn', value='foo'} LdapAttribute: {type='givenName', value='bar'} LdapAttribute: {type='displayName', value='foo bar'} LdapAttribute: {type='sAMAccountName', value='foo_user'} LdapAttribute: {type='userPrincipalName', value='foo_user@ad.jsl.co.jp'}

# GetAttribute()の結果
foo bar

 

ソースコード

GitHubに上げました。
https://github.com/thinkAmi-sandbox/CSharp_Ldap_Auth-sample

*1:2系は大文字の名前でしたが、3系はCamelCaseになりました

C# + AutoMapperで、ArrayListをオブジェクトへマッピングする

AutoMapperを使うことで、オブジェクト同士を簡単にマッピングできます。

そんな中、ArrayListをオブジェクトへマッピングすることがあったため、対応したときのメモを残します。

 
目次

 

環境

 
今回は

new ArrayList {"1", "すいか", "夏", 1000};

のようなArrayList

public class Fruit
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Season { get; set; }
    public decimal UnitPrice { get; set; }
}

へとマッピングすることを考えます。

 

ArrayListをオブジェクトへマッピング

以下のstackoverflowを参考にしました。
c# - How to map an array to multiple properties using AutoMapper? - Stack Overflow

 
ただ、AutoMapperからstatic APIが取り除かれたため、それに対応して実装します。
Removing the static API from AutoMapper · Los Techies

 
まずはマッピングです。

// ArrayListの要素をプロパティにマッピング
var config = new MapperConfiguration(c => 
    c.CreateMap<ArrayList, Fruit>()
        .ForMember(d => d.Id, o => o.MapFrom(s => s[0]))
        .ForMember(d => d.Name, o => o.MapFrom(s => s[1]))
        .ForMember(d => d.Season, o => o.MapFrom(s => s[2]))
        .ForMember(d => d.UnitPrice, o => o.MapFrom(s => s[3]))
    );

// インスタンス化
var mapper = config.CreateMapper();

 
続いて変換します。Mapメソッドに、変換先のクラスを指定します。

// 元データ
var source = new ArrayList {"1", "すいか", "夏", 1000};

// 変換
var dest = mapper.Map<Fruit>(source);

 
結果を確認します。マッピングできています。

Console.WriteLine("ArrayList to Fruit");
Console.WriteLine(
    $"ID: {dest.Id}, Name: {dest.Name}, Season: {dest.Season}, UnitPrice: {dest.UnitPrice}");

// =>
// ArrayList to Fruit
// ID: 1, Name: すいか, Season: 夏, UnitPrice: 1000

 

ListをListへマッピング

AutoMapperではListなどもマッピングできますので、試してみます。
Lists and Arrays — AutoMapper documentation

マッピング方法は先ほどと変わりません。

var config = new MapperConfiguration(c => 
    c.CreateMap<ArrayList, Fruit>()
        .ForMember(d => d.Id, o => o.MapFrom(s => s[0]))
        .ForMember(d => d.Name, o => o.MapFrom(s => s[1]))
        .ForMember(d => d.Season, o => o.MapFrom(s => s[2]))
        .ForMember(d => d.UnitPrice, o => o.MapFrom(s => s[3]))
);

var mapper = config.CreateMapper();

 
Map()の使い方が変わります。

Map()の第一引数に変換元のクラスを、第二引数に変換先のクラスをそれぞれ指定します。

// データ
var source = new List<ArrayList>
{
    new ArrayList {"1", "すいか", "夏", 1000},
    new ArrayList {"2", "りんご", "秋", 100},
    new ArrayList {"3", "みかん", "冬", 150}
};

// 変換
var dest = mapper.Map<List<ArrayList>, List<Fruit>>(source);

 
結果です。List同士のマッピングもできました。

Console.WriteLine("List<ArrayList> to List<Fruit>");
foreach (var fruit in dest)
{
    Console.WriteLine(
        $"ID: {fruit.Id}, Name: {fruit.Name}, Season: {fruit.Season}, UnitPrice: {fruit.UnitPrice}");
}

// =>
// List<ArrayList> to List<Fruit>
// ID: 1, Name: すいか, Season: 夏, UnitPrice: 1000
// ID: 2, Name: りんご, Season: 秋, UnitPrice: 100
// ID: 3, Name: みかん, Season: 冬, UnitPrice: 150

 

双方向マッピング

AutoMapperでは、

を同時に設定できます。

逆方向をマッピングするには ReverseMap() を使います。
Reverse Mapping and Unflattening — AutoMapper documentation

通常同じプロパティへ自動的にマッピングしますが、今回はArrayListへのマッピングとなるため、手動でマッピングする必要があります。

そのため、 ConstructUsing() を使って、マッピングを定義しています。
https://docs.automapper.org/en/stable/Queryable-Extensions.html#custom-destination-type-constructors

var config = new MapperConfiguration(c => 
    c.CreateMap<ArrayList, Fruit>()
        .ForMember(d => d.Id, o => o.MapFrom(s => s[0]))
        .ForMember(d => d.Name, o => o.MapFrom(s => s[1]))
        .ForMember(d => d.Season, o => o.MapFrom(s => s[2]))
        .ForMember(d => d.UnitPrice, o => o.MapFrom(s => s[3]))
        
        // 逆マップ
        .ReverseMap()
        .ConstructUsing(x => new ArrayList
        {
            x.Id, x.Name, x.Season, x.UnitPrice
        })
);
var mapper = config.CreateMapper();

 

使い方です。

// 結果
var reverseSource = new Fruit {Id = 1, Name = "すいか", Season = "夏", UnitPrice = 1000};

// 変換
var reverseDst = mapper.Map<ArrayList>(reverseSource);

 
結果確認です。

Console.WriteLine(
    $"ID: {reverseDst[0]}, Name: {reverseDst[1]}, Season: {reverseDst[2]}, UnitPrice: {reverseDst[3]}");

// =>
// 逆方向
// ID: 1, Name: すいか, Season: 夏, UnitPrice: 1000

 

双方向マッピング (List)

Listも可能です。

定義は変わりません。

var config = new MapperConfiguration(c => 
    c.CreateMap<ArrayList, Fruit>()
        .ForMember(d => d.Id, o => o.MapFrom(s => s[0]))
        .ForMember(d => d.Name, o => o.MapFrom(s => s[1]))
        .ForMember(d => d.Season, o => o.MapFrom(s => s[2]))
        .ForMember(d => d.UnitPrice, o => o.MapFrom(s => s[3]))
        
        // 逆マップ
        .ReverseMap()
        .ConstructUsing(x => new ArrayList
        {
            x.Id, x.Name, x.Season, x.UnitPrice
        })
);
var mapper = config.CreateMapper();

 
使い方が変わります。

Map()の第一引数が変換元のList、第二引数が変換先のListを指定します。

var reverseSourceList = new List<Fruit>
{
    new Fruit {Id = 1, Name = "すいか", Season = "夏", UnitPrice = 1000},
    new Fruit {Id = 2, Name = "りんご", Season = "秋", UnitPrice = 100},
    new Fruit {Id = 3, Name = "みかん", Season = "冬", UnitPrice = 150}

};

// 変換
var reverseDestList = mapper.Map<List<Fruit>, List<ArrayList>>(reverseSourceList);

// 結果
foreach (var dst in reverseDestList)
{
    Console.WriteLine(
        $"ID: {dst[0]}, Name: {dst[1]}, Season: {dst[2]}, UnitPrice: {dst[3]}");
}

// =>
// 逆方向List
// ID: 1, Name: すいか, Season: 夏, UnitPrice: 1000
// ID: 2, Name: りんご, Season: 秋, UnitPrice: 100
// ID: 3, Name: みかん, Season: 冬, UnitPrice: 150

 

マッピング設定をクラス化 (Profile)

Profileを使うことで、マッピング設定をクラス化できます。 https://docs.automapper.org/en/stable/Configuration.html#profile-instances

先ほどの双方向マッピングをクラスにします。  

public class AutoMapperProfile : Profile
{
    public AutoMapperProfile()
    {
        CreateMap<ArrayList, Fruit>()
            .ForMember(d => d.Id, o => o.MapFrom(s => s[0]))
            .ForMember(d => d.Name, o => o.MapFrom(s => s[1]))
            .ForMember(d => d.Season, o => o.MapFrom(s => s[2]))
            .ForMember(d => d.UnitPrice, o => o.MapFrom(s => s[3]))
            
            // 逆マップ
            .ReverseMap()
            .ConstructUsing(x => new ArrayList
            {
                x.Id, x.Name, x.Season, x.UnitPrice
            });
    }
}

 
設定クラスを読み込みます。

AutoMapperでは明示的に指定する他、アセンブリから自動で検索することもできます。
http://docs.automapper.org/en/stable/Configuration.html#assembly-scanning-for-auto-configuration

今回はアセンブリを指定してみます。

// Profileを検索
var config = new MapperConfiguration(c => 
    c.AddMaps(new []
    {
        "MyApp",
    })
);

// インスタンス化
var mapper = config.CreateMapper();

 
あとは今までと同じです。

ここでは正方向だけ示しますが、逆方向も動作します。

Console.WriteLine("正方向 (Profile使用)");
var source = new ArrayList {"1", "すいか", "夏", 1000};
var dest = mapper.Map<Fruit>(source);

Console.WriteLine(
    $"ID: {dest.Id}, Name: {dest.Name}, Season: {dest.Season}, UnitPrice: {dest.UnitPrice}");

// =>
// 正方向 (Profile使用)
// ID: 1, Name: すいか, Season: 夏, UnitPrice: 1000

 

ソースコード

GitHubに上げました。
thinkAmi-sandbox/AutoMapper-sample

ASP.NET Core 3.1 & Vue.js 上で、Handsontableを動かしてみた

ASP.NET Core 3.1 & Vue.js 上で、Handsontableを動かす機会があったため、メモを残します。

 
目次

 

環境

 

環境構築

ASP.NET Core 3.1向けのVue.jsプロジェクトテンプレートについて

RiderなどのIDEでは、ASP.NET Coreで使えるVue.jsのテンプレートがGUIでは選択できません。

Vue向けの公式テンプレートは、dotnetコマンドでインストールできるようですが、試してみたところ、Core3.x系のテンプレートにはなりませんでした。
ASP.NET Core のテンプレートで Vue をインストール - Qiita

ほかのテンプレートを探したところ、starの多い aspnetcore-Vue-starter がありました。
TrilonIO/aspnetcore-Vue-starter: NEW Asp.net Core & Vue.js (ES6) SPA Starter kit - Vuex, webpack, Web API, Docker, and more! By @TrilonIO

ただ、こちらもCore3系には対応していませんでした。
Please Upgrade to Core 3 · Issue #138 · TrilonIO/aspnetcore-Vue-starter

Core3系に対応したテンプレートを探したところ、以下がありました。
SoftwareAteliers/asp-net-core-vue-starter: ASP.NET Core + Vue.js starter project

また、

(Optional) Scaffold Vue.js app with custom configuration

と、Vue.jsのカスタマイズもできそうでした。

そのため、このテンプレートを使うことにしました。

 

ASP.NET & Vue.jsの環境構築
テンプレートのインストール
dotnet new -i SoftwareAteliers.AspNetCoreVueStarter

 
テンプレートのリストを見ると、他のVue.js向けテンプレートをインストールしていたせいか、Short Nameが重複してしまいました。

そのため、テンプレートを使う時は、 Templatesに記載されている .NET Core Vue.js を指定します。

$ dotnet new list

Templates                                         Short Name
------------------------------------------------------------
...
ASP.NET Core with Vue.js                          vue       
...                         
.NET Core Vue.js                                  vue       

 

プロジェクトの作成
# ソリューションファイルを作成
$ dotnet new sln -n HandsonTableVueOnAspNetCore

# テンプレート名を使って、プロジェクトを作成
$ dotnet new ".NET Core Vue.js" -o HandsonTableVueOnAspNetCore

# プロジェクトをソリューションに追加
$ dotnet sln add ./HandsonTableVueOnAspNetCore
プロジェクト `HandsonTableVueOnAspNetCore/HandsonTableVueOnAspNetCore.csproj` をソリューションに追加しました。

# Vue.jsのカスタマイズをするため、READMEにある通り、ClientAppを削除
$ rm -rf ClientApp/

 

Vue CLIによる、Vue.jsの環境構築

READMEに従い、 client-app という名前で生成しました。

ひとまずRouterだけ追加しておきます。

$ vue create client-app
Vue CLI v4.1.2
? Please pick a preset: Manually select features
? Check the features needed for your project: Router
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No

 

TypeScriptの追加

client-appに対し、TypeScriptを追加します。

ただし、今回はTypeScript化までは行わないため、不要な場合はパスしても大丈夫です。

現時点では、Handsontable向けには、TypeScriptは3.6系を使っておくのが良さそうなので、バージョン指定してインストールします。
Import declaration conflicts with local declaration of 'HotTableProps'. · Issue #145 · handsontable/vue-handsontable-official

$ cd client-app/
$ npm install --save-dev typescript@3.6.4
+ typescript@3.6.4

 
続いて、Vue.jsに追加します。

TSLintの代わりにESLintを使うため、TSLint以外はデフォルトのままで進めます。

ワーニングが出ますが、とりあえずこのままで進めます。

$ vue add typescript
? Still proceed? Yes

✔  Successfully installed plugin: @vue/cli-plugin-typescript

? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Use TSLint? No
? Convert all .js files to .ts? Yes
? Allow .js files to be compiled? No

WARN  conflicting versions for project dependency "typescript":

- ^3.6.4 injected by generator "undefined"
- ~3.5.3 injected by generator "@vue/cli-plugin-typescript"

Using newer version (^3.6.4), but this may cause build errors.

 

ESLintの追加
$ npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-vue
+ eslint@6.8.0
+ eslint-plugin-vue@6.1.2
+ @typescript-eslint/parser@2.14.0
+ @typescript-eslint/eslint-plugin@2.14.0

 

Prettier
$ npm install --save-dev prettier eslint-plugin-prettier eslint-config-prettier
+ prettier@1.19.1
+ eslint-config-prettier@6.9.0
+ eslint-plugin-prettier@3.1.2

 

client-app内にある、不要なgitリポジトリを削除

今回はASP.NET Coreも含めてリポジトリ管理するため、削除しておきます。

$ rm -rf .git/

 

ディレクトリの名前を変更

READMEにある通り、 client-app から ClientApp へと変更します。

$ cd ..
$ mv client-app/ ClientApp/

 

動作確認

dotnet run コマンドで起動し、ASP.NET Core & Vue.jsが動作していることを確認します。

$ dotnet run

f:id:thinkAmi:20200105230500p:plain:w400

 

Vue.js & Handsontableの環境構築
vue-handsontable-officialのインストール

Vue.js向けとして、Handsontable公式が vue-handsontable-official を提供しています。
handsontable/vue-handsontable-official: Vue Data Grid with Spreadsheet Look & Feel. Official Vue wrapper for Handsontable.

そのため、インストールします。

$ npm install handsontable @handsontable/vue
...
Handsontable is no longer released under the MIT license. Read more about this change on our blog at https://handsontable.com/blog.
+ handsontable@7.3.0
+ @handsontable/vue@4.1.1

 

実装
Handsontableを使ったコンポーネントを作成

ClientApp/src/components/HelloHandsontable.vue としてコンポーネントを作成します。

<template>
    <div>
        <hot-table :settings="hotSettings" />
    </div>
    
</template>

<script>
    import {HotTable} from "@handsontable/vue";
    
    export default {
        name: "HelloHandsontable",
        components: {
            HotTable
        },
        data() {
            return {
                hotSettings: {
                    // 非商用向けのライセンス
                    licenseKey: 'non-commercial-and-evaluation',
                    
                    data: [
                        [1, "紅あずま", 10],
                        [2, "紅はるか", 20],
                        [3, "シルクスイート", 30],
                    ],
                    colHeaders: ["No", "Name", "Price"],
                    rowHeaders: ["1st", "2nd", "3rd"],
                    
                    // コンテキストメニューまわり
                    // contextMenu: true,  // trueにすると、ブラウザのコンテキストメニューが表示されない
                    allowInsertColumn: false,
                    allowRemoveColumn: false,
                    
                    // 列のソートインディケータを表示
                    columnSorting: {
                        indicator: true
                    }
                }
            }
        }
    }
</script>

<style>
    @import '~handsontable/dist/handsontable.full.css';

    /* 列ヘッダの色を変更する */
    .handsontable thead th .relative {
        background-color: deepskyblue;
    }
</style>

 

App.vueの修正

templateタグの差し替えと、script内にコンポーネントを追加します。

<template>
  <div id="app">
    <!-- Handsontableの表示へと変更
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
    -->
    <div>
      <h3>Hello Handsontable</h3>
      <HelloHandsontable />
    </div>
  </div>
</template>

<script lang="ts">
// 追加
import HelloHandsontable from "@/components/HelloHandsontable.vue";

@Component({
  components: {
    HelloHandsontable,  // 追加
    HelloWorld,
  },
})
export default class App extends Vue {}
</script>

 

開発証明書の適用

もし開発証明書の信頼を行っていない場合、行っておきます。
Windows および macOS で ASP.NET Core HTTPS 開発証明書を信頼する | ASP.NET Core に HTTPS を適用する | Microsoft Docs

$ dotnet dev-certs https --trust

 

動作確認

ここまでで、ASP.NET Core & Vue.js & Handsontableの環境ができましたので、動作を確認します。

dotnet run コマンドで起動します。

$ dotnet run

 
https://localhost:5000 へアクセスし、以下が表示されればOKです。

f:id:thinkAmi:20200105230555p:plain:w400

 

ASP.NET CoreでJSONを返すAPIの作成と連携

まずは、定形JSONを返すASP.NET Core APOIを作成し、そのレスポンスをHandsontableへと反映させてみます。

 

Vueコンポーネントの作成

先ほどと同じようなコンポーネントを作成します。

違いとしては、

  • hotSettingsに渡す data の初期値は、空の配列にする
  • created()で、APIからJSONを受け取り、Handsontableに反映する

となります。

<template>
    <div>
        <hot-table :settings="hotSettings" />
    </div>
    
</template>

<script>
    import {HotTable} from "@handsontable/vue";
    
    export default {
        name: "ApiResponseHandsontable",
        components: {
            HotTable
        },
        data() {
            return {
                hotSettings: {
                    // 非商用向けのライセンス
                    licenseKey: 'non-commercial-and-evaluation',
                    
                    // 初期データなし
                    data: [],
                    // Name列だけ、幅を指定する
                    colWidths: [null, 200, null],
                    
                    colHeaders: ["No", "Name", "Price"],
                    rowHeaders: ["1st", "2nd", "3rd"],
                    
                    // コンテキストメニューまわり
                    // contextMenu: true,  // trueにすると、ブラウザのコンテキストメニューが表示されない
                    allowInsertColumn: false,
                    allowRemoveColumn: false,
                    
                    // 列のソートインディケータを表示
                    columnSorting: {
                        indicator: true
                    }
                }
            }
        },
        created: function() {
            // ロードされた時にAPIを呼んで、Handsontableの初期値を取得する
            fetch('/api/const')
                .then(res => { return res.json() })
                .then(data => {
                    this.hotSettings.data = JSON.parse(data);
                })
        },
    }
</script>

<style>
    @import '~handsontable/dist/handsontable.full.css';

    /* 列ヘッダの色を変更する */
    .handsontable thead th .relative {
        background-color: deepskyblue;
    }
</style>

 

App.vueの修正

コンポーネントを追加します。

<template>
  <div id="app">
    <!-- Handsontableの表示へと変更
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
    -->
    <div>
      <h3>Hello Handsontable</h3>
      <HelloHandsontable />
    </div>
    
    <hr>
    
    <div>
      <h3>Const Response Handsontable</h3>
      <ConstResponseHandsontable />
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
//...
// 追加
import ConstResponseHandsontable from "@/components/ConstResponseHandsontable.vue";

@Component({
  components: {
    ConstResponseHandsontable,  // 追加
    HelloHandsontable,
    HelloWorld,
  },
})
export default class App extends Vue {}
</script>

 

ASP.NET Coreのコントローラーを作成

/api/const にアクセスしたときにJSONレスポンスを返すコントローラーを作成します。

コントローラークラスに

  • ControllerBaseを継承
  • [Route("api/")] 属性で、APIのルーティングを設定
  • [ApiController] 属性で、API向けコントローラーとして動作するよう設定
  • [Produces(MediaTypeNames.Application.Json)] 属性で、レスポンスをJSONにするように指定

とします。

あとは、 GetConstResponse() メソッドに、JSONでレスポンスする内容を記載します。

 
なお、Handsontableのデータ投入メソッド(loadData())に不具合があるようで、7.3.0時点では配列の配列でしかデータを投入できないようです。
Using loadData on an object data doesn't work · Issue #4204 · handsontable/handsontable

8系がリリースされるとオブジェクト配列で投入できそうなので、その時は以下のコードは書き換えたほうが良いかもしれません。

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mime;
using System.Text.Json;
using Bogus;
using HandsonTableVueOnAspNetCore.Models;
using Microsoft.AspNetCore.Mvc;

namespace HandsonTableVueOnAspNetCore.Controllers
{
    [Route("api/")]
    [ApiController]
    [Produces(MediaTypeNames.Application.Json)]
    public class HandsontableApiController : ControllerBase
    {
        [HttpGet("const")]
        public IActionResult GetConstResponse()
        {
            var apples = new List<dynamic>
            {
                new List<dynamic> {1, "秋映", 100},
                new List<dynamic> {2, "シナノゴールド", 200},
                new List<dynamic> {3, "ピンクレディ", 300}
            };
            return Ok(JsonSerializer.Serialize(apples));
        }
    }
}

 

動作確認

まずはAPIの動作を確認します。

localhost:5000/api/const にアクセスすると、JSONが返ってくればOKです。

$ curl http://localhost:5000/api/const
"[[1,\"\\u79CB\\u6620\",100],[2,\"\\u30B7\\u30CA\\u30CE\\u30B4\\u30FC\\u30EB\\u30C9\",200],[3,\"\\u30D4\\u30F3\\u30AF\\u30EC\\u30C7\\u30A3\",300]]

 

続いて、 localhost:5000 にアクセスし、JSONの内容がHandosontableに反映されていればOKです。

f:id:thinkAmi:20200105230523p:plain:w400

 

ASP.NET CoreでモデルのJSONを返すAPIの作成と連携

最後に、ASP.NET CoreのモデルをHandsontableに表示してみます。

 

環境構築

今回はEntityFramework Core & SQLiteを使うため、必要なパッケージを追加します。

dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SQLite

また、念のため、発行されたSQLの中身もコンソール出力するため、パッケージを追加します。

dotnet add package Microsoft.Extensions.Logging.Console

 

Vue.jsのコンポーネントを作成

create()でアクセスするAPIのエンドポイントが変更となっただけで、あとは同じです。

<template>
    <div>
        <hot-table :settings="hotSettings" />
    </div>
    
</template>

<script>
    import {HotTable} from "@handsontable/vue";
    
    export default {
        name: "ModelResponseHandsontable",
        components: {
            HotTable
        },
        data() {
            return {
                hotSettings: {
                    // 非商用向けのライセンス
                    licenseKey: 'non-commercial-and-evaluation',
                    
                    // 初期データなし
                    data: [],
                    // Name列だけ、幅を指定する
                    colWidths: [null, 200, null],
                    
                    colHeaders: ["No", "Name", "Age"],
                    rowHeaders: ["1st", "2nd", "3rd"],
                    
                    // コンテキストメニューまわり
                    // contextMenu: true,  // trueにすると、ブラウザのコンテキストメニューが表示されない
                    allowInsertColumn: false,
                    allowRemoveColumn: false,
                    
                    // 列のソートインディケータを表示
                    columnSorting: {
                        indicator: true
                    }
                }
            }
        },
        created: function() {
            // ロードされた時にAPIを呼んで、Handsontableの初期値を取得する
            fetch('/api/model')
                .then(res => { return res.json() })
                .then(data => {
                    this.hotSettings.data = JSON.parse(data);
                })
        },
    }
</script>

<style>
    @import '~handsontable/dist/handsontable.full.css';

    /* 列ヘッダの色を変更する */
    .handsontable thead th .relative {
        background-color: deepskyblue;
    }
</style>

 

App.vueの修正

こちらも、コンポーネントを追加するだけです。

<template>
  <div id="app">
    <!-- Handsontableの表示へと変更
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
    -->
    <div>
      <h3>Hello Handsontable</h3>
      <HelloHandsontable />
    </div>
    
    <hr>
    
    <div>
      <h3>Const Response Handsontable</h3>
      <ConstResponseHandsontable />
    </div>
    
    <hr>
    
    <div>
      <h3>Model Response Handsontable</h3>
      <ModelResponseHandsontable />
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HelloWorld from './components/HelloWorld.vue';
import HelloHandsontable from "@/components/HelloHandsontable.vue";
import ConstResponseHandsontable from "@/components/ConstResponseHandsontable.vue";

// 追加
import ModelResponseHandsontable from "@/components/ModelResponseHandsontable.vue";

@Component({
  components: {
    ModelResponseHandsontable,  // 追加
    ConstResponseHandsontable,
    HelloHandsontable,
    HelloWorld,
  },
})
export default class App extends Vue {}
</script>

 

モデルの作成

Models/Customer.cs として作成します。

namespace HandsonTableVueOnAspNetCore.Models
{
    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }
    }
}

 

DbContextの作成

Models/HandsontableContext.cs として、

  • ログをコンソールに出力
  • SQLiteを使う

のDbContextを作成します。

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace HandsonTableVueOnAspNetCore.Models
{
    public class HandsontableContext : DbContext
    {
        public HandsontableContext(DbContextOptions<HandsontableContext> options) : base(options) {}

        public DbSet<Customer> Customers { get; set; }

        public static readonly ILoggerFactory MyLoggerFactory
            = LoggerFactory.Create(builder =>
            {
                builder
                    .AddFilter(DbLoggerCategory.Database.Command.Name, LogLevel.Information)
                    .AddConsole();
            });
        
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);
            optionsBuilder
                .EnableSensitiveDataLogging()
                .UseLoggerFactory(MyLoggerFactory);
        }
    }
}

 

appsettings.Development.jsonの修正

SQLite向けの接続文字列を追加します。

今回は開発環境なので、 appsettings.Development.json ファイルに追加します。

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },

  // 以下を追加
  "ConnectionStrings": {
    "HandsontableContext": "Data Source=./WebApplication.db"
  }
}

 

Startup.csの修正

ConfigureServicesに、DbContextを追加します。

public void ConfigureServices(IServiceCollection services)
{
    //...

    // 追加
    services.AddDbContext<HandsontableContext>(options => options.UseSqlite(
        Configuration.GetConnectionString("HandsontableContext")));
}

 

マイグレーション

モデルとDbContextができたため、マイグレーションを行います。

# マイグレーションファイルを作成
$ dotnet ef migrations add InitialCreate

# SQLiteへ反映
$ dotnet ef database update

 

コントローラーの修正
Seed用URLを作成

デフォルトデータとして投入する方法として以前は OnModelCreating() をオーバーライドしました。
データシード処理-EF Core | Microsoft Docs

ただ、今回は何度でも使えるよう、Seed用URLを作成します。
c# - How to seed in Entity Framework Core 2? - Stack Overflow

また、デフォルトデータはランダムなもので良いので、 Bogus を使います。
bchavez/Bogus: A simple and sane fake data generator for C#, F#, and VB.NET. Based on and ported from the famed faker.js.

パッケージをインストールします。

$ dotnet add package Bogus

あとは、コントローラーでDbContextを受け取り、Seed処理を実装します。

GETメソッドでのSeedでいいのか感はありますが、開発用途なのでヨシとします。

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mime;
using System.Text.Json;
using Bogus;
using HandsonTableVueOnAspNetCore.Models;
using Microsoft.AspNetCore.Mvc;

namespace HandsonTableVueOnAspNetCore.Controllers
{
    [Route("api/")]
    [ApiController]
    [Produces(MediaTypeNames.Application.Json)]
    public class HandsontableApiController : ControllerBase
    {
        // 追加
        private readonly HandsontableContext _context;
        public HandsontableApiController(HandsontableContext context)
        {
            this._context = context;
        }
        
        //...

        // 追加
        [HttpGet("seed")]
        public IActionResult Seed()
        {
            var faker = new Faker<Customer>("ja")
                .RuleFor(r => r.Name, f => $"{f.Name.LastName()} {f.Name.FirstName()}")
                .RuleFor(r => r.Age, f => f.Random.Number(20, 60));

            var fakes = faker.Generate(3);
            
            _context.Customers.AddRange(fakes.ToArray());
            _context.SaveChanges();

            var customers = fakes.Select(a => new ArrayList
            { 
                a.Id, a.Name, a.Age
            });

            return Ok(JsonSerializer.Serialize(customers));
        }
    }
}

 

モデルからデータを読み込んでJSONレスポンスするAPI作成

同じくコントローラーに追加します。

/api/modelJSONレスポンスを行います。

[HttpGet("model")]
public IActionResult GetApplesResponse()
{
    var response = _context.Customers.Select(c => new ArrayList
    {
        c.Id, c.Name, c.Age
    });
    
    return Ok(JsonSerializer.Serialize(response));
}

 

動作確認

データベースへのSeedを行います。

$ curl http://localhost:5000/api/seed
"[[1,\"\\u9AD8\\u6A4B \\u9678\\u6597\",40],[2,\"\\u677E\\u672C \\u7F8E\\u7FBD\",56],[3,\"\\u658E\\u85E4 \\u592A\\u4E00\",52]]"

 
モデルAPIのレスポンスも確認します。

$ curl http://localhost:5000/api/model
"[[1,\"\\u9AD8\\u6A4B \\u9678\\u6597\",40],[2,\"\\u677E\\u672C \\u7F8E\\u7FBD\",56],[3,\"\\u658E\\u85E4 \\u592A\\u4E00\",52]]"

 
最後に localhost:5000 にアクセスし、表示できればOKです。 (中身はBogusによるダミーデータです)

f:id:thinkAmi:20200105230624p:plain:w400

 

ソースコード

Githubに上げました。
thinkAmi-sandbox/Handsontable_On_ASP_NET_Core_Vue-sample