Herschreven Server Database Verbinding¶
Doelstelling¶
Met de nieuwe herschreven server is er niet een standaard database verbinding, terwijl wij dat wel nodig hebben voor K1. Dus moest ik een nieuwe verbinding gaan maken.
Toegepaste oplossing¶
Dotenv¶
Wat als eerste nodig is om een verbinding met de database te maken zijn de inloggegevens. Dit had gekund met de user-secrets van dotnet, maar deze zijn niet gemakkelijk over te zetten naar oege door een bestand te kopiëren en plakken. Daarom heb ik in plaats hiervan gekozen voor de simpele .env zoals met node gebruikelijk is.
Het laad de variabelen simpelweg in de environment variabelen zodat ze gemakkelijk bereikbaar zijn zoals alle andere opties voor de server.
builder.Services.AddMySqlDataSource(
string.Concat([
"Server=", Environment.GetEnvironmentVariable("dbHost"),
";Port=", Environment.GetEnvironmentVariable("dbPort"),
";Database=", Environment.GetEnvironmentVariable("dbName"),
";Uid=", Environment.GetEnvironmentVariable("dbUsername"),
";Pwd=", Environment.GetEnvironmentVariable("dbPassword")
])
);
MySqlConnector¶
Voor de sql verbinding zelf heb ik gekozen voor MySqlConnector omdat het betere performance heeft dan MySql.Data
van Oracle zelf(MySqlConnector Performance, z.d.).
Ook heeft het support voor ASP.net dat het gemakkelijk maakt om toe te voegen aan ons project.
Resultaten lezen¶
Het enigste probleem met de MySqlConnector, en trouwens met MySql.Data
ook,
is dat er niet een simpele manier is om de resultaten in objecten te stoppen.
Daarom heb ik twee manieren gemaakt waarmee dit wel makkelijk mogelijk is.
IDbReadable¶
Als eerste is hier een interface waarmee ervoor gezorgd kan worden dat het wel makkelijk is om resultaten in te lezen, zo lang als het object de interface implementeert. Dit is min of meer de bedoelde manier om de resultaten in te lezen, maar het zorgt er wel voor dat er extra boilerplate geschreven moet worden.
public interface IDbReadable<T>
{
public static abstract Task<T> DbReadAsync(DbDataReader reader);
}
En in de reader helper word het gemakkelijk beschikbaar gemaakt met deze methods. Ook met een variant dat meerdere rijen in kan lezen.
public static Task<T> ReadSingleStaticAsync<T>(DbDataReader reader) where T : IDbReadable<T>
=> T.DbReadAsync(reader);
public static async IAsyncEnumerable<T> ReadAllStaticAsync<T>(DbDataReader reader, CancellationToken? cancellationToken = null) where T : IDbReadable<T>
{
var cancel = cancellationToken ?? CancellationToken.None;
while (!cancel.IsCancellationRequested && await reader.ReadAsync())
yield return await ReadSingleStaticAsync<T>(reader);
yield break;
}
Dynamische reader¶
Omdat de interface zorgt voor extra boilerplate dat minder gemakkelijk aangepast kan worden heb ik ook een dynamische manier gemaakt. Dat natuurlijk wel wat langzamer is maar veel makkelijker is om te gebruiken.
public static async Task<T> ReadSingleDynamicAsync<T>(DbDataReader reader, DatabaseReaderOptions? option = null) where T : new()
{
var options = option ?? new();
var properties = typeof(T).GetProperties();
var fields = typeof(T).GetFields();
var obj = new T();
Door middel van reflection word er informatie van het object waarin de informatie gestopt moet worden. Dan word hieronder geloopt door elke kolom van de rij in de reader en gekeken bij welke field of property het past uit het object. Samen met een paar extra checks of de field of property privé is of niet en of er wel überhaupt een field of property gevonden kan worden voor de kolom.
for (var i = 0; i < reader.FieldCount; i++)
{
var name = reader.GetName(i);
var val = await reader.GetFieldValueAsync<object?>(i);
val = val is DBNull ? null : val;
if (!options.IgnoreFields)
{
var field = fields.FirstOrDefault(f => {
var fieldName = f.GetCustomAttribute<DatabasePropertyNameAttribute>()?.name ?? f.Name;
return fieldName.Equals(name, StringComparison.InvariantCultureIgnoreCase);
});
if (field != null)
{
try {
field.SetValue(obj, val);
} catch(FieldAccessException _)
{
if (!options.IgnoreHiddenFieldsProperties)
throw;
}
continue;
}
}
var property = properties.FirstOrDefault(p => {
var propertyName = p.GetCustomAttribute<DatabasePropertyNameAttribute>()?.name ?? p.Name;
return propertyName.Equals(name, StringComparison.InvariantCultureIgnoreCase);
});
if (property != null)
{
try {
property.SetValue(obj, val);
} catch(FieldAccessException _)
{
if (!options.IgnoreHiddenFieldsProperties)
throw;
}
continue;
}
if (!options.IgnoreMissingColumns)
throw new MissingFieldException(typeof(T).Name, name);
}
return obj;
}
Voor deze heb ik ook een versie gemaakt dat meerdere rijen in kan lezen. Het is natuurlijk niet veel anders omdat het dezelfde function signature heeft.
public static async IAsyncEnumerable<T> ReadAllDynamicAsync<T>(DbDataReader reader, DatabaseReaderOptions? option = null, CancellationToken? cancellationToken = null) where T : new()
{
var options = option ?? new();
var cancel = cancellationToken ?? CancellationToken.None;
while (!cancel.IsCancellationRequested && await reader.ReadAsync())
yield return await ReadSingleDynamicAsync<T>(reader, options);
yield break;
}
Zoals je al had kunnen merken heb ik voor de dynamische lezer ook opties gemaakt,
zoals met de JsonSerializer
.
Zelfs als deze opties niet aangepast worden maken ze wel duidelijk welke aannames de lezer maakt waar je rekening mee moet houden.
public struct DatabaseReaderOptions()
{
public bool IgnoreFields = false;
public bool IgnoreHiddenFieldsProperties = false;
public bool IgnoreMissingColumns = true;
}
Als laatste heb ik een variant gemaakt van de JsonPropertyNameAttribute
zodat ook voor de database een aangepaste kolom naam gekozen kan worden.
Want anders zou het precies hetzelfde moeten zijn zoals het in de database staat.
Gebruik¶
Eerst netjes een losse model waarin de informatie opgeslagen word. Waar ook gelijk de attributes worden gebruikt.
public class PlayerModel
{
public int id;
public string username;
public string email;
public int money;
[DatabasePropertyName(name = "updated_at")]
public DateTime updatedAt;
[DatabasePropertyName(name = "signed_up_at")]
public DateTime signedUpAt;
}
Los een repository met daarin methods om verschillende queries uit te voeren. Netjes los zoals gebruikelijk is met ASP.net, maar eigenlijk had het samen gekund.
Hier word gelijk ook de reader gebruikt,
maar zoals je kan zien aan de Task<IAsyncEnumerable<...>>
zorgt het er wel voor dat er meerdere await
s nodig zijn.
Dat zorgt wel dat ik mij een beetje zorgen maak dat ik het niet weer te ingewikkeld heb gemaakt.
public static class PlayerRepository
{
public static async Task<IAsyncEnumerable<PlayerModel>> GetWithLimit(MySqlConnection conn, int limit = 50)
{
var cmd = conn.CreateCommand();
cmd.CommandText = @"
SELECT
*
FROM
`match_3__player`
LIMIT
@limit
";
cmd.Parameters.AddWithValue("@limit", limit);
return DatabaseReader.ReadAllDynamicAsync<PlayerModel>(await cmd.ExecuteReaderAsync());
}
...
}
Uiteindelijk hier in de logs is te zien dat het werkt. Alleen is het voor nu nog test data van mijn vorige project, omdat wij nog niet een echte database structuur hebben.
1 john.doe@example.com, John Doe $10000; s 01/12/2024 17:49:16 u 01/12/2024 17:49:16
2 jane.doe@example.com, Jane Doe $15000; s 01/12/2024 17:49:16 u 01/12/2024 17:49:16
3 admin@example.com, Admin $2147483647; s 01/12/2024 17:49:16 u 01/12/2024 17:49:16
5 new@example.com, new player! $10000; s 01/13/2024 20:00:04 u 01/13/2024 20:00:04
6 me@example.com, test player :) $10000; s 01/13/2024 20:04:19 u 01/13/2024 20:04:19
Bronnen¶
- C# documentation. (z.d.) learn.microsoft.com.
Laatst geraadpleegd op 23 maart 2024, van https://learn.microsoft.com/en-uk/dotnet/csharp/ - MySqlConnector Performance. (z.d.) mysqlconnector.net.
Laatst geraadpleegd op 23 maart 2024, van https://mysqlconnector.net/#performance - MySqlConnector API. (z.d.) mysqlconnector.net.
Laatst geraadpleegd op 23 maart 2024, van https://mysqlconnector.net/api/mysqlconnectorassembly/
Gecreëerd: March 25, 2024