Thank you for your interest in contributing to JD.Efcpt.Build! This document provides guidelines and instructions for contributing.
By participating in this project, you agree to maintain a respectful and inclusive environment for all contributors.
Before creating bug reports, please check the existing issues to avoid duplicates. When creating a bug report, include:
- Clear title and description
- Steps to reproduce the issue
- Expected vs actual behavior
- Environment details:
- OS (Windows/Linux/macOS)
- .NET SDK version (
dotnet --info) - JD.Efcpt.Build version
- EF Core Power Tools CLI version
- Relevant logs with
EfcptLogVerbosityset todetailed - Sample project if possible (minimal reproduction)
Feature suggestions are welcome! Please:
- Check existing feature requests first
- Describe the use case clearly
- Explain why this feature would be useful
- Provide examples of how it would work
- Fork the repository and create a branch from
main - Follow existing code style and patterns
- Add tests for new functionality
- Update documentation as needed
- Ensure all tests pass before submitting
- Write clear commit messages
- .NET SDK 8.0 or later
- Visual Studio 2022, VS Code, or JetBrains Rider
- EF Core Power Tools CLI (
dotnet tool install -g ErikEJ.EFCorePowerTools.Cli) - SQL Server or SQL Server Express (for testing)
# Clone your fork
git clone https://github.com/YOUR-USERNAME/JD.Efcpt.Build.git
cd JD.Efcpt.Build
# Restore dependencies
dotnet restore
# Build
dotnet build
# Run tests
dotnet testJD.Efcpt.Build/
├── src/
│ ├── JD.Efcpt.Build/ # NuGet package project
│ │ ├── build/ # MSBuild .props and .targets
│ │ ├── buildTransitive/ # Transitive MSBuild files
│ │ └── defaults/ # Default configuration files
│ └── JD.Efcpt.Build.Tasks/ # MSBuild tasks implementation
│ ├── ResolveSqlProjAndInputs.cs
│ ├── EnsureDacpacBuilt.cs
│ ├── StageEfcptInputs.cs
│ ├── ComputeFingerprint.cs
│ ├── RunEfcpt.cs
│ └── RenameGeneratedFiles.cs
├── tests/
│ ├── JD.Efcpt.Build.Tasks.Tests/ # Unit tests
│ └── TestAssets/ # Test projects
├── samples/
│ └── simple-generation/ # Sample usage
└── docs/ # Documentation
- Follow existing C# coding conventions
- Use meaningful variable and method names
- Add XML documentation comments for public APIs
- Keep methods focused and single-purpose
- Prefer readability over cleverness
When adding or modifying MSBuild targets:
- Keep targets small and focused
- Use descriptive target names prefixed with
Efcpt - Document target dependencies clearly
- Add logging at appropriate verbosity levels
- Test incremental build scenarios
When adding or modifying tasks:
- Inherit from
Microsoft.Build.Utilities.Task - Mark parameters with
[Required]or[Output]attributes - Add XML documentation for all public properties
- Implement proper error handling
- Log diagnostic information
- Write unit tests
JD.Efcpt.Build uses TinyBDD for behavior-driven testing. All tests follow a consistent Given-When-Then pattern.
We use TinyBDD for all tests (not traditional xUnit Arrange-Act-Assert). This provides:
- ✅ Clear behavior specifications
- ✅ Readable test scenarios
- ✅ Consistent patterns across the codebase
- ✅ Self-documenting tests
# Run all tests
dotnet test
# Run with detailed output
dotnet test -v detailed
# Run specific test category
dotnet test --filter "FullyQualifiedName~SchemaReader"
# Run with code coverage
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencoverTest Structure:
using TinyBDD.Xunit;
using Xunit;
[Feature("Component: brief description of functionality")]
[Collection(nameof(AssemblySetup))]
public sealed class ComponentTests(ITestOutputHelper output) : TinyBddXunitBase(output)
{
// Define state records
private sealed record SetupState(
string InputValue,
ITestOutputHelper Output);
private sealed record ExecutionResult(
bool Success,
string Output,
Exception? Error = null);
[Scenario("Description of specific behavior")]
[Fact]
public async Task Scenario_Name()
{
await Given("context setup", () => new SetupState("test-value", Output))
.When("action is performed", state => PerformAction(state))
.Then("expected outcome occurs", result => result.Success)
.And("additional assertion", result => result.Output == "expected")
.Finally(result => CleanupResources(result))
.AssertPassed();
}
private static ExecutionResult PerformAction(SetupState state)
{
try
{
// Execute the action being tested
var output = DoSomething(state.InputValue);
return new ExecutionResult(true, output);
}
catch (Exception ex)
{
return new ExecutionResult(false, "", ex);
}
}
private static void CleanupResources(ExecutionResult result)
{
// Clean up any resources
}
}DO:
- ✅ Use TinyBDD for all new tests
- ✅ Write descriptive scenario names (e.g., "Should detect changed fingerprint when DACPAC modified")
- ✅ Use state records for Given context
- ✅ Use result records for When outcomes
- ✅ Test both success and failure paths
- ✅ Clean up resources in
Finallyblocks - ✅ Use meaningful assertion messages
DON'T:
- ❌ Use traditional Arrange-Act-Assert (use Given-When-Then)
- ❌ Skip the
Finallyblock if cleanup is needed - ❌ Write tests without clear scenarios
- ❌ Test implementation details (test behavior)
- ❌ Create inter-dependent tests
Pattern 1: Simple Value Transformation
[Scenario("Should compute fingerprint from byte array")]
[Fact]
public async Task Computes_fingerprint_from_bytes()
{
await Given("byte array with known content", () => new byte[] { 1, 2, 3, 4 })
.When("computing fingerprint", bytes => ComputeFingerprint(bytes))
.Then("fingerprint is deterministic", fp => !string.IsNullOrEmpty(fp))
.And("fingerprint has expected format", fp => fp.Length == 16)
.AssertPassed();
}Pattern 2: File System Operations
[Scenario("Should create output directory when it doesn't exist")]
[Fact]
public async Task Creates_missing_output_directory()
{
await Given("non-existent directory path", () =>
{
var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
return new SetupState(path, Output);
})
.When("ensuring directory exists", state =>
{
Directory.CreateDirectory(state.Path);
return new Result(Directory.Exists(state.Path), state.Path);
})
.Then("directory is created", result => result.Exists)
.Finally(result =>
{
if (Directory.Exists(result.Path))
Directory.Delete(result.Path, true);
})
.AssertPassed();
}Pattern 3: Exception Testing
[Scenario("Should throw when connection string is invalid")]
[Fact]
public async Task Throws_on_invalid_connection_string()
{
await Given("invalid connection string", () => "not-a-valid-connection-string")
.When("reading schema", connectionString =>
{
try
{
reader.ReadSchema(connectionString);
return (false, null as Exception);
}
catch (Exception ex)
{
return (true, ex);
}
})
.Then("exception is thrown", result => result.Item1)
.And("exception message is descriptive", result =>
result.Item2!.Message.Contains("connection") ||
result.Item2!.Message.Contains("invalid"))
.AssertPassed();
}Pattern 4: Integration Tests with Testcontainers
[Feature("PostgreSqlSchemaReader: integration with real database")]
[Collection(nameof(PostgreSqlContainer))]
public sealed class PostgreSqlSchemaIntegrationTests(
PostgreSqlFixture fixture,
ITestOutputHelper output) : TinyBddXunitBase(output)
{
[Scenario("Should read schema from PostgreSQL database")]
[Fact]
public async Task Reads_schema_from_postgres()
{
await Given("PostgreSQL database with test schema", () => fixture.ConnectionString)
.When("reading schema", cs => new PostgreSqlSchemaReader().ReadSchema(cs))
.Then("schema contains expected tables", schema => schema.Tables.Count > 0)
.And("tables have columns", schema => schema.Tables.All(t => t.Columns.Any()))
.AssertPassed();
}
}| Component | Target | Current |
|---|---|---|
| MSBuild Tasks | 95%+ | ~90% |
| Schema Readers | 90%+ | ~85% |
| Resolution Chains | 90%+ | ~88% |
| Utilities | 85%+ | ~82% |
Database Provider Tests:
- Use Testcontainers for SQL Server, PostgreSQL, MySQL
- Use in-memory SQLite for fast tests
- Mock unavailable providers (Snowflake requires LocalStack Pro)
Sample Projects:
- Create minimal test projects in
tests/TestAssets/ - Test actual MSBuild integration
- Verify generated code compiles
# Requires Docker for Testcontainers
docker info
# Run integration tests
dotnet test --filter "Category=Integration"
# Run specific provider tests
dotnet test --filter "FullyQualifiedName~PostgreSql"// TinyBDD provides detailed output on failure
await Given("setup", CreateSetup)
.When("action", Execute)
.Then("assertion", result => result.IsValid)
.AssertPassed();
// On failure, you'll see:
// ❌ Scenario failed at step: Then "assertion"
// Expected: True
// Actual: False
// State: { ... }For more details, see TinyBDD documentation.
When contributing, please update:
- README.md - For user-facing features
- docs/ - For detailed documentation in docs/user-guide/
- XML comments - For all public APIs
- Code comments - For complex logic
Documentation and README files use PACKAGE_VERSION as a placeholder for version numbers. This placeholder is automatically replaced with the actual version during the CI/CD build process.
When to use placeholders:
Use PACKAGE_VERSION in documentation for:
- SDK version references:
Sdk="JD.Efcpt.Sdk/PACKAGE_VERSION" - PackageReference version attributes:
Version="PACKAGE_VERSION" - Any mention of the current package version in examples
Example:
<!-- In documentation, write: -->
<Project Sdk="JD.Efcpt.Sdk/PACKAGE_VERSION">
<ItemGroup>
<PackageReference Include="JD.Efcpt.Build" Version="PACKAGE_VERSION" />
</ItemGroup>
</Project>
<!-- During CI build, this becomes (e.g., for version 1.2.3): -->
<Project Sdk="JD.Efcpt.Sdk/1.2.3">
<ItemGroup>
<PackageReference Include="JD.Efcpt.Build" Version="1.2.3" />
</ItemGroup>
</Project>Testing version replacement locally:
# Dry run (shows what would be replaced)
pwsh ./build/replace-version.ps1 -Version "1.2.3" -DryRun
# Actually replace versions
pwsh ./build/replace-version.ps1 -Version "1.2.3"
# Revert changes after testing
git checkout README.md docs/ samples/Important: Always commit documentation with PACKAGE_VERSION placeholders, not actual version numbers. The CI/CD workflow automatically replaces these during the build and package process.
Follow conventional commits format:
<type>(<scope>): <subject>
<body>
<footer>
Types:
feat: New featurefix: Bug fixdocs: Documentation changesstyle: Code style changes (formatting, etc.)refactor: Code refactoringtest: Adding or updating testschore: Maintenance tasks
Examples:
feat(staging): add TemplateOutputDir parameter
Allows templates to be staged to custom subdirectories within
the output directory. Supports both relative and absolute paths.
Closes #123
fix(fingerprint): detect deleted obj/efcpt folder
Added stamp file existence check to trigger regeneration when
intermediate directory is deleted.
Fixes #456
- Create a descriptive PR title following commit message format
- Fill out the PR template (if provided)
- Link related issues using keywords (Fixes #123, Closes #456)
- Ensure CI passes (all tests, builds)
- Respond to review feedback promptly
- Squash commits if requested
- Update documentation if feature changes user-facing behavior
Maintainers handle releases using this process:
- Update version in
JD.Efcpt.Build.csproj - Create git tag:
git tag -a v0.2.4 -m "Release v0.2.4" - Push tag:
git push origin v0.2.4 - Build NuGet package:
dotnet pack -c Release - Publish to NuGet.org
- GitHub Issues - For bugs and feature requests
- GitHub Discussions - For questions and community support
- Documentation - Check README.md and docs/user-guide/ first
Contributors will be recognized in:
- GitHub contributors page
- Release notes (for significant contributions)
- Special thanks in README (for major features)
By contributing, you agree that your contributions will be licensed under the MIT License.
Thank you for contributing to JD.Efcpt.Build! 🎉