Web
You have to make a few decisions when setting up any web project.
- IOC
- Authentication
- Error-handling
- Logging
Quino has reasonable defaults for a lot of these component, but you'll still have to configure a few things explicitly.
This documentation describes the default behavior or Quino and the minimum configuration required for most web applications. An application is free to replace much or all of this behavior with custom components.
OWIN
Quino's web support currently uses OWIN. To include web support for Quino, use the following command from the Quino.Owin
package.
application.UseMetaOwin();
This is a good start, but you'll need a bit more to finish configuring your application.
In particular, you almost certain want to configure the URL on which your application will listen. For this, Quino offers a specialization that takes a default URL and also adds support for using a custom URL from the configuration file (located under the server/hostUrl
node, by default).
application.UseMetaOwin("http://localhost:9000/");
Configuration
Web API applications are configured with the .NET System.Web.Http.HttpConfiguration
object. Quino applications can configure this object by registering an IHttpConfigurationCustomizer
.
The default factory is HttpConfigurationCustomizer
. This implementation injects two filters by default:
- An
IAuthenticationFilter
that determines how the user/principal is determined for an incoming request. See Authentication below for more details. - An
IExceptionFilter
that determines how unhandled exceptions in controller methods are handled. The default registration isLoggingExceptionFilter
which logs them.
Setting up the IOC
Quino strongly encourages a pattern of using an IOC with constructor-injection.
You can use this customizer as-is, but it's highly recommended to register the SimpleInjectorHttpConfigurationCustomizer
(found in the Quino.Owin.SimpleInjector
package). The SimpleInjectorHttpConfigurationCustomizer
is a straightforward base class that configures ASP.NET to use the same SimpleInjector IOC as the one configured in the Quino application.
If you want to use a different IOC, then you can set the DependencyResolver
on the HttpConfiguration
object to a different implementation, wiring up with the Quino application's underlying container. See the sources for SimpleInjector for more information.
So far, our application configuration looks like this:
application
.UseMetaOwin("http://localhost:9000/")
.UseRegisterSingle<IHttpConfigurationCustomizer, SimpleInjectorHttpConfigurationCustomizer>();
Customizing the HttpConfiguration
In the step above, we used a standard customizer from Quino to wire up ASP.NET with Quino. However, most applications will want to customize the HttpConfiguration
object further. To do this, simply inherit from the SimpleInjectorHttpConfigurationCustomizer
and override the Apply()
method. An example from the Quino Sandbox is shown below.
internal class SandboxHttpConfigurationCustomizer : SimpleInjectorHttpConfigurationCustomizer
{
/// <inheritdoc />
public override void Apply(HttpConfiguration configuration)
{
base.Apply(configuration);
configuration.MapHttpAttributeRoutes();
configuration.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional });
configuration.UseAssemblies(typeof(OfficeController), typeof(MetaDataController));
configuration.EnableCors(new EnableCorsAttribute("*", "*", "*"));
Mapper.Initialize(c => c.AddProfile<OfficeDtoProfile>());
}
}
With this configuration, we're ready to create controllers.
Controllers & Data
Quino provides a lot of support for retrieving data from an ASP.NET controller.
First, you can use one of the following base classes:
DataApiControllerBase
: The base class for Web API controllersDataControllerBase
: The base class for MVC controllers
Both of these controllers inject an IControllerDataSessionFactory
. This object is responsible for creating an IDataSession
for the controller. It can create a session from either a Web API or MVC request context, extracting the following information:
- Preferred languages
- User/Principal
- Name
These parameters are distilled from the web request and passed on to the IRequestDataSessionFactory
.
Preferred languages
Each HTTP request contains a list of acceptable languages in descending order of desirability.
Your application has a model that contains display languages. To add support for more languages, extend the list of supported languages in IMetaModel.DisplayLanguages
. This topic is covered in more detail in building metadata.
The standard implementation of IRequestDataSessionFactory
uses the ILanguageResolver
to determine the language to use for a given request.
Authentication
Quino 6 will support ASP.Net Identity. Some of the implementation details below are subject to change.
Each HTTP Request should also contain a user (or principal) that describes who is making the request.
The user/principal is set on the request by an implementation of the ASP.Net IAuthenticationFilter
. The default registration is MetaWebApiAuthenticationFilter
which queries the IWebAuthenticatorProvider
to see if an authenticator is capable of determining an authenticated user for a given request.
Token-based authentication
When the client makes a request, the server checks the request for a token. If one exists and is valid, the request is considered to be from that authenticated user. Otherwise, treat the request as unauthenticated.
How does the server find a token?
- It asks each of the
ITokenSource
objects in theITokenSourceProvider
to extract a token from the request. Examples are the authorization header or a specially named cookie. - With token in hand, it asks each of the
ITokenScheme
objects in theITokenSchemeProvider
if they can extract information from the token in the request. Examples are 'bearer'. - If the token can be extracted, it is considered valid.
- If the token has expired, the it is invalid.
- If OK, the request is assigned a user/principal based on the credentials encoded in the token.
- If failed, the server returns a 401
How does a user log in?
- The client authenticates via a login action
- The server returns the token in the response to the login action
- The client retains the token and passes it along with each request made to the server
- If the client receives a 401, it asks the user to log in again
Token-based configuration
Most applications will want to use token-based authentication for which Quino provides very good default support. The token support consists of a few parts:
TokenBasedWebAuthenticator
: This object is used on every request to determine the token (as outlined above). This is an implementation of theIWebAuthentication
registered with theIWebAuthenticatorProvider
TokenAuthenticator
: This object is used from the login action to insert the token into the response. It is also used for non-web-based authentication (e.g. token-based authentication in desktop applications that don't use HTTP). This is an implementation of theIAuthenticator
registered with theIAuthenticationProvider
To include all of this, an application calls:
application.UseTokenAuthentication()
OpenId/IdentityServer configuration
An application that integrates OpenID using the IdentityServer packages will have the principal already set up by that package. In that case, the application would use:
application.Configure<IWebAuthenticatorProvider>(p => p.Register<ClaimsPrincipalWebAuthenticator>())
Since the IdentityServer package takes care of reading/writing tokens, the application does not need to integrate Quino's token-based support.
Configuring fallback authentication
In previous versions, Quino automatically used an anonymous user when a user could not be determined from the request. This behavior is not ideal; instead, the standard implementation of the IControllerDataSessionFactory
requires a user/principal to be set on the request in order to create a DataSession
. Any unauthenticated request should return a 401. If this does not happen and the controller action attempts to access data, it results in a 500
response.
Again, these are just default settings in order to ensure security by default.
Applications that use external authentication providers will not need to service unauthenticated actions that use data access. However, an application that uses data-access to process the login action (i.e. users are stored in the local database) will need to specify which user/principal to use as a fallback.
This isn't very difficult. As you can imagine by now, the application registers another IWebAuthenticationProvider
, as shown below.
application.Configure<IWebAuthenticatorProvider>(p => p.Register<CurrentClaimsPrincipalWebAuthenticator>())
This is a simple implementation that uses the ClaimsPrincipal.Current
as the principal for unauthenticated requests.
Configuring Web with Windows Single-sign-on
An application can include support for using Windows single sign-on with ASP.NET by including the WindowsIdentityWebAuthenticator
, as shown below.
application.Configure<IWebAuthenticatorProvider>(p => p.Register<WindowsIdentityWebAuthenticator>())
Testing
Please consult the standard testing documentation for an introduction.
Like Quino-Standard, the Quino-WebApi includes a few general-purpose testing assemblies.
The Quino.Web.Testing
Assembly
This assembly contains the following registration methods for an IApplication
:
UseWebTesting()
: Enable Web-API and MVC testing supportUseWebTestingLogin()
: Similar toUseTestingLogin()
, this method sets the logged-in user for the web as well as non-web parts of the application.UseSessionForControllers()
: Use theSession
from the test fixture for controllers instead of creating a new one. Tests that use the[RunInTransaction]
attribute will want to enable this feature.
The Quino.Web.Testing.Fixtures
Assembly
Quino's web support also includes some standard fixture base classes.
WebApiQuinoTestsBase
: A version ofQuinoTestsBase
that includes web-testing support.WebApiPostgreSqlPunchclockWithPeopleTestsBase
: A version ofPostgreSqlPunchclockWithPeopleTestsBase
that includes web-testing support.WebApiQuinoTestsBase
: A version ofQuinoTestsBase
that includes web-testing support.
The following example shows an example of a standard base class for testing WebAPI controllers.
public class SandboxControllerTestsBase<TController> : SandboxTestsBase
where TController : ApiController
{
protected TController CreateController() => Application.GetInstance<IWebApiTestingControllerFactory>().CreateController<TController>();
/// <inheritdoc />
protected override IApplication SetUpApplication(IApplication application)
{
return base
.SetUpApplication(application)
.UseMetaWeb()
.UseWebTesting();
}
}
As you can see, it's not that hard to set up web testing. The base fixture above defines a helpful CreateController()
method for descendants to easily create properly configured controllers.
An application can further set a standard user with UseWebTestingLogin("TestUser")
.
MVC
Quino 6 will target ASP.Net Core. Most of the MVC-specific implementation details will no longer necessary as Web API and MVC are configured in the same way in .NET Core.
Quino provides support for ASP.Net MVC applications as well. Much of the discussion above applies to MVC as well.
- Use the
MetaHttpApplicationBase
base class instead ofHttpApplication
- Implement
CreateApplication()
to return your configured application - Call
UseMetaWeb
to include standard web support for Quino applications - Override
Application_Start
to customize yourHttpConfiguration
A simple example is shown below.
public class CustomWebApplication : MetaHttpApplicationBase
{
/// <inheritdoc/>
protected override void Application_Start(object sender, EventArgs args)
{
base.Application_Start(sender, args);
var customizer = BaseApplication.GetInstance<IHttpConfigurationCustomizer>();
GlobalConfiguration.Configure(c => customizer.Apply(c));
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
/// <inheritdoc/>
protected override IApplication CreateApplication(IApplicationCreationSettings applicationCreationSettings)
{
return new SandboxApplication()
.UseMetaWeb();
}
}
CORS
Any application with more than one URL (e.g. client application at https://www.abc-product.ch
and API server at https://api.abc-product.ch
) will have to configure CORS (Cross-origin resource sharing).
By default, browsers block requests between URLs unless the server for the URL being requested allows the request from the source address. A product's API server must indicate its preferences by using CORS support.
There are two ways to configure CORS:
- Using the
IHttpConfigurationCustomizer
- Using the OWin/ASP.NET
IApplicationBuilder
The following example shows how to enable CORS for local development, using the IHttpConfigurationCustomizer
.
internal class SandboxHttpConfigurationCustomizer : SimpleInjectorHttpConfigurationCustomizer
{
/// <inheritdoc />
public override void Apply(HttpConfiguration configuration)
{
base.Apply(configuration);
// Other configuration...
configuration.EnableCors(new EnableCorsAttribute("*", "*", "*"));
}
}
This is absolutely not recommended for production, as it basically disables CORS.
For production, you'll want to configure CORS much more precisely, limiting access to only the single URL from which requests can be made. You can even use an online tool to generate the code for your product. The example below was generated with that tool.
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// ...
// Shows UseCors with CorsPolicyBuilder.
app.UseCors(builder =>
builder.WithOrigins("https://www.abc-product.ch")
.AllowAnyMethod()
.AllowCredentials());
app.UseMvc();
// ...
}
To use the customizer instead, call EnableCors()
with the following parameters instead.
internal class SandboxHttpConfigurationCustomizer : SimpleInjectorHttpConfigurationCustomizer
{
/// <inheritdoc />
public override void Apply(HttpConfiguration configuration)
{
base.Apply(configuration);
// Other configuration...
configuration.EnableCors(new EnableCorsAttribute("https://www.abc-product.ch", "*", "*"));
}
}