Skip to content

Commit 6e6e1a1

Browse files
authored
Merge pull request #336 from hassanhabib/users/cjdutoit/coderub-class-visibility
CODE RUB: Class Visibility and Exposure Control
2 parents 95d7f58 + 664e38c commit 6e6e1a1

2 files changed

Lines changed: 217 additions & 102 deletions

File tree

2. Services/2. Services.md

Lines changed: 215 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -287,155 +287,268 @@ Services/Foundations/Students/V1/StudentV1Service.cs
287287

288288
Every time there is a behavior change, increment the method behavior version in the method name by creating a new method:
289289

290-
```csharp
291-
ValueTask<StudentV1> RetrieveStudentByIdV1Async(Guid studentId)
290+
We will follow this convention for all behaviour changes:
291+
```plaintext
292+
[Operation] + [OperationBehaviorVersion] + [Resource] + [ResourceContractVersion] + [Optional QueryDescriptor] + [AsyncSuffix]
292293
```
293-
294-
The existing method without the version suffix (V0) remains available for backward compatibility, while the new method with the version suffix indicates the updated behavior. This allows consumers to choose when to adopt the new behavior without breaking existing integrations.
295-
296-
```csharp
297-
ValueTask<StudentV1> RetrieveStudentByIdAsync(Guid studentId)
298-
ValueTask<StudentV1> RetrieveStudentByIdV1Async(Guid studentId)
294+
e.g.
295+
```plaintext
296+
Retrieve + V1 + Student + V1 + ById + Async
299297
```
300298

301-
The behavior version always appears after the method name and before the Async suffix, making the behavior versions clear and unambiguous.
299+
__(V0 is implied and does not need to be explicitly stated in [OperationBehaviorVersion] OR [ResourceContractVersion])__
302300

303-
**IMPORTANT:**
304-
The version suffix on methods applies only to behavior changes, **not** model changes. This allows for granular control and makes it easy for consumers to identify which version of a method they are using, independent of the model version.
301+
Example of method behavior versioning for a model version at V0:
305302

306-
- Do **not** include the model version in the method name, as it is already reflected in the service file name and interface contract. Including it in the method name is redundant and can cause confusion.
307-
- Do **not** include the model version in the parameter name. This is also redundant and reduces readability.
308-
309-
**Correct:**
303+
```plaintext
304+
Services/Foundations/Students/StudentService.cs
305+
```
310306

311307
```csharp
312-
ValueTask<EventAddressV1> AddEventAddressV1Async(EventAddressV1 eventAddress)
313-
ValueTask<IQueryable<EventAddressV1>> RetrieveAllEventAddressesV1Async()
308+
ValueTask<Student> RetrieveStudentByIdAsync(Guid studentId)
309+
ValueTask<Student> RetrieveV1StudentByIdAsync(Guid studentId)
310+
ValueTask<Student> RetrieveV2StudentByIdAsync(Guid studentId)
314311
```
315312

316-
**Incorrect:**
313+
Example of method behavior versioning for a model version greater than V0:
317314

318-
- parameter names should not include the model version
319-
```csharp
320-
ValueTask<EventAddressV1> AddEventAddressV1Async(EventAddressV1 eventAddressV1)
321-
```
322-
```csharp
323-
ValueTask<StudentV1> RetrieveStudentV1ByIdV1Async(Guid studentV1Id)
324-
```
325-
- method names should not include the model version
326-
```csharp
327-
ValueTask<IQueryable<EventAddressV1>> RetrieveAllEventAddressV1sV1Async()
328-
```
315+
```plaintext
316+
Services/Foundations/Students/V1/StudentV1Service.cs
317+
```
329318

330-
#### 2.0.3.3 Combined Model and Behavior Changes
319+
```csharp
320+
ValueTask<StudentV1> RetrieveStudentV1ByIdAsync(Guid studentId)
321+
ValueTask<StudentV1> RetrieveV1StudentV1ByIdAsync(Guid studentId)
322+
ValueTask<StudentV1> RetrieveV2StudentV1ByIdAsync(Guid studentId)
323+
```
324+
325+
#### 2.0.3.3 Model Upgrade and Behavior Version Reset
331326

332-
When both the model and service behavior change, increment the model version and reset the service method behavior version to V0:
327+
Whenever the model version is incremented, the service filename and folder path must reflect the new model version, and the behavior version resets to V0:
333328

334-
The service file name and path reflect the new model version:
335329
```plaintext
336330
Services/Foundations/Students/V2/StudentV2Service.cs
337331
```
338332

339-
The service method behavior version reset to V0 whenever a new model version is introduced.
340-
Any further behavior changes for that model version will result in an increment of the
341-
method behavior version (V1, V2, etc.), while the model version remains constant.
333+
The service method behavior version resets to V0 whenever a new model version is introduced. Any further behavior changes for that model version will result in an increment of the method behavior version (V1, V2, etc.), while the model version remains constant.
342334

343335

344336
**Example:**
345337

346338
Before model upgrade (model V1 with multiple behavior versions):
347339

348340
```csharp
349-
internal interface IEventAddressV1Service
341+
internal interface IStudentV1Service
350342
{
351-
ValueTask<EventAddressV1> AddEventAddressAsync(EventAddressV1 eventAddress);
352-
ValueTask<EventAddressV1> AddEventAddressV1Async(EventAddressV1 eventAddress);
353-
ValueTask<EventAddressV1> AddEventAddressV2Async(EventAddressV1 eventAddress);
343+
ValueTask<StudentV1> RetrieveStudentV1ByIdAsync(Guid studentId)
344+
ValueTask<StudentV1> RetrieveV1StudentV1ByIdAsync(Guid studentId)
345+
ValueTask<StudentV1> RetrieveV2StudentV1ByIdAsync(Guid studentId)
354346
}
355347
```
356348

357349
After model upgrade (model V2 with only the latest behavior version):
358350

359351
```csharp
360-
internal interface IEventAddressV2Service
352+
internal interface IStudentV2Service
361353
{
362-
ValueTask<EventAddressV2> AddEventAddressAsync(EventAddressV2 eventAddress);
354+
ValueTask<StudentV2> RetrieveStudentV2ByIdAsync(Guid studentId);
363355
}
364356
```
365357

366358
The new V2 model service only exposes the latest behavior under the implied V0 naming convention.
367-
The parameter name in the method signature does not include the model version,
368-
as it is already reflected in the service file name and path.
359+
The V2 in this instance refers to the model version since it is appended to the resource.
360+
The behavior version resets on a model upgrade and since the model is now at the implied V0 we do
361+
not have to indicate it after the operation.
369362

370-
TIP: Use XML documentation comments to indicate versioning details only at exposure layers where consumers do not have access to the source code. This ensures external consumers understand which model version is used and any behaviour changes, while avoiding unnecessary comments within internal or non-exposed layers where the code itself is already visible.
371363

372-
**Example:**
364+
#### 2.0.3.4 Naming Conventions
365+
The naming convention for versioned files
366+
367+
- A versioned model is named `{Entity}V{n}.cs`
368+
- A versioned service named after the model version `{Entity}V{n}Service.cs`
369+
- Any behaviour versioning is reflected in the method name, immediately after the operation.
370+
When the behaviour version is omitted, it is implied to be V0. The version token immediately
371+
after the operation belongs to the behaviour, while the version token appended to the resource
372+
name belongs to the modelthe two are tracked independently:
373+
- V0 (implied) -> `ValueTask<EventAddressV1> AddEventAddressV1Async(EventAddressV1 eventAddress);`
374+
- V1 -> `ValueTask<EventAddressV1> AddV1EventAddressV1Async(EventAddressV1 eventAddress);`
375+
- V2 -> `ValueTask<EventAddressV1> AddV2EventAddressV1Async(EventAddressV1 eventAddress);`
376+
377+
- When the model version changes, the service filename increments to reflect the new model version, and the behavior version resets to V0. Following the formula from `2.0.3.2`, the implied V0 behaviour version is omitted:
378+
- Before (model V1): `{Entity}V1Service.cs`
379+
- After (model V2): `{Entity}V2Service.cs`
380+
- The method naming resets to the implied V0 convention:
381+
- V0 (implied, reset) -> `ValueTask<EventAddressV2> AddEventAddressV2Async(EventAddressV2 eventAddress);`
382+
383+
This ensures that the model version, behaviour version, and file structure are all independently readable and unambiguous at every level.
373384

385+
386+
### 2.0.4 Class Visibility and Exposure Control
387+
388+
Classes must be declared with the lowest possible visibility required for their intended use.
389+
By default, types should be internal unless there is a clear and deliberate need to expose them outside the assembly.
390+
391+
This principle reduces the exposure surface area of a library, protects internal implementation details, and prevents unintended coupling by consumers.
392+
393+
A smaller public surface:
394+
395+
- Improves maintainability by allowing internal refactoring without breaking consumers
396+
- Enforces clear architectural boundaries
397+
- Limits misuse of internal logic that was never designed for external consumption
398+
399+
400+
#### 2.0.4.0 Exception Visibility Rules
401+
402+
Exception types follow a stricter, intent-driven visibility model:
403+
404+
- **Localization Exceptions** — `public`
405+
These represent well-defined, domain-specific failures that are meaningful to consumers.
406+
They are expected to propagate beyond the library boundary and must therefore be accessible.
407+
408+
- **Categorization Exceptions** — `internal`
409+
These are used strictly for internal classification and logging.
410+
They must never escape the library boundary and should be stripped or translated at each layer.
411+
Keeping them internal ensures they cannot leak into external contracts.
412+
413+
- **Exposure Exceptions** — `public`
414+
These represent failures that are intentionally exposed to consumers (e.g., validation or dependency failures).
415+
They form part of the contract and must be accessible outside the library.
416+
417+
**Example**
418+
419+
420+
Public localization exceptions - meaningful to consumers
374421
```csharp
375-
public interface IEventAddressClient
422+
public class NotFoundStudentException : Xeption
423+
{
424+
public NotFoundStudentException(string message)
425+
: base(message)
426+
{ }
427+
}
428+
429+
public class InvalidStudentException : Xeption
376430
{
377-
/// <summary>
378-
/// Adds a new <see cref="EventAddressV1"/> (V0).
379-
/// </summary>
380-
/// <remarks>
381-
/// Version details: V0
382-
/// - Baseline behaviour.
383-
/// - Enforces required validation on <c>Name</c>.
384-
/// </remarks>
385-
/// <param name="eventAddress">The event address to add.</param>
386-
/// <returns>The persisted <see cref="EventAddressV1"/>.</returns>
387-
ValueTask<EventAddressV1> AddEventAddressAsync(EventAddressV1 eventAddress);
388-
389-
/// <summary>
390-
/// Adds a new <see cref="EventAddressV1"/> (V1).
391-
/// </summary>
392-
/// <remarks>
393-
/// Version details: V1
394-
/// - Extends V0 behaviour.
395-
/// - Enforces <c>CreatedDate</c> and <c>UpdatedDate</c> as mandatory fields.
396-
/// </remarks>
397-
/// <param name="eventAddress">The event address to add.</param>
398-
/// <returns>The persisted <see cref="EventAddressV1"/>.</returns>
399-
ValueTask<EventAddressV1> AddEventAddressV1Async(EventAddressV1 eventAddress);
400-
401-
/// <summary>
402-
/// Adds a new <see cref="EventAddressV1"/> (V2).
403-
/// </summary>
404-
/// <remarks>
405-
/// Version details: V2
406-
/// - Extends V1 behaviour.
407-
/// - Retains mandatory <c>CreatedDate</c> and <c>UpdatedDate</c>.
408-
/// - Enforces a maximum length of 450 characters on <c>Description</c>.
409-
/// </remarks>
410-
/// <param name="eventAddress">The event address to add.</param>
411-
/// <returns>The persisted <see cref="EventAddressV1"/>.</returns>
412-
ValueTask<EventAddressV1> AddEventAddressV2Async(EventAddressV1 eventAddress);
431+
public InvalidStudentException(string message)
432+
: base(message)
433+
{ }
434+
}
435+
436+
public class FailedStudentServiceException : Xeption
437+
{
438+
public FailedStudentServiceException(string message, Exception innerException)
439+
: base(message, innerException)
440+
{ }
413441
}
414442
```
415443

416-
#### 2.0.3.4 Naming Conventions
417-
The naming convention for versioned files
444+
Internal categorization exceptions - used only within the library
445+
```csharp
446+
internal class StudentValidationException : Xeption
447+
{
448+
public StudentValidationException(string message, Xeption innerException)
449+
: base(message, innerException)
450+
{ }
451+
}
418452

419-
- A versioned model is named `{Entity}V{n}.cs`
420-
- A versioned service named after the model version `{Entity}V{n}Service.cs`
421-
- Any behaviour versioning should be reflected in the method name, not the service name, to avoid confusion with model versioning.
422-
- V0 -> `ValueTask<EventAddressV1> AddEventAddressAsync(EventAddressV1 eventAddress);`
423-
- V1 -> `ValueTask<EventAddressV1> AddEventAddressV1Async(EventAddressV1 eventAddress);`
424-
- V2 -> `ValueTask<EventAddressV1> AddEventAddressV2Async(EventAddressV1 eventAddress);`
453+
internal class StudentDependencyValidationException : Xeption
454+
{
455+
public StudentDependencyValidationException(string message, Xeption innerException)
456+
: base(message, innerException)
457+
{ }
458+
}
459+
460+
internal class StudentDependencyException : Xeption
461+
{
462+
public StudentDependencyException(string message, Xeption innerException)
463+
: base(message, innerException)
464+
{ }
465+
}
425466

426-
- A versioned service with both model and behavior changes `{Entity}V{m}ServiceV{n}.cs`
427-
- As the model version takes precedence, the behavior version resets to V0 with each new model version, and the method naming reflects only the new / latest behavior version:
428-
- V0 -> `ValueTask<EventAddressV2> AddEventAddressAsync(EventAddressV2 eventAddress);`
467+
internal class StudentServiceException : Xeption
468+
{
469+
public StudentServiceException(string message, Xeption innerException)
470+
: base(message, innerException)
471+
{ }
472+
}
473+
474+
internal class StudentService : IStudentService
475+
{
476+
// Internal logic hidden from consumers
477+
}
478+
```
479+
480+
Public contract for student operations
481+
```csharp
482+
public interface IStudentClient
483+
{
484+
ValueTask<Student> GetStudentByIdAsync(Guid studentId);
485+
}
486+
```
487+
488+
Internal implementation of the public contract, with all dependencies and logic hidden
489+
```csharp
490+
internal class StudentClient : IStudentClient
491+
{
492+
private readonly IStudentService studentService;
493+
494+
public StudentClient(IStudentService studentService) =>
495+
this.studentService = studentService;
496+
497+
public ValueTask<Student> GetStudentByIdAsync(Guid studentId) =>
498+
this.studentService.RetrieveStudentByIdAsync(studentId);
499+
}
500+
```
501+
502+
Public university client exposing only the public contract, with all internal details hidden
503+
```csharp
504+
public interface IUniversityClient
505+
{
506+
IStudentClient Students { get; }
507+
}
508+
509+
public class UniversityClient : IUniversityClient
510+
{
511+
public UniversityClient(UniversityClientConfigurations configurations)
512+
{
513+
IServiceProvider serviceProvider = RegisterServices(configurations);
514+
InitializeClients(serviceProvider);
515+
}
516+
517+
public IStudentClient Students { get; private set; }
518+
519+
private void InitializeClients(IServiceProvider serviceProvider)
520+
{
521+
this.Students = serviceProvider.GetRequiredService<IStudentClient>();
522+
}
523+
524+
private static IServiceProvider RegisterServices(
525+
UniversityClientConfigurations configurations)
526+
{
527+
var services = new ServiceCollection()
528+
// Internal dependencies
529+
.AddTransient<IStorageBroker, StorageBroker>()
530+
.AddTransient<IDateTimeBroker, DateTimeBroker>()
531+
.AddTransient<IStudentService, StudentService>()
532+
533+
// Internal client mapped to public interface
534+
.AddTransient<IStudentClient, StudentClient>()
535+
536+
// Configurations
537+
.AddSingleton(configurations);
538+
539+
return services.BuildServiceProvider();
540+
}
541+
}
542+
```
429543

430-
This makes the version visible at every level
544+
**Key Takeaways**
545+
The consumer only interacts with UniversityClient and interfaces
546+
StudentClient and all supporting services are hidden (internal)
547+
Dependency injection is fully encapsulated within the library
548+
The library retains full control over implementation while exposing a stable contract
431549

432-
- Model is reflected in the folder structure, in the file name, and in the type name itself.
433-
- Behavior versioning is only reflected in the method name, not the service name, to avoid confusion with model versioning.
550+
**Guiding Principle**
434551

435-
**Quick Reference Table:**
552+
If a type is not explicitly part of the public contract, it must remain internal.
436553

437-
| Change Type | File Name Example | Method Name Example |
438-
|---------------------|--------------------------|------------------------------------------|
439-
| Model V1 | `StudentV1Service.cs` | `RetrieveStudentByIdAsync` |
440-
| Behavior V1 (V1) | `StudentV1Service.cs` | `RetrieveStudentByIdV1Async` |
441-
| Model V2 | `StudentV2Service.cs` | `RetrieveStudentByIdAsync` (reset to V0) |
554+
Visibility is not just an access modifier—it is a design decision that defines the boundary between what a library promises and what it is free to change.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ Chat with us here on Discord:
112112
- [2.0.3.2 Service Behavior Versioning](https://github.com/hassanhabib/The-Standard/blob/master/2.%20Services/2.%20Services.md#2032-service-behavior-versioning)
113113
- [2.0.3.3 Combined Model and Behavior Changes](https://github.com/hassanhabib/The-Standard/blob/master/2.%20Services/2.%20Services.md#2033-combined-model-and-behavior-changes)
114114
- [2.0.3.4 Naming Conventions](https://github.com/hassanhabib/The-Standard/blob/master/2.%20Services/2.%20Services.md#2034-naming-conventions)
115+
- [2.0.4 Class Visibility and Exposure Control](https://github.com/hassanhabib/The-Standard/blob/master/2.%20Services/2.%20Services.md#204-class-visibility-and-exposure-control)
116+
- [2.0.4.0 Exception Visibility Rules](https://github.com/hassanhabib/The-Standard/blob/master/2.%20Services/2.%20Services.md#2040-exception-visibility-rules)
115117
- [2.1 Foundation Services](https://github.com/hassanhabib/The-Standard/blob/master/2.%20Services/2.1%20Foundations/2.1%20Foundations.md#21-foundation-services-broker-neighboring)
116118
- [2.1.0 Introduction](https://github.com/hassanhabib/The-Standard/blob/master/2.%20Services/2.1%20Foundations/2.1%20Foundations.md#210-introduction)
117119
- [2.1.1 On The Map](https://github.com/hassanhabib/The-Standard/blob/master/2.%20Services/2.1%20Foundations/2.1%20Foundations.md#211-on-the-map)

0 commit comments

Comments
 (0)