Skip to content

Commit

Permalink
Bugfixing & unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
aritchie committed May 14, 2024
1 parent 200b758 commit 30893d0
Show file tree
Hide file tree
Showing 13 changed files with 507 additions and 43 deletions.
36 changes: 34 additions & 2 deletions apiservices.sln
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shiny.Extensions.EntityFram
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shiny.Extensions.WebHosting", "src\Shiny.Extensions.WebHosting\Shiny.Extensions.WebHosting.csproj", "{EC01A0CE-325C-4232-AC68-D2A5BF95EF9A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{B3661F42-EDD4-41AE-A194-7C0B53C4CC5E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shiny.Extensions.EntityFramework.Tests", "tests\Shiny.Extensions.EntityFramework.Tests\Shiny.Extensions.EntityFramework.Tests.csproj", "{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shiny.Extensions.WebHosting.Tests", "tests\Shiny.Extensions.WebHosting.Tests\Shiny.Extensions.WebHosting.Tests.csproj", "{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -247,6 +253,30 @@ Global
{EC01A0CE-325C-4232-AC68-D2A5BF95EF9A}.Release|iPhone.Build.0 = Release|Any CPU
{EC01A0CE-325C-4232-AC68-D2A5BF95EF9A}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{EC01A0CE-325C-4232-AC68-D2A5BF95EF9A}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Debug|iPhone.Build.0 = Debug|Any CPU
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Release|Any CPU.Build.0 = Release|Any CPU
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Release|iPhone.ActiveCfg = Release|Any CPU
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Release|iPhone.Build.0 = Release|Any CPU
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Debug|iPhone.Build.0 = Debug|Any CPU
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Release|Any CPU.Build.0 = Release|Any CPU
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Release|iPhone.ActiveCfg = Release|Any CPU
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Release|iPhone.Build.0 = Release|Any CPU
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -257,18 +287,20 @@ Global
{FBA41A83-81AC-41C6-A656-6F697E377B00} = {651C11E3-BBFA-473C-BCFB-91FA22F6DD8A}
{1F3D6907-DD9A-458C-A6BC-A232D7213AFE} = {109FDCE7-7624-4A44-8BBD-4CA986C7A1D7}
{7FF64E64-128A-465E-800D-60C2547C602B} = {109FDCE7-7624-4A44-8BBD-4CA986C7A1D7}
{E77C9155-95ED-4418-8129-4BDCC6BFE501} = {109FDCE7-7624-4A44-8BBD-4CA986C7A1D7}
{0579D360-C000-4EB1-B065-226340DFBBDE} = {8B31ACF0-86B8-4208-A054-940EA9378C1D}
{4D49EF5F-BBF5-4FEB-8EDE-EBDAAD685C04} = {8B31ACF0-86B8-4208-A054-940EA9378C1D}
{E57E68D0-37AE-4ED2-8C60-3A15FD66453E} = {109FDCE7-7624-4A44-8BBD-4CA986C7A1D7}
{E13A95DE-743F-4DFE-834F-2CAF80D2B20B} = {109FDCE7-7624-4A44-8BBD-4CA986C7A1D7}
{7BDAF7A4-A2E3-47B1-95C7-F696591A98C8} = {8B31ACF0-86B8-4208-A054-940EA9378C1D}
{C6F08DCD-2BDA-44DF-8A67-9CA42AF552EC} = {8B31ACF0-86B8-4208-A054-940EA9378C1D}
{6C206667-645A-4777-9098-E2E861E37BA0} = {8B31ACF0-86B8-4208-A054-940EA9378C1D}
{6052CECA-A866-435B-87DB-8AC81F5919FF} = {8B31ACF0-86B8-4208-A054-940EA9378C1D}
{42A541B5-1722-43BF-9475-C710B9032099} = {8B31ACF0-86B8-4208-A054-940EA9378C1D}
{2292E505-10FD-4348-93EC-00541074E841} = {24C20221-23EA-427B-AAEB-5A5F475E7A78}
{EC01A0CE-325C-4232-AC68-D2A5BF95EF9A} = {24C20221-23EA-427B-AAEB-5A5F475E7A78}
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE} = {B3661F42-EDD4-41AE-A194-7C0B53C4CC5E}
{7BDAF7A4-A2E3-47B1-95C7-F696591A98C8} = {B3661F42-EDD4-41AE-A194-7C0B53C4CC5E}
{E77C9155-95ED-4418-8129-4BDCC6BFE501} = {B3661F42-EDD4-41AE-A194-7C0B53C4CC5E}
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF} = {B3661F42-EDD4-41AE-A194-7C0B53C4CC5E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {97760152-ADDA-421F-9345-9C7825A99FB7}
Expand Down
17 changes: 12 additions & 5 deletions src/Shiny.Extensions.EntityFramework/Auditing/AuditEntry.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Text.Json;

namespace Shiny.Auditing;

public class AuditEntry
Expand All @@ -6,15 +8,20 @@ public class AuditEntry
public string EntityId { get; set; }
public string EntityType { get; set; }

public AuditInfo? Info { get; set; }
public string? UserIdentifier { get; set; }
public string? Tenant { get; set; }
public string? AppLocation { get; set; }
public string? UserIpAddress { get; set; }

public DbOperation Operation { get; set; }
public DateTimeOffset Timestamp { get; set; }
public Dictionary<string, object> ChangeSet { get; set; } // TODO: from current main record
public JsonDocument ChangeSet { get; set; }
// public Dictionary<string, object> ChangeSet { get; set; }
}

public enum DbOperation
{
Insert,
Update,
Delete
Insert = 1,
Update = 2,
Delete = 3
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,26 @@
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Diagnostics;
using System.Linq;
using System.Text.Json;

namespace Shiny.Auditing;


public class AuditSaveChangesInterceptor(IAuditInfoProvider provider) : SaveChangesInterceptor
{
public override int SavedChanges(SaveChangesCompletedEventData eventData, int result)
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
{
var entries = this.GetAuditEntries(eventData);
eventData.Context!.AddRange(entries);

var actualResult = base.SavedChanges(eventData, result);
return actualResult;
eventData.Context!.AddRange(entries);
return base.SavingChangesAsync(eventData, result, cancellationToken);
}

public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
var entries = this.GetAuditEntries(eventData);
eventData.Context!.AddRange(entries);
return base.SavingChanges(eventData, result);
}

static DbOperation ToOperation(EntityState state)
{
Expand All @@ -29,11 +34,9 @@ static DbOperation ToOperation(EntityState state)
return DbOperation.Update;
}


protected virtual List<AuditEntry> GetAuditEntries(SaveChangesCompletedEventData eventData)
protected virtual List<AuditEntry> GetAuditEntries(DbContextEventData eventData)
{
var entries = new List<AuditEntry>();
var auditInfo = provider.GetAuditInfo();
var changeTracker = eventData.Context!.ChangeTracker;
changeTracker.DetectChanges();

Expand All @@ -44,19 +47,29 @@ protected virtual List<AuditEntry> GetAuditEntries(SaveChangesCompletedEventData
entry.State != EntityState.Unchanged &&
entry.Entity is IAuditable auditable)
{
auditable.LastEditUserIdentifier = auditInfo.UserIdentifier;
if (auditable.DateCreated == DateTimeOffset.MinValue)
auditable.DateCreated = DateTimeOffset.UtcNow;
if (entry.State == EntityState.Modified)
{
entry.CurrentValues[nameof(IAuditable.DateUpdated)] = DateTimeOffset.UtcNow;
}
else if (entry.State == EntityState.Added)
{
entry.CurrentValues[nameof(IAuditable.DateUpdated)] = DateTimeOffset.UtcNow;
entry.CurrentValues[nameof(IAuditable.DateCreated)] = DateTimeOffset.UtcNow;
}

entry.DetectChanges();
entry.CurrentValues[nameof(IAuditable.LastEditUserIdentifier)] = provider.UserIdentifier;
var auditEntry = new AuditEntry
{
Operation = ToOperation(entry.State),
EntityId = entry.Properties.Single(p => p.Metadata.IsPrimaryKey()).CurrentValue!.ToString()!,
EntityType = entry.Metadata.ClrType.Name,
Timestamp = DateTime.UtcNow,
ChangeSet = this.CalculateChangeSet(entry),
Info = auditInfo
ChangeSet = this.CalculateChangeSet(entry), // TODO: NULL on add

UserIdentifier = provider.UserIdentifier,
UserIpAddress = provider.UserIpAddress,
Tenant = provider.Tenant,
AppLocation = provider.AppLocation
};
entries.Add(auditEntry);
}
Expand All @@ -65,8 +78,9 @@ protected virtual List<AuditEntry> GetAuditEntries(SaveChangesCompletedEventData
}


protected virtual Dictionary<string, object> CalculateChangeSet(EntityEntry entry)
protected virtual JsonDocument CalculateChangeSet(EntityEntry entry)
{
// TODO: if I'm deleting, I want all the original values (even ignored?)
var dict = new Dictionary<string, object>();
foreach (var property in entry.Properties)
{
Expand All @@ -75,7 +89,9 @@ protected virtual Dictionary<string, object> CalculateChangeSet(EntityEntry entr
dict.Add(property.Metadata.Name, property.OriginalValue ?? "NULL");
}
}
return dict;

var json = JsonSerializer.SerializeToDocument(dict);
return json;
}


Expand All @@ -93,10 +109,11 @@ protected virtual bool IsAuditedProperty(PropertyEntry entry)
return true;
}


protected virtual bool IsPropertyIgnored(string propertyName) => propertyName switch
{
nameof(IAuditable.LastEditUserIdentifier) => false,
nameof(IAuditable.DateCreated) => false,
_ => true
nameof(IAuditable.LastEditUserIdentifier) => true,
nameof(IAuditable.DateCreated) => true,
_ => false
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,23 @@ namespace Shiny.Auditing;

public interface IAuditInfoProvider
{
AuditInfo GetAuditInfo();
/// <summary>
/// Can be a URL or anything else if available
/// </summary>
string? AppLocation { get; }

/// <summary>
/// For multi-tenanted apps if available
/// </summary>
string? Tenant { get; }

/// <summary>
/// Your user ID or name if available
/// </summary>
string? UserIdentifier { get; }

/// <summary>
/// The IP address of the remote user if available
/// </summary>
string? UserIpAddress { get; }
}
12 changes: 1 addition & 11 deletions src/Shiny.Extensions.EntityFramework/Auditing/IAuditable.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
using Microsoft.EntityFrameworkCore;

namespace Shiny.Auditing;

public interface IAuditable
{
string? LastEditUserIdentifier { get; set; }
DateTimeOffset DateUpdated { get; set; }
DateTimeOffset DateCreated { get; set; }
}

[Owned]
public class AuditInfo
{
public string? UserIdentifier { get; set; }
public string? Url { get; set; }
public string? IpAddress { get; set; }
public DateTimeOffset DateCreated { get; set; }
}
63 changes: 63 additions & 0 deletions src/Shiny.Extensions.EntityFramework/EntityFrameworkExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Shiny.Auditing;

namespace Shiny;


public static class EntityFrameworkExtensions
{
public static ModelConfigurationBuilder SetDefaultStringLength(this ModelConfigurationBuilder configurationBuilder, int length = 50, bool unicode = true)
{
configurationBuilder
.Properties<string>()
.AreUnicode(unicode)
.HaveMaxLength(length);

return configurationBuilder;
}


// modelBuilder
// .Entity<Person>(
// eb =>
// {
// eb.Property(p => p.Addresses).HasConversion(
//
// v => JsonConvert.SerializeObject(v, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }),
// v => JsonConvert.DeserializeObject<IList<Address>>(v, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })
// );
// });

public static ModelBuilder MapAuditing(this ModelBuilder modelBuilder)
{
var map = modelBuilder.Entity<AuditEntry>();

map.HasKey(x => x.Id);
map.Property(x => x.Id).ValueGeneratedOnAdd();
map.Property(x => x.EntityId).HasMaxLength(100);
map.Property(x => x.EntityType).HasMaxLength(255);
map.Property(x => x.Operation);
map.Property(x => x.Timestamp);
map.Property(x => x.ChangeSet);

map.Property(x => x.UserIdentifier).HasMaxLength(50);
map.Property(x => x.UserIpAddress).HasMaxLength(39);
map.Property(x => x.Tenant).HasMaxLength(50);
map.Property(x => x.AppLocation).HasMaxLength(1024);

return modelBuilder;
}


public static ModelBuilder MapEasyPropertyIds(this ModelBuilder modelBuilder)
{
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
var idProperty = entityType.GetProperties().FirstOrDefault(x => x.Name.Equals("Id"));
if (idProperty != null && idProperty.GetColumnName().Equals("Id"))
idProperty.SetColumnName(entityType.ClrType.Name + "Id");
}
return modelBuilder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="$(MicrosoftExtensionsVersion)" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="$(MicrosoftExtensionsVersion)" />
</ItemGroup>

</Project>
13 changes: 8 additions & 5 deletions src/Shiny.Extensions.WebHosting/RegistrationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,21 @@ public static WebApplicationBuilder AddInfrastructure(this WebApplicationBuilder
{
Console.WriteLine("Registering Infrastructure Module: " + moduleType.FullName);
var module = (IInfrastructureModule)Activator.CreateInstance(moduleType)!;
builder.AddInfrastructureModule(module);
builder.AddInfrastructure(module);
Console.WriteLine("Successfully Registered Infrastructure Module: " + moduleType.FullName);
}
}
return builder;
}



public static WebApplicationBuilder AddInfrastructureModule(this WebApplicationBuilder builder, IInfrastructureModule module)
public static WebApplicationBuilder AddInfrastructure(this WebApplicationBuilder builder, params IInfrastructureModule[] modules)
{
module.Add(builder);
builder.Services.AddTransient<IInfrastructureModule>(_ => module);
foreach (var module in modules)
{
module.Add(builder);
builder.Services.AddTransient<IInfrastructureModule>(_ => module);
}
return builder;
}

Expand Down
Loading

0 comments on commit 30893d0

Please sign in to comment.