Claiton Lovato
Claiton Lovato

Reputation: 2382

Azure Mobile Service TableController not returning inner objects

I am creating a basic (my first) Azure Mobile Service with Table Storage to control a simple Events app. My DataObjects consists of 2 object types: Coordinator and Event, I want Coordinators to be a separate table to store specific info that I don't want it denormalized inside the Events, but Events also has a inner object Location to store details for the event's location, but that I want to store denormalized as I don't want to maintain this details separately from the Event.

Here the objects I have so far: DataObjests:

public class Coordinator : EntityData {
    public string Name { get; set; }
    public int Points { get; set; }
    public bool IsActive { get; set; }
}
public class Event: EntityData {
        public Coordinator Coordinator { get; set; }
        public DateTime EventDate { get; set; }
        public int Attendees { get; set; }
        public Location Location { get; set; }
}
public class Location {
        public string Name { get; set; }
        public string Address1 { get; set; }
        public string Address2 { get; set; }
        public string City { get; set; }
        public string County { get; set; }
        public string PostCode { get; set; }
}

The controllers as basic TableController generated by VS's scaffolding, the only change I made was to expose the MobileServiceContext on the Event Controller to enable the Post method to find an existing Coordinator when saving as the client will only post the Coordinator's ID:

public class EventController : TableController<Event> {
        private MobileServiceContext context;
        protected override void Initialize(HttpControllerContext controllerContext) {
            base.Initialize(controllerContext);
            context = new MobileServiceContext();
            DomainManager = new EntityDomainManager<Event>(context, Request, Services);
        }
        protected override void Dispose(bool disposing) {
            context?.Dispose();
            base.Dispose(disposing);
        }

        public IQueryable<Event> GetAllEvent() {
            return Query();
        }

        public async Task<IHttpActionResult> PostEvent(Event item) {
            var coordinator = context.Coordinators.Find(item.Coordinator.Id);
            item.Coordinator = coordinator;
            Event current = await InsertAsync(item);
            return CreatedAtRoute("Tables", new { id = current.Id }, current);
        }

}

Posting data from the client works as expected, I have a Coordinator table with the right data:

ID  Name    Points  IsActive    Version CreatedAt   UpdatedAt   Deleted
cdf96b0f93644f1e945bd16d63aa96e0    John Smith  10  True    0x00000000000007D2  04/09/2015 09:15:02 +00:00  04/09/2015 09:15:02 +00:00  False
f216406eb9ad4cdcb7b8866fdf126727    Rebecca Jones   10  True    0x00000000000007D4  04/09/2015 09:15:30 +00:00  04/09/2015 09:15:30 +00:00  False

And a Event associated to the first Coordinator:

Id  EventDate   Attendees   Location_Name   Location_Address1   Location_Address2   Location_City   Location_County Location_PostCode   Version CreatedAt   UpdatedAt   Deleted Coordinator_Id
961abbf839bf4e3481ff43a214372b7f    04/11/2015 09:00:00 0   O2 Arena    Peninsula Square        London      SE10 0DX    0x00000000000007D6  04/09/2015 09:18:11 +00:00  04/09/2015 09:18:11 +00:00  False   cdf96b0f93644f1e945bd16d63aa96e0

At this point all looks good, my 2 problems are with the Get of the Event, where both Coordinator and Location objects are not returned and the Json of my EventController's Get is simply this:

[{"id":"961abbf839bf4e3481ff43a214372b7f","attendees":0,"eventDate":"2015-11-04T09:00:00Z"}]

So my 2 questions are:

1) The Location object is loaded correctly by the EventController on the server, I can see the properties correctly loaded if I break before returning, what for me looks like a Json serialization problem, but I already tried to change the serializer's configuration (on WebApiConfig) without much effect, the last option I tried was the MaxDepth but still the Location object is not returned.

2) The Coordinator object I not loaded on the server at all, not even the Id (that is correctly stored on the table) so I cant force the load of the entire object, and of course it isn't returned to the client.

Any ideas on what I am doing wrong here?

Thanks in advance

Claiton Lovato

Upvotes: 4

Views: 1029

Answers (2)

Maxim Lazarev
Maxim Lazarev

Reputation: 1254

I have managed to make complex properties be serialized and sent back to the client. Maybe the solution is not the cleanest, but it works in my case. Hope someone else will find it useful too.

First, create a class that inherits from EnableQueryAttribute and override GetModel method like this:

public class CustomEnableQueryAttribute : EnableQueryAttribute
{
    public override IEdmModel GetModel(Type elementClrType, HttpRequestMessage request, HttpActionDescriptor actionDescriptor)
    {
        var modelBuilder = new ODataConventionModelBuilder();
        modelBuilder.EntitySet<Event>("Events");

        return modelBuilder.GetEdmModel();
    }
}

Then, in your Initialize method of the controller, add these lines:

    protected override void Initialize(HttpControllerContext controllerContext)
    {
        base.Initialize(controllerContext);
        context = new MobileServiceContext();
        DomainManager = new EntityDomainManager<Event>(context, Request, Services);

        //Add these lines
        var service = Configuration.Services.GetServices(typeof(IFilterProvider)).ToList();
        service.Remove(service.FirstOrDefault(f => f.GetType() == typeof(TableFilterProvider)));
        service.Add(new TableFilterProvider(new CustomEnableQueryAttribute()));
        Configuration.Services.ReplaceRange(typeof(IFilterProvider), service.ToList().AsEnumerable());

        Request.Properties.Add("MS_IsQueryableAction", true);
    }

Upvotes: 1

Massimo Prota
Massimo Prota

Reputation: 1236

This is default behavior for TableController. In order to achieve what you're looking for, in a fashion way, you should implement OData $expand in your controller.

This article provides a good walk-throughout for the solution
Retrieving data from 1:n relationship using .NET backend Azure Mobile Services

As further extension, I've implemented a custom attribute that you can use in your controller methods to specify which properties the client can request to expand. You may not want to always returned all child relationships (expanded objects)

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class ExpandablePropertyAttribute : ActionFilterAttribute
{
    #region [ Constants ]
    private const string ODATA_EXPAND = "$expand=";
    #endregion

    #region [ Fields ]
    private string _propertyName;
    private bool _alwaysExpand;
    #endregion

    #region [ Ctor ]
    public ExpandablePropertyAttribute(string propertyName, bool alwaysExpand = false)
    {
        this._propertyName = propertyName;
        this._alwaysExpand = alwaysExpand;
    }
    #endregion

    #region [ Public Methods - OnActionExecuting ]
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        base.OnActionExecuting(actionContext);
        var uriBuilder = new UriBuilder(actionContext.Request.RequestUri);
        var queryParams = uriBuilder.Query.TrimStart('?').Split(new char[1] { '&' }, StringSplitOptions.RemoveEmptyEntries).ToList();
        int expandIndex = -1;

        for (var i = 0; i < queryParams.Count; i++)
        {
            if (queryParams[i].StartsWith(ODATA_EXPAND, StringComparison.Ordinal))
            {
                expandIndex = i;
                break;
            }
        }

        if (expandIndex >= 0 || this._alwaysExpand)
        {
            if (expandIndex < 0)
            {
                queryParams.Add(string.Concat(ODATA_EXPAND, this._propertyName));
            }
            else
            {
                queryParams[expandIndex] = queryParams[expandIndex] + "," + this._propertyName;
            }

            uriBuilder.Query = string.Join("&", queryParams);
            actionContext.Request.RequestUri = uriBuilder.Uri;
        }

    }
    #endregion
}

Which I use in my controller in this way:

    [ExpandableProperty("Documents", false)]
    public IQueryable<ClientActivity> GetAllClientActivities()
    {
        return Query(); 
    }

Upvotes: 4

Related Questions