Support IModel being configured once (#473)

This commit is contained in:
Simon Cropp 2022-12-19 09:46:01 +11:00 коммит произвёл GitHub
Родитель 74ee6ddefc
Коммит 18d67f1061
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 133 добавлений и 48 удалений

Просмотреть файл

@ -23,14 +23,25 @@ Enable VerifyEntityFramework once at assembly load time:
<!-- snippet: EnableCore -->
<a id='snippet-enablecore'></a>
```cs
static IModel GetDbModel()
{
var options = new DbContextOptionsBuilder<SampleDbContext>();
options.UseSqlServer("fake");
using var data = new SampleDbContext(options.Options);
return data.Model;
}
[ModuleInitializer]
public static void Init()
{
VerifyEntityFramework.Enable();
var model = GetDbModel();
VerifyEntityFramework.Enable(model);
```
<sup><a href='/src/Verify.EntityFramework.Tests/ModuleInitializer.cs#L3-L10' title='Snippet source file'>snippet source</a> | <a href='#snippet-enablecore' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.EntityFramework.Tests/ModuleInitializer.cs#L5-L21' title='Snippet source file'>snippet source</a> | <a href='#snippet-enablecore' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
The `GetDbModel` pattern allows an instance of the `IModel` to be stored for use when `IgnoreNavigationProperties` is called inside tests. This is optional, and instead can be passed explicitly to `IgnoreNavigationProperties`.
### EF Classic
@ -68,7 +79,7 @@ builder.UseSqlServer(connection);
builder.EnableRecording();
var data = new SampleDbContext(builder.Options);
```
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L245-L252' title='Snippet source file'>snippet source</a> | <a href='#snippet-enablerecording' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L280-L287' title='Snippet source file'>snippet source</a> | <a href='#snippet-enablerecording' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
`EnableRecording` should only be called in the test context.
@ -91,12 +102,12 @@ await data.SaveChangesAsync();
EfRecording.StartRecording();
await data.Companies
.Where(x => x.Content == "Title")
.Where(_ => _.Content == "Title")
.ToListAsync();
await Verify(data.Companies.Count());
```
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L342-L359' title='Snippet source file'>snippet source</a> | <a href='#snippet-recording' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L377-L394' title='Snippet source file'>snippet source</a> | <a href='#snippet-recording' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
Will result in the following verified file:
@ -144,7 +155,7 @@ await data.SaveChangesAsync();
EfRecording.StartRecording();
await data.Companies
.Where(x => x.Content == "Title")
.Where(_ => _.Content == "Title")
.ToListAsync();
var entries = EfRecording.FinishRecording();
@ -155,7 +166,7 @@ await Verify(new
sql = entries
});
```
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L466-L489' title='Snippet source file'>snippet source</a> | <a href='#snippet-recordingspecific' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L501-L524' title='Snippet source file'>snippet source</a> | <a href='#snippet-recordingspecific' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
@ -181,12 +192,12 @@ await data1.SaveChangesAsync();
await using var data2 = new SampleDbContext(builder.Options);
await data2.Companies
.Where(x => x.Content == "Title")
.Where(_ => _.Content == "Title")
.ToListAsync();
await Verify(data2.Companies.Count());
```
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L311-L333' title='Snippet source file'>snippet source</a> | <a href='#snippet-multidbcontexts' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L346-L368' title='Snippet source file'>snippet source</a> | <a href='#snippet-multidbcontexts' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
<!-- snippet: CoreTests.MultiDbContexts.verified.txt -->
@ -377,10 +388,10 @@ This test:
<a id='snippet-queryable'></a>
```cs
var queryable = data.Companies
.Where(x => x.Content == "value");
.Where(_ => _.Content == "value");
await Verify(queryable);
```
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L213-L219' title='Snippet source file'>snippet source</a> | <a href='#snippet-queryable' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L248-L254' title='Snippet source file'>snippet source</a> | <a href='#snippet-queryable' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
Will result in the following verified file:
@ -426,7 +437,7 @@ await Verify(data.AllData())
serializer =>
serializer.TypeNameHandling = TypeNameHandling.Objects);
```
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L197-L204' title='Snippet source file'>snippet source</a> | <a href='#snippet-alldata' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L232-L239' title='Snippet source file'>snippet source</a> | <a href='#snippet-alldata' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
Will result in the following verified file with all data in the database:
@ -506,7 +517,7 @@ public async Task IgnoreNavigationProperties()
Company = company
};
await Verify(employee)
.IgnoreNavigationProperties(data);
.IgnoreNavigationProperties();
}
```
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L65-L87' title='Snippet source file'>snippet source</a> | <a href='#snippet-ignorenavigationproperties' title='Start of snippet'>anchor</a></sup>
@ -520,9 +531,9 @@ public async Task IgnoreNavigationProperties()
```cs
var options = DbContextOptions();
using var data = new SampleDbContext(options);
VerifyEntityFramework.IgnoreNavigationProperties(data.Model);
VerifyEntityFramework.IgnoreNavigationProperties();
```
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L91-L97' title='Snippet source file'>snippet source</a> | <a href='#snippet-ignorenavigationpropertiesglobal' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L115-L121' title='Snippet source file'>snippet source</a> | <a href='#snippet-ignorenavigationpropertiesglobal' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
@ -542,7 +553,7 @@ To be able to use [WebApplicationFactory](https://docs.microsoft.com/en-us/dotne
.Options);
});
```
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L424-L435' title='Snippet source file'>snippet source</a> | <a href='#snippet-enablerecordingwithidentifier' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L459-L470' title='Snippet source file'>snippet source</a> | <a href='#snippet-enablerecordingwithidentifier' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
Then use the same identifier for recording:
@ -558,7 +569,7 @@ var companies = await httpClient.GetFromJsonAsync<Company[]>("/companies");
var entries = EfRecording.FinishRecording(testName);
```
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L391-L401' title='Snippet source file'>snippet source</a> | <a href='#snippet-recordwithidentifier' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L426-L436' title='Snippet source file'>snippet source</a> | <a href='#snippet-recordwithidentifier' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
The results will not be automatically included in verified file so it will have to be verified manually:
@ -572,7 +583,7 @@ await Verify(new
sql = entries
});
```
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L403-L411' title='Snippet source file'>snippet source</a> | <a href='#snippet-verifyrecordedcommandswithidentifier' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L438-L446' title='Snippet source file'>snippet source</a> | <a href='#snippet-verifyrecordedcommandswithidentifier' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Просмотреть файл

@ -2,7 +2,7 @@
<Project>
<PropertyGroup>
<NoWarn>CS1591;CS0649;CS8632;EF1001</NoWarn>
<Version>9.2.0</Version>
<Version>9.3.0</Version>
<AssemblyVersion>1.0.0</AssemblyVersion>
<PackageTags>EntityFramework, Verify</PackageTags>
<Description>Extends Verify (https://github.com/VerifyTests/Verify) to allow verification of EntityFramework bits.</Description>

Просмотреть файл

@ -0,0 +1,3 @@
{
Content: employee
}

Просмотреть файл

@ -71,6 +71,30 @@ public class CoreTests
await using var data = new SampleDbContext(options);
var company = new Company
{
Content = "company"
};
var employee = new Employee
{
Content = "employee",
Company = company
};
await Verify(employee)
.IgnoreNavigationProperties();
}
#endregion
#region IgnoreNavigationPropertiesExplicit
[Test]
public async Task IgnoreNavigationPropertiesExplicit()
{
var options = DbContextOptions();
await using var data = new SampleDbContext(options);
var company = new Company
{
Content = "company"
@ -90,6 +114,17 @@ public class CoreTests
{
#region IgnoreNavigationPropertiesGlobal
var options = DbContextOptions();
using var data = new SampleDbContext(options);
VerifyEntityFramework.IgnoreNavigationProperties();
#endregion
}
void IgnoreNavigationPropertiesGlobalExplicit()
{
#region IgnoreNavigationPropertiesGlobalExplicit
var options = DbContextOptions();
using var data = new SampleDbContext(options);
VerifyEntityFramework.IgnoreNavigationProperties(data.Model);
@ -213,7 +248,7 @@ public class CoreTests
#region Queryable
var queryable = data.Companies
.Where(x => x.Content == "value");
.Where(_ => _.Content == "value");
await Verify(queryable);
#endregion
@ -226,7 +261,7 @@ public class CoreTests
var data = database.Context;
var query = data.Set<Company>()
.Select(x => x.Id);
.Select(_ => _.Id);
await Verify(query);
}
@ -236,7 +271,7 @@ public class CoreTests
var database = await DbContextBuilder.GetDatabase("NestedQueryable");
var data = database.Context;
var queryable = data.Companies
.Where(x => x.Content == "value");
.Where(_ => _.Content == "value");
await Verify(queryable);
}
@ -286,7 +321,7 @@ public class CoreTests
{
var s = i.ToString();
await data.Companies
.Where(x => x.Content == s)
.Where(_ => _.Content == s)
.ToListAsync();
}
@ -325,7 +360,7 @@ public class CoreTests
await using var data2 = new SampleDbContext(builder.Options);
await data2.Companies
.Where(x => x.Content == "Title")
.Where(_ => _.Content == "Title")
.ToListAsync();
await Verify(data2.Companies.Count());
@ -351,7 +386,7 @@ public class CoreTests
EfRecording.StartRecording();
await data.Companies
.Where(x => x.Content == "Title")
.Where(_ => _.Content == "Title")
.ToListAsync();
await Verify(data.Companies.Count());
@ -368,7 +403,7 @@ public class CoreTests
// Not actually the test name, the variable name is for README.md to make sense
var testName = nameof(RecordingWebApplicationFactory) + run;
using var connection = new SqliteConnection($"Data Source={testName};Mode=Memory;Cache=Shared");
await using var connection = new SqliteConnection($"Data Source={testName};Mode=Memory;Cache=Shared");
await connection.OpenAsync();
var factory = new CustomWebApplicationFactory(testName);
@ -413,7 +448,7 @@ public class CoreTests
class CustomWebApplicationFactory : WebApplicationFactory<Startup>
{
readonly string testName;
string testName;
public CustomWebApplicationFactory(string testName) =>
this.testName = testName;
@ -436,7 +471,7 @@ public class CoreTests
protected override IHostBuilder CreateHostBuilder() =>
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());
.ConfigureWebHostDefaults(builder => builder.UseStartup<Startup>());
}
public class Startup
@ -453,7 +488,7 @@ public class CoreTests
app.UseRouting();
app.UseEndpoints(endpoints
=> endpoints.MapGet("/companies", async (SampleDbContext data) => await data.Companies.ToListAsync()));
=> endpoints.MapGet("/companies", (SampleDbContext data) => data.Companies.ToListAsync()));
}
}
@ -475,7 +510,7 @@ public class CoreTests
EfRecording.StartRecording();
await data.Companies
.Where(x => x.Content == "Title")
.Where(_ => _.Content == "Title")
.ToListAsync();
var entries = EfRecording.FinishRecording();

Просмотреть файл

@ -1,11 +1,22 @@
public static class ModuleInitializer
using Microsoft.EntityFrameworkCore.Metadata;
public static class ModuleInitializer
{
#region EnableCore
static IModel GetDbModel()
{
var options = new DbContextOptionsBuilder<SampleDbContext>();
options.UseSqlServer("fake");
using var data = new SampleDbContext(options.Options);
return data.Model;
}
[ModuleInitializer]
public static void Init()
{
VerifyEntityFramework.Enable();
var model = GetDbModel();
VerifyEntityFramework.Enable(model);
#endregion

Просмотреть файл

@ -9,12 +9,12 @@
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
protected override void OnModelCreating(ModelBuilder builder)
{
modelBuilder.Entity<Company>()
builder.Entity<Company>()
.HasMany(c => c.Employees)
.WithOne(e => e.Company)
.IsRequired();
modelBuilder.Entity<Employee>();
builder.Entity<Employee>();
}
}

Просмотреть файл

@ -39,7 +39,7 @@
static void HandleAdded(List<EntityEntry> entries, VerifyJsonWriter writer)
{
var added = entries
.Where(x => x.State == EntityState.Added)
.Where(_ => _.State == EntityState.Added)
.ToList();
if (!added.Any())
{

Просмотреть файл

@ -3,7 +3,7 @@ class LogCommandInterceptor :
{
static AsyncLocal<State?> asyncLocal = new();
static ConcurrentDictionary<string, List<LogEntry>> namedEvents = new(StringComparer.OrdinalIgnoreCase);
readonly string? identifier;
string? identifier;
public static void Start() => asyncLocal.Value = new();
public static void Start(string identifier) => namedEvents.GetOrAdd(identifier, _ => new());

Просмотреть файл

@ -2,6 +2,8 @@
public static class VerifyEntityFramework
{
static List<(Type type, string name)>? modelNavigations;
public static async IAsyncEnumerable<object> AllData(this DbContext data)
{
foreach (var entityType in data
@ -25,9 +27,9 @@ public static class VerifyEntityFramework
public static SettingsTask IgnoreNavigationProperties(this SettingsTask settings, DbContext context) =>
settings.IgnoreNavigationProperties(context.Model);
public static SettingsTask IgnoreNavigationProperties(this SettingsTask settings, IModel model)
public static SettingsTask IgnoreNavigationProperties(this SettingsTask settings, IModel? model = null)
{
foreach (var (type, name) in model.GetNavigations())
foreach (var (type, name) in model.GetNavigationsOrShared())
{
settings.IgnoreMember(type, name);
}
@ -35,22 +37,37 @@ public static class VerifyEntityFramework
return settings;
}
public static void IgnoreNavigationProperties(this VerifySettings settings, IModel model)
public static void IgnoreNavigationProperties(this VerifySettings settings, IModel? model = null)
{
foreach (var (type, name) in model.GetNavigations())
foreach (var (type, name) in model.GetNavigationsOrShared())
{
settings.IgnoreMember(type, name);
}
}
public static void IgnoreNavigationProperties(IModel model)
public static void IgnoreNavigationProperties(IModel? model = null)
{
foreach (var (type, name) in model.GetNavigations())
foreach (var (type, name) in model.GetNavigationsOrShared())
{
VerifierSettings.IgnoreMember(type, name);
}
}
static IEnumerable<(Type type, string name)> GetNavigationsOrShared(this IModel? model)
{
if (model == null)
{
if (modelNavigations != null)
{
return modelNavigations;
}
throw new("The `model` parameter must be provided wither on this method or on VerifyEntityFramework.Enable()");
}
return GetNavigations(model);
}
static IEnumerable<(Type type, string name)> GetNavigations(this IModel model)
{
foreach (var type in model.GetEntityTypes())
@ -62,8 +79,16 @@ public static class VerifyEntityFramework
}
}
public static void Enable()
public static void Enable(DbContext context) =>
Enable(context.Model);
public static void Enable(IModel? model = null)
{
if (model != null)
{
modelNavigations = model.GetNavigations().ToList();
}
VerifierSettings.RegisterJsonAppender(_ =>
{
var entries = LogCommandInterceptor.Stop();

Просмотреть файл

@ -119,7 +119,7 @@ public class ClassicTests
{
var database = await DbContextBuilder.GetDatabase("Queryable");
var data = database.Context;
var queryable = data.Companies.Where(x => x.Content == "value");
var queryable = data.Companies.Where(_ => _.Content == "value");
await Verify(queryable);
}

Просмотреть файл

@ -31,7 +31,7 @@
static void HandleDeleted(List<DbEntityEntry> entries, VerifyJsonWriter writer, DbContext data)
{
var deleted = entries
.Where(x => x.State == EntityState.Deleted)
.Where(_ => _.State == EntityState.Deleted)
.ToList();
if (!deleted.Any())
{
@ -54,7 +54,7 @@
static void HandleAdded(List<DbEntityEntry> entries, VerifyJsonWriter writer)
{
var added = entries
.Where(x => x.State == EntityState.Added)
.Where(_ => _.State == EntityState.Added)
.ToList();
if (!added.Any())
{
@ -83,7 +83,7 @@
static void HandleModified(List<DbEntityEntry> entries, VerifyJsonWriter writer, DbContext context)
{
var modified = entries
.Where(x => x.State == EntityState.Modified)
.Where(_ => _.State == EntityState.Modified)
.ToList();
if (!modified.Any())
{