How save globally catched exceptions in database - c#

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>();

Related

Implementing an Interface on a class with Dependency Injection

I'm developing a Blazor app and somewhere I'm implementing an interface:
public class UserData : IUserData
{
private readonly ISqlDataAccess _db;
public UserData(ISqlDataAccess db)
{
_db = db;
}
public void SomeFunction ()
{
...
}
}
public interface IUserData
{
void SomeFunction();
}
While on .razor I can do: #inject IUserData UserData; and ((UserData)UserData).SomeFunction();; I'm failing to discover how to do it on .cs file.
//There is no such constructor and If I create a new one, then I won't get the _db dependecy injection
IUserData userDate = new UserData();
userDate.SomeFunction();
Edit
So now, when I'm calling the method from the .cs file, the app freezes; it doesn't throw an error and I am able to refresh the page, so it seems it's stuck on the call to the db; but if I call it from the .razor it works flawlessy.
.cs
public AccountService(IUserData userData)
{
_userData = userData;
}
...
public async Task<bool> Validate(string userId, string password)
{
...
try
{
List<UserModel> users = new List<UserModel<();
users = await _userData.GetUsers();
//NEVER GETS HERE
return true;
}
catch (Exception ex)
{
return false;
}
...
}
.razor
#inject IUserData _db;
#code {
private List<UserModel> users;
...
protected override async Task OnInitializedAsync()
{
users = await _db.GetUsers();
}
...
UserData
public class UserData : IUserData
{
private readonly ISqlDataAccess _db;
public UserData(ISqlDataAccess db)
{
_db = db;
}
public Task<List<UserModel>> GetUsers()
{
string sql = "Select *from dbo.Users";
return _db.LoadData<UserModel, dynamic>(sql, new { });
}
...
}
IUserData
public interface IUserData
{
Task<List<UserModel>> GetUsers();
...
}
Edit2
It turns out I was missing an await when calling Validate() service, and thus not running it asynchronous.
At some point in the program, you need to setup dependency injection. This is most common to do in the ConfigureServices method in Startup.cs by convention.
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonConfigurationProvider("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient <ISqlDataAccess, SqlDataAccess>(); //Second argument is the implementation of the interface
services.AddTransient <IUserData, UserData>();
}
}
You need to pass the ISqlDataAccess to the constructor of the UserData but you had it covered already.
public class UserData : IUserData
{
private readonly ISqlDataAccess _db;
public UserData(ISqlDataAccess db)
{
_db = db;
}
//...
}
Then you need to pass your IUserData to your objects via constructors:
public class ClassWithIUserDataDependency {
private IUserData _userData;
public ClassWithIUserDataDependency (IUserData userData) {
_userData = userData;
}
//rest of the class
}
One note: You would need to pass IUserData to all dependency classes. Based on the name, this looks like a POCO object (If it is not, don't mind this comment) If this is a POCO class, or anything representing a DTO or Data, then it is better to separate db from it and allow users to just new it. If it is not, you may want to change its name.
In Startup.cs you can register your interface and implementation;
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IUserData, Userdata>();
}
Then you can use the interface in a class:
public class TestClass{
private IUserData _userData;
public TestClass(IUserData userdata){
_userData = userdata;
}
}

How to configure MassTransit with Mediator to publish messages?

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.

Calling nested dependency in .net core

I have two class which using nested dependency. My class are ExceptionHandler and JwtHelper. Startup.cs call ExceptionHandler. Then ExceptionHandler call JwtHelper. But while I getting exceptionHandler in startup.cs, it gave me error
Unable to resolve service for type 'Hsys.WebToken.JwtHelper' while attempting to activate 'Hsys.ExceptionHandling.ExceptionHandler'
I added dependencies to Startup.cs. What can be reason of this error?
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddSingleton<IJwtHelper, JwtHelper>();
services.AddSingleton<IExceptionHandler, ExceptionHandler>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IHttpContextAccessor httpContextAccessor)
{
app.UseExceptionHandler(builder => builder.Run(async context =>
{
var error = context.Features.Get<IExceptionHandlerFeature>();
//This line give me error while Getting Required Service
IExceptionHandler exceptionHandler = app.ApplicationServices.GetRequiredService<IExceptionHandler>();
await exceptionHandler.AddApplicationError(context, error);
await context.Response.WriteAsync(error.Error.Message);
}));
app.UseHttpsRedirection();
app.UseMvc();
}
ExceptionHandler.cs
public class ExceptionHandler : IExceptionHandler
{
private readonly JwtHelper jwtHelper;
public ExceptionHandler(JwtHelper jwtHelper)
{
this.jwtHelper = jwtHelper;
}
public async Task AddApplicationError()
{
var userId = jwtHelper.GetValueFromToken("userId");
}
}
JwtHelper.cs
public class JwtHelper : IJwtHelper
{
private readonly IHttpContextAccessor httpContextAccessor;
public JwtHelper(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public string GetValueFromToken(string propertyName)
{
var handler = new JwtSecurityTokenHandler();
var tokens = handler.ReadToken(httpContextAccessor.HttpContext.Request.Headers["Authorization"]) as JwtSecurityToken;
return tokens.Claims.FirstOrDefault(claim => claim.Type == propertyName).Value;
}
}
You Registered IJwtHelper with the DI container, with a concrete class of JwtHelper. However inside your exception handler you are telling the DI container you want JwtHelper injected. You never registered JwtHelper, but its interface. You want your ExceptionHandler class to look like this instead.
public class ExceptionHandler : IExceptionHandler
{
private readonly IJwtHelper jwtHelper;
public ExceptionHandler(IJwtHelper jwtHelper)
{
this.jwtHelper = jwtHelper;
}
public async Task AddApplicationError()
{
var userId = jwtHelper.GetValueFromToken("userId");
}
}

Dependeny Injection Error Unable to resolve service for type while attempting to activate

I'm getting Dependency Injection Eror when I try POST data via Postman.
This is code in controller and service file.
DashboardController.cs
using AutoMapper;
using System.Threading.Tasks;
namespace Web.API.Controllers
{
[Route("api/dashboards")]
public class DashboardController : Controller
{
private readonly IDashboardService dashboardService;
public DashboardController(IDashboardService dashboardService)
{
this.dashboardService = dashboardService;
}
/// <summary>
/// Describe this method here.
/// </summary>
[HttpPost]
public async Task<IActionResult> CreateDashboard([FromBody]Dashboard dashboardResource)
{
try
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
var createdDashboard = await dashboardService.Create(dashboardResource);
return Ok(createdDashboard);
}
catch (Exception)
{
return BadRequest();
}
}
}
}
DashboardService.cs
using System.Threading.Tasks;
namespace Domain.Services
{
public class DashboardService: IDashboardService
{
private readonly IRepository<Dashboard> dashboardRepository;
public DashboardService(IRepository<Dashboard> dashboardRepository)
{
this.dashboardRepository = dashboardRepository;
}
public async Task<Dashboard> Create(Dashboard dashboard)
{
var createdDashboard = await dashboardRepository.Create(dashboard);
return createdDashboard;
}
}
}
In Startup method ConfigureServices I've addded AddScoped
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAutoMapper();
services.AddScoped<IDashboardService, DashboardService>();
services.AddScoped<IRepository<Dashboard>, DashboardRepository>();
services.AddDbContext<DbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("Default")));
services.AddMvc();
services.AddTransient<DatabaseDeployer>();
}
I'm not sure what I did wrong. I use N-tier arhitecture with repository, automapper etc..
DashboardRepository.cs
using AutoMapper;
namespace Repositories.Repositories
{
public class DashboardRepository : IRepository<domain.Dashboard>
{
private readonly DbContext context;
private readonly IMapper mapper;
public DashboardRepository(DbContext context, IMapper mapper)
{
this.context = context;
this.mapper = mapper;
}
public async Task<domain.Dashboard> Create(domain.Dashboard dashboard)
{
var entityToCreate = mapper.Map<domain.Dashboard, dbModels.Dashboard>(dashboard);
var createdEntity = context.Add(entityToCreate);
await context.SaveChangesAsync();
var createDashboard = mapper.Map<dbModels.Dashboard, domain.Dashboard>(dashboard);
}
Looks like you have referenced incorrect Dashboard in Startup and DashboardRepository class.
DashboardRepository.cs
public class DashboardRepository : IRepository<Domain.Entities.Dashboard.Dashboard>
Startup.cs
services.AddScoped<IRepository<Domain.Entities.Dashboard.Dashboard>, DashboardRepository>();
Note
Look at DashboardRepository class, you have defined
using domain = Domain.Entities;
but are using Dashboard class directly
public class DashboardRepository : IRepository<Dashboard>
instead of
public class DashboardRepository : IRepository<domain.Dashboard>
So, Dashboard refers to Repositories.Models.Dashboard, since you have added following using statement
using Repositories.Models;
In my case, i forgot to inject dependecny in startup.cs
Public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ISyncRequestRepository, SyncRequestManager>();
}

There is no argument given in context

I am trying to implement the repository pattern in asp core. Everything seems to work fine with a few adjustments,except adding it to the controller:
public class HomeController : Controller
{
private IDocumentRepository _context;
public HomeController()
{
_context = new DocumentRepository(new myContext());
}
}
DocumentRepository.cs
public class DocumentRepository : IDocumentRepository, IDisposable
{
private myContext context;
public DocumentRepository(myContext context) : base()
{
this.context = context;
}
public IEnumerable<Document> GetDocuments()
{
return context.Document.ToList();
}
public Document GetDocumentByID(int id)
{
return context.Document.FirstOrDefault(x => x.Id == id);
}
IDocumentRepository.cs
public interface IDocumentRepository : IDisposable
{
IEnumerable<Document> GetDocuments();
Document GetDocumentByID(int documentId);
void InsertDocument(Document student);
void DeleteDocument(int documentID);
void UpdateDocument(Document document);
void Save();
}
The error
There is no argument given that corresponds to the required formal
parameter 'options' of
'myContext.myContext(DbContextOptions)
dotnetcore..NETCoreApp,Version=v1.0
Simply resolve IDocumentRepository from the DI container using constructor injection instead of manually instantiating it and it should work:
public class HomeController : Controller {
private IDocumentRepository _repository;
public HomeController(IDocumentRepository repository) {
_repository = repository;
}
}
For that, you'll need to ensure IDocumentRepository is correctly registered in ConfigureServices:
public void ConfigureServices(IServiceCollection services) {
services.AddScoped<IDocumentRepository, DocumentRepository>();
}

Categories