I get an error of:
System.InvalidOperationException: 'Unable to resolve service for type 'Microsoft.AspNetCore.SignalR.IHubContext`1[Common.RfidHub]' while attempting to activate 'Rfid.RfidEngine'.'
on the part of var rfidEngine = serviceProvider.GetService<IRfidEngine>();
I have this code:
Start
public void ConfigureServices(IServiceCollection services)
{
services.AddSignalR();
var collection = new ServiceCollection();
collection.AddScoped<IApplicationLogger, ApplicationLogger>();
collection.AddScoped<IRfidEngine, RfidEngine>();
IServiceProvider serviceProvider = collection.BuildServiceProvider();
var applicationLogger = serviceProvider.GetService<IApplicationLogger>();
applicationLogger.LoggingSettings = applicationSetting.LoggingSetting;
var signalR = serviceProvider.GetService<IHubContext<RfidHub>>();
var applicationLogger = serviceProvider.GetService<IApplicationLogger>();
applicationLogger.LoggingSettings = applicationSetting.LoggingSetting;
var rfidEngine = serviceProvider.GetService<IRfidEngine>();
rfidEngine.Start();
applicationLogger.LogInformation($#"Program has started", LoggerType.Console);
}
public void Configure(IApplicationBuilder app)
{
app.UseCors("ApplicationCors");
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<RfidHub>("/rfidhub");
});
}
Hub
public class RfidHub : Hub
{
public async Task SendReadings(string user, string message)
{
await Clients.All.SendAsync("ReceiveReading", user, message);
}
}
RfidEngine
public class RfidEngine : IDisposable, IRfidEngine
{
public RfidEngine(IApplicationLogger applicationLogger, IHubContext<RfidHub> hubContext)
{
this.applicationLogger = applicationLogger;
this.hubContext = hubContext;
}
private void SendViaHub(string message)
{
hubContext.Clients.All.SendAsync("ReceiveReadings", message);
}
}
This is my version of Signalr:
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
I need to be able to call the HubContext on a different class. What can I try to resolve this?
Related
I have a Web API configured to send a POST to the SQL server.
I also have a server app (SignalR) that sends a list of strings to this POST API.
The problem: The Post only receives one item per time, so I send a request multiple times inside a loop, and with each iteration, a new item is sent.
It works but I believe there's an optimized way to do this, and if something goes wrong inside an iteration, the correct thing to do was canceling the transaction, but with this loop method, it is not possible.
I'm accepting tips on how to handle this better.
WebApi:
VisitaItemControl.cs
public class VisitaItemControl
{
public string ItemID { get; set; }
public string VisitaID { get; set; }
}
VisitaItemControlController.cs
[Route("api/[controller]")]
[ApiController]
public class VisitaItemControlController : ControllerBase
{
private readonly IConfiguration _configuration;
public VisitaItemControlController(IConfiguration configuration)
{
_configuration = configuration;
}
[HttpPost]
public JsonResult Post(VisitaItemControl visitaItemControl)
{
string query = #"INSERT INTO VisitaItemControl (
ItemID,
VisitaID)
VALUES (
#ItemID,
#VisitaID
)";
DataTable dt = new DataTable();
string sqlDataSource = _configuration.GetConnectionString("connectionstring");
SqlDataReader sqlDataReader;
using (SqlConnection sqlConnection = new SqlConnection(sqlDataSource))
{
sqlConnection.Open();
using (SqlCommand cmd = new SqlCommand(query, sqlConnection))
{
cmd.Parameters.AddWithValue(#"ItemID", visitaItemControl.ItemID);
cmd.Parameters.AddWithValue(#"VisitaID", visitaItemControl.VisitaID);
sqlDataReader = cmd.ExecuteReader();
dt.Load(sqlDataReader);
sqlDataReader.Close();
sqlConnection.Close();
}
}
return new JsonResult("Saved!");
}
}
SignalR app:
foreach (var item in addedItems)
{
var postObject = new VisitaItemControl()
{
ItemID = item.ItemID,
VisitaID = empObj.VisitaID,
};
var request2 = new HttpRequestMessage(HttpMethod.Post, config["API_POST"]);
request2.Content = new StringContent(JsonSerializer.Serialize(postObject), null, "application/json");
var client2 = ClientFactory.CreateClient();
var response2 = await client.SendAsync(request2);
using var responseStream2 = await response2.Content.ReadAsStreamAsync();
string res2 = await JsonSerializer.DeserializeAsync<string>(responseStream2);
}
await JS.InvokeVoidAsync("alert", "Saved!");
await refreshList();
uriHelper.NavigateTo("/", forceLoad: true);
}
Here's the basics of a structured approach to what you're trying to do.
I've used Entity Framework to manage the database and the InMemory Implemnentation for demo purposes. I've implemented everything in a Blazor Server project so we can test and manage the data in the UI and use Postman for interacting with the API.
Project Packages:
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.4" />
<PackageReference Include="System.Net.Http.Json" Version="6.0.0" />
</ItemGroup>
My InMemory Db Context:
public class InMemoryDbContext : DbContext
{
public DbSet<VisitaItemControl>? VisitaItemControl { get; set; }
public InMemoryDbContext(DbContextOptions<InMemoryDbContext> options) : base(options) { }
}
We'll use a DBContextFactory service to manage the DB connections that will use this as it's source DbContext.
My Data Broker Interface - this will normally implement all the CRUD processes. We use an interface to decouple the application from the data store.
public interface IDataBroker
{
public ValueTask<bool> AddItems<TRecord>(IEnumerable<TRecord> items) where TRecord : class;
public ValueTask<IEnumerable<TRecord>> GetItems<TRecord>(int count) where TRecord : class;
}
And my Server implementation - note I inject the DbContextFactory to manage my database connections.
public class ServerDataBroker : IDataBroker
{
private readonly IDbContextFactory<InMemoryDbContext> database;
public ServerDataBroker(IDbContextFactory<InMemoryDbContext> db)
=> this.database = db;
public async ValueTask<bool> AddItems<TRecord>(IEnumerable<TRecord> items) where TRecord : class
{
var result = false;
using var dbContext = database.CreateDbContext();
foreach (var item in items)
dbContext.Add(item);
var rowsAdded = await dbContext.SaveChangesAsync();
if (rowsAdded == items.Count())
result = true;
// Do something if not all rows are added
return result;
}
public async ValueTask<IEnumerable<TRecord>> GetItems<TRecord>(int count) where TRecord : class
{
using var dbContext = database.CreateDbContext();
return await dbContext.Set<TRecord>()
.Take(count)
.ToListAsync();
}
}
For the UI I've built a very simple View Service to hold and manage the data:
public class VisitaItemControlService
{
private IDataBroker _broker;
public event EventHandler? ListUpdated;
public IEnumerable<VisitaItemControl> Records { get; protected set; } = new List<VisitaItemControl>();
public VisitaItemControlService(IDataBroker dataBroker)
=> _broker = dataBroker;
public async ValueTask<bool> AddItems(IEnumerable<VisitaItemControl> items)
{
var result = await _broker.AddItems<VisitaItemControl>(items);
if (result)
{
await this.GetItems(1000);
this.ListUpdated?.Invoke(this, EventArgs.Empty);
}
return result;
}
public async ValueTask GetItems(int count)
=> this.Records = await _broker.GetItems<VisitaItemControl>(count);
}
And here's my Index page to test the system.
#page "/"
#inject VisitaItemControlService service;
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
<div>
<button class="btn btn-primary" #onclick=AddItems>Add Some Items</button>
</div>
#if (loaded)
{
#foreach (var item in this.service.Records)
{
<div class="p-2">
<span>
Item : #item.ItemID
</span>
<span>
Visita : #item.VisitaID
</span>
</div>
}
}
#code {
private bool loaded = false;
protected async override Task OnInitializedAsync()
{
await this.service.GetItems(1000);
this.service.ListUpdated += this.OnListUpdated;
this.loaded = true;
}
private async Task AddItems()
{
var addList = new List<VisitaItemControl> {
new VisitaItemControl { ItemID = Guid.NewGuid().ToString(), VisitaID = "AA" },
new VisitaItemControl { ItemID = Guid.NewGuid().ToString(), VisitaID = "BB" },
new VisitaItemControl { ItemID = Guid.NewGuid().ToString(), VisitaID = "CC" }
};
await this.service.AddItems(addList);
}
private void OnListUpdated(object? sender, EventArgs e)
=> this.InvokeAsync(StateHasChanged);
}
Note the use of events to notify the UI that the list has changed and trigger a re-render.
Here's my API controller:
[ApiController]
public class VisitaItemControlController : ControllerBase
{
private IDataBroker _dataBroker;
public VisitaItemControlController(IDataBroker dataBroker)
=> _dataBroker = dataBroker;
[Route("/api/[controller]/list")]
[HttpGet]
public async Task<IActionResult> GetRecordsAsync()
{
var list = await _dataBroker.GetItems<VisitaItemControl>(1000);
return Ok(list);
}
[Route("/api/[controller]/addlist")]
[HttpPost]
public async Task<bool> AddRecordsAsync([FromBody] IEnumerable<VisitaItemControl> records)
=> await _dataBroker.AddItems(records);
}
And finally Program to configure all the services and middleware.
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddDbContextFactory<InMemoryDbContext>(options => options.UseInMemoryDatabase("TestDb"));
builder.Services.AddSingleton<IDataBroker, ServerDataBroker>();
builder.Services.AddScoped<VisitaItemControlService>();
builder.Services.AddSingleton<WeatherForecastService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
Some postman screen captures:
And the project structure:
I try everything I can think of but I must miss something.
I have a hostedService project in netcoreapp3.1
I referenced the following
<PackageReference Include="Microsoft.ApplicationInsights.NLogTarget" Version="2.20.0" />
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.20.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="3.1.10" />
<PackageReference Include="NLog" Version="4.7.15" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
my appsetting is as follow
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Warning"
},
"ApplicationInsights": {
"LogLevel": {
"Default": "Warning",
"WorkerServiceTestLog": "Debug"
}
}
},
"ApplicationInsights": {
"InstrumentationKey": "XXX"
}
}
The nlog config look like that
<extensions>
<add assembly="Microsoft.ApplicationInsights.NLogTarget" />
</extensions>
<targets>
<target type="ApplicationInsightsTarget" name="aiTarget" >
</target>
<target type="Console" name="consolelog"/>
<target type="Debugger" name="debuglog" />
</targets>
<rules>
<logger name="*" minlevel="Debug" writeTo="debuglog" />
<logger name="*" minlevel="Debug" writeTo="consolelog" />
<logger name="*" minlevel="Debug" writeTo="aiTarget" />
</rules>
I build the host like that
var config = new ConfigurationBuilder()
.SetBasePath(System.IO.Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.Build();
return Host.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddNLog(config);
})
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
services.AddApplicationInsightsTelemetryWorkerService();
});
The worker simply test the logs
public class Worker : BackgroundService
{
private readonly NLog.ILogger _nlogger = LogManager.GetCurrentClassLogger();
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
for (var i = 0; i < 10; i++)
{
_logger.LogError("Microsoft Logging:Error");
_logger.LogInformation("Microsoft Logging:Information");
_nlogger.Error("NLOG:Error");
_nlogger.Info("NLOG:Information");
await Task.Delay(2000, stoppingToken);
}
}
}
Now I have all log information in the console and debugger as excepted
But only the log from Microsoft extension is going in application insight
What am I missing there?
Follow the workaround to fix the issue:
Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddApplicationInsightsTelemetry(Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
{
logger.LogInformation(
"Configuring for {Environment} environment",
env.EnvironmentName);
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
}
Program.cs
public class Program
{
public static void Main(string[] args)
{
// Setting up the Nlog
var logger = LogManager.Setup()
.LoadConfigurationFromAppSettings()
.GetCurrentClassLogger();
try
{
logger.Debug("Nlog Log: init main");
CreateHostBuilder(args).Build().Run();
}
catch (Exception exception)
{
//NLog: catch setup errors
logger.Error(exception, " Nlog Log:Stopped program because of exception");
throw;
}
finally
{
// Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
NLog.LogManager.Shutdown();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
//Configuring the Nlog
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
})
.UseNLog()
//Calling the Host Services (Worker)
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
});
}
Here is my worker.cs
private readonly NLog.ILogger _nlogger = LogManager.Setup()
.LoadConfigurationFromAppSettings()
.GetCurrentClassLogger();
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
for (var i = 0; i < 10; i++)
{
_logger.LogError("Microsoft Logging:Error Iteration:" + i);
_logger.LogInformation("Microsoft Logging:Information Iteration:" + i);
_nlogger.Error("NLOG:Error Iteration:"+i);
_nlogger.Info("NLOG:Information Iteration:" + i);
await Task.Delay(2000, stoppingToken);
}
}
Result in Application Insights
Updated Answer:
Here you can see the Application Insights connection string get from secrect.json
I've set up signalr in my blazor server side application and for some reason this hubconnection is not being triggered, when the hubconnection is on, it completely ignores the BroadcastData method and doesnt even fire it:
private HubConnection hubConnection;
private string _hubUrl;
protected override async Task OnInitializedAsync()
{
string baseUrl = NavigationManager.BaseUri;
_hubUrl = baseUrl.TrimEnd('/') + SignalRHub.HubUrl;
_hubConnection = new HubConnectionBuilder()
.WithUrl(_hubUrl)
.Build();
hubConnection.On<ClientDTO>("BroadcastData", BroadcastData);
await hubConnection.StartAsync();
}
private void BroadcastData(ClientDTO payload)
{
dashboardData = payload;
StateHasChanged();
}
I have everything setup for this to be "working" but clearly it isn't working and I'm completely lost at what could be the problem... Please take a look at what I have so far and see if you can see what's going on:
Startup:
public Startup(IConfiguration configuration)
{
Configuration = configuration;
StartTimer();
}
private void StartTimer()
{
_timer = new System.Timers.Timer();
_timer.Interval = 5000;
_timer.Elapsed += TimerElapsed;
_timer.Start();
}
private void TimerElapsed(Object source, ElapsedEventArgs e)
{
Trigger();
}
public void Trigger()
{
try
{
using (HttpClient client = new HttpClient())
{
//Trigger on elapsed
var response = client.GetAsync(Configuration.GetConnectionString("ApiTriggerURL")).Result;
}
}
catch
{
Console.WriteLine("something terrible has happened...");
}
}
services.AddScoped(typeof(SignalRHub));
services.AddScoped<IHub, SignalRHub>();
services.AddScoped<HttpClient>();
services.AddSignalR();
services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
public void Configure(IApplicationBuilde app, IWebHostEnvironment env)
{
app.UseResponseCompression();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
endpoints.MapHub<SignalRHub>(SignalRHub.HubUrl);
});
}
appsettings.json: (fyi, the trigger is working, the api endpoint is being hit as it returns a status 200 ok result)
"ConnectionStrings":
{
"ApiTriggerURL": "http://localhost:5000/api/SignalRHub/GetMyData"
}
Then we have my api controller: (here you can see the status 200 ok)
private readonly SignalRHub _signalRHub;
public SignalRHubController(SignalRHub signalRHub)
{
_signalRHub = signalRHub;
}
[HttpGet]
public ObjectResult GetMyData()
{
try
{
Task.WhenAll(_signalRHub.BroadcastData()); // Call hub broadcast method
return this.StatusCode((int)HttpStatusCode.OK, "trigger has been triggered");
}
catch
{
return this.StatusCode((int)HttpStatusCode.InternalServerError, "christ, the ting is broken fam");
}
}
When we look into the _signalRHub.BroadcastData(), we see this:
public class SignalRHub : Hub, IHub
{
private readonly ClientService _clientService;
readonly IHubContext<SignalRHub> _hubContext;
public const string HubUrl = "/chathub"; //this is being read in the startup in the endpoints
public SignalRHub(ClientService clientService, IHubContext<SignalRHub> hubContext)
{
_clientService = clientService;
_hubContext = hubContext;
}
public async Task BroadcastData()
{
var data = _clientService .GetDataAsync().Result;
await _hubContext.Clients.All.SendAsync("BroadcastData", data); //send data to all clients
}
}
And this in turn should basically do this signalrhub every x seconds (depending on timer)
I know my code is a whole load of madness, but please look pass this and help me to understand why this isn't working! Thank you in advance!
Try following:
hubConnection.On<ClientDTO>("BroadcastData", (payload)=>
BroadcastData(payload);
);
Instead of
hubConnection.On<ClientDTO>("BroadcastData", BroadcastData);
I have .net core 3.1 console application and I want to run it as a windows service, my program.cs looks like
public class Program
{
public static async Task Main(string[] args)
{
var isService = !(Debugger.IsAttached || args.Contains("--console"));
var builder = CreateHostBuilder(args);
if (isService)
{
await builder.RunAsServiceAsync();
}
else
{
await builder.RunConsoleAsync();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseWindowsService()
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker1>();
services.AddHostedService<Worker2>();
});
}
and the .csproj is
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<UserSecretsId>dotnet-MyWorkerService-16487890-DF99-45C2-8DC4-5475A21D6B75</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="3.1.16" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="3.1.16" />
</ItemGroup>
</Project>
but for RunAsServiceAsync() error is coming like "IHostBuilder does not contain definition for RunAsServiceAsync"
Can anyone please point to me where / what I am missing?
RunAsServiceAsync appears to be 3rd party extension on IHostBuilder.
It does not appear to be a built in function, native to .NET Core.
I found an old implementation on GitHub here that you could probably implement yourself
public static class ServiceBaseLifetimeHostExtensions
{
public static IHostBuilder UseServiceBaseLifetime(this IHostBuilder hostBuilder)
{
return hostBuilder.ConfigureServices((hostContext, services) => services.AddSingleton<IHostLifetime, ServiceBaseLifetime>());
}
public static Task RunAsServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)
{
return hostBuilder.UseServiceBaseLifetime().Build().RunAsync(cancellationToken);
}
}
public class ServiceBaseLifetime : ServiceBase, IHostLifetime
{
private TaskCompletionSource<object> _delayStart = new TaskCompletionSource<object>();
public ServiceBaseLifetime(IApplicationLifetime applicationLifetime)
{
ApplicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime));
}
private IApplicationLifetime ApplicationLifetime { get; }
public Task WaitForStartAsync(CancellationToken cancellationToken)
{
cancellationToken.Register(() => _delayStart.TrySetCanceled());
ApplicationLifetime.ApplicationStopping.Register(Stop);
new Thread(Run).Start(); // Otherwise this would block and prevent IHost.StartAsync from finishing.
return _delayStart.Task;
}
private void Run()
{
try
{
Run(this); // This blocks until the service is stopped.
_delayStart.TrySetException(new InvalidOperationException("Stopped without starting"));
}
catch (Exception ex)
{
_delayStart.TrySetException(ex);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
Stop();
return Task.CompletedTask;
}
// Called by base.Run when the service is ready to start.
protected override void OnStart(string[] args)
{
_delayStart.TrySetResult(null);
base.OnStart(args);
}
// Called by base.Stop. This may be called multiple times by service Stop, ApplicationStopping, and StopAsync.
// That's OK because StopApplication uses a CancellationTokenSource and prevents any recursion.
protected override void OnStop()
{
ApplicationLifetime.StopApplication();
base.OnStop();
}
}
But it appears that this service based functionality is now built in when UseWindowsService is called on the builder.
So in that case you would need to refactor your code accordingly to get the desired behavior
public class Program {
public static async Task Main(string[] args) {
var isService = !(Debugger.IsAttached || args.Contains("--console"));
var builder = CreateHostBuilder(args);
if (isService) {
await builder.RunAsServiceAsync();
} else {
await builder.RunConsoleAsync();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker1>();
services.AddHostedService<Worker2>();
});
}
public static class ServiceBaseLifetimeHostExtensions {
public static Task RunAsServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default) {
return hostBuilder.UseWindowsService().Build().RunAsync(cancellationToken);
}
}
I have some classes that receive DbContexts thru dependency injection, that i would like to test. I am using AutoMapper's ProjectTo, as my entities are often much bigger than the objects (dto) I am returning from my class. I really like having AutoMapper adjust my query so that it only selects fields which are in my DTOs.
I've been trying to mock my DbContext, using Moq.EntityFrameworkCore. It works relatively well but it does cause issues with AutoMapper ProjectTo(). I end up getting InvalidCastException.
Obviously, I am not interested in "testing AutoMapper" or my DbContext, I just want to test my code which is around. However, I can't test my code since it crashes on the Projections.
Here's a minimalist repro, using AutoFixture to shorten the code a bit, I've thrown everything into a single file so that it's easy for anyone to try out for themselves:
using AutoFixture;
using AutoFixture.AutoMoq;
using AutoMapper;
using Microsoft.EntityFrameworkCore;
using Moq;
using Moq.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;
namespace UnitTestEFMoqProjectTo
{
public class MyBusinessFixture
{
private IFixture _fixture;
public MyBusinessFixture()
{
_fixture = new Fixture()
.Customize(new AutoMoqCustomization());
var mockMapper = new MapperConfiguration(cfg =>
{
cfg.AddProfile(new MappingProfile());
});
var mapper = mockMapper.CreateMapper();
_fixture.Register(() => mapper);
}
[Fact]
public async Task DoSomething_WithMocksAndProjectTo_ValidatesMyLogic()
{
// Arrange
var mockContext = new Mock<MyDbContext>();
mockContext.Setup(x => x.MyEntities).ReturnsDbSet(new List<MyEntity>(_fixture.CreateMany<MyEntity>(10)));
_fixture.Register(() => mockContext.Object);
var business = _fixture.Create<MyBusiness>();
// Act
await business.DoSomething();
// Assert
Assert.True(true);
}
}
public class MyDbContext : DbContext
{
public virtual DbSet<MyEntity> MyEntities { get; set; }
}
public class MyEntity
{
public int Id { get; set; }
public string Name { get; set; }
public string SomeProperty { get; set; }
public string SomeOtherProperty { get; set; }
}
public class MyDto
{
public int Id { get; set; }
public string Name { get; set; }
}
public interface IMyBusiness
{
Task DoSomething();
}
public class MyBusiness : IMyBusiness
{
private readonly MyDbContext _myDbContext;
private readonly IMapper _mapper;
public MyBusiness(MyDbContext myDbContext, IMapper mapper)
{
_myDbContext = myDbContext;
_mapper = mapper;
}
public async Task DoSomething()
{
// My program's logic here, that I want to test.
// Query projections and enumeration
var projectedEntities = await _mapper.ProjectTo<MyDto>(_myDbContext.MyEntities).ToListAsync();
// Some more of my program's logic here, that I want to test.
}
}
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<MyEntity, MyDto>();
}
}
}
Should output the following error :
Message:
System.InvalidCastException : Unable to cast object of type 'Moq.EntityFrameworkCore.DbAsyncQueryProvider.InMemoryAsyncEnumerable`1[UnitTestEFMoqProjectTo.MyEntity]' to type 'System.Linq.IQueryable`1[UnitTestEFMoqProjectTo.MyDto]'.
Stack Trace:
ProjectionExpression.ToCore[TResult](Object parameters, IEnumerable`1 memberPathsToExpand)
ProjectionExpression.To[TResult](Object parameters, Expression`1[] membersToExpand)
Extensions.ProjectTo[TDestination](IQueryable source, IConfigurationProvider configuration, Object parameters, Expression`1[] membersToExpand)
Mapper.ProjectTo[TDestination](IQueryable source, Object parameters, Expression`1[] membersToExpand)
MyBusiness.DoSomething() line 79
MyBusinessFixture.DoSomething_WithMocksAndProjectTo_ShouldMap() line 39
Any idea of how I could keep doing projections with AutoMapper but also have unit tests working ?
For reference, here is my project file content :
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoFixture.AutoMoq" Version="4.11.0" />
<PackageReference Include="AutoMapper" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.5" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="Moq.EntityFrameworkCore" Version="3.1.2.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
I bumped into the similar issue the other day but finally I was able to run my unit tests.
I use MockQueryable.Moq (https://github.com/romantitov/MockQueryable) for mocking DBSet, probably, you should try this package because an issue might be here.
So my code looks like it:
public class Project
{
public int Id { get; set; }
public string Name { get; set; }
}
public class ProjectDto
{
public int Id { get; set; }
public string Name { get; set; }
}
class GetProjectsHandler : IRequestHandler<GetProjectsRequest, GetProjectsResponse>
{
private readonly IMyDbContext context;
private readonly IMapper mapper;
public GetProjectsHandler(IMyDbContext context,IMapper mapper)
{
this.context = context;
this.mapper = mapper;
}
public async Task<GetProjectsResponse> Handle(GetProjectsRequest request, CancellationToken cancellationToken)
{
IReadOnlyList<ProjectDto> projects = await context
.Projects
.ProjectTo<ProjectDto>(mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);
return new GetProjectsResponse
{
Projects = projects
};
}
}
Simplified unit test looks like the following:
public class GetProjectsTests
{
[Fact]
public async Task GetProjectsTest()
{
var projects = new List<Project>
{
new Project
{
Id = 1,
Name = "Test"
}
};
var context = new Mock<IMyDbContext>();
context.Setup(c => c.Projects).Returns(projects.AsQueryable().BuildMockDbSet().Object);
var mapper = new Mock<IMapper>();
mapper.Setup(x => x.ConfigurationProvider)
.Returns(
() => new MapperConfiguration(
cfg => { cfg.CreateMap<Project, ProjectDto>(); }));
var getProjectsRequest = new GetProjectsRequest();
var handler = new GetProjectsHandler(context.Object, mapper.Object);
GetProjectsResponse response = await handler.Handle(getProjectsRequest, CancellationToken.None);
Assert.True(response.Projects.Count == 1);
}
}
I agree that it's a debatable question how should we mock DBSet in unit tests. I think that InMemoryDatabase should be used in integration tests rather than unit tests.