Najlot.Audit.SourceGenerator by Najlot
NuGet / site data
Details
Info
Name: Najlot.Audit.SourceGenerator
Source generator for Najlot.Audit that provides property audit code generation.
Author: Najlot
NuGet: https://www.nuget.org/packages/Najlot.Audit.SourceGenerator/
You can find more details at https://github.com/najlot/Audit
Source: https://github.com/najlot/Audit
Author
Najlot

Original Readme
Audit
Najlot.Audit is a small .NET library for tracking object changes by taking a snapshot of an entity and comparing it later.
You register a provider for a given entity type, create a snapshot, mutate the entity, and then ask the snapshot for the list of changed properties.
The library supports two ways of defining providers:
- Source-generated providers, which are the preferred option for most projects.
- Manual providers, where you return property values by hand.
What the library gives you
- Snapshot-based change tracking.
- Flat property paths such as
Age,Customer.Name, orChecklist[3].IsDone. - Automatic comparison of old and new values.
- Optional source generation for provider implementation and registration.
- Support for provider factories when providers need constructor dependencies.
Installation
Install the runtime package:
dotnet add package Najlot.Audit
If you want to use the preferred source-generator workflow, add the generator package as well:
dotnet add package Najlot.Audit.SourceGenerator
If you reference projects directly instead of NuGet packages, reference the source generator as an analyzer:
<ItemGroup>
<ProjectReference Include="..\Najlot.Audit\Najlot.Audit.csproj" />
<ProjectReference Include="..\Najlot.Audit.SourceGenerator\Najlot.Audit.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
The runtime library targets netstandard2.0 and net8.0.
Preferred way: source generator
For most consumers, this is the best option. You declare what should be audited and the generator creates the provider implementation for you.
######### 1. Create an entity
public sealed class User
{
public Guid Id \{ get; set; }
public string Name \{ get; set; \} = string.Empty;
public int Age \{ get; set; }
public string Password \{ get; set; \} = string.Empty;
}
######### 2. Create an audit provider
Mark the provider class with AuditProvider and declare a partial method that returns IEnumerable<PropertyValue>.
using Najlot.Audit;
using Najlot.Audit.Attributes;
[AuditProvider]
public partial class UserAuditProvider
{
[AuditIgnore(nameof(User.Password))]
public static partial IEnumerable<PropertyValue> GetPropertyValues(User entity);
}
The generator will inspect the entity and emit the method body.
######### 3. Register generated providers
The generator also emits an extension method that registers all public provider methods found in your assembly.
using Najlot.Audit;
var audit = new Audit();
audit.RegisterMyAppAuditProviders();
The exact method name depends on your assembly name. For an assembly named MyApp, the generated method is RegisterMyAppAuditProviders().
You can also register a generated provider explicitly:
var audit = new Audit();
audit.RegisterProvider<UserAuditProvider>();
######### 4. Create a snapshot and read changes
var user = new User
{
Id = Guid.NewGuid(),
Name = "Alice",
Age = 30,
Password = "secret-1"
};
var snapshot = audit.CreateSnapshot(user);
user.Age = 31;
user.Password = "secret-2";
var changes = snapshot.GetChanges().ToList();
foreach (var change in changes)
{
Console.WriteLine($"{change.Path}: {change.OldValue} -> {change.NewValue}");
}
Output:
Age: 30 -> 31
Password is ignored because of AuditIgnore.
Manual way: write providers by hand
If you need full control over paths, derived values, formatting, lookups, or unsupported shapes, write the provider yourself.
using Najlot.Audit;
using Najlot.Audit.Attributes;
public sealed class Order
{
public Guid Id \{ get; set; }
public decimal Total \{ get; set; }
}
[AuditProvider]
public sealed class OrderAuditProvider
{
public IEnumerable<PropertyValue> GetPropertyValues(Order entity)
{
yield return new PropertyValue(nameof(Order.Id), entity.Id);
yield return new PropertyValue(nameof(Order.Total), entity.Total);
}
}
Register it and use it the same way:
var audit = new Audit();
audit.RegisterProvider<OrderAuditProvider>();
var order = new Order
{
Id = Guid.NewGuid(),
Total = 100m
};
var snapshot = audit.CreateSnapshot(order);
order.Total = 125m;
var changes = snapshot.GetChanges().ToList();
Manual providers are useful when:
- You want to emit custom paths.
- You need computed values.
- You want to combine entity state with external data.
- You do not want automatic traversal of nested objects.
Ignoring properties
Ignore values from generated providers by declaring ignored paths on the provider method.
Ignore a path from the provider method:
[AuditProvider]
public partial class UserAuditProvider
{
[AuditIgnore(nameof(User.Password))]
[AuditIgnore(nameof(User.LastLoginAt))]
public static partial IEnumerable<PropertyValue> GetPropertyValues(User entity);
}
Apply AuditIgnore to the provider method. The generator only reads ignore paths declared on provider methods.
Nested objects
Generated providers walk public readable properties. For nested objects, paths are flattened.
Example paths:
Customer.NameAddress.CityMetadata.CreatedBy
If a nested type also has its own audit provider, the generator can delegate to that provider instead of expanding the type inline.
Collections
Generated providers enumerate collection items instead of storing the collection object reference.
If you do not specify a key, the generator falls back to the item index and produces paths such as Checklist[0].IsDone or Tags[1].
If items can be reordered, inserted, or removed and you want stable matching across snapshots, specify a stable key with AuditCollectionKey.
using Najlot.Audit;
using Najlot.Audit.Attributes;
public sealed class ChecklistItem
{
public int Id \{ get; set; }
public string Text \{ get; set; \} = string.Empty;
public bool IsDone \{ get; set; }
}
public sealed class TaskItem
{
public string Title \{ get; set; \} = string.Empty;
public List<ChecklistItem> Checklist \{ get; set; \} = [];
}
[AuditProvider]
public partial class TaskItemAuditProvider
{
[AuditCollectionKey(nameof(entity.Checklist), nameof(ChecklistItem.Id))]
public partial IEnumerable<PropertyValue> GetPropertyValues(TaskItem entity);
}
This produces paths such as Checklist[1].IsDone and lets the audit logic track items by key instead of by list position.
Providers with dependencies
If a provider has constructor dependencies, register a factory before registering the provider or before calling the generated registration extension.
var audit = new Audit();
audit.RegisterFactory(type =>
{
if (type == typeof(UserAuditProviderWithLookup))
{
return new UserAuditProviderWithLookup(new UserNameLookup());
}
return Activator.CreateInstance(type)!;
});
audit.RegisterMyAppAuditProviders();
You can force the factory to be used even when a public parameterless constructor exists:
audit.RegisterFactory(type => Activator.CreateInstance(type)!, alwaysUseFactory: true);
Core API
The main runtime API is intentionally small:
public interface IAudit
{
IAudit Register<T>(AuditProviderMethod<T> method);
void RegisterProvider<T>();
IAudit RegisterFactory(FactoryMethod factory, bool alwaysUseFactory = false);
T Create<T>();
AuditSnapshot<T> CreateSnapshot<T>(T source);
}
The changes returned by a snapshot are PropertyChange values with:
PathOldValueNewValue
When to choose which approach
Use the source generator when:
- Your entities are regular object graphs with public properties.
- You want minimal boilerplate.
- You want generated registration for all providers in the assembly.
Use manual providers when:
- You need custom logic for values or paths.
- You need data from services or lookups.
- You want complete control over the emitted property set.
Notes and limitations
- A provider must be registered before you call
CreateSnapshotfor that entity type. RegisterProvider<T>()discovers supported provider methods through public instance and static methods.- Generated auto-registration includes public provider methods only.
- Collection auditing works best when the chosen key is stable and unique inside the collection.
Development
Run the test suite from the src folder:
dotnet test Najlot.Audit.slnx
About
Generating audit code for classes with properties.
How to use
Example (source csproj, source files)
- CSharp Project
- Program.cs
- Person.cs
This is the CSharp Project that references Najlot.Audit.SourceGenerator
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Najlot.Audit" Version="0.0.1" />
<PackageReference Include="Najlot.Audit.SourceGenerator" Version="0.0.1" />
</ItemGroup>
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
</Project>
This is the use of Najlot.Audit.SourceGenerator in Program.cs
using AuditDemo;
using Najlot.Audit;
Person p = new() \{ Id = 1, FirstName = "John", LastName = "Doe" };
var audit = new Audit();
audit.RegisterProvider<PersonAuditProvider>();
var snapshot = audit.CreateSnapshot(p);
p.LastName = "Ignat";
p.FirstName = "Andrei";
var changes= snapshot.GetChanges().ToArray();
foreach (var change in changes)
{
Console.WriteLine($"Property: {change.Path}, Old Value: {change.OldValue}, New Value: {change.NewValue}");
}
This is the use of Najlot.Audit.SourceGenerator in Person.cs
using Najlot.Audit;
using Najlot.Audit.Attributes;
namespace AuditDemo;
[AuditProvider]
public partial class Person
{
public string FirstName \{ get; set; }= string.Empty;
public string LastName \{ get; set; }= string.Empty;
public string FullName() => $"{FirstName} {LastName}";
public int Id \{ get; set; }
}
[AuditProvider]
public partial class PersonAuditProvider
{
[AuditIgnore(nameof(Person.Id))]
public static partial IEnumerable<PropertyValue> GetPropertyValues(Person entity);
}
Generated Files
Those are taken from $(BaseIntermediateOutputPath)\GX
- globalAuditDemoPersonAuditProvider_AuditProvider.g.cs
- AuditRegistrationExtensions_AuditDemo.g.cs
// <auto-generated />
#nullable enable
namespace AuditDemo
{
public partial class PersonAuditProvider
{
public static partial global::System.Collections.Generic.IEnumerable<global::Najlot.Audit.PropertyValue> GetPropertyValues(global::AuditDemo.Person entity)
{
yield return new global::Najlot.Audit.PropertyValue("FirstName", entity.FirstName);
yield return new global::Najlot.Audit.PropertyValue("LastName", entity.LastName);
}
}
}
// <auto-generated/>
#nullable enable
using Najlot.Audit;
namespace AuditDemo
{
public static class AuditRegistrationExtensions_AuditDemo
{
public static global::Najlot.Audit.IAudit RegisterAuditDemoAuditProviders(this global::Najlot.Audit.IAudit audit)
{
audit.Register<global::AuditDemo.Person>(global::AuditDemo.PersonAuditProvider.GetPropertyValues);
return audit;
}
}
}
Useful
Download Example (.NET C#)
Share Najlot.Audit.SourceGenerator
https://ignatandrei.github.io/RSCG_Examples/v2/docs/Najlot.Audit.SourceGenerator
Category "Audit" has the following generators:
1 Najlot.Audit.SourceGenerator
2026-04-06