diff --git a/.claude/rules/architecture.md b/.claude/rules/architecture.md new file mode 100644 index 0000000..7a20da3 --- /dev/null +++ b/.claude/rules/architecture.md @@ -0,0 +1,7 @@ +# Architecture + +Rules governing physical project and solution structure (as opposed to logical design, which lives in `design-principles.md`): + +- Repo/solution layout: source under `src//`, tests under `test/.UnitTests/` (singular `test`), one solution file (`.slnx`) per repo or example, at its root, referencing every project beneath it. +- Centralize shared MSBuild properties (`TargetFramework`, `Nullable`, `TreatWarningsAsErrors`, etc.) in a `Directory.Build.props` at that same root instead of repeating them per `.csproj`. +- `dotnet pack` runs in CI from day one so packaging bugs surface early. diff --git a/.claude/rules/coding-standards.md b/.claude/rules/coding-standards.md new file mode 100644 index 0000000..53ac9e8 --- /dev/null +++ b/.claude/rules/coding-standards.md @@ -0,0 +1,13 @@ +# Coding Standards + +Always follow the Microsoft C# coding conventions and .NET naming guidelines: + +- PascalCase for types, methods, properties, constants, and public members; camelCase for locals and parameters; `_camelCase` for private fields (`private static readonly` values are constant-like and stay PascalCase); `I` prefix for interfaces; `Async` suffix for async methods. +- One top-level type per file; file name matches the type name. Exception: a small supporting type (typically a record or struct) that exists only to serve the file's primary type — a companion DTO, result, or value type it owns and produces — may be declared in the same file. The exception covers helpers bound to that one type; it does not license unrelated types or a type intended for reuse across the codebase, which still get their own file. +- Use file-scoped namespaces, `var` when the type is apparent, expression-bodied members only when they improve readability. +- Enable and respect nullable reference types (`enable`); never suppress warnings with `!` without a comment justifying it. +- Prefer records for immutable data, `readonly` where possible, and pattern matching over type checks/casts. Exception: ORM-mapped entities that require mutable, parameterless-constructible state (e.g., EF Core) may remain classes. +- Public APIs must have XML doc comments; internal code is documented only where intent is not obvious from the code. Test projects are exempt — their `public` types exist only for test-framework discovery, not as a consumed API surface. +- Never include unnecessary using directives. +- Codify these conventions in an `.editorconfig` at the solution root (naming rules, `file_scoped` namespaces, `IDE0005` for unused usings, etc.) so they are tool-enforced rather than prose-only, and set `true` so style violations fail the build alongside `TreatWarningsAsErrors`. +- All code must pass `dotnet format` and build with zero warnings (`TreatWarningsAsErrors` is on). diff --git a/.claude/rules/design-principles.md b/.claude/rules/design-principles.md new file mode 100644 index 0000000..e424425 --- /dev/null +++ b/.claude/rules/design-principles.md @@ -0,0 +1,11 @@ +# Design Principles + +- Always apply well-established object-oriented design practices: favor composition over inheritance, encapsulate invariants inside domain types, keep classes small and cohesive, and depend on abstractions at layer boundaries. +- Adhere to SOLID to keep the code adaptive to change: + - **S** — one reason to change per class; split classes that mix concerns (e.g., validation + persistence). + - **O** — extend behavior via new implementations of existing abstractions rather than modifying stable code with switch/if chains on type. + - **L** — subtypes must honor base contracts; never throw `NotSupportedException` from an inherited member. + - **I** — prefer small, role-specific interfaces (e.g., `IOrderReader` / `IOrderWriter`) over fat ones. + - **D** — high-level modules (Application, Domain) define interfaces; Infrastructure implements them. Dependencies always point inward. +- Apply design patterns only when there is a tangible benefit compared to simpler alternatives. State the benefit in the PR/commit description when introducing one (e.g., Strategy to eliminate a growing conditional; Decorator for cross-cutting behavior). Do not introduce a pattern speculatively. +- IMPORTANT: Do not overengineer. No abstractions "for the future," no interfaces with a single implementation unless required for testing or layer isolation, no generic frameworks for one use case. Prefer the simplest design that satisfies current requirements and remains easy to change (YAGNI). diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 0000000..2482724 --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,19 @@ +# Testing — Test-Driven Development + +YOU MUST follow a test-driven development approach for all production code: + +1. **Red** — write a failing test that specifies the desired behavior before writing implementation code. +2. **Green** — write the minimum implementation to make the test pass. +3. **Refactor** — clean up the code and tests while keeping everything green. + +Rules: + +- Never write production code without a failing test that motivates it. Never mark a task complete while any test fails. +- Ensure all relevant edge cases are covered: null/empty inputs, boundary values, invalid state transitions, concurrency where applicable, and failure paths (exceptions, timeouts, cancellation via `CancellationToken`). +- When a task explicitly scopes test coverage (e.g., "one test", "happy path only"), that explicit scope takes precedence over this checklist — but say so out loud (commit message, notes, or conversation) rather than silently under-testing. +- Test naming: `Should_ExpectedOutcome_When_Scenario` (e.g., `Should_ThrowInsufficientStockException_When_InventoryInsufficient`). +- Structure tests as Arrange–Act–Assert. +- Unit tests must be fast and deterministic: no real I/O, network, clock, or `Task.Delay`. Abstract time behind `TimeProvider`. +- Run `dotnet test` after every change and before declaring any work finished. +- Generator output is snapshot-tested with the Roslyn testing SDK. +- Benchmarks (BenchmarkDotNet, vs Dapper and raw ADO.NET) live in the repo. diff --git a/.claude/rules/workflow.md b/.claude/rules/workflow.md new file mode 100644 index 0000000..ed783c8 --- /dev/null +++ b/.claude/rules/workflow.md @@ -0,0 +1,13 @@ +# Workflow + +- For each milestone, draft a plan first and present it to the user; execution starts only after the user approves the plan. +- Never open a pull request automatically — always confirm with the user first. +- Make minimal, focused changes; do not refactor unrelated code in the same change. +- One logical change per commit, with an imperative-mood message explaining *why*. +- When unsure between two designs, present both with trade-offs and ask before implementing. +- Never commit or push directly to the default branch (`master`/`main`). Milestones are used to track the progress of the project. All work happens on a `feature/M-`-prefixed branch. +- The branch lands on the default branch only through a reviewed pull request. Enforce this with branch protection (require a PR, disallow direct pushes, require the squash-merge strategy) so it can't be bypassed by accident. +- Merge by squash so each feature arrives as a single logical commit on the default branch, consistent with the "one logical change per commit" rule above; the squash commit message keeps the imperative, *why*-focused form. +- Versioning follows semantic versioning: each phase gets its own minor version (Phase 1 → `0.1.x`, Phase 2 → `0.2.x`, …; Phase 6 ships `1.0.0`). +- When a phase completes, tag it on the default branch with an annotated tag (e.g., `git tag -a v0.1.0 -m "..."`) and push the tag to GitHub for reference. +- Update this file when a new convention or correction is established. diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..95cb50f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,97 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space + +[*.cs] +indent_size = 4 + +# Language style +csharp_style_namespace_declarations = file_scoped:error +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = false:suggestion +csharp_style_expression_bodied_methods = when_on_single_line:suggestion +csharp_style_expression_bodied_properties = when_on_single_line:suggestion +csharp_prefer_simple_using_statement = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion + +# Never include unnecessary using directives +dotnet_diagnostic.IDE0005.severity = error + +# --- Naming conventions (coding-standards.md) --- + +# Interfaces: PascalCase with 'I' prefix +dotnet_naming_rule.interfaces_should_be_i_prefixed.severity = error +dotnet_naming_rule.interfaces_should_be_i_prefixed.symbols = interface_symbol +dotnet_naming_rule.interfaces_should_be_i_prefixed.style = i_prefix_pascal_style + +dotnet_naming_symbols.interface_symbol.applicable_kinds = interface +dotnet_naming_symbols.interface_symbol.applicable_accessibilities = * + +dotnet_naming_style.i_prefix_pascal_style.capitalization = pascal_case +dotnet_naming_style.i_prefix_pascal_style.required_prefix = I + +# Async methods: PascalCase with 'Async' suffix (checked before the general method rule) +dotnet_naming_rule.async_methods_should_be_async_suffixed.severity = error +dotnet_naming_rule.async_methods_should_be_async_suffixed.symbols = async_method_symbol +dotnet_naming_rule.async_methods_should_be_async_suffixed.style = async_suffix_pascal_style + +dotnet_naming_symbols.async_method_symbol.applicable_kinds = method +dotnet_naming_symbols.async_method_symbol.required_modifiers = async +dotnet_naming_symbols.async_method_symbol.applicable_accessibilities = * + +dotnet_naming_style.async_suffix_pascal_style.capitalization = pascal_case +dotnet_naming_style.async_suffix_pascal_style.required_suffix = Async + +# Constant fields and private static readonly (constant-like): PascalCase +dotnet_naming_rule.constants_should_be_pascal_case.severity = error +dotnet_naming_rule.constants_should_be_pascal_case.symbols = constant_symbol +dotnet_naming_rule.constants_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.constant_symbol.applicable_kinds = field +dotnet_naming_symbols.constant_symbol.required_modifiers = const +dotnet_naming_symbols.constant_symbol.applicable_accessibilities = * + +dotnet_naming_rule.private_static_readonly_should_be_pascal_case.severity = error +dotnet_naming_rule.private_static_readonly_should_be_pascal_case.symbols = private_static_readonly_symbol +dotnet_naming_rule.private_static_readonly_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.private_static_readonly_symbol.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_symbol.required_modifiers = static,readonly +dotnet_naming_symbols.private_static_readonly_symbol.applicable_accessibilities = private + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# Other private fields: _camelCase +dotnet_naming_rule.private_fields_should_be_underscore_camel_case.severity = error +dotnet_naming_rule.private_fields_should_be_underscore_camel_case.symbols = private_field_symbol +dotnet_naming_rule.private_fields_should_be_underscore_camel_case.style = underscore_camel_case_style + +dotnet_naming_symbols.private_field_symbol.applicable_kinds = field +dotnet_naming_symbols.private_field_symbol.applicable_accessibilities = private + +dotnet_naming_style.underscore_camel_case_style.capitalization = camel_case +dotnet_naming_style.underscore_camel_case_style.required_prefix = _ + +# Types and remaining members (classes, structs, enums, methods, properties, events): PascalCase +dotnet_naming_rule.types_and_members_should_be_pascal_case.severity = error +dotnet_naming_rule.types_and_members_should_be_pascal_case.symbols = types_and_members_symbol +dotnet_naming_rule.types_and_members_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.types_and_members_symbol.applicable_kinds = class, struct, enum, property, method, event, delegate, type_parameter +dotnet_naming_symbols.types_and_members_symbol.applicable_accessibilities = * + +# Locals and parameters: camelCase +dotnet_naming_rule.locals_and_parameters_should_be_camel_case.severity = suggestion +dotnet_naming_rule.locals_and_parameters_should_be_camel_case.symbols = locals_and_parameters_symbol +dotnet_naming_rule.locals_and_parameters_should_be_camel_case.style = camel_case_style + +dotnet_naming_symbols.locals_and_parameters_symbol.applicable_kinds = parameter, local + +dotnet_naming_style.camel_case_style.capitalization = camel_case diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cc8bfb9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + branches: + - master + - 'feature/**' + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Restore + run: dotnet restore + + - name: Verify formatting + run: dotnet format --verify-no-changes + + - name: Build + run: dotnet build --configuration Release --no-restore + + - name: Test + run: dotnet test --configuration Release --no-build + + - name: Pack + run: dotnet pack --configuration Release --no-build --output artifacts + + - name: Upload NuGet artifacts + uses: actions/upload-artifact@v4 + with: + name: nuget-packages + path: artifacts/*.nupkg diff --git a/.gitignore b/.gitignore index d5a18de..80fa0fa 100644 --- a/.gitignore +++ b/.gitignore @@ -427,3 +427,6 @@ FodyWeavers.xsd *.msix *.msm *.msp + +# JetBrains Rider +.idea/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2343335 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,43 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this project is + +SqlBound is a planned .NET/C# library providing SQLx-equivalent functionality (Rust's SQLx): **compile-time verified SQL queries** checked against a real database schema, reflection-free row materialization via Roslyn source generators, and SQL-file migrations. It will be published to NuGet. + +Follow the design constraints below; they are deliberate decisions, not suggestions. + +## Core design constraints + +**Dapper coexistence is a hard requirement.** Both libraries must coexist cleanly in the same project, on the same `DbConnection` and `DbTransaction`, with zero conflict. Concretely: + +- Build on `System.Data.Common` primitives; never own or wrap connections. +- Do NOT define extension methods on `IDbConnection`/`DbConnection` named `Query*`, `Execute*`, or `ExecuteScalar*` — those are Dapper's and would cause ambiguity errors when both namespaces are imported. Use generated static partial methods (the primary API) or distinct verbs (`FetchAsync`, `RunAsync`) / an instance-based `SqlSession` for the dynamic surface. +- No global static configuration state (Dapper's `SqlMapper` pattern) — conversions are resolved at compile time by the generator. +- SqlBound targets static, build-time-known queries; Dapper remains the tool for runtime-composed SQL. Don't add features that chase Dapper's niche. + +**Verification architecture (the SQLx `query!` equivalent):** + +- `[SqlQuery("...")]` attributes on `static partial` methods; an incremental source generator emits straight-line `DbDataReader` code (`GetInt32`, `IsDBNull`, …) — no reflection, no IL emit, Native AOT and trimming compatible. +- Database round-trips (prepare/describe against `SQLBOUND_DATABASE_URL`) happen only in the CLI `prepare` step or an opt-in MSBuild task — never inside the Roslyn analyzer. The in-IDE analyzer validates only against committed JSON snapshots in `.sqlbound/` (SQLx's offline `.sqlx` directory equivalent). This split is the load-bearing DX decision; there is an ADR planned for it. +- Diagnostic IDs use the `SQLB###` prefix. +- SQL Server is the pilot provider; then SQLite, Postgres, MySQL. + +**Package layout (planned):** + +- `SqlBound` — runtime core, `net8.0`+, dependency-free. +- `SqlBound.Generators` — generator + analyzer, `netstandard2.0` (Roslyn requirement), packed into `analyzers/dotnet/cs`, never a runtime dependency. +- `SqlBound.SqlServer` / `.Sqlite` / `.Npgsql` / `.MySql` — per-provider introspection and type mapping. +- `SqlBound.Cli` — dotnet tool: `prepare`, `verify`, `migrate add/run/revert`, `database create/drop`. + +## Roadmap + +Work proceeds milestone by milestone (M1–M16) across six phases: Bedrock (skeleton/CI, execution core, Dapper-coexistence sample), Codegen (materialization, query shapes, AOT + benchmarks), Verification (SQL Server introspection, diagnostics, offline snapshots), Providers, Migrations & CLI, and Ship (API freeze, NuGet 1.0). The Dapper-coexistence sample project (M3) doubles as a permanent CI regression test. Codegen precedes verification because the generator defines the shapes the verifier checks. + +## Conventions +@.claude/rules/coding-standards.md +@.claude/rules/architecture.md +@.claude/rules/design-principles.md +@.claude/rules/testing.md +@.claude/rules/workflow.md diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..a72b156 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,37 @@ + + + + net8.0;net10.0 + enable + enable + latest + true + true + true + + + + Luís Amorim + 0.1.0-preview.1 + Apache-2.0 + https://github.com/lgamorim/sqlbound + https://github.com/lgamorim/sqlbound + git + true + true + snupkg + true + + + + + $(NoWarn);CS1591 + false + + + diff --git a/LICENSE b/LICENSE index 261eeb9..24a07ef 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2026 Luís Amorim Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/docs/adr/0001-verification-split.md b/docs/adr/0001-verification-split.md new file mode 100644 index 0000000..300a611 --- /dev/null +++ b/docs/adr/0001-verification-split.md @@ -0,0 +1,85 @@ +# 1. Split compile-time SQL verification between the CLI/MSBuild task and the Roslyn analyzer + +## Status + +Accepted + +## Context + +SqlBound's core differentiator (its SQLx `query!`-macro equivalent) is verifying each +`[SqlQuery]`-annotated method's SQL against a real database schema at build time: preparing the +statement, describing its result columns and parameters, and comparing that metadata against the +method's declared C# signature. + +That verification requires a live round-trip to a database. Roslyn analyzers, however, are +expected to be fast, deterministic, and side-effect-free: they run repeatedly as the developer +types, inside the IDE's live analysis loop, and are not guaranteed to execute in an environment +with network or database access. An analyzer that opens a database connection on every keystroke +would make the IDE unusably slow at best, and leave analysis failing or hanging in environments +without DB connectivity (a fresh CI checkout, a teammate without local credentials) at worst. + +We need database-backed verification without coupling it to the fast, always-on analyzer loop. + +## Decision + +Split the verification pipeline into two stages that run in separate processes and communicate +only through committed snapshot files: + +1. **Preparation (I/O-bound, explicit, opt-in).** A CLI command (`dotnet sqlbound prepare`) and an + optional opt-in MSBuild task connect to a real database — via `SQLBOUND_DATABASE_URL` — prepare + and describe every `[SqlQuery]` in the project, and write the resulting metadata (parameter + types, column names, types, and nullability) as JSON snapshots to a `.sqlbound/` directory that + is committed to source control. This is the only place a live database connection is opened. +2. **Verification (fast, deterministic, offline).** The Roslyn analyzer that runs inside the IDE + and on every build reads only the committed `.sqlbound/` snapshots — it never opens a network + or database connection itself. It compares each snapshot against the partial method's declared + signature and reports mismatches as `SQLB###` diagnostics. + + The analyzer consumes the snapshots through Roslyn's `AdditionalFiles`/`AdditionalTexts` + mechanism, wired up automatically for consumers via a `buildTransitive` `.props` file in the + NuGet package. This is not an implementation detail but the enabling mechanism of the split: + analyzers are prohibited from arbitrary file I/O (RS1035), and `AdditionalTexts` participate in + Roslyn's change tracking, so editing a snapshot correctly re-triggers analysis. + +**Deferred to ADR 0002:** whether the *source generator* also consumes the `.sqlbound/` snapshots +(snapshot-driven codegen, where a missing snapshot is a build error) or emits materialization code +purely from the declared C# method signature (signature-driven codegen, where snapshots feed only +the analyzer). This ADR constrains both options equally — no database I/O outside the prepare +step — but does not choose between them. + +This mirrors SQLx's own offline mode (the `.sqlx` directory it commits for `cargo check` without a +live database). + +## Consequences + +**Positive** + +- The IDE stays responsive: the analyzer's hot path never performs I/O. +- Builds and CI are deterministic and don't require database connectivity by default — only the + explicit `prepare` step does. +- Teammates and CI agents without local database access can still build and get full verification + against the last-committed snapshot. + +**Negative / trade-offs** + +- Introduces a two-step workflow: a developer who changes a query or the underlying schema must + remember to re-run `prepare` before the analyzer will reflect it. +- Snapshots can go stale silently if `prepare` isn't re-run and nothing checks for drift. +- The opt-in MSBuild `prepare` task raises a build-ordering question the CLI path does not have: + it would regenerate snapshots mid-build, after the analyzer has already read the old ones. This + must be resolved when the task is designed (e.g., by requiring a second build, or by scoping the + task to run before compilation). + +**Follow-up** + +- A `prepare --check` (or equivalent) mode that fails if regenerating snapshots would produce a + diff is required to catch staleness in CI. This is scoped to M9 (offline mode), not this + milestone. +- Snapshot file granularity is one file per query (keyed by a content hash), not a single + aggregate file. SQLx abandoned its original single `sqlx-data.json` for a per-query `.sqlx/` + directory precisely because the aggregate file caused constant merge conflicts between parallel + branches. The exact file layout and naming are finalized in M9. +- The analyzer's behavior when a query has *no* snapshot at all (as opposed to a mismatched one) + is deliberately left open here and must be defined in M8's diagnostic design. It interacts with + ADR 0002: under snapshot-driven codegen a missing snapshot is necessarily a build error, while + under signature-driven codegen it could be anything from silence to a warning. diff --git a/docs/adr/0002-generator-snapshot-consumption.md b/docs/adr/0002-generator-snapshot-consumption.md new file mode 100644 index 0000000..8567ccb --- /dev/null +++ b/docs/adr/0002-generator-snapshot-consumption.md @@ -0,0 +1,28 @@ +# 2. Whether the source generator consumes `.sqlbound/` snapshots + +## Status + +Proposed — to be decided before M4 (row materialization) starts. + +## Context + +ADR 0001 confines database I/O to the `prepare` step and has the analyzer verify against committed +`.sqlbound/` snapshots. It deliberately leaves open which inputs the *source generator* uses when +emitting `DbDataReader` materialization code. Two coherent designs exist: + +1. **Signature-driven codegen.** The generator emits code purely from the declared C# partial + method signature (parameter types, return shape, nullability annotations). Snapshots feed only + the analyzer; a missing or stale snapshot degrades verification but never breaks the build. + Simpler generator, weaker guarantees, column access via `GetOrdinal` at runtime. +2. **Snapshot-driven codegen** (closer to SQLx). The generator reads snapshot metadata and can + emit ordinal-based reads and database-informed conversions; a missing snapshot is a build + error because codegen cannot proceed without it. Stronger guarantees and potentially faster + generated code, but couples every build to snapshot freshness. + +## Decision + +Not yet taken. To be resolved when M4 is planned, informed by generator prototyping. + +## Consequences + +Pending decision. diff --git a/global.json b/global.json new file mode 100644 index 0000000..f365c8c --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.301", + "rollForward": "latestFeature" + } +} diff --git a/sqlbound.slnx b/sqlbound.slnx new file mode 100644 index 0000000..350349d --- /dev/null +++ b/sqlbound.slnx @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/SqlBound/SqlBound.csproj b/src/SqlBound/SqlBound.csproj new file mode 100644 index 0000000..3089b53 --- /dev/null +++ b/src/SqlBound/SqlBound.csproj @@ -0,0 +1,7 @@ + + + + Compile-time verified SQL for .NET. + + + diff --git a/test/SqlBound.UnitTests/AssemblyLoadTests.cs b/test/SqlBound.UnitTests/AssemblyLoadTests.cs new file mode 100644 index 0000000..67994b6 --- /dev/null +++ b/test/SqlBound.UnitTests/AssemblyLoadTests.cs @@ -0,0 +1,14 @@ +using System.Reflection; + +namespace SqlBound.UnitTests; + +public class AssemblyLoadTests +{ + [Fact] + public void Should_LoadSqlBoundAssembly_When_TestProjectBuilds() + { + var assembly = Assembly.Load("SqlBound"); + + Assert.Equal("SqlBound", assembly.GetName().Name); + } +} diff --git a/test/SqlBound.UnitTests/SqlBound.UnitTests.csproj b/test/SqlBound.UnitTests/SqlBound.UnitTests.csproj new file mode 100644 index 0000000..0ffbace --- /dev/null +++ b/test/SqlBound.UnitTests/SqlBound.UnitTests.csproj @@ -0,0 +1,23 @@ + + + + + net10.0 + + + + + + + + + + + + + + + + + +