Skip to content

pnagoorkar/Baubit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Baubit

CircleCI NuGet

Introduction

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.

Why Use Baubit?

  • 🧩 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.

πŸš€ Getting Started

1️⃣ Installation

dotnet add package Baubit

πŸ“¦ Defining a Module

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

βš™οΈ Loading a Module

Baubit supports multiple ways to load modules:

1. Via appsettings.json (Recommended)

appsettings.json Example:

{
  "rootModule": [
    "type": "Baubit.DI.RootModule, Baubit",
    "configurationSource": {
      "embeddedJsonResources": [ "MyApp;myConfig.json" ]
    }
  ]
}

Using HostApplicationBuilder

await Host.CreateApplicationBuilder()
          .UseConfiguredServiceProviderFactory()
          .Build()
          .RunAsync();

Using WebApplicationBuilder

var webApp = WebApplication.CreateBuilder()
                           .UseConfiguredServiceProviderFactory()
                           .Build();

// Use HTTPS, HSTS, CORS, Auth and other middleware
// Map endpoints

await webApp.RunAsync();

2. Via ConfigurationSource (Direct Code-Based)

Using HostApplicationBuilder

var configSource = new ConfigurationSource { EmbeddedJsonSources = ["MyApp;myConfig.json"] };
await Host.CreateApplicationBuilder()
          .UseConfiguredServiceProviderFactory(configSource.Build())
          .Build()
          .RunAsync();

Using WebApplicationBuilder

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();

3. Manual DI (Without a Host Builder)

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

πŸ—‚οΈ Configuration Sources

Baubit supports a mix of external and embedded configuration options:

βœ… Supported Sources

  • jsonUriStrings: Local or remote JSON paths
  • embeddedJsonResources: Embedded resources within assemblies
  • localSecrets: User secrets via GUID ID

1. jsonUriStrings

Loads JSON files from paths accessible to the application.

{
  "modules": [
    {
        "type": "...",
        "configurationSource": {
          "jsonUriStrings": [ "/path/to/myConfig.json" ]
        }
    }
  ]
}

2. embeddedJsonResources

Loads JSON configuration embedded as a resource in a .NET assembly.

{
  "modules": [
    {
        "type": "...",
        "configurationSource": {
          "embeddedJsonResources": [ "MyApp;MyComponent.SubComponent.myConfig.json" ]
        }
    }
  ]
}

3. localSecrets

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

πŸ”— Combining Multiple Sources

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.

βž• Combining Sources with Explicit Configuration

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.

πŸͺ† Nesting Modules

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.

πŸ” Nested Configuration Example

{
  "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.

βœ… Validation

Baubit introduces a powerful validation mechanism to ensure the integrity of your module configurations and their interdependencies.

Configuration Validation

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>
        }
    }
  ]
}

Module Validation

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

πŸ“œ Roadmap

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.

🀝 Contributing

Contributions are welcome! If you’d like to improve Baubit:

  1. Fork the repository.
  2. Create a new branch (feature/new-feature).
  3. Submit a pull request with detailed changes.

For major contributions, open an issue first to discuss the change.

πŸ›  Troubleshooting & FAQs

Q: How do I use multiple modules together?

A: You can initialize multiple modules and inject them into your service container.

Q: Can I override module configurations?

A: Yes! You can extend configurations by passing custom settings to ConfigurationSource.

For more support, open an issue on GitHub.

πŸ”— Resources


Acknowledgments & Inspiration

See ACKNOWLEDGEMENT.md and INSPIRATION.md for details on libraries and ideas that influenced this project.


Copyright

Copyright (c) Prashant Nagoorkar. See LICENSE for details.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages