henkmollema / Dapper-FluentMap

Provides a simple API to fluently map POCO properties to database columns when using Dapper.
MIT License
427 stars 86 forks source link

data not mapping #112

Closed WellspringCS closed 4 years ago

WellspringCS commented 4 years ago

I've been scratching my head for the past 6 hours. I cannot get the convention-based approach to work. My db is in Postgres so it's the standard "convert-to-snake-case" situation.

Clues along the way: CustomTableNameResolver() does run. A stop in the code is reached. ColumnConventionMapper code never runs. (Might only get run when I use SqlBuilder.

Any thoughts? My startup.cs code includes

  FluentMapper.Initialize(config =>
  {
  // this works, but is manual
    config.AddMap(new ClientMap());

    // this does not work at all.
    // config.AddConvention<PropertyTransformConvention>()
    //   .ForEntity<Client>();
    config.ForDommel();
  });
  DommelMapper.SetColumnNameResolver(new ColumnConventionMapper());
  DommelMapper.SetTableNameResolver(new CustomTableNameResolver());

as noted in above code comments, this works...

config.AddMap(new ClientMap());

...but requires a manual setup that I'm trying to avoid.

I am sure the above looks too much like a mix-and-match but since nothing is working, I've been trying every combination I can think of.

I would love to be able to (instead of config.AddMap(new ClientMap()) ) use this:

public class PropertyTransformConvention : Convention
{
    public PropertyTransformConvention()
    {
        Properties()
            .Configure(c => c.Transform(s => Regex.Replace(input: s, pattern: "([A-Z])([A-Z][a-z])|([a-z0-9])([A-Z])", replacement: "$1$3_$2$4")));
    }
}

and then call ...

var x = (await db.GetAllAsync<Client>()).ToList();

but per my comments, the operation does not map data from the receiving entity into my Client class. Is there anything obvious that I'm doing wrong? If not (or maybe regardless) is there anywhere a working demo project? I have hunted high and low for example code and am coming up empty handed.

WellspringCS commented 4 years ago

I'm also confused regarding the relationship between FluentMapper.Initialize() and DommelMapper.

I find that I must use DommelMapper to get table names corrected automagically. But DommelMapper does not run any code for the column names, though I have it set up to do so.

Once again, I am sure a working demo would set me straight. I just cannot find one.

henkmollema commented 4 years ago

Which versions of Dommel and Dapper FluentMap are you using?

WellspringCS commented 4 years ago
    <PackageReference Include="Dapper" Version="2.0.35" />
    <PackageReference Include="Dapper.FluentMap.Dommel" Version="1.7.0" />

I don't have any other packages specific to this topic. You're not going to tell me it's a missing include? Yikes. During the churn I at one point removed this:

    <PackageReference Include="Dapper.FluentMap" Version="1.8.0" />
    <PackageReference Include="Dommel" Version="1.11.0" />

If there's a magic combo, I'm on the edge of my seat to know it. :)

henkmollema commented 4 years ago

Hmm, the 1.x versions are supposed to work together. Could you try that?

WellspringCS commented 4 years ago

I'm so desperate, I will try anything ... but while I'm trying, could you clarify? What includes are you suggesting? Something like...

    <PackageReference Include="Dapper" Version="1.60.6" />
    <PackageReference Include="Dapper.FluentMap.Dommel" Version="1.7.0" />

If it speaks to my desperation, I was reading the code in here just now: https://github.com/StackExchange/Dapper/pull/1349/files

...Wondering if a new version of Dapper is about to provide some of this functionality internally.

henkmollema commented 4 years ago

That seems fine. This does not however:

DommelMapper.SetColumnNameResolver(new ColumnConventionMapper());
DommelMapper.SetTableNameResolver(new CustomTableNameResolver());

What are those?

WellspringCS commented 4 years ago
public class CustomTableNameResolver : DommelMapper.ITableNameResolver
{
  public string ResolveTableName(Type type)
  {
    return Regex.Replace(input: type.Name,
      pattern: "([A-Z])([A-Z][a-z])|([a-z0-9])([A-Z])", replacement: "$1$3_$2$4").ToLower() + "s";
  }
}

public class ColumnConventionMapper : DommelMapper.IColumnNameResolver
{
  public string ResolveColumnName(PropertyInfo propertyInfo)
  {
    return Regex.Replace(input: propertyInfo.Name,
      pattern: "([A-Z])([A-Z][a-z])|([a-z0-9])([A-Z])", replacement: "$1$3_$2$4").ToLower();
  }
}

or were you meaning why am I using them?

WellspringCS commented 4 years ago

Sorry to be providing info by drip feed. is there a demo project anywhere that shows a working sample (even a tiny one) that from end-to-end handles Dommel's version of Contrib as well as fluent translation of entities to Postgres snake_case from standard C# classes?

WellspringCS commented 4 years ago

Ok, so to share a minor victory and maybe shed light... This actually works:

<PackageReference Include="Dapper" Version="1.60.6" />
<PackageReference Include="Dapper.FluentMap.Dommel" Version="1.7.0" />
...
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
FluentMapper.Initialize(config =>
{
  config.AddMap(new ClientMap());
  config.AddMap(new VwClientMap());
  config.ForDommel();
});
...
DommelMapper.SetTableNameResolver(new CustomTableNameResolver());
...
var x = (await _dapperContext.Connection().GetAllAsync<VwClient>()).ToList();

whereas if you comment out the SetTableNameResolver() call, the above code fails.

GraphQL.ExecutionError: Error trying to resolve clients
henkmollema commented 4 years ago

Did you take a look at the docs? You should either use

Dapper.DefaultTypeMap.MatchNamesWithUnderscores

Or

FluentMapper.Initialize

Don't use DommelMapper.SetTableNameResolver directly. You should create a convention and add that to the Initialize method.

WellspringCS commented 4 years ago

@henkmollema if I had been doing this for years or even months, I'd remember exactly where and how my problems arose, but my background in FluentMapper/Dommel/Contrib is measured in hours/days. Even where Dapper is concerned my depth has been shallow. So I cannot elaborate with clarity on all routes taken.

THAT SAID...

My experience with Dapper.DefaultTypeMap.MatchNamesWithUnderscores and understanding of it is that it works one-way. It's not going to solve my insert/update issues. (Note lament at end of this one)

FluentMapper.Initialize is what i worked with for some 10-15 hours over the past few days. No combination that I could find worked. It wasn't for lack of effort.

I'm very thankful for Dapper and your contributions to the community. I really did want to use Dommel/FluentMapper. However I simply couldn't figure out how to make it work. If there were a working example out there, I'd have mimicked it and saved 2 days. Sadly, I'm not able to see one.

If you happen to know of example code that uses Dapper/Dommel/FluentMapper so as to manage the back-and-forth between C# standard syntax (AwesomePascalCasing) and Postgres snake_in_the_grass (am I tipping my hand regarding my preferences?) I'd be all over it!

Right now I'm writing up a customized column-mapping solution for my project. I don't know what else to do.

henkmollema commented 4 years ago

There is a sample at https://github.com/henkmollema/Dapper-FluentMap#convention-based-mapping. If that doesn't work please let me know and I will try it tomorrow.

WellspringCS commented 4 years ago

@henkmollema that was the code I was staring at all yesterday. Could very much be entirely my failure to see what I was doing wrong, but again, it wasn't for lack of reading. :)

WellspringCS commented 4 years ago

BTW I am certain that I tried for many hours on variations that did not include Dapper.DefaultTypeMap.MatchNamesWithUnderscores -- it was because that wasn't going to work with Contrib nor Dommel for that matter that I attempted to use a clean combo of just Dommel/FluentMapper . Thus my comment on SO here...

henkmollema commented 4 years ago

It seems that conventions aren't working with Dommel. I'm releasing a fix in a few hours.

WellspringCS commented 4 years ago

I'll definitely give it a whirl when it's ready. Thx in advance.

henkmollema commented 4 years ago

Dapper.FluentMap.Dommel version 1.7.1 should fix it. This works for me:

class Program
{
    static void Main(string[] args)
    {
        FluentMapper.Initialize(config =>
        {
            config.AddConvention<PropertyTransformConvention>()
                .ForEntity<Product>();

            config.ForDommel();
        });

        using var con = new MySqlConnection("...");
        con.Execute("create table if not exists Products (id int not null auto_increment, product_name varchar(255), primary key (Id))");

        var id = con.Insert(new Product
        {
            ProductName = "Foo"
        });

        var product = con.Get<Product>(id);
        Console.WriteLine(product?.ProductName ?? $"Product {id} not found");
        Console.ReadKey();
    }
}

public class Product
{
    public int Id { get; set; }

    public string ProductName { get; set; }
}

public class PropertyTransformConvention : Convention
{
    public PropertyTransformConvention()
    {
        Properties()
            .Configure(c => c.Transform(s =>
                Regex.Replace(input: s, pattern: "([A-Z])([A-Z][a-z])|([a-z0-9])([A-Z])", replacement: "$1$3_$2$4").ToLower()));
    }
}
WellspringCS commented 4 years ago

Cool! I'll take a look and let you know how I fared. I'm wrapping up my custom fallback option but will hit this today. Thx @henkmollema

WellspringCS commented 4 years ago

@henkmollema I've had the chance to try out 1.7.1 now. Notes below.

I'm not sure what I might be doing improperly, but I can say that the route shown here was the one that worked for my code, whereas others simply did not. (Once again, I did try a number of flavors...)

Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
FluentMapper.Initialize(config =>
{
  config.AddConvention<PropertyTransformConvention>()
    .ForEntity<Client>()
    .ForEntity<VwClient>();
  config.ForDommel();
});
DommelMapper.SetKeyPropertyResolver(new CustomKeyPropertyResolver());
DommelMapper.SetColumnNameResolver(new ColumnConventionMapper());
DommelMapper.SetTableNameResolver(new CustomTableNameResolver());
...
...
public class PropertyTransformConvention : Convention
{
  public PropertyTransformConvention()
  {
    Properties()
      .Configure(c => c.Transform(s => Regex.Replace(input: s,
        pattern: "([A-Z])([A-Z][a-z])|([a-z0-9])([A-Z])", replacement: "$1$3_$2$4")));
  }
}

public class CustomKeyPropertyResolver : DommelMapper.IKeyPropertyResolver
{
  public PropertyInfo ResolveKeyProperty(Type type, out bool x)
  {
    x = false;
    string xy = type.Name;
    if (xy.Substring(0, 2) == "Vw") xy = xy[2..];
    var z = type.GetProperties().Single(p => p.Name == $"{xy}Id");
    return z;
  }

  public PropertyInfo ResolveKeyProperty(Type type)
  {
    return type.GetProperties().Single(p => p.Name == $"{type.Name}Id");
  }
}

public class CustomTableNameResolver : DommelMapper.ITableNameResolver
{
  public string ResolveTableName(Type type)
  {
    return Regex.Replace(input: type.Name,
      pattern: "([A-Z])([A-Z][a-z])|([a-z0-9])([A-Z])", replacement: "$1$3_$2$4").ToLower() + "s";
  }
}

public class ColumnConventionMapper : DommelMapper.IColumnNameResolver
{
  public string ResolveColumnName(PropertyInfo propertyInfo)
  {
    return Regex.Replace(input: propertyInfo.Name,
      pattern: "([A-Z])([A-Z][a-z])|([a-z0-9])([A-Z])", replacement: "$1$3_$2$4").ToLower();
  }
}

FWIW my table was really a view, so the postgres name was public.vw_clients and this needed to be mapped to VwClients. And because the name was VwClients for the entity, further massaging was necessary to arrive at the proper key. I've forgotten, honestly, but as I write this, I am wondering if several issues would have been avoidable if I provided attributes for the class objects ([Key], etc.)

Short is, it did work. I was able to retrieve and update data as well. So your fix definitely made things possible that weren't before.

Thx!