Skip to content

Instantly share code, notes, and snippets.

@yreynhout
Last active December 22, 2015 14:52
Show Gist options
  • Save yreynhout/702005922c6dff4f9f69 to your computer and use it in GitHub Desktop.
Save yreynhout/702005922c6dff4f9f69 to your computer and use it in GitHub Desktop.
System.Runtime.Caching meets Projac.Connector
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