In my application I eventually want to have two views, A and B. View A will show A objects and their related B objects. View B will show B objects and their related A objects. I need to get data in from the data layer to the modeling layer going both ways from A to B, and vice versa. Unfortunately, it seems to cause recursive constructor calls, and I don’t see any way to avoid this issue. I’d rather not use a tool like Automapper. How to I get the data into the model going both ways, while avoiding recursive constructor calls?
In the database I have a one-to-many relationship between A and B.
A has many B's
B has one A
I scaffolded in the Context and Data classes from the database and it generated these classes in the data layer:
- Data.MyDatabaseNameContext
- Data.A
- Data.B
I didn’t make any changes to the above scaffolded classes and I’m confident that they scaffolded in correctly.
In I create 2 new View Models for A and B, then initialize their properties in the constructors:
View Model for A:
public class A
{
public readonly Data.A _A;
public Guid AId { get; set; }
public List<Models.B> Bs { get; set; }
// Inside this constructor a new B object is always made
public A(A dbobject)
{
_A = dbobject;
Bs = new List<B>();
foreach (var item in dbobject.B)
{
B.Add(new B(item));
}
}
}
View Model For B:
public class B
{
public readonly Data.B _B;
public Guid BId { get; set; }
public Guid AId { get; set; }
public Models.A A { get; set; }
// inside this constructor a new A object is always made, thus causing the recursion
public B(B dbobject)
{
_B = dbobject;
A.Id = dbobject.AId
this.A = new A(dbobject.A)
}
}
And in the repository:
public interface IARepository {
Task<A> A(Guid id);
// Other CRUD signatures omitted
}
public class ARepository : IARepository
{
private readonly MyDatabaseNameContext _context;
public ARepository(MyDatabaseNameContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public async Task<A> A(Guid id)
{
var a = await this._context.A.AsNoTracking()
.Include(a => a.B)
.Select(a => new A(a))
.SingleOrDefaultAsync();
return a == null ? null : a;
}
}
// OTHER CRUD METHODS OMITTED
And B has a similar IBRepository interface and IBRepository repository as above just with all the a's and b's swapped.
CodePudding user response:
Ah, ok I see now you have Data domain A and View Model A.. Probably worth naming these a bit more clearly like Customer and CustomerViewModel rather that Data.Customer and Model.Customer or such.
For a start, Repositories should normally be dealing with Entities, not ViewModels/DTOs. The Generic Repository pattern is an anti-pattern for EF as it severely limits the capabilities that EF can provide for features like Projection. The Repository pattern is still valuable for enabling things like unit testing, but I recommend treating the repository like a Controller in terms of it's scope where a Repository serves a Controller rather than having a repository per entity. The pattern I find helpful is to have repositories return IQueryable<T>
rather than IEnumerable<T>
or T
or Task
of either of those. This gives consumers full control to apply sorting, filtering, projection, pagination, etc.
However, if you want a Repository per ViewModel, that's not all that much different, it's better than Repository per Entity provided your ViewModels will explicitly serve the needs of the view, and aren't just a straight copy of the entity.
When projecting via EF you don't need to eager load, just leverage Select
:
public async Task<AViewModel> GetAViewModel(Guid id)
{
var a = await this._context.A
.Select(a => new AViewModel
{
// copy values from a -> viewModel properties
Bs = a.Bs.Select(b => new BViewModel
{
// copy values from b -> viewModel
}).ToList()
}).SingleOrDefaultAsync();
return a;
}
I wouldn't be reluctant to learn Automapper as this is an extremely powerful tool for projection via it's ProjectTo
method. You set up the configuration either once for all view models, or on an as-needed basis, then this simplifies to:
public async Task<AViewModel> GetAViewModel(Guid id)
{
var a = await this._context.A
.ProjectTo<AViewModel>(_config)
.SingleOrDefaultAsync();
return a;
}
Where _config
represents the mapping configuration that defines how an A becomes an AViewModel and any dependent relations for the ViewModel (I.e. B -> BViewModel, etc.)