-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from erikshafer/student_aggregate
Introduce Student Aggregate
- Loading branch information
Showing
16 changed files
with
193 additions
and
147 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
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
This file was deleted.
Oops, something went wrong.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,6 @@ | ||
namespace StudentEnrollmentConsoleApp.Events; | ||
|
||
public abstract class Event | ||
public abstract record Event | ||
{ | ||
public abstract string StreamId { get; } | ||
|
||
public DateTime CreatedAtUtc { get; set; } | ||
public string Id { get; init; } = default!; | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
using System.Collections.Concurrent; | ||
|
||
namespace StudentEnrollmentConsoleApp.Events; | ||
|
||
public class EventTypeMapper | ||
{ | ||
public static readonly EventTypeMapper Instance = new(); | ||
|
||
private readonly ConcurrentDictionary<string, Type?> _typeMap = new(); | ||
private readonly ConcurrentDictionary<Type, string> _typeNameMap = new(); | ||
|
||
public string ToName<TEventType>() => ToName(typeof(TEventType)); | ||
|
||
public string ToName(Type eventType) => _typeNameMap.GetOrAdd(eventType, _ => | ||
{ | ||
var eventTypeName = eventType.FullName!; | ||
_typeMap.TryAdd(eventTypeName, eventType); | ||
return eventTypeName; | ||
}); | ||
|
||
public Type? ToType(string eventTypeName) => _typeMap.GetOrAdd(eventTypeName, _ => | ||
{ | ||
var type = AppDomain.CurrentDomain | ||
.GetAssemblies() | ||
.SelectMany(a => a.GetTypes().Where(x => x.FullName == eventTypeName || x.Name == eventTypeName)) | ||
.FirstOrDefault(); | ||
|
||
if (type == null) | ||
return null; | ||
|
||
_typeNameMap.TryAdd(type, eventTypeName); | ||
|
||
return type; | ||
}); | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,9 @@ | ||
namespace StudentEnrollmentConsoleApp.Events; | ||
|
||
public class StudentCreated : Event | ||
public record StudentCreated : Event | ||
{ | ||
public required string StudentId { get; init; } | ||
public required string FullName { get; init; } | ||
public required string Email { get; init; } | ||
public required DateTime DateOfBirth { get; init; } | ||
|
||
public override string StreamId => StudentId; | ||
public DateTime CreatedAtUtc { get; init; } | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
namespace StudentEnrollmentConsoleApp.Events; | ||
|
||
public record StudentEmailChanged : Event | ||
{ | ||
public required string Email { get; init; } | ||
public DateTime ChangedAtUtc { get; init; } | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,7 @@ | ||
namespace StudentEnrollmentConsoleApp.Events; | ||
|
||
public class StudentEnrolled : Event | ||
public record StudentEnrolled : Event | ||
{ | ||
public required string StudentId { get; init; } | ||
public required string CourseName { get; init; } | ||
|
||
public override string StreamId => StudentId; | ||
public DateTime EnrolledAtUtc { get; init; } | ||
} |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
namespace StudentEnrollmentConsoleApp.Events; | ||
|
||
public record StudentWithdrawn : Event | ||
{ | ||
public required string CourseName { get; init; } | ||
public DateTime WithdrawnAtUtc { get; init; } | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,58 +1,107 @@ | ||
using System.Text; | ||
using System.Text.Json; | ||
using EventStore.Client; | ||
using StudentEnrollmentConsoleApp; | ||
using StudentEnrollmentConsoleApp.Events; | ||
|
||
// Register events to a singleton for ease-of-reference | ||
EventTypeMapper.Instance.ToName(typeof(StudentCreated)); | ||
EventTypeMapper.Instance.ToName(typeof(StudentEnrolled)); | ||
EventTypeMapper.Instance.ToName(typeof(StudentWithdrawn)); | ||
EventTypeMapper.Instance.ToName(typeof(StudentEmailChanged)); | ||
|
||
var id = Guid.Parse("a662d446-4920-415e-8c2a-0dd4a6c58908"); | ||
var studentId = $"student-{id}"; | ||
var now = DateTime.Now; | ||
var streamId = $"student-{id}"; | ||
|
||
var studentCreated = new StudentCreated | ||
{ | ||
StudentId = studentId, | ||
FullName = "Erik Shafer", | ||
Email = "[email protected]", | ||
DateOfBirth = new DateTime(1987, 1, 1) | ||
}; | ||
const string eventType = nameof(studentCreated); //"StudentCreated"; | ||
const string streamName = "some-stream"; | ||
var created = new EventData( | ||
Uuid.NewUuid(), | ||
"StudentCreated", | ||
JsonSerializer.SerializeToUtf8Bytes(new StudentCreated | ||
{ | ||
Id = streamId, | ||
FullName = "Erik Shafer", | ||
Email = "[email protected]", | ||
DateOfBirth = new DateTime(1987, 1, 1), | ||
CreatedAtUtc = DateTime.UtcNow | ||
}) | ||
); | ||
|
||
var inMemoryDb = new StudentDatabase(); | ||
inMemoryDb.Append(studentCreated); | ||
var enrolled = new EventData( | ||
Uuid.NewUuid(), | ||
"StudentEnrolled", | ||
JsonSerializer.SerializeToUtf8Bytes(new StudentEnrolled | ||
{ | ||
Id = streamId, | ||
CourseName = "From Zero to Hero: REST APis in .NET", | ||
EnrolledAtUtc = DateTime.UtcNow | ||
}) | ||
); | ||
|
||
var emailChanged = new EventData( | ||
Uuid.NewUuid(), | ||
"StudentEmailChanged", | ||
JsonSerializer.SerializeToUtf8Bytes(new StudentEmailChanged | ||
{ | ||
Id = streamId, | ||
Email = "[email protected]", | ||
ChangedAtUtc = DateTime.UtcNow | ||
}) | ||
); | ||
|
||
// Our EventStoreDB (ESDB) | ||
const string connectionString = "esdb://admin:changeit@localhost:2113?tls=false&tlsVerifyCert=false"; | ||
var settings = EventStoreClientSettings.Create(connectionString); | ||
var client = new EventStoreClient(settings); | ||
|
||
var eventData = new EventData( | ||
Uuid.NewUuid(), | ||
eventType, | ||
JsonSerializer.SerializeToUtf8Bytes(studentCreated) | ||
); | ||
|
||
// Append to ESDB | ||
await client.AppendToStreamAsync( | ||
streamName, | ||
streamId, | ||
StreamState.Any, | ||
new[] { eventData }, | ||
new[] { created, enrolled, emailChanged }, | ||
cancellationToken: default | ||
); | ||
|
||
var result = client.ReadStreamAsync( | ||
// Read from ESDB | ||
var readStreamResult = client.ReadStreamAsync( | ||
Direction.Forwards, | ||
streamName, | ||
streamId, | ||
StreamPosition.Start, | ||
cancellationToken: default | ||
); | ||
var eventStream = await readStreamResult.ToListAsync(); | ||
|
||
var events = await result.ToListAsync(); | ||
// Write out the events from the stream | ||
Console.WriteLine("Events from selected stream: "); | ||
foreach (var resolved in eventStream) | ||
{ | ||
Console.WriteLine($"\tEventId: {resolved.Event.EventId}"); | ||
Console.WriteLine($"\tEventStreamId: {resolved.Event.EventStreamId}"); | ||
Console.WriteLine($"\tEventType: {resolved.Event.EventType}"); | ||
Console.WriteLine($"\tData: {Encoding.UTF8.GetString(resolved.Event.Data.ToArray())}"); | ||
Console.WriteLine(""); | ||
} | ||
|
||
foreach (var @event in events) | ||
// Write out all the courses the student enrolled in | ||
var enrolledCourses = eventStream | ||
.Where(re => re.Event.EventType == "StudentEnrolled") | ||
.Select(re => JsonSerializer.Deserialize<StudentEnrolled>(re.Event.Data.ToArray())) | ||
.Select(se => se!.CourseName) | ||
.ToList(); | ||
Console.WriteLine("Courses enrolled in: "); | ||
enrolledCourses.ForEach(ec => Console.WriteLine($"\t- {ec}")); | ||
Console.WriteLine(""); | ||
|
||
// Write out using the mapper | ||
Console.WriteLine("Deserialized events:"); | ||
foreach (var resolved in eventStream) | ||
{ | ||
Console.WriteLine($"EventType: {@event.Event.EventId}"); | ||
Console.WriteLine($"EventStreamId: {@event.Event.EventStreamId}"); | ||
Console.WriteLine($"EventType: {@event.Event.EventType}"); | ||
var data = Encoding.UTF8.GetString(@event.Event.Data.ToArray()); | ||
Console.WriteLine($"Data: {data}"); | ||
Console.WriteLine("-----"); | ||
var eventType = EventTypeMapper.Instance.ToType(resolved.Event.EventType); | ||
|
||
if (eventType == null) | ||
break; | ||
|
||
var deserializedEvent = JsonSerializer.Deserialize(Encoding.UTF8.GetString(resolved.Event.Data.Span), eventType); | ||
|
||
Console.WriteLine($"\t{deserializedEvent}"); | ||
} | ||
|
||
Console.WriteLine(""); |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
using StudentEnrollmentConsoleApp.Events; | ||
|
||
namespace StudentEnrollmentConsoleApp; | ||
|
||
public class Student | ||
{ | ||
public string Id { get; set; } = default!; | ||
public string FullName { get; set; } = default!; | ||
public string Email { get; set; } = default!; | ||
public DateTime DateOfBirth { get; set; } | ||
public DateTime CreatedAtUtc { get; set; } | ||
public List<string> EnrolledCourses { get; set; } = []; | ||
|
||
public void Apply(Event @event) | ||
{ | ||
switch (@event) | ||
{ | ||
case StudentCreated created: | ||
Apply(created); | ||
break; | ||
case StudentEmailChanged emailChanged: | ||
Apply(emailChanged); | ||
break; | ||
case StudentEnrolled enrolled: | ||
Apply(enrolled); | ||
break; | ||
case StudentWithdrawn withdrawn: | ||
Apply(withdrawn); | ||
break; | ||
} | ||
} | ||
|
||
private void Apply(StudentCreated @event) | ||
{ | ||
Id = @event.Id; | ||
FullName = @event.FullName; | ||
Email = @event.Email; | ||
CreatedAtUtc = @event.CreatedAtUtc; | ||
} | ||
|
||
private void Apply(StudentEmailChanged @event) | ||
{ | ||
Email = @event.Email; | ||
} | ||
|
||
private void Apply(StudentEnrolled @event) | ||
{ | ||
if (EnrolledCourses.Contains(@event.CourseName) is false) | ||
EnrolledCourses.Add(@event.CourseName); | ||
} | ||
|
||
private void Apply(StudentWithdrawn @event) | ||
{ | ||
if (EnrolledCourses.Contains(@event.CourseName)) | ||
EnrolledCourses.Add(@event.CourseName); | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.