-
-
Save lnickers2004/8486730 to your computer and use it in GitHub Desktop.
WebApi2: DTO ModelFactory which has been refactored to include the creation of HATEOAS URIs-- HyperMedia Links NOTE: we were not able to use automapper to Map between Entities and DTO's because we wanted to create Hypertext URIs for the DTOs inside the Factory. So we opted to use the Factory Pattern for the creation of our DTO's
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using CountingKs.Data; | |
using CountingKs.Models; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Net; | |
using System.Net.Http; | |
using System.Web.Http; | |
namespace CountingKs.Controllers | |
{ | |
//make base controller abstract so it cannot be instantiated | |
//all our controllers will derive from base controller to | |
//inherit common funtionality | |
public abstract class BaseApiController : ApiController | |
{ | |
private ModelFactory _modelFactory; | |
private ICountingKsRepository _repo; | |
public BaseApiController( ICountingKsRepository repo ) | |
{ | |
_repo = repo; | |
} | |
protected ICountingKsRepository TheRepository | |
{ | |
get | |
{ | |
return _repo; | |
} | |
} | |
//KEY POINT: we need to defer the creation of the model factory until | |
// it is needed. So we create a deferred property | |
//Note: this is basically a singleton pattern.. | |
//the factory will exist for the lifetime of the controller | |
protected ModelFactory TheModelFactory | |
{ | |
//remember, we're using factory pattern to copy database entities into DTO's that contain | |
//discoverable Hyperlinks for each resource returned to the user | |
get | |
{ | |
//the first get will cause the factory to be created | |
if(_modelFactory == null) | |
{ | |
//it should be late enough for the request to not be null | |
//the model factory will be crated as a result of a user request | |
//so we can create the model factory and pass it the request so we may | |
//use it and the Url helper to generate the requested resource's URI Hyperlinks for including | |
//in the DTO that we pass back to the user | |
_modelFactory = new ModelFactory(this.Request); | |
} | |
return _modelFactory; | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Web; | |
namespace CountingKs.Models | |
{ | |
public class FoodModel | |
{ | |
public string Url { get; set; } | |
public string Description { get; set; } | |
public IEnumerable<MeasureModel> Measures { get; set; } | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using CountingKs.Data; | |
using CountingKs.Data.Entities; | |
using CountingKs.Models; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Net; | |
using System.Net.Http; | |
using System.Web.Http; | |
namespace CountingKs.Controllers | |
{ | |
public class FoodsController : BaseApiController | |
{ | |
//hand off the injected repo to our base controller | |
public FoodsController( ICountingKsRepository repo ): base(repo) | |
{ | |
} | |
//users may optionally supply ?includeMeasures=true on query string | |
//we defualt it to true so the route is matched even if the querystring does not have includeMeasures | |
//supplied | |
public IEnumerable<FoodModel> Get( bool includeMeasures = true ) | |
{ | |
//The following code causes exception | |
// | |
//message: "An error has occurred." | |
//exceptionMessage: "The 'ObjectContent`1' type failed to serialize the response body for content type 'application/json; charset=utf-8'." | |
//exceptionType: "System.InvalidOperationException" | |
//stackTrace: null | |
//innerException: { | |
//message: "An error has occurred." | |
//exceptionMessage: "Self referencing loop detected for property 'food' with type 'CountingKs.Data.Entities.Food'. Path '[0].measures[0]'." | |
//exceptionType: "Newtonsoft.Json.JsonSerializationException" | |
// | |
//var results = _repo.GetAllFoodsWithMeasures() | |
// .OrderBy(f => f.Description) | |
// .Take(25) | |
// .ToList(); | |
//There are a few ways to fix this | |
//Method 1: project the childe object without a reference back to the parent of the relation | |
//this is a fragile... | |
//var results = _repo.GetAllFoodsWithMeasures() | |
// .OrderBy(f => f.Description) | |
// .Take(25) | |
// .ToList().Select(f => new{ | |
// Description = f.Description, | |
// Measures = f.Measures.Select(m => | |
// new | |
// { | |
// Description = m.Description, | |
// Calories = m.Calories | |
// }) | |
// }); | |
// return results; | |
//Method 2 add a DTO, ViewModel that holds only what you wish to serialize to clients | |
//(Automapper can help here-- See http://lostechies.com/jimmybogard/2013/08/25/automapper-3-0-released/ | |
//var results = _repo.GetAllFoodsWithMeasures() | |
// .OrderBy(f => f.Description) | |
// .Take(25) | |
// .ToList().Select(f => new FoodModel | |
// { | |
// Description = f.Description, | |
// Measures = f.Measures.Select(m => | |
// new MeasureModel | |
// { | |
// Description = m.Description, | |
// Calories = m.Calories | |
// }) | |
// }); | |
// return results; | |
//Method 3: Use a Factory Pattern to return our DTO view models | |
//NOTE: WE CHOOSE NOT to use Automapper, because we want to be able to build return URI's etc into | |
//to return data e.g. HATEOS Hypermedia etc | |
//using a Model Factory, we have only one place to handling Mapping from | |
//Database entities to DTO's | |
//NOTE: the query returns a collection of foods | |
IEnumerable<Food> query; | |
if(includeMeasures) | |
{ | |
query = TheRepository.GetAllFoodsWithMeasures(); | |
} | |
else | |
{ | |
query = TheRepository.GetAllFoods(); | |
}; | |
//note results is a list of FoodModels from the factory | |
var results = query.OrderBy(f => f.Description) | |
.Take(25) | |
.ToList().Select(f => TheModelFactory.Create(f)); | |
return results; | |
} | |
public FoodModel Get( int foodid ) | |
{ | |
return TheModelFactory.Create(TheRepository.GetFood(foodid)); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
namespace CountingKs.Models | |
{ | |
public class MeasureModel | |
{ | |
public string Url { get; set; } | |
public string Description { get; set; } | |
public double Calories { get; set; } | |
} | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using CountingKs.Data; | |
using CountingKs.Data.Entities; | |
using CountingKs.Models; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Net; | |
using System.Net.Http; | |
using System.Web.Http; | |
namespace CountingKs.Controllers | |
{ | |
public class MeasuresController : BaseApiController | |
{ | |
//hand off the injected repo to our base controller | |
public MeasuresController( ICountingKsRepository repo): base( repo) | |
{ | |
} | |
public IEnumerable<MeasureModel> Get( int foodid) | |
{ | |
//we call ToList before the select to force the retrieval of data before we transform it | |
//with the select projection | |
var results = TheRepository.GetMeasuresForFood(foodid) | |
.ToList() | |
.Select(m => TheModelFactory.Create(m)); | |
return results; | |
} | |
public MeasureModel Get( int foodid, int id) | |
{ | |
var results = TheRepository.GetMeasure(id); | |
if ( results.Food.Id == foodid) | |
{ | |
return TheModelFactory.Create(results); | |
} | |
else | |
{ | |
return null; | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Web; | |
using CountingKs.Data.Entities; | |
using System.Web.Http.Routing; | |
using System.Net.Http; | |
namespace CountingKs.Models | |
{ | |
//using a Model Factory, we have only one place to handling Mapping from | |
//Database entities to DTO's | |
public class ModelFactory | |
{ | |
private UrlHelper _urlHelper; | |
//LEN NOTE: THE REASON WE DID NOT USE AUTOMAPPER IS BECAUSE WE WANTED TO USE THE REQUEST | |
//AND THE URL HELPER TO AID US IN CREATING OUR HYPERLINKS | |
public ModelFactory( HttpRequestMessage request ) | |
{ | |
//Remember to use the Url helper for WebAPI | |
//which is in System.Web.Http.Routing | |
_urlHelper = new UrlHelper(request); //this will help us to build | |
//the URLs for developer API discovery | |
} | |
public FoodModel Create( Food food ) | |
{ | |
return new FoodModel() | |
{ | |
//get an url for the Food roud and supply the id of the food that we're returning | |
Url = _urlHelper.Link("Food", new { foodid = food.Id }), | |
Description = food.Description, | |
Measures = food.Measures.Select(m => Create(m)) | |
}; | |
} | |
public MeasureModel Create( Measure measure ) | |
{ | |
return new MeasureModel() | |
{ | |
Url = _urlHelper.Link("Measures", new { foodid = measure.Food.Id, id = measure.Id }), | |
Description = measure.Description, | |
Calories = Math.Round(measure.Calories) | |
}; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
oops originally created this before i logged in so i forked it from anonymous user... oh well