Reputation: 2382
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
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
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