f796b7d1e4
Production regression in 3.9.0: Telegram bot silently dropped every update. WizardDraftRepository.GetActiveAsync was called on every Telegram update (via UpdateRouter -> TryGetWizardContext) and threw System.PlatformNotSupportedException in NativeAOT, because Dapper.AOT 1.0.48 only generates interceptors for the (sql, object?) extension overloads and NOT for the (CommandDefinition) overload. The runtime then fell back to Dapper.SqlMapper.CreateParamInfoGenerator, which uses Reflection.Emit and fails on AOT. TelegramBotService swallowed the exception, so /newsession appeared to start but no button press reached the wizard and no session was created. Two related changes in WizardDraft: 1. Switched WizardDraftRepository.* from 'new CommandDefinition(sql, params, cancellationToken: ct)' to the direct 'connection.Query*(sql, params)' overload, matching the working pattern in JoinSessionHandler. Dapper.AOT now generates CommandFactory30<WizardDraft> + RowFactory17<WizardDraft> + QuerySingleOrDefaultAsync37<WizardDraft> for all four methods. 2. WizardDraft.CreatedAt/UpdatedAt/ExpiresAt are now DateTime (UTC) instead of DateTimeOffset. AOT RowFactory calls reader.GetDateTime() directly and does not perform DateTime -> DateTimeOffset conversion; the previous type raised InvalidCastException on the very first wizard_drafts query. All 588/590 tests pass (2 pre-existing skipped, +5 new AOT regression tests in WizardDraftRepositoryAotShapeTests). dotnet format clean. Bumps: 3.9.0 -> 3.9.1. Note: GetOwnerClubsAsync (Telegram/Discord), DiscordPermissionLookup, and DiscordWizardInteractionModule.GetOwnerClubsAsync still use CommandDefinition and will hit the same Reflection.Emit AOT failure when the user reaches the PickClub visibility step. Follow-up in 3.9.2.
77 lines
3.4 KiB
C#
77 lines
3.4 KiB
C#
using System;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Dapper;
|
|
using Npgsql;
|
|
|
|
namespace GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
|
|
|
public sealed class WizardDraftRepository(NpgsqlDataSource dataSource) : IWizardDraftRepository
|
|
{
|
|
// NOTE: NativeAOT — Dapper.AOT 1.0.48 only generates interceptors for the
|
|
// (sql, param) extension overloads, NOT for the (CommandDefinition) overload.
|
|
// Passing a CommandDefinition here would skip the interceptor and fall back to
|
|
// Dapper.SqlMapper.CreateParamInfoGenerator, which uses Reflection.Emit and
|
|
// throws PlatformNotSupportedException on AOT (issue: wizard silently dropped
|
|
// every Telegram update in v3.9.0). All four methods therefore use the plain
|
|
// (sql, param) overload, matching the pattern in JoinSessionHandler.
|
|
public async Task<WizardDraft?> GetActiveAsync(string platform, string ownerId, CancellationToken ct)
|
|
{
|
|
const string sql = """
|
|
SELECT id AS Id,
|
|
chat_id AS ChatId,
|
|
message_thread_id AS MessageThreadId,
|
|
owner_id AS OwnerId,
|
|
platform AS Platform,
|
|
step AS Step,
|
|
payload::text AS PayloadJson,
|
|
draft_message_id AS DraftMessageId,
|
|
created_at AS CreatedAt,
|
|
updated_at AS UpdatedAt,
|
|
expires_at AS ExpiresAt
|
|
FROM wizard_drafts
|
|
WHERE platform = @Platform
|
|
AND owner_id = @OwnerId
|
|
AND expires_at > NOW()
|
|
ORDER BY updated_at DESC
|
|
LIMIT 1
|
|
""";
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
return await connection.QuerySingleOrDefaultAsync<WizardDraft>(
|
|
sql,
|
|
new { Platform = platform, OwnerId = ownerId });
|
|
}
|
|
|
|
public async Task UpsertAsync(WizardDraft draft, CancellationToken ct)
|
|
{
|
|
const string sql = """
|
|
INSERT INTO wizard_drafts
|
|
(id, chat_id, message_thread_id, owner_id, platform, step, payload, draft_message_id, created_at, updated_at, expires_at)
|
|
VALUES
|
|
(@Id, @ChatId, @MessageThreadId, @OwnerId, @Platform, @Step, @PayloadJson::jsonb, @DraftMessageId, @CreatedAt, @UpdatedAt, @ExpiresAt)
|
|
ON CONFLICT (id) DO UPDATE
|
|
SET step = EXCLUDED.step,
|
|
payload = EXCLUDED.payload,
|
|
draft_message_id = EXCLUDED.draft_message_id,
|
|
updated_at = EXCLUDED.updated_at,
|
|
expires_at = EXCLUDED.expires_at;
|
|
""";
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
await connection.ExecuteAsync(sql, draft);
|
|
}
|
|
|
|
public async Task DeleteAsync(Guid id, CancellationToken ct)
|
|
{
|
|
const string sql = "DELETE FROM wizard_drafts WHERE id = @Id";
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
await connection.ExecuteAsync(sql, new { Id = id });
|
|
}
|
|
|
|
public async Task<int> DeleteExpiredAsync(CancellationToken ct)
|
|
{
|
|
const string sql = "DELETE FROM wizard_drafts WHERE expires_at <= NOW()";
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
return await connection.ExecuteAsync(sql);
|
|
}
|
|
}
|