Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NodaTime.Serialization.ModelBinding #368

Closed
wants to merge 8 commits into from

Conversation

zaccharles
Copy link

Keep in mind this will need some more work, such as tests, comments, and probably reverting some of the changes to the solution that VS2015RC made. I first did this for 2.0.0 so I was using 2015RC. This version is basically the same though; I just couldn't use nameof().


This is a proposal for a new package, NodaTime.Serialization.ModelBinding (maybe a different name?), which adds functionality so that Web API can bind URI parameters to Noda Time types.

To do this in a simplistic way, you need to implement an IModelBinder for each Noda Time type, and attach it to said Noda Time type in some way. Since I imagine we don't want to pollute the NodaTime project with System.Web.Http... dependencies, this should be standalone. The normal way to do that is to add a ModelBinderAttribute to each action parameter. Usually this attribute would take the Type of the IModelBinder to use. However, I didn't want users to need to do this.

In fact, I wanted using this package it to be very easy, like the Json.Net one. I got it to the point where you can just call config.ConfigureForNodelTime(DateTimeZoneProviders.Tzdb); on the HttpConfiguration in your startup code. I'm quite happy with this.

So how do we avoid all this attribute nonsense. Well, this package works by replacing the IActionValueBinder service with a NodaActionValueBinder. In case this service has already been replaced by user code, or another package, NodaActionValueBinder wraps the previously registered IActionValueBinder, and passes calls down to it for anything unrelated to Noda Time types.

The IActionValueBinder service is responsible for telling Web API which IModelBinder to use for a given parameter. At startup, it's given each parameter and asked that question. The DefaultActionValueBinder responds this by looking at the ModelBinderAttribute. The NodaActionValueBinder is slightly different. The first thing it does is identify whether the current parameter is a Noda Time type. If it is, it creates a new NodaModelBinderAttribute (which extends ModelBinderAttribute) and attaches it to the property. It then passes the request on to the IActionValueBinder it's wrapping. Now, the IActionValueBinder has a ModelBinderAttribute to look at, and we didn't need to manually add it to every parameter in code.

So why do we need NodaModelBinderAttribute? Well, by default, ModelBinderAttribute is responsible for instantiating the IModelBinder. We don't want this because we want to be able to pass the IDateTimeZoneProvider all the way down from the top.
ModelBinderAttribute first tries to resolve an instance of the IModelBinder from the configured dependency resolver. If that's not possible, it will try to instantiate a new one with Activator.CreateInstance. NodaModelBinderAttribute's job is to temporarily replace the IDependencyResolver used to try and resolve the IModelBinder.

This is where NodaDependencyResolver comes in. It's a simple wrapper for IDependencyResolver. This interface has GetService(Type) which is implemented so that if it's a Noda model binder being resolved, an instance with the correct IDateTimeZoneProvider will be returned. If it's not a Noda model binder, the call will be passed on to the wrapped IDependencyResolver.

The above could probably be avoided via the initial configuration extension method setting a static variable somewhere, and the model binders reading it as necessary. That way all model binders could have parameterless contructors and be instantiated with Activator.CreateInstance. It's simpler, but didn't seem as nice.

Lastly, the model binders are quite simple wrappers for the various IPattern<T> where available, and code ripped from the Json.Net package for other types like Interval and DateTimeZone. I'm not 100% sure I got the behavior correct for the Interval, especially the IsoInterval.

As I said at the top, this is a work in progress, though it does work currently to the extent of my manual testing. I didn't want to go too far in case it's rubbish and I'm crazy. All feedback welcome.

@jskeet
Copy link
Member

jskeet commented Jun 12, 2015

Hi Zac - thanks very much for this. Will try to review it over the weekend, but it may take a bit longer than that.

# Visual Studio 2013
VisualStudioVersion = 12.0.30501.0
# Visual Studio 14
VisualStudioVersion = 14.0.22823.1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 1.3.x branch should be built with VS2013. 2.0 (master) uses VS 14.

@zaccharles
Copy link
Author

@mj1856 I originally did this against 2.0 and then retrofitted it to 1.3.x. As I mentioned in the PR description, I'd fix these solution file / assembly info problems before it would be merged.

@mattjohnsonpint
Copy link
Contributor

Thanks. Also, if I'm understanding correctly - the intention here is to make it easy to bind to things such as querystring parameters. If so, we should probably allow for some variability in how those parameters are formatted. For example, It's fairly common to see the short-form of ISO dates on a URI, such as 20151231 instead of 2015-12-31.

@zaccharles
Copy link
Author

I agree that that would be nice, however, to explain: I based the current
version off the Json.Net integration which also has fixed formats. I
suppose we could consider overriding the formats during the
ConfigureForNodaTime call, but then what if someone wants to change it for
a specific parameter. In that case it would be an attribute most likely. I
imagine this is why it was avoided for Json.Net, though that's not a great
reason to avoid it here :)

@zaccharles
Copy link
Author

I reverted the solution file and readded the new project, so solution changes are now minimal. I also corrected the version to match the other projects.

@BenJenkinson
Copy link

Since I'm about to implement something like this myself, did @jskeet get a chance to review it?

@jskeet
Copy link
Member

jskeet commented Sep 14, 2015

@BenJenkinson: Sorry, no, I haven't had a chance yet. Any comments you have would be useful, given that you're likely to be more knowledable on the binding code than I am... looks like some more XML documentation is required and I'll need to check the impact it has on the build as well, of course... then we can port it to 2.0 when required.

@jskeet
Copy link
Member

jskeet commented Jan 28, 2016

Okay, this is taking a really embarrassingly-long time to get to. Realistically, I'm out of my depth anyway.

@BenJenkinson and @mj1856 - are either of you able to do a reasonably-thorough review? I would definitely like to get this in, although I'm still not sure whether doing it in the 1.3.x branch rather than 2.0 is a good idea... I'd mostly considered 1.x to be "done" other than for a 1.4 compatibility release. Having said that, adding a 1.3.1 version of just this package could work too... Worth discussing separately.

@BenJenkinson
Copy link

I did try using this in my own project, but I had to make my own anyway as it inherits/implements System.Web.Http IModelBinder rather than the System.Web.Mvc IModelBinder

The (non-vNext) versions of ASP.NET MVC and ASP.NET WebAPI often have identically named classes, but have no shared inherited classes or interfaces, so we'd need an implementation & configuration for each.

@jskeet
Copy link
Member

jskeet commented Jan 28, 2016

Oh joy. And presumably ASP.NET Core will have other classes as well? This is more complicated than I thought :(

Do we have an example of anyone else doing this that we could learn from?

@BenJenkinson
Copy link

I'm not sure what ASP.NET Core will be like, I've held off looking at it until it was more final. But I believe the idea was to reconcile MVC and WebAPI into the same stack, so there should only be one implementation required for that.

At least with MVC/WebApi the methods are usually very similar, the types are just from different namespaces, so you can't write just one class to do both.

@jskeet
Copy link
Member

jskeet commented Jan 28, 2016

As a thought - I wonder whether it would make sense to separate this from the core Noda Time repository and even organization potentially. It feels like it could have an entirely disparate release schedule, for example - unless it requires changes to Noda Time itself.

(I should potentially have done the same for the NodaTime.Serialization.JsonNet...)

@zaccharles
Copy link
Author

It doesn't require any changes to Noda Time itself, so it could potentially go in its own repo.

@BenJenkinson
Copy link

I think it could be made its own repo, and its own Nuget package ala NodaTime.Serialization.JsonNet, but I definitely think it should be owned by the NodaTime organisation.

It's always quite annoying if say NodaTime has been updated to 2.2, but then you're stuck waiting for some 3rd-party contributor to get around to updating the ModelBinding NuGet package, so you can't use 2.2

@jskeet
Copy link
Member

jskeet commented Jan 30, 2016

Okay - that makes sense. I wonder whether I should start a NodaTime.Serialization repo to contain this and the Json.NET project.

We also need to consider the versioning - we could version them in parallel even if there are no new features to require revving, or we could version them separately. Will open a new issue to discuss all of this.

@jskeet
Copy link
Member

jskeet commented Feb 1, 2016

Okay, ready to start adding stuff to a new repository - could you add the normal licence text to all the source files? That way it's obvious from the logs that you really do agree with that licence :)

@zaccharles
Copy link
Author

Yup, sure. I'll still need to do some work breaking this out into different projects for WebAPI and MVC, but probably best to get this in there and then rework it. I'm happy to that.

@zaccharles
Copy link
Author

Done @jskeet.

I've noticed the other projects are using project references and the lib folder rather than their own NuGet dependencies. Should I change this one to do the same? Maybe we should worry about that once its in its own repo since there will be more work anyway.

Same goes of the nuspec file, there isn't one, but that will change anyway.

@jskeet
Copy link
Member

jskeet commented Feb 1, 2016

Go for NuGet dependencies now - that better represents the separation we're effectively introducing between the serialization stuff and Noda Time. Likewise for other dependencies such as NUnit - I've already done that in 2.0.

@zaccharles
Copy link
Author

Ah right. That's probably why I did it, as I originally based it off master before realising you use a branch for 1.3.x. I'm still half remembering things as it's been so long.

Anyway, keep me informed. I'm got plenty of spare time and would love to contribute to make this happen :)

@TheBeardedLlama
Copy link

TheBeardedLlama commented Jun 17, 2016

I've also started looking at doing this as I'm working on a new project.

Not sure what stage you're at now, but I'd be happy to assist @zaccharles with this effort if my assistance is indeed required. Otherwise, happy to be a guinea pig and reap the rewards!

I'll wedge the bits I need into my project for now and I'll feedback if I come across anything.

PS. we're using nodatime v2 and aspnet core

@lundmikkel
Copy link

What is the status of this issue? Are there any other model bindings available for NodaTime?

@jskeet
Copy link
Member

jskeet commented Oct 13, 2016

@lundmikkel: I haven't done anything with it, I'm afraid. (Any time I do get on Noda Time is pushing towards the 2.0 release...)

@lundmikkel
Copy link

@jskeet Yeah, don't think about it. Even I would rather have you work on version 2.0 😃

@jskeet
Copy link
Member

jskeet commented Mar 24, 2017

Assuming the code in here is still useful, it should be moved to a PR in https://github.com/nodatime/nodatime.serialization - I'm going to close this PR.

@zaccharles Is this still code you're interested in getting into a public build, or have you moved on? (Given the time frame, that would be entirely understandable...)

@zaccharles
Copy link
Author

@jskeet It's been almost two years, but I'd still like to try and follow it through to completion. I haven't done anything in this area for a while, so I'll need to refresh.

I've just created an issue (nodatime/nodatime.serialization#3) to track progress in the new repo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants