EF Core 6: How to implement controller that returns joined data? - entity-framework-core

I have a database with several different, but related tables:
class AccountInfo {
string id;
string name;
string email;
}
class ExtraInfo {
string id;
string proxy;
}
class UserInfo {
AccountInfo account;
ExtraInfo extra;
}
public class MyDbContext : DbContext {
public DbSet<AccountInfo> AccountInfo { get; set; }
public DbSet<ExtraInfo> ExtraInfo { get; set; }
}
public class ExtraInfoController : ODataController
{
private readonly DS2DbContext _context;
private readonly ILogger<UserInfoController> _logger;
public ExtraInfoController(DS2DbContext context) {
_context = context;
}
[EnableQuery(PageSize = 15)]
public IQueryable<UserInfo> Get() {
IQueryable<UserInfo> query =
from a in _context.AccountInfo
from x in _context.ExtraInfo
where (a.id == x.id)
select new UserInfo() { account = a, extra = x };
return query;
}
The query in ExtraInfoController.Get() works, but the result cannot be transferred back to the calling code, because the DbSet is declared for type ExtraInfo and I guess because of the way Blazor mangles all the pseudo code passed to it into working code, so it expects the return type to be IQueryable and not IQeryable.
I am new to EF core, so I don't know how to create a controller that is not directly related to a db table that would do the desired join and return an IQueryable without there being a UserInfo table in the db.

Related

Integration Testing in .NET Core 3.1 with AutoMapper, WebApplicationFactory, Entity Framework, and DTOs

We have an API with about a dozen integration tests. All the tests passed until I added some DTOs and used AutoMapper. Now, all the tests that test methods that use AutoMapper and the DTOs are failing. I have provided all the code needed to understand one of the failing tests. Also, I read a lot about AutoMapper and the following StackOverflow posts:
Integration Testing with AutoMapper fails to initialise configuration
A kind of integration testing in ASP.NET Core, with EF and AutoMapper
Startup.cs
This is our Startup.ConfigureServices(). I have tried every code block commented out and/or marked "ATTEMPTED".
public void ConfigureServices(IServiceCollection services)
{
services
.AddDbContext<OurContext>(options =>
options.UseSqlServer(Configuration["ConnectionString"]))
.AddDbContext<OurContext>()
.AddRazorPages()
.AddMvcOptions(options => options.EnableEndpointRouting = false)
.AddNewtonsoftJson(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver());
services
.AddControllersWithViews();
//ATTEMPTED
//services
// .AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
//ATTEMPTED
//MapperConfiguration mapperConfiguration = new MapperConfiguration(mc =>
//{
// mc.AddProfile(new OurProfile());
//});
//IMapper mapper = mapperConfiguration.CreateMapper();
//services
// .AddSingleton(mapper);
//ATTEMPTED
//services
// .AddAutoMapper(typeof(Startup));
//ATTEMPTED
//var assembly = typeof(Program).GetTypeInfo().Assembly;
//services
// .AddAutoMapper(assembly);
//ATTEMPTED
var assembly = typeof(Program).GetTypeInfo().Assembly;
services.AddAutoMapper(cfg =>
{
cfg.AllowNullDestinationValues = true;
cfg.CreateMap<OurModel, OurDto>()
.IgnoreAllPropertiesWithAnInaccessibleSetter();
}, assembly);
}
Controller
This is our controller.
[Route("api/[controller]")]
[ApiController]
public class OurController : ControllerBase
{
private readonly OurContext _context;
protected readonly ILogger<OurController> Logger;
private readonly IMapper _mapper;
public OurController(OurContext context, ILogger<OurController> logger,
IMapper mapper)
{
_context = context ??
throw new ArgumentNullException(nameof(context));
Logger = logger ??
throw new ArgumentNullException(nameof(logger));
_mapper = mapper ??
throw new ArgumentNullException(nameof(mapper));
}
[HttpGet]
public async Task<ActionResult<IEnumerable<OurDto>>> GetAll()
{
IQueryable<OurModel> models = _context.OurModel;
IQueryable<OurDto> dtos =
_mapper.Map<IQueryable<OurDto>>(models);
return await dtos.ToListAsync();
}
}
Profile, Model, and DTO
Profile
public class OurProfile : Profile
{
public OurProfile()
{
CreateMap<OurModel, OurDto>();
}
}
Model
public partial class OurModel
{
public string Number { get; set; }
public string Name1 { get; set; }
public string Name2 { get; set; }
public string Status { get; set; }
public DateTime? Date { get; set; }
public string Description { get; set; }
public string Comment { get; set; }
public string District { get; set; }
}
DTO
public class OurDto
{
public string Number { get; set; }
public string Name1 { get; set; }
public string Name2 { get; set; }
public string Status { get; set; }
public DateTime? Date { get; set; }
public string Description { get; set; }
public string Comment { get; set; }
public string District { get; set; }
}
Test Fixture
This is our test fixture.
public abstract class ApiClientFixture : IClassFixture<WebApplicationFactory<Startup>>
{
private readonly WebApplicationFactory<Startup> _factory;
protected abstract string RelativeUrl { get; }
protected ApiClientFixture(WebApplicationFactory<Startup> factory)
{
_factory = factory;
}
protected HttpClient CreateClient()
{
HttpClient client;
var builder = new UriBuilder();
client = _factory.CreateClient();
builder.Host = client.BaseAddress.Host;
builder.Path = $"{RelativeUrl}";
client.BaseAddress = builder.Uri;
return client;
}
}
Test
This is our test class. The single test in this test class fails.
public class Tests : ApiClientFixture
{
public Tests(WebApplicationFactory<Startup> factory) : base(factory)
{
}
protected override string RelativeUrl => "api/OurController/";
[Fact]
public async void GetAllReturnsSomething()
{
var response = await CreateClient().GetAsync("");
Assert.True(response.IsSuccessStatusCode);
}
}
When I debug the test I see that a 500 status code is returned from the URL provided to the in-memory API.
Does anybody have some suggestions? More than half of our tests currently fail, and I suspect that AutoMapper is not configured properly for integration testing.
Creating a map for IQueryable<T> is not really a good solution. In your answer you are losing proper flow of asynchronous database querying. I wrote about IQueryable<T> in a comment because you were looking for a 500 error cause. Making it work it's a one thing, making it a good solution it's another thing, however.
I'd strongly suggest to use AutoMapper ProjectTo() extension which you can use directly on a IQueryable<T> sequence. It let's you combine mapping and querying in one go. More or less it does a Select() based on your mappings, so it not only gives you proper model right away with the query result, but it also reduces the amount of columns obtained from database, which can make the query run faster. But, there are of course limitations to it, e.g. you can't use custom type converters or conditional mapping. You can read more about Project() in the documentation.
Usage:
public async Task<ActionResult<List<OurDto>>> GetAll()
{
return await _context
.OurModel
.ProjectTo<OutDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
Thanks to #Prolog for his comment. I realized that I need to map each element of the IQueryable individually, so I rewrote my Controller method.
Also, side note: IList.AsQueryable().ToListAsync() does not work, so I wrote:
IQueryable<OurDto> dtosQueryable = dtos.AsQueryable();
return await Task.FromResult(dtosQueryable.ToList());
Old Controller Method
[HttpGet]
public async Task<ActionResult<IEnumerable<OurDto>>> GetAll()
{
IQueryable<OurModel> models = _context.OurModel;
IQueryable<OurDto> dtos =
_mapper.Map<IQueryable<OurDto>>(models);
return await dtos.ToListAsync();
}
New Controller Method
public async Task<ActionResult<IEnumerable<OurDto>>> GetAll()
{
IQueryable<OurModel> models = _context.OurModel;
IList<OurDto> dtos = new List<OurDto>();
foreach (OurModel model in models)
{
OurDto dto = _mapper.Map<OurDto>(model);
dtos.Add(dto);
}
IQueryable<OurDto> dtosQueryable = dtos.AsQueryable();
return await Task.FromResult(dtosQueryable.ToList());
}

Entity framework relations

im trying to make an app and got a slight problem. my structure looks like this:
public class BaseModel
{
[Key]
private int _Id;
public int Id {
get { return _Id; }
set { _Id = value; }
}
}
public class SupplierModel : BaseModel
{
[ForeginKey("CountryCode")] // This should map to say "se" or "no" or whatever in the CountryModel table
public virtual CountryModel Country;
}
public class CountryModel : BaseModel
{
private string _CountryCode;
[Key] // This should be another key in the table to get the actual country.
public string CountryCode {
get { return _CountryCode; }
set { _CountryCode = value; }
}
private string _CountryName;
public string CountryName {
get { return _CountryName; }
set { _CountryName = value; }
}
}
Now i want SupplierModel to link to CountryModel (Works fine by the Id) but i want it to be the country code to be the relationship not the Id between the Entities.
So accessing CountryModel.Country should map to the CountryModel table and pull out the one that matches the country model.
Hope i didnt mess it up totaly for you, hard to explain when i do not fully understand Entity framework and database relations .. trying to learn =)

Get identity value after insert in entity framework while using dependancy injection

I am using entity frame work in my mvc core application. I am also using dependency injection technique. Now i want to get the value of new record identity column. I am using the code as..
public interface IGenericRepositoryStudent<T> where T : class
{
void Add(T item);
}
public class GenericRepositoryStudent<T> : IGenericRepositoryStudent<T> where T : class
{
private eerp_studentContext _context;
private DbSet<T> _dbSet;
public GenericRepositoryStudent(eerp_studentContext context)
{
_context = context;
_dbSet = _context.Set<T>();
}
public void Add(T item)
{
_dbSet.Add(item);
_context.SaveChanges();
}
}
Controller:
public class StudentController : Controller
{
private IStudentRepository _dbSet;
public StudentController(IStudentRepository dbSet)
{
_dbSet = dbSet;
}
public long Add([FromBody]Student _student)
{
if (ModelState.IsValid)
{
_dbSet.Add(acJournal);
long _id = _student.id; // value of _id is always 0.
return _id;
}
}
}
Apparently every T that you want to Add has a Primary Key. You want the value of this primary Key after the T has been added and changes are saved.
This means, that you can't add objects of every class, you can only add classes that have a Primary Key.
The easiest, and a very type safe way (checked by compiler) is to allow only adding of object that have a primary key.
// interface of objects that have Id (primary key)
public interface IID
{
public long Id {get; set;}
}
If your database uses another type for primary keys like int, or GUID, use this other type as return type
public interface IGenericRepositoryStudent<T> where T : IID
{
T Add(T item);
}
public class GenericRepositoryStudent<T> : IGenericRepositoryStudent<T> where T : IID
{
...
public T Add(T itemToAdd)
{
T addedItem = _dbSet.Add(item);
_context.SaveChanges();
return addedItem
}
}
Usage:
class Student : IID
{
public long Id {get; set;} // primary key
...
}
public class StudentController : Controller
{
...
public T Add([FromBody]Student _student)
{
if (ModelState.IsValid)
{
return _dbSet.Add(_student);
}
}
}
I chose to return the complete T instead of only the Id, because if you need some of the properties of the added item, you don't need to fetch the item you just added. Besides this is just the return value of DbSet.Add(T)
If you really want, just return the Student's Id.

EF Context not keeping values after adding entity

Edit Is this post lacking sufficient information to get some guidance?
I have this method to insert an entity into the database:
public void Insert(T entity)
{
_context.Set<T>().Add(entity);
_context.SaveChanges();
}
When I inspect entity before adding it to the context, my CustomerRole field is there. Once the add has taken place, the context doesn't seem to have it. Because of this, I am receiving this error:
Entities in 'CcDataContext.Customers' participate in the
'Customer_CustomerRole' relationship. 0 related
'Customer_CustomerRole_Target' were found. 1
'Customer_CustomerRole_Target' is expected.
These images show what I mean:
Inspecting my entity
Inspecting the context
Can anyone explain this behaviour and what I can do about it?
This is the structure of my classes (cut down for brevity):
public class Customer : BaseEntity
{
public CustomerRole CustomerRole { get; set; }
}
class CustomerMap : EntityTypeConfiguration<Customer>
{
public CustomerMap()
{
HasRequired(t => t.CustomerRole)
.WithMany(t => t.Customers);
}
}
public class CustomerRole : BaseEntity
{
private ICollection<Customer> _customers;
public ICollection<Customer> Customers
{
get { return _customers ?? (new List<Customer>()); }
set { _customers = value; }
}
}
I can confirm that customer map is being added to the configuration and my database is built in line with them.
This is the call I am making which does the insert:
public Customer InsertGuestCustomer()
{
var customer = new Customer();
CustomerRole guestRole = GetCustomerRoleByName("Guest");
if (guestRole == null)
throw new Exception("Customer Role is not defined!");
customer.UserName = "";
customer.EmailAddress = "";
customer.Password = "";
customer.IsAdmin = false;
customer.CustomerRole = guestRole;
_customerRepository.Insert(customer);
return customer;
}
I have no other data in my database, this would be the first customer record and only one CustomerRole. My Customer table has a Foreign Key pointing to my CustomerRole.Id table / column.
Mark your navigation properties as virtual and initialize the collection property in the entity constructor rather than from the property getter.
public class Customer : BaseEntity
{
public virtual CustomerRole CustomerRole { get; set; }
}
...
public class CustomerRole : BaseEntity
{
public CustomerRole()
{
Customers = new List<Customer>();
}
public virtual ICollection<Customer> Customers { get; protected set; }
}
In your Customers property, you were returning a new List in the getter when the backing field was null, but you never assigned this to your backing field.

generic repository EF4 CTP5 getById

I have a generic repository an I am trying to add a GetById method as shown here
C# LINQ to SQL: Refactoring this Generic GetByID method
The problem is my repository does not use System.Data.Linq.DataContext
instead I use System.Data.Entity.DbContext
So I get errors where I try to use
Mapping.GetMetaType
and
return _set.Where( whereExpression).Single();
How can I implement a generic GetById method in CTP5? Should I be using System.Data.Entity.DbContext in my Repository.
Here is the start of my repository class
public class BaseRepository<T> where T : class
{
private DbContext _context;
private readonly DbSet<T> _set;
public BaseRepository()
{
_context = new MyDBContext();
_set = _context.Set<T>();
}
The most basic approach is simply
public T GetById(params object[] keys)
{
_set.Find(keys);
}
If you know that all your entities have primary key called Id (it doesn't have to be called Id in DB but it must be mapped to property Id) of defined type you can use simply this:
public interface IEntity
{
int Id { get; }
}
public class BaseRepository<T> where T : class, IEntity
{
...
public T GetById(int id)
{
_set.Find(id);
}
}
If data type is not always the same you can use:
public interface IEntity<TKey>
{
TKey Id { get; }
}
public class BaseRepository<TEntity, TKey> where TEntity : class, IEntity<TKey>
{
...
public TEntity GetById(TKey id)
{
_set.Find(id);
}
}
You can also simply use:
public class BaseRepository<TEntity, TKey> where TEntity : class
{
...
public TEntity GetById(TKey id)
{
_set.Find(id);
}
}
try this
public virtual T GetByID(object id)
{
// Define the entity key values.
IEnumerable<KeyValuePair<string, object>> entityKeyValues =
new KeyValuePair<string, object>[] {
new KeyValuePair<string, object>("Id", id) };
string qualifiedEntitySetName = _context.DefaultContainerName + "." + typeof(T).Name;
EntityKey key = new EntityKey(qualifiedEntitySetName, entityKeyValues);
return (T)_context.GetObjectByKey(key);
}