apeescape2.com
  • メイン
  • デザイナーライフ
  • アジャイル
  • 技術
  • 収益と成長
バックエンド

ASP.NETCoreを使用したASP.NETWebAPIの構築

前書き

数年前、「Pro ASP.NETWebAPI」という本を手に入れました。この記事は、この本からのアイデアの派生物、小さなCQRS、およびクライアントサーバーシステムの開発に関する私自身の経験です。

この記事では、以下について説明します。

  • .NET Core、EF Core、AutoMapper、およびXUnitを使用してRESTAPIを最初から作成する方法
  • 変更後にAPIが機能することを確認する方法
  • RESTAPIシステムの開発とサポートを可能な限り簡素化する方法

なぜASP.NETCoreなのか?

ASP.NET Coreは、ASP.NET MVC / WebAPIに比べて多くの改善を提供します。まず、これは2つではなく1つのフレームワークになりました。便利で混乱が少ないのでとても気に入っています。次に、追加のライブラリがないロギングコンテナとDIコンテナがあるため、時間を節約でき、最適なライブラリを選択して分析するのではなく、より適切なコードの記述に集中できます。



クエリプロセッサとは何ですか?

クエリプロセッサは、システムの1つのエンティティに関連するすべてのビジネスロジックが1つのサービスにカプセル化され、このエンティティへのアクセスまたはアクションがこのサービスを介して実行される場合のアプローチです。このサービスは通常、{EntityPluralName} QueryProcessorと呼ばれます。必要に応じて、クエリプロセッサには、このエンティティのCRUD(作成、読み取り、更新、削除)メソッドが含まれています。要件によっては、すべてのメソッドが実装されるとは限りません。具体的な例を示すために、ChangePasswordを見てみましょう。クエリプロセッサのメソッドが入力データを必要とする場合は、必要なデータのみを提供する必要があります。通常、メソッドごとに個別のクエリクラスが作成され、単純なケースでは、クエリクラスを再利用することが可能です(ただし望ましくありません)。

私たちの目的

この記事では、認証とアクセス制御の基本設定を含む、小規模なコスト管理システム用のAPIを作成する方法を紹介しますが、認証サブシステムについては説明しません。システムのビジネスロジック全体をモジュラーテストでカバーし、1つのエンティティの例でAPIメソッドごとに少なくとも1つの統合テストを作成します。

開発されたシステムの要件: ユーザーは自分の経費を追加、編集、削除でき、自分の経費のみを表示できます。

このシステムのコード全体は、で入手できます。 Github 。

それでは、小さいながらも非常に便利なシステムの設計を始めましょう。

Layers API

APIレイヤーを示す図。

この図は、システムに4つのレイヤーがあることを示しています。

  • データベース-ここでは、データだけを保存し、ロジックも保存しません。
  • DAL-データにアクセスするために、作業単位パターンを使用し、実装では、コードファーストと移行パターンでORMEFコアを使用します。
  • ビジネスロジック-ビジネスロジックをカプセル化するために、クエリプロセッサを使用します。このレイヤーのみが、ビジネスロジックを処理します。例外は、必須フィールドなどの最も単純な検証であり、APIのフィルターを使用して実行されます。
  • REST API-クライアントがAPIを操作できる実際のインターフェイスは、ASP.NETCoreを介して実装されます。ルート構成は属性によって決定されます。

説明したレイヤーに加えて、いくつかの重要な概念があります。 1つ目は、データモデルの分離です。クライアントデータモデルは、主にRESTAPIレイヤーで使用されます。クエリをドメインモデルに、またはその逆にドメインモデルからクライアントデータモデルに変換しますが、クエリモデルはクエリプロセッサでも使用できます。変換はAutoMapperを使用して行われます。

プロジェクト構造

VS 2017Professionalを使用してプロジェクトを作成しました。私は通常、ソースコードとテストをさまざまなフォルダーで共有します。快適で見栄えがよく、CIのテストは便利に実行されます。マイクロソフトでは、次のようにすることを推奨しているようです。

VS 2017Professionalのフォルダー構造。

プロジェクトの説明:

事業 説明
経費 コントローラのプロジェクト、ドメインモデルとAPIモデル間のマッピング、API構成
Expenses.Api.Common この時点で、フィルターによって特定の方法で解釈され、エラーのある正しいHTTPコードをユーザーに返す例外クラスが収集されます。
Expenses.Api.Models APIモデルのプロジェクト
Expenses.Data.Access 作業単位パターンのインターフェースと実装のためのプロジェクト
Expenses.Data.Model ドメインモデルのプロジェクト
Expenses.Queries クエリプロセッサとクエリ固有のクラスのプロジェクト
Expenses.Security 現在のユーザーのセキュリティコンテキストのインターフェイスと実装のためのプロジェクト

プロジェクト間の参照:

特定のインタラクションをテストしようとした場合、どのようにユーザーインタビューを実施しますか?

プロジェクト間の参照を示す図。

テンプレートから作成された経費:

テンプレートから作成された経費のリスト。

テンプレートごとのsrcフォルダー内の他のプロジェクト:

テンプレートごとのsrcフォルダー内の他のプロジェクトのリスト。

テンプレートごとのtestsフォルダー内のすべてのプロジェクト:

テンプレートごとのtestsフォルダー内のプロジェクトのリスト。

実装

この記事では、実装されていますが、UIに関連する部分については説明しません。

最初のステップは、アセンブリExpenses.Data.Modelにあるデータモデルを開発することでした。

役割間の関係の図

Expenseクラスには次の属性が含まれています。

public class Expense { public int Id { get; set; } public DateTime Date { get; set; } public string Description { get; set; } public decimal Amount { get; set; } public string Comment { get; set; } public int UserId { get; set; } public virtual User User { get; set; } public bool IsDeleted { get; set; } }

このクラスは、IsDeletedによる「ソフト削除」をサポートします。属性であり、将来私たちに役立つ特定のユーザーの1つの費用のすべてのデータが含まれています。

User、Role、およびUserRoleクラスはアクセスサブシステムを参照します。このシステムは今年のシステムのふりをしておらず、このサブシステムの説明はこの記事の目的ではありません。したがって、データモデルと実装の詳細は省略されます。アクセス組織のシステムは、ビジネスロジックを変更することなく、より完璧なシステムに置き換えることができます。

次に、作業単位テンプレートがExpenses.Data.Accessに実装されました。アセンブリ、このプロジェクトの構造が示されています:

Expenses.Data.Accessプロジェクト構造

アセンブリには次のライブラリが必要です。

  • Microsoft.EntityFrameworkCore.SqlServer

EFを実装する必要があります特定のフォルダー内のマッピングを自動的に見つけるコンテキスト:

public class MainDbContext : DbContext { public MainDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { var mappings = MappingsHelper.GetMainMappings(); foreach (var mapping in mappings) { mapping.Visit(modelBuilder); } } }

マッピングはMappingsHelperを介して行われますクラス:

public static class MappingsHelper { public static IEnumerable GetMainMappings() { var assemblyTypes = typeof(UserMap).GetTypeInfo().Assembly.DefinedTypes; var mappings = assemblyTypes // ReSharper disable once AssignNullToNotNullAttribute .Where(t => t.Namespace != null && t.Namespace.Contains(typeof(UserMap).Namespace)) .Where(t => typeof(IMap).GetTypeInfo().IsAssignableFrom(t)); mappings = mappings.Where(x => !x.IsAbstract); return mappings.Select(m => (IMap) Activator.CreateInstance(m.AsType())).ToArray(); } }

クラスへのマッピングはMapsにありますフォルダ、およびExpensesのマッピング:

public class ExpenseMap : IMap { public void Visit(ModelBuilder builder) { builder.Entity() .ToTable('Expenses') .HasKey(x => x.Id); } }

インターフェースIUnitOfWork:

public interface IUnitOfWork : IDisposable { ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Snapshot); void Add(T obj) where T: class ; void Update(T obj) where T : class; void Remove(T obj) where T : class; IQueryable Query() where T : class; void Commit(); Task CommitAsync(); void Attach(T obj) where T : class; }

その実装はEF DbContextのラッパーです。

public class EFUnitOfWork : IUnitOfWork { private DbContext _context; public EFUnitOfWork(DbContext context) { _context = context; } public DbContext Context => _context; public ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Snapshot) { return new DbTransaction(_context.Database.BeginTransaction(isolationLevel)); } public void Add(T obj) where T : class { var set = _context.Set(); set.Add(obj); } public void Update(T obj) where T : class { var set = _context.Set(); set.Attach(obj); _context.Entry(obj).State = EntityState.Modified; } void IUnitOfWork.Remove(T obj) { var set = _context.Set(); set.Remove(obj); } public IQueryable Query() where T : class { return _context.Set(); } public void Commit() { _context.SaveChanges(); } public async Task CommitAsync() { await _context.SaveChangesAsync(); } public void Attach(T newUser) where T : class { var set = _context.Set(); set.Attach(newUser); } public void Dispose() { _context = null; } }

インターフェースITransactionこのアプリケーションに実装されているものは使用されません。

独自のプログラミング言語を作成する方法
public interface ITransaction : IDisposable { void Commit(); void Rollback(); }

その実装は単にEFをラップしますトランザクション:

public class DbTransaction : ITransaction { private readonly IDbContextTransaction _efTransaction; public DbTransaction(IDbContextTransaction efTransaction) { _efTransaction = efTransaction; } public void Commit() { _efTransaction.Commit(); } public void Rollback() { _efTransaction.Rollback(); } public void Dispose() { _efTransaction.Dispose(); } }

また、この段階では、単体テストの場合、ISecurityContext APIの現在のユーザーを定義するインターフェースが必要です(プロジェクトはExpenses.Securityです):

public interface ISecurityContext { User User { get; } bool IsAdministrator { get; } }

次に、クエリプロセッサのインターフェイスと実装を定義する必要があります。これには、コストを処理するためのすべてのビジネスロジック(この場合はIExpensesQueryProcessor)が含まれます。およびExpensesQueryProcessor:

public interface IExpensesQueryProcessor { IQueryable Get(); Expense Get(int id); Task Create(CreateExpenseModel model); Task Update(int id, UpdateExpenseModel model); Task Delete(int id); } public class ExpensesQueryProcessor : IExpensesQueryProcessor { public IQueryable Get() { throw new NotImplementedException(); } public Expense Get(int id) { throw new NotImplementedException(); } public Task Create(CreateExpenseModel model) { throw new NotImplementedException(); } public Task Update(int id, UpdateExpenseModel model) { throw new NotImplementedException(); } public Task Delete(int id) { throw new NotImplementedException(); } }

次のステップは、Expenses.Queries.Testsを構成することです。アセンブリ。次のライブラリをインストールしました。

  • Moq
  • FluentAssertions

次にExpenses.Queries.Testsでアセンブリでは、単体テストのフィクスチャを定義し、単体テストについて説明します。

public class ExpensesQueryProcessorTests { private Mock _uow; private List _expenseList; private IExpensesQueryProcessor _query; private Random _random; private User _currentUser; private Mock _securityContext; public ExpensesQueryProcessorTests() { _random = new Random(); _uow = new Mock(); _expenseList = new List(); _uow.Setup(x => x.Query()).Returns(() => _expenseList.AsQueryable()); _currentUser = new User{Id = _random.Next()}; _securityContext = new Mock(MockBehavior.Strict); _securityContext.Setup(x => x.User).Returns(_currentUser); _securityContext.Setup(x => x.IsAdministrator).Returns(false); _query = new ExpensesQueryProcessor(_uow.Object, _securityContext.Object); } [Fact] public void GetShouldReturnAll() { _expenseList.Add(new Expense{UserId = _currentUser.Id}); var result = _query.Get().ToList(); result.Count.Should().Be(1); } [Fact] public void GetShouldReturnOnlyUserExpenses() { _expenseList.Add(new Expense { UserId = _random.Next() }); _expenseList.Add(new Expense { UserId = _currentUser.Id }); var result = _query.Get().ToList(); result.Count().Should().Be(1); result[0].UserId.Should().Be(_currentUser.Id); } [Fact] public void GetShouldReturnAllExpensesForAdministrator() { _securityContext.Setup(x => x.IsAdministrator).Returns(true); _expenseList.Add(new Expense { UserId = _random.Next() }); _expenseList.Add(new Expense { UserId = _currentUser.Id }); var result = _query.Get(); result.Count().Should().Be(2); } [Fact] public void GetShouldReturnAllExceptDeleted() { _expenseList.Add(new Expense { UserId = _currentUser.Id }); _expenseList.Add(new Expense { UserId = _currentUser.Id, IsDeleted = true}); var result = _query.Get(); result.Count().Should().Be(1); } [Fact] public void GetShouldReturnById() { var expense = new Expense { Id = _random.Next(), UserId = _currentUser.Id }; _expenseList.Add(expense); var result = _query.Get(expense.Id); result.Should().Be(expense); } [Fact] public void GetShouldThrowExceptionIfExpenseOfOtherUser() { var expense = new Expense { Id = _random.Next(), UserId = _random.Next() }; _expenseList.Add(expense); Action get = () => { _query.Get(expense.Id); }; get.ShouldThrow(); } [Fact] public void GetShouldThrowExceptionIfItemIsNotFoundById() { var expense = new Expense { Id = _random.Next(), UserId = _currentUser.Id }; _expenseList.Add(expense); Action get = () => { _query.Get(_random.Next()); }; get.ShouldThrow(); } [Fact] public void GetShouldThrowExceptionIfUserIsDeleted() { var expense = new Expense { Id = _random.Next(), UserId = _currentUser.Id, IsDeleted = true}; _expenseList.Add(expense); Action get = () => { _query.Get(expense.Id); }; get.ShouldThrow(); } [Fact] public async Task CreateShouldSaveNew() { var model = new CreateExpenseModel { Description = _random.Next().ToString(), Amount = _random.Next(), Comment = _random.Next().ToString(), Date = DateTime.Now }; var result = await _query.Create(model); result.Description.Should().Be(model.Description); result.Amount.Should().Be(model.Amount); result.Comment.Should().Be(model.Comment); result.Date.Should().BeCloseTo(model.Date); result.UserId.Should().Be(_currentUser.Id); _uow.Verify(x => x.Add(result)); _uow.Verify(x => x.CommitAsync()); } [Fact] public async Task UpdateShouldUpdateFields() { var user = new Expense {Id = _random.Next(), UserId = _currentUser.Id}; _expenseList.Add(user); var model = new UpdateExpenseModel { Comment = _random.Next().ToString(), Description = _random.Next().ToString(), Amount = _random.Next(), Date = DateTime.Now }; var result = await _query.Update(user.Id, model); result.Should().Be(user); result.Description.Should().Be(model.Description); result.Amount.Should().Be(model.Amount); result.Comment.Should().Be(model.Comment); result.Date.Should().BeCloseTo(model.Date); _uow.Verify(x => x.CommitAsync()); } [Fact] public void UpdateShoudlThrowExceptionIfItemIsNotFound() { Action create = () => { var result = _query.Update(_random.Next(), new UpdateExpenseModel()).Result; }; create.ShouldThrow(); } [Fact] public async Task DeleteShouldMarkAsDeleted() { var user = new Expense() { Id = _random.Next(), UserId = _currentUser.Id}; _expenseList.Add(user); await _query.Delete(user.Id); user.IsDeleted.Should().BeTrue(); _uow.Verify(x => x.CommitAsync()); } [Fact] public async Task DeleteShoudlThrowExceptionIfItemIsNotBelongTheUser() { var expense = new Expense() { Id = _random.Next(), UserId = _random.Next() }; _expenseList.Add(expense); Action execute = () => { _query.Delete(expense.Id).Wait(); }; execute.ShouldThrow(); } [Fact] public void DeleteShoudlThrowExceptionIfItemIsNotFound() { Action execute = () => { _query.Delete(_random.Next()).Wait(); }; execute.ShouldThrow(); }

単体テストについて説明した後、クエリプロセッサの実装について説明します。

public class ExpensesQueryProcessor : IExpensesQueryProcessor { private readonly IUnitOfWork _uow; private readonly ISecurityContext _securityContext; public ExpensesQueryProcessor(IUnitOfWork uow, ISecurityContext securityContext) { _uow = uow; _securityContext = securityContext; } public IQueryable Get() { var query = GetQuery(); return query; } private IQueryable GetQuery() { var q = _uow.Query() .Where(x => !x.IsDeleted); if (!_securityContext.IsAdministrator) { var userId = _securityContext.User.Id; q = q.Where(x => x.UserId == userId); } return q; } public Expense Get(int id) { var user = GetQuery().FirstOrDefault(x => x.Id == id); if (user == null) { throw new NotFoundException('Expense is not found'); } return user; } public async Task Create(CreateExpenseModel model) { var item = new Expense { UserId = _securityContext.User.Id, Amount = model.Amount, Comment = model.Comment, Date = model.Date, Description = model.Description, }; _uow.Add(item); await _uow.CommitAsync(); return item; } public async Task Update(int id, UpdateExpenseModel model) { var expense = GetQuery().FirstOrDefault(x => x.Id == id); if (expense == null) { throw new NotFoundException('Expense is not found'); } expense.Amount = model.Amount; expense.Comment = model.Comment; expense.Description = model.Description; expense.Date = model.Date; await _uow.CommitAsync(); return expense; } public async Task Delete(int id) { var user = GetQuery().FirstOrDefault(u => u.Id == id); if (user == null) { throw new NotFoundException('Expense is not found'); } if (user.IsDeleted) return; user.IsDeleted = true; await _uow.CommitAsync(); } }

ビジネスロジックの準備ができたら、APIコントラクトを決定するためのAPI統合テストの作成を開始します。

最初のステップはプロジェクトを準備することですExpenses.Api.IntegrationTests

  1. nugetパッケージをインストールします。
    • FluentAssertions
    • Moq
    • Microsoft.AspNetCore.TestHost
  2. プロジェクト構造を設定する 経費フォルダ構造
  3. 作成する CollectionDefinition これを利用して、各テスト実行の開始時に作成され、各テスト実行の終了時に破棄されるリソースを決定します。
[CollectionDefinition('ApiCollection')] public class DbCollection : ICollectionFixture { } ~~~ And define our test server and the client to it with the already authenticated user by default:

public class ApiServer:IDisposable {public const string Username =“ admin”; public const string Password =“ admin”;

private IConfigurationRoot _config; public ApiServer() { _config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile('appsettings.json') .Build(); Server = new TestServer(new WebHostBuilder().UseStartup()); Client = GetAuthenticatedClient(Username, Password); } public HttpClient GetAuthenticatedClient(string username, string password) { var client = Server.CreateClient(); var response = client.PostAsync('/api/Login/Authenticate', new JsonContent(new LoginModel {Password = password, Username = username})).Result; response.EnsureSuccessStatusCode(); var data = JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().Result); client.DefaultRequestHeaders.Add('Authorization', 'Bearer ' + data.Token); return client; } public HttpClient Client { get; private set; } public TestServer Server { get; private set; } public void Dispose() { if (Client != null) { Client.Dispose(); Client = null; } if (Server != null) { Server.Dispose(); Server = null; } } } ~~~

HTTPでの作業の便宜のために統合テストでのリクエスト、私はヘルパーを書きました:

public class HttpClientWrapper { private readonly HttpClient _client; public HttpClientWrapper(HttpClient client) { _client = client; } public HttpClient Client => _client; public async Task PostAsync(string url, object body) { var response = await _client.PostAsync(url, new JsonContent(body)); response.EnsureSuccessStatusCode(); var respnoseText = await response.Content.ReadAsStringAsync(); var data = JsonConvert.DeserializeObject(respnoseText); return data; } public async Task PostAsync(string url, object body) { var response = await _client.PostAsync(url, new JsonContent(body)); response.EnsureSuccessStatusCode(); } public async Task PutAsync(string url, object body) { var response = await _client.PutAsync(url, new JsonContent(body)); response.EnsureSuccessStatusCode(); var respnoseText = await response.Content.ReadAsStringAsync(); var data = JsonConvert.DeserializeObject(respnoseText); return data; } }

この段階で、エンティティごとにREST APIコントラクトを定義する必要があります。これは、RESTAPIの費用として記述します。

URL 方法 ボディタイプ 結果の種類 説明
費用 取得する - DataResult クエリパラメータ「コマンド」でフィルタとソータを使用する可能性のあるすべての費用を取得します
経費/ {id} 取得する - ExpenseModel IDで経費を取得する
経費 役職 CreateExpenseModel ExpenseModel 新しい経費記録を作成する
経費/ {id} プット UpdateExpenseModel ExpenseModel 既存の経費を更新する

コストのリストを要求するときは、を使用してさまざまなフィルタリングおよび並べ替えコマンドを適用できます。 AutoQueryableライブラリ 。フィルタリングと並べ替えを使用したクエリの例:

/expenses?commands=take=25%26amount%3E=12%26orderbydesc=date

デコードコマンドのパラメータ値はtake=25&amount>=12&orderbydesc=dateです。したがって、クエリ内のページング、フィルタリング、および並べ替えの部分を見つけることができます。すべてのクエリオプションはOData構文と非常に似ていますが、残念ながら、ODataはまだ.NET Coreに対応していないため、別の便利なライブラリを使用しています。

下部には、このAPIで使用されるすべてのモデルが表示されます。

コールバックは関数ノードjsではありません
public class DataResult { public T[] Data { get; set; } public int Total { get; set; } } public class ExpenseModel { public int Id { get; set; } public DateTime Date { get; set; } public string Description { get; set; } public decimal Amount { get; set; } public string Comment { get; set; } public int UserId { get; set; } public string Username { get; set; } } public class CreateExpenseModel { [Required] public DateTime Date { get; set; } [Required] public string Description { get; set; } [Required] [Range(0.01, int.MaxValue)] public decimal Amount { get; set; } [Required] public string Comment { get; set; } } public class UpdateExpenseModel { [Required] public DateTime Date { get; set; } [Required] public string Description { get; set; } [Required] [Range(0.01, int.MaxValue)] public decimal Amount { get; set; } [Required] public string Comment { get; set; } }

モデルCreateExpenseModelおよびUpdateExpenseModelデータ注釈属性を使用して、属性を介してRESTAPIレベルで簡単なチェックを実行します。

次に、HTTPごとにメソッドでは、プロジェクトに個別のフォルダーが作成され、その中のファイルは各HTTPのフィクスチャーによって作成されます。リソースでサポートされているメソッド:

パッケージマネージャーコンソール

経費のリストを取得するための統合テストの実装:

[Collection('ApiCollection')] public class GetListShould { private readonly ApiServer _server; private readonly HttpClient _client; public GetListShould(ApiServer server) { _server = server; _client = server.Client; } public static async Task Get(HttpClient client) { var response = await client.GetAsync($'api/Expenses'); response.EnsureSuccessStatusCode(); var responseText = await response.Content.ReadAsStringAsync(); var items = JsonConvert.DeserializeObject(responseText); return items; } [Fact] public async Task ReturnAnyList() { var items = await Get(_client); items.Should().NotBeNull(); } }

idで経費データを取得するための統合テストの実装:

[Collection('ApiCollection')] public class GetItemShould { private readonly ApiServer _server; private readonly HttpClient _client; private Random _random; public GetItemShould(ApiServer server) { _server = server; _client = _server.Client; _random = new Random(); } [Fact] public async Task ReturnItemById() { var item = await new PostShould(_server).CreateNew(); var result = await GetById(_client, item.Id); result.Should().NotBeNull(); } public static async Task GetById(HttpClient client, int id) { var response = await client.GetAsync(new Uri($'api/Expenses/{id}', UriKind.Relative)); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(result); } [Fact] public async Task ShouldReturn404StatusIfNotFound() { var response = await _client.GetAsync(new Uri($'api/Expenses/-1', UriKind.Relative)); response.StatusCode.ShouldBeEquivalentTo(HttpStatusCode.NotFound); } }

経費を作成するための統合テストの実装:

[Collection('ApiCollection')] public class PostShould { private readonly ApiServer _server; private readonly HttpClientWrapper _client; private Random _random; public PostShould(ApiServer server) { _server = server; _client = new HttpClientWrapper(_server.Client); _random = new Random(); } [Fact] public async Task CreateNew() { var requestItem = new CreateExpenseModel() { Amount = _random.Next(), Comment = _random.Next().ToString(), Date = DateTime.Now.AddMinutes(-15), Description = _random.Next().ToString() }; var createdItem = await _client.PostAsync('api/Expenses', requestItem); createdItem.Id.Should().BeGreaterThan(0); createdItem.Amount.Should().Be(requestItem.Amount); createdItem.Comment.Should().Be(requestItem.Comment); createdItem.Date.Should().Be(requestItem.Date); createdItem.Description.Should().Be(requestItem.Description); createdItem.Username.Should().Be('admin admin'); return createdItem; } }

経費を変更するための統合テストの実装:

[Collection('ApiCollection')] public class PutShould { private readonly ApiServer _server; private readonly HttpClientWrapper _client; private readonly Random _random; public PutShould(ApiServer server) { _server = server; _client = new HttpClientWrapper(_server.Client); _random = new Random(); } [Fact] public async Task UpdateExistingItem() { var item = await new PostShould(_server).CreateNew(); var requestItem = new UpdateExpenseModel { Date = DateTime.Now, Description = _random.Next().ToString(), Amount = _random.Next(), Comment = _random.Next().ToString() }; await _client.PutAsync($'api/Expenses/{item.Id}', requestItem); var updatedItem = await GetItemShould.GetById(_client.Client, item.Id); updatedItem.Date.Should().Be(requestItem.Date); updatedItem.Description.Should().Be(requestItem.Description); updatedItem.Amount.Should().Be(requestItem.Amount); updatedItem.Comment.Should().Contain(requestItem.Comment); } }

経費を削減するための統合テストの実装:

[Collection('ApiCollection')] public class DeleteShould { private readonly ApiServer _server; private readonly HttpClient _client; public DeleteShould(ApiServer server) { _server = server; _client = server.Client; } [Fact] public async Task DeleteExistingItem() { var item = await new PostShould(_server).CreateNew(); var response = await _client.DeleteAsync(new Uri($'api/Expenses/{item.Id}', UriKind.Relative)); response.EnsureSuccessStatusCode(); } }

この時点で、REST APIコントラクトを完全に定義したので、ASP.NETCoreに基づいて実装を開始できます。

APIの実装

プロジェクト経費を準備します。このために、次のライブラリをインストールする必要があります。

  • AutoMapper
  • AutoQueryable.AspNetCore.Filter
  • Microsoft.ApplicationInsights.AspNetCore
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.SqlServer.Design
  • Microsoft.EntityFrameworkCore.Tools
  • Swashbuckle.AspNetCore

その後、パッケージマネージャーコンソールを開き、Expenses.Data.Accessに切り替えて、データベースの初期移行の作成を開始する必要があります。プロジェクト(EFコンテキストがそこにあるため)およびAdd-Migration InitialCreateの実行コマンド:

APIドキュメント

次のステップでは、事前に構成ファイルappsettings.jsonを準備します。 準備後もプロジェクトにコピーする必要がありますExpenses.Api.IntegrationTestsそこから、テストインスタンスAPIを実行するためです。

{ 'Logging': { 'IncludeScopes': false, 'LogLevel': { 'Default': 'Debug', 'System': 'Information', 'Microsoft': 'Information' } }, 'Data': { 'main': 'Data Source=.; Initial Catalog=expenses.main; Integrated Security=true; Max Pool Size=1000; Min Pool Size=12; Pooling=True;' }, 'ApplicationInsights': { 'InstrumentationKey': 'Your ApplicationInsights key' } }

ロギングセクションは自動的に作成されます。 Dataを追加しましたデータベースと私のApplicationInsightsへの接続文字列を格納するセクションキー。

アプリケーション構成

アプリケーションで利用可能なさまざまなサービスを構成する必要があります。

ApplicationInsightsをオンにする: services.AddApplicationInsightsTelemetry(Configuration);

電話でサービスを登録します。 ContainerSetup.Setup(services, Configuration);

ContainerSetupは作成されたクラスであるため、すべてのサービス登録をStartupに保存する必要はありません。クラス。このクラスは、ExpensesプロジェクトのIoCフォルダーにあります。

public static class ContainerSetup { public static void Setup(IServiceCollection services, IConfigurationRoot configuration) { AddUow(services, configuration); AddQueries(services); ConfigureAutoMapper(services); ConfigureAuth(services); } private static void ConfigureAuth(IServiceCollection services) { services.AddSingleton(); services.AddScoped(); services.AddScoped(); } private static void ConfigureAutoMapper(IServiceCollection services) { var mapperConfig = AutoMapperConfigurator.Configure(); var mapper = mapperConfig.CreateMapper(); services.AddSingleton(x => mapper); services.AddTransient(); } private static void AddUow(IServiceCollection services, IConfigurationRoot configuration) { var connectionString = configuration['Data:main']; services.AddEntityFrameworkSqlServer(); services.AddDbContext(options => options.UseSqlServer(connectionString)); services.AddScoped(ctx => new EFUnitOfWork(ctx.GetRequiredService())); services.AddScoped(); services.AddScoped(); } private static void AddQueries(IServiceCollection services) { var exampleProcessorType = typeof(UsersQueryProcessor); var types = (from t in exampleProcessorType.GetTypeInfo().Assembly.GetTypes() where t.Namespace == exampleProcessorType.Namespace && t.GetTypeInfo().IsClass && t.GetTypeInfo().GetCustomAttribute() == null select t).ToArray(); foreach (var type in types) { var interfaceQ = type.GetTypeInfo().GetInterfaces().First(); services.AddScoped(interfaceQ, type); } } }

このクラスのほとんどすべてのコードはそれ自体を物語っていますが、ConfigureAutoMapperに入りたいと思います。もう少し方法。

private static void ConfigureAutoMapper(IServiceCollection services) { var mapperConfig = AutoMapperConfigurator.Configure(); var mapper = mapperConfig.CreateMapper(); services.AddSingleton(x => mapper); services.AddTransient(); }

このメソッドは、ヘルパークラスを使用してモデルとエンティティ間のすべてのマッピングを検索し、その逆も同様です。IMapperを取得します。 IAutoMapperを作成するためのインターフェースコントローラで使用されるラッパー。このラッパーには特別なことは何もありません。AutoMapperへの便利なインターフェイスを提供するだけです。メソッド。

public class AutoMapperAdapter : IAutoMapper { private readonly IMapper _mapper; public AutoMapperAdapter(IMapper mapper) { _mapper = mapper; } public IConfigurationProvider Configuration => _mapper.ConfigurationProvider; public T Map(object objectToMap) { return _mapper.Map(objectToMap); } public TResult[] Map(IEnumerable sourceQuery) { return sourceQuery.Select(x => _mapper.Map(x)).ToArray(); } public IQueryable Map(IQueryable sourceQuery) { return sourceQuery.ProjectTo(_mapper.ConfigurationProvider); } public void Map(TSource source, TDestination destination) { _mapper.Map(source, destination); } }

AutoMapperを構成するには、ヘルパークラスを使用します。ヘルパークラスのタスクは、特定の名前空間クラスのマッピングを検索することです。すべてのマッピングは、Expenses / Mapsフォルダーにあります。

public static class AutoMapperConfigurator { private static readonly object Lock = new object(); private static MapperConfiguration _configuration; public static MapperConfiguration Configure() { lock (Lock) { if (_configuration != null) return _configuration; var thisType = typeof(AutoMapperConfigurator); var configInterfaceType = typeof(IAutoMapperTypeConfigurator); var configurators = thisType.GetTypeInfo().Assembly.GetTypes() .Where(x => !string.IsNullOrWhiteSpace(x.Namespace)) // ReSharper disable once AssignNullToNotNullAttribute .Where(x => x.Namespace.Contains(thisType.Namespace)) .Where(x => x.GetTypeInfo().GetInterface(configInterfaceType.Name) != null) .Select(x => (IAutoMapperTypeConfigurator)Activator.CreateInstance(x)) .ToArray(); void AggregatedConfigurator(IMapperConfigurationExpression config) { foreach (var configurator in configurators) { configurator.Configure(config); } } _configuration = new MapperConfiguration(AggregatedConfigurator); return _configuration; } } }

すべてのマッピングは、特定のインターフェースを実装する必要があります。

public interface IAutoMapperTypeConfigurator { void Configure(IMapperConfigurationExpression configuration); }

エンティティからモデルへのマッピングの例:

ソフトウェア詳細設計ドキュメントの例
public class ExpenseMap : IAutoMapperTypeConfigurator { public void Configure(IMapperConfigurationExpression configuration) { var map = configuration.CreateMap(); map.ForMember(x => x.Username, x => x.MapFrom(y => y.User.FirstName + ' ' + y.User.LastName)); } }

また、Startup.ConfigureServicesでメソッド、JWTベアラートークンによる認証が構成されています:

services.AddAuthorization(auth => { auth.AddPolicy('Bearer', new AuthorizationPolicyBuilder() .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme) .RequireAuthenticatedUser().Build()); });

そして、サービスはISecurityContextの実装を登録しました。これは、現在のユーザーを判別するために実際に使用されます。

public class SecurityContext : ISecurityContext { private readonly IHttpContextAccessor _contextAccessor; private readonly IUnitOfWork _uow; private User _user; public SecurityContext(IHttpContextAccessor contextAccessor, IUnitOfWork uow) { _contextAccessor = contextAccessor; _uow = uow; } public User User { get { if (_user != null) return _user; var username = _contextAccessor.HttpContext.User.Identity.Name; _user = _uow.Query() .Where(x => x.Username == username) .Include(x => x.Roles) .ThenInclude(x => x.Role) .FirstOrDefault(); if (_user == null) { throw new UnauthorizedAccessException('User is not found'); } return _user; } } public bool IsAdministrator { get { return User.Roles.Any(x => x.Role.Name == Roles.Administrator); } } }

また、カスタムエラーフィルターを使用して例外を正しいエラーコードに変換するために、デフォルトのMVC登録を少し変更しました。

services.AddMvc(options => { options.Filters.Add(new ApiExceptionFilter()); });

ApiExceptionFilterの実装フィルタ:

public class ApiExceptionFilter : ExceptionFilterAttribute { public override void OnException(ExceptionContext context) { if (context.Exception is NotFoundException) { // handle explicit 'known' API errors var ex = context.Exception as NotFoundException; context.Exception = null; context.Result = new JsonResult(ex.Message); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; } else if (context.Exception is BadRequestException) { // handle explicit 'known' API errors var ex = context.Exception as BadRequestException; context.Exception = null; context.Result = new JsonResult(ex.Message); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; } else if (context.Exception is UnauthorizedAccessException) { context.Result = new JsonResult(context.Exception.Message); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; } else if (context.Exception is ForbiddenException) { context.Result = new JsonResult(context.Exception.Message); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } base.OnException(context); } }

他のユーザー向けの優れたAPIの説明を取得するには、Swaggerを忘れないことが重要です。 https://www.toptal.com/api :

services.AddSwaggerGen(c => { c.SwaggerDoc('v1', new Info {Title = 'Expenses', Version = 'v1'}); c.OperationFilter(); });

Startup.ConfigureメソッドはInitDatabaseへの呼び出しを追加します最後の移行までデータベースを自動的に移行するメソッド:

private void InitDatabase(IApplicationBuilder app) { using (var serviceScope = app.ApplicationServices.GetRequiredService().CreateScope()) { var context = serviceScope.ServiceProvider.GetService(); context.Database.Migrate(); } }

Swaggerアプリケーションが開発環境で実行され、それにアクセスするために認証を必要としない場合にのみオンになります。

app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint('/swagger/v1/swagger.json', 'My API V1'); });

次に、認証を接続します(詳細はリポジトリにあります):

転換社債はどのように機能しますか

ConfigureAuthentication(app);

この時点で、統合テストを実行して、すべてがコンパイルされていることを確認できますが、何も機能せず、コントローラーExpensesControllerに移動します。

注:すべてのコントローラーはExpenses / Serverフォルダーにあり、条件付きでControllersとRestApiの2つのフォルダーに分割されています。フォルダー内のコントローラーは、古い正常なMVCでコントローラーとして機能するコントローラーです。つまり、マークアップを返し、RestApiではRESTコントローラーです。

Expenses / Server / RestApi / ExpensesControllerクラスを作成し、それをControllerクラスから継承する必要があります。

public class ExpensesController : Controller { }

次に、~ / api / Expensesのルーティングを構成しますクラスを属性[Route ('api / [controller]')]でマークして入力します。

ビジネスロジックとマッパーにアクセスするには、次のサービスを挿入する必要があります。

private readonly IExpensesQueryProcessor _query; private readonly IAutoMapper _mapper; public ExpensesController(IExpensesQueryProcessor query, IAutoMapper mapper) { _query = query; _mapper = mapper; }

この段階で、メソッドの実装を開始できます。最初の方法は、経費のリストを取得することです。

[HttpGet] [QueryableResult] public IQueryable Get() { var result = _query.Get(); var models = _mapper.Map(result); return models; }

メソッドの実装は非常に簡単で、IQueryable にマップされているデータベースへのクエリを取得します。 ExpensesQueryProcessorから、結果として返されます。

ここでのカスタム属性はQueryableResultで、AutoQueryableを使用します。サーバー側でページング、フィルタリング、および並べ替えを処理するライブラリ。属性はフォルダーExpenses/Filtersにあります。その結果、このフィルターはタイプDataResult のデータを返します。 APIクライアントに。

public class QueryableResult : ActionFilterAttribute { public override void OnActionExecuted(ActionExecutedContext context) { if (context.Exception != null) return; dynamic query = ((ObjectResult)context.Result).Value; if (query == null) throw new Exception('Unable to retreive value of IQueryable from context result.'); Type entityType = query.GetType().GenericTypeArguments[0]; var commands = context.HttpContext.Request.Query.ContainsKey('commands') ? context.HttpContext.Request.Query['commands'] : new StringValues(); var data = QueryableHelper.GetAutoQuery(commands, entityType, query, new AutoQueryableProfile {UnselectableProperties = new string[0]}); var total = System.Linq.Queryable.Count(query); context.Result = new OkObjectResult(new DataResult{Data = data, Total = total}); } }

また、Postメソッドの実装を見て、フローを作成しましょう。

[HttpPost] [ValidateModel] public async Task Post([FromBody]CreateExpenseModel requestModel) { var item = await _query.Create(requestModel); var model = _mapper.Map(item); return model; }

ここでは、属性ValidateModelに注意する必要があります。この属性は、データ注釈属性に従って入力データの単純な検証を実行します。これは、組み込みのMVCチェックによって実行されます。

public class ValidateModelAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { if (!context.ModelState.IsValid) { context.Result = new BadRequestObjectResult(context.ModelState); } } }

ExpensesControllerの完全なコード:

[Route('api/[controller]')] public class ExpensesController : Controller { private readonly IExpensesQueryProcessor _query; private readonly IAutoMapper _mapper; public ExpensesController(IExpensesQueryProcessor query, IAutoMapper mapper) { _query = query; _mapper = mapper; } [HttpGet] [QueryableResult] public IQueryable Get() { var result = _query.Get(); var models = _mapper.Map(result); return models; } [HttpGet('{id}')] public ExpenseModel Get(int id) { var item = _query.Get(id); var model = _mapper.Map(item); return model; } [HttpPost] [ValidateModel] public async Task Post([FromBody]CreateExpenseModel requestModel) { var item = await _query.Create(requestModel); var model = _mapper.Map(item); return model; } [HttpPut('{id}')] [ValidateModel] public async Task Put(int id, [FromBody]UpdateExpenseModel requestModel) { var item = await _query.Update(id, requestModel); var model = _mapper.Map(item); return model; } [HttpDelete('{id}')] public async Task Delete(int id) { await _query.Delete(id); } }

結論

問題から始めましょう。主な問題は、ソリューションの初期構成の複雑さとアプリケーションのレイヤーの理解ですが、アプリケーションの複雑さが増すにつれて、システムの複雑さはほとんど変わりません。これは大きな問題です。さらに、そのようなシステムを伴う場合。また、ビジネスロジックの統合テストのセットと単体テストの完全なセットが含まれるAPIを用意することが非常に重要です。ビジネスロジックは、使用されているサーバーテクノロジから完全に分離されており、完全にテストできます。このソリューションは、複雑なAPIと複雑なビジネスロジックを備えたシステムに最適です。

APIを使用するAngularアプリの構築を検討している場合は、チェックアウトしてください Angular5とASP.NETCore 仲間のApeeScapeerPabloAlbellaによる。

基本を理解する

データ転送オブジェクトとは何ですか?

データ転送オブジェクト(DTO)は、データベース内の1つ以上のオブジェクトの表現です。単一のデータベースエンティティは、任意の数のDTOの有無にかかわらず表すことができます

Web APIとは何ですか?

Web APIは、データベースへのシステムのビジネスロジックアクセスへのインターフェイスを提供し、基盤となるロジックはAPIにカプセル化されます。

REST APIとは何ですか?

クライアントがWebAPIを操作できる実際のインターフェース。 HTTP(s)プロトコル上でのみ機能します。

ユニットテストとは何ですか?

単体テストは、コードの小さな単位をカバーする、小さな、特定の、非常に高速なテストのセットです。クラス。統合テストとは異なり、単体テストでは、ユニットのすべての側面がアプリケーション全体の他のコンポーネントから分離してテストされることが保証されます。

Web API統合テストとは何ですか?

統合テストは、特定のAPIエンドポイントに対する一連のテストです。単体テストとは異なり、統合テストでは、APIを強化するすべてのコードユニットが期待どおりに機能することを確認します。これらのテストは、単体テストよりも遅い場合があります。

ASP.NET Coreとは何ですか?

ASP.NET Coreは、ASP.NET4.xの書き直しであり次世代です。クロスプラットフォームであり、Windows、Linux、およびDockerコンテナーと互換性があります。

JWT Bearerトークンとは何ですか?

JWT(JSON Web Token)ベアラートークンは、ステートレスで署名されたJSONオブジェクトであり、APIへのアクセスを提供するために最新のWebおよびモバイルアプリケーションで広く使用されています。これらのトークンには独自のクレームが含まれており、署名が有効である限り受け入れられます。

Swaggerとは何ですか?

Swaggerは、RESTAPIを文書化するために使用されるライブラリです。ドキュメント自体を使用して、さまざまなプラットフォーム用のAPIのクライアントを自動的に生成することもできます。

デジタル企業と無形資産の評価

財務プロセス

デジタル企業と無形資産の評価
UXにおける色の役割

UXにおける色の役割

Uxデザイン

人気の投稿
Ember.js開発者が犯す8つの最も一般的な間違い
Ember.js開発者が犯す8つの最も一般的な間違い
なぜ私はNode.jsを使うのでしょうか?ケースバイケースのチュートリアル
なぜ私はNode.jsを使うのでしょうか?ケースバイケースのチュートリアル
強化学習の詳細
強化学習の詳細
成長の成長:このオープンソースコードを使用して独自のコホート分析を実行します
成長の成長:このオープンソースコードを使用して独自のコホート分析を実行します
書体を選択する際のヒントと考慮事項(インフォグラフィック付き)
書体を選択する際のヒントと考慮事項(インフォグラフィック付き)
 
Android TVの開発–大画面が登場します、準備をしてください!
Android TVの開発–大画面が登場します、準備をしてください!
更新可能なD3.jsチャートに向けて
更新可能なD3.jsチャートに向けて
開発者向けのiOS9スポットライト検索の謎を解き明かす
開発者向けのiOS9スポットライト検索の謎を解き明かす
第一印象–UXのオンボーディングガイド
第一印象–UXのオンボーディングガイド
共謀:iOSのMultipeerConnectivityを使用した近くのデバイスネットワーキング
共謀:iOSのMultipeerConnectivityを使用した近くのデバイスネットワーキング
人気の投稿
  • c ++どこから始めるか
  • anglejsでウェブサイトを作成する方法
  • 投資契約フォーマット
  • 2016年の才能を求める音楽投資家
  • Javaのユニットテストケース
  • スクリプト言語の作成方法
カテゴリー
エンジニアリング管理 製品の担当者とチーム プロジェクト管理 モバイルデザイン プロセスとツール デザイナーライフ 投資家と資金調達 計画と予測 人とチーム 技術

© 2021 | 全著作権所有

apeescape2.com