Feature
Features consist of two parts; abstraction (port) and implementation (adapter).
Each adapter exposes a class that implements IFeature
interface.
Conventions
For a consistent developer experience, follow below conventions when implementing a new feature;
Abstraction
- Place all abstraction classes under a folder named after feature, e.g.,
Greeting/
. - Provide a configurator and an extension class for abstraction part, e.g.,
Greeting/GreetingConfigurator.cs
,Greeting/GreetingExtensions.cs
. - Provide an
Add
method to add feature to an application, e.g.,AddGreeting()
.- If feature allows multiple implementations, indicate this with a plural
add method, e.g.,
AddCodingStyles()
. Also accept a list of features instead of a single feature.
- If feature allows multiple implementations, indicate this with a plural
add method, e.g.,
Implementation
- Place all implementation classes under their own folder in the abstraction
folder, e.g.,
Greeting/WelcomePage/
. - Provide an extension method with the implementation name to allow adding
that implementation, e.g.,
WelcomePage()
.- This method should be in an extension class under
Baked
namespace, e.g.,Greeting/WelcomePage/WelcomePageGreetingExtensions.cs
.
- This method should be in an extension class under
- Name feature class after implementation name with abstraction name as a
suffix, e.g.,
WelcomePageGreetingFeature
. - Features depend on other features through their abstraction parts. Direct dependency between feature implementations is forbidden.
- To create a configuration overrider, add an extension and feature class
directly under the feature folder, e.g.,
ConfigurationOverrider/ConfigurationOverriderExtensions.cs
andConfigurationOverrider/ConfigurationOverriderFeature.cs
.- Unlike regular features, provide
AddConfigurationOverrider()
extension method directly to allowapp.Features.AddConfigurationOverrider()
usage. - Implement
IFeature
inConfigurationOverriderFeature
where you add all your configuration overrides.
- Unlike regular features, provide
Please refer to existing features in github.com/mouseless/baked for examples.
Creating A Feature
To create a feature implementation, create a class using above conventions and
implement IFeature<TConfigurator>
where TConfigurator
is the configurator
class of your feature abstraction.
WelcomePageGreetingFeature.cs
public class WelcomePageGreetingFeature : IFeature<GreetingConfigurator>
{
public void Configure(LayerConfigurator configurator)
{
...
}
}
Id
of a Feature
IFeature
has an Id
property which should be unique per Application
instance. By default, value of this property is name of the implementing
feature.
You can override its value by implementing IFeature.Id
property in feature
implementation class as shown below;
public class WelcomePageGreetingFeature : IFeature<GreetingConfigurator>
{
public string Id => "CustomFeatureId";
...
}
Disabling a Feature
To allow a feature to be disabled, you can provide a Disable
method in your
configurator which returns Feature.Empty<TConfigurator>()
, this feautre does
not configure any layers.
GreetingConfigurator.cs
public class GreetingConfigurator
{
public IFeature<GreetingConfigurator> Disabled() =>
Feature.Empty<GreetingConfigurator>();
}
Configuring Layers
To configure layers, a LayerCofigurator
instance is passed to the
Configure()
method of the IFeature
interface. Using extension methods on
the given configurator, a feature accesses configuration targets of layers.
WelcomePageGreetingFeature.cs
public class WelcomePageGreetingFeature : IFeature<GreetingConfigurator>
{
public void Configure(LayerConfigurator configurator)
{
configurator.ConfigureMiddlewareCollection(middlewares =>
{
middlewares.Add(app => app.UseWelcomePage());
});
}
}
Layers allow up to three configuration targets per configuration action.
Configure
method will be called multiple times, each time to configure a
different part of the application. LayerConfigurator
ensures that given
configuration action is only applied to the related target, e.g.,
IMiddlewareCollection
in the above code.
A layer might provide the same object in different configurators. For example,
WebApplication
implements IEndpointRouteBuilder
but HttpServerLayer
provides it with its interface not its concrete type.
Do NOT cast given configuration objects to their other interfaces. A layer provides a separate extension method, e.g.,
ConfigureEndpointRouteBuilder()
.
The order of the configuration calls does not have an effect in the outcome. Feel free to organize these calls in the way you like.
Using Phase Artifacts
To access and use objects stored in application context in a feature, a
reference to the context is provided through LayerConfigurator
's Context
property.
Unlike configuration targets, phase artifacts may or may not exists in the application context or not configured properly at the moment
LayerConfigurator
applies configurations. Phase execution orders and configurations should be taken into consideration when using phase artifacts.
Including an Option
To include an option in a feature, take the option as a parameter in configurator extension and pass it to the feature implementation as shown below;
WelcomePageGreetingFeature.cs
public class WelcomePageGreetingFeature(string _path)
: IFeature<GreetingConfigurator>
{
public void Configure(LayerConfigurator configurator)
{
configurator.ConfigureApplicationBuilder(app =>
{
app.UseWelcomePage(_path);
});
}
}
WelcomePageGreetingExtensions.cs
public static class WelcomePageGreetingExtensions
{
public static WelcomePageGreetingFeature WelcomePage(
this GreetingConfigurator source,
string? path = default
) => new(path ?? "/");
}