Ich habe bei meinen Anwendungen - glaube ich - nie irgendeine Stelle, wo ich wirklich eine gesamte, einzelne Entität benötige.
Das kostet ja auch alles Zeit, Performance, Load, Traffic...
Ich benötige eigentlich immer einen Ausschnitt einer Entität, oder die Summe aus einer gejointen Abfrage.
Der Vorteil einer Projektion ist ja auch, dass Du - in der Regel - immer nur eine Abfrage gegen die DB hast und keinen Data Merge Aufwand in der Logik hast.
Beispiel: Die Ansicht eines Profils (ex
Abt) hier im Forum ist eine Summe aus mehreren Tabellen:
- Forum Threads
- Forum Posts
- Users
- User Profiles
- User Avatars
Die Abfrage einer Profile View sieht so aus:
namespace MyCSharp.Portal.Engine.QueryHandlers
{
public class GetUserProfileViewProjectionHandler : IQueryHandler<GetUserProfileViewProjection, UserProfileProjection>
{
private readonly IMyCSharpDbContext _dbContext;
private readonly IMapper _mapper;
public GetUserProfileViewProjectionHandler(IMyCSharpDbContext dbContext, IMapper mapper)
{
_dbContext = dbContext;
_mapper = mapper;
}
public async Task<UserProfileProjection> Handle(GetUserProfileViewProjection request, CancellationToken cancellationToken)
{
// inputs
int userId = request.UserId;
// query
var query = _dbContext.UserAccounts.AsNoTracking()
.Where(DbUserEx.HasId(userId))
.ProjectTo<UserProfileProjection>(_mapper.ConfigurationProvider);
// load
var result = await query.SingleOrDefaultAsync(cancellationToken).ConfigureAwait(false);
return result;
}
}
}
D.h. alle Informationen werden über einen relativ großen aber sehr performanten Join geladen. Eine einzige Abfrage.
Das wichtige ist folgende Zeile:
.ProjectTo<UserProfileProjection>(_mapper.ConfigurationProvider);
ProjectTo ermittelt anhand von Reflection, welche Linq Expression aufgebaut werden muss, damit nur das geladen wird, was benötigt wird.
Die Linq Expression ist in EF Core die Basis für das, was nachher den SQL Query String erzeugt.
Der einzige Wartungsaufwand, den man hier noch hat:
- Man muss eine Projektionsklasse erstellen, die die geladenen Daten erzeugt
- Man muss ein Proktionsprofile erzeugen, sodass nachher die Expression aufgebaut werden kann
Wenn man eine Änderung hat, dann muss man nur die Projektion und dessen Profile anpassen - mehr nicht.
Die Projektion sieht in diesem Fall bei uns so aus:
namespace MyCSharp.Portal.Data.Projections
{
public class UserProfileProjection
{
public UserProfilePropertyProjection Profile { get; set; } = null!;
public UserAvatarProjection? Avatar { get; set; }
public UserForumStatsProjection ForumStats { get; set; } = null!;
public UserLatestActivityProjection? LatestActivity { get; set; }
public ForumPostHeadProjection? LatestPost { get; set; }
}
}
Und so das Profile:
namespace MyCSharp.Portal.Data.Profiles
{
public class UserProfileProjectionProfile : AutoMapper.Profile
{
public UserProfileProjectionProfile()
{
CreateMap<UserAccountEntity, UserProfileProjection>()
.ForMember(p => p.Profile,
opt => opt.MapFrom(e => e))
.ForMember(p => p.Avatar,
opt=> opt.MapFrom(e => e.Avatar))
.ForMember(p => p.ForumStats,
opt=> opt.MapFrom(e => e))
.ForMember(p => p.LatestActivity,
opt => opt.MapFrom(e => e))
.ForMember(p => p.LatestActivity,
opt => opt.MapFrom(e => e));
}
}
}
Das hier ist viel komfortabler und einfacher umzusetzen als die Keyless Types in EF Core - wobei das Ziel das selbe ist.
Im Endeffekt ist das eine sehr moderne Darstellung einer mehrschichtigen Architektur, da es keinerlei Abhängigkeiten zu den Daten-Modellen gibt, sondern nur zu Projektionen.
PS noch zum Naming:
- Views sind Resultate aus multiplen DB Queries
- Projections das Resultat eines einzelnen Queries (auch über Joins)