Refactor the API
Now we have a functioning get endpoint we will refactor the project to move the logic into appropriate files.
This will make the application more maintainable and easy to read.
Create a folder within the project called 'Models'
Add a new class file to this folder and call it 'TaskDto'
Cut the model we created in the program.cs file and move it here
namespace CrudAppAPI.Models;
public class TaskDto
{
public int Id { get; set; }
public string Title { get; set; } = default!;
public string? Description { get; set; }
public DateTime? DueDate { get; set; }
public bool Completed { get; set; }
}
Create a folder within the project called 'Services'
Add a new class file to this folder and call it 'TaskService.cs'
We will inject our configuration settings into this service. Add the following to pass the config across
namespace CrudAppAPI.Services;
public class TaskService
{
private readonly IConfiguration _config;
public TaskService(IConfiguration config)
{
_config = config;
}
}
Add the method to return our data, we will call it GetAllTasksAsync. As this method is asynchronous it must return a Task<>, which will contain our IEnumerable of type TaskDto
We will need to add the using statement to import our TaskDto model
namespace CrudAppAPI.Services;
using CrudAppAPI.Models;
public class TaskService
{
private readonly IConfiguration _config;
public TaskService(IConfiguration config)
{
_config = config;
}
public async Task<IEnumerable<TaskDto>> GetAllTasksAsync()
{
}
}
Next, go back to the program.cs file and cut the entire contents of the endpoint method and paste it into our new GetTransactionsAsync method
We will also need to add the using statements required for the database connection, these can then be removed from the program.cs file
using System.Data;
using CrudAppAPIV2.Models;
using Dapper;
using Microsoft.Data.SqlClient;
namespace CrudAppAPIV2.Services;
public class TaskService
{
private readonly IConfiguration _config;
public TaskService(IConfiguration config)
{
_config = config;
}
public async Task<IEnumerable<TaskDto>> GetAllTasksAsync()
{
var connectionString = _config.GetConnectionString("DefaultConnection");
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
var tasks = await connection.QueryAsync<TaskDto>(
"GetTasks",
commandType: CommandType.StoredProcedure
);
return tasks;
}
}
Now there are just a few amendments that need to be made
We need to alter the connectionString variable to use the config we are injecting into the service
// var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var connectionString = _config.GetConnectionString("DefaultConnection");
And last of all, we just need to simplify the return statement to just return tasks
using System.Data;
using Microsoft.Data.SqlClient;
using CrudAppAPI.Models;
namespace CrudAppAPI.Services;
public class TaskService
{
private readonly IConfiguration _config;
public TaskService(IConfiguration config)
{
_config = config;
}
public async Task<List<TaskDto>> GetAllTasksAsync()
{
var tasks = new List<TaskDto>();
var connectionString = _config.GetConnectionString("DefaultConnection");
using var conn = new SqlConnection(connectionString);
using var cmd = new SqlCommand("GetTasks", conn)
{
CommandType = CommandType.StoredProcedure
};
await conn.OpenAsync();
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
tasks.Add(new TaskDto
{
Id = reader.GetInt32(0),
Title = reader.GetString(1),
Description = reader.IsDBNull(2) ? null : reader.GetString(2),
DueDate = reader.IsDBNull(3) ? null : reader.GetDateTime(3),
Completed = reader.GetBoolean(4)
});
}
return tasks;
}
}
Update the Program.cs file to add the Service, add it below the existing builder.services method (you again will also need to add the files using statement)
builder.Services.AddScoped<TaskService>();
Create another folder within the project and call it Endpoints, add a new class file within called TaskEndpoints.cs
Make the class static
Add the following method called MapTransactionEndpoints to the class
namespace ExpenseTrackerAPI.Endpoints;
public static class Endpoints
{
public static void MapTaskEndpoints(this WebApplication app)
{
}
}
Then cut what is remaining of our get endpoint from the Program.cs file and paste it into this method
namespace CrudAppAPI.Endpoints;
public class TaskEndpoints
{
public static void MapTaskEndpoints(this WebApplication app)
{
// GET /api/tasks
app.MapGet("/api/tasks", async () =>
{
});
}
}
Pass in a parameter of 'TaskService service'
Create a variable called tasks that calls out the service method
Add a return statement that returns the tasks method results
using CrudAppAPI.Services;
namespace CrudAppAPI.Endpoints;
public static class TaskEndpoints
{
public static void MapTaskEndpoints(this WebApplication app)
{
app.MapGet("/api/tasks", async (TaskService service) =>
{
var tasks = await service.GetAllTasksAsync();
return Results.Ok(tasks);
});
}
}
In the Program.cs file add the endpoint file to the app at the end just before app.Run() is called, You will also need again to import the file with a using statement
app.MapTaskEndpoints();
That should be the refactoring completed, and the Program.cs file should look like this
using CrudAppAPI.Endpoints;
using CrudAppAPI.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
builder.Services.AddScoped<TaskService>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.MapTaskEndpoints();
app.Run();
We will now create a database connection factory which we can then inject into our service methods. This keeps the database logic in one place and stops the logic from being tightly coupled to the service methods.
Add a new folder to the project and call it Data
Add a file to this directory and call it DbConnectionFactory.cs
using System.Data;
using Microsoft.Data.SqlClient;
namespace CrudAppAPIV2.Data;
public interface IDbConnectionFactory
{
Task<IDbConnection> CreateConnectionAsync();
}
public class SqlConnectionFactory : IDbConnectionFactory
{
private readonly IConfiguration _config;
public SqlConnectionFactory(IConfiguration config)
{
_config = config;
}
public async Task<IDbConnection> CreateConnectionAsync()
{
var connection = new SqlConnection(_config.GetConnectionString("DefaultConnection"));
await connection.OpenAsync();
return connection;
}
}
Update Your Service Class to Use the Factory. In your TaskService.cs file, replace this:
private readonly IConfiguration _config;
public TaskService(IConfiguration config)
{
_config = config;
}
with
private readonly IDbConnectionFactory _connectionFactory;
public TaskService(IDbConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
Then replace
var connectionString = _config.GetConnectionString("DefaultConnection");
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
with
using var connection = await _connectionFactory.CreateConnectionAsync();
Finally, register the IDbConnectionFactory and its implementation SqlConnectionFactory with the dependency injection container
Add this line to Program.cs
builder.Services.AddScoped<IDbConnectionFactory, SqlConnectionFactory>();