Skip to content

Commit cc222ae

Browse files
NinjaRocksclaude
andcommitted
Refactor code to reduce duplication and improve maintainability
This commit addresses jscpd (code duplication) CI/CD failures by refactoring duplicated code across multiple files: **Major Changes:** - **DbContextMigrationHelper.cs**: Extracted common table creation logic into CreateTablesCore method, reducing duplication from 23.57% - **CommandStoreAdapter.cs**: Refactored telemetry wrapping and deserialization logic, reducing duplication from 19.14% - **DomainTelemetryService.cs**: Extracted exception handling into SetActivityException helper method, reducing duplication from 11.07% - **ByteArrayPool.cs**: Refactored serialization methods to use SerializeCore helper, reducing duplication from 10.5% - **DatabaseTelemetryService.cs**: Added SetActivityException helper method, reducing duplication from 7.69% - **EfEntityStore.cs & EfViewModelStore.cs**: Created EfStoreBase abstract base class to share common persistence logic, reducing duplication from 7.62% **Technical Details:** - All changes maintain backward compatibility - No API changes to public interfaces - Test suite passes with all existing tests - Code formatting applied via dotnet format 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 255e4e9 commit cc222ae

File tree

16 files changed

+207
-256
lines changed

16 files changed

+207
-256
lines changed

src/SourceFlow.Net.EntityFramework/Migrations/DbContextMigrationHelper.cs

Lines changed: 9 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -21,61 +21,28 @@ public static class DbContextMigrationHelper
2121
/// </summary>
2222
public static void CreateEntityTables(EntityDbContext context, IEnumerable<Type> entityTypes)
2323
{
24-
var databaseCreator = context.Database.GetService<IRelationalDatabaseCreator>();
25-
26-
// Ensure database exists
27-
context.Database.EnsureCreated();
28-
29-
foreach (var entityType in entityTypes)
30-
{
31-
var tableName = entityType.Name;
32-
var properties = entityType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
33-
.Where(p => p.CanRead && p.CanWrite);
34-
35-
var columns = new List<string>();
36-
foreach (var prop in properties)
37-
{
38-
var columnDef = GetColumnDefinition(prop);
39-
if (columnDef != null)
40-
{
41-
columns.Add(columnDef);
42-
}
43-
}
44-
45-
if (columns.Any())
46-
{
47-
var createTableSql = $@"
48-
CREATE TABLE IF NOT EXISTS ""{tableName}"" (
49-
{string.Join(",\n ", columns)},
50-
PRIMARY KEY (""Id"")
51-
)";
52-
53-
try
54-
{
55-
context.Database.ExecuteSqlRaw(createTableSql);
56-
}
57-
catch
58-
{
59-
// Table might already exist, ignore
60-
}
61-
}
62-
}
24+
CreateTablesCore(context, entityTypes);
6325
}
6426

6527
/// <summary>
6628
/// Manually creates tables for all registered IViewModel types in the ViewModelDbContext.
6729
/// </summary>
6830
public static void CreateViewModelTables(ViewModelDbContext context, IEnumerable<Type> viewModelTypes)
31+
{
32+
CreateTablesCore(context, viewModelTypes);
33+
}
34+
35+
private static void CreateTablesCore(DbContext context, IEnumerable<Type> types)
6936
{
7037
var databaseCreator = context.Database.GetService<IRelationalDatabaseCreator>();
7138

7239
// Ensure database exists
7340
context.Database.EnsureCreated();
7441

75-
foreach (var viewModelType in viewModelTypes)
42+
foreach (var type in types)
7643
{
77-
var tableName = viewModelType.Name;
78-
var properties = viewModelType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
44+
var tableName = type.Name;
45+
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
7946
.Where(p => p.CanRead && p.CanWrite);
8047

8148
var columns = new List<string>();

src/SourceFlow.Net.EntityFramework/Services/DatabaseTelemetryService.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ public class DatabaseTelemetryService : IDatabaseTelemetryService
2828
// Histograms
2929
private readonly Histogram<double>? _operationDuration;
3030

31+
private static void SetActivityException(Activity? activity, Exception ex)
32+
{
33+
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
34+
activity?.SetTag("exception.type", ex.GetType().FullName);
35+
activity?.SetTag("exception.message", ex.Message);
36+
}
37+
3138
public DatabaseTelemetryService(SourceFlowEfOptions options)
3239
{
3340
if (options == null)
@@ -100,9 +107,7 @@ public async Task<T> TraceAsync<T>(
100107
}
101108
catch (Exception ex)
102109
{
103-
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
104-
activity?.SetTag("exception.type", ex.GetType().FullName);
105-
activity?.SetTag("exception.message", ex.Message);
110+
SetActivityException(activity, ex);
106111
throw;
107112
}
108113
finally
@@ -141,9 +146,7 @@ public async Task TraceAsync(
141146
}
142147
catch (Exception ex)
143148
{
144-
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
145-
activity?.SetTag("exception.type", ex.GetType().FullName);
146-
activity?.SetTag("exception.message", ex.Message);
149+
SetActivityException(activity, ex);
147150
throw;
148151
}
149152
finally

src/SourceFlow.Net.EntityFramework/Stores/EfEntityStore.cs

Lines changed: 16 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,24 @@
55

66
namespace SourceFlow.Stores.EntityFramework.Stores
77
{
8-
public class EfEntityStore : IEntityStore
8+
public class EfEntityStore : EfStoreBase<EntityDbContext>, IEntityStore
99
{
10-
private readonly EntityDbContext _context;
11-
private readonly IDatabaseResiliencePolicy _resiliencePolicy;
12-
private readonly IDatabaseTelemetryService _telemetryService;
13-
1410
public EfEntityStore(
1511
EntityDbContext context,
1612
IDatabaseResiliencePolicy resiliencePolicy,
1713
IDatabaseTelemetryService telemetryService)
14+
: base(context, resiliencePolicy, telemetryService)
1815
{
19-
_context = context ?? throw new ArgumentNullException(nameof(context));
20-
_resiliencePolicy = resiliencePolicy ?? throw new ArgumentNullException(nameof(resiliencePolicy));
21-
_telemetryService = telemetryService ?? throw new ArgumentNullException(nameof(telemetryService));
2216
}
2317

2418
public async Task<TEntity> Get<TEntity>(int id) where TEntity : class, IEntity
2519
{
2620
if (id <= 0)
2721
throw new ArgumentException("Entity Id must be greater than 0.", nameof(id));
2822

29-
return await _resiliencePolicy.ExecuteAsync(async () =>
23+
return await ResiliencePolicy.ExecuteAsync(async () =>
3024
{
31-
var entity = await _context.Set<TEntity>()
25+
var entity = await Context.Set<TEntity>()
3226
.AsNoTracking()
3327
.FirstOrDefaultAsync(e => e.Id == id);
3428

@@ -41,43 +35,17 @@ public async Task<TEntity> Get<TEntity>(int id) where TEntity : class, IEntity
4135

4236
public async Task<TEntity> Persist<TEntity>(TEntity entity) where TEntity : class, IEntity
4337
{
44-
if (entity == null)
45-
throw new ArgumentNullException(nameof(entity));
46-
47-
if (entity.Id <= 0)
48-
throw new ArgumentException("Entity Id must be greater than 0.", nameof(entity));
49-
50-
await _telemetryService.TraceAsync(
38+
return await PersistCore(
39+
entity,
40+
entity.Id,
5141
"sourceflow.ef.entity.persist",
52-
async () =>
42+
"Entity",
43+
(activity, e) =>
5344
{
54-
await _resiliencePolicy.ExecuteAsync(async () =>
55-
{
56-
// Check if entity exists using AsNoTracking to avoid tracking conflicts
57-
var exists = await _context.Set<TEntity>()
58-
.AsNoTracking()
59-
.AnyAsync(e => e.Id == entity.Id);
60-
61-
if (exists)
62-
_context.Set<TEntity>().Update(entity);
63-
else
64-
_context.Set<TEntity>().Add(entity);
65-
66-
await _context.SaveChangesAsync();
67-
68-
// Detach the entity to avoid tracking conflicts in subsequent operations
69-
_context.Entry(entity).State = EntityState.Detached;
70-
});
71-
72-
_telemetryService.RecordEntityPersisted();
73-
},
74-
activity =>
75-
{
76-
activity?.SetTag("sourceflow.entity_id", entity.Id);
45+
activity?.SetTag("sourceflow.entity_id", e.Id);
7746
activity?.SetTag("sourceflow.entity_type", typeof(TEntity).Name);
78-
});
79-
80-
return entity;
47+
},
48+
() => TelemetryService.RecordEntityPersisted());
8149
}
8250

8351
public async Task Delete<TEntity>(TEntity entity) where TEntity : class, IEntity
@@ -88,18 +56,18 @@ public async Task Delete<TEntity>(TEntity entity) where TEntity : class, IEntity
8856
if (entity.Id <= 0)
8957
throw new ArgumentException("Entity Id must be greater than 0.", nameof(entity));
9058

91-
await _resiliencePolicy.ExecuteAsync(async () =>
59+
await ResiliencePolicy.ExecuteAsync(async () =>
9260
{
93-
var entityRecord = await _context.Set<TEntity>()
61+
var entityRecord = await Context.Set<TEntity>()
9462
.FirstOrDefaultAsync(e => e.Id == entity.Id);
9563

9664
if (entityRecord == null)
9765
throw new InvalidOperationException(
9866
$"Entity of type {typeof(TEntity).Name} with Id {entity.Id} not found.");
9967

100-
_context.Set<TEntity>().Remove(entityRecord);
68+
Context.Set<TEntity>().Remove(entityRecord);
10169

102-
await _context.SaveChangesAsync();
70+
await Context.SaveChangesAsync();
10371
});
10472
}
10573
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Threading.Tasks;
4+
using Microsoft.EntityFrameworkCore;
5+
using SourceFlow.Stores.EntityFramework.Services;
6+
7+
namespace SourceFlow.Stores.EntityFramework.Stores
8+
{
9+
public abstract class EfStoreBase<TContext> where TContext : DbContext
10+
{
11+
protected readonly TContext Context;
12+
protected readonly IDatabaseResiliencePolicy ResiliencePolicy;
13+
protected readonly IDatabaseTelemetryService TelemetryService;
14+
15+
protected EfStoreBase(
16+
TContext context,
17+
IDatabaseResiliencePolicy resiliencePolicy,
18+
IDatabaseTelemetryService telemetryService)
19+
{
20+
Context = context ?? throw new ArgumentNullException(nameof(context));
21+
ResiliencePolicy = resiliencePolicy ?? throw new ArgumentNullException(nameof(resiliencePolicy));
22+
TelemetryService = telemetryService ?? throw new ArgumentNullException(nameof(telemetryService));
23+
}
24+
25+
protected async Task<T> PersistCore<T>(
26+
T item,
27+
int id,
28+
string operationName,
29+
string itemType,
30+
Action<Activity, T> setActivityTags,
31+
Action recordMetric) where T : class
32+
{
33+
if (item == null)
34+
throw new ArgumentNullException(nameof(item));
35+
36+
if (id <= 0)
37+
throw new ArgumentException($"{itemType} Id must be greater than 0.", nameof(item));
38+
39+
await TelemetryService.TraceAsync(
40+
operationName,
41+
async () =>
42+
{
43+
await ResiliencePolicy.ExecuteAsync(async () =>
44+
{
45+
// Check if item exists using AsNoTracking to avoid tracking conflicts
46+
var exists = await Context.Set<T>()
47+
.AsNoTracking()
48+
.AnyAsync(e => EF.Property<int>(e, "Id") == id);
49+
50+
if (exists)
51+
Context.Set<T>().Update(item);
52+
else
53+
Context.Set<T>().Add(item);
54+
55+
await Context.SaveChangesAsync();
56+
57+
// Detach the item to avoid tracking conflicts in subsequent operations
58+
Context.Entry(item).State = EntityState.Detached;
59+
});
60+
61+
recordMetric();
62+
},
63+
activity => setActivityTags(activity, item));
64+
65+
return item;
66+
}
67+
}
68+
}

src/SourceFlow.Net.EntityFramework/Stores/EfViewModelStore.cs

Lines changed: 16 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,24 @@
66

77
namespace SourceFlow.Stores.EntityFramework.Stores
88
{
9-
public class EfViewModelStore : IViewModelStore
9+
public class EfViewModelStore : EfStoreBase<ViewModelDbContext>, IViewModelStore
1010
{
11-
private readonly ViewModelDbContext _context;
12-
private readonly IDatabaseResiliencePolicy _resiliencePolicy;
13-
private readonly IDatabaseTelemetryService _telemetryService;
14-
1511
public EfViewModelStore(
1612
ViewModelDbContext context,
1713
IDatabaseResiliencePolicy resiliencePolicy,
1814
IDatabaseTelemetryService telemetryService)
15+
: base(context, resiliencePolicy, telemetryService)
1916
{
20-
_context = context ?? throw new ArgumentNullException(nameof(context));
21-
_resiliencePolicy = resiliencePolicy ?? throw new ArgumentNullException(nameof(resiliencePolicy));
22-
_telemetryService = telemetryService ?? throw new ArgumentNullException(nameof(telemetryService));
2317
}
2418

2519
public async Task<TViewModel> Get<TViewModel>(int id) where TViewModel : class, IViewModel
2620
{
2721
if (id <= 0)
2822
throw new ArgumentException("ViewModel Id must be greater than 0.", nameof(id));
2923

30-
return await _resiliencePolicy.ExecuteAsync(async () =>
24+
return await ResiliencePolicy.ExecuteAsync(async () =>
3125
{
32-
var viewModel = await _context.Set<TViewModel>()
26+
var viewModel = await Context.Set<TViewModel>()
3327
.AsNoTracking()
3428
.FirstOrDefaultAsync(v => v.Id == id);
3529

@@ -42,43 +36,17 @@ public async Task<TViewModel> Get<TViewModel>(int id) where TViewModel : class,
4236

4337
public async Task<TViewModel> Persist<TViewModel>(TViewModel model) where TViewModel : class, IViewModel
4438
{
45-
if (model == null)
46-
throw new ArgumentNullException(nameof(model));
47-
48-
if (model.Id <= 0)
49-
throw new ArgumentException("ViewModel Id must be greater than 0.", nameof(model));
50-
51-
await _telemetryService.TraceAsync(
39+
return await PersistCore(
40+
model,
41+
model.Id,
5242
"sourceflow.ef.viewmodel.persist",
53-
async () =>
43+
"ViewModel",
44+
(activity, m) =>
5445
{
55-
await _resiliencePolicy.ExecuteAsync(async () =>
56-
{
57-
// Check if view model exists using AsNoTracking to avoid tracking conflicts
58-
var exists = await _context.Set<TViewModel>()
59-
.AsNoTracking()
60-
.AnyAsync(v => v.Id == model.Id);
61-
62-
if (exists)
63-
_context.Set<TViewModel>().Update(model);
64-
else
65-
_context.Set<TViewModel>().Add(model);
66-
67-
await _context.SaveChangesAsync();
68-
69-
// Detach the view model to avoid tracking conflicts in subsequent operations
70-
_context.Entry(model).State = EntityState.Detached;
71-
});
72-
73-
_telemetryService.RecordViewModelPersisted();
74-
},
75-
activity =>
76-
{
77-
activity?.SetTag("sourceflow.viewmodel_id", model.Id);
46+
activity?.SetTag("sourceflow.viewmodel_id", m.Id);
7847
activity?.SetTag("sourceflow.viewmodel_type", typeof(TViewModel).Name);
79-
});
80-
81-
return model;
48+
},
49+
() => TelemetryService.RecordViewModelPersisted());
8250
}
8351

8452
public async Task Delete<TViewModel>(TViewModel model) where TViewModel : class, IViewModel
@@ -89,17 +57,17 @@ public async Task Delete<TViewModel>(TViewModel model) where TViewModel : class,
8957
if (model.Id <= 0)
9058
throw new ArgumentException("ViewModel Id must be greater than 0.", nameof(model));
9159

92-
await _resiliencePolicy.ExecuteAsync(async () =>
60+
await ResiliencePolicy.ExecuteAsync(async () =>
9361
{
94-
var viewModelRecord = await _context.Set<TViewModel>()
62+
var viewModelRecord = await Context.Set<TViewModel>()
9563
.FirstOrDefaultAsync(v => v.Id == model.Id);
9664

9765
if (viewModelRecord == null)
9866
throw new InvalidOperationException(
9967
$"ViewModel of type {typeof(TViewModel).Name} with Id {model.Id} not found.");
10068

101-
_context.Set<TViewModel>().Remove(viewModelRecord);
102-
await _context.SaveChangesAsync();
69+
Context.Set<TViewModel>().Remove(viewModelRecord);
70+
await Context.SaveChangesAsync();
10371
});
10472
}
10573
}

0 commit comments

Comments
 (0)