Asp.net Core 2.1 Web Api Upload File From Webapi

Introduction

Several years ago, I got the "Pro ASP.Cyberspace Web API" book. This article is the adjunct of ideas from this book, a trivial CQRS, and my ain experience developing client-server systems.

In this commodity, I'll be roofing:

  • How to create a REST API from scratch using .NET Core, EF Core, AutoMapper, and XUnit
  • How to exist sure that the API works afterward changes
  • How to simplify the development and support of the REST API organisation as much every bit possible

Why ASP.NET Core?

ASP.NET Cadre provides many improvements over the ASP.Net MVC/Web API. Firstly, it is now one framework and not 2. I really like it because it is convenient and there is less confusion. Secondly, we take logging and DI containers without any additional libraries, which saves me time and allows me to concentrate on writing better code instead of choosing and analyzing the best libraries.

What Are Query Processors?

A query processor is an approach when all concern logic relating to one entity of the system is encapsulated in 1 service and any admission or deportment with this entity are performed through this service. This service is usually called {EntityPluralName}QueryProcessor. If necessary, a query processor includes Grime (create, read, update, delete) methods for this entity. Depending on the requirements, not all methods may be implemented. To requite a specific example, let's accept a look at ChangePassword. If the method of a query processor requires input data, and then but the required information should be provided. Normally, for each method, a separate query form is created, and in simple cases, it is possible (only non desirable) to reuse the query class.

Our Aim

In this article, I'll show you how to make an API for a small toll management arrangement, including basic settings for authentication and access command, only I volition not go into the authentication subsystem. I will comprehend the whole business logic of the system with modular tests and create at to the lowest degree one integration exam for each API method on an example of one entity.

Requirements for the developed organisation: The user tin can add, edit, delete his expenses and can run into but their expenses.

The unabridged lawmaking of this system is available at on Github.

Then, let'southward start designing our small only very useful system.

API Layers

A diagram showing API layers.

The diagram shows that the system will take 4 layers:

  • Database - Hither we store data and nothing more, no logic.
  • DAL - To access the information, we use the Unit of measurement of Work pattern and, in the implementation, we utilise the ORM EF Core with lawmaking first and migration patterns.
  • Business logic - to encapsulate concern logic, we use query processors, only this layer processes business logic. The exception is the simplest validation such as mandatory fields, which volition exist executed by means of filters in the API.
  • Residuum API - The bodily interface through which clients tin can work with our API will be implemented through ASP.Net Core. Route configurations are determined by attributes.

In add-on to the described layers, we have several important concepts. The first is the separation of data models. The client data model is mainly used in the Remainder API layer. It converts queries to domain models and vice versa from a domain model to a client data model, but query models can also be used in query processors. The conversion is done using AutoMapper.

Project Construction

I used VS 2017 Professional to create the projection. I usually share the source code and tests on unlike folders. Information technology's comfortable, it looks good, the tests in CI run conveniently, and it seems that Microsoft recommends doing information technology this way:

Folder structure in VS 2017 Professional.

Project Description:

Project Description
Expenses Projection for controllers, mapping between domain model and API model, API configuration
Expenses.Api.Common At this bespeak, there are collected exception classes that are interpreted in a certain way by filters to return correct HTTP codes with errors to the user
Expenses.Api.Models Project for API models
Expenses.Data.Access Project for interfaces and implementation of the Unit of Work pattern
Expenses.Data.Model Project for domain model
Expenses.Queries Project for query processors and query-specific classes
Expenses.Security Project for the interface and implementation of the electric current user's security context

References between projects:

Diagram showing references between projects.

Expenses created from the template:

List of expenses created from the template.

Other projects in the src folder by template:

List of other projects in the src folder by template.

All projects in the tests folder by template:

List of projects in the tests folder by template.

Implementation

This article will not describe the part associated with the UI, though it is implemented.

The first stride was to develop a information model that is located in the assembly Expenses.Data.Model:

Diagram of the relationship between roles

The Expense class contains the following attributes:

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

This class supports "soft deletion" by means of the IsDeleted attribute and contains all the data for ane expense of a particular user that will exist useful to usa in the time to come.

The User, Role, and UserRole classes refer to the access subsystem; this system does not pretend to exist the system of the year and the description of this subsystem is not the purpose of this commodity; therefore, the data model and some details of the implementation will be omitted. The system of access organization can be replaced by a more perfect i without irresolute the business logic.

Next, the Unit of Piece of work template was implemented in the Expenses.Data.Access assembly, the structure of this project is shown:

Expenses.Data.Access project structure

The post-obit libraries are required for assembly:

  • Microsoft.EntityFrameworkCore.SqlServer

It is necessary to implement an EF context that will automatically find the mappings in a specific folder:

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

Mapping is washed through the MappingsHelper class:

          public static course MappingsHelper     {         public static IEnumerable<IMap> GetMainMappings()         {             var assemblyTypes = typeof(UserMap).GetTypeInfo().Associates.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(ten => !x.IsAbstract);             return mappings.Select(m => (IMap) Activator.CreateInstance(k.AsType())).ToArray();         } }                  

The mapping to the classes is in the Maps folder, and mapping for Expenses:

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

Interface IUnitOfWork:

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

Its implementation is a wrapper for 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 together<T>(T obj)             where T : class         {             var set = _context.Set<T>();             set up.Add together(obj);         }           public void Update<T>(T obj)             where T : class         {             var set = _context.Set<T>();             set.Adhere(obj);             _context.Entry(obj).State = EntityState.Modified;         }           void IUnitOfWork.Remove<T>(T obj)         {             var gear up = _context.Prepare<T>();             ready.Remove(obj);         }           public IQueryable<T> Query<T>()             where T : grade         {             return _context.Ready<T>();         }           public void Commit()         {             _context.SaveChanges();         }           public async Chore CommitAsync()         {             await _context.SaveChangesAsync();         }           public void Attach<T>(T newUser) where T : class         {             var set = _context.Gear up<T>();             set.Attach(newUser);         }           public void Dispose()         {             _context = zero;         } }                  

The interface ITransaction implemented in this application will non be used:

          public interface ITransaction : IDisposable     {         void Commit();         void Rollback();     }                  

Its implementation simply wraps the EF transaction:

          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();         } }                  

Also at this stage, for the unit tests, the ISecurityContext interface is needed, which defines the electric current user of the API (the projection is Expenses.Security):

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

Next, you lot need to ascertain the interface and implementation of the query processor, which will contain all the business organization logic for working with costs—in our case, IExpensesQueryProcessor and ExpensesQueryProcessor:

          public interface IExpensesQueryProcessor {         IQueryable<Expense> Get();         Expense Become(int id);         Task<Expense> Create(CreateExpenseModel model);         Chore<Expense> Update(int id, UpdateExpenseModel model);         Task Delete(int id); }  public form ExpensesQueryProcessor : IExpensesQueryProcessor     {         public IQueryable<Expense> Get()         {             throw new NotImplementedException();         }           public Expense Go(int id)         {             throw new NotImplementedException();         }           public Task<Expense> Create(CreateExpenseModel model)         {             throw new NotImplementedException();         }           public Task<Expense> Update(int id, UpdateExpenseModel model)         {             throw new NotImplementedException();         }           public Task Delete(int id)         {             throw new NotImplementedException();         } }                  

The next step is to configure the Expenses.Queries.Tests assembly. I installed the following libraries:

  • Moq
  • FluentAssertions

Then in the Expenses.Queries.Tests assembly, we define the fixture for unit tests and describe our unit tests:

          public class ExpensesQueryProcessorTests {         private Mock<IUnitOfWork> _uow;         private Listing<Expense> _expenseList;         private IExpensesQueryProcessor _query;         private Random _random;         private User _currentUser;         individual Mock<ISecurityContext> _securityContext;           public ExpensesQueryProcessorTests()         {             _random = new Random();             _uow = new Mock<IUnitOfWork>();               _expenseList = new Listing<Expense>();             _uow.Setup(10 => x.Query<Expense>()).Returns(() => _expenseList.AsQueryable());               _currentUser = new User{Id = _random.Next()};             _securityContext = new Mock<ISecurityContext>(MockBehavior.Strict);             _securityContext.Setup(ten => ten.User).Returns(_currentUser);             _securityContext.Setup(10 => x.IsAdministrator).Returns(faux);               _query = new ExpensesQueryProcessor(_uow.Object, _securityContext.Object);         }           [Fact]         public void GetShouldReturnAll()         {             _expenseList.Add(new Expense{UserId = _currentUser.Id});               var result = _query.Go().ToList();             issue.Count.Should().Be(one);         }           [Fact]         public void GetShouldReturnOnlyUserExpenses()         {             _expenseList.Add(new Expense { UserId = _random.Adjacent() });             _expenseList.Add together(new Expense { UserId = _currentUser.Id });               var result = _query.Get().ToList();             outcome.Count().Should().Exist(i);             consequence[0].UserId.Should().Exist(_currentUser.Id);         }           [Fact]         public void GetShouldReturnAllExpensesForAdministrator()         {             _securityContext.Setup(x => ten.IsAdministrator).Returns(truthful);               _expenseList.Add(new Expense { UserId = _random.Next() });             _expenseList.Add(new Expense { UserId = _currentUser.Id });               var upshot = _query.Go();             effect.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();             outcome.Count().Should().Exist(ane);         }           [Fact]         public void GetShouldReturnById()         {             var expense = new Expense { Id = _random.Next(), UserId = _currentUser.Id };             _expenseList.Add(expense);               var result = _query.Go(expense.Id);             effect.Should().Be(expense);         }           [Fact]         public void GetShouldThrowExceptionIfExpenseOfOtherUser()         {             var expense = new Expense { Id = _random.Next(), UserId = _random.Adjacent() };             _expenseList.Add(expense);               Activity become = () =>             {                 _query.Get(expense.Id);             };               get.ShouldThrow<NotFoundException>();         }           [Fact]         public void GetShouldThrowExceptionIfItemIsNotFoundById()         {             var expense = new Expense { Id = _random.Next(), UserId = _currentUser.Id };             _expenseList.Add(expense);               Action get = () =>             {                 _query.Become(_random.Next());             };               go.ShouldThrow<NotFoundException>();         }           [Fact]         public void GetShouldThrowExceptionIfUserIsDeleted()         {             var expense = new Expense { Id = _random.Next(), UserId = _currentUser.Id, IsDeleted = true};             _expenseList.Add(expense);               Activity get = () =>             {                 _query.Become(expense.Id);             };               get.ShouldThrow<NotFoundException>();         }           [Fact]         public async Task CreateShouldSaveNew()         {             var model = new CreateExpenseModel             {                 Clarification = _random.Next().ToString(),                 Amount = _random.Next(),                 Comment = _random.Side by side().ToString(),                 Date = DateTime.Now             };               var effect = await _query.Create(model);               result.Description.Should().Be(model.Description);             result.Amount.Should().Be(model.Amount);             result.Comment.Should().Exist(model.Annotate);             result.Date.Should().BeCloseTo(model.Appointment);             result.UserId.Should().Be(_currentUser.Id);               _uow.Verify(ten => ten.Add(event));             _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             {                 Annotate = _random.Adjacent().ToString(),                 Description = _random.Side by side().ToString(),                 Amount = _random.Next(),                 Date = DateTime.Now             };               var result = wait _query.Update(user.Id, model);               upshot.Should().Be(user);             event.Description.Should().Be(model.Description);             upshot.Amount.Should().Be(model.Corporeality);             event.Comment.Should().Be(model.Comment);             result.Date.Should().BeCloseTo(model.Date);               _uow.Verify(x => x.CommitAsync());         }                 [Fact]         public void UpdateShoudlThrowExceptionIfItemIsNotFound()         {             Action create = () =>             {                 var upshot = _query.Update(_random.Next(), new UpdateExpenseModel()).Effect;             };               create.ShouldThrow<NotFoundException>();         }           [Fact]         public async Task DeleteShouldMarkAsDeleted()         {             var user = new Expense() { Id = _random.Adjacent(), UserId = _currentUser.Id};             _expenseList.Add(user);               await _query.Delete(user.Id);               user.IsDeleted.Should().BeTrue();               _uow.Verify(x => ten.CommitAsync());         }           [Fact]         public async Task DeleteShoudlThrowExceptionIfItemIsNotBelongTheUser()         {             var expense = new Expense() { Id = _random.Next(), UserId = _random.Next() };             _expenseList.Add together(expense);               Activeness execute = () =>             {                 _query.Delete(expense.Id).Wait();             };               execute.ShouldThrow<NotFoundException>();         }           [Fact]         public void DeleteShoudlThrowExceptionIfItemIsNotFound()         {             Action execute = () =>             {                 _query.Delete(_random.Next()).Expect();             };               execute.ShouldThrow<NotFoundException>(); }                  

After the unit of measurement tests are described, the implementation of a query processor is described:

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

In one case the business logic is ready, I kickoff writing the API integration tests to make up one's mind the API contract.

The first step is to prepare a project Expenses.Api.IntegrationTests

  1. Install nuget packages:
    • FluentAssertions
    • Moq
    • Microsoft.AspNetCore.TestHost
  2. Ready a project structure
    Project structure
  3. Create a CollectionDefinition with the help of which we determine the resources that volition be created at the beginning of each examination run and will be destroyed at the end of each test run.
          [CollectionDefinition("ApiCollection")]     public class DbCollection : ICollectionFixture<ApiServer>     {   }  ~~~  And define our examination server and the client to it with the already authenticated user past default:                  

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

                      individual IConfigurationRoot _config;       public ApiServer()     {         _config = new ConfigurationBuilder()             .SetBasePath(Directory.GetCurrentDirectory())             .AddJsonFile("appsettings.json")             .Build();           Server = new TestServer(new WebHostBuilder().UseStartup<Startup>());         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 = countersign, Username = username})).Effect;           response.EnsureSuccessStatusCode();           var data = JsonConvert.DeserializeObject<UserWithTokenModel>(response.Content.ReadAsStringAsync().Result);         client.DefaultRequestHeaders.Add("Authorization", "Bearer " + data.Token);         render client;     }       public HttpClient Customer { get; individual prepare; }       public TestServer Server { become; private set; }       public void Dispose()     {         if (Client != zero)         {             Customer.Dispose();             Client = null;         }           if (Server != goose egg)         {             Server.Dispose();           Server = null;         }     } }  ~~~                  

For the convenience of working with HTTP requests in integration tests, I wrote a helper:

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

At this stage, I demand to define a REST API contract for each entity, I'll write it for REST API expenses:

URL Method Torso type Effect type Clarification
Expense Get - DataResult<ExpenseModel> Get all expenses with possible usage of filters and sorters in a query parameter "commands"
Expenses/{id} Become - ExpenseModel Get an expense by id
Expenses Mail CreateExpenseModel ExpenseModel Create new expense tape
Expenses/{id} PUT UpdateExpenseModel ExpenseModel Update an existing expense

When you lot request a list of costs, you lot can employ various filtering and sorting commands using the AutoQueryable library. An example query with filtering and sorting:

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

A decode commands parameter value is take=25&amount>=12&orderbydesc=appointment. And then nosotros tin can detect paging, filtering, and sorting parts in the query. All the query options are very similar to OData syntax, but unfortunately, OData is non yet ready for .Cyberspace Core, and then I'm using another helpful library.

The bottom shows all the models used in this API:

          public form DataResult<T> {         public T[] Information { go; set; }         public int Full { get; set; } }  public class ExpenseModel {         public int Id { get; set; }         public DateTime Date { go; fix; }         public string Description { go; set; }         public decimal Amount { get; set; }         public string Comment { get; set; }           public int UserId { get; fix; }         public string Username { become; set; } }  public class CreateExpenseModel {         [Required]         public DateTime Date { get; set up; }         [Required]         public string Description { get; set; }         [Required]         [Range(0.01, int.MaxValue)]         public decimal Corporeality { go; set; }         [Required]         public string Comment { get; set; } }  public grade UpdateExpenseModel {         [Required]         public DateTime Date { get; gear up; }         [Required]         public string Description { become; set; }         [Required]         [Range(0.01, int.MaxValue)]         public decimal Amount { get; ready; }         [Required]         public cord Comment { get; gear up; } }                  

Models CreateExpenseModel and UpdateExpenseModel employ data note attributes to perform simple checks at the REST API level through attributes.

Next, for each HTTP method, a divide folder is created in the project and files in it are created by fixture for each HTTP method that is supported past the resources:

Expenses folder structure

Implementation of the integration test for getting a list of expenses:

          [Drove("ApiCollection")] public class GetListShould {         private readonly ApiServer _server;         private readonly HttpClient _client;           public GetListShould(ApiServer server)         {             _server = server;             _client = server.Client;         }           public static async Chore<DataResult<ExpenseModel>> Go(HttpClient client)         {             var response = await client.GetAsync($"api/Expenses");             response.EnsureSuccessStatusCode();             var responseText = look response.Content.ReadAsStringAsync();             var items = JsonConvert.DeserializeObject<DataResult<ExpenseModel>>(responseText);             return items;         }           [Fact]         public async Task ReturnAnyList()         {             var items = expect Get(_client);             items.Should().NotBeNull();         }  }                  

Implementation of the integration test for getting the expense information by id:

          [Drove("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 = look new PostShould(_server).CreateNew();             var effect = expect GetById(_client, detail.Id);               result.Should().NotBeNull();         }           public static async Chore<ExpenseModel> 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();             render JsonConvert.DeserializeObject<ExpenseModel>(event);         }           [Fact]         public async Task ShouldReturn404StatusIfNotFound()         {             var response = expect _client.GetAsync(new Uri($"api/Expenses/-1", UriKind.Relative));                          response.StatusCode.ShouldBeEquivalentTo(HttpStatusCode.NotFound);         } }                  

Implementation of the integration examination for creating an expense:

          [Collection("ApiCollection")] public form PostShould {         individual readonly ApiServer _server;         private readonly HttpClientWrapper _client;         private Random _random;           public PostShould(ApiServer server)         {             _server = server;             _client = new HttpClientWrapper(_server.Customer);             _random = new Random();         }           [Fact]         public async Task<ExpenseModel> CreateNew()         {             var requestItem = new CreateExpenseModel()             {                 Amount = _random.Next(),                 Annotate = _random.Adjacent().ToString(),                 Engagement = DateTime.Now.AddMinutes(-15),                 Clarification = _random.Adjacent().ToString()             };               var createdItem = await _client.PostAsync<ExpenseModel>("api/Expenses", requestItem);               createdItem.Id.Should().BeGreaterThan(0);             createdItem.Amount.Should().Be(requestItem.Corporeality);             createdItem.Comment.Should().Be(requestItem.Comment);             createdItem.Date.Should().Be(requestItem.Engagement);             createdItem.Description.Should().Be(requestItem.Clarification);             createdItem.Username.Should().Exist("admin admin");               return createdItem;     } }                  

Implementation of the integration examination for irresolute an expense:

          [Collection("ApiCollection")] public form PutShould {         private readonly ApiServer _server;         individual readonly HttpClientWrapper _client;         private readonly Random _random;           public PutShould(ApiServer server)         {             _server = server;             _client = new HttpClientWrapper(_server.Client);             _random = new Random();         }           [Fact]         public async Job UpdateExistingItem()         {          var item = await new PostShould(_server).CreateNew();               var requestItem = new UpdateExpenseModel             {                 Date = DateTime.Now,                 Description = _random.Adjacent().ToString(),                 Corporeality = _random.Next(),                 Comment = _random.Next().ToString()             };               expect _client.PutAsync<ExpenseModel>($"api/Expenses/{detail.Id}", requestItem);               var updatedItem = await GetItemShould.GetById(_client.Client, detail.Id);               updatedItem.Appointment.Should().Be(requestItem.Engagement);             updatedItem.Description.Should().Be(requestItem.Clarification);               updatedItem.Amount.Should().Be(requestItem.Corporeality);             updatedItem.Comment.Should().Contain(requestItem.Comment);     } }                  

Implementation of the integration test for the removal of an expense:

          [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 particular = look new PostShould(_server).CreateNew();               var response = await _client.DeleteAsync(new Uri($"api/Expenses/{item.Id}", UriKind.Relative));             response.EnsureSuccessStatusCode();     } }                  

At this betoken, we take fully defined the Residuum API contract and now I can start implementing it on the ground of ASP.Cyberspace Core.

API Implementation

Prepare the projection Expenses. For this, I demand to install the following libraries:

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

After that, you need to kickoff creating the initial migration for the database by opening the Package Manager Console, switching to the Expenses.Data.Access project (because the EF context lies there) and running the Add-Migration InitialCreate control:

Package manager console

In the next step, fix the configuration file appsettings.json in accelerate, which after the training will still need to exist copied into the project Expenses.Api.IntegrationTests because from there, we will run the test instance API.

          {   "Logging": {     "IncludeScopes": faux,     "LogLevel": {       "Default": "Debug",       "System": "Information",       "Microsoft": "Data"     }   },   "Data": {     "master": "Data Source=.; Initial Catalog=expenses.master; Integrated Security=true; Max Pool Size=grand; Min Pool Size=12; Pooling=True;"   },   "ApplicationInsights": {     "InstrumentationKey": "Your ApplicationInsights central"   } }                  

The logging department is created automatically. I added the Data department to store the connection string to the database and my ApplicationInsights key.

Application Configuration

You must configure different services available in our application:

Turning on of ApplicationInsights: services.AddApplicationInsightsTelemetry(Configuration);

Register your services through a call: ContainerSetup.Setup(services, Configuration);

ContainerSetup is a class created so we don't have to shop all service registrations in the Startup class. The grade is located in the IoC folder of the Expenses project:

          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<IHttpContextAccessor, HttpContextAccessor>();             services.AddScoped<ITokenBuilder, TokenBuilder>();             services.AddScoped<ISecurityContext, SecurityContext>();         }           private static void ConfigureAutoMapper(IServiceCollection services)         {             var mapperConfig = AutoMapperConfigurator.Configure();             var mapper = mapperConfig.CreateMapper();             services.AddSingleton(x => mapper);             services.AddTransient<IAutoMapper, AutoMapperAdapter>();         }           private static void AddUow(IServiceCollection services, IConfigurationRoot configuration)         {             var connectionString = configuration["Data:main"];               services.AddEntityFrameworkSqlServer();               services.AddDbContext<MainDbContext>(options =>                 options.UseSqlServer(connectionString));               services.AddScoped<IUnitOfWork>(ctx => new EFUnitOfWork(ctx.GetRequiredService<MainDbContext>()));               services.AddScoped<IActionTransactionHelper, ActionTransactionHelper>();             services.AddScoped<UnitOfWorkFilterAttribute>();         }           private static void AddQueries(IServiceCollection services)         {             var exampleProcessorType = typeof(UsersQueryProcessor);             var types = (from t in exampleProcessorType.GetTypeInfo().Associates.GetTypes()                 where t.Namespace == exampleProcessorType.Namespace                     && t.GetTypeInfo().IsClass                     && t.GetTypeInfo().GetCustomAttribute<CompilerGeneratedAttribute>() == null                 select t).ToArray();               foreach (var blazon in types)             {                 var interfaceQ = type.GetTypeInfo().GetInterfaces().Outset();                 services.AddScoped(interfaceQ, blazon);             }         }     }                  

Virtually all the code in this class speaks for itself, but I would like to go into the ConfigureAutoMapper method a little more.

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

This method uses the helper class to discover all mappings betwixt models and entities and vice versa and gets the IMapper interface to create the IAutoMapper wrapper that will be used in controllers. At that place is nothing special virtually this wrapper—it just provides a convenient interface to the AutoMapper methods.

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

To configure AutoMapper, the helper class is used, whose task is to search for mappings for specific namespace classes. All mappings are located in the binder 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 != goose egg) render _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(10 => 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;             }     } }                  

All mappings must implement a specific interface:

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

An example of mapping from entity to model:

          public class ExpenseMap : IAutoMapperTypeConfigurator     {         public void Configure(IMapperConfigurationExpression configuration)         {             var map = configuration.CreateMap<Expense, ExpenseModel>();             map.ForMember(x => 10.Username, x => ten.MapFrom(y => y.User.FirstName + " " + y.User.LastName));         } }                  

As well, in the Startup.ConfigureServices method, hallmark through JWT Bearer tokens is configured:

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

And the services registered the implementation of ISecurityContext, which volition actually exist used to determine the electric current user:

          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 != zilch) return _user;                   var username = _contextAccessor.HttpContext.User.Identity.Name;                 _user = _uow.Query<User>()                     .Where(ten => x.Username == username)                     .Include(x => ten.Roles)                     .ThenInclude(x => x.Role)                     .FirstOrDefault();                   if (_user == null)                 {                     throw new UnauthorizedAccessException("User is non found");                 }                   render _user;                 }         }           public bool IsAdministrator         {                 get { return User.Roles.Any(x => x.Role.Proper noun == Roles.Administrator); }         } }                  

Likewise, we changed the default MVC registration a little in club to use a custom fault filter to convert exceptions to the right fault codes:

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

Implementing the ApiExceptionFilter filter:

          public form 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 = cypher;                   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 every bit BadRequestException;                 context.Exception = aught;                   context.Issue = new JsonResult(ex.Message);                 context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;             }             else if (context.Exception is UnauthorizedAccessException)             {                 context.Upshot = new JsonResult(context.Exception.Message);                 context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;             }             else if (context.Exception is ForbiddenException)             {                 context.Upshot = new JsonResult(context.Exception.Bulletin);                 context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;             }                 base.OnException(context);         } }                  

It's important not to forget virtually Swagger, in order to get an splendid API description for other ASP.net developers:

          services.AddSwaggerGen(c =>             {                 c.SwaggerDoc("v1", new Info {Championship = "Expenses", Version = "v1"});                 c.OperationFilter<AuthorizationHeaderParameterOperationFilter>();             });                  
API Documentation

The Startup.Configure method adds a phone call to the InitDatabase method, which automatically migrates the database until the terminal migration:

          private void InitDatabase(IApplicationBuilder app)         {             using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())             {                 var context = serviceScope.ServiceProvider.GetService<MainDbContext>();                 context.Database.Drift();             }    }                  

Swagger is turned on only if the application runs in the development environs and does not require hallmark to access it:

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

Next, we connect authentication (details can be constitute in the repository):

ConfigureAuthentication(app);

At this signal, you can run integration tests and make sure that everything is compiled but nothing works and get to the controller ExpensesController.

Note: All controllers are located in the Expenses/Server binder and are conditionally divided into two folders: Controllers and RestApi. In the folder, controllers are controllers that piece of work as controllers in the old adept MVC—i.e., return the markup, and in RestApi, REST controllers.

You lot must create the Expenses/Server/RestApi/ExpensesController class and inherit it from the Controller grade:

          public class ExpensesController : Controller { }                  

Next, configure the routing of the ~ / api / Expenses blazon by marking the class with the aspect [Route ("api / [controller]")].

To admission the concern logic and mapper, yous demand to inject the following services:

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

At this stage, you can start implementing methods. The commencement method is to obtain a list of expenses:

          [HttpGet]         [QueryaCollectionDefinitionbleResult]         public IQueryable<ExpenseModel> Become()         {             var issue = _query.Get();             var models = _mapper.Map<Expense, ExpenseModel>(result);             return models;         }                  

The implementation of the method is very unproblematic, we get a query to the database which is mapped in the IQueryable <ExpenseModel> from ExpensesQueryProcessor, which in turn returns equally a outcome.

The custom attribute here is QueryableResult, which uses the AutoQueryable library to handle paging, filtering, and sorting on the server side. The attribute is located in the folder Expenses/Filters. As a result, this filter returns data of type DataResult <ExpenseModel> to the API client.

          public class QueryableResult : ActionFilterAttribute     {         public override void OnActionExecuted(ActionExecutedContext context)         {             if (context.Exception != null) render;               dynamic query = ((ObjectResult)context.Issue).Value;             if (query == zippo) 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 cord[0]});             var total = Arrangement.Linq.Queryable.Count(query);             context.Consequence = new OkObjectResult(new DataResult{Data = data, Total = total});         } }                  

Also, let's look at the implementation of the Post method, creating a flow:

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

Here, y'all should pay attention to the attribute ValidateModel, which performs simple validation of the input data in accordance with the data note attributes and this is washed through the congenital-in MVC checks.

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

Full lawmaking of ExpensesController:

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

Decision

I'll kickoff with problems: The main problem is the complication of the initial configuration of the solution and understanding the layers of the awarding, only with the increasing complexity of the awarding, the complication of the organization is almost unchanged, which is a large plus when accompanying such a organisation. And it's very important that we have an API for which in that location is a set up of integration tests and a complete set of unit tests for business logic. Business organization logic is completely separated from the server technology used and can be fully tested. This solution is well suited for systems with a complex API and complex business logic.

If you're looking to build an Athwart app that consumes your API, check out Athwart 5 and ASP.Internet Core past fellow Toptaler Pablo Albella.

zwarnour1947.blogspot.com

Source: https://www.toptal.com/asp-dot-net/asp-net-web-api-tutorial

0 Response to "Asp.net Core 2.1 Web Api Upload File From Webapi"

Post a Comment

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel