You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I started working with Counter<T> recently and noticed that its API surface is very redundant, with lots of overloads that do "basically the same thing" but with additional data attached to the measurement:
I'd like to propose that this design be reconsidered to something simpler, where the main abstraction contains a "core" method that performs the work, while "auxiliary" overloads are hosted in a separate place.
The framework currently has a few places where this approach is taken today. For example:
HttpClient contains the "friendly access methods" and wraps a HttpClientHandler which actually contains a single method and is the "core" interface. The behavior is controlled in a centralized manner and can be easily replaced due to it being a single entry point.
ILogger (and consequently ILogger<T>) have most of the "friendly" overloads defined as extension methods in Microsoft.Extensions.Logging.LoggerExtensions, while the core functionality of logging is defined in a single method in the actual interface, the void Log<TState>(LogLevel, EventId, TState, Exception?, Func<TState, Exception?, string>) method. This again means the core behavior is unified in a single entry point that is easily implemented/mocked, while the "friendly" overloads are isolated and can be reused
In the same vein as ILogger, we also have IServiceCollection, which defines most "helper" methods in an extensions class in Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions, while the main functionality is abstracted in an extremely simple fashion by IServiceCollection being "a collection of ServiceDescriptor" that can be appended to.
If we look at the Metrics/Instrument API and compare them with these examples, its clear to see how redundant/verbose their APIs are.
I think it would be cleaner if there was a single, main API for instruments that is then extended with "helper" overloads using the extension methods (ILogger/IServiceCollection) or wrapper (HttpClient) approaches. This would make it much easier to implement new instruments and to create a more testable abstraction that relies on a single main entry point to do its work.
Something like:
publicabstractclassInstrument<T>:InstrumentwhereT:struct{protectedvoidRecordMeasurement(Tmeasurement,inTagListtagList)}publicstaticclassInstrumentExtensions{publicstaticvoidRecordMeasurement<T>(thisInstrument<T>instrument,Tmeasurement)=>// redirect to core 'RecordMeasurement(T, in TagList)// ... other overloads here}
Also, I must say that having "the same interface" with "different method names" (RecordMeasurement vs Add) seems like a hacky workaround to a missing language feature here which would allow "renaming" the abstraction on inheritance... not sure if this has ever been discussed but it just feels ugly to me the way it was done here (making RecordMeasurement invisible, and expose it using a different name in the inheritor).
For context, this comes from the perspective of testing code that relies on Counter<int> to perform conditional increments based on business logic. More details in:
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
I started working with
Counter<T>
recently and noticed that its API surface is very redundant, with lots of overloads that do "basically the same thing" but with additional data attached to the measurement:runtime/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Counter.cs
Lines 26 to 75 in 65008a9
As we can see, the implementation basically redirects every call to a similar abstraction in the
Instrument<T>
abstract class:I'd like to propose that this design be reconsidered to something simpler, where the main abstraction contains a "core" method that performs the work, while "auxiliary" overloads are hosted in a separate place.
The framework currently has a few places where this approach is taken today. For example:
HttpClient
contains the "friendly access methods" and wraps aHttpClientHandler
which actually contains a single method and is the "core" interface. The behavior is controlled in a centralized manner and can be easily replaced due to it being a single entry point.ILogger
(and consequentlyILogger<T>
) have most of the "friendly" overloads defined as extension methods inMicrosoft.Extensions.Logging.LoggerExtensions
, while the core functionality of logging is defined in a single method in the actual interface, thevoid Log<TState>(LogLevel, EventId, TState, Exception?, Func<TState, Exception?, string>)
method. This again means the core behavior is unified in a single entry point that is easily implemented/mocked, while the "friendly" overloads are isolated and can be reusedILogger
, we also haveIServiceCollection
, which defines most "helper" methods in an extensions class inMicrosoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions
, while the main functionality is abstracted in an extremely simple fashion byIServiceCollection
being "a collection ofServiceDescriptor
" that can be appended to.If we look at the Metrics/Instrument API and compare them with these examples, its clear to see how redundant/verbose their APIs are.
I think it would be cleaner if there was a single, main API for instruments that is then extended with "helper" overloads using the extension methods (
ILogger
/IServiceCollection
) or wrapper (HttpClient
) approaches. This would make it much easier to implement new instruments and to create a more testable abstraction that relies on a single main entry point to do its work.Something like:
Also, I must say that having "the same interface" with "different method names" (
RecordMeasurement
vsAdd
) seems like a hacky workaround to a missing language feature here which would allow "renaming" the abstraction on inheritance... not sure if this has ever been discussed but it just feels ugly to me the way it was done here (makingRecordMeasurement
invisible, and expose it using a different name in the inheritor).For context, this comes from the perspective of testing code that relies on
Counter<int>
to perform conditional increments based on business logic. More details in:Beta Was this translation helpful? Give feedback.
All reactions