Skip to content
This repository was archived by the owner on Feb 20, 2025. It is now read-only.

Commit 30893d0

Browse files
committed
Bugfixing & unit tests
1 parent 200b758 commit 30893d0

13 files changed

+507
-43
lines changed

apiservices.sln

+34-2
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shiny.Extensions.EntityFram
6969
EndProject
7070
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shiny.Extensions.WebHosting", "src\Shiny.Extensions.WebHosting\Shiny.Extensions.WebHosting.csproj", "{EC01A0CE-325C-4232-AC68-D2A5BF95EF9A}"
7171
EndProject
72+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{B3661F42-EDD4-41AE-A194-7C0B53C4CC5E}"
73+
EndProject
74+
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}"
75+
EndProject
76+
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}"
77+
EndProject
7278
Global
7379
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7480
Debug|Any CPU = Debug|Any CPU
@@ -247,6 +253,30 @@ Global
247253
{EC01A0CE-325C-4232-AC68-D2A5BF95EF9A}.Release|iPhone.Build.0 = Release|Any CPU
248254
{EC01A0CE-325C-4232-AC68-D2A5BF95EF9A}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
249255
{EC01A0CE-325C-4232-AC68-D2A5BF95EF9A}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
256+
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
257+
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
258+
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Debug|iPhone.ActiveCfg = Debug|Any CPU
259+
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Debug|iPhone.Build.0 = Debug|Any CPU
260+
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
261+
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
262+
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
263+
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Release|Any CPU.Build.0 = Release|Any CPU
264+
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Release|iPhone.ActiveCfg = Release|Any CPU
265+
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Release|iPhone.Build.0 = Release|Any CPU
266+
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
267+
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
268+
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
269+
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Debug|Any CPU.Build.0 = Debug|Any CPU
270+
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Debug|iPhone.ActiveCfg = Debug|Any CPU
271+
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Debug|iPhone.Build.0 = Debug|Any CPU
272+
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
273+
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
274+
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Release|Any CPU.ActiveCfg = Release|Any CPU
275+
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Release|Any CPU.Build.0 = Release|Any CPU
276+
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Release|iPhone.ActiveCfg = Release|Any CPU
277+
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Release|iPhone.Build.0 = Release|Any CPU
278+
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
279+
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
250280
EndGlobalSection
251281
GlobalSection(SolutionProperties) = preSolution
252282
HideSolutionNode = FALSE
@@ -257,18 +287,20 @@ Global
257287
{FBA41A83-81AC-41C6-A656-6F697E377B00} = {651C11E3-BBFA-473C-BCFB-91FA22F6DD8A}
258288
{1F3D6907-DD9A-458C-A6BC-A232D7213AFE} = {109FDCE7-7624-4A44-8BBD-4CA986C7A1D7}
259289
{7FF64E64-128A-465E-800D-60C2547C602B} = {109FDCE7-7624-4A44-8BBD-4CA986C7A1D7}
260-
{E77C9155-95ED-4418-8129-4BDCC6BFE501} = {109FDCE7-7624-4A44-8BBD-4CA986C7A1D7}
261290
{0579D360-C000-4EB1-B065-226340DFBBDE} = {8B31ACF0-86B8-4208-A054-940EA9378C1D}
262291
{4D49EF5F-BBF5-4FEB-8EDE-EBDAAD685C04} = {8B31ACF0-86B8-4208-A054-940EA9378C1D}
263292
{E57E68D0-37AE-4ED2-8C60-3A15FD66453E} = {109FDCE7-7624-4A44-8BBD-4CA986C7A1D7}
264293
{E13A95DE-743F-4DFE-834F-2CAF80D2B20B} = {109FDCE7-7624-4A44-8BBD-4CA986C7A1D7}
265-
{7BDAF7A4-A2E3-47B1-95C7-F696591A98C8} = {8B31ACF0-86B8-4208-A054-940EA9378C1D}
266294
{C6F08DCD-2BDA-44DF-8A67-9CA42AF552EC} = {8B31ACF0-86B8-4208-A054-940EA9378C1D}
267295
{6C206667-645A-4777-9098-E2E861E37BA0} = {8B31ACF0-86B8-4208-A054-940EA9378C1D}
268296
{6052CECA-A866-435B-87DB-8AC81F5919FF} = {8B31ACF0-86B8-4208-A054-940EA9378C1D}
269297
{42A541B5-1722-43BF-9475-C710B9032099} = {8B31ACF0-86B8-4208-A054-940EA9378C1D}
270298
{2292E505-10FD-4348-93EC-00541074E841} = {24C20221-23EA-427B-AAEB-5A5F475E7A78}
271299
{EC01A0CE-325C-4232-AC68-D2A5BF95EF9A} = {24C20221-23EA-427B-AAEB-5A5F475E7A78}
300+
{E7EA9679-03E6-4EC4-A1BD-04F92DC152EE} = {B3661F42-EDD4-41AE-A194-7C0B53C4CC5E}
301+
{7BDAF7A4-A2E3-47B1-95C7-F696591A98C8} = {B3661F42-EDD4-41AE-A194-7C0B53C4CC5E}
302+
{E77C9155-95ED-4418-8129-4BDCC6BFE501} = {B3661F42-EDD4-41AE-A194-7C0B53C4CC5E}
303+
{E14F625D-B2D1-48FA-89C7-66909A1C7CFF} = {B3661F42-EDD4-41AE-A194-7C0B53C4CC5E}
272304
EndGlobalSection
273305
GlobalSection(ExtensibilityGlobals) = postSolution
274306
SolutionGuid = {97760152-ADDA-421F-9345-9C7825A99FB7}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Text.Json;
2+
13
namespace Shiny.Auditing;
24

35
public class AuditEntry
@@ -6,15 +8,20 @@ public class AuditEntry
68
public string EntityId { get; set; }
79
public string EntityType { get; set; }
810

9-
public AuditInfo? Info { get; set; }
11+
public string? UserIdentifier { get; set; }
12+
public string? Tenant { get; set; }
13+
public string? AppLocation { get; set; }
14+
public string? UserIpAddress { get; set; }
15+
1016
public DbOperation Operation { get; set; }
1117
public DateTimeOffset Timestamp { get; set; }
12-
public Dictionary<string, object> ChangeSet { get; set; } // TODO: from current main record
18+
public JsonDocument ChangeSet { get; set; }
19+
// public Dictionary<string, object> ChangeSet { get; set; }
1320
}
1421

1522
public enum DbOperation
1623
{
17-
Insert,
18-
Update,
19-
Delete
24+
Insert = 1,
25+
Update = 2,
26+
Delete = 3
2027
}

src/Shiny.Extensions.EntityFramework/Auditing/AuditSaveChangesInterceptor.cs

+36-19
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,26 @@
22
using Microsoft.EntityFrameworkCore.ChangeTracking;
33
using Microsoft.EntityFrameworkCore.Diagnostics;
44
using System.Linq;
5+
using System.Text.Json;
56

67
namespace Shiny.Auditing;
78

89

910
public class AuditSaveChangesInterceptor(IAuditInfoProvider provider) : SaveChangesInterceptor
1011
{
11-
public override int SavedChanges(SaveChangesCompletedEventData eventData, int result)
12+
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
1213
{
1314
var entries = this.GetAuditEntries(eventData);
14-
eventData.Context!.AddRange(entries);
15-
16-
var actualResult = base.SavedChanges(eventData, result);
17-
return actualResult;
15+
eventData.Context!.AddRange(entries);
16+
return base.SavingChangesAsync(eventData, result, cancellationToken);
1817
}
1918

19+
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
20+
{
21+
var entries = this.GetAuditEntries(eventData);
22+
eventData.Context!.AddRange(entries);
23+
return base.SavingChanges(eventData, result);
24+
}
2025

2126
static DbOperation ToOperation(EntityState state)
2227
{
@@ -29,11 +34,9 @@ static DbOperation ToOperation(EntityState state)
2934
return DbOperation.Update;
3035
}
3136

32-
33-
protected virtual List<AuditEntry> GetAuditEntries(SaveChangesCompletedEventData eventData)
37+
protected virtual List<AuditEntry> GetAuditEntries(DbContextEventData eventData)
3438
{
3539
var entries = new List<AuditEntry>();
36-
var auditInfo = provider.GetAuditInfo();
3740
var changeTracker = eventData.Context!.ChangeTracker;
3841
changeTracker.DetectChanges();
3942

@@ -44,19 +47,29 @@ protected virtual List<AuditEntry> GetAuditEntries(SaveChangesCompletedEventData
4447
entry.State != EntityState.Unchanged &&
4548
entry.Entity is IAuditable auditable)
4649
{
47-
auditable.LastEditUserIdentifier = auditInfo.UserIdentifier;
48-
if (auditable.DateCreated == DateTimeOffset.MinValue)
49-
auditable.DateCreated = DateTimeOffset.UtcNow;
50+
if (entry.State == EntityState.Modified)
51+
{
52+
entry.CurrentValues[nameof(IAuditable.DateUpdated)] = DateTimeOffset.UtcNow;
53+
}
54+
else if (entry.State == EntityState.Added)
55+
{
56+
entry.CurrentValues[nameof(IAuditable.DateUpdated)] = DateTimeOffset.UtcNow;
57+
entry.CurrentValues[nameof(IAuditable.DateCreated)] = DateTimeOffset.UtcNow;
58+
}
5059

51-
entry.DetectChanges();
60+
entry.CurrentValues[nameof(IAuditable.LastEditUserIdentifier)] = provider.UserIdentifier;
5261
var auditEntry = new AuditEntry
5362
{
5463
Operation = ToOperation(entry.State),
5564
EntityId = entry.Properties.Single(p => p.Metadata.IsPrimaryKey()).CurrentValue!.ToString()!,
5665
EntityType = entry.Metadata.ClrType.Name,
5766
Timestamp = DateTime.UtcNow,
58-
ChangeSet = this.CalculateChangeSet(entry),
59-
Info = auditInfo
67+
ChangeSet = this.CalculateChangeSet(entry), // TODO: NULL on add
68+
69+
UserIdentifier = provider.UserIdentifier,
70+
UserIpAddress = provider.UserIpAddress,
71+
Tenant = provider.Tenant,
72+
AppLocation = provider.AppLocation
6073
};
6174
entries.Add(auditEntry);
6275
}
@@ -65,8 +78,9 @@ protected virtual List<AuditEntry> GetAuditEntries(SaveChangesCompletedEventData
6578
}
6679

6780

68-
protected virtual Dictionary<string, object> CalculateChangeSet(EntityEntry entry)
81+
protected virtual JsonDocument CalculateChangeSet(EntityEntry entry)
6982
{
83+
// TODO: if I'm deleting, I want all the original values (even ignored?)
7084
var dict = new Dictionary<string, object>();
7185
foreach (var property in entry.Properties)
7286
{
@@ -75,7 +89,9 @@ protected virtual Dictionary<string, object> CalculateChangeSet(EntityEntry entr
7589
dict.Add(property.Metadata.Name, property.OriginalValue ?? "NULL");
7690
}
7791
}
78-
return dict;
92+
93+
var json = JsonSerializer.SerializeToDocument(dict);
94+
return json;
7995
}
8096

8197

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

112+
96113
protected virtual bool IsPropertyIgnored(string propertyName) => propertyName switch
97114
{
98-
nameof(IAuditable.LastEditUserIdentifier) => false,
99-
nameof(IAuditable.DateCreated) => false,
100-
_ => true
115+
nameof(IAuditable.LastEditUserIdentifier) => true,
116+
nameof(IAuditable.DateCreated) => true,
117+
_ => false
101118
};
102119
}

src/Shiny.Extensions.EntityFramework/Auditing/IAuditInfoProvider.cs

+19-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,23 @@ namespace Shiny.Auditing;
22

33
public interface IAuditInfoProvider
44
{
5-
AuditInfo GetAuditInfo();
5+
/// <summary>
6+
/// Can be a URL or anything else if available
7+
/// </summary>
8+
string? AppLocation { get; }
9+
10+
/// <summary>
11+
/// For multi-tenanted apps if available
12+
/// </summary>
13+
string? Tenant { get; }
14+
15+
/// <summary>
16+
/// Your user ID or name if available
17+
/// </summary>
18+
string? UserIdentifier { get; }
19+
20+
/// <summary>
21+
/// The IP address of the remote user if available
22+
/// </summary>
23+
string? UserIpAddress { get; }
624
}
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,8 @@
1-
using Microsoft.EntityFrameworkCore;
2-
31
namespace Shiny.Auditing;
42

53
public interface IAuditable
64
{
75
string? LastEditUserIdentifier { get; set; }
6+
DateTimeOffset DateUpdated { get; set; }
87
DateTimeOffset DateCreated { get; set; }
9-
}
10-
11-
[Owned]
12-
public class AuditInfo
13-
{
14-
public string? UserIdentifier { get; set; }
15-
public string? Url { get; set; }
16-
public string? IpAddress { get; set; }
17-
public DateTimeOffset DateCreated { get; set; }
188
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System.Linq;
2+
using Microsoft.EntityFrameworkCore;
3+
using Shiny.Auditing;
4+
5+
namespace Shiny;
6+
7+
8+
public static class EntityFrameworkExtensions
9+
{
10+
public static ModelConfigurationBuilder SetDefaultStringLength(this ModelConfigurationBuilder configurationBuilder, int length = 50, bool unicode = true)
11+
{
12+
configurationBuilder
13+
.Properties<string>()
14+
.AreUnicode(unicode)
15+
.HaveMaxLength(length);
16+
17+
return configurationBuilder;
18+
}
19+
20+
21+
// modelBuilder
22+
// .Entity<Person>(
23+
// eb =>
24+
// {
25+
// eb.Property(p => p.Addresses).HasConversion(
26+
//
27+
// v => JsonConvert.SerializeObject(v, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }),
28+
// v => JsonConvert.DeserializeObject<IList<Address>>(v, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })
29+
// );
30+
// });
31+
32+
public static ModelBuilder MapAuditing(this ModelBuilder modelBuilder)
33+
{
34+
var map = modelBuilder.Entity<AuditEntry>();
35+
36+
map.HasKey(x => x.Id);
37+
map.Property(x => x.Id).ValueGeneratedOnAdd();
38+
map.Property(x => x.EntityId).HasMaxLength(100);
39+
map.Property(x => x.EntityType).HasMaxLength(255);
40+
map.Property(x => x.Operation);
41+
map.Property(x => x.Timestamp);
42+
map.Property(x => x.ChangeSet);
43+
44+
map.Property(x => x.UserIdentifier).HasMaxLength(50);
45+
map.Property(x => x.UserIpAddress).HasMaxLength(39);
46+
map.Property(x => x.Tenant).HasMaxLength(50);
47+
map.Property(x => x.AppLocation).HasMaxLength(1024);
48+
49+
return modelBuilder;
50+
}
51+
52+
53+
public static ModelBuilder MapEasyPropertyIds(this ModelBuilder modelBuilder)
54+
{
55+
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
56+
{
57+
var idProperty = entityType.GetProperties().FirstOrDefault(x => x.Name.Equals("Id"));
58+
if (idProperty != null && idProperty.GetColumnName().Equals("Id"))
59+
idProperty.SetColumnName(entityType.ClrType.Name + "Id");
60+
}
61+
return modelBuilder;
62+
}
63+
}

src/Shiny.Extensions.EntityFramework/Shiny.Extensions.EntityFramework.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
<ItemGroup>
99
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="$(MicrosoftExtensionsVersion)" />
10+
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="$(MicrosoftExtensionsVersion)" />
1011
</ItemGroup>
1112

1213
</Project>

src/Shiny.Extensions.WebHosting/RegistrationExtensions.cs

+8-5
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,21 @@ public static WebApplicationBuilder AddInfrastructure(this WebApplicationBuilder
5454
{
5555
Console.WriteLine("Registering Infrastructure Module: " + moduleType.FullName);
5656
var module = (IInfrastructureModule)Activator.CreateInstance(moduleType)!;
57-
builder.AddInfrastructureModule(module);
57+
builder.AddInfrastructure(module);
5858
Console.WriteLine("Successfully Registered Infrastructure Module: " + moduleType.FullName);
5959
}
6060
}
6161
return builder;
6262
}
63+
6364

64-
65-
public static WebApplicationBuilder AddInfrastructureModule(this WebApplicationBuilder builder, IInfrastructureModule module)
65+
public static WebApplicationBuilder AddInfrastructure(this WebApplicationBuilder builder, params IInfrastructureModule[] modules)
6666
{
67-
module.Add(builder);
68-
builder.Services.AddTransient<IInfrastructureModule>(_ => module);
67+
foreach (var module in modules)
68+
{
69+
module.Add(builder);
70+
builder.Services.AddTransient<IInfrastructureModule>(_ => module);
71+
}
6972
return builder;
7073
}
7174

0 commit comments

Comments
 (0)