Building a RESTful eBOL in Microsoft Dotnet Core

Building a RESTful eBOL in Microsoft Dotnet Core

Aside from Java, one of the most versatile and portable stacks you can implement any API in happens to be the Microsoft stack. With the National Motor Freight Traffic Association's Digital LTL Council formulating new LTL industry standard APIs at a record pace, I receive many questions about how-to implement.

With that, I've put together a brief implementation guide that I feel covers many of the frequent questions I am asked. Implementing this API does not need to be a significant undertaking, nor does it require a significant amount of resources.

In this example project implementation, we will cover the following:

  1. Creating a Dotnet Core API Project / Solution
  2. Creating Required Database Objects Using Code-First
  3. Building a Repository Pattern and Unit of Work
  4. Creating HTTP Methods in a Controller
  5. Securing Your API
  6. Properly Document your API using Swagger / OpenAPI

What we will use for this project:

  1. Dotnet Core 7.0 (though I recommend 6.0 or the next LTS release)
  2. Visual Studio Code
  3. PostgreSQL accessible from your local machine.

A video version of this can be found here.

Creating a Dotnet Core API Project/Solution

You may create your project in one of two ways.

Visual Studio

I open Visual Studio 2022, select Create New Project.? I select the web application project, name it, and create it.

Visual Studio Code

From my command line, I create a directory for my new project, and then change to that directory.? I enter dotnet new webapi, knowing that by default it will create the requisite controllers, models, and root Program.cs file that I need to get started.?

You’ll note, too that the two projects are entirely similar, except that the one I created in Visual Studio is using .NET 6.0, which is in LTS, and by default, the CLI uses the .NET 7.0 as its target framework.? I truly do recommend using 6.0 until the next long-term support (LTS) release.?

For the duration of this example, we will focus on using Visual Studio Code.?

Creating Required Database Objects Using Code-First

The first thing I will do is install all our dependencies, some of which have to do with the database, some of which do not.

Our .csproj file

Here is the list:

??????????? Microsoft.AspNetCore.Authentication.JwtBearer

??????????? Microsoft.EntityFrameworkCore

??????????? Microsoft.EntityFrameworkCore.Design

??????????? Newtonsoft.Json

??????????? Npgsql.EntityFrameworkCore.PostgreSQL

??????????? System.Linq.Dynamic.Core

PostgreSQL’s implementation requires a third party, community-maintained set.? I always include System.Linq.Dynamic.Core lately because of the potential to execute queries with dynamic strings, which I generally try to avoid, but sometimes you just need that flexibility.

We are creating a code-first project here, as in we will create our database models and then create migrations that create and update our database.

Creating our Models

In my example project, because I am assuming I develop this as part of an existing Fleet Management system, we will create a base model for all our objects that contains a unique identifier as a primary key field, always called Id, a DateTime object called CreatedDate, and a nullable DateTime field called UpdatedDate.

BaseModel.cs

You’ll note that we explicitly define that the key is a database generated identity, which with PostgreSQL requires some finagling in a dotnet core project.

We’re going to create a Company model first.? This will inherit our BaseModel, and it will contain the following fields:

A string field for Name A nullable string field for MeasureUnits

Three decimal fields, LimitTemperature, IdealPressure, and DeltaPressure

A nullable DateTime filed called alert.

Most of these fields are fluff to add a little dimension and color to this project. They’re not actually of any real value here.?

Company.cs

The Contact Model also inherits our BaseModel and includes a variety of data that we usually need to contain a generic set of company or individual information.

Contact.cs

Now I will not go into creating all the objects for the BOL database objects, all nested.? You may refer to the example project to collect those.? The top-level bill of lading object, too, inherits our BaseModel object.

I am saving many details from the BOL requests to that Contact table, because it would make the most sense in my data hierarchy. You will likely find the same thing as you integrate with your systems.

I also create specific request and response models.? I prefer to use specific Data Transfer Objects (DTOs), especially for requests, rather than trying to bloat my persistence entities with non-persistence related annotations.

We also may want to add links for Hypermedia as the engine of application state (HATEOAS).? This is entirely your preference but consider the value in your implementation of the e-Bill of Lading of providing a definitive link to all or part of a created BOL with retrieval APIs.? These should never appear in a model used for persistence.

Creating a Data Context?

After we create our models, it’s time to write the Data Context, so we can move on to standing up our database.

Because I am using PostgreSQL and intended to use unique identifiers for my primary key fields, you’ll see right away we must introduce some variances to code that I wouldn’t have to with SQL Server.

We’re going to override our DbConnectOptions first and set our API to use a legacy timestamp behavior.? In this case, when it reads a timestamp with a time zone it does so as a DateTimeOffset object and returns a local offset based on the time zone of our server/container.

from BaseDataContext.cs

I also override OnConfiguring to log to console for trace logging during development.? This is an optional step.

from BaseDataContext.cs

In OnModelCreating, this is where I get a little creative with enabling PostgreSQL to autogenerate our primary keys as unique identifiers.? PostgreSQL must have the uuid-ossp extension so that it may generate universally unique identifiers using standard algorithms.? We also iterate our model entities, look for the ubiquitously named Id field, and set the default value to be the Postgres unique identifier generation function.

from BaseDataContext.cs

I can also return to this override function and run the seed, after I create all the DbSet properties I will use in this system.?

Let’s go to our Program.cs and add the DataContext with our connection string.? You’ll note I intentionally use Environment.GetEnvironmentVariable vs. references to AppSettings and configuration.? For one, this gives me control in development and in production thanks to a margin of platform agnosticism, and fewer opportunities for a well-intentioned developer to accidentally check in toxic configuration data.

from BaseDataContext.cs

Opinions vary on this, so do what you believe to be appropriate according to your internal governance.

Finally, I go to my command line and enter the following:

dotnet ef migrations add InitialCreate

If that progresses without issue, I then run:

dotnet ef database update –connection connection

You may wonder why I include the connection string in this, and it’s simple.? Because I use Environment variables to store my connection strings, those are not available at build time, as they would be in an AppSettings location.

Building a Repository Pattern and Unit of Work

If you aren’t familiar with the repository pattern, it’s okay.? It’s my preferred design pattern.? Even though I don’t often sweat developing for future state/scenarios where we migrate code, because we are working with a specific database product in this system, you may want to change to Microsoft SQL.? The advantage here becomes very apparent, very quickly.

Let’s say I want to replace EntityFramework tomorrow and solely use Dapper.? I don’t have to change any of my functional code to make that happen.

I also have learned the hard way that it’s not ideal, nor is it secure, to use an exposed context within controller functions.

I also really don’t like typing.

So before I create my generic interface, I like to do a PagedResponse class.? This would be a good place, if I so chose, to write HATEOAS links, but I opt not to for these purposes.

PagedResponse.cs

I create a static function ToPagedResponse that takes a generic IQueryable which performs Count and Skip to retrieve paged records.

I next create my generic repository interface.

IDataRepository.cs

I now create my generic class implementation of that generic interface.

DataRepository.cs

This means, then, that for Company and Contact, I can inherit my generic class implementation of my generic interface, and all my CRUD options are implemented with no additional typing.? And if I need to extend them, as I will with the Bill of Lading, life is good.?

The Digital LTL Council’s API specification includes specific request and response objects, which differ from how it makes sense for me to abstract that data in my existing schema, so I use them as DTOs for my underlying data structure.?

I’m also conscientious that I’m using version 2.0.2 of the eBOL specification, so I need to ensure that is annotated, in case I want to additionally support previous or later versions.? So I create an interface called IBOLv2Repository, inheriting our generic interface with a type of BOL, our internal database object.

IBOLv2Repository.cs

I add a function in this interface with an asynchronous return type of BOLResponse, and I’ll call it AddWithBOLResponse.? It takes a BOLRequest as our incoming payload.

I then implement this in a class version of the same, called BOLV2Repository, inheriting both the generic repository class with the BOL entity type, and our BOLv2Repository interface.

I must write out the incoming response object to the matching layers in our context design.? I leave commented placeholders for a method that will create a PRO number if the shipper didn’t include one, a method for generating or retrieving our internal shipment reference number, and PDF generation/retrieval for our printable bill of lading and shipping labels.

from BOLv2Repository.cs

You’ll note that our repositories do not contain a SaveChanges call.? Let’s say you have a requirement to update two separate pieces of data in one transaction.? What would happen if we were able to save one element of data but not the other?? For the integrity of our data, we save changes on a unit of work interface.

I create this as a combined service and unit of work.? Note that you could further split this into a service abstraction of the unit of work, but I’m melding them into one for the sake of efficacy.?

IUnitOfWork.cs
UnitOfWork.cs

I then add this to my Program.cs dependency injection with AddTransient.? We’re going for lightweight and stateless, believe it or not, so this works well for us.? You could probably get away with AddScoped as needed, but I haven’t any problems with using this in my design patterns thus far.?

from Program.cs

This will save us so much time in writing line upon line of injections to any controllers in our project.

Creating HTTP Methods in a Controller

Building our controller is a simple operation now that we have our Data Context, our repositories, and our unit of work service created.

I will add all the following methods:

GetAll

GetById

Create

Update

Delete

They will use our Http verb attributes as respective to their function.

I will do the same with our Contact controller.? In all cases, you will note I add the attributes for response types.?

an example API method from ContactController.cs

When we come to our BOL Controller, I’m starting with just two methods – our create and our retrieval.? Because I am again writing these against the version 2.0.2 specification, I am adding v202 to the route.? This way, if I add repos for earlier versions later, I can create separate functions for them.?

Our BOL Create function from BOLController.cs

It’s really that easy when you put in the levels of effort below this.? You may have other complexities you want to introduce as well, but these are the starting points for your controller actions.

We can run the project at this point, and we can already get a glimpse at the generated Swagger.?

Looking good!? Our endpoints are all there, our objects appear as we’d expect.? Let’s do some work to secure this, and then we’ll improve our generated documentation.

Securing your API

The NMFTA will not make any specific recommendations about what security to use for your API implementation.? Much of it will depend on your implementation.? Perhaps you’re folding the bill of lading API into an existing fleet management system that is using some variation of single sign on with multifactor authentication from Microsoft, Amazon, Okta, or Auth0.? Perhaps you’ve rolled your own implementation.

What I can say is that if you understand dependency injection, it is remarkably simple to secure your API.? There are other means, too, that are not in the purview of the NMFTA in an advisory capacity, such as configurations in Azure API Management or Amazon Web Service’s API Gateway.

Since this is a dotnet specific example, we will go over how we will implement this in an API that we assume will be supporting an Angular or React front end, i.e. the authentication will take place between in this case Auth0 and the UI, and we will take a Jason Web Token (JWT) as an Authorization header element on any API request.? We can secure entire API controllers or individual calls with a combination of Authorization attributes and authorization policies.

I’m logging in to my Auth0 account and adding the following permissions/scopes to my API, read:data and write:data.

Now I will also implement my authorization in Program.cs.? No special framework is required with Auth0 beyond what we’ve already added.? We add a JwtBearer scheme, with environment variables for Authority and Audience.

Check out the source code for this example to see details of what we're doing with scope handlers.

We then go and mark up our secured functions accordingly.? I want people logged in to read or write data, so we add the right scope to both.

If you are maintaining your own roster of assigned keys in a database table, you may implement your own middleware that looks those up and validates those keys, but this generally will apply to all calls in your API.? You may be more granular by implementing your own custom attribute that you decorate your functions or API controllers with.? Both solutions are widely documented and are searchable on the Internet.

Properly Document your API Using Swagger / OpenAPI

One of the last things we’ll want to do is ensure that our autogenerated Swagger documentation tells this API’s story.? Whether it’s something you’re making available to partners for integration, or for your own internal development team to use and consume, it’s an important step, and there are often many questions about how this works.

Not only that, but we also want it to correctly add your security to calls as they are implemented.? It’s useful for testing, but also for correctly implementing in your associated UI, if that is what you are doing.

The first step is to configure our general Swagger documentation.? Since I am creating this as version one of my API, that Is identified.? I add my overall security definition as well.

from Program.cs

Then, you’ll note, I add a customer operation filter that evaluates each controller and method for the presence of the Authorize attribute, and places that in the OpenAPI documentation accordingly.

SecurityRequirementsOperationFilter.cs

Conclusion?

NMFTA works hard with our partners in the Digital LTL council to ensure we have put together a decent industry standard.? In the modern LTL workspace, integrations with shippers and third-party logistics companies require significantly more agility than the old EDI standards allow.? Just as we demonstrated with our Java Spring Boot example, I hope this demonstrates for you how easy it is to implement the e-Bill of Lading API standard.

Thank you, and good luck in your own integrations!

Source code for this project sample can be found here: https://github.com/parkerfly38/dotnet-bol-example

要查看或添加评论,请登录

社区洞察

其他会员也浏览了