EntityTracker 1.0.0

dotnet add package EntityTracker --version 1.0.0
                    
NuGet\Install-Package EntityTracker -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="EntityTracker" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EntityTracker" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="EntityTracker" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add EntityTracker --version 1.0.0
                    
#r "nuget: EntityTracker, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package EntityTracker@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=EntityTracker&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=EntityTracker&version=1.0.0
                    
Install as a Cake Tool

EntityTracker

NuGet License: MIT .NET

�@�ӱj�j�B�F���� Entity Framework Core �����ܧ�l�ܨ禡�w�A�䴩�h�خ榡�ƼҪO�B�ƥ�B�z�M�F�����L�o����]�w�C

? �S��\��

?? �֤ߥ\��

  • �۰��ܧ�l���G�z�L EF Core �d�I���۰ʰl�ܹ����ܧ�
  • MetadataType �䴩�G�ϥ� [TrackChanges] �ݩʼаO�ݭn�l�ܪ��ݩ�
  • �F���L�o�G�䴩�զW��/�¦W��Ҧ��A��T����l�ܽd��
  • �ƥ��X�ʬ[�c�G�����ܧ�e�᪺����ƥ�ͩR�g��

?? �榡�ƼҪO�t��

  • �h�ؤ��ؼҪO�Gdefault�Bcompact�Bverbose�Bjson�Bbbcode
  • �۩w�q�ҪO�G���P�إ߲ŦX�ݨD���榡�ƼҪO
  • �ܼƴ��������G�䴩�F�����ҪO�ܼƨt��
  • �ʺA�����G����ɰʺA���U�M�����ҪO

?? �i���\��

  • �u���űƧ��G�i�]�w�ݩʪ��l���u����
  • �����޿��G�䴩���W�U�媺����榡��
  • ��ڤƤ䴩�G���P�إߦh�y���榡�ƼҪO
  • �į��u���G���ا֨�����A���C�į�}�P

?? �w��

NuGet Package Manager

Install-Package EntityTracker

.NET CLI

dotnet add package EntityTracker

PackageReference

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

?? �ֳt�}�l

1. �򥻳]�w

�b Program.cs �� Startup.cs �����U�A�ȡG

using EntityTracker.Extensions;

// ���U�ܧ�榡�ƪA��
services.AddChangeFormatting();

// ���U DbContext �òK�[�d�I��
services.AddDbContext<YourDbContext>((serviceProvider, options) =>
{
    options.UseSqlServer(connectionString);
    options.AddInterceptors(
        new ChangeTrackingInterceptor(
            serviceProvider.GetRequiredService<ILogger<ChangeTrackingInterceptor>>(),
            serviceProvider
        )
    );
});

2. �аO�ݭn�l�ܪ��ݩ�

�ϥ� [TrackChanges] �ݩʼаO�ݭn�l�ܪ��ݩʡG

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

[MetadataType(typeof(UserMetadata))]
public partial class User
{
    internal class UserMetadata
    {
        [TrackChanges("�ϥΪ̦W��", Priority = 1)]
        [Display(Name = "�ϥΪ̦W��")]
        public string? Name { get; set; }

        [TrackChanges("�q�l�l��", Priority = 2)]
        [Display(Name = "�q�l�l��")]
        public string? Email { get; set; }

        [TrackChanges("�q�ܸ��X", Priority = 3)]
        [Display(Name = "�q�ܸ��X")]
        public string? Phone { get; set; }

        // ���l�ܪ��ݩ�
        [Display(Name = "�Ƶ�")]
        public string? Memo { get; set; }

        // ���T�T�ΰl��
        [TrackChanges(Enabled = false)]
        [Display(Name = "���� ID")]
        public string? InternalId { get; set; }
    }
}

3. �ϥ��ܧ�l��

�����Q�ק�ëO�s�ɡA�ܧ�|�۰ʳQ�O���G

var user = await dbContext.Users.FindAsync(userId);
user.Name = "�s�W��";
user.Email = "new@example.com";

await dbContext.SaveChangesAsync();
// ��X�G�ϥΪ̦W��: �¦W�� -> �s�W�� | �q�l�l��: old@example.com -> new@example.com

?? �ϥΫ��n

�榡�ƼҪO

�ϥΤ��ؼҪO

�t�δ��Ѧh�ؤ��ؼҪO�G

services.AddDbContext<YourDbContext>((serviceProvider, options) =>
{
    var interceptor = new ChangeTrackingInterceptor(logger, serviceProvider)
    {
        FormatTemplateName = "verbose" // �ϥθԲӮ榡
    };
    options.AddInterceptors(interceptor);
});

�i�Ϊ����ؼҪO�G

�ҪO�W�� ���� �d�ҿ�X
default �зǮ榡 �W��: �­� -> �s��
compact ²��榡 �W��:�­�->�s��
verbose �ԲӮ榡 [EntityType] �W�� �q '�­�' �ܧ� '�s��'
json JSON �榡 {"property":"Name","from":"�­�","to":"�s��"}
bbcode BBCode �榡 [color=#ff6875][User][/color] �­� -> �s��
�إߦ۩w�q�ҪO
��k�@�G�~�ӹw�]�ҪO
using EntityTracker.Formatting;

public class MyCustomTemplate : DefaultChangeFormatTemplate
{
    public MyCustomTemplate()
    {
        PropertyChangeTemplate = "?? {{DisplayName}}: {{OriginalValue}} ?? {{CurrentValue}}";
        SummaryTemplate = "?? {{EntityType}} �o�� {{ChangeCount}} ���ܧ�G{{Changes}}";
        ChangesSeparator = " | ";
    }
}

// ���U�ҪO
services.AddChangeFormatting(registry =>
{
    registry.RegisterTemplate("custom", new MyCustomTemplate());
    registry.SetDefaultTemplate("custom");
});
��k�G�G��@ 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)}";
    }
}
�i�Ϊ��ҪO�ܼ�

�b�ҪO�r�ꤤ�i�ϥΥH�U�ܼơ]�榡�� {{�ܼƦW}}�^�G

  • {{EntityType}} - ���������W��
  • {{PropertyName}} - �ݩʦW��
  • {{DisplayName}} - �ݩ���ܦW��
  • {{OriginalValue}} - ��l��
  • {{CurrentValue}} - ��e��
  • {{EntityState}} - ���骬�A
  • {{ChangeCount}} - �ܧ�ƶq�]�K�n�ҪO�^
  • {{Changes}} - �榡�ƪ��ܧ�C��]�K�n�ҪO�^

�L�o���]�w

�զW��Ҧ��]�Ȱl�ܫ��w����^
using EntityTracker.ChangeTracking;

services.AddSingleton<IChangeTrackingFilter>(provider =>
{
    var options = new ChangeTrackingFilterOptions()
        .UseWhitelistMode()
        .TrackEntity<User>()
        .TrackEntity<Order>()
        .TrackEntity<Product>();
    
    return new ChangeTrackingFilter(options);
});
�¦W��Ҧ��]�ư��S�w����^
services.AddSingleton<IChangeTrackingFilter>(provider =>
{
    var options = new ChangeTrackingFilterOptions()
        .UseBlacklistMode()
        .ExcludeEntity<AuditLog>()
        .ExcludeEntity<SystemLog>()
        .ExcludeEntity<TemporaryData>();
    
    return new ChangeTrackingFilter(options);
});

�ƥ�B�z

��@����S�w���ƥ�B�z��
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));
        
        // �o�e�q���l��
        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);
    }
}

// ���U�ƥ�B�z��
services.AddScoped<IEntitySpecificChangeTrackedEventHandler, UserChangeHandler>();
services.AddScoped<ChangeTrackedEventDispatcher>();

?? �i���d��

����榡��

�ھڤ��P�ݩ������ϥΤ��P�榡�G

public class ConditionalTemplate : DefaultChangeFormatTemplate
{
    public override string FormatPropertyChange(PropertyChangeContext context)
    {
        // �K�X���S��B�z
        if (context.PropertyName.Contains("Password", StringComparison.OrdinalIgnoreCase))
        {
            return $"{context.DisplayName}: [�K�X�w�ܧ�]";
        }
        
        // �ŭȳB�z
        if (context.OriginalValue == null)
        {
            return $"{context.DisplayName}: �]�w�� {context.CurrentValue}";
        }
        
        if (context.CurrentValue == null)
        {
            return $"{context.DisplayName}: �w�M�� (���: {context.OriginalValue})";
        }
        
        // �Ʀr�t�����
        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}�G{context.OriginalValue} �� {context.CurrentValue}",
            "ja" => $"{context.DisplayName}: {context.OriginalValue} ?? {context.CurrentValue} ?",
            _ => $"{context.DisplayName}: {context.OriginalValue} -> {context.CurrentValue}"
        };
    }
    
    // ... FormatChangesSummary ��@
}

// �ھڨϥΪ̻y�����U
services.AddChangeFormatting(registry =>
{
    registry.RegisterTemplate("en", new LocalizedTemplate("en"));
    registry.RegisterTemplate("zh-TW", new LocalizedTemplate("zh-TW"));
    registry.RegisterTemplate("ja", new LocalizedTemplate("ja"));
});

JSON ��X�� API �O��

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

�аO�ݭn�l�ܪ��ݩʡC

�ݩʡG

  • DisplayName (string?): �ܧ�O������ܦW��
  • Enabled (bool): �O�_�ҥΰl�ܡA�w�]�� true
  • Priority (int): �l���u���šA�Ʀr�V�p�u���ŶV��
IgnoreTrackingAttribute

���T�аO���l�ܪ��ݩʡ]�Ω����O�h�Űl�ܮɱư��S�w�ݩʡ^�C

�֤ߤ���

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);
}

?? �]�w�ﶵ

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();
}

? �į�Ҷq

  1. �֨������GMetadataAttributeHelper �ϥΧ֨��קK���ƤϮg
  2. ��������G�u�b�ݭn�ɤ~����榡��
  3. �L�o���G�ϥιL�o����֤����n���l��
  4. �妸�B�z�GSaveChanges �ɧ妸�B�z�Ҧ��ܧ�

��ij

  • �Ȱl�ܥ��n���ݩ�
  • �ϥΥզW��Ҧ�����l�ܽd��
  • ���j�q��ƾާ@�A�Ҽ{�ȮɸT�ΰl��
  • ��ܾA�X���榡�ƼҪO�]²��ҪO�į��n�^

?? �^�m

�w�ﴣ�� Pull Request �Φ^�����D�I

  1. Fork ���M��
  2. �إ߱z���S�ʤ��� (git checkout -b feature/AmazingFeature)
  3. ����z����� (git commit -m 'Add some AmazingFeature')
  4. ���e����� (git push origin feature/AmazingFeature)
  5. �}�� Pull Request

?? ���v

���M�ױĥ� MIT ���v - �Ԩ� LICENSE �ɮ�

?? �����s��

?? ��s��x

Version 1.0.0

  • ? ��l�����o��
  • ?? �䴩 .NET 6/7/8/9
  • ?? �h�ؤ��خ榡�ƼҪO
  • ?? ���㪺�ƥ�B�z�t��
  • ?? NuGet �M��o��

?? �`�����D

Q: �p��ȮɸT�ΰl�ܡH

A: �i�H�b DbContext �h�ų]�w�L�o���A�Ψϥ� IgnoreTrackingAttribute�G

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

Q: �i�H�l�ܾɯ��ݩʪ��ܧ�ܡH

A: �ثe�D�n�䴩�¶q�ݩʪ��l�ܡC�ɯ��ݩʪ��ܧ�ݭn�z�L�~���ݩʨӰl�ܡC

Q: �p��b����ɰʺA�����ҪO�H

A: �i�H�b�إ��d�I���ɳ]�w FormatTemplateName �ݩʡG

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

Q: �į�v�T���h�j�H

A: �z�L�֨��M�L�o���u�ơA��@�����ε{�����į�v�T�ܤp�C��ij�b�į�ӷP���ާ@���ϥΥզW��Ҧ��C


Made with ?? by Antfire70007

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 395 12/1/2025