In the last part, we completed the UserManagement.Application project that is basically the business logic of our application, in this part, we are going to create the RESTful APIs project containing Post, Put, Get and Delete User APIs, we will also see how to configure OpenAPI for web base HTTP client to test the APIs and how to dynamically generate the TypeScript file containing HttpClient code for User APIs, UserDTO, UserVM, etc. for Angular application. In the latest version of .NET Core 5.0, the OpenAPI is built-in, just check the checkbox at the bottom while creating the .NET Core API project.
Let’s Start
Let’s clean the UserManagement.API project, delete the default WeatherForecastController
from the Controllers folder and WeatherForecast
from the root folder. Before creating the UserController
, let’s work on:
- API Exception Filter Attribute: A middleware that will intercept validation and not found exception classes (that we created in the Application project), serialize validation error messages to JSON, and send appropriate HTTP status code in case of not found error to the calling API.
- Nswag: We can create the Nswag configuration file by nswag studio for the Web API project to generate the HTTP client for controller actions (APIs), DTO, and VMs in a Typescript file that is going to be used by our Angular front end application.
- Startup Class: We will register:
- Dependency injection services (created in Application and Persistence projects).
- Register fluent validation service so that all validation classes extending
AbstractValidator
invoke automatically for each API call. - Configure OpenAPI for web HTTP clients, etc.
Install Project and Packages References
Add the UserManagement.Application and UserManagement.Persistence projects reference to UserManagement.API project.
Install follwoing packages:
- FluentValidation.AspNetCore
- NSwag.AspNetCore
- NSwag.Core
- NSwag.MSBuild
API Exception Filter Attribute
Create folder Filters in UserManagement.API project and add a class ApiExceptionFilterAttribute
in it, replace the code with the following:
namespace UserManagement.API.Filters
{
using UserManagement.Application.Common.Exceptions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Collections.Generic;
public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
{
private readonly IDictionary<Type, Action<ExceptionContext>> _exceptionHandlers;
public ApiExceptionFilterAttribute()
{
// Register known exception types and handlers.
_exceptionHandlers = new Dictionary<Type, Action<ExceptionContext>>
{
{ typeof(ValidationException), HandleValidationException },
{ typeof(NotFoundException), HandleNotFoundException },
};
}
public override void OnException(ExceptionContext context)
{
HandleException(context);
base.OnException(context);
}
private void HandleException(ExceptionContext context)
{
Type type = context.Exception.GetType();
if (_exceptionHandlers.ContainsKey(type))
{
_exceptionHandlers[type].Invoke(context);
return;
}
if (!context.ModelState.IsValid)
{
HandleInvalidModelStateException(context);
return;
}
HandleUnknownException(context);
}
private void HandleUnknownException(ExceptionContext context)
{
var details = new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = "An error occurred while processing your request.",
Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1"
};
context.Result = new ObjectResult(details)
{
StatusCode = StatusCodes.Status500InternalServerError
};
context.ExceptionHandled = true;
}
private void HandleValidationException(ExceptionContext context)
{
var exception = context.Exception as ValidationException;
var details = new ValidationProblemDetails(exception.Errors)
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"
};
context.Result = new BadRequestObjectResult(details);
context.ExceptionHandled = true;
}
private void HandleInvalidModelStateException(ExceptionContext context)
{
var details = new ValidationProblemDetails(context.ModelState)
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"
};
context.Result = new BadRequestObjectResult(details);
context.ExceptionHandled = true;
}
private void HandleNotFoundException(ExceptionContext context)
{
var exception = context.Exception as NotFoundException;
var details = new ProblemDetails()
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Title = "The specified resource was not found.",
Detail = exception.Message
};
context.Result = new NotFoundObjectResult(details);
context.ExceptionHandled = true;
}
}
}
In the class constructor, we are checking the class types of ValidationException
and NotFoundException
that we created in the Application project and assigning them corresponding functions to return the error messages and HTTP status code.
In the HandleValidationException
function, we are getting the validation errors along with a bad request code. Similarly, we are taking care of not found exception in the HandleNotFoundException
function.
Nswag Configuration File
Since we are using the Swagger tool to implement the Open API specification, we can use the NSwag Studio to generate the JSON configuration file containing web HTTP client (may refer as swagger page) as well as typescript file for Angular HTTP client code of User APIs (GET, PUT, POST and DELETE). we will add the middleware in the Startup class to read the dynamically generated specification file (by reading the nswag configuration file) for the swagger HTTP client page as well as to generate the typescript file for the Angular application.
I already have created the file so it can be used here, create a nswag.json file in UserManagement.API root folder. Add the following code to it:
{
"runtime": "NetCore31",
"defaultVariables": null,
"documentGenerator": {
"aspNetCoreToOpenApi": {
"project": "UserManagement.API.csproj",
"msBuildProjectExtensionsPath": null,
"configuration": null,
"runtime": null,
"targetFramework": null,
"noBuild": true,
"verbose": false,
"workingDirectory": null,
"requireParametersWithoutDefault": true,
"apiGroupNames": null,
"defaultPropertyNameHandling": "CamelCase",
"defaultReferenceTypeNullHandling": "Null",
"defaultDictionaryValueReferenceTypeNullHandling": "NotNull",
"defaultResponseReferenceTypeNullHandling": "NotNull",
"defaultEnumHandling": "String",
"flattenInheritanceHierarchy": false,
"generateKnownTypes": true,
"generateEnumMappingDescription": false,
"generateXmlObjects": false,
"generateAbstractProperties": false,
"generateAbstractSchemas": true,
"ignoreObsoleteProperties": false,
"allowReferencesWithProperties": false,
"excludedTypeNames": [],
"serviceHost": null,
"serviceBasePath": null,
"serviceSchemes": [],
"infoTitle": "UserManagement APIs",
"infoDescription": null,
"infoVersion": "1.0.0",
"documentTemplate": null,
"documentProcessorTypes": [],
"operationProcessorTypes": [],
"typeNameGeneratorType": null,
"schemaNameGeneratorType": null,
"contractResolverType": null,
"serializerSettingsType": null,
"useDocumentProvider": true,
"documentName": "v1",
"aspNetCoreEnvironment": null,
"createWebHostBuilderMethod": null,
"startupType": null,
"allowNullableBodyParameters": true,
"output": "wwwroot/api/specification.json",
"outputType": "OpenApi3",
"assemblyPaths": [],
"assemblyConfig": null,
"referencePaths": [],
"useNuGetCache": false
}
},
"codeGenerators": {
"openApiToTypeScriptClient": {
"className": "{controller}Service",
"moduleName": "",
"namespace": "",
"typeScriptVersion": 2.7,
"template": "Angular",
"promiseType": "Promise",
"httpClass": "HttpClient",
"useSingletonProvider": true,
"injectionTokenType": "InjectionToken",
"rxJsVersion": 6.0,
"dateTimeType": "Date",
"nullValue": "Undefined",
"generateClientClasses": true,
"generateClientInterfaces": true,
"generateOptionalParameters": false,
"exportTypes": true,
"wrapDtoExceptions": false,
"exceptionClass": "SwaggerException",
"clientBaseClass": null,
"wrapResponses": false,
"wrapResponseMethods": [],
"generateResponseClasses": true,
"responseClass": "SwaggerResponse",
"protectedMethods": [],
"configurationClass": null,
"useTransformOptionsMethod": false,
"useTransformResultMethod": false,
"generateDtoTypes": true,
"operationGenerationMode": "MultipleClientsFromOperationId",
"markOptionalProperties": true,
"generateCloneMethod": false,
"typeStyle": "Class",
"classTypes": [],
"extendedClasses": [],
"extensionCode": null,
"generateDefaultValues": true,
"excludedTypeNames": [],
"excludedParameterNames": [],
"handleReferences": false,
"generateConstructorInterface": true,
"convertConstructorInterfaceData": false,
"importRequiredTypes": true,
"useGetBaseUrlMethod": false,
"baseUrlTokenName": "API_BASE_URL",
"queryNullValue": "",
"inlineNamedDictionaries": false,
"inlineNamedAny": false,
"templateDirectory": null,
"typeNameGeneratorType": null,
"propertyNameGeneratorType": null,
"enumNameGeneratorType": null,
"serviceHost": null,
"serviceSchemes": null,
"withCredentials": true,
"output": "ClientApp/src/app/user-management-api.ts"
}
}
}
In the above file, look for documentGenerator
and codeGenerators
keys. The documentGenerator
contains all the settings to generate the Web HTTP Client, we will configure URL in Startup
class. When we will build the project, it will check the settings keys in documentGenerator
section and spit out the specification.json file that would have metadata of all APIs required to generate/view the swagger HTTP client page to test. Create a new folder api in wwwroot where the specification.json file would automatically be generated after each build. The codeGenerators
is used to generate the HTTPClient code in TypeScript for Angular application, In short, we wouldn’t need to create the HTTPClient POST, GET, PUT and DELETE requests or create model classes. The swagger will use reflection concepts to dig into all our controller’s APIs, generate corresponding HTTPClient code, and also will bring underlying VMs, DTOs, and entities along with it. The output
key’s value specifies the auto-generated API client file i.e. ClientApp/src/app/user-management-api.ts. Just go through other keys, they are self-explanatory. We can also modify the nswag.json file according to our requirements.
DO NOT try to modify the user-management-api.ts file, it is an auto-generated file with every build.
Add Services and Middleware in Startup Class
Replace the code in Startup class with the following:
namespace UserManagement.API
{
using FluentValidation.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SpaServices.AngularCli;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using UserManagement.API.Filters;
using UserManagement.Application;
using UserManagement.Persistence;
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddApplication();
services.AddPersistance();
services.AddHttpContextAccessor();
services.AddControllersWithViews(options =>
options.Filters.Add(new ApiExceptionFilterAttribute()))
.AddFluentValidation();
services.AddRazorPages();
// Customise default API behaviour
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
services.AddOpenApiDocument(configure =>
{
configure.Title = "UserManagement API";
});
services.AddLogging();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
if (!env.IsDevelopment())
{
app.UseSpaStaticFiles();
}
app.UseSwaggerUi3(settings =>
{
settings.Path = "/api";
settings.DocumentPath = "/api/specification.json";
});
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
}
}
}
We created the dependency injection services classes in Application and Persistence projects. Since we created the ConfigureServices
extension method, we are adding these services as:
services.AddApplication();
services.AddPersistance();
For model validation, we are using the FluentValidation package, following is the code to configure ApiExceptionFilterAttribute
class with FluentValidation, in short, it tells model validation errors to flow to ApiExceptionFilterAttribute class to serialize the errors into JSON and append appropriate HTTP status code:
services.AddControllersWithViews(options =>
options.Filters.Add(new ApiExceptionFilterAttribute()))
.AddFluentValidation();
Next, we are using the OpenAPI specification for Web HTTPClient page. Following line of code is taking care of it:
services.AddOpenApiDocument(configure =>
{
configure.Title = "UserManagement API";
});
In the Configure
method, you can see the following line to use the specification.json to generate the HTTP client page, we are also specifying the path i.e. /api so the URL would be http://localhost:[PORT]/api:
app.UseSwaggerUi3(settings =>
{
settings.Path = "/api";
settings.DocumentPath = "/api/specification.json";
});
The rest of the Startup class is self-explanatory. That’s pretty much it with helping classes in API project, let go ahead and create a base and User controller and add the required User APIs.
Create Base Controller Class
Create a new class BaseController
in Controller folder and replace its content with following:
namespace UserManagement.API.Controllers
{
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
[Route("api/[controller]")]
[ApiController]
public class BaseController : ControllerBase
{
private IMediator mediator;
/// <summary>
/// Gets the Mediator.
/// </summary>
protected IMediator Mediator => this.mediator ??= this.HttpContext.RequestServices.GetService<IMediator>();
}
}
Since we are using the MedatR package to communicate between API and command/query in the Application project, the base controller is the best place to initialize it. All of our other controllers are supposed to extend BaseController
.
And finally, let’s create the UserController
, in the same Controller folder, create a new controller or class UserController
and replace its content with the following:
namespace UserManagement.API.Controllers
{
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using UserManagement.Application.User.Commands;
using UserManagement.Application.User.Queries;
using UserManagement.Application.User.VM;
[Route("api/[controller]")]
[ApiController]
public class UserController : BaseController
{
[HttpGet("[action]")]
public async Task<ActionResult<UserVM>> Get(int userID)
{
return await this.Mediator.Send(new GetSingleUserQuery { UserID = userID });
}
[HttpGet("[action]")]
public async Task<ActionResult<UserVM>> GetAll()
{
return await this.Mediator.Send(new GetAllUserQuery());
}
[HttpPost("[action]")]
public async Task<ActionResult<int>> Post(AddUserCommand command)
{
return await this.Mediator.Send(command);
}
[HttpPut("[action]")]
public async Task<ActionResult<bool>> Put(UpdateUserCommand command)
{
return await this.Mediator.Send(command);
}
[HttpDelete("[action]")]
public async Task<ActionResult<bool>> Delete(int userID)
{
return await this.Mediator.Send(new DeleteUserCommand { UserID = userID });
}
}
}
Pretty straight forward, you can see we are taking different commands e.g. AddUserCommand
, UpdateUserCommand
, etc. as input parameters for APIs, these commands are acting as requests in the Mediator pattern containing properties to perform the operation in handle function. The MedatR class object Mediator(that we are initializing in the base class) is used to send the request (command or query) to the Application project where our actual business logic is getting executed.
Add AppSettings and DB Connection String
In previous parts, we created the IConfigConstants
interface and concrete class ConfigConstants
to have strongly typed config values loading from appsettings.json file in UserManagment.API project. Let’s add these keys in appsettings.json now. Replace the appsettings.json file’s content with the following:
{
"ConnectionStrings": {
"FullStackConnection": "Data Source=localhost\\SQLEXPRESS;Persist Security Info=True;Integrated Security=SSPI;Initial Catalog=FullstackHub",
"TestFullStackConnection": "Data Source=localhost\\SQLEXPRESS;Persist Security Info=True;Integrated Security=SSPI;Initial Catalog=TestFullstackHub"
},
"AppSettings": {
"LongRunningProcessMilliseconds": "1500",
"MSG_USER_NULLUSERID": "User ID is required!",
"MSG_USER_NULLFIRSTNAME": "First Name is required!",
"MSG_USER_NULLLASTNAME": "Last Name is required!",
"MSG_USER_NULLDOB": "Date of birth is required!",
"MSG_USER_NULLGENDER": "Gender is required!",
"MSG_USER_GENDER_LEN": "Gender can be only M/F!",
"MSG_USER_NULLEMAILADDR": "Email Address is required!",
"MSG_USER_NULLPHNUM": "Phone Number is required!",
"MSG_USER_NULLCITY": "City is required!",
"MSG_USER_NULLSTATE": "State is required!",
"MSG_USER_NULLCOUNTRY": "Country is required!"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
Change the connection string’s Data source value accoding to local settings if above given one is not working.
Compile and Run the API Project
Compile and run the API project, try to browse the path: https://localhost:[PORT]/api, you would see the loading spinner with no content. We need one more step to make it work. Right-click on UserManagement.API and select option, Edit Project File, add following in project file to let it know to look for nswag.json configuration file and generate the specification.json file in wwwroot/api folder and user-management-api.ts file in ClientApp -> src -> app folder:
<Target Name="NSwag" AfterTargets="Build" Condition="'$(Configuration)' == 'Debug'">
<Copy SourceFiles="@(Reference)" DestinationFolder="$(OutDir)References" />
<Exec Command="$(NSwagExe_Core31) run /variables:Configuration=$(Configuration)" />
<RemoveDir Directories="$(OutDir)References" />
</Target>
Now Swagger API page should work just fine and you should see the following page, feel free to check all APIs by adding, updating, getting, and deleting users.
Let’s create the front-end application using Angular 10 in next step.