diff --git a/src/RecurringThings.MongoDB/Documents/RecurringThingDocument.cs b/src/RecurringThings.MongoDB/Documents/RecurringThingDocument.cs
index f0e88dd..ce9bb4f 100644
--- a/src/RecurringThings.MongoDB/Documents/RecurringThingDocument.cs
+++ b/src/RecurringThings.MongoDB/Documents/RecurringThingDocument.cs
@@ -203,7 +203,7 @@ public static class DocumentTypes
///
/// Provides mapping methods between domain entities and MongoDB documents.
///
-public static class DocumentMapper
+internal static class DocumentMapper
{
///
/// Converts a to a .
diff --git a/src/RecurringThings.MongoDB/RecurringThings.MongoDB.csproj b/src/RecurringThings.MongoDB/RecurringThings.MongoDB.csproj
index 7e4b676..05498a6 100644
--- a/src/RecurringThings.MongoDB/RecurringThings.MongoDB.csproj
+++ b/src/RecurringThings.MongoDB/RecurringThings.MongoDB.csproj
@@ -8,9 +8,13 @@
true
+
+
+
+
RecurringThings.MongoDB
- RecurringThings Contributors
+ Miguel Tremblay
MongoDB persistence provider for RecurringThings library.
https://github.com/ChuckNovice/RecurringThings
Apache-2.0
diff --git a/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceExceptionRepository.cs b/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceExceptionRepository.cs
index ab42d50..a4bc3c1 100644
--- a/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceExceptionRepository.cs
+++ b/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceExceptionRepository.cs
@@ -16,7 +16,7 @@ namespace RecurringThings.MongoDB.Repositories;
///
/// MongoDB implementation of .
///
-public sealed class MongoOccurrenceExceptionRepository : IOccurrenceExceptionRepository
+internal sealed class MongoOccurrenceExceptionRepository : IOccurrenceExceptionRepository
{
private readonly IMongoCollection _collection;
diff --git a/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceOverrideRepository.cs b/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceOverrideRepository.cs
index 85e7ef9..6b479be 100644
--- a/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceOverrideRepository.cs
+++ b/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceOverrideRepository.cs
@@ -16,7 +16,7 @@ namespace RecurringThings.MongoDB.Repositories;
///
/// MongoDB implementation of .
///
-public sealed class MongoOccurrenceOverrideRepository : IOccurrenceOverrideRepository
+internal sealed class MongoOccurrenceOverrideRepository : IOccurrenceOverrideRepository
{
private readonly IMongoCollection _collection;
diff --git a/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceRepository.cs b/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceRepository.cs
index 034eb7b..061937d 100644
--- a/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceRepository.cs
+++ b/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceRepository.cs
@@ -15,7 +15,7 @@ namespace RecurringThings.MongoDB.Repositories;
///
/// MongoDB implementation of .
///
-public sealed class MongoOccurrenceRepository : IOccurrenceRepository
+internal sealed class MongoOccurrenceRepository : IOccurrenceRepository
{
private readonly IMongoCollection _collection;
diff --git a/src/RecurringThings.MongoDB/Repositories/MongoRecurrenceRepository.cs b/src/RecurringThings.MongoDB/Repositories/MongoRecurrenceRepository.cs
index 25fb3d7..578daea 100644
--- a/src/RecurringThings.MongoDB/Repositories/MongoRecurrenceRepository.cs
+++ b/src/RecurringThings.MongoDB/Repositories/MongoRecurrenceRepository.cs
@@ -15,7 +15,7 @@ namespace RecurringThings.MongoDB.Repositories;
///
/// MongoDB implementation of .
///
-public sealed class MongoRecurrenceRepository : IRecurrenceRepository
+internal sealed class MongoRecurrenceRepository : IRecurrenceRepository
{
private readonly IMongoCollection _collection;
diff --git a/src/RecurringThings/Domain/Occurrence.cs b/src/RecurringThings/Domain/Occurrence.cs
index 3d735f9..dde21d4 100644
--- a/src/RecurringThings/Domain/Occurrence.cs
+++ b/src/RecurringThings/Domain/Occurrence.cs
@@ -12,11 +12,12 @@ namespace RecurringThings.Domain;
/// are stored directly in the database.
///
///
-/// After creation, , , and can be modified.
+/// After creation, , , ,
+/// , and can be modified.
/// When StartTime or Duration changes, is automatically recomputed.
///
///
-public sealed class Occurrence
+internal sealed class Occurrence
{
private DateTime _startTime;
private TimeSpan _duration;
@@ -35,22 +36,22 @@ public sealed class Occurrence
public required string Organization { get; init; }
///
- /// Gets the hierarchical resource scope.
+ /// Gets or sets the hierarchical resource scope.
///
///
/// Used for organizing resources hierarchically (e.g., "user123/calendar", "store456").
/// Must be between 0 and 100 characters. Empty string is allowed.
///
- public required string ResourcePath { get; init; }
+ public required string ResourcePath { get; set; }
///
- /// Gets the user-defined type of this occurrence.
+ /// Gets or sets the user-defined type of this occurrence.
///
///
/// Used to differentiate between different kinds of occurrences (e.g., "appointment", "meeting").
/// Must be between 1 and 100 characters. Empty string is NOT allowed.
///
- public required string Type { get; init; }
+ public required string Type { get; set; }
///
/// Gets or sets the UTC timestamp when this occurrence starts.
diff --git a/src/RecurringThings/Domain/OccurrenceException.cs b/src/RecurringThings/Domain/OccurrenceException.cs
index ed7e67c..a28ab14 100644
--- a/src/RecurringThings/Domain/OccurrenceException.cs
+++ b/src/RecurringThings/Domain/OccurrenceException.cs
@@ -16,7 +16,7 @@ namespace RecurringThings.Domain;
/// Once an exception is created, the occurrence cannot be restored through the API.
///
///
-public sealed class OccurrenceException
+internal sealed class OccurrenceException
{
///
/// Gets the unique identifier for this exception.
diff --git a/src/RecurringThings/Domain/OccurrenceOverride.cs b/src/RecurringThings/Domain/OccurrenceOverride.cs
index 3f6a5de..03d704b 100644
--- a/src/RecurringThings/Domain/OccurrenceOverride.cs
+++ b/src/RecurringThings/Domain/OccurrenceOverride.cs
@@ -20,7 +20,7 @@ namespace RecurringThings.Domain;
/// When StartTime or Duration changes, is automatically recomputed.
///
///
-public sealed class OccurrenceOverride
+internal sealed class OccurrenceOverride
{
private DateTime _startTime;
private TimeSpan _duration;
diff --git a/src/RecurringThings/Domain/Recurrence.cs b/src/RecurringThings/Domain/Recurrence.cs
index 01fd63f..e59aea0 100644
--- a/src/RecurringThings/Domain/Recurrence.cs
+++ b/src/RecurringThings/Domain/Recurrence.cs
@@ -16,7 +16,7 @@ namespace RecurringThings.Domain;
/// To change other fields, delete and recreate the recurrence.
///
///
-public sealed class Recurrence
+internal sealed class Recurrence
{
///
/// Gets the unique identifier for this recurrence.
diff --git a/src/RecurringThings/Engine/IRecurrenceEngine.cs b/src/RecurringThings/Engine/IRecurrenceEngine.cs
index b3c3e08..be15cc4 100644
--- a/src/RecurringThings/Engine/IRecurrenceEngine.cs
+++ b/src/RecurringThings/Engine/IRecurrenceEngine.cs
@@ -4,7 +4,6 @@ namespace RecurringThings.Engine;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
-using RecurringThings.Domain;
using RecurringThings.Models;
using Transactional.Abstractions;
@@ -118,7 +117,7 @@ IAsyncEnumerable GetRecurrencesAsync(
/// Optional user-defined key-value metadata.
/// Optional transaction context.
/// A token to cancel the operation.
- /// The created .
+ /// A representing the created recurrence with .
///
/// Thrown when validation fails (invalid RRule, missing UNTIL, COUNT used, field length violations, etc.).
///
@@ -131,7 +130,7 @@ IAsyncEnumerable GetRecurrencesAsync(
/// };
/// pattern.ByDay.Add(new WeekDay(DayOfWeek.Monday));
///
- /// var recurrence = await engine.CreateRecurrenceAsync(
+ /// var entry = await engine.CreateRecurrenceAsync(
/// organization: "tenant1",
/// resourcePath: "user123/calendar",
/// type: "appointment",
@@ -141,7 +140,7 @@ IAsyncEnumerable GetRecurrencesAsync(
/// timeZone: "America/New_York");
///
///
- Task CreateRecurrenceAsync(
+ Task CreateRecurrenceAsync(
string organization,
string resourcePath,
string type,
@@ -165,7 +164,7 @@ Task CreateRecurrenceAsync(
/// Optional user-defined key-value metadata.
/// Optional transaction context.
/// A token to cancel the operation.
- /// The created .
+ /// A representing the created occurrence with .
///
/// EndTime is automatically computed as StartTime + Duration.
///
@@ -174,7 +173,7 @@ Task CreateRecurrenceAsync(
///
///
///
- /// var occurrence = await engine.CreateOccurrenceAsync(
+ /// var entry = await engine.CreateOccurrenceAsync(
/// organization: "tenant1",
/// resourcePath: "user123/calendar",
/// type: "meeting",
@@ -183,7 +182,7 @@ Task CreateRecurrenceAsync(
/// timeZone: "America/New_York");
///
///
- Task CreateOccurrenceAsync(
+ Task CreateOccurrenceAsync(
string organization,
string resourcePath,
string type,
diff --git a/src/RecurringThings/Engine/RecurrenceEngine.cs b/src/RecurringThings/Engine/RecurrenceEngine.cs
index cffd635..cde07b6 100644
--- a/src/RecurringThings/Engine/RecurrenceEngine.cs
+++ b/src/RecurringThings/Engine/RecurrenceEngine.cs
@@ -31,7 +31,7 @@ namespace RecurringThings.Engine;
/// - Streams results as objects
///
///
-public sealed class RecurrenceEngine : IRecurrenceEngine
+internal sealed class RecurrenceEngine : IRecurrenceEngine
{
private readonly IRecurrenceRepository _recurrenceRepository;
private readonly IOccurrenceRepository _occurrenceRepository;
@@ -211,7 +211,7 @@ public async IAsyncEnumerable GetOccurrencesAsync(
}
///
- public async Task CreateRecurrenceAsync(
+ public async Task CreateRecurrenceAsync(
string organization,
string resourcePath,
string type,
@@ -248,14 +248,17 @@ public async Task CreateRecurrenceAsync(
};
// Persist via repository
- return await _recurrenceRepository.CreateAsync(
+ var created = await _recurrenceRepository.CreateAsync(
recurrence,
transactionContext,
cancellationToken).ConfigureAwait(false);
+
+ // Convert to CalendarEntry
+ return CreateRecurrenceEntry(created);
}
///
- public async Task CreateOccurrenceAsync(
+ public async Task CreateOccurrenceAsync(
string organization,
string resourcePath,
string type,
@@ -287,10 +290,13 @@ public async Task CreateRecurrenceAsync(
occurrence.Initialize(startTimeUtc, duration);
// Persist via repository
- return await _occurrenceRepository.CreateAsync(
+ var created = await _occurrenceRepository.CreateAsync(
occurrence,
transactionContext,
cancellationToken).ConfigureAwait(false);
+
+ // Convert to CalendarEntry
+ return CreateStandaloneEntry(created);
}
///
@@ -539,17 +545,20 @@ public async Task UpdateOccurrenceAsync(
}
///
- /// Updates a standalone occurrence. StartTime, Duration, and Extensions are mutable.
+ /// Updates a standalone occurrence. StartTime, Duration, Extensions, Type, and ResourcePath are mutable.
///
private async Task UpdateStandaloneOccurrenceAsync(
CalendarEntry entry,
ITransactionContext? transactionContext,
CancellationToken cancellationToken)
{
+ // Use OriginalResourcePath if ResourcePath was modified
+ var lookupResourcePath = entry.OriginalResourcePath ?? entry.ResourcePath;
+
var occurrence = await _occurrenceRepository.GetByIdAsync(
entry.OccurrenceId!.Value,
entry.Organization,
- entry.ResourcePath,
+ lookupResourcePath,
transactionContext,
cancellationToken).ConfigureAwait(false) ?? throw new KeyNotFoundException(
$"Occurrence with ID '{entry.OccurrenceId}' not found.");
@@ -561,6 +570,8 @@ private async Task UpdateStandaloneOccurrenceAsync(
occurrence.StartTime = entry.StartTime;
occurrence.Duration = entry.Duration;
occurrence.Extensions = entry.Extensions;
+ occurrence.Type = entry.Type;
+ occurrence.ResourcePath = entry.ResourcePath;
var updated = await _occurrenceRepository.UpdateAsync(
occurrence,
@@ -581,18 +592,6 @@ private static void ValidateImmutableOccurrenceFields(CalendarEntry entry, Domai
"Cannot modify Organization. This field is immutable after creation.");
}
- if (entry.ResourcePath != existing.ResourcePath)
- {
- throw new InvalidOperationException(
- "Cannot modify ResourcePath. This field is immutable after creation.");
- }
-
- if (entry.Type != existing.Type)
- {
- throw new InvalidOperationException(
- "Cannot modify Type. This field is immutable after creation.");
- }
-
if (entry.TimeZone != existing.TimeZone)
{
throw new InvalidOperationException(
@@ -611,10 +610,13 @@ private async Task CreateOverrideForVirtualizedOccurrenceAsync(
var recurrenceId = entry.RecurrenceId
?? throw new InvalidOperationException("Cannot create override: RecurrenceId is missing.");
+ // Use OriginalResourcePath if ResourcePath was modified (though this should fail validation)
+ var lookupResourcePath = entry.OriginalResourcePath ?? entry.ResourcePath;
+
var recurrence = await _recurrenceRepository.GetByIdAsync(
recurrenceId,
entry.Organization,
- entry.ResourcePath,
+ lookupResourcePath,
transactionContext,
cancellationToken).ConfigureAwait(false) ?? throw new KeyNotFoundException(
$"Parent recurrence with ID '{recurrenceId}' not found.");
@@ -662,10 +664,13 @@ private async Task UpdateVirtualizedOccurrenceWithOverrideAsync(
ITransactionContext? transactionContext,
CancellationToken cancellationToken)
{
+ // Use OriginalResourcePath if ResourcePath was modified (though this should fail validation)
+ var lookupResourcePath = entry.OriginalResourcePath ?? entry.ResourcePath;
+
var @override = await _overrideRepository.GetByIdAsync(
entry.OverrideId!.Value,
entry.Organization,
- entry.ResourcePath,
+ lookupResourcePath,
transactionContext,
cancellationToken).ConfigureAwait(false) ?? throw new KeyNotFoundException(
$"Override with ID '{entry.OverrideId}' not found.");
@@ -674,7 +679,7 @@ private async Task UpdateVirtualizedOccurrenceWithOverrideAsync(
var recurrence = await _recurrenceRepository.GetByIdAsync(
@override.RecurrenceId,
entry.Organization,
- entry.ResourcePath,
+ lookupResourcePath,
transactionContext,
cancellationToken).ConfigureAwait(false) ?? throw new KeyNotFoundException(
$"Parent recurrence with ID '{@override.RecurrenceId}' not found.");
diff --git a/src/RecurringThings/Models/CalendarEntry.cs b/src/RecurringThings/Models/CalendarEntry.cs
index 76f948f..85e1b73 100644
--- a/src/RecurringThings/Models/CalendarEntry.cs
+++ b/src/RecurringThings/Models/CalendarEntry.cs
@@ -28,28 +28,72 @@ namespace RecurringThings.Models;
///
/// is never set in query results because excepted occurrences are not returned.
///
+///
+/// Mutable properties:
+///
+///
+/// - , , - mutable on standalone occurrences and virtualized occurrences
+/// - , - mutable only on standalone occurrences and recurrences (not on virtualized occurrences)
+///
///
public sealed class CalendarEntry
{
+ private string _resourcePath = null!;
+
///
- /// Gets or sets the tenant identifier for multi-tenant isolation.
+ /// Gets the tenant identifier for multi-tenant isolation.
///
- public required string Organization { get; set; }
+ ///
+ /// This property is immutable after creation.
+ ///
+ public required string Organization { get; init; }
///
/// Gets or sets the hierarchical resource scope.
///
- public required string ResourcePath { get; set; }
+ ///
+ ///
+ /// This property is mutable on standalone occurrences and recurrences.
+ /// Changing ResourcePath on virtualized occurrences is not allowed.
+ ///
+ ///
+ public required string ResourcePath
+ {
+ get => _resourcePath;
+ set
+ {
+ // Track original value for database lookups
+ OriginalResourcePath ??= _resourcePath;
+ _resourcePath = value;
+ }
+ }
+
+ ///
+ /// Gets the original resource path value when the entry was loaded.
+ ///
+ ///
+ /// Used internally to locate the record in the database when ResourcePath has been modified.
+ ///
+ internal string? OriginalResourcePath { get; private set; }
///
/// Gets or sets the user-defined type of this entry.
///
+ ///
+ ///
+ /// This property is mutable on standalone occurrences and recurrences.
+ /// Changing Type on virtualized occurrences is not allowed.
+ ///
+ ///
public required string Type { get; set; }
///
- /// Gets or sets the type of this calendar entry.
+ /// Gets the type of this calendar entry.
///
- public CalendarEntryType EntryType { get; set; }
+ ///
+ /// This property is immutable after creation.
+ ///
+ public CalendarEntryType EntryType { get; init; }
///
/// Gets or sets the local timestamp when this entry starts.
@@ -60,12 +104,17 @@ public sealed class CalendarEntry
public DateTime StartTime { get; set; }
///
- /// Gets or sets the local timestamp when this entry ends.
+ /// Gets the local timestamp when this entry ends.
///
///
+ ///
/// The time is in the local timezone specified by .
+ ///
+ ///
+ /// EndTime is computed as StartTime + Duration and cannot be set directly.
+ ///
///
- public DateTime EndTime { get; set; }
+ public DateTime EndTime { get; internal set; }
///
/// Gets or sets the duration of this entry.
@@ -73,9 +122,12 @@ public sealed class CalendarEntry
public TimeSpan Duration { get; set; }
///
- /// Gets or sets the IANA time zone identifier.
+ /// Gets the IANA time zone identifier.
///
- public required string TimeZone { get; set; }
+ ///
+ /// This property is immutable after creation.
+ ///
+ public required string TimeZone { get; init; }
///
/// Gets or sets the user-defined key-value metadata.
@@ -83,39 +135,59 @@ public sealed class CalendarEntry
public Dictionary? Extensions { get; set; }
///
- /// Gets or sets the recurrence ID.
+ /// Gets the recurrence ID.
///
///
+ ///
/// Set when is
/// or .
+ ///
+ ///
+ /// This property is immutable after creation.
+ ///
///
- public Guid? RecurrenceId { get; set; }
+ public Guid? RecurrenceId { get; init; }
///
- /// Gets or sets the standalone occurrence ID.
+ /// Gets the standalone occurrence ID.
///
///
+ ///
/// Set only when is .
+ ///
+ ///
+ /// This property is immutable after creation.
+ ///
///
- public Guid? OccurrenceId { get; set; }
+ public Guid? OccurrenceId { get; init; }
///
- /// Gets or sets the override ID if this virtualized occurrence has been modified.
+ /// Gets the override ID if this virtualized occurrence has been modified.
///
///
+ ///
/// Set when this is a virtualized occurrence that has an override applied.
/// Check for a convenient boolean check.
+ ///
+ ///
+ /// This property is immutable after creation.
+ ///
///
- public Guid? OverrideId { get; set; }
+ public Guid? OverrideId { get; init; }
///
- /// Gets or sets the exception ID.
+ /// Gets the exception ID.
///
///
+ ///
/// This property is never set in query results because excepted (deleted) occurrences
/// are not returned by queries.
+ ///
+ ///
+ /// This property is immutable after creation.
+ ///
///
- public Guid? ExceptionId { get; set; }
+ public Guid? ExceptionId { get; init; }
///
/// Gets a value indicating whether this entry has an override applied.
@@ -127,17 +199,22 @@ public sealed class CalendarEntry
public bool IsOverridden => OverrideId.HasValue;
///
- /// Gets or sets the recurrence details.
+ /// Gets the recurrence details.
///
///
+ ///
/// Populated when is
/// or . Contains the RRule that defines or
/// generated this entry.
+ ///
+ ///
+ /// This property is immutable after creation.
+ ///
///
- public RecurrenceDetails? RecurrenceDetails { get; set; }
+ public RecurrenceDetails? RecurrenceDetails { get; init; }
///
- /// Gets or sets the original values before an override was applied.
+ /// Gets the original values before an override was applied.
///
///
///
@@ -147,6 +224,9 @@ public sealed class CalendarEntry
/// When an override is applied to a virtualized occurrence, this property contains
/// the original start time, duration, and extensions before modification.
///
+ ///
+ /// This property is immutable after creation.
+ ///
///
- public OriginalDetails? Original { get; set; }
+ public OriginalDetails? Original { get; init; }
}
diff --git a/src/RecurringThings/RecurringThings.csproj b/src/RecurringThings/RecurringThings.csproj
index 690ab9d..7301a8a 100644
--- a/src/RecurringThings/RecurringThings.csproj
+++ b/src/RecurringThings/RecurringThings.csproj
@@ -10,8 +10,8 @@
RecurringThings
- RecurringThings Contributors
- A .NET library for managing recurring events with on-demand virtualization, supporting MongoDB and PostgreSQL persistence.
+ Miguel Tremblay
+ A .NET library for managing recurring events with efficient on-demand virtualization.
https://github.com/ChuckNovice/RecurringThings
Apache-2.0
README.md
@@ -26,7 +26,10 @@
+
+
+
diff --git a/src/RecurringThings/Repository/IOccurrenceExceptionRepository.cs b/src/RecurringThings/Repository/IOccurrenceExceptionRepository.cs
index 34b3e1e..399b120 100644
--- a/src/RecurringThings/Repository/IOccurrenceExceptionRepository.cs
+++ b/src/RecurringThings/Repository/IOccurrenceExceptionRepository.cs
@@ -14,7 +14,7 @@ namespace RecurringThings.Repository;
/// Occurrence exceptions are used to cancel specific virtualized occurrences
/// from a recurrence pattern without deleting the entire recurrence.
///
-public interface IOccurrenceExceptionRepository
+internal interface IOccurrenceExceptionRepository
{
///
/// Creates a new occurrence exception.
diff --git a/src/RecurringThings/Repository/IOccurrenceOverrideRepository.cs b/src/RecurringThings/Repository/IOccurrenceOverrideRepository.cs
index f79c1fa..8e266a4 100644
--- a/src/RecurringThings/Repository/IOccurrenceOverrideRepository.cs
+++ b/src/RecurringThings/Repository/IOccurrenceOverrideRepository.cs
@@ -14,7 +14,7 @@ namespace RecurringThings.Repository;
/// Occurrence overrides are used to modify specific virtualized occurrences
/// from a recurrence pattern (e.g., change time, duration, or metadata).
///
-public interface IOccurrenceOverrideRepository
+internal interface IOccurrenceOverrideRepository
{
///
/// Creates a new occurrence override.
diff --git a/src/RecurringThings/Repository/IOccurrenceRepository.cs b/src/RecurringThings/Repository/IOccurrenceRepository.cs
index a4ea7c9..5b83d71 100644
--- a/src/RecurringThings/Repository/IOccurrenceRepository.cs
+++ b/src/RecurringThings/Repository/IOccurrenceRepository.cs
@@ -10,7 +10,7 @@ namespace RecurringThings.Repository;
///
/// Repository interface for managing standalone entities.
///
-public interface IOccurrenceRepository
+internal interface IOccurrenceRepository
{
///
/// Creates a new standalone occurrence.
diff --git a/src/RecurringThings/Repository/IRecurrenceRepository.cs b/src/RecurringThings/Repository/IRecurrenceRepository.cs
index 67edb1c..b0d1718 100644
--- a/src/RecurringThings/Repository/IRecurrenceRepository.cs
+++ b/src/RecurringThings/Repository/IRecurrenceRepository.cs
@@ -10,7 +10,7 @@ namespace RecurringThings.Repository;
///
/// Repository interface for managing entities.
///
-public interface IRecurrenceRepository
+internal interface IRecurrenceRepository
{
///
/// Creates a new recurrence.
diff --git a/src/RecurringThings/Validation/Validator.cs b/src/RecurringThings/Validation/Validator.cs
index 58747ac..358d8f2 100644
--- a/src/RecurringThings/Validation/Validator.cs
+++ b/src/RecurringThings/Validation/Validator.cs
@@ -21,7 +21,7 @@ public static partial class Validator
///
/// Thrown when the child entity has a different Organization or ResourcePath than the parent.
///
- public static void ValidateTenantScope(
+ internal static void ValidateTenantScope(
Recurrence parentRecurrence,
string childOrganization,
string childResourcePath)
diff --git a/tests/RecurringThings.Tests/Engine/RecurrenceEngineCrudTests.cs b/tests/RecurringThings.Tests/Engine/RecurrenceEngineCrudTests.cs
index dc36794..2fce028 100644
--- a/tests/RecurringThings.Tests/Engine/RecurrenceEngineCrudTests.cs
+++ b/tests/RecurringThings.Tests/Engine/RecurrenceEngineCrudTests.cs
@@ -68,15 +68,15 @@ public async Task CreateRecurrenceAsync_WithValidParameters_ReturnsRecurrenceWit
// Assert
result.Should().NotBeNull();
- result.Id.Should().NotBe(Guid.Empty);
+ result.RecurrenceId.Should().NotBe(Guid.Empty);
+ result.EntryType.Should().Be(CalendarEntryType.Recurrence);
result.Organization.Should().Be(TestOrganization);
result.ResourcePath.Should().Be(TestResourcePath);
result.Type.Should().Be(TestType);
result.StartTime.Should().Be(startTime);
result.Duration.Should().Be(duration);
- // RecurrenceEndTime is extracted from RRule UNTIL clause
- result.RecurrenceEndTime.Should().Be(new DateTime(2025, 12, 31, 23, 59, 59, DateTimeKind.Utc));
- result.RRule.Should().Be(TestRRule);
+ result.RecurrenceDetails.Should().NotBeNull();
+ result.RecurrenceDetails!.RRule.Should().Be(TestRRule);
result.TimeZone.Should().Be(TestTimeZone);
result.Extensions.Should().BeEquivalentTo(extensions);
}
@@ -210,7 +210,8 @@ public async Task CreateOccurrenceAsync_WithValidParameters_ReturnsOccurrenceWit
// Assert
result.Should().NotBeNull();
- result.Id.Should().NotBe(Guid.Empty);
+ result.OccurrenceId.Should().NotBe(Guid.Empty);
+ result.EntryType.Should().Be(CalendarEntryType.Standalone);
result.Organization.Should().Be(TestOrganization);
result.StartTime.Should().Be(startTime);
result.Duration.Should().Be(duration);
@@ -338,21 +339,26 @@ public async Task UpdateAsync_StandaloneOccurrence_UpdatesStartTimeDurationExten
}
[Fact]
- public async Task UpdateAsync_StandaloneOccurrenceWithImmutableTypeChange_ThrowsInvalidOperationException()
+ public async Task UpdateAsync_StandaloneOccurrenceWithTypeChange_UpdatesTypeSuccessfully()
{
// Arrange
var occurrenceId = Guid.NewGuid();
var existingOccurrence = CreateOccurrence(occurrenceId);
+ var newType = "different-type";
_occurrenceRepo
.Setup(r => r.GetByIdAsync(occurrenceId, TestOrganization, TestResourcePath, null, default))
.ReturnsAsync(existingOccurrence);
+ _occurrenceRepo
+ .Setup(r => r.UpdateAsync(It.IsAny(), null, default))
+ .ReturnsAsync((Occurrence o, ITransactionContext? _, CancellationToken _) => o);
+
var entry = new CalendarEntry
{
Organization = TestOrganization,
ResourcePath = TestResourcePath,
- Type = "different-type", // Changed!
+ Type = newType, // Changed - Type is mutable on standalone occurrences
StartTime = existingOccurrence.StartTime,
Duration = existingOccurrence.Duration,
TimeZone = TestTimeZone,
@@ -360,11 +366,14 @@ public async Task UpdateAsync_StandaloneOccurrenceWithImmutableTypeChange_Throws
};
// Act
- var act = () => _engine.UpdateOccurrenceAsync(entry);
+ var result = await _engine.UpdateOccurrenceAsync(entry);
// Assert
- await act.Should().ThrowAsync()
- .WithMessage("*Type*immutable*");
+ result.Type.Should().Be(newType);
+ _occurrenceRepo.Verify(r => r.UpdateAsync(
+ It.Is(o => o.Type == newType),
+ null,
+ default), Times.Once);
}
#endregion