Do you make this mistake in your .NET Core API project?


Data models are the back-bone of any .NET Core API server.

At least, that's been my experience.

And building on the data models are the API controllers that give the world access to your data.

I%20have%20the%20data

A common mistake I often see is a .NET API server project that has repeated CRUD operations for each data model. In other words, you have CRUD functions for each data model. And every time a new data model is added to the project you have to create a new controller and write the CRUD end-points from scratch.

Why is this a mistake?

Because it adds repetitive code to your .NET Core application. And because any developer worth his dough knows that the fewer lines of code it takes the better. Elon Musk says that he would pay developers at least twice as much for removing lines of code from an application without removing functionality.

So, wouldn't it be clever to create a base controller with CRUD (Create, Read, Update, Delete) functions for every data model you have?

Depending on the size of your project you could save yourself thousands of lines of code.

delete

And become the hero of your development team? 🤩

Today, I'll give you the complete guide on how to create CRUD controllers for your data models. Without breaking DRY principles. 🥳

Yes buddy, using a base controller in your .NET Core API application will make you shine. And it's not complicated. In fact, it's so easy to do it's underwhelming.

So, how do we create a CRUD controller for every data model in our .NET Core project?

Create the demo API server with .NET Core

Note: You can find and clone the entire repository on GitHub.

For this post, I'll create a demo project using the .NET CLI.

dotnet new webapi -o BaseController

This will generate a basic API server with one controller called the WeatherForecastController.cs.

base%20controller%20.net%20core%20project

On to the next important step!

Installing and configuring Entity Framework Core

To keep this demo simple, we'll use the InMemory package that allows us to spin up a fake database in memory. Here's how we install it.

dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 3.1.15

Next, we'll create our database context. I put the code below in a file called BaseControllerDBContext.cs.

using Microsoft.EntityFrameworkCore;

namespace BaseController
{
    public class BaseControllerDBContext : DbContext
    {
        public DbSet<WeatherForecast> WeatherForecasts { get; set; }
        public BaseControllerDBContext(DbContextOptions<BaseControllerDBContext> options) : base(options)
        {

        }
    }
}

And last of all, we'll need to add our database context to the Startup.cs file.

public void ConfigureServices(IServiceCollection services)
{
            services.AddDbContext<BaseControllerDBContext>(options => options.UseInMemoryDatabase("BaseController"));
            services.AddControllers();
}

Good going! This is coming together lickety-split!

Create the data models

Our next step will be to create a base class that all other data classes will inherit from.

Here's what it looks like.

using System;

namespace BaseController
{
    public class BaseModel
    {
        public Guid Id { get; set; }        
    }
}

To keep the example simple, I've only included an Id field. But for other scenarios it's common to also add a Created and Updated and anything else that you want all of your data models to include.

Now we need to update the WeatherForecast class to inherit from our base model.

using System;

namespace BaseController
{
    public class WeatherForecast : BaseModel
    {
        public DateTime Date { get; set; }

        public int TemperatureC { get; set; }

        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

        public string Summary { get; set; }
    }
}

Bravo! Our data models are ready! The next step is to create a base controller with CRUD operations.

Create base controller

We've reached the secret sauce. This, my friend, is where you will start to shine. So sit up and pay attention.

In our Controllers folder we'll create a file called BaseController.cs. This API controller is going to have 5 different CRUD functions.

  • Create a record
  • Get a record
  • Get all records associated with type
  • Delete a record
  • Update a record

Here's the code.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace BaseController.Controllers
{
    [ApiController]
    public class BaseController<TEntity> : ControllerBase where TEntity : BaseModel
    {
        protected readonly BaseControllerDBContext _context;
        protected DbSet<TEntity> _dbSet { get; set; }
        public BaseController(BaseControllerDBContext context) 
        { 
            _context = context;
            _dbSet = _context.Set<TEntity>();
        }

        [HttpGet]
        public virtual async Task<IEnumerable<TEntity>> GetAllAsync()
        {
            return await _dbSet.ToListAsync();
        }

        [HttpGet("{id}")]
        public virtual async Task<TEntity> GetAsync(Guid id)
        {           
            return await _dbSet.Where(e => e.Id == id).FirstOrDefaultAsync();
        }

        [HttpDelete("{id}")]
        public virtual async Task DeleteAsync(Guid id)
        {           
            var entity = await _dbSet.Where(e => e.Id == id).FirstOrDefaultAsync();
            _dbSet.Remove(entity);
            await _context.SaveChangesAsync();
        }

        [HttpPost]
        public virtual async Task<TEntity> CreateAsync(TEntity entity)
        {           
            await _dbSet.AddAsync(entity);
            await _context.SaveChangesAsync();
            return entity;
        }

        [HttpPost]
        public virtual async Task<TEntity> UpdateAsync(TEntity entity)
        {           
            _dbSet.Update(entity);
            await _context.SaveChangesAsync();
            return entity;
        }
    }
}

Nice going! Things are coming together fast. And we're almost done!

Extend that controller

Now, we'll grab the WeatherForecastController.cs file and change it to inherit the functionality of the base controller.

Here's what it needs to look like.

using Microsoft.AspNetCore.Mvc;

namespace BaseController.Controllers
{
    [ApiController]
    [Route("api/weather-forecast")]
    public class WeatherForecastController : BaseController<WeatherForecast>
    {
        public WeatherForecastController(BaseControllerDBContext context) : base(context)
        {

        }
    }
}

BOOM! 💥

You've created an end-point for the WeatherForecast data model without having to re-create CRUD functions for that model.

Extra: How do we add sorting, pagination and filtering?

Now that we've created a basic API, how do we extend it even further to add the ability to sort our results? Or send paginated responses? Or even search for specific values in our database?

Pagination isn't that hard. In our BaseController.cs we can do something like this.

[HttpGet]
public virtual async Task<IEnumerable<TEntity>> GetAllAsync(int count = -1, int skip = -1)
{
    if (count != -1 && skip != -1)
    {
        return await _dbSet.Skip(skip).Take(count).ToListAsync();
    }

    return await _dbSet.ToListAsync();
}

The count field defines how many results to return and the skip field defines how many database rows to skip.

But what about sorting and filtering?

This is a bit harder because the properties in our data models are different. We'll need to override some functionality in our WeatherForecaseController but first, let's install a new Nuget package.

dotnet add package System.Linq.Dynamic.Core

Then, modify the base controller a bit to take a search parameter and a sort parameter.

[HttpGet]
public virtual async Task<IEnumerable<TEntity>> GetAllAsync(int count = -1, int skip = -1, string searchTerm = null, string sortBy = null)
{
    if (!String.IsNullOrEmpty(searchTerm))
    {
        throw new Exception("Cannot search in base controller.");
    }

    if (!String.IsNullOrEmpty(sortBy))
    {
        throw new Exception("Cannot sort in base controller.");
    }

    if (count != -1 && skip != -1)
    {
        return await _dbSet.Skip(skip).Take(count).ToListAsync();
    }

    return await _dbSet.ToListAsync();
}

Finally, in our `WeatherForecaseController we'll override the search and sort functionality like this.

[HttpGet]
public override async Task<IEnumerable<WeatherForecast>> GetAllAsync(int count = -1, int skip = -1, string searchTerm = null, string orderBy = null)
{
    if (!String.IsNullOrEmpty(searchTerm))
    {
        return await _dbSet.Where(d => d.Summary.Contains(searchTerm)).ToListAsync();
    }

    if (!String.IsNullOrEmpty(orderBy))
    {
        return await _dbSet.AsQueryable().OrderBy(orderBy).ToListAsync();
    }

    return await base.GetAllAsync(count, skip, searchTerm);
}

Notice how I'm only running a search on the Summary field? That's to keep this example as simple as possible (keep it simple stupid - kiss). 😋

Of course, you can configure your project to search what-ever-the-properties-you-like.

And in case it's helpful, you can find and clone the entire repository on GitHub.

Conclusion

In this article we've discovered how to create a base controller with CRUD operations. We then took that base controller and used it to create an API end-point for our other data model without re-coding the CRUD operations.

This approach could save you thousands of lines of code. And it will make you a more productive developer. Cool stuff if you want to know what I believe!

Questions? Comments? Don't hesitate to contact me.

signature

Angular Consultant