Skip to content

Commit b12f0d7

Browse files
dmytroettDmytro Ett
andauthored
[ExcludeInterfaceMember] to exclude specific members from interface generation (#19)
* Added a feature to exclude interface members. * Added unit tests for the feature * Adding int test harness. * Finished int tests * Refactored interface generation tests * Rework int tests to test how IServiceProvider behaves (useful for future compiled time DI) * WIP * Refactor asserts --------- Co-authored-by: Dmytro Ett <dmytro.ett@outlook.com>
1 parent 4acdac9 commit b12f0d7

File tree

17 files changed

+344
-84
lines changed

17 files changed

+344
-84
lines changed

.codex/skills/snapshot-unit-testing/SKILL.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Favor a small number of representative scenarios; combine related assertions to
1313

1414
Avoid opening or reading `.received`/`.verified` files; rely on `dotnet test` output and VerifyException details only.
1515

16-
Treat `scripts/accept-snapshot` and `scripts/accept-all-snapshots` as black boxes; do not open or inspect them.
16+
Treat `.codex/skills/snapshot-unit-testing/scripts/accept-snapshot` and `.codex/skills/snapshot-unit-testing/scripts/accept-all-snapshots` as black boxes; do not open or inspect them.
1717

1818
# Workflow
1919

@@ -24,8 +24,8 @@ Run targeted tests with `dotnet test --filter` and add `--no-build`/`--no-restor
2424
Inspect `dotnet test` output to decide whether the test logic is wrong or snapshots need updating; do not open raw snapshot files.
2525

2626
Accept snapshots only when the behavior change is intended and clearly understood:
27-
- Use `scripts/accept-snapshot <TestClassName> <TestMethodName>` for a single test.
28-
- Use `scripts/accept-all-snapshots` for bulk updates.
27+
- Use `.codex/skills/snapshot-unit-testing/scripts/accept-snapshot <TestClassName> <TestMethodName>` for a single test.
28+
- Use `.codex/skills/snapshot-unit-testing/scripts/accept-all-snapshots` for bulk updates.
2929
- Skip these scripts for non-snapshot tests.
3030

3131
Re-run tests and iterate until they pass.
@@ -35,9 +35,9 @@ Re-run tests and iterate until they pass.
3535
- Run a single test:
3636
- `dotnet test --filter "FullyQualifiedName~<TestClass>.<TestMethod>"`
3737
- Accept one snapshot:
38-
- `scripts/accept-snapshot <TestClassName> <TestMethodName>`
38+
- `.codex/skills/snapshot-unit-testing/scripts/accept-snapshot <TestClassName> <TestMethodName>`
3939
- Accept all snapshots:
40-
- `scripts/accept-all-snapshots`
40+
- `.codex/skills/snapshot-unit-testing/scripts/accept-all-snapshots`
4141

4242
# Verify failure pattern
4343

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,17 @@ This project is about building a library to simplify dependency registration in
2121

2222
When done with code generation or modification, you must:
2323

24-
1. Run `dotnet format --no-restore --include <list-of-changed-files>`
24+
1. Run `dotnet format --include <list-of-changed-files>`
2525
2. Ensure no formatting issues remain
2626

2727
Example:
2828
```
29-
dotnet format --no-restore --include src/AttributedDI/MyClass.cs
29+
dotnet format --include src/AttributedDI/MyClass.cs
3030
```
3131

3232
For multiple files:
3333
```
34-
dotnet format --no-restore --include src/AttributedDI/File1.cs src/AttributedDI/File2.cs
34+
dotnet format --include src/AttributedDI/File1.cs src/AttributedDI/File2.cs
3535
```
3636

3737
## Public API Documentation

src/AttributedDI.SourceGenerator/InterfacesGeneration/InterfaceGenerationPipeline.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ private static ImmutableArray<string> CollectMembers(INamedTypeSymbol typeSymbol
105105
continue;
106106
}
107107

108+
if (IsExcludedFromInterface(member))
109+
{
110+
continue;
111+
}
112+
108113
switch (member)
109114
{
110115
case IMethodSymbol method when ShouldIgnoreMethod(method):
@@ -146,6 +151,28 @@ private static void AddMemberIfNeeded(
146151
}
147152
}
148153

154+
private static bool IsExcludedFromInterface(ISymbol member)
155+
{
156+
foreach (var attribute in member.GetAttributes())
157+
{
158+
var attributeClass = attribute.AttributeClass;
159+
if (attributeClass is null)
160+
{
161+
continue;
162+
}
163+
164+
if (string.Equals(
165+
attributeClass.ToDisplayString(),
166+
KnownAttributes.ExcludeInterfaceMemberAttribute,
167+
StringComparison.Ordinal))
168+
{
169+
return true;
170+
}
171+
}
172+
173+
return false;
174+
}
175+
149176
private static bool ShouldIgnoreMethod(IMethodSymbol method)
150177
{
151178
if (method.DeclaredAccessibility != Accessibility.Public)

src/AttributedDI.SourceGenerator/KnownAttributes.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ internal static class KnownAttributes
1818

1919
// Interface generation attributes
2020
public const string GenerateInterfaceAttribute = "AttributedDI.GenerateInterfaceAttribute";
21+
public const string ExcludeInterfaceMemberAttribute = "AttributedDI.ExcludeInterfaceMemberAttribute";
2122

2223
// Assembly-level attributes
2324
public const string GeneratedModuleNameAttribute = "AttributedDI.GeneratedModuleNameAttribute";
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System;
2+
3+
namespace AttributedDI;
4+
5+
/// <summary>
6+
/// Excludes a member from generated interfaces.
7+
/// </summary>
8+
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event | AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
9+
public sealed class ExcludeInterfaceMemberAttribute : Attribute
10+
{
11+
}

src/AttributedDI/GenerateInterfaceAttribute.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ namespace AttributedDI;
2222
/// <item><description>Non-public members.</description></item>
2323
/// <item><description>Static members and operators.</description></item>
2424
/// <item><description><c>ref</c> returns or <c>ref</c>/<c>in</c>/<c>out</c> parameters.</description></item>
25+
/// <item><description>Members annotated with <see cref="ExcludeInterfaceMemberAttribute"/>.</description></item>
2526
/// <item><description>Members excluded via conditional compilation.</description></item>
2627
/// </list>
2728
/// <para>

src/AttributedDI/RegisterAsGeneratedInterfaceAttribute.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ namespace AttributedDI;
2222
/// <item><description>Non-public members.</description></item>
2323
/// <item><description>Static members and operators.</description></item>
2424
/// <item><description><c>ref</c> returns or <c>ref</c>/<c>in</c>/<c>out</c> parameters.</description></item>
25+
/// <item><description>Members annotated with <see cref="ExcludeInterfaceMemberAttribute"/>.</description></item>
2526
/// <item><description>Members excluded via conditional compilation.</description></item>
2627
/// </list>
2728
/// <para>

test/AttributedDI.IntegrationTests/AllModulesRegistrationTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ public void AllServicesRegisteredCorrectly()
1212
ServiceCollection services = new();
1313

1414
services.AddAttributedDi();
15+
using var provider = services.BuildServiceProvider();
1516

16-
AssertContainsService<IMyAmazingService, MyAmazingService>(services, ServiceLifetime.Transient);
17-
AssertContainsService<IInternalService, InternalService>(services, ServiceLifetime.Transient);
17+
ServiceProviderAssert.Resolves<IMyAmazingService, MyAmazingService>(provider, ServiceLifetime.Transient);
18+
ServiceProviderAssert.Resolves<IInternalService, InternalService>(provider, ServiceLifetime.Transient);
1819
}
1920
}

test/AttributedDI.IntegrationTests/CoreScenariosTests.cs

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,34 +11,35 @@ public void ServiceRegistrationTests()
1111
var services = new ServiceCollection();
1212

1313
services.AddCompanyTeamNameProjectAPI();
14+
using var provider = services.BuildServiceProvider();
1415

1516
// RegisterAsSelf implicit transient
16-
AssertContainsService<RegisterAsSelfTransientImplicitService, RegisterAsSelfTransientImplicitService>(services, ServiceLifetime.Transient);
17+
ServiceProviderAssert.Resolves<RegisterAsSelfTransientImplicitService, RegisterAsSelfTransientImplicitService>(provider, ServiceLifetime.Transient);
1718

1819
// RegisterAsSelf singleton
19-
AssertContainsService<RegisterAsSelfSingletonService, RegisterAsSelfSingletonService>(services, ServiceLifetime.Singleton);
20+
ServiceProviderAssert.Resolves<RegisterAsSelfSingletonService, RegisterAsSelfSingletonService>(provider, ServiceLifetime.Singleton);
2021

2122
// RegisterAsSelf scoped
22-
AssertContainsService<RegisterAsSelfScopedService, RegisterAsSelfScopedService>(services, ServiceLifetime.Scoped);
23+
ServiceProviderAssert.Resolves<RegisterAsSelfScopedService, RegisterAsSelfScopedService>(provider, ServiceLifetime.Scoped);
2324

2425
// RegisterAs interface
25-
AssertContainsService<IRegisterAsInterfaceService, RegisterAsInterfaceScopedService>(services, ServiceLifetime.Scoped);
26+
ServiceProviderAssert.Resolves<IRegisterAsInterfaceService, RegisterAsInterfaceScopedService>(provider, ServiceLifetime.Scoped);
2627

2728
// RegisterAsImplementedInterfaces should register concrete but not IDisposable/IAsyncDisposable
28-
AssertContainsService<IFirstService, MultiInterfaceSingletonService>(services, ServiceLifetime.Singleton);
29-
AssertContainsService<ISecondService, MultiInterfaceSingletonService>(services, ServiceLifetime.Singleton);
30-
AssertDoesNotContainService<IDisposable, MultiInterfaceSingletonService>(services);
31-
AssertDoesNotContainService<IAsyncDisposable, MultiInterfaceSingletonService>(services);
29+
ServiceProviderAssert.Resolves<IFirstService, MultiInterfaceSingletonService>(provider, ServiceLifetime.Singleton);
30+
ServiceProviderAssert.Resolves<ISecondService, MultiInterfaceSingletonService>(provider, ServiceLifetime.Singleton);
31+
ServiceProviderAssert.DoesNotResolve<IDisposable>(provider);
32+
ServiceProviderAssert.DoesNotResolve<IAsyncDisposable>(provider);
3233

3334
// Lifetime-only attribute registers as self
34-
AssertContainsService<LifetimeOnlyTransientService, LifetimeOnlyTransientService>(services, ServiceLifetime.Transient);
35+
ServiceProviderAssert.Resolves<LifetimeOnlyTransientService, LifetimeOnlyTransientService>(provider, ServiceLifetime.Transient);
3536

3637
// Keyed services - RegisterAs<T> with key
37-
AssertContainsKeyedService<IKeyedService, KeyedServiceOne>(services, "key1", ServiceLifetime.Singleton);
38-
AssertContainsKeyedService<IKeyedService, KeyedServiceTwo>(services, "key2", ServiceLifetime.Singleton);
38+
ServiceProviderAssert.ResolvesKeyed<IKeyedService, KeyedServiceOne>(provider, "key1", ServiceLifetime.Singleton);
39+
ServiceProviderAssert.ResolvesKeyed<IKeyedService, KeyedServiceTwo>(provider, "key2", ServiceLifetime.Singleton);
3940

4041
// Keyed services - RegisterAsSelf with key
41-
AssertContainsKeyedService<RegisterAsSelfKeyedTransientService, RegisterAsSelfKeyedTransientService>(services, "transientKey", ServiceLifetime.Transient);
42-
AssertContainsKeyedService<RegisterAsSelfKeyedSingletonService, RegisterAsSelfKeyedSingletonService>(services, "singletonKey", ServiceLifetime.Singleton);
42+
ServiceProviderAssert.ResolvesKeyed<RegisterAsSelfKeyedTransientService, RegisterAsSelfKeyedTransientService>(provider, "transientKey", ServiceLifetime.Transient);
43+
ServiceProviderAssert.ResolvesKeyed<RegisterAsSelfKeyedSingletonService, RegisterAsSelfKeyedSingletonService>(provider, "singletonKey", ServiceLifetime.Singleton);
4344
}
4445
}

test/AttributedDI.IntegrationTests/CustomModuleNameTests.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ public void ExtensionMethod()
1212
var services = new ServiceCollection();
1313

1414
services.AddMyAmazingCustomServices();
15+
using var provider = services.BuildServiceProvider();
1516

16-
AssertContainsService<AliasedAssemblyService, AliasedAssemblyService>(services, ServiceLifetime.Scoped);
17+
ServiceProviderAssert.Resolves<AliasedAssemblyService, AliasedAssemblyService>(provider, ServiceLifetime.Scoped);
1718
}
1819

1920
[Fact]
@@ -23,7 +24,8 @@ public void DirectModuleRegistration()
2324

2425
var module = new MyIncredibleCustomModule();
2526
module.ConfigureServices(services);
27+
using var provider = services.BuildServiceProvider();
2628

27-
AssertContainsService<AliasedAssemblyService, AliasedAssemblyService>(services, ServiceLifetime.Scoped);
29+
ServiceProviderAssert.Resolves<AliasedAssemblyService, AliasedAssemblyService>(provider, ServiceLifetime.Scoped);
2830
}
2931
}

0 commit comments

Comments
 (0)