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

allow applications to use their own db context #5

Merged
merged 8 commits into from
Jul 1, 2024
52 changes: 29 additions & 23 deletions src/Crdt.Linq2db/Linq2dbKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,38 @@ public static IServiceCollection AddCrdtLinq2db(this IServiceCollection services
(provider, builder) =>
{
configureOptions.Invoke(provider, builder);
builder.UseLinqToDB(optionsBuilder =>
{
var mappingSchema = optionsBuilder.DbContextOptions.GetLinqToDBOptions()?.ConnectionOptions
.MappingSchema;
if (mappingSchema is null)
{
mappingSchema = new MappingSchema();
optionsBuilder.AddMappingSchema(mappingSchema);
}

new FluentMappingBuilder(mappingSchema).HasAttribute<Commit>(new ColumnAttribute("DateTime",
nameof(Commit.HybridDateTime) + "." + nameof(HybridDateTime.DateTime)))
.HasAttribute<Commit>(new ColumnAttribute(nameof(HybridDateTime.Counter),
nameof(Commit.HybridDateTime) + "." + nameof(HybridDateTime.Counter)))
.Entity<Commit>()
//need to use ticks here because the DateTime is stored as UTC, but the db records it as unspecified
.Property(commit => commit.HybridDateTime.DateTime).HasConversionFunc(dt => dt.UtcDateTime, dt => new DateTimeOffset(dt.Ticks, TimeSpan.Zero))
.Build();

var loggerFactory = provider.GetService<ILoggerFactory>();
if (loggerFactory is not null)
optionsBuilder.AddCustomOptions(dataOptions => dataOptions.UseLoggerFactory(loggerFactory));
});
builder.UseLinqToDbCrdt(provider);
},
configureCrdt
);
return services;
}

public static DbContextOptionsBuilder UseLinqToDbCrdt(this DbContextOptionsBuilder builder, IServiceProvider provider)
{
return builder.UseLinqToDB(optionsBuilder =>
{
var mappingSchema = optionsBuilder.DbContextOptions.GetLinqToDBOptions()?.ConnectionOptions
.MappingSchema;
if (mappingSchema is null)
{
mappingSchema = new MappingSchema();
optionsBuilder.AddMappingSchema(mappingSchema);
}

new FluentMappingBuilder(mappingSchema).HasAttribute<Commit>(new ColumnAttribute("DateTime",
nameof(Commit.HybridDateTime) + "." + nameof(HybridDateTime.DateTime)))
.HasAttribute<Commit>(new ColumnAttribute(nameof(HybridDateTime.Counter),
nameof(Commit.HybridDateTime) + "." + nameof(HybridDateTime.Counter)))
.Entity<Commit>()
//need to use ticks here because the DateTime is stored as UTC, but the db records it as unspecified
.Property(commit => commit.HybridDateTime.DateTime).HasConversionFunc(dt => dt.UtcDateTime,
dt => new DateTimeOffset(dt.Ticks, TimeSpan.Zero))
.Build();

var loggerFactory = provider.GetService<ILoggerFactory>();
if (loggerFactory is not null)
optionsBuilder.AddCustomOptions(dataOptions => dataOptions.UseLoggerFactory(loggerFactory));
});
}
}
68 changes: 34 additions & 34 deletions src/Crdt.Sample/CrdtSampleKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,48 @@
using Crdt.Linq2db;
using Crdt.Sample.Changes;
using Crdt.Sample.Models;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace Crdt.Sample;

public static class CrdtSampleKernel
{
public static IServiceCollection AddCrdtDataSample(this IServiceCollection services,
string dbPath,
bool enableProjectedTables = true)
public static IServiceCollection AddCrdtDataSample(this IServiceCollection services, string dbPath)
{
services.AddCrdtLinq2db(
(_, builder) =>
{
builder.UseSqlite($"Data Source={dbPath}");
builder.EnableDetailedErrors();
builder.EnableSensitiveDataLogging();
#if DEBUG
builder.LogTo(s => Debug.WriteLine(s));
#endif
},
config =>
{
config.EnableProjectedTables = enableProjectedTables;
config.ChangeTypeListBuilder
.Add<NewWordChange>()
.Add<NewDefinitionChange>()
.Add<NewExampleChange>()
.Add<EditExampleChange>()
.Add<SetWordTextChange>()
.Add<SetWordNoteChange>()
.Add<AddAntonymReferenceChange>()
.Add<SetOrderChange<Definition>>()
.Add<DeleteChange<Word>>()
.Add<DeleteChange<Definition>>()
.Add<DeleteChange<Example>>()
;
config.ObjectTypeListBuilder
.Add<Word>()
.Add<Definition>()
.Add<Example>();
});
LinqToDBForEFTools.Initialize();
services.AddDbContext<SampleDbContext>((provider, builder) =>
{
builder.UseLinqToDbCrdt(provider);
builder.UseSqlite($"Data Source={dbPath}");
builder.EnableDetailedErrors();
builder.EnableSensitiveDataLogging();
#if DEBUG
builder.LogTo(s => Debug.WriteLine(s));
#endif
});
services.AddCrdtData<SampleDbContext>(config =>
{
config.EnableProjectedTables = true;
config.ChangeTypeListBuilder
.Add<NewWordChange>()
.Add<NewDefinitionChange>()
.Add<NewExampleChange>()
.Add<EditExampleChange>()
.Add<SetWordTextChange>()
.Add<SetWordNoteChange>()
.Add<AddAntonymReferenceChange>()
.Add<SetOrderChange<Definition>>()
.Add<DeleteChange<Word>>()
.Add<DeleteChange<Definition>>()
.Add<DeleteChange<Example>>()
;
config.ObjectTypeListBuilder
.Add<Word>()
.Add<Definition>()
.Add<Example>();
});
return services;
}
}
19 changes: 19 additions & 0 deletions src/Crdt.Sample/SampleDbContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Crdt.Changes;
using Crdt.Core;
using Crdt.Db;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

namespace Crdt.Sample;

public class SampleDbContext(DbContextOptions<SampleDbContext>options, IOptions<CrdtConfig> crdtConfig): DbContext(options), ICrdtDbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.UseCrdt(crdtConfig.Value);
}

public DbSet<Commit> Commits => Set<Commit>();
public DbSet<ChangeEntity<IChange>> ChangeEntities => Set<ChangeEntity<IChange>>();
public DbSet<ObjectSnapshot> Snapshots => Set<ObjectSnapshot>();
}
4 changes: 2 additions & 2 deletions src/Crdt.Tests/DataModelTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class DataModelTestBase : IAsyncLifetime
protected readonly ServiceProvider _services;
protected readonly Guid _localClientId = Guid.NewGuid();
public readonly DataModel DataModel;
public readonly CrdtDbContext DbContext;
public readonly SampleDbContext DbContext;
protected readonly MockTimeProvider MockTimeProvider = new();

public DataModelTestBase()
Expand All @@ -25,7 +25,7 @@ public DataModelTestBase()
.AddCrdtDataSample(":memory:")
.Replace(ServiceDescriptor.Singleton<IHybridDateTimeProvider>(MockTimeProvider))
.BuildServiceProvider();
DbContext = _services.GetRequiredService<CrdtDbContext>();
DbContext = _services.GetRequiredService<SampleDbContext>();
DbContext.Database.OpenConnection();
DbContext.Database.EnsureCreated();
DataModel = _services.GetRequiredService<DataModel>();
Expand Down
2 changes: 1 addition & 1 deletion src/Crdt.Tests/ModuleInit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static void Initialize()
var services = new ServiceCollection()
.AddCrdtDataSample(":memory:")
.BuildServiceProvider();
var model = services.GetRequiredService<CrdtDbContext>().Model;
var model = services.GetRequiredService<SampleDbContext>().Model;
VerifyEntityFramework.Initialize(model);
VerifierSettings.AddExtraSettings(s =>
{
Expand Down
4 changes: 2 additions & 2 deletions src/Crdt.Tests/RepositoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class RepositoryTests : IAsyncLifetime
{
private readonly ServiceProvider _services;
private CrdtRepository _repository;
private CrdtDbContext _crdtDbContext;
private SampleDbContext _crdtDbContext;

public RepositoryTests()
{
Expand All @@ -21,7 +21,7 @@ public RepositoryTests()
.BuildServiceProvider();

_repository = _services.GetRequiredService<CrdtRepository>();
_crdtDbContext = _services.GetRequiredService<CrdtDbContext>();
_crdtDbContext = _services.GetRequiredService<SampleDbContext>();
}

public async Task InitializeAsync()
Expand Down
48 changes: 40 additions & 8 deletions src/Crdt/CrdtConfig.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json.Serialization.Metadata;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using Crdt.Changes;
using Crdt.Db;
using Crdt.Entities;
Expand All @@ -12,6 +13,16 @@ public class CrdtConfig
public bool EnableProjectedTables { get; set; } = false;
public ChangeTypeListBuilder ChangeTypeListBuilder { get; } = new();
public ObjectTypeListBuilder ObjectTypeListBuilder { get; } = new();
public JsonSerializerOptions JsonSerializerOptions => _lazyJsonSerializerOptions.Value;
private readonly Lazy<JsonSerializerOptions> _lazyJsonSerializerOptions;

public CrdtConfig()
{
_lazyJsonSerializerOptions = new Lazy<JsonSerializerOptions>(() => new JsonSerializerOptions(JsonSerializerDefaults.General)
{
TypeInfoResolver = MakeJsonTypeResolver()
});
}

public Action<JsonTypeInfo> MakeJsonTypeModifier()
{
Expand All @@ -28,6 +39,8 @@ public IJsonTypeInfoResolver MakeJsonTypeResolver()

private void JsonTypeModifier(JsonTypeInfo typeInfo)
{
ChangeTypeListBuilder.Freeze();
ObjectTypeListBuilder.Freeze();
if (typeInfo.Type == typeof(IChange))
{
foreach (var type in ChangeTypeListBuilder.Types)
Expand All @@ -48,10 +61,22 @@ private void JsonTypeModifier(JsonTypeInfo typeInfo)

public class ChangeTypeListBuilder
{
private bool _frozen;

public void Freeze()
hahn-kev marked this conversation as resolved.
Show resolved Hide resolved
{
_frozen = true;
}

private void CheckFrozen()
{
if (_frozen) throw new InvalidOperationException("ObjectTypeListBuilder is frozen");
hahn-kev marked this conversation as resolved.
Show resolved Hide resolved
}
internal List<JsonDerivedType> Types { get; } = [];

public ChangeTypeListBuilder Add<TDerived>() where TDerived : IChange, IPolyType
{
CheckFrozen();
if (Types.Any(t => t.DerivedType == typeof(TDerived))) return this;
Types.Add(new JsonDerivedType(typeof(TDerived), TDerived.TypeName));
return this;
Expand All @@ -60,26 +85,33 @@ public ChangeTypeListBuilder Add<TDerived>() where TDerived : IChange, IPolyType

public class ObjectTypeListBuilder
{
internal List<JsonDerivedType> Types { get; } = [];

internal List<Action<ModelBuilder, CrdtConfig>> ModelConfigurations { get; } = [];
public List<Action<ModelConfigurationBuilder>> ModelConventions { get; } = [];
private bool _frozen;
public void Freeze()
{
_frozen = true;
}

public ObjectTypeListBuilder AddDbModelConvention(Action<ModelConfigurationBuilder> modelConvention)
private void CheckFrozen()
{
ModelConventions.Add(modelConvention);
return this;
if (_frozen) throw new InvalidOperationException("ObjectTypeListBuilder is frozen");
}

internal List<JsonDerivedType> Types { get; } = [];

internal List<Action<ModelBuilder, CrdtConfig>> ModelConfigurations { get; } = [];

public ObjectTypeListBuilder AddDbModelConfig(Action<ModelBuilder> modelConfiguration)
{
CheckFrozen();
ModelConfigurations.Add((builder, _) => modelConfiguration(builder));
return this;
}


public ObjectTypeListBuilder Add<TDerived>(Action<EntityTypeBuilder<TDerived>>? configureDb = null)
where TDerived : class, IObjectBase
{
CheckFrozen();
if (Types.Any(t => t.DerivedType == typeof(TDerived))) throw new InvalidOperationException($"Type {typeof(TDerived)} already added");
Types.Add(new JsonDerivedType(typeof(TDerived), TDerived.TypeName));
ModelConfigurations.Add((builder, config) =>
Expand Down
40 changes: 16 additions & 24 deletions src/Crdt/CrdtKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,31 @@ namespace Crdt;

public static class CrdtKernel
{
public static IServiceCollection AddCrdtData(this IServiceCollection services,
Action<DbContextOptionsBuilder> configureOptions,
Action<CrdtConfig> configureCrdt)
{
return AddCrdtData(services, (_, builder) => configureOptions(builder), configureCrdt);
}

public static IServiceCollection AddCrdtData(this IServiceCollection services,
Action<IServiceProvider, DbContextOptionsBuilder> configureOptions,
Action<CrdtConfig> configureCrdt)
{
services.AddOptions<CrdtConfig>().Configure(configureCrdt);
services.AddSingleton(sp => new JsonSerializerOptions(JsonSerializerDefaults.General)
services.AddDbContext<CrdtDbContext>((provider, builder) =>
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers =
{
sp.GetRequiredService<IOptions<CrdtConfig>>().Value.MakeJsonTypeModifier()
}
}
configureOptions(provider, builder);
builder
.EnableDetailedErrors()
.EnableSensitiveDataLogging();
});
return AddCrdtData<CrdtDbContext>(services, configureCrdt);
}

public static IServiceCollection AddCrdtData<TContext>(this IServiceCollection services,
Action<CrdtConfig> configureCrdt) where TContext : class, ICrdtDbContext
{
services.AddOptions<CrdtConfig>().Configure(configureCrdt);
services.AddSingleton(sp => sp.GetRequiredService<IOptions<CrdtConfig>>().Value.JsonSerializerOptions);
services.AddSingleton(TimeProvider.System);
services.AddScoped<IHybridDateTimeProvider>(NewTimeProvider);
services.AddDbContext<CrdtDbContext>((provider, builder) =>
{
configureOptions(provider, builder);
builder
.AddInterceptors(provider.GetServices<IInterceptor>().ToArray())
.EnableDetailedErrors()
.EnableSensitiveDataLogging();
},
ServiceLifetime.Scoped);
//must use factory, otherwise one context will be created for this registration, and one for the application.
//we want to have one context per application
services.AddScoped<ICrdtDbContext>(p => p.GetRequiredService<TContext>());
services.AddScoped<CrdtRepository>();
//must use factory method because DataModel constructor is internal
services.AddScoped<DataModel>(provider => new DataModel(
Expand Down
Loading