I'm using ASP.NET Boilerplate with .NET Core 3.1.
I'm trying to save SignalR chat history to the database. The problem is when I want to create a subclass of AsyncCrudAppService and Hub, an error occurred with below text:
Class MessageAppService cannot have multiple base classes 'Hub' and 'AsyncCrudAppService'
Here is my code:
namespace MyProject.ChatAppService
{
public class MessageAppService : Hub, AsyncCrudAppService<Message, MessageDto, int, PagedAndSortedResultRequestDto, CreateMessageDto, UpdateMessageDto, ReadMessageDto>
{
private readonly IRepository<Message> _repository;
private readonly IDbContextProvider<MyProjectDbContext> _dbContextProvider;
private MyProjectPanelDbContext db => _dbContextProvider.GetDbContext();
public MessageAppService(
IDbContextProvider<MyProjectDbContext> dbContextProvider,
IRepository<Message> repository)
: base(repository)
{
_repository = repository;
_dbContextProvider = dbContextProvider;
}
public List<Dictionary<long, Tuple<string, string>>> InboxChat()
{
// The result will be List<userid, Tuple<username, latest message>>();
List<Dictionary<long, Tuple<string, string>>> result = new List<Dictionary<long, Tuple<string, string>>>();
List<User> listOfAllUsers = db.Set<User>().ToList();
listOfAllUsers.ForEach((user) =>
{
try
{
var dict = new Dictionary<long, Tuple<string, string>>();
var latestMessage = (from msg in db.Set<Message>() select msg)
.Where(msg => msg.CreatorUserId == user.Id && msg.receiverID == AbpSession.UserId)
.OrderByDescending(x => x.CreationTime)
.FirstOrDefault()
.Text.ToString();
dict.Add(user.Id, Tuple.Create(user.UserName, latestMessage));
result.Add(dict);
}
catch (Exception ex)
{
new UserFriendlyException(ex.Message.ToString());
}
});
return result;
}
public List<Message> getMessageHistory(int senderId)
{
return _repository.GetAll()
.Where(x => x.CreatorUserId == senderId && x.receiverID == AbpSession.UserId )
.ToList();
}
}
}
How could I avoid this error?
Update
Here is MyChatHub code that I wanted to combine with the AsyncCrudAppService subclass to become one class (I don't know if this way is correct but this was what came to my mind!).
public class MyChatHub : Hub, ITransientDependency
{
public IAbpSession AbpSession { get; set; }
public ILogger Logger { get; set; }
public MyChatHub()
{
AbpSession = NullAbpSession.Instance;
Logger = NullLogger.Instance;
}
public async Task SendMessage(string message)
{
await Clients.All.SendAsync("getMessage", string.Format("User {0}: {1}", AbpSession.UserId, "the message that has been sent from client is "+message));
}
public async Task ReceiveMessage(string msg, long userId)
{
if (this.Clients != null)
{
await Clients.User(userId.ToString())
.SendAsync("ReceiveMessage", msg, "From Server by userID ", Context.ConnectionId, Clock.Now);
}
else
{
throw new UserFriendlyException("something wrong");
}
}
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
Logger.Debug("A client connected to MyChatHub: " + Context.ConnectionId);
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await base.OnDisconnectedAsync(exception);
Logger.Debug("A client disconnected from MyChatHub: " + Context.ConnectionId);
}
}
Your AsyncCrudAppService subclass can't and shouldn't inherit Hub.
Instead, inject and use IHubContext<MyChatHub> similar to ABP's SignalRRealTimeNotifier.
public MessageAppService(
IHubContext<MyChatHub> hubContext,
IDbContextProvider<MyProjectDbContext> dbContextProvider,
IRepository<Message> repository)
: base(repository)
{
_dbContextProvider = dbContextProvider;
_hubContext = hubContext;
_repository = repository;
}
To send a message to all clients, call _hubContext.Clients.All.SendAsync(...).
References:
https://learn.microsoft.com/en-us/aspnet/core/signalr/hubcontext?view=aspnetcore-3.1
https://aspnetboilerplate.com/Pages/Documents/SignalR-AspNetCore-Integration
Related
I have an issue that my FluentValidations does not work on my Clean (Onion) Architecture.
It checks in my AddVehicleCommand class, and checks some rules, but does not apply them (it goes to the repository, and work as there no validations at all)
Here is the usage of FluentValidations
public class AddVehicleCommandValidator : AbstractValidator<AddVehicleCommand>
{
private readonly IVehicleRepositoryAsync _repositoryVehicle;
public AddVehicleCommandValidator(IVehicleRepositoryAsync repositoryVehicle)
{
_repositoryVehicle = repositoryVehicle;
RuleFor(x => x.Vehicle.Model).NotNull().WithMessage("Model is required.");
RuleFor(x => x.Vehicle.Maker).NotNull().WithMessage("Maker is required.");
RuleFor(x => x.Vehicle.UniqueId).NotNull().WithMessage("UniqueId is required").Must(CheckKeyComposition).WithMessage("UniqueId needs to be in fomrat C<number>");
RuleFor(x => x.Vehicle.UniqueId).MustAsync((x, cancellation) => AlreadyInUse(x)).WithMessage("This id is already in use.");
}
private async Task<bool> AlreadyInUse(string key)
{
var entity = await _repositoryVehicle.GetById(key);
if(entity == null)
{
return true;
}
var id = entity.UniqueId;
if(key == id)
{
return true;
}
return false;
}
private bool CheckKeyComposition(string key)
{
var firstChar = key.Substring(0);
var secondChar = key.Substring(1, key.Length-1);
int number;
bool res = int.TryParse(secondChar, out number);
if (firstChar.Equals("C") && res)
{
return true;
}
else
{
return false;
}
}
}
and I implemented Behavior for this FLuentValidation:
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
if (_validators.Any())
{
var context = new ValidationContext<TRequest>(request);
var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
var failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToList();
}
return await next();
}
}
Also I have registered FLuentValidation in my ServiceExtension class (DI):
public static class ServiceExtension
{
public static void AddApplicationLayer(this IServiceCollection services)
{
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
services.AddMediatR(Assembly.GetExecutingAssembly());
services.AddAutoMapper(Assembly.GetExecutingAssembly());
}
}
The example of calling logic for AddVehicleCommand:
public class AddVehicleCommand : IRequest<Result<VehicleDto>>
{
public VehicleDto? Vehicle { get; set; }
}
public class AddVehicleCommandHanlder : IRequestHandler<AddVehicleCommand, Result<VehicleDto>>
{
private readonly IVehicleRepositoryAsync _vehicleRepository;
private IMapper _mapper;
public AddVehicleCommandHanlder(IVehicleRepositoryAsync vehicleRepository, IMapper mapper)
{
_vehicleRepository = vehicleRepository;
_mapper = mapper;
}
public async Task<Result<VehicleDto>> Handle(AddVehicleCommand request, CancellationToken cancellationToken)
{
Result<VehicleDto> result = new();
try
{
if (request.Vehicle != null)
{
VehicleDto vehicle = new();
vehicle.Maker = request.Vehicle.Maker;
vehicle.Model = request.Vehicle.Model;
vehicle.UniqueId = await getNewId();
Vehicle entity = _mapper.Map<Vehicle>(vehicle);
var response = await _vehicleRepository.AddAsync(entity);
result.Success = true;
result.StatusCode = System.Net.HttpStatusCode.OK;
result.Data = vehicle;
return result;
}
}
catch (Exception ex)
{
result.ErrorMessage = ex.Message;
result.StatusCode = System.Net.HttpStatusCode.InternalServerError;
result.Success = false;
return result;
}
result.ErrorMessage = "Bad request.";
result.StatusCode = System.Net.HttpStatusCode.BadRequest;
result.Success = false;
return result;
}
private async Task<string> getNewId()
{
var latestId = await getLatestFreeId();
var newId = (Convert.ToInt32(latestId.Substring(1, latestId.Length-1)) + 1).ToString();
return string.Format("C{0}", newId);
}
private async Task<string> getLatestFreeId()
{
var latestId = await _vehicleRepository.GetLatestFreeId();
if (string.IsNullOrEmpty(latestId))
{
//default Vehicle Id
return "C0";
}
return latestId;
}
}
It hit the validator, but doesnt apply it (does not return any error, but success code). Why?
UPDATE#1:
I partially succeeded to present errors with this Behavior:
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
var context = new ValidationContext<TRequest>(request);
var failures = _validators
.Select(v => v.Validate(context))
.SelectMany(result => result.Errors)
.Where(f => f != null)
.ToList();
if (failures.Count != 0)
{
throw new FluentValidation.ValidationException(failures);
}
return next();
}
}
Also, I changed the registration of fluentvalidation:
public static IServiceCollection AddApplicationLayer(this IServiceCollection services)
{
services.AddMediatR(Assembly.GetExecutingAssembly());
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
services.AddAutoMapper(Assembly.GetExecutingAssembly());
return services;
}
But Status code is 500, and I would like to be 400 BadRequest, also, I would like to have better preview in some kind of list or something
I am building a inbox transactional pattern approach for "intercepting" messages using an oberserver and filter to log them to a database. The idea is if a consumer failed to let a background worker periodically check the database for any records where the Integration event has failed, retrieve them and re-execute them.
The data being stored in the DB looks like this:
public class InboxMessage
{
public long Id { get; private set; }
public DateTime CreatedDate { get; private set; }
public string CreatedUser { get; private set; }
public DateTime EditedDate { get; private set; }
public string EditedUser { get; private set; }
public string MessageType { get; private set; }
public string? ConsumerType { get; private set; }
public string Data { get; private set; }
public Guid EventNumber { get; private set; }
public EventStatus Status { get; private set; }
}
The idea is to retrieve all messages with a Status = Failed, use reflection (or perhaps something else?) to Deserialise the "Data" prop to the MessageType. The ConsumerType would then be used to re-execute the consumer.
I use a Filter to log the initial message (before being transfered to the consumer):
public class InboxPatternConsumerFilter<T> : IFilter<ConsumeContext<T>> where T : class
{
private readonly IntegrationEventsContext _context;
private readonly ILogger _logger;
private const string MassTransitDynamicTypeName = "MassTransit.DynamicInternal.";
public InboxPatternConsumerFilter(ILoggerFactory logger, IntegrationEventsContext context)
{
_logger = logger.CreateLogger("InboxPatternConsumerFilter");
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public async Task Send(ConsumeContext<T> context, IPipe<ConsumeContext<T>> next)
{
if (context.Message is IIntegrationEvent #event)
{
try
{
_logger.LogInformation("Integration event is type of - {generic}. Applying inbox pattern.", nameof(IGenericIntegrationEvent));
var message = new InboxMessage(
#event.EditedUser ?? "unknown",
context.Message.GetType().FullName?.Replace(MassTransitDynamicTypeName, string.Empty) ?? string.Empty,
null,
System.Text.Json.JsonSerializer.Serialize(context.Message),
#event.EventId,
EventStatus.Received);
_context.InboxMessages.Add(message);
await _context.SaveChangesAsync(context.CancellationToken);
}
catch (Exception ex)
{
// exception is catched to ensure the consumer can still continue.
_logger.LogError(ex, "Failed to create inbox message");
}
}
await next.Send(context);
}
public void Probe(ProbeContext context) {}
}
My reason for using a filter would be to check the EventNumber to confirm whether this message already exists in the DB, this should allow me to prevent sending this message to the consumer to resolve the idempotent issue in cases where we are using Retry mechanism for failed messaged.
I use a basic ReceiveObserver to update the messages as follows:
public class ReceiveObserver : IReceiveObserver
{
private readonly IntegrationEventsContext _context;
private readonly ILogger<ReceiveObserver> _logger;
public ReceiveObserver(IntegrationEventsContext context, ILogger<ReceiveObserver> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public async Task PostConsume<T>(ConsumeContext<T> context, TimeSpan duration, string consumerType) where T : class
{
try
{
if (context.Message is IIntegrationEvent #event)
{
var message = await _context.InboxMessages.FirstOrDefaultAsync(x => x.EventNumber == #event.EventId);
if (message is not null)
{
message.Update(EventStatus.Completed, "post-consumer", consumerType);
_context.Update(message);
await _context.SaveChangesAsync();
var typeTest = System.Reflection.Assembly
.GetEntryAssembly()?
.GetType(consumerType);
}
else
{
_logger.LogWarning("Inbox Message not found");
}
}
}
catch (Exception ex)
{
_logger.LogError("An error occurred trying to update the Message's complete status", ex);
}
// called when the message was consumed, once for each consumer
}
public async Task ConsumeFault<T>(ConsumeContext<T> context, TimeSpan elapsed, string consumerType, Exception exception) where T : class
{
if (context.Message is IIntegrationEvent #event)
{
var message = await _context.InboxMessages.FirstOrDefaultAsync(x => x.EventNumber == #event.EventId);
if (message is not null)
{
message.Update(EventStatus.Failed, "consumer-fault", consumerType);
_context.Update(message);
await _context.SaveChangesAsync();
}
else
{
_logger.LogWarning("Inbox Message not found");
}
}
// called when the message is consumed but the consumer throws an exception
}
public Task ReceiveFault(ReceiveContext context, Exception exception)
{
// TODO: Get the message id and update the status in db.
// called when an exception occurs early in the message processing, such as deserialization, etc.
return Task.CompletedTask;
}
}
The idea is then to use a background service to check for any failed messages as follows:
public class InboxMessageService : IHostedService
{
private readonly IBusControl _bus;
private readonly IntegrationEventsContext _context;
private readonly ILogger<InboxMessageService> _logger;
private readonly IServiceProvider _serviceProvider;
public InboxMessageService(
IBusControl bus,
IntegrationEventsContext context,
ILogger<InboxMessageService> logger,
IServiceProvider serviceProvider)
{
_bus = bus ?? throw new ArgumentNullException(nameof(context)); ;
_context = context ?? throw new ArgumentNullException(nameof(context));
_logger = logger ?? throw new ArgumentNullException(nameof(context));
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
public async Task StartAsync(CancellationToken cancellationToken)
{
List<InboxMessage> messages = await _context.InboxMessages
.Where(x => x.Status != EventStatus.Completed)
.ToListAsync(cancellationToken);
foreach(InboxMessage message in messages)
{
try
{
if (message.ConsumerType is null)
{
_logger.LogWarning("Unable to find the consumer type to start this message.");
continue;
}
var typeTest = System.Reflection.Assembly
.GetEntryAssembly()?
.GetType(message.ConsumerType);
if (typeTest is null)
{
throw new Exception();
}
var constructor = typeTest.GetConstructors().First(); // We can always assume that the consumer will contain a ctor
var parameters = new List<object?>();
foreach (var param in constructor.GetParameters())
{
var service = _serviceProvider.GetService(param.ParameterType);//get instance of the class
parameters.Add(service);
}
var obj = Activator.CreateInstance(typeTest, parameters.ToArray());
// TODO: fiqure out how to create a ConsumeContext<T> message from the DB data
typeTest.GetMethod("Consume")?.Invoke(obj, System.Reflection.BindingFlags.InvokeMethod, Type.DefaultBinder, null, null);
}
catch (Exception ex)
{
// ...
}
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
The part I am struggling with is the TODO in the background service to figure out how to essentially recreate the Message (using the MessageType and Data props) and invoke the Consumer (using the ConsumerType).
I'm stuck and the docks for the lib are unhelpful. Given the below saga definition:
public class GetOrdersStateMachine : MassTransitStateMachine<GetOrdersState>
{
public State? FetchingOrdersAndItems { get; private set; }
public Event<GetOrders>? GetOrdersIntegrationEventReceived { get; private set; }
public GetOrdersStateMachine()
{
Initially(
When(GetOrdersIntegrationEventReceived)
.Activity(AddAccountIdToState)
.TransitionTo(FetchingOrdersAndItems));
}
private EventActivityBinder<GetOrdersState, GetOrders> AddAccountIdToState(
IStateMachineActivitySelector<GetOrdersState, GetOrders> sel) =>
sel.OfType<AddAccountIdToStateActivity>();
}
And the below activity definition:
public class AddAccountIdToStateActivity : Activity<GetOrdersState, GetOrders>
{
private readonly IPartnerService _partnerService;
public AddAccountIdToStateActivity(IPartnerService partnerService) => _partnerService = partnerService;
public void Probe(ProbeContext context) =>
context.CreateScope($"GetOrders{nameof(AddAccountIdToStateActivity)}");
public void Accept(StateMachineVisitor visitor) => visitor.Visit(this);
public async Task Execute(
BehaviorContext<GetOrdersState, GetOrders> context,
Behavior<GetOrdersState, GetOrders> next)
{
context.Instance.AccountId = await _partnerService.GetAccountId(context.Data.PartnerId);
await next.Execute(context);
}
public Task Faulted<TException>(
BehaviorExceptionContext<GetOrdersState, GetOrders, TException> context,
Behavior<GetOrdersState, GetOrders> next) where TException : Exception =>
next.Faulted(context);
}
And the below test definition:
var machine = new GetOrdersStateMachine();
var harness = new InMemoryTestHarness();
var sagaHarness = harness.StateMachineSaga<GetOrdersState, GetOrdersStateMachine>(machine);
var #event = new GetOrders("1", new[] {MarketplaceCode.De}, DateTime.UtcNow);
await harness.Start();
try
{
await harness.Bus.Publish(#event);
await harness.Bus.Publish<ListOrdersErrorResponseReceived>(new
{
#event.CorrelationId,
AmazonError = "test"
});
var errorMessages = sagaHarness.Consumed.Select<ListOrdersErrorResponseReceived>().ToList();
var sagaResult = harness.Published.Select<AmazonOrdersReceived>().ToList();
var state = sagaHarness.Sagas.Contains(#event.CorrelationId);
harness.Consumed.Select<GetOrders>().Any().Should().BeTrue();
sagaHarness.Consumed.Select<GetOrders>().Any().Should().BeTrue();
harness.Consumed.Select<ListOrdersErrorResponseReceived>().Any().Should().BeTrue();
errorMessages.Any().Should().BeTrue();
sagaResult.First().Context.Message.IsFaulted.Should().BeTrue();
errorMessages.First().Context.Message.CorrelationId.Should().Be(#event.CorrelationId);
errorMessages.First().Context.Message.AmazonError.Should().Be("test");
state.IsFaulted.Should().BeTrue();
}
finally
{
await harness.Stop();
}
As you can see, the AddAccountToStateActivity has a dependency on the IPartnerService. I can't figure a way to configure that dependency.There's nothing in the docs and neither can I find anything on the github. How do I do it?
Thanks to the help of one of the library's authors I ended up writing this code:
private static (InMemoryTestHarness harness, IStateMachineSagaTestHarness<GetOrdersState, GetOrdersStateMachine> sagaHarness) ConfigureAndGetHarnesses()
{
var provider = new ServiceCollection()
.AddMassTransitInMemoryTestHarness(cfg =>
{
cfg.AddSagaStateMachine<GetOrdersStateMachine, GetOrdersState>().InMemoryRepository();
cfg.AddSagaStateMachineTestHarness<GetOrdersStateMachine, GetOrdersState>();
})
.AddLogging()
.AddSingleton(Mock.Of<IPartnerService>())
.AddSingleton(Mock.Of<IStorage>())
.BuildServiceProvider(true);
var harness = provider.GetRequiredService<InMemoryTestHarness>();
var sagaHarness = provider
.GetRequiredService<IStateMachineSagaTestHarness<GetOrdersState, GetOrdersStateMachine>>();
return (harness, sagaHarness);
}
As you can see I'm registering my mocks with the ServiceProvider.
I am working on an API and am having problems with making multiple calls to a service and it's different methods, I have each method creating and using new DBContext (or at least that's the intention), but after the first service call the others complain that the DBContext has been disposed, I was hoping you could point me in the right direction, because as far as I can see I am creating a new context for each of these calls - obviously I am doing something wrong here, any help would be much appreciated.
The actual error I am getting is "Cannot access a disposed object."
I know I can maybe pull the db interaction and context creation code out of the service and into the controller method here (it's a simplified example), but will need to use more services in other parts of the application and have encountered the problem there also, so would like to try and identify what is causing my problem in this example so that I can apply the fix elsewhere.
Here are the simplified classes involved.
public class UserController : Controller
{
private readonly IUserService userService;
public UserController(IUserService userService)
{
this.userService = userService;
}
[HttpPost]
[ActionName("PostUserDetails")]
public async Task<IActionResult> PostUserDetails([FromBody]UserDetailsContract userDetailsContract)
{
// this call is fine
var user = await userService.GetUserByCode(userDetailsContract.Code);
if (user == null)
{
return BadRequest("User not found");
}
// this call fails with the object disposed error
var userDetails = await userService.GetUserDetailsByCode(userDetailsContract.Code);
if (userDetails != null)
{
return BadRequest("UserDetails already exists");
}
// .. go on to save new entity
return Ok();
}
}
public class UserService : IUserService
{
private readonly IDatabaseFactory databaseFactory;
public UserService(IDatabaseFactory databaseFactory)
{
this.databaseFactory = databaseFactory;
}
public async Task<User> GetUserByCode(string code)
{
using (var db = databaseFactory.Create())
{
return await db.Users.GetByCode(code);
}
}
public async Task<IEnumerable<UserDetail>> GetUserDetailsByCode(string code)
{
using (var db = databaseFactory.Create())
{
return await db.UserDetails.GetByCode(code);
}
}
}
public class ApiDbContext : DbContext, IApiDbContext
{
public DbSet<User> Users { get; set; }
public DbSet<UserDetail> UserDetails { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(#"Server=192.168.1.1;Database=dbname;User Id=user; Password=pwd; MultipleActiveResultSets=True;");
}
}
public class DatabaseFactory : IDatabaseFactory
{
public IApiDatabase Create()
{
return new ApiDatabase(new ApiDbContext());
}
}
public class ApiDatabase : RepositoriesBase, IApiDatabase
{
private IUserRepository userRepository;
private IUserDetailsRepository userDetailsRepository;
public ApiDatabase(ApiDbContext context) : base(context)
{
}
public IUserRepository Users => userRepository ?? (userRepository = new UserRepository(context));
public IUserDetailsRepository UserExchange => userDetailsRepository ?? (userDetailsRepository = new UserDetailsRepository(context));
}
public abstract class RepositoriesBase : IRepositories
{
internal readonly ApiDbContext context;
private bool isDisposing;
protected RepositoriesBase(ApiDbContext context)
{
}
public void Dispose()
{
if (!isDisposing)
{
isDisposing = true;
context?.Dispose();
}
}
public Task SaveChanges() => context.SaveChangesAsync();
}
public class UserRepository : Repository<User>, IUserRepository
{
public UserRepository(ApiDbContext context) : base(context)
{
}
public async Task<User> GetByCode(string code)
{
return Filter(x => x.code == code).Result.FirstOrDefault();
}
}
public class UserDetailsRepository : Repository<UserDetail>, IUserDetailRepository
{
public UserExchangeRepository(ApiDbContext context) : base(context)
{
}
public async Task<IEnumerable<UserDetail>> GetByUserId(int userId)
{
return await Filter(x => x.UserId == userId);
}
}
public class Repository<T> : IRepository<T> where T : class, IEntity
{
private readonly ApiDbContext context;
public Repository(ApiDbContext context) => this.context = context;
public async Task Add(T entity)
{
context.Set<T>().Add(entity);
}
public async Task Add(IEnumerable<T> entities)
{
foreach (var entity in entities)
{
context.Set<T>().Add(entity);
}
}
public async Task Delete(T entity)
{
context.Set<T>().Remove(entity);
}
public async Task Delete(IEnumerable<T> entities)
{
foreach (var entity in entities)
{
context.Set<T>().Remove(entity);
}
}
public async Task Delete(int id)
{
var entityToDelete = context.Set<T>().FirstOrDefault(e => e.Id == id);
if (entityToDelete != null)
{
context.Set<T>().Remove(entityToDelete);
}
}
public async Task Update(T entity)
{
context.Set<T>().Update(entity);
}
public async Task Edit(T entity)
{
var editedEntity = context.Set<T>().FirstOrDefault(e => e.Id == entity.Id);
editedEntity = entity;
}
public async Task<IEnumerable<T>> GetAll(Expression<Func<T, bool>> predicate = null)
{
var query = context.Set<T>().Include(context.GetIncludePaths(typeof(T)));
if (predicate != null)
{
query = query.Where(predicate);
}
return await query.ToListAsync();
}
public async Task<T> GetById(int id)
{
return context.Set<T>().FirstOrDefault(e => e.Id == id);
}
public async Task<IEnumerable<T>> Filter()
{
return context.Set<T>();
}
public virtual async Task<IEnumerable<T>> Filter(Func<T, bool> predicate)
{
return context.Set<T>().Where(predicate);
}
public async Task SaveChanges() => context.SaveChanges();
}
In my DI config I have DatabaseFactory and UserService defined as singletons.
Error: "Cannot access a disposed object."
More error details: " at
Microsoft.EntityFrameworkCore.DbContext.CheckDisposed() at
Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies()
at Microsoft.EntityFrameworkCore.DbContext.get_Model() at
Microsoft.EntityFrameworkCore.Internal.InternalDbSet1.get_EntityType()
at
Microsoft.EntityFrameworkCore.Internal.InternalDbSet1.get_EntityQueryable()
at
Microsoft.EntityFrameworkCore.Internal.InternalDbSet1.System.Collections.Generic.IEnumerable<TEntity>.GetEnumerator()
at System.Linq.Enumerable.WhereEnumerableIterator1.MoveNext() at
System.Linq.Enumerable.Any[TSource](IEnumerable1 source, Func2
predicate) at
App.Api.Controllers.UserController.PostUserDetail(UserDetailContract
userDetailContract) in
D:\Repositories\application\src\App\Api\Controllers\UserController.cs:line
89"
Thank you
I think you may be a victim of delayed execution. The following piece of code creates an instance of of ApiDatabase which in turn creates a new ApiDbContext:
public IApiDatabase Create() //in DatabaseFactory
{
return new ApiDatabase(new ApiDbContext());
}
I detect a code smell here, by the way, as ApiDbContext is disposable so you should be tracking this reference and disposing of it properly.
Anyways, ApiDatabase is disposable since it's wrapped in a using statement, so I think the the context is being disposed after the call to GetByUserId:
public async Task<IEnumerable<UserDetail>> GetByUserId(int userId)
{
return await Filter(x => x.UserId == userId);
}
Notice you are returning an enumeration. I think it may not be materialized by the time you use it, hence the error. Add a cast to an array to force materialization:
return await Filter(x => x.UserId == userId).ToArray();
Your problem is the signature of this method:
public async Task<IEnumerable<UserDetail>> GetUserDetailsByCode(string code)
{
using (var db = databaseFactory.Create())
{
return await db.UserDetails.GetByCode(code);
}
}
IEnumerable<T> is an enumerable, which are generally lazy-evaluated. In the meantime, the Task<T> is considered complete once the enumerable is defined (not when it is completed). And the context is disposed once that enumerable is defined. You would have the same problem if the code was synchronous.
The fix is to "reify" (evaluate) the enumerable before the context is disposed:
public async Task<IReadOnlyCollection<UserDetail>> GetUserDetailsByCode(string code)
{
using (var db = databaseFactory.Create())
{
return await db.UserDetails.GetByCode(code).ToList();
}
}
I have partial interface
public partial interface ISponsoredService { GetX(), AddX() }
public partial interface ISponsoredService { GetY(), AddY() }
And partial implementations
public partial class SponsoredService : ISponsoredService {
public SponsoredService(
ISponsoredBrandsRepository sponsoredBrandRepository,
ISponsoredDiscountsRepository sponsoredDiscountsRepository) { }
}
public partial class SponsoredService { GetX(); AddX(); }
public partial class SponsoredService { GetY(); AddY(); }
And the problem is with these repositories because they use the same context injected in request scope like services and repositories.
public class SponsoredDiscountsRepository : BaseSponsoredRepository, ISponsoredDiscountsRepository
{
public SponsoredDiscountsRepository(
Context sponsoredContext) : base(sponsoredContext)
{
}
}
public class SponsoredBrandsRepository : BaseSponsoredRepository, ISponsoredBrandsRepository
{
public SponsoredBrandsRepository (
Context sponsoredContext) : base(sponsoredContext)
{
}
}
Ninject configuration:
kernel.Bind<Context>().ToSelf();
kernel.Bind<ISponsoredService>().To<SponsoredService>().InRequestScope();
kernel.Bind<ISponsoredBrandsRepository>().To<SponsoredBrandsRepository>().InRequestScope();
kernel.Bind<ISponsoredDiscountsRepository>().To<SponsoredDiscountsRepository>().InRequestScope();
And where is a problem?
When I execute AddX() and then GetX() in the same HTTP request the second operation(GetX()) hangs on the connection to the base. It lasts forever and never ends.
But if I do AddX() and then in other HTTP request GetX() it work.
Combination AddY() and in the same request GetX() do not work as well.
What is going on here? Context are not disposed because it's created in request scope. In the same request scope is created SponsoredService which contains repository1 and repository2 which contains created context.
Edit: Add implementation of repositories
These two repositories use one generic wchich contains:
protected readonly Context SponsoredContext;
protected readonly DbSet<TEntity> DbSet;
and all operations:
public virtual async Task<IEnumerable<TEntity>> GetAsync()
{
return await this.DbSet.ToListAsync();
}
GetX() { sponsoredBrandsRepository.GetAsync(); }
AddX() {
sponsoredBrandsRepository.GetAsync();
some operations
sponsoredBrandsRepository.AddAsync();
}
Edit add full code
WebAPI:
[RoutePrefix("api/sponsored")]
public partial class SponsoredController : BaseApiController
{
private readonly ISponsoredService _sponsoredService;
public SponsoredController(
ISponsoredService sponsoredService,
IBrandService brandService,
ICampaignsService discountService)
{
_sponsoredService = sponsoredService;
_discountService = discountService;
_brandService = brandService;
}
}
public partial class SponsoredController
{
private readonly IBrandService _brandService;
[Route("brands"), HttpGet]
public async Task<IHttpActionResult> GetBrands()
{
try
{
var viewModels = await GetBrandViewModels();
return Ok(viewModels);
}
catch (Exception e)
{
base.Log(e);
return InternalServerError();
}
}
[Route("brands"), HttpPost, ValidateModelStateFilter]
public async Task<IHttpActionResult> Post([FromBody] IEnumerable<SponsoredBrandAddOrUpdateViewModel> viewModels)
{
try
{
await this._sponsoredService.AddOrUpdate(viewModels.Select(vm => (SponsoredBrand)vm));
return Created("api/sponsored/brands", GetBrandViewModels());
}
catch (Exception e)
{
base.Log(e, viewModels);
return InternalServerError();
}
}
private async Task<List<SponsoredBrandListViewModel>> GetBrandViewModels()
{
var dbSponsoredBrands = await this._sponsoredService.GetSponsoredBrandsAsync();
var viewModels =
dbSponsoredBrands.Select(sponsoredBrand =>
{
var viewModel = (SponsoredBrandListViewModel)sponsoredBrand;
viewModel.Name = (this._brandService.GetBrandByBrandIdAsync(viewModel.BrandId).Result).Entity.Name;
return viewModel;
}).ToList();
return viewModels;
}
}
public partial class SponsoredController
{
private readonly ICampaignsService _discountService;
[Route("discounts"), HttpGet]
public async Task<IHttpActionResult> GetDiscounts()
{
try
{
var viewModels = await GetDiscountsViewModels();
return Ok(viewModels);
}
catch (Exception e)
{
base.Log(e);
return InternalServerError();
}
}
[Route("discounts"), HttpPost, ValidateModelStateFilter]
public async Task<IHttpActionResult> Post([FromBody] IEnumerable<SponsoredDiscountAddOrUpdateViewModel> viewModels)
{
try
{
await this._sponsoredService.AddOrUpdate(viewModels.Select(vm => (SponsoredDiscount)vm));
return Created("api/sponsored/discounts", GetDiscountsViewModels());
}
catch (Exception e)
{
base.Log(e, viewModels);
return InternalServerError();
}
}
private async Task<List<SponsoredDiscountListViewModel>> GetDiscountsViewModels()
{
var dbSponsoredBrands = await this._sponsoredService.GetSponsoredDiscountsAsync();
var viewModels =
dbSponsoredBrands.Select(sponsoredBrand =>
{
var viewModel = (SponsoredDiscountListViewModel)sponsoredBrand;
viewModel.Name = (this._discountService.GetCampaignByCampaignIdAsync(viewModel.DiscountId).Result)?.Entity?.Discount?.Name;
return viewModel;
}).ToList();
return viewModels;
}
}
Service:
public partial interface ISponsoredService
{
Task<IEnumerable<SponsoredDiscount>> GetSponsoredDiscountsAsync();
Task<IEnumerable<SponsoredDiscount>> AddOrUpdate(IEnumerable<SponsoredDiscount> sponsoredDiscounts);
}
public partial interface ISponsoredService
{
Task<IEnumerable<SponsoredDiscount>> GetSponsoredDiscountsAsync();
Task<IEnumerable<SponsoredDiscount>> AddOrUpdate(IEnumerable<SponsoredDiscount> sponsoredDiscounts);
}
public partial class SponsoredService : ISponsoredService
{
public SponsoredService(
ISponsoredBrandsRepository sponsoredBrandRepository,
ISponsoredDiscountsRepository sponsoredDiscountsRepository,
IBrandService brandService,
ICampaignsService discountsService)
{
_sponsoredBrandRepository = sponsoredBrandRepository;
_brandService = brandService;
_discountsService = discountsService;
_sponsoredDiscountsRepository = sponsoredDiscountsRepository;
}
}
public partial class SponsoredService
{
private readonly ISponsoredBrandsRepository _sponsoredBrandRepository;
private readonly IBrandService _brandService;
public async Task<IEnumerable<SponsoredBrand>> GetSponsoredBrandsAsync() => await this._sponsoredBrandRepository.GetAsync();
public async Task<IEnumerable<SponsoredBrand>> AddOrUpdate(IEnumerable<SponsoredBrand> sponsoredBrands)
{
// remove
var dbSponsored = await this.GetSponsoredBrandsAsync();
foreach (var dbS in dbSponsored)
{
if (!sponsoredBrands.Any(s => s.RelatedEntityId == dbS.RelatedEntityId))
{
await this.DeleteSponsoredBrand(dbS.Id);
}
}
// new
foreach (var newS in sponsoredBrands)
{
var brand = (await this._brandService.GetBrandByBrandIdAsync(newS.RelatedEntityId)).Entity;
brand.BrandRules = new List<BrandRule>
{
new BrandRule
{
IsHighlighted = true, Order = newS.Order, ValidTo = newS.To, ValidFrom = newS.From
}
}.ToList();
await this._brandService.UpdateAsync(brand);
}
this._sponsoredBrandRepository.Clear();
await this._sponsoredBrandRepository.Add(sponsoredBrands);
return null;
}
}
public partial class SponsoredService
{
private readonly ISponsoredDiscountsRepository _sponsoredDiscountsRepository;
private readonly ICampaignsService _discountsService;
public async Task<IEnumerable<SponsoredDiscount>> GetSponsoredDiscountsAsync()
=> await this._sponsoredDiscountsRepository.GetAsync();
public async Task<IEnumerable<SponsoredDiscount>> AddOrUpdate(IEnumerable<SponsoredDiscount> sponsoredDiscounts)
{
// remove
var dbSponsored = await this.GetSponsoredDiscountsAsync();
foreach (var dbS in dbSponsored)
if (!sponsoredDiscounts.Any(s => s.RelatedEntityId == dbS.RelatedEntityId))
await this.DeleteSponsoredDiscount(dbS.Id);
// new
foreach (var newS in sponsoredDiscounts)
if (!await this._discountsService.X(newS.RelatedEntityId, newS))
return null;
this._sponsoredDiscountsRepository.Clear();
await this._sponsoredDiscountsRepository.Add(sponsoredDiscounts);
return null;
}
}
Repositories:
public interface ISponsoredRepository<TEntity> : IBaseRepository<TEntity, int>
where TEntity : Sponsored.Sponsored
{
void Clear();
Task Add(IEnumerable<TEntity> entities);
}
public interface ISponsoredBrandsRepository : ISponsoredRepository<SponsoredBrand> { }
public interface ISponsoredDiscountsRepository : ISponsoredRepository<SponsoredDiscount> { }
public abstract class SponsoredRepository<TEntity> : ISponsoredRepository<TEntity>
where TEntity : Domain.Sponsored.Sponsored
{
protected readonly Context SponsoredContext;
protected readonly DbSet<TEntity> DbSet;
protected SponsoredRepository(Context sponsoredContext)
{
SponsoredContext = sponsoredContext;
DbSet = this.SponsoredContext.Set<TEntity>();
}
public virtual async Task<IEnumerable<TEntity>> GetAsync()
{
return await this.DbSet.ToListAsync();
}
public virtual void Clear()
{
this.DbSet.RemoveRange(this.DbSet);
this.SponsoredContext.SaveChanges();
}
public virtual async Task Add(IEnumerable<TEntity> entities)
{
this.DbSet.AddRange(entities);
await this.SponsoredContext.SaveChangesAsync();
}
}
public class SponsoredBrandsRepository : SponsoredRepository<SponsoredBrand>, ISponsoredBrandsRepository
{
public SponsoredBrandsRepository(
Context sponsoredContext) : base(sponsoredContext)
{
}
}
public class SponsoredDiscountsRepository : SponsoredRepository<SponsoredDiscount>, ISponsoredDiscountsRepository
{
public SponsoredDiscountsRepository(
Context sponsoredContext) : base(sponsoredContext)
{
}
}
ContexT:
public class Context
{
public virtual DbSet<SponsoredBrand> Brands { get; set; }
public virtual DbSet<SponsoredDiscount> Discounts { get; set; }
public Context()
{
}
}
IoC configuration(Ninject):
kernel.Bind<Context>().ToSelf();
kernel.Bind<ISponsoredService>().To<SponsoredService>().InRequestScope();
kernel.Bind<ISponsoredBrandsRepository>().To<SponsoredBrandsRepository>().InRequestScope();
kernel.Bind<ISponsoredDiscountsRepository>().To<SponsoredDiscountsRepository>().InRequestScope();
You're not binding Context InRequestScope, so by default it gets created as Transient scope, and therefore is probably not being disposed/closed.
more on Scopes:
https://github.com/ninject/ninject/wiki/Object-Scopes
Managing a single DbContext in a request is hard. You may have lots of different classes requesting it as a dependency and using it different ways. I ended up with a complex scoping mechanism to manage the DbContexts. However, it may be easier/better to inject a DbContext factory and use using to control the lifetime of the DbContext (unit of work pattern), or just new it up in your repository (and dispose it).
if you Google "entity framework dbcontext request", you will find many discussions and opinions about the matter. here is a good overview/intro: http://mehdi.me/ambient-dbcontext-in-ef6/