Giter Site home page Giter Site logo

argo's Introduction

Argo

Argo

A Json.API 1.0 C# ORM client to translate Json.API semantics into familiar, friendly POCOs.

GitHub license

Weaver

Branch Nuget Build
master N/A N/A
development N/A VSTS

Client

Branch Nuget Build
master N/A N/A
development N/A VSTS

What

With the advent of NoSQL databases, we all need to decide how to relate these schemaless resources over web APIs. This often introduces new challenges, like how to model relationships bewteen resources. These challenges compound when you introduce filtering or paging and sorting features to your API. JSON API is a specification to define a common approach to overcoming these challenges. However, additional challenges arise when you realize you need to somehow map a JSON API resource to a POCO. The goal of Argo is to solve all of these challenges (and then some!) for you.

Domain Language

Before going any further, it's important to establish some common language to describe these rather abstract concepts:

  • Model: The C# POCO representation of your domain entity that is used by your c# runtime
  • Resource: The Json.API representation of your domain entity that is used to communicate with a Json.API compatible web API
  • Session: A stateful instance used to invoke one or many transactions with the API.

Why

Here is an example of a JSON API resource:

{
  "type": "person",
  "id": "02e8ae9d-b9b5-40e9-9746-906f1fb26b64",
  "attributes": {
    "firstName": "Nick",
    "lastname": "O'Tyme",
    "age": 42
  },
  "meta": {
    "lastAccessed": "2017-05-17T19:53:55.6534431Z"
  }
  "relationships": {
    "bestFriend": {
      "data": { "type": "person", "id": "2dba2b39-a419-41c1-8d3c-46eb1250dfe8" }
    },
    "friends": {
      "data": [
        { "type": "person", "id": "d893ef57-7939-4b4e-9dfa-be4d1af27c5e" },
        { "type": "person", "id": "2dba2b39-a419-41c1-8d3c-46eb1250dfe8" },
        { "type": "person", "id": "c2ac33ab-e9fa-4fb4-9a63-24b168854023" }
      ]
    }
  }
}

This could be the Model of the above Resource:

public class Person
{
  public Guid Id { get; set; }                        // $.id
  public string FirstName { get; set; }               // $.attributes.firstName
  public string LastName { get; set; }                // $.attributes.lastName
  public int Age { get; set; }                        // $.attributes.age
  public DateTime LastAccessed { get; set; }          // $.meta.lastAccessed
  public Person BestFriend { get; set; }              // $.relationships.bestFriend.data.???
  public ICollection<Person> Friends { get; set; }    // $.relationships.friends.data.???
}

Let's imagine we fetched the above Resource from our Json.API compatible web API and wanted to map it to our Model. First, notice bestFriend and friends in the Resource do not have the information we need to fully hydrate these properties, but just id and type - the minimum information needed to fetch (read: lazy load) thier respective Resources from the web API. We would need to make additional requests to the server to fetch the necessary Resources for our BestFriend and Friends properties. As object models grow and become more complex, this becomes increasingly difficult and expensive to manage. Also, notice the same Resource with id 2dba2b39-a419-41c1-8d3c-46eb1250dfe8 is not only our bestFriend, but is also included in our friends collection, of course. We don't want to fetch this Resource from the server twice! Ideally, we fetch it once and cache it somewhere to be referenced from either property as needed.

Getting Started

There are two components to Argo: the model weaver, and the remote client. Both are distributed as NuGet packages. You'll want to add RedArrow.Argo.Fody to the project where your models are located. When using .NetStandard projects, you may need to manually edit the project file to add the Fody weaver build task.

  <Import Project="..\packages\Fody.1.29.4\build\dotnet\Fody.targets" Condition="Exists('..\packages\Fody.1.29.4\build\dotnet\Fody.targets')" />
  <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
    <PropertyGroup>
      <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
    </PropertyGroup>
    <Error Condition="!Exists('..\packages\Fody.1.29.4\build\dotnet\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Fody.1.29.4\build\dotnet\Fody.targets'))" />
  </Target>

Next, install RedArrow.Argo.Client as a NuGet dependency to your project that will act as the client to the Json.Api compliant web API. That's it! The next section will walk you through how to get Argo configured.

Note: Argo is currently distributed as an internal NuGet package, maintained by Red Arrow Labs. If you are interested in using this package, please create an issue to open a dialog. The only reason this isn't being distrubuted on nuget.org is because the need has not arisen, yet.

Configuring

Argo gives you a pleasent, easy-to-understand configuration API. If you've worked with Fluent NHibernate, this should look a little familiar. This configuration step performs a potentially significant amount of reflection and caching and should only be performed once throughout your application's lifecycle, typically at startup. Aside from telling Argo where to scan for your [Model]s, all of this is optional. The idea is to offer a large amount of options and flexibility to meet your application's unique needs.

// the ISessionFactory is the long-lived object you would (ideally) register in your IoC container
var sessionFactory = Fluently.Configure("http://api.host.com")
  .Remote()
    .Configure(httpClient => httpClient
      .DefaultRequestHeaders
      .Authorization = new AuthenticationHeaderValue("Bearer", token))
    .ConfigureAsync(() => YourTokenManagerInstance.GetAccessTokenAsync())
    .Configure(builder => builder
      .UseExceptionLogger()
      .Use<YourCustomDelegatingHandler>(ctorArg1, ctorArg2)
      .UseResponseHandler(new ResponseHandlerOptions{
        ResponseReceived = response => YourAsyncMethod(response),
        ResourceCreated = response => YourAsyncMethod(response),
        ResourceUpdated = response => YourAsyncMethod(response),
        ResourceRetrieved = response => YourAsyncMethod(response),
        ResourceDeleted = response => YourAsyncMethod(response),
      })
      .Use(new CustomHttpRequestModifier(ctorArg1))
      .Use(new CustomHttpResponseListener(ctorArg1))
      .UseGZipCompression()
      .UseRequestHandler<Xamarin.Android.Net.AndroidClientHandler>())
  .Models()
    .Configure(scan => scan.AssemblyOf<Person>())
    .Configure(scan => scan.Assembly(Assembly.GetExecutingAssembly()))
  .BuildSessionFactory();

If that's not enough configurability, you have full access to the HttpClient when a new Session is created to do any last-minute configuration.

using (var session = sessionFactory.CreateSession(client => YourHttpClientConfigureFunction(client))
{
  // do some stuff
}

private void YourHttpClientConfigureFunction(HttpClient client)
{
  client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "access token value")
}

In Action

Guid crossSessionPersonId;
// ISession is a short-lived state/cache of request/responses
using (var session = sessionFactory.CreateSession())
{
  var person = new Person
  {
    FirstName = "Justin",
    LastName = "Case"
  };
  
  // sends a POST to the server and returns a session-managed Person
  person = await session.Create(person);
  // Id was created by the server and populated by the session
  crossSessionPersonId = person.Id;
  Assert.NotEqual(Guid.Empty, crossSessionPersonId);
}
// later that day...
using (var session = sessionFactory.CreateSession())
{
  // fetch our person from the server
  var person = await session.Get<Person>(crossSessionPersonId);
  Assert.Equal("Justin", person.FirstName);
  Assert.Equal("Case", person.LastName);

  // session receives this setter value and creates a patch for this model
  person.FirstName = "Charity";
  person.BestFriend = new Friend
  {
    FirstName = "Keri",
    LastName = "Oki"
  };
  
  // the new BestFriend will be created and related in the person all in one request
  await session.Update(person);
}
// cleaning up...
using (var session = sessionFactory.CreateSession())
{
  // sends a DELETE to the server (no cascade options yet)
  await session.Delete<Person>(crossSessionPersonId);
}

LINQ

A LINQ provider is available. This allows "queries" to be compile-safe instead of magic strings.

Supported APIs

  • Where
  • OrderBy
  • OrderByDescending
  • ThenBy
  • ThenByDescending
  • Skip
  • Take
  • First
  • FirstOrDefault
  • Single
  • SingleOrDefault
  • Last
  • LastOrDefault

Don't see your favorite LINQ extension? Create an issue and I can get started on adding it!

You can use the LINQ provider via CreateQuery.

using (var session = sessionFactory.CreateSession())
{
  var pageNumber = 5;
  var pageSize = 25;
  var pageOffset = pageNumber * pageSize;

  var people = session.CreateQuery<Person>()
    .Where(x => x.Age < 65)
    .OrderBy(x => x.LastName)
    .ThenBy(x => x.FirstName)
    .Skip(pageOffset)
    .Take(pageSize)
    .ToArray();
  
  var firstPerson = people.First();
  
  // this would lazy-load all friends (potentially expensive)
  var allFriends = firstPerson.Friends.ToArray();
  
  // linq for relationships - person's top 10 friends
  var bestFriends = session.CreateQuery(firstPerson, p => p.Friends)
    .OrderByDescending(f => f.FriendshipFactor)
    .Take(10)
    .ToArray();
}

Please note that the .Where(...) extension currently follows a proprietary filter clause format specified as part of the Titan platform. The filter query parameter is specified, but not defined, in the Json.API spec.

How

Argo takes advantage of Fody to weave code into your POCO Model at compile time in order to bridge the gap between the POCO semantics developers expect and the Json.API json structure.

Advantages:

  1. Cross Platform! Argo targets netstandard 1.4
  2. Most expensive reflection-based mapping is executed at compile time.. This allows post-startup runtime opterations to have minimal overhead.
  3. Simple. Argo does all the heavy lifting so you don't have to!

Most ORMs leverage proxies to abstract your POCO from a database, for example. Unfortunately, .net proxy implementations will not play nice with Xamarin iOS projects due to their use of Reflection.Emit APIs, which are not permitted on iOS. Argo aims to be a cross-platform library, so we get around this by modifying the POCO itself at compile time instead of proxying it at runtime.

Examples

Maybe the thought of a 3rd party library modifying your code is scary. It may be scary at first, but you'll soon realize it's awesome. Bring on the examples!

Model Weaving

Take a look at this example Model. Models must be marked with a [Model] attribute, as well as contain a System.Guid property marked with the [Id] attribute.

[Model]
public class Person
{
  [Id]
  public Guid Id { get; set; }
  [Property]
  public string FirstName { get; set; }
  [Property]
  public string LastName { get; set; }
  [Property]
  public int Age { get; set; }
  [Meta]
  public DateTime LastAccessed { get; set; }
  [HasOne]
  public Friend BestFriend { get; set; }
  [HasMany]
  public ICollection<Person> Friends { get; set; }
}

Argo will weave some magic into your Model:

[Model]
public class Person
{
  private static readonly string __argo__generated_include = "";

  private IModelSession __argo__generated_session;

  public IResourceIdentifier __argo__generated_Patch { get; set; }
  public IResourceIdentifier __argo__generated_Resource { get; set; }

  public bool __argo__generated_SessionManaged => this.__argo__generated_session != null
    && !this.__argo__generated_session.Disposed;

  private Guid id;
  [Id]
  public Guid Id
  {
    get
    {
      return this.id;
    }
    private set
    {
      if(!this.__argo__generated_SessionManaged)
      {
        this.id = value;
      }
    }
  }

  public Person(IResourceIdentifier resource, IModelSession session)
  {
    this.__argo__generated_Resource = resource;
    this.__argo__generated_session = session;
    this.id = this.__argo__generated_session.GetId<Person>(this);

    this.firstName = __argo__generated_session.GetAttribute<Person, string>(this, "firstName");
    this.lastName = __argo__generated_session.GetAttribute<Person, string>(this, "lastName");
  }

  // property weaving explained below
}

That may be a lot for you to process. Let's break it down:

  • __argo__generated_session the Session instance for this Model. Mutative operations are delegated to the Session.
  • __argo__generated_include specifies the relationships Argo should eagerly load during a GET operation - more on this later.
  • __argo__generated_Resource is the backing Json.API Resource for this Model. This repsresents the transport object used to communicate with the API.
  • __argo__generated_Patch is also a Resource, but only contains the changes made to its Model since the last communication with the server. Having the Resource and the Patch directly in the Model is hugely powerful for developers. While debugging, you can see the base Resource that was retrieved from the server, as well as any pending deltas not yet flushed to the server in the Patch.

A ctor is added that should only be invoked by the Session. Notice BestFriend marked as [HasOne] was not initialized. Relationships are lazy-loaded by default.

You may also notice the Id was enhanced a bit. Model Ids are settable as long as the Model is not currently bound to a Session. This means you may create a new Model, set its Id, and persist it. However, if you use the Session to fetch a Model by Id, that Model is now managed by a Session and you may not change that Id anymore for data consistency reasons.

Property Weaving

This Model property...

[Property]
public string FirstName { get; set; }

becomes...

private string firstName;
[Property]
public string FirstName
{
  get
  {
    return this.firstName;
  }
  set
  {
    if (this.__argo__generated_session != null
      && !string.Equals(this.firstName, value, StringComparison.Ordinal))
    {
      this.__argo__generated_session.SetAttribute<Person, string>(this, "firstName", this.firstName);
    }
    this.firstName = value;
  }
}

Notice the string equality check. Most property setters have type-specific equality checks to verify the value is actually being changed before delegating that change to the Session.

The default naming convention from Model property to Resource attribute names is camel-case. As shown above, a Model property FirstName will be mapped to a Resource attribute firstName. However, all names are overridable to allow you to chose your own naming convention destiny.

[Model("person")]
public class Actor
{
  [Property("first-name")]
  public string FirstName { get; set; }
}

If dot-notation is used when specifying a property name override, this will create/maintain nested attributes within the stored JSON.

[Model("person")]
public class Actor
{
  [Property("additionalInfo.middleName")]
  public string MiddleName { get; set; }
}

The above will cause the stored JSON to take the form:

{
  "type": "person",
  "id": "02e8ae9d-b9b5-40e9-9746-906f1fb26b64",
  "attributes": {
    "additionalInfo": {
      "middleName": "Edward"
    }
  }
}

Relationship Weaving

We can also relate models

[HasOne]
public Person BestFriend { get; set; }

will be woven into

private Person bestFriend;
[HasOne]
public Person BestFriend
{
  get
  {
    if(this.__argo__generated_session != null)
    {
      this.bestFriend = this.__argo__generated_session.GetReference<Person, Person>(
      this,
        "bestFriend");
    }
    return this.bestFriend;
  }
  set
  {
    this.bestFriend = value;
    if(this.__argo__generated_session != null
    && this.bestFriend != value)
    {
      this.__argo__generated_session.SetReference<Person, Person>(
      this,
        "bestFriend",
        this.bestFriend);
    }
  }
}

With the Model delegating to the Session, the Session can do a lot of great stuff for us.

  • cache retrieved models for subsequent gets
  • track changes for building PATCH requests
  • manage lazy-loading relationships via Session-managed collections in place of IEnumerable<T> or ICollection<T>
  • linq provider to allow Session-managed sorting, paging, and filtering of collections

Eager Loading

By default, relationships are lazy-loaded. You may override this behavior by specifying the LoadStrategy of a relationship.

[HasOne(LoadStrategy.Eager)]
public Person BestFriend { get; set; }

This will update the value of __argo__generated_include in your Model. The Session will use this value for all GET requests for the respective Model type. In this case, Argo would weave:

private static readonly string __argo__generated_include = "bestFriend";

The Session will use the value "bestFriend" when retrieving the a Resource of type person, so the data for both the primary Person Model and Person.BestFriend Model is retrieved in a single request. For more info on this behavior, read up on fetching included relationships in the Json.API spec.

Roadmap

We're still evaluating the long-term roadmap for this project, but initial tentative ideas:

1.0

  • Linq provider
    • sorting via OrderBy
    • paging via Skip/Take
    • filtering via Where
  • Configurable eager loading
  • GZip Compression

2.0

  • Refactor/upgrade of LINQ provider
  • Cache provider plugins with initial support for Akavache

3.0

  • server to client eventing/sync push via Rx.NET

...

  • your idea could go here...

Name

In Greek mythology, Argo was the ship JSON Jason and the Argonauts sailed in search of the golden fleece.

Logo

Sail Boat designed by Celia Jaber from The Noun Project

argo's People

Contributors

engenb avatar kwal avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

argo's Issues

content-specific json.api models

Relationship Data properties may contain a single ResourceIdentifier or an array of ResourceIdentifiers, based on the type of relationship. To support this, Data is currently a JToken. This is not ideal, as it requires a lot of checks on Data.TokenType for Object or Array.

A far better solution would be an IRelationship or abstract base class, with a custom JsonConverter for that type, that switches deserialization based on TokenType.

The same approach should apply to Resource and other types that have newtonsoft.json types.

Query on relationship IDs causes unexpected exception

When trying to query on a HasManyIds field, Argo throws an unexpected exception. If this is not supported, then Argo should throw a more appropriate exception. If it should be supported, then this should succeed.

[Model]
public class Person
{
    [HasManyIds("addresses")]
    public IEnumerable<Guid> AddressIds { get; }
    [HasMany]
    public ICollection<Address> Addresses { get; set; }
}

// This throws the exception below
session.CreateQuery<Person>()
    .Where(p => p.AddressIds.Contains(addressId))
    .ToList();
System.InvalidOperationException: Sequence contains no matching element
   at System.Linq.Enumerable.Single[TSource](IEnumerable`1 source, Func`2 predicate)
   at RedArrow.Argo.Client.Linq.RemoteQueryable`1.GetJsonName(MemberInfo member)
   at RedArrow.Argo.Client.Linq.Queryables.WhereQueryable`1.TranslateMemberExpression(Expression expression)
   at RedArrow.Argo.Client.Linq.Queryables.WhereQueryable`1.TranslateExpression(Expression expression)
   at RedArrow.Argo.Client.Linq.Queryables.WhereQueryable`1.TranslateMethodCallExpression(Expression expression)
   at RedArrow.Argo.Client.Linq.Queryables.WhereQueryable`1.TranslateExpression(Expression expression)
   at RedArrow.Argo.Client.Linq.Queryables.WhereQueryable`1.BuildQuery()
   at RedArrow.Argo.Client.Linq.RemoteQueryable`1.GetEnumerator()
   at System.Linq.Buffer`1..ctor(IEnumerable`1 source)
   at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)
   ***censored***

Support for in-place upgrades

Consider the scenario where the desired object structure changes:

  • Properties are added and may need a default value
  • Property names are modified or removed
  • Property values are transformed
  • Properties are wrapped in another object

The two main options are

  1. Run script to upgrade all objects at same time. Requires downtime during the upgrade since serializing/saving an non-upgraded object would cause data issues.
  2. In-place upgrade. V1 objects are transformed into V2 objects on read. Any saves will upgrade the object to V2 permanently. Upgrade scripts can be run during off-peak hours (so versions do not get too far behind).

For Argo to support in-place upgrades, it would need to:

  • Support transforming the result from the API into another structure. That might be json serialization options or some explicit transformation from structure A to B. This also needs to be supported for relationship fetches.
  • Support saving these structure changes. Specifically, the Patch property would not currently pick up any object changes to the raw json. It only tracks the property setters.
[Model("person")]
public class PersonV1
{
    [Property]
    public string FullName { get; set; }
    [Property]
    public DateTime DateOfBirth { get; set; }
    // Other properties...
}
[Model("person")]
public class PersonV2
{
    [Property]
    public Identity Identity { get; set; }
    // Other properties...
}
public class Identity
{
    // Split FullName of V1 object into 3 components
    public string FirstName { get; set; }
    public string MiddleName { get;set; }
    public string LastName { get; set; }
    public DateTime DateOfBirth { get; set; } // Moved from root Person into Identity
    public Gender Gender { get; set; } // Default me to Gender.Unknown
}

cache update: don't replace model. replace resource

when the basic cache updates a model, it's just overriding any previous model in the cache with that id. this is bad. models may be updated from various apis at various times during a session's lifetime. doing so will mess up any model pointers that haven been returned from the session.

i.e. the reference that was returned from the session is no longer the cached model because it was overwritten by some other caching operation.

instead, the cache should check for the existence of a model and replace its Resource with the one fetched from the server.

this would require a new method be woven into the Model to refresh all the attribute Property values from the newly set Resource.

model.UpdateResource(IResourceIdentifier resource)
{
  foreach Property marked as [Property], refresh value from session
}

Cannot set property to null on managed object

[Model]
public class MovieQuote
{
    [Property]
    public string ActorName { get; set; }
    [Property]
    public string Quote { get; set; }
}

var quote = new MovieQuote
{
    ActorName = "Adam Sandler",
    Quote = "Now that's what I call high quality H2O"
};
quote = await session.Create(quote);
// This throws an exception in Resource.SetAttribute
quote.ActorName = null;

Resources.SetAttribute throws an exception because the value supplied to JToken.FromObject(value) is null

GetAttributes()[attrName] = JToken.FromObject(value);

Disallow HasMany setters

HasMany properties are created as session-managed collections. Setting those managed collections to unmanaged collections can cause mysterious bugs or other issues. Argo should automatically initialize the HasMany collection and remove/prevent a setter on that property. This would force consumers to use the appropriate Add/Remove/Clear methods, which would prevent certain bugs at compile time.

.net core-esk middleware request pipeline builder

thinking of adding an IClientBuilder interface that is similar to asp.net core's IAppBuilder with .Use(object[] args) signature.

T would need to be a DelegatingHandler with a ctor signature matching the arg types given.

constructed DelegatingHandler would have its InnerHandler set to the next DelegatingHandler in the pipeline.

last DelegatingHandler in the pipeline gets the default HttpClientHandler

Meta is patched in a different way than it received

See the example below. The Meta is received as a json object with multiple layers, but is patched as a property named after the "metaName" on the attribute. If Meta changes were respected by the server implementation, this would unintentionally create a new property rather than update the existing one.

// Resource
{
  "meta": {
    "system": {
      "createdAt": "2017-10-19T21:37:06.5406333Z",
      "updatedAt": "2017-10-19T21:37:27.1095531Z",
      "eTag": "\"0000e809-0000-0000-0000-59e91b160000\""
    }
  }
}
// Patch request
{
  "meta": {
    "system.eTag": "\"0000e809-0000-0000-0000-59e91b160000\""
  }
}

HasManyIds does not reflect additions to HasMany relationship

If an entity is added to a HasMany relationship collection, it should be added to the Ids in HasManyIds. That is currently not the case within the same session.

[Model]
public class Person
{
    [HasManyIds("pizzas")]
    public IEnumerable<Guid> PizzaIds { get; set; }
    [HasMany]
    public ICollection<Pizza> Pizzas { get; set; }
}

var moarPizza = session.Get<Pizza>(moarPizzaId);
person.Pizzas.Add(moarPizza);
await session.Update(person);
// FAILS in same session.  Succeeds in new session
Assert.True(person.PizzaIds.Contains(moarPizzaId));

Meta is excessively patched because object is typically never fully equal

Usually, the meta on the Fody model will be a subset of all meta being returned by the server. For example, my application is only interested in the created/updated timestamps while the server may return the users that performed the action as well. That causes issues in complex meta structures, since the current implementation only checks the equality of the 1st level property in meta. In this instance, the full contents of the "system" property are not equal and therefore will always be patched. In reality, the meta properties in the Fody class have not been changed, so the patch should not be sent.

// Contents of the Meta property
// Response from JsonApi
{
  "system": {
    "createdBy": "91773e2b-f121-435d-b3bf-be26cb3c9503",
    "createdAt": "2018-02-12T15:29:18.6158073Z",
    "updatedBy": "91773e2b-f121-435d-b3bf-be26cb3c9503",
    "updatedAt": "2018-02-12T16:01:56.8293298Z",
    "eTag": "\"0b0099b9-0000-0000-0000-5a81ba740000\""
  }
}
// Patch request produced by Argo
{
  "system": {
    "createdAt": "2018-02-12T15:29:18.6158073Z",
    "updatedAt": "2018-02-12T16:01:56.8293298Z",
    "eTag": "\"0b0099b9-0000-0000-0000-5a81ba740000\""
  }
}

Request failures do not produce useful information

Argo always uses response.EnsureSuccessStatusCode() to check the response code, which throws System.Net.Http.HttpRequestException. That exception does not contain the content of the exception or the request being sent, which makes debugging difficult. I'm currently receiving a 400 response code and having a hard time figuring out what is wrong.
A custom exception containing the request and response content would be super useful, but either one would greatly help debugging.

Session Create does not respect instance type

Session Create relies on the cast type rather than the actual type. For instance, attempting to save a Model class that's cast to an object will cause an exception. Checking the instance type ( entity.GetType() ) would properly save the model in all cases.

var patient = new Patient();
// This explodes because Argo assumes the type is object
await session.Create((object)patient);

investigate moving away from model Patch property

the Model acts like a proxy for the underlying Resource. Each setter calls into the session, which in tern updates the source Model's Patch with depending on what changed.

this falls down when dealing with [Property]s that are ICollection. When collections are mutated by adding/removing items, the session is not informed because this was not done through a Model setter, thus the Patch is not correct.

an ideal approach would be to get the current state of the object by converting it to a Resource JObject, and then doing a diff against the Resource since the last request to the server.

Content is null in HttpResponseMessage given to ResponseHandler methods

Copying Content to the ResponseHandler methods is a TODO, so Content is always null. The HttpResponseMessage given to these methods is only a copy of the actual message instance.

.UseResponseHandler(new ResponseHandlerOptions{
        ResponseReceived = response => YourAsyncMethod(response),
        ResourceCreated = response => YourAsyncMethod(response),
        ResourceUpdated = response => YourAsyncMethod(response),
        ResourceRetrieved = response => YourAsyncMethod(response),
        ResourceDeleted = response => YourAsyncMethod(response),
      })

Specify load strategy per query

Eager loading of relationships is a performance improvement where the relationship is accessed in the same session. However, not all queries on the same resource will require the same relationship access, which means those queries actually take a performance hit instead. The Load Strategy is defined on the model itself, which means the implementer has to guess at how the relationships will be used in general and choose the better of two evils.

It would be better to override the Load Strategy per query, so that the implementer can tune performance on a query-by-query basis. If the implementer knows the relationship data will be used, it can be eagerly loaded for that query. If the Load Strategy on the model is eager and that relationship data will not be used, then the implementer could turn off eager loading in that instance. This is consistent with other ORMs and feasible using JsonApi.

Below is one possible look. Otherwise CreateQuery could have additional parameters, or the implementation could be "opt-in only" where the only option is to specify which relationships will be loaded.

// Method PeopleToVisit
var puppies = session
    .CreateQuery<Puppy>()
    .LoadStrategy(x => x.Owners, LoadStrategy.Eager)
    .LoadStrategy(x => x.Leash, LoadStrategy.Lazy)
    .Where(x => x.Breed = BreedType.GoldenRetriever);
return puppies
    .Select(x => new VisitResponse {
        Puppy = x,
        OwnerNames = x.Owners.Select(x => x.Name).ToArray();
    });

// Method PuppiesToWalk
var puppies = session
    .CreateQuery<Puppy>()
    .LoadStrategy(x => x.Leash, LoadStrategy.Eager)
    .LoadStrategy(x => x.Owners, LoadStrategy.Lazy)
    .Where(x => x.LastWalkTime <= DateTime.Now.AddHours(-1));
return puppies
    .Select(x => new WalkResponse {
        PuppyName = x.Name,
        Leash = x.Leash
    });

// Opt-in possible look
var puppies = session
    .CreateQuery<Puppy>()
    .EagerLoad(x => x.Owners)
    .EagerLoad("leash");

// CreateQuery possible look
var puppies = session
    .CreateQuery<Puppy>(eagerLoad: new [] { "owners" });

Utilize body of 201 response to patch backing resource

When using the Create method, if the server returns a 201, it'd be nice patch the session data with the body of the response. This would be especially helpful for any meta properties that are added/updated server side.

collection proeprties not using proper array contains filter

different collection interfaces need to be treated differently for .Contains.

.Contains on IEnumerable and Enumerable is an extension method that has two parameters

ICollection<> / IList<> have a (non-extension) .Contains

IList .Contains performs a Convert on the arg to convert from whatever type to Object

gzip compression

gzip is a defacto standard for compressing transport data between clients and servers. textual information like json can be compressed to ~20% its original size with minimal cpu overhead.

can probably add something to IConfigureRemote like .UseGzip() that will add an additional DelegatingHandler to the HttpClient request pipeline with something like this:

using (var Stream = new MemoryStream())
{
	using (var Zipper = new GZipStream(Stream, CompressionMode.Compress, true)) Zipper.Write(Bytes, 0, Bytes.Length);
	var Content = new ByteArrayContent(Stream.ToArray());
	Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
	Content.Headers.ContentEncoding.Add("gzip");
	return Content;
}

Do not cache includes on create/update

When JSON API responds with updated relationship information, it does not include the full resource. This causes out-of-date or missing Meta attributes on resources that were created as includes on the operation. When a model is created or updated with includes, the included models should not be cached and the relationship should be forced to be re-fetched.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.