DCB 4 - Vertical Slices¶
This example is another attempt at the “course subscriptions” challenge. This time we show another variation of the higher-level, more refactored style that we used in the previous example.
This implementation introduces the notion “slice” from the “vertical slices” style advocated by the event modelling community.
Each slice defines a tight consistency boundary that is specific to its use case. Events for a slice are selected according to its consistency boundary, and projected into a “current state” for a slice according to its own projector function. This produces a “current state” which is used sometimes as a “decision model” for slices that generate new events in application command methods, and in other cases to support returning values from application query methods.
Events¶
The “decision” events that the slices depend on are shown below. They all derive from the decision class.
class StudentJoinedCourse(Decision):
student_id: StudentID
course_id: CourseID
class StudentLeftCourse(Decision):
student_id: StudentID
course_id: CourseID
class StudentRegistered(Decision):
student_id: StudentID
name: str
max_courses: int
class StudentNameUpdated(Decision):
student_id: StudentID
name: str
class StudentMaxCoursesUpdated(Decision):
student_id: StudentID
max_courses: int
class CourseRegistered(Decision):
course_id: CourseID
name: str
places: int
class CourseNameUpdated(Decision):
course_id: CourseID
name: str
class CoursePlacesUpdated(Decision):
course_id: CourseID
places: int
Slices¶
Each slice shown below derives from the base class Slice.
The slices shown below are entirely independent of each other. They depend only on the “decision” event classes that are relevant to their use case. The big advantage of this style is that because slices define tight consistency boundaries according to their requirements, they will select only the events they actually require. And it naturally arises that there will be much less accidental overlap in the consistency boundaries between use cases. This means, in theory, there will be less contention between concurrent operations involving different use cases with the same tags. But more importantly, the decision models and the consistency boundaries are well-matched to each other and to the use case. That’s why this is a great style.
class RegisterStudent(Slice[Decision]):
def __init__(self, name: str, max_courses: int):
self.student_id = StudentID(f"student-{uuid4()}")
self.name = name
self.max_courses = max_courses
def consistency_boundary(self) -> Selector:
return Selector(types=[StudentRegistered], tags=[self.student_id])
def execute(self) -> None:
self.trigger_event(
StudentRegistered,
tags=[self.student_id],
student_id=self.student_id,
name=self.name,
max_courses=self.max_courses,
)
class UpdateStudentName(Slice[Decision]):
def __init__(self, student_id: StudentID, name: str) -> None:
self.id = student_id
self.name = name
self.student_was_registered: bool = False
def consistency_boundary(self) -> Selector:
return Selector(types=[StudentRegistered, StudentNameUpdated], tags=[self.id])
@event(StudentRegistered)
def _(self) -> None:
self.student_was_registered = True
def execute(self) -> None:
assert self.student_was_registered
self.trigger_event(
StudentNameUpdated,
tags=[self.id],
student_id=self.id,
name=self.name,
)
class UpdateMaxCourses(Slice[Decision]):
def __init__(self, student_id: StudentID, max_courses: int) -> None:
self.student_was_registered: bool = False
self.id = student_id
self.max_courses = max_courses
def consistency_boundary(self) -> Selector:
return Selector(
types=[StudentRegistered, StudentMaxCoursesUpdated],
tags=[self.id],
)
@event(StudentRegistered)
def _(self) -> None:
self.student_was_registered = True
def execute(self) -> None:
assert self.student_was_registered
self.trigger_event(
StudentMaxCoursesUpdated,
tags=[self.id],
student_id=self.id,
max_courses=self.max_courses,
)
class RegisterCourse(Slice[Decision]):
def __init__(self, name: str, places: int):
self.course_id = CourseID(f"course-{uuid4()}")
self.name = name
self.places = places
def consistency_boundary(self) -> Selector:
return Selector(types=[CourseRegistered], tags=[self.course_id])
def execute(self) -> None:
self.trigger_event(
CourseRegistered,
tags=[self.course_id],
course_id=self.course_id,
name=self.name,
places=self.places,
)
class UpdateCourseName(Slice[Decision]):
def __init__(self, course_id: CourseID, name: str) -> None:
self.id = course_id
self.name = name
self.course_was_registered: bool = False
def consistency_boundary(self) -> Selector:
return Selector(types=[CourseRegistered, CourseNameUpdated], tags=[self.id])
@event(CourseRegistered)
def _(self) -> None:
self.course_was_registered = True
def execute(self) -> None:
assert self.course_was_registered
self.trigger_event(
CourseNameUpdated,
tags=[self.id],
course_id=self.id,
name=self.name,
)
class UpdatePlaces(Slice[Decision]):
def __init__(self, course_id: CourseID, places: int) -> None:
self.id = course_id
self.places = places
self.course_was_registered: bool = False
def consistency_boundary(self) -> Selector:
return Selector(types=[CourseRegistered, CoursePlacesUpdated], tags=[self.id])
@event(CourseRegistered)
def _(self) -> None:
self.course_was_registered = True
def execute(self) -> None:
assert self.course_was_registered
self.trigger_event(
CoursePlacesUpdated,
tags=[self.id],
course_id=self.id,
places=self.places,
)
class StudentJoinsCourse(Slice[Decision]):
def __init__(self, student_id: StudentID, course_id: CourseID) -> None:
self.student_id = student_id
self.course_id = course_id
self.course_was_registered = False
self.student_was_registered = False
self.student_max_courses = 0
self.course_places = 0
self.students_on_course: list[StudentID] = []
self.courses_for_student: list[CourseID] = []
def consistency_boundary(self) -> list[Selector]:
return [
Selector(
types=[
StudentRegistered,
StudentMaxCoursesUpdated,
StudentJoinedCourse,
StudentLeftCourse,
],
tags=[self.student_id],
),
Selector(
types=[
CourseRegistered,
CoursePlacesUpdated,
StudentJoinedCourse,
StudentLeftCourse,
],
tags=[self.course_id],
),
]
@event(StudentRegistered)
def _(self, max_courses: int) -> None:
self.student_was_registered = True
self.student_max_courses = max_courses
@event(CourseRegistered)
def _(self, places: int) -> None:
self.course_was_registered = True
self.course_places = places
@event(StudentJoinedCourse)
def _(self, student_id: StudentID, course_id: CourseID) -> None:
if student_id == self.student_id:
self.courses_for_student.append(course_id)
if course_id == self.course_id:
self.students_on_course.append(student_id)
@event(StudentLeftCourse)
def _(self, student_id: StudentID, course_id: CourseID) -> None:
if student_id == self.student_id:
self.courses_for_student.remove(course_id)
if course_id == self.course_id:
self.students_on_course.remove(student_id)
@event(StudentMaxCoursesUpdated)
def _(self, max_courses: int) -> None:
self.student_max_courses = max_courses
@event(CoursePlacesUpdated)
def _(self, places: int) -> None:
self.course_places = places
def execute(self) -> None:
if not self.course_was_registered:
raise CourseNotFoundError(self.course_id)
if not self.student_was_registered:
raise StudentNotFoundError(self.student_id)
if len(self.students_on_course) >= self.course_places:
raise FullyBookedError(self.course_id)
if len(self.courses_for_student) >= self.student_max_courses:
raise TooManyCoursesError(self.student_id)
if self.student_id in self.students_on_course:
raise AlreadyJoinedError((self.student_id, self.course_id))
self.trigger_event(
StudentJoinedCourse,
tags=[self.student_id, self.course_id],
student_id=self.student_id,
course_id=self.course_id,
)
class StudentLeavesCourse(Slice[Decision]):
def __init__(self, student_id: StudentID, course_id: CourseID) -> None:
self.student_id = student_id
self.course_id = course_id
self.course_was_registered = False
self.student_was_registered = False
self.students_on_course: list[StudentID] = []
self.courses_for_student: list[CourseID] = []
def consistency_boundary(self) -> list[Selector]:
return [
Selector(
types=[StudentRegistered, StudentJoinedCourse, StudentLeftCourse],
tags=[self.student_id],
),
Selector(
types=[CourseRegistered, StudentJoinedCourse, StudentLeftCourse],
tags=[self.course_id],
),
]
@event(StudentRegistered)
def _(self) -> None:
self.student_was_registered = True
@event(CourseRegistered)
def _(self) -> None:
self.course_was_registered = True
@event(StudentJoinedCourse)
def _(self, student_id: StudentID, course_id: CourseID) -> None:
if student_id == self.student_id:
self.courses_for_student.append(course_id)
if course_id == self.course_id:
self.students_on_course.append(student_id)
@event(StudentLeftCourse)
def _(self, student_id: StudentID, course_id: CourseID) -> None:
if student_id == self.student_id:
self.courses_for_student.remove(course_id)
if course_id == self.course_id:
self.students_on_course.remove(student_id)
def execute(self) -> None:
if not self.course_was_registered:
raise CourseNotFoundError
if not self.student_was_registered:
raise StudentNotFoundError
if self.student_id not in self.students_on_course:
raise NotAlreadyJoinedError
self.trigger_event(
StudentLeftCourse,
tags=[self.student_id, self.course_id],
student_id=self.student_id,
course_id=self.course_id,
)
class StudentsIDs(Slice[Decision]):
def __init__(self, course_id: CourseID) -> None:
self.course_id = course_id
self.student_ids: list[StudentID] = []
def consistency_boundary(self) -> Selector:
return Selector(types=type(self).projected_types, tags=[self.course_id])
@event(StudentJoinedCourse)
def _(self, student_id: StudentID) -> None:
self.student_ids.append(student_id)
@event(StudentLeftCourse)
def _(self, student_id: StudentID) -> None:
self.student_ids.remove(student_id)
class StudentNames(Slice[Decision]):
def __init__(self, student_ids: list[StudentID]) -> None:
self.student_id_names: dict[StudentID, str | None] = dict.fromkeys(
student_ids, None
)
def consistency_boundary(self) -> list[Selector]:
return [
Selector(types=type(self).projected_types, tags=[student_id])
for student_id in self.student_id_names
]
@event(StudentRegistered)
def _(self, student_id: StudentID, name: str) -> None:
self.student_id_names[student_id] = name
@event(StudentNameUpdated)
def _(self, student_id: StudentID, name: str) -> None:
self.student_id_names[student_id] = name
@property
def names(self) -> list[str]:
return [n for n in self.student_id_names.values() if n]
class CourseIDs(Slice[Decision]):
def __init__(self, student_id: StudentID) -> None:
self.student_id = student_id
self.course_ids: list[CourseID] = []
def consistency_boundary(self) -> Selector:
return Selector(types=type(self).projected_types, tags=[self.student_id])
@event(StudentJoinedCourse)
def _(self, course_id: CourseID) -> None:
self.course_ids.append(course_id)
@event(StudentLeftCourse)
def _(self, course_id: CourseID) -> None:
self.course_ids.remove(course_id)
class CourseNames(Slice[Decision]):
def __init__(self, course_ids: list[CourseID]) -> None:
self.course_id_names: dict[CourseID, str | None] = dict.fromkeys(
course_ids, None
)
def consistency_boundary(self) -> list[Selector]:
return [
Selector(types=type(self).projected_types, tags=[student_id])
for student_id in self.course_id_names
]
@event(CourseRegistered)
def _(self, course_id: CourseID, name: str) -> None:
self.course_id_names[course_id] = name
@event(CourseNameUpdated)
def _(self, course_id: CourseID, name: str) -> None:
self.course_id_names[course_id] = name
@property
def names(self) -> list[str]:
return [n for n in self.course_id_names.values() if n]
class Student(Slice[Decision]):
def __init__(self, student_id: StudentID) -> None:
self.student_was_registered: bool = False
self.id = student_id
self.name: str = ""
self.max_courses: int = 0
self.course_ids: list[CourseID] = []
def consistency_boundary(self) -> Selector:
return Selector(tags=[self.id])
@event(StudentRegistered)
def _(self, name: str, max_courses: int) -> None:
self.student_was_registered = True
self.name = name
self.max_courses = max_courses
@event(StudentNameUpdated)
def _(self, name: str) -> None:
self.name = name
@event(StudentMaxCoursesUpdated)
def _(self, max_courses: int) -> None:
self.max_courses = max_courses
@event(StudentJoinedCourse)
def _(self, course_id: CourseID) -> None:
self.course_ids.append(course_id)
@event(StudentLeftCourse)
def _(self, course_id: CourseID) -> None:
self.course_ids.remove(course_id)
class Course(Slice[Decision]):
def __init__(self, course_id: CourseID) -> None:
self.course_was_registered: bool = False
self.id = course_id
self.name: str = ""
self.places = 0
self.student_ids: list[StudentID] = []
def consistency_boundary(self) -> Selector:
return Selector(tags=[self.id])
@event(CourseRegistered)
def _(self, name: str, places: int) -> None:
self.course_was_registered = True
self.name = name
self.places = places
@event(CourseNameUpdated)
def _(self, name: str) -> None:
self.name = name
@event(CoursePlacesUpdated)
def _(self, places: int) -> None:
self.places = places
@event(StudentJoinedCourse)
def _(self, student_id: StudentID) -> None:
self.student_ids.append(student_id)
@event(StudentLeftCourse)
def _(self, student_id: StudentID) -> None:
self.student_ids.remove(student_id)
Application¶
The nice thing about this implementation is that the application command methods have all become straightforward one-liners that simply allow the domain model defined by the slices to enjoy the persistence infrastructure provided by the application. This happens naturally by pushing into the slices all responsibilities for selecting events, for projecting events into a decision model, and for generating new events, keeping their differences encapsulated behind a standard interface.
class EnrolmentWithVerticalSlices(DCBApplication, EnrolmentInterface):
env: Mapping[str, str] = {
"MAPPER_TOPIC": get_topic(MessagePackMapper),
**DCBApplication.env,
}
def register_student(self, name: str, max_courses: int) -> StudentID:
return self.do(RegisterStudent(name, max_courses)).student_id
def register_course(self, name: str, places: int) -> CourseID:
return self.do(RegisterCourse(name, places)).course_id
def join_course(self, student_id: StudentID, course_id: CourseID) -> None:
self.do(StudentJoinsCourse(student_id, course_id))
def leave_course(self, student_id: StudentID, course_id: CourseID) -> None:
self.do(StudentLeavesCourse(student_id, course_id))
def list_students_for_course(self, course_id: CourseID) -> list[str]:
return self.do(StudentNames(self.do(StudentsIDs(course_id)).student_ids)).names
def list_courses_for_student(self, student_id: StudentID) -> list[str]:
return self.do(CourseNames(self.do(CourseIDs(student_id)).course_ids)).names
def update_student_name(self, student_id: StudentID, name: str) -> None:
self.do(UpdateStudentName(student_id, name))
def update_max_courses(self, student_id: StudentID, max_courses: int) -> None:
self.do(UpdateMaxCourses(student_id, max_courses))
def update_course_name(self, course_id: CourseID, name: str) -> None:
self.do(UpdateCourseName(course_id, name))
def update_places(self, course_id: CourseID, places: int) -> None:
self.do(UpdatePlaces(course_id, places))
def get_student(self, student_id: StudentID) -> Student:
return self.do(Student(student_id=student_id))
def get_course(self, course_id: CourseID) -> Course:
return self.do(Course(course_id=course_id))
Test case¶
The enrolment test case is extended to check
EnrolmentWithVerticalSlices.
It has some extra steps to cover the extra methods that we have implemented, such as a student leaving a course, changes of name, changes to the number of places on a course and the “max courses” for student. The extra steps also show, a little bit, the non-conflicting nature of different slices.
class TestEnrolmentWithVerticalSlices(EnrolmentTestCase):
def test_enrolment_in_memory(self) -> None:
self.assert_implementation(EnrolmentWithVerticalSlices())
def test_enrolment_with_postgres(self) -> None:
env = {
"PERSISTENCE_MODULE": "eventsourcing.dcb.postgres_tt",
"POSTGRES_DBNAME": "eventsourcing",
"POSTGRES_HOST": "127.0.0.1",
"POSTGRES_PORT": "5432",
"POSTGRES_USER": "eventsourcing",
"POSTGRES_PASSWORD": "eventsourcing",
}
try:
self.assert_implementation(EnrolmentWithVerticalSlices(env))
finally:
drop_tables()
def test_enrolment_with_umadb(self) -> None:
env = {
"PERSISTENCE_MODULE": "eventsourcing_umadb",
"UMADB_URI": "http://127.0.0.1:50051",
}
self.assert_implementation(EnrolmentWithVerticalSlices(env))
def assert_implementation(self, app: EnrolmentInterface) -> None:
super().assert_implementation(app)
assert isinstance(app, EnrolmentWithVerticalSlices)
# Register student.
student_id = app.register_student(name="Max", max_courses=4)
# Update name.
app.update_student_name(student_id, "Maxine")
student = app.get_student(student_id)
self.assertEqual("Maxine", student.name)
# Register course.
course_id = app.register_course(name="Bio", places=3)
# Update name.
app.update_course_name(course_id, "Biology")
course = app.get_course(course_id)
self.assertEqual("Biology", course.name)
# Join course.
app.join_course(student_id=student_id, course_id=course_id)
student = app.get_student(student_id)
course = app.get_course(course_id)
self.assertEqual([course_id], student.course_ids)
self.assertEqual([student_id], course.student_ids)
# List students for course.
names = app.list_students_for_course(course_id)
self.assertEqual(["Maxine"], names)
# List courses for student.
names = app.list_courses_for_student(student_id)
self.assertEqual(["Biology"], names)
# Leave course.
app.leave_course(student_id=student_id, course_id=course_id)
student = app.get_student(student_id)
course = app.get_course(course_id)
self.assertEqual([], student.course_ids)
self.assertEqual([], course.student_ids)
# Update max_courses for student.
app.update_max_courses(student_id, 0)
student = app.get_student(student_id)
self.assertEqual(0, student.max_courses)
# Update places for course.
app.update_places(course_id, 0)
course = app.get_course(course_id)
self.assertEqual(0, course.places)
# Check leaves course, updated max_courses, and places
# events are effective when joining course.
with self.assertRaises(FullyBookedError):
app.join_course(student_id=student_id, course_id=course_id)
# Increase places.
app.update_places(course_id, 1)
with self.assertRaises(TooManyCoursesError):
app.join_course(student_id=student_id, course_id=course_id)
# Increase max_courses.
app.update_max_courses(student_id, 1)
# Student can now rejoin course.
app.join_course(student_id=student_id, course_id=course_id)
# Check leaving a course doesn't conflict with concurrent name changes.
leave = StudentLeavesCourse(student_id, course_id)
app.repository.advance(leave)
leave.execute()
app.update_student_name(student_id, "Mollie")
app.update_course_name(course_id, "Bio-science")
app.repository.save(leave)
# Check leaving a course doesn't conflict with concurrent name changes.
join = StudentJoinsCourse(student_id, course_id)
app.repository.advance(join)
join.execute()
app.update_student_name(student_id, "Millie")
app.update_course_name(course_id, "Biological-science")
app.repository.save(join)
# Check leaving doesn't conflict with updating max_courses and places.
leave = StudentLeavesCourse(student_id, course_id)
app.repository.advance(leave)
leave.execute()
app.update_max_courses(student_id, 31)
app.update_places(course_id, 28)
app.repository.save(leave)
# Check joining does conflict with updating max_courses and places.
join = StudentJoinsCourse(student_id, course_id)
app.repository.advance(join)
join.execute()
app.update_max_courses(student_id, 39)
app.update_places(course_id, 43)
with self.assertRaises(IntegrityError):
app.repository.save(join)
# Check updating max_courses doesn't conflict with updating name.
rename = UpdateStudentName(student_id, "Maddy")
app.repository.advance(rename)
rename.execute()
app.update_max_courses(student_id, 101)
app.repository.save(rename)
max_courses = UpdateMaxCourses(student_id, 50)
app.repository.advance(max_courses)
max_courses.execute()
app.update_student_name(student_id, "Mandy")
app.repository.save(max_courses)
student = app.get_student(student_id)
self.assertEqual("Mandy", student.name)
self.assertEqual(50, student.max_courses)
self.assertEqual([], student.course_ids)
# Can't call non-command underscore methods.
with self.assertRaisesRegex(ProgrammingError, "cannot be used"):
student._() # type: ignore[call-arg]
Discussion¶
It must be said, that this is great style. We think “slices” and “dynamic consistency boundaries” really go very well together, and we hope you agree.
To be fair, as we saw with the shopping cart example, “vertical slices” make the code altogether slightly repetitive, relatively verbose, and relatively complicated. Whilst some may enjoy the absolute separation between use cases, others may find “vertical slices” harder to reason about than event-sourced aggregates and the “enduring objects” that were discussed in the previous example. Certainly, unlike the event-sourced aggregates, these slices do not all fit on one screen. The “decision” events that naturally fall into sequences under one tag are not coherently encapsulated in a single entity, and there are many different consistency boundaries. For these reasons, there will tend to be a greater need for careful thought, and longer tests, without which this style seems to invite a greater likelihood of unseen programming errors.
When considering contention, we must remember that compared with, for example, event-sourced aggregates which use a simpler and therefore faster persistence model, DCB read and append operations are inherently more complex and therefore slower. As a result, although with “vertical slices” we can more easily code for tighter consistency boundaries, due to the slower operations of DCB, there will, in theory, be more risk of roughly contemporary operations actually happening concurrently, causing contention where contention would not have occurred. But of course, there will be no conflict between operations involving sequences for different continuity IDs, in DCB, with or without “slices”, and with or without event-sourced aggregates, and so this is a relatively marginal consideration in most cases.
Whatever the relative merits, above all, we value having and supporting different styles and persistence models for event sourcing. And DCB with “vertical slices” is indeed a very commendable style.
Code reference¶
- class examples.dcb_enrolment_with_vertical_slices.application.StudentJoinedCourse(student_id: StudentID, course_id: CourseID)[source]¶
Bases:
Decision- student_id: StudentID¶
- course_id: CourseID¶
- class examples.dcb_enrolment_with_vertical_slices.application.StudentLeftCourse(student_id: StudentID, course_id: CourseID)[source]¶
Bases:
Decision- student_id: StudentID¶
- course_id: CourseID¶
- class examples.dcb_enrolment_with_vertical_slices.application.StudentRegistered(student_id: StudentID, name: str, max_courses: int)[source]¶
Bases:
Decision- student_id: StudentID¶
- name: str¶
- max_courses: int¶
- class examples.dcb_enrolment_with_vertical_slices.application.StudentNameUpdated(student_id: StudentID, name: str)[source]¶
Bases:
Decision- student_id: StudentID¶
- name: str¶
- class examples.dcb_enrolment_with_vertical_slices.application.StudentMaxCoursesUpdated(student_id: StudentID, max_courses: int)[source]¶
Bases:
Decision- student_id: StudentID¶
- max_courses: int¶
- class examples.dcb_enrolment_with_vertical_slices.application.CourseRegistered(course_id: CourseID, name: str, places: int)[source]¶
Bases:
Decision- course_id: CourseID¶
- name: str¶
- places: int¶
- class examples.dcb_enrolment_with_vertical_slices.application.CourseNameUpdated(course_id: CourseID, name: str)[source]¶
Bases:
Decision- course_id: CourseID¶
- name: str¶
- class examples.dcb_enrolment_with_vertical_slices.application.CoursePlacesUpdated(course_id: CourseID, places: int)[source]¶
Bases:
Decision- course_id: CourseID¶
- places: int¶
- class examples.dcb_enrolment_with_vertical_slices.application.RegisterStudent(*_: Any, **__: Any)[source]¶
-
- do_projection = False¶
- projected_types = []¶
- last_known_position: int | None¶
- class examples.dcb_enrolment_with_vertical_slices.application.UpdateStudentName(*_: Any, **__: Any)[source]¶
-
- do_projection = True¶
- projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.StudentRegistered'>]¶
- last_known_position: int | None¶
- class examples.dcb_enrolment_with_vertical_slices.application.UpdateMaxCourses(*_: Any, **__: Any)[source]¶
-
- do_projection = True¶
- projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.StudentRegistered'>]¶
- last_known_position: int | None¶
- class examples.dcb_enrolment_with_vertical_slices.application.RegisterCourse(*_: Any, **__: Any)[source]¶
-
- do_projection = False¶
- projected_types = []¶
- last_known_position: int | None¶
- class examples.dcb_enrolment_with_vertical_slices.application.UpdateCourseName(*_: Any, **__: Any)[source]¶
-
- do_projection = True¶
- projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.CourseRegistered'>]¶
- last_known_position: int | None¶
- class examples.dcb_enrolment_with_vertical_slices.application.UpdatePlaces(*_: Any, **__: Any)[source]¶
-
- do_projection = True¶
- projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.CourseRegistered'>]¶
- last_known_position: int | None¶
- class examples.dcb_enrolment_with_vertical_slices.application.StudentJoinsCourse(*_: Any, **__: Any)[source]¶
-
- do_projection = True¶
- projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.StudentRegistered'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.CourseRegistered'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentJoinedCourse'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentLeftCourse'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentMaxCoursesUpdated'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.CoursePlacesUpdated'>]¶
- last_known_position: int | None¶
- class examples.dcb_enrolment_with_vertical_slices.application.StudentLeavesCourse(*_: Any, **__: Any)[source]¶
-
- do_projection = True¶
- projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.StudentRegistered'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.CourseRegistered'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentJoinedCourse'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentLeftCourse'>]¶
- last_known_position: int | None¶
- class examples.dcb_enrolment_with_vertical_slices.application.StudentsIDs(*_: Any, **__: Any)[source]¶
-
- do_projection = True¶
- projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.StudentJoinedCourse'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentLeftCourse'>]¶
- last_known_position: int | None¶
- class examples.dcb_enrolment_with_vertical_slices.application.StudentNames(*_: Any, **__: Any)[source]¶
-
- property names: list[str]¶
- do_projection = True¶
- projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.StudentRegistered'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentNameUpdated'>]¶
- last_known_position: int | None¶
- class examples.dcb_enrolment_with_vertical_slices.application.CourseIDs(*_: Any, **__: Any)[source]¶
-
- do_projection = True¶
- projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.StudentJoinedCourse'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentLeftCourse'>]¶
- last_known_position: int | None¶
- class examples.dcb_enrolment_with_vertical_slices.application.CourseNames(*_: Any, **__: Any)[source]¶
-
- property names: list[str]¶
- do_projection = True¶
- projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.CourseRegistered'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.CourseNameUpdated'>]¶
- last_known_position: int | None¶
- class examples.dcb_enrolment_with_vertical_slices.application.Student(*_: Any, **__: Any)[source]¶
-
- do_projection = True¶
- projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.StudentRegistered'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentNameUpdated'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentMaxCoursesUpdated'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentJoinedCourse'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentLeftCourse'>]¶
- last_known_position: int | None¶
- class examples.dcb_enrolment_with_vertical_slices.application.Course(*_: Any, **__: Any)[source]¶
-
- do_projection = True¶
- projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.CourseRegistered'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.CourseNameUpdated'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.CoursePlacesUpdated'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentJoinedCourse'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentLeftCourse'>]¶
- last_known_position: int | None¶
- class examples.dcb_enrolment_with_vertical_slices.application.EnrolmentWithVerticalSlices(env: Mapping[str, str] | None = None)[source]¶
Bases:
DCBApplication,EnrolmentInterface- name = 'EnrolmentWithVerticalSlices'¶
- env: Mapping[str, str] = {'MAPPER_TOPIC': 'eventsourcing.dcb.msgpack:MessagePackMapper', 'PERSISTENCE_MODULE': 'eventsourcing.dcb.popo'}¶
- list_students_for_course(course_id: CourseID) list[str][source]¶
List students enrolled on a course.