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

Mechanism/API to specify a default conversion for any property of a given type in the model #10784

Open
ajcvickers opened this issue Jan 27, 2018 · 10 comments
Assignees
Milestone

Comments

@ajcvickers
Copy link
Member

@ajcvickers ajcvickers commented Jan 27, 2018

Value conversions were introduced by #242. Currently conversions are only set per-property, although bulk configuration can be used at the end of OnModelCreating in the normal way. This issue is about adding some kind of mechanism to set the conversion for all properties of a given type in the model. This could be through configuration on the model, or it could be via improved bulk config APIs.

@ajcvickers

This comment has been minimized.

Copy link
Member Author

@ajcvickers ajcvickers commented Jun 8, 2018

Note that the converter isn't really for "every property" even though it will also have that effect. It should also be for any time the CLR type is used in the query--that is, it is really a custom type mapping setup by the application.

@bugproof

This comment has been minimized.

Copy link

@bugproof bugproof commented Aug 24, 2018

As a temporary workaround you can use this extension method(I think it works):

public static class ModelBuilderExtensions
{
    public static ModelBuilder UseValueConverterForType<T>(this ModelBuilder modelBuilder, ValueConverter converter)
    {
        return modelBuilder.UseValueConverterForType(typeof(T), converter);
    }

    public static ModelBuilder UseValueConverterForType(this ModelBuilder modelBuilder, Type type, ValueConverter converter)
    {
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            // note that entityType.GetProperties() will throw an exception, so we have to use reflection 
            var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == type);
            foreach (var property in properties)
            {
                modelBuilder.Entity(entityType.Name).Property(property.Name)
                    .HasConversion(converter);
            }
        }

        return modelBuilder;
    }
}
@ajcvickers

This comment has been minimized.

Copy link
Member Author

@ajcvickers ajcvickers commented Aug 24, 2018

@ZeroNightzz What exception is thrown by entityType.GetProperties()?

@bugproof

This comment has been minimized.

Copy link

@bugproof bugproof commented Aug 26, 2018

@ajcvickers

System.InvalidOperationException: The property 'CurrencyExchangeRate.Currency' could not be mapped, because it is of type 'Currency' which is not a supported primitive type or a valid entity type. Either explicitly map this property, or ignore it using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.PropertyMappingValidationConvention.Apply(InternalModelBuilder modelBuilder)
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.ImmediateConventionScope.OnModelBuilt(InternalModelBuilder modelBuilder)
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.OnModelBuilt(InternalModelBuilder modelBuilder)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.Model.Validate()
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.CreateModel(DbContext context, IConventionSetBuilder conventionSetBuilder, IModelValidator validator)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.<>c__DisplayClass5_0.<GetModel>b__1()
   at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
   at System.Lazy`1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)
   at System.Lazy`1.CreateValue()
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.GetModel(DbContext context, IConventionSetBuilder conventionSetBuilder, IModelValidator validator)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel()
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model()
   at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServicesBuilder.<>c.<TryAddCoreServices>b__7_1(IServiceProvider p)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactory(FactoryCallSite factoryCallSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass1_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies()
   at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider()
   at Microsoft.EntityFrameworkCore.DbContext.Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure<System.IServiceProvider>.get_Instance()
   at Microsoft.EntityFrameworkCore.Internal.InternalAccessorExtensions.GetService[TService](IInfrastructure`1 accessor)
   at Microsoft.EntityFrameworkCore.Infrastructure.AccessorExtensions.GetService[TService](IInfrastructure`1 accessor)
   at Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations.CreateContext(Func`1 factory)
   at Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations.CreateContext(String contextType)
   at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.AddMigration(String name, String outputDir, String contextType)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigrationImpl(String name, String outputDir, String contextType)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigration.<>c__DisplayClass0_1.<.ctor>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.<Execute>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)
The property 'CurrencyExchangeRate.Currency' could not be mapped, because it is of type 'Currency' which is not a supported primitive type or a valid entity type. Either explicitly map this property, or ignore it using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.

this is caused, so I have to use GetProperties() by accessing ClrType. I think the conversion must be known before using GetProperties() defined in EF.

@ajcvickers

This comment has been minimized.

Copy link
Member Author

@ajcvickers ajcvickers commented Aug 27, 2018

@Necronux Thanks. I thought you were indicating an exception when calling the GetProperties method itself.

@AndriySvyryd

This comment has been minimized.

Copy link
Member

@AndriySvyryd AndriySvyryd commented Nov 30, 2018

The bulk configuration should be performed before set discovery as it could affect whether a property would be considered a navigation.

@ajcvickers

This comment has been minimized.

Copy link
Member Author

@ajcvickers ajcvickers commented Jan 5, 2019

Consider allowing converters to be registered only for a given provider, or only for cases where the provider doesn't have built-in support for the type being converted. See #14319

@kskalski

This comment has been minimized.

Copy link

@kskalski kskalski commented Apr 30, 2019

Is there a way to use converters (or other mechanism) to map property (or in general its type) to a navigational property (basically what using ICollection<> would provide)?

My use-case is, I would like to have a property that is an IDictionary, which acts as navigational property containing related entities accessible by their primary key. The simple approach like below doesn't seem to work, I guess it would require re-discovering nagivational properties after their type was mapped by conversions:

    class Garden {
      public IDictionary<long, Flower> Flowers { get; set; }
    }
...
    protected override void OnModelCreating(ModelBuilder modelBuilder) {
      modelBuilder.Entity<Garden>()
        .Property(m => m.Flowers)
        .HasConversion(v => v.Values, v => v.ToDictionary(flower => flower.Id, flower => flower));
    }

A dedicated API to support IDictionary or other bags would probably allow for more efficient implementation, I guess this issue #2919 was created with that in mind. But I'm curious if there is any way available right now.

@ajcvickers

This comment has been minimized.

Copy link
Member Author

@ajcvickers ajcvickers commented Apr 30, 2019

@kskalski I'm not aware of any way to do this now; as you said, #2919 is tracking this.

@BjarkeMeier

This comment has been minimized.

Copy link

@BjarkeMeier BjarkeMeier commented May 16, 2019

@backdoormanUC It works like a charm. Thx for sharing! I made a few small extensions to target my personal needs:

public static class ModelBuilderExtensions
  {
    /// <summary>
    ///   Based on BackDoorManUC's suggestion: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754.
    /// </summary>
    public static ModelBuilder UseValueConverterForType<T>(this ModelBuilder modelBuilder, ValueConverter converter, string sqlType = null, bool useTypeNameSuffix = false)
    {
      return modelBuilder.UseValueConverterForType(typeof(T), converter, sqlType, useTypeNameSuffix);
    }

    public static ModelBuilder UseValueConverterForType(this ModelBuilder modelBuilder, Type type, ValueConverter converter, string sqlType = null, bool useTypeNameSuffix = false)
    {
      foreach (var entityType in modelBuilder.Model.GetEntityTypes())
      {
        // note that entityType.GetProperties() will throw an exception, so we have to use reflection 
        var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == type);
        foreach (var property in properties)
        {
          var prop = modelBuilder.Entity(entityType.Name).Property(property.Name);
          prop.HasConversion(converter);
          if (sqlType != null)
            prop.HasColumnType(sqlType);
          if (useTypeNameSuffix)
            // Apparantly property.Name doesn't provide the current property name
            prop.HasColumnName($"{prop.Metadata.Relational().ColumnName}_{type.Name}");
        }
      }

      return modelBuilder;
    }
  }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
6 participants
You can’t perform that action at this time.