How to configure MassTransit with Mediator to publish messages? - c#

I'm new with MassTransit and Mediator, I have a series of events to execute in consecutive order, I'm using MassTransit in-process and in-memory, for my use case no transport is required.
I want to send and publish messages to consumers, sagas, activities through Mediator, I have the code below, but I want to improve it by registering MassTransit in startup.cs:
//asp net core 3.1 Controller
[ApiController]
public class MyController : ControllerBase
{
private readonly IProductService _productService ;
private readonly IMediator mediator;
public MyController(IProductService productService)
{
_productService = productService;
var repository = new InMemorySagaRepository<ApiSaga>();
mediator = Bus.Factory.CreateMediator(cfg =>
{
cfg.Saga<ProductSaga>(repository);
});
}
[HttpPost]
public async Task<IActionResult> Post([FromBody] ProductContract productContract)
{
try
{
var result = await _productService.DoSomeThingAsync(productContract);
await mediator.Publish<ProductSubmittedEvent>(new { CorrelationId = Guid.NewGuid(), result.Label });
return Ok();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
}
//My saga
public class ProductSaga :
ISaga,
InitiatedBy<ProductSubmittedEvent>
{
public Guid CorrelationId { get; set; }
public string State { get; private set; } = "Not Started";
public Task Consume(ConsumeContext<ProductSubmittedEvent> context)
{
var label= context.Message.Label;
State = "AwaitingForNextStep";
//...
//send next command
}
}
Like this it works but it's not proper, I want to configure masstransit with Mediator in my startup.cs to have one proper instance, to do that I started by deleting the IMediator, using an IPublishEndpoint to publish messages to Saga and configuring my startup.cs, but it doesn't work as expected:
//startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMediator(cfg =>
{
cfg.AddSaga<ProductSaga>().InMemoryRepository();
});
}
//in controller using:
private readonly IPublishEndpoint _publishEndpoint;
//then
await _publishEndpoint.Publish<ProductSubmittedEvent>(
new { CorrelationId = Guid.NewGuid(), result.Label });
I got a System.InvalidOperationException:
Unable to resolve service for type 'MassTransit.IPublishEndpoint' while attempting to activate 'GaaS.API.Controllers.ManageApiController'.
I tried to update my startup.cs:
var repository = new InMemorySagaRepository<ApiSaga>();
services.AddMassTransit(cfg =>
{
cfg.AddBus(provider =>
{
return Bus.Factory.CreateMediator(x =>
{
x.Saga<ProductSaga>(repository);
});
});
});
I got:
Cannot implicitly convert type 'MassTransit.Mediator.IMediator' to 'MassTransit.IBusControl'.
If you have any recommendation ideas thanks for sharing and challenging me 😊

The proper way to configure MassTransit Mediator in your project is through the Startup.cs file, which you seem to have tried.
public void ConfigureServices(IServiceCollection services)
{
services.AddMediator(cfg =>
{
cfg.AddSaga<ProductSaga>().InMemoryRepository();
});
}
Using mediator, you need to depend upon the IMediator interface. You cannot use IPublishEndpoint or ISendEndpointProvider, as those are bus interfaces. Since you can have both mediator and a bus instance in the container at the same time, this would lead to confusion when resolving services from the container.
[ApiController]
public class MyController : ControllerBase
{
private readonly IProductService _productService ;
private readonly IMediator _mediator;
public MyController(IProductService productService, IMediator mediator)
{
_productService = productService;
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> Post([FromBody] ProductContract productContract)
{
try
{
var result = await _productService.DoSomeThingAsync(productContract);
await _mediator.Publish<ProductSubmittedEvent>(new { CorrelationId = NewId.NextGuid(), result.Label });
return Ok();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
}
If you are only using mediator, and want to use IPublishEndpoint, you could add that to the container yourself and delegate it.
services.AddSingleton<IPublishEndpoint>(provider => provider.GetService<IMediator>());

I got to this from the (excellent) youtube video - MassTransit starting with Mediator, in that sample there is a line of code
AddMediator()
which I couldn't locate. I believe the following setup provides everything needed to get code working based on that video...
services.AddMassTransit(config =>
{
config.AddRequestClient<ISubmitOrder>();
config.AddConsumersFromNamespaceContaining<SubmitOrderConsumer>();
config.UsingInMemory(ConfigureBus);
});
and ConfigureBus is then:
private void ConfigureBus(IBusRegistrationContext context, IInMemoryBusFactoryConfigurator configurator)
{
configurator.ConfigureEndpoints(context);
}
I couldn't readily find this elsewhere, hence posting here.

Related

Cannot access a disposed context instance with N-layer architecture

I'm trying to make a N-layer architecture for my Telegram Bot. I created DAL, BLL and PL. I would like to add entity News to my DB. But I have some issue with my context.
My DB Context:
public class ApplicationContext : DbContext
{
public DbSet<News> News { get; set; }
public DbSet<User> Users { get; set; }
public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<News>().Property(tn => tn.Id).ValueGeneratedOnAdd();
modelBuilder.Entity<User>().Property(tn => tn.Id).ValueGeneratedOnAdd();
modelBuilder.Entity<News>().Property(tn => tn.Title).IsRequired();
modelBuilder.Entity<News>().Property(tn => tn.Href).IsRequired();
modelBuilder.Entity<News>().Property(tn => tn.Image).IsRequired();
modelBuilder.Entity<News>().Property(tn => tn.Date).IsRequired();
modelBuilder.Entity<User>().Property(tn => tn.UserId).IsRequired();
modelBuilder.Entity<User>().Property(tn => tn.UserName).IsRequired();
modelBuilder.Entity<User>().Property(tn => tn.DateOfStartSubscription).IsRequired();
base.OnModelCreating(modelBuilder);
}
}
Interface UoW:
public interface IUnitOfWork : IDisposable
{
INewsRepository News { get; }
IUserRepository Users { get; }
int Complete();
}
Class UoW:
public class UnitOfWork : IUnitOfWork
{
public IUserRepository Users { get; }
public INewsRepository News { get; }
private readonly ApplicationContext _context;
public UnitOfWork(ApplicationContext context)
{
_context = context;
Users = new UserRepository.UserRepository(_context);
News = new NewsRepository.NewsRepository(_context);
}
public int Complete() => _context.SaveChanges();
public void Dispose() => _context.Dispose();
}
My DAL Generic Repository:
async Task IGenericRepository<T>.AddAsync(T entity) => await _context.Set<T>().AddAsync(entity);
DAL Injection:
public static class DALInjection
{
public static void Injection(IServiceCollection services)
{
services.AddTransient(typeof(IGenericRepository<>), typeof(GenericRepository<>));
services.AddTransient<IUserRepository, UserRepository.UserRepository>();
services.AddTransient<INewsRepository, NewsRepository.NewsRepository>();
services.AddTransient<IUnitOfWork, UnitOfWork.UnitOfWork>();
}
}
My BLL Service class:
public class ParserService : IParser
{
private IUnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public ParserService(IUnitOfWork unitOfWork, IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
private async Task SaveArticles(IEnumerable<NewsDTO> articlesDTO)
{
var articles = _mapper.Map<IEnumerable<NewsDTO>, IEnumerable<News>>(articlesDTO);
await _unitOfWork.News.AddAsync(articles.First());
_unitOfWork.Complete();
}
BLL Injection:
public static class BLLInjection
{
public static void Injection(IServiceCollection services)
{
DALInjection.Injection(services);
services.AddTransient<IParser, ParserService>();
services.AddTransient<IArticleService, ArticleService>();
services.AddAutoMapper(typeof(CommonMappingProfile));
}
}
My PL:
private static async Task SendArticleAsync(long chatId, int offset, int count)
{
var articles = await _parser.MakeHtmlRequest(offset, count);
foreach (var article in articles)
{
var linkButton = KeyboardGoOver("Перейти", article.Href);
await _client.SendPhotoAsync(chatId: chatId, photo: article.Image,
caption: $"*{article.Title}*", parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown, replyMarkup: linkButton);
}
await OnLoadMoreNewsAsync(chatId, offset + count, count);
}
PL Startup class:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext<ApplicationContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection"),
b => b.MigrationsAssembly(typeof(ApplicationContext).Assembly.FullName)));
BLLInjection.Injection(services);
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "TelegramBot.WebApi", Version = "v1" });
});
}
When I tried to debug, I had this error but I could not resolve this issue.
_context = Database = {"Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may o...
Could someone help me with this issue?
There are few problems in your code.
Controllers are scoped entities, their instances created per http request and disposed after request is finished. It means controller is not good place to subscribe to events. When you call /start endpoint you create an instance of TelegramController and TelegramBotClient, but once the request is finished, the controller and all its non-singleton dependencies (IParser in your case) are disposed. But you subscribed for TelegramBotClient events that captured reference to IParser. It means all events that will arrive after request is finished will try to access disposed IParser instance and this is the reason for your exception.
For event based messages it's better to use IHostedService. You will need to use IServiceScopeFactory to create a scope for each message and resolve your dependencies from this scope.
public class TelegramHostedService : IHostedService
{
private IServiceScopeFactory _scopeFactory;
public TimedHostedService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public Task StartAsync(CancellationToken stoppingToken)
{
_client = new TelegramBotClient(_token);
_client.OnMessage += OnMessageHandlerAsync;
_client.OnCallbackQuery += OnLoadCallBackAsync;
_client.StartReceiving();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken stoppingToken)
{
// TODO: Unsubscribe from events
return Task.CompletedTask;
}
public static async void OnMessageHandlerAsync(object sender, MessageEventArgs e)
{
using var scope = _scopeFactory.CreateScope();
var handler = scope.ServiceProvider.GetRequiredService<MessageHandler>();
await handler.Handle(TODO: pass required args); // Move the logic to separate handler class to keep hosted service clean
}
...
}
I moved call to _client.StartReceiving(); after event subscription otherwise there is a chance for race condition when you receive event but you don't yet have subscribers and this event will be lost.
The second issue is as #PanagiotisKanavos said: async void can't be awaited, hence once your code hit first true async method (like DB access, http request, file read or any other I/O operation) the control is returned to the point where async void method was called and continues execution without waiting for operation completion. The whole app can even crash if you throw unhandled exception from such method, hence async void should be avoided. To prevent these problems wrap your async event handlers with sync methods that will block the execution with Wait() method:
public class TelegramHostedService : IHostedService
{
private IServiceScopeFactory _scopeFactory;
public TimedHostedService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public Task StartAsync(CancellationToken stoppingToken)
{
_client = new TelegramBotClient(_token);
_client.OnMessage += OnMessageHandler;
_client.OnCallbackQuery += OnLoadCallBack;
_client.StartReceiving();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken stoppingToken)
{
// TODO: Unsubscribe from events
return Task.CompletedTask;
}
public static void OnMessageHandler(object sender, MessageEventArgs e)
{
OnMessageHandlerAsync(sender, e).Wait();
}
public static async Task OnMessageHandlerAsync(object sender, MessageEventArgs e)
{
using var scope = _scopeFactory.CreateScope();
var handler = scope.ServiceProvider.GetRequiredService<MessageHandler>();
await handler.Handle(TODO: pass required args); // Move the logic to separate handler class to keep hosted service clean
}
...
}

How to resolve MassTransit Mediator "some services are not able to be constructed" for request client

I'm having a look at MassTransit, and I used the masstransit dotnet temaplate to generate a worker, as per https://masstransit-project.com/getting-started/ ( everything up till RabbitMQ )
Then I was interested in getting the built in mediator working with responses, so changed the code according to https://masstransit-project.com/articles/mediator.html
so it the setup looks like ...
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddMediator(x =>
{
x.AddConsumer<MessageConsumer>();
x.AddRequestClient<Message>();
});
services.AddMassTransit(x =>
{
x.AddConsumersFromNamespaceContaining<MessageConsumer>();
x.UsingInMemory((context,cfg) =>
{
cfg.ConfigureEndpoints(context);
});
});
services.AddMassTransitHostedService(true);
services.AddHostedService<Worker>();
});
and the consumer / contract now looks like
public class Message { public string Text { get; set; } }
public class MessageResult { public string Text { get; set; } }
public class MessageConsumer : IConsumer<Message>
{
readonly ILogger<MessageConsumer> _logger;
public MessageConsumer(ILogger<MessageConsumer> logger)
{
_logger = logger;
}
public Task Consume(ConsumeContext<Message> context)
{
_logger.LogInformation("Received Text: {Text}", context.Message.Text);
return context.RespondAsync(new MessageResult() {Text = $"Got {context.Message.Text}"});
}
}
and the worker looks like
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly IBus _bus;
private readonly IRequestClient<Message> _request;
public Worker(ILogger<Worker> logger, IBus bus, IRequestClient<Message> request)
{
_logger = logger;
_bus = bus;
_request = request;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var response = await _request.GetResponse<MessageResult>(new Message {Text = $"The time is {DateTimeOffset.Now}"}, stoppingToken);
await Task.Delay(1000, stoppingToken);
}
}
}
however when I run it, the injection of IRequestClient seems to fail ( for some reason it wasn't registered? ) with the exception
Error while validating the service descriptor 'ServiceType: Microsoft.Extensions.Hosting.IHostedService Lifetime: Singleton ImplementationType: MTGettingStarted.Worker': Cannot consume scoped service 'MassTransit.IRequestClient`1[MTGettingStarted.Message]' from singleton 'Microsoft.Extensions.Hosting.IHostedService'.
Which is what I thought x.AddRequestClient<Message>(); should have done. Maybe the documentation is incomplete? or I missed something?
If I change the worker to manaully get the request, then it does work
var client = _mediator.CreateRequestClient<Message>();
var response = await client.GetResponse<MessageResult>(new Message {Text = $"The time is {DateTimeOffset.Now}"}, stoppingToken);
But still curious why the DI doesn't work?
IRequestClient<T> is registered in the container as scoped, which the error you posted indicates:
Cannot consume scoped service
Your hosted service is a singleton.
To get the sample working you have to enable your worker to use scoped services.
Take a look at the docs at: https://learn.microsoft.com/en-us/dotnet/core/extensions/scoped-service
In the implementation behind IScopedProcessingService you have to inject IRequestClient<>.
private async Task DoWorkAsync(CancellationToken stoppingToken)
{
using (var scope = _serviceProvider.CreateScope())
{
var scopedProcessingService = scope.ServiceProvider.GetRequiredService<IScopedProcessingService>();
await scopedProcessingService.DoWorkAsync(stoppingToken);
}
}

How to make SignalR HttpGet request

I need to make a way for app user to listen to server , and get updated HTTPGET request every time database is updated. Client side implementation is not needed, only server side. I have small experience in SignalR and would appreciate any help on Hub side .
My code so far
Startup
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DataContext>(options =>
{
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"));
});
services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly);
services.AddScoped<IContactRepository, ContactRepository>();
services.AddControllers();
services.AddSignalR();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Projekt", Version = "v1" });
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Projekt v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<UpdatesHub>("hubs/");
});
} ```
here are my ApiCalls user can use
[ApiController]
[Route("api/[controller]")]
public class ContactsController : ControllerBase
{
private readonly IContactRepository _contactRepository;
private readonly IMapper _mapper;
public ContactsController(IContactRepository contactRepository, IMapper mapper)
{
_contactRepository = contactRepository;
_mapper = mapper;
}
#region API CALLS
[HttpGet]
public async Task<ActionResult<IEnumerable<AppUserDto>>> GetUsers([FromQuery]UserParams userParams)
{
//Ego loading phone numbers, gives circular reference problem,
//use DTOs and Mapping instead
var users = await _contactRepository.GetUsersAsync(userParams);
Response.AddpaginationHeader(users.CurrentPage, users.PageSize,
users.TotalCount, users.TotalPages);
var usersToReturn = _mapper.Map<IEnumerable<AppUserDto>>(users);
return Ok(usersToReturn);
}
//~/api/Contacts/1
[HttpGet("{id}")]
public async Task<ActionResult<AppUser>> GetUser(int id)
{
var user = await _contactRepository.GetUserByIdAsync(id);
var userToReturn = _mapper.Map<AppUserDto>(user);
return Ok(userToReturn);
}
[HttpPost("addContact")]
public async Task<ActionResult> AddContact(AppUser appUser)
{
var IsConstrained = await _contactRepository.CheckIfConstrained(appUser);
if (IsConstrained)
{
return BadRequest("user already exists");
}
else
{
_contactRepository.AddContact(appUser);
var result = await _contactRepository.SaveAllAsync();
if (result)
return Ok();
else
return BadRequest("user not saved to database");
}
}
[HttpPut("update")]
public async Task<ActionResult> Update(AppUser user)
{
//Check if user changed his name, if changed ->check if it's unique then update,
//if name not changed just update
var UserBeforeUpdate = _contactRepository.GetUserByIdAsync(user.Id);
var IsNameChangedBool = _contactRepository.CheckIfNameChanged(user , UserBeforeUpdate.Result);
if (IsNameChangedBool)
{
var IsConstrained = await _contactRepository.CheckIfConstrained(user);
if (IsConstrained)
{
return BadRequest("user already exists");
}
else
{
_contactRepository.Update(user);
if (await _contactRepository.SaveAllAsync()) return Ok("Contact updated");
return BadRequest("user not saved to database");
}
}
else
{
_contactRepository.Update(user);
if (await _contactRepository.SaveAllAsync()) return Ok("Contact updated");
return BadRequest("user not saved to database");
}
} ```
This is Hub part i don't know how to send updated httpget request to user
{
public class UpdatesHub : Hub
{
public async Task SendMessageToCaller()
{
await Clients.Caller.SendAsync("RecieveMessage");
}
}
}
Please, be sure that the route to your hub is correct. For example, if your hub is in the hubs folder you need to add endpoints.MapHub<UpdatesHub>("/hubs/hubName");
Clients can connect to SignalR in your javaScript with:
var connection = new signalR.HubConnectionBuilder().withUrl("/hubs/hubName").build();
The clients can subscribe to messages sent from the server. Something like that:
connection.on("ReceiveMessage", function (message) {
alert(message);
});
Create or modify an overloaded constructor of the class in which the database update function is implemented, in order to access the hub:
...
public class DatabaseClass
{
private IHubContext<UpdatesHub> _hub;
public DatabaseClass(IHubContext<UpdatesHub> hub)
{
_hub = hub;
}
...
Now you can use the hub to send a message to the clients, after updating your database:
...
public class DatabaseClass
{
private IHubContext<UpdatesHub> _hub;
public DatabaseClass(IHubContext<UpdatesHub> hub)
{
_hub = hub;
}
public MyUpdateFunction()
{
//Update database
//
//
_hub.Clients.All.SendAsync("ReceiveMessage", "Database updated!!");
}
...

.Using a Factory Pattern with NET Core 3.1 Dependency Injection

I have a .NET Core 3.1 API that may either be using RabbitMq or Azure Service Bus. The choice will be determined via a configuration parameter. Since the configuration to use is a runtime decision, I wish to use a factory pattern along with .NET Core's dependency injection. I found an article at https://medium.com/#mailbox.viksharma/factory-pattern-using-built-in-dependency-injection-of-asp-net-core-f91bd3b58665, but cannot get the factory to work. Any help will be greatly appreciated.
The issue is occurring within the Factory class due to IServiceProvider. I am receiving the error System.NullReferenceException: Object reference not set to an instance of an object. from the attempt to GetService.
Factory class
public class MessageServiceFactory
{
readonly IServiceProvider serviceProvider;
public MessageServiceFactory(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
}
public IMessagingService GetMessagingService()
{
var messageProvider = ConfigProvider.GetConfig("MessageService", "Messaging_Service");
switch(messageProvider)
{
case "AzureServiceBus": return (IMessagingService)serviceProvider.GetService(typeof(MassTransitAzureServiceBusMessagingService));
case "RabbitMq": return (IMessagingService)serviceProvider.GetService(typeof(MassTransitRabbitMqMessagingService));
default: throw new ArgumentException("Invalid message service");
};
}
}
Service Interface
public interface IMessagingService
{
Task Publish(object payload);
}
RabbitMq Concrete Implementation
public class MassTransitRabbitMqMessagingService : IMessagingService
{
readonly IMassTransitRabbitMqTransport massTransitTransport;
public MassTransitRabbitMqMessagingService(IMassTransitRabbitMqTransport massTransitTransport)
{
//transport bus config already happens in massTransitTransport constructor
this.massTransitTransport = massTransitTransport;
}
public async Task Publish(object payload)
{
....
}
}
ConfigureServices in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(Configuration);
services.AddScoped<IMassTransitRabbitMqTransport, MassTransitRabbitMqTransport>();
services.AddScoped<IMassTransitAzureServiceBusTransport, MassTransitAzureServiceBusTransport>();
services.AddScoped<MessageServiceFactory>();
services.AddScoped<IMessagingService, MassTransitAzureServiceBusMessagingService>(s => s.GetService<MassTransitAzureServiceBusMessagingService>());
services.AddScoped<IMessagingService, MassTransitRabbitMqMessagingService>(s => s.GetService<MassTransitRabbitMqMessagingService>());
services.AddControllers();
}
Controller
[ApiController]
[Route("api/[controller]")]
public class ListenerController : ControllerBase
{
readonly ILogger<ListenerController> logger;
readonly MessageServiceFactory messageFactory;
public ListenerController(
ILogger<ListenerController> logger,
MessageServiceFactory messageFactory)
{
this.logger = logger;
this.messageFactory = messageFactory;
}
[HttpPost]
public async Task<IActionResult> Post()
{
var payload = new
{
...
};
await messageFactory.GetMessagingService().Publish(payload);
return Ok(
new GDMSResponse()
{
ProcessedDate = DateTime.Now,
SuccessFlag = true
}
);
}
}
This requires restart after changing the configuration, but I see no problem in doing it like this.
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(Configuration);
services.AddScoped<IMassTransitRabbitMqTransport, MassTransitRabbitMqTransport>();
services.AddScoped<IMassTransitAzureServiceBusTransport, MassTransitAzureServiceBusTransport>();
var messageProvider = Configuration.GetConfig("MessageService", "Messaging_Service");
switch(messageProvider)
{
case "AzureServiceBus":
services.AddScoped<IMessagingService, MassTransitAzureServiceBusMessagingService>();
break;
case "RabbitMq":
services.AddScoped<IMessagingService, MassTransitRabbitMqMessagingService>();
break;
default:
throw new ArgumentException("Invalid message service");
};
services.AddControllers();
}
Other note
I noticed that you supplied both the concrete type and a factory:
services.AddScoped<IMessagingService, MassTransitAzureServiceBusMessagingService>(s => s.GetService<MassTransitAzureServiceBusMessagingService>());
I think it should be:
services.AddScoped<IMessagingService>(s => s.GetService<MassTransitAzureServiceBusMessagingService>());
Not sure it it makes a difference.
UPDATE Jan 2021
Recently I had to do this myself and came up with this solution:
public static IServiceCollection ConfigureEventBus(this IServiceCollection services, IConfiguration configuration)
{
var queueSettings = new QueueSettings();
configuration.GetSection("QueueSettings").Bind(queueSettings);
if (configuration.GetValue<bool>("AzureServiceBusEnabled"))
{
services.AddMassTransit(x =>
{
x.UsingAzureServiceBus((context, cfg) =>
{
cfg.Host(queueSettings.HostName);
});
});
}
else
{
services.AddMassTransit(x =>
{
x.UsingRabbitMq((context, cfg) =>
{
cfg.ConfigureEndpoints(context);
cfg.Host(queueSettings.HostName, queueSettings.VirtualHost, h =>
{
h.Username(queueSettings.UserName);
h.Password(queueSettings.Password);
});
});
});
}
services.AddMassTransitHostedService();
services.AddSingleton<IEventBus, MassTransitEventBus>();
return services;
}

How save globally catched exceptions in database

I am building a web application where I will have a lot of controllers with their corresponding action methods in them.
I want to save every exception in database and for this reason I have created
ExceptionService (DbContext is injected in it).
let's say that this is the general form of my controllers:
[Route("api/[controller]")]
[ApiController]
public class UserController : ControllerBase
{
private readonly UserManager userManager;
private readonly IExceptionService exceptionService;
public UserController(UserManager userManager, IExceptionService exceptionService)
{
this.userManager = userManager;
this.exceptionService = exceptionService;
}
[HttpPost]
public async Task<IActionResult> Post([FromBody] User user)
{
try
{
//some code
}
catch (Exception e)
{
exceptionService.Save(e);
//some code
}
}
}
In order to avoid so many try-catch blocks I decided to create a filter which looks like this:
public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
{
private readonly IExceptionService exceptionService;
public ApiExceptionFilterAttribute(IExceptionService exceptionService)
{
this.exceptionService = exceptionService;
}
public override void OnException(ExceptionContext context)
{
Exception e = context.Exception;
exceptionService.Save(e);
//some code
}
}
Code in ConfigureServices method in StartUp.cs looks like this (some code removed for simplicity):
services
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddJsonOptions(options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
services
.AddDbContext<AppDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("Default")));
services.AddScoped<UserManager>();
services.AddScoped<SignInManager>();
services.AddScoped<IExceptionService, ExceptionService>();
services.AddSingleton<IConfigureOptions<MvcOptions>, ConfigureMvcOptions>();
ConfgureMvcOptions class looks like this:
public class ConfigureMvcOptions : IConfigureOptions<MvcOptions>
{
private readonly IExceptionService exceptionService;
public ConfigureMvcOptions(IExceptionService exceptionService)
{
this.exceptionService = exceptionService;
}
public void Configure(MvcOptions options)
{
options.Filters.Add(new ApiExceptionFilterAttribute(exceptionService));
}
}
When I run this application, I get the following error:
System.InvalidOperationException: 'Cannot consume scoped service 'SmartWay.Services.IExceptionService' from singleton 'Microsoft.Extensions.Options.IConfigureOptions`1[Microsoft.AspNetCore.Mvc.MvcOptions]'.'
If I change IExceptionServcise's lifetime to transient than I have to do so for
Dbcontext, then for DbContextOptions... It seems that it isn't right way..
So, How can I solve this problem?
For resolving scoped service from singleton service, try _serviceProvider.CreateScope.
Follow steps below:
ExceptionService
public interface IExceptionService
{
void Save(Exception ex);
}
public class ExceptionService : IExceptionService
{
private readonly IServiceProvider _serviceProvider;
public ExceptionService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void Save(Exception ex)
{
using (var scope = _serviceProvider.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<MVCProContext>();
_context.Add(new Book() { Title = ex.Message });
_context.SaveChanges();
}
}
}
Startup.cs
services.AddSingleton<IExceptionService, ExceptionService>();
services.AddSingleton<IConfigureOptions<MvcOptions>, ConfigureMvcOptions>();

Categories