からめもぶろぐ。

SharePoint が得意なフレンズなんだね!すごーい!

JSOM の executeQueryAsync で使われている Function.createDelegate について

JSOM のサンプルを見ると当たり前のように使われている Function.createDelegate について。

引数に this を渡しているのでだいたい想像が付くとは思いますが、JavaScript の this の面倒なアレを解決してくれるんですね。

<asp:Content ContentPlaceHolderID="PlaceHolderMain" runat="server">
    <script type="text/javascript">
        var obj = {
            web: null,
            retrieveTitle: function () {
                var ctx = SP.ClientContext.get_current();
                this.web = ctx.get_web();
                ctx.load(this.web);
                ctx.executeQueryAsync(this.onSuccess); // NG
                ctx.executeQueryAsync(Function.createDelegate(this, this.onSuccess)); //OK
            },
            onSuccess: function () {
                alert(this.web.get_title());
            }
        };
        SP.SOD.executeFunc("sp.js", "SP.ClientContext", function () {
            obj.retrieveTitle();
        });
    </script>
</asp:Content>

ちなみに Function.createDelegate は MicrosoftAjax.js に定義されています。

データ取得の概要
Function.createDelegate 関数

SharePoint Online の REST API を PowerShell から叩いてみる

この記事は「PowerShell Advent Calendar 2016」の参加記事です。

qiita.com

前回の記事で「REST API を Invoke-RestMethod で叩けるかも」と丸投げなことを書いてしまったので、実際にやってみたいと思います。

blog.karamem0.jp

OAuth についておさらい

SharePoint Online の REST API を実行するには、Azure Active Directory (AAD) の OAuth によるアクセス許可が必要です。AAD では Access Token を取得するためにいくつかの方法 (grant_type) をサポートしています。

  • authorization_code (https://login.windows.net/common/oauth2/authorize)
  • client_credentials (X.509 証明書)
  • password (ユーザー名とパスワード、非推奨)
  • device_code (デバイス コード)

authorization_code については Web ブラウザーを使用するアプリケーション (Web アプリケーションやモバイル アプリケーション) が前提なので、PowerShell の場合はそれ以外の方法を選択することになります。password が最も簡単ですが非推奨なので除外して、実質的には device_code か client_credentials の二択になります。それぞれを簡単に比較すると以下の通りになります。

grant_type 手間 無人化
device_code 少ない 不可
client_credentials 多い

どちらもメリットとデメリットがありますので目的に応じて使い分けるのがいいと思います。今回は device_code による認証を行います。

試してみる

手順については松崎さんのブログを参照ください。

blogs.msdn.microsoft.com

アプリケーションの登録

現時点ではプレビュー段階ですが、新しい Azure 管理ポータルからも AAD のアプリ登録ができるようになりましたので、適当な名前で登録します。[リダイレクト URI] は使わないので http://localhost/ としています。

f:id:karamem0:20161215235141p:plain

使用する API に [Office 365 SharePoint Online] を選択します。

f:id:karamem0:20161215235152p:plain

アクセス許可を選択します。今回はサンプルなのでフル コントロールを付けていますが、適切なアクセス許可を選択してください。

f:id:karamem0:20161215235156p:plain

これで事前準備は完了です。

スクリプトの作成

github.com

C# だと WebClient や HttpClient を使って面倒な処理を書くことになりますが、PowerShell の場合は Invoke-RestMethod を使ってかなりすっきり書くことができます。

$tenantId = "<tenantid>"
$resourceUri = "<resourceuri>"
$clientId = "<clientid>"

# デバイス コードの取得
$uri = "https://login.microsoftonline.com/" + $TenantId + "/oauth2/devicecode?" + `
       "resource=" + [System.Uri]::EscapeDataString($resourceUri) + "&" + `
       "client_id=" + $clientId
$headers = @{
    "Accept" = "application/json"
}
$result = Invoke-RestMethod -Method "Get" -Uri $uri -Headers $headers

$userCode = $result.user_code
$deviceCode = $result.device_code

Write-Output $userCode
Start-Process "https://aka.ms/devicelogin"

Read-Host | Out-Null

# トークンの取得
$uri = "https://login.microsoftonline.com/" + $TenantId + "/oauth2/token"
$headers = @{ 
    "Accept" = "application/json"
    "Content-Type" = "application/x-www-form-urlencoded"
}
$body = "resource=" + [System.Uri]::EscapeDataString($resourceUri) + "&" + `
        "client_id=" + $clientId + "&" + `
        "grant_type=device_code&" + `
        "code=" + [System.Uri]::EscapeDataString($deviceCode)

$result = Invoke-RestMethod -Method "Post" -Uri $uri -Headers $headers -Body $body

$accessToken = $result.access_token

# サイトのタイトルを取得
$uri = $resourceUri + "/_api/web/title"
$headers = @{ 
    "Accept" = "application/json"
    "Authorization" = "Bearer " + $accessToken
}
$result = Invoke-RestMethod -Method "Get" -Uri $uri -Headers $headers
Write-Output $result.value

# ドキュメントの一覧を取得
$uri = $resourceUri + "/_api/web/getfolderbyserverrelativeurl('/Shared%20Documents')/files"
$headers = @{ 
    "Accept" = "application/json"
    "Authorization" = "Bearer " + $accessToken
}
$result = Invoke-RestMethod -Method "Get" -Uri $uri -Headers $headers
$result.value | select Name, TimeCreated, TimeLastModified

実行

実行するとプロンプトにコードが表示されます。認証が行われるまでスクリプトは入力待ち状態になっています。

f:id:karamem0:20161215235254p:plain

合わせてブラウザーが起動するので、コンソールに表示されたコードを入力します。

f:id:karamem0:20161215235302p:plain

アプリケーション名を確認して [続行] をクリックします。組織アカウントでのサインインを要求されますのでユーザー名とパスワードを入力します。

f:id:karamem0:20161215235311p:plain

サインインすると完了になります。ブラウザーは閉じても構いません。

f:id:karamem0:20161215235322p:plain

プロンプトで Enter キーをクリックするとスクリプトを再開します。SharePoint Online からサイトのタイトルとドキュメント ライブラリのファイルの一覧を取得できていることがわかります。

f:id:karamem0:20161215235329p:plain

まとめ

SharePoint Online 単体だと CSOM のほうが便利かもしれませんが、他の Office 365 サービスと連携する場合などは、REST API を使うと統一された方法で書くことができそうです。

おまけ

ちなみに、CSOM がどうやって SharePoint Online に認証をしているかというと、BPOSIDCRL という方式を使っているようです。BPOS の名前の通り、レガシーな方法なので、いつまでサポートされるんでしょうか。気になります。

RFC 6749 - The OAuth 2.0 Authorization Framework

SharePoint を PowerShell から操作する方法まとめ

この記事は「PowerShell Advent Calendar 2016」の参加記事です。

qiita.com

今は SharePoint をメインでやっているこのブログもいつの間にか PowerShell タグがついた記事が結構増えているわけですが、SharePoint の構築や運用にあたって PowerShell は欠かせない存在になりました。過去のバージョンの SharePoint では stsadm というコマンド ライン ツールを使っていたそうですが、今はお役御免となりました。*1
また SharePoint Online の登場により、新しく追加されたライブラリもありますので、合わせて整理したいと思います。

SharePoint Server (オンプレミス) の場合

SharePoint をインストールすると、[スタート] メニューに [SharePoint 20nn 管理シェル] が登録されます。ここから SharePoint を操作するためのさまざまなコマンドレットを実行できます。スクリプトで実行する場合や、ISE を使いたい場合は、以下のコマンドレットを実行することで、SharePoint の管理シェルを実行できるようになります。

Add-PSSnapin "Microsoft.SharePoint.PowerShell"

SharePoint 管理シェルの実体は Microsoft.SharePoint.dll (およびその関連ライブラリ) なので、SharePoint のサーバー オブジェクト モデルがそのまま適用されます。そのため、SharePoint 開発経験のある開発者には非常に取っつきやすいです。また、便利なコマンドレット群も提供されています。*2

Visual Studio で SharePoint ソリューション開発をするときに、デプロイ前後のコマンドで PowerShell スクリプトを呼び出してデバッグ先のサイトにテスト データを投入すると非常に捗ります。ただし、そのとき注意しないといけないのは、Visual Studio は x86 プロセスなので PowerShell も x86 が呼び出されてしまう点です。SharePoint の管理シェルは x64 でしか動作しないので、PowerShell も x64 で動かすよう工夫する必要があります。

"%WINDIR%\SysNative\WindowsPowerShell\v1.0\powershell.exe" -ExecutionPolicy Unrestricted -File "$(ProjectDir)\Hoge.ps1"

SharePoint Online の場合

SharePoint Online では、サーバーにログインすることはできないので、代わりに SharePoint Online 管理シェルが提供されています。しかし、ここで特に注意しなければならないのは、SharePoint Online 管理シェルでできることは非常に限定的であるという点です。実質的にはユーザーの管理とサイト コレクションの管理しかできません。ならばどうするかというと、SharePoint クライアント ライブラリ コード、すなわち CSOM を使うことになります。というより、CSOM でできないことを SharePoint Online 管理シェルがカバーしているという感じのようです。

CSOM は PowerShell を考慮して作られているわけではないので、割と辛い目に遭います。具体的にいうと、CSOM では Load メソッドで関連プロパティを読み込むときに Include という Entity Framework みたいな呼び出しをするわけですが、ラムダ式なので PowerShell では死にます。やればできるみたいですが、そこまでやるメリットもなさそう。

www.itunity.com

試したことないですが、REST API を Invoke-RestMethod で呼び出す方法もあるかもしれません。でも今度は認証の壁が立ちはだかるのです…。こちらもやればできますけど。

blog.karamem0.jp

*1:たぶん 2007 以前だと思いますが、その頃の SharePoint は触ったことがないので詳しいことはわかりません。

*2:個人的には Get-SPWeb が最強で、C# で SPSite.OpenWeb() するたびに「これ PowerShell だと一発なのに…」と思っています。

SharePoint で SharePoint じゃないページを作る

この記事は「Office 365 Advent Calendar 2016」の参加記事です。

www.adventar.org


SharePoint は標準でもそこそこそれなりに使えるのですが、やはりデザインをカスタマイズしたいという要望は多いです。でも SharePoint のスタイル シート構造を理解してカスタマイズするのは結構大変ですし、Bootstrap のような CSS フレームワークを使うこともできません。カスタマイズに時間をかけるならいっそ最初から作ってしまえ!となってしまいます。ということで、今回は SharePoint 上で SharePoint じゃないページを作ってみたいと思います。*1

マスター ページをダウンロードする

発行機能が有効になっているサイトではデザイン マネージャーから最低限のマスター ページを作成してダウンロードすることができます。対象のサイトで発行機能が有効になっていない場合は、SharePoint Online で既定で作成される検索サイト (https://***.sharepoint.com/search) を表示してください。検索サイトは発行機能が有効になっていますので、デザイン マネージャーを使用できます。

f:id:karamem0:20161202231756p:plain

余談ですが、発行機能はよほどの理由がない限り無効にしておくことをお勧めします。UI のサポートは受けられませんが、既定のマスター ページを変更したり、異なるマスター ページを使用するサイト ページを作るのに発行機能は必要ありません。

適当な名前でマスター ページを作成したら、マスター ページ ギャラリーからダウンロードして、対象のサイトにアップロードします。

サイト ページを作成する

空のサイト ページを作成します。MasterPageFile を "~masterurl/default.master" から先ほどアップロードしたマスター ページに変更します。

<%@ Page Language="C#" MasterPageFile="~sitecollection/_catalogs/masterpage/custom.master" inherits="Microsoft.SharePoint.WebPartPages.WebPartPage, Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<%@ Assembly Name="Microsoft.Web.CommandUI, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<asp:Content ContentPlaceHolderID="PlaceHolderPageTitle" runat="server">
</asp:Content>
<asp:Content ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server">
</asp:Content>
<asp:Content ContentPlaceHolderID="PlaceHolderSearchArea" runat="server">
</asp:Content>
<asp:Content ContentPlaceHolderID="PlaceHolderPageDescription" runat="server">
</asp:Content>
<asp:Content ContentPlaceHolderID="PlaceHolderMain" runat="server">
</asp:Content>

エクスプローラーまたは SharePoint Designer からアップロードします。

f:id:karamem0:20161204231030p:plain

リボンだけの空白のページが表示されます。

マスター ページを編集する

Bootstrap を設定してみましょう。<head> の適当な位置に以下の記述を追加します。

<SharePoint:CssRegistration name="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css" runat="server" />
<SharePoint:ScriptLink language="javascript" name="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.1.1.min.js" runat="server" />
<SharePoint:ScriptLink language="javascript" name="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.js" runat="server" />

既定のリボンの記述である <div id="ms-designer-ribbon"> および <wssucw:Welcome> はサクッと消してしまい、代わりに Bootstrap のナビゲーション バーを追加します。

<nav class="navbar navbar-inverse">
    <div class="container-fluid">
        <div class="collapse navbar-collapse">
            <p class="navbar-text navbar-right"><span id="current-user"></span></p>
        </div>
    </div>
</nav>

現在のユーザー名を表示するスクリプトを追加します。最低限のマスター ページでは SP.js を読み込んでいますので、JSOM を使用できます。

<script type="text/javascript">
    SP.SOD.executeFunc("sp.js", "SP.ClientContext", function () {
        var ctx = new SP.ClientContext.get_current();
        var web = ctx.get_web();
        var user = web.get_currentUser();
        ctx.load(user);
        ctx.executeQueryAsync(function () {
            $('#current-user').text(user.get_title());
        });
    });
</script>

マスター ページをアップロードします。
サイトにアクセスするとマスター ページで設定した Bootstrap のナビゲーションが表示されていることがわかります。

f:id:karamem0:20161204231034p:plain

サイト ページを編集する

リストの一覧を出してみましょう。JSOM を使ってリストのデータを読み込みます。

<asp:Content ContentPlaceHolderID="PlaceHolderMain" runat="server">
    <div class="container">
        <div class="table-responsive">
            <table id="table" class="table">
                <thead>
                    <tr>
                        <th>Id</th>
                        <th>Name</th>
                    </tr>
                </thead>
                <tbody>
                </tbody>
            </table>
        </div>
    </div>
    <script type="text/javascript">
        SP.SOD.executeFunc("sp.js", "SP.ClientContext", function () {
            var ctx = new SP.ClientContext.get_current();
            var web = ctx.get_web();
            var list = web.get_lists().getByTitle("ドキュメント");
            var items = list.getItems("");
            ctx.load(items, "Include(Id, File)");
            ctx.executeQueryAsync(function () {
                var collection = items.getEnumerator();
                while (collection.moveNext()) {
                    var item = collection.get_current();
                    var id = item.get_id();
                    var title = item.get_file().get_name();
                    $("#table tbody").append("<tr><td>" + id + "</td><td>" + title + "</td></tr>");
                }
            });
        });
    </script>
</asp:Content>

できました!簡単ですね。

f:id:karamem0:20161204231037p:plain

まとめ

今回は JSOM を使用する構成にしたので、最小限のマスター ページから作成しましたが、REST API だけを使うのであれば、SP.js を使う必要もないので、まったく新規のマスター ページからも作成できます。REST API であれば、SharePoint の知識がなくても取っつきやすいですし、jQuery との相性もよく、また単体テストもしやすいと思います。

*1:念のためお断りとして言っておくと、基本的にはプロバイダー ホスト型アドインを作って Azure なんかに置くのが推奨された手段です。ういうことができない特殊な事情がある場合の手段だと思ってくださいね!

ASP.NET MVC の Repository パターン再考

ASP.NET MVC が出始めた頃、Repository パターンが話題になりました。その後、あまり Repository パターンについての議論がされていないようなので、改めて Repository パターンについて考えてみたいと思います。

Repository パターンの基本

Repository パターンの目的はビジネス ロジックとデータ アクセス ロジックを分離することです。具体的には以下のようなコードになります。

まずはデータ アクセス ロジックを抽象化するためのインターフェイスを定義します。

public interface IRepository<T> {

    T GetOne(int id);

    IEnumerable<T> GetAll();

    void Add(T item);

    void Update(T item);

    void Delete(T item);

}

IRepository インターフェイスを実装し Entity Framework にアクセスするクラスを作成します。

public class PersonRepository : IRepository<Person> {

    private DbContext dbContext;

    public PersonRepository() {
        this.dbContext = new SampleDbContext();
    }

    public Person GetOne(int id) {
        return this.dbContext.Set<Person>().Find(id);
    }

    public IEnumerable<Person> GetAll() {
        return this.dbContext.Set<Person>().AsEnumerable();
    }

    public void Add(Person item) {
        this.dbContext.Set<Person>.Add(item);
        this.dbContext.SubmitChanges();
    }

    public void Update(Person item) {
        this.dbContext.SubmitChanges();
    }

    public void Delete(Person item) {
        this.dbContext.Set<Person>.Remove(item);
        this.dbContext.SubmitChanges();
    }

}

Repository を操作するビジネス ロジックを作成します。

public class SalaryService {

    private IRepository<Person> personRepository;

    public class SomeService() {
        this.personRepository = new PersonRepository();
    }

    public class SomeService(IRepository<Person> personRepository) {
        this.personRepository = personRepository;
    }

    public IEnumerable<Salary> GetSalaries() {
        // 何かしらのビジネス ロジック
    }

}

このクラスでは、抽象化された IRepository インターフェイスでアクセスするため、モックをインジェクションすることで、データベースに依存しない単体テスト ロジックを実施することができます。

[TestClass()]
public class SalaryServiceTest {

    [TestMethod()]
    public void GetSalariesTest() {
        var data = new [] { new Person() };
        var mock = new Mock<IRepository<Person>>();
        mock.Setup(repos => repos.GetAll())
            .Returns(data);
        var target = new SalaryService(mock.Object);
        var actual = target.GetSalaries();
    }

}

IRepository インターフェイスの必要性

そもそもとして「IRepository インターフェイスは必要か?」という話があります。データ アクセス ロジックをモックしたい、つまり単体テストからデータベースを分離したい理由としては、以下の理由が挙げられると思います。

  • 接続文字列に固有の情報が含まれるため、ビルド サーバーに持っていったときに自動テストが実施できない
  • 連続実行した場合など、データベースのデータによって、テスト結果が左右される可能性がある

SQL Server LocalDB を使えば、単体の mdf ファイルとして動作するので、環境による依存性を考慮する必要がありません。また CI サービスである AppVeyor でもサポートしていますので、ビルドサーバーでも自動テストを通すことができます。

<connectionStrings>
    <add name="DefaultConnection"
         connectionString="Data Source=(localdb)\mssqllocaldb; Initial Catalog=sampledb; AttachDbFilename=|DataDirectory|\sampledb.mdf; Integrated Security=True;"
         providerName="System.Data.SqlClient" />
</connectionStrings>
[ClassInitialize()]
public static void ClassInitialize(TestContext testContext) {
    AppDomain.CurrentDomain.SetData("DataDirectory", testContext.TestDeploymentDir);
}

App.config に DataDirectory を指定し、テスト クラスで単体テストの TestDeploymentDir に書き換えてあげれば、単体テストの実施ごとに異なる mdf が使われます。あとは Code First であれば Database.Create() するだけです。わざわざモックを使わなくても、ほとんどの場合は、こちらのほうが簡単に単体テストを実施できるはずです。

どうしてもデータベースを使いたくない場合でも、Entity Framework の DbContext クラスをモックしてしまうという方法があります。というのも、DbContext クラスはすでに Repository パターンを実装しているからです。

DbContext クラス (System.Data.Entity)

DbContext インスタンスは、データベースに照会してすべての変更をグループ化し、1 つの単位としてストアに書き戻すことができるような、作業単位パターンとリポジトリ パターンの組み合わせを表します。

DbContext クラスと DbSet クラスをモックすればデータベースにアクセスしない単体テストを実施することができます。

[TestClass()]
public class SalaryServiceTest {

    [TestMethod()]
    public void GetSalariesTest() {
        var mockData = new List<Person>() { new Person() };
        var mockDbSet = new Mock<DbSet<Person>>();
        mockDbSet.As<IEnumerable<Person>>()
            .Setup(dbSet => dbSet.GetEnumerator())
            .Returns(mockData.GetEnumerator());
        var mockDbContext = new Mock<DbContext>();
        mockDbContext.Setup(dbContext => dbContext.Set<Person>())
            .Returns(mockDbSet);
        var target = new SalaryService(mockDbContext.Object);
        var actual = target.GetSalaries();
    }

}

それでも IRepository インターフェイスを使う理由

データ アクセス ロジックが必ずしもデータベースであるとは限りません。REST や OData な Web API の可能性もあります。特定のデータ ストアに限定されない場合、IRepository インターフェイスでデータ アクセスを抽象化しておくと、ビジネス ロジックからはデータ ストアの種類を意識する必要なくアクセスができます。

public class OfferRepository : IRepository<Offer> {

    private const string BaseUrl = "http://example.com/Offers";

    public OfferRepository() { }

    public Offer GetOne(int id) {
        var webRequest = WebRequest.Create(BaseUrl + "/" + item.Id);
        webRequest.Method = "GET";
        var webResponse = webRequest.GetResponse();
        using (var stream = webResponse.GetResponseStream()) {
            var serializer = new JsonSerializer();
            using (var reader = new JsonTextReader(new StreamReader(stream))) {
                return serializer.Deserialize<Offer>(stream);
            }
        }
    }

    public IEnumerable<Offer> GetAll() {
        var webRequest = WebRequest.Create(BaseUrl);
        webRequest.Method = "GET";
        var webResponse = webRequest.GetResponse();
        using (var stream = webResponse.GetResponseStream()) {
            var serializer = new JsonSerializer();
            using (var reader = new JsonTextReader(new StreamReader(stream))) {
                return serializer.Deserialize<IEmumerable<Offer>>(stream);
            }
        }
    }

    public void Add(Offer item) {
        var webRequest = WebRequest.Create(BaseUrl);
        webRequest.Method = "POST";
        webRequest.ContentType = "application/json";
        using (var stream = webRequest.GetRequestStream()) {
            var serializer = new JsonSerializer();
            using (var writer = new JsonTextWriter(new StreamWriter(stream))) {
                serializer.Serialize(writer, item);
            }
        }
        webRequest.GetResponse();
    }

    public void Update(Offer item) {
        var webRequest = WebRequest.Create(BaseUrl + "/" + item.Id);
        webRequest.Method = "PUT";
        webRequest.ContentType = "application/json";
        using (var stream = webRequest.GetRequestStream()) {
            var serializer = new JsonSerializer();
            using (var writer = new JsonTextWriter(new StreamWriter(stream))) {
                serializer.Serialize(writer, item);
            }
        }
        webRequest.GetResponse();
    }

    public void Delete(Offer item) {
        var webRequest = WebRequest.Create(BaseUrl + "/" + item.Id);
        webRequest.Method = "DELETE";
        webRequest.GetResponse();
    }

}

Repository パターンを適切に実装するために

汎用の IRepository インターフェイス実装では、クエリ処理が実装されていないため、ビジネス ロジックでクエリを書いてしまいがちです。しかし、これは間違いです。

public class SalaryService {

    private IRepository<Person> personRepository;

    public class SomeService() {
        this.personRepository = new PersonRepository();
    }

    public class SomeService(IRepository<Person> personRepository) {
        this.personRepository = personRepository;
    }

    public decimal GetSalary(string name) {
        // この例のようにビジネス ロジックでクエリを書いたら駄目
        return this.personRepository.GetAll()
            .Where(person => person.Name == name)
            .Select(person => person.Salary)
            .Single();
    }

}

GetAll メソッドは IEnumerable を返しますので、Entity Framework では意図しない SQL が流れてしまう結果になります。

itkaeru.blogspot.jp

かといって、GetAll メソッドを IQueryable にしてしまうと、今度はせっかく分離したデータベースとの依存性が復活してしまいます。
できるだけクエリ処理は Repository の中に閉じ込めておき、汎用の IRepository で定義されているメソッドのうち、必要ないものは実装しないようにする必要があります。

public interface IPersonRepository {

    decimal GetSalary(string name);

}

public class PersonRepository : IRepository<Person>, IPersonRepository {

    private DbContext dbContext;

    public PersonRepository() {
        this.dbContext = new SampleDbContext();
    }

    Person IRepository<Person>.GetOne(int id) {
        throw new NotSupportedException();
    }

    IEnumerable<Person> IRepository<Person>.GetAll() {
        throw new NotSupportedException();
    }

    void IRepository<Person>.Add(Person item) {
        throw new NotSupportedException();
    }

    void IRepository<Person>.Update(Person item) {
        throw new NotSupportedException();
    }

    void IRepository<Person>.Delete(Person item) {
        throw new NotSupportedException();
    }

    public decimal GetSalary(string name) {
        // クエリ処理
        return this.dbContext.Set<Person>()
            .Where(person => person.Name == name)
            .Select(person => person.Salary)
            .Single();
    }

}