HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

Building a custom response body based on API consumer's need

Submitted by: @import:stackexchange-codereview··
0
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:

api/characters/2?fields=id,displayname,mainImageUrl


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 DtoBuilder.Assemble(), but am unsure of how to improve here. Any advice on these areas and others is much appreciated.

Gist link to IMetadataService

MetadataService.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 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 lines

private 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 so

private 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 fieldsNamesList to not having any duplicate names in it



  • used fieldsNamesList.Count == 0 condition instead of !fieldsNamesList.Any() because it already is a List so we can just evaluate the Count property which doesn't involve getting an Enumerator and a call to MoveNext() under the hood of the Any() 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.