I'm having a concurrency issue with MassTransit sagas.
I'm currently working on a POC with this flow:
One thread produces 100 event that are published to MassTransit in a sequence.
When the saga is instantiated it publishes another event to MassTransit.
The new event is picked up by a Consumer that perform some business logic and publishes one of two resulting event to MassTransit.
The resulting events from step 3. triggers a state change in the saga
I sometimes get exceptions like this Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded. in step 4, and the state change is not persisted.
Here is the business logic code:
public interface IInitialSagaEvent : CorrelatedBy<Guid> { }
public interface IExternalCheckRequest : CorrelatedBy<Guid> { }
public interface IExternalCheckOk : CorrelatedBy<Guid> { }
public interface IExternalCheckNotOk : CorrelatedBy<Guid> { }
public class MySaga : SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; }
public byte[] RowVersion { get; set; }
}
public class MyStateMachine : MassTransitStateMachine<MySaga>
{
public MyStateMachine()
{
InstanceState(instance => instance.CurrentState);
Initially(
When(InitialSagaEvent)
.ThenAsync(context => context.GetPayload<ConsumeContext>().Publish<IExternalCheckRequest>(new { context.Instance.CorrelationId }))
.TransitionTo(AwaitingExternalCheck)
);
During(AwaitingExternalCheck,
Ignore(InitialSagaEvent),
When(ExternalCheckOk)
.TransitionTo(CheckedOk),
When(ExternalCheckNotOk)
.TransitionTo(CheckedNotOk)
);
During(CheckedOk,
When(InitialSagaEvent)
.ThenAsync(context => context.GetPayload<ConsumeContext>().Publish<IExternalCheckRequest>(new { context.Instance.CorrelationId }))
.TransitionTo(AwaitingExternalCheck)
);
During(CheckedNotOk,
When(InitialSagaEvent)
.ThenAsync(context => context.GetPayload<ConsumeContext>().Publish<IExternalCheckRequest>(new { context.Instance.CorrelationId }))
.TransitionTo(AwaitingExternalCheck)
);
}
public Event<IInitialSagaEvent> InitialSagaEvent { get; private set; }
public Event<IExternalCheckOk> ExternalCheckOk { get; private set; }
public Event<IExternalCheckNotOk> ExternalCheckNotOk { get; private set; }
public State AwaitingExternalCheck { get; private set; }
public State CheckedOk { get; private set; }
public State CheckedNotOk { get; private set; }
}
public class ExternalCheckRequestConsumer : IConsumer<IExternalCheckRequest>
{
private readonly IExternalChecker externalChecker;
public ExternalCheckRequestConsumer(IExternalChecker externalChecker)
{
this.externalChecker = externalChecker;
}
public async Task Consume(ConsumeContext<IExternalCheckRequest> context)
{
var ok = await externalChecker.PerformCheck(context.Message, context.CancellationToken);
if (ok)
{
await context.Publish<IExternalCheckOk>(new { context.Message.CorrelationId }, context.CancellationToken);
}
else
{
await context.Publish<IExternalCheckNotOk>(new { context.Message.CorrelationId }, context.CancellationToken);
}
}
}
public interface IExternalChecker
{
Task<bool> PerformCheck(IExternalCheckRequest request, CancellationToken cancellationToken);
}
public class Publisher
{
private readonly IPublishEndpoint publishEndpoint;
public Publisher(IPublishEndpoint publishEndpoint)
{
this.publishEndpoint = publishEndpoint;
}
public async Task Publish(Guid correlationId, CancellationToken cancellationToken)
{
await publishEndpoint.Publish<IInitialSagaEvent>(new { CorrelationId = correlationId }, cancellationToken);
}
}
Here it the configuration code
public class MySagaDbContext : SagaDbContext
{
public MySagaDbContext(DbContextOptions<MySagaDbContext> options) : base(options) { }
protected override IEnumerable<ISagaClassMap> Configurations
{
get
{
yield return new MySagaClassMap();
}
}
}
public class MySagaClassMap : SagaClassMap<MySaga>
{
protected override void Configure(EntityTypeBuilder<MySaga> entity, ModelBuilder model)
{
entity.Property(x => x.CurrentState).HasMaxLength(128);
entity.Property(x => x.RowVersion).IsRowVersion();
}
}
public class ExternalCheckRequestConsumerDefinition : ConsumerDefinition<ExternalCheckRequestConsumer>
{
protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator<ExternalCheckRequestConsumer> consumerConfigurator) =>
endpointConfigurator.UseRetry(r =>
{
r.Handle<DbUpdateConcurrencyException>();
// This is the SQLServer error code for duplicate key
r.Handle<DbUpdateException>(y => y.InnerException is SqlException e && e.Number == 2627);
r.Interval(5, TimeSpan.FromMilliseconds(100));
});
}
public class Program
{
public static async Task Main(string[] args)
{
var services = new ServiceCollection();
services.AddDbContext<DbContext, MySagaDbContext>((provider, builder)
=> builder.UseSqlServer(connectionString, m =>
{
m.MigrationsAssembly(typeof(MySagaDbContext).Assembly.GetName().Name);
m.MigrationsHistoryTable($"__EFMigrationsHistory_Sagas");
}));
services.AddMassTransit(configureMassTransit =>
{
configureMassTransit.AddConsumer<ExternalCheckRequestConsumer, ExternalCheckRequestConsumerDefinition>();
configureMassTransit.AddSagaStateMachine<MyStateMachine, MySaga>()
.EntityFrameworkRepository(r =>
{
r.ConcurrencyMode = ConcurrencyMode.Optimistic;
r.ExistingDbContext<MySagaDbContext>();
});
configureMassTransit.SetEndpointNameFormatter(new DefaultEndpointNameFormatter(true));
configureMassTransit.UsingActiveMq((context, config) =>
{
config.Host("artemis", 61616, configureHost =>
{
configureHost.Username("admin");
configureHost.Password("admin");
});
config.UseInMemoryOutbox(); // ref https://masstransit-project.com/articles/outbox.html#the-in-memory-outbox
config.EnableArtemisCompatibility();
config.ConfigureEndpoints(context);
});
});
var serviceProvider = services.BuildServiceProvider();
var busControl = serviceProvider.GetRequiredService<IBusControl>();
await busControl.StartAsync();
await RunPoc(serviceProvider);
}
private static async Task RunPoc(IServiceProvider serviceProvider)
{
await Task.CompletedTask;
}
static string connectionString = string.Empty;
}
My guess is that I need to get in a UseRetry at the correct point, so I've tried to configure the AddSagaStateMachine with UseRetry like this:
configureMassTransit.AddSagaStateMachine<MyStateMachine, MySaga>(
configure =>
{
configure.UseRetry(r =>
{
r.Handle<DbUpdateConcurrencyException>();
// This is the SQLServer error code for duplicate key
r.Handle<DbUpdateException>(y => y.InnerException is SqlException e && e.Number == 2627);
r.Interval(5, TimeSpan.FromMilliseconds(100));
});
})
.EntityFrameworkRepository(r =>
{
r.ConcurrencyMode = ConcurrencyMode.Optimistic;
r.ExistingDbContext<MySagaDbContext>();
});
But with this UseRetry in AddSagaStateMachine nothing works, I just get loads of exception like this:
fail: MassTransit.ReceiveTransport[0]
R - FAULT activemq://artemis:61616/XXXX
System.ArgumentException: THe message could not be retrieved: IInitialSagaEvent(Parameter 'context')
at MassTransit.Saga.Pipeline.Pipes.SagaMergePipe`2.Send(SagaConsumeContext`1 context)
at GreenPipes.Filters.RetryFilter`1.GreenPipes.IFilter<TContext>.Send(TContext context, IPipe`1 next)
at GreenPipes.Filters.RetryFilter`1.GreenPipes.IFilter<TContext>.Send(TContext context, IPipe`1 next)
at MassTransit.Saga.SendSagaPipe`2.Send(SagaRepositoryContext`2 context)
at MassTransit.Saga.SendSagaPipe`2.Send(SagaRepositoryContext`2 context)
at MassTransit.EntityFrameworkCoreIntegration.Saga.Context.EntityFrameworkSagaRepositoryContextFactory`1.<> c__DisplayClass5_0`1.<< Send > b__1 > d.MoveNext()
-- - End of stack trace from previous location ---
at MassTransit.EntityFrameworkCoreIntegration.Saga.Context.EntityFrameworkSagaRepositoryContextFactory`1.<> c__DisplayClass8_0.<< WithinTransaction > g__Create | 0 > d.MoveNext()
-- - End of stack trace from previous location ---
at MassTransit.EntityFrameworkCoreIntegration.Saga.Context.EntityFrameworkSagaRepositoryContextFactory`1.WithinTransaction[T](DbContext context, CancellationToken cancellationToken, Func`1 callback)
at MassTransit.EntityFrameworkCoreIntegration.Saga.Context.EntityFrameworkSagaRepositoryContextFactory`1.WithinTransaction[T](DbContext context, CancellationToken cancellationToken, Func`1 callback)
at MassTransit.EntityFrameworkCoreIntegration.Saga.Context.EntityFrameworkSagaRepositoryContextFactory`1.WithinTransaction[T](DbContext context, CancellationToken cancellationToken, Func`1 callback)
at MassTransit.EntityFrameworkCoreIntegration.Saga.Context.EntityFrameworkSagaRepositoryContextFactory`1.Send[T](ConsumeContext`1 context, IPipe`1 next)
at MassTransit.EntityFrameworkCoreIntegration.Saga.Context.EntityFrameworkSagaRepositoryContextFactory`1.Send[T](ConsumeContext`1 context, IPipe`1 next)
at MassTransit.ExtensionsDependencyInjectionIntegration.ScopeProviders.DependencyInjectionSagaRepositoryContextFactory`1.<> c__DisplayClass6_0`1.<< Send > g__CreateScope | 0 > d.MoveNext()
-- - End of stack trace from previous location ---
at MassTransit.ExtensionsDependencyInjectionIntegration.ScopeProviders.DependencyInjectionSagaRepositoryContextFactory`1.<> c__DisplayClass6_0`1.<< Send > g__CreateScope | 0 > d.MoveNext()
-- - End of stack trace from previous location ---
at MassTransit.Saga.Pipeline.Filters.CorrelatedSagaFilter`2.GreenPipes.IFilter<MassTransit.ConsumeContext<TMessage>>.Send(ConsumeContext`1 context, IPipe`1 next)
I'm using .Net 6 and have tried MassTransit v 7.3.1 and v 8.0.0-develop.391, but both has the same behavior.
I've tried defining the messages as interfaces and publishing them both as anonymous classes and as actual implementations, and also tried to define the messages as classes, but with no luck.
My hope it that there is just some small configuration detail I'm missing, but I'm out of ideas, so any help is deeply appreciated.
The proper configuration in your SagaDefinition is shown below. Note the use of UseMessageRetry, instead of UseRetry.
public class ExternalCheckRequestConsumerDefinition :
ConsumerDefinition<ExternalCheckRequestConsumer>
{
protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator,
IConsumerConfigurator<ExternalCheckRequestConsumer> consumerConfigurator) =>
endpointConfigurator.UseMessageRetry(r =>
{
r.Handle<DbUpdateConcurrencyException>();
// This is the SQLServer error code for duplicate key
r.Handle<DbUpdateException>(y => y.InnerException is SqlException e && e.Number == 2627);
r.Interval(5, TimeSpan.FromMilliseconds(100));
});
}
UPDATE
The above Consumer definition isn't used by the saga. You'd need to create a Saga definition, and specify it when adding the saga, for the retry to apply to the saga. Which would do the same as configuring it inline when adding the saga:
.AddSagaStateMachine<MyStateMachine, MySaga, MySagaDefinition>(
Also, in your state machine, replace the overly noisy:
.ThenAsync(context => context.GetPayload<ConsumeContext>().Publish<IExternalCheckRequest>(new { context.Instance.CorrelationId }))
With:
.PublishAsync(context => context.Init<IExternalCheckRequest>(new { context.Instance.CorrelationId }))
Here is the .AddSagaStateMachine I used, ref Chris Pattersons solution in the other answer.
configureMassTransit.AddSagaStateMachine<MyStateMachine, MySaga>(
configure =>
{
configure.UseMessageRetry(r =>
{
r.Handle<DbUpdateConcurrencyException>();
// This is the SQLServer error code for duplicate key
r.Handle<DbUpdateException>(y => y.InnerException is SqlException e && e.Number == 2627);
r.Interval(5, TimeSpan.FromMilliseconds(100));
});
})
.EntityFrameworkRepository(r =>
{
r.ConcurrencyMode = ConcurrencyMode.Optimistic;
r.ExistingDbContext<MySagaDbContext>();
});
Related
In my class constrctur, we had multiple concrete class dependencies. as per automoq documentation, we can only interface or abstraction.
System Under Test Class, in that ManageLocationRepository is concrete class dependency.
public class CityEventListener : IEvent<LocationChangeEventData>
{
private readonly ILocationAdapterCaller _locationAdapterCaller;
private readonly ManageLocationRepository _managerLocationRepository;
public CityEventListener(ILocationAdapterCaller locationAdapterCaller, ManageLocationRepository managerLocationRepository)
{
_locationAdapterCaller = locationAdapterCaller;
_managerLocationRepository = managerLocationRepository;
}
public async Task<bool> ProcessEvent(LocationChangeEventData eventData)
{
}
}
Test Case -
[Theory(DisplayName = "Valid value test")]
[ClassAutoMoqData(typeof(ValidValueTests))]
public async Task ProcessEvent_WithCreateOrUpdateOperation_CallsUpsertCityAndReturnsResult(ExpectedValueTestData<Parameters, bool> data,
[Frozen] Mock<ILocationAdapterCaller> locationAdapterCallerMock, [Frozen] Mock<ManageLocationRepository> managerLocationRepositoryMock,
CityEventListener sut)
{
// fakes
var cityDetail = _fixture.Build<CityDetail>()
.With(x => x.Id, data.Params.locationChangeEventData.Id).Create();
// Arrange
locationAdapterCallerMock.Setup(mock => mock.GetCityDetail(data.Params.locationChangeEventData.Id))
.ReturnsAsync(cityDetail).Verifiable();
managerLocationRepositoryMock
.Setup(mock => mock.UpsertCity(cityDetail))
.ReturnsAsync(data.ExpectedValue).Verifiable();
var result = await sut.ProcessEvent(data.Params.locationChangeEventData);
// Assert
using (new AssertionScope())
{
Assert.IsType<bool>(result);
Assert.Equal(data.ExpectedValue, result);
locationAdapterCallerMock.Verify();
managerLocationRepositoryMock.Verify();
}
}
ClassAutoMoq Attribute
public class ClassAutoMoqDataAttribute : CompositeDataAttribute
{
public ClassAutoMoqDataAttribute(Type values)
: base(new ClassDataAttribute(values), new AutoMoqDataAttribute())
{
}
}
public class AutoMoqDataAttribute : AutoDataAttribute
{
public AutoMoqDataAttribute() : base(() =>
{
var fixture = new Fixture().Customize(new CompositeCustomization(
new AutoMoqCustomization() { ConfigureMembers = true, GenerateDelegates = true },
new SupportMutableValueTypesCustomization()));
fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList().ForEach(b => fixture.Behaviors.Remove(b));
fixture.Behaviors.Add(new OmitOnRecursionBehavior());
return fixture;
})
{
}
}
what are the alternate way to moq such dependencies through automoq attribute.
I solved my problem by taking two actions -
mark concrete class method virtual
create custom class automoq attribute to freeze dependencies
public class CityEventListenerClassAutoMoqAttribute : CompositeDataAttribute
{
public CityEventListenerClassAutoMoqAttribute(Type values)
: base(new ClassDataAttribute(values), new CityEventListenerAutoMoqAttribute())
{
}
}
public class CityEventListenerAutoMoqAttribute : AutoDataAttribute
{
public CityEventListenerAutoMoqAttribute()
: base(() =>
{
var fixture = new Fixture().Customize(new CompositeCustomization(
new AutoMoqCustomization() { ConfigureMembers = true, GenerateDelegates = true },
new SupportMutableValueTypesCustomization()));
var managerLocationRepositoryMock =
fixture.Freeze<Mock<ManageLocationRepository>>();
fixture.Inject(managerLocationRepositoryMock.Object);
return fixture;
})
{
}
}
Now my test case looks like this -
[Theory(DisplayName = "Valid value test")]
[CityEventListenerClassAutoMoq(typeof(ValidValueTests))]
public async Task ProcessEvent_WithCreateOrUpdateOperation_CallsUpsertCityAndReturnsResult(ExpectedValueTestData<Parameters, bool> data,
[Frozen] Mock<ILocationAdapterCaller> locationAdapterCallerMock, [Frozen] Mock<ManageLocationRepository> managerLocationRepositoryMock,
CityEventListener sut)
{
// fakes
var cityDetail = _fixture.Build<CityDetail>()
.With(x => x.Id, data.Params.locationChangeEventData.Id).Create();
// Arrange
locationAdapterCallerMock.Setup(mock => mock.GetCityDetail(data.Params.locationChangeEventData.Id))
.ReturnsAsync(cityDetail).Verifiable();
managerLocationRepositoryMock
.Setup(mock => mock.UpsertCity(cityDetail))
.ReturnsAsync(data.ExpectedValue).Verifiable();
var result = await sut.ProcessEvent(data.Params.locationChangeEventData);
// Assert
using (new AssertionScope())
{
Assert.IsType<bool>(result);
Assert.Equal(data.ExpectedValue, result);
locationAdapterCallerMock.Verify();
managerLocationRepositoryMock.Verify();
}
}
Do let me know if I can improve anything in my approach.
My Create handler uses AutoMapper to map from command to entity.
public async Task<int> Handle(Command command, CancellationToken cancellationToken)
{
var entity = _mapper.Map<ExampleEntity>(command);
await _db.ExampleEntity.AddAsync(entity, cancellationToken);
await _db.SaveChangesAsync(cancellationToken);
return entity.Id;
}
My test looks like this
var command = new Create.Command
{
Name = "Example 1",
...
};
var id = await _fixture.SendAsync(command, mapper);
id.ShouldNotBeNull();
This is the SliceFixture
[CollectionDefinition(nameof(SliceFixture))]
public class SliceFixtureCollection : ICollectionFixture<SliceFixture> { }
public class SliceFixture : IAsyncLifetime
{
private readonly Checkpoint _checkpoint;
private readonly IConfiguration _configuration;
private readonly IServiceScopeFactory _scopeFactory;
private readonly WebApplicationFactory<Startup> _factory;
public SliceFixture()
{
_factory = new ContosoTestApplicationFactory();
_configuration = _factory.Services.GetRequiredService<IConfiguration>();
_scopeFactory = _factory.Services.GetRequiredService<IServiceScopeFactory>();
_checkpoint = new Checkpoint();
}
public class ContosoTestApplicationFactory : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration((_, configBuilder) =>
{
configBuilder.AddInMemoryCollection(new Dictionary<string, string>
{
{"ConnectionStrings:DefaultConnection", _connectionString}
});
});
}
private readonly string _connectionString = "";
}
public async Task ExecuteScopeAsync(Func<IServiceProvider, Task> action)
{
using var scope = _scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<SchoolContext>();
try
{
await dbContext.BeginTransactionAsync();
await action(scope.ServiceProvider);
await dbContext.CommitTransactionAsync();
}
catch (Exception)
{
dbContext.RollbackTransaction();
throw;
}
}
public async Task<T> ExecuteScopeAsync<T>(Func<IServiceProvider, Task<T>> action)
{
using var scope = _scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<SchoolContext>();
try
{
await dbContext.BeginTransactionAsync();
var result = await action(scope.ServiceProvider);
await dbContext.CommitTransactionAsync();
return result;
}
catch (Exception)
{
dbContext.RollbackTransaction();
throw;
}
}
public Task ExecuteDbContextAsync(Func<SchoolContext, Task> action) => ExecuteScopeAsync(sp => action(sp.GetService<SchoolContext>()));
public Task ExecuteDbContextAsync(Func<SchoolContext, ValueTask> action) => ExecuteScopeAsync(sp => action(sp.GetService<SchoolContext>()).AsTask());
public Task ExecuteDbContextAsync(Func<SchoolContext, IMediator, Task> action) => ExecuteScopeAsync(sp => action(sp.GetService<SchoolContext>(), sp.GetService<IMediator>()));
public Task<T> ExecuteDbContextAsync<T>(Func<SchoolContext, Task<T>> action) => ExecuteScopeAsync(sp => action(sp.GetService<SchoolContext>()));
public Task<T> ExecuteDbContextAsync<T>(Func<SchoolContext, ValueTask<T>> action) => ExecuteScopeAsync(sp => action(sp.GetService<SchoolContext>()).AsTask());
public Task<T> ExecuteDbContextAsync<T>(Func<SchoolContext, IMediator, Task<T>> action) => ExecuteScopeAsync(sp => action(sp.GetService<SchoolContext>(), sp.GetService<IMediator>()));
public Task InsertAsync<T>(params T[] entities) where T : class
{
return ExecuteDbContextAsync(db =>
{
foreach (var entity in entities)
{
db.Set<T>().Add(entity);
}
return db.SaveChangesAsync();
});
}
public Task<T> FindAsync<T>(int id)
where T : class, IEntity
{
return ExecuteDbContextAsync(db => db.Set<T>().FindAsync(id).AsTask());
}
public Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request)
{
return ExecuteScopeAsync(sp =>
{
var mediator = sp.GetRequiredService<IMediator>();
return mediator.Send(request);
});
}
public Task SendAsync(IRequest request)
{
return ExecuteScopeAsync(sp =>
{
var mediator = sp.GetRequiredService<IMediator>();
return mediator.Send(request);
});
}
private int _courseNumber = 1;
public int NextCourseNumber() => Interlocked.Increment(ref _courseNumber);
public Task InitializeAsync() => _checkpoint.Reset(_configuration.GetConnectionString("DefaultConnection"));
public Task DisposeAsync()
{
_factory?.Dispose();
return Task.CompletedTask;
}
}
When I run the test I get -
Object reference not set to an instance of an object.
because _mapper is null.
I know I probably need to modify the SliceFixture but can't figure out what to do.
I was looking to implement Jimmy’s example here with the additional AutoMapper implementation.
I have a method in my repository that I’m trying to test
public User UpdateUserManyToMany(User user, List<Guid> manyToManyds)
{
var dbContext = _databaseContext as DbContext;
dbContext?.TryUpdateManyToMany(user.ManyToMany, manyToManyds
.Select(x => new ManyToMany{
OtherEntityId = x,
UserId = user.Id,
}), x => x.OtherEntityId);
return user;
}
My ManyToMany Entity :
public class ManyToMany
{
public Guid OtherEntityId { get; set; }
public OtherEntity OtherEntityId { get; set; }
public Guid UserId { get; set; }
public User User { get; set; }
}
My TryUpdateManyToMany :
public static class ManyToManyExtensions
{
public static void TryUpdateManyToMany<T, TKey>(this DbContext db, IEnumerable<T> currentItems, IEnumerable<T> newItems, Func<T, TKey> getKey) where T : class
{
db.Set<T>().RemoveRange(currentItems.Except(newItems, getKey));
db.Set<T>().AddRange(newItems.Except(currentItems, getKey));
}
public static IEnumerable<T> Except<T, TKey>(this IEnumerable<T> items, IEnumerable<T> other, Func<T, TKey> getKeyFunc)
{
return items
.GroupJoin(other, getKeyFunc, getKeyFunc, (item, tempItems) => new { item, tempItems })
.SelectMany(t => t.tempItems.DefaultIfEmpty(), (t, temp) => new { t, temp })
.Where(t => ReferenceEquals(null, t.temp) || t.temp.Equals(default(T)))
.Select(t => t.t.item);
}
}
Here’s my unit test :
using (var context = new InMemoryDataBaseContext())
{
// Arrange
var repository = new UserRepository(context);
await context.Users.AddRangeAsync(GetUser());
await context.SaveChangesAsync();
// Act
var manyIds = new List<Guid>();
manyIds.Add(Guid.Parse("855d1a64-a707-40d5-ab93-34591a923abf"));
manyIds.Add(Guid.Parse("855d1a64-a787-40d9-ac93-34591a923abf"));
manyIds.Add(Guid.Parse("855d1a64-a707-41d9-ab93-39591a923abf"));
var user = new User();
var expected = repository.UpdateUserManyToMany(GetUser(), manyIds);
// Assert
}
But I get the following error in my test :
Message:
System.InvalidOperationException : The instance of entity type 'ManyToMany' cannot be tracked because another instance with the same key value for {'UserId', 'OtherEntityId'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.
Arborescence des appels de procédure:
IdentityMap`1.ThrowIdentityConflict(InternalEntityEntry entry)
IdentityMap`1.Add(TKey key, InternalEntityEntry entry, Boolean updateDuplicate)
IdentityMap`1.Add(TKey key, InternalEntityEntry entry)
NullableKeyIdentityMap`1.Add(InternalEntityEntry entry)
StateManager.StartTracking(InternalEntityEntry entry)
InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, Boolean acceptChanges, Boolean modifyProperties)
InternalEntityEntry.SetEntityState(EntityState entityState, Boolean acceptChanges, Boolean modifyProperties, Nullable`1 forceStateWhenUnknownKey)
EntityGraphAttacher.PaintAction(EntityEntryGraphNode`1 node)
EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1 node, Func`2 handleNode)
EntityGraphAttacher.AttachGraph(InternalEntityEntry rootEntry, EntityState targetState, EntityState storeGeneratedWithKeySetTargetState, Boolean forceStateWhenUnknownKey)
DbContext.SetEntityState(InternalEntityEntry entry, EntityState entityState)
DbContext.RemoveRange(IEnumerable`1 entities)
InternalDbSet`1.RemoveRange(IEnumerable`1 entities)
ManyToManyExtensions.TryUpdateManyToMany[T,TKey](DbContext db, IEnumerable`1 currentItems, IEnumerable`1 newItems, Func`2 getKey) ligne 24
UserRepository.UpdateUserManyToMany(User user, List`1 manyToManyds) ligne 59
MyRepoUnitTest.MyTestMethod() ligne 102
--- End of stack trace from previous location where exception was thrown ```
The following sample program, that is based on the code you provided, works without issues:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
public class User
{
public Guid Id { get; set; }
public string Name { get; set; }
public ICollection<ManyToMany> ManyToMany { get; set; } = new HashSet<ManyToMany>();
}
public class OtherEntity
{
public Guid Id { get; set; }
public string Name { get; set; }
public ICollection<ManyToMany> ManyToMany { get; set; } = new HashSet<ManyToMany>();
}
public class ManyToMany
{
public Guid OtherEntityId { get; set; }
public Guid UserId { get; set; }
public OtherEntity OtherEntity { get; set; }
public User User { get; set; }
}
public class Context : DbContext
{
public DbSet<OtherEntity> OtherEntities { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<ManyToMany> ManyToMany { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(#"Data Source=.\MSSQL14;Integrated Security=SSPI;Initial Catalog=So63077461")
.UseLoggerFactory(
LoggerFactory.Create(
b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<OtherEntity>(
entity =>
{
entity.HasKey(e => e.Id);
entity.HasMany(e => e.ManyToMany)
.WithOne(e => e.OtherEntity)
.HasForeignKey(e => e.OtherEntityId);
entity.HasData(
new OtherEntity
{
Id = new Guid("855d1a64-a707-40d5-ab93-34591a923abf"),
Name = "Bicycle"
},
new OtherEntity
{
Id = new Guid("855d1a64-a787-40d9-ac93-34591a923abf"),
Name = "Bus"
},
new OtherEntity
{
Id = new Guid("855d1a64-a707-41d9-ab93-39591a923abf"),
Name = "Plane"
});
});
modelBuilder.Entity<User>(
entity =>
{
entity.HasKey(e => e.Id);
entity.HasMany(e => e.ManyToMany)
.WithOne(e => e.User)
.HasForeignKey(e => e.UserId);
});
modelBuilder.Entity<ManyToMany>(
entity =>
{
entity.HasKey(e => new {e.OtherEntityId, e.UserId});
});
}
}
public static class ManyToManyExtensions
{
public static void TryUpdateManyToMany<T, TKey>(this DbContext db, IEnumerable<T> currentItems, IEnumerable<T> newItems, Func<T, TKey> getKey) where T : class
{
db.Set<T>().RemoveRange(currentItems.Except(newItems, getKey));
db.Set<T>().AddRange(newItems.Except(currentItems, getKey));
}
public static IEnumerable<T> Except<T, TKey>(this IEnumerable<T> items, IEnumerable<T> other, Func<T, TKey> getKeyFunc)
{
return items
.GroupJoin(other, getKeyFunc, getKeyFunc, (item, tempItems) => new { item, tempItems })
.SelectMany(t => t.tempItems.DefaultIfEmpty(), (t, temp) => new { t, temp })
.Where(t => ReferenceEquals(null, t.temp) || t.temp.Equals(default(T)))
.Select(t => t.t.item);
}
}
internal class UserRepository
{
private readonly Context _databaseContext;
public UserRepository(Context context)
{
_databaseContext = context;
}
public User UpdateUserManyToMany(User user, List<Guid> manyToManyds)
{
var dbContext = _databaseContext as DbContext;
dbContext?.TryUpdateManyToMany(user.ManyToMany, manyToManyds
.Select(x => new ManyToMany{
OtherEntityId = x,
UserId = user.Id,
}), x => x.OtherEntityId);
return user;
}
}
internal static class Program
{
private static async Task Main()
{
//
// Operations with referential integrity intact:
//
using var context = new Context();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
// Arrange
var repository = new UserRepository(context);
await context.Users.AddRangeAsync(GetUser());
await context.SaveChangesAsync();
// Act
var manyIds = new List<Guid>
{
new Guid("855d1a64-a707-40d5-ab93-34591a923abf"),
new Guid("855d1a64-a787-40d9-ac93-34591a923abf"),
new Guid("855d1a64-a707-41d9-ab93-39591a923abf")
};
var expected = repository.UpdateUserManyToMany(GetUser(), manyIds);
}
private static User GetUser()
=> User;
private static readonly User User = new User
{
Id = new Guid("30c35d2e-77fd-480b-9974-6ebf037a8f86"),
Name = "John"
};
}
}
System.InvalidOperationException : The instance of entity type 'ManyToMany' cannot be tracked because another instance with the same key value for {'UserId', 'OtherEntityId'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.
The error message says, that at least one of your ManyToMany entries already exists in the database (there is an entry with the same UserId and OtherEntityId combination).
You can verify this, by running the following code directly after your filled the manyIds variable with the 3 IDs:
var user = GetUser();
var alreadyExistingManyToMany = context.ManyToMany
.Where(m => m.UserId == user.Id &&
manyIds.Contains(m.OtherEntityId))
.ToList();
Debug.Assert(alreadyExistingManyToMany.Count == 0);
How do you log the HTTP Requests that the Kentico Kontent .NET delivery API here: https://github.com/Kentico/kontent-delivery-sdk-net
Specifically what I am looking for is how to log the HTTP Get requests to delivery.kentico.ai (the end point that you retrieve your content JSON from).
You can enrich and inject an HttpClient to the DeliveryClient.
Enrich:
public class LoggingHandler : DelegatingHandler
{
public LoggingHandler(HttpMessageHandler innerHandler, Microsoft.Extensions.Logging.ILogger logger)
: base(innerHandler)
{
Logger = logger;
}
public Microsoft.Extensions.Logging.ILogger Logger { get; }
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Logger.LogInformation(request.Method + " " + request.RequestUri);
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
Logger.LogInformation(response.StatusCode + " " + response.Content.Headers);
return response;
}
}
Use e.g. Serilog
services.AddLogging(builder =>
{
// Add Serilog
builder.AddSerilog(new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.File("logs\\log.log", rollingInterval: RollingInterval.Day)
.CreateLogger());
});
var serviceProvider = services.BuildServiceProvider();
var logger = serviceProvider.GetRequiredService<ILogger<Startup>>();
HttpClient httpClient = new HttpClient(new LoggingHandler(new HttpClientHandler(), logger));
var deliveryOptions = new DeliveryOptions();
Configuration.GetSection(nameof(DeliveryOptions)).Bind(deliveryOptions);
Inject:
var deliveryClient = DeliveryClientBuilder
.WithOptions(_ => deliveryOptions)
.WithHttpClient(httpClient)
.Build();
Additional resources:
https://merbla.com/2018/04/25/exploring-serilog-v2---using-the-http-client-factory/
UseSerilogRequestLogging()
Alternative approach using HttpClientFactory:
public void ConfigureServices(IServiceCollection services)
{
services.AddLogging(builder =>
{
// Add Serilog
builder.AddSerilog(new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.File("logs\\log.log", rollingInterval: RollingInterval.Day)
.CreateLogger());
});
services.AddTransient<LoggingHandler>();
services.AddHttpClient("FactoryClient", c => { /* Do whatever else you wish here... */ })
.AddHttpMessageHandler<LoggingHandler>()
.AddTypedClient(c => DeliveryClientBuilder.WithOptions(...).WithHttpClient(c).Build());
services.AddControllersWithViews();
}
LoggingHandler.cs
public class LoggingHandler : DelegatingHandler
{
public Microsoft.Extensions.Logging.ILogger Logger { get; }
public LoggingHandler(ILoggerFactory loggerFactory) : base()
{
Logger = loggerFactory.CreateLogger<LoggingHandler>();
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Logger.LogInformation(request.Method + " " + request.RequestUri);
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
Logger.LogInformation(response.StatusCode + " " + response.Content.Headers);
return response;
}
}
Yet another approach is to use the Serilog.AspNetCore NuGet package: https://github.com/serilog/serilog-aspnetcore
Program.cs
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.UseSerilog(); // <-- Add this line;
}
Startup.cs
public void Configure(IApplicationBuilder app)
{
app.UseSerilogRequestLogging();
}
Example code: https://github.com/Kentico/kontent-sample-app-net/commit/44f1a0e6b245b7ad0be2f0e48a1085adbf80584a
I am new to ReactiveUI and trying to test a view model that looks like this:
public interface IService
{
Task<SessionModel> GetData(string id);
}
/// Provides a group of schedulers available to be used
public interface ISchedulers
{
IScheduler Default { get; }
IScheduler Dispatcher { get; }
}
public class MyVm : ReactiveObject
{
IService service;
public MyVm(ISchedulers schedulers, IService service)
{
this.service = service;
this.session = this.WhenAnyValue(x => x.SessionId)
.SelectMany(SearchSession)
.ObserveOn(schedulers.Default)
.ToProperty(this, x => x.Session);
}
private async Task<SessionModel> SearchSession(string id)
{
return await this.service.GetData(id);
}
private string sessionId;
public string SessionId
{
get => sessionId;
set => this.RaiseAndSetIfChanged(ref sessionId, value);
}
readonly ObservableAsPropertyHelper<SessionModel> session;
public SessionModel Session
{
get { return session.Value; }
}
}
public class SessionModel { }
I'm mocking the service call to return dummy data, but not sure what I need to do with a TestScheduler in order to get the SelectMany to work.
Here's a test class that shows how i would create a test for the view model. The goal is to eventually be able to check that the model got set:
[TestClass]
public class MyVmTests
{
[TestMethod]
public void CreateClass
{
var subject = new MyVm(/*pass in mocks*/);
subject.SessionId="test";
Assert.IsNotNull(subject.Session);
}
}
I don't think using TestScheduler is necessary. The following passes for me (using Moq):
var mockSchedulers = new Mock<ISchedulers>();
mockSchedulers.Setup(s => s.Default).Returns(Scheduler.Immediate);
var id = "123";
var mockService = new Mock<IService>();
var returnSession = new SessionModel();
mockService.Setup(s => s.GetData(It.Is<string>(i => i == id)))
.ReturnsAsync(returnSession);
var target = new MyVm(mockSchedulers.Object, mockService.Object);
target.SessionId = id;
Assert.IsNotNull(target.Session);
Assert.AreEqual(returnSession, target.Session);
TestScheduler is best when you're trying to test something with time (like a Delay, proving that the Delay actually happened). You're not really doing that here.