Skip to content

Commit 24e6efc

Browse files
authored
✨ validate string lengths in domain (#493)
1 parent d65feba commit 24e6efc

17 files changed

Lines changed: 230 additions & 97 deletions

src/Domain/Common/Base/Auditable.cs

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,48 @@
55
/// </summary>
66
public abstract class Auditable : IAuditable
77
{
8+
public const int CreatedByMaxLength = 128;
9+
public const int UpdatedByMaxLength = 128;
10+
11+
private const string SystemUser = "System";
12+
private string _createdBy = null!;
13+
private string? _updatedBy;
14+
815
public DateTimeOffset CreatedAt { get; private set; }
9-
public string? CreatedBy { get; private set; }
16+
17+
public string CreatedBy
18+
{
19+
get => _createdBy;
20+
private set
21+
{
22+
ThrowIfNullOrWhiteSpace(value, nameof(CreatedBy));
23+
ThrowIfGreaterThan(value.Length, CreatedByMaxLength, nameof(CreatedBy));
24+
_createdBy = value;
25+
}
26+
}
27+
1028
public DateTimeOffset? UpdatedAt { get; private set; }
11-
public string? UpdatedBy { get; private set; }
1229

13-
public void SetCreated(DateTimeOffset createdAt, string? createdBy)
30+
public string? UpdatedBy
31+
{
32+
get => _updatedBy;
33+
private set
34+
{
35+
ThrowIfNullOrWhiteSpace(value, nameof(UpdatedBy));
36+
ThrowIfGreaterThan(value.Length, UpdatedByMaxLength, nameof(UpdatedBy));
37+
_updatedBy = value;
38+
}
39+
}
40+
41+
public void SetCreated(TimeProvider timeProvider, string? createdBy)
1442
{
15-
CreatedAt = createdAt;
16-
CreatedBy = createdBy;
43+
CreatedAt = timeProvider.GetUtcNow();
44+
CreatedBy = createdBy ?? SystemUser;
1745
}
1846

19-
public void SetUpdated(DateTimeOffset updatedAt, string? updatedBy)
47+
public void SetUpdated(TimeProvider timeProvider, string? updatedBy)
2048
{
21-
UpdatedAt = updatedAt;
22-
UpdatedBy = updatedBy;
49+
UpdatedAt = timeProvider.GetUtcNow();
50+
UpdatedBy = updatedBy ?? SystemUser;
2351
}
2452
}

src/Domain/Common/Interfaces/IAuditable.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
public interface IAuditable
44
{
55
DateTimeOffset CreatedAt { get; }
6-
string? CreatedBy { get; } // TODO: String as userId? (https://github.com/SSWConsulting/SSW.CleanArchitecture/issues/76)
6+
string CreatedBy { get; }
77
DateTimeOffset? UpdatedAt { get; }
8-
string? UpdatedBy { get; } // TODO: String as userId? (https://github.com/SSWConsulting/SSW.CleanArchitecture/issues/76)
8+
string? UpdatedBy { get; }
99

10-
void SetCreated(DateTimeOffset createdAt, string? createdBy);
10+
void SetCreated(TimeProvider timeProvider, string? createdBy = null);
1111

12-
void SetUpdated(DateTimeOffset updatedAt, string? updatedBy);
12+
void SetUpdated(TimeProvider timeProvider, string? updatedBy = null);
1313
}

src/Domain/Heroes/Hero.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ namespace SSW.CleanArchitecture.Domain.Heroes;
88

99
public class Hero : AggregateRoot<HeroId>
1010
{
11+
public const int NameMaxLength = 100;
12+
public const int AliasMaxLength = 100;
13+
1114
private readonly List<Power> _powers = [];
1215

1316
private string _name = null!;
@@ -19,6 +22,7 @@ public string Name
1922
set
2023
{
2124
ThrowIfNullOrWhiteSpace(value, nameof(Name));
25+
ThrowIfGreaterThan(value.Length, NameMaxLength, nameof(Name));
2226
_name = value;
2327
}
2428
}
@@ -29,6 +33,7 @@ public string Alias
2933
set
3034
{
3135
ThrowIfNullOrWhiteSpace(value, nameof(Alias));
36+
ThrowIfGreaterThan(value.Length, AliasMaxLength, nameof(Alias));
3237
_alias = value;
3338
}
3439
}
@@ -38,7 +43,7 @@ public string Alias
3843

3944
public IReadOnlyList<Power> Powers => _powers.AsReadOnly();
4045

41-
private Hero() { }
46+
private Hero() { } // Needed for EF Core
4247

4348
public static Hero Create(string name, string alias)
4449
{

src/Domain/Heroes/Power.cs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,37 @@
22

33
public record Power : IValueObject
44
{
5+
public const int NameMaxLength = 50;
6+
7+
private string _name = null!;
8+
private int _powerLevel;
9+
510
// Private setters needed for EF
6-
public string Name { get; private set; }
11+
public string Name
12+
{
13+
get => _name;
14+
private set
15+
{
16+
ThrowIfNullOrWhiteSpace(value, nameof(Name));
17+
ThrowIfGreaterThan(value.Length, NameMaxLength, nameof(Name));
18+
_name = value;
19+
}
20+
}
721

822
// Private setters needed for EF
9-
public int PowerLevel { get; private set; }
23+
public int PowerLevel
24+
{
25+
get => _powerLevel;
26+
private set
27+
{
28+
ThrowIfLessThan(value, 1, nameof(PowerLevel));
29+
ThrowIfGreaterThan(value, 10, nameof(PowerLevel));
30+
_powerLevel = value;
31+
}
32+
}
1033

1134
public Power(string name, int powerLevel)
1235
{
13-
ThrowIfLessThan(powerLevel, 1);
14-
ThrowIfGreaterThan(powerLevel, 10);
1536
Name = name;
1637
PowerLevel = powerLevel;
1738
}

src/Domain/Teams/Mission.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,19 @@
66

77
public class Mission : Entity<MissionId>
88
{
9-
public string Description { get; private set; } = null!;
9+
private string _description = null!;
10+
public const int DescriptionMaxLength = 500;
11+
12+
public string Description
13+
{
14+
get => _description;
15+
private set
16+
{
17+
ThrowIfNullOrWhiteSpace(value, nameof(Description));
18+
ThrowIfGreaterThan(value.Length, DescriptionMaxLength, nameof(Description));
19+
_description = value;
20+
}
21+
}
1022

1123
public MissionStatus Status { get; private set; }
1224

@@ -15,7 +27,6 @@ public class Mission : Entity<MissionId>
1527
// NOTE: Internal so that missions can only be created by the aggregate
1628
internal static Mission Create(string description)
1729
{
18-
ThrowIfNullOrWhiteSpace(description);
1930
return new Mission
2031
{
2132
Id = MissionId.From(Guid.CreateVersion7()),

src/Domain/Teams/Team.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,30 @@ namespace SSW.CleanArchitecture.Domain.Teams;
88

99
public class Team : AggregateRoot<TeamId>
1010
{
11+
public const int NameMaxLength = 100;
12+
13+
private string _name = null!;
14+
private readonly List<Hero> _heroes = [];
15+
private readonly List<Mission> _missions = [];
16+
private Mission? CurrentMission => _missions.FirstOrDefault(m => m.Status == MissionStatus.InProgress);
17+
1118
public string Name
1219
{
1320
get => _name;
1421
private set
1522
{
1623
ThrowIfNullOrWhiteSpace(value, nameof(Name));
24+
ThrowIfGreaterThan(value.Length, NameMaxLength, nameof(Name));
1725
_name = value;
1826
}
1927
}
2028

2129
public int TotalPowerLevel { get; private set; }
2230
public TeamStatus Status { get; private set; }
23-
24-
private readonly List<Mission> _missions = [];
2531
public IReadOnlyList<Mission> Missions => _missions.AsReadOnly();
26-
private Mission? CurrentMission => _missions.FirstOrDefault(m => m.Status == MissionStatus.InProgress);
27-
28-
private readonly List<Hero> _heroes = [];
29-
private string _name = null!;
3032
public IReadOnlyList<Hero> Heroes => _heroes.AsReadOnly();
3133

32-
private Team() { }
34+
private Team() { } // Needed for EF Core
3335

3436
public static Team Create(string name)
3537
{

src/Infrastructure/Persistence/ApplicationDbContext.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura
2626
{
2727
base.ConfigureConventions(configurationBuilder);
2828

29-
configurationBuilder.Properties<string>().HaveMaxLength(256);
3029
configurationBuilder.RegisterAllInVogenEfCoreConverters();
3130
}
3231

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using Microsoft.EntityFrameworkCore.Metadata.Builders;
3+
using SSW.CleanArchitecture.Domain.Common.Base;
4+
5+
namespace SSW.CleanArchitecture.Infrastructure.Persistence.Configuration;
6+
7+
public abstract class AuditableConfiguration<T> : IEntityTypeConfiguration<T>
8+
where T : Auditable
9+
{
10+
public virtual void Configure(EntityTypeBuilder<T> builder)
11+
{
12+
builder.Property(e => e.CreatedBy)
13+
.HasMaxLength(Auditable.CreatedByMaxLength)
14+
.IsRequired();
15+
16+
builder.Property(e => e.CreatedAt)
17+
.IsRequired();
18+
19+
builder.Property(e => e.UpdatedBy)
20+
.HasMaxLength(Auditable.UpdatedByMaxLength);
21+
22+
PostConfigure(builder);
23+
}
24+
25+
public abstract void PostConfigure(EntityTypeBuilder<T> builder);
26+
}

src/Infrastructure/Persistence/Configuration/HeroConfiguration.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,27 @@
44

55
namespace SSW.CleanArchitecture.Infrastructure.Persistence.Configuration;
66

7-
public class HeroConfiguration : IEntityTypeConfiguration<Hero>
7+
public class HeroConfiguration : AuditableConfiguration<Hero>
88
{
9-
public void Configure(EntityTypeBuilder<Hero> builder)
9+
public override void PostConfigure(EntityTypeBuilder<Hero> builder)
1010
{
1111
builder.HasKey(t => t.Id);
1212

1313
builder.Property(t => t.Name)
14+
.HasMaxLength(Hero.NameMaxLength)
1415
.IsRequired();
1516

1617
builder.Property(t => t.Alias)
18+
.HasMaxLength(Hero.AliasMaxLength)
1719
.IsRequired();
1820

1921
// This is to highlight that we can also serialise to JSON for simple content instead of adding a new table
20-
builder.OwnsMany(t => t.Powers, b => b.ToJson());
22+
builder.OwnsMany(t => t.Powers, b =>
23+
{
24+
b.ToJson();
25+
b.Property(t => t.Name)
26+
.HasMaxLength(Power.NameMaxLength)
27+
.IsRequired();
28+
});
2129
}
2230
}
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
using Microsoft.EntityFrameworkCore;
22
using Microsoft.EntityFrameworkCore.Metadata.Builders;
3+
using SSW.CleanArchitecture.Domain.Common.Interfaces;
34
using SSW.CleanArchitecture.Domain.Teams;
45

56
namespace SSW.CleanArchitecture.Infrastructure.Persistence.Configuration;
67

7-
public class MissionConfiguration : IEntityTypeConfiguration<Mission>
8+
public class MissionConfiguration : AuditableConfiguration<Mission>
89
{
9-
public void Configure(EntityTypeBuilder<Mission> builder)
10+
public override void PostConfigure(EntityTypeBuilder<Mission> builder)
1011
{
1112
builder.HasKey(t => t.Id);
1213

1314
builder.Property(t => t.Description)
15+
.HasMaxLength(Mission.DescriptionMaxLength)
1416
.IsRequired();
1517
}
1618
}

0 commit comments

Comments
 (0)