Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dapper works for a while, then can't find constructor #2142

Open
jimAtLoyal opened this issue Jan 21, 2025 · 9 comments
Open

Dapper works for a while, then can't find constructor #2142

jimAtLoyal opened this issue Jan 21, 2025 · 9 comments

Comments

@jimAtLoyal
Copy link

Dapper 2.1.35
Dapper.Contrib 2.0.78

We have two pods running the same image in K8s that query a table and map to an object using Dapper. Both will work fine for a while, then one will start consistently getting this error. The object has the four properties show in the constructor in the error message and two properties with [Compute]. We are using select * in the query and are going to remove that and see if that helps.

System.InvalidOperationException: A parameterless default constructor or one matching signature (System.Int32 Id, System.String Name, System.Boolean IsActive, System.DateTime LastUsedDate) is required for Reputation.Core.Models.Schema.Tag materialization
  File "/_/Dapper/SqlMapper.cs", line 3473, col 25, in void SqlMapper.GenerateDeserializerFromMap(Type type, DbDataReader reader, int startBound, int length, bool returnNullIfFirstMissing, ILGenerator il)
  File "/_/Dapper/SqlMapper.cs", line 3304, col 17, in Func<DbDataReader, object> SqlMapper.GetTypeDeserializerImpl(Type type, DbDataReader reader, int startBound, int length, bool returnNullIfFirstMissing)
  File "/_/Dapper/SqlMapper.TypeDeserializerCache.cs", line 151, col 17, in Func<DbDataReader, object> TypeDeserializerCache.GetReader(DbDataReader reader, int startBound, int length, bool returnNullIfFirstMissing)
  File "/_/Dapper/SqlMapper.TypeDeserializerCache.cs", line 50, col 17, in Func<DbDataReader, object> TypeDeserializerCache.GetReader(Type type, DbDataReader reader, int startBound, int length, bool returnNullIfFirstMissing)
  File "/_/Dapper/SqlMapper.cs", line 1941, col 17, in Func<DbDataReader, object> SqlMapper.GetDeserializer(Type type, DbDataReader reader, int startBound, int length, bool returnNullIfFirstMissing)
  File "/_/Dapper/SqlMapper.Async.cs", line 442, col 21, in async Task<IEnumerable<T>> SqlMapper.QueryAsync<T>(IDbConnection cnn, Type effectiveType, CommandDefinition command)
  File "/app/Reputation.Core/Repositories/LookupDataRepository.cs", line 28, col 9, in async Task<IEnumerable<Tag>> LookupDataRepository.GetTagsAsync(string clientId)
  File "/app/Reputation.Core/Services/TagService.cs", line 29, col 9, in async Task<IEnumerable<Tag>> TagService.GetAllAsync()
  File "Controllers/TagController.cs", line 37, col 13, in async Task<ActionResult<IEnumerable<LookupData>>> TagController.GetTags()+GetTagData(?)
  File "Controllers/ReputationBaseController.cs", line 21, col 13, in async Task<ActionResult<T>> ReputationBaseController.DoActionAsync<T>(AsyncActionDelegate<T> asyncAction)

We have a constructor that maps the signature. To try to fix it, we added a default constructor and now it doesn't error out, but we get back n objects with default values.

The other pod running the same image continues to work and will return n objects with values from the db. Recycling the pod fixes it -- for a while.

It's as if the generated mapping worked, then got regenerated, but fails.

To Reproduce
I wish I could tell you more. Running locally it never happens. Only in prod (of course) in AKS. Non-prod AKS doesn't seem to have the problem.

Expected and actual behavior
The mapping to the object should not throw an exception and fill out the object

Additional context
Add any other context about the problem here:

  • Azure SQL
  • ASP.NET 8.0 base image
  • AKS on node running Ubuntu 22.04.4 LTS
@mgravell
Copy link
Member

Random question: are the columns perhaps in a different order on the two systems, using select * (which preserves original DB column order)? Dapper "vanilla" is fussy about column order in this scenario, which isn't ideal - Dapper AOT is far more forgiving! (and is usually just a "turn it on" switchover). Without seeing the exact schema and type, it is hard to speculate about exactly why it isn't matching.

@jimAtLoyal
Copy link
Author

It's the same Docker image, running in the same K8s environment, just scaled out to 2 instances. The obfuscated query is like this. We are testing without t.* . The class follows

            SELECT t.*, x.[LastUsedDate]
            FROM ... t
            LEFT JOIN (
                SELECT rt.TagId, Max(rt.UpdateDate) as [LastUsedDate]
                FROM ...
            ) x
                ON x.TagId = t.Id;

[System.ComponentModel.DataAnnotations.Schema.Table("Reputation.Tags")]
public class Tag : IEquatable<Tag>, IAuditable
{

    public int Id { get; init; }
    public string Name { get; init; } = string.Empty;
    
    public bool IsActive { get; set; }
    
    [Sortable(IsDefault = true)]
    [Write(false)]
    public DateTime? LastUsedDate { get; }
    
    [Computed]
    public string AuditEntityId => Id.ToString();
    [Computed]
    public string AuditEntityType => this.GetType().Name ?? "";
    
    protected Tag() { }
    
    public Tag(int id, string name, bool isActive = true, DateTime? lastUsedDate = null)
    {
        Id = id;
        Name = name;
        IsActive = isActive;
        LastUsedDate = lastUsedDate;
    }

    public static Tag Create(string name) => new(default, name, true, DateTime.MinValue.ToUniversalTime());

    #region IEquatable methods
<snip>
};

@jimAtLoyal
Copy link
Author

Looking at the FindConstructor method in Dapper and the error message kicked out after it returns null here it looks like there's a match of name, type, and order.

// Error message parameters 
(System.Int32 Id, System.String Name, System.Boolean IsActive,  System.DateTime LastUsedDate)
// constructor
(         int id,        string name,           bool isActive = true, DateTime? lastUsedDate = null)

@jimAtLoyal
Copy link
Author

After changing the select * to explicit columns, we haven't had issues for two days. It's a mystery as to why it would work for a while then start failing in one pod.

@mgravell
Copy link
Member

That's good guidance generally - in fact, if you install either Dapper.AOT or Dapper.Advisor, it will (if it understands your SQL variant) offer DAP219 warnings on SQL with select *

@jimAtLoyal jimAtLoyal reopened this Feb 5, 2025
@jimAtLoyal
Copy link
Author

jimAtLoyal commented Feb 5, 2025

It ran several days without any problems, but came back after the pods ran for 5 days. Any other hints/tips would be appreciated.

@mgravell
Copy link
Member

mgravell commented Feb 5, 2025

My suggestion would be : try enabling AOT, even if just on that one path - the AOT path never has to resolve anything - the analysis happens at build time: https://aot.dapperlib.dev/gettingstarted

@jimAtLoyal
Copy link
Author

We could try it, but couldn't use it in production, since doesn't that require interceptors, which is still in preview until 9.0.2xx according to https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md

Latest is 9.0.102.

It's so weird that it runs for days fine then suddenly can't find the constructor.

@mgravell
Copy link
Member

mgravell commented Feb 6, 2025

If your policy prohibits you from flipping that switch, then I guess we'll have to wait. I would suggest perhaps trying it locally to see if there are any hurdles, to minimise delays. I have no explanation for why it loses the constructor, but I do know that AOT doesn't ever need to go looking for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants