Skip to content

Commit

Permalink
Merge pull request #1 from erikshafer/student_aggregate
Browse files Browse the repository at this point in the history
Introduce Student Aggregate
  • Loading branch information
erikshafer authored May 21, 2024
2 parents 90573cd + 633633d commit 67ba88c
Show file tree
Hide file tree
Showing 16 changed files with 193 additions and 147 deletions.
25 changes: 0 additions & 25 deletions .dockerignore

This file was deleted.

1 change: 0 additions & 1 deletion StudentEnrollmentConsoleApp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
docker-compose.yml = docker-compose.yml
.gitignore = .gitignore
.dockerignore = .dockerignore
global.json = global.json
README.md = README.md
EndProjectSection
Expand Down
21 changes: 0 additions & 21 deletions StudentEnrollmentConsoleApp/Dockerfile

This file was deleted.

6 changes: 2 additions & 4 deletions StudentEnrollmentConsoleApp/Events/Event.cs
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!;
}
35 changes: 35 additions & 0 deletions StudentEnrollmentConsoleApp/Events/EventTypeMapper.cs
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;
});
}
6 changes: 2 additions & 4 deletions StudentEnrollmentConsoleApp/Events/StudentCreated.cs
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; }
}
7 changes: 7 additions & 0 deletions StudentEnrollmentConsoleApp/Events/StudentEmailChanged.cs
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; }
}
6 changes: 2 additions & 4 deletions StudentEnrollmentConsoleApp/Events/StudentEnrolled.cs
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; }
}
9 changes: 0 additions & 9 deletions StudentEnrollmentConsoleApp/Events/StudentUnEnrolled.cs

This file was deleted.

10 changes: 0 additions & 10 deletions StudentEnrollmentConsoleApp/Events/StudentUpdated.cs

This file was deleted.

7 changes: 7 additions & 0 deletions StudentEnrollmentConsoleApp/Events/StudentWithdrawn.cs
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; }
}
113 changes: 81 additions & 32 deletions StudentEnrollmentConsoleApp/Program.cs
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("");
57 changes: 57 additions & 0 deletions StudentEnrollmentConsoleApp/Student.cs
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);
}
}
20 changes: 0 additions & 20 deletions StudentEnrollmentConsoleApp/StudentDatabase.cs

This file was deleted.

Loading

0 comments on commit 67ba88c

Please sign in to comment.