Usages
This page demonstrates how to leverage Caerius.NET in your projects with various examples.
Given the unique nature of each project, this guide assumes readers are well-versed in best practices and possess solid knowledge of C# and TSQL.
For C#:
- Proficiency in C# 14 and .NET 10 is required.
- Familiarity with
Best Practices,Clean Code,SOLID Principles,Repository Pattern, andDependency Injectionis recommended. - Understanding of
sealed classandrecordtypes is also recommended.
For TSQL:
- A strong foundation in TSQL is essential.
- Knowledge of
Stored Proceduresis mandatory. - Familiarity with
Transactionsis advised. - For advanced usage, understanding of
Table-Valued Parametersis beneficial.
C#: Repository
In your C# application, specific classes should manage database operations. Typically, these reside in a Repositories directory, listing each Repository class responsible for database interactions, each paired with an Interface.
Below is an example of an Interface and a Record:
namespace TestProject.Repositories.Interfaces;
public interface IUserRepository
{
Task<IEnumerable<UserDto>> GetUserOlderThanAsync(byte age);
Task UpdateUserAgeByGuidAsync(Guid guid, byte age);
}namespace TestProject.Repositories;
public sealed record UserRepository(ICaeriusDbContext DbContext)
: IUserRepository
{
public async Task<IEnumerable<UserDto>> GetUserOlderThanAsync(byte age)
{
throw new NotImplementedException();
}
public async Task UpdateCustomUserAgeByGuidAsync(Guid guid, byte age)
{
throw new NotImplementedException();
}
}namespace TestProject.Repositories;
public sealed class UserRepository(ICaeriusDbContext DbContext)
: IUserRepository
{
public async Task<IEnumerable<UserDto>> GetUserOlderThanAsync(byte age)
{
throw new NotImplementedException();
}
public async Task UpdateUserAgeByGuidAsync(Guid guid, byte age)
{
throw new NotImplementedException();
}
}We basically have an Interface that defines the methods that will be implemented in the Repository class.
- One for the
Read(SELECT) operations. - One for the
Write(INSERT INTO,DELETE,UPDATE,MERGE) operations.
In the below section, we will see how to implement the Read and Write operations.
Read Operations :
The Read operations are the SELECT operations.
To implement the GetUserOlderThanAsync method, we need to follow these steps :
- Start on the
TSQLside, by creating aStored Procedure. - Turn to the
C#side, by creating the C# DTO Mapping. - Continue with C# by implementing the
GetUserOlderThanAsyncwith the usage of theStoredProcedureParametersBuilderclass.
TSQL : Stored Procedure
To make it possible, you need to use the SELECT statement and make it match the column names of your DTO properties.
For this you can use aliases (AS) to match them.
CREATE PROCEDURE dbo.sp_GetUser_By_Age
@Age TINYINT = 18
AS
BEGIN
SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
BEGIN TRY
BEGIN TRANSACTION
SELECT
myColumnGuid AS Guid,
myColumnId AS Id,
myColumnUsername AS Username,
myColumnAge Age,
myColumnCreatedAt CreatedAt,
myColumnPoints Points
FROM
dbo.Users
WHERE
Age >= @Age
IF @@TRANCOUNT > 0
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION
END CATCH
ENDExplanations
To explain this Stored Procedure example, we are using aliases to match the column names of our DTO.
For people who are surprised by the absence of AS keyword, it's optional in TSQL.
In any case, those usages are working the same way, chosen one that you prefer.
TIP
We're using the default schema (dbo) on this example, it's highly recommended to use your own schema for your Stored Procedures usage.
C# : DTO Mapping
We heavy recommend to use sealed records for your DTOs.
The reasons are :
- Specified by the
sealedkeyword, records cannot be inherited. - Records are immutable.
- Records are lightweight.
- Records override by nature the
EqualsandGetHashCodemethods. - Records use by default the
Primary Constructor. - Records are easy to read and to use.
You need to use the ISpMapper<T> interface to map the result of your Stored Procedure to your DTO.
namespace TestProject.Models.Dtos;
public sealed record UserDto(
Guid Guid,
int Id,
string Username,
byte Age,
DateTime CreatedAt,
ushort Points)
: ISpMapper<CustomUsersDto>
{
public static UserDto MapFromReader(SqlDataReader reader)
{
return new UserDto(
Guid = reader.GetGuid(0),
Id = reader.GetInt32(1),
Username = reader.GetString(2),
Age = reader.GetByte(3),
CreatedAt = reader.GetDateTime(4),
Points = reader.GetUInt16(5)
);
}
}Explanations
To explain the ISpMapper<T> interface, it's a simple interface that contains a MapFromReader method. This method will be used to map the result of the Stored Procedure to the DTO.
The SqlDataReader object is used to read the result of the Stored Procedure, using the stream of data returned by the SQL Server from the TDS (Tabular Data Stream) protocol.
Because it's working as a buffer, you need to specify the index of the column you want to read, and the type of the column you want to read.
This help to map correctly the result of the Stored Procedure to the DTO.
C# : Stored Procedure Builder
It's the following step after making the Repository class, C# : Repository.
On the example below, we will implement the GetUserOlderThanAsync method.
namespace TestProject.Repositories;
public sealed record UserRepository(ICaeriusDbContext DbContext)
: IUserRepository
{
public async Task<IEnumerable<UserDto>> GetUserOlderThanAsync(byte userAge)
{
var spParams = new StoredProcedureParametersBuilder("dbo.sp_GetUser_By_Age", 450)
.AddParameter("Age", userAge, SqlDbType.TinyInt)
.Build();
var users = await DbContext.FirstQueryAsync<UserDto>(spParams);
return users;
}
}Explanations
To explain the StoredProcedureParametersBuilder class, it's a class that will help you to build the parameters of the Stored Procedure.
You need to specify the name of the Stored Procedure and the List Capacity, because behind the scene, we are using a List<T> to store the result of the Stored Procedure.
You need to be the most accurate as possible to the number of rows that will be returned by the Stored Procedure.
Check our Benchmarks to see the impact of the List Capacity.
After that, you need to add the parameters of the Stored Procedure, with the name of the parameter, the value of the parameter, and the type of the parameter.
Write Operations :
The Write operations are the INSERT INTO, DELETE, UPDATE, MERGE operations.
To implement the UpdateUserAgeByGuidAsync method, we need to follow these steps :
- Start on the
TSQLside, by creating aStored Procedure. - Back to the
C#side, by implementing theUpdateUserAgeByGuidAsyncwith the usage of theStoredProcedureParametersBuilderclass.
TSQL : Stored Procedure
You need to create a Stored Procedure for the INSERT, UPDATE, DELETE or MERGE operations, here we will see the UPDATE operation.
For this specific example, we will update the Age of a user by his Guid, with the possibility to use multiple parameters.
CREATE PROCEDURE dbo.sp_UpdateUserAge_By_Guid
@Guid UNIQUEIDENTIFIER,
@Age TINYINT
AS
BEGIN
SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
BEGIN TRY
BEGIN TRANSACTION
UPDATE dbo.Users
SET Age = @Age
WHERE Guid = @Guid
IF @@TRANCOUNT > 0
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION
END CATCH
ENDC# : Stored Procedure Builder
It's the following step after making the Repository class, 4. C# : Repository.
On the example below, we will implement the UpdateUserAgeByGuidAsync method, based on the Stored Procedure we have created.
With this method we are required to define the Guid and the Age parameters.
namespace TestProject.Repositories;
public sealed record UserRepository(ICaeriusDbContext DbContext)
: IUserRepository
{
public async Task<int> UpdateUserAgeByGuidAsync(Guid userGuid, byte userAge)
{
var spParams = new StoredProcedureParametersBuilder("dbo.sp_UpdateCustomUserAge_By_Guid")
.AddParameter("Guid", userGuid, SqlDbType.UniqueIdentifier)
.AddParameter("Age", userAge, SqlDbType.TinyInt)
.Build();
var rows = await DbContext.ExecuteAsync(spParams);
return rows;
}
}namespace TestProject.Repositories;
public sealed record UserRepository(ICaeriusDbContext DbContext)
: IUserRepository
{
public async Task UpdateUserAgeByGuidAsync(Guid userGuid, byte userAge)
{
var spParams = new StoredProcedureParametersBuilder("dbo.sp_UpdateCustomUserAge_By_Guid")
.AddParameter("Guid", userGuid, SqlDbType.UniqueIdentifier)
.AddParameter("Age", userAge, SqlDbType.TinyInt)
.Build();
return await DbContext.ExecuteScalarAsync(spParams);
}
}Explanations
To explain the ExecuteAsync and ExecuteScalarAsync methods, they are used to execute the Stored Procedure, as Fire and Forget.
ExecuteAsyncis used forINSERT INTO,DELETE,UPDATE,MERGEoperations,
and will not return the number of rows affected by the operation.ExecuteScalarAsyncis used forINSERT INTO,DELETE,UPDATE,MERGEoperations,
and will return the number of rows affected by the operation.
Note : You don't have to specify the List Capacity for the ExecuteAsync and ExecuteScalarAsync methods, because they are not returning a List<T>.
