private AppendContext Prepare(DateTimeOffset now) { var index = this.ReadIndex(); var firstLink = index.Links .Where(l => l.IsFirstLink) .DefaultIfEmpty(AtomLink.CreateFirstLink(this.CreateNewFeedAddress())) .Single(); index = index.WithLinks(index.Links.Union(new[] { firstLink })); var lastLink = index.Links.SingleOrDefault(l => l.IsLastLink); var lastLinkAdded = false; if (lastLink == null) { lastLink = firstLink.ToLastLink(); lastLinkAdded = true; } var lastPage = this.ReadTrueLastPage(lastLink.Href); var lastLinkCorrected = false; if (lastPage.Links.Single(l => l.IsSelfLink).Href != lastLink.Href) { lastLink = lastPage.Links.Single(l => l.IsSelfLink).ToLastLink(); lastLinkCorrected = true; } index = index.WithLinks(index.Links .Where(l => !l.IsLastLink) .Concat(new[] { lastLink })); return(new AppendContext( index, lastPage, now, lastLinkAdded, lastLinkCorrected)); }
/// <summary> /// Appends an event to the event stream. /// </summary> /// <param name="event"> /// The event to append to the event stream. /// </param> /// <returns> /// A <see cref="Task" /> representing the asynchronous operation of /// appending the event to the event stream. /// </returns> /// <remarks> /// <para> /// This method appends <paramref name="event" /> to the current event /// stream. Appending an event indicates that it happened /// <em>after</em> all previous events. /// </para> /// <para> /// Since this method conceptually involves writing the event to the /// underlying <see cref="Storage" />, it may take significant time to /// complete; for that reason, it's an asynchronous method, returning a /// <see cref="Task" />. The operation is not guaranteed to be complete /// before the Task completes successfully. /// </para> /// <para> /// When updating the underlying Storage, the method typically only /// updates the index feed, using /// <see cref="IAtomEventStorage.CreateFeedWriterFor(AtomFeed)" />. /// However, when the number of entries in the index surpasses /// <see cref="PageSize" />, a new feed page is created for the new /// entry. This page is also written using the CreateFeedWriterFor /// method, and only after this succeeds is the old feed page updated /// with a link to the new page. Since these two operations are not /// guaranteed to happen within an ACID transaction, it's possible that /// the new page is saved, but that the update of the old page fails. /// If the underlying storage throws an exception at that point, that /// exception will bubble up to the caller of the AppendAsync method. /// It's up to the caller to retry the operation. /// </para> /// <para> /// However, in that situation, an orphaned Atom feed page is likely to /// have been left in the underlying storage. This doesn't affect /// consistency of the system, but may take up unnecessary disk space. /// If this is the case, a separate clean-up task should find and /// delete orphaned pages. /// </para> /// </remarks> /// <example> /// This example shows how to create a UserCreated event and write it /// using the AppendAsync method. Notice that since /// AtomEventObserver<T> uses the standard Task Parallel Library /// (TPL) model, you can use it with 'async' and 'await'. /// <code> /// var obs = new AtomEventObserver<IUserEvent>( /// eventStreamId, // a Guid /// pageSize, // an Int32 /// storage, // an IAtomEventStorage object /// serializer); // an IContentSerializer object /// /// var userCreated = new UserCreated /// { /// UserId = eventStreamId, /// UserName = "******", /// Password = "******", /// Email = "*****@*****.**" /// }; /// await obs.AppendAsync(userCreated); /// </code> /// </example> /// <seealso cref="OnNext" /> public Task AppendAsync(T @event) { return(Task.Factory.StartNew(() => { var now = DateTimeOffset.Now; var index = this.ReadIndex(); var firstLink = index.Links .Where(l => l.IsFirstLink) .DefaultIfEmpty(AtomLink.CreateFirstLink(this.CreateNewFeedAddress())) .Single(); var lastLink = index.Links.SingleOrDefault(l => l.IsLastLink); var lastLinkChanged = false; if (lastLink == null) { lastLink = firstLink.ToLastLink(); lastLinkChanged = true; } var lastPage = this.ReadLastPage(lastLink.Href); if (lastPage.Links.Single(l => l.IsSelfLink).Href != lastLink.Href) { lastLink = lastPage.Links.Single(l => l.IsSelfLink).ToLastLink(); lastLinkChanged = true; } index = index.WithLinks(index.Links.Union(new[] { firstLink })); index = index.WithLinks(index.Links .Where(l => !l.IsLastLink) .Concat(new[] { lastLink })); var entry = CreateEntry(@event, now); if (lastPage.Entries.Count() >= this.pageSize) { var nextAddress = this.CreateNewFeedAddress(); var nextPage = this.ReadPage(nextAddress); nextPage = AddEntryTo(nextPage, entry, now); var nextLink = AtomLink.CreateNextLink(nextAddress); var previousPage = lastPage .WithLinks(lastPage.Links.Concat(new[] { nextLink })); var previousLink = previousPage.Links .Single(l => l.IsSelfLink) .ToPreviousLink(); nextPage = nextPage.WithLinks( nextPage.Links.Concat(new[] { previousLink })); index = index.WithLinks(index.Links .Where(l => !l.IsLastLink) .Concat(new[] { nextLink.ToLastLink() })); this.Write(nextPage); this.Write(previousPage); this.Write(index); } else { lastPage = AddEntryTo(lastPage, entry, now); this.Write(lastPage); if (lastLinkChanged) { this.Write(index); } } })); }