Skip to main content
Version: 0.6.x

Context and Dependencies

When implementing a server, ZIO gRPC allows you to specify that your service methods depend depends on a context of type Context which can be any Scala type.

For example, we can define a service with handlers that expect a context of type User for each request:

import zio.ZIO
import zio.Console
import zio.Console.printLine
import scalapb.zio_grpc.RequestContext
import myexample.testservice.ZioTestservice.ZSimpleService
import myexample.testservice.{Request, Response}
import io.grpc.{Status, StatusException}

case class User(name: String)

object MyService extends ZSimpleService[User] {
def sayHello(req: Request, user: User): ZIO[Any, StatusException, Response] =
for {
_ <- printLine("I am here!").orDie
} yield Response(s"Hello, ${user.name}")
}

Context transformations

In order to be able to bind our service to a gRPC server, we need to have the service's Context type to be one of the supported types:

  • scalapb.zio_grpc.RequestContext
  • scalapb.zio_grpc.SafeMetadata
  • Any

The service MyService as defined above expects User as a context. In order to be able to bind it, we will transform it into a service that depends on a context of type RequestContext. To do this, we need to provide the function to produce a User out of a RequestContext. This way, when a request comes in, ZIO gRPC can take the RequestContext (which is request metadata such as headers and options), and use our function to construct a User and provide it into the environment of our original service.

In many typical cases, we may need to retrieve the user from a database, and thus we are using an effectful function RequestContext => IO[Status, User] to find the user.

For example, we can provide a function that returns an effect that always succeeds:

val fixedUserService =
MyService.transformContextZIO((rc: RequestContext) => ZIO.succeed(User("foo")))
// fixedUserService: myexample.testservice.ZioTestservice.GSimpleService[RequestContext, StatusException] = myexample.testservice.ZioTestservice$GSimpleService$$anon$5@2eaf4b72

and we got our service with context of type RequestContext so it can be bound to a gRPC server.

Accessing metadata

Here is how we would extract a user from a metadata header:

import zio.IO
import scalapb.zio_grpc.{ServiceList, ServerMain}

val UserKey = io.grpc.Metadata.Key.of(
"user-key", io.grpc.Metadata.ASCII_STRING_MARSHALLER)
// UserKey: io.grpc.Metadata.Key[String] = Key{name='user-key'}

def findUser(rc: RequestContext): IO[StatusException, User] =
rc.metadata.get(UserKey).flatMap {
case Some(name) => ZIO.succeed(User(name))
case _ => ZIO.fail(
Status.UNAUTHENTICATED.withDescription("No access!").asException)
}

val rcService =
MyService.transformContextZIO(findUser)
// rcService: myexample.testservice.ZioTestservice.GSimpleService[RequestContext, StatusException] = myexample.testservice.ZioTestservice$GSimpleService$$anon$5@56589821

object MyServer extends ServerMain {
def services = ServiceList.add(rcService)
}

Context transformations that depends on a service

A context transformation may introduce a dependency on another service. For example, you may want to organize your code such that there is a UserDatabase service that provides a fetchUser effect that retrieves users from a database. Here is how you can do this:

trait UserDatabase {
def fetchUser(name: String): IO[StatusException, User]
}

object UserDatabase {
val layer = zio.ZLayer.succeed(
new UserDatabase {
def fetchUser(name: String): IO[StatusException, User] =
ZIO.succeed(User(name))
})
}

Now, The context transformation effect we apply may introduce an additional environmental dependency to our service. For example:

import zio.Clock._
import zio.Duration._

val myServiceAuthWithDatabase: ZIO[UserDatabase, Nothing, ZSimpleService[RequestContext]] =
ZIO.serviceWith[UserDatabase](
userDatabase =>
MyService.transformContextZIO {
(rc: RequestContext) =>
rc.metadata.get(UserKey)
.someOrFail(Status.UNAUTHENTICATED.asException)
.flatMap(userDatabase.fetchUser(_))
}
)
// myServiceAuthWithDatabase: ZIO[UserDatabase, Nothing, ZSimpleService[RequestContext]] = OnSuccess(
// trace = "repl.MdocSession.MdocApp.myServiceAuthWithDatabase(context.md:104)",
// first = Sync(
// trace = "repl.MdocSession.MdocApp.myServiceAuthWithDatabase(context.md:104)",
// eval = zio.ZIO$ServiceWithZIOPartiallyApplied$$$Lambda$14975/0x00000001040ac840@458927e4
// ),
// successK = zio.ZIO$$$Lambda$14966/0x000000010409e840@e46e6ba
// )

Now our service can be built from an effect that depends on UserDatabase. This effect can be added to a ServiceList using addZIO:

object MyServer2 extends ServerMain {
def services = ServiceList
.addZIO(myServiceAuthWithDatabase)
.provide(UserDatabase.layer)
}

Using a service as ZLayer

If you require more flexibility than provided through ServerMain, you can construct the server directly.

We first turn our service into a ZLayer:

val myServiceLayer = zio.ZLayer(myServiceAuthWithDatabase)
// myServiceLayer: zio.ZLayer[UserDatabase, Nothing, ZSimpleService[RequestContext]] = Suspend(
// self = zio.ZLayer$$$Lambda$15006/0x00000001040c2440@69168e41
// )

Notice how the dependencies moved to the input side of the ZLayer and the resulting layer is of type ZSimpleService[RequestContext].

To use this layer in an app, we can wire it like so:

import scalapb.zio_grpc.ServerLayer
import scalapb.zio_grpc.Server
import zio.ZLayer

val serviceList = ServiceList
.addFromEnvironment[ZSimpleService[RequestContext]]
// serviceList: ServiceList[Any with ZSimpleService[RequestContext]] = scalapb.zio_grpc.ServiceList@244c13ca

val serverLayer =
ServerLayer.fromServiceList(
io.grpc.ServerBuilder.forPort(9000),
serviceList
)
// serverLayer: ZLayer[Any with ZSimpleService[RequestContext], Throwable, Server] = Suspend(
// self = zio.ZLayer$ScopedEnvironmentPartiallyApplied$$$Lambda$15027/0x00000001040cb040@1d828596
// )

val ourApp =
ZLayer.make[Server](
serverLayer,
myServiceLayer,
UserDatabase.layer
)
// ourApp: ZLayer[Any, Throwable, Server] = Suspend(
// self = zio.ZLayer$ZLayerProvideSomeOps$$$Lambda$15029/0x00000001040cf840@781b8ec3
// )

object LayeredApp extends zio.ZIOAppDefault {
def run = ourApp.launch.exitCode
}

serverLayer creates a Server from a ZSimpleService layer and still depends on a UserDatabase. Then, ourApp feeds a UserDatabase.layer into serverLayer to produce a Server that doesn't depend on anything. In the run method we launch the server layer.

Implementing a service with dependencies

In this scenario, your service depends on two additional services, DepA and DepB. Following ZIO's service pattern, we accept the (interaces of the ) dependencies as constructor parameters.

trait DepA {
def methodA(param: String): ZIO[Any, Nothing, Int]
}

object DepA {
val layer = ZLayer.succeed[DepA](new DepA {
def methodA(param: String) = ???
})
}

object DepB {
val layer = ZLayer.succeed[DepB](new DepB {
def methodB(param: Float) = ???
})
}

trait DepB {
def methodB(param: Float): ZIO[Any, Nothing, Double]
}

case class MyService2(depA: DepA, depB: DepB) extends ZSimpleService[User] {
def sayHello(req: Request, user: User): ZIO[Any, StatusException, Response] =
for {
num1 <- depA.methodA(user.name)
num2 <- depB.methodB(12.3f)
_ <- printLine("I am here $num1 $num2!").orDie
} yield Response(s"Hello, ${user.name}")
}

object MyService2 {
val layer: ZLayer[DepA with DepB, Nothing, ZSimpleService[RequestContext]] =
ZLayer.fromFunction {
(depA: DepA, depB: DepB) =>
MyService2(depA, depB).transformContextZIO(findUser(_))
}
}

Our service layer now depends on the DepA and DepB interfaces. A server can be created like this:

object MyServer3 extends zio.ZIOAppDefault {

val serverLayer =
ServerLayer.fromServiceList(
io.grpc.ServerBuilder.forPort(9000),
ServiceList.addFromEnvironment[ZSimpleService[RequestContext]]
)

val appLayer = ZLayer.make[Server](
serverLayer,
DepA.layer,
DepB.layer,
MyService2.layer
)

def run = ourApp.launch.exitCode
}