Last active
December 22, 2015 14:52
-
-
Save yreynhout/702005922c6dff4f9f69 to your computer and use it in GitHub Desktop.
System.Runtime.Caching meets Projac.Connector
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Runtime.Caching; | |
using System.Text; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using KellermanSoftware.CompareNetObjects; | |
using Newtonsoft.Json.Linq; | |
using NUnit.Framework; | |
using Projac.Connector; | |
using Projac.Connector.Testing; | |
namespace KickIt | |
{ | |
//Events | |
public class DonationCollectionStarted | |
{ | |
public readonly Guid DonationCollectionId; | |
public readonly Guid DonationId; | |
public readonly string ReferralCode; | |
public readonly DateTimeOffset StartDateTime; | |
public DonationCollectionStarted(Guid donationCollectionId, Guid donationId, string referralCode, DateTimeOffset startDateTime) | |
{ | |
DonationCollectionId = donationCollectionId; | |
DonationId = donationId; | |
ReferralCode = referralCode; | |
StartDateTime = startDateTime; | |
} | |
} | |
public class DonationToSuspendedCauseAttempted | |
{ | |
public readonly Guid DonationCollectionId; | |
public readonly Guid DonationId; | |
public DonationToSuspendedCauseAttempted(Guid donationCollectionId, Guid donationId) | |
{ | |
DonationCollectionId = donationCollectionId; | |
DonationId = donationId; | |
} | |
} | |
//Specs | |
[TestFixture] | |
public class DonationProjectionSpecs | |
{ | |
[Test] | |
public Task when_a_donation_collection_started() | |
{ | |
return MemoryCacheProjection.For(new DonationProjection(MemoryAdditionalDataProvider.Empty)). | |
Given( | |
new DonationCollectionStarted( | |
new Guid("438AEA16-54A2-4C76-B82E-97563C475D5E"), | |
new Guid("69AB3034-C7DD-4629-A1F8-97AF099C8556"), | |
"BLA-BLA", | |
DateTimeOffset.Parse("2015-12-15 11:14:01.088"))). | |
Expect(new CacheItem( | |
"69AB3034-C7DD-4629-A1F8-97AF099C8556".ToLower(), | |
new DonationDataModel | |
{ | |
DonationCollectionId = new Guid("438AEA16-54A2-4C76-B82E-97563C475D5E"), | |
DonationStartedTime = DateTimeOffset.Parse("2015-12-15 11:14:01.088"), | |
ReferralCode = "BLA-BLA", | |
AdditionalData = null, | |
Invalid = false, | |
Reason = null | |
})); | |
} | |
[Test] | |
public Task when_a_donation_to_a_suspended_cause_was_attempted() | |
{ | |
return MemoryCacheProjection.For(new DonationProjection(MemoryAdditionalDataProvider.Empty)). | |
Given( | |
new DonationCollectionStarted( | |
new Guid("438AEA16-54A2-4C76-B82E-97563C475D5E"), | |
new Guid("69AB3034-C7DD-4629-A1F8-97AF099C8556"), | |
"BLA-BLA", | |
DateTimeOffset.Parse("2015-12-15 11:14:01.088")), | |
new DonationToSuspendedCauseAttempted( | |
new Guid("438AEA16-54A2-4C76-B82E-97563C475D5E"), | |
new Guid("69AB3034-C7DD-4629-A1F8-97AF099C8556"))). | |
Expect(new CacheItem( | |
"69AB3034-C7DD-4629-A1F8-97AF099C8556".ToLower(), | |
new DonationDataModel | |
{ | |
DonationCollectionId = new Guid("438AEA16-54A2-4C76-B82E-97563C475D5E"), | |
DonationStartedTime = DateTimeOffset.Parse("2015-12-15 11:14:01.088"), | |
ReferralCode = "BLA-BLA", | |
AdditionalData = null, | |
Invalid = true, | |
Reason = "Can't donate to a suspended cause" | |
})); | |
} | |
} | |
//Data Model | |
public class DonationDataModel | |
{ | |
public Guid DonationCollectionId { get; set; } | |
public string ReferralCode { get; set; } | |
public DateTimeOffset DonationStartedTime { get; set; } | |
public string AdditionalData { get; set; } | |
public bool Invalid { get; set; } | |
public string Reason { get; set; } | |
} | |
//Projection | |
public class DonationProjection : ConnectedProjection<MemoryCache> | |
{ | |
public DonationProjection(IAdditionalDataProvider additionalDataProvider) | |
{ | |
When<DonationCollectionStarted>( | |
async (cache, message) => | |
{ | |
var additionalData = await additionalDataProvider.GetAsync(message.DonationCollectionId); | |
cache.Add( | |
new CacheItem( | |
message.DonationId.ToString(), | |
new DonationDataModel | |
{ | |
DonationCollectionId = message.DonationCollectionId, | |
DonationStartedTime = message.StartDateTime, | |
ReferralCode = message.ReferralCode, | |
AdditionalData = additionalData, | |
Invalid = false, | |
Reason = null | |
}), | |
new CacheItemPolicy | |
{ | |
AbsoluteExpiration = ObjectCache.InfiniteAbsoluteExpiration | |
}); | |
}); | |
WhenSync<DonationToSuspendedCauseAttempted>( | |
(cache, message) => | |
{ | |
var item = cache.GetCacheItem(message.DonationId.ToString()); | |
if (item != null) | |
{ | |
var model = (DonationDataModel)item.Value; | |
model.Invalid = true; | |
model.Reason = "Can't donate to a suspended cause"; | |
} | |
}); | |
} | |
} | |
//Services | |
public interface IAdditionalDataProvider | |
{ | |
Task<string> GetAsync(Guid donationId); | |
} | |
public class MemoryAdditionalDataProvider : IAdditionalDataProvider | |
{ | |
public static readonly IAdditionalDataProvider Empty = | |
new MemoryAdditionalDataProvider(new Dictionary<Guid, string>()); | |
private readonly IReadOnlyDictionary<Guid, string> _store; | |
public MemoryAdditionalDataProvider(IReadOnlyDictionary<Guid, string> store) | |
{ | |
if (store == null) | |
throw new ArgumentNullException("store"); | |
_store = store; | |
} | |
public Task<string> GetAsync(Guid donationId) | |
{ | |
string data; | |
return Task.FromResult(_store.TryGetValue(donationId, out data) ? data : null); | |
} | |
} | |
//Infrastructure | |
public static class MemoryCacheProjection | |
{ | |
public static ConnectedProjectionScenario<MemoryCache> For(ConnectedProjectionHandler<MemoryCache>[] handlers) | |
{ | |
return new ConnectedProjectionScenario<MemoryCache>( | |
ConcurrentResolve.WhenEqualToHandlerMessageType(handlers) | |
); | |
} | |
public static Task ExpectNone(this ConnectedProjectionScenario<MemoryCache> scenario) | |
{ | |
return scenario. | |
Verify(cache => | |
{ | |
if (cache.GetCount() != 0) | |
{ | |
return Task.FromResult( | |
VerificationResult.Fail( | |
string.Format("Expected no cache items, but found {0} cache item(s) ({1}).", | |
cache.GetCount(), | |
string.Join(",", cache.Select(pair => pair.Key))))); | |
} | |
return Task.FromResult(VerificationResult.Pass()); | |
}). | |
Assert(); | |
} | |
public static async Task Assert(this ConnectedProjectionTestSpecification<MemoryCache> specification) | |
{ | |
if (specification == null) | |
throw new ArgumentNullException("specification"); | |
using (var cache = new MemoryCache(new Random().Next().ToString())) | |
{ | |
await new ConnectedProjector<MemoryCache>(specification.Resolver). | |
ProjectAsync(cache, specification.Messages); | |
var result = await specification.Verification(cache, CancellationToken.None); | |
if (result.Failed) | |
{ | |
throw new AssertionException(result.Message); | |
} | |
} | |
} | |
public static Task Expect(this ConnectedProjectionScenario<MemoryCache> scenario, params CacheItem[] items) | |
{ | |
if (items == null) | |
throw new ArgumentNullException("items"); | |
if (items.Length == 0) | |
{ | |
return scenario.ExpectNone(); | |
} | |
return scenario. | |
Verify(cache => | |
{ | |
if (cache.GetCount() != items.Length) | |
{ | |
if (cache.GetCount() == 0) | |
{ | |
return Task.FromResult( | |
VerificationResult.Fail( | |
string.Format("Expected {0} cache item(s), but found 0 cache items.", | |
items.Length))); | |
} | |
return Task.FromResult( | |
VerificationResult.Fail( | |
string.Format("Expected {0} cache item(s), but found {1} cache item(s) ({2}).", | |
items.Length, | |
cache.GetCount(), | |
string.Join(",", cache.Select(pair => pair.Key))))); | |
} | |
if (!cache.Select(pair => cache.GetCacheItem(pair.Key)).SequenceEqual(items, new CacheItemEqualityComparer())) | |
{ | |
var builder = new StringBuilder(); | |
builder.AppendLine("Expected the following cache items:"); | |
foreach (var expectedItem in items) | |
{ | |
builder.AppendLine(expectedItem.Key + ": " + JToken.FromObject(expectedItem.Value).ToString()); | |
} | |
builder.AppendLine(); | |
builder.AppendLine("But found the following cache items:"); | |
foreach (var actualItem in cache) | |
{ | |
builder.AppendLine(actualItem.Key + ": " + JToken.FromObject(actualItem.Value).ToString()); | |
} | |
return Task.FromResult(VerificationResult.Fail(builder.ToString())); | |
} | |
return Task.FromResult(VerificationResult.Pass()); | |
}). | |
Assert(); | |
} | |
} | |
public class CacheItemEqualityComparer : IEqualityComparer<CacheItem> | |
{ | |
public bool Equals(CacheItem x, CacheItem y) | |
{ | |
if (x == null && y == null) return true; | |
if (x == null || y == null) return false; | |
return x.Key.Equals(y.Key) && | |
new CompareLogic().Compare(x.Value, y.Value).AreEqual; | |
} | |
public int GetHashCode(CacheItem obj) | |
{ | |
throw new NotSupportedException(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment