原始程式碼展示一個 Web Forms 頁面,使用者從 `CheckBoxList` 選取多位收件者,點擊按鈕後驗證選取人數 (最多 5 人),將收件者 ID 串聯成字串重導至 `info.aspx` 進行訊息編輯發送。以下重構為現代、可維護、安全的實作。
public class MessageRecipient
{
public string UserId { get; set; }
public string DisplayName { get; set; }
}
public class SendMessageRequest
{
public List Recipients { get; set; } = new();
public string Subject { get; set; }
public string Body { get; set; }
}
public static class MessagePolicy
{
public const int MaxRecipients = 5;
public const int MaxSubjectLength = 200;
public const int MaxBodyLength = 5000;
}
public interface IMessageService
{
// 驗證收件者是否存在、有效,回傳合法清單
Task> ValidateRecipientsAsync(IEnumerable userIds);
// 暫存訊息草稿,回傳 Token (避免 QueryString 傳大量資料)
Task SaveDraftAsync(SendMessageRequest request, string senderId);
// 發送訊息
Task SendAsync(string draftToken, string senderId);
}
public class MessageService : IMessageService
{
private readonly IDistributedCache _cache; // Redis / MemoryCache
private readonly IUserRepository _users;
public async Task> ValidateRecipientsAsync(IEnumerable userIds)
{
var distinctIds = userIds.Distinct().ToList();
if (distinctIds.Count > MessagePolicy.MaxRecipients)
throw new ArgumentException($"收件者不可超過 {MessagePolicy.MaxRecipients} 人");
var users = await _users.GetByIdsAsync(distinctIds);
return users.Select(u => new MessageRecipient { UserId = u.Id, DisplayName = u.Name }).ToList();
}
public async Task SaveDraftAsync(SendMessageRequest request, string senderId)
{
var token = Guid.NewGuid().ToString("N");
var draft = new MessageDraft
{
Token = token,
SenderId = senderId,
Recipients = request.Recipients,
Subject = request.Subject,
Body = request.Body,
CreatedAt = DateTime.UtcNow
};
await _cache.SetStringAsync($"msg:draft:{token}", JsonSerializer.Serialize(draft),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) });
return token;
}
public async Task SendAsync(string draftToken, string senderId)
{
var json = await _cache.GetStringAsync($"msg:draft:{draftToken}");
if (json == null) throw new InvalidOperationException("草稿不存在或已過期");
var draft = JsonSerializer.Deserialize(json);
if (draft.SenderId != senderId) throw new UnauthorizedAccessException();
// 實際發送邏輯 (寫入資料庫、推播通知、Email 等)
await _messageRepository.CreateAsync(draft);
await _cache.RemoveAsync($"msg:draft:{draftToken}");
}
}
public class MessageController : Controller
{
private readonly IMessageService _msgService;
// 步驟 1:選擇收件者 → POST 驗證 → 重導編輯頁
[HttpPost]
[ValidateAntiForgeryToken]
public async Task SelectRecipients(string[] recipientIds)
{
try
{
var recipients = await _msgService.ValidateRecipientsAsync(recipientIds);
// 暫存收件者至 TempData (跨重導保留一請求)
TempData["Recipients"] = JsonSerializer.Serialize(recipients);
return RedirectToAction("Compose");
}
catch (ArgumentException ex)
{
ModelState.AddModelError("", ex.Message);
return View("Index", GetUserList());
}
}
// 步驟 2:編輯頁面
public IActionResult Compose()
{
var recipients = TempData.Peek("Recipients") as string; // 不消費,供表單再次使用
if (recipients == null) return RedirectToAction("Index");
return View(new ComposeViewModel { RecipientsJson = recipients });
}
// 步驟 3:送出發送
[HttpPost]
[ValidateAntiForgeryToken]
public async Task Send(ComposeViewModel model)
{
if (!ModelState.IsValid) return View("Compose", model);
var recipients = JsonSerializer.Deserialize>(model.RecipientsJson);
var request = new SendMessageRequest
{
Recipients = recipients,
Subject = model.Subject,
Body = model.Body
};
string draftToken = await _msgService.SaveDraftAsync(request, User.Identity.Name);
await _msgService.SendAsync(draftToken, User.Identity.Name);
TempData.Remove("Recipients");
TempData["SuccessMessage"] = "訊息已發送";
return RedirectToAction("Index");
}
}
@model ComposeViewModel
| 原始做法 | 重構做法 | 效益 |
|---|---|---|
| 字串拼接 ID 串 | 強型別 List | 型別安全、可擴充屬性、無分隔符問題 |
| QueryString 傳遞收件者 | TempData / 分散式快取 (Redis) 暫存草稿 Token | 無長度限制、敏感資料不外露、支援大量收件者 |
| 硬式編碼 5 人限制 | MessagePolicy.MaxRecipients 集中管理 | 易維護、可配置、單元測試友善 |
| 無收件者驗證 | 服務層 ValidateRecipientsAsync 驗證存在性、有效性 | 資料完整性、安全性 |
| UI 事件處理器包含業務邏輯 | Controller 薄、Service 富、Model 富 | 關注點分離、可測試、可復用 |
protected void btnSend_Click(object sender, EventArgs e)
{
var selectedIds = cblRecipients.Items.Cast()
.Where(i => i.Selected)
.Select(i => i.Value)
.Distinct()
.ToList();
if (selectedIds.Count > 5)
{
lblError.Text = "最多選擇 5 位收件者";
return;
}
// 驗證收件者有效性 (略)
// 使用 TempData 等價機制:Session + 頁面標記
string token = Guid.NewGuid().ToString("N");
Session[$"MsgDraft_{token}"] = new { Recipients = selectedIds, Time = DateTime.Now };
Response.Redirect($"ComposeMessage.aspx?token={token}");
}
核心原則:**資料結構化、驗證服務化、狀態伺服器端暫存、敏感資訊不進 URL、策略集中管理**。