Skip to content

Refreshable Details & Views

An application usually needs composed views of multiple objects (e.g., combining Student and Course data). In Sharpino, these composed read models are called Details.

Even though details benefit from single-object caching, caching the fully composed Detail view models themselves provides significant performance improvements.

Consider a Student aggregate:

type Student =
{
Id: StudentId
Name: string
Courses: List<CourseId>
MaxNumberOfCourses: int
}

To display student details along with their full course objects, we define a composite StudentDetails model:

type StudentDetails =
{
Student: Student
Courses: List<Course>
}

Without caching the composed view, retrieving it requires resolving the dependencies every single time:

let getStudentDetails studentId =
result {
let! student = this.GetStudent studentId
let! courses = this.GetCourses student.Courses
return { Student = student; Courses = courses }
}

To optimize this, we can cache the StudentDetails and automatically refresh it only when the student or any associated course is modified.


Every detail view cache candidate must implement the Refreshable<'T> interface:

type StudentDetails =
{
Student: Student
Courses: List<Course>
Refresher: unit -> Result<Student * List<Course>, string>
}
member this.Refresh () =
result {
let! student, courses = this.Refresher ()
return { this with Student = student; Courses = courses }
}
interface Refreshable<StudentDetails> with
member this.Refresh () =
this.Refresh ()

To enable reactive cache invalidations, you must associate the aggregate IDs with the details cache. This is registered using a DetailsCacheKey and the Memoize function:

member this.GetCourseDetails (id: CourseId) =
let detailsBuilder =
fun () ->
let refresher =
fun () ->
result {
let! course = this.GetCourse id
let! students = this.GetStudents course.Students
return course, students
}
result {
let! course, students = refresher ()
return
(
{
Course = course
Students = students
Refresher = refresher
} :> Refreshable<_>
,
// Specify the IDs that trigger a refresh when updated
id.Id :: (students |> List.map _.Id.Id)
)
}
let key = DetailsCacheKey (typeof<CourseDetails>, id.Id)
StateView.getRefreshableDetails<CourseDetails> detailsBuilder key

The detailsBuilder returns a tuple: the Refreshable instance and a list of Guids representing dependencies. The DetailsCache registers these associations. If any of those IDs are updated, the cache invalidates and triggers Refresh().