Let’s create a Web API with the latest version of ASP.NET Core and Entity Framework Core.
In this guide, we’ll use WideWorldImporters database to create a Web API.
REST APIs provide at least the following operations:
There are other operations for REST, but they aren’t necessary for this guide.
Those operations allow clients to perform actions through REST API, so our Web API must contain those operations.
WideWorldImporters database contains 4 schemas:
Application Purchasing Sales Warehouse
In this guide, we’ll work with Warehouse.StockItems table. We’ll add code to work with this entity: allow to retrieve stock items, retrieve stock item by id, create, update and delete stock items from database.
The version for this API is 1.
This is the route table for API:
| VERB |
URL |
DESCRIPTION |
| GET |
api/v1/Warehouse/StockItem |
Retrieves stock items |
| GET |
api/v1/Warehouse/StockItem/id |
Retrieves a stock item by id |
| POST |
api/v1/Warehouse/StockItem |
Creates a new stock item |
| PUT |
api/v1/Warehouse/StockItem/id |
Updates an existing stock item |
| DELETE |
api/v1/Warehouse/StockItem/id |
Deletes an existing stock item |
Keep these routes in mind because API must implement all routes.
Prerequisites
Software
- .NET Core
- NodeJS
- Visual Studio 2017 with last update
- SQL Server
WideWorldImporters database
Skills
- C#
- ORM (Object Relational Mapping)
- TDD (Test Driven Development)
- RESTful services
Using the Code
For this guide, the working directory for source code is C:\Projects.
Step 01 – Create Project
Open Visual Studio and follow these steps:
Go to File > New > Project
Go to Installed > Visual C# > .NET Core
Set the name for project as WideWorldImporters.API
Click OK
In the next window, select API and the latest version for .ASP.NET Core, in this case is 2.1:
Once Visual Studio has finished with creation for solution, we’ll see this window:
Step 02 – Install Nuget Packages
In this step, We need to install the following NuGet packages:
* EntityFrameworkCore.SqlServer
* Swashbuckle.AspNetCore
Now, we’ll proceed to install EntityFrameworkCore.SqlServer package from Nuget, right click on WideWorldImporters.API project:
Change to Browse tab and type Microsoft.EntityFrameworkCore.SqlServer:
Next, install Swashbuckle.AspNetCore package:
This is the structure for project.
Now run the project to check if solution is ready, press F5 and Visual Studio will show this browser window:
By default, Visual Studio adds a file with name ValuesController in Controllers directory, remove it from project.
Step 03 – Add Models
Now, create a directory with name Models and add the following files:
Entities.cs
Extensions.cs
Requests.cs
Responses.cs
Entities.cs will contains all code related to Entity Framework Core.
Extensions.cs will contain the extension methods for DbContext.
Requests.cs will contain definitions for request models.
Responses.cs will contain definitions for response models.
Code for Entities.cs file:
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace WideWorldImporters.API.Models
{
#pragma warning disable CS1591
public partial class StockItem
{
public StockItem()
{
}
public StockItem(int? stockItemID)
{
StockItemID = stockItemID;
}
public int? StockItemID { get; set; }
public string StockItemName { get; set; }
public int? SupplierID { get; set; }
public int? ColorID { get; set; }
public int? UnitPackageID { get; set; }
public int? OuterPackageID { get; set; }
public string Brand { get; set; }
public string Size { get; set; }
public int? LeadTimeDays { get; set; }
public int? QuantityPerOuter { get; set; }
public bool? IsChillerStock { get; set; }
public string Barcode { get; set; }
public decimal? TaxRate { get; set; }
public decimal? UnitPrice { get; set; }
public decimal? RecommendedRetailPrice { get; set; }
public decimal? TypicalWeightPerUnit { get; set; }
public string MarketingComments { get; set; }
public string InternalComments { get; set; }
public string CustomFields { get; set; }
public string Tags { get; set; }
public string SearchDetails { get; set; }
public int? LastEditedBy { get; set; }
public DateTime? ValidFrom { get; set; }
public DateTime? ValidTo { get; set; }
}
public class StockItemsConfiguration : IEntityTypeConfiguration<StockItem>
{
public void Configure(EntityTypeBuilder builder)
{
builder.ToTable("StockItems", "Warehouse");
builder.HasKey(p => p.StockItemID);
builder.Property(p => p.StockItemName).HasColumnType("nvarchar(200)").IsRequired();
builder.Property(p => p.SupplierID).HasColumnType("int").IsRequired();
builder.Property(p => p.ColorID).HasColumnType("int");
builder.Property(p => p.UnitPackageID).HasColumnType("int").IsRequired();
builder.Property(p => p.OuterPackageID).HasColumnType("int").IsRequired();
builder.Property(p => p.Brand).HasColumnType("nvarchar(100)");
builder.Property(p => p.Size).HasColumnType("nvarchar(40)");
builder.Property(p => p.LeadTimeDays).HasColumnType("int").IsRequired();
builder.Property(p => p.QuantityPerOuter).HasColumnType("int").IsRequired();
builder.Property(p => p.IsChillerStock).HasColumnType("bit").IsRequired();
builder.Property(p => p.Barcode).HasColumnType("nvarchar(100)");
builder.Property(p => p.TaxRate).HasColumnType("decimal(18, 3)").IsRequired();
builder.Property(p => p.UnitPrice).HasColumnType("decimal(18, 2)").IsRequired();
builder.Property(p => p.RecommendedRetailPrice).HasColumnType("decimal(18, 2)");
builder.Property(p => p.TypicalWeightPerUnit).HasColumnType("decimal(18, 3)").IsRequired();
builder.Property(p => p.MarketingComments).HasColumnType("nvarchar(max)");
builder.Property(p => p.InternalComments).HasColumnType("nvarchar(max)");
builder.Property(p => p.CustomFields).HasColumnType("nvarchar(max)");
builder.Property(p => p.LastEditedBy).HasColumnType("int").IsRequired();
builder
.Property(p => p.StockItemID)
.HasColumnType("int")
.IsRequired()
.HasComputedColumnSql("NEXT VALUE FOR [Sequences].[StockItemID]");
builder
.Property(p => p.Tags)
.HasColumnType("nvarchar(max)")
.HasComputedColumnSql("json_query([CustomFields],N'$.Tags')");
builder
.Property(p => p.SearchDetails)
.HasColumnType("nvarchar(max)")
.IsRequired()
.HasComputedColumnSql("concat([StockItemName],N' ',[MarketingComments])");
builder
.Property(p => p.ValidFrom)
.HasColumnType("datetime2")
.IsRequired()
.ValueGeneratedOnAddOrUpdate();
builder
.Property(p => p.ValidTo)
.HasColumnType("datetime2")
.IsRequired()
.ValueGeneratedOnAddOrUpdate();
}
}
public class WideWorldImportersDbContext : DbContext
{
public WideWorldImportersDbContext(DbContextOptions options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.ApplyConfiguration(new StockItemsConfiguration());
base.OnModelCreating(modelBuilder);
}
public DbSet StockItems { get; set; }
}
#pragma warning restore CS1591
}
Code for Extensions.cs file:
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace WideWorldImporters.API.Models
{
#pragma warning disable CS1591
public static class WideWorldImportersDbContextExtensions
{
public static IQueryable GetStockItems(this WideWorldImportersDbContext dbContext, int pageSize = 10, int pageNumber = 1, int? lastEditedBy = null, int? colorID = null, int? outerPackageID = null, int? supplierID = null, int? unitPackageID = null)
{
var query = dbContext.StockItems.AsQueryable();
if (lastEditedBy.HasValue)
query = query.Where(item => item.LastEditedBy == lastEditedBy);
if (colorID.HasValue)
query = query.Where(item => item.ColorID == colorID);
if (outerPackageID.HasValue)
query = query.Where(item => item.OuterPackageID == outerPackageID);
if (supplierID.HasValue)
query = query.Where(item => item.SupplierID == supplierID);
if (unitPackageID.HasValue)
query = query.Where(item => item.UnitPackageID == unitPackageID);
return query;
}
public static async Task GetStockItemsAsync(this WideWorldImportersDbContext dbContext, StockItem entity)
=> await dbContext.StockItems.FirstOrDefaultAsync(item => item.StockItemID == entity.StockItemID);
public static async Task GetStockItemsByStockItemNameAsync(this WideWorldImportersDbContext dbContext, StockItem entity)
=> await dbContext.StockItems.FirstOrDefaultAsync(item => item.StockItemName == entity.StockItemName);
}
public static class IQueryableExtensions
{
public static IQueryable Paging(this IQueryable query, int pageSize = 0, int pageNumber = 0) where TModel : class
=> pageSize > 0 && pageNumber > 0 ? query.Skip((pageNumber - 1) * pageSize).Take(pageSize) : query;
}
#pragma warning restore CS1591
}
Code for Requests.cs file:
using System;
using System.ComponentModel.DataAnnotations;
namespace WideWorldImporters.API.Models
{
#pragma warning disable CS1591
public class PostStockItemsRequest
{
[Key]
public int? StockItemID { get; set; }
[Required]
[StringLength(200)]
public string StockItemName { get; set; }
[Required]
public int? SupplierID { get; set; }
public int? ColorID { get; set; }
[Required]
public int? UnitPackageID { get; set; }
[Required]
public int? OuterPackageID { get; set; }
[StringLength(100)]
public string Brand { get; set; }
[StringLength(40)]
public string Size { get; set; }
[Required]
public int? LeadTimeDays { get; set; }
[Required]
public int? QuantityPerOuter { get; set; }
[Required]
public bool? IsChillerStock { get; set; }
[StringLength(100)]
public string Barcode { get; set; }
[Required]
public decimal? TaxRate { get; set; }
[Required]
public decimal? UnitPrice { get; set; }
public decimal? RecommendedRetailPrice { get; set; }
[Required]
public decimal? TypicalWeightPerUnit { get; set; }
public string MarketingComments { get; set; }
public string InternalComments { get; set; }
public string CustomFields { get; set; }
public string Tags { get; set; }
[Required]
public string SearchDetails { get; set; }
[Required]
public int? LastEditedBy { get; set; }
public DateTime? ValidFrom { get; set; }
public DateTime? ValidTo { get; set; }
}
public class PutStockItemsRequest
{
[Required]
[StringLength(200)]
public string StockItemName { get; set; }
[Required]
public int? SupplierID { get; set; }
public int? ColorID { get; set; }
[Required]
public decimal? UnitPrice { get; set; }
}
public static class Extensions
{
public static StockItem ToEntity(this PostStockItemsRequest request)
=> new StockItem
{
StockItemID = request.StockItemID,
StockItemName = request.StockItemName,
SupplierID = request.SupplierID,
ColorID = request.ColorID,
UnitPackageID = request.UnitPackageID,
OuterPackageID = request.OuterPackageID,
Brand = request.Brand,
Size = request.Size,
LeadTimeDays = request.LeadTimeDays,
QuantityPerOuter = request.QuantityPerOuter,
IsChillerStock = request.IsChillerStock,
Barcode = request.Barcode,
TaxRate = request.TaxRate,
UnitPrice = request.UnitPrice,
RecommendedRetailPrice = request.RecommendedRetailPrice,
TypicalWeightPerUnit = request.TypicalWeightPerUnit,
MarketingComments = request.MarketingComments,
InternalComments = request.InternalComments,
CustomFields = request.CustomFields,
Tags = request.Tags,
SearchDetails = request.SearchDetails,
LastEditedBy = request.LastEditedBy,
ValidFrom = request.ValidFrom,
ValidTo = request.ValidTo
};
}
#pragma warning restore CS1591
}
Code for Responses.cs file:
using System.Collections.Generic;
using System.Net;
using Microsoft.AspNetCore.Mvc;
namespace WideWorldImporters.API.Models
{
#pragma warning disable CS1591
public interface IResponse
{
string Message { get; set; }
bool DidError { get; set; }
string ErrorMessage { get; set; }
}
public interface ISingleResponse<TModel> : IResponse
{
TModel Model { get; set; }
}
public interface IListResponse<TModel> : IResponse
{
IEnumerable Model { get; set; }
}
public interface IPagedResponse<TModel> : IListResponse<TModel>
{
int ItemsCount { get; set; }
double PageCount { get; }
}
public class Response : IResponse
{
public string Message { get; set; }
public bool DidError { get; set; }
public string ErrorMessage { get; set; }
}
public class SingleResponse<TModel> : ISingleResponse<TModel>
{
public string Message { get; set; }
public bool DidError { get; set; }
public string ErrorMessage { get; set; }
public TModel Model { get; set; }
}
public class ListResponse<TModel> : IListResponse<TModel>
{
public string Message { get; set; }
public bool DidError { get; set; }
public string ErrorMessage { get; set; }
public IEnumerable Model { get; set; }
}
public class PagedResponse<TModel> : IPagedResponse<TModel>
{
public string Message { get; set; }
public bool DidError { get; set; }
public string ErrorMessage { get; set; }
public IEnumerable Model { get; set; }
public int PageSize { get; set; }
public int PageNumber { get; set; }
public int ItemsCount { get; set; }
public double PageCount
=> ItemsCount < PageSize ? 1 : (int)(((double)ItemsCount / PageSize) + 1);
}
public static class ResponseExtensions
{
public static IActionResult ToHttpResponse(this IResponse response)
{
var status = response.DidError ? HttpStatusCode.InternalServerError : HttpStatusCode.OK;
return new ObjectResult(response)
{
StatusCode = (int)status
};
}
public static IActionResult ToHttpResponse(this ISingleResponse response)
{
var status = HttpStatusCode.OK;
if (response.DidError)
status = HttpStatusCode.InternalServerError;
else if (response.Model == null)
status = HttpStatusCode.NotFound;
return new ObjectResult(response)
{
StatusCode = (int)status
};
}
public static IActionResult ToHttpResponse(this IListResponse response)
{
var status = HttpStatusCode.OK;
if (response.DidError)
status = HttpStatusCode.InternalServerError;
else if (response.Model == null)
status = HttpStatusCode.NoContent;
return new ObjectResult(response)
{
StatusCode = (int)status
};
}
}
#pragma warning restore CS1591
}
Understanding Models
ENTITIES
StockItems class is the representation for Warehouse.StockItems table.
StockItemsConfiguration class contains the mapping for StockItems class.
WideWorldImportersDbContext class is the link between database and C# code, this class handles queries and commits the changes in database and of course, another things.
EXTENSIONS
WideWorldImportersDbContextExtensions contains extension methods to provide linq queries.
IQueryableExtensions contains extension methods to allow paging in IQueryable instances.
REQUESTS
We have the following definitions:
PostStockItemsRequest
PutStockItemsRequest
PostStockItemsRequestModel represents the model to create a new stock item, contains all required properties to save in database.
PutStockItemsRequestModel represents the model to update an existing stock item, in this case contains only 4 properties: StockItemName, SupplierID, ColorID and UnitPrice. This class doesn’t contain StockItemID property because id is in route for controller’s action.
The models for requests do not require to contain all properties like entities, because we don’t need to expose full definition in a request or response, it’s a good practice to limit data using models with few properties.
Extensions class contains an extension method for PostStockItemsRequestModel, to return an instance of StockItem class from request model.
RESPONSES
These are the interfaces:
IResponse
ISingleResponse
IListResponse
IPagedResponse
Each one of these interfaces has implementations, why do we need these definitions if it’s more simple to return objects without wrapping them in these models? Keep in mind this Web API will provide operations for clients, with UI or without UI and it’s more easy to have properties to send message, to have a model or send information if an error occurs, in addition, we set Http status code in response to describe the result from request.
These classes are generic, because in this way, we save time to define responses in future, this Web API only returns a response for a single entity, a list and a paged list.
ISingleResponse represents a response for a single entity.
IListResponse represents a response with a list, for example all shipping to existing order without paging.
IPagedResponse represents a response with pagination, for example all orders in a date range.
ResponseExtensions class contains extension methods to convert a response in a Http response, these methods return InternalServerError (500) status if an error occurs, OK (200) if it’s OK and NotFound (404) if an entity does not exist in database or NoContent (204) for list responses without model.
Step 04 – Add Controller
Now, inside of Controllers directory, add a code file with name WarehouseController.cs and add this code:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using WideWorldImporters.API.Models;
namespace WideWorldImporters.API.Controllers
{
#pragma warning disable CS1591
[ApiController]
[Route("api/v1/[controller]")]
public class WarehouseController : ControllerBase
{
protected readonly ILogger Logger;
protected readonly WideWorldImportersDbContext DbContext;
public WarehouseController(ILogger logger, WideWorldImportersDbContext dbContext)
{
Logger = logger;
DbContext = dbContext;
}
#pragma warning restore CS1591
[HttpGet("StockItem")]
[ProducesResponseType(200)]
[ProducesResponseType(500)]
public async Task GetStockItemsAsync(int pageSize = 10, int pageNumber = 1, int? lastEditedBy = null, int? colorID = null, int? outerPackageID = null, int? supplierID = null, int? unitPackageID = null)
{
Logger?.LogDebug("'{0}' has been invoked", nameof(GetStockItemsAsync));
var response = new PagedResponse();
try
{
var query = DbContext.GetStockItems();
response.PageSize = pageSize;
response.PageNumber = pageNumber;
response.ItemsCount = await query.CountAsync();
response.Model = await query.Paging(pageSize, pageNumber).ToListAsync();
response.Message = string.Format("Page {0} of {1}, Total of products: {2}.", pageNumber, response.PageCount, response.ItemsCount);
Logger?.LogInformation("The stock items have been retrieved successfully.");
}
catch (Exception ex)
{
response.DidError = true;
response.ErrorMessage = "There was an internal error, please contact to technical support.";
Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(GetStockItemsAsync), ex);
}
return response.ToHttpResponse();
}
[HttpGet("StockItem/{id}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task GetStockItemAsync(int id)
{
Logger?.LogDebug("'{0}' has been invoked", nameof(GetStockItemAsync));
var response = new SingleResponse();
try
{
response.Model = await DbContext.GetStockItemsAsync(new StockItem(id));
}
catch (Exception ex)
{
response.DidError = true;
response.ErrorMessage = "There was an internal error, please contact to technical support.";
Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(GetStockItemAsync), ex);
}
return response.ToHttpResponse();
}
[HttpPost("StockItem")]
[ProducesResponseType(200)]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(500)]
public async Task PostStockItemAsync([FromBody]PostStockItemsRequest request)
{
Logger?.LogDebug("'{0}' has been invoked", nameof(PostStockItemAsync));
var response = new SingleResponse();
try
{
var existingEntity = await DbContext
.GetStockItemsByStockItemNameAsync(new StockItem { StockItemName = request.StockItemName });
if (existingEntity != null)
ModelState.AddModelError("StockItemName", "Stock item name already exists");
if (!ModelState.IsValid)
return BadRequest();
var entity = request.ToEntity();
DbContext.Add(entity);
await DbContext.SaveChangesAsync();
response.Model = entity;
}
catch (Exception ex)
{
response.DidError = true;
response.ErrorMessage = "There was an internal error, please contact to technical support.";
Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(PostStockItemAsync), ex);
}
return response.ToHttpResponse();
}
[HttpPut("StockItem/{id}")]
[ProducesResponseType(200)]
[ProducesResponseType(400)]
[ProducesResponseType(500)]
public async Task PutStockItemAsync(int id, [FromBody]PutStockItemsRequest request)
{
Logger?.LogDebug("'{0}' has been invoked", nameof(PutStockItemAsync));
var response = new Response();
try
{
var entity = await DbContext.GetStockItemsAsync(new StockItem(id));
if (entity == null)
return NotFound();
entity.StockItemName = request.StockItemName;
entity.SupplierID = request.SupplierID;
entity.ColorID = request.ColorID;
entity.UnitPrice = request.UnitPrice;
DbContext.Update(entity);
await DbContext.SaveChangesAsync();
}
catch (Exception ex)
{
response.DidError = true;
response.ErrorMessage = "There was an internal error, please contact to technical support.";
Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(PutStockItemAsync), ex);
}
return response.ToHttpResponse();
}
[HttpDelete("StockItem/{id}")]
[ProducesResponseType(200)]
[ProducesResponseType(500)]
public async Task DeleteStockItemAsync(int id)
{
Logger?.LogDebug("'{0}' has been invoked", nameof(DeleteStockItemAsync));
var response = new Response();
try
{
var entity = await DbContext.GetStockItemsAsync(new StockItem(id));
if (entity == null)
return NotFound();
DbContext.Remove(entity);
await DbContext.SaveChangesAsync();
}
catch (Exception ex)
{
response.DidError = true;
response.ErrorMessage = "There was an internal error, please contact to technical support.";
Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(DeleteStockItemAsync), ex);
}
return response.ToHttpResponse();
}
}
}
The process for all controller’s actions is:
Log the invocation for method.
Create the instance for response according to action (Paged, list or single).
Perform access to database through DbContext instance.
If invocation for DbContext extension method fails, set DidError property as true and set ErrorMessage property with: There was an internal error, please contact to technical support., because it isn't recommended to expose error details in response, it's better to save all exception details in log file.
Return result as Http response.
Keep in mind all names for methods that end with Async sufix because all operations are async but in Http attributes, we don’t use this suffix.
Step 05 – Setting Up Dependency Injection
ASP.NET Core enables dependency injection in native way, this means we don’t need any 3rd party framework to inject dependencies in controllers.
This is a great challenge because we need to change our mind from Web Forms and ASP.NET MVC, for those technologies use a framework to inject dependencies it was a luxury, now in ASP.NET Core dependency injection is a basic aspect.
Project template for ASP.NET Core has a class with name Startup, in this class we must to add the configuration to inject instances for DbContext, Repositories, Loggers, etc.
Modify the code of Startup.cs file to look like this:
using System;
using System.IO;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Swashbuckle.AspNetCore.Swagger;
using WideWorldImporters.API.Controllers;
using WideWorldImporters.API.Models;
namespace WideWorldImporters.API
{
#pragma warning disable CS1591
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddDbContext(options =>
{
options.UseSqlServer(Configuration["AppSettings:ConnectionString"]);
});
services.AddScoped<ILogger, Logger>();
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new Info { Title = "WideWorldImporters API", Version = "v1" });
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "WideWorldImporters API V1");
});
app.UseMvc();
}
}
#pragma warning restore CS1591
}
The ConfigureServices method specifies how dependencies will be resolved, in this method. We need to set up DbContext, Repositories and Logging.
The Configure method adds the configuration for Http request runtime.
Step 06 – Running Web API
Before you run Web API project, add the connection string in appsettings.json file:
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"AppSettings": {
"ConnectionString": "server=(local);database=WideWorldImporters;integrated security=yes;"
}
}
In order to show descriptions in help page, enable XML documentation for your Web API project:
1. Right click on Project > Properties
2. Go to Build > Output
3. Enable XML documentation file
4. Save changes
Now, press F5 to start debugging for Web API project, if everything it’s OK, we’ll get the following output in the browser: