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
Addmethod 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
Bakednamespace, 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 an override, add an extension class directly under the feature
folder, e.g.,
Override/OverrideExtensions.cs- Unlike regular features, provide
AddOverrides()extension method directly to allowapp.Features.AddOverrides()usage. - Create as many override features as you need under the folders specific
to the layers you want to configure, e.g.,
Override/Runtime/ServicesRuntimeOverrideFeature.cs - Add all override features to
List<IFeature>inAddOverrides()extension method.
- 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,
LayerConfigurator provides Use<T> helper which will invoke given
action with context phase artifact.
configurator.ConfigureApiModel(api =>
{
configurator.UseDomainModel(domain =>
{
...
});
});
Unlike configuration targets, phase artifacts may or may not exist in the application context or not configured properly at the moment
LayerConfiguratorapplies 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 ?? "/");
}