DEV Community

Cover image for ASP.Net 5 Web API Basics with C#: A Tutorial
RoryJZauner
RoryJZauner

Posted on • Edited on

ASP.Net 5 Web API Basics with C#: A Tutorial

There are quite a few tools at your disposal, when it comes to creating an API for your product.

In this article we will be looking at .Net 5 as an option. For those of you who do not know, .Net 5 is the open-source framework from Microsoft for building products.

The framework can be used on any operating system. We can now enjoy the type-safety of C# on a Mac or using one of the awesome Linux-Distributions.

What you will learn:

  • Use the .Net SDK to create a new project
  • API Routing
  • Setting up Sqlite
  • API Controller Methods
  • Data Transfer Objects (DTOs)

What you will need

I am using Visual Studio Code in this tutorial and installed the recommended extension.

After that we are all set to start.

Creating a new Project

The SDK comes with a very handy command-line tool that we can use to scaffold just about any project we want.

We will be creating a new Web API Project using the following command:

dotnet new webapi -o webApiCrud

This will download and install all of the necessary dependencies and place them in a new project directory called webApiCrud.

Once everything is installed, we can open up our new project using your IDE of choice.

To check that everything works we can fire up our development server using the following command:

dotnet run

This will build our project and then make it available at https://localhost: 5001/.

You won't see anything, because the route "/" is not configured to return anything. To check that we are returning something from our API, we can run https://localhost:5001/weatherforecast/ and will see a response object.

Now that we have verified that everything works, let us have a look at the routing.

API Routing

If you already have experience with backend frameworks, you will know that routing is one of the more important aspects to get right. Routing essentially exposes the data you want to share with your client.

The first part of our routing takes place in the startup.cs file. It is here where our routing is configured:

app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
Enter fullscreen mode Exit fullscreen mode

This method indicates to the framework, that we are defining our routes within the controllers themselves.

.Net 5 APIs use what is referred to as attribute routing. This means we are defining the route on the controller or the action we want to use to handle the response when the URL is being called.

Working with our example route from above (https://localhost:5001/weatherforecast/) we know that we made a GET Request to that endpoint.

Seeing as each route is mapped to a controller and/or action, we know that we will find the action mapped to our GET Request in the WeatherForecastController.cs file.

Here we can see the following:

    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
Enter fullscreen mode Exit fullscreen mode

We can see the [ApiController] annotation that indicates to our program that this controller is used to return resources.

The [Route("[controller]")] is the part where our routing continues. It essentially defines a route that takes the controller name and removes the "controller" part. That would mean we are left with "weatherforecast" as our route name.

The final part of our routing is a few lines further down. It is the Http verb attribute [HttpGet] that tells our program that the action with this attribute responds to GET requests.

Putting it all together means that when the request was made to the endpoint https://localhost:5001/weatherforecast/, our program passed the request on to the WeatherForecastController who then passed it on to its Get() function.

What if we wanted all of our API routes to begin with "/api"?

We can simply change the Route attribute in our controller to get this done:

    [ApiController]
    [Route("api/[controller]")] //added "api"
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
Enter fullscreen mode Exit fullscreen mode

Now we need to stop the development server with Ctrl + C and re-build our project by running dotnet run again.

If we navigate to our old route https://localhost:5001/weatherforecast/ we will see no response. The resource is now available using our new route https://localhost:5001/api/weatherforecast/.

We can also define route attributes on the actions themselves. Let us assume we wanted to give our action the additional "all" at the end:

[ApiController]
    [Route("api/[controller]/")] //added "api" and "/"
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet("all")]   //defines additional part of name
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
Enter fullscreen mode Exit fullscreen mode

With this we can further specify the route we want our actions to map to.

We need to again stop the development server and re-build our project to see our changes. We can start up a watcher that will watch our project for changes and initiate a build as soon as we save our work. That way we won't have to stop and start each time we change something. To start our watcher we can run dotnet watch run.

Now if we navigate to https://localhost:5001/api/weatherforecast/all we can see our resource being returned.

Now that we have a better idea of how routing works, we can now have a look at setting up a development Sqlite database to persist our data to.

Setting up Sqlite

Be it for development or just to practice, Sqlite is a great way to get up and running quickly without having to set up a whole database system.

We will need to install the following packages:

dotnet tool install --global dotnet-ef

This we will need to create migrations and apply the changes to our database.

dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 5.0.9

Will allow us to connect to and use our Sqlite database.

Another package we will be needing for our tooling is dotnet add package Microsoft.EntityFrameworkCore.Design --version 5.0.9.

We first create a new Dbcontext class that will allow us to interact with our database.

I am going to create a new Models directory in our project root and in then directory I am going to place the ApiContext.cs file:

using Microsoft.EntityFrameworkCore;

namespace webApiCrud.Models
{
    public class ApiContext: DbContext
    {
        public TodoContext(DbContextOptions<TodoContext> options)
            : base(options)
        {
        }

        //place the models being used here
    }
}
Enter fullscreen mode Exit fullscreen mode

You don't have to name your file like that, you can call it whatever you want to.

As you can see from the code above, we are going to be placing our models within the context class so that we can make use of them in our project.

We first need a model to add so in the Models directory create a new file called Note.cs:

namespace webApiCrud.Models
{
    public class Note
    {
        public int Id { get; set; }
        public string NoteTitle { get; set; }
        public string NoteText { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

This is what our table in the database will look like. This is essentially the blueprint for our database.

Now we can update our ApiContext.cs:

using Microsoft.EntityFrameworkCore;

namespace webApiCrud.Models
{
    public class ApiContext: DbContext
    {
        public ApiContext(DbContextOptions<ApiContext> options)
            : base(options)
        {
        }

        public DbSet<Note> Notes { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we can make use of our model to not only create the structure of our database, but we can also use this to make queries.

The next step is to register our ApiContext.cs in our Startup.cs class so that our program is aware of it.

Our Startup.cs will look like this:

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.AddControllers();

            services.AddDbContext<ApiContext>(options => options.UseSqlite(Configuration.GetConnectionString("DevDb")));

            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "webApiCrud", Version = "v1" });
            });
        }

        // 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();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "webApiCrud v1"));
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
Enter fullscreen mode Exit fullscreen mode

You will most likely encounter a missing directive error when writing these lines. In VSCode you can simply hover over the offending element and use the command Ctrl + . or on a Mac cmd + . to receive options as to what to import. In this case you will need to import using webApiCrud.Models for our ApiContext and using Microsoft.EntityFrameworkCore for the UseSqlite-method.

You may have noticed that we are using a connection string for our database. These can be found in appsetting.json:

{
  "ConnectionStrings": {
    "DevDb": "Data Source=./testdb.db"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

Enter fullscreen mode Exit fullscreen mode

That takes care of that.

Once we have completed all of that we can start with our migrations.

Migration

These allow us to create and update our database structure without having to write SQL-Statements ourselves. It also makes sharing your work easier, because a new developer on your team will simply have to run the migrations to get the database structure they need.

With .Net 5 we use a toolset called Entity Framework Core. These tools allow us to manage our database structure and take care of migrating our changes.

The migration process involves first creating a migration file with our changes in it and then applying those changes to our database.

We can create a new migration using the following command:

dotnet ef migrations add InitialMigration

The last part of the command is the name of your migration - you can name it whatever you want.

On completion you will see a new directory in your project with the name "Migrations" which stores, you guessed it, all of your migrations.

We can now apply those changes to our database:

dotnet ef database update

The command will not only apply any database updates, but will also create our database file for us.

Now it is time to get started on some controller methods.

API Controller Methods

To being with, we will be needing a new controller class, so in our Controller directory, we can create a new NotesController.cs file with the following barebones structure:

using Microsoft.AspNetCore.Mvc;

namespace webApiCrud.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class NotesController : ControllerBase
    {

    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see we have the [ApiController] and the [Route("api/[controller]")] annotations. The routing defines as such would mean that the [controller] part will be replaced by the controller name, in this case by "notes".

We can now define each route and also specify what will happen, when that route is being called.

We first need to ensure that we can use our context we created previously within our controller:

using Microsoft.AspNetCore.Mvc;
using webApiCrud.Models;

namespace webApiCrud.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class NotesController : ControllerBase
    {
        private readonly ApiContext _context;

        public NotesController(ApiContext context)
        {
            _context = context;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

We create a private readonly attribute called _context and then assign the context in the constructor of the controller class to our readonly _context. This way we have access to it throughout our controller.

We can now implement our create, read, update and delete methods:

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

namespace webApiCrud.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class NotesController : ControllerBase
    {
        private readonly ApiContext _context;

        public NotesController(ApiContext context)
        {
            _context = context;
        }

        [HttpPost("create")]
        public async Task<ActionResult<Note>> Create(Note note)
        {
            var new_note = new Note{
                NoteTitle = note.NoteTitle,
                NoteText = note.NoteText,
            };

            _context.Notes.Add(new_note);
            await _context.SaveChangesAsync();

            return CreatedAtAction(
                nameof(GetAll),
                new_note
            );

        }

        [HttpGet("all")]
        public async Task<ActionResult<IEnumerable<Note>>> GetAll()
        {
            var notes = await _context.Notes.Select(row => row).ToListAsync();

            return notes;
        }

        [HttpGet("{id}/get")]
        public async Task<ActionResult<Note>> GetById(int id)
        {
            var note = await _context.Notes.FindAsync(id);

            if(note == null)
            {
                return NotFound();
            }

            return note;
        }

        [HttpPut("{id}/update")]
        public async Task<ActionResult<Note>> Update(int id, Note note)
        {
            var noteToBeUpdated = await _context.Notes.FindAsync(id);

            if(noteToBeUpdated == null)
            {
                return NotFound();
            }

            noteToBeUpdated.NoteTitle = note.NoteTitle;
            noteToBeUpdated.NoteText = note.NoteText;

            await _context.SaveChangesAsync();

            return noteToBeUpdated;
        }

        [HttpDelete("{id}/delete")]
        public async Task<ActionResult<Note>> Delete(int id)
        {
            var noteToBeRemoved = await _context.Notes.FindAsync(id);

            if(noteToBeRemoved == null)
            {
                return NotFound();
            }

            _context.Notes.Remove(noteToBeRemoved);
            await _context.SaveChangesAsync();

            return NoContent();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Each method has its corresponding Http verb as annotation along with a specific route ending.

Each of our methods her has an "async" keyword, which tells our API that it can process request parallel to each other. For this, we use a Task<>which forms part of the task-based asynchronous patterns. Together they allow us to run our program asynchronously.

Inside our Task<> we have an ActionResult<> that defines the typical return type for controllers. All methods inside our controllers are referred to as actions, which is why the return type if these is called action result.

We then define what is being returned alongside the result. Here we are simply defining the model we want to return.

Interacting with our database

Inside each method, you will see that we use our _context every time we want to interact with our database.

In our POST request we create a new Note and use the data provided to us from the client.

We use Linq to create a new query:

_context.Notes.Add(new_note)

Linq is an awesome feature of C# that allows us to write these queries directly in our code.

I would definitely encourage you to learn more about Linq as it can make your life writing C# a lot easier.

In the example above we also used Linq queries such as Remove() for our delete route, FindAsync() to find a particular entry and the good old Select() to get all of our items from the database.

You will notice that each time we have an "Async" in our method name, we have an await at the start of the line. This is the way to write asynchronous lines of code inside our controller actions.

With the next line we write the data into our database:

await _context.SaveChangesAsync()

Only at this line are we actually doing anything in our database.

Testing our API

We can now check to see if everything works by first making sure we are not getting any build errors.

After that you can use different tools to mock requests. You can use curl which you should be able to use without having to set anything up.

The other firm favourite of mine is Postman. You can create an account and set everything up in Postman and then mock some requests.

It does not really matter which tool you choose, important is that you are comfortable using it and can test everything you want to test.

Updating our Database using Migrations

Now that we have our initial database structure, we want to update our table.

Let us assume we wanted to add information that tells us when the note was created and when it was last updated. These two timestamps are going to be two columns within our table:

using System;
using System.ComponentModel.DataAnnotations;

namespace webApiCrud.Models
{
    public class Note
    {
        public int Id { get; set; }
        public string NoteTitle { get; set; }
        public string NoteText { get; set; }

        [DataType(DataType.Date)]
        public DateTime CreatedOn { get; set; }

        [DataType(DataType.Date)]
        public DateTime UpdatedOn { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

We are again going to be using annotations to specify that these fields are of type datetime.

Now then, how are we going to get these changes reflected in our database?

You guessed it - migrations.

Let us create a new migration that we can then use to update our database with:

dotnet ef migrations add AddsCreateAndUpdateDateFields

We can hit enter and a new migration file will be added to our project.

Now we can update our database:

dotnet ef database update

We have successfully updated our database without writing a single line of SQL!

We need to update our Controller methods to populate the new data fields we just created:

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

namespace webApiCrud.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class NotesController : ControllerBase
    {
        private readonly ApiContext _context;

        public NotesController(ApiContext context)
        {
            _context = context;
        }

        [HttpPost("create")]
        public async Task<ActionResult<Note>> Create(Note note)
        {
            var new_note = new Note{
                NoteTitle = note.NoteTitle,
                NoteText = note.NoteText,
                CreatedOn = DateTime.Now,
            };

            _context.Notes.Add(new_note);
            await _context.SaveChangesAsync();

            return CreatedAtAction(
                nameof(GetAll),
                new_note
            );

        }

        [HttpGet("all")]
        public async Task<ActionResult<IEnumerable<Note>>> GetAll()
        {
            var notes = await _context.Notes.Select(row => row).ToListAsync();

            return notes;
        }

        [HttpGet("{id}/get")]
        public async Task<ActionResult<Note>> GetById(int id)
        {
            var note = await _context.Notes.FindAsync(id);

            if(note == null)
            {
                return NotFound();
            }

            return note;
        }

        [HttpPut("{id}/update")]
        public async Task<ActionResult<Note>> Update(int id, Note note)
        {
            var noteToBeUpdated = await _context.Notes.FindAsync(id);

            if(noteToBeUpdated == null)
            {
                return NotFound();
            }

            noteToBeUpdated.NoteTitle = note.NoteTitle;
            noteToBeUpdated.NoteText = note.NoteText;
            noteToBeUpdated.UpdatedOn = DateTime.Now;

            await _context.SaveChangesAsync();

            return noteToBeUpdated;
        }

        [HttpDelete("{id}/delete")]
        public async Task<ActionResult<Note>> Delete(int id)
        {
            var noteToBeRemoved = await _context.Notes.FindAsync(id);

            if(noteToBeRemoved == null)
            {
                return NotFound();
            }

            _context.Notes.Remove(noteToBeRemoved);
            await _context.SaveChangesAsync();

            return NoContent();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

It is as easy as that.

I would encourage you to create new model classes, migrate the changes and create new controller methods to further practice what we have covered so far in this article.

Lastly, we will tackle data transfer objects.

Data Transfer Objects

When working with our API in its current state, we see that we are working with all of the fields we defined in our model. Now this may well be what you are after, but there may be instances where we need all of the fields defined, but the client only needs a subset of those fields.

This is where data transfer objects come into play. They allow us to define the fields that our client is allowed to see and use.

Let us assume that the client only needs the title of the note as well as the id and the text. The timestamps are only meant for admin users or are added for analytical purposes.

We first create a new class where we define the fields that are going to be exposed as part of our DTO (Data Transfer Object):

namespace webApiCrud.Models
{
    public class NoteDTO
    {
        public int Id { get; set; }
        public string NoteTitle { get; set; }
        public string NoteText { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

Our new NoteDTO class holds our fields.

We can now update our controller actions to use the new DTO instead of our whole model:

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

namespace webApiCrud.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class NotesController : ControllerBase
    {
        private readonly ApiContext _context;

        public NotesController(ApiContext context)
        {
            _context = context;
        }

        [HttpPost("create")]
        public async Task<ActionResult<NoteDTO>> Create(Note note)
        {
            var new_note = new Note{
                NoteTitle = note.NoteTitle,
                NoteText = note.NoteText,
                CreatedOn = DateTime.Now,
            };

            _context.Notes.Add(new_note);
            await _context.SaveChangesAsync();

            return CreatedAtAction(
                nameof(GetAll),
                NoteDTO(new_note)
            );

        }

        [HttpGet("all")]
        public async Task<ActionResult<IEnumerable<NoteDTO>>> GetAll()
        {
            var notes = await _context.Notes.Select(row => NoteDTO(row)).ToListAsync();

            return notes;
        }

        [HttpGet("{id}/get")]
        public async Task<ActionResult<NoteDTO>> GetById(int id)
        {
            var note = await _context.Notes.FindAsync(id);

            if(note == null)
            {
                return NotFound();
            }

            return NoteDTO(note);
        }

        [HttpPut("{id}/update")]
        public async Task<ActionResult<NoteDTO>> Update(int id, Note note)
        {
            var noteToBeUpdated = await _context.Notes.FindAsync(id);

            if(noteToBeUpdated == null)
            {
                return NotFound();
            }

            noteToBeUpdated.NoteTitle = note.NoteTitle;
            noteToBeUpdated.NoteText = note.NoteText;
            noteToBeUpdated.UpdatedOn = DateTime.Now;

            await _context.SaveChangesAsync();

            return NoteDTO(noteToBeUpdated);
        }

        [HttpDelete("{id}/delete")]
        public async Task<ActionResult<NoteDTO>> Delete(int id)
        {
            var noteToBeRemoved = await _context.Notes.FindAsync(id);

            if(noteToBeRemoved == null)
            {
                return NotFound();
            }

            _context.Notes.Remove(noteToBeRemoved);
            await _context.SaveChangesAsync();

            return NoContent();
        }

        private static NoteDTO NoteDTO(Note note) =>
        new NoteDTO
        {
            Id = note.Id,
            NoteTitle = note.NoteTitle,
            NoteText = note.NoteText
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

At the end of our controller, we have a small function that we can use to return an instance of our DTO instead of the full model.

If you now test your API you will see that the timestamps have been removed from the response and we returning only the fields we defined as part of our DTO.

Conclusion

That is it as far as basics go when it comes to using .Net 5. There is obviously so much more to know and I would definitely encourage you to build a few small projects and experiment.

Let me know in the comments if you are currently learning C# and .Net 5 and what your experience has been like so far!

Top comments (0)