I'm in a situation where there are many generated types from different domains/projects/systems, but there's a lot of overlap between them.
In the past I've used things like Automapper to provide some safety in converting objects between types, and proving correctness.
In this case I'd really like to have something more like duck typing.
I have an idea of what a few of the moving parts should be but I'm having trouble pulling it together.
Create a nuget package with a single method,
Convert<TSource, TTarget>. Make the actual implementation overridable, but treat this as an entry point for the duck typing analysis. 1b. Perhaps mark the method with an annotation to filter for it later during code analysisWrite a roslyn analyzer that scans for usage of this particular function (at either design or compile time). When calls are found, extract the generic types passed and run some block of code to assert TSource and TTarget are equivalent, properties wise, recursively. Perhaps use something like DeepEqual 2b. Perhaps implement more than just
Convert, likeConvertSubset/ConvertDownandConvertSuperset/ConvertUp.Ideally the distribution of this can be a single nuget package with both the DuckType Convert lib, and the roslyn analyzer, and when the user downloads it it'll enable the analyzer... I don't know if any of this is true yet.
I've studied the roslyn analyzers pages on learn.microsoft.com 1 2 3 and Meziantou's blog 3 4 to get started with Roslyn analyzers.
I'm puzzled how to specifically add checking to calls to a specific function.
I wonder if there's some sort of stylecop or resharper attribute that might allow me custom design time checking. I searched but didn't find anything like this.
I looked into decorating my Convert method with a custom Attribute (e.g. DuckTypesEqualAttribute).
I see learn.microsoft.com page on generics and attributes has some info, but nothing seems super helpful.
I try refactoring the Convert method into a <TSource, TTarget> genericized class and create a DuckTypeFactory that will delegate to generating the correct typed class and call Convert.
I hoped the attribute could somehow capture (at design or compile time) the generic type arguments, and I'd scan those in the roslyn analyzer.
No luck here. Each time I try to have open generic types, or reference generic types in the attribute properties it gives errors.
At the end of the page it says
Beginning with C# 11, a generic type can inherit from Attribute I tried this with the genericized class, and it also fails with CS8968 "An attribute type argument cannot use type parameters"
So I'm back to creating a simple attribute as a marker for the method, and then have the roslyn analyzer find each place the method is called.
This is an awkward fit, things aren't working for me yet.
I would love some guidance or links to code where people have done similar things. Or suggestions in a different direction than a roslyn analyzer if you know of something more fitting.
Thank you!
example code - the DuckTyping lib:
using System;
namespace DuckTyping
{
//marker attributes for roslyn analyzer to find methods
class DuckTypeEqualAttribute: Attribute {}
class DuckTypeSubsetAttribute: Attribute {}
class DuckTypeSupersetAttribute: Attribute {}
public class DuckTyping
{
/// <summary>
/// static Converter function. Override with desired conversion function. Recommend NewtonsoftJson Convert.
/// </summary>
public static Func<object, Type, Type, object> Converter { get; set; } = (obj, t1, t2) => System.Convert.ChangeType(obj, t2);//avoid an explicit reference to newtonsoft json, user to override impl at startup
[DuckTypeEqual]
public static TDest Convert<TSource, TDest>(TSource obj) => (TDest) Converter(obj, typeof(TSource), typeof(TDest));
[DuckTypeSubset]
public static TDest ConvertSubset<TSource, TDest>(TSource obj) => (TDest) Converter(obj, typeof(TSource), typeof(TDest));
[DuckTypeSuperset]
public static TDest ConvertSuperset<TSource, TDest>(TSource obj) => (TDest) Converter(obj, typeof(TSource), typeof(TDest));
}
}
Roslyn analyzer code... I'm stuck :-(
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.Operations;
namespace DuckTyping
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class DuckTypingAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "DuckTyping";
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AnalyzerTitle), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.AnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.AnalyzerDescription), Resources.ResourceManager, typeof(Resources));
private const string Category = "Type Checking";
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSymbolAction(handler, SymbolKind.Method);
context.RegisterOperationAction(handler, OperationKind.ExpressionStatement);
}
private static void handler(SymbolAnalysisContext ctx)
{
var methodSymbol = (IMethodSymbol)ctx.Symbol;
if (false) // ??? somehow check if the method called is DuckTyping.Convert
{
// ??? somehow extract the generic types in the call
//use DeepEqual to verify equal types, if not equal, report
ctx.ReportDiagnostic(Diagnostic.Create(Rule, methodSymbol.Locations[0], methodSymbol.Name));
}
}
private static void handler(OperationAnalysisContext ctx)
{
var expressionStatement = (IExpressionStatementOperation)ctx.Operation;
if (false) // ??? somehow check if the expression statement is calling DuckTyping.Convert
{
// ??? somehow extract the generic types in the call
//use DeepEqual to verify equal types, if not equal, report
ctx.ReportDiagnostic(Diagnostic.Create(Rule, expressionStatement.Syntax.GetLocation(), expressionStatement.Syntax.GetText()));
}
}
}
}
edit: I'm finding a direction using asp.net's Logging.Analyzer as a reference source