patterncsharpMinor
Building a custom response body based on API consumer's need
Viewed 0 times
needresponsebuildingcustomconsumerbasedbodyapi
Problem
I've been researching ways to reduce response size on requests in my rest API without adding lots of calls to get back just the ID and single properties. What I've ended up attempting is creating the ability for the API consumer to specify the fields they want back from a default response model and have the response body send back only those fields out of the default model. This way, ideally, they get only exactly what they need.
An example request would look like:
Not specifying any fields would bring back the full default model as the response body.
This is a Web API 2 project with Entity Framework 6 using C# 6. I'm using AutoMapper to go from Entity object to Dto where necessary. What I have right now functions as I wanted for the most part, but I'm sure there are areas that need to be improved.
Specifically, I believe the use of joins in my Linq in general could be more generic as well as the amount of reflection in
Gist link to
MetadataService.cs
This is the entry point from a Controller. It's injected into the controller's ctor via IoC (
```
public interface IMetadataService
{ /link above/ }
public class MetadataService : BaseService, IMetadataService
{
public MetadataService(IApplicationDbContext db)
: base(db)
{ }
public dynamic GetWithMovesOnEntity(int id, string fields = "")
where TEntity : class, IMoveIdEntity
where TDto : class
{
var dto = (from entity in Db.Set()
join joinEntity in Db.Moves
on entity.MoveId equals joinEntity.Id
where entity.Id == id
select entity).ProjectTo()
.SingleOrDefault();
return BuildContentResponse(dto, fields);
}
public dynamic
An example request would look like:
api/characters/2?fields=id,displayname,mainImageUrlNot specifying any fields would bring back the full default model as the response body.
This is a Web API 2 project with Entity Framework 6 using C# 6. I'm using AutoMapper to go from Entity object to Dto where necessary. What I have right now functions as I wanted for the most part, but I'm sure there are areas that need to be improved.
Specifically, I believe the use of joins in my Linq in general could be more generic as well as the amount of reflection in
DtoBuilder.Assemble(), but am unsure of how to improve here. Any advice on these areas and others is much appreciated.Gist link to
IMetadataServiceMetadataService.cs
This is the entry point from a Controller. It's injected into the controller's ctor via IoC (
IMetadataService).```
public interface IMetadataService
{ /link above/ }
public class MetadataService : BaseService, IMetadataService
{
public MetadataService(IApplicationDbContext db)
: base(db)
{ }
public dynamic GetWithMovesOnEntity(int id, string fields = "")
where TEntity : class, IMoveIdEntity
where TDto : class
{
var dto = (from entity in Db.Set()
join joinEntity in Db.Moves
on entity.MoveId equals joinEntity.Id
where entity.Id == id
select entity).ProjectTo()
.SingleOrDefault();
return BuildContentResponse(dto, fields);
}
public dynamic
Solution
Possible problem
Assume one would pass a request like
api/characters/2?fields=id,displayname,mainImageUrl,displayname
then the
MetadataService
This class is well written and structured and easy to read. The only thing I don't like is the name
IMO
BaseService
Cleraly at the top of the class we see
which is all good, you are using
But if we take a closer look at your class, we see that the
So if you would use the class level
and used in the
There are a few things I have changed in that method:
Assume one would pass a request like
api/characters/2?fields=id,displayname,mainImageUrl,displayname
then the
Assemble() method which is called by the DtoBuilder.Build() method would blow in your face because displayname would exists twice in the fieldsNamesList and therefor the call to customDto.Add(propInfo.Name, value); would throw an ArgumentException because the key already exists in the dictionary.MetadataService
This class is well written and structured and easy to read. The only thing I don't like is the name
where for the passed in Expression> in the overloaded Get() and GetAll() methods.IMO
where is a little bit abstract and hurts my eyes a little bit. Maybe whereCondition would be better ?BaseService
Cleraly at the top of the class we see
protected readonly DtoBuilder DtoBuilder;
protected readonly IApplicationDbContext Db;
protected BaseService(IApplicationDbContext db)
{
Guard.VerifyObjectNotNull(db, nameof(db));
Db = db;
DtoBuilder = new DtoBuilder();
}which is all good, you are using
readonly , sure one could question the PascalCase casing of the protected fields, but otherwise this is ok.But if we take a closer look at your class, we see that the
DtoBuilder DtoBuilder isn't used anywhere in that class but only in the constructor. If you use a DtoBuilder object you are creating a new one each time you need it (BuildContentResponse and BuildContentResponseMultiple).So if you would use the class level
DtoBuilder you could limit the usage of reflection by caching the PropertyInfo's in a Dictionary> by first checking if the desired TDto is in the dictionary and if not you could add a new Dcitionary containing all the PropertyInfo's of that type. Something along these linesprivate readonly Dictionary> cachedPropertyInfos = new Dictionary>();
private Dictionary FetchProperties()
{
var type = typeof(TDto);
Dictionary typeProperties = null;
if (cachedPropertyInfos.TryGetValue(type, out typeProperties))
{
return typeProperties;
}
var properties = type.GetProperties(Flags);
typeProperties = properties.ToDictionary(p => p.Name.ToLowerInvariant(), p => p);
cachedPropertyInfos.Add(type, typeProperties);
return typeProperties;
}and used in the
Assemble() method like soprivate dynamic Assemble(TEntity entity, IEnumerable requestedFieldNames)
{
Guard.VerifyObjectNotNull(requestedFieldNames, nameof(requestedFieldNames));
var fieldsNamesList = requestedFieldNames.Select(f => f.ToLowerInvariant()).Distinct().ToList();
Dictionary typeProperties = FetchProperties();
//if no field names exist add all public instance ones for a 'default' dto object
if (fieldsNamesList.Count == 0)
{
fieldsNamesList.AddRange(typeProperties.Keys);
}
var customDto = new Dictionary();
foreach (var field in fieldsNamesList)
{
PropertyInfo propInfo = null;
if (typeProperties.TryGetValue(field, out propInfo))
{
//if null make empty so result is more web friendly
var value = propInfo.GetValue(entity) ?? string.Empty;
customDto.Add(propInfo.Name, value);
}
}
return customDto.ToDynamicObject();
}There are a few things I have changed in that method:
- added a call to the new
FetchProperties()method
- restricted the
fieldsNamesListto not having any duplicate names in it
- used
fieldsNamesList.Count == 0condition instead of!fieldsNamesList.Any()because it already is aListso we can just evaluate theCountproperty which doesn't involve getting anEnumeratorand a call toMoveNext()under the hood of theAny()method.
- removed the variable
dynamic resultObj
Code Snippets
protected readonly DtoBuilder DtoBuilder;
protected readonly IApplicationDbContext Db;
protected BaseService(IApplicationDbContext db)
{
Guard.VerifyObjectNotNull(db, nameof(db));
Db = db;
DtoBuilder = new DtoBuilder();
}private readonly Dictionary<Type, Dictionary<string, PropertyInfo>> cachedPropertyInfos = new Dictionary<Type, Dictionary<string, PropertyInfo>>();
private Dictionary<string, PropertyInfo> FetchProperties<TDto>()
{
var type = typeof(TDto);
Dictionary<string, PropertyInfo> typeProperties = null;
if (cachedPropertyInfos.TryGetValue(type, out typeProperties))
{
return typeProperties;
}
var properties = type.GetProperties(Flags);
typeProperties = properties.ToDictionary(p => p.Name.ToLowerInvariant(), p => p);
cachedPropertyInfos.Add(type, typeProperties);
return typeProperties;
}private dynamic Assemble<TEntity, TDto>(TEntity entity, IEnumerable<string> requestedFieldNames)
{
Guard.VerifyObjectNotNull(requestedFieldNames, nameof(requestedFieldNames));
var fieldsNamesList = requestedFieldNames.Select(f => f.ToLowerInvariant()).Distinct().ToList();
Dictionary<string, PropertyInfo> typeProperties = FetchProperties<TDto>();
//if no field names exist add all public instance ones for a 'default' dto object
if (fieldsNamesList.Count == 0)
{
fieldsNamesList.AddRange(typeProperties.Keys);
}
var customDto = new Dictionary<string, object>();
foreach (var field in fieldsNamesList)
{
PropertyInfo propInfo = null;
if (typeProperties.TryGetValue(field, out propInfo))
{
//if null make empty so result is more web friendly
var value = propInfo.GetValue(entity) ?? string.Empty;
customDto.Add(propInfo.Name, value);
}
}
return customDto.ToDynamicObject();
}Context
StackExchange Code Review Q#136716, answer score: 3
Revisions (0)
No revisions yet.