From 756ce9d1cf66cc6ffb988a501e98b91213beea28 Mon Sep 17 00:00:00 2001 From: Tomas Lycken Date: Tue, 5 Dec 2017 15:07:29 +0100 Subject: [PATCH] Ef core connector (#44) * Implement provider for EF Core * Improve naming --- .../EventStoreTests/ExtraMetaTests.cs | 120 ++++++++++++++++++ .../EventStoreTests/QueryEventsTests.cs | 66 ++++++++++ .../EventStoreTests/WriteEventTests.cs | 99 +++++++++++++++ .../NonDefaultImplementationsTests.cs | 99 +++++++++++++++ .../Infrastructure/EventStoreFixture.cs | 43 +++++++ .../Infrastructure/EventStoreTestBase.cs | 32 +++++ .../RdbmsEventStore.EFCore.Tests.csproj | 23 ++++ .../TestData/EventTypes.cs | 36 ++++++ src/RdbmsEventStore.EFCore/EFCoreEvent.cs | 29 +++++ .../EFCoreEventStore.cs | 73 +++++++++++ .../EFCoreEventStoreContext.cs | 25 ++++ .../IEFCoreEventStoreContext.cs | 9 ++ .../RdbmsEventStore.EFCore.csproj | 15 +++ src/RdbmsEventStore.sln | 16 ++- 14 files changed, 683 insertions(+), 2 deletions(-) create mode 100644 src/RdbmsEventStore.EFCore.Tests/EventStoreTests/ExtraMetaTests.cs create mode 100644 src/RdbmsEventStore.EFCore.Tests/EventStoreTests/QueryEventsTests.cs create mode 100644 src/RdbmsEventStore.EFCore.Tests/EventStoreTests/WriteEventTests.cs create mode 100644 src/RdbmsEventStore.EFCore.Tests/ExtensibilityTests/NonDefaultImplementationsTests.cs create mode 100644 src/RdbmsEventStore.EFCore.Tests/Infrastructure/EventStoreFixture.cs create mode 100644 src/RdbmsEventStore.EFCore.Tests/Infrastructure/EventStoreTestBase.cs create mode 100644 src/RdbmsEventStore.EFCore.Tests/RdbmsEventStore.EFCore.Tests.csproj create mode 100644 src/RdbmsEventStore.EFCore.Tests/TestData/EventTypes.cs create mode 100644 src/RdbmsEventStore.EFCore/EFCoreEvent.cs create mode 100644 src/RdbmsEventStore.EFCore/EFCoreEventStore.cs create mode 100644 src/RdbmsEventStore.EFCore/EFCoreEventStoreContext.cs create mode 100644 src/RdbmsEventStore.EFCore/IEFCoreEventStoreContext.cs create mode 100644 src/RdbmsEventStore.EFCore/RdbmsEventStore.EFCore.csproj diff --git a/src/RdbmsEventStore.EFCore.Tests/EventStoreTests/ExtraMetaTests.cs b/src/RdbmsEventStore.EFCore.Tests/EventStoreTests/ExtraMetaTests.cs new file mode 100644 index 0000000..dea1227 --- /dev/null +++ b/src/RdbmsEventStore.EFCore.Tests/EventStoreTests/ExtraMetaTests.cs @@ -0,0 +1,120 @@ +using System; +using System.Threading.Tasks; +using RdbmsEventStore.EFCore.Tests.Infrastructure; +using Xunit; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using RdbmsEventStore.EFCore.Tests.TestData; +using RdbmsEventStore.EventRegistry; +using RdbmsEventStore.Serialization; + +namespace RdbmsEventStore.EFCore.Tests.EventStoreTests +{ + public class ExtraMetaTests : IClassFixture + { + private readonly ExtraMetaEventFactoryFixture _fixture; + private readonly EFCoreEventStoreContext _dbContext; + + public ExtraMetaTests(ExtraMetaEventFactoryFixture fixture) + { + _fixture = fixture; + var options = new DbContextOptionsBuilder>() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + _dbContext = new EFCoreEventStoreContext(options); + + var stream1 = _fixture.EventFactory.Create("stream-1", 0, new object[] { + new FooEvent { Foo = "Foo" }, + new BarEvent { Bar = "Bar" }, + new FooEvent { Foo = "Baz" } + }) + .Select(_fixture.EventSerializer.Serialize); + var stream2 = _fixture.EventFactory.Create("stream-2", 0, new object[] { + new FooEvent { Foo = "Boo" }, + new BarEvent { Bar = "Far" } + }) + .Select(_fixture.EventSerializer.Serialize); + + _dbContext.Events.AddRange(stream1); + _dbContext.Events.AddRange(stream2); + _dbContext.SaveChanges(); + } + + [Theory] + [InlineData("stream-1", 3)] + [InlineData("stream-2", 2)] + public async Task ReturnsEventsFromCorrectStreamOnly(string streamId, long expectedCount) + { + var store = _fixture.BuildEventStore(_dbContext) as IEventStore; + var events = await store.Events(streamId); + Assert.Equal(expectedCount, events.Count()); + } + + [Theory] + [InlineData("stream-1", 2)] + [InlineData("stream-2", 1)] + public async Task ReturnsEventsAccordingToQuery(string streamId, long expectedCount) + { + var store = _fixture.BuildEventStore(_dbContext) as IEventStore; + var events = await store.Events(streamId, es => es.Where(e => e.ExtraMeta.StartsWith("Foo"))); + Assert.Equal(expectedCount, events.Count()); + } + + [Theory] + [InlineData("stream-1", 2)] + [InlineData("stream-2", 1)] + public async Task ReturnsEventsWithMetadata(string streamId, long expectedCount) + { + var store = _fixture.BuildEventStore(_dbContext) as IEventStore; + var events = await store + .Events(streamId, es => es.Where(e => e.ExtraMeta.StartsWith("Foo"))) + .ToReadOnlyCollection(); + + Assert.Equal(expectedCount, events.Count); + Assert.All(events, @event => Assert.StartsWith("Foo", @event.ExtraMeta)); + } + + [Theory] + [InlineData("stream-1", 2)] + [InlineData("stream-2", 1)] + public async Task CanQueryByExtraMetadata(string streamId, long expectedCount) + { + var store = _fixture.BuildEventStore(_dbContext) as IEventStore; + var events = await store.Events(streamId, es => es.Where(e => e.ExtraMeta.StartsWith("Foo"))); + Assert.Equal(expectedCount, events.Count()); + } + } + + public class ExtraMetaEventFactory : DefaultEventFactory + { + private int _total; + + protected override ExtraMetaStringEvent CreateSingle(string streamId, long version, object payload) + { + var @event = base.CreateSingle(streamId, version, payload); + @event.ExtraMeta = $"{payload.GetType().Name}-{_total++}"; + return @event; + } + } + + public class ExtraMetaEventSerializer : DefaultEventSerializer + { + public ExtraMetaEventSerializer(IEventRegistry registry) : base(registry) + { + } + + public override ExtraMetaLongStringPersistedEventMetadata Serialize(ExtraMetaStringEvent @event) + { + var serialized = base.Serialize(@event); + serialized.ExtraMeta = @event.ExtraMeta; + return serialized; + } + + public override ExtraMetaStringEvent Deserialize(ExtraMetaLongStringPersistedEventMetadata @event) + { + var deserialized = base.Deserialize(@event); + deserialized.ExtraMeta = @event.ExtraMeta; + return deserialized; + } + } +} \ No newline at end of file diff --git a/src/RdbmsEventStore.EFCore.Tests/EventStoreTests/QueryEventsTests.cs b/src/RdbmsEventStore.EFCore.Tests/EventStoreTests/QueryEventsTests.cs new file mode 100644 index 0000000..7842205 --- /dev/null +++ b/src/RdbmsEventStore.EFCore.Tests/EventStoreTests/QueryEventsTests.cs @@ -0,0 +1,66 @@ +using System.Linq; +using System.Threading.Tasks; +using RdbmsEventStore.EFCore.Tests.Infrastructure; +using RdbmsEventStore.EFCore.Tests.TestData; +using Xunit; + +namespace RdbmsEventStore.EFCore.Tests.EventStoreTests +{ + public class QueryEventsTests : EventStoreTestBase, LongStringPersistedEvent> + { + public QueryEventsTests(EventStoreFixture, LongStringPersistedEvent> fixture) : base(fixture) + { + var stream1 = _fixture.EventFactory.Create("stream-1", 0, new object[] { + new FooEvent { Foo = "Foo" }, + new BarEvent { Bar = "Bar" }, + new FooEvent { Foo = "Baz" } + }) + .Select(_fixture.EventSerializer.Serialize); + var stream2 = _fixture.EventFactory.Create("stream-2", 0, new object[] { + new FooEvent { Foo = "Boo" }, + new BarEvent { Bar = "Far" } + }) + .Select(_fixture.EventSerializer.Serialize); + + _dbContext.Events.AddRange(stream1); + _dbContext.Events.AddRange(stream2); + _dbContext.SaveChanges(); + } + + [Theory] + [InlineData("stream-1", 3)] + [InlineData("stream-2", 2)] + public async Task ReturnsEventsFromCorrectStreamOnly(string streamId, long expectedCount) + { + var store = _fixture.BuildEventStore(_dbContext) as IEventStore>; + var events = await store.Events(streamId); + Assert.Equal(expectedCount, events.Count()); + } + + [Theory] + [InlineData("stream-1", 2)] + [InlineData("stream-2", 1)] + public async Task ReturnsEventsAccordingToQuery(string streamId, long expectedCount) + { + var store = _fixture.BuildEventStore(_dbContext) as IEventStore>; + var events = await store.Events(streamId, es => es.Where(e => e.Version > 1)); + Assert.Equal(expectedCount, events.Count()); + } + + [Fact] + public async Task ReturnsAllEvents() + { + var store = _fixture.BuildEventStore(_dbContext) as IEventStore>; + var events = await store.Events(); + Assert.Equal(5, events.Count()); + } + + [Fact] + public async Task ReturnsAllEventsAccordingToQuery() + { + var store = _fixture.BuildEventStore(_dbContext) as IEventStore>; + var events = await store.Events(es => es.Where(e => e.Version > 1)); + Assert.Equal(3, events.Count()); + } + } +} \ No newline at end of file diff --git a/src/RdbmsEventStore.EFCore.Tests/EventStoreTests/WriteEventTests.cs b/src/RdbmsEventStore.EFCore.Tests/EventStoreTests/WriteEventTests.cs new file mode 100644 index 0000000..086924e --- /dev/null +++ b/src/RdbmsEventStore.EFCore.Tests/EventStoreTests/WriteEventTests.cs @@ -0,0 +1,99 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Moq; +using RdbmsEventStore.EFCore.Tests.Infrastructure; +using RdbmsEventStore.EFCore.Tests.TestData; +using Xunit; + +namespace RdbmsEventStore.EFCore.Tests.EventStoreTests +{ + public class WriteEventTests : EventStoreTestBase, GuidGuidPersistedEvent> + { + public WriteEventTests(EventStoreFixture, GuidGuidPersistedEvent> fixture) : base(fixture) + { + } + + [Fact] + public async Task CommittingEventStoresEventInContext() + { + var store = _fixture.BuildEventStore(_dbContext); + await store.Append(Guid.NewGuid(), 0, new[] { new FooEvent { Foo = "Bar" } }); + Assert.Equal(1, await _dbContext.Events.CountAsync()); + } + + [Fact] + public async Task CommittingWithOutOfSyncDataThrowsConflictException() + { + var store = _fixture.BuildEventStore(_dbContext); + var stream = Guid.NewGuid(); + _dbContext.Events.AddRange(_fixture.EventFactory.Create(stream, 0, new[] { new FooEvent { Foo = "Bar" } }).Select(_fixture.EventSerializer.Serialize)); + await _dbContext.SaveChangesAsync(); + + await Assert.ThrowsAsync(() => store.Append(stream, 0, new[] { new FooEvent { Foo = "Qux" } })); + } + + [Fact] + public async Task CommittingNoEventsExitsEarly() { + var context = new Mock>(MockBehavior.Strict); + var set = new Mock>(MockBehavior.Strict); + context.Setup(c => c.Set()).Returns(set.Object); + var stream = Guid.NewGuid(); + + var store = _fixture.BuildEventStore(context.Object); + + try { + await store.Append(stream, 0, new object[] { }); + } catch (NotImplementedException) { + // Thrown by the mock DbSet if we try to query for existing events + // This indicates that we didn't exit early + + Assert.False(true, "Expected to exit early, but apparently didn't."); + } + } + + [Fact] + public async Task CommittingMultipleEventsStoresAllEventsInContext() + { + Assert.Empty(await _dbContext.Events.ToListAsync()); + + var store = _fixture.BuildEventStore(_dbContext); + + var events = new[] { new FooEvent { Foo = "Foo" }, new FooEvent { Foo = "Bar" } }; + await store.Append(Guid.NewGuid(), 0, events); + + Assert.Equal(2, await _dbContext.Events.CountAsync()); + } + + [Fact] + public async Task CommittingMultipleEventsStoresEventsInOrder() + { + Assert.Empty(await _dbContext.Events.ToListAsync()); + + var store = _fixture.BuildEventStore(_dbContext); + + var events = new object[] { new FooEvent { Foo = "Foo" }, new BarEvent { Bar = "Bar" } }; + await store.Append(Guid.NewGuid(), 0, events); + + Assert.Collection(await _dbContext.Events.OrderBy(e => e.Version).ToListAsync(), + foo => Assert.Equal(typeof(FooEvent), _fixture.EventRegistry.TypeFor(foo.Type)), + bar => Assert.Equal(typeof(BarEvent), _fixture.EventRegistry.TypeFor(bar.Type))); + } + + [Fact] + public async Task CommittingMultipleEventsIncrementsVersionForEachEvent() + { + Assert.Empty(await _dbContext.Events.ToListAsync()); + + var store = _fixture.BuildEventStore(_dbContext); + var events = new object[] { new FooEvent { Foo = "Foo" }, new BarEvent { Bar = "Bar" } }; + await store.Append(Guid.NewGuid(), 0, events); + + var storedEvents = await _dbContext.Events.OrderBy(e => e.Timestamp).ToListAsync(); + Assert.Collection(storedEvents, + foo => Assert.Equal(1, foo.Version), + bar => Assert.Equal(2, bar.Version)); + } + } +} diff --git a/src/RdbmsEventStore.EFCore.Tests/ExtensibilityTests/NonDefaultImplementationsTests.cs b/src/RdbmsEventStore.EFCore.Tests/ExtensibilityTests/NonDefaultImplementationsTests.cs new file mode 100644 index 0000000..b16cd76 --- /dev/null +++ b/src/RdbmsEventStore.EFCore.Tests/ExtensibilityTests/NonDefaultImplementationsTests.cs @@ -0,0 +1,99 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using RdbmsEventStore.EFCore.Tests.Infrastructure; +using RdbmsEventStore.EFCore.Tests.TestData; +using RdbmsEventStore.Serialization; +using Xunit; + +namespace RdbmsEventStore.EFCore.Tests.ExtensibilityTests +{ + public class NonDefaultEvent : IMutableEvent + { + public DateTimeOffset Timestamp { get; set; } + public long StreamId { get; set; } + public long Version { get; set; } + public Type Type { get; set; } + public object Payload { get; set; } + } + + public class NonDefaultPersistedEvent : IPersistedEvent + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long EventId { get; set; } + [Required] + public long StreamId { get; set; } + [Required] + public DateTimeOffset Timestamp { get; set; } + [Required] + public long Version { get; set; } + [Required] + public string Type { get; set; } + [Required] + public byte[] Payload { get; set; } + } + + public class NonDefaultContext : DbContext, IEFCoreEventStoreContext + { + public NonDefaultContext(DbContextOptions options) : base(options) + { + } + + public DbSet Events { get; set; } + } + + public class NonDefaultImplementationsTests : IClassFixture, NonDefaultPersistedEvent>>, IDisposable + { + private readonly EventStoreFixture, NonDefaultPersistedEvent> _fixture; + private readonly NonDefaultContext _dbContext; + + public NonDefaultImplementationsTests(EventStoreFixture, NonDefaultPersistedEvent> fixture) + { + _fixture = fixture; + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + + _dbContext = new NonDefaultContext(options); + } + + [Fact] + public async Task CanCommitEventsToStoreWithDefaultImplementations() + { + var store = _fixture.BuildEventStore(_dbContext); + + await store.Append(1, 0, new[] { new FooEvent { Foo = "Bar" } }); + } + + [Fact] + public async Task CanReadEventsFromStoreWithNonDefaultImplementations() + { + _dbContext.Events.AddRange(new[] + { + new NonDefaultPersistedEvent + { + StreamId = 1, + Timestamp = DateTimeOffset.UtcNow, + Version = 1, + Type = "FooEvent", + Payload = Encoding.UTF8.GetBytes(@"{""Foo"":""Bar""}") + } + }); + await _dbContext.SaveChangesAsync(); + + var store = _fixture.BuildEventStore(_dbContext) as IEventStore>; + + var events = await store.Events(1); + + Assert.Single(events); + } + public void Dispose() + { + _dbContext?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/RdbmsEventStore.EFCore.Tests/Infrastructure/EventStoreFixture.cs b/src/RdbmsEventStore.EFCore.Tests/Infrastructure/EventStoreFixture.cs new file mode 100644 index 0000000..0c995f2 --- /dev/null +++ b/src/RdbmsEventStore.EFCore.Tests/Infrastructure/EventStoreFixture.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore; +using RdbmsEventStore.EFCore.Tests.EventStoreTests; +using RdbmsEventStore.EFCore.Tests.TestData; +using RdbmsEventStore.EventRegistry; +using RdbmsEventStore.Serialization; + +namespace RdbmsEventStore.EFCore.Tests.Infrastructure +{ + public class EventStoreFixture + where TId : IEquatable + where TStreamId : IEquatable + where TEvent : class, TEventMetadata, IMutableEvent, new() + where TPersistedEvent : class, TEventMetadata, IPersistedEvent, new() + where TEventMetadata : class, IEventMetadata + { + public EventStoreFixture() + { + EventRegistry = new AssemblyEventRegistry(typeof(TEvent), type => type.Name, type => !type.Name.StartsWith("<>")); + EventSerializer = new DefaultEventSerializer(EventRegistry); + EventFactory = new DefaultEventFactory(); + WriteLock = new WriteLock(); + } + + public IEventRegistry EventRegistry { get; protected set; } + public IEventSerializer EventSerializer { get; protected set; } + public IEventFactory EventFactory { get; protected set; } + public IWriteLock WriteLock { get; protected set; } + + public EFCoreEventStore BuildEventStore(TEventStoreContext dbContext) + where TEventStoreContext : DbContext, IEFCoreEventStoreContext + => new EFCoreEventStore(dbContext, EventFactory, WriteLock, EventSerializer); + } + + public class ExtraMetaEventFactoryFixture : EventStoreFixture + { + public ExtraMetaEventFactoryFixture() + { + EventFactory = new ExtraMetaEventFactory(); + EventSerializer = new ExtraMetaEventSerializer(EventRegistry); + } + } +} \ No newline at end of file diff --git a/src/RdbmsEventStore.EFCore.Tests/Infrastructure/EventStoreTestBase.cs b/src/RdbmsEventStore.EFCore.Tests/Infrastructure/EventStoreTestBase.cs new file mode 100644 index 0000000..efea31f --- /dev/null +++ b/src/RdbmsEventStore.EFCore.Tests/Infrastructure/EventStoreTestBase.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.EntityFrameworkCore; +using RdbmsEventStore.Serialization; +using Xunit; + +namespace RdbmsEventStore.EFCore.Tests.Infrastructure +{ + public class EventStoreTestBase : IClassFixture>, IDisposable + where TId : IEquatable + where TStreamId : IEquatable + where TEventMetadata : class, IEventMetadata + where TEvent : class, TEventMetadata, IMutableEvent, new() + where TPersistedEvent : class, TEventMetadata, IPersistedEvent, new() + { + protected readonly EventStoreFixture _fixture; + protected readonly EFCoreEventStoreContext _dbContext; + + public EventStoreTestBase(EventStoreFixture fixture) + { + _fixture = fixture; + var options = new DbContextOptionsBuilder>() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + _dbContext = new EFCoreEventStoreContext(options); + } + + public void Dispose() + { + _dbContext?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/RdbmsEventStore.EFCore.Tests/RdbmsEventStore.EFCore.Tests.csproj b/src/RdbmsEventStore.EFCore.Tests/RdbmsEventStore.EFCore.Tests.csproj new file mode 100644 index 0000000..745a23a --- /dev/null +++ b/src/RdbmsEventStore.EFCore.Tests/RdbmsEventStore.EFCore.Tests.csproj @@ -0,0 +1,23 @@ + + + + netcoreapp2.0 + + false + + + + + + + + + + + + + + + + + diff --git a/src/RdbmsEventStore.EFCore.Tests/TestData/EventTypes.cs b/src/RdbmsEventStore.EFCore.Tests/TestData/EventTypes.cs new file mode 100644 index 0000000..eaca107 --- /dev/null +++ b/src/RdbmsEventStore.EFCore.Tests/TestData/EventTypes.cs @@ -0,0 +1,36 @@ +using System; + +namespace RdbmsEventStore.EFCore.Tests.TestData +{ + public class GuidEvent : Event { } + public class GuidGuidPersistedEvent : EFCoreEvent { } + public class StringEvent : Event { } + public class LongStringPersistedEvent : EFCoreEvent { } + public class LongEvent : Event { } + public class LongLongPersistedEvent : EFCoreEvent { } + + public interface IExtraMeta : IEventMetadata + { + string ExtraMeta { get; set; } + } + + public class ExtraMetaStringEvent : Event, IExtraMeta + { + public string ExtraMeta { get; set; } + } + + public class ExtraMetaLongStringPersistedEventMetadata : EFCoreEvent, IExtraMeta + { + public string ExtraMeta { get; set; } + } + + public class FooEvent + { + public string Foo { get; set; } + } + + public class BarEvent + { + public string Bar { get; set; } + } +} \ No newline at end of file diff --git a/src/RdbmsEventStore.EFCore/EFCoreEvent.cs b/src/RdbmsEventStore.EFCore/EFCoreEvent.cs new file mode 100644 index 0000000..bae3e4f --- /dev/null +++ b/src/RdbmsEventStore.EFCore/EFCoreEvent.cs @@ -0,0 +1,29 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using RdbmsEventStore.Serialization; + +namespace RdbmsEventStore.EFCore +{ + public class EFCoreEvent : IPersistedEvent + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public TId EventId { get; set; } + + [Required] + public DateTimeOffset Timestamp { get; set; } + + [Required] + public TStreamId StreamId { get; set; } + + [Required] + public long Version { get; set; } + + [Required] + public string Type { get; set; } + + [Required] + public byte[] Payload { get; set; } + } +} \ No newline at end of file diff --git a/src/RdbmsEventStore.EFCore/EFCoreEventStore.cs b/src/RdbmsEventStore.EFCore/EFCoreEventStore.cs new file mode 100644 index 0000000..d38912f --- /dev/null +++ b/src/RdbmsEventStore.EFCore/EFCoreEventStore.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using RdbmsEventStore.Serialization; + +namespace RdbmsEventStore.EFCore +{ + public class EFCoreEventStore + : IEventStore + where TId : IEquatable + where TStreamId : IEquatable + where TEventStoreContext : DbContext, IEFCoreEventStoreContext + where TEvent : class, TEventMetadata, IMutableEvent, new() + where TPersistedEvent : class, IPersistedEvent, TEventMetadata, new() + where TEventMetadata : class, IEventMetadata + { + private readonly TEventStoreContext _dbContext; + private readonly IEventFactory _eventFactory; + private readonly IWriteLock _writeLock; + private readonly IEventSerializer _eventSerializer; + + public EFCoreEventStore(TEventStoreContext dbContext, IEventFactory eventFactory, IWriteLock writeLock, IEventSerializer eventSerializer) + { + _dbContext = dbContext; + _eventFactory = eventFactory; + _writeLock = writeLock; + _eventSerializer = eventSerializer; + } + + public async Task> Events(Func, IQueryable> query) + { + var storedEvents = await _dbContext.Events + .AsNoTracking() + .Apply(query) + .OrderBy(e => e.Timestamp) + .ToListAsync(); + + var events = storedEvents + .Cast() + .Select(_eventSerializer.Deserialize); + + return events; + } + + public async Task Append(TStreamId streamId, long versionBefore, IEnumerable payloads) + { + if (!payloads.Any()) + { + return; + } + + using (await _writeLock.Aquire(streamId)) + { + var highestVersionNumber = await _dbContext.Events + .Where(e => e.StreamId.Equals(streamId)) + .Select(e => e.Version) + .DefaultIfEmpty(0) + .MaxAsync(); + + if (highestVersionNumber != versionBefore) + { + throw new ConflictException(streamId, versionBefore, highestVersionNumber, payloads); + } + + var events = _eventFactory.Create(streamId, versionBefore, payloads).Select(_eventSerializer.Serialize); + _dbContext.Events.AddRange(events); + await _dbContext.SaveChangesAsync(); + } + } + } +} diff --git a/src/RdbmsEventStore.EFCore/EFCoreEventStoreContext.cs b/src/RdbmsEventStore.EFCore/EFCoreEventStoreContext.cs new file mode 100644 index 0000000..1bffd65 --- /dev/null +++ b/src/RdbmsEventStore.EFCore/EFCoreEventStoreContext.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; +using RdbmsEventStore.Serialization; + +namespace RdbmsEventStore.EFCore +{ + public class EFCoreEventStoreContext : DbContext, IEFCoreEventStoreContext where TEvent : class, IPersistedEvent + { + public EFCoreEventStoreContext() + { + } + + public EFCoreEventStoreContext(DbContextOptions dbContextOptions) : base(dbContextOptions) + { + } + + public DbSet Events { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity() + .HasIndex(e => e.StreamId); + } + } +} \ No newline at end of file diff --git a/src/RdbmsEventStore.EFCore/IEFCoreEventStoreContext.cs b/src/RdbmsEventStore.EFCore/IEFCoreEventStoreContext.cs new file mode 100644 index 0000000..1c7d2d1 --- /dev/null +++ b/src/RdbmsEventStore.EFCore/IEFCoreEventStoreContext.cs @@ -0,0 +1,9 @@ +using Microsoft.EntityFrameworkCore; + +namespace RdbmsEventStore.EFCore +{ + public interface IEFCoreEventStoreContext where TEvent : class + { + DbSet Events { get; } + } +} \ No newline at end of file diff --git a/src/RdbmsEventStore.EFCore/RdbmsEventStore.EFCore.csproj b/src/RdbmsEventStore.EFCore/RdbmsEventStore.EFCore.csproj new file mode 100644 index 0000000..46a758d --- /dev/null +++ b/src/RdbmsEventStore.EFCore/RdbmsEventStore.EFCore.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp2.0 + + + + + + + + + + + diff --git a/src/RdbmsEventStore.sln b/src/RdbmsEventStore.sln index 8d7dac3..dfaf8c9 100644 --- a/src/RdbmsEventStore.sln +++ b/src/RdbmsEventStore.sln @@ -1,7 +1,6 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27004.2005 +VisualStudioVersion = 15.0.27004.2010 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RdbmsEventStore", "RdbmsEventStore\RdbmsEventStore.csproj", "{4EBC13FC-FB6E-4B51-9EFD-A867434E8953}" EndProject @@ -25,6 +24,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RdbmsEventStore.Tests", "Rd EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RdbmsEventStore.EntityFramework.Tests", "RdbmsEventStore.EntityFramework.Tests\RdbmsEventStore.EntityFramework.Tests.csproj", "{0096C881-9347-454E-B0A0-73C7CABDBF37}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RdbmsEventStore.EFCore.Tests", "RdbmsEventStore.EFCore.Tests\RdbmsEventStore.EFCore.Tests.csproj", "{E0F240DB-17CB-4388-B9FA-FED3D0EC4499}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RdbmsEventStore.EFCore", "RdbmsEventStore.EFCore\RdbmsEventStore.EFCore.csproj", "{93044D18-12C9-4F0B-B509-0F8585465909}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -47,6 +50,14 @@ Global {0096C881-9347-454E-B0A0-73C7CABDBF37}.Debug|Any CPU.Build.0 = Debug|Any CPU {0096C881-9347-454E-B0A0-73C7CABDBF37}.Release|Any CPU.ActiveCfg = Release|Any CPU {0096C881-9347-454E-B0A0-73C7CABDBF37}.Release|Any CPU.Build.0 = Release|Any CPU + {E0F240DB-17CB-4388-B9FA-FED3D0EC4499}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0F240DB-17CB-4388-B9FA-FED3D0EC4499}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0F240DB-17CB-4388-B9FA-FED3D0EC4499}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0F240DB-17CB-4388-B9FA-FED3D0EC4499}.Release|Any CPU.Build.0 = Release|Any CPU + {93044D18-12C9-4F0B-B509-0F8585465909}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93044D18-12C9-4F0B-B509-0F8585465909}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93044D18-12C9-4F0B-B509-0F8585465909}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93044D18-12C9-4F0B-B509-0F8585465909}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -54,6 +65,7 @@ Global GlobalSection(NestedProjects) = preSolution {E58D4AE3-79B6-4B36-9693-9D852222D06E} = {B3BDC65F-EA8A-4CDC-85FC-0AA1C48C95A8} {0096C881-9347-454E-B0A0-73C7CABDBF37} = {B3BDC65F-EA8A-4CDC-85FC-0AA1C48C95A8} + {E0F240DB-17CB-4388-B9FA-FED3D0EC4499} = {B3BDC65F-EA8A-4CDC-85FC-0AA1C48C95A8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CA4860B3-3208-41C0-8FB9-719E00FBAD0D}