Some thoughts about rethinking Dream services as POCO classes that retain the capability of DreamService, without having to inherit from it, no longer require a DreamContext and have collections of features, similar to MVC Controllers.

    Code-only configuration

    Currently a DreamHost is best set-up as a standalone service process, windows service or an IIS HttpHandler. It is possible to create the host as an embedded server, but it's not quite as obvious. In addition, all forms are XML configured. It would be nice if the host could be configured and started via code-only fluent API, most appropriate for simple IIS handler or embedded use.

    A POCO DreamService

    Should be a Plain-old C# Object (POCO), i.e. it's just a class without any inheritance or interface dependency, that is inspected and wired up by Dream into a service instance. Just like current dream services, the class is marked up with DreamServiceAttribute and the attribute, rather than IDreamService is used to discover the service.

    Before it is ever instantiated, the class is inspected to create the blueprint as well as the DreamFeature directory and stages. This information is static information that can be used for any instance of the service and is called with contextual data on the instance in question.

    The instance's lifetime is the lifetime of the service. Service start is instance construction and constructors can be injected with service dependencies, including the config document. The constructor can do any service level initialization required.

    Stopping the service calls Dispose on the instance (if it is disposable).

    The service can declare Prologues, Epilogues and Features.

    Features are declared as now (including the new sync/async signatures and parameter injection).

    Prologues and Epilogues are discovered either by name (starts with Prologue or Epilogue) or using the Dream(Prologue|Epilogue)Attribute which can be used to provide an ExecutionOrder (otherwise execution order is alphabetically. Other than that they are simply features, i.e. they take parameter injection.

    Dream Feature signatures

    In addition to having most of the environment injected as parameters, features can also have a return type of any of the following:

    • XDoc
    • String
    • Stream
    • byte[]
    • DreamMessage

    The appropriate DreamMessage.Ok() is constructed from the data payload. All of these can be either the return type for sync features or Result<T> for async features.

    Standard Features

    A service is represented by a DreamServiceBlueprint object that carries the service specific information such as the blueprint document, the Self Plug, and the DreamFeature stages along with the instance and instance specific configuration and cookies. It is through this blueprint that services are invoked. This removes the IDreamService instance depedendency that a lot of the invocation chain currently has.

    Since features do not require instance state, we can attach standard features currently existing in DreamService (and thereby requiring inheritance) without them existing on the POCO service. Here's what happens to the standard features:

    • GET:@config
      • invoked via the blueprint object without need for service instance
    • PUT:@config
      • goes away, since right now the only one that calls it is DreamHostService and it does so right after creating the service
    • DELETE:@config
      • goes away, again, it's an extra step in the delete that only DreamHostService know about and is not needed
    • GET:@blueprint
      • invoked via the blueprint object without need for service instance
    • GET:@about
      • invoked via the blueprint object without need for service instance
    • DELETE:
      • stays and deletes the service
    • POST:@grants
      • invoked via the blueprint object without need for service instance

    Dream Controllers

    Dream Controllers are a new addition to allow grouping of features with a common prefix and provide common execution state, i.e. a Controller is instantiated per request. This means that a controller can have constructor injection for dependencies every feature it represents requires, without having to repeat it in each feature signature and can contain methods called by features that can take advantage of those per request depedencies. Combined with parameter injection, DreamControllers allow better application of DRY in features and the complete elimination of DreamContext (which isn't compatible with these new services, anyhow, since they aren't IDreamService instances).

    Controllers can also have prologues and epilogues, so that these grouped features can have common pre and post conditions. They are discovered and ordered the same way as in the Service, but controller prologues always fire after service prologues and controller Epilogues always fire before service epilogues. In general, controller prologue and epilogue functiontionality can usually be accomplished in the controller constructor and dispose method, respectively, since a controller is instantiated per request.

    Controllers are purely a code organizational facility. They are invisible to the service blueprint, to the extent that even though the controller may provide a common subpath, the feature directory for every controller feature will show the full path of that feature and document any controller consumed path parameter as a feature parameter.

    Examples

    Hello World Service

    [DreamService("Hello World", "Copyright (c) 2010 MindTouch, Inc.",
        SID = new[] { "http://services.mindtouch.com/dream/test/2010/08/helloworldservice" }
    )]
    public class HelloWorldService {
    
        [DreamFeature("GET:/helloworld","Get hello world")]
        public string GetHelloWorld() {
            return "hello world";
        }
    }

    Nothing else is required.

    Setting up the service state

    [DreamService("MindTouch Example", "Copyright (c) 2010 MindTouch, Inc.",
        SID = new[] { "http://services.mindtouch.com/dream/test/2010/08/example" }
    )]
    public class ExampleService : IDisposable{
        private readonly IDreamEnvironment _env;
        private readonly XDoc _config;
        private readonly Plug _self;
        private readonly ISomeDependency _dependency;
        private readonly Plug _storage;
    
        public ExampleService(
            IDreamEnvironment env,       // injected by Type 
            XDoc config,                 // injected by Name 
            Plug self,                   // injected by Name 
            ISomeDependency dependency,  // injected by Type 
            Plug storage,                // injected by Name
            IContainer serviceContainer  // injected by Type
        ) {
            _env = env;               
            _dependency = dependency; 
            _config = config;
            _self = self;
            _storage = storage;
    
            // configure the serviceContainer
            // (it is bad practive to store the container, even if it's legal)
            var builder = new ContainerBuilder();
            ... // add additional registrations to container
            builder.Build(serviceContainer);
        }
    
        public void Dispose() {
            _dependency.Dispose();
        }
        ...
    }

    Controller associated to Service via Service mark-up

    A controller can be associated to a Service by adding WithDreamController attributes to the servioce. Since the parent's private members are not accessible, the parent needs to publicly expose any state it wants the Controller to access. Since instances of services are never accessible to outside parties, this does not actually leak access to unintended parties.

    [DreamService("MindTouch Example", "Copyright (c) 2010 MindTouch, Inc.",
        SID = new[] { "http://services.mindtouch.com/dream/test/2010/08/example" }
    )]
    [WithDreamController(typeof(ExampleService),"foo/{id}")]
    public class ExampleService {
        private readonly XDoc _config;
    
        public ExampleService(XDoc config) {
            _config = config;
        }
    
        public XDoc Config { get { return _config;}}
    }
    
    [DreamController]
    public class ExampleController {
        private readonly ExampleService _parent;
        private readonly IRequestDependency _dependency;
    
        public ExampleController(
            int id,                        // injected path parameter for entire controller
            ExampleService parent,         // injected by Type
            IRequestDependency dependency  // injected by Type
        ) {
            _parent = parent;
            _dependency = dependency;
        }
    
        [DreamFeature("GET:baz", "")] // GET:foo/bar/baz at the service root
        public DreamMessage GetBarBaz(DreamMessage request) {
            return DreamMessage.Ok(_parent.Config["baz"]);
        }
    }

    This example also uses a path paramether at the Controller level, e.g. if you have a whole subset of features that all get an Id, you can capture it at the controller rather than feature level.

    Controller associated to Service via Controller mark-up

    Instead of associating the Controller at the Service, a Controller can mark itself up to attach itself to a service. This method allows Controllers to be used as service mix-ins, i.e. a controller that can be attached to multiple services:

    [DreamController(typeof(ServiceA), "extra")]
    [DreamController(typeof(ServiceB), "extra")]
    public class MixinController {
    
        // constructor used when Controller is called by ServiceA
        public MixinController(ServiceA parent ) {
            
        }
    
        // constructor used when no service specific one is found
        public MixinController(XDoc config) {
            
        }
    }

    This mixin will be used for both ServiceA and ServiceB. The most appropriate constructor is mapped for the parent service. The second constructor also shows that anything that can be injected into the service can be injected into the controller, including instances that the service attached to its container. This provides another means by which service state can be shared with the controller. This kind of controller could then also live in a child assembly to allow DreamServices to be extended after the fact.

    Tag page
    You must login to post a comment.

    Copyright © 2011 MindTouch, Inc. Powered by