Skip to content

Commit 5b15989

Browse files
committed
VPR-59 refactor(cms): share stale-edit and permission-list helpers
Three byte-identical copies of AssertNotStale and CleanList lived in the file, content-block, and left-nav services; extract them to CmsServiceHelpers so the concurrency contract cannot drift.
1 parent 2e6fcd7 commit 5b15989

2 files changed

Lines changed: 44 additions & 22 deletions

File tree

web/Areas/CMS/Services/CmsFileService.cs

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -434,20 +434,8 @@ public async Task<CmsFileDto> CreateFileAsync(CmsFileCreateRequest request, IFor
434434
return CmsFileMapper.ToCmsFileDto(entity, names);
435435
}
436436

437-
private static void AssertNotStale(File entity, DateTime? lastModifiedOn)
438-
{
439-
if (lastModifiedOn == null)
440-
{
441-
throw new ArgumentException("LastModifiedOn is required so concurrent edits can be detected.");
442-
}
443-
// Compare to the second: serialized timestamps lose sub-second precision round-tripping
444-
// through the client.
445-
if (Math.Abs((entity.ModifiedOn - lastModifiedOn.Value).TotalSeconds) >= 1)
446-
{
447-
throw new CmsConcurrencyException(
448-
$"This file was modified by {entity.ModifiedBy} on {entity.ModifiedOn:g}. Reload to get the latest version.");
449-
}
450-
}
437+
private static void AssertNotStale(File entity, DateTime? lastModifiedOn) =>
438+
CmsServiceHelpers.AssertNotStale("file", entity.ModifiedOn, entity.ModifiedBy, lastModifiedOn);
451439

452440
/// <summary>
453441
/// A replacement upload keeps the record's name and path, so its bytes must be an allowed
@@ -899,14 +887,7 @@ private static string BuildCreateDetail(File entity)
899887
return string.Join("; ", parts);
900888
}
901889

902-
private static List<string> CleanList(List<string> values)
903-
{
904-
return values
905-
.Where(v => !string.IsNullOrWhiteSpace(v))
906-
.Select(v => v.Trim())
907-
.Distinct(StringComparer.OrdinalIgnoreCase)
908-
.ToList();
909-
}
890+
private static List<string> CleanList(List<string> values) => CmsServiceHelpers.CleanList(values);
910891

911892
private static string? NullIfEmpty(string? value)
912893
{
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace Viper.Areas.CMS.Services
2+
{
3+
/// <summary>
4+
/// Helpers shared by the CMS write services (files, content blocks, left navs) so the
5+
/// optimistic-concurrency contract and permission-list normalization cannot drift between
6+
/// copies. Each service wraps these with its own entity-typed one-liner.
7+
/// </summary>
8+
internal static class CmsServiceHelpers
9+
{
10+
/// <summary>
11+
/// A missing stamp is a 400 (the client must send it) and a stale one is a 409 (someone
12+
/// saved after the editor loaded). Compared to the second: serialized timestamps lose
13+
/// sub-second precision round-tripping through the client.
14+
/// </summary>
15+
public static void AssertNotStale(string noun, DateTime modifiedOn, string? modifiedBy, DateTime? lastModifiedOn)
16+
{
17+
if (lastModifiedOn == null)
18+
{
19+
throw new ArgumentException("LastModifiedOn is required so concurrent edits can be detected.");
20+
}
21+
if (Math.Abs((modifiedOn - lastModifiedOn.Value).TotalSeconds) >= 1)
22+
{
23+
throw new CmsConcurrencyException(
24+
$"This {noun} was modified by {modifiedBy} on {modifiedOn:g}. Reload to get the latest version.");
25+
}
26+
}
27+
28+
/// <summary>
29+
/// Trim, drop blanks, and de-duplicate case-insensitively. A client can post
30+
/// "permissions": null explicitly; treat it as empty rather than 500.
31+
/// </summary>
32+
public static List<string> CleanList(IEnumerable<string>? values)
33+
{
34+
return (values ?? [])
35+
.Where(v => !string.IsNullOrWhiteSpace(v))
36+
.Select(v => v.Trim())
37+
.Distinct(StringComparer.OrdinalIgnoreCase)
38+
.ToList();
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)