Better way to send a list of items trough WebApi - signalr

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:

Related

How can I read request header from program.cs file in .netcore6 webapi

I have tenant id in request header. I want to connect database string based on the tenant id , how can i achieve this? I am using .netcore6 webapi. Thanks in advance.
You can achieve this requirement through Middleware. Here is the test result and sample.
My Test files and picutres
Test Method in HomeController
public IActionResult GetConnectionStringByTenantID()
{
bool testResult = false;
string msg = string.Empty;
try
{
testResult = _dbcontext.Database.CanConnect();
if (testResult)
{
msg = "connect sucessfully";
}
else
{
msg = "connect failed";
}
}
catch (Exception e)
{
msg = e.ToString();
throw;
}
return Ok(msg);
}
TenantMiddleware.cs
using WebApplication7.Models;
namespace WebApplication7
{
public class TenantMiddleware
{
private readonly RequestDelegate _next;
public TenantMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// Read the tenant ID from the request header
var tenantId = context.Request.Headers["Tenant-Id"].FirstOrDefault();
// Get the connection string for the tenant
var connectionString = GetConnectionStringForTenant(tenantId);
// Create a Tenant object with the tenant ID and connection string
var tenant = new Tenant
{
Id = tenantId,
ConnectionString = connectionString
};
// Set the Tenant object in the context
context.Items["Tenant"] = tenant;
// Call the next middleware component in the pipeline
await _next(context);
}
private string GetConnectionStringForTenant(string tenantId)
{
// Implement logic to get the connection string for the tenant
// This can be from a configuration file or a database
// For example, you can have a dictionary of tenant IDs and connection strings
var connectionStrings = new Dictionary<string, string>
{
{ "tenant1", "Data Source=...My real test connectionstring..." },
{ "tenant2", "Server=server2;Database=database2;User Id=user2;Password=password2;" }
};
if (tenantId == null || tenantId.Equals(string.Empty))
{
tenantId = "tenant1";
}
// Return the connection string for the tenant ID
if (connectionStrings.TryGetValue(tenantId, out var connectionString))
{
return connectionString;
}
// If the tenant ID is not found, throw an exception
throw new ArgumentException($"Invalid tenant ID: {tenantId}");
}
}
}
MyDbContext.cs
using Microsoft.EntityFrameworkCore;
using WebApplication7.Models;
namespace WebApplication7
{
public class MyDbContext : DbContext
{
private readonly Tenant _tenant = null!;
public MyDbContext()
{
}
public MyDbContext(DbContextOptions<MyDbContext> options, IHttpContextAccessor httpContextAccessor)
: base(options)
{
_tenant = httpContextAccessor.HttpContext.Items["Tenant"] as Tenant;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(_tenant.ConnectionString);
}
}
}
Program.cs
using Microsoft.EntityFrameworkCore;
namespace WebApplication7
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpContextAccessor();
builder.Services.AddDbContext<MyDbContext>();
// Add services to the container.
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/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.UseMiddleware<TenantMiddleware>();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
}
}
}
Tenant.cs
namespace WebApplication7.Models
{
public class Tenant
{
public string? Id { get; set; }
public string? ConnectionString { get; set; }
}
}

Quartz .NET The instance of entity type 'TABLENAME' cannot be tracked because

We have built an API with .NET Core 3.1 that extracts data from an Excel and stores it via
EF Core into a MS SQL database. We use Quartz. NET so that it is handled in a background thread. For DI we use Autofac.
We use Scoped Services to be able to use the DBContext via DI (as described here https://andrewlock.net/creating-a-quartz-net-hosted-service-with-asp-net-core/).
Unfortunately, saving the data still does not work when multiple users are using the application at the same time. We get the following error message:
The instance of entity type 'TABLENAME' cannot be tracked because another instance with the same key value for {'TABLEKEY'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.
Here our related code:
Startup.cs
// Add DbContext
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("Default"), b => b.MigrationsAssembly("XY.Infrastructure")));
// Add Quartz
services.AddQuartz(q =>
{
// as of 3.3.2 this also injects scoped services (like EF DbContext) without problems
q.UseMicrosoftDependencyInjectionJobFactory();
// these are the defaults
q.UseSimpleTypeLoader();
q.UseDefaultThreadPool(tp =>
{
tp.MaxConcurrency = 24;
});
});
services.AddQuartzServer(options =>
{
// when shutting down we want jobs to complete gracefully
options.WaitForJobsToComplete = true;
});
// Add Services
services.AddHostedService<QuartzHostedService>();
services.AddSingleton<IJobFactory, SingletonJobFactory>();
services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>();
services.AddTransient<ImportJob>();
services.AddScoped<IApplicationDbContext, ApplicationDbContext>();
services.AddScoped<IMyRepository, MyRepository>();
Logic.cs
// Grab the Scheduler instance from the Factory
var factory = new StdSchedulerFactory();
var scheduler = await factory.GetScheduler();
var parameters = new JobDataMap()
{
new KeyValuePair<string, object>("request", message),
new KeyValuePair<string, object>("sales", sales),
};
var jobId = $"processJob{Guid.NewGuid()}";
var groupId = $"group{Guid.NewGuid()}";
// defines the job
IJobDetail job = JobBuilder.Create<ImportJob>()
.WithIdentity(jobId, groupId)
.UsingJobData(parameters)
.Build();
// defines the trigger
ITrigger trigger = TriggerBuilder.Create()
.WithIdentity($"Trigger{Guid.NewGuid()}", groupId)
.ForJob(job)
.StartNow()
.Build();
// schedule Job
await scheduler.ScheduleJob(job, trigger);
// and start it off
await scheduler.Start();
QuartzHostedService.cs
public class QuartzHostedService : IHostedService
{
private readonly ISchedulerFactory _schedulerFactory;
private readonly IJobFactory _jobFactory;
private readonly ILogger<QuartzHostedService> _logger;
private readonly IEnumerable<JobSchedule> _jobSchedules;
public QuartzHostedService(
ISchedulerFactory schedulerFactory,
IJobFactory jobFactory,
IEnumerable<JobSchedule> jobSchedules,
ILogger<QuartzHostedService> logger)
{
_schedulerFactory = schedulerFactory;
_jobSchedules = jobSchedules;
_jobFactory = jobFactory;
_logger = logger;
}
public IScheduler Scheduler { get; set; }
public async Task StartAsync(CancellationToken cancellationToken)
{
try
{
Scheduler = await _schedulerFactory.GetScheduler(cancellationToken);
Scheduler.JobFactory = _jobFactory;
foreach (var jobSchedule in _jobSchedules)
{
var job = CreateJob(jobSchedule);
var trigger = CreateTrigger(jobSchedule);
await Scheduler.ScheduleJob(job, trigger, cancellationToken);
}
await Scheduler.Start(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await Scheduler?.Shutdown(cancellationToken);
}
private static IJobDetail CreateJob(JobSchedule schedule)
{
var jobType = schedule.JobType;
return JobBuilder
.Create(jobType)
.WithIdentity(jobType.FullName)
.WithDescription(jobType.Name)
.Build();
}
private static ITrigger CreateTrigger(JobSchedule schedule)
{
return TriggerBuilder
.Create()
.WithIdentity($"{schedule.JobType.FullName}.trigger")
.StartNow()
.Build();
}
}
SingletonJobFactory.cs
public class SingletonJobFactory : IJobFactory
{
private readonly IServiceProvider _serviceProvider;
public SingletonJobFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
try
{
return _serviceProvider.GetRequiredService(bundle.JobDetail.JobType) as IJob;
}
catch (Exception ex)
{
throw;
}
}
public void ReturnJob(IJob job) { }
}
Importjob.cs
[DisallowConcurrentExecution]
public class ImportJob : IJob
{
private readonly IServiceProvider _provider;
private readonly ILogger<ImportJob> _logger;
public ImportJob(IServiceProvider provider, ILogger<ImportJob> logger)
{
_provider = provider;
_logger = logger;
}
public async Task Execute(IJobExecutionContext context)
{
try
{
using (var scope = _provider.CreateScope())
{
var jobType = context.JobDetail.JobType;
var job = scope.ServiceProvider.GetRequiredService(jobType) as IJob;
var repo = _provider.GetRequiredService<MYRepository>();
var importFactSales = _provider.GetRequiredService<IImportData>();
var savedRows = 0;
var request = (MyRequest)context.JobDetail.JobDataMap.Get("request");
var sales = (IEnumerable<MyData>)context.JobDetail.JobDataMap.Get("sales");
await importFactSales.saveValidateItems(repo, request, sales, savedRows);
}
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
}
I have found a solution in the meantime. As #marko-lahma described in the comment, use the built-in hosted service and don't implement your own JobFactory.
Remove the SingletonJobFactory.cs and QuartzHostedService.cs
Use the Autofac.Extras.Quartz and Quartz.Extensions.Hosting Nuget Package
Don't use CreateScope anymore, inject all needed Dependencies over the Constructor
Register QuartzAutofacFactoryModule and QuartzAutofacJobsModule in the Startup.

Get current windows user in Blazor Server

I want to get the name of the current Windows user for my Blazor Server project.
I tried it via HttpContext, which is unreliable, according to this github issue.
Then I went with the MS documentation, without success.
Still returned null for User
At this point I'm wondering if it's my incompetence or something with my whole idea of using Blazor Server for this.
This is pretty much all the code:
#page "/"
#using System.Security.Claims
#using Microsoft.AspNetCore.Components.Authorization
#inject AuthenticationStateProvider AuthenticationStateProvider
<h3>ClaimsPrincipal Data</h3>
<button #onclick="GetClaimsPrincipalData">Get ClaimsPrincipal Data</button>
<p>#_authMessage</p>
#if (_claims.Count() > 0)
{
<ul>
#foreach (var claim in _claims)
{
<li>#claim.Type: #claim.Value</li>
}
</ul>
}
<p>#_surnameMessage</p>
#code {
private string _authMessage;
private string _surnameMessage;
private IEnumerable<Claim> _claims = Enumerable.Empty<Claim>();
private async Task GetClaimsPrincipalData()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity.IsAuthenticated)
{
_authMessage = $"{user.Identity.Name} is authenticated.";
_claims = user.Claims;
_surnameMessage =
$"Surname: {user.FindFirst(c => c.Type == ClaimTypes.Surname)?.Value}";
}
else
{
_authMessage = "The user is NOT authenticated.";
}
}
There is also the part from the docu with the user.Identity.Name, but since I dont even get the claims, as of now, I am lost with what to do.
Edit 1:
Startup.cs
public class CustomAuthStateProvider : AuthenticationStateProvider
{
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, "mrfibuli"),
}, "Fake authentication type");
var user = new ClaimsPrincipal(identity);
return Task.FromResult(new AuthenticationState(user));
}
}
In your .razor file:
<AuthorizeView>
Hello, #context.User.Identity?.Name!
</AuthorizeView>
Or in your code-behind:
[Inject]
AuthenticationStateProvider? AuthenticationStateProvider { get; set; }
public string? CurrentUserName { get; set; }
protected override async Task OnInitializedAsync()
{
if (AuthenticationStateProvider is not null)
{
var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
CurrentUserName = authenticationState.User.Identity?.Name;
}
}

Unit Tests with AutoMapper ProjectTo and Moq.EntityFrameworkCore

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.

Use asyn method for synchronous Sqlite on createMethod

I'm using Xamarin, also my SQLite tables contain a large amount of data.
Because I want to avoid UIThread problems in OnCreate(), I need to perform database actions asynchronously.
I'm looking for guidance if I am handling this properly.
First method, which I found on the net:
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
SetContentView(Resource.Layout.InventoryPreviewMain);
Thread thread = new Thread(() =>
{
SQLiteConnection db = new SQLiteConnection(dpPath);
var table = db.Query<InventoryPreviewClass>("select * from InventoryPreviewClass where CategoryID =" + Connection.CategoryID + "");
mItems = new List<InventoryPreviewClass>();
foreach (var item in table)
{
mItems.Add(new InventoryPreviewClass() { InventoryItemID = item.InventoryItemID, InventoryItemName = item.InventoryItemName, InventoryItemPrice = item.InventoryItemPrice });
}
MyListViewAdapterInventory adapter = new MyListViewAdapterInventory(this, Resource.Layout.InventoryPreview, mItems);
mlistview.Adapter = adapter;
});
thread.Start();
Second Method, using async
public async void StartTimer()
{
SQLiteConnection db = new SQLiteConnection(dpPath);
var table = db.Query<InventoryPreviewClass>("select * from InventoryPreviewClass where CategoryID =" + Connection.CategoryID + "");
mItems = new List<InventoryPreviewClass>();
foreach (var item in table)
{
mItems.Add(new InventoryPreviewClass() { InventoryItemID = item.InventoryItemID, InventoryItemName = item.InventoryItemName, InventoryItemPrice = item.InventoryItemPrice });
}
MyListViewAdapterInventory adapter = new MyListViewAdapterInventory(this, Resource.Layout.InventoryPreview, mItems);
mlistview.Adapter = adapter;
await Task.Delay(500);
}
Which of two examples are more safe for keeping alive UIthread? Is there any other solution for making this?What is more reccomended to do?
Answer
Use the Cross-platform SQLite Library made by #FrankKruger to create/access SQLite databases for Xamarin mobile apps.
This library has a built-in asynchronous connection, so you'll never need to worry about accessing the database from the UI Thread again!
Xamarin.Android Example
"Second Method"
public async Task StartTimer()
{
mItems = await InventoryPreviewClassDatabase.GetAllInventoryPreviewClassAsync();
MyListViewAdapterInventory adapter = new MyListViewAdapterInventory(this, Resource.Layout.InventoryPreview, mItems);
mlistview.Adapter = adapter;
}
BaseDatabase Class
using System;
using System.Threading.Tasks;
using SQLite;
namespace SampleApp
{
public abstract class BaseDatabase
{
#region Constant Fields
static readonly Lazy<SQLiteAsyncConnection> _databaseConnectionHolder = new Lazy<SQLiteAsyncConnection>(() => GetDatabaseConnection());
#endregion
#region Fields
static bool _isInitialized;
#endregion
#region Properties
static SQLiteAsyncConnection DatabaseConnection => _databaseConnectionHolder.Value;
#endregion
#region Methods
protected static async Task<SQLiteAsyncConnection> GetDatabaseConnectionAsync()
{
if (!_isInitialized)
await Initialize().ConfigureAwait(false);
return DatabaseConnection;
}
static async Task Initialize()
{
await DatabaseConnection.CreateTableAsync<InventoryPreviewClass>().ConfigureAwait(false);
_isInitialized = true;
}
SQLiteAsyncConnection GetDatabaseConnection()
{
var sqliteFilename = "YourDatabaseName.db3";
string documentsPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal); // Documents folder
var path = Path.Combine(documentsPath, sqliteFilename);
var conn = new SQLiteAsyncConnection(path, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.SharedCache);
return conn;
}
#endregion
}
}
Parent Database Class
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
namespace SampleApp
{
public abstract class InventoryPreviewClassDatabase : BaseDatabase
{
#region Methods
public static async Task<IList<InventoryPreviewClass>> GetAllInventoryPreviewClassAsync()
{
var databaseConnection = await GetDatabaseConnectionAsync().ConfigureAwait(false);
return await databaseConnection.Table<InventoryPreviewClass>().ToListAsync().ConfigureAwait(false);
}
public static async Task<InventoryPreviewClass> GetInventoryPreviewClassByIDAsync(int id)
{
var databaseConnection = await GetDatabaseConnectionAsync().ConfigureAwait(false);
return await databaseConnection.Table<InventoryPreviewClass>().Where(x => x.ID.Equals(id)).FirstOrDefaultAsync().ConfigureAwait(false);
}
public static async Task<int> SaveInventoryPreviewClassAsync(InventoryPreviewClass inventoryPreview)
{
var databaseConnection = await GetDatabaseConnectionAsync().ConfigureAwait(false);
var isObjectInDatabase = await GetInventoryPreviewClassByIDAsync(inventoryPreview.ID).ConfigureAwait(false) != null;
if (isObjectInDatabase)
return await databaseConnection.UpdateAsync(inventoryPreview).ConfigureAwait(false);
return await databaseConnection.InsertAsync(inventoryPreview).ConfigureAwait(false);
}
public static async Task<int> DeleteItemAsync(OpportunityModel opportunity)
{
var databaseConnection = await GetDatabaseConnectionAsync().ConfigureAwait(false);
return await databaseConnection.DeleteAsync(opportunity).ConfigureAwait(false);
}
public static async Task<int> GetNumberOfRowsAsync()
{
var databaseConnection = await GetDatabaseConnectionAsync().ConfigureAwait(false);
return await databaseConnection.Table<InventoryPreviewClass>().CountAsync().ConfigureAwait(false);
}
#endregion
}
}
This code was inspired from this Xamarin.Forms sample app

Resources