Skip to content

Instantly share code, notes, and snippets.

@JKamsker
Last active October 18, 2019 06:22
Show Gist options
  • Save JKamsker/45ef8e3c2d6f9173bd93344678cc2705 to your computer and use it in GitHub Desktop.
Save JKamsker/45ef8e3c2d6f9173bd93344678cc2705 to your computer and use it in GitHub Desktop.
GraphQl-Projector
public class CategoryDao : IDisposable, IAsyncDisposable
{
private readonly GraphContext context;
public CategoryDao(GraphContext context)
{
this.context = context;
}
public async Task<Category> FindByIdAsync(int categoryId, IEnumerable<string> requiredFields = null)
{
var query = this.context.Categories.AsNoTracking();
if (StaticSettings.ProjectionEnabled && (requiredFields?.Any() ?? false))
{
var projection = ProjectionBuilder
.Get<Category>()
.WithProperties(requiredFields.Append(nameof(Category.CategoryID)))
.ExcludeProperties(this.context?.IgnoredMembers?.GetIgnoredMembers<Category>())
.Build();
query = query.Select(projection);
}
return await query.FirstOrDefaultAsync(x => x.CategoryID == categoryId);
}
}
public class IgnoredMembersCollection
{
private readonly ModelBuilder builder;
private Dictionary<Type, IReadOnlyList<string>> ignoredLists;
public IgnoredMembersCollection(ModelBuilder builder)
{
this.builder = builder;
this.ignoredLists = new Dictionary<Type, IReadOnlyList<string>>();
}
public IReadOnlyList<string> GetIgnoredMembers<TEntity>() where TEntity : class
{
if (!this.ignoredLists.TryGetValue(typeof(TEntity), out var metadata))
{
this.ignoredLists[typeof(TEntity)] = metadata = this.builder.Entity<TEntity>().Metadata.GetIgnoredMembers();
}
return metadata;
}
}
public class GraphContext : DbContext, IDisposable
{
private static IgnoredMembersCollection ignoredMembers { get; set; }
public IgnoredMembersCollection IgnoredMembers
{
get
{
if (ignoredMembers == null)
{
//Forcing initialisation
base.Model.GetEntityTypes();
}
if (ignoredMembers == null)
{
Debugger.Break();
}
return ignoredMembers;
}
}
public DbSet<Category> Categories { get; set; }
public GraphContext(DbContextOptions options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
ignoredMembers = new IgnoredMembersCollection(builder);
}
}
public static class GraphQLExtensions {
public static IEnumerable<string> GetRequiredFieldNames<TSource>(this ResolveFieldContext<TSource> source, bool includeSubSelections = false)
{
var fieldTypes = GetFieldTypes(source.ReturnType);
var query = source.FieldAst.SelectionSet.Selections
.OfType<Field>()
.Where(x => !x.Arguments.Any());
if (!includeSubSelections)
{
query = query.Where(x => x.SelectionSet.Selections.Count == 0);
}
return SelectOriginalName(query);
IEnumerable<FieldType> GetFieldTypes(IGraphType returnType)
{
if (returnType is IComplexGraphType cgt)
{
return cgt.Fields;
}
if (returnType is ListGraphType lgt)
{
return GetFieldTypes(lgt.ResolvedType);
}
throw new NotImplementedException($"Cannot resolve returntype of '{returnType.GetType().FullName}'");
}
IEnumerable<string> SelectOriginalName(IEnumerable<Field> input)
{
foreach (var field in input)
{
var fieldType = fieldTypes.FirstOrDefault(x => string.Equals(x.Name, field.Name, StringComparison.OrdinalIgnoreCase));
if (fieldType == null)
{
yield return field.Name;
continue;
}
if (fieldType.Metadata.TryGetValue(SharedConstants.OriginalPropertyName, out var originalName))
{
yield return originalName.ToString();
continue;
}
yield return field.Name;
}
}
}
}
public class GraphQuery {
public GraphQuery(IMapper mapper)
{
this.FieldAsync<CategoryType>(
"category",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<IntGraphType>> { Name = "id", Description = "id of the category" }
),
resolve: async context =>
{
var categoryId = context.GetArgument<int>("id");
var requiredFields = context.GetRequiredFieldNames();
var cat = await context.Get<CategoryDao>().FindByIdAsync(categoryId, requiredFields);
return cat;
});
}
}
public class ProjectionBuilder
{
public static ProjectionBuilder<T> Get<T>()
{
return new ProjectionBuilder<T>();
}
}
public class ProjectionBuilder<T>
{
private IEnumerable<string> properties;
private IEnumerable<string> excludedProperties;
public ProjectionBuilder()
{
this.properties = Enumerable.Empty<string>();
this.excludedProperties = Enumerable.Empty<string>();
}
public ProjectionBuilder<T> WithProperties(IEnumerable<string> properties)
{
if (properties == null)
{
return this;
}
this.properties = this.properties.Concat(properties);
return this;
}
public ProjectionBuilder<T> ExcludeProperties(IEnumerable<string> properties)
{
if (properties == null)
{
return this;
}
this.excludedProperties = this.excludedProperties.Concat(properties);
return this;
}
public Expression<Func<T, T>> Build()
{
var requiredProperties = properties.Except(this.excludedProperties, StringComparer.InvariantCultureIgnoreCase)
.Select(x => typeof(T).GetProperty(x, comparisonType: StringComparison.InvariantCultureIgnoreCase))
.Where(x => x != null);
return this.Build(requiredProperties);
}
private Expression<Func<T, T>> Build(IEnumerable<PropertyInfo> requiredProperties)
{
var parameterExpression = Expression.Parameter(typeof(T), "x");
var memberBindings = requiredProperties.Select(x => Expression.Bind(x, Expression.Property(parameterExpression, x)));
return Expression.Lambda<Func<T, T>>(Expression.MemberInit(Expression.New(typeof(T)), memberBindings), new ParameterExpression[] { parameterExpression });
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment