If you are willing to use a c# precompiler, you could resolve this so that it does have compile time constraints:
// Used attribute
[AttributeUsage(AttributeTargets.Parameter)]
class ResolvedAsAttribute : Attribute
{
public string Expression;
public ResolvedAsAttribute(string expression)
{
this.Expression = expression;
}
}
// Fruit manager source:
class FruitManager {
...
public void AddFruit<TFruit>([ResolvedAs("(int p) => new TFruit(p)")] Func<int,TFruit> ctor = null)where TFruit: BaseFruit{
BaseFruit fruit = ctor(weight); /*new Apple(150);*/
fruit.Enlist(fruitManager);
}
}
// Fruit user source:
#ResolveInclude ../Managers/FruitManager.cs
...
fruitManager.AddFruit<Apple>();
...
Your precompiler would then turn the Fruit user source into:
...
fruitManager.AddFruit<Apple>((int p) => new Apple(p));
...
Using Roslyn, your precompiler could look something like this (here is room for improvement):
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using System.Threading;
using System.Text.RegularExpressions;
public class CsResolveIncludeAnalyser : CSharpSyntaxWalker
{
private List<(string key, MethodDeclarationSyntax node)> methodsToResolve = new List<(string key, MethodDeclarationSyntax node)>();
public List<(string key, MethodDeclarationSyntax node)> Analyse(string source)
{
var tree = CSharpSyntaxTree.ParseText(source);
var syntaxRoot = tree.GetRoot();
Visit(tree.GetRoot());
return methodsToResolve;
}
public override void VisitMethodDeclaration(MethodDeclarationSyntax methodDeclaration)
{
base.VisitMethodDeclaration(methodDeclaration);
if (methodDeclaration.ParameterList.Parameters.Count > 0)
{
foreach (var parm in methodDeclaration.ParameterList.Parameters)
{
var parmHasResolvedAs = parm.AttributeLists.Where((el) => el.Attributes.Where((attr) => attr.Name is IdentifierNameSyntax && ((IdentifierNameSyntax)attr.Name).Identifier.Text.Contains("ResolvedAs")).Any()).Any();
if (parmHasResolvedAs)
{
var name = methodDeclaration.Identifier.ValueText;
methodsToResolve.Add((name, methodDeclaration));
return;
}
}
}
}
}
public class CsSwiftRewriter : CSharpSyntaxRewriter
{
private string currentFileName;
private bool withWin32ErrorHandling;
private Dictionary<string,MethodDeclarationSyntax> methodsToResolve = new Dictionary<string, MethodDeclarationSyntax>();
private Dictionary<string, MethodDeclarationSyntax> getMethodsToResolve(string source, string fileName)
{
Dictionary<string, MethodDeclarationSyntax> methodsToResolve = new Dictionary<string, MethodDeclarationSyntax>();
var path = Path.GetDirectoryName(fileName);
var lines = source.Split(new[] { '\r', '\n' });
var resolveIncludes = (from el in lines where el.StartsWith("#ResolveInclude") select el.Substring("#ResolveInclude".Length).Trim()).ToList();
var analyser = new CsResolveIncludeAnalyser();
foreach (var resolveInclude in resolveIncludes)
{
var src = File.ReadAllText(path + "/" + resolveInclude);
var list = analyser.Analyse(src);
foreach (var el in list)
{
methodsToResolve.Add(el.key, el.node);
}
}
return methodsToResolve;
}
public static string Convert(string source, string fileName)
{
return Convert(source, fileName, false);
}
public static string Convert(string source, string fileName, bool isWithWin32ErrorHandling)
{
var rewriter = new CsSwiftRewriter() { currentFileName = fileName, withWin32ErrorHandling = isWithWin32ErrorHandling };
rewriter.methodsToResolve = rewriter.getMethodsToResolve(source, fileName);
var resolveIncludeRegex = new Regex(@"(\#ResolveInclude)\b");
source = resolveIncludeRegex.Replace(source, "//$1");
var tree = CSharpSyntaxTree.ParseText(source);
var syntaxRoot = tree.GetRoot();
var result = rewriter.Visit(tree.GetRoot());
return "#line 1 \"" + Path.GetFileName(fileName) + "\"\r\n" + result.ToFullString();
}
internal List<string> transformGenericArguments(List<string> arguments, GenericNameSyntax gName, TypeParameterListSyntax typeParameterList)
{
var res = new List<string>();
var typeParameters = typeParameterList.ChildNodes().ToList();
foreach (var argument in arguments)
{
var arg = argument;
for (int i = 0; i < gName.TypeArgumentList.Arguments.Count; i++)
{
var key = typeParameters[i];
var replacement = gName.TypeArgumentList.Arguments[i].ToString();
var regex = new System.Text.RegularExpressions.Regex($@"\b{key}\b");
arg = regex.Replace(arg, replacement);
}
res.Add(arg);
}
return res;
}
const string prefix = "";
internal List<string> extractExtraArguments(MethodDeclarationSyntax methodDeclaration)
{
var res = new List<String>();
foreach (var parm in methodDeclaration.ParameterList.Parameters)
{
foreach (var attrList in parm.AttributeLists)
{
foreach (var attr in attrList.Attributes)
{
if (attr.Name is IdentifierNameSyntax && string.Compare(((IdentifierNameSyntax)attr.Name).Identifier.Text, "ResolvedAs") == 0)
{
var programmCode = attr.ArgumentList.Arguments.First().ToString().Trim();
var trimmedProgrammCode = (programmCode.Length >= 2 && programmCode[0] == '"' && programmCode[programmCode.Length - 1] == '"') ? programmCode.Substring(1, programmCode.Length - 2) : programmCode;
res.Add(prefix + parm.Identifier.Text + ":" + trimmedProgrammCode);
}
}
}
}
return res;
}
internal List<string> extractExtraArguments(MethodDeclarationSyntax methodDeclaration, SimpleNameSyntax name)
{
var arguments = extractExtraArguments(methodDeclaration);
if (name != null && name is GenericNameSyntax)
{
var gName = name as GenericNameSyntax;
return transformGenericArguments(arguments, gName, methodDeclaration.TypeParameterList);
}
return arguments;
}
public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax c_expressionStatement)
{
InvocationExpressionSyntax expressionStatement = (InvocationExpressionSyntax) base.VisitInvocationExpression(c_expressionStatement);
List<string> addedArguments = null;
switch (expressionStatement.Expression)
{
case MemberAccessExpressionSyntax exp:
if (methodsToResolve.ContainsKey(exp.Name?.Identifier.ValueText))
{
addedArguments = extractExtraArguments(methodsToResolve[exp.Name.Identifier.ValueText], exp.Name);
}
break;
case GenericNameSyntax gName:
if (methodsToResolve.ContainsKey(gName.Identifier.ValueText))
{
addedArguments = extractExtraArguments(methodsToResolve[gName.Identifier.ValueText], gName);
}
break;
default:
var name = (from el in expressionStatement.ChildNodes()
where el is GenericNameSyntax
select (el as GenericNameSyntax)).FirstOrDefault();
if (name != default(GenericNameSyntax))
{
if (methodsToResolve.ContainsKey(name.Identifier.ValueText))
{
addedArguments = extractExtraArguments(methodsToResolve[name.Identifier.ValueText], name);
}
}
break;
}
if (addedArguments?.Count > 0)
{
var addedArgumentsString = string.Join(",", addedArguments);
var args = expressionStatement.ArgumentList.ToFullString();
var paras = $"({(expressionStatement.ArgumentList.Arguments.Count > 0 ? string.Join(",", args.Substring(1,args.Length - 2), addedArgumentsString) : addedArgumentsString)})" ;
var argList = SyntaxFactory.ParseArgumentList(paras);
return expressionStatement.WithArgumentList(argList);
}
return expressionStatement;
}
}
The Precompiler could be called using a T4 script, optionally regenerating the source at compile time.