or from Github Part6 branch here
Note: source excludes the machine generated code below.
Introduction
So far this series of articles has been describing a way to integrate ASP.Net Core with Angular 2.
Part 1 - How to integrate ASP.Net Core and Angular 2
Part 2 - How to use Tag Helpers to display data
Part 3 - How to use Tag Helpers for data input, added SQL backend using EF Core.
Part 4 - Adding token authentication with JWT using OpenIdDict.
Part 5 - Adding async services, server-side validation, a simple Angular 2 datagrid & more tag helpers.
This is part 6 - Where we're adding Swagger documentation for our Web API methods using NSwag, and then using the (free) NSwag Studio tool to automatically create Typescript data models and data services for Angular 2.
Note: the topic of deployment to IIS will be held over until part 7, as this article was getting a little too long.
Adding NSwag - a Swagger library for Dot Net
First we'll add NSwag then use it create an online self-documenting test page.
We could use Postman to test the Web API methods, and personally I find this very useful for testing and debugging, however it is not going always useful semi-technical end-users, or for documentation, and using Postman is still quite manual, that is every time you alter a Web API data service, you've now created something else that needs to be maintained. The point of these articles has been in part to show another way to use ASP.Net Core with Angular 2 (or really any client side framework) but also to show how you can reduce the amount of cut/past/manual hackwork .
So first NSwag's Swagger docs.
Right click the A2SPA project, click Manage NuGet packages
Click on Browse, the enter nswag in the search box,
and then after a second or two, click on NSwag.AspNetCore
Click on the Install button, after a few seconds, you'll get a review changes dialogue
Click OK
Click I Accept
Add Nswag.Annotations using NuGet
Next we need to update our Startup.cs file to register NSwag to provide us Swagger documentation
Just above the line:
app.UseMvc(routes =>
{
Add
app.UseSwaggerUi(typeof(Startup).GetTypeInfo().Assembly, new SwaggerUiOwinSettings()
{
DefaultPropertyNameHandling = PropertyNameHandling.CamelCase
});
To resolve the dependencies, add these to the "usings" at the top of the Startup.cs
class:
using NJsonSchema;
using NSwag.AspNetCore;
using System.Reflection;
Let's try building this, and then hitting Ctrl-F5 .. It's not going to work, but let see how badly
Our home page is still there, manually change the URL to from …/home to …/swagger
This message tells us NSwag thinks we have a problem with CORS, or cross origin request settings.
Since we don't have a CORS policy (and haven't needed one so far) this is entirely possible.
But, before we go fixing something that isn't necessarily broken, let's have a look at the F12 debug, using the network tab first.
Hit F12, click on the network tab, clear the cache, then hit refresh, scroll through the various requests until you reach the swagger.json file, then view the response itself:
We're getting the error
System.InvalidOperationException: The method 'Post' on path '/api/Home' is registered multiple times.
Since we're not looking to provide details on our Home controller, as it's not used for Web API data services, we can tell Swagger to ignore it.
Update the HomeController.cs file to add
using NSwag.Annotations;
And then decorate the class with the [SwaggerIgnore]
attribute, so that the final HomeController.cs
looks like this:
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
namespace A2SPA.Controllers
{
[SwaggerIgnore]
public class HomeController : Controller
{
public IActionResult Index()
{
ViewData["Title"] = "Home";
return View();
}
public IActionResult Error()
{
return View();
}
}
}
Once again build and press ctrl-F5 and we'll see we're getting something of a trend here, now the PartialController needs to have the same SwaggerIgnore treatment:
Add the same using statement and Swagger Ignore attribute, again build and hit ctrl-F5
And, as before we get another error exposed in Nswag, this time in relation to OpenIdDict's data services.
System.InvalidOperationException: The JSON property 'item' is defined multiple times on type 'AspNet.Security.OpenIdConnect.Primitives.OpenIdConnectParameter'.
As we've already got services for this, we'll ignore this too, repeat the same procedure for each of these files, AccountController.cs, AuthorizationController.cs and ResourceController.cs and then once again rebuild and press ctrl-F5 (or refresh if you still have your browser open).
Finally we should see our Swagger page, with the swagger.json no longer generating an exception, but being a fully-fledged JSON document:
Close the debug window
Click on the link on the Swagger documentation page, where it has "SampleData" in grey text, it will expand to show all of the services available under that particular Web Api class:
Each of these entries correspond to a different Web API method, they represent all of the new CRUD operations we added in the last article, part 5.
Click on each of (say) the Get methods and these will open up to a test page, as below:
Those methods such as "get by id", that requires an Id, will have entry text boxes which will be used to feed the query (if required). Enter say 1 into the ID box and click the "Try it out!" button, and well see our security being enforced, as we're given a 401 error
To verify this is indeed the issue, go to the SampleDataController.cs class, and immediately above the GetById method add the [AllowAnonymous] attribute:
[AllowAnonymous]
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
Because we have the [Authorize] attribute at a class level , this change will allow just the one method, GetById to be accessible to check this is indeed the issue. Rebuild, and if you so happen to still have the browser open you could even click the same "Try it out" button that failed a moment ago (again if the web server too is still running, else hit ctrl-F5 to launch the web server + browser), once you click the button a dotty spinner starts:
And it all is well you should see a result - assuming you picked a valid Id of course.
Since we really don't want to leave this method without security, remove the [AllowAnonymous]
attribute from the GetById method in the SampleDataController class
During development, you might get away with putting in the [AllowAnonymous]
however there will always remain the risk of leaving this in place, in the code, and thereby failing to protect your Web API data services. Ideally NSwag could add a bearer token for us, so that we can use the OpenIdDict JWT security with our data services.
To fix this, next edit the configure method of Startup.cs
where we added the configuration earlier for Nswag, change this:
app.UseSwaggerUi(typeof(Startup).GetTypeInfo().Assembly, new SwaggerUiOwinSettings()
{
DefaultPropertyNameHandling = PropertyNameHandling.CamelCase
});
To this:
app.UseSwaggerUi(typeof(Startup).GetTypeInfo().Assembly, new SwaggerUiOwinSettings()
{
OperationProcessors =
{
new OperationSecurityScopeProcessor("apikey")
},
DocumentProcessors =
{
new SecurityDefinitionAppender("apikey", new SwaggerSecurityScheme
{
Type = SwaggerSecuritySchemeType.ApiKey,
Name = "Authorization",
In = SwaggerSecurityApiKeyLocation.Header
})
},
DefaultPropertyNameHandling = PropertyNameHandling.CamelCase
});
Rebuild and hit ctrl-F5 to launch your browser, then log on, press F12 to get the browser debug window,
In Chrome you need to select the Application tab,
In Firefox this is called the Storage tab
In Edge and IE the location is a little more obscure, you press F12
for debug, then need to go to the Console (ctrl 2) is a shortcut.
Then type
sessionStorage
As you type, intellisense kicks in and you see the choices narrow,
Once you get one choice hit enter twice, and then you see the session storage values. (Note: if using local storage in place of session storage, type local storage here instead)
Expand the node:
Once again select all of the token, this can be a little painful ctrl-A does not work, and double-clicking will only select to the nearest symbol such as a - so instead try clicking just inside the first double quote, then and drag down (it's shorter that way), but remember you will pick up the trailing double quote if you do this.
So far we've had to enter the URL manually, to get to NSwag's Swagger documentation page. Although you could use duplicate the tab (see if your browser supports this, right click on your browser tab), we'll create a menu link for this in our view.
Since the NSwag Swagger docs are going to be secured, we'll hide them from people that have not yet logged into the web application.
Go to the Views folder, to the AppComponent.cshtml
page edit and add just below this code:
<li [hidden]="!isLoggedIn()">
<a class="nav-link" (click)="setTitle('About - A2SPA')" routerLink="/about">About</a>
</li>
The following:
<li [hidden]="!isLoggedIn()">
<a class="nav-link" (click)="setTitle('About - A2SPA')" routerLink="/about">About</a>
</li>
<li [hidden]="!isLoggedIn()">
<a class="nav-link" href="/swagger">NSwag</a>
</li>
Notice we've used a standard hyperlink as opposed to the Angular 2 routerLink as we're navigating away from the rest of our SPA.
Once again rebuild and refresh your browser if it is already open, or hit ctr-F5 if it is not, you should see the new menu item:
Right click the new NSwag menu entry and open this in a new tab, leaving the logged-in "About" tab open for reference. In the about tab press F12, find the sessionStorage grab a copy of the bearer token, then switch to the tab looking at NSwag's Swagger docs, click the Authorise button at the top,
In the "value" text box enter the word Bearer followed by a space then paste the bearer token you've just copied:
Click the Authorize button, nothing apparent happens, the window closes, but you'll now find the requests will be authorized with a bearer token.
Open up the list of operations, click Get - to get all, as we tried earlier
Then once again click try it out:
And finally, we have authorization and a test-bed for our developers or our end-users.
Note: if you want to limit this to developers, one quick method is to wrap the configure code in Startup.cs inside
if (env.IsDevelopment())
{
}
You can easily now see what is happening during development (which you can do of course with Postman or similar tools) except here the documentation is generated automatically around your API.
The last thing we'll add, again with view of code that does more for us, will be to expand the descriptions on our Web API methods to provide further tips and pointers to consumers of the new Swagger documentation.
Refining the Swagger documentation with NSwag
Initially our documentation may seem great, automatically created, with little or no work, but as you start adding more and more services, operations and data types, your users are likely to start asking for more information.
Consider the GetById
method
Compare this with the code that it is documenting:
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var testData = await _context.TestData
.DefaultIfEmpty(null as TestData)
.SingleOrDefaultAsync(a => a.Id == id);
if (testData == null)
{
return Json(NoContent());
}
return Json(Ok(testData));
}
Position your cursor to the left of the [HttpGet]
attribute, press enter, then up arrow then hit the forward slash key 3 times, you'll automatically create the shell of a comment:
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var testData = await _context.TestData
.DefaultIfEmpty(null as TestData)
.SingleOrDefaultAsync(a => a.Id == id);
if (testData == null)
{
return Json(NoContent());
}
return Json(Ok(testData));
}
You can delete the previous comment
And then fill in the details with something a little more meaningful, say:
Hit enter at the end of the </summary>
closing tag, you'll create an empty line, type <r
and intellisense will give you <remarks>
, press enter, here you'll get an opportunity to add an further remarks.
And lastly the trap I fall into all of the time. You might almost think it should work, with all we have now. Since adding these comments now means our methods have extra help when hovering or for intellisense - so at least we may be helping other developers.
NSwag will automatically use the XML documentation once there is an XML file to refer to, however this is not generated by default. And this is one of those settings typically done once and then forgotten about, that is actually turning on XML documentation.
In VS2015 right click the project, then click properties, and then select "Build", then at the bottom of the properties, ensure that there is a tick in the checkbox "XML Documentation File"
Similarly, if using VS2017 right click the project, then click properties, and then select "Build", and as with VS2015, at the bottom of the properties, ensure the checkbox "XML Documentation File" is ticked:
Now rebuild, hit ctrl-F5 or refresh your swagger docs page, and you should see the results of our efforts:
When closed we get a comment added to the right:
Opened, we get much more information:
There are many more features, too many to add here without this becoming more about NSwag.
You can expand the other methods, adding documentation to the other methods as you like, or refer to the final source for further ideas.
Automatically creating the Typescript data models and services for Angular2
Before we turn our attention to deployment, while still working with NSwag and in the automation mindset, one last piece is automation of the Typescript data models and Typescript data services used in for our Angular 2 client side code.
As we've generated the Swagger docs, you will have seen the Swagger formatted JSON data, for example:
http://localhost:57149/swagger/v1/swagger.json
The swagger.json file is generated by NSwag as it looks over your Web API classes, and then consumed in the swagger client which you see when you navigate to ../swagger path noted earlier.
This same swagger.json file can also be used by the NSwag Studio tool. NSwag Studio is also from Rico Sutter, creator/maintainer of NSwag, and can be installed from here:
The above link is from the GitHub repo of NSwag here: https://github.com/NSwag/NSwag
Once installed, grab the URL of the swagger.json file from your swagger page:
The in NSwag Studio, select the Documents Tab on the left, then "Swagger Specification", paste in the swagger.json URL you just copied from your swagger page. The click the "Create Local Copy" button to fetch a copy of the JSON data into NSwag Studio:
Now on the right hand side of NSwag Studio,
Check the box marked "TypeScript Client", you will then see a new tab appear:
Select the TypeScript Client tab, change the Typescript version from 1.8 to 2.0:
Next scroll down, and select "Angular 2" from the choices in the "Template" drop down.
We'll leave the defaults as-is for now,
Click Generate Outputs:
This is fine if you want to cut and paste back into your source, but a better way is to set up NSwag Studio so tht it writes back directly into your Visual Studio project.
To generate the typescript file directly, at the bottom of the typescript settings page click on the ".." button to the right of the "Output file path:" entry box. Navigate to you’re the wwwroot\app folder within your project's file space. Then add the name (say) nswag to create nswag.ts
Click the Save button then click Generate Files. Back in Visual Studio, right click the app folder, then select Add, and then Existing Item.
Pick out the wwwroot\app\nswag.ts file
Next to use our generated file, in place of the existing files. Update /wwwroot/app/app.module.ts fie and change these sections of code:
From existing:
import { SampleDataService } from './services/sampleData.service';
import { TestData } from './models/testData';
To the new:
import { SampleDataClient, TestData, SwaggerException } from './nswag';
From:
constructor(private sampleDataService: SampleDataService, private toastrService: ToastrService) { }
To:
constructor(private sampleDataService: SampleDataClient, private toastrService: ToastrService) { }
The change the names of our data service calls (intellisense will hint here), from these:
this.sampleDataService.addSampleData(this.testData)
…
this.sampleDataService.getSampleData()
…
this.sampleDataService.editSampleData(this.testData)
…
this.sampleDataService.deleteRecord(itemToDelete)
To these:
this.sampleDataService.post(this.testData)
…
this.sampleDataService.get()
…
this.sampleDataService.put(this.testData)
…
this.sampleDataService.delete(itemToDelete.id)
Save, clean and rebuild the project, then hit ctrl-F5 and voila, it should work as before:
Links
For further information on NSwag:
NSwag source on GitHub here: https://github.com/NSwag/NSwag
NSwag Sample source on GitHub here: https://github.com/NSwag/Samples
and author and maintainer of NSwag, Rico Suter, his blog here: http://blog.rsuter.com/
Points of Interest
(Q) Did you learn anything interesting/fun/annoying while writing the code?
(A) Reminded again of the annoying US spelling for some words. And how "Authorization" tends to work better than "Authorisation" - when sending tokens, funnily enough, it doesn't get Authorised er Authorized.
(Q) Did you do anything particularly clever or wild or zany?
(A) Love the code generation features of NSwag and NSwag studio ... why do code by hand when you can automatically generate or re-generate it, and have this from one place - your data model.
History
Part 1 - How to integrate ASP.Net Core and Angular 2
Part 2 - How to use Tag Helpers to display data
Part 3 - How to use Tag Helpers for data input, added SQL backend using EF Core.
Part 4 - Adding token authentication with JWT using OpenIdDict.
Part 5 - Adding async services, server-side validation, a simple Angular 2 datagrid & more tag helpers.
This is part 6 - Where we're adding Swagger documentation for our Web API methods using NSwag, and then using the (free) NSwag Studio tool to automatically create Typescript data models and data services for Angular 2.
Part 7 will cover deployment of our SPA / Single Page Application to IIS.