Baubit is a lightweight, modular framework for building scalable and maintainable .NET applications. It provides a clean abstraction for organizing functionality into independently configured modules, supporting recursive loading, dependency injection, and multiple configuration sources.
- π§© Modular Architecture: Encapsulate related functionality in self-contained units (modules).
- ποΈ Configuration Management: Modules support their own typed configuration with support for JSON, embedded resources, and secrets.
- βοΈ Clean DI Integration: Each module registers services into
IServiceCollection
, respecting lifecycle and separation of concerns. - π Recursive Nesting: Modules can declare and load other modules as dependencies.
- π¦ Configurable Bootstrapping: Load and configure modules via JSON, appsettings, code, or embedded resources.
- π§ͺ Testability & Reusability: Modules are isolated and easily testable.
- π‘οΈ Validatable: Not only are modules and configurations validated (in isolation) before loding, modules are checked against the module tree to avoid redundant / missing modules.
dotnet add package Baubit
A Baubit module is a self-contained unit that adds one or more services to the application's IoC container.
public class MyConfiguration : AConfiguration
{
public string MyStringProperty { get; set; }
}
public class MyModule : AModule<MyConfiguration>
{
public MyModule(ConfigurationSource configurationSource) : base(configurationSource) { }
public MyModule(IConfiguration configuration) : base(configuration) { }
public MyModule(MyConfiguration configuration,
List<AModule> nestedModules,
List<IConstraint> constraints) : base(configuration, nestedModules, constraints) { }
public override void Load(IServiceCollection services)
{
var myStrProp = Configuration.MyStringProperty;
services.AddSingleton(new MyService(myStrProp));
//register other services as needed
base.Load(services);
}
}
Configuration for each registered service can be passed via the Module's specific configuration
Baubit supports multiple ways to load modules:
appsettings.json
Example:
{
"rootModule": [
"type": "Baubit.DI.RootModule, Baubit",
"configurationSource": {
"embeddedJsonResources": [ "MyApp;myConfig.json" ]
}
]
}
await Host.CreateApplicationBuilder()
.UseConfiguredServiceProviderFactory()
.Build()
.RunAsync();
var webApp = WebApplication.CreateBuilder()
.UseConfiguredServiceProviderFactory()
.Build();
// Use HTTPS, HSTS, CORS, Auth and other middleware
// Map endpoints
await webApp.RunAsync();
var configSource = new ConfigurationSource { EmbeddedJsonSources = ["MyApp;myConfig.json"] };
await Host.CreateApplicationBuilder()
.UseConfiguredServiceProviderFactory(configSource.Build())
.Build()
.RunAsync();
var configSource = new ConfigurationSource { EmbeddedJsonSources = ["MyApp;myConfig.json"] };
var webApp = WebApplication.CreateBuilder()
.UseConfiguredServiceProviderFactory(configSource.Build())
.Build();
// Use HTTPS, HSTS, CORS, Auth and other middleware
// Map endpoints
await webApp.RunAsync();
var configSource = new ConfigurationSource { EmbeddedJsonSources = ["MyApp;myConfig.json"] };
var services = new ServiceCollection();
services.AddFrom(configSource); // Loads all modules (recursively) defined in myConfig.json
var serviceProvider = services.BuildServiceProvider();
This approach is particularly useful when used in unit tests. See Baubit.xUnit for developing modular unit tests using Baubit
Baubit supports a mix of external and embedded configuration options:
jsonUriStrings
: Local or remote JSON pathsembeddedJsonResources
: Embedded resources within assemblieslocalSecrets
: User secrets via GUID ID
Loads JSON files from paths accessible to the application.
{
"modules": [
{
"type": "...",
"configurationSource": {
"jsonUriStrings": [ "/path/to/myConfig.json" ]
}
}
]
}
Loads JSON configuration embedded as a resource in a .NET assembly.
{
"modules": [
{
"type": "...",
"configurationSource": {
"embeddedJsonResources": [ "MyApp;MyComponent.SubComponent.myConfig.json" ]
}
}
]
}
Loads configuration from secrets.json
files using a GUID reference (User Secrets ID).
{
"modules": [
{
"type": "...",
"configurationSource": {
"localSecrets": [ "0657aef1-6dc5-48f1-8ae4-172674291be0" ]
}
}
]
}
This resolves to:
<user_secrets_path>/UserSecrets/{ID}/secrets.json
You can merge different configuration sources. Example:
{
"modules": [
{
"type": "...",
"configurationSource": {
"jsonUriStrings": [ "/path/to/myConfig.json" ],
"embeddedJsonResources": [ "MyApp;MyComponent.SubComponent.myConfig.json" ],
"localSecrets": [ "0657aef1-6dc5-48f1-8ae4-172674291be0" ]
}
}
]
}
All sources are merged in order.
Itβs also valid to define configuration values explicitly alongside configuration sources. The sources are merged with the explicit keys.
{
"modules": [
{
"type": "...",
"configuration": {
"myConfigurationProperty": "some value"
},
"configurationSource": {
"jsonUriStrings": [ "/path/to/myConfig.json" ],
"embeddedJsonResources": [ "MyApp;MyComponent.SubComponent.myConfig.json" ],
"localSecrets": [ "0657aef1-6dc5-48f1-8ae4-172674291be0" ]
}
}
]
}
This will result in a configuration that combines values from all three sources plus the inline configuration
block.
One of Baubit's most powerful features is its ability to recursively load modules, especially from configuration files. This enables complex service registration trees to be configured externally, promoting reusability and modularity.
{
"modules": [
{
"type": "<module1>",
"configuration": {
"<module1ConfigProperty>": "some value",
"modules": [
{
"type": "<module2>",
"configuration": {
"module2ConfigProperty": "some value"
}
},
{
"type": "<module3>",
"configuration": {
"module3ConfigProperty": "some value",
"modules": [
{
"type": "<module4>",
"configuration": {
"module4ConfigProperty": "some value"
}
}
]
}
}
]
}
}
]
}
This configuration will load Module 1, along with its nested modules 2, 3, and 4, in a hierarchical manner. Each module can define its own configuration and optionally nest further modules.
π§ This approach allows dynamic and flexible service registration β driven entirely by configuration without changing code.
Baubit introduces a powerful validation mechanism to ensure the integrity of your module configurations and their interdependencies.
Create your configuration
public class Configuration : AConfiguration
{
public string MyStringProperty { get; init;}
}
Implement the AValidator class to define validation logic for configuration.
public class MyConfigurationValidator : AValidator<Configuration>
{
protected override IEnumerable<Expression<Func<Configuration, Result>>> GetRules()
{
return [config => Result.OkIf(!string.IsNullOrEmpty(config.MyStringProperty), new Error($"{nameof(config.MyStringProperty)} cannot be null or empty")];
}
}
Multiple validators can be defined via the validatorKeys configuration property. Validators for modules can also be defined in the similar fashion
{
"modules": [
{
"type": "...",
"configuration": {
"validatorKeys": [ "MyLib.MyConfigurationValidator, MyLib" ],
"moduleValidatorKeys": [ "MyLib.MyModuleValidator, MyLib" ]
//"myStringProperty" : "" //<not_defined_on_purpose>
}
}
]
}
While modules can also be validated in isolation (similar to shown above), Baubit allows defining constraints under which modules can/cannot be included in a module tree
public class SingularityConstraint<TModule> : IConstraint
{
public string ReadableName => string.Empty;
public Result Check(List<IModule> modules)
{
return modules.Count(mod => mod is TModule) == 1 ? Result.Ok() : Result.Fail(string.Empty).AddReasonIfFailed(new SingularityCheckFailed());
}
}
public class SingularityCheckFailed : AReason
{
}
A module can then simply use these constraints to valide the module tree
public class Module : AModule<Configuration>
{
public Module(ConfigurationSource configurationSource) : base(configurationSource)
{
}
public Module(IConfiguration configuration) : base(configuration)
{
}
public Module(Configuration configuration,
List<Baubit.DI.AModule> nestedModules,
List<IConstraint> constraints) : base(configuration, nestedModules, constraints)
{
}
protected override IEnumerable<IConstraint> GetKnownConstraints()
{
return [new SingularityConstraint<Module>()];
}
}
This allows preventing redundant service registrations and checking module dependencies at bootstrapping
Future enhancements for Baubit:
- β Configuration Extensions: Support for more configuration sources.
- β Middleware Support: Integrate modules with ASP.NET middleware.
- π§ Logging & Monitoring: Improve logging support within modules.
- π§ Community Contributions: Open-source enhancements and community-driven improvements.
Contributions are welcome! If youβd like to improve Baubit:
- Fork the repository.
- Create a new branch (
feature/new-feature
). - Submit a pull request with detailed changes.
For major contributions, open an issue first to discuss the change.
A: You can initialize multiple modules and inject them into your service container.
A: Yes! You can extend configurations by passing custom settings to ConfigurationSource
.
For more support, open an issue on GitHub.
- Samples
- Official Documentation (Coming Soon)
- Issue Tracker: GitHub Issues
- Discussions: GitHub Discussions
See ACKNOWLEDGEMENT.md and INSPIRATION.md for details on libraries and ideas that influenced this project.
Copyright (c) Prashant Nagoorkar. See LICENSE for details.