tgstation-server 6.19.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
Projectable{TQueried,TResult}.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Linq.Expressions;
5using System.Threading;
6using System.Threading.Tasks;
7
8using Microsoft.EntityFrameworkCore;
9
11
13{
19 public sealed class Projectable<TQueried, TResult>
20 where TQueried : EntityId
21 where TResult : notnull
22 {
27
31 readonly Expression<Func<ProjectedPair<TQueried, TResult>, ProjectedPair<object?, TResult>>> selector;
32
36 readonly Func<ProjectedPair<object?, TResult>?, AuthorityResponse<TResult>> resultMapper;
37
41 readonly CancellationToken cancellationToken;
42
49 public static async ValueTask<Dictionary<long, AuthorityResponse<TResult>>> Combine(Func<IQueryable<TQueried>, IQueryable<ProjectedPair<TQueried, TResult>>> projection, params Projectable<TQueried, TResult>[] inputs)
50 {
51 ArgumentNullException.ThrowIfNull(projection);
52 ArgumentNullException.ThrowIfNull(inputs);
53
54 if (inputs.Length == 0)
55 return new Dictionary<long, AuthorityResponse<TResult>>();
56
57 var firstProjectable = inputs[0];
58 var workingSet = projection(firstProjectable.query);
59 foreach (var projectable in inputs.Skip(1))
60 {
61 if (firstProjectable.resultMapper != projectable.resultMapper)
62 throw new InvalidOperationException($"Different implementations of {nameof(resultMapper)} in combined {firstProjectable.GetType().Name}.");
63 if (firstProjectable.selector != projectable.selector)
64 throw new InvalidOperationException($"Different implementations of {nameof(selector)} in combined {firstProjectable.GetType().Name}.");
65
66 workingSet = workingSet.Union(projection(projectable.query));
67 }
68
69 using var cts = CancellationTokenSource.CreateLinkedTokenSource(inputs.Select(projectable => projectable.cancellationToken).ToArray());
70 var finalQueryable = workingSet
71 .Select(CombinedProjectionExpression(firstProjectable.selector));
72
73 return await finalQueryable
74 .ToDictionaryAsync(
75 result => result.Id,
76 result => firstProjectable.resultMapper(result.Projected));
77 }
78
89 CancellationToken cancellationToken)
90 => Create(
91 query,
92 projected => new ProjectedPair<object?, TResult>
93 {
94 Queried = null,
95 Result = projected.Result,
96 },
99
113 CancellationToken cancellationToken)
114 {
115 Expression<Func<ProjectedPair<TSelection, TResult>, ProjectedPair<object?, TResult>>> makeProjectedGeneric = projected => new ProjectedPair<object?, TResult>
116 {
117 Queried = projected.Queried,
118 Result = projected.Result,
119 };
120
121 var parameter = Expression.Parameter(typeof(ProjectedPair<TQueried, TResult>), "innerProjectedParam");
122 return new(
123 query,
124 Expression.Lambda<Func<ProjectedPair<TQueried, TResult>, ProjectedPair<object?, TResult>>>(
125 Expression.Invoke(
126 makeProjectedGeneric,
127 Expression.Invoke(
128 selector,
129 parameter)),
130 parameter),
131 projected => resultMapper(
132 projected != null
134 {
135 Queried = (TSelection)projected.Queried!,
136 Result = projected.Result,
137 }
138 : null),
140 }
141
147 private static Expression<Func<ProjectedPair<TQueried, TResult>, CombinedProjection>> CombinedProjectionExpression(Expression<Func<ProjectedPair<TQueried, TResult>, ProjectedPair<object?, TResult>>> selector)
148 {
149 Expression<Func<ProjectedPair<TQueried, TResult>, long>> idSelector = projected => projected.Queried.Id!.Value;
150
151 var parameter = Expression.Parameter(typeof(ProjectedPair<TQueried, TResult>), "projectedParamForCombined");
152 var selection = Expression.Invoke(selector, parameter);
153 var id = Expression.Invoke(idSelector, parameter);
154
155 var ourType = typeof(CombinedProjection);
156
157 var memberInitExpr = Expression.MemberInit(
158 Expression.New(ourType),
159 Expression.Bind(
160 ourType.GetProperty(nameof(CombinedProjection.Id))!,
161 id),
162 Expression.Bind(
163 ourType.GetProperty(nameof(CombinedProjection.Projected))!,
164 selection));
165
166 var finalExpr = Expression.Lambda<Func<ProjectedPair<TQueried, TResult>, CombinedProjection>>(memberInitExpr, parameter);
167
168 return finalExpr;
169 }
170
178 private Projectable(
182 CancellationToken cancellationToken)
183 {
184 this.query = query ?? throw new ArgumentNullException(nameof(query));
185 this.selector = selector ?? throw new ArgumentNullException(nameof(selector));
186 this.resultMapper = resultMapper ?? throw new ArgumentNullException(nameof(resultMapper));
187 this.cancellationToken = cancellationToken;
188 }
189
195 public async ValueTask<AuthorityResponse<TResult>> Resolve(Func<IQueryable<TQueried>, IQueryable<ProjectedPair<TQueried, TResult>>> projection)
196 {
197 ArgumentNullException.ThrowIfNull(projection);
198 var selection = await projection(query)
199 .Select(selector)
200 .FirstOrDefaultAsync(cancellationToken);
201 return resultMapper(selection);
202 }
203
207 private class CombinedProjection
208 {
212 public required long Id { get; init; }
213
217 public required ProjectedPair<object?, TResult> Projected { get; init; }
218 }
219 }
220}
Common base of entities with IDs.
Definition EntityId.cs:7
DTO for selecting the Id from TQueried alongside the selection ProjectedPair<TQueried,...
required ProjectedPair< object?, TResult > Projected
The selection/TResult ProjectedPair<TQueried, TResult>.
A projectable TResult based on an underlying TQueried .
readonly Expression< Func< ProjectedPair< TQueried, TResult >, ProjectedPair< object?, TResult > > > selector
The selector for the ProjectedPair<TQueried, TResult> data required by the resultMapper.
async ValueTask< AuthorityResponse< TResult > > Resolve(Func< IQueryable< TQueried >, IQueryable< ProjectedPair< TQueried, TResult > > > projection)
Resolve the Projectable<TQueried, TResult>.
readonly CancellationToken cancellationToken
CancellationToken for the operation.
readonly Func< ProjectedPair< object?, TResult >?, AuthorityResponse< TResult > > resultMapper
Mapper for transforming the ProjectedPair<TQueried, TResult> TResult into an AuthorityResponse<TResu...
static Expression< Func< ProjectedPair< TQueried, TResult >, CombinedProjection > > CombinedProjectionExpression(Expression< Func< ProjectedPair< TQueried, TResult >, ProjectedPair< object?, TResult > > > selector)
Create an Expression<TDelegate> to transform a ProjectedPair<TQueried, TResult> into a CombinedProjec...
static Projectable< TQueried, TResult > Create< TSelection >(IQueryable< TQueried > query, Expression< Func< ProjectedPair< TQueried, TResult >, ProjectedPair< TSelection, TResult > > > selector, Func< ProjectedPair< TSelection, TResult >?, AuthorityResponse< TResult > > resultMapper, CancellationToken cancellationToken)
Create a Projectable<TQueried, TResult>.
static async ValueTask< Dictionary< long, AuthorityResponse< TResult > > > Combine(Func< IQueryable< TQueried >, IQueryable< ProjectedPair< TQueried, TResult > > > projection, params Projectable< TQueried, TResult >[] inputs)
Combine a set of Projectable<TQueried, TResult>s into the resulting AuthorityResponse<TResult>s.
Projectable(IQueryable< TQueried > query, Expression< Func< ProjectedPair< TQueried, TResult >, ProjectedPair< object?, TResult > > > selector, Func< ProjectedPair< object?, TResult >?, AuthorityResponse< TResult > > resultMapper, CancellationToken cancellationToken)
Initializes a new instance of the Projectable<TQueried, TResult> class.
static Projectable< TQueried, TResult > Create(IQueryable< TQueried > query, Func< ProjectedPair< object?, TResult >?, AuthorityResponse< TResult > > resultMapper, CancellationToken cancellationToken)
Create a Projectable<TQueried, TResult>.
readonly IQueryable< TQueried > query
The underlying IQueryable<T>. Should only select one entity.
DTO for moving database projected objects through the system.
required TQueried Queried
The originally queried TQueried .