Skip to content

Commit

Permalink
Update Documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
milandjurdjevic committed May 4, 2024
1 parent 4aa65b5 commit f8e26e0
Show file tree
Hide file tree
Showing 13 changed files with 194 additions and 149 deletions.
68 changes: 19 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Audentity [![latest version](https://img.shields.io/nuget/v/Audentity)](https://www.nuget.org/packages/Audentity)

### Auditing library for Entity Framework
**Auditing library for Entity Framework**

The Audentity project is designed to enhance data integrity and accountability within applications utilizing the Entity
Framework
Expand All @@ -14,57 +14,27 @@ applications.

## Usage

To collect traces, you want to catch the state of the `Microsoft.EntityFrameworkCore.ChangeTracking.ChangeTracker`
before saving changes. That can be done simply by overriding the `SaveChanges()` & `SaveChangesAsync(CancellationToken)`
methods in your `Microsoft.EntityFrameworkCore.DbContext` implementation.
To read more about how to use Audentity, please refer to the [documentation](src/Audentity/README.md#usage) link.

```csharp
public class Database : DbContext
{
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new())
{
ImmutableList<EntityTrace> traces = ChangeTracker.Entries()
.Select(EntityTrace.FromEntry)
.ToImmutableList();

int result = await base.SaveChangesAsync(cancellationToken);
// Process traces...
return result;
}
}
```
## Benchmark

### Shadow Entries
| Method | Count | Mean | Error | StdDev | Gen0 | Allocated |
|-----------|-------|---------:|---------:|---------:|-------:|----------:|
| FromEntry | 1 | 46.55 ns | 0.080 ns | 0.067 ns | 0.0051 | 32 B |
| FromEntry | 10 | 46.82 ns | 0.082 ns | 0.073 ns | 0.0051 | 32 B |
| FromEntry | 100 | 46.89 ns | 0.041 ns | 0.036 ns | 0.0051 | 32 B |
| FromEntry | 1000 | 46.66 ns | 0.160 ns | 0.150 ns | 0.0051 | 32 B |

If you have many-to-many relationships in your database model, Entity Framework will generate a shadow entity that
represents a reference between two entities - unless you have defined such an entity yourself.
### Legend
- Count : Value of the 'Count' parameter
- Mean : Arithmetic mean of all measurements
- Error : Half of 99.9% confidence interval
- StdDev : Standard deviation of all measurements
- Gen0 : GC Generation 0 collects per 1000 operations
- Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
- 1 ns : 1 Nanosecond (0.000000001 sec)

```csharp
public class Project
{
public Guid Id { get; set; }
public IEnumerable<User> Users { get; set; }
}

public class User
{
public Guid Id { get; set; }
public IEnumerable<Project> Projects { get; set; }
}
## License

// Shadow entity generated by Entity Framework:
public class ProjectUser
{
public Guid ProjectId { get; set; }
public Guid UserId { get; set; }
}
```

Those entities, even if they are not defined in the code itself, will still end up in our trace collection.
To exclude them from traces, you can filter all entries by their CLR type before collecting traces.

```csharp
ChangeTracker.Entries()
.Where(e => e.Metadata.ClrType != typeof(Dictionary<string, object>))
.Select(Entity.FromEntry);
```
This project is licensed under the [MIT License](LICENSE).
5 changes: 4 additions & 1 deletion src/Audentity.Benchmarks/Executor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ public Executor()
[Benchmark]
public void FromEntry()
{
_ = _database.ChangeTracker.Entries().Select(Entity.FromEntry);
foreach (EntityTrace _ in _database.ChangeTracker.Entries().Select(EntityTrace.FromEntry))
{
// Ignore.
}
}
}
12 changes: 12 additions & 0 deletions src/Audentity.Tests/Audentity.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,16 @@
<ProjectReference Include="..\Audentity\Audentity.csproj"/>
</ItemGroup>

<ItemGroup>
<None Update="EntityTests.FromEntry_AddEntity_Collects.verified.json">
<DependentUpon>EntityTraceTests.cs</DependentUpon>
</None>
<None Update="EntityTests.FromEntry_DeleteEntity_Collects.verified.json">
<DependentUpon>EntityTraceTests.cs</DependentUpon>
</None>
<None Update="EntityTests.FromEntry_ModifyEntity_Collects.verified.json">
<DependentUpon>EntityTraceTests.cs</DependentUpon>
</None>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace Audentity.Tests;

[UsesVerify]
public class EntityTests
public class EntityTraceTests
{
private readonly Database _database = new();
private readonly Tenant _tenant = Seeding.Seed().First();
Expand All @@ -10,10 +10,10 @@ public class EntityTests
public Task FromEntry_AddEntity_Collects()
{
_database.Add(_tenant);
IEnumerable<Entity> traces = _database
IEnumerable<EntityTrace> traces = _database
.ChangeTracker
.Entries()
.Select(Entity.FromEntry);
.Select(EntityTrace.FromEntry);

return Verify(traces);
}
Expand All @@ -26,10 +26,10 @@ public Task FromEntry_ModifyEntity_Collects()
_tenant.Name += "Updated";
_tenant.Users.First().Name += "Updated";
_tenant.Projects.First().Name += "Updated";
IEnumerable<Entity> traces = _database
IEnumerable<EntityTrace> traces = _database
.ChangeTracker
.Entries()
.Select(Entity.FromEntry);
.Select(EntityTrace.FromEntry);
return Verify(traces);
}

Expand All @@ -39,9 +39,9 @@ public Task FromEntry_DeleteEntity_Collects()
_database.Add(_tenant);
_database.SaveChanges();
_database.Remove(_tenant);
IEnumerable<Entity> traces = _database.ChangeTracker
IEnumerable<EntityTrace> traces = _database.ChangeTracker
.Entries()
.Select(Entity.FromEntry);
.Select(EntityTrace.FromEntry);
return Verify(traces);
}
}
4 changes: 2 additions & 2 deletions src/Audentity/Audentity.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
</ItemGroup>

<ItemGroup>
<None Include="../../img/icon.png" Pack="true" PackagePath="\"/>
<None Include="../../README.md" Pack="true" PackagePath="\"/>
<None Include="icon.png" Pack="true" PackagePath="\"/>
<None Include="README.md" Pack="true" PackagePath="\"/>
</ItemGroup>

</Project>
30 changes: 0 additions & 30 deletions src/Audentity/Entity.cs

This file was deleted.

28 changes: 28 additions & 0 deletions src/Audentity/EntityTrace.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace Audentity;

public record EntityTrace(
Type Type,
EntityState State,
IReadOnlyCollection<PropertyTrace> Properties,
IReadOnlyCollection<ReferenceTrace> References)
{
public static EntityTrace FromEntry(EntityEntry entry)
{
IReadOnlyCollection<PropertyTrace> properties =
entry
.Properties
.Select(PropertyTrace.FromEntry)
.ToArray();

IReadOnlyCollection<ReferenceTrace> references =
entry
.Navigations
.SelectMany(ReferenceTrace.FromEntry)
.ToArray();

return new EntityTrace(entry.Entity.GetType(), entry.State, properties, references);
}
}
10 changes: 5 additions & 5 deletions src/Audentity/Link.cs → src/Audentity/LinkTrace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@

namespace Audentity;

public record Link(string Name, string Value)
public record LinkTrace(string Name, string Value)
{
internal static Link FromProperty(IProperty property, object entity)
internal static LinkTrace FromProperty(IProperty property, object entity)
{
string value = property.PropertyInfo?.GetValue(entity)?.ToString() ?? String.Empty;
return new Link(property.Name, value);
return new LinkTrace(property.Name, value);
}

internal static Link FromEntry(PropertyEntry entry)
internal static LinkTrace FromEntry(PropertyEntry entry)
{
string value = entry.CurrentValue?.ToString() ?? String.Empty;
return new Link(entry.Metadata.Name, value);
return new LinkTrace(entry.Metadata.Name, value);
}
}
6 changes: 3 additions & 3 deletions src/Audentity/Property.cs → src/Audentity/PropertyTrace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

namespace Audentity;

public record Property(string Name, string? CurrentValue, string? OriginalValue)
public record PropertyTrace(string Name, string? CurrentValue, string? OriginalValue)
{
internal static Property FromEntry(PropertyEntry entry)
internal static PropertyTrace FromEntry(PropertyEntry entry)
{
string? currentValue = entry.CurrentValue?.ToString();
string? originalValue = entry.OriginalValue?.ToString();
string name = entry.Metadata.Name;
return new Property(name, currentValue, originalValue);
return new PropertyTrace(name, currentValue, originalValue);
}
}
60 changes: 60 additions & 0 deletions src/Audentity/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Audentity

**Auditing library for Entity Framework**

## Usage

Collect traces by catching the state of the `Microsoft.EntityFrameworkCore.ChangeTracking.ChangeTracker`
before saving changes. That can be done simply by overriding the `SaveChanges()` & `SaveChangesAsync(CancellationToken)`
methods in your `Microsoft.EntityFrameworkCore.DbContext` implementation.

```csharp
public class MyDbContext : DbContext
{
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new())
{
ImmutableList<EntityTrace> traces = ChangeTracker.Entries()
.Select(EntityTrace.FromEntry)
.ToList();

int result = await base.SaveChangesAsync(cancellationToken);
// Process traces...
return result;
}
}
```

### Shadow Entries

If you have many-to-many relationships in your database model, Entity Framework will generate a shadow entity that
represents a reference between two entities - unless you have defined such an entity yourself.

```csharp
public class Project
{
public Guid Id { get; set; }
public IEnumerable<User> Users { get; set; }
}

public class User
{
public Guid Id { get; set; }
public IEnumerable<Project> Projects { get; set; }
}

// Shadow entity generated by Entity Framework:
public class ProjectUser
{
public Guid ProjectId { get; set; }
public Guid UserId { get; set; }
}
```

Those entities, even if they are not defined in the code itself, will still end up in our trace collection.
To exclude them from traces, you can filter all entries by their CLR type before collecting traces.

```csharp
ChangeTracker.Entries()
.Where(e => e.Metadata.ClrType != typeof(Dictionary<string, object>))
.Select(EntityTrace.FromEntry);
```
52 changes: 0 additions & 52 deletions src/Audentity/Reference.cs

This file was deleted.

Loading

0 comments on commit f8e26e0

Please sign in to comment.