Lookup by name in tasks, build services, and extensions

Suppose I am currently developing a plugin that with an extension:

interface MyExtension {
    abstract val endpoints: NamedDomainObjectContainer<EndpointConfiguration>

I also have a shared build service:

abstract class MySharedBuildService: BuildService<MySharedBuildService.Parameters> {
    interface Parameters: BuildServiceParameters {
        val endpoints: NamedDomainObjectCollection<EndpointConfiguration>
    // Container for Service objects, to be created lazily from parameters.endpoints
    abstract val services: NamedDomainObjectCollection<Service>

The general idea is that in my plugin code, when I register my MySharedBuildService, I want to pass in the extension’s endpoints to the endpoints of the Parameters object. Is this a proper case of using addRule() to lazily populate containers? eg.

val extension = project.extensions.create<MyExtension>("myExtension")
val mySharedBuildService = project.gradle.sharedServices.registerIfAbsent("mySharedBuildService", MySharedBuildService::class.java) {
    parameters.endpoints.addRule("My Rule") {

The eventual goal is to have the services object be basically a named collection of objects, with every name in the extension’s endpoints having a named object in the build service’s services. (Aside: is calling services.addRule()in an init block safe?)

Following from that, suppose I have a task like so:

abstract class MyTask: DefaultTask() {
    @get:Input // @get:Optional?
    abstract val serviceName: Property<String>
    abstract val service: Property<Service>

Suppose I want to set a convention for the service property of this task such that it effectively uses serviceName to look up a Service from the registered MySharedBuildService. Something like:

project.tasks.withType<MyTask>().configureEach {
    service.convention(mySharedBuildService.flatMap {

Is this a good idea, or should I omit having a serviceName property entirely?

Some proper design advice would be appreciated.