Nano ASP.NET SaaS Boilerplate
Admin credentials (all tenants): admin@email.com / Password123!
Sample data resets every hour
Nano ASP.NET SaaS Boilerplate
General
Front-End Development
Back-End Development
Back-End Development

ASP.NET Web API Backend: Part 3

In this lesson, we will make our data model more complex by adding another entity for suppliers, which will form a one-to-many relationship with products. We will add a Suppliers API controller for the new entity. Then we’ll see how we can query and return related data in our APIs.

This is a continuation from the previous two guides. In the first lesson, we set up an ASP.NET Web API project and learned how to create RESTful API controllers to perform CRUD operations. In the second lesson, we added Entity Framework and a SQL Server database to persist and manage data.

If you would like to follow along, you can download the code samples on our GitHub page for free.

Begin by creating a new entity class in the Domain folder called Supplier. Add the following properties to define the model.

    public class Supplier
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Address { get; set; }
        public ICollection<Product> Products { get; set; } // navigation property
    }

Entity Framework can infer a lot of information about relationships just by following conventions. In a simple one-to-many relationship like the one we will create, all we need is add a field called SupplierId to the Product entity. Whenever Entity Framework detects an entity name plus Id, it treats it as a foreign key and creates a one-to-many relationship.

    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public int SupplierId { get; set; }
        public Supplier Supplier { get; set; } // navigation property
    }

Just going this far won’t allow us to query the related data however, so it’s important that we also add the Supplier entity as a property. Also we should add an ICollection for Products in the Supplier entity. These are what are known as navigation properties. They don’t actually make any change in the database, no columns are added, but they allow Entity Framework to find related entities when you construct queries. In other words, they only affect in memory operations not persistent ones.

Finally, add the DbSet for Suppliers in the ApplicationDbContext so that Entity Framework knows about the new domain entity.

    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
        // -- add DbSets here for new application entities
        public DbSet<Product> Products { get; set; }
        public DbSet<Supplier> Suppliers { get; set; }
    }

Now that we have made changes to our data model, we should create a new migration using the add-migration command and name it something like AddSupplierEntity. Then use the update-database command to apply the changes. When that’s complete, the database will mirror the data model as it is currently. If someone else were to copy the project and run update database, all the migrations would be applied in sequential order and their database would be identical.

One thing to note however is that if you already had some products created in your database, Entity Framework will throw an error. The reason is that the new SupplierID column requires a value, and the existing rows of products won’t have one. For this example, just delete the existing products and run the command.

In the case that you needed to keep the data, you would first create the column as nullable by adding a question mark to the property. You would create a migration for this and update the database, manually adding in values for the SupplierID. When none contain a null value, create another migration, this time removing the question mark, denoting that the SupplierID is now required. When you run the update-database command again, no errors will get thrown.

We can build another API controller now for the Supplier entity which will look mostly like the Products controller we made earlier. Start with the Create method and add the following code:

        // create
        [HttpPost]
        public IActionResult CreateSupplier(CreateSupplierRequest request)
        {
            var newSupplier = new Supplier()
            {
                Name = request.Name,
                Address = request.Address,
            };

            _context.Suppliers.Add(newSupplier);
            _context.SaveChanges();

            var response = "Create a new supplier with ID: " + newSupplier.Id + " and NAME: " + newSupplier.Name;
            return Ok(response);
        }

Add the CreateSupplierRequest and UpdateSupplierRequest DTO classes in the DTO folder, both will contain the same properties for now.

    public class CreateSupplierRequest
    {
        public string Name { get; set; }
        public string Address { get; set; }
    }

Run the application. We need to create a few new suppliers; taking note of the Ids they are assigned. To do so, use Postman and create a new post request. Send a supplier object in the body of the request. New Ids will be assigned by the database automatically and in sequential order.

Back in the Products controller, we need to make some changes. Any new products we create must have a SupplierId value. In the CreateProductRequest, add a new field for SupplierId. Also, add this property to the new product entity that is passed to the database. Perhaps in our application, the supplier can only be assigned when a product is created, so let’s not add anything new to the update method.

    public class CreateProductRequest
    {
        public string Name { get; set; }
        public string Description { get; set; }
        public int SupplierId { get; set; }
    }

Open up Postman and create a new product. This time add the SupplierId field to your JSON object. We must send a valid SupplierId when we create the new product otherwise Entity Framework will throw an error. This is one of the features provided by Entity Framework; enforcing data integrity. Create a few new products.

Next we’ll see how to query related data, starting with getting a list of products. In Postman, send the request to get a list of products. Notice that while we do see the SupplierId being returned, the Supplier property is null. This is because Entity Framework will not load related data unless we explicitly tell it to do so. 

We need to add an include clause to our query to explicitly load the related supplier entity for each product. Include is a method provided by Entity Framework so we need to add a using statement at the top of the class. Within the include statement pass a lambda expression like this:

var response = _context.Products.Include(x => x.Supplier).ToList();

Now run the application and try to get a list of products. Technically its working so no error will get thrown in the code, but the response will show an error saying that ‘A possible object cycle was detected’. What’s happening is that in the list, we return a product and this product has a supplier. But the supplier also has a list of products, and then these products have the supplier, and so on, repeating forever. To better understand this, set a breakpoint where we return the OK response and you’ll see if you expand the entities.

It’s bad practice to return domain objects from your APIs in any situation. Instead, we should create DTOs which only expose the data we want to send back to the client. Often times, the domain entities will contain fields which we don’t want to share publicly, like audit fields i.e. created by, created on, last modified by, etc. It’s also common to combine some data from two different entities or return fields with computed values. Creating DTOs for suppliers and products will fix the issue with circular data.

In the DTOs folder create a new class called ProductDTO. This class will contain the same fields as the Product entity but instead of an object for supplier, we will just have a string property for SupplierName.

    public class ProductDTO
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public string SupplierName { get; set; }
        public int SupplierId { get; set; }
    }

Create a SupplierDTO and instead of an ICollection of Product entities, create a List of ProductDTOs.

    public class SupplierDTO
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Address { get; set; }
        public List<ProductDTO> Products { get; set; }
    }

Back in the products controller, we need to create a list of ProductDTOs and return that instead of the domain entities in the get list of products endpoint. This code iterates over each entity object in the response, and maps them to a new ProductDTO. Each DTO is then added to the DTO list that we return to the client.

        // full list
        [HttpGet]
        public IActionResult GetProducts()
        {
            var response = _context.Products.Include(x => x.Supplier).ToList();
            var products = new List<ProductDTO>();

            foreach (var item in response)
            {
                var product = new ProductDTO()
                {
                    Id = item.Id,
                    Name = item.Name,
                    Description = item.Description,
                    SupplierId = item.Supplier.Id,
                    SupplierName = item.Supplier.Name,
                };
                products.Add(product);
            }

            return Ok(products);
        }

Now try to get a list of products from by calling the endpoint from Postman. All should be working fine without issues and we’ll see the DTO response in the results window.

You can do the same thing in the get product by ID endpoint. Update and delete methods don’t need any changes.

In the get list of suppliers endpoint, add an include statement to load the related products for each supplier. Add a using statement for Entity Framework. As you may predict, when we run the app and call this endpoint, the object cycle issue will arise again.

To solve the issue, we need to do some similar steps. First we need to iterate over each supplier entity returned in the query and map the entity data to a new SupplierDTO. We also need another loop nested within, to iterate over the related products and map them to DTOs. This list of ProductDTOs will be added to each SupplierDTO. The SupplierDTOs will be added to a list and that will be returned.

// full list
[HttpGet]
public IActionResult GetSuppliers()
{
    var response = _context.Suppliers.Include(x => x.Products).ToList();
    var supplierList = new List<SupplierDTO>();
    foreach (var supplier in response) {

        var productList = new List<ProductDTO>();
        foreach (var product in supplier.Products) {
            var productDTO = new ProductDTO()
            {
                Id = product.Id,
                Name = product.Name,
                Description = product.Description,
                SupplierId = product.SupplierId,
                SupplierName = supplier.Name
            };
            productList.Add(productDTO);
        }



        var supplierDTO = new SupplierDTO()
        {
            Id = supplier.Id,
            Name = supplier.Name,
            Address = supplier.Address,
            Products = productList,
        };
        supplierList.Add(supplierDTO);
    }

    return Ok(supplierList);
}

Run the application and call the endpoint from Postman to see the results.

You can do the same thing for the get supplier by ID endpoint, this time for a single supplier.

One other thing Entity Framework does to enforce data integrity is cascade delete related entities. This means that if we ever delete a supplier, all of the products that are children of that product will also get deleted. This cascade delete behavior is the default action whenever we set up a one-to-many relationship. All of the default behavior can be changed however by setting explicit configurations on each entity.

One last thing we can do in preparation for the next section, is add a CORS policy. CORS stands for Cross-Origin Resource Sharing. By default as a security feature, an ASP.NET Web API will not allow API requests to be made from any other origin other than itself. Usually a front-end client is hosted by the ASP.NET Web API itself as static files, meaning that it resides on the same origin when making requests. However, when we are developing, that front-end client is going to appear as if its from a different origin. The solution is to set a new CORS policy that allows any origin when in development mode.

In the main Program.cs file of our Web API application, add the following code to create a new CORS policy called defaultPolicy:

// allow cross origin requests
builder.Services.AddCors(p => p.AddPolicy("defaultPolicy", builder =>
{
    _ = builder.WithOrigins("*").AllowAnyMethod().AllowAnyHeader();
}));

Then add this code within the block for app.Environment.IsDevelopment to activate that policy when in development mode:

app.UseCors("defaultPolicy");

Now we won’t have any issues when we try to make requests from our development front-end client.

We’ve see a few examples of why DTOs are necessary and learned that it’s always good practice to return DTOs to the client instead of domain entities. It may seem tedious to have to map each property of an entity to a DTO class anytime we want to return something. There are libraries though that exist such as AutoMapper and Mapperly which can automate a lot of work when building an ASP.NET Web API.

We also saw how Entity Framework enforces data integrity. It will throw an error if we ever try to add non-existent Ids and perform cascade deletion on related data.

With this all in place, our back-end is ready. In the next lesson, we’ll see how to set up our front-end client to interact with these endpoints. That will complete out full stack introductory course!