Dependency injection for Starcounter 2.4
Table of contents generated with markdown-toc
This package is available on nuget. You can get it there. To install with CLI run:
Install-Package Starcounter.Startup
Create a startup class (commonly called Startup
). It has to implement Starcounter.Startup.Abstractions.IStartup
.
using Microsoft.Extensions.DependencyInjection;
using Starcounter.Startup.Abstractions;
public class Startup : IStartup
{
public void ConfigureServices(IServiceCollection services)
{
// here you can configure your DI container
}
public void Configure(IApplicationBuilder applicationBuilder)
{
// here you perform any start-up tasks for your application
// applicationBuilder can be used to access the IServiceProvider
}
}
IStartup
mimics asp.net core's startup concept. You can read about it on docs.microsoft.com
In your Program.cs
you should have only a single call to the bootstrapper:
using Starcounter.Startup;
public class Program
{
public static void Main()
{
DefaultStarcounterBootstrapper.Start(new Startup());
}
}
This will start your application according to the Startup
class.
Router
is a class that helps creating the view-models and register them with URIs. To use it you have to add it to the DI container in ConfigureServices
:
// using Microsoft.Extensions.DependencyInjection;
// using Starcounter.Startup.Routing;
public void ConfigureServices(IServiceCollection services)
{
services.AddRouter();
}
You can annotate your view-model with Starcounter.Startup.Routing.UrlAttribute
:
// using Starcounter.Startup.Routing;
[Url("/DogsApp/Dogs")]
[Url("/DogsApp/AllDogs")] // you can use UrlAttribute multiple times
public partial class DogViewModel: Json
{
// ...
}
UrlAttribute
from Starcounter.Startup.Routing
namespace. Common mistake is to use one from System.Runtime.Remoting.Activation
instead.
You have to tell the Router
to scan your assembly for all view-models to register them:
// using Starcounter.Startup.Abstractions;
// using Starcounter.Startup.Routing;
public void Configure(IApplicationBuilder applicationBuilder)
{
applicationBuilder.ApplicationServices.GetRouter().RegisterAllFromCurrentAssembly();
// see also RegisterAllFromAssembly() if you keep your view-models in a separate project
}
The snippets above will register a handler under "/DogsApp/Dogs" and "/DogsApp/AllDogs". This handler will return DogViewModel
.
By default, applying [UrlAttribute]
will expose your view-model to the browser (or anyone who uses HTTP) under the supplied URI, and to the Blending Engine under the partial URI.
// using Starcounter.Startup.Routing;
[Url("/DogsApp/Dogs/{?}")]
public partial class DogViewModel: Json
{
// ...
}
The code above will expose your view-model under /DogsApp/Dogs/{?}
to the browser, and /DogsApp/partial/Dogs/{?}
to the Blending Engine. You won't be able to call the second URI from the browser.
You can also expose your view-model to Blending or browser only.
// blending only
[Url("/DogsApp/Dogs/{?}", External = false)]
// browser only
[Url("/DogsApp/Dogs/{?}", Blendable = false)]
Sometimes you want to have more control over the registration of your handlers. You can achieve that with manual registration:
// using Starcounter.Startup.Abstractions;
// using Starcounter.Startup.Routing;
public void Configure(IApplicationBuilder applicationBuilder)
{
applicationBuilder.ApplicationServices.GetRouter()
.HandleGet<DogViewModel>("/DogsApp/very-custom-uri/Dogs", new HandlerOptions {SelfOnly = true});
// see also other overloads of HandleGet
}
The snippet above will register a handler under "/DogsApp/very-custom-uri/Dogs". This handler will return DogViewModel
, but will only be available to Blending engine.
In most cases when you use URI parameters, they correspond to a database entity of type T
and your view-model implements IBound<T>
. This case, illustrated below, is handled automatically:
// using Starcounter.Startup.Routing;
[Url("/DogsApp/Dog/{?}")]
public partial class DogViewModel: Json, IBound<Dog>
{
// ...
}
In the example below, if you open URI /DogsApp/Dog/123
, then:
- Router (more specifically,
ContextMiddleware
) will look for aDog
with id123
in the database - If it's not found, then
404
response will be returned - If it's found, then
DogViewModel
will be initialized, and then itsData
property will be populated with theDog
found in the database
This is usually the desired behavior and if it satisfies you, can skip the rest of this chapter.
To understand how this works you first need to know what Context is. Context is the data represented by URI arguments. If the URI has a Dog
id a parameter 123
(/DogsApp/Dog/123
), then Context is the Dog
entity with id 123
.
If the view-model implements IBound<T>
, then the type of the Context is inferred to be T
. You can, however override this by implementing IPageContext<T>
interface. Consider following example:
You want the following view-model to be accessible by ID of the Dog
. i.e. accessing /DogsApp/Dog/123/Owner
should initialize DogOwnerViewModel.Data
to the owner of the Dog
with id 123
.
// using Starcounter.Startup.Routing;
[Url("/DogsApp/Dog/{?}/Owner")]
public partial class DogOwnerViewModel: Json, IBound<Person>, IPageContext<Dog>
{
public void HandleContext(Dog context)
{
this.Data = context.Owner;
}
// ...
}
Here, the context type would've been inferred to Person
, but we explicitly declared it to be Dog
. To implement the interface we also had to specify what happens with the context. If you don't implement this interface, but implement IBound<T>
, the context is simply assigned to Data
property.
But how is this context object fetched? By default, if the URI has only one parameter and Context is a database entity, the parameter value is used to fetch the Context from the database. However, if one of those conditions are not met or you want to override the default behavior, you must use [UriToContext]
:
// using Starcounter.Startup.Routing;
// using Starcounter.Linq;
[Url("/DogsApp/DogByName/{?}")]
public partial class DogViewModel: Json, IBound<Dog>
{
[UriToContext]
// the name of this method is irrelevant, but calling it UriToContext is a good practice
public static Dog UriToContext(string[] args, IDogsRepository dogsRepository)
{
// args is guaranteed to have one element, because its only URI has only one parameter
// returning null will cause the Router to respond with 404
return dogsRepository.GetByName(args[0]);
}
// ...
}
In this example instead of fetching the Context by its ID, we fetch it using its Name
property.
To use [UriToContext]
, apply it to one method that:
- is
public static
- has return type assignable to Context type
- has the first parameter of type
string[]
This method will be invoked before the view-model is created. It will be passed URI parameters as its sole argument. If it returns null, the Router
will respond with 404
. Otherwise, the return value will be used as the Context
.
This method can accept more than one parameter. Any additional parameters will be treated as a dependency and resolved using Dependency Injection container.
[UriToContext]
and IPageMiddleware<T>
features are connected, but independent. You can use them both or just one them.
Sometimes you want to define a behavior that will be applied to all the requests processed by the Router
.
To achieve this, implement Starcounter.Startup.Routing.IPageMiddleware
interface and register it in the DI container.
using System;
using Microsoft.Extensions.Logging;
using Starcounter;
using Starcounter.Startup.Routing;
public class LoggingMiddleware : IPageMiddleware
{
private readonly ILogger<LoggingMiddleware> _logger;
public LoggingMiddleware(ILogger<LoggingMiddleware> logger)
{
_logger = logger;
}
public Response Run(RoutingInfo routingInfo, Func<Response> next)
{
_logger.LogInformation("Processing request");
return next();
}
}
// using Microsoft.Extensions.DependencyInjection;
// using Starcounter.Startup.Abstractions;
// using Starcounter.Startup.Routing;
public void ConfigureServices(IServiceCollection services)
{
services.AddRouter();
services.AddTransient<IPageMiddleware, LoggingMiddleware>();
}
The above snippet will register LoggingMiddleware
to run at every request processed by the Router
.
AddRouter
extension method mentioned before adds two pieces of middleware by default: MasterPageMiddleware
and ContextMiddleware
. If you want to prevent that behavior you can do that by passing false
to includeDefaultMiddleware
parameter:
// using Microsoft.Extensions.DependencyInjection;
// using Starcounter.Startup.Abstractions;
// using Starcounter.Startup.Routing;
public void ConfigureServices(IServiceCollection services)
{
services.AddRouter(false);
// you can add them manually:
// services.AddTransient<IPageMiddleware, MasterPageMiddleware>();
// services.AddTransient<IPageMiddleware, ContextMiddleware>();
}
By default, every view-model you register with [Url]
is both page URI and partial URI registered. When you access its page URI, Router
creates a Db.Scope
and retrieves its blending URI. This means, that if you request your view-model in the browser, the response can contain other, blended view-models as well. Router
makes sure that they all share a common transaction.
A common application feature is to have some layout that wraps every response of an app and adds navigation features. This wrapping page would be called a master page. To enable it, create a view-model deriving from MasterPageBase
and register it using SetMasterPage<T>
:
{
"Html": "/MyApplication/views/MasterNavigation.html",
"InnerJson": {}
}
using Starcounter;
using Starcounter.Startup.Routing.Middleware;
public partial class MasterNavigationPage : MasterPageBase
{
public override void SetPartial(Json partial)
{
InnerJson = partial;
}
}
<template>
<dom-bind>
<template is="dom-bind">
<h1>Application-wide header</h1>
<a href="/MyApplication/Home">Go home</a>
<starcounter-include view-model="{{model.InnerJson}}"></starcounter-include>
</template>
</dom-bind>
</template>
// using Microsoft.Extensions.DependencyInjection;
// using Starcounter.Startup.Abstractions;
// using Starcounter.Startup.Routing;
public void ConfigureServices(IServiceCollection services)
{
services
.AddRouter()
.SetMasterPage<MasterNavigationPage>();
}
Without master page, all the blended view-models share a common transaction. With a master page like one defined above, all the blended view-models share a common transaction, but the master page itself has no transaction. You can change that if you want.
To put the master page in a transaction, you have to create it in Db.Scope
. To do it, register your custom master page factory:
// using Microsoft.Extensions.DependencyInjection;
// using Starcounter.Startup.Abstractions;
// using Starcounter.Startup.Routing;
public void ConfigureServices(IServiceCollection services)
{
services
.AddRouter()
.SetMasterPage((provider, routingInfo) => Db.Scope(() => new MasterNavigationPage()))
}
The code above will create MasterNavigationPage
in a transaction scope, but it will not be shared with the blended view-models. If you want it to be in shared transaction, you can change it by overriding ExecuteInScope
in your master page:
using Starcounter;
using Starcounter.Startup.Routing.Middleware;
public partial class MasterNavigationPage : MasterPageBase
{
public override void SetPartial(Json partial)
{
InnerJson = partial;
}
public override T ExecuteInScope(Func<T> innerJsonFactory)
{
return AttachedScope.Scope(innerJsonFactory);
}
}
The code above will attach the blended view-models to the scope of the master page. That way all the view-models will share a transaction.
To use services from the DI container in your view-model, declare a constructor that accepts dependencies as arguments. For more information about Dependency Injection, consult microsoft docs on DI.
public partial class DogViewModel: Json
{
public DogViewModel(IDogService dogService)
{
_dogService = dogService;
}
}
For a long time, Starcounter didn't support constructor injection, and used IInitPageWithDependncies
marker interface instead. You would implement it and create public, non-static, void Init
method that accepted your dependencies as parameters. Below is an example of that practice. It can be now safely converted to constructor injection.
// using Starcounter.Startup.Routing.Activation;
// LEGACY CODE
public partial class DogViewModel: Json, IInitPageWithDependencies
{
public void Init(IDogService dogService)
{
_dogService = dogService;
}
}
// WON'T WORK
// AllDogsViewModel.json
{
"Children": [ {} ]
}
// AllDogsViewModel.json.cs
public partial class AllDogsViewModel: Json
{
public DogViewModel(IDogService dogService)
{
Children.Data = dogService.GetAllDogs();
}
[AllDogsViewModel_json.Children]
public partial class ChildViewModel: Json
{
// this won't even compile
public ChildViewModel(IDogService dogService)
{
// ...
}
}
}
To fill dependencies for a nested view-model you have to create it by hand:
// AllDogsViewModel.json.cs
public partial class AllDogsViewModel: Json
{
public DogViewModel(IDogService dogService)
{
foreach (var dog in dogService.GetAllDogs())
{
Children.Add().Init(dogService);
}
}
[AllDogsViewModel_json.Children]
public partial class ChildViewModel: Json
{
public void Init(IDogService dogService)
{
// ...
}
}
}
DefaultStarcounterBootstrapper
registers two aspnet.core's features by default - logging and options. You can read about them on docs.microsoft.com.
All logs are printed on Standard Output by default.
UriHelper
is a collection of static methods which ease working with Starcounter URIs. It exposes following methods. Each method below is accompanied by an example with output.
public static string PartialToPage(string partialUri)
Converts partial URI to page URI. E.g. PartialToPage("/MyApp/partial/dog")
will return "/MyApp/dog"
.
public static string PageToPartial(string pageUri)
Converts page URI to partial URI. E.g. PageToPartial("/MyApp/dog")
will return "/MyApp/partial/dog"
.
public static bool IsPartialUri(string uri)
Returns true if the supplied URI is a partial URI. E.g. IsPartialUri("/MyApp/partial/dog")
will return true
, but IsPartialUri("/MyApp/dog")
will return false
.
public static string WithArguments(string uriTemplate, params string[] arguments)
Returns the supplied URI with its arguments filled. E.g. WithArguments("/MyApp/partial/dog/{?}", "xyz")
will return "/MyApp/partial/dog/xyz"
.
Usually when you want some code to execute during the startup of the application, you just put it in Configure
method of your startup class. However, there's a second way to achieve it: define a class implementing IStartupFilter
interface and register it in ConfigureServices
. Below is a sample startup filter and a snippet to register it:
using System;
using Microsoft.Extensions.Logging;
using Starcounter.Startup.Abstractions;
namespace Starcounter.Authorization.Authentication
{
public class LoggingStartupFilter: IStartupFilter
{
private readonly ILogger<LoggingStartupFilter> _logger;
public LoggingStartupFilter(ILogger<LoggingStartupFilter> logger)
{
_logger = logger;
}
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
_logger.LogInformation("Application started");
next(app);
};
}
}
}
// using Microsoft.Extensions.DependencyInjection;
// using Starcounter.Startup.Abstractions;
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IStartupFilter, LoggingStartupFilter>();
}
This feature is especially useful in libraries, which do not directly control application's startup.