Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ public static readonly DiagnosticDescriptor
ConstructorAmbiguous = LibraryError("DAP036", "Ambiguous constructors", "Type '{0}' has more than 1 constructor; mark one constructor with [ExplicitConstructor] or reduce constructors"),
UserTypeNoSettableMembersFound = LibraryError("DAP037", "No settable members exist for user type", "Type '{0}' has no settable fields or properties"),
ValueTypeSingleFirstOrDefaultUsage = LibraryWarning("DAP038", "Value-type single row 'OrDefault' usage", "Type '{0}' is a value-type; it will not be trivial to identify missing rows from {1}"),
FactoryMethodMultipleExplicit = LibraryError("DAP039", "Multiple explicit factory methods", "Only one factory method should be marked [ExplicitConstructor] for type '{0}'"),
FactoryMethodAmbiguous = LibraryError("DAP040", "Ambiguous factory methods", "Type '{0}' has more than 1 factory method; mark one factory method with [ExplicitConstructor] or reduce factory methods"),
ConstructorOverridesFactoryMethod = LibraryWarning("DAP041", "Constructor overrides factory method", "Type '{0}' has both constructor and factory method; Constructor will be used instead of a factory method"),

// SQL parse specific
GeneralSqlError = SqlWarning("DAP200", "SQL error", "SQL error: {0}"),
Expand Down
23 changes: 21 additions & 2 deletions src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,18 +205,37 @@ private void ValidateDapperMethod(in OperationAnalysisContext ctx, IOperation sq
ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.ValueTypeSingleFirstOrDefaultUsage, location, resultMap.ElementType.Name, invoke.TargetMethod.Name));
}

// check for constructors on the materialized type
DiagnosticDescriptor? ctorFault = ChooseConstructor(resultMap.ElementType, out var ctor) switch
// check for constructors and factoryMethods on the materialized type
var ctorFault = ChooseConstructor(resultMap.ElementType, out var ctor) switch
{
ConstructorResult.FailMultipleExplicit => Diagnostics.ConstructorMultipleExplicit,
ConstructorResult.FailMultipleImplicit when aotEnabled => Diagnostics.ConstructorAmbiguous,
_ => null,
};
var factoryMethodFault = ChooseFactoryMethod(resultMap.ElementType, out var factoryMethod) switch
{
FactoryMethodResult.FailMultipleExplicit => Diagnostics.FactoryMethodMultipleExplicit,
FactoryMethodResult.FailMultipleImplicit when aotEnabled => Diagnostics.FactoryMethodAmbiguous,
_ => null,
};

// we cant use both ctor and factoryMethod, so reporting a warning that ctor is prioritized
if (ctor is not null && factoryMethod is not null)
{
var loc = factoryMethod?.Locations.FirstOrDefault() ?? resultMap.ElementType.Locations.FirstOrDefault();
ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.ConstructorOverridesFactoryMethod, loc, resultMap.ElementType.GetDisplayString()));
}

if (ctorFault is not null)
{
var loc = ctor?.Locations.FirstOrDefault() ?? resultMap.ElementType.Locations.FirstOrDefault();
ctx.ReportDiagnostic(Diagnostic.Create(ctorFault, loc, resultMap.ElementType.GetDisplayString()));
}
else if (factoryMethodFault is not null)
{
var loc = factoryMethod?.Locations.FirstOrDefault() ?? resultMap.ElementType.Locations.FirstOrDefault();
ctx.ReportDiagnostic(Diagnostic.Create(factoryMethodFault, loc, resultMap.ElementType.GetDisplayString()));
}
else if (resultMap.Members.IsDefaultOrEmpty && IsPublicOrAssemblyLocal(resultType, parseState, out _))
{
// there are so settable members + there is no constructor to use
Expand Down
90 changes: 65 additions & 25 deletions src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@ private static void WriteRowFactory(in GenerateState context, CodeWriter sb, ITy

var members = map.Members;

if (members.IsDefaultOrEmpty && map.Constructor is null)
if (members.IsDefaultOrEmpty && map.Constructor is null && map.FactoryMethod is null)
{
// error is emitted, but we still generate default RowFactory to not emit more errors for this type
WriteRowFactoryHeader();
Expand All @@ -648,8 +648,13 @@ private static void WriteRowFactory(in GenerateState context, CodeWriter sb, ITy
}

var hasInitOnlyMembers = members.Any(member => member.IsInitOnly);
var hasGetOnlyMembers = members.Any(member => member.IsGettable && !member.IsSettable && !member.IsInitOnly);
var useDeferredConstruction = map.Constructor is not null || hasInitOnlyMembers || hasGetOnlyMembers;
var hasGetOnlyMembers = members.Any(member => member is { IsGettable: true, IsSettable: false, IsInitOnly: false });
var useConstructorDeferred = map.Constructor is not null;
var useFactoryMethodDeferred = map.FactoryMethod is not null;

// Implementation detail:
// constructor takes advantage over factory method.
var useDeferredConstruction = useConstructorDeferred || useFactoryMethodDeferred || hasInitOnlyMembers || hasGetOnlyMembers;

WriteRowFactoryHeader();

Expand Down Expand Up @@ -704,7 +709,8 @@ void WriteReadMethod()
sb.Append("public override ").Append(type).Append(" Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan<int> tokens, int columnOffset, object? state)").Indent().NewLine();

int token = 0;
var constructorArgumentsOrdered = new SortedList<int, string>();
var deferredMethodArgumentsOrdered = new SortedList<int, string>();

if (useDeferredConstruction)
{
// don't create an instance now, but define the variables to create an instance later like
Expand All @@ -720,12 +726,14 @@ void WriteReadMethod()
if (Inspection.CouldBeNullable(member.CodeType)) sb.Append(CodeWriter.GetTypeName(member.CodeType.WithNullableAnnotation(NullableAnnotation.Annotated)));
else sb.Append(CodeWriter.GetTypeName(member.CodeType));
sb.Append(' ').Append(variableName).Append(" = default;").NewLine();

// filling in the constructor arguments in first iteration through members
// will be used afterwards to create the instance
if (member.ConstructorParameterOrder is not null)

if (useConstructorDeferred && member.ConstructorParameterOrder is not null)
{
constructorArgumentsOrdered.Add(member.ConstructorParameterOrder.Value, variableName);
deferredMethodArgumentsOrdered.Add(member.ConstructorParameterOrder.Value, variableName);
}
else if (useFactoryMethodDeferred && member.FactoryMethodParameterOrder is not null)
{
deferredMethodArgumentsOrdered.Add(member.FactoryMethodParameterOrder.Value, variableName);
}

token++;
Expand Down Expand Up @@ -786,31 +794,53 @@ void WriteReadMethod()

if (useDeferredConstruction)
{
// create instance using constructor. like
// create instance using constructor or factory method. like
// ```
// return new Type(member0, member1, member2, ...)
// {
// SettableMember1 = member3,
// SettableMember2 = member4,
// }
// ```

sb.Append("return new ").Append(type);
if (map.Constructor is not null && constructorArgumentsOrdered.Count != 0)
// or in case of factory method:
// return Type.Create(member0, member1, member2, ...)
// ```

if (useConstructorDeferred)
{
// write `(member0, member1, member2, ...)` part of constructor
sb.Append('(');
foreach (var constructorArg in constructorArgumentsOrdered)
{
sb.Append(constructorArg.Value).Append(", ");
}
sb.RemoveLast(2); // remove last ', ' generated in the loop
// `return new Type(member0, member1, member2, ...);`
sb.Append("return new ").Append(type).Append('(');
WriteDeferredMethodArgs();
sb.Append(')');
WriteDeferredInitialization();
sb.Append(";").Outdent();
}

// if all members are constructor arguments, no need to set them again
if (constructorArgumentsOrdered.Count != members.Length)
else if (useFactoryMethodDeferred)
{
// `return Type.FactoryCreate(member0, member1, member2, ...);`
sb.Append("return ").Append(type)
.Append('.').Append(map.FactoryMethod!.Name).Append('(');
WriteDeferredMethodArgs();
sb.Append(')').Append(";").Outdent();
}
else
{
// left case is GetOnly or InitOnly - we can use only init syntax like:
// return new Type
// {
// Member1 = value1,
// Member2 = value2
// }
sb.Append("return new ").Append(type);
WriteDeferredInitialization();
sb.Append(";").Outdent();
}

void WriteDeferredInitialization()
{
// if all members are constructor arguments, no need to set them again
if (deferredMethodArgumentsOrdered!.Count == members.Length) return;

sb.Indent().NewLine();
token = -1;
foreach (var member in members)
Expand All @@ -821,8 +851,18 @@ void WriteReadMethod()
}
sb.Outdent(withScope: false).Append("}");
}

sb.Append(";").Outdent();

void WriteDeferredMethodArgs()
{
if (deferredMethodArgumentsOrdered!.Count == 0) return;

// write `member0, member1, member2, ...` part of method
foreach (var constructorArg in deferredMethodArgumentsOrdered!)
{
sb.Append(constructorArg.Value).Append(", ");
}
sb.RemoveLast(2); // remove last ', ' generated in the loop
}
}
else
{
Expand Down
Loading