diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..5a1025e --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,25 @@ +name: .NET + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal diff --git a/EFCoreAuditing.sln b/EFCoreAuditing.sln index 23e83d9..88f680d 100644 --- a/EFCoreAuditing.sln +++ b/EFCoreAuditing.sln @@ -1,9 +1,11 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28307.852 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31808.319 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCoreAuditing", "EFCoreAuditing\EFCoreAuditing.csproj", "{79B289BE-9E5C-44AF-B4DE-62E75E708DE9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCoreAuditing", "EFCoreAuditing\EFCoreAuditing.csproj", "{79B289BE-9E5C-44AF-B4DE-62E75E708DE9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCoreAuditing.Samples.Cars.API", "Samples\EFCoreAuditing.Samples.Cars.API\EFCoreAuditing.Samples.Cars.API.csproj", "{EAF9D501-92EF-400B-B56D-1F2D28903AA7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,6 +17,10 @@ Global {79B289BE-9E5C-44AF-B4DE-62E75E708DE9}.Debug|Any CPU.Build.0 = Debug|Any CPU {79B289BE-9E5C-44AF-B4DE-62E75E708DE9}.Release|Any CPU.ActiveCfg = Release|Any CPU {79B289BE-9E5C-44AF-B4DE-62E75E708DE9}.Release|Any CPU.Build.0 = Release|Any CPU + {EAF9D501-92EF-400B-B56D-1F2D28903AA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EAF9D501-92EF-400B-B56D-1F2D28903AA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EAF9D501-92EF-400B-B56D-1F2D28903AA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EAF9D501-92EF-400B-B56D-1F2D28903AA7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/EFCoreAuditing/AuditLogDbContext.cs b/EFCoreAuditing/AuditLogDbContext.cs index 140fa1b..1f59774 100644 --- a/EFCoreAuditing/AuditLogDbContext.cs +++ b/EFCoreAuditing/AuditLogDbContext.cs @@ -11,199 +11,199 @@ namespace EFCoreAuditing { - public class AuditLogDbContext : DbContext where TKey : IEquatable - { - #region Variables - public string CurrentUserId { get; set; } - IContextConfiguration _contextConfiguration { get; } - public DbSet Audits { get; set; } - #endregion - - public AuditLogDbContext( - DbContextOptions options, - IContextConfiguration contextConfiguration = null) - : base(options) - { - _contextConfiguration = contextConfiguration; - } - - protected override void OnModelCreating(ModelBuilder builder) - { - var entity = builder.Entity(); - - entity.Property(p => p.NewValues).HasColumnType("text"); - entity.Property(p => p.OldValues).HasColumnType("text"); - entity.Property(p => p.KeyValues).HasColumnType("text"); - - base.OnModelCreating(builder); - } - - public override async Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)) - { - UpdateAuditEntities(); - - SoftDelete(); - - List auditEntries = OnBeforeSaveChanges(); - - var result = await base.SaveChangesAsync(cancellationToken); - - await OnAfterSaveChangesAsync(auditEntries); - - return result; - } - - - private List OnBeforeSaveChanges() - { - ChangeTracker.DetectChanges(); - - var auditEntries = new List(); - - foreach (EntityEntry entry in ChangeTracker.Entries().ToList()) - { - if (entry.Entity is Audit || entry.State == EntityState.Detached || - entry.State == EntityState.Unchanged) - continue; - - var auditEntry = new AuditEntry(entry) - { - TableName = entry.Metadata.Relational().TableName, - ModifiedBy = CurrentUserId - }; - - auditEntries.Add(auditEntry); - - foreach (var property in entry.Properties) - { - if (property.IsTemporary) - { - // value will be generated by the database, get the value after saving - auditEntry.TemporaryProperties.Add(property); - continue; - } - - var propertyName = property.Metadata.Name; - if (property.Metadata.IsPrimaryKey()) - { - auditEntry.KeyValues[propertyName] = property.CurrentValue; - continue; - } - - switch (entry.State) - { - case EntityState.Added: - auditEntry.NewValues[propertyName] = property.CurrentValue; - break; - - case EntityState.Modified: - if (property.IsModified) - { - auditEntry.OldValues[propertyName] = property.OriginalValue; - auditEntry.NewValues[propertyName] = property.CurrentValue; - } - break; - - case EntityState.Detached: - case EntityState.Unchanged: - case EntityState.Deleted: - default: - break; - } - } - } - - // Save audit entities that have all the modifications - foreach (var auditEntry in auditEntries.Where(_ => !_.HasTemporaryProperties)) - { - Audits.Add(auditEntry.ToAudit()); - } - - // keep a list of entries where the value of some properties are unknown at this step - return auditEntries - .Where(_ => _.HasTemporaryProperties) - .ToList(); - } - - private Task OnAfterSaveChangesAsync(IReadOnlyCollection auditEntries) - { - if (auditEntries == null || auditEntries.Count == 0) - return Task.CompletedTask; - - foreach (var auditEntry in auditEntries) - { - // Get the final value of the temporary properties - foreach (var prop in auditEntry.TemporaryProperties) - { - if (prop.Metadata.IsPrimaryKey()) - { - auditEntry.KeyValues[prop.Metadata.Name] = prop.CurrentValue; - } - else - { - auditEntry.NewValues[prop.Metadata.Name] = prop.CurrentValue; - } - } - - // Save the Audit entry - Audits.Add(auditEntry.ToAudit()); - } - - return base.SaveChangesAsync(); - } - - private void UpdateAuditEntities() - { - var modifiedEntries = ChangeTracker.Entries() - .Where(x => x.Entity is IAuditableEntity && - (x.State == EntityState.Added || x.State == EntityState.Modified)); - - foreach (var entry in modifiedEntries) - { - var entity = (IAuditableEntity)entry.Entity; - var now = DateTime.UtcNow; - - if (entry.State == EntityState.Added) - { - entity.CreatedDate = now; - entity.CreatedBy = CurrentUserId; - } - else - { - entity.UpdatedDate = now; - entity.UpdatedBy = CurrentUserId; - Entry(entity).Property(x => x.CreatedBy).IsModified = false; - Entry(entity).Property(x => x.CreatedDate).IsModified = false; - } - } - } - - private void SoftDelete() - { - if (!ModelBuilderExtensions.IsEnabledSoftDelete) - return; - - ChangeTracker.DetectChanges(); - - List markedAsDeleted = ChangeTracker - .Entries() - .Where(x => x.State == EntityState.Deleted) - .ToList(); - - foreach (EntityEntry entry in markedAsDeleted) - { - if (entry.State == EntityState.Deleted) - { - Attach(entry.Entity); - - foreach (var property in entry.Properties) - { - property.IsModified = false; - } - - entry.CurrentValues["is_deleted"] = true; - } - } - } - } + public class AuditLogDbContext : DbContext where TKey : IEquatable + { + #region Variables + public string CurrentUserId { get; set; } + IContextConfiguration _contextConfiguration { get; } + public DbSet Audits { get; set; } + #endregion + + public AuditLogDbContext( + DbContextOptions options, + IContextConfiguration contextConfiguration = null) + : base(options) + { + _contextConfiguration = contextConfiguration; + } + + protected override void OnModelCreating(ModelBuilder builder) + { + var entity = builder.Entity(); + + entity.Property(p => p.NewValues).HasColumnType("text"); + entity.Property(p => p.OldValues).HasColumnType("text"); + entity.Property(p => p.KeyValues).HasColumnType("text"); + + base.OnModelCreating(builder); + } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + UpdateAuditEntities(); + + SoftDelete(); + + List auditEntries = OnBeforeSaveChanges(); + + var result = await base.SaveChangesAsync(cancellationToken); + + await OnAfterSaveChangesAsync(auditEntries); + + return result; + } + + + private List OnBeforeSaveChanges() + { + ChangeTracker.DetectChanges(); + + var auditEntries = new List(); + + foreach (EntityEntry entry in ChangeTracker.Entries().ToList()) + { + if (entry.Entity is Audit || entry.State == EntityState.Detached || + entry.State == EntityState.Unchanged) + continue; + + var auditEntry = new AuditEntry(entry) + { + TableName = entry.Metadata.GetTableName(), + ModifiedBy = CurrentUserId + }; + + auditEntries.Add(auditEntry); + + foreach (var property in entry.Properties) + { + if (property.IsTemporary) + { + // value will be generated by the database, get the value after saving + auditEntry.TemporaryProperties.Add(property); + continue; + } + + var propertyName = property.Metadata.Name; + if (property.Metadata.IsPrimaryKey()) + { + auditEntry.KeyValues[propertyName] = property.CurrentValue; + continue; + } + + switch (entry.State) + { + case EntityState.Added: + auditEntry.NewValues[propertyName] = property.CurrentValue; + break; + + case EntityState.Modified: + if (property.IsModified) + { + auditEntry.OldValues[propertyName] = property.OriginalValue; + auditEntry.NewValues[propertyName] = property.CurrentValue; + } + break; + + case EntityState.Detached: + case EntityState.Unchanged: + case EntityState.Deleted: + default: + break; + } + } + } + + // Save audit entities that have all the modifications + foreach (var auditEntry in auditEntries.Where(_ => !_.HasTemporaryProperties)) + { + Audits.Add(auditEntry.ToAudit()); + } + + // keep a list of entries where the value of some properties are unknown at this step + return auditEntries + .Where(_ => _.HasTemporaryProperties) + .ToList(); + } + + private Task OnAfterSaveChangesAsync(IReadOnlyCollection auditEntries) + { + if (auditEntries == null || auditEntries.Count == 0) + return Task.CompletedTask; + + foreach (var auditEntry in auditEntries) + { + // Get the final value of the temporary properties + foreach (var prop in auditEntry.TemporaryProperties) + { + if (prop.Metadata.IsPrimaryKey()) + { + auditEntry.KeyValues[prop.Metadata.Name] = prop.CurrentValue; + } + else + { + auditEntry.NewValues[prop.Metadata.Name] = prop.CurrentValue; + } + } + + // Save the Audit entry + Audits.Add(auditEntry.ToAudit()); + } + + return base.SaveChangesAsync(); + } + + private void UpdateAuditEntities() + { + var modifiedEntries = ChangeTracker.Entries() + .Where(x => x.Entity is IAuditableEntity && + (x.State == EntityState.Added || x.State == EntityState.Modified)); + + foreach (var entry in modifiedEntries) + { + var entity = (IAuditableEntity)entry.Entity; + var now = DateTime.UtcNow; + + if (entry.State == EntityState.Added) + { + entity.CreatedDate = now; + entity.CreatedBy = CurrentUserId; + } + else + { + entity.UpdatedDate = now; + entity.UpdatedBy = CurrentUserId; + Entry(entity).Property(x => x.CreatedBy).IsModified = false; + Entry(entity).Property(x => x.CreatedDate).IsModified = false; + } + } + } + + private void SoftDelete() + { + if (!ModelBuilderExtensions.IsEnabledSoftDelete) + return; + + ChangeTracker.DetectChanges(); + + List markedAsDeleted = ChangeTracker + .Entries() + .Where(x => x.State == EntityState.Deleted) + .ToList(); + + foreach (EntityEntry entry in markedAsDeleted) + { + if (entry.State == EntityState.Deleted) + { + Attach(entry.Entity); + + foreach (var property in entry.Properties) + { + property.IsModified = false; + } + + entry.CurrentValues["is_deleted"] = true; + } + } + } + } } \ No newline at end of file diff --git a/EFCoreAuditing/EFCoreAuditing.csproj b/EFCoreAuditing/EFCoreAuditing.csproj index d8e54c5..d3bc39e 100644 --- a/EFCoreAuditing/EFCoreAuditing.csproj +++ b/EFCoreAuditing/EFCoreAuditing.csproj @@ -1,7 +1,7 @@ - netstandard2.0 + netstandard2.1 Oktay Kır https://github.com/OKTAYKIR/EFCoreAuditing https://github.com/OKTAYKIR/EFCoreAuditing @@ -16,8 +16,8 @@ - - + + all runtime; build; native; contentfiles; analyzers diff --git a/EFCoreAuditing/Extensions/ModelBuilderExtensions.cs b/EFCoreAuditing/Extensions/ModelBuilderExtensions.cs index d9df4c2..a0502d8 100644 --- a/EFCoreAuditing/Extensions/ModelBuilderExtensions.cs +++ b/EFCoreAuditing/Extensions/ModelBuilderExtensions.cs @@ -4,154 +4,154 @@ namespace EFCoreAuditing.Extensions { - public static class ModelBuilderExtensions - { - internal static bool IsEnabledSoftDelete = false; - - //public static void ApplyAllTypeConfigurations(this ModelBuilder modelBuilder, string nameSpace) - // where TContext : DbContext - //{ - // var applyConfigurationMethodInfo = modelBuilder - // .GetType() - // .GetMethods(BindingFlags.Instance | BindingFlags.Public) - // .First(m => m.Name.Equals("ApplyConfiguration", StringComparison.OrdinalIgnoreCase)); - - // var ret = typeof(TContext).Assembly - // .GetTypes() - // .Where(t => t.Namespace == nameSpace) - // .Select(t => - // (t, i: t.GetInterfaces().FirstOrDefault(i => - // i.Name.Equals(typeof(IEntityTypeConfiguration<>).Name, StringComparison.Ordinal)))) - // .Where(it => it.i != null) - // .Select(it => (et: it.i.GetGenericArguments()[0], cfgObj: Activator.CreateInstance(it.t))) - // .Select(it => applyConfigurationMethodInfo.MakeGenericMethod(it.et) - // .Invoke(modelBuilder, new[] { it.cfgObj })) - // .ToList(); - //} - - //public static ModelBuilder ModifyAllTypeConfigurations(this ModelBuilder modelBuilder) - //{ - // foreach (var entity in modelBuilder.Model.GetEntityTypes()) - // { - // SnakeCaseifyTableName(entity); - - // foreach (var property in entity.GetProperties()) - // { - // SnakeCaseifyColumnName(property); - - // ModifyProperty(property); - // } - // } - // return modelBuilder; - //} - - //private static void ModifyProperty(Microsoft.EntityFrameworkCore.Metadata.IMutableProperty property) - //{ - // if (property.ClrType == typeof(string)) - // { - // if (property.GetMaxLength() == null && property.Relational().ColumnType == null) - // property.SetMaxLength(256); - // } - //} - - //private static void SnakeCaseifyColumnName(Microsoft.EntityFrameworkCore.Metadata.IMutableProperty property) - //{ - // property.Relational().ColumnName = CamelCaseToSnakeCase(property.Relational().ColumnName); - //} - - //private static void SnakeCaseifyTableName(Microsoft.EntityFrameworkCore.Metadata.IMutableEntityType entity) - //{ - // entity.Relational().TableName = CamelCaseToSnakeCase(entity.Relational().TableName); - //} - - public static ModelBuilder SnakeCaseifyNames(this ModelBuilder modelBuilder) - { - foreach (var entity in modelBuilder.Model.GetEntityTypes()) - { - // modify table name - entity.Relational().TableName = CamelCaseToSnakeCase(entity.Relational().TableName); - - // modify column names - foreach (var property in entity.GetProperties()) - { - property.Relational().ColumnName = CamelCaseToSnakeCase(property.Relational().ColumnName); - } - } - - return modelBuilder; - } - - //public static ModelBuilder UseCitext(this ModelBuilder modelBuilder, DataStoreType dataStoreType = DataStoreType.PostgreSql) - //{ - - // modelBuilder = modelBuilder.HasPostgresExtension("citext"); - - // foreach (var entity in modelBuilder.Model.GetEntityTypes()) - // { - // foreach (var property in entity.GetProperties()) - // { - // if (dataStoreType == DataStoreType.PostgreSql) - // { - // if (property.ClrType == typeof(string)) - // { - // property.Npgsql().ColumnType = "public.citext"; - // } - // } - // } - // } - - // return modelBuilder; - //} - - public static ModelBuilder EnableSoftDelete(this ModelBuilder modelBuilder) - { - IsEnabledSoftDelete = true; - - foreach (var entityType in modelBuilder.Model.GetEntityTypes()) - { - // 1. Add the IsDeleted property - entityType.GetOrAddProperty("is_deleted", typeof(bool)); - - // 2. Create the query filter - - var parameter = Expression.Parameter(entityType.ClrType); - - // EF.Property(post, "is_deleted") - var propertyMethodInfo = typeof(EF).GetMethod("Property").MakeGenericMethod(typeof(bool)); - var isDeletedProperty = - Expression.Call(propertyMethodInfo, parameter, Expression.Constant("is_deleted")); - - // EF.Property(post, "is_deleted") == false - BinaryExpression compareExpression = Expression.MakeBinary(ExpressionType.Equal, isDeletedProperty, - Expression.Constant(false)); - - // post => EF.Property(post, "is_deleted") == false - var lambda = Expression.Lambda(compareExpression, parameter); - - modelBuilder.Entity(entityType.ClrType).HasQueryFilter(lambda); - } - - return modelBuilder; - } - - private static string CamelCaseToSnakeCase(string clrName) - { - var sb = new StringBuilder(); - for (var i = 0; i < clrName.Length; i++) - { - var c = clrName[i]; - if (char.IsUpper(c)) - { - if (i > 0) - sb.Append('_'); - sb.Append(char.ToLowerInvariant(c)); - continue; - } - - sb.Append(c); - } - - return sb.ToString(); - } - } + public static class ModelBuilderExtensions + { + internal static bool IsEnabledSoftDelete = false; + + //public static void ApplyAllTypeConfigurations(this ModelBuilder modelBuilder, string nameSpace) + // where TContext : DbContext + //{ + // var applyConfigurationMethodInfo = modelBuilder + // .GetType() + // .GetMethods(BindingFlags.Instance | BindingFlags.Public) + // .First(m => m.Name.Equals("ApplyConfiguration", StringComparison.OrdinalIgnoreCase)); + + // var ret = typeof(TContext).Assembly + // .GetTypes() + // .Where(t => t.Namespace == nameSpace) + // .Select(t => + // (t, i: t.GetInterfaces().FirstOrDefault(i => + // i.Name.Equals(typeof(IEntityTypeConfiguration<>).Name, StringComparison.Ordinal)))) + // .Where(it => it.i != null) + // .Select(it => (et: it.i.GetGenericArguments()[0], cfgObj: Activator.CreateInstance(it.t))) + // .Select(it => applyConfigurationMethodInfo.MakeGenericMethod(it.et) + // .Invoke(modelBuilder, new[] { it.cfgObj })) + // .ToList(); + //} + + //public static ModelBuilder ModifyAllTypeConfigurations(this ModelBuilder modelBuilder) + //{ + // foreach (var entity in modelBuilder.Model.GetEntityTypes()) + // { + // SnakeCaseifyTableName(entity); + + // foreach (var property in entity.GetProperties()) + // { + // SnakeCaseifyColumnName(property); + + // ModifyProperty(property); + // } + // } + // return modelBuilder; + //} + + //private static void ModifyProperty(Microsoft.EntityFrameworkCore.Metadata.IMutableProperty property) + //{ + // if (property.ClrType == typeof(string)) + // { + // if (property.GetMaxLength() == null && property.Relational().ColumnType == null) + // property.SetMaxLength(256); + // } + //} + + //private static void SnakeCaseifyColumnName(Microsoft.EntityFrameworkCore.Metadata.IMutableProperty property) + //{ + // property.Relational().ColumnName = CamelCaseToSnakeCase(property.Relational().ColumnName); + //} + + //private static void SnakeCaseifyTableName(Microsoft.EntityFrameworkCore.Metadata.IMutableEntityType entity) + //{ + // entity.Relational().TableName = CamelCaseToSnakeCase(entity.Relational().TableName); + //} + + public static ModelBuilder SnakeCaseifyNames(this ModelBuilder modelBuilder) + { + foreach (var entity in modelBuilder.Model.GetEntityTypes()) + { + // modify table name + entity.SetTableName(CamelCaseToSnakeCase(entity.GetTableName())); + + // modify column names + foreach (var property in entity.GetProperties()) + { + property.SetColumnName(CamelCaseToSnakeCase(property.GetColumnName())); + } + } + + return modelBuilder; + } + + //public static ModelBuilder UseCitext(this ModelBuilder modelBuilder, DataStoreType dataStoreType = DataStoreType.PostgreSql) + //{ + + // modelBuilder = modelBuilder.HasPostgresExtension("citext"); + + // foreach (var entity in modelBuilder.Model.GetEntityTypes()) + // { + // foreach (var property in entity.GetProperties()) + // { + // if (dataStoreType == DataStoreType.PostgreSql) + // { + // if (property.ClrType == typeof(string)) + // { + // property.Npgsql().ColumnType = "public.citext"; + // } + // } + // } + // } + + // return modelBuilder; + //} + + public static ModelBuilder EnableSoftDelete(this ModelBuilder modelBuilder) + { + IsEnabledSoftDelete = true; + + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + // 1. Add the IsDeleted property + entityType.AddProperty("is_deleted", typeof(bool)); + + // 2. Create the query filter + + var parameter = Expression.Parameter(entityType.ClrType); + + // EF.Property(post, "is_deleted") + var propertyMethodInfo = typeof(EF).GetMethod("Property").MakeGenericMethod(typeof(bool)); + var isDeletedProperty = + Expression.Call(propertyMethodInfo, parameter, Expression.Constant("is_deleted")); + + // EF.Property(post, "is_deleted") == false + BinaryExpression compareExpression = Expression.MakeBinary(ExpressionType.Equal, isDeletedProperty, + Expression.Constant(false)); + + // post => EF.Property(post, "is_deleted") == false + var lambda = Expression.Lambda(compareExpression, parameter); + + modelBuilder.Entity(entityType.ClrType).HasQueryFilter(lambda); + } + + return modelBuilder; + } + + private static string CamelCaseToSnakeCase(string clrName) + { + var sb = new StringBuilder(); + for (var i = 0; i < clrName.Length; i++) + { + var c = clrName[i]; + if (char.IsUpper(c)) + { + if (i > 0) + sb.Append('_'); + sb.Append(char.ToLowerInvariant(c)); + continue; + } + + sb.Append(c); + } + + return sb.ToString(); + } + } } diff --git a/Samples/EFCoreAuditing.Samples.Cars.API/AddOrUpdateCarEntity.cs b/Samples/EFCoreAuditing.Samples.Cars.API/AddOrUpdateCarEntity.cs new file mode 100644 index 0000000..c0f6262 --- /dev/null +++ b/Samples/EFCoreAuditing.Samples.Cars.API/AddOrUpdateCarEntity.cs @@ -0,0 +1,10 @@ +namespace EFCoreAuditing.Samples.Cars.API +{ + public class AddOrUpdateCarEntity + { + public string Brand { get; set; } + public string RegistrationNumber { get; set; } + public string Owner { get; set; } + public string MarketValue { get; set; } + } +} diff --git a/Samples/EFCoreAuditing.Samples.Cars.API/Car.cs b/Samples/EFCoreAuditing.Samples.Cars.API/Car.cs new file mode 100644 index 0000000..9775a93 --- /dev/null +++ b/Samples/EFCoreAuditing.Samples.Cars.API/Car.cs @@ -0,0 +1,17 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace EFCoreAuditing.Samples.Cars.API +{ + public class Car + { + [Key] + public Guid Id { get; set; } + public string Brand { get; set; } + public string RegistrationNumber { get; set; } + public string Owner { get; set; } + public string MarketValue { get; set; } + public DateTimeOffset CreatedDate { get; set; } + + } +} diff --git a/Samples/EFCoreAuditing.Samples.Cars.API/CarViewModel.cs b/Samples/EFCoreAuditing.Samples.Cars.API/CarViewModel.cs new file mode 100644 index 0000000..1440786 --- /dev/null +++ b/Samples/EFCoreAuditing.Samples.Cars.API/CarViewModel.cs @@ -0,0 +1,14 @@ +using System; + +namespace EFCoreAuditing.Samples.Cars.API +{ + public class CarViewModel + { + public Guid Id { get; set; } + public string Brand { get; set; } + public string RegistrationNumber { get; set; } + public string Owner { get; set; } + public string MarketValue { get; set; } + public DateTimeOffset CreatedDate { get; set; } + } +} diff --git a/Samples/EFCoreAuditing.Samples.Cars.API/CarsrDbContext.cs b/Samples/EFCoreAuditing.Samples.Cars.API/CarsrDbContext.cs new file mode 100644 index 0000000..3a9b87c --- /dev/null +++ b/Samples/EFCoreAuditing.Samples.Cars.API/CarsrDbContext.cs @@ -0,0 +1,25 @@ +using EFCoreAuditing.Extensions; +using Microsoft.EntityFrameworkCore; +using System; + +namespace EFCoreAuditing.Samples.Cars.API +{ + public class CarsrDbContext : AuditLogDbContext + { + public CarsrDbContext(DbContextOptions dbOptions) + : base(dbOptions) + { + } + + public DbSet Cars { get; set; } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder + .SnakeCaseifyNames() //Change all the table names and column names to snake_case. + .EnableSoftDelete(); //Enable soft-delete functionality. + } + } +} diff --git a/Samples/EFCoreAuditing.Samples.Cars.API/Controllers/AuditsController.cs b/Samples/EFCoreAuditing.Samples.Cars.API/Controllers/AuditsController.cs new file mode 100644 index 0000000..c0ae519 --- /dev/null +++ b/Samples/EFCoreAuditing.Samples.Cars.API/Controllers/AuditsController.cs @@ -0,0 +1,26 @@ +using EFCoreAuditing.Models; +using Microsoft.AspNetCore.Mvc; +using Pagination.EntityFrameworkCore.Extensions; +using System.Threading.Tasks; + +namespace EFCoreAuditing.Samples.Cars.API.Controllers +{ + [ApiController] + [Route("[controller]")] + public class AuditsController : ControllerBase + { + private readonly CarsrDbContext _context; + public AuditsController(CarsrDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task>> GetAudits(int page = 1, int limit = 20) + { + var audits = await _context.Audits.AsPaginationAsync(page, limit); + return Ok(audits); + } + + } +} diff --git a/Samples/EFCoreAuditing.Samples.Cars.API/Controllers/CarsController.cs b/Samples/EFCoreAuditing.Samples.Cars.API/Controllers/CarsController.cs new file mode 100644 index 0000000..e4fe810 --- /dev/null +++ b/Samples/EFCoreAuditing.Samples.Cars.API/Controllers/CarsController.cs @@ -0,0 +1,120 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Pagination.EntityFrameworkCore.Extensions; +using System; +using System.Threading.Tasks; + +namespace EFCoreAuditing.Samples.Cars.API.Controllers +{ + [ApiController] + [Route("[controller]")] + public class CarsController : ControllerBase + { + private readonly CarsrDbContext _context; + public CarsController(CarsrDbContext context) + { + _context = context; + } + + [HttpPost] + public async Task> AddCar(AddOrUpdateCarEntity entity) + { + if (string.IsNullOrEmpty(entity?.Brand)) + { + return BadRequest("Brand is required."); + } + if (string.IsNullOrEmpty(entity?.RegistrationNumber)) + { + return BadRequest("RegistrationNumber is required."); + } + if (string.IsNullOrEmpty(entity?.MarketValue)) + { + return BadRequest("MarketValue is required."); + } + if (string.IsNullOrEmpty(entity?.Owner)) + { + return BadRequest("Owner is required."); + } + var car = new Car + { + Brand = entity.Brand, + CreatedDate = DateTimeOffset.UtcNow, + Id = Guid.NewGuid(), + MarketValue = entity.MarketValue, + Owner = entity.Owner, + RegistrationNumber = entity.RegistrationNumber, + }; + var addedCar = _context.Cars.Add(car); + await _context.SaveChangesAsync(); + return Ok(ConvertToCarViewModel(addedCar.Entity)); + } + + [HttpPut("id")] + public async Task> UpdateCar(Guid id, AddOrUpdateCarEntity entity) + { + if (string.IsNullOrEmpty(entity?.Brand)) + { + return BadRequest("Brand is required."); + } + if (string.IsNullOrEmpty(entity?.RegistrationNumber)) + { + return BadRequest("RegistrationNumber is required."); + } + if (string.IsNullOrEmpty(entity?.MarketValue)) + { + return BadRequest("MarketValue is required."); + } + if (string.IsNullOrEmpty(entity?.Owner)) + { + return BadRequest("Owner is required."); + } + var car = await _context.Cars.FirstOrDefaultAsync(a => a.Id == id); + if (car == null) + { + return NotFound(); + } + car.Brand = entity.Brand; + car.RegistrationNumber = entity.RegistrationNumber; + car.MarketValue = entity.MarketValue; + car.Owner = entity.Owner; + + var addedCar = _context.Cars.Update(car); + await _context.SaveChangesAsync(); + return Ok(ConvertToCarViewModel(addedCar.Entity)); + } + + [HttpGet("id")] + public async Task> GetCar(Guid id) + { + + var car = await _context.Cars.FirstOrDefaultAsync(a => a.Id == id); + if (car == null) + { + return NotFound(); + } + var addedCar = _context.Cars.Update(car); + await _context.SaveChangesAsync(); + return Ok(ConvertToCarViewModel(addedCar.Entity)); + } + + [HttpGet] + public async Task>> GetCars(int page = 1, int limit = 20) + { + var cars = await _context.Cars.AsPaginationAsync(page, limit, ConvertToCarViewModel); + return Ok(cars); + } + + private CarViewModel ConvertToCarViewModel(Car car) + { + return new CarViewModel + { + Brand = car.Brand, + Id = car.Id, + CreatedDate = car.CreatedDate, + MarketValue = car.MarketValue, + Owner = car.Owner, + RegistrationNumber = car.RegistrationNumber + }; + } + } +} diff --git a/Samples/EFCoreAuditing.Samples.Cars.API/EFCoreAuditing.Samples.Cars.API.csproj b/Samples/EFCoreAuditing.Samples.Cars.API/EFCoreAuditing.Samples.Cars.API.csproj new file mode 100644 index 0000000..8f1e731 --- /dev/null +++ b/Samples/EFCoreAuditing.Samples.Cars.API/EFCoreAuditing.Samples.Cars.API.csproj @@ -0,0 +1,25 @@ + + + + netcoreapp3.1 + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/Samples/EFCoreAuditing.Samples.Cars.API/Migrations/20211015221422_Initial_migration.Designer.cs b/Samples/EFCoreAuditing.Samples.Cars.API/Migrations/20211015221422_Initial_migration.Designer.cs new file mode 100644 index 0000000..3738374 --- /dev/null +++ b/Samples/EFCoreAuditing.Samples.Cars.API/Migrations/20211015221422_Initial_migration.Designer.cs @@ -0,0 +1,100 @@ +// +using System; +using EFCoreAuditing.Samples.Cars.API; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace EFCoreAuditing.Samples.Cars.API.Migrations +{ + [DbContext(typeof(CarsrDbContext))] + [Migration("20211015221422_Initial_migration")] + partial class Initial_migration + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("EFCoreAuditing.Models.Audit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("uniqueidentifier"); + + b.Property("DateTime") + .HasColumnName("date_time") + .HasColumnType("datetime2"); + + b.Property("KeyValues") + .HasColumnName("key_values") + .HasColumnType("text"); + + b.Property("ModifiedBy") + .HasColumnName("modified_by") + .HasColumnType("nvarchar(max)"); + + b.Property("NewValues") + .HasColumnName("new_values") + .HasColumnType("text"); + + b.Property("OldValues") + .HasColumnName("old_values") + .HasColumnType("text"); + + b.Property("TableName") + .HasColumnName("table_name") + .HasColumnType("nvarchar(max)"); + + b.Property("is_deleted") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("audits"); + }); + + modelBuilder.Entity("EFCoreAuditing.Samples.Cars.API.Car", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("uniqueidentifier"); + + b.Property("Brand") + .HasColumnName("brand") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnName("created_date") + .HasColumnType("datetimeoffset"); + + b.Property("MarketValue") + .HasColumnName("market_value") + .HasColumnType("nvarchar(max)"); + + b.Property("Owner") + .HasColumnName("owner") + .HasColumnType("nvarchar(max)"); + + b.Property("RegistrationNumber") + .HasColumnName("registration_number") + .HasColumnType("nvarchar(max)"); + + b.Property("is_deleted") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("cars"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Samples/EFCoreAuditing.Samples.Cars.API/Migrations/20211015221422_Initial_migration.cs b/Samples/EFCoreAuditing.Samples.Cars.API/Migrations/20211015221422_Initial_migration.cs new file mode 100644 index 0000000..51d1bf4 --- /dev/null +++ b/Samples/EFCoreAuditing.Samples.Cars.API/Migrations/20211015221422_Initial_migration.cs @@ -0,0 +1,55 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace EFCoreAuditing.Samples.Cars.API.Migrations +{ + public partial class Initial_migration : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "audits", + columns: table => new + { + id = table.Column(nullable: false), + modified_by = table.Column(nullable: true), + table_name = table.Column(nullable: true), + date_time = table.Column(nullable: false), + key_values = table.Column(type: "text", nullable: true), + old_values = table.Column(type: "text", nullable: true), + new_values = table.Column(type: "text", nullable: true), + is_deleted = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_audits", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "cars", + columns: table => new + { + id = table.Column(nullable: false), + brand = table.Column(nullable: true), + registration_number = table.Column(nullable: true), + owner = table.Column(nullable: true), + market_value = table.Column(nullable: true), + created_date = table.Column(nullable: false), + is_deleted = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_cars", x => x.id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "audits"); + + migrationBuilder.DropTable( + name: "cars"); + } + } +} diff --git a/Samples/EFCoreAuditing.Samples.Cars.API/Migrations/CarsrDbContextModelSnapshot.cs b/Samples/EFCoreAuditing.Samples.Cars.API/Migrations/CarsrDbContextModelSnapshot.cs new file mode 100644 index 0000000..f003316 --- /dev/null +++ b/Samples/EFCoreAuditing.Samples.Cars.API/Migrations/CarsrDbContextModelSnapshot.cs @@ -0,0 +1,98 @@ +// +using System; +using EFCoreAuditing.Samples.Cars.API; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace EFCoreAuditing.Samples.Cars.API.Migrations +{ + [DbContext(typeof(CarsrDbContext))] + partial class CarsrDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("EFCoreAuditing.Models.Audit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("uniqueidentifier"); + + b.Property("DateTime") + .HasColumnName("date_time") + .HasColumnType("datetime2"); + + b.Property("KeyValues") + .HasColumnName("key_values") + .HasColumnType("text"); + + b.Property("ModifiedBy") + .HasColumnName("modified_by") + .HasColumnType("nvarchar(max)"); + + b.Property("NewValues") + .HasColumnName("new_values") + .HasColumnType("text"); + + b.Property("OldValues") + .HasColumnName("old_values") + .HasColumnType("text"); + + b.Property("TableName") + .HasColumnName("table_name") + .HasColumnType("nvarchar(max)"); + + b.Property("is_deleted") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("audits"); + }); + + modelBuilder.Entity("EFCoreAuditing.Samples.Cars.API.Car", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("uniqueidentifier"); + + b.Property("Brand") + .HasColumnName("brand") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnName("created_date") + .HasColumnType("datetimeoffset"); + + b.Property("MarketValue") + .HasColumnName("market_value") + .HasColumnType("nvarchar(max)"); + + b.Property("Owner") + .HasColumnName("owner") + .HasColumnType("nvarchar(max)"); + + b.Property("RegistrationNumber") + .HasColumnName("registration_number") + .HasColumnType("nvarchar(max)"); + + b.Property("is_deleted") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("cars"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Samples/EFCoreAuditing.Samples.Cars.API/Program.cs b/Samples/EFCoreAuditing.Samples.Cars.API/Program.cs new file mode 100644 index 0000000..abeb1a2 --- /dev/null +++ b/Samples/EFCoreAuditing.Samples.Cars.API/Program.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; + +namespace EFCoreAuditing.Samples.Cars.API +{ + public class Program + { + public static async Task Main(string[] args) + { + var host = CreateHostBuilder(args).Build(); + using (var serviceScope = host.Services.CreateScope()) + { + var services = serviceScope.ServiceProvider; + + try + { + var usersDbContext = services.GetRequiredService(); + await usersDbContext.Database.MigrateAsync(); + } + catch (Exception ex) + { + var logger = services.GetRequiredService>(); + logger.LogError(ex, "An error occurred."); + } + } + + await host.RunAsync(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/Samples/EFCoreAuditing.Samples.Cars.API/Properties/launchSettings.json b/Samples/EFCoreAuditing.Samples.Cars.API/Properties/launchSettings.json new file mode 100644 index 0000000..62a0dbf --- /dev/null +++ b/Samples/EFCoreAuditing.Samples.Cars.API/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:21287", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "EFCoreAuditing.Samples.Cars.API": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "", + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Samples/EFCoreAuditing.Samples.Cars.API/Startup.cs b/Samples/EFCoreAuditing.Samples.Cars.API/Startup.cs new file mode 100644 index 0000000..fcee028 --- /dev/null +++ b/Samples/EFCoreAuditing.Samples.Cars.API/Startup.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace EFCoreAuditing.Samples.Cars.API +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddDbContext(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); + + services.AddControllers(); + services.AddSwaggerGen(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + // Enable middleware to serve generated Swagger as a JSON endpoint. + app.UseSwagger(); + + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "Cars API - v1"); + c.RoutePrefix = string.Empty; + }); + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/Samples/EFCoreAuditing.Samples.Cars.API/appsettings.Development.json b/Samples/EFCoreAuditing.Samples.Cars.API/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/Samples/EFCoreAuditing.Samples.Cars.API/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/Samples/EFCoreAuditing.Samples.Cars.API/appsettings.json b/Samples/EFCoreAuditing.Samples.Cars.API/appsettings.json new file mode 100644 index 0000000..f288ac1 --- /dev/null +++ b/Samples/EFCoreAuditing.Samples.Cars.API/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Server=.;Database=cars;Integrated Security=True;", + } +}