Skip to content

Antfire70007/EntityTracker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

EntityTracker

NuGet License: MIT .NET

一個強大且靈活的 Entity Framework Core 實體變更追蹤函式庫,支援多種格式化模板、事件處理和靈活的過濾條件設定。

? 特色功能

?? 核心功能

  • 自動變更追蹤:透過 EF Core 攔截器自動追蹤實體變更
  • MetadataType 支援:使用 [TrackChanges] 屬性標記需要追蹤的屬性
  • 靈活過濾:支援白名單/黑名單模式,精確控制追蹤範圍
  • 事件驅動架構:提供變更前後的完整事件生命週期

?? 格式化模板系統

  • 多種內建模板:default、compact、verbose、json、bbcode
  • 自定義模板:輕鬆建立符合需求的格式化模板
  • 變數替換引擎:支援靈活的模板變數系統
  • 動態切換:執行時動態註冊和切換模板

?? 進階功能

  • 優先級排序:可設定屬性的追蹤優先級
  • 條件邏輯:支援基於上下文的條件格式化
  • 國際化支援:輕鬆建立多語言格式化模板
  • 效能優化:內建快取機制,降低效能開銷

?? 安裝

NuGet Package Manager

Install-Package EntityTracker

.NET CLI

dotnet add package EntityTracker

PackageReference

<PackageReference Include="EntityTracker" Version="1.0.0" />

?? 快速開始

1. 基本設定

Program.csStartup.cs 中註冊服務:

using EntityTracker.Extensions;

// 註冊變更格式化服務
services.AddChangeFormatting();

// 註冊 DbContext 並添加攔截器
services.AddDbContext<YourDbContext>((serviceProvider, options) =>
{
    options.UseSqlServer(connectionString);
    options.AddInterceptors(
        new ChangeTrackingInterceptor(
            serviceProvider.GetRequiredService<ILogger<ChangeTrackingInterceptor>>(),
            serviceProvider
        )
    );
});

2. 標記需要追蹤的屬性

使用 [TrackChanges] 屬性標記需要追蹤的屬性:

using EntityTracker.Attributes;
using System.ComponentModel.DataAnnotations;

[MetadataType(typeof(UserMetadata))]
public partial class User
{
    internal class UserMetadata
    {
        [TrackChanges("使用者名稱", Priority = 1)]
        [Display(Name = "使用者名稱")]
        public string? Name { get; set; }

        [TrackChanges("電子郵件", Priority = 2)]
        [Display(Name = "電子郵件")]
        public string? Email { get; set; }

        [TrackChanges("電話號碼", Priority = 3)]
        [Display(Name = "電話號碼")]
        public string? Phone { get; set; }

        // 不追蹤的屬性
        [Display(Name = "備註")]
        public string? Memo { get; set; }

        // 明確禁用追蹤
        [TrackChanges(Enabled = false)]
        [Display(Name = "內部 ID")]
        public string? InternalId { get; set; }
    }
}

3. 使用變更追蹤

當實體被修改並保存時,變更會自動被記錄:

var user = await dbContext.Users.FindAsync(userId);
user.Name = "新名稱";
user.Email = "new@example.com";

await dbContext.SaveChangesAsync();
// 輸出:使用者名稱: 舊名稱 -> 新名稱 | 電子郵件: old@example.com -> new@example.com

?? 使用指南

格式化模板

使用內建模板

系統提供多種內建模板:

services.AddDbContext<YourDbContext>((serviceProvider, options) =>
{
    var interceptor = new ChangeTrackingInterceptor(logger, serviceProvider)
    {
        FormatTemplateName = "verbose" // 使用詳細格式
    };
    options.AddInterceptors(interceptor);
});

可用的內建模板:

模板名稱 說明 範例輸出
default 標準格式 名稱: 舊值 -> 新值
compact 簡潔格式 名稱:舊值->新值
verbose 詳細格式 [EntityType] 名稱 從 '舊值' 變更為 '新值'
json JSON 格式 {"property":"Name","from":"舊值","to":"新值"}
bbcode BBCode 格式 [color=#ff6875][User][/color] 舊值 -> 新值

建立自定義模板

方法一:繼承預設模板
using EntityTracker.Formatting;

public class MyCustomTemplate : DefaultChangeFormatTemplate
{
    public MyCustomTemplate()
    {
        PropertyChangeTemplate = "?? {{DisplayName}}: {{OriginalValue}} ?? {{CurrentValue}}";
        SummaryTemplate = "?? {{EntityType}} 發生 {{ChangeCount}} 項變更:{{Changes}}";
        ChangesSeparator = " | ";
    }
}

// 註冊模板
services.AddChangeFormatting(registry =>
{
    registry.RegisterTemplate("custom", new MyCustomTemplate());
    registry.SetDefaultTemplate("custom");
});
方法二:實作 IChangeFormatTemplate
public class AuditLogTemplate : IChangeFormatTemplate
{
    public string FormatPropertyChange(PropertyChangeContext context)
    {
        var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        return $"[{timestamp}] {context.EntityType.Name}.{context.PropertyName}: " +
               $"\"{context.OriginalValue}\"\"{context.CurrentValue}\"";
    }

    public string FormatChangesSummary(IEnumerable<PropertyChangeContext> contexts)
    {
        var contextList = contexts.ToList();
        if (contextList.Count == 0) return string.Empty;
        
        var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        var changes = contextList.Select(FormatPropertyChange);
        
        return $"[{timestamp}] AUDIT: {contextList.First().EntityType.Name} " +
               $"modified ({contextList.Count} changes)\n{string.Join("\n", changes)}";
    }
}

可用的模板變數

在模板字串中可使用以下變數(格式為 {{變數名}}):

  • {{EntityType}} - 實體類型名稱
  • {{PropertyName}} - 屬性名稱
  • {{DisplayName}} - 屬性顯示名稱
  • {{OriginalValue}} - 原始值
  • {{CurrentValue}} - 當前值
  • {{EntityState}} - 實體狀態
  • {{ChangeCount}} - 變更數量(摘要模板)
  • {{Changes}} - 格式化的變更列表(摘要模板)

過濾器設定

白名單模式(僅追蹤指定實體)

using EntityTracker.ChangeTracking;

services.AddSingleton<IChangeTrackingFilter>(provider =>
{
    var options = new ChangeTrackingFilterOptions()
        .UseWhitelistMode()
        .TrackEntity<User>()
        .TrackEntity<Order>()
        .TrackEntity<Product>();
    
    return new ChangeTrackingFilter(options);
});

黑名單模式(排除特定實體)

services.AddSingleton<IChangeTrackingFilter>(provider =>
{
    var options = new ChangeTrackingFilterOptions()
        .UseBlacklistMode()
        .ExcludeEntity<AuditLog>()
        .ExcludeEntity<SystemLog>()
        .ExcludeEntity<TemporaryData>();
    
    return new ChangeTrackingFilter(options);
});

事件處理

實作實體特定的事件處理器

using EntityTracker.ChangeTracking;
using Microsoft.EntityFrameworkCore.ChangeTracking;

public class UserChangeHandler : EntitySpecificChangeTrackedEventHandler<User>
{
    private readonly ILogger<UserChangeHandler> _logger;
    private readonly IEmailService _emailService;

    public UserChangeHandler(
        ILogger<UserChangeHandler> logger,
        IEmailService emailService)
    {
        _logger = logger;
        _emailService = emailService;
    }

    public override async Task ChangedAsync(User entity, EntityEntry entry, List<string> changes)
    {
        _logger.LogInformation("User {UserId} was modified: {Changes}", 
            entity.Id, string.Join(", ", changes));
        
        // 發送通知郵件
        if (changes.Any(c => c.Contains("Email")))
        {
            await _emailService.SendEmailChangeNotificationAsync(entity);
        }
    }

    public override async Task AddedAsync(User entity, EntityEntry entry)
    {
        _logger.LogInformation("New user {UserId} was created", entity.Id);
        await _emailService.SendWelcomeEmailAsync(entity);
    }

    public override async Task DeletedAsync(User entity, EntityEntry entry)
    {
        _logger.LogWarning("User {UserId} was deleted", entity.Id);
        await _emailService.SendAccountClosureEmailAsync(entity);
    }
}

// 註冊事件處理器
services.AddScoped<IEntitySpecificChangeTrackedEventHandler, UserChangeHandler>();
services.AddScoped<ChangeTrackedEventDispatcher>();

?? 進階範例

條件格式化

根據不同屬性類型使用不同格式:

public class ConditionalTemplate : DefaultChangeFormatTemplate
{
    public override string FormatPropertyChange(PropertyChangeContext context)
    {
        // 密碼欄位特殊處理
        if (context.PropertyName.Contains("Password", StringComparison.OrdinalIgnoreCase))
        {
            return $"{context.DisplayName}: [密碼已變更]";
        }
        
        // 空值處理
        if (context.OriginalValue == null)
        {
            return $"{context.DisplayName}: 設定為 {context.CurrentValue}";
        }
        
        if (context.CurrentValue == null)
        {
            return $"{context.DisplayName}: 已清除 (原值: {context.OriginalValue})";
        }
        
        // 數字差異顯示
        if (decimal.TryParse(context.OriginalValue?.ToString(), out var original) &&
            decimal.TryParse(context.CurrentValue?.ToString(), out var current))
        {
            var difference = current - original;
            var sign = difference >= 0 ? "+" : "";
            return $"{context.DisplayName}: {context.OriginalValue}{context.CurrentValue} ({sign}{difference})";
        }
        
        return base.FormatPropertyChange(context);
    }
}

國際化支援

public class LocalizedTemplate : IChangeFormatTemplate
{
    private readonly string _culture;
    
    public LocalizedTemplate(string culture = "zh-TW") => _culture = culture;

    public string FormatPropertyChange(PropertyChangeContext context)
    {
        return _culture switch
        {
            "en" => $"{context.DisplayName}: {context.OriginalValue} -> {context.CurrentValue}",
            "zh-TW" => $"{context.DisplayName}{context.OriginalValue}{context.CurrentValue}",
            "ja" => $"{context.DisplayName}: {context.OriginalValue} ?? {context.CurrentValue} ?",
            _ => $"{context.DisplayName}: {context.OriginalValue} -> {context.CurrentValue}"
        };
    }
    
    // ... FormatChangesSummary 實作
}

// 根據使用者語言註冊
services.AddChangeFormatting(registry =>
{
    registry.RegisterTemplate("en", new LocalizedTemplate("en"));
    registry.RegisterTemplate("zh-TW", new LocalizedTemplate("zh-TW"));
    registry.RegisterTemplate("ja", new LocalizedTemplate("ja"));
});

JSON 輸出用於 API 記錄

public class JsonAuditTemplate : IChangeFormatTemplate
{
    public string FormatPropertyChange(PropertyChangeContext context)
    {
        return JsonSerializer.Serialize(new
        {
            timestamp = DateTime.UtcNow,
            entityType = context.EntityType.Name,
            property = context.PropertyName,
            displayName = context.DisplayName,
            originalValue = context.OriginalValue?.ToString(),
            currentValue = context.CurrentValue?.ToString()
        });
    }

    public string FormatChangesSummary(IEnumerable<PropertyChangeContext> contexts)
    {
        var contextList = contexts.ToList();
        return JsonSerializer.Serialize(new
        {
            timestamp = DateTime.UtcNow,
            entityType = contextList.First().EntityType.Name,
            changeCount = contextList.Count,
            changes = contextList.Select(c => new
            {
                property = c.PropertyName,
                displayName = c.DisplayName,
                originalValue = c.OriginalValue?.ToString(),
                currentValue = c.CurrentValue?.ToString()
            })
        }, new JsonSerializerOptions { WriteIndented = true });
    }
}

?? API 文件

核心屬性

TrackChangesAttribute

標記需要追蹤的屬性。

屬性:

  • DisplayName (string?): 變更記錄的顯示名稱
  • Enabled (bool): 是否啟用追蹤,預設為 true
  • Priority (int): 追蹤優先級,數字越小優先級越高

IgnoreTrackingAttribute

明確標記不追蹤的屬性(用於類別層級追蹤時排除特定屬性)。

核心介面

IChangeFormatTemplate

public interface IChangeFormatTemplate
{
    string FormatPropertyChange(PropertyChangeContext context);
    string FormatChangesSummary(IEnumerable<PropertyChangeContext> contexts);
}

IChangeTrackingFilter

public interface IChangeTrackingFilter
{
    bool ShouldTrackEntity(Type entityType);
}

IEntitySpecificChangeTrackedEventHandler

public interface IEntitySpecificChangeTrackedEventHandler
{
    Type EntityType { get; }
    bool SupportsEntity(Type entityType);
    Task ChangingAsync(EntityEntry entry);
    Task ChangedAsync(EntityEntry entry, List<string> changes);
    Task AddingAsync(EntityEntry entry);
    Task AddedAsync(EntityEntry entry);
    Task DeletingAsync(EntityEntry entry);
    Task DeletedAsync(EntityEntry entry);
}

?? 設定選項

ChangeTrackingFilterOptions

public class ChangeTrackingFilterOptions
{
    public HashSet<Type> TrackedEntityTypes { get; }
    public HashSet<Type> ExcludedEntityTypes { get; }
    public bool WhitelistMode { get; set; }
    
    // Fluent API
    public ChangeTrackingFilterOptions TrackEntity<TEntity>();
    public ChangeTrackingFilterOptions ExcludeEntity<TEntity>();
    public ChangeTrackingFilterOptions UseWhitelistMode();
    public ChangeTrackingFilterOptions UseBlacklistMode();
}

? 效能考量

  1. 快取機制:MetadataAttributeHelper 使用快取避免重複反射
  2. 延遲評估:只在需要時才執行格式化
  3. 過濾器:使用過濾器減少不必要的追蹤
  4. 批次處理:SaveChanges 時批次處理所有變更

效能建議

  • 僅追蹤必要的屬性
  • 使用白名單模式限制追蹤範圍
  • 對於大量資料操作,考慮暫時禁用追蹤
  • 選擇適合的格式化模板(簡單模板效能更好)

?? 貢獻

歡迎提交 Pull Request 或回報問題!

  1. Fork 此專案
  2. 建立您的特性分支 (git checkout -b feature/AmazingFeature)
  3. 提交您的更改 (git commit -m 'Add some AmazingFeature')
  4. 推送到分支 (git push origin feature/AmazingFeature)
  5. 開啟 Pull Request

?? 授權

本專案採用 MIT 授權 - 詳見 LICENSE 檔案

?? 相關連結

?? 更新日誌

Version 1.0.0

  • ? 初始版本發布
  • ?? 支援 .NET 6/7/8/9
  • ?? 多種內建格式化模板
  • ?? 完整的事件處理系統
  • ?? NuGet 套件發布

?? 常見問題

Q: 如何暫時禁用追蹤?

A: 可以在 DbContext 層級設定過濾器,或使用 IgnoreTrackingAttribute

[IgnoreTracking]
public string? TemporaryData { get; set; }

Q: 可以追蹤導航屬性的變更嗎?

A: 目前主要支援純量屬性的追蹤。導航屬性的變更需要透過外鍵屬性來追蹤。

Q: 如何在執行時動態切換模板?

A: 可以在建立攔截器時設定 FormatTemplateName 屬性:

var interceptor = new ChangeTrackingInterceptor(logger, serviceProvider)
{
    FormatTemplateName = userPreferences.TemplatePreference
};

Q: 效能影響有多大?

A: 透過快取和過濾器優化,對一般應用程式的效能影響很小。建議在效能敏感的操作中使用白名單模式。


Made with ?? by Antfire70007

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages