Blog API with EF Core in ASP.NET Core
We will create a blog API with EF Core in ASP.NET Core. We will use the code-first approach to create the database schema. We will use the following tools and technologies:
Visual Studio Code
.NET Core SDK
.NET CLI
In-memory database provider
Entity Framework Core
Prerequisites
- Basic knowledge of C#
- Basic knowledge of ASP.NET Core
- Basic knowledge of Entity Framework Core
- Basic knowledge of Visual Studio Code
Setup
Create a new folder
mkdir Blogging
cd Blogging
Create a new solution
dotnet new sln
The above command will create a new solution file in the Blogging folder.
dotnet CLI
Install the dotnet ef tool globally
dotnet tool install --global dotnet-ef
The above command will install the dotnet ef tool globally. You can check the version of the dotnet ef tool using the following command:
dotnet ef --version
Create a new ASP.NET Core Web API project
dotnet new webapi --use-controllers -o Blogging
We passed the --use-controllers
option to the dotnet new webapi
command to create the project with controllers. The above command will create a new ASP.NET Core Web API project with controllers in the Blogging folder.
Add the project to the solution
dotnet sln add **/*.csproj
The above command will add all the projects in the Blogging folder to the solution file.
Build the project
cd Blogging
dotnet build
Install the Entity Framework Core tools
cd Blogging and run the following command:
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.EntityFrameworkCore.SqlServer #this is for SQL Server, it is required for scaffolding the controller
dotnet add package Microsoft.EntityFrameworkCore.Tools
Tools: The tools package contains the EF Core commands for the dotnet CLI. We will use these commands to create the database schema from the model classes.
InMemory: The InMemory package contains the in-memory database provider. We will use this provider to create an in-memory database for testing purposes.
Design: The design package contains the EF Core design-time components. We will use these components to create the database schema from the model classes.
Create the model classes
mkdir Models
Use the prop
short cut to create properties in the model classes. The prop short cut will create a property with a getter and a setter and a private field.
Blog.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Blogging.Models
{
public class Blog
{
public int Id { get; set; }
public string? Url { get; set; } // ? means nullable, i.e. it can be null
public string? Topic { get; set; }
public string? Author { get; set; }
public string? Content { get; set; }
public DateTime Date { get; set; } = DateTime.UtcNow;
public DateTime Updated { get; set; } = default!;
}
}
DbContext class
We need a connector between the model classes and the database. The DbContext class is a bridge between the model classes and the database. The DbContext class is responsible for the following tasks:
- Establishing a connection to the database
- Mapping the model classes to the database tables
- Querying the database
- Saving the data to the database
Data folder
Create a new file named BloggingContext.cs in the Data folder and add the following code to it:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Data
{
public class BloggingContext : DbContext
{
public BloggingContext(DbContextOptions<BloggingContext> options)
: base(options)
{
}
public DbSet<Blog> Blogs { get; set; }
}
}
Let's understand the above code:
- DbContext : The DbContext class is a bridge between the model classes and the database.
- DbSet : The DbSet class represents a database table. We will use the DbSet class to query and save data to the database.
- DbContextOptions : The DbContextOptions class is used to configure the DbContext class. We will use the DbContextOptions class to configure the database provider, connection string, etc.
- base(options) : The base(options) statement is used to pass the options to the base class constructor.
Note: The argument from the base class constructor is passed from the Program.cs file.
Register the DbContext class in the Program.cs file
using Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Registering the DbContext class in the IoC container
builder.Services.AddDbContext<BloggingContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Let's understand the above code:
- The AddDbContext method is used to register the DbContext class in the IoC container. The AddDbContext method takes a lambda expression as an argument. The lambda expression is used to configure the DbContext class. We have configured the DbContext class to use the in-memory database provider. We have also configured the DbContext class to use the BloggingDatabase as the database name.
- The AddControllers method is used to register the controllers in the IoC container.
- The AddEndpointsApiExplorer method is used to register the API explorer in the IoC container.
- The AddSwaggerGen method is used to register the Swagger generator in the IoC container.
- The UseSwagger method is used to enable the Swagger middleware.
- The UseSwaggerUI method is used to enable the Swagger UI middleware.
- The UseHttpsRedirection method is used to redirect HTTP requests to HTTPS.
Code generation
We will utilize the code generation feature global dotnet-aspnet-codegenerator
to generate the controller and the views. The code generation feature is available as a global tool. We need to install the global tool to use the code generation feature. We can install the global tool using the following command:
dotnet tool install --global dotnet-aspnet-codegenerator
The above command will install the global tool.
Scaffold the controller
Then run:
dotnet aspnet-codegenerator controller -name BlogsController -async -api -m Blog -dc BloggingContext -outDir Controllers
Breaking down the above command:
- dotnet aspnet-codegenerator : The dotnet aspnet-codegenerator command is used to generate the controller and the views.
- controller : The controller argument is used to generate the controller.
- -name BlogsController : The -name BlogsController argument is used to specify the name of the controller.
- -async : The -async argument is used to generate the asynchronous action methods.
- -api : The -api argument is used to generate the controller with the API template.
- -m Blog : The -m Blog argument is used to specify the model class.
- -dc BloggingContext : The -dc BloggingContext argument is used to specify the DbContext class.
- -outDir Controllers : The -outDir Controllers argument is used to specify the output directory.
If you run the above command, you will get the following error:
So we need to install the following packages:
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
Then we need to run the command again:
dotnet aspnet-codegenerator controller -name BlogsController -async -api -m Blog -dc BloggingContext -outDir Controllers
The above command will generate the BlogsController.cs file in the Controllers folder. Let's understand the code generated by the code generator.
BlogsController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Blogging.Models;
using Data;
namespace Blogging.Controllers
{
[Route("api/[controller]")] // This attribute indicates that the controller responds to requests at the api/blogs URL path
[ApiController] // This attribute indicates that the controller responds to web API requests
public class BlogsController : ControllerBase
{
// The controller's constructor uses dependency injection to inject the database context (BloggingContext) into the controller. The database context is used in each of the CRUD methods in the controller.
private readonly BloggingContext _context;
public BlogsController(BloggingContext context)
{
_context = context;
}
// GET: api/Blogs
[HttpGet]
public async Task<ActionResult<IEnumerable<Blog>>> GetBlogs()
// The return type of the method (Task<ActionResult<IEnumerable<Blog>>>) is an ActionResult type. The ActionResult type is a return type for actions that return an HTTP response with a specific HTTP status code and optional value.
{
return await _context.Blogs.ToListAsync(); // The ToListAsync method converts the Blogs DbSet into a List of Blog entities asynchronously.
}
// GET: api/Blogs/5
[HttpGet("{id}")] // The id parameter is passed to the method. The id parameter is defined as a route parameter (in the [HttpGet("{id}")] attribute) and is specified in the route template string for the GetBlog method.
public async Task<ActionResult<Blog>> GetBlog(int id)
{
var blog = await _context.Blogs.FindAsync(id);
if (blog == null)
{
return NotFound();
}
return blog;
}
// PUT: api/Blogs/5
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPut("{id}")]
public async Task<IActionResult> PutBlog(int id, Blog blog)
{
if (id != blog.Id)
{
return BadRequest();
}
_context.Entry(blog).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!BlogExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// POST: api/Blogs
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPost]
public async Task<ActionResult<Blog>> PostBlog(Blog blog)
{
_context.Blogs.Add(blog);
await _context.SaveChangesAsync();
return CreatedAtAction("GetBlog", new { id = blog.Id }, blog);
}
// DELETE: api/Blogs/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteBlog(int id)
{
var blog = await _context.Blogs.FindAsync(id);
if (blog == null)
{
return NotFound();
}
_context.Blogs.Remove(blog);
await _context.SaveChangesAsync();
return NoContent();
}
private bool BlogExists(int id)
{
return _context.Blogs.Any(e => e.Id == id);
}
}
}
Home Controller
Run the app
To run with HTTPS support in development, run the following command:
dotnet dev-certs https --trust
If you get an error, run the following command to clean the certificate:
dotnet dev-certs https --clean
Then run the following command:
dotnet dev-certs https --trust
To run the app, run the following command:
dotnet run --launch-profile https
Now Command + Click on the URL to open the app in the browser.
Test the API endpoints
Go to https://localhost:/swagger/index.html
Create a new blog
Request:
Response:
Add Fluent Validation
Install the Fluent Validation package:
dotnet add package FluentValidation.AspNetCore
Add the following code to the ConfigureServices method in the Program.cs file:
builder.Services.AddControllers().AddFluentValidation(s =>
{
s.RegisterValidatorsFromAssemblyContaining<Program>();
});
Validate the DTOs using Fluent Validation
It is a good practice to validate the DTOs before passing them to the controller.
Create a new folder named Dtos.
Create a new file named CreateBlog.cs in the Dtos folder and add the following code to it:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Blogging.Dtos
{
public class CreateBlog
{
public string? Topic { get; set; }
public string? Author { get; set; }
public string? Content { get; set; }
}
}
Validate the DTOs using Fluent Validation
Create a new file named CreateBlogValidator.cs in the Dtos folder and add the following code to it:
using FluentValidation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Blogging.Dtos
{
public class CreateBlogValidator : AbstractValidator<CreateBlog>
{
public CreateBlogValidator()
{
RuleFor(x => x.Topic).NotEmpty().WithMessage("Topic is required");
RuleFor(x => x.Author).NotEmpty().WithMessage("Author is required");
RuleFor(x => x.Content).NotEmpty().WithMessage("Content is required");
}
}
}
Add the following code to the BlogsController.cs file:
using FluentValidation;
using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Blogging.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class BlogsController : ControllerBase
{
private readonly BloggingContext _context;
private readonly IValidator<CreateBlog> _validator;
public BlogsController(BloggingContext context, IValidator<CreateBlog> validator)
{
_context = context;
_validator = validator;
}
// POST: api/Blogs
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPost]
public async Task<ActionResult<Blog>> PostBlog(CreateBlog createBlog)
{
ValidationResult result = _validator.Validate(createBlog);
if (!result.IsValid)
{
return BadRequest(result.Errors);
}
Blog blog = new Blog
{
Topic = createBlog.Topic,
Author = createBlog.Author,
Content = createBlog.Content
};
_context.Blogs.Add(blog);
await _context.SaveChangesAsync();
return CreatedAtAction("GetBlog", new { id = blog.Id }, blog);
}
}
}
Create a Custom Api Response to return the validation errors
Create a new file in the models folder named ApiResponse.cs and add the following code to it:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Text.Json.Serialization;
namespace Blogging.Models
{
public class ApiResponse
{
public bool Status { get; set; }
public string Message { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public object? Data { get; set; }
[System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)]
public List<string>? Errors { get; set; }
}
}
Update the BlogsController.cs file
Update the BlogsController.cs file as shown below:
using FluentValidation;
using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Blogging.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class BlogsController : ControllerBase
{
private readonly BloggingContext _context;
private readonly IValidator<CreateBlog> _validator;
public BlogsController(BloggingContext context, IValidator<CreateBlog> validator)
{
_context = context;
_validator = validator;
}
// POST: api/Blogs
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPost]
public async Task<ActionResult<Blog>> PostBlog(CreateBlog createBlog)
{
ValidationResult result = _validator.Validate(createBlog);
if (!result.IsValid)
{
return BadRequest(new ApiResponse { Status = false, Errors = result.Errors.Select(e => e.ErrorMessage).ToList() });
}
Blog blog = new Blog
{
Topic = createBlog.Topic,
Author = createBlog.Author,
Content = createBlog.Content
};
_context.Blogs.Add(blog);
await _context.SaveChangesAsync();
return CreatedAtAction("GetBlog", new { id = blog.Id }, blog);
}
}
}
Conclusion
In this article, we learned how to create a blog API with EF Core in ASP.NET Core. We learned how to use the code-first approach to create the database schema. We learned how to use the following tools and technologies:
- Visual Studio Code
- .NET Core SDK
- .NET CLI
- In-memory database provider
- Entity Framework Core
- Fluent Validation
Watch the video version here:
If you have any questions or feedback, then please drop a comment below. Thank you for reading. Cheers!
Top comments (2)
Abayomi Ogunnusi,
Thanks for sharing
Great article
Glad you found it useful