Skip to content

Instantly share code, notes, and snippets.

@warappa
Last active June 17, 2019 15:32
Show Gist options
  • Save warappa/f1fa1a46857eb9c144ced518f250dcbd to your computer and use it in GitHub Desktop.
Save warappa/f1fa1a46857eb9c144ced518f250dcbd to your computer and use it in GitHub Desktop.
Using global value converters with Entity Framework Core 2.2.5. Based on Andrew Lock's StronglyTypedIdValueConverterSelector https://andrewlock.net/strongly-typed-ids-in-ef-core-using-strongly-typed-entity-ids-to-avoid-primitive-obsession-part-4/
using Microsoft.EntityFrameworkCore;
namespace GlobalValueConverterSample
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Owned<ArticleId>()
.HasConversion(x => x.Value, x => new ArticleId(x));
base.OnModelCreating(modelBuilder);
}
}
public class ArticleId
{
public Guid Value { get; private set; }
public ArticleId(Guid value)
{
Value = value;
}
}
}
public class ConverterValueGenerator : ValueGenerator<object>
{
private ValueConverter converter;
public ConverterValueGenerator(ValueConverter converter)
{
this.converter = converter;
}
public override object Next(EntityEntry entry)
{
return converter.ConvertFromProvider(Guid.NewGuid());
}
public override bool GeneratesTemporaryValues => false;
}
public class ConverterValueGeneratorSelector : ValueGeneratorSelector
{
// The dictionary in the base type is private, so we need our own one here.
private static readonly ConcurrentDictionary<Type, ValueGenerator> _generators
= new ConcurrentDictionary<Type, ValueGenerator>();
public ConverterValueGeneratorSelector(ValueGeneratorSelectorDependencies dependencies)
: base(dependencies)
{
}
public override ValueGenerator Select(IProperty property, IEntityType entityType)
{
if (!_generators.TryGetValue(property.ClrType, out var generator))
{
if (GlobalValueConverterSelector.TryGetConverter(property.ClrType, out var converter))
{
generator = new ConverterValueGenerator(converter);
_generators.TryAdd(property.ClrType, generator);
return generator;
}
}
else
{
generator = base.Select(property, entityType);
_generators.TryAdd(property.ClrType, generator);
}
return generator;
}
}
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace GlobalValueConverterSample
{
public static class DbContextOptionsBuilderValueConverterExtensions
{
public static DbContextOptionsBuilder UseGlobalValueConverterSelector(this DbContextOptionsBuilder builder)
{
builder.ReplaceService<IValueConverterSelector, GlobalValueConverterSelector>();
builder.ReplaceService<IValueGeneratorSelector, ConverterValueGeneratorSelector>();
return builder;
}
}
}
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace GlobalValueConverterSample
{
public class GlobalValueConverterSelector : ValueConverterSelector
{
// The dictionary in the base type is private, so we need our own one here.
private static readonly ConcurrentDictionary<(Type ModelClrType, Type ProviderClrType), ValueConverterInfo> _converters
= new ConcurrentDictionary<(Type ModelClrType, Type ProviderClrType), ValueConverterInfo>();
public GlobalValueConverterSelector(ValueConverterSelectorDependencies dependencies) : base(dependencies)
{ }
public override IEnumerable<ValueConverterInfo> Select(Type modelClrType, Type providerClrType = null)
{
var baseConverters = base.Select(modelClrType, providerClrType);
foreach (var converter in baseConverters)
{
yield return converter;
}
// Extract the "real" type T from Nullable<T> if required
var underlyingModelType = UnwrapNullableType(modelClrType);
var underlyingProviderType = UnwrapNullableType(providerClrType);
if (_converters.TryGetValue((underlyingModelType, underlyingProviderType), out var globalConverter))
{
yield return globalConverter;
}
}
private static Type UnwrapNullableType(Type type)
{
if (type is null) { return null; }
return Nullable.GetUnderlyingType(type) ?? type;
}
public static void RegisterValueConverter<TValueConverter>(Type modelClrType, Type providerClrType)
where TValueConverter : ValueConverter
{
Func<ValueConverterInfo, ValueConverter> factory =
info => (ValueConverter)Activator.CreateInstance(typeof(TValueConverter), info.MappingHints);
_converters.TryAdd((modelClrType, providerClrType), new ValueConverterInfo(modelClrType, typeof(Guid), factory));
}
public static void RegisterValueConverter<TProperty, TProvider>(ValueConverter<TProperty, TProvider> valueConverter)
{
Func<ValueConverterInfo, ValueConverter> factory =
info => valueConverter;
_converters.TryAdd((typeof(TProperty), typeof(TProvider)), new ValueConverterInfo(typeof(TProperty), typeof(TProvider), factory));
// also register for cases where the underlying provider type could not be resolved
_converters.TryAdd((typeof(TProperty), null), new ValueConverterInfo(typeof(TProperty), typeof(TProvider), factory));
}
public static bool TryGetConverter(Type modelClrType, out ValueConverter converter)
{
if (!_converters.TryGetValue((modelClrType, typeof(Guid)), out var valueConverterInfo))
{
converter = null;
return false;
}
converter = valueConverterInfo.Create();
return true;
}
}
}
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace GlobalValueConverterSample
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
var connectionString = $"Data Source=app.db";
services.AddEntityFrameworkSqlite()
.AddDbContext<AppDbContext>(x =>
{
x.UseSqlite(connectionString);
x.UseGlobalValueConverterSelector(); // this line matters
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc();
}
}
}
using System;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace GlobalValueConverterSample
{
public static class ValueConverterExtensions
{
public static ModelBuilder HasConversion<TProperty, TProvider>(
this ModelBuilder modelBuilder,
Expression<Func<TProperty, TProvider>> convertToProviderExpression,
Expression<Func<TProvider, TProperty>> convertFromProviderExpression)
{
var valueConverter = new ValueConverter<TProperty, TProvider>(convertToProviderExpression, convertFromProviderExpression);
GlobalValueConverterSelector.RegisterValueConverter(valueConverter);
return modelBuilder;
}
public static OwnedEntityTypeBuilder<TProperty> HasConversion<TProperty, TProvider>(
this OwnedEntityTypeBuilder<TProperty> ownedEntityTypeBuilder,
Expression<Func<TProperty, TProvider>> convertToProviderExpression,
Expression<Func<TProvider, TProperty>> convertFromProviderExpression)
{
var valueConverter = new ValueConverter<TProperty, TProvider>(convertToProviderExpression, convertFromProviderExpression);
GlobalValueConverterSelector.RegisterValueConverter(valueConverter);
return ownedEntityTypeBuilder;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment