Proposal: Scoped or Internal Extension Methods (Helper Methods) #9583
-
Problem StatementCurrently, in C#, there is no standard mechanism for providing reusable, functional, stateless logic (such as pure methods) that can be scoped only to a specific group of types (e.g., repositories with shared CRUD behavior) without exposing those methods to the entire codebase. While extension methods allow attaching utility logic to types, they are always globally visible wherever the target type and the static class are accessible. This breaks encapsulation and may expose helper algorithms or internal logic to unintended parts of the codebase, increasing the risk of misuse and complicating maintainability. Developers often rely on public or static utility classes, which don't provide semantic grouping or controlled visibility, making the API surface less predictable and more error-prone. Real-world ScenarioFor example, when working with a group of repository classes that must all support a common behavior, such as soft delete, there is currently no way to provide a utility method that is only available to those repositories and hidden from the rest of the codebase. The typical approach is to create a public or static utility class with methods like Proposed SolutionTo improve separation of concerns and centralize reusable logic, we propose introducing a new construct—"helper methods"—that function similarly to extension methods but with controlled visibility. Unlike extension methods (globally visible), helper methods would only be accessible within the target class, interface, or a specific scope (such as within an assembly). This would encapsulate reusable logic appropriately, reducing misuse. One possible approach is allowing extension methods to have access modifiers such as Benefits and Code Example
Proposed Syntax Example: private static async Task SafeDelete(this IRepositoryMarker _, string tableName, object id, IDbConnection db, CancellationToken cancellationToken)
=> await db.CreateQuery($"UPDATE {tableName} SET IsDelete = 1, DeleteOn = GETDATE() WHERE Id = @Id")
.AddParameter("Id", id)
.ExecuteAsync(cancellationToken);
private static async Task SafeDelete(this IRepositoryMarker _, string tableName, object id, IConnectionFactory factory, CancellationToken cancellationToken)
{
var db = await factory.CreateWriteConnection(cancellationToken);
await SafeDelete(_, tableName, id, db, cancellationToken);
} Related Work in Other LanguagesSome languages (Kotlin, Swift) support scoped visibility (file-level), and recent versions of C# (since C# 11) support file-scoped types ( Potential DrawbacksIntroducing scoped visibility helper methods similar to extension methods could increase language complexity. Developers may initially find it challenging to determine when to use extension versus helper methods, potentially leading to confusion. However, this complexity can be mitigated by careful design, clear documentation, and possibly IDE tooling indicating the visibility and scope of helper methods. Open Question to the CommunityWould you find scoped-visibility helper methods useful in your projects, or do you have alternative suggestions for addressing the encapsulation issues discussed above? |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 14 replies
-
First of all, designs restricting accessibility of members are usually for custom analyzers instead of the language itself.
It is always supported. You can limit the accessibility by type or individual method. |
Beta Was this translation helpful? Give feedback.
-
@mirmostafa |
Beta Was this translation helpful? Give feedback.
-
Real-world Example: Need for Truly Scoped/Internal Extension Methods in C#Let's consider a real business case with marker interfaces for soft deletion: public interface ISoftDeletableModel
{
bool IsDeleted { get; set; }
DateTime? DeletedAt { get; set; }
string? DeletedBy { get; set; }
}
public interface ISoftDeletableRepository<TModel>
where TModel : ISoftDeletableModel
{ } Suppose we have multiple repositories (e.g., public static class Helpers
{
extension<TModel>(ISoftDeletableRepository<TModel> repository)
where TModel : ISoftDeletableModel
{
// Imagine this extension contains multiple methods with different visibility:
public void UsefulHelper1(TModel model) { /* ... */ }
public void UsefulHelper2(TModel model) { /* ... */ }
// The key problem:
private void SoftDelete(TModel model, string deletedBy)
{
if (model == null) throw new ArgumentNullException(nameof(model));
model.IsDeleted = true;
model.DeletedAt = DateTime.UtcNow;
model.DeletedBy = deletedBy;
// Save changes in repository
}
}
} The ProblemThere is currently no way in C# to ensure that ONLY the
For example: You might want This means accidental misuse is always possible: as long as someone has a reference to the repository, they can call every extension—regardless of whether it's intended by the domain logic. What We NeedWe need a language feature where access modifiers (such as extension<TModel>(ISoftDeletableRepository<TModel> repository)
where TModel : ISoftDeletableModel
{
public void UsefulHelper1(TModel model) { /* ... */ }
public void UsefulHelper2(TModel model) { /* ... */ }
private void SoftDelete(TModel model, string deletedBy)
{
// Only visible INSIDE the implementing repository types
}
} This is a real, unsolved problem for proper encapsulation and domain safety in modern C#. |
Beta Was this translation helpful? Give feedback.
-
I can't think of a use case where I want this.
Why would this be accidental misuse? |
Beta Was this translation helpful? Give feedback.
Yes, language features can have better developer support, especially as language designers and IDE developers are responsible for implementing them. That's also what makes them prohibitively expensive and massively time consuming to implement.
The pattern you are requesting is an unusual one, and one extension members were not designed to address. You also seem to be the only person who has asked for such accessibility features for extension methods, despite the fact that the language feature is 18 years old and enjoys very widespread usage throughout the ecosystem. That would make it seem that the beneficiary of such a language feature would be incredibly small. As such, it would be very…