Efcore: Do I need to specify a Column Size for the GuidToString ValueConverter?

Created on 20 Oct 2020  路  3Comments  路  Source: dotnet/efcore

Hi,

I have solved a problem that was vexing me, and I would like to verify that I have the correct solution. If I have solved this problem correctly, I strongly think that we need to update the relevant EF Core docs on this issue, as it's super frustrating to solve this.

Problem Scenario:

I need to access a table in a third-party-managed database, that looks like this:

CREATE TABLE [dbo].[tbComputerTarget](
    [TargetID] [int] NOT NULL,
    [ComputerID] [nvarchar](256) NOT NULL,
    [LastSyncTime] [datetime] NULL
)

There is a UNIQUE Index constraint on the [ComputerID] column.
All string values stored in the [ComputerID] column are non-Null fully-parseable GUIDs.
(There are also a lot of other columns that I am ignoring).

I cannot change the schema, I just need to read it. I am building a code-first model of it -- the database has hundreds of tables and views, and I need to just read a few values from from a single table.

My Model is very simple:
```C#
public class WsusClientInfo
{
public Guid WsusClientId { get; set; } = Guid.Empty;
public DateTime LastSyncTime? { get; private set; } = DateTime.MinValue;
}

public class WsusClientInfoConfiguration : IEntityTypeConfiguration<WsusClientInfo>
{
    public void Configure(EntityTypeBuilder<WsusClientInfo> builder)
    {
        builder.ToTable("tbComputerTarget");

        builder.HasKey(w => w.WsusClientId);

        builder.Property(w => w.WsusClientId)
            .HasConversion<GuidToStringConverter>()
            .HasColumnName("ComputerID")
            .HasMaxLength(256)
            .ValueGeneratedNever();


However this yields a runtime Exception in the `ModelValidator`:

Message: The property 'WsusClientInfo.WsusClientId' is of type 'Guid' which is not supported by current database provider. Either change the property CLR type or ignore the property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.

at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.ValidatePropertyMapping(IModel model, IDiagnosticsLogger1 logger) at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.Validate(IModel model, IDiagnosticsLogger1 logger)
at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.Validate(IModel model, IDiagnosticsLogger1 logger) at Microsoft.EntityFrameworkCore.SqlServer.Internal.SqlServerModelValidator.Validate(IModel model, IDiagnosticsLogger1 logger)
at Microsoft.EntityFrameworkCore.Metadata.Conventions.ValidatingConvention.ProcessModelFinalized(IConventionModelBuilder modelBuilder, IConventionContext`1 context)
at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.ImmediateConventionScope.OnModelFinalized(IConventionModelBuilder modelBuilder)
at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.OnModelFinalized(IConventionModelBuilder modelBuilder)
at Microsoft.EntityFrameworkCore.Metadata.Internal.Model.FinalizeModel()
at Microsoft.EntityFrameworkCore.ModelBuilder.FinalizeModel()
at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.CreateModel(DbContext context, IConventionSetBuilder conventionSetBuilder)
at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.GetModel(DbContext context, IConventionSetBuilder conventionSetBuilder)
at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel()
at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model()
at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServicesBuilder.<>c.b__7_3(IServiceProvider p)
...


It does work when I use type `string` for the `WsusClientId`, but I shouldn't need to - the ValueConverter should work in this scenario.


### Provider and Version Information

EF Core version:  3.1.6
Database provider:  Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET Core 3.1  (C# with nullable refs enabled)
Operating system:  Windows 10
IDE:  Visual Studio 2019 16.7

## My Solution

I have come up with the following solution, which seems to work:

```C#
    public class WsusClientInfo
    {
        public Guid WsusClientId { get; set; } = Guid.Empty;
        public DateTime LastSyncTime? { get; private set; } = DateTime.MinValue;
    }

    public class WsusClientInfoConfiguration : IEntityTypeConfiguration<WsusClientInfo>
    {
        public void Configure(EntityTypeBuilder<WsusClientInfo> builder)
        {
            builder.ToTable("tbComputerTarget");

            builder.HasKey(w => w.WsusClientId);

            GuidToStringConverter wsusGuidToWSUSDBComputerId = new GuidToStringConverter(mappingHints: new ConverterMappingHints(size: 256));

            builder.Property(w => w.WsusClientId)
                .HasConversion(wsusGuidToWSUSDBComputerId)
                .HasColumnName("ComputerID")
                .HasMaxLength(256)
                .ValueGeneratedNever();

If this solution is correct, the page listing EF Core's in-built ValueConverters should be updated to specify that these defaults are highly dependent on column width, and will fail if the actual column sizes do not match this default.

Thanks,
Milton.

closed-question customer-reported

Most helpful comment

@TarquinQ You could create an instance of that converter directly and pass it to the overload of HasConversion that accepts a converter instance. However, this is usually done for custom value converters, rather than for conversions between common types where, as you say, the subsystem takes care of choosing a built-in converter.

Interestingly, the same Guid to string mapping can be achieved using store types directly through:

```C#
b.Property(w => w.WsusClientId)
.HasColumnName("ComputerID")
.HasColumnType("nvarchar(256)")
.ValueGeneratedNever();


or 

```C#
    [Column("ComputerID", TypeName = "nvarchar(256)")]
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public Guid WsusClientId { get; set; } = Guid.Empty;

All 3 comments

@TarquinQ The generic type for HasConversion<TProvider> is the type to convert to, not the converter type. This works:

C# b.Property(w => w.WsusClientId) .HasConversion<string>() .HasColumnName("ComputerID") .HasMaxLength(256) .ValueGeneratedNever();

Note for triage: consider throwing a better exception when a converter is used as the generic type.

Thanks, that is quite helpful.... but now this leaves me wondering what does the GuidToStringConverter actually do then? Or equally, when would it get used? Will I ever need to use it, or is it something used internally by the ValueConverter subsystem?

@TarquinQ You could create an instance of that converter directly and pass it to the overload of HasConversion that accepts a converter instance. However, this is usually done for custom value converters, rather than for conversions between common types where, as you say, the subsystem takes care of choosing a built-in converter.

Interestingly, the same Guid to string mapping can be achieved using store types directly through:

```C#
b.Property(w => w.WsusClientId)
.HasColumnName("ComputerID")
.HasColumnType("nvarchar(256)")
.ValueGeneratedNever();


or 

```C#
    [Column("ComputerID", TypeName = "nvarchar(256)")]
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public Guid WsusClientId { get; set; } = Guid.Empty;
Was this page helpful?
0 / 5 - 0 ratings