EntityTracker 1.0.0
dotnet add package EntityTracker --version 1.0.0
NuGet\Install-Package EntityTracker -Version 1.0.0
<PackageReference Include="EntityTracker" Version="1.0.0" />
<PackageVersion Include="EntityTracker" Version="1.0.0" />
<PackageReference Include="EntityTracker" />
paket add EntityTracker --version 1.0.0
#r "nuget: EntityTracker, 1.0.0"
#:package EntityTracker@1.0.0
#addin nuget:?package=EntityTracker&version=1.0.0
#tool nuget:?package=EntityTracker&version=1.0.0
EntityTracker
�@�ӱ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�]�� truePriority(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
- �֨������GMetadataAttributeHelper �ϥΧ֨��קK���ƤϮg
- ��������G�u�b�ݭn�ɤ~����榡��
- �L�o���G�ϥιL�o����֤����n���l��
- �妸�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
- Fork ���M��
- �إ߱z���S�ʤ��� (
git checkout -b feature/AmazingFeature) - ����z����� (
git commit -m 'Add some AmazingFeature') - ���e����� (
git push origin feature/AmazingFeature) - �}�� 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 | Versions 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. |
-
net6.0
- Microsoft.EntityFrameworkCore.Proxies (>= 6.0.0)
-
net7.0
- Microsoft.EntityFrameworkCore.Proxies (>= 7.0.0)
-
net8.0
- Microsoft.EntityFrameworkCore.Proxies (>= 8.0.0)
-
net9.0
- Microsoft.EntityFrameworkCore.Proxies (>= 9.0.9)
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 |