Work around MySQL Identity user query failure

This commit is contained in:
biss committed 2026-08-18 11:28:53 +08:00
1 parent b483857421
commit 4f031d4b60
1 file changed
+110
@@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MySql.Data.MySqlClient;
namespace Jiaowu.Api.Controllers;
@@ -33,6 +34,14 @@ public sealed class UsersController(
if (page < 1 || pageSize is < 1 or > 100)
return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。");
// Connector/NET 10.0.9 currently has a provider-side command creation
// failure with this Identity query on EF Core 10.0.11. Keep the normal
// LINQ path for SQLite/tests and bypass only this affected MySQL read.
if (db.Database.ProviderName?.Contains("MySql", StringComparison.OrdinalIgnoreCase) == true)
return Ok(await GetUsersFromMySqlAsync(
page, pageSize, keyword, roleName, collegeId, isEnabled,
hasLoggedIn, cancellationToken));
var query = userManager.Users.AsNoTracking();
if (!string.IsNullOrWhiteSpace(keyword))
{
@@ -111,6 +120,107 @@ public sealed class UsersController(
return Ok(new PagedResult<UserListItem>(items, total, page, pageSize));
}
private async Task<PagedResult<UserListItem>> GetUsersFromMySqlAsync(
int page,
int pageSize,
string? keyword,
string? roleName,
Guid? collegeId,
bool? isEnabled,
bool? hasLoggedIn,
CancellationToken cancellationToken)
{
var clauses = new List<string>();
if (!string.IsNullOrWhiteSpace(keyword))
clauses.Add("(" +
"u.`UserName` LIKE CONCAT('%', @keyword, '%') OR " +
"u.`DisplayName` LIKE CONCAT('%', @keyword, '%') OR " +
"u.`StaffNumber` LIKE CONCAT('%', @keyword, '%') OR " +
"EXISTS (SELECT 1 FROM `AspNetUserRoles` ur " +
"JOIN `AspNetRoles` r ON ur.`RoleId` = r.`Id` " +
"WHERE ur.`UserId` = u.`Id` AND r.`Name` LIKE CONCAT('%', @keyword, '%')))" );
if (!string.IsNullOrWhiteSpace(roleName))
clauses.Add("EXISTS (SELECT 1 FROM `AspNetUserRoles` ur " +
"JOIN `AspNetRoles` r ON ur.`RoleId` = r.`Id` " +
"WHERE ur.`UserId` = u.`Id` AND r.`Name` = @roleName)");
if (collegeId.HasValue) clauses.Add("u.`CollegeId` = @collegeId");
if (isEnabled.HasValue) clauses.Add("u.`IsEnabled` = @isEnabled");
if (hasLoggedIn.HasValue) clauses.Add(hasLoggedIn.Value
? "u.`LastLoginAt` IS NOT NULL" : "u.`LastLoginAt` IS NULL");
var where = clauses.Count == 0 ? string.Empty : " WHERE " + string.Join(" AND ", clauses);
var connection = (MySqlConnection)db.Database.GetDbConnection();
await connection.OpenAsync(cancellationToken);
try
{
await using var countCommand = CreateCommand(
connection, "SELECT COUNT(*) FROM `AspNetUsers` u" + where,
keyword, roleName, collegeId, isEnabled);
var total = Convert.ToInt32(await countCommand.ExecuteScalarAsync(cancellationToken));
await using var usersCommand = CreateCommand(connection,
"SELECT u.`Id`, u.`UserName`, u.`DisplayName`, u.`StaffNumber`, u.`CollegeId`, " +
"u.`IsEnabled`, u.`LastLoginAt`, u.`CreatedAt` FROM `AspNetUsers` u" + where +
" ORDER BY u.`UserName`, u.`Id` LIMIT @take OFFSET @skip",
keyword, roleName, collegeId, isEnabled);
usersCommand.Parameters.AddWithValue("@take", pageSize);
usersCommand.Parameters.AddWithValue("@skip", (page - 1) * pageSize);
var users = new List<UserListItem>();
await using (var reader = await usersCommand.ExecuteReaderAsync(cancellationToken))
{
while (await reader.ReadAsync(cancellationToken))
{
var id = Guid.Parse(reader.GetString(0));
users.Add(new UserListItem(id, reader.GetString(1), reader.GetString(2),
reader.IsDBNull(3) ? null : reader.GetString(3),
reader.IsDBNull(4) ? null : Guid.Parse(reader.GetString(4)),
Convert.ToBoolean(reader.GetValue(5)),
reader.IsDBNull(6) ? null : reader.GetDateTime(6),
reader.GetDateTime(7), Array.Empty<string>()));
}
}
var rolesByUser = new Dictionary<Guid, List<string>>();
if (users.Count > 0)
{
await using var rolesCommand = new MySqlCommand(
"SELECT ur.`UserId`, r.`Name` FROM `AspNetUserRoles` ur " +
"JOIN `AspNetRoles` r ON ur.`RoleId` = r.`Id` WHERE ur.`UserId` IN (" +
string.Join(", ", users.Select((_, index) => "@user" + index)) + ")", connection);
foreach (var (user, index) in users.Select((user, index) => (user, index)))
rolesCommand.Parameters.AddWithValue("@user" + index, user.Id.ToString());
await using var roleReader = await rolesCommand.ExecuteReaderAsync(cancellationToken);
while (await roleReader.ReadAsync(cancellationToken))
{
var userId = Guid.Parse(roleReader.GetString(0));
if (!rolesByUser.TryGetValue(userId, out var roles)) rolesByUser[userId] = roles = [];
roles.Add(roleReader.GetString(1));
}
}
var items = users.Select(user => user with
{
Roles = rolesByUser.GetValueOrDefault(user.Id, []).OrderBy(x => x).ToArray()
}).ToArray();
return new PagedResult<UserListItem>(items, total, page, pageSize);
}
finally
{
await connection.CloseAsync();
}
}
private static MySqlCommand CreateCommand(
MySqlConnection connection, string sql, string? keyword, string? roleName,
Guid? collegeId, bool? isEnabled)
{
var command = new MySqlCommand(sql, connection);
if (!string.IsNullOrWhiteSpace(keyword)) command.Parameters.AddWithValue("@keyword", keyword.Trim());
if (!string.IsNullOrWhiteSpace(roleName)) command.Parameters.AddWithValue("@roleName", roleName.Trim());
if (collegeId.HasValue) command.Parameters.AddWithValue("@collegeId", collegeId.Value.ToString());
if (isEnabled.HasValue) command.Parameters.AddWithValue("@isEnabled", isEnabled.Value);
return command;
}
[HttpGet("roles")]
public async Task<ActionResult<object>> GetRoles(CancellationToken cancellationToken) =>
Ok(await roleManager.Roles.AsNoTracking()