ASP.NET Core 5.0: Create API with Swagger(OpenAPI) Specifications and Consuming it in Blazor Web Assembly Project using Connected Service
In this tutorial, we will see implementation of ASP.NET Core 5.0 API creation using Swagger/OpenAPI specifications. We will also see the mechanism of consuming the API using Managed Proxy class in Blazor WebAssembly client application with the help of Visual Studio Connected Services. ASP.NET Core 5.0, have come up with the support for OpenAPI specifications for APIs. This is a language-agnostic specification for describing REST APIs. This description helps client applications to understand the capabilities of REST APIs. Main advantage of the OpenAPI is to Minimize the amount of work needed to connect the REST APIs in decoupled way and to Reduce the time required to document the service. The OpenAPI makes sure that action methods from the APIs are exposed in developer friendly manner.
The Connected Service is used to generate HTTP API clients so that the API can be easily consumed (remember WCF Service Proxy classes). This makes it easy to access APIs using the managed client applications e.g. ASP.NET Core Apps, Blazor Apps, etc. The generated client class contains code to access action methods from APIs. This makes the code of the client application easy and maintainable.
In this tutorial, we will create a ASP.NET Core 5.0 API for performing CRUD Operations with SQL Server. This API will be consumed in Blazor WebAssembly application. The figure 1 shows the implementation.
Figure 1: The Application Implementation
Step 1: Create a following database and table in it as shown in listing 1
Create Database ProductCatelog USE [ProductCatelog] GO CREATE TABLE [dbo].[ProductsInfo]( [ProductRowId] [int] IDENTITY(1,1) NOT NULL, [ProductId] [varchar](20) NOT NULL, [ProductName] [varchar](200) NOT NULL, [CategoryName] [varchar](20) NOT NULL, [Description] [varchar](200) NOT NULL, [Manufacturer] [varchar](200) NOT NULL, [Price] [int] NOT NULL, PRIMARY KEY CLUSTERED ( [ProductRowId] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] G
Listing 1: Creating database and table
We will use the ProductsInfo table to perform CRUD Operations
Step 2: Open Visual Studio 2019, and create a blank solution and name it as Blaze_API. In this solution add a new ASP.NET Core API project, name this project as Service_API. Make sure that, the version is selected as NET Core 5.0 and OpenAPI checkbox is checked as shown in figure 2
Figure 2: Selecting the ASP.NET Core 5.0 version and OpenAPI
Step 3: Since, we will be using EntityFrameworkCore 5.0 (EFCore) for database operations, we need to add EFCore packages in the project. Add following packages in API Project
Microsoft.EntityFrameworkCore,Microsoft.EntityFrameworkCore.SqlServer, Microsoft.EntityFrameworkCore.Tools, Microsoft.EntityFrameworkCore.Design.
Please note: You can add these packages using the NuGet Package Manager Window or else, you can using DotNet CLI command as follows:
dotnet add package <PACKAGE-NAME>
Step 3: To use EF Core database first approach, open the Command Prompt and navigate to the project folder. Run the following command
dotnet ef dbcontext scaffold "Data Source=.;Initial Catalog=ProductCatelog;Integrated Security=SSPI" Microsoft.EntityFrameworkCore.SqlServer -o Models
Once the command is successfully executed, the Models folder will be added in the project. This folder will contain class files for ProductsInfo and ProductCatelogContext. The ProductCatelogContext class contains the connection-string in OnConfiguring() method. Cut this connection string and paste it in appsettings.json file as shown in listing 2
"ConnectionStrings": { "ApplicationConnection": "Data Source=.;Initial Catalog=ProductCatalog;Integrated Security=SSPI" }
Listing 2: The Connection String in appsettings.json
Step 4: Modify the ConfigureServices() method of the Startup class to register ProductCatelogContext class in dependency container. We will also add the CORS policy for the API application as shown in the listing 3
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ProductCatalogContext>(options=> { options.UseSqlServer(Configuration.GetConnectionString("ApplicationConnection")); }); services.AddCors(options => { options.AddPolicy("corspolicy", policy => { policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); }); }); services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; }); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "Service_API", Version = "v1" }); }); }
Listing 3: The Startup class
Note that the code clearly shows the Swagger documentation service is by default added in the project. We will also modify the Configure() method of the Startup class to register middleware for CORS as shown in listing 4
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseSwagger(); app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Service_API v1")); } app.UseCors("corspolicy"); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
Listing 4: The Middleware registration
Please note that the Swagger and SwaggerUI middlewares are added in the application by default.
Step 5:In the Controllers folder, add a new API Controller with actions, using EntityFramework as shown in Figure 3
Figure 3: Adding the API Controller
Click on Add button, this will ask to select the Model class and Data Context class. Set the name of the controller as ProductInfoeController as shown in figure 4
Figure 4: Adding Controller with Data Context
Click on the Add button. The controller will be added with default code for CRUD operations using EFCore.
Step 6: We will modify the code in the ProductInfoeController to define HTTP Template methods for HttpGet, HttpPost, HttpPut, HttpDelete. We will also add the filters for response type format as shown in listing 5
[Route("api/[controller]")] [ApiController] [Produces(MediaTypeNames.Application.Json)] [Consumes(MediaTypeNames.Application.Json)] public class ProductsInfoeController : ControllerBase { private readonly ProductCatalogContext _context; public ProductsInfoeController(ProductCatalogContext context) { _context = context; } // GET: api/ProductsInfoe [HttpGet("/getall")] public async Task<ActionResult<IEnumerable<ProductsInfo>>> GetProductsInfos() { return await _context.ProductsInfos.ToListAsync(); } // GET: api/ProductsInfoe/5 [HttpGet("/getone/{id}")] public async Task<ActionResult<ProductsInfo>> GetProductsInfo(int id) { var productsInfo = await _context.ProductsInfos.FindAsync(id); if (productsInfo == null) { return NotFound(); } return productsInfo; } // PUT: api/ProductsInfoe/5 // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 [HttpPut("/update/{id}")] public async Task<IActionResult> PutProductsInfo(int id, ProductsInfo productsInfo) { if (id != productsInfo.ProductRowId) { return BadRequest(); } _context.Entry(productsInfo).State = EntityState.Modified; try { await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!ProductsInfoExists(id)) { return NotFound(); } else { throw; } } return NoContent(); } // POST: api/ProductsInfoe // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 [HttpPost("/create")] public async Task<ActionResult<ProductsInfo>> PostProductsInfo(ProductsInfo productsInfo) { _context.ProductsInfos.Add(productsInfo); await _context.SaveChangesAsync(); return CreatedAtAction("GetProductsInfo", new { id = productsInfo.ProductRowId }, productsInfo); } // DELETE: api/ProductsInfoe/5 [HttpDelete("/delete/{id}")] public async Task<IActionResult> DeleteProductsInfo(int id) { var productsInfo = await _context.ProductsInfos.FindAsync(id); if (productsInfo == null) { return NotFound(); } _context.ProductsInfos.Remove(productsInfo); await _context.SaveChangesAsync(); return NoContent(); } private bool ProductsInfoExists(int id) { return _context.ProductsInfos.Any(e => e.ProductRowId == id); } }
Listing 5: The Controller code
Step 7: Run the API project, the browser will show the swagger document as shown in figure 5
Figure 5: The Swagger Document for the API
The figure 5 shows the ASP.NET Core 5.0 API swagger documentation which uses OpenAPI Specifications 3.0. We are also providing developer friendly methods for Http methods. We have applied these in Http method attributes in the API Controller class as shown in Listing 5.
Click on the JSON link, the browser will show OAS3 documentation as shown in figure 6
Figure 6: The OAS 3 documentation
The document clearly shows the Request and Response type for each HTTP request made to the API.
Once the API is ready, we can create a Blazor WebAssembly client application and consume the API using the Http client proxy generated using the Connected service.
Step 8: In the same solution add a new Blazor WebAssembly project. Name this project as ClientApp. Make sure that the API Project is running. In this project, right-click on the Connected Services and select Manage Connected Services. The Connected Service tab will be opened as shown in the figure 7
Figure 7: The Connected Service Tab
The figure 7 show that we can add Service References for OpenAPI, gRPC and WCF Seb Services. This will generate the client proxy client class in the client application. Click on the + sign, the Add Service Reference window will be displayed as shown in figure 8
Figure 8: The Add Service Reference Window
On this window select the OpenAPI, so that we can add the service reference of the API based on OpenAPI specifications. Click on the Next button, the will Add new OpenAPI service reference window will be displayed. On this window, enter URL of the swagger.json which we have copied from the browser in which the API project is running. Set the namespace for the generated code as ClientNS and the proxy class name as ClientProxy. Make sure that the Code generation language selected as C# as shown in figure 9
Figure 9: The Add Service Reference details
Click on the Finish button. This will generate the proxy class. The client project will add a new folder of name OpenAPI that contains swagger.json file. The proxy class code file will not be displayed by default. This file is present in the obj folder. You can see the file by selecting Show All Files option from the Solution Explorer panel. The name of the file is swaggerClient.cs as shown in figure 10
Figure 10: The client file
You can see the source code of this file. The ClientProxy class contains code for making call to API. Please note that the CreateAsync() method contains the code for creating a new record by accessing API. But this method contains an if condition to check the expected response from API as 200 or 204 from the API, if this response code is not received then the method will throw the ApiException. When we create a new record the HTTP response statuscode received as 201, so modify the code in if condition to check for the status code as 201 as listing 6
if (status_ != "200" && status_ != "204" && status_ != "201") { var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); throw new ApiException("The HTTP status code of the response was not expected (" + (int)response_.StatusCode + ").", (int)response_.StatusCode, responseData_, headers_, null); }
Listing 6: The modification in Proxy class for CreateAsync() method
Step 9: In the Pages folder of the Blazor WebAssembly project delete all other components except Index.razor. In this folder add a new Blazor Component and name it as ProductsList.razor. We will use this component to add code for making call to API to fetch the Products Details. This call will be managed by the ClientProxy class. Add the code in this razor component as shown in listing 7
@page "/productslist" @using ClientNS @inject HttpClient httpClient; @inject NavigationManager navigationManager
<h2>List of Products</h2> <div class="container"> <input type="button" value="Add Product" class="btn btn-danger" @onclick="navigateToAdd"> <table class="table table-bordered table-striped table-dark"> <thead> <tr> <th>Product Row Id</th> <th>Product Id</th> <th>Product Name</th> <th>Category Name</th> <th>Description</th> <th>Manufacturer</th> <th>Price</th> </tr> </thead> <tbody> @foreach(var record in products) { <tr> <td>@record.ProductRowId</td> <td>@record.ProductId</td> <td>@record.ProductName</td> <td>@record.CategoryName</td> <td>@record.Manufacturer</td> <td>@record.Description</td> <td>@record.Price</td> </tr> } </tbody> </table> </div>
@code { private List<ProductsInfo> products; private ClientProxy proxy; protected override async Task OnInitializedAsync() { products = new List<ProductsInfo>(); proxy = new ClientProxy("http://localhost:55071/", httpClient); var result = await proxy.GetallAsync(); products = result.ToList(); } void navigateToAdd() { navigationManager.NavigateTo("/createproducts"); } protected override bool ShouldRender() { return true; } }
Listing 7: The ProductsList Component
The code in listing shows that the component is injected with HttpClient and NavigationManager classes. The HttpClient object is required to create an instance of the ClientProxy class. The component makes call to the API using ClientProxy class in the OnInitializedAsync() method. The GetAllAsync() method returns the Products list from the API. The component generates the HTML table based on the Products received from the API. The navigateToAdd() method is used to navigate to the createproducts component. We will add this component in next step.
Step 10: In the Pages folder add a new Blazor component and name it as CreateProduct.razor. This component will contain UI for creating Product. In this component add the code as shown in listing 8
@page "/createproducts" @using ClientNS @inject HttpClient httpClient; @inject NavigationManager navigationManager
<h2>Create Product</h2> <div class="container"> <EditForm Model="@product" OnValidSubmit="@save"> <div class="form-group"> <label>Product Id</label> <InputText @bind-Value="@product.ProductId" class="form-control"></InputText> </div> <div class="form-group"> <label>Product Name</label> <InputText @bind-Value="@product.ProductName" class="form-control"></InputText> </div> <div class="form-group"> <label>Category Name</label> <InputSelect @bind-Value="@product.CategoryName" class="form-control"> <option>Select Category Name</option> @foreach(var cat in Categorise) { <option>@cat</option> } </InputSelect> </div> <div class="form-group"> <label>Manufacturer</label> <InputSelect @bind-Value="@product.Manufacturer" class="form-control"> <option>Select Manufacturer</option> @foreach(var man in Mannfacturers) { <option>@man</option> } </InputSelect> </div> <div class="form-group"> <label>Description</label> <InputTextArea @bind-Value="@product.Description" class="form-control"></InputTextArea> </div> <div class="form-group"> <label>Product Name</label> <InputNumber @bind-Value="@product.Price" class="form-control"></InputNumber> </div> <div class="form-group"> <input type="reset" value="Clear" class="btn btn-warning"> <input type="submit" value="Save" class="btn btn-success"> </div> </EditForm> </div>
@code { private ProductsInfo product; private List<string> Categorise; private List<string> Mannfacturers; private ClientProxy proxy; protected override Task OnInitializedAsync() { product = new ProductsInfo(); Categorise = new List<string>() {"Electronics", "Electrical","Food", "Fashion","Home Appliances"}; Mannfacturers = new List<string>() {"HP","IBM","Bajaj","TATA" }; proxy = new ClientProxy("http://localhost:55071/", httpClient); return base.OnInitializedAsync(); } private async Task save() { var res = await proxy.CreateAsync(product); navigationManager.NavigateTo("/productslist"); } }
Listing 8: The CreateProduct component
The component is injected with HttpClient and NavigationManager classes. The HttpClient object is passed to the ClientProxy class. This component makes call to the CreateAsync() method of the ClientProxy class to post new product to the API. Once the record is saved successfully the navigation to the ProductsList component will take place.
Step 11: Modify the NavMenu.razor file in the Shared folder to create navigation links for the ProductsList and CreateProduct component as shown in the listing 9
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu"> <ul class="nav flex-column"> <li class="nav-item px-3"> <NavLink class="nav-link" href="" Match="NavLinkMatch.All"> <span class="oi oi-home" aria-hidden="true"></span> Home </NavLink> </li> <li class="nav-item px-3"> <NavLink class="nav-link" href="productslist"> <span class="oi oi-plus" aria-hidden="true"></span> Products List </NavLink> </li> <li class="nav-item px-3"> <NavLink class="nav-link" href="fetchdata"> <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data </NavLink> </li> </ul> </div>
Listing 9: The Navigation Links
To run API and Blazor Application, right-click on the solution name in solution explorer and set multiple startup projects. Set API and the Blazor Projects as startup. Run the application, two Browser instances will be loaded to show API and Blazor projects as shown in figure 11
Figure 11: Project loaded in browser
Click on the Products List link. This will show list of Products as shown in figure 12
Figure 12: List of Products
Click on Add Product button, this will show the Create Product component. In this component, add the Products details as shown in figure 13
Figure 13: Adding Product
After entering product details, click on the Save button. Once the record is saved, the Products list will be displayed with newly added product as shown in figure 14
Figure 14: Newly added Product
This is how the Blazor WebAssembly uses the Http Client proxy generated using Connected Services to access API with simple coding.
The Code of this tutorial is available on this link
Conclusion: The ASP.NET Core 5 API with Swagger/OpenAPI 3.0 Specifications documentation along with connected services, helps to create managed C# proxy code so that managed client application can easily use it to access methods from APIs. This makes it easy top design and develop Full-Stack app using a complete C# code.